commit 473970b813bf3fe6d2eb971aa1ec7ff85a9a230f Author: Prad Nukala Date: Wed Jul 17 15:23:16 2024 -0400 (no commit message provided) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01a20f1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..74acbbb --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +.cache +docs/dist +docs/search.json +docs/**/*.min.js +dist +examples +node_modules +src/react +scripts + diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..81b84cf --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,211 @@ +/* eslint-env node */ + +module.exports = { + plugins: [ + '@typescript-eslint', + 'wc', + 'lit', + 'lit-a11y', + 'chai-expect', + 'chai-friendly', + 'import', + 'sort-imports-es6-autofix' + ], + extends: [ + 'eslint:recommended', + 'plugin:wc/recommended', + 'plugin:wc/best-practice', + 'plugin:lit/recommended', + 'plugin:lit-a11y/recommended' + ], + env: { + es2021: true, + browser: true + }, + parserOptions: { + sourceType: 'module' + }, + overrides: [ + { + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking' + ], + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + project: './tsconfig.json', + tsconfigRootDir: __dirname + }, + files: ['*.ts'], + rules: { + 'default-param-last': 'off', + '@typescript-eslint/default-param-last': 'error', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': 'warn', + 'no-implied-eval': 'off', + '@typescript-eslint/no-implied-eval': 'error', + 'no-invalid-this': 'off', + '@typescript-eslint/no-invalid-this': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + 'no-throw-literal': 'off', + '@typescript-eslint/no-throw-literal': 'error', + 'no-unused-expressions': 'off', + '@typescript-eslint/prefer-regexp-exec': 'off', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: false + } + ], + '@typescript-eslint/consistent-type-assertions': [ + 'warn', + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'never' + } + ], + '@typescript-eslint/consistent-type-imports': 'warn', + '@typescript-eslint/no-base-to-string': 'error', + '@typescript-eslint/no-confusing-non-null-assertion': 'error', + '@typescript-eslint/no-invalid-void-type': 'error', + '@typescript-eslint/no-require-imports': 'error', + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn', + '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/no-unnecessary-qualifier': 'warn', + '@typescript-eslint/non-nullable-type-assertion-style': 'warn', + '@typescript-eslint/prefer-for-of': 'warn', + '@typescript-eslint/prefer-optional-chain': 'warn', + '@typescript-eslint/prefer-ts-expect-error': 'warn', + '@typescript-eslint/prefer-return-this-type': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'warn', + '@typescript-eslint/require-array-sort-compare': 'error', + '@typescript-eslint/unified-signatures': 'warn', + '@typescript-eslint/array-type': 'warn', + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/member-delimiter-style': 'warn', + '@typescript-eslint/method-signature-style': 'warn', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/parameter-properties': 'error', + '@typescript-eslint/strict-boolean-expressions': 'off' + } + }, + { + files: ['**/*.cjs'], + env: { + node: true + } + }, + { + extends: ['plugin:chai-expect/recommended', 'plugin:chai-friendly/recommended'], + files: ['*.test.ts'], + rules: { + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unused-expressions': 'off' + } + } + ], + rules: { + 'no-template-curly-in-string': 'error', + 'array-callback-return': 'error', + 'comma-dangle': 'off', + 'consistent-return': 'error', + curly: 'off', + 'default-param-last': 'error', + eqeqeq: 'error', + 'lit-a11y/click-events-have-key-events': 'off', + 'no-constructor-return': 'error', + 'no-empty-function': 'warn', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-floating-decimal': 'error', + 'no-implicit-coercion': 'off', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-invalid-this': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-return-assign': 'warn', + 'no-script-url': 'error', + 'no-self-compare': 'warn', + 'no-sequences': 'warn', + 'no-throw-literal': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unused-expressions': 'warn', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-useless-return': 'warn', + 'prefer-promise-reject-errors': 'error', + radix: 'off', + 'require-await': 'error', + 'wrap-iife': ['warn', 'inside'], + 'no-shadow': 'error', + 'no-array-constructor': 'error', + 'no-bitwise': 'error', + 'no-multi-assign': 'warn', + 'no-new-object': 'error', + 'no-useless-computed-key': 'warn', + 'no-useless-rename': 'warn', + 'no-var': 'error', + 'prefer-const': 'warn', + 'prefer-numeric-literals': 'warn', + 'prefer-object-spread': 'warn', + 'prefer-rest-params': 'warn', + 'prefer-spread': 'warn', + 'prefer-template': 'off', + 'no-else-return': 'off', + 'func-names': ['warn', 'never'], + 'one-var': ['warn', 'never'], + 'operator-assignment': 'warn', + 'prefer-arrow-callback': 'warn', + 'no-restricted-imports': [ + 'warn', + { + paths: [ + { + name: '.', + message: 'Usage of local index imports is not allowed.' + }, + { + name: './index', + message: 'Import from the source file instead.' + } + ] + } + ], + 'import/extensions': [ + 'error', + 'always', + { + ignorePackages: true, + pattern: { + js: 'always', + ts: 'never' + } + } + ], + 'import/no-duplicates': 'warn', + 'sort-imports-es6-autofix/sort-imports-es6': [ + 2, + { + ignoreCase: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'] + } + ], + 'wc/guard-super-call': 'off' + } +}; diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8c7b60d --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,44 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cory@abeautifulsite.net. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4531395 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [claviska] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a05a514 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug Report +about: Create a bug report to help us fix a demonstrable problem with code in the library. +title: '' +labels: bug +assignees: +--- + +### Describe the bug +A bug is _a demonstrable problem_ caused by code in the library. Please provide a clear and concise description of what the bug is here. + +### To Reproduce +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +### Demo + +If the bug isn't obvious, please provide a link to a CodePen or Fiddle with a minimal reproduction. Bugs that have repros get attention faster than those that don't. + +Tip: use the CodePen button on any example in the docs! + +### Screenshots +If applicable, add screenshots to help explain the bug. + +### Browser / OS + - OS: [e.g. Mac, Windows] + - Browser: [e.g. Chrome, Firefox, Safari] + - Browser version: [e.g. 22] + +### Additional information +Provide any additional information about the bug here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..976fb26 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: Feature Requests + url: https://github.com/shoelace-style/shoelace/discussions/categories/ideas + about: All requests for new features should go here. + - name: Help & Support + url: https://github.com/shoelace-style/shoelace/discussions/categories/help + about: Please don't create issues for personal help requests. Instead, ask your question on the discussion forum. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..5645387 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Issues + +We take security issues in Shoelace very seriously and appreciate your efforts to disclose your findings responsibly. + +To report a security issue, email [cory@abeautifulsite.net](mailto:cory@abeautifulsite.net) and include "SHOELACE SECURITY" in the subject line. + +We'll respond as soon as possible and keep you updated throughout the process. diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..c74c047 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,30 @@ +# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [next] + pull_request: + branches: [next] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npx playwright install --with-deps + - run: npm ci + - run: npm run verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7449ea9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,17 @@ +# This workflow will create a GitHub release every time a tag is pushed +name: Create GitHub Release + +on: + push: + tags: + - "v2.*" + - "v3.*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: "marvinpinto/action-automatic-releases@v1.2.1" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c244c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +_site +.cache +.DS_Store +cdn +dist +docs/assets/images/sprite.svg +node_modules +src/react +.aider* diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..053d74c --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,28 @@ +tasks: + - init: npm install && npm run build + command: npm run start + +ports: + - port: 3001 + onOpen: ignore + - port: 4000-4999 + onOpen: open-preview + +github: + prebuilds: + # enable for the master/default branch (defaults to true) + master: true + # enable for all branches in this repo (defaults to false) + branches: true + # enable for pull requests coming from this repo (defaults to true) + pullRequests: true + # enable for pull requests coming from forks (defaults to false) + pullRequestsFromForks: true + # add a check to pull requests (defaults to true) + addCheck: true + # add a "Review in Gitpod" button as a comment to pull requests (defaults to false) + addComment: false + # add a "Review in Gitpod" button to the pull request's description (defaults to false) + addBadge: true + # add a label once the prebuild is ready to pull requests (defaults to false) + addLabel: true diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..d37daa0 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e3f4ba0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +*.hbs +.cache +.github +cspell.json +dist +docs/search.json +src/components/icon/icons +src/react/index.ts +node_modules +package.json +package-lock.json +tsconfig.json +cdn +_site diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..cb77e25 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bierner.lit-html", + "bashmish.es6-string-css", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..02b5dc1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c1c554c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing to Shoelace + +Before contributing, please review the contributions guidelines at: + +[shoelace.style/resources/contributing](https://shoelace.style/resources/contributing) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..40f8e2e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2020 A Beautiful Site, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e6eed9 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Shoelace + +A forward-thinking library of web components. + +- Works with all frameworks 🧩 +- Works with CDNs 🚛 +- Fully customizable with CSS 🎨 +- Includes an official dark theme 🌛 +- Built with accessibility in mind ♿️ +- Open source 😸 + +Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska). + +--- + +Documentation: [shoelace.style](https://shoelace.style) + +Source: [github.com/shoelace-style/shoelace](https://github.com/shoelace-style/shoelace) + +Twitter: [@shoelace_style](https://twitter.com/shoelace_style) + +--- + +## Shoemakers 🥾 + +Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14.17 to build and run the project locally. + +**You don't need to do any of this to use Shoelace!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Shoelace. + +If that's not what you're trying to do, the [documentation website](https://shoelace.style) is where you want to be. + +### What are you using to build Shoelace? + +Components are built with [LitElement](https://lit-element.polymer-project.org/), a custom elements base class that provides an intuitive API and reactive data binding. The build is a custom script with bundling powered by [esbuild](https://esbuild.github.io/). + +### Forking the Repo + +Start by [forking the repo](https://github.com/shoelace-style/shoelace/fork) on GitHub, then clone it locally and install dependencies. + +```bash +git clone https://github.com/YOUR_GITHUB_USERNAME/shoelace +cd shoelace +npm install +``` + +### Developing + +Once you've cloned the repo, run the following command. + +```bash +npm start +``` + +This will spin up the dev server. After the initial build, a browser will open automatically. There is currently no hot module reloading (HMR), as browser's don't provide a way to reregister custom elements, but most changes to the source will reload the browser automatically. + +### Building + +To generate a production build, run the following command. + +```bash +npm run build +``` + +### Creating New Components + +To scaffold a new component, run the following command, replacing `sl-tag-name` with the desired tag name. + +```bash +npm run create sl-tag-name +``` + +This will generate a source file, a stylesheet, and a docs page for you. When you start the dev server, you'll find the new component in the "Components" section of the sidebar. + +### Contributing + +Shoelace is an open source project and contributions are encouraged! If you're interesting in contributing, please review the [contribution guidelines](CONTRIBUTING.md) first. + +## License + +Shoelace was created by [Cory LaViska](https://twitter.com/claviska) and is available under the terms of the MIT license. + +Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾 diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..a829f88 Binary files /dev/null and b/bun.lockb differ diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..e4bf8c6 --- /dev/null +++ b/cspell.json @@ -0,0 +1,186 @@ +{ + "version": "0.2", + "words": [ + "activedescendant", + "allowfullscreen", + "animationend", + "Animista", + "apos", + "atrule", + "autocorrect", + "autofix", + "autoload", + "autoloader", + "autoloading", + "autoplay", + "bezier", + "boxicons", + "CACHEABLE", + "callout", + "callouts", + "cdndir", + "chatbubble", + "checkmark", + "claviska", + "Clippy", + "codebases", + "codepen", + "colocated", + "colour", + "combobox", + "Commonmark", + "Composability", + "Consolas", + "contenteditable", + "copydir", + "Cotte", + "coverpage", + "crossorigin", + "crutchcorn", + "csspart", + "cssproperty", + "datetime", + "describedby", + "Docsify", + "dogfood", + "dropdowns", + "easings", + "endraw", + "enterkeyhint", + "eqeqeq", + "erroneou", + "errormessage", + "esbuild", + "exportmaps", + "exportparts", + "fieldsets", + "formaction", + "formdata", + "formenctype", + "formmethod", + "formnovalidate", + "formtarget", + "FOUC", + "FOUCE", + "fullscreen", + "gestern", + "giga", + "globby", + "Grayscale", + "haspopup", + "heroicons", + "hexa", + "Iconoir", + "Iframes", + "iife", + "inputmode", + "ionicon", + "ionicons", + "jsDelivr", + "jsfiddle", + "jsonata", + "keydown", + "keyframes", + "Kool", + "labelledby", + "Laravel", + "LaViska", + "linkify", + "listbox", + "listitem", + "litelement", + "lowercasing", + "Lucide", + "maxlength", + "Menlo", + "menuitemcheckbox", + "menuitemradio", + "middlewares", + "minlength", + "monospace", + "mousedown", + "mousemove", + "mouseout", + "mouseup", + "multiselectable", + "nextjs", + "nocheck", + "noopener", + "noreferrer", + "novalidate", + "npmdir", + "Numberish", + "onscrollend", + "outdir", + "ParamagicDev", + "peta", + "petabit", + "prismjs", + "progressbar", + "radiogroup", + "Railsbyte", + "remixicon", + "reregister", + "resizer", + "resizers", + "retargeted", + "RETRYABLE", + "rgba", + "roadmap", + "Roboto", + "roledescription", + "Sapan", + "saturationl", + "Schilp", + "scrollbars", + "scrollend", + "scroller", + "Segoe", + "semibold", + "sitedir", + "slotchange", + "smartquotes", + "spacebar", + "stylesheet", + "Tabbable", + "tabindex", + "tabler", + "tablist", + "tabpanel", + "templating", + "tera", + "testid", + "textareas", + "textfield", + "tinycolor", + "transitionend", + "treeitem", + "treeshaking", + "Triaging", + "turbolinks", + "typeof", + "unbundles", + "unbundling", + "unicons", + "unsanitized", + "unsupportive", + "valpha", + "valuenow", + "valuetext", + "WEBP", + "Webpacker", + "wordmark" + ], + "ignorePaths": [ + "package.json", + "package-lock.json", + "docs/assets/examples/include.html", + ".vscode/**", + "src/translations/!(en).ts", + "**/*.min.js" + ], + "ignoreRegExpList": [ + "(^|[^a-z])sl[a-z]*(^|[^a-z])" + ], + "useGitignore": true +} diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js new file mode 100644 index 0000000..cb8c3ae --- /dev/null +++ b/custom-elements-manifest.config.js @@ -0,0 +1,230 @@ +import * as path from 'path'; +import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration'; +import { customElementVsCodePlugin } from 'custom-element-vs-code-integration'; +import { customElementVuejsPlugin } from 'custom-element-vuejs-integration'; +import { parse } from 'comment-parser'; +import { pascalCase } from 'pascal-case'; +import commandLineArgs from 'command-line-args'; +import fs from 'fs'; + +const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +const { name, description, version, author, homepage, license } = packageData; + +const { outdir } = commandLineArgs([ + { name: 'litelement', type: String }, + { name: 'analyze', defaultOption: true }, + { name: 'outdir', type: String } +]); + +function noDash(string) { + return string.replace(/^\s?-/, '').trim(); +} + +function replace(string, terms) { + terms.forEach(({ from, to }) => { + string = string?.replace(from, to); + }); + + return string; +} + +export default { + globs: ['src/components/**/*.component.ts'], + exclude: ['**/*.styles.ts', '**/*.test.ts'], + plugins: [ + // Append package data + { + name: 'shoelace-package-data', + packageLinkPhase({ customElementsManifest }) { + customElementsManifest.package = { name, description, version, author, homepage, license }; + } + }, + + // Infer tag names because we no longer use @customElement decorators. + { + name: 'shoelace-infer-tag-names', + analyzePhase({ ts, node, moduleDoc }) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: { + const className = node.name.getText(); + const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); + + const importPath = moduleDoc.path; + + // This is kind of a best guess at components. "thing.component.ts" + if (!importPath.endsWith('.component.ts')) { + return; + } + + const tagNameWithoutPrefix = path.basename(importPath, '.component.ts'); + const tagName = 'sl-' + tagNameWithoutPrefix; + + classDoc.tagNameWithoutPrefix = tagNameWithoutPrefix; + classDoc.tagName = tagName; + + // This used to be set to true by @customElement + classDoc.customElement = true; + } + } + } + }, + + // Parse custom jsDoc tags + { + name: 'shoelace-custom-tags', + analyzePhase({ ts, node, moduleDoc }) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: { + const className = node.name.getText(); + const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); + const customTags = ['animation', 'dependency', 'documentation', 'since', 'status', 'title']; + let customComments = '/**'; + + node.jsDoc?.forEach(jsDoc => { + jsDoc?.tags?.forEach(tag => { + const tagName = tag.tagName.getText(); + + if (customTags.includes(tagName)) { + customComments += `\n * @${tagName} ${tag.comment}`; + } + }); + }); + + // This is what allows us to map JSDOC comments to ReactWrappers. + classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n'); + + const parsed = parse(`${customComments}\n */`); + parsed[0].tags?.forEach(t => { + switch (t.tag) { + // Animations + case 'animation': + if (!Array.isArray(classDoc['animations'])) { + classDoc['animations'] = []; + } + classDoc['animations'].push({ + name: t.name, + description: noDash(t.description) + }); + break; + + // Dependencies + case 'dependency': + if (!Array.isArray(classDoc['dependencies'])) { + classDoc['dependencies'] = []; + } + classDoc['dependencies'].push(t.name); + break; + + // Value-only metadata tags + case 'documentation': + case 'since': + case 'status': + case 'title': + classDoc[t.tag] = t.name; + break; + + // All other tags + default: + if (!Array.isArray(classDoc[t.tag])) { + classDoc[t.tag] = []; + } + + classDoc[t.tag].push({ + name: t.name, + description: t.description, + type: t.type || undefined + }); + } + }); + } + } + } + }, + + { + name: 'shoelace-react-event-names', + analyzePhase({ ts, node, moduleDoc }) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: { + const className = node.name.getText(); + const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); + + if (classDoc?.events) { + classDoc.events.forEach(event => { + event.reactName = `on${pascalCase(event.name)}`; + event.eventName = `${pascalCase(event.name)}Event`; + }); + } + } + } + } + }, + + { + name: 'shoelace-translate-module-paths', + packageLinkPhase({ customElementsManifest }) { + customElementsManifest?.modules?.forEach(mod => { + // + // CEM paths look like this: + // + // src/components/button/button.ts + // + // But we want them to look like this: + // + // components/button/button.js + // + const terms = [ + { from: /^src\//, to: '' }, // Strip the src/ prefix + { from: /\.component.(t|j)sx?$/, to: '.js' } // Convert .ts to .js + ]; + + mod.path = replace(mod.path, terms); + + for (const ex of mod.exports ?? []) { + ex.declaration.module = replace(ex.declaration.module, terms); + } + + for (const dec of mod.declarations ?? []) { + if (dec.kind === 'class') { + for (const member of dec.members ?? []) { + if (member.inheritedFrom) { + member.inheritedFrom.module = replace(member.inheritedFrom.module, terms); + } + } + } + } + }); + } + }, + + // Generate custom VS Code data + customElementVsCodePlugin({ + outdir, + cssFileName: null, + referencesTemplate: (_, tag) => [ + { + name: 'Documentation', + url: `https://shoelace.style/components/${tag.replace('sl-', '')}` + } + ] + }), + + customElementJetBrainsPlugin({ + outdir: './dist', + excludeCss: true, + packageJson: false, + referencesTemplate: (_, tag) => { + return { + name: 'Documentation', + url: `https://shoelace.style/components/${tag.replace('sl-', '')}` + }; + } + }), + + customElementVuejsPlugin({ + outdir: './dist/types/vue', + fileName: 'index.d.ts', + componentTypePath: (_, tag) => `../../components/${tag.replace('sl-', '')}/${tag.replace('sl-', '')}.component.js` + }) + ] +}; diff --git a/docs/_includes/component.njk b/docs/_includes/component.njk new file mode 100644 index 0000000..e377e69 --- /dev/null +++ b/docs/_includes/component.njk @@ -0,0 +1,349 @@ +{% extends "default.njk" %} + +{# Find the component based on the `tag` front matter #} +{% set component = getComponent('sl-' + page.fileSlug) %} + +{% block content %} + {# Determine the badge variant #} + {% if component.status == 'stable' %} + {% set badgeVariant = 'primary' %} + {% elseif component.status == 'experimental' %} + {% set badgeVariant = 'warning' %} + {% elseif component.status == 'planned' %} + {% set badgeVariant = 'neutral' %} + {% elseif component.status == 'deprecated' %} + {% set badgeVariant = 'danger' %} + {% else %} + {% set badgeVariant = 'neutral' %} + {% endif %} + + {# Header #} +
+

{{ component.name | classNameToComponentName }}

+ +
+ <{{ component.tagName }}> | {{ component.name }} +
+ +
+ + Since {{component.since or '?' }} + + + {{ component.status }} + +
+
+ +

+ {% if component.summary %} + {{ component.summary | markdownInline | safe }} + {% endif %} +

+ + {# Markdown content #} + {{ content | safe }} + + {# Importing #} +

Importing

+

+ If you're using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use + any of the following snippets to cherry pick this component. +

+ + + Script + Import + Bundler + React + + +

+ To import this component from the CDN + using a script tag: +

+
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}"></script>
+
+ + +

+ To import this component from the CDN + using a JavaScript import: +

+
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}';
+
+ + +

+ To import this component using a bundler: +

+
import '@shoelace-style/shoelace/{{ meta.npmdir }}/{{ component.path }}';
+
+ + +

+ To import this component as a React component: +

+
import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';
+
+
+ + {# Slots #} + {% if component.slots.length %} +

Slots

+ + + + + + + + + + {% for slot in component.slots %} + + + + + {% endfor %} + +
NameDescription
+ {% if slot.name %} + {{ slot.name }} + {% else %} + (default) + {% endif %} + {{ slot.description | markdownInline | safe }}
+ +

Learn more about using slots.

+ {% endif %} + + {# Properties #} + {% if component.properties.length %} +

Properties

+ + + + + + + + + + + + + {% for prop in component.properties %} + + + + + + + + {% endfor %} + + + + + + + + +
NameDescriptionReflectsTypeDefault
+ {{ prop.name }} + {% if prop.attribute | length > 0 %} + {% if prop.attribute != prop.name %} +
+ + + + {{ prop.attribute }} + + + + {% endif %} + {% endif %} +
+ {{ prop.description | markdownInline | safe }} + + {% if prop.reflects %} + + {% endif %} + + {% if prop.type.text %} + {{ prop.type.text | trimPipes | markdownInline | safe }} + {% else %} + - + {% endif %} + + {% if prop.default %} + {{ prop.default | markdownInline | safe }} + {% else %} + - + {% endif %} +
updateComplete + A read-only promise that resolves when the component has + finished updating. +
+ +

Learn more about attributes and properties.

+ {% endif %} + + {# Events #} + {% if component.events.length %} +

Events

+ + + + + + + + + + + + {% for event in component.events %} + + + + + + + {% endfor %} + +
NameReact EventDescriptionEvent Detail
{{ event.name }}{{ event.reactName }}{{ event.description | markdownInline | safe }} + {% if event.type.text %} + {{ event.type.text | trimPipes }} + {% else %} + - + {% endif %} +
+ +

Learn more about events.

+ {% endif %} + + {# Methods #} + {% if component.methods.length %} +

Methods

+ + + + + + + + + + + {% for method in component.methods %} + + + + + + {% endfor %} + +
NameDescriptionArguments
{{ method.name }}(){{ method.description | markdownInline | safe }} + {% if method.parameters.length %} + + {% for param in method.parameters %} + {{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %} + {% endfor %} + + {% else %} + - + {% endif %} +
+ +

Learn more about methods.

+ {% endif %} + + {# Custom Properties #} + {% if component.cssProperties.length %} +

Custom Properties

+ + + + + + + + + + + {% for cssProperty in component.cssProperties %} + + + + + + {% endfor %} + +
NameDescriptionDefault
{{ cssProperty.name }}{{ cssProperty.description | markdownInline | safe }}{{ cssProperty.default }}
+ +

Learn more about customizing CSS custom properties.

+ {% endif %} + + {# CSS Parts #} + {% if component.cssParts.length %} +

Parts

+ + + + + + + + + + {% for cssPart in component.cssParts %} + + + + + {% endfor %} + +
NameDescription
{{ cssPart.name }}{{ cssPart.description | markdownInline | safe }}
+ +

Learn more about customizing CSS parts.

+ {% endif %} + + {# Animations #} + {% if component.animations.length %} +

Animations

+ + + + + + + + + + {% for animation in component.animations %} + + + + + {% endfor %} + +
NameDescription
{{ animation.name }}{{ animation.description | markdownInline | safe }}
+ +

Learn more about customizing animations.

+ {% endif %} + + {# Dependencies #} + {% if component.dependencies.length %} +

Dependencies

+ +

This component automatically imports the following dependencies.

+ + + {% endif %} +{% endblock %} diff --git a/docs/_includes/default.njk b/docs/_includes/default.njk new file mode 100644 index 0000000..315d11e --- /dev/null +++ b/docs/_includes/default.njk @@ -0,0 +1,167 @@ + + + + {# Metadata #} + + + + {{ meta.title }} + + {# Opt out of Turbo caching #} + + + {# Stylesheets #} + + + + + {# Favicons #} + + + {# Twitter Cards #} + + + + + {# OpenGraph #} + + + + + + {# Shoelace #} + + + + + {# Set the initial theme and menu states here to prevent flashing #} + + + {# Turbo + Scroll positioning #} + + + + + + + + + Skip to main content + + + {# Menu toggle #} + + + {# Icon toolbar #} +
+ {# GitHub #} + + + + + {# Twitter #} + + + + + {# Theme selector #} + + + + + + + Light + Dark + + System + + +
+ + + + + + + + Get ready for more awesome! + Web Awesome, the next iteration of Shoelace, is on Kickstarter. + + + Read Our Story + + + + + {# Content #} +
+ +
+ {% if toc %} + + {% endif %} + +
+ {% block content %} + {{ content | safe }} + {% endblock %} +
+
+
+ + diff --git a/docs/_includes/sidebar.njk b/docs/_includes/sidebar.njk new file mode 100644 index 0000000..11745e6 --- /dev/null +++ b/docs/_includes/sidebar.njk @@ -0,0 +1,66 @@ + diff --git a/docs/_utilities/active-links.cjs b/docs/_utilities/active-links.cjs new file mode 100644 index 0000000..7a99805 --- /dev/null +++ b/docs/_utilities/active-links.cjs @@ -0,0 +1,35 @@ +function normalizePathname(pathname) { + // Remove /index.html + if (pathname.endsWith('/index.html')) { + pathname = pathname.replace(/\/index\.html/, ''); + } + + // Remove trailing slashes + return pathname.replace(/\/$/, ''); +} + +/** + * Adds a class name to links that are currently active. + */ +module.exports = function (doc, options) { + options = { + className: 'active-link', // the class to add to active links + pathname: undefined, // the current pathname to compare + within: 'body', // element containing the target links + ...options + }; + + const within = doc.querySelector(options.within); + + if (!within) { + return doc; + } + + within.querySelectorAll('a').forEach(link => { + if (normalizePathname(options.pathname) === normalizePathname(link.pathname)) { + link.classList.add(options.className); + } + }); + + return doc; +}; diff --git a/docs/_utilities/anchor-headings.cjs b/docs/_utilities/anchor-headings.cjs new file mode 100644 index 0000000..64a7166 --- /dev/null +++ b/docs/_utilities/anchor-headings.cjs @@ -0,0 +1,64 @@ +const { createSlug } = require('./strings.cjs'); + +/** + * Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM. + * The same document will be returned with the appropriate DOM manipulations. + */ +module.exports = function (doc, options) { + options = { + levels: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], // the headings to convert + className: 'anchor-heading', // the class name to add + within: 'body', // the element containing the target headings + ...options + }; + + const within = doc.querySelector(options.within); + + if (!within) { + return doc; + } + + within.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => { + const hasAnchor = heading.querySelector('a'); + const anchor = doc.createElement('a'); + let id = heading.textContent ?? ''; + let suffix = 0; + + // Skip heading levels we don't care about + if (!options.levels?.includes(heading.tagName.toLowerCase())) { + return; + } + + // Convert dots to underscores + id = id.replace(/\./g, '_'); + + // Turn it into a slug + id = createSlug(id); + + // Make sure it starts with a letter + if (!/^[a-z]/i.test(id)) { + id = `id_${id}`; + } + + // Make sure the id is unique + const originalId = id; + while (doc.getElementById(id) !== null) { + id = `${originalId}-${++suffix}`; + } + + if (hasAnchor || !id) return; + + heading.setAttribute('id', id); + anchor.setAttribute('href', `#${encodeURIComponent(id)}`); + anchor.setAttribute('aria-label', `Direct link to "${heading.textContent}"`); + + if (options.className) { + heading.classList.add(options.className); + } + + // Append the anchor + heading.append(anchor); + }); + + return doc; +}; diff --git a/docs/_utilities/cem.cjs b/docs/_utilities/cem.cjs new file mode 100644 index 0000000..8fb1bc1 --- /dev/null +++ b/docs/_utilities/cem.cjs @@ -0,0 +1,71 @@ +const customElementsManifest = require('../../dist/custom-elements.json'); + +// +// Export it here so we can import it elsewhere and use the same version +// +module.exports.customElementsManifest = customElementsManifest; + +// +// Gets all components from custom-elements.json and returns them in a more documentation-friendly format. +// +module.exports.getAllComponents = function () { + const allComponents = []; + + customElementsManifest.modules?.forEach(module => { + module.declarations?.forEach(declaration => { + if (declaration.customElement) { + // Generate the dist path based on the src path and attach it to the component + declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js'); + + // Remove members that are private or don't have a description + const members = declaration.members?.filter(member => member.description && member.privacy !== 'private'); + const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private'); + const properties = members?.filter(prop => { + // Look for a corresponding attribute + const attribute = declaration.attributes?.find(attr => attr.fieldName === prop.name); + if (attribute) { + prop.attribute = attribute.name || attribute.fieldName; + } + + return prop.kind === 'field' && prop.privacy !== 'private'; + }); + allComponents.push({ + ...declaration, + methods, + properties + }); + } + }); + }); + + // Build dependency graphs + allComponents.forEach(component => { + const dependencies = []; + + // Recursively fetch sub-dependencies + function getDependencies(tag) { + const cmp = allComponents.find(c => c.tagName === tag); + if (!cmp || !Array.isArray(component.dependencies)) { + return; + } + + cmp.dependencies?.forEach(dependentTag => { + if (!dependencies.includes(dependentTag)) { + dependencies.push(dependentTag); + } + getDependencies(dependentTag); + }); + } + + getDependencies(component.tagName); + + component.dependencies = dependencies.sort(); + }); + + // Sort by name + return allComponents.sort((a, b) => { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); +}; diff --git a/docs/_utilities/code-previews.cjs b/docs/_utilities/code-previews.cjs new file mode 100644 index 0000000..89840f6 --- /dev/null +++ b/docs/_utilities/code-previews.cjs @@ -0,0 +1,138 @@ +let count = 1; + +function escapeHtml(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +/** + * Turns code fields with the :preview suffix into interactive code previews. + */ +module.exports = function (doc, options) { + options = { + within: 'body', // the element containing the code fields to convert + ...options + }; + + const within = doc.querySelector(options.within); + if (!within) { + return doc; + } + + within.querySelectorAll('[class*=":preview"]').forEach(code => { + const pre = code.closest('pre'); + if (!pre) { + return; + } + const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null; + const reactCode = adjacentPre?.querySelector('code[class$="react"]'); + const sourceGroupId = `code-preview-source-group-${count}`; + const isExpanded = code.getAttribute('class').includes(':expanded'); + const noCodePen = code.getAttribute('class').includes(':no-codepen'); + + count++; + + const htmlButton = ` + + `; + + const reactButton = ` + + `; + + const codePenButton = ` + + `; + + const codePreview = ` +
+
+ ${code.textContent} +
+ +
+
+ +
+
+
${escapeHtml(code.textContent)}
+
+ + ${ + reactCode + ? ` +
+
${escapeHtml(reactCode.textContent)}
+
+ ` + : '' + } +
+ +
+ + + ${reactCode ? ` ${htmlButton} ${reactButton} ` : ''} + + ${noCodePen ? '' : codePenButton} +
+
+ `; + + pre.insertAdjacentHTML('afterend', codePreview); + pre.remove(); + + if (adjacentPre) { + adjacentPre.remove(); + } + }); + + // Wrap code preview scripts in anonymous functions so they don't run in the global scope + doc.querySelectorAll('.code-preview__preview script').forEach(script => { + if (script.type === 'module') { + // Modules are already scoped + script.textContent = script.innerHTML; + } else { + // Wrap non-modules in an anonymous function so they don't run in the global scope + script.textContent = `(() => { ${script.innerHTML} })();`; + } + }); + + return doc; +}; diff --git a/docs/_utilities/copy-code-buttons.cjs b/docs/_utilities/copy-code-buttons.cjs new file mode 100644 index 0000000..11b1c8c --- /dev/null +++ b/docs/_utilities/copy-code-buttons.cjs @@ -0,0 +1,23 @@ +let codeBlockId = 0; + +/** + * Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same + * document will be returned with the appropriate DOM manipulations. + */ +module.exports = function (doc) { + doc.querySelectorAll('pre > code').forEach(code => { + const pre = code.closest('pre'); + const button = doc.createElement('sl-copy-button'); + + if (!code.id) { + code.id = `code-block-${++codeBlockId}`; + } + + button.classList.add('copy-code-button'); + button.setAttribute('from', code.id); + + pre.append(button); + }); + + return doc; +}; diff --git a/docs/_utilities/external-links.cjs b/docs/_utilities/external-links.cjs new file mode 100644 index 0000000..36a9589 --- /dev/null +++ b/docs/_utilities/external-links.cjs @@ -0,0 +1,41 @@ +const { isExternalLink } = require('./strings.cjs'); + +/** + * Transforms external links to make them safer and optionally add a target. The provided doc should be a document + * object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations. + */ +module.exports = function (doc, options) { + options = { + className: 'external-link', // the class name to add to links + noopener: true, // sets rel="noopener" + noreferrer: true, // sets rel="noreferrer" + ignore: () => false, // callback function to filter links that should be ignored + within: 'body', // element that contains the target links + target: '', // sets the target attribute + ...options + }; + + const within = doc.querySelector(options.within); + + if (within) { + within.querySelectorAll('a').forEach(link => { + if (isExternalLink(link) && !options.ignore(link)) { + link.classList.add(options.className); + + const rel = []; + if (options.noopener) rel.push('noopener'); + if (options.noreferrer) rel.push('noreferrer'); + + if (rel.length) { + link.setAttribute('rel', rel.join(' ')); + } + + if (options.target) { + link.setAttribute('target', options.target); + } + } + }); + } + + return doc; +}; diff --git a/docs/_utilities/highlight-code.cjs b/docs/_utilities/highlight-code.cjs new file mode 100644 index 0000000..bb4c01f --- /dev/null +++ b/docs/_utilities/highlight-code.cjs @@ -0,0 +1,63 @@ +const Prism = require('prismjs'); +const PrismLoader = require('prismjs/components/index.js'); + +PrismLoader('diff'); +PrismLoader.silent = true; + +/** Highlights a code string. */ +function highlight(code, language) { + const alias = language.replace(/^diff-/, ''); + const isDiff = /^diff-/i.test(language); + + // Auto-load the target language + if (!Prism.languages[alias]) { + PrismLoader(alias); + + if (!Prism.languages[alias]) { + throw new Error(`Unsupported language for code highlighting: "${language}"`); + } + } + + // Register diff-* languages to use the diff grammar + if (isDiff) { + Prism.languages[language] = Prism.languages.diff; + } + + return Prism.highlight(code, Prism.languages[language], language); +} + +/** + * Highlights all code fields that have a language parameter. If the language has a colon in its name, the first chunk + * will be the language used and additional chunks will be applied as classes to the `
`. For example, a code field
+ * tagged with "html:preview" will be rendered as `
`.
+ *
+ * The provided doc should be a document object provided by JSDOM. The same document will be returned with the
+ * appropriate DOM manipulations.
+ */
+module.exports = function (doc) {
+  doc.querySelectorAll('pre > code[class]').forEach(code => {
+    // Look for class="language-*" and split colons into separate classes
+    code.classList.forEach(className => {
+      if (className.startsWith('language-')) {
+        //
+        // We use certain suffixes to indicate code previews, expanded states, etc. The class might look something like
+        // this:
+        //
+        //  class="language-html:preview:expanded"
+        //
+        // The language will always come first, so we need to drop the "language-" prefix and everything after the first
+        // color to get the highlighter language.
+        //
+        const language = className.replace(/^language-/, '').split(':')[0];
+
+        try {
+          code.innerHTML = highlight(code.textContent ?? '', language);
+        } catch (err) {
+          // Language not found, skip it
+        }
+      }
+    });
+  });
+
+  return doc;
+};
diff --git a/docs/_utilities/markdown.cjs b/docs/_utilities/markdown.cjs
new file mode 100644
index 0000000..4a73e8f
--- /dev/null
+++ b/docs/_utilities/markdown.cjs
@@ -0,0 +1,67 @@
+const MarkdownIt = require('markdown-it');
+const markdownItContainer = require('markdown-it-container');
+const markdownItIns = require('markdown-it-ins');
+const markdownItKbd = require('markdown-it-kbd');
+const markdownItMark = require('markdown-it-mark');
+const markdownItReplaceIt = require('markdown-it-replace-it');
+
+const markdown = MarkdownIt({
+  html: true,
+  xhtmlOut: false,
+  breaks: false,
+  langPrefix: 'language-',
+  linkify: false,
+  typographer: false
+});
+
+// Third-party plugins
+markdown.use(markdownItContainer);
+markdown.use(markdownItIns);
+markdown.use(markdownItKbd);
+markdown.use(markdownItMark);
+markdown.use(markdownItReplaceIt);
+
+// Callouts
+['tip', 'warning', 'danger'].forEach(type => {
+  markdown.use(markdownItContainer, type, {
+    render: function (tokens, idx) {
+      if (tokens[idx].nesting === 1) {
+        return `\n';
+    }
+  });
+});
+
+// Asides
+markdown.use(markdownItContainer, 'aside', {
+  render: function (tokens, idx) {
+    if (tokens[idx].nesting === 1) {
+      return `\n';
+  }
+});
+
+// Details
+markdown.use(markdownItContainer, 'details', {
+  validate: params => params.trim().match(/^details\s+(.*)$/),
+  render: (tokens, idx) => {
+    const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
+    if (tokens[idx].nesting === 1) {
+      return `
\n${markdown.utils.escapeHtml(m[1])}\n`; + } + return '
\n'; + } +}); + +// Replace [#1234] with a link to GitHub issues +markdownItReplaceIt.replacements.push({ + name: 'github-issues', + re: /\[#([0-9]+)\]/gs, + sub: '#$1', + html: true, + default: true +}); + +module.exports = markdown; diff --git a/docs/_utilities/prettier.cjs b/docs/_utilities/prettier.cjs new file mode 100644 index 0000000..db18fba --- /dev/null +++ b/docs/_utilities/prettier.cjs @@ -0,0 +1,26 @@ +const { format } = require('prettier'); + +/** Formats markup using prettier. */ +module.exports = function (content, options) { + options = { + arrowParens: 'avoid', + bracketSpacing: true, + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + bracketSameLine: false, + jsxSingleQuote: false, + parser: 'html', + printWidth: 120, + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'none', + useTabs: false, + ...options + }; + + return format(content, options); +}; diff --git a/docs/_utilities/replacer.cjs b/docs/_utilities/replacer.cjs new file mode 100644 index 0000000..5d7908e --- /dev/null +++ b/docs/_utilities/replacer.cjs @@ -0,0 +1,22 @@ +/** + * @typedef {object} Replacement + * @property {string | RegExp} pattern + * @property {string} replacement + */ + +/** + * @typedef {Array} Replacements + */ + +/** + * @param {String} rawContent + * @param {Replacements} replacements + */ +module.exports = function (rawContent, replacements) { + let content = rawContent; + replacements.forEach(replacement => { + content = content.replaceAll(replacement.pattern, replacement.replacement); + }); + + return content; +}; diff --git a/docs/_utilities/scrolling-tables.cjs b/docs/_utilities/scrolling-tables.cjs new file mode 100644 index 0000000..148248d --- /dev/null +++ b/docs/_utilities/scrolling-tables.cjs @@ -0,0 +1,21 @@ +/** + * Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM. + * The same document will be returned with the appropriate DOM manipulations. + */ +module.exports = function (doc, options) { + const tables = [...doc.querySelectorAll('table')]; + + options = { + className: 'table-scroll', // the class name to add to the table's container + ...options + }; + + tables.forEach(table => { + const div = doc.createElement('div'); + div.classList.add(options.className); + table.insertAdjacentElement('beforebegin', div); + div.append(table); + }); + + return doc; +}; diff --git a/docs/_utilities/strings.cjs b/docs/_utilities/strings.cjs new file mode 100644 index 0000000..6831d66 --- /dev/null +++ b/docs/_utilities/strings.cjs @@ -0,0 +1,16 @@ +const slugify = require('slugify'); + +/** Creates a slug from an arbitrary string of text. */ +module.exports.createSlug = function (text) { + return slugify(String(text), { + remove: /[^\w|\s]/g, + lower: true + }); +}; + +/** Determines whether or not a link is external. */ +module.exports.isExternalLink = function (link) { + // We use the "internal" hostname when initializing JSDOM so we know that those are local links + if (!link.hostname || link.hostname === 'internal') return false; + return true; +}; diff --git a/docs/_utilities/table-of-contents.cjs b/docs/_utilities/table-of-contents.cjs new file mode 100644 index 0000000..1ac04fd --- /dev/null +++ b/docs/_utilities/table-of-contents.cjs @@ -0,0 +1,42 @@ +/** + * Generates an in-page table of contents based on headings. + */ +module.exports = function (doc, options) { + options = { + levels: ['h2'], // headings to include (they must have an id) + container: 'nav', // the container to append links to + listItem: true, // if true, links will be wrapped in
  • + within: 'body', // the element containing the headings to summarize + ...options + }; + + const container = doc.querySelector(options.container); + const within = doc.querySelector(options.within); + const headingSelector = options.levels.map(h => `${h}[id]`).join(', '); + + if (!container || !within) { + return doc; + } + + within.querySelectorAll(headingSelector).forEach(heading => { + const listItem = doc.createElement('li'); + const link = doc.createElement('a'); + const level = heading.tagName.slice(1); + + link.href = `#${heading.id}`; + link.textContent = heading.textContent; + + if (options.listItem) { + // List item + link + listItem.setAttribute('data-level', level); + listItem.append(link); + container.append(listItem); + } else { + // Link only + link.setAttribute('data-level', level); + container.append(link); + } + }); + + return doc; +}; diff --git a/docs/_utilities/typography.cjs b/docs/_utilities/typography.cjs new file mode 100644 index 0000000..53fe84b --- /dev/null +++ b/docs/_utilities/typography.cjs @@ -0,0 +1,23 @@ +const smartquotes = require('smartquotes'); + +smartquotes.replacements.push([/---/g, '\u2014']); // em dash +smartquotes.replacements.push([/--/g, '\u2013']); // en dash +smartquotes.replacements.push([/\.\.\./g, '\u2026']); // ellipsis +smartquotes.replacements.push([/\(c\)/gi, '\u00A9']); // copyright +smartquotes.replacements.push([/\(r\)/gi, '\u00AE']); // registered trademark +smartquotes.replacements.push([/\?!/g, '\u2048']); // ?! +smartquotes.replacements.push([/!!/g, '\u203C']); // !! +smartquotes.replacements.push([/\?\?/g, '\u2047']); // ?? +smartquotes.replacements.push([/([0-9]\s?)-(\s?[0-9])/g, '$1\u2013$2']); // number ranges use en dash + +/** + * Improves typography by adding smart quotes and similar corrections within the specified element(s). + * + * The provided doc should be a document object provided by JSDOM. The same document will be returned with the + * appropriate DOM manipulations. + */ +module.exports = function (doc, selector = 'body') { + const elements = [...doc.querySelectorAll(selector)]; + elements.forEach(el => smartquotes.element(el)); + return doc; +}; diff --git a/docs/assets/examples/carousel/field.jpg b/docs/assets/examples/carousel/field.jpg new file mode 100644 index 0000000..a742b19 Binary files /dev/null and b/docs/assets/examples/carousel/field.jpg differ diff --git a/docs/assets/examples/carousel/mountains.jpg b/docs/assets/examples/carousel/mountains.jpg new file mode 100644 index 0000000..17a7ea3 Binary files /dev/null and b/docs/assets/examples/carousel/mountains.jpg differ diff --git a/docs/assets/examples/carousel/sunset.jpg b/docs/assets/examples/carousel/sunset.jpg new file mode 100644 index 0000000..811b0df Binary files /dev/null and b/docs/assets/examples/carousel/sunset.jpg differ diff --git a/docs/assets/examples/carousel/valley.jpg b/docs/assets/examples/carousel/valley.jpg new file mode 100644 index 0000000..bd575fb Binary files /dev/null and b/docs/assets/examples/carousel/valley.jpg differ diff --git a/docs/assets/examples/carousel/waterfall.jpg b/docs/assets/examples/carousel/waterfall.jpg new file mode 100644 index 0000000..b8fa264 Binary files /dev/null and b/docs/assets/examples/carousel/waterfall.jpg differ diff --git a/docs/assets/examples/include.html b/docs/assets/examples/include.html new file mode 100644 index 0000000..3a5dc62 --- /dev/null +++ b/docs/assets/examples/include.html @@ -0,0 +1,19 @@ +

    + The content in this example was included from + a separate file. 🤯 +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Fringilla urna porttitor rhoncus dolor purus + non enim. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Gravida in fermentum et sollicitudin. +

    +

    + Cursus sit amet dictum sit amet justo donec enim. Sed id semper risus in hendrerit gravida. Viverra accumsan in nisl + nisi scelerisque eu ultrices vitae. Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit. Nec + ullamcorper sit amet risus nullam. Et egestas quis ipsum suspendisse ultrices gravida dictum. Lorem donec massa sapien + faucibus et molestie. A cras semper auctor neque vitae. +

    + + diff --git a/docs/assets/images/awesome.svg b/docs/assets/images/awesome.svg new file mode 100644 index 0000000..cca7602 --- /dev/null +++ b/docs/assets/images/awesome.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/assets/images/chrome.png b/docs/assets/images/chrome.png new file mode 100644 index 0000000..3024356 Binary files /dev/null and b/docs/assets/images/chrome.png differ diff --git a/docs/assets/images/edge.png b/docs/assets/images/edge.png new file mode 100644 index 0000000..931cb1c Binary files /dev/null and b/docs/assets/images/edge.png differ diff --git a/docs/assets/images/firefox.png b/docs/assets/images/firefox.png new file mode 100644 index 0000000..fc71232 Binary files /dev/null and b/docs/assets/images/firefox.png differ diff --git a/docs/assets/images/gitpod.svg b/docs/assets/images/gitpod.svg new file mode 100644 index 0000000..a30505a --- /dev/null +++ b/docs/assets/images/gitpod.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/logo.svg b/docs/assets/images/logo.svg new file mode 100644 index 0000000..6d15ad2 --- /dev/null +++ b/docs/assets/images/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/assets/images/og-image.png b/docs/assets/images/og-image.png new file mode 100644 index 0000000..282010f Binary files /dev/null and b/docs/assets/images/og-image.png differ diff --git a/docs/assets/images/opera.png b/docs/assets/images/opera.png new file mode 100644 index 0000000..472a152 Binary files /dev/null and b/docs/assets/images/opera.png differ diff --git a/docs/assets/images/safari.png b/docs/assets/images/safari.png new file mode 100644 index 0000000..aba5f86 Binary files /dev/null and b/docs/assets/images/safari.png differ diff --git a/docs/assets/images/shoe.svg b/docs/assets/images/shoe.svg new file mode 100644 index 0000000..98e53d1 --- /dev/null +++ b/docs/assets/images/shoe.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + blue boot + 2009-06-18T11:16:58 + A draw of a boot + https://openclipart.org/detail/26707/blue-boot-by-badaman + + + badaman + + + + + azul + blue + boot + bota + deporte + inkscape + line art + monochrome + sport + vectorial + + + + + + + + + + + diff --git a/docs/assets/images/tie.webp b/docs/assets/images/tie.webp new file mode 100644 index 0000000..cc83a85 Binary files /dev/null and b/docs/assets/images/tie.webp differ diff --git a/docs/assets/images/touch-icon.png b/docs/assets/images/touch-icon.png new file mode 100644 index 0000000..8398dad Binary files /dev/null and b/docs/assets/images/touch-icon.png differ diff --git a/docs/assets/images/twitter-card.png b/docs/assets/images/twitter-card.png new file mode 100644 index 0000000..f547340 Binary files /dev/null and b/docs/assets/images/twitter-card.png differ diff --git a/docs/assets/images/undraw-content-team.svg b/docs/assets/images/undraw-content-team.svg new file mode 100644 index 0000000..0b01be2 --- /dev/null +++ b/docs/assets/images/undraw-content-team.svg @@ -0,0 +1,80 @@ + + + + undraw-content-team + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/undraw-not-found.svg b/docs/assets/images/undraw-not-found.svg new file mode 100644 index 0000000..aea1d50 --- /dev/null +++ b/docs/assets/images/undraw-not-found.svg @@ -0,0 +1 @@ +not found \ No newline at end of file diff --git a/docs/assets/images/undraw-taken.svg b/docs/assets/images/undraw-taken.svg new file mode 100644 index 0000000..3b52384 --- /dev/null +++ b/docs/assets/images/undraw-taken.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/images/walk.gif b/docs/assets/images/walk.gif new file mode 100644 index 0000000..5aaedb1 Binary files /dev/null and b/docs/assets/images/walk.gif differ diff --git a/docs/assets/images/wordmark.svg b/docs/assets/images/wordmark.svg new file mode 100644 index 0000000..f853ed3 --- /dev/null +++ b/docs/assets/images/wordmark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/assets/scripts/code-previews.js b/docs/assets/scripts/code-previews.js new file mode 100644 index 0000000..8963f56 --- /dev/null +++ b/docs/assets/scripts/code-previews.js @@ -0,0 +1,249 @@ +(() => { + function convertModuleLinks(html) { + html = html + .replace(/@shoelace-style\/shoelace/g, `https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}`) + .replace(/from 'react'/g, `from 'https://esm.sh/react@${reactVersion}'`) + .replace(/from "react"/g, `from "https://esm.sh/react@${reactVersion}"`); + + return html; + } + + function getAdjacentExample(name, pre) { + let currentPre = pre.nextElementSibling; + + while (currentPre?.tagName.toLowerCase() === 'pre') { + if (currentPre?.getAttribute('data-lang').split(' ').includes(name)) { + return currentPre; + } + + currentPre = currentPre.nextElementSibling; + } + + return null; + } + + function runScript(script) { + const newScript = document.createElement('script'); + + if (script.type === 'module') { + newScript.type = 'module'; + newScript.textContent = script.innerHTML; + } else { + newScript.appendChild(document.createTextNode(`(() => { ${script.innerHTML} })();`)); + } + + script.parentNode.replaceChild(newScript, script); + } + + function getFlavor() { + return sessionStorage.getItem('flavor') || 'html'; + } + + function setFlavor(newFlavor) { + flavor = ['html', 'react'].includes(newFlavor) ? newFlavor : 'html'; + sessionStorage.setItem('flavor', flavor); + + // Set the flavor class on the body + document.documentElement.classList.toggle('flavor-html', flavor === 'html'); + document.documentElement.classList.toggle('flavor-react', flavor === 'react'); + } + + function syncFlavor() { + setFlavor(getFlavor()); + + document.querySelectorAll('.code-preview__button--html').forEach(preview => { + if (flavor === 'html') { + preview.classList.add('code-preview__button--selected'); + } + }); + + document.querySelectorAll('.code-preview__button--react').forEach(preview => { + if (flavor === 'react') { + preview.classList.add('code-preview__button--selected'); + } + }); + } + + const shoelaceVersion = document.documentElement.getAttribute('data-shoelace-version'); + const reactVersion = '18.2.0'; + const cdndir = 'cdn'; + const npmdir = 'dist'; + let flavor = getFlavor(); + let count = 1; + + // We need the version to open + if (!shoelaceVersion) { + throw new Error('The data-shoelace-version attribute is missing from .'); + } + + // Sync flavor UI on page load + syncFlavor(); + + // + // Resizing previews + // + document.addEventListener('mousedown', handleResizerDrag); + document.addEventListener('touchstart', handleResizerDrag, { passive: true }); + + function handleResizerDrag(event) { + const resizer = event.target.closest('.code-preview__resizer'); + const preview = event.target.closest('.code-preview__preview'); + + if (!resizer || !preview) return; + + let startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX; + let startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10); + + event.preventDefault(); + preview.classList.add('code-preview__preview--dragging'); + document.documentElement.addEventListener('mousemove', dragMove); + document.documentElement.addEventListener('touchmove', dragMove); + document.documentElement.addEventListener('mouseup', dragStop); + document.documentElement.addEventListener('touchend', dragStop); + + function dragMove(event) { + const width = startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX; + preview.style.width = `${width}px`; + } + + function dragStop() { + preview.classList.remove('code-preview__preview--dragging'); + document.documentElement.removeEventListener('mousemove', dragMove); + document.documentElement.removeEventListener('touchmove', dragMove); + document.documentElement.removeEventListener('mouseup', dragStop); + document.documentElement.removeEventListener('touchend', dragStop); + } + } + + // + // Toggle source mode + // + document.addEventListener('click', event => { + const button = event.target.closest('.code-preview__button'); + const codeBlock = button?.closest('.code-preview'); + + if (button?.classList.contains('code-preview__button--html')) { + // Show HTML + setFlavor('html'); + toggleSource(codeBlock, true); + } else if (button?.classList.contains('code-preview__button--react')) { + // Show React + setFlavor('react'); + toggleSource(codeBlock, true); + } else if (button?.classList.contains('code-preview__toggle')) { + // Toggle source + toggleSource(codeBlock); + } else { + return; + } + + // Update flavor buttons + [...document.querySelectorAll('.code-preview')].forEach(cb => { + cb.querySelector('.code-preview__button--html')?.classList.toggle( + 'code-preview__button--selected', + flavor === 'html' + ); + cb.querySelector('.code-preview__button--react')?.classList.toggle( + 'code-preview__button--selected', + flavor === 'react' + ); + }); + }); + + function toggleSource(codeBlock, force) { + codeBlock.classList.toggle('code-preview--expanded', force); + event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-preview--expanded')); + } + + // + // Open in CodePen + // + document.addEventListener('click', event => { + const button = event.target.closest('button'); + + if (button?.classList.contains('code-preview__button--codepen')) { + const codeBlock = button.closest('.code-preview'); + const htmlExample = codeBlock.querySelector('.code-preview__source--html > pre > code')?.textContent; + const reactExample = codeBlock.querySelector('.code-preview__source--react > pre > code')?.textContent; + const isReact = flavor === 'react' && typeof reactExample === 'string'; + const theme = document.documentElement.classList.contains('sl-theme-dark') ? 'dark' : 'light'; + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const isDark = theme === 'dark' || (theme === 'auto' && prefersDark); + const editors = isReact ? '0010' : '1000'; + let htmlTemplate = ''; + let jsTemplate = ''; + let cssTemplate = ''; + + const form = document.createElement('form'); + form.action = 'https://codepen.io/pen/define'; + form.method = 'POST'; + form.target = '_blank'; + + // HTML templates + if (!isReact) { + htmlTemplate = + `\n` + + `\n${htmlExample}`; + jsTemplate = ''; + } + + // React templates + if (isReact) { + htmlTemplate = '
    '; + jsTemplate = + `import React from 'https://esm.sh/react@${reactVersion}';\n` + + `import ReactDOM from 'https://esm.sh/react-dom@${reactVersion}';\n` + + `import { setBasePath } from 'https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/utilities/base-path';\n` + + `\n` + + `// Set the base path for Shoelace assets\n` + + `setBasePath('https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${npmdir}/')\n` + + `\n${convertModuleLinks(reactExample)}\n` + + `\n` + + `ReactDOM.render(, document.getElementById('root'));`; + } + + // CSS templates + cssTemplate = + `@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/themes/${ + isDark ? 'dark' : 'light' + }.css';\n` + + '\n' + + 'body {\n' + + ' font: 16px sans-serif;\n' + + ' background-color: var(--sl-color-neutral-0);\n' + + ' color: var(--sl-color-neutral-900);\n' + + ' padding: 1rem;\n' + + '}'; + + // Docs: https://blog.codepen.io/documentation/prefill/ + const data = { + title: '', + description: '', + tags: ['shoelace', 'web components'], + editors, + head: ``, + html_classes: `sl-theme-${isDark ? 'dark' : 'light'}`, + css_external: ``, + js_external: ``, + js_module: true, + js_pre_processor: isReact ? 'babel' : 'none', + html: htmlTemplate, + css: cssTemplate, + js: jsTemplate + }; + + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'data'; + input.value = JSON.stringify(data); + form.append(input); + + document.documentElement.append(form); + form.submit(); + form.remove(); + } + }); + + // Set the initial flavor + window.addEventListener('turbo:load', syncFlavor); +})(); diff --git a/docs/assets/scripts/docs.js b/docs/assets/scripts/docs.js new file mode 100644 index 0000000..a98bc0b --- /dev/null +++ b/docs/assets/scripts/docs.js @@ -0,0 +1,272 @@ +// +// Sidebar +// +// When the sidebar is hidden, we apply the inert attribute to prevent focus from reaching it. Due to the many states +// the sidebar can have (e.g. static, hidden, expanded), we test for visibility by checking to see if it's placed +// offscreen or not. Then, on resize/transition we make sure to update the attribute accordingly. +// +(() => { + function getSidebar() { + return document.getElementById('sidebar'); + } + + function isSidebarOpen() { + return document.documentElement.classList.contains('sidebar-open'); + } + + function isSidebarVisible() { + return getSidebar().getBoundingClientRect().x >= 0; + } + + function toggleSidebar(force) { + const isOpen = typeof force === 'boolean' ? force : !isSidebarOpen(); + return document.documentElement.classList.toggle('sidebar-open', isOpen); + } + + function updateInert() { + getSidebar().inert = !isSidebarVisible(); + } + + // Toggle the menu + document.addEventListener('click', event => { + const menuToggle = event.target.closest('#menu-toggle'); + if (!menuToggle) return; + toggleSidebar(); + }); + + // Update the sidebar's inert state when the window resizes and when the sidebar transitions + window.addEventListener('resize', () => toggleSidebar(false)); + + document.addEventListener('transitionend', event => { + const sidebar = event.target.closest('#sidebar'); + if (!sidebar) return; + updateInert(); + }); + + // Close when a menu item is selected on mobile + document.addEventListener('click', event => { + const sidebar = event.target.closest('#sidebar'); + const link = event.target.closest('a'); + if (!sidebar || !link) return; + + if (isSidebarOpen()) { + toggleSidebar(); + } + }); + + // Close when open and escape is pressed + document.addEventListener('keydown', event => { + if (event.key === 'Escape' && isSidebarOpen()) { + event.stopImmediatePropagation(); + toggleSidebar(); + } + }); + + // Close when clicking outside of the sidebar + document.addEventListener('mousedown', event => { + if (isSidebarOpen() & !event.target?.closest('#sidebar, #menu-toggle')) { + event.stopImmediatePropagation(); + toggleSidebar(); + } + }); + + updateInert(); +})(); + +// +// Theme selector +// +(() => { + function getTheme() { + return localStorage.getItem('theme') || 'auto'; + } + + function isDark() { + if (theme === 'auto') { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + return theme === 'dark'; + } + + function setTheme(newTheme) { + theme = newTheme; + localStorage.setItem('theme', theme); + + // Update the UI + updateSelection(); + + // Toggle the dark mode class + document.documentElement.classList.toggle('sl-theme-dark', isDark()); + } + + function updateSelection() { + const menu = document.querySelector('#theme-selector sl-menu'); + if (!menu) return; + [...menu.querySelectorAll('sl-menu-item')].map(item => (item.checked = item.getAttribute('value') === theme)); + } + + let theme = getTheme(); + + // Selection is not preserved when changing page, so update when opening dropdown + document.addEventListener('sl-show', event => { + const themeSelector = event.target.closest('#theme-selector'); + if (!themeSelector) return; + updateSelection(); + }); + + // Listen for selections + document.addEventListener('sl-select', event => { + const menu = event.target.closest('#theme-selector sl-menu'); + if (!menu) return; + setTheme(event.detail.item.value); + }); + + // Update the theme when the preference changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setTheme(theme)); + + // Toggle with backslash + document.addEventListener('keydown', event => { + if ( + event.key === '\\' && + !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) + ) { + event.preventDefault(); + setTheme(isDark() ? 'light' : 'dark'); + } + }); + + // Set the initial theme and sync the UI + setTheme(theme); +})(); + +// +// Open details when printing +// +(() => { + const detailsOpenOnPrint = new Set(); + + window.addEventListener('beforeprint', () => { + detailsOpenOnPrint.clear(); + document.querySelectorAll('details').forEach(details => { + if (details.open) { + detailsOpenOnPrint.add(details); + } + details.open = true; + }); + }); + + window.addEventListener('afterprint', () => { + document.querySelectorAll('details').forEach(details => { + details.open = detailsOpenOnPrint.has(details); + }); + detailsOpenOnPrint.clear(); + }); +})(); + +// +// Smooth links +// +(() => { + document.addEventListener('click', event => { + const link = event.target.closest('a'); + const id = (link?.hash ?? '').substr(1); + const isFragment = link?.hasAttribute('href') && link?.getAttribute('href').startsWith('#'); + + if (!link || !isFragment || link.getAttribute('data-smooth-link') === 'false') { + return; + } + + // Scroll to the top + if (link.hash === '') { + event.preventDefault(); + window.scroll({ top: 0, behavior: 'smooth' }); + history.pushState(undefined, undefined, location.pathname); + } + + // Scroll to an id + if (id) { + const target = document.getElementById(id); + + if (target) { + event.preventDefault(); + window.scroll({ top: target.offsetTop, behavior: 'smooth' }); + history.pushState(undefined, undefined, `#${id}`); + } + } + }); +})(); + +// +// Table of Contents scrollspy +// +(() => { + // This will be stale if its not a function. + const getLinks = () => [...document.querySelectorAll('.content__toc a')]; + const linkTargets = new WeakMap(); + const visibleTargets = new WeakSet(); + const observer = new IntersectionObserver(handleIntersect, { rootMargin: '0px 0px' }); + let debounce; + + function handleIntersect(entries) { + entries.forEach(entry => { + // Remember which targets are visible + if (entry.isIntersecting) { + visibleTargets.add(entry.target); + } else { + visibleTargets.delete(entry.target); + } + }); + + updateActiveLinks(); + } + + function updateActiveLinks() { + const links = getLinks(); + // Find the first visible target and activate the respective link + links.find(link => { + const target = linkTargets.get(link); + + if (target && visibleTargets.has(target)) { + links.forEach(el => el.classList.toggle('active', el === link)); + return true; + } + + return false; + }); + } + + // Observe link targets + function observeLinks() { + getLinks().forEach(link => { + const hash = link.hash.slice(1); + const target = hash ? document.querySelector(`.content__body #${hash}`) : null; + + if (target) { + linkTargets.set(link, target); + observer.observe(target); + } + }); + } + + observeLinks(); + + document.addEventListener('turbo:load', updateActiveLinks); + document.addEventListener('turbo:load', observeLinks); +})(); + +// +// Show custom versions in the sidebar +// +(() => { + function updateVersion() { + const el = document.querySelector('.sidebar-version'); + if (!el) return; + + if (location.hostname === 'next.shoelace.style') el.textContent = 'Next'; + if (location.hostname === 'localhost') el.textContent = 'Development'; + } + + updateVersion(); + + document.addEventListener('turbo:load', updateVersion); +})(); diff --git a/docs/assets/scripts/search.js b/docs/assets/scripts/search.js new file mode 100644 index 0000000..ead4efa --- /dev/null +++ b/docs/assets/scripts/search.js @@ -0,0 +1,384 @@ +(() => { + // Append the search dialog to the body + const siteSearch = document.createElement('div'); + const scrollbarWidth = Math.abs(window.innerWidth - document.documentElement.clientWidth); + + siteSearch.classList.add('search'); + siteSearch.innerHTML = ` +
    + +
    +
    +
    + + + +
    +
    +
    +
      +
      No matching pages
      +
      +
      + Navigate + Select + Esc Close +
      +
      +
      + `; + + const overlay = siteSearch.querySelector('.search__overlay'); + const dialog = siteSearch.querySelector('.search__dialog'); + const input = siteSearch.querySelector('.search__input'); + const clearButton = siteSearch.querySelector('.search__clear-button'); + const results = siteSearch.querySelector('.search__results'); + const version = document.documentElement.getAttribute('data-shoelace-version'); + const key = `search_${version}`; + const searchDebounce = 50; + const animationDuration = 150; + let isShowing = false; + let searchTimeout; + let searchIndex; + let map; + + const loadSearchIndex = new Promise(resolve => { + const cache = localStorage.getItem(key); + const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame; + + // Cleanup older search indices (everything before this version) + try { + const items = { ...localStorage }; + + Object.keys(items).forEach(k => { + if (key > k) { + localStorage.removeItem(k); + } + }); + } catch { + /* do nothing */ + } + + // Look for a cached index + try { + if (cache) { + const data = JSON.parse(cache); + + searchIndex = window.lunr.Index.load(data.searchIndex); + map = data.map; + + return resolve(); + } + } catch { + /* do nothing */ + } + + // Wait until idle to fetch the index + wait(() => { + fetch('/assets/search.json') + .then(res => res.json()) + .then(data => { + if (!window.lunr) { + console.error('The Lunr search client has not yet been loaded.'); + } + + searchIndex = window.lunr.Index.load(data.searchIndex); + map = data.map; + + // Cache the search index for this version + if (version) { + try { + localStorage.setItem(key, JSON.stringify(data)); + } catch (err) { + console.warn(`Unable to cache the search index: ${err}`); + } + } + + resolve(); + }); + }); + }); + + async function show() { + isShowing = true; + document.body.append(siteSearch); + document.body.classList.add('search-visible'); + document.body.style.setProperty('--docs-search-scroll-lock-size', `${scrollbarWidth}px`); + clearButton.hidden = true; + requestAnimationFrame(() => input.focus()); + updateResults(); + + dialog.showModal(); + + await Promise.all([ + dialog.animate( + [ + { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' }, + { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' } + ], + { duration: animationDuration } + ).finished, + overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished + ]); + + dialog.addEventListener('mousedown', handleMouseDown); + dialog.addEventListener('keydown', handleKeyDown); + } + + async function hide() { + isShowing = false; + + await Promise.all([ + dialog.animate( + [ + { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' }, + { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' } + ], + { duration: animationDuration } + ).finished, + overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished + ]); + + dialog.close(); + + input.blur(); // otherwise Safari will scroll to the bottom of the page on close + input.value = ''; + document.body.classList.remove('search-visible'); + document.body.style.removeProperty('--docs-search-scroll-lock-size'); + siteSearch.remove(); + updateResults(); + + dialog.removeEventListener('mousedown', handleMouseDown); + dialog.removeEventListener('keydown', handleKeyDown); + } + + function handleInput() { + clearButton.hidden = input.value === ''; + + // Debounce search queries + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce); + } + + function handleClear() { + clearButton.hidden = true; + input.value = ''; + input.focus(); + updateResults(); + } + + function handleMouseDown(event) { + if (!event.target.closest('.search__content')) { + hide(); + } + } + + function handleKeyDown(event) { + // Close when pressing escape + if (event.key === 'Escape') { + event.preventDefault(); // prevent from closing immediately so it can animate + event.stopImmediatePropagation(); + hide(); + return; + } + + // Handle keyboard selections + if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) { + event.preventDefault(); + + const currentEl = results.querySelector('[data-selected="true"]'); + const items = [...results.querySelectorAll('li')]; + const index = items.indexOf(currentEl); + let nextEl; + + if (items.length === 0) { + return; + } + + switch (event.key) { + case 'ArrowUp': + nextEl = items[Math.max(0, index - 1)]; + break; + case 'ArrowDown': + nextEl = items[Math.min(items.length - 1, index + 1)]; + break; + case 'Home': + nextEl = items[0]; + break; + case 'End': + nextEl = items[items.length - 1]; + break; + case 'Enter': + currentEl?.querySelector('a')?.click(); + break; + } + + // Update the selected item + items.forEach(item => { + if (item === nextEl) { + input.setAttribute('aria-activedescendant', item.id); + item.setAttribute('data-selected', 'true'); + nextEl.scrollIntoView({ block: 'nearest' }); + } else { + item.setAttribute('data-selected', 'false'); + } + }); + } + } + + async function updateResults(query = '') { + try { + await loadSearchIndex; + + const hasQuery = query.length > 0; + const searchTerms = query + .split(' ') + .map((term, index, arr) => { + // Search API: https://lunrjs.com/guides/searching.html + if (index === arr.length - 1) { + // The last term is not mandatory and 1x fuzzy. We also duplicate it with a wildcard to match partial words + // as the user types. + return `${term}~1 ${term}*`; + } else { + // All other terms are mandatory and 1x fuzzy + return `+${term}~1`; + } + }) + .join(' '); + const matches = hasQuery ? searchIndex.search(searchTerms) : []; + const hasResults = hasQuery && matches.length > 0; + + siteSearch.classList.toggle('search--has-results', hasQuery && hasResults); + siteSearch.classList.toggle('search--no-results', hasQuery && !hasResults); + + input.setAttribute('aria-activedescendant', ''); + results.innerHTML = ''; + + matches.forEach((match, index) => { + const page = map[match.ref]; + const li = document.createElement('li'); + const a = document.createElement('a'); + const displayTitle = page.title ?? ''; + const displayDescription = page.description ?? ''; + const displayUrl = page.url.replace(/^\//, '').replace(/\/$/, ''); + let icon = 'file-text'; + + a.setAttribute('role', 'option'); + a.setAttribute('id', `search-result-item-${match.ref}`); + + if (page.url.includes('getting-started/')) { + icon = 'lightbulb'; + } + if (page.url.includes('resources/')) { + icon = 'book'; + } + if (page.url.includes('components/')) { + icon = 'puzzle'; + } + if (page.url.includes('tokens/')) { + icon = 'palette2'; + } + if (page.url.includes('utilities/')) { + icon = 'wrench'; + } + if (page.url.includes('tutorials/')) { + icon = 'joystick'; + } + + li.classList.add('search__result'); + li.setAttribute('role', 'option'); + li.setAttribute('id', `search-result-item-${match.ref}`); + li.setAttribute('data-selected', index === 0 ? 'true' : 'false'); + + a.href = page.url; + a.innerHTML = ` + +
      +
      +
      +
      +
      + `; + a.querySelector('.search__result-title').textContent = displayTitle; + a.querySelector('.search__result-description').textContent = displayDescription; + a.querySelector('.search__result-url').textContent = displayUrl; + + li.appendChild(a); + results.appendChild(li); + }); + } catch { + // Ignore query errors as the user types + } + } + + // Show the search dialog when clicking on data-plugin="search" + document.addEventListener('click', event => { + const searchButton = event.target.closest('[data-plugin="search"]'); + if (searchButton) { + show(); + } + }); + + // Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element + document.addEventListener('keydown', event => { + if ( + !isShowing && + (event.key === '/' || (event.key === 'k' && (event.metaKey || event.ctrlKey))) && + !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) + ) { + event.preventDefault(); + show(); + } + }); + + // Purge cache when we press CMD+CTRL+R + document.addEventListener('keydown', event => { + if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'r') { + localStorage.clear(); + } + }); + + input.addEventListener('input', handleInput); + clearButton.addEventListener('click', handleClear); + + // Close when a result is selected + results.addEventListener('click', event => { + if (event.target.closest('a')) { + hide(); + } + }); + + // We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search + // UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't + // get trapped. + window.addEventListener('turbo:render', () => { + document.body.classList.remove('search-visible'); + document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove()); + }); +})(); diff --git a/docs/assets/scripts/turbo.js b/docs/assets/scripts/turbo.js new file mode 100644 index 0000000..7075217 --- /dev/null +++ b/docs/assets/scripts/turbo.js @@ -0,0 +1,29 @@ +import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm'; + +(() => { + if (!window.scrollPositions) { + window.scrollPositions = {}; + } + + function preserveScroll() { + document.querySelectorAll('[data-preserve-scroll').forEach(element => { + scrollPositions[element.id] = element.scrollTop; + }); + } + + function restoreScroll(event) { + document.querySelectorAll('[data-preserve-scroll').forEach(element => { + element.scrollTop = scrollPositions[element.id]; + }); + + if (event.detail && event.detail.newBody) { + event.detail.newBody.querySelectorAll('[data-preserve-scroll').forEach(element => { + element.scrollTop = scrollPositions[element.id]; + }); + } + } + + window.addEventListener('turbo:before-cache', preserveScroll); + window.addEventListener('turbo:before-render', restoreScroll); + window.addEventListener('turbo:render', restoreScroll); +})(); diff --git a/docs/assets/styles/code-previews.css b/docs/assets/styles/code-previews.css new file mode 100644 index 0000000..0fa5efa --- /dev/null +++ b/docs/assets/styles/code-previews.css @@ -0,0 +1,173 @@ +/* Interactive code blocks */ +.code-preview { + position: relative; + border-radius: 3px; + background-color: var(--sl-color-neutral-50); + margin-bottom: 1.5rem; +} + +.code-preview__preview { + position: relative; + border: solid 1px var(--sl-color-neutral-200); + border-bottom: none; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + background-color: var(--sl-color-neutral-0); + min-width: 20rem; + max-width: 100%; + padding: 1.5rem 3.25rem 1.5rem 1.5rem; +} + +/* Block the preview while dragging to prevent iframes from intercepting drag events */ +.code-preview__preview--dragging:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: ew-resize; +} + +.code-preview__resizer { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 1.75rem; + font-size: 20px; + color: var(--sl-color-neutral-600); + background-color: var(--sl-color-neutral-0); + border-left: solid 1px var(--sl-color-neutral-200); + border-top-right-radius: 3px; + cursor: ew-resize; +} + +@media screen and (max-width: 600px) { + .code-preview__preview { + padding-right: 1.5rem; + } + + .code-preview__resizer { + display: none; + } +} + +.code-preview__source { + border: solid 1px var(--sl-color-neutral-200); + border-bottom: none; + border-radius: 0 !important; + display: none; +} + +.code-preview--expanded .code-preview__source { + display: block; +} + +.code-preview__source pre { + margin: 0; +} + +.code-preview__buttons { + position: relative; + border: solid 1px var(--sl-color-neutral-200); + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + display: flex; +} + +.code-preview__button { + flex: 0 0 auto; + height: 2.5rem; + min-width: 2.5rem; + border: none; + border-radius: 0; + background: var(--sl-color-neutral-0); + font: inherit; + font-size: 0.7rem; + font-weight: 500; + text-transform: uppercase; + color: var(--sl-color-neutral-600); + padding: 0 1rem; + cursor: pointer; +} + +.code-preview__button:not(:last-of-type) { + border-right: solid 1px var(--sl-color-neutral-200); +} + +.code-preview__button--html, +.code-preview__button--react { + width: 70px; + display: flex; + place-items: center; + justify-content: center; +} + +.code-preview__button--selected { + font-weight: 700; + color: var(--sl-color-primary-600); +} + +.code-preview__button--codepen { + display: flex; + place-items: center; + width: 6rem; +} + +.code-preview__button:first-of-type { + border-bottom-left-radius: 3px; +} + +.code-preview__button:last-of-type { + border-bottom-right-radius: 3px; +} + +.code-preview__button:hover, +.code-preview__button:active { + box-shadow: 0 0 0 1px var(--sl-color-primary-400); + border-right-color: transparent; + background-color: var(--sl-color-primary-50); + color: var(--sl-color-primary-600); + z-index: 1; +} + +.code-preview__button:focus-visible { + outline: none; + outline: var(--sl-focus-ring); + z-index: 2; +} + +.code-preview__toggle { + position: relative; + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; + width: 100%; + color: var(--sl-color-neutral-600); + cursor: pointer; +} + +.code-preview__toggle svg { + width: 1em; + height: 1em; + margin-left: 0.25rem; +} + +.code-preview--expanded .code-preview__toggle svg { + transform: rotate(180deg); +} + +/* We can apply data-flavor="html|react" to any element on the page to toggle it when the flavor changes */ +.flavor-html [data-flavor]:not([data-flavor='html']) { + display: none; +} + +.flavor-react [data-flavor]:not([data-flavor='react']) { + display: none; +} diff --git a/docs/assets/styles/docs.css b/docs/assets/styles/docs.css new file mode 100644 index 0000000..9182898 --- /dev/null +++ b/docs/assets/styles/docs.css @@ -0,0 +1,1507 @@ +:root { + --docs-background-color: var(--sl-color-neutral-0); + --docs-border-color: var(--sl-color-neutral-200); + --docs-border-width: 1px; + --docs-border-radius: var(--sl-border-radius-medium); + --docs-content-max-width: 860px; + --docs-sidebar-width: 320px; + --docs-sidebar-transition-speed: 250ms; + --docs-content-toc-max-width: 260px; + --docs-content-padding: 2rem; + --docs-content-vertical-spacing: 2rem; + --docs-search-overlay-background: rgb(0 0 0 / 0.2); + --docs-skip-to-main-width: 200px; +} + +/* Light theme */ +:root { + color-scheme: normal; + + --sl-color-primary-50: var(--sl-color-sky-50); + --sl-color-primary-100: var(--sl-color-sky-100); + --sl-color-primary-200: var(--sl-color-sky-200); + --sl-color-primary-300: var(--sl-color-sky-300); + --sl-color-primary-400: var(--sl-color-sky-400); + --sl-color-primary-500: var(--sl-color-sky-500); + --sl-color-primary-600: var(--sl-color-sky-600); + --sl-color-primary-700: var(--sl-color-sky-700); + --sl-color-primary-800: var(--sl-color-sky-800); + --sl-color-primary-900: var(--sl-color-sky-900); + --sl-color-primary-950: var(--sl-color-sky-950); + + --docs-overlay-color: hsl(240 3.8% 46.1% / 33%); + --docs-shadow-x-small: 0 1px 2px hsl(240 3.8% 46.1% / 12%); + --docs-shadow-small: 0 1px 2px hsl(240 3.8% 46.1% / 24%); + --docs-shadow-medium: 0 2px 4px hsl(240 3.8% 46.1% / 24%); + --docs-shadow-large: 0 2px 8px hsl(240 3.8% 46.1% / 24%); + --docs-shadow-x-large: 0 4px 16px hsl(240 3.8% 46.1% / 24%); +} + +/* Dark theme */ +.sl-theme-dark { + color-scheme: dark; + + --docs-overlay-color: hsl(0 0% 0% / 66%); + --docs-shadow-x-small: 0 1px 2px rgb(0 0 0 / 36%); + --docs-shadow-small: 0 1px 2px rgb(0 0 0 / 48%); + --docs-shadow-medium: 0 2px 4px rgb(0 0 0 / 48%); + --docs-shadow-large: 0 2px 8px rgb(0 0 0 / 48%); + --docs-shadow-x-large: 0 4px 16px rgb(0 0 0 / 48%); +} + +/* Utils */ +html.sl-theme-dark .only-light, +html:not(.sl-theme-dark) .only-dark { + display: none !important; +} + +.visually-hidden:not(:focus-within) { + position: absolute !important; + width: 1px !important; + height: 1px !important; + clip: rect(0 0 0 0) !important; + clip-path: inset(50%) !important; + border: none !important; + overflow: hidden !important; + white-space: nowrap !important; + padding: 0 !important; +} + +.nowrap { + white-space: nowrap; +} + +@media screen and (max-width: 900px) { + :root { + --docs-content-padding: 1rem; + } +} + +/* Bare styles */ +*, +*:before, +*:after { + box-sizing: inherit; +} + +a:focus, +button:focus { + outline: none; +} + +a:focus-visible, +button:focus-visible { + outline: var(--sl-focus-ring); + outline-offset: var(--sl-focus-ring-offset); +} + +::selection { + background-color: var(--sl-color-primary-200); + color: var(--sl-color-neutral-600); +} + +/* Show custom elements only after they're registered */ +:not(:defined), +:not(:defined) * { + opacity: 0; +} + +:defined { + opacity: 1; + transition: 0.1s opacity; +} + +html { + height: 100%; + box-sizing: border-box; + line-height: var(--sl-line-height-normal); + padding: 0; + margin: 0; +} + +body { + height: 100%; + font: 16px var(--sl-font-sans); + font-weight: var(--sl-font-weight-normal); + background-color: var(--docs-background-color); + line-height: var(--sl-line-height-normal); + color: var(--sl-color-neutral-900); + padding: 0; + margin: 0; + overflow-x: hidden; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +/* Common elements */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--sl-font-sans); + font-weight: var(--sl-font-weight-semibold); + margin: 3rem 0 1.5rem 0; +} + +h1:first-of-type { + margin-top: 1rem; +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 1.875rem; + border-bottom: solid var(--docs-border-width) var(--docs-border-color); +} + +h3 { + font-size: 1.625rem; +} + +h4 { + font-size: 1.375rem; +} + +h5 { + font-size: 1.125rem; +} + +h6 { + font-size: 0.875rem; +} + +p { + margin-block-end: 1.5em; +} + +img { + max-width: 100%; +} + +.badges img { + border-radius: var(--sl-border-radius-medium); +} + +.callout img, +details img { + width: 100%; + margin-left: 0; + margin-right: 0; +} + +details pre { + border: solid var(--docs-border-width) var(--docs-border-color); +} + +a { + color: var(--sl-color-primary-700); +} + +a:hover { + color: var(--sl-color-primary-800); +} + +abbr[title] { + text-decoration: none; + border-bottom: dashed 1px var(--sl-color-primary-700); + cursor: help; +} + +em { + font-style: italic; +} + +strong { + font-weight: var(--sl-font-weight-bold); +} + +code { + font-family: var(--sl-font-mono); + font-size: 0.9125em; + background-color: rgba(0 0 0 / 0.025); + background-blend-mode: darken; + border-radius: var(--docs-border-radius); + padding: 0.125em 0.25em; +} + +.sl-theme-dark code { + background-color: rgba(255 255 255 / 0.03); +} + +kbd { + background: var(--sl-color-neutral-100); + border: solid 1px var(--sl-color-neutral-200); + box-shadow: + inset 0 1px 0 0 var(--sl-color-neutral-0), + inset 0 -1px 0 0 var(--sl-color-neutral-200); + font-family: var(--sl-font-mono); + font-size: 0.9125em; + border-radius: var(--docs-border-radius); + color: var(--sl-color-neutral-800); + padding: 0.125em 0.4em; +} + +ins { + background-color: var(--sl-color-green-200); + color: var(--sl-color-green-900); + border-radius: var(--docs-border-radius); + text-decoration: none; + padding: 0.125em 0.25em; +} + +s { + background-color: var(--sl-color-red-200); + color: var(--sl-color-red-900); + border-radius: var(--docs-border-radius); + text-decoration: none; + padding: 0.125em 0.25em; +} + +mark { + background-color: var(--sl-color-yellow-200); + border-radius: var(--docs-border-radius); + padding: 0.125em 0.25em; +} + +hr { + border: none; + border-bottom: solid var(--docs-border-width) var(--docs-border-color); + margin: calc(var(--docs-content-vertical-spacing) * 2) 0; +} + +/* Block quotes */ +blockquote { + position: relative; + font-family: var(--sl-font-serif); + font-size: 1.33rem; + font-style: italic; + color: var(--sl-color-neutral-700); + background-color: var(--sl-color-neutral-100); + border-radius: var(--docs-border-radius); + border-left: solid 6px var(--sl-color-neutral-300); + padding: 1.5rem; + margin: 0 0 1.5rem 0; +} + +blockquote > :first-child { + margin-top: 0; +} + +blockquote > :last-child { + margin-bottom: 0; +} + +/* Lists */ +ul, +ol { + padding: 0; + margin: 0 0 var(--docs-content-vertical-spacing) 2rem; +} + +ul { + list-style: disc; +} + +li { + padding: 0; + margin: 0 0 0.25rem 0; +} + +li ul, +li ol { + margin-top: 0.25rem; +} + +ul ul:last-child, +ul ol:last-child, +ol ul:last-child, +ol ol:last-child { + margin-bottom: 0; +} + +/* Anchor headings */ +.anchor-heading { + position: relative; + color: inherit; + text-decoration: none; +} + +.anchor-heading a { + text-decoration: none; + color: inherit; +} + +.anchor-heading a::after { + content: '#'; + color: var(--sl-color-primary-700); + margin-inline: 0.5rem; + opacity: 0; + transition: 100ms opacity; +} + +.anchor-heading:hover a::after, +.anchor-heading:focus-within a::after { + opacity: 1; +} + +/* External links */ +.external-link__icon { + width: 0.75em; + height: 0.75em; + vertical-align: 0; + margin-left: 0.25em; + margin-right: 0.125em; +} + +/* Tables */ +table { + max-width: 100%; + border: none; + border-collapse: collapse; + color: inherit; + margin-bottom: var(--docs-content-vertical-spacing); +} + +table tr { + border-bottom: solid var(--docs-border-width) var(--docs-border-color); +} + +table th { + font-weight: var(--sl-font-weight-semibold); + text-align: left; +} + +table td, +table th { + line-height: var(--sl-line-height-normal); + padding: 1rem 1rem; +} + +table th p:first-child, +table td p:first-child { + margin-top: 0; +} + +table th p:last-child, +table td p:last-child { + margin-bottom: 0; +} + +.table-scroll { + max-width: 100%; + overflow-x: auto; +} + +.table-scroll code { + white-space: nowrap; +} + +th.table-name, +th.table-event-detail { + min-width: 15ch; +} + +th.table-description { + min-width: 50ch !important; + max-width: 70ch; +} + +/* Code blocks */ +pre { + position: relative; + background-color: var(--sl-color-neutral-50); + font-family: var(--sl-font-mono); + color: var(--sl-color-neutral-900); + border-radius: var(--docs-border-radius); + padding: 1rem; + white-space: pre; +} + +.sl-theme-dark pre { + background-color: var(--sl-color-neutral-50); +} + +pre:not(:last-child) { + margin-bottom: 1.5rem; +} + +pre > code { + display: block; + background: none !important; + border-radius: 0; + hyphens: none; + tab-size: 2; + white-space: pre; + padding: 1rem; + margin: -1rem; + overflow: auto; +} + +pre .token.comment { + color: var(--sl-color-neutral-600); +} + +pre .token.prolog, +pre .token.doctype, +pre .token.cdata, +pre .token.operator, +pre .token.punctuation { + color: var(--sl-color-neutral-700); +} + +.namespace { + opacity: 0.7; +} + +pre .token.property, +pre .token.keyword, +pre .token.tag, +pre .token.url { + color: var(--sl-color-blue-700); +} + +pre .token.symbol, +pre .token.deleted { + color: var(--sl-color-red-700); +} + +pre .token.boolean, +pre .token.constant, +pre .token.selector, +pre .token.attr-name, +pre .token.string, +pre .token.char, +pre .token.builtin, +pre .token.inserted { + color: var(--sl-color-emerald-700); +} + +pre .token.atrule, +pre .token.attr-value, +pre .token.number, +pre .token.variable { + color: var(--sl-color-violet-700); +} + +pre .token.function, +pre .token.class-name, +pre .token.regex { + color: var(--sl-color-orange-700); +} + +pre .token.important { + color: var(--sl-color-red-700); +} + +pre .token.important, +pre .token.bold { + font-weight: bold; +} + +pre .token.italic { + font-style: italic; +} + +/* Copy code button */ +.copy-code-button { + position: absolute; + top: 0; + right: 0; + white-space: normal; + color: var(--sl-color-neutral-800); + transition: + 150ms opacity, + 150ms scale; +} + +.copy-code-button::part(button) { + background-color: var(--sl-color-neutral-50); + border-radius: 0 var(--docs-border-radius) 0 var(--docs-border-radius); + padding: 0.75rem; +} + +.copy-code-button::part(button):hover { + background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 3%); +} + +.copy-code-button::part(button):active { + background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 6%); +} + +pre .copy-code-button { + opacity: 0; + scale: 0.75; +} + +pre:hover .copy-code-button, +.copy-code-button:focus-within { + opacity: 1; + scale: 1; +} + +/* Callouts */ +.callout { + position: relative; + background-color: var(--sl-color-neutral-100); + border-left: solid 4px var(--sl-color-neutral-500); + border-radius: var(--docs-border-radius); + color: var(--sl-color-neutral-800); + padding: 1.5rem 1.5rem 1.5rem 2rem; + margin-bottom: var(--docs-content-vertical-spacing); +} + +.callout > :first-child { + margin-top: 0; +} + +.callout > :last-child { + margin-bottom: 0; +} + +.callout--tip { + background-color: var(--sl-color-primary-100); + border-left-color: var(--sl-color-primary-600); + color: var(--sl-color-primary-800); +} + +.callout::before { + content: ''; + position: absolute; + top: calc(50% - 0.8rem); + left: calc(-0.8rem - 2px); + width: 1.6rem; + height: 1.6rem; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--sl-font-serif); + font-weight: var(--sl-font-weight-bold); + color: var(--sl-color-neutral-0); + clip-path: circle(50% at 50% 50%); +} + +.callout--tip::before { + content: 'i'; + font-style: italic; + background-color: var(--sl-color-primary-600); +} + +.callout--warning { + background-color: var(--sl-color-warning-100); + border-left-color: var(--sl-color-warning-600); + color: var(--sl-color-warning-800); +} + +.callout--warning::before { + content: '!'; + background-color: var(--sl-color-warning-600); +} + +.callout--danger { + background-color: var(--sl-color-danger-100); + border-left-color: var(--sl-color-danger-600); + color: var(--sl-color-danger-800); +} + +.callout--danger::before { + content: '‼'; + background-color: var(--sl-color-danger-600); +} + +.callout + .callout { + margin-top: calc(-0.5 * var(--docs-content-vertical-spacing)); +} + +.callout a { + color: inherit; +} + +/* Aside */ +.content aside { + float: right; + min-width: 300px; + max-width: 50%; + background: var(--sl-color-neutral-100); + border-radius: var(--docs-border-radius); + padding: 1rem; + margin-left: 1rem; +} + +.content aside > :first-child { + margin-top: 0; +} + +.content aside > :last-child { + margin-bottom: 0; +} + +@media screen and (max-width: 600px) { + .content aside { + float: none; + width: calc(100% + (var(--docs-content-padding) * 2)); + max-width: none; + margin: var(--docs-content-vertical-spacing) calc(-1 * var(--docs-content-padding)); + } +} + +/* Details */ +.content details { + background-color: var(--sl-color-neutral-100); + border-radius: var(--docs-border-radius); + padding: 1rem; + margin: 0 0 var(--docs-content-vertical-spacing) 0; +} + +.content details summary { + font-weight: var(--sl-font-weight-semibold); + border-radius: var(--docs-border-radius); + padding: 1rem; + margin: -1rem; + cursor: pointer; + user-select: none; + -webkit-user-select: none; +} + +.content details summary span { + padding-left: 0.5rem; +} + +.content details[open] summary { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + margin-bottom: 1rem; +} + +.content details[open] summary:focus-visible { + border-radius: var(--docs-border-radius); +} + +.content details > :last-child { + margin-bottom: 0; +} + +.content details > :nth-child(2) { + margin-top: 0; +} + +.content details + details { + margin-top: calc(-1 * var(--docs-content-vertical-spacing) + (2 * var(--docs-border-width))); +} + +/* Sidebar */ +#sidebar { + position: fixed; + flex: 0; + top: 0; + left: 0; + bottom: 0; + z-index: 20; + width: var(--docs-sidebar-width); + background-color: var(--docs-background-color); + border-right: solid var(--docs-border-width) var(--docs-border-color); + border-radius: 0; + padding: 2rem; + margin: 0; + overflow: auto; + scrollbar-width: thin; + transition: var(--docs-sidebar-transition-speed) translate ease-in-out; +} + +#sidebar::-webkit-scrollbar { + width: 4px; +} + +#sidebar::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 9999px; +} + +#sidebar:hover::-webkit-scrollbar-thumb { + background: var(--sl-color-neutral-400); +} + +#sidebar:hover::-webkit-scrollbar-track { + background: var(--sl-color-neutral-100); +} + +#sidebar > header { + margin-bottom: 1.5rem; +} + +#sidebar > header h1 { + margin: 0; +} + +#sidebar > header a { + display: block; +} + +#sidebar nav a { + text-decoration: none; +} + +#sidebar nav h2 { + font-size: var(--sl-font-size-medium); + font-weight: var(--sl-font-weight-semibold); + border-bottom: solid var(--docs-border-width) var(--docs-border-color); + margin: 1.5rem 0 0.5rem 0; +} + +#sidebar ul { + padding: 0; + margin: 0; +} + +#sidebar ul li { + list-style: none; + padding: 0; + margin: 0.125rem 0.5rem; +} + +#sidebar ul ul ul { + margin-left: 0.75rem; +} + +#sidebar ul li a { + line-height: 1.33; + color: inherit; + display: inline-block; + padding: 0; +} + +#sidebar ul li a:not(.active-link):hover { + color: var(--sl-color-primary-700); +} + +#sidebar nav .active-link { + color: var(--sl-color-primary-700); + border-bottom: dashed 1px var(--sl-color-primary-700); +} + +#sidebar > header img { + display: block; + width: 100%; + height: auto; + margin: 0 auto; +} + +@media screen and (max-width: 900px) { + #sidebar { + translate: -100%; + } + + .sidebar-open #sidebar { + translate: 0; + } +} + +.sidebar-version { + font-size: var(--sl-font-size-x-small); + color: var(--sl-color-neutral-500); + text-align: right; + margin-top: -0.5rem; + margin-right: 1rem; + margin-bottom: -0.5rem; +} + +.sidebar-buttons { + display: flex; + justify-content: space-between; +} + +/* Main content */ +main { + position: relative; + padding: var(--docs-content-vertical-spacing) var(--docs-content-padding) + calc(var(--docs-content-vertical-spacing) * 2) var(--docs-content-padding); + margin-left: var(--docs-sidebar-width); +} + +.sidebar-open .content { + margin-left: 0; +} + +.content__body > :last-child { + margin-bottom: 0; +} + +@media screen and (max-width: 900px) { + main { + margin-left: 0; + } +} + +/* Component layouts */ +.content { + display: grid; + grid-template-columns: 100%; + gap: 2rem; + position: relative; + max-width: var(--docs-content-max-width); + margin: 0 auto; +} + +.content--with-toc { + /* There's a 2rem gap, so we need to remove it from the column */ + grid-template-columns: calc(75% - 2rem) min(25%, var(--docs-content-toc-max-width)); + max-width: calc(var(--docs-content-max-width) + var(--docs-content-toc-max-width)); +} + +.content__body { + order: 1; + width: 100%; +} + +.content:not(.content--with-toc) .content__toc { + display: none; +} + +.content__toc { + order: 2; + display: flex; + flex-direction: column; + margin-top: 0; +} + +.content__toc ul { + position: sticky; + top: 5rem; + max-height: calc(100vh - 6rem); + font-size: var(--sl-font-size-small); + line-height: 1.33; + border-left: solid 1px var(--sl-color-neutral-200); + list-style: none; + padding: 1rem 0; + margin: 0; + padding-left: 1rem; + overflow-y: auto; +} + +.content__toc li { + padding: 0 0 0 0.5rem; + margin: 0; +} + +.content__toc li[data-level='3'] { + margin-left: 1rem; +} + +/* We don't use them, but just in case */ +.content__toc li[data-level='4'], +.content__toc li[data-level='5'], +.content__toc li[data-level='6'] { + margin-left: 2rem; +} + +.content__toc li:not(:last-of-type) { + margin-bottom: 0.6rem; +} + +.content__toc a { + color: var(--sl-color-neutral-700); + text-decoration: none; +} + +.content__toc a:hover { + color: var(--sl-color-primary-700); +} + +.content__toc a.active { + color: var(--sl-color-primary-700); + border-bottom: dashed 1px var(--sl-color-primary-700); +} + +.content__toc .top a { + font-weight: var(--sl-font-weight-semibold); + color: var(--sl-color-neutral-500); +} + +@media screen and (max-width: 1024px) { + .content { + grid-template-columns: 100%; + gap: 0; + } + + .content__toc { + position: relative; + order: 1; + } + + .content__toc ul { + display: flex; + justify-content: start; + gap: 1rem 1.5rem; + position: static; + border: none; + border-bottom: solid 1px var(--sl-color-neutral-200); + border-radius: 0; + padding: 1rem 1.5rem 1rem 0.5rem; /* extra right padding to hide the fade effect */ + margin-top: 1rem; + overflow-x: auto; + } + + .content__toc ul::after { + content: ''; + position: absolute; + top: 0; + bottom: 1rem; /* don't cover the scrollbar */ + right: 0; + width: 2rem; + background: linear-gradient(90deg, rgba(0 0 0 / 0) 0%, var(--sl-color-neutral-0) 100%); + } + + .content__toc li { + white-space: nowrap; + } + + .content__toc li:not(:last-of-type) { + margin-bottom: 0; + } + + .content__toc [data-level]:not([data-level='2']) { + display: none; + } + + .content__body { + order: 2; + } +} + +/* Menu toggle */ +#menu-toggle { + display: none; + position: fixed; + z-index: 30; + top: 0.25rem; + left: 0.25rem; + height: auto; + width: auto; + color: black; + border: none; + border-radius: 50%; + background: var(--sl-color-neutral-0); + padding: 0.5rem; + margin: 0; + cursor: pointer; + transition: + 250ms scale ease, + 250ms rotate ease; +} + +@media screen and (max-width: 900px) { + #menu-toggle { + display: flex; + } +} + +.sl-theme-dark #menu-toggle { + color: white; +} + +#menu-toggle:hover { + scale: 1.1; +} + +#menu-toggle svg { + width: 1.25rem; + height: 1.25rem; +} + +html.sidebar-open #menu-toggle { + rotate: 180deg; +} + +/* Skip to main content */ +#skip-to-main { + position: fixed; + top: 0.25rem; + left: calc(50% - var(--docs-skip-to-main-width) / 2); + z-index: 100; + width: var(--docs-skip-to-main-width); + text-align: center; + text-decoration: none; + border-radius: 9999px; + background: var(--sl-color-neutral-0); + color: var(--sl-color-neutral-1000); + padding: 0.5rem; +} + +/* Icon toolbar */ +#icon-toolbar { + display: flex; + position: fixed; + top: 0; + right: 1rem; + z-index: 10; + background: var(--sl-color-neutral-800); + border-bottom-left-radius: calc(var(--docs-border-radius) * 2); + border-bottom-right-radius: calc(var(--docs-border-radius) * 2); + padding: 0.125rem 0.25rem; +} + +#icon-toolbar button, +#icon-toolbar a { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + width: auto; + height: auto; + background: transparent; + border: none; + border-radius: var(--docs-border-radius); + font-size: 1.25rem; + color: var(--sl-color-neutral-0); + text-decoration: none; + padding: 0.5rem; + margin: 0; + cursor: pointer; +} + +#theme-selector:not(:defined) { + /* Hide when not defined to prevent extra wide icon toolbar while loading */ + display: none; +} + +#theme-selector sl-menu { + /* Set an initial size to prevent width being too small when first opening on small screen width */ + width: 140px; +} + +#theme-selector sl-button { + transition: 250ms scale ease; +} + +#theme-selector sl-button::part(base) { + color: var(--sl-color-neutral-0); +} + +#theme-selector sl-button::part(label) { + display: flex; + padding: 0.5rem; +} + +#theme-selector sl-icon { + font-size: 1.25rem; +} + +.sl-theme-dark #theme-selector sl-button::part(base) { + color: var(--sl-color-neutral-1000); +} + +.sl-theme-dark #icon-toolbar { + background: var(--sl-color-neutral-200); +} + +.sl-theme-dark #icon-toolbar button, +.sl-theme-dark #icon-toolbar a { + color: var(--sl-color-neutral-1000); +} + +#icon-toolbar a:not(:last-child), +#icon-toolbar button:not(:last-child) { + margin-right: 0.25rem; +} + +@media screen and (max-width: 900px) { + #icon-toolbar { + right: 0; + border-bottom-right-radius: 0; + } + + #icon-toolbar button, + #icon-toolbar a { + font-size: 1rem; + padding: 0.5rem; + } + + #theme-selector sl-icon { + font-size: 1rem; + } +} + +/* Sidebar addons */ +#sidebar-addons:not(:empty) { + margin-bottom: var(--docs-content-vertical-spacing); +} + +/* Print styles */ +@media print { + a:not(.anchor-heading)[href]::after { + content: ' (' attr(href) ')'; + } + + details, + pre, + .callout { + border: solid var(--docs-border-width) var(--docs-border-color); + } + + details summary { + list-style: none; + } + + details summary span { + padding-left: 0; + } + + details summary::marker, + details summary::-webkit-details-marker { + display: none; + } + + .callout::before { + border: solid var(--docs-border-width) var(--docs-border-color); + } + + .component-page__navigation, + .copy-code-button, + .code-preview__buttons, + .code-preview__resizer { + display: none !important; + } + + .flavor-html .code-preview__source--html, + .flavor-react .code-preview__source--react { + display: block !important; + } + + .flavor-html .code-preview__source--html > pre, + .flavor-react .code-preview__source--react > pre { + border: none; + } + + .code-preview__source-group { + border-bottom: solid 1px var(--sl-color-neutral-200); + border-bottom-left-radius: var(--sl-border-radius-medium); + border-bottom-right-radius: var(--sl-border-radius-medium); + } + + #sidebar { + display: none; + } + + #content { + margin-left: 0; + } + + #menu-toggle, + #icon-toolbar, + .external-link__icon { + display: none; + } +} + +/* Splash */ +.splash { + display: flex; + padding-top: 2rem; +} + +.splash-start { + min-width: 420px; +} + +.splash-start h1 { + font-size: var(--sl-font-size-large); + font-weight: var(--sl-font-weight-normal); +} + +.splash li img { + width: 1em; + height: 1em; + vertical-align: -2px; +} + +.splash-end { + display: flex; + align-items: flex-end; + width: auto; + padding-left: 1rem; +} + +.splash-image { + width: 100%; + height: auto; +} + +.splash-logo { + max-width: 22rem; +} + +.splash-start h1:first-of-type { + font-size: var(--sl-font-size-large); + margin: 0 0 0.5rem 0; +} + +@media screen and (max-width: 1280px) { + .splash { + display: block; + } + + .splash-start { + min-width: 0; + padding-bottom: 1rem; + } + + .splash-end { + padding: 0; + } + + .splash-image { + display: block; + max-width: 400px; + } + + /* Shields */ + .splash + p { + margin-top: 2rem; + } +} + +/* Component headers */ +.component-header h1 { + margin-bottom: 0; +} + +.component-header__tag { + margin-top: -0.5rem; + margin-bottom: 0.5rem; +} + +.component-header__tag code { + background: none; + color: var(--sl-color-neutral-600); + font-size: var(--sl-font-size-large); + padding: 0; + margin: 0; +} + +.component-header__info { + margin-bottom: var(--sl-spacing-x-large); +} + +.component-summary { + font-size: var(--sl-font-size-large); + line-height: 1.6; + margin: 2rem 0; +} + +/* Repo buttons */ +.sidebar-buttons { + display: flex; + gap: 0.125rem; + justify-content: space-between; +} + +.sidebar-buttons .repo-button { + flex: 1 1 auto; +} + +.repo-button--github sl-icon { + color: var(--sl-color-neutral-700); +} + +.repo-button--star sl-icon { + color: var(--sl-color-yellow-500); +} + +.repo-button--twitter sl-icon { + color: var(--sl-color-sky-500); +} + +@media screen and (max-width: 400px) { + :not(.sidebar-buttons) > .repo-button { + width: 100%; + margin-bottom: 1rem; + } +} + +body[data-page^='/tokens/'] .table-wrapper td:first-child, +body[data-page^='/tokens/'] .table-wrapper td:first-child code { + white-space: nowrap; +} + +/* Border radius demo */ +.border-radius-demo { + width: 3rem; + height: 3rem; + background: var(--sl-color-primary-600); +} + +/* Transition demo */ +.transition-demo { + position: relative; + background: var(--sl-color-neutral-200); + width: 8rem; + height: 2rem; +} + +.transition-demo:after { + content: ''; + position: absolute; + background-color: var(--sl-color-primary-600); + top: 0; + left: 0; + width: 0; + height: 100%; + transition-duration: inherit; + transition-property: width; +} + +.transition-demo:hover:after { + width: 100%; +} + +/* Spacing demo */ +.spacing-demo { + width: 100px; + background: var(--sl-color-primary-600); +} + +/* Elevation demo */ +.elevation-demo { + background: transparent; + border-radius: 3px; + width: 4rem; + height: 4rem; + margin: 1rem; +} + +/* Color palettes */ +.color-palette { + display: grid; + grid-template-columns: 200px repeat(11, 1fr); + gap: 1rem var(--sl-spacing-2x-small); + margin: 2rem 0; +} + +.color-palette__name { + font-size: var(--sl-font-size-medium); + font-weight: var(--sl-font-weight-semibold); + grid-template-columns: repeat(11, 1fr); +} + +.color-palette__name code { + background: none; + font-size: var(--sl-font-size-x-small); +} + +.color-palette__example { + font-size: var(--sl-font-size-x-small); + text-align: center; +} + +.color-palette__swatch { + height: 3rem; + border-radius: var(--sl-border-radius-small); +} + +.color-palette__swatch--border { + box-shadow: inset 0 0 0 1px var(--sl-color-neutral-300); +} + +@media screen and (max-width: 1200px) { + .color-palette { + grid-template-columns: repeat(6, 1fr); + } + + .color-palette__name { + grid-column-start: span 6; + } +} + +.ks-banner { + display: flex; + gap: 1rem; + position: absolute; + top: 1rem; + width: 950px; + left: calc(50% - 475px); + font-size: 0.9375rem; + align-items: center; + justify-content: space-between; + background: #1a3256; + border-radius: var(--sl-border-radius-large); + padding: 1rem 1.25rem; + color: #fdfdfd; + text-decoration: none; + line-height: 1.4; + z-index: 2; + margin-left: 160px; +} + +.ks-banner:hover { + color: #fdfdfd; +} + +.ks-banner > span { + display: flex; + align-items: center; + gap: 1rem; +} + +.ks-banner svg { + flex: 0 0 1.5rem; + width: 1.5rem; + height: 1.5rem; +} + +.ks-banner .faux-button { + display: inline-flex; + align-items: center; + height: 30px; + background: white; + border: solid 1px #d4d4d4; + border-radius: var(--sl-border-radius-medium); + font-size: 0.8375rem; + color: #353439; + padding: 0.5rem 1rem; + white-space: nowrap; +} + +.ks-banner.with-toc { + width: 1100px; + left: calc(50% - 550px); + margin-left: 140px; +} + +main { + margin-top: 70px; +} + +@media screen and (max-width: 1650px) { + .ks-banner, + .ks-banner.with-toc { + width: 540px !important; + top: 50px; + left: calc(50% - 270px); + } + + main { + margin-top: 140px; + } +} + +@media screen and (max-width: 900px) { + .ks-banner, + .ks-banner.with-toc { + margin-left: 0; + } +} + +@media screen and (max-width: 680px) { + .ks-banner, + .ks-banner.with-toc { + width: calc(100% - 2rem) !important; + left: 1rem; + flex-direction: column; + } + + main { + margin-top: 150px; + } +} diff --git a/docs/assets/styles/search.css b/docs/assets/styles/search.css new file mode 100644 index 0000000..c5a7d17 --- /dev/null +++ b/docs/assets/styles/search.css @@ -0,0 +1,347 @@ +/* Search plugin */ +:root, +:root.sl-theme-dark { + --docs-search-box-background: var(--sl-color-neutral-0); + --docs-search-box-border-width: 1px; + --docs-search-box-border-color: var(--sl-color-neutral-300); + --docs-search-box-color: var(--sl-color-neutral-600); + --docs-search-dialog-background: var(--sl-color-neutral-0); + --docs-search-border-width: var(--docs-border-width); + --docs-search-border-color: var(--docs-border-color); + --docs-search-text-color: var(--sl-color-neutral-900); + --docs-search-text-color-muted: var(--sl-color-neutral-500); + --docs-search-font-weight-normal: var(--sl-font-weight-normal); + --docs-search-font-weight-semibold: var(--sl-font-weight-semibold); + --docs-search-border-radius: calc(2 * var(--docs-border-radius)); + --docs-search-accent-color: var(--sl-color-primary-600); + --docs-search-icon-color: var(--sl-color-neutral-500); + --docs-search-icon-color-active: var(--sl-color-neutral-600); + --docs-search-shadow: var(--docs-shadow-x-large); + --docs-search-result-background-hover: var(--sl-color-neutral-100); + --docs-search-result-color-hover: var(--sl-color-neutral-900); + --docs-search-result-background-active: var(--sl-color-primary-600); + --docs-search-result-color-active: var(--sl-color-neutral-0); + --docs-search-focus-ring: var(--sl-focus-ring); + --docs-search-overlay-background: rgb(0 0 0 / 0.33); +} + +:root.sl-theme-dark { + --docs-search-overlay-background: rgb(71 71 71 / 0.33); +} + +body.search-visible { + padding-right: var(--docs-search-scroll-lock-size) !important; + overflow: hidden !important; +} + +/* Search box */ +.search-box { + flex: 1 1 auto; + display: flex; + align-items: center; + width: 100%; + border: none; + border-radius: 9999px; + background: var(--docs-search-box-background); + border: solid var(--docs-search-box-border-width) var(--docs-search-box-border-color); + font: inherit; + color: var(--docs-search-box-color); + padding: 0.75rem 1rem; + margin: var(--sl-spacing-large) 0; + cursor: pointer; +} + +.search-box span { + flex: 1 1 auto; + width: 1rem; + height: 1rem; + text-align: left; + line-height: 1; + margin: 0 0.75rem; +} + +.search-box:focus { + outline: none; +} + +.search-box:focus-visible { + outline: var(--docs-search-focus-ring); +} + +/* Site search */ +.search { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; +} + +.search[hidden] { + display: none; +} + +.search__overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--docs-search-overlay-background); + z-index: -1; +} + +.search__dialog { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + background: transparent; + border: none; + padding: 0; + margin: 0; +} + +.search__dialog:focus { + outline: none; +} + +.search__dialog::backdrop { + display: none; +} + +/* Fixes an iOS Safari 16.4 bug that draws the parent element's border radius incorrectly when showing/hiding results */ +.search__header { + background-color: var(--docs-search-dialog-background); + border-radius: var(--docs-search-border-radius); +} + +.search--has-results .search__header { + border-top-left-radius: var(--docs-search-border-radius); + border-top-right-radius: var(--docs-search-border-radius); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.search__content { + display: flex; + flex-direction: column; + width: 100%; + max-width: 500px; + max-height: calc(100vh - 20rem); + background-color: var(--docs-search-dialog-background); + border-radius: var(--docs-search-border-radius); + box-shadow: var(--docs-search-shadow); + padding: 0; + margin: 10rem auto; +} + +@media screen and (max-width: 900px) { + .search__content { + max-width: calc(100% - 2rem); + max-height: calc(90svh); + margin: 4vh 1rem; + } +} + +.search__input-wrapper { + display: flex; + align-items: center; +} + +.search__input-wrapper sl-icon { + width: 1.5rem; + height: 1.5rem; + flex: 0 0 auto; + color: var(--docs-search-icon-color); + margin: 0 1.5rem; +} + +.search__clear-button { + display: flex; + background: none; + border: none; + font: inherit; + padding: 0; + margin: 0; + cursor: pointer; +} + +.search__clear-button[hidden] { + display: none; +} + +.search__clear-button:active sl-icon { + color: var(--docs-search-icon-color-active); +} + +.search__input { + flex: 1 1 auto; + min-width: 0; + border: none; + font: inherit; + font-size: 1.5rem; + font-weight: var(--docs-search-font-weight-normal); + color: var(--docs-search-text-color); + background: transparent; + padding: 1rem 0; + margin: 0; +} + +.search__input::placeholder { + color: var(--docs-search-text-color-muted); +} + +.search__input::-webkit-search-decoration, +.search__input::-webkit-search-cancel-button, +.search__input::-webkit-search-results-button, +.search__input::-webkit-search-results-decoration { + display: none; +} + +.search__input:focus, +.search__input:focus-visible { + outline: none; +} + +.search__body { + flex: 1 1 auto; + overflow: auto; +} + +.search--has-results .search__body { + border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); +} + +.search__results { + display: none; + line-height: 1.2; + list-style: none; + padding: 0.5rem 0; + margin: 0; +} + +.search--has-results .search__results { + display: block; +} + +.search__results a { + display: block; + text-decoration: none; + padding: 0.5rem 1.5rem; +} + +.search__results a:focus-visible { + outline: var(--docs-search-focus-ring); +} + +.search__results li a:hover, +.search__results li a:hover small { + background-color: var(--docs-search-result-background-hover); + color: var(--docs-search-result-color-hover); +} + +.search__results li[data-selected='true'] a, +.search__results li[data-selected='true'] a * { + outline: none; + background-color: var(--docs-search-result-background-active); + color: var(--docs-search-result-color-active); +} + +.search__results h3 { + font-weight: var(--docs-search-font-weight-semibold); + margin: 0; +} + +.search__results small { + display: block; + color: var(--docs-search-text-color-muted); +} + +.search__result { + padding: 0; + margin: 0; +} + +.search__result a { + display: flex; + align-items: center; + gap: 1rem; +} + +.search__result-icon { + flex: 0 0 auto; + display: flex; + color: var(--docs-search-text-color-muted); +} + +.search__result-icon sl-icon { + font-size: 1.5rem; +} + +.search__result__details { + width: calc(100% - 3rem); +} + +.search__result-title, +.search__result-description, +.search__result-url { + max-width: 400px; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.search__result-title { + font-size: 1.2rem; + font-weight: var(--docs-search-font-weight-semibold); + color: var(--docs-search-accent-color); +} + +.search__result-description { + font-size: 0.875rem; + color: var(--docs-search-text-color); +} + +.search__result-url { + font-size: 0.875rem; + color: var(--docs-search-text-color-muted); +} + +.search__empty { + display: none; + border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); + text-align: center; + color: var(--docs-search-text-color-muted); + padding: 2rem; +} + +.search--no-results .search__empty { + display: block; +} + +.search__footer { + display: flex; + justify-content: center; + gap: 2rem; + border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; + padding: 1rem; +} + +.search__footer small { + color: var(--docs-search-text-color-muted); +} + +.search__footer small kbd:last-of-type { + margin-right: 0.25rem; +} + +@media screen and (max-width: 900px) { + .search__footer { + display: none; + } +} diff --git a/docs/eleventy.config.cjs b/docs/eleventy.config.cjs new file mode 100644 index 0000000..4908401 --- /dev/null +++ b/docs/eleventy.config.cjs @@ -0,0 +1,246 @@ +/* eslint-disable no-invalid-this */ +const fs = require('fs'); +const path = require('path'); +const lunr = require('lunr'); +const { capitalCase } = require('change-case'); +const { JSDOM } = require('jsdom'); +const { customElementsManifest, getAllComponents } = require('./_utilities/cem.cjs'); +const shoelaceFlavoredMarkdown = require('./_utilities/markdown.cjs'); +const activeLinks = require('./_utilities/active-links.cjs'); +const anchorHeadings = require('./_utilities/anchor-headings.cjs'); +const codePreviews = require('./_utilities/code-previews.cjs'); +const copyCodeButtons = require('./_utilities/copy-code-buttons.cjs'); +const externalLinks = require('./_utilities/external-links.cjs'); +const highlightCodeBlocks = require('./_utilities/highlight-code.cjs'); +const tableOfContents = require('./_utilities/table-of-contents.cjs'); +const prettier = require('./_utilities/prettier.cjs'); +const scrollingTables = require('./_utilities/scrolling-tables.cjs'); +const typography = require('./_utilities/typography.cjs'); +const replacer = require('./_utilities/replacer.cjs'); + +const assetsDir = 'assets'; +const cdndir = 'cdn'; +const npmdir = 'dist'; +const allComponents = getAllComponents(); +let hasBuiltSearchIndex = false; + +module.exports = function (eleventyConfig) { + // + // Global data + // + eleventyConfig.addGlobalData('baseUrl', 'https://shoelace.style/'); // the production URL + eleventyConfig.addGlobalData('layout', 'default'); // make 'default' the default layout + eleventyConfig.addGlobalData('toc', true); // enable the table of contents + eleventyConfig.addGlobalData('meta', { + title: 'Shoelace', + description: 'A forward-thinking library of web components.', + image: 'images/og-image.png', + version: customElementsManifest.package.version, + components: allComponents, + cdndir, + npmdir + }); + + // + // Layout aliases + // + eleventyConfig.addLayoutAlias('default', 'default.njk'); + + // + // Copy assets + // + eleventyConfig.addPassthroughCopy(assetsDir); + eleventyConfig.setServerPassthroughCopyBehavior('passthrough'); // emulates passthrough copy during --serve + + // + // Functions + // + + // Generates a URL relative to the site's root + eleventyConfig.addNunjucksGlobal('rootUrl', (value = '', absolute = false) => { + value = path.join('/', value); + return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value; + }); + + // Generates a URL relative to the site's asset directory + eleventyConfig.addNunjucksGlobal('assetUrl', (value = '', absolute = false) => { + value = path.join(`/${assetsDir}`, value); + return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value; + }); + + // Fetches a specific component's metadata + eleventyConfig.addNunjucksGlobal('getComponent', tagName => { + const component = allComponents.find(c => c.tagName === tagName); + if (!component) { + throw new Error( + `Unable to find a component called "${tagName}". Make sure the file name is the same as the component's tag ` + + `name (minus the sl- prefix).` + ); + } + return component; + }); + + // + // Custom markdown syntaxes + // + eleventyConfig.setLibrary('md', shoelaceFlavoredMarkdown); + + // + // Filters + // + eleventyConfig.addFilter('markdown', content => { + return shoelaceFlavoredMarkdown.render(content); + }); + + eleventyConfig.addFilter('markdownInline', content => { + return shoelaceFlavoredMarkdown.renderInline(content); + }); + + // Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited. + // With Prettier 3, this means a leading pipe will exist if the line wraps. + eleventyConfig.addFilter('trimPipes', content => { + return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content; + }); + + eleventyConfig.addFilter('classNameToComponentName', className => { + let name = capitalCase(className.replace(/^Sl/, '')); + if (name === 'Qr Code') name = 'QR Code'; // manual override + return name; + }); + + eleventyConfig.addFilter('removeSlPrefix', tagName => { + return tagName.replace(/^sl-/, ''); + }); + + // + // Transforms + // + eleventyConfig.addTransform('html-transform', function (rawContent) { + let content = replacer(rawContent, [ + { pattern: '%VERSION%', replacement: customElementsManifest.package.version }, + { pattern: '%CDNDIR%', replacement: cdndir }, + { pattern: '%NPMDIR%', replacement: npmdir } + ]); + + // Parse the template and get a Document object + const doc = new JSDOM(content, { + // We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily + // identify which ones are internal and which ones are external. + url: `https://internal/` + }).window.document; + + // DOM transforms + activeLinks(doc, { pathname: this.page.url }); + anchorHeadings(doc, { + within: '#content .content__body', + levels: ['h2', 'h3', 'h4', 'h5'] + }); + tableOfContents(doc, { + levels: ['h2', 'h3'], + container: '#content .content__toc > ul', + within: '#content .content__body' + }); + codePreviews(doc); + externalLinks(doc, { target: '_blank' }); + highlightCodeBlocks(doc); + scrollingTables(doc); + copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks + typography(doc, '#content'); + + // Serialize the Document object to an HTML string and prepend the doctype + content = `\n${doc.documentElement.outerHTML}`; + + // String transforms + content = prettier(content); + + return content; + }); + + // + // Build a search index + // + eleventyConfig.on('eleventy.after', ({ results }) => { + // We only want to build the search index on the first run so all pages get indexed. + if (hasBuiltSearchIndex) { + return; + } + + const map = {}; + const searchIndexFilename = path.join(eleventyConfig.dir.output, assetsDir, 'search.json'); + const lunrInput = path.resolve('../node_modules/lunr/lunr.min.js'); + const lunrOutput = path.join(eleventyConfig.dir.output, assetsDir, 'scripts/lunr.js'); + const searchIndex = lunr(function () { + // The search index uses these field names extensively, so shortening them can save some serious bytes. The + // initial index file went from 468 KB => 401 KB by using single-character names! + this.ref('id'); // id + this.field('t', { boost: 50 }); // title + this.field('h', { boost: 25 }); // headings + this.field('c'); // content + + results.forEach((result, index) => { + const url = path + .join('/', path.relative(eleventyConfig.dir.output, result.outputPath)) + .replace(/\\/g, '/') // convert backslashes to forward slashes + .replace(/\/index.html$/, '/'); // convert trailing /index.html to / + const doc = new JSDOM(result.content, { + // We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily + // identify which ones are internal and which ones are external. + url: `https://internal/` + }).window.document; + const content = doc.querySelector('#content'); + + // Get title and headings + const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim(); + const headings = [...content.querySelectorAll('h1, h2, h3, h4')] + .map(heading => heading.textContent) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + + // Remove code blocks and whitespace from content + [...content.querySelectorAll('code[class|=language]')].forEach(code => code.remove()); + const textContent = content.textContent.replace(/\s+/g, ' ').trim(); + + // Update the index and map + this.add({ id: index, t: title, h: headings, c: textContent }); + map[index] = { title, url }; + }); + }); + + // Copy the Lunr search client and write the index + fs.mkdirSync(path.dirname(lunrOutput), { recursive: true }); + fs.copyFileSync(lunrInput, lunrOutput); + fs.writeFileSync(searchIndexFilename, JSON.stringify({ searchIndex, map }), 'utf-8'); + + hasBuiltSearchIndex = true; + }); + + // + // Send a signal to stdout that let's the build know we've reached this point + // + eleventyConfig.on('eleventy.after', () => { + console.log('[eleventy.after]'); + }); + + // + // Dev server options (see https://www.11ty.dev/docs/dev-server/#options) + // + eleventyConfig.setServerOptions({ + domDiff: false, // disable dom diffing so custom elements don't break on reload, + port: 4000, // if port 4000 is taken, 11ty will use the next one available + watch: ['cdn/**/*'] // additional files to watch that will trigger server updates (array of paths or globs) + }); + + // + // 11ty config + // + return { + dir: { + input: 'pages', + output: '../_site', + includes: '../_includes' // resolved relative to the input dir + }, + markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files + templateEngineOverride: ['njk'] // just Nunjucks and then markdown + }; +}; diff --git a/docs/pages/404.md b/docs/pages/404.md new file mode 100644 index 0000000..085c8d4 --- /dev/null +++ b/docs/pages/404.md @@ -0,0 +1,19 @@ +--- +meta: + title: Page Not Found + description: "The page you were looking for couldn't be found." +permalink: 404.html +toc: false +--- + +
      + +# Page Not Found + +![A UFO takes one of the little worker monsters](/assets/images/undraw-taken.svg) + +The page you were looking for couldn't be found. + +Press [[/]] to search, or [head back to the homepage](/). + +
      diff --git a/docs/pages/components/alert.md b/docs/pages/components/alert.md new file mode 100644 index 0000000..5ea30cb --- /dev/null +++ b/docs/pages/components/alert.md @@ -0,0 +1,443 @@ +--- +meta: + title: Alert + description: Alerts are used to display important messages inline or as toast notifications. +layout: component +--- + +```html:preview + + + This is a standard alert. You can customize its content and even the icon. + +``` + +```jsx:react +import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + + + This is a standard alert. You can customize its content and even the icon. + +); +``` + +:::tip +Alerts will not be visible if the `open` attribute is not present. +::: + +## Examples + +### Variants + +Set the `variant` attribute to change the alert's variant. + +```html:preview + + + This is super informative
      + You can tell by how pretty the alert is. +
      + +
      + + + + Your changes have been saved
      + You can safely exit the app now. +
      + +
      + + + + Your settings have been updated
      + Settings will take effect on next login. +
      + +
      + + + + Your session has ended
      + Please login again to continue. +
      + +
      + + + + Your account has been deleted
      + We're very sorry to see you go! +
      +``` + +```jsx:react +import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> + + + This is super informative +
      + You can tell by how pretty the alert is. +
      + +
      + + + + Your changes have been saved +
      + You can safely exit the app now. +
      + +
      + + + + Your settings have been updated +
      + Settings will take effect on next login. +
      + +
      + + + + Your session has ended +
      + Please login again to continue. +
      + +
      + + + + Your account has been deleted +
      + We're very sorry to see you go! +
      + +); +``` + +### Closable + +Add the `closable` attribute to show a close button that will hide the alert. + +```html:preview + + + You can close this alert any time! + + + +``` + +```jsx:react +import { useState } from 'react'; +import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => { + const [open, setOpen] = useState(true); + + function handleHide() { + setOpen(false); + setTimeout(() => setOpen(true), 2000); + } + + return ( + + + You can close this alert any time! + + ); +}; +``` + +### Without Icons + +Icons are optional. Simply omit the `icon` slot if you don't want them. + +```html:preview + Nothing fancy here, just a simple alert. +``` + +```jsx:react +import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; + +const App = () => ( + + Nothing fancy here, just a simple alert. + +); +``` + +### Duration + +Set the `duration` attribute to automatically hide an alert after a period of time. This is useful for alerts that don't require acknowledgement. + +```html:preview +
      + Show Alert + + + + This alert will automatically hide itself after three seconds, unless you interact with it. + +
      + + + + +``` + +```jsx:react +import { useState } from 'react'; +import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const css = ` + .alert-duration sl-alert { + margin-top: var(--sl-spacing-medium); + } +`; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> +
      + setOpen(true)}> + Show Alert + + + setOpen(false)}> + + This alert will automatically hide itself after three seconds, unless you interact with it. + +
      + + + + ); +}; +``` + +### Toast Notifications + +To display an alert as a toast notification, or "toast", create the alert and call its `toast()` method. This will move the alert out of its position in the DOM and into [the toast stack](#the-toast-stack) where it will be shown. Once dismissed, it will be removed from the DOM completely. To reuse a toast, store a reference to it and call `toast()` again later on. + +You should always use the `closable` attribute so users can dismiss the notification. It's also common to set a reasonable `duration` when the notification doesn't require acknowledgement. + +```html:preview +
      + Primary + Success + Neutral + Warning + Danger + + + + This is super informative
      + You can tell by how pretty the alert is. +
      + + + + Your changes have been saved
      + You can safely exit the app now. +
      + + + + Your settings have been updated
      + Settings will take effect on next login. +
      + + + + Your session has ended
      + Please login again to continue. +
      + + + + Your account has been deleted
      + We're very sorry to see you go! +
      +
      + + +``` + +```jsx:react +import { useRef } from 'react'; +import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +function showToast(alert) { + alert.toast(); +} + +const App = () => { + const primary = useRef(null); + const success = useRef(null); + const neutral = useRef(null); + const warning = useRef(null); + const danger = useRef(null); + + return ( + <> + primary.current.toast()}> + Primary + + + success.current.toast()}> + Success + + + neutral.current.toast()}> + Neutral + + + warning.current.toast()}> + Warning + + + danger.current.toast()}> + Danger + + + + + This is super informative +
      + You can tell by how pretty the alert is. +
      + + + + Your changes have been saved +
      + You can safely exit the app now. +
      + + + + Your settings have been updated +
      + Settings will take effect on next login. +
      + + + + Your session has ended +
      + Please login again to continue. +
      + + + + Your account has been deleted +
      + We're very sorry to see you go! +
      + + ); +}; +``` + +### Creating Toasts Imperatively + +For convenience, you can create a utility that emits toast notifications with a function call rather than composing them in your HTML. To do this, generate the alert with JavaScript, append it to the body, and call the `toast()` method as shown in the example below. + +```html:preview +
      + Create Toast +
      + + +``` + +### The Toast Stack + +The toast stack is a fixed position singleton element created and managed internally by the alert component. It will be added and removed from the DOM as needed when toasts are shown. When more than one toast is visible, they will stack vertically in the toast stack. + +By default, the toast stack is positioned at the top-right of the viewport. You can change its position by targeting `.sl-toast-stack` in your stylesheet. To make toasts appear at the top-left of the viewport, for example, use the following styles. + +```css +.sl-toast-stack { + left: 0; + right: auto; +} +``` + +:::tip +By design, it is not possible to show toasts in more than one stack simultaneously. Such behavior is confusing and makes for a poor user experience. +::: diff --git a/docs/pages/components/animated-image.md b/docs/pages/components/animated-image.md new file mode 100644 index 0000000..5fc634c --- /dev/null +++ b/docs/pages/components/animated-image.md @@ -0,0 +1,130 @@ +--- +meta: + title: Animated Image + description: A component for displaying animated GIFs and WEBPs that play and pause on interaction. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; + +const App = () => ( + +); +``` + +:::tip +This component uses `` to draw freeze frames, so images are subject to [cross-origin restrictions](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image). +::: + +## Examples + +### WEBP Images + +Both GIF and WEBP images are supported. + +```html:preview + +``` + +```jsx:react +import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; + +const App = () => ( + +); +``` + +### Setting a Width and Height + +To set a custom size, apply a width and/or height to the host element. + +```html:preview + + +``` + +{% raw %} + +```jsx:react +import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; + +const App = () => ( + +); +``` + +{% endraw %} + +### Customizing the Control Box + +You can change the appearance and location of the control box by targeting the `control-box` part in your styles. + +```html:preview + + + +``` + +```jsx:react +import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; + +const css = ` + .animated-image-custom-control-box::part(control-box) { + top: auto; + right: auto; + bottom: 1rem; + left: 1rem; + background-color: deeppink; + border: none; + color: pink; + } +`; + +const App = () => ( + <> + + + + +); +``` diff --git a/docs/pages/components/animation.md b/docs/pages/components/animation.md new file mode 100644 index 0000000..6d99353 --- /dev/null +++ b/docs/pages/components/animation.md @@ -0,0 +1,348 @@ +--- +meta: + title: Animation + description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. +layout: component +--- + +To animate an element, wrap it in `` and set an animation `name`. The animation will not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options. + +```html:preview +
      +
      +
      +
      +
      +
      + + +``` + +```jsx:react +import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; + +const css = ` + .animation-overview .box { + display: inline-block; + width: 100px; + height: 100px; + background-color: var(--sl-color-primary-600); + margin: 1.5rem; + } +`; + +const App = () => ( + <> +
      + +
      + + +
      + + +
      + + +
      + +
      + + + +); +``` + +:::tip +The animation will only be applied to the first child element found in ``. +::: + +## Examples + +### Animations & Easings + +This example demonstrates all of the baked-in animations and easings. Animations are based on those found in the popular [Animate.css](https://animate.style/) library. + +```html:preview +
      + +
      +
      + +
      + + + +
      +
      + + + + +``` + +### Using Intersection Observer + +Use an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to control the animation when an element enters or exits the viewport. For example, scroll the box below in and out of your screen. The animation stops when the box exits the viewport and restarts each time it enters the viewport. + +```html:preview +
      +
      +
      + + + + +``` + +```jsx:react +import { useEffect, useRef, useState } from 'react'; +import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; + +const css = ` + .animation-scroll { + height: calc(100vh + 100px); + } + + .animation-scroll .box { + display: inline-block; + width: 100px; + height: 100px; + background-color: var(--sl-color-primary-600); + } +`; + +const App = () => { + const animation = useRef(null); + const box = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + animation.current.play = true; + } else { + animation.current.play = false; + animation.current.currentTime = 0; + } + }); + + if (box.current) { + observer.observe(box.current); + } + }, [box]); + + return ( + <> +
      + +
      + +
      + + + + ); +}; +``` + +### Custom Keyframe Formats + +Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats) to build custom animations. + +```html:preview +
      + +
      +
      +
      + + + + +``` + +```jsx:react +import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; + +const css = ` + .animation-keyframes .box { + width: 100px; + height: 100px; + background-color: var(--sl-color-primary-600); + } +`; + +const App = () => ( + <> +
      + +
      + +
      + + + +); +``` + +### Playing Animations on Demand + +Animations won't play until you apply the `play` attribute. You can omit it initially, then apply it on demand such as after a user interaction. In this example, the button will animate once every time the button is clicked. + +```html:preview +
      + + Click me + +
      + + +``` + +```jsx:react +import { useState } from 'react'; +import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => { + const [play, setPlay] = useState(false); + + return ( +
      + setPlay(false)}> + setPlay(true)}> + Click me + + +
      + ); +}; +``` diff --git a/docs/pages/components/avatar.md b/docs/pages/components/avatar.md new file mode 100644 index 0000000..9a3fc26 --- /dev/null +++ b/docs/pages/components/avatar.md @@ -0,0 +1,210 @@ +--- +meta: + title: Avatar + description: Avatars are used to represent a person or object. +layout: component +--- + +By default, a generic icon will be shown. You can personalize avatars by adding custom icons, initials, and images. You should always provide a `label` for assistive devices. + +```html:preview + +``` + +```jsx:react +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; + +const App = () => ; +``` + +## Examples + +### Images + +To use an image for the avatar, set the `image` and `label` attributes. This will take priority and be shown over initials and icons. +Avatar images can be lazily loaded by setting the `loading` attribute to `lazy`. + +```html:preview + + +``` + +```jsx:react +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; + +const App = () => ( + + +); +``` + +### Initials + +When you don't have an image to use, you can set the `initials` attribute to show something more personalized than an icon. + +```html:preview + +``` + +```jsx:react +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; + +const App = () => ; +``` + +### Custom Icons + +When no image or initials are set, an icon will be shown. The default avatar shows a generic "user" icon, but you can customize this with the `icon` slot. + +```html:preview + + + + + + + + + + + +``` + +```jsx:react +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> + + + + + + + + + + + + +); +``` + +### Shapes + +Avatars can be shaped using the `shape` attribute. + +```html:preview + + + +``` + +```jsx:react +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> + + + + +); +``` + +### Avatar Groups + +You can group avatars with a few lines of CSS. + +```html:preview +
      + + + + + + + +
      + + +``` + +```jsx:react +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const css = ` + .avatar-group sl-avatar:not(:first-of-type) { + margin-left: -1rem; + } + + .avatar-group sl-avatar::part(base) { + border: solid 2px var(--sl-color-neutral-0); + } +`; + +const App = () => ( + <> +
      + + + + + + + +
      + + + +); +``` diff --git a/docs/pages/components/badge.md b/docs/pages/components/badge.md new file mode 100644 index 0000000..1710eee --- /dev/null +++ b/docs/pages/components/badge.md @@ -0,0 +1,235 @@ +--- +meta: + title: Badge + description: Badges are used to draw attention and display statuses or counts. +layout: component +--- + +```html:preview +Badge +``` + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; + +const App = () => Badge; +``` + +## Examples + +### Variants + +Set the `variant` attribute to change the badge's variant. + +```html:preview +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; + +const App = () => ( + <> + Primary + Success + Neutral + Warning + Danger + +); +``` + +### Pill Badges + +Use the `pill` attribute to give badges rounded edges. + +```html:preview +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; + +const App = () => ( + <> + + Primary + + + Success + + + Neutral + + + Warning + + + Danger + + +); +``` + +### Pulsating Badges + +Use the `pulse` attribute to draw attention to the badge with a subtle animation. + +```html:preview +
      + 1 + 1 + 1 + 1 + 1 +
      + + +``` + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; + +const css = ` + .badge-pulse sl-badge:not(:last-of-type) { + margin-right: 1rem; + } +`; + +const App = () => ( + <> +
      + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + +
      + + + +); +``` + +### With Buttons + +One of the most common use cases for badges is attaching them to buttons. To make this easier, badges will be automatically positioned at the top-right when they're a child of a button. + +```html:preview + + Requests + 30 + + + + Warnings + 8 + + + + Errors + 6 + +``` + +{% raw %} + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Requests + 30 + + + + Warnings + + 8 + + + + + Errors + + 6 + + + +); +``` + +{% endraw %} + +### With Menu Items + +When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly. + +```html:preview + + Messages + Comments 4 + Replies 12 + +``` + +{% raw %} + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; +import SlMenuLabel from '@shoelace-style/shoelace/dist/react/menu-label'; + +const App = () => ( + + Messages + + Comments + + 4 + + + + Replies + + 12 + + + +); +``` + +{% endraw %} diff --git a/docs/pages/components/breadcrumb-item.md b/docs/pages/components/breadcrumb-item.md new file mode 100644 index 0000000..d89b637 --- /dev/null +++ b/docs/pages/components/breadcrumb-item.md @@ -0,0 +1,38 @@ +--- +meta: + title: Breadcrumb Item + description: Breadcrumb Items are used inside breadcrumbs to represent different links. +layout: component +--- + +```html:preview + + + + Home + + Clothing + Shirts + +``` + +```jsx:react +import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; +import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + + + + Home + + Clothing + Shirts + +); +``` + +:::tip +Additional demonstrations can be found in the [breadcrumb examples](/components/breadcrumb). +::: diff --git a/docs/pages/components/breadcrumb.md b/docs/pages/components/breadcrumb.md new file mode 100644 index 0000000..9f70715 --- /dev/null +++ b/docs/pages/components/breadcrumb.md @@ -0,0 +1,258 @@ +--- +meta: + title: Breadcrumb + description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy. +layout: component +--- + +Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation. + +```html:preview + + Catalog + Clothing + Women's + Shirts & Tops + +``` + +```jsx:react +import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; +import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; + +const App = () => ( + + Catalog + Clothing + Women's + Shirts & Tops + +); +``` + +## Examples + +### Breadcrumb Links + +By default, breadcrumb items are rendered as buttons so you can use them to navigate single-page applications. In this case, you'll need to add event listeners to handle clicks. + +For websites, you'll probably want to use links instead. You can make any breadcrumb item a link by applying an `href` attribute to it. Now, when the user activates it, they'll be taken to the corresponding page — no event listeners required. + +```html:preview + + Homepage + + Our Services + + Digital Media + + Web Design + +``` + +```jsx:react +import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; +import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; + +const App = () => ( + + Homepage + + Our Services + + Digital Media + + Web Design + +); +``` + +### Custom Separators + +Use the `separator` slot to change the separator that goes between breadcrumb items. Icons work well, but you can also use text or an image. + +```html:preview + + + First + Second + Third + + +
      + + + + First + Second + Third + + +
      + + + / + First + Second + Third + +``` + +```jsx:react +import '@shoelace-style/shoelace/dist/components/icon/icon.js'; +import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; +import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; + +const App = () => ( + <> + + + First + Second + Third + + +
      + + + + First + Second + Third + + +
      + + + / + First + Second + Third + + +); +``` + +### Prefixes + +Use the `prefix` slot to add content before any breadcrumb item. + +```html:preview + + + + Home + + Articles + Traveling + +``` + +```jsx:react +import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; +import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + + + + Home + + Articles + Traveling + +); +``` + +### Suffixes + +Use the `suffix` slot to add content after any breadcrumb item. + +```html:preview + + Documents + Policies + + Security + + + +``` + +```jsx:react +import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; +import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + + Documents + Policies + + Security + + + +); +``` + +### With Dropdowns + +Dropdown menus can be placed in a prefix or suffix slot to provide additional options. + +```html:preview + + Homepage + Our Services + Digital Media + + Web Design + + + + + + Web Design + Web Development + Marketing + + + + +``` + +```jsx:react +import { + SlBreadcrumb, + SlBreadcrumbItem, + SlButton, + SlDropdown, + SlIcon, + SlMenu, + SlMenuItem +} from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Homepage + Our Services + Digital Media + + Web Design + + + + + + + Web Design + + Web Development + Marketing + + + + +); +``` diff --git a/docs/pages/components/button-group.md b/docs/pages/components/button-group.md new file mode 100644 index 0000000..27e5c83 --- /dev/null +++ b/docs/pages/components/button-group.md @@ -0,0 +1,511 @@ +--- +meta: + title: Button Group + description: Button groups can be used to group related buttons into sections. +layout: component +--- + +```html:preview + + Left + Center + Right + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; + +const App = () => ( + + Left + Center + Right + +); +``` + +## Examples + +### Button Sizes + +All button sizes are supported, but avoid mixing sizes within the same button group. + +```html:preview + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; + +const App = () => ( + <> + + Left + Center + Right + + +
      +
      + + + Left + Center + Right + + +
      +
      + + + Left + Center + Right + + +); +``` + +### Theme Buttons + +Theme buttons are supported through the button's `variant` attribute. + +```html:preview + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; + +const App = () => ( + <> + + Left + Center + Right + + +
      +
      + + + Left + Center + Right + + +
      +
      + + + Left + Center + Right + + +
      +
      + + + Left + Center + Right + + +
      +
      + + + Left + Center + Right + + +); +``` + +### Pill Buttons + +Pill buttons are supported through the button's `pill` attribute. + +```html:preview + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; + +const App = () => ( + <> + + + Left + + + Center + + + Right + + + +
      +
      + + + + Left + + + Center + + + Right + + + +
      +
      + + + + Left + + + Center + + + Right + + + +); +``` + +### Dropdowns in Button Groups + +Dropdowns can be placed inside button groups as long as the trigger is an `` element. + +```html:preview + + Button + Button + + Dropdown + + Item 1 + Item 2 + Item 3 + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Button + Button + + + Dropdown + + + Item 1 + Item 2 + Item 3 + + + +); +``` + +### Split Buttons + +Create a split button using a button and a dropdown. Use a [visually hidden](/components/visually-hidden) label to ensure the dropdown is accessible to users with assistive devices. + +```html:preview + + Save + + + More options + + + Save + Save as… + Save all + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Save + + + + Save + Save as… + Save all + + + +); +``` + +### Tooltips in Button Groups + +Buttons can be wrapped in tooltips to provide more detail when the user interacts with them. + +```html:preview + + + Left + + + + Center + + + + Right + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; +import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; + +const App = () => ( + <> + + + Left + + + + Center + + + + Right + + + +); +``` + +### Toolbar Example + +Create interactive toolbars with button groups. + +```html:preview +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; + +const css = ` + .button-group-toolbar sl-button-group:not(:last-of-type) { + margin-right: var(--sl-spacing-x-small); + } +`; + +const App = () => ( + <> +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +); +``` diff --git a/docs/pages/components/button.md b/docs/pages/components/button.md new file mode 100644 index 0000000..4b8a5a9 --- /dev/null +++ b/docs/pages/components/button.md @@ -0,0 +1,545 @@ +--- +meta: + title: Button + description: Buttons represent actions that are available to the user. +layout: component +--- + +```html:preview +Button +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => Button; +``` + +## Examples + +### Variants + +Use the `variant` attribute to set the button's variant. + +```html:preview +Default +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + Default + Primary + Success + Neutral + Warning + Danger + +); +``` + +### Sizes + +Use the `size` attribute to change a button's size. + +```html:preview +Small +Medium +Large +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + Small + Medium + Large + +); +``` + +### Outline Buttons + +Use the `outline` attribute to draw outlined buttons with transparent backgrounds. + +```html:preview +Default +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Default + + + Primary + + + Success + + + Neutral + + + Warning + + + Danger + + +); +``` + +### Pill Buttons + +Use the `pill` attribute to give buttons rounded edges. + +```html:preview +Small +Medium +Large +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Small + + + Medium + + + Large + + +); +``` + +### Circle Buttons + +Use the `circle` attribute to create circular icon buttons. When this attribute is set, the button expects a single `` in the default slot. + +```html:preview + + + + + + + + + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> + + + + + + + + + + +); +``` + +### Text Buttons + +Use the `text` variant to create text buttons that share the same size as regular buttons but don't have backgrounds or borders. + +```html:preview +Text +Text +Text +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Text + + + Text + + + Text + + +); +``` + +### Link Buttons + +It's often helpful to have a button that works like a link. This is possible by setting the `href` attribute, which will make the component render an `` under the hood. This gives you all the default link behavior the browser provides (e.g. [[CMD/CTRL/SHIFT]] + [[CLICK]]) and exposes the `target` and `download` attributes. + +```html:preview +Link +New Window +Download +Disabled +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + Link + + New Window + + + Download + + + Disabled + + +); +``` + +:::tip +When a `target` is set, the link will receive `rel="noreferrer noopener"` for [security reasons](https://mathiasbynens.github.io/rel-noopener/). +::: + +### Setting a Custom Width + +As expected, buttons can be given a custom width by passing inline styles to the component (or using a class). This is useful for making buttons span the full width of their container on smaller screens. + +```html:preview +Small +Medium +Large +``` + +{% raw %} + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Small + + + Medium + + + Large + + +); +``` + +{% endraw %} + +### Prefix and Suffix Icons + +Use the `prefix` and `suffix` slots to add icons. + +```html:preview + + + Settings + + + + + Refresh + + + + + + Open + + +

      + + + + Settings + + + + + Refresh + + + + + + Open + + +

      + + + + Settings + + + + + Refresh + + + + + + Open + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> + + + Settings + + + + + Refresh + + + + + + Open + + +
      +
      + + + + Settings + + + + + Refresh + + + + + + Open + + +
      +
      + + + + Settings + + + + + Refresh + + + + + + Open + + +); +``` + +### Caret + +Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover. + +```html:preview +Small +Medium +Large +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Small + + + Medium + + + Large + + +); +``` + +### Loading + +Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. + +```html:preview +Default +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Default + + + Primary + + + Success + + + Neutral + + + Warning + + + Danger + + +); +``` + +### Disabled + +Use the `disabled` attribute to disable a button. + +```html:preview +Default +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; + +const App = () => ( + <> + + Default + + + + Primary + + + + Success + + + + Neutral + + + + Warning + + + + Danger + + +); +``` + +### Styling Buttons + +This example demonstrates how to style buttons using a custom class. This is the recommended approach if you need to add additional variations. To customize an existing variation, modify the selector to target the button's `variant` attribute instead of a class (e.g. `sl-button[variant="primary"]`). + +```html:preview +Pink Button + + +``` diff --git a/docs/pages/components/card.md b/docs/pages/components/card.md new file mode 100644 index 0000000..4d3bb7f --- /dev/null +++ b/docs/pages/components/card.md @@ -0,0 +1,307 @@ +--- +meta: + title: Card + description: Cards can be used to group related subjects in a container. +layout: component +--- + +```html:preview + + A kitten sits patiently between a terracotta pot and decorative grasses. + + Mittens
      + This kitten is as cute as he is playful. Bring him home today!
      + 6 weeks old + +
      + More Info + +
      +
      + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlCard from '@shoelace-style/shoelace/dist/react/card'; +import SlRating from '@shoelace-style/shoelace/dist/react/rating'; + +const css = ` + .card-overview { + max-width: 300px; + } + + .card-overview small { + color: var(--sl-color-neutral-500); + } + + .card-overview [slot="footer"] { + display: flex; + justify-content: space-between; + align-items: center; + } +`; + +const App = () => ( + <> + + A kitten sits patiently between a terracotta pot and decorative grasses. + Mittens +
      + This kitten is as cute as he is playful. Bring him home today! +
      + 6 weeks old +
      + + More Info + + +
      +
      + + + +); +``` + +## Examples + +### Basic Card + +Basic cards aren't very exciting, but they can display any content you want them to. + +```html:preview + + This is just a basic card. No image, no header, and no footer. Just your content. + + + +``` + +```jsx:react +import SlCard from '@shoelace-style/shoelace/dist/react/card'; + +const css = ` + .card-basic { + max-width: 300px; + } +`; + +const App = () => ( + <> + + This is just a basic card. No image, no header, and no footer. Just your content. + + + + +); +``` + +### Card with Header + +Headers can be used to display titles and more. + +```html:preview + +
      + Header Title + +
      + + This card has a header. You can put all sorts of things in it! +
      + + +``` + +```jsx:react +import SlCard from '@shoelace-style/shoelace/dist/react/card'; +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const css = ` + .card-header { + max-width: 300px; + } + + .card-header [slot="header"] { + display: flex; + align-items: center; + justify-content: space-between; + } + + .card-header h3 { + margin: 0; + } + + .card-header sl-icon-button { + font-size: var(--sl-font-size-medium); + } +`; + +const App = () => ( + <> + +
      + Header Title + +
      + This card has a header. You can put all sorts of things in it! +
      + + + +); +``` + +### Card with Footer + +Footers can be used to display actions, summaries, or other relevant content. + +```html:preview + + This card has a footer. You can put all sorts of things in it! + +
      + + Preview +
      +
      + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlCard from '@shoelace-style/shoelace/dist/react/card'; +import SlRating from '@shoelace-style/shoelace/dist/react/rating'; + +const css = ` + .card-footer { + max-width: 300px; + } + + .card-footer [slot="footer"] { + display: flex; + justify-content: space-between; + align-items: center; + } +`; + +const App = () => ( + <> + + This card has a footer. You can put all sorts of things in it! +
      + + + Preview + +
      +
      + + + +); +``` + +### Images + +Cards accept an `image` slot. The image is displayed atop the card and stretches to fit. + +```html:preview + + A kitten walks towards camera on top of pallet. + This is a kitten, but not just any kitten. This kitten likes walking along pallets. + + + +``` + +```jsx:react +import SlCard from '@shoelace-style/shoelace/dist/react/card'; + +const css = ` + .card-image { + max-width: 300px; + } +`; + +const App = () => ( + <> + + A kitten walks towards camera on top of pallet. + This is a kitten, but not just any kitten. This kitten likes walking along pallets. + + + + +); +``` diff --git a/docs/pages/components/carousel-item.md b/docs/pages/components/carousel-item.md new file mode 100644 index 0000000..3c9417f --- /dev/null +++ b/docs/pages/components/carousel-item.md @@ -0,0 +1,85 @@ +--- +meta: + title: Carousel Item + description: A carousel item represent a slide within a carousel. +layout: component +--- + +```html:preview + + + The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash + + + A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash + + + The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash + + + A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash + + + A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + + + The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash + + + A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash + + + The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash + + + A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash + + + A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash + + +); +``` + +:::tip +Additional demonstrations can be found in the [carousel examples](/components/carousel). +::: diff --git a/docs/pages/components/carousel.md b/docs/pages/components/carousel.md new file mode 100644 index 0000000..25d8e76 --- /dev/null +++ b/docs/pages/components/carousel.md @@ -0,0 +1,1259 @@ +--- +meta: + title: Carousel + description: Carousels display an arbitrary number of content slides along a horizontal or vertical axis. +layout: component +--- + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +); +``` + +## Examples + +### Pagination + +Use the `pagination` attribute to show the total number of slides and the current slide as a set of interactive dots. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Navigation + +Use the `navigation` attribute to show previous and next buttons. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Looping + +By default, the carousel will not advanced beyond the first and last slides. You can change this behavior and force the carousel to "wrap" with the `loop` attribute. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Autoplay + +The carousel will automatically advance when the `autoplay` attribute is used. To change how long a slide is shown before advancing, set `autoplay-interval` to the desired number of milliseconds. For best results, use the `loop` attribute when autoplay is enabled. Note that autoplay will pause while the user interacts with the carousel. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Mouse Dragging + +The carousel uses [scroll snap](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap) to position slides at various snap positions. This allows users to scroll through the slides very naturally, especially on touch devices. Unfortunately, desktop users won't be able to click and drag with a mouse, which can feel unnatural. Adding the `mouse-dragging` attribute can help with this. + +This example is best demonstrated using a mouse. Try clicking and dragging the slide to move it. Then toggle the switch and try again. + +```html:preview +
      + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + Enable mouse dragging +
      + + +``` + +```jsx:react +import { useState } from 'react'; +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlSwitch from '@shoelace-style/shoelace/dist/react/switch'; + +const App = () => { + const [isEnabled, setIsEnabled] = useState(false); + + return ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + setIsEnabled(!isEnabled)}> + Enable mouse dragging + + + ); +}; +``` + +### Multiple Slides Per View + +The `slides-per-page` attribute makes it possible to display multiple slides at a time. You can also use the `slides-per-move` attribute to advance more than once slide at a time, if desired. + +```html:preview + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + +``` + +{% raw %} + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const App = () => ( + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + +); +``` + +{% endraw %} + +### Adding and Removing Slides + +The content of the carousel can be changed by adding or removing carousel items. The carousel will update itself automatically. + +```html:preview + + Slide 1 + Slide 2 + Slide 3 + + + + + + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const css = ` + .dynamic-carousel { + --aspect-ratio: 3 / 2; + } + + .dynamic-carousel ~ .carousel-options { + display: flex; + justify-content: center; + margin-top: var(--sl-spacing-large); + } + + .dynamic-carousel sl-carousel-item { + flex: 0 0 100%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: var(--sl-font-size-2x-large); + } +`; + +const App = () => { + const [slides, setSlides] = useState(['#204ed8', '#be133d', '#6e28d9']); + const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']; + + const addSlide = () => { + setSlides([...slides, getRandomColor()]); + }; + + const removeSlide = () => { + setSlides(slides.slice(0, -1)); + }; + + return ( + <> + + {slides.map((color, i) => ( + + Slide {i} + + ))} + + +
      + Add slide + Remove slide +
      + + + + ); +}; +``` + +{% endraw %} + +### Vertical Scrolling + +Setting the `orientation` attribute to `vertical` will render the carousel in a vertical layout. If the content of your slides vary in height, you will need to set amn explicit `height` or `max-height` on the carousel using CSS. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +``` + +```jsx:react +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; + +const css = ` + .vertical { + max-height: 400px; + } + + .vertical::part(base) { + grid-template-areas: 'slides slides pagination'; + } + + .vertical::part(pagination) { + flex-direction: column; + } + + .vertical::part(navigation) { + transform: rotate(90deg); + display: flex; + } +`; + +const App = () => ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + +); +``` + +### Aspect Ratio + +Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport from the default value of 16/9. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + + 1/1 + 3/2 + 16/9 + + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlSelect from '@shoelace-style/shoelace/dist/react/select'; +import SlOption from '@shoelace-style/shoelace/dist/react/option'; + +const App = () => { + const [aspectRatio, setAspectRatio] = useState('3/2'); + + return ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + setAspectRatio(event.target.value)} + > + 1 / 1 + 3 / 2 + 16 / 9 + + + + + ); +}; +``` + +{% endraw %} + +### Scroll Hint + +Use the `--scroll-hint` custom property to add inline padding in horizontal carousels and block padding in vertical carousels. This will make the closest slides slightly visible, hinting that there are more items in the carousel. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlRange from '@shoelace-style/shoelace/dist/react/range'; + +const App = () => ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +); +``` + +{% endraw %} + +### Gallery Example + +The carousel has a robust API that makes it possible to extend and customize. This example syncs the active slide with a set of thumbnails, effectively creating a gallery-style carousel. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +
      +
      + Thumbnail by 1 + Thumbnail by 2 + Thumbnail by 3 + Thumbnail by 4 + Thumbnail by 5 +
      +
      + + + + +``` + +```jsx:react +import { useRef } from 'react'; +import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; +import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlRange from '@shoelace-style/shoelace/dist/react/range'; + +const css = ` + .carousel-thumbnails { + --slide-aspect-ratio: 3 / 2; + } + + .thumbnails { + display: flex; + justify-content: center; + } + + .thumbnails__scroller { + display: flex; + gap: var(--sl-spacing-small); + overflow-x: auto; + scrollbar-width: none; + scroll-behavior: smooth; + scroll-padding: var(--sl-spacing-small); + } + + .thumbnails__scroller::-webkit-scrollbar { + display: none; + } + + .thumbnails__image { + width: 64px; + height: 64px; + object-fit: cover; + + opacity: 0.3; + will-change: opacity; + transition: 250ms opacity; + + cursor: pointer; + } + + .thumbnails__image.active { + opacity: 1; + } +`; + +const images = [ + { + src: '/assets/examples/carousel/mountains.jpg', + alt: 'The sun shines on the mountains and trees (by Adam Kool on Unsplash' + }, + { + src: '/assets/examples/carousel/waterfall.jpg', + alt: 'A waterfall in the middle of a forest (by Thomas Kelly on Unsplash' + }, + { + src: '/assets/examples/carousel/sunset.jpg', + alt: 'The sun is setting over a lavender field (by Leonard Cotte on Unsplash' + }, + { + src: '/assets/examples/carousel/field.jpg', + alt: 'A field of grass with the sun setting in the background (by Sapan Patel on Unsplash' + }, + { + src: '/assets/examples/carousel/valley.jpg', + alt: 'A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash' + } +]; + +const App = () => { + const carouselRef = useRef(); + const thumbnailsRef = useRef(); + const [currentSlide, setCurrentSlide] = useState(0); + + useEffect(() => { + const thumbnails = Array.from(thumbnailsRef.current.querySelectorAll('.thumbnails__image')); + + thumbnails[currentSlide]..scrollIntoView({ + block: 'nearest' + }); + }, [currentSlide]); + + const handleThumbnailClick = (index) => { + carouselRef.current.goToSlide(index); + } + + const handleSlideChange = (event) => { + const slideIndex = e.detail.index; + setCurrentSlide(slideIndex); + } + + return ( + <> + + {images.map({ src, alt }) => ( + + {alt} + + )} + + +
      +
      + {images.map({ src, alt }, i) => ( + {`Thumbnail handleThumbnailClick(i)} + src={src} + /> + )} +
      +
      + + + ); +}; +``` diff --git a/docs/pages/components/checkbox.md b/docs/pages/components/checkbox.md new file mode 100644 index 0000000..f5197b9 --- /dev/null +++ b/docs/pages/components/checkbox.md @@ -0,0 +1,178 @@ +--- +meta: + title: Checkbox + description: Checkboxes allow the user to toggle an option on or off. +layout: component +--- + +```html:preview +Checkbox +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Checkbox; +``` + +:::tip +This component works with standard `
      ` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. +::: + +## Examples + +### Checked + +Use the `checked` attribute to activate the checkbox. + +```html:preview +Checked +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Checked; +``` + +### Indeterminate + +Use the `indeterminate` attribute to make the checkbox indeterminate. + +```html:preview +Indeterminate +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Indeterminate; +``` + +### Disabled + +Use the `disabled` attribute to disable the checkbox. + +```html:preview +Disabled +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Disabled; +``` + +### Sizes + +Use the `size` attribute to change a checkbox's size. + +```html:preview +Small +
      +Medium +
      +Large +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => ( + <> + Small +
      + Medium +
      + Large + +); +``` + +### Help Text + +Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead. + +```html:preview +Label +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Label; +``` + +### Custom Validity + +Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string. + +```html:preview + + Check me +
      + Submit +
      + +``` + +{% raw %} + +```jsx:react +import { useEffect, useRef } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => { + const checkbox = useRef(null); + const errorMessage = `Don't forget to check me!`; + + function handleChange() { + checkbox.current.setCustomValidity(checkbox.current.checked ? '' : errorMessage); + } + + function handleSubmit(event) { + event.preventDefault(); + alert('All fields are valid!'); + } + + useEffect(() => { + checkbox.current.setCustomValidity(errorMessage); + }, []); + + return ( +
      + + Check me + +
      + + Submit + +
      + ); +}; +``` + +{% endraw %} diff --git a/docs/pages/components/color-picker.md b/docs/pages/components/color-picker.md new file mode 100644 index 0000000..a85dbdd --- /dev/null +++ b/docs/pages/components/color-picker.md @@ -0,0 +1,140 @@ +--- +meta: + title: Color Picker + description: Color pickers allow the user to select a color. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ; +``` + +:::tip +This component works with standard `
      ` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. +::: + +## Examples + +### Initial Value + +Use the `value` attribute to set an initial value for the color picker. + +```html:preview + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ; +``` + +### Opacity + +Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA, or HSVA based on `format`. + +```html:preview + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ; +``` + +### Formats + +Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl`, and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option. + +To prevent users from toggling the format themselves, add the `no-format-toggle` attribute. + +```html:preview + + + + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ( + <> + + + + + +); +``` + +### Swatches + +Use the `swatches` attribute to add convenient presets to the color picker. Any format the color picker can parse is acceptable (including CSS color names), but each value must be separated by a semicolon (`;`). Alternatively, you can pass an array of color values to this property using JavaScript. + +```html:preview + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ( + +); +``` + +### Sizes + +Use the `size` attribute to change the color picker's trigger size. + +```html:preview + + + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ( + <> + + + + +); +``` + +### Inline + +The color picker can be rendered inline instead of in a dropdown using the `inline` attribute. + +```html:preview + +``` + +```jsx:react +import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; + +const App = () => ; +``` diff --git a/docs/pages/components/copy-button.md b/docs/pages/components/copy-button.md new file mode 100644 index 0000000..1565e13 --- /dev/null +++ b/docs/pages/components/copy-button.md @@ -0,0 +1,258 @@ +--- +meta: + title: Copy Button + description: Copies data to the clipboard when the user clicks the button. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; + +const App = () => ( + +); +``` + +## Examples + +### Custom Labels + +Copy Buttons display feedback in a tooltip. You can customize the labels using the `copy-label`, `success-label`, and `error-label` attributes. + +```html:preview + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; + +const App = () => ( + +); +``` + +### Custom Icons + +Use the `copy-icon`, `success-icon`, and `error-icon` slots to customize the icons that get displayed for each state. You can use [``](/components/icon) or your own images. + +```html:preview + + + + + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; +import { SlIcon } from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> + + + + + + +); +``` + +### Copying Values From Other Elements + +Normally, the data that gets copied will come from the component's `value` attribute, but you can copy data from any element within the same document by providing its `id` to the `from` attribute. + +When using the `from` attribute, the element's [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) will be copied by default. Passing an attribute or property modifier will let you copy data from one of the element's attributes or properties instead. + +To copy data from an attribute, use `from="id[attr]"` where `id` is the id of the target element and `attr` is the name of the attribute you'd like to copy. To copy data from a property, use `from="id.prop"` where `id` is the id of the target element and `prop` is the name of the property you'd like to copy. + +```html:preview + ++1 (234) 456-7890 + + +

      + + + + + +

      + + +
      Shoelace Website + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; +import { SlInput } from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ( + <> + {/* Copies the span's textContent */} + +1 (234) 456-7890 + + +

      + + {/* Copies the input's "value" property */} + + + +

      + + {/* Copies the link's "href" attribute */} + Shoelace Website + + +); +``` + +### Handling Errors + +A copy error will occur if the value is an empty string, if the `from` attribute points to an id that doesn't exist, or if the browser rejects the operation for any reason. When this happens, the `sl-error` event will be emitted. + +This example demonstrates what happens when a copy error occurs. You can customize the error label and icon using the `error-label` attribute and the `error-icon` slot, respectively. + +```html:preview + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; + +const App = () => ( + +); +``` + +### Disabled + +Copy buttons can be disabled by adding the `disabled` attribute. + +```html:preview + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; + +const App = () => ( + +); +``` + +### Changing Feedback Duration + +A success indicator is briefly shown after copying. You can customize the length of time the indicator is shown using the `feedback-duration` attribute. + +```html:preview + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; + +const App = () => ( + +); +``` + +### Custom Styles + +You can customize the button to your liking with CSS. + +```html:preview + + + + + + + +``` + +```jsx:react +import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; + +const css = ` + .custom-styles { + --success-color: white; + --error-color: white; + color: white; + } + + .custom-styles::part(button) { + background-color: #ff1493; + border: solid 4px #ff7ac1; + border-right-color: #ad005c; + border-bottom-color: #ad005c; + border-radius: 0; + transition: 100ms scale ease-in-out, 100ms translate ease-in-out; + } + + .custom-styles::part(button):hover { + scale: 1.1; + } + + .custom-styles::part(button):active { + translate: 0 2px; + } + + .custom-styles::part(button):focus-visible { + outline: dashed 2px deeppink; + outline-offset: 4px; + } +`; + +const App = () => ( + <> + + + + +); +``` diff --git a/docs/pages/components/details.md b/docs/pages/components/details.md new file mode 100644 index 0000000..dc052b6 --- /dev/null +++ b/docs/pages/components/details.md @@ -0,0 +1,137 @@ +--- +meta: + title: Details + description: Details show a brief summary and expand to show additional content. +layout: component +--- + + + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +``` + +```jsx:react +import SlDetails from '@shoelace-style/shoelace/dist/react/details'; + +const App = () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +); +``` + +## Examples + +### Disabled + +Use the `disable` attribute to prevent the details from expanding. + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +``` + +```jsx:react +import SlDetails from '@shoelace-style/shoelace/dist/react/details'; + +const App = () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +); +``` + +### Customizing the Summary Icon + +Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. To disable the animation, override the `rotate` property on the `summary-icon` part as shown below. + +```html:preview + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + + + +``` + +```jsx:react +import SlDetails from '@shoelace-style/shoelace/dist/react/details'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const css = ` + sl-details.custom-icon::part(summary-icon) { + /* Disable the expand/collapse animation */ + rotate: none; + } +`; + +const App = () => ( + <> + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. + + + + +); +``` + +### Grouping Details + +Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `sl-show` event. + +```html:preview +
      + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +
      + + + + +``` diff --git a/docs/pages/components/dialog.md b/docs/pages/components/dialog.md new file mode 100644 index 0000000..46dca36 --- /dev/null +++ b/docs/pages/components/dialog.md @@ -0,0 +1,329 @@ +--- +meta: + title: Dialog + description: 'Dialogs, sometimes called "modals", appear above the page and require the user''s immediate attention.' +layout: component +--- + + + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Dialog + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +## Examples + +### Custom Width + +Use the `--width` custom property to set the dialog's width. + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Dialog + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +{% endraw %} + +### Scrolling + +By design, a dialog's height will never exceed that of the viewport. As such, dialogs will not scroll with the page ensuring the header and footer are always accessible to the user. + +```html:preview + +
      +

      Scroll down and give it a try! 👇

      +
      + Close +
      + +Open Dialog + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> +
      +

      Scroll down and give it a try! 👇

      +
      + + setOpen(false)}> + Close + +
      + + setOpen(true)}>Open Dialog + + ); +}; +``` + +{% endraw %} + +### Header Actions + +The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed. + +```html:preview + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Dialog + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + window.open(location.href)} + /> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +### Preventing the Dialog from Closing + +By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur. + +To keep the dialog open in such cases, you can cancel the `sl-request-close` event. When canceled, the dialog will remain open and pulse briefly to draw the user's attention to it. + +You can use `event.detail.source` to determine what triggered the request to close. This example prevents the dialog from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it. + +```html:preview + + This dialog will not close when you click on the overlay. + Close + + +Open Dialog + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; + +const App = () => { + const [open, setOpen] = useState(false); + + // Prevent the dialog from closing when the user clicks on the overlay + function handleRequestClose(event) { + if (event.detail.source === 'overlay') { + event.preventDefault(); + } + } + + return ( + <> + setOpen(false)}> + This dialog will not close when you click on the overlay. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +### Customizing Initial Focus + +By default, the dialog's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the dialog. If you want a different element to have focus, add the `autofocus` attribute to it as shown below. + +```html:preview + + + Close + + +Open Dialog + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +:::tip +You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler. +::: diff --git a/docs/pages/components/divider.md b/docs/pages/components/divider.md new file mode 100644 index 0000000..2d3e062 --- /dev/null +++ b/docs/pages/components/divider.md @@ -0,0 +1,158 @@ +--- +meta: + title: Divider + description: Dividers are used to visually separate or group elements. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; + +const App = () => ; +``` + +## Examples + +### Width + +Use the `--width` custom property to change the width of the divider. + +```html:preview + +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; + +const App = () => ; +``` + +{% endraw %} + +### Color + +Use the `--color` custom property to change the color of the divider. + +```html:preview + +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; + +const App = () => ; +``` + +{% endraw %} + +### Spacing + +Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements. + +```html:preview +
      + Above + + Below +
      +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; + +const App = () => ( + <> + Above + + Below + +); +``` + +{% endraw %} + +### Vertical + +Add the `vertical` attribute to draw the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container. + +```html:preview +
      + First + + Middle + + Last +
      +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; + +const App = () => ( +
      + First + + Middle + + Last +
      +); +``` + +{% endraw %} + +### Menu Dividers + +Use dividers in [menus](/components/menu) to visually group menu items. + +```html:preview + + Option 1 + Option 2 + Option 3 + + Option 4 + Option 5 + Option 6 + +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Option 1 + Option 2 + Option 3 + + Option 4 + Option 5 + Option 6 + +); +``` + +{% endraw %} diff --git a/docs/pages/components/drawer.md b/docs/pages/components/drawer.md new file mode 100644 index 0000000..329d38d --- /dev/null +++ b/docs/pages/components/drawer.md @@ -0,0 +1,534 @@ +--- +meta: + title: Drawer + description: Drawers slide in from a container to expose additional options and information. +layout: component +--- + + + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +## Examples + +### Slide in From Start + +By default, drawers slide in from the end. To make the drawer slide in from the start, set the `placement` attribute to `start`. + +```html:preview + + This drawer slides in from the start. + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + This drawer slides in from the start. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +### Slide in From Top + +To make the drawer slide in from the top, set the `placement` attribute to `top`. + +```html:preview + + This drawer slides in from the top. + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + This drawer slides in from the top. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +### Slide in From Bottom + +To make the drawer slide in from the bottom, set the `placement` attribute to `bottom`. + +```html:preview + + This drawer slides in from the bottom. + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + This drawer slides in from the bottom. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +### Contained to an Element + +By default, drawers slide out of their [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport. To make a drawer slide out of a parent element, add the `contained` attribute to the drawer and apply `position: relative` to its parent. + +Unlike normal drawers, contained drawers are not modal. This means they do not show an overlay, they do not trap focus, and they are not dismissible with [[Escape]]. This is intentional to allow users to interact with elements outside of the drawer. + +```html:preview +
      + The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer opens. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + +
      + +Toggle Drawer + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> +
      + The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer + opens. + setOpen(false)} + style={{ '--size': '50%' }} + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + +
      + + setOpen(true)}>Open Drawer + + ); +}; +``` + +{% endraw %} + +### Custom Size + +Use the `--size` custom property to set the drawer's size. This will be applied to the drawer's width or height depending on its `placement`. + +```html:preview + + This drawer is always 50% of the viewport. + Close + + +Open Drawer + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)} style={{ '--size': '50vw' }}> + This drawer is always 50% of the viewport. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +{% endraw %} + +### Scrolling + +By design, a drawer's height will never exceed 100% of its container. As such, drawers will not scroll with the page to ensure the header and footer are always accessible to the user. + +```html:preview + +
      +

      Scroll down and give it a try! 👇

      +
      + Close +
      + +Open Drawer + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> +
      +

      Scroll down and give it a try! 👇

      +
      + setOpen(false)}> + Close + +
      + + setOpen(true)}>Open Drawer + + ); +}; +``` + +{% endraw %} + +### Header Actions + +The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed. + +```html:preview + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + window.open(location.href)} /> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +### Preventing the Drawer from Closing + +By default, drawers will close when the user clicks the close button, clicks the overlay, or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur. + +To keep the drawer open in such cases, you can cancel the `sl-request-close` event. When canceled, the drawer will remain open and pulse briefly to draw the user's attention to it. + +You can use `event.detail.source` to determine what triggered the request to close. This example prevents the drawer from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it. + +```html:preview + + This drawer will not close when you click on the overlay. + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; + +const App = () => { + const [open, setOpen] = useState(false); + + // Prevent the drawer from closing when the user clicks on the overlay + function handleRequestClose(event) { + if (event.detail.source === 'overlay') { + event.preventDefault(); + } + } + + return ( + <> + setOpen(false)}> + This drawer will not close when you click on the overlay. + setOpen(false)}> + Save & Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +### Customizing Initial Focus + +By default, the drawer's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the drawer. If you want a different element to have focus, add the `autofocus` attribute to it as shown below. + +```html:preview + + + Close + + +Open Drawer + + +``` + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + + setOpen(false)}> + Close + + + + setOpen(true)}>Open Drawer + + ); +}; +``` + +:::tip +You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler. +::: diff --git a/docs/pages/components/dropdown.md b/docs/pages/components/dropdown.md new file mode 100644 index 0000000..b187605 --- /dev/null +++ b/docs/pages/components/dropdown.md @@ -0,0 +1,482 @@ +--- +meta: + title: Dropdown + description: 'Dropdowns expose additional content that "drops down" in a panel.' +layout: component +--- + +Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it. + +Dropdowns are designed to work well with [menus](/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker)). The API gives you complete control over showing, hiding, and positioning the panel. + +```html:preview + + Dropdown + + Dropdown Item 1 + Dropdown Item 2 + Dropdown Item 3 + + Checkbox + Disabled + + + Prefix + + + + Suffix Icon + + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + + Dropdown + + + Dropdown Item 1 + Dropdown Item 2 + Dropdown Item 3 + + + Checkbox + + Disabled + + + Prefix + + + + Suffix Icon + + + + +); +``` + +## Examples + +### Getting the Selected Item + +When dropdowns are used with [menus](/components/menu), you can listen for the [`sl-select`](/components/menu#events) event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands. + +```html:preview + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => { + function handleSelect(event) { + const selectedItem = event.detail.item; + console.log(selectedItem.value); + } + + return ( + + + Edit + + + Cut + Copy + Paste + + + ); +}; +``` + +Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event. + +```html:preview + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => { + function handleCut() { + console.log('cut'); + } + + function handleCopy() { + console.log('copy'); + } + + function handlePaste() { + console.log('paste'); + } + + return ( + + + Edit + + + Cut + Copy + Paste + + + ); +}; +``` + +### Placement + +The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. + +```html:preview + + Edit + + Cut + Copy + Paste + + Find + Replace + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + + Edit + + + Cut + Copy + Paste + + Find + Replace + + +); +``` + +### Distance + +The distance from the panel to the trigger can be customized using the `distance` attribute. This value is specified in pixels. + +```html:preview + + Edit + + Cut + Copy + Paste + + Find + Replace + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + + Edit + + + Cut + Copy + Paste + + Find + Replace + + +); +``` + +### Skidding + +The offset of the panel along the trigger can be customized using the `skidding` attribute. This value is specified in pixels. + +```html:preview + + Edit + + Cut + Copy + Paste + + Find + Replace + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + + Edit + + + Cut + Copy + Paste + + Find + Replace + + +); +``` + +### Submenus + +To create a submenu, nest an `` element in a [menu item](/components/menu-item). + +```html:preview + + Edit + + + Undo + Redo + + Cut + Copy + Paste + + + Find + + Find… + Find Next + Find Previous + + + + Transformations + + Make uppercase + Make lowercase + Capitalize + + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const css = ` + .dropdown-hoist { + border: solid 2px var(--sl-panel-border-color); + padding: var(--sl-spacing-medium); + overflow: hidden; + } +`; + +const App = () => ( + <> + + Edit + + + Undo + Redo + + Cut + Copy + Paste + + + Find + + Find… + Find Next + Find Previous + + + + Transformations + + Make uppercase + Make lowercase + Capitalize + + + + + +); +``` + +:::warning +As a UX best practice, avoid using more than one level of submenu when possible. +::: + +### Hoisting + +Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details. + +```html:preview + + + +``` + +```jsx:react +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const css = ` + .dropdown-hoist { + border: solid 2px var(--sl-panel-border-color); + padding: var(--sl-spacing-medium); + overflow: hidden; + } +`; + +const App = () => ( + <> +
      + + + No Hoist + + + Item 1 + Item 2 + Item 3 + + + + + + Hoist + + + Item 1 + Item 2 + Item 3 + + +
      + + + +); +``` diff --git a/docs/pages/components/format-bytes.md b/docs/pages/components/format-bytes.md new file mode 100644 index 0000000..82f95d9 --- /dev/null +++ b/docs/pages/components/format-bytes.md @@ -0,0 +1,134 @@ +--- +meta: + title: Format Bytes + description: Formats a number as a human readable bytes value. +layout: component +--- + +```html:preview +
      + The file is in size.

      + +
      + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlButton from '@shoelace-style/shoelace/dist/react/button'; +import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => { + const [value, setValue] = useState(1000); + + return ( + <> + The file is in size. +
      +
      + setValue(event.target.value)} + /> + + ); +}; +``` + +{% endraw %} + +## Examples + +### Formatting Bytes + +Set the `value` attribute to a number to get the value in bytes. + +```html:preview +
      +
      +
      + +``` + +```jsx:react +import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; + +const App = () => ( + <> + +
      + +
      + +
      + + +); +``` + +### Formatting Bits + +To get the value in bits, set the `unit` attribute to `bit`. + +```html:preview +
      +
      +
      + +``` + +```jsx:react +import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; + +const App = () => ( + <> + +
      + +
      + +
      + + +); +``` + +### Localization + +Use the `lang` attribute to set the number formatting locale. + +```html:preview +
      +
      +
      + +``` + +```jsx:react +import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; + +const App = () => ( + <> + +
      + +
      + +
      + + +); +``` diff --git a/docs/pages/components/format-date.md b/docs/pages/components/format-date.md new file mode 100644 index 0000000..10e39a8 --- /dev/null +++ b/docs/pages/components/format-date.md @@ -0,0 +1,127 @@ +--- +meta: + title: Format Date + description: Formats a date/time using the specified locale and options. +layout: component +--- + +Localization is handled by the browser's [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). No language packs are required. + +```html:preview + + +``` + +```jsx:react +import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; + +const App = () => ; +``` + +The `date` attribute determines the date/time to use when formatting. It must be a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object set via JavaScript. If omitted, the current date/time will be assumed. + +:::tip +When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, always use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure the date will be parsed properly by all clients. +::: + +## Examples + +### Date & Time Formatting + +Formatting options are based on those found in the [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). When formatting options are provided, the date/time will be formatted according to those values. When no formatting options are provided, a localized, numeric date will be displayed instead. + +```html:preview + +
      + + +
      + + +
      + + +
      + + +
      + + + +``` + +```jsx:react +import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; + +const App = () => ( + <> + {/* Human-readable date */} + +
      + + {/* Time */} + +
      + + {/* Weekday */} + +
      + + {/* Month */} + +
      + + {/* Year */} + +
      + + {/* No formatting options */} + + +); +``` + +### Hour Formatting + +By default, the browser will determine whether to use 12-hour or 24-hour time. To force one or the other, set the `hour-format` attribute to `12` or `24`. + +```html:preview +
      + +``` + +```jsx:react +import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; + +const App = () => ( + <> + +
      + + +); +``` + +### Localization + +Use the `lang` attribute to set the date/time formatting locale. + +```html:preview +English:
      +French:
      +Russian: +``` + +```jsx:react +import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; + +const App = () => ( + <> + English: +
      + French: +
      + Russian: + +); +``` diff --git a/docs/pages/components/format-number.md b/docs/pages/components/format-number.md new file mode 100644 index 0000000..d54187d --- /dev/null +++ b/docs/pages/components/format-number.md @@ -0,0 +1,139 @@ +--- +meta: + title: Format Number + description: Formats a number using the specified locale and options. +layout: component +--- + +Localization is handled by the browser's [`Intl.NumberFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required. + +```html:preview +
      + +

      + +
      + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => { + const [value, setValue] = useState(1000); + + return ( + <> + +
      +
      + setValue(event.target.value)} + /> + + ); +}; +``` + +{% endraw %} + +## Examples + +### Percentages + +To get the value as a percent, set the `type` attribute to `percent`. + +```html:preview +
      +
      +
      +
      + +``` + +```jsx:react +import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; + +const App = () => ( + <> + +
      + +
      + +
      + +
      + + +); +``` + +### Localization + +Use the `lang` attribute to set the number formatting locale. + +```html:preview +English:
      +German:
      +Russian: +``` + +```jsx:react +import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; + +const App = () => ( + <> + English: +
      + German: +
      + Russian: + +); +``` + +### Currency + +To format a number as a monetary value, set the `type` attribute to `currency` and set the `currency` attribute to the desired ISO 4217 currency code. You should also specify `lang` to ensure the the number is formatted correctly for the target locale. + +```html:preview +
      +
      +
      +
      + +``` + +```jsx:react +import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; + +const App = () => ( + <> + +
      + +
      + +
      + +
      + + +); +``` diff --git a/docs/pages/components/icon-button.md b/docs/pages/components/icon-button.md new file mode 100644 index 0000000..2606e1d --- /dev/null +++ b/docs/pages/components/icon-button.md @@ -0,0 +1,153 @@ +--- +meta: + title: Icon Button + description: Icons buttons are simple, icon-only buttons that can be used for actions and in toolbars. +layout: component +--- + +For a full list of icons that come bundled with Shoelace, refer to the [icon component](/components/icon). + +```html:preview + +``` + +```jsx:react +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const App = () => ; +``` + +## Examples + +### Sizes + +Icon buttons inherit their parent element's `font-size`. + +```html:preview + + + +``` + +{% raw %} + +```jsx:react +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const App = () => ( + <> + + + + +); +``` + +{% endraw %} + +### Colors + +Icon buttons are designed to have a uniform appearance, so their color is not inherited. However, you can still customize them by styling the `base` part. + +```html:preview +
      + + + +
      + + +``` + +```jsx:react +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const css = ` + .icon-button-color sl-icon-button::part(base) { + color: #b00091; + } + + .icon-button-color sl-icon-button::part(base):hover, + .icon-button-color sl-icon-button::part(base):focus { + color: #c913aa; + } + + .icon-button-color sl-icon-button::part(base):active { + color: #960077; + } +`; + +const App = () => ( + <> +
      + + + +
      + + + +); +``` + +### Link Buttons + +Use the `href` attribute to convert the button to a link. + +```html:preview + +``` + +```jsx:react +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const App = () => ; +``` + +### Icon Button with Tooltip + +Wrap a tooltip around an icon button to provide contextual information to the user. + +```html:preview + + + +``` + +```jsx:react +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; +import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; + +const App = () => ( + + + +); +``` + +### Disabled + +Use the `disabled` attribute to disable the icon button. + +```html:preview + +``` + +```jsx:react +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; + +const App = () => ; +``` diff --git a/docs/pages/components/icon.md b/docs/pages/components/icon.md new file mode 100644 index 0000000..9a8b8bd --- /dev/null +++ b/docs/pages/components/icon.md @@ -0,0 +1,869 @@ +--- +meta: + title: Icon + description: Icons are symbols that can be used to represent various options within an application. +layout: component +--- + +Shoelace comes bundled with over 1,500 icons courtesy of the [Bootstrap Icons](https://icons.getbootstrap.com/) project. These icons are part of the `default` icon library. If you prefer, you can register [custom icon libraries](#icon-libraries) as well. + +:::tip +Depending on how you're loading Shoelace, you may need to copy icon assets and/or [set the base path](/getting-started/installation/#setting-the-base-path) so Shoelace knows where to load them from. Otherwise, icons may not appear and you'll see 404 Not Found errors in the dev console. +::: + +## Default Icons + +All available icons in the `default` icon library are shown below. Click or tap on any icon to copy its name, then you can use it in your HTML like this. + +```html + +``` + + + +## Examples + +### Colors + +Icons inherit their color from the current text color. Thus, you can set the `color` property on the `` element or an ancestor to change the color. + +```html:preview +
      + + + + +
      +
      + + + + +
      +
      + + + + +
      +
      + + + + +
      +``` + +{% raw %} + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( + <> +
      + + + + +
      +
      + + + + +
      +
      + + + + +
      +
      + + + + +
      + +); +``` + +{% endraw %} + +### Sizing + +Icons are sized relative to the current font size. To change their size, set the `font-size` property on the icon itself or on a parent element as shown below. + +```html:preview +
      + + + + + + + + + + + + + + + + +
      +``` + +{% raw %} + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ( +
      + + + + + + + + + + + + + + + + +
      +); +``` + +{% endraw %} + +### Labels + +For non-decorative icons, use the `label` attribute to announce it to assistive devices. + +```html:preview + +``` + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ; +``` + +### Custom Icons + +Custom icons can be loaded individually with the `src` attribute. Only SVGs on a local or CORS-enabled endpoint are supported. If you're using more than one custom icon, it might make sense to register a [custom icon library](#icon-libraries). + +```html:preview + +``` + +{% raw %} + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; + +const App = () => ; +``` + +{% endraw %} + +## Icon Libraries + +You can register additional icons to use with the `` component through icon libraries. Icon files can exist locally or on a CORS-enabled endpoint (e.g. a CDN). There is no limit to how many icon libraries you can register and there is no cost associated with registering them, as individual icons are only requested when they're used. + +Shoelace ships with two built-in icon libraries, `default` and `system`. The [default icon library](#customizing-the-default-library) contains all of the icons in the Bootstrap Icons project. The [system icon library](#customizing-the-system-library) contains only a small subset of icons that are used internally by Shoelace components. + +To register an additional icon library, use the `registerIconLibrary()` function that's exported from `utilities/icon-library.js`. At a minimum, you must provide a name and a resolver function. The resolver function translates an icon name to a URL where the corresponding SVG file exists. Refer to the examples below to better understand how it works. + +If necessary, a mutator function can be used to mutate the SVG element before rendering. This is necessary for some libraries due to the many possible ways SVGs are crafted. For example, icons should ideally inherit the current text color via `currentColor`, so you may need to apply `fill="currentColor` or `stroke="currentColor"` to the SVG element using this function. + +Here's an example that registers an icon library located in the `/assets/icons` directory. + +```html + +``` + +To display an icon, set the `library` and `name` attributes of an `` element. + +```html + + +``` + +If an icon is used before registration occurs, it will be empty initially but shown when registered. + +The following examples demonstrate how to register a number of popular, open source icon libraries via CDN. Feel free to adapt the code as you see fit to use your own origin or naming conventions. + +### Boxicons + +This will register the [Boxicons](https://boxicons.com/) library using the jsDelivr CDN. This library has three variations: regular (`bx-*`), solid (`bxs-*`), and logos (`bxl-*`). A mutator function is required to set the SVG's `fill` to `currentColor`. + +Icons in this library are licensed under the [Creative Commons 4.0 License](https://github.com/atisawd/boxicons#license). + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Lucide + +This will register the [Lucide](https://lucide.dev/) icon library using the jsDelivr CDN. This project is a community-maintained fork of the popular [Feather](https://feathericons.com/) icon library. + +Icons in this library are licensed under the [MIT License](https://github.com/lucide-icons/lucide/blob/master/LICENSE). + +```html:preview +
      + + + + + + +
      + + +``` + +### Font Awesome + +This will register the [Font Awesome Free](https://fontawesome.com/) library using the jsDelivr CDN. This library has three variations: regular (`far-*`), solid (`fas-*`), and brands (`fab-*`). A mutator function is required to set the SVG's `fill` to `currentColor`. + +Icons in this library are licensed under the [Font Awesome Free License](https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt). Some of the icons that appear on the Font Awesome website require a license and are therefore not available in the CDN. + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Heroicons + +This will register the [Heroicons](https://heroicons.com/) library using the jsDelivr CDN. + +Icons in this library are licensed under the [MIT License](https://github.com/tailwindlabs/heroicons/blob/master/LICENSE). + +```html:preview + + +
      + + + + + + +
      +``` + +### Iconoir + +This will register the [Iconoir](https://iconoir.com/) library using the jsDelivr CDN. + +Icons in this library are licensed under the [MIT License](https://github.com/lucaburgio/iconoir/blob/master/LICENSE). + +```html:preview + + +
      + + + + + + +
      +``` + +### Ionicons + +This will register the [Ionicons](https://ionicons.com/) library using the jsDelivr CDN. This library has three variations: outline (default), filled (`*-filled`), and sharp (`*-sharp`). A mutator function is required to polyfill a handful of styles we're not including. + +Icons in this library are licensed under the [MIT License](https://github.com/ionic-team/ionicons/blob/master/LICENSE). + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Jam Icons + +This will register the [Jam Icons](https://jam-icons.com/) library using the jsDelivr CDN. This library has two variations: regular (default) and filled (`*-f`). A mutator function is required to set the SVG's `fill` to `currentColor`. + +Icons in this library are licensed under the [MIT License](https://github.com/michaelampr/jam/blob/master/LICENSE). + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Material Icons + +This will register the [Material Icons](https://material.io/resources/icons/?style=baseline) library using the jsDelivr CDN. This library has three variations: outline (default), round (`*_round`), and sharp (`*_sharp`). A mutator function is required to set the SVG's `fill` to `currentColor`. + +Icons in this library are licensed under the [Apache 2.0 License](https://github.com/google/material-design-icons/blob/master/LICENSE). + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Remix Icon + +This will register the [Remix Icon](https://remixicon.com/) library using the jsDelivr CDN. This library groups icons by categories, so the name must include the category and icon separated by a slash, as well as the `-line` or `-fill` suffix as needed. A mutator function is required to set the SVG's `fill` to `currentColor`. + +Icons in this library are licensed under the [Apache 2.0 License](https://github.com/Remix-Design/RemixIcon/blob/master/License). + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Tabler Icons + +This will register the [Tabler Icons](https://tabler-icons.io/) library using the jsDelivr CDN. This library features over 1,950 open source icons. + +Icons in this library are licensed under the [MIT License](https://github.com/tabler/tabler-icons/blob/master/LICENSE). + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Unicons + +This will register the [Unicons](https://iconscout.com/unicons) library using the jsDelivr CDN. This library has two variations: line (default) and solid (`*-s`). A mutator function is required to set the SVG's `fill` to `currentColor`. + +Icons in this library are licensed under the [Apache 2.0 License](https://github.com/Iconscout/unicons/blob/master/LICENSE). Some of the icons that appear on the Unicons website, particularly many of the solid variations, require a license and are therefore not available in the CDN. + +```html:preview + + +
      + + + + + + +
      + + + + + + +
      +``` + +### Customizing the Default Library + +The default icon library contains over 1,300 icons courtesy of the [Bootstrap Icons](https://icons.getbootstrap.com/) project. These are the icons that display when you use `` without the `library` attribute. If you prefer to have these icons resolve elsewhere or to a different icon library, register an icon library using the `default` name and a custom resolver. + +This example will load the same set of icons from the jsDelivr CDN instead of your local assets folder. + +```html + +``` + +#### Customize the default library to use SVG sprites + +To improve performance you can use a SVG sprites to avoid multiple trips for each SVG. The browser will load the sprite sheet once and then you reference the particular SVG within the sprite sheet using hash selector. + +As always, make sure to benchmark these changes. When using HTTP/2, it may in fact be more bandwidth-friendly to use multiple small requests instead of 1 large sprite sheet. + +:::danger +When using sprite sheets, the `sl-load` and `sl-error` events will not fire. +::: + +:::danger +For security reasons, browsers may apply the same-origin policy on `` elements located in the `` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `` elements. For this reason, sprite sheets should only be used if you're self-hosting them. +::: + +```html:preview + + +
      + + +
      +``` + +### Customizing the System Library + +The system library contains only the icons used internally by Shoelace components. Unlike the default icon library, the system library does not rely on physical assets. Instead, its icons are hard-coded as data URIs into the resolver to ensure their availability. + +If you want to change the icons Shoelace uses internally, you can register an icon library using the `system` name and a custom resolver. If you choose to do this, it's your responsibility to provide all of the icons that are required by components. You can reference `src/components/library.system.ts` for a complete list of system icons used by Shoelace. + +```html + +``` + + + + + diff --git a/docs/pages/components/image-comparer.md b/docs/pages/components/image-comparer.md new file mode 100644 index 0000000..9e1836a --- /dev/null +++ b/docs/pages/components/image-comparer.md @@ -0,0 +1,82 @@ +--- +meta: + title: Image Comparer + description: Compare visual differences between similar photos with a sliding panel. +layout: component +--- + +For best results, use images that share the same dimensions. The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.) + +```html:preview + + Grayscale version of kittens in a basket looking around. + Color version of kittens in a basket looking around. + +``` + +```jsx:react +import SlImageComparer from '@shoelace-style/shoelace/dist/react/image-comparer'; + +const App = () => ( + + Grayscale version of kittens in a basket looking around. + Color version of kittens in a basket looking around. + +); +``` + +## Examples + +### Initial Position + +Use the `position` attribute to set the initial position of the slider. This is a percentage from `0` to `100`. + +```html:preview + + A person sitting on bricks wearing untied boots. + A person sitting on a yellow curb tying shoelaces on a boot. + +``` + +```jsx:react +import SlImageComparer from '@shoelace-style/shoelace/dist/react/image-comparer'; + +const App = () => ( + + A person sitting on bricks wearing untied boots. + A person sitting on a yellow curb tying shoelaces on a boot. + +); +``` diff --git a/docs/pages/components/include.md b/docs/pages/components/include.md new file mode 100644 index 0000000..7637376 --- /dev/null +++ b/docs/pages/components/include.md @@ -0,0 +1,48 @@ +--- +meta: + title: Include + description: Includes give you the power to embed external HTML files into the page. +layout: component +--- + +Included files are asynchronously requested using `window.fetch()`. Requests are cached, so the same file can be included multiple times, but only one request will be made. + +The included content will be inserted into the `` element's default slot so it can be easily accessed and styled through the light DOM. + +```html:preview + +``` + +```jsx:react +import SlInclude from '@shoelace-style/shoelace/dist/react/include'; + +const App = () => ; +``` + +## Examples + +### Listening for Events + +When an include file loads successfully, the `sl-load` event will be emitted. You can listen for this event to add custom loading logic to your includes. + +If the request fails, the `sl-error` event will be emitted. In this case, `event.detail.status` will contain the resulting HTTP status code of the request, e.g. 404 (not found). + +```html + + + +``` diff --git a/docs/pages/components/input.md b/docs/pages/components/input.md new file mode 100644 index 0000000..2a60cbf --- /dev/null +++ b/docs/pages/components/input.md @@ -0,0 +1,281 @@ +--- +meta: + title: Input + description: Inputs collect data from the user. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +:::tip +This component works with standard `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. +::: + +## Examples + +### Labels + +Use the `label` attribute to give the input an accessible label. For labels that contain HTML, use the `label` slot instead. + +```html:preview + +``` + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Help Text + +Add descriptive help text to an input with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead. + +```html:preview + +``` + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Placeholders + +Use the `placeholder` attribute to add a placeholder. + +```html:preview + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Clearable + +Add the `clearable` attribute to add a clear button when the input has content. + +```html:preview + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Toggle Password + +Add the `password-toggle` attribute to add a toggle button that will show the password when activated. + +```html:preview + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Filled Inputs + +Add the `filled` attribute to draw a filled input. + +```html:preview + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Disabled + +Use the `disabled` attribute to disable an input. + +```html:preview + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ; +``` + +### Sizes + +Use the `size` attribute to change an input's size. + +```html:preview + +
      + +
      + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ( + <> + +
      + +
      + + +); +``` + +### Pill + +Use the `pill` attribute to give inputs rounded edges. + +```html:preview + +
      + +
      + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ( + <> + +
      + +
      + + +); +``` + +### Input Types + +The `type` attribute controls the type of input the browser renders. + +```html:preview + +
      + +
      + +``` + +```jsx:react +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ( + <> + +
      + +
      + + +); +``` + +### Prefix & Suffix Icons + +Use the `prefix` and `suffix` slots to add icons. + +```html:preview + + + + +
      + + + + +
      + + + + +``` + +```jsx:react +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlInput from '@shoelace-style/shoelace/dist/react/input'; + +const App = () => ( + <> + + + + +
      + + + + +
      + + + + + +); +``` + +### Customizing Label Position + +Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the label to the left of the control, but the possible orientations are nearly endless. The same technique works for inputs, textareas, radio groups, and similar form controls. + +```html:preview + + + + + +``` diff --git a/docs/pages/components/menu-item.md b/docs/pages/components/menu-item.md new file mode 100644 index 0000000..b509884 --- /dev/null +++ b/docs/pages/components/menu-item.md @@ -0,0 +1,274 @@ +--- +meta: + title: Menu Item + description: Menu items provide options for the user to pick from in a menu. +layout: component +--- + +```html:preview + + Option 1 + Option 2 + Option 3 + + Checkbox + Disabled + + + Prefix Icon + + + + Suffix Icon + + + +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Option 1 + Option 2 + Option 3 + + + Checkbox + + Disabled + + + Prefix Icon + + + + Suffix Icon + + + +); +``` + +{% endraw %} + +## Examples + +### Prefix & Suffix + +Add content to the start and end of menu items using the `prefix` and `suffix` slots. + +```html:preview + + + + Home + + + + + Messages + 12 + + + + + + + Settings + + +``` + +{% raw %} + +```jsx:react +import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + + + Home + + + + + Messages + + 12 + + + + + + + + Settings + + +); +``` + +{% endraw %} + +### Disabled + +Add the `disabled` attribute to disable the menu item so it cannot be selected. + +```html:preview + + Option 1 + Option 2 + Option 3 + +``` + +{% raw %} + +```jsx:react +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Option 1 + Option 2 + Option 3 + +); +``` + +{% endraw %} + +### Loading + +Use the `loading` attribute to indicate that a menu item is busy. Like a disabled menu item, clicks will be suppressed until the loading state is removed. + +```html:preview + + Option 1 + Option 2 + Option 3 + +``` + +{% raw %} + +```jsx:react +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Option 1 + Option 2 + Option 3 + +); +``` + +{% endraw %} + +### Checkbox Menu Items + +Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state. + +Checkbox menu items are visually indistinguishable from regular menu items. Their ability to be toggled is primarily inferred from context, much like you'd find in the menu of a native app. + +```html:preview + + Autosave + Check Spelling + Word Wrap + +``` + +{% raw %} + +```jsx:react +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Autosave + + Check Spelling + + Word Wrap + +); +``` + +{% endraw %} + +### Value & Selection + +The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `sl-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more. + +```html:preview + + Option 1 + Option 2 + Option 3 + + Checkbox 4 + Checkbox 5 + Checkbox 6 + + + +``` + +{% raw %} + +```jsx:react +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => { + function handleSelect(event) { + const item = event.detail.item; + + // Toggle checked state + item.checked = !item.checked; + + // Log value + console.log(`Selected value: ${item.value}`); + } + + return ( + + Option 1 + Option 2 + Option 3 + + ); +}; +``` + +{% endraw %} diff --git a/docs/pages/components/menu-label.md b/docs/pages/components/menu-label.md new file mode 100644 index 0000000..2b19037 --- /dev/null +++ b/docs/pages/components/menu-label.md @@ -0,0 +1,45 @@ +--- +meta: + title: Menu Label + description: Menu labels are used to describe a group of menu items. +layout: component +--- + +```html:preview + + Fruits + Apple + Banana + Orange + + Vegetables + Broccoli + Carrot + Zucchini + +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuLabel from '@shoelace-style/shoelace/dist/react/menu-label'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Fruits + Apple + Banana + Orange + + Vegetables + Broccoli + Carrot + Zucchini + +); +``` + +{% endraw %} diff --git a/docs/pages/components/menu.md b/docs/pages/components/menu.md new file mode 100644 index 0000000..dd9c662 --- /dev/null +++ b/docs/pages/components/menu.md @@ -0,0 +1,155 @@ +--- +meta: + title: Menu + description: Menus provide a list of options for the user to choose from. +layout: component +--- + +You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option. + +```html:preview + + Undo + Redo + + Cut + Copy + Paste + Delete + +``` + +{% raw %} + +```jsx:react +import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; +import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; +import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; + +const App = () => ( + + Undo + Redo + + Cut + Copy + Paste + Delete + +); +``` + +{% endraw %} + +:::tip +Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `