Compare commits

..

5 Commits

Author SHA1 Message Date
Cory LaViska
8ee519d40a fix positioning 2021-10-30 15:54:56 -04:00
Cory LaViska
6bc17d48c3 update examples 2021-10-29 18:35:24 -04:00
Cory LaViska
a1263f1b9d add experimental context menu 2021-10-29 18:32:45 -04:00
Cory LaViska
d69ebab765 update examples 2021-10-29 18:32:26 -04:00
Cory LaViska
0504946dac refactor popper creation 2021-10-29 18:32:12 -04:00
577 changed files with 33244 additions and 75532 deletions

View File

@@ -1,4 +1,4 @@
# https://editorconfig.org
# http://editorconfig.org
root = true

View File

@@ -1,10 +0,0 @@
.cache
docs/dist
docs/search.json
docs/**/*.min.js
dist
examples
node_modules
src/react
scripts

View File

@@ -1,211 +0,0 @@
/* 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'
}
};

View File

@@ -3,7 +3,8 @@ name: Bug Report
about: Create a bug report to help us fix a demonstrable problem with code in the library.
title: ''
labels: bug
assignees:
assignees: claviska
---
### Describe the bug
@@ -28,8 +29,8 @@ 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]
- Browser [e.g. Chrome, Firefox, Safari]
- Browser version [e.g. 22]
### Additional information
Provide any additional information about the bug here.

View File

@@ -3,6 +3,8 @@ name: Feature Request
about: Suggest an idea for this project.
title: ''
labels: feature
assignees: claviska
---
### What issue are you having?

2
.github/SECURITY.md vendored
View File

@@ -4,4 +4,4 @@ We take security issues in Shoelace very seriously and appreciate your efforts t
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.
Well respond as soon as possible and keep you updated throughout the process.

View File

@@ -5,26 +5,27 @@ name: Node.js CI
on:
push:
branches: [next]
branches: [ next ]
pull_request:
branches: [next]
branches: [ next ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
node-version: [14.x, 16.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-deps
- run: npm ci
- run: npm run verify
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test

10
.gitignore vendored
View File

@@ -1,9 +1,7 @@
_site
.cache
.DS_Store
.cache
docs/dist
docs/search.json
dist
docs/assets/images/sprite.svg
examples
node_modules
src/react
cdn
web-types.json

View File

@@ -1,28 +0,0 @@
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

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install lint-staged

View File

@@ -1,14 +1,10 @@
*.hbs
*.md
.cache
.github
cspell.json
dist
docs/search.json
docs/*.md
src/components/icon/icons
src/react/index.ts
node_modules
package.json
package-lock.json
tsconfig.json
cdn
_site

View File

@@ -1,9 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-tslint-plugin",
"esbenp.prettier-vscode",
"bierner.lit-html",
"bashmish.es6-string-css",
"streetsidesoftware.code-spell-checker"
"bashmish.es6-string-css"
]
}

View File

@@ -1,7 +1,4 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

View File

@@ -23,7 +23,7 @@ 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.
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14 to build and run the project locally. It is preferred, but not required, to use npm 7.
**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.
@@ -51,7 +51,9 @@ Once you've cloned the repo, run the following command.
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.
This will spin up the Shoelace 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.
The documentation is powered by Docsify, which uses raw markdown files to generate pages. As such, no static files are built for the docs.
### Building

View File

@@ -1,184 +0,0 @@
{
"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",
"mouseup",
"multiselectable",
"nextjs",
"nocheck",
"noopener",
"noreferrer",
"novalidate",
"npmdir",
"Numberish",
"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
}

View File

@@ -1,80 +1,31 @@
import * as path from 'path';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
import { parse } from 'comment-parser';
import { pascalCase } from 'pascal-case';
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import commentParser from 'comment-parser';
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;
}
const noDash = string => string.replace(/^\s?-/, '').trim();
export default {
globs: ['src/components/**/*.component.ts'],
exclude: ['**/*.styles.ts', '**/*.test.ts'],
globs: ['src/components/**/*.ts'],
exclude: ['**/*.test.ts'],
plugins: [
// Append package data
{
name: 'shoelace-package-data',
packageLinkPhase({ customElementsManifest }) {
packageLinkPhase({ customElementsManifest, context }) {
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 }) {
analyzePhase({ ts, node, moduleDoc, context }) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: {
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'];
const customTags = ['animation', 'dependency', 'since', 'status'];
let customComments = '/**';
node.jsDoc?.forEach(jsDoc => {
@@ -87,11 +38,8 @@ export default {
});
});
// 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 => {
const parsed = commentParser.parse(customComments + '\n */');
parsed[0].tags?.map(t => {
switch (t.tag) {
// Animations
case 'animation':
@@ -113,10 +61,8 @@ export default {
break;
// Value-only metadata tags
case 'documentation':
case 'since':
case 'status':
case 'title':
classDoc[t.tag] = t.name;
break;
@@ -133,83 +79,8 @@ export default {
});
}
});
}
}
}
},
{
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({
excludeCss: true,
referencesTemplate: (_, tag) => {
return {
name: 'Documentation',
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
};
}
})
}
]
};

5
docs/404.md Normal file
View File

@@ -0,0 +1,5 @@
# Not Found
<img class="not-found-image" src="/assets/images/undraw-not-found.svg" alt="Cute monsters hiding behind a tree">
Sorry, I couldn't find that page. Have you tried pressing <kbd>/</kbd> to search?

View File

@@ -1,349 +0,0 @@
{% 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 #}
<header class="component-header">
<h1>{{ component.name | classNameToComponentName }}</h1>
<div class="component-header__tag">
<code>&lt;{{ component.tagName }}&gt; | {{ component.name }}</code>
</div>
<div class="component-header__info">
<sl-badge variant="neutral" pill>
Since {{component.since or '?' }}
</sl-badge>
<sl-badge variant="{{ badgeVariant }}" pill style="text-transform: capitalize;">
{{ component.status }}
</sl-badge>
</div>
</header>
<p class="component-summary">
{% if component.summary %}
{{ component.summary | markdownInline | safe }}
{% endif %}
</p>
{# Markdown content #}
{{ content | safe }}
{# Importing #}
<h2>Importing</h2>
<p>
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 <a href="/getting-started/installation#cherry-picking">cherry pick</a> this component.
</p>
<sl-tab-group>
<sl-tab slot="nav" panel="script">Script</sl-tab>
<sl-tab slot="nav" panel="import">Import</sl-tab>
<sl-tab slot="nav" panel="bundler">Bundler</sl-tab>
<sl-tab slot="nav" panel="react">React</sl-tab>
<sl-tab-panel name="script">
<p>
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
using a script tag:
</p>
<pre><code class="language-html">&lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}&quot;&gt;&lt;/script&gt;</code></pre>
</sl-tab-panel>
<sl-tab-panel name="import">
<p>
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
using a JavaScript import:
</p>
<pre><code class="language-js">import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}';</code></pre>
</sl-tab-panel>
<sl-tab-panel name="bundler">
<p>
To import this component using <a href="{{ rootUrl('/getting-started/installation#bundling') }}">a bundler</a>:
</p>
<pre><code class="language-js">import '@shoelace-style/shoelace/{{ meta.npmdir }}/{{ component.path }}';</code></pre>
</sl-tab-panel>
<sl-tab-panel name="react">
<p>
To import this component as a <a href="/frameworks/react">React component</a>:
</p>
<pre><code class="language-js">import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';</code></pre>
</sl-tab-panel>
</sl-tab-group>
{# Slots #}
{% if component.slots.length %}
<h2>Slots</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
</tr>
</thead>
<tbody>
{% for slot in component.slots %}
<tr>
<td class="nowrap">
{% if slot.name %}
<code>{{ slot.name }}</code>
{% else %}
(default)
{% endif %}
</td>
<td>{{ slot.description | markdownInline | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#slots') }}">using slots</a>.</em></p>
{% endif %}
{# Properties #}
{% if component.properties.length %}
<h2>Properties</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-reflects">Reflects</th>
<th class="table-type">Type</th>
<th class="table-default">Default</th>
</tr>
</thead>
<tbody>
{% for prop in component.properties %}
<tr>
<td>
<code class="nowrap">{{ prop.name }}</code>
{% if prop.attribute | length > 0 %}
{% if prop.attribute != prop.name %}
<br>
<sl-tooltip content="This attribute is different from its property">
<small>
<code class="nowrap">
{{ prop.attribute }}
</code>
</small>
</sl-tooltip>
{% endif %}
{% endif %}
</td>
<td>
{{ prop.description | markdownInline | safe }}
</td>
<td style="text-align: center;">
{% if prop.reflects %}
<sl-icon label="yes" name="check-lg"></sl-icon>
{% endif %}
</td>
<td>
{% if prop.type.text %}
<code>{{ prop.type.text | markdownInline | safe }}</code>
{% else %}
-
{% endif %}
</td>
<td>
{% if prop.default %}
<code>{{ prop.default | markdownInline | safe }}</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
<tr>
<td class="nowrap"><code>updateComplete</code></td>
<td>
A read-only promise that resolves when the component has
<a href="/getting-started/usage?#component-rendering-and-updating">finished updating</a>.
</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
{% endif %}
{# Events #}
{% if component.events.length %}
<h2>Events</h2>
<table>
<thead>
<tr>
<th class="table-name" data-flavor="html">Name</th>
<th class="table-name" data-flavor="react">React Event</th>
<th class="table-description">Description</th>
<th class="table-event-detail">Event Detail</th>
</tr>
</thead>
<tbody>
{% for event in component.events %}
<tr>
<td data-flavor="html"><code class="nowrap">{{ event.name }}</code></td>
<td data-flavor="react"><code class="nowrap">{{ event.reactName }}</code></td>
<td>{{ event.description | markdownInline | safe }}</td>
<td>
{% if event.type.text %}
<code>{{ event.type.text }}</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#events') }}">events</a>.</em></p>
{% endif %}
{# Methods #}
{% if component.methods.length %}
<h2>Methods</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-arguments">Arguments</th>
</tr>
</thead>
<tbody>
{% for method in component.methods %}
<tr>
<td class="nowrap"><code>{{ method.name }}()</code></td>
<td>{{ method.description | markdownInline | safe }}</td>
<td>
{% if method.parameters.length %}
<code>
{% for param in method.parameters %}
{{ param.name }}: {{ param.type.text }}{% if not loop.last %},{% endif %}
{% endfor %}
</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#methods') }}">methods</a>.</em></p>
{% endif %}
{# Custom Properties #}
{% if component.cssProperties.length %}
<h2>Custom Properties</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-default">Default</th>
</tr>
</thead>
<tbody>
{% for cssProperty in component.cssProperties %}
<tr>
<td class="nowrap"><code>{{ cssProperty.name }}</code></td>
<td>{{ cssProperty.description | markdownInline | safe }}</td>
<td>{{ cssProperty.default }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#custom-properties') }}">customizing CSS custom properties</a>.</em></p>
{% endif %}
{# CSS Parts #}
{% if component.cssParts.length %}
<h2>Parts</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
</tr>
</thead>
<tbody>
{% for cssPart in component.cssParts %}
<tr>
<td class="nowrap"><code>{{ cssPart.name }}</code></td>
<td>{{ cssPart.description | markdownInline | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
{% endif %}
{# Animations #}
{% if component.animations.length %}
<h2>Animations</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
</tr>
</thead>
<tbody>
{% for animation in component.animations %}
<tr>
<td class="nowrap"><code>{{ animation.name }}</code></td>
<td>{{ animation.description | markdownInline | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
{% endif %}
{# Dependencies #}
{% if component.dependencies.length %}
<h2>Dependencies</h2>
<p>This component automatically imports the following dependencies.</p>
<ul>
{% for dependency in component.dependencies %}
<li><code>&lt;{{ dependency }}&gt;</code></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -1,150 +0,0 @@
<!DOCTYPE html>
<html
lang="en"
data-layout="{{ layout }}"
data-shoelace-version="{{ meta.version }}"
>
<head>
{# Metadata #}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ meta.description }}" />
<title>{{ meta.title }}</title>
{# Opt out of Turbo caching #}
<meta name="turbo-cache-control">
{# Stylesheets #}
<link rel="stylesheet" href="{{ assetUrl('styles/docs.css') }}" />
<link rel="stylesheet" href="{{ assetUrl('styles/code-previews.css') }}" />
<link rel="stylesheet" href="{{ assetUrl('styles/search.css') }}" />
{# Favicons #}
<link rel="icon" href="{{ assetUrl('images/logo.svg') }}" type="image/x-icon" />
{# Twitter Cards #}
<meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content="shoelace_style" />
<meta name="twitter:image" content="{{ assetUrl(meta.image, true) }}" />
{# OpenGraph #}
<meta property="og:url" content="{{ rootUrl(page.url, true) }}" />
<meta property="og:title" content="{{ meta.title }}" />
<meta property="og:description" content="{{ meta.description }}" />
<meta property="og:image" content="{{ assetUrl(meta.image, true) }}" />
{# Shoelace #}
<link rel="stylesheet" href="/dist/themes/light.css" />
<link rel="stylesheet" href="/dist/themes/dark.css" />
<script type="module" src="/dist/shoelace-autoloader.js"></script>
{# Set the initial theme and menu states here to prevent flashing #}
<script>
(() => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = localStorage.getItem('theme') || 'auto';
document.documentElement.classList.toggle('sl-theme-dark', theme === 'dark' || (theme === 'auto' && prefersDark));
})();
</script>
{# Turbo + Scroll positioning #}
<script src="{{ assetUrl('scripts/turbo.js') }}" type="module"></script>
<script src="{{ assetUrl('scripts/docs.js') }}" defer></script>
<script src="{{ assetUrl('scripts/code-previews.js') }}" defer></script>
<script src="{{ assetUrl('scripts/lunr.js') }}" defer></script>
<script src="{{ assetUrl('scripts/search.js') }}" defer></script>
</head>
<body>
<a id="skip-to-main" class="visually-hidden" href="#main-content" data-smooth-link="false">
Skip to main content
</a>
{# Menu toggle #}
<button id="menu-toggle" type="button" aria-label="Menu">
<svg width="148" height="148" viewBox="0 0 148 148" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="18" fill="none" fill-rule="evenodd" stroke-linecap="round">
<path d="M9.5 125.5h129M9.5 74.5h129M9.5 23.5h129"></path>
</g>
</svg>
</button>
{# Icon toolbar #}
<div id="icon-toolbar">
{# GitHub #}
<a href="https://github.com/shoelace-style/shoelace" title="View Shoelace on GitHub">
<sl-icon name="github"></sl-icon>
</a>
{# Twitter #}
<a href="https://twitter.com/shoelace_style" title="Follow Shoelace on Twitter">
<sl-icon name="twitter"></sl-icon>
</a>
{# Theme selector #}
<sl-dropdown id="theme-selector" placement="bottom-end" distance="3">
<sl-button slot="trigger" size="small" variant="text" caret title="Press \ to toggle">
<sl-icon class="only-light" name="sun-fill"></sl-icon>
<sl-icon class="only-dark" name="moon-fill"></sl-icon>
</sl-button>
<sl-menu>
<sl-menu-item type="checkbox" value="light">Light</sl-menu-item>
<sl-menu-item type="checkbox" value="dark">Dark</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item type="checkbox" value="auto">System</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<aside id="sidebar" data-preserve-scroll>
<header>
<a href="/">
<img src="{{ assetUrl('images/wordmark.svg') }}" alt="Shoelace" />
</a>
<div class="sidebar-version">
{{ meta.version }}
</div>
</header>
<div class="sidebar-buttons">
<sl-button size="small" class="repo-button repo-button--github" href="https://github.com/shoelace-style/shoelace" target="_blank">
<sl-icon slot="prefix" name="github"></sl-icon> Code
</sl-button>
<sl-button size="small" class="repo-button repo-button--star" href="https://github.com/shoelace-style/shoelace/stargazers" target="_blank">
<sl-icon slot="prefix" name="star-fill"></sl-icon> Star
</sl-button>
<sl-button size="small" class="repo-button repo-button--twitter" href="https://twitter.com/shoelace_style" target="_blank">
<sl-icon slot="prefix" name="twitter"></sl-icon> Follow
</sl-button>
</div>
<button class="search-box" type="button" title="Press / to search" aria-label="Search" data-plugin="search">
<sl-icon name="search"></sl-icon>
<span>Search</span>
</button>
<nav>
{% include 'sidebar.njk' %}
</nav>
</aside>
{# Content #}
<main>
<a id="main-content"></a>
<article id="content" class="content{% if toc %} content--with-toc{% endif %}">
{% if toc %}
<div class="content__toc">
<ul>
<li class="top"><a href="#">{{ meta.title }}</a></li>
</ul>
</div>
{% endif %}
<div class="content__body">
{% block content %}
{{ content | safe }}
{% endblock %}
</div>
</article>
</main>
</body>
</html>

View File

@@ -1,65 +0,0 @@
<ul>
<li>
<h2>Getting Started</h2>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/getting-started/installation">Installation</a></li>
<li><a href="/getting-started/usage">Usage</a></li>
<li><a href="/getting-started/themes">Themes</a></li>
<li><a href="/getting-started/customizing">Customizing</a></li>
<li><a href="/getting-started/form-controls">Form Controls</a></li>
<li><a href="/getting-started/localization">Localization</a></li>
</ul>
</li>
<li>
<h2>Frameworks</h2>
<ul>
<li><a href="/frameworks/react">React</a></li>
<li><a href="/frameworks/vue">Vue</a></li>
<li><a href="/frameworks/angular">Angular</a></li>
</ul>
</li>
<li>
<h2>Resources</h2>
<ul>
<li><a href="/resources/community">Community</a></li>
<li><a href="https://github.com/shoelace-style/shoelace/discussions">Help &amp; Support</a></li>
<li><a href="/resources/accessibility">Accessibility</a></li>
<li><a href="/resources/contributing">Contributing</a></li>
<li><a href="/resources/changelog">Changelog</a></li>
</ul>
</li>
<li>
<h2>Components</h2>
<ul>
{% for component in meta.components %}
<li>
<a href="/components/{{ component.tagName | removeSlPrefix }}">
{{ component.name | classNameToComponentName }}
</a>
</li>
{% endfor %}
</ul>
</li>
<li>
<h2>Design Tokens</h2>
<ul>
<li><a href="/tokens/typography">Typography</a></li>
<li><a href="/tokens/color">Color</a></li>
<li><a href="/tokens/spacing">Spacing</a></li>
<li><a href="/tokens/elevation">Elevation</a></li>
<li><a href="/tokens/border-radius">Border Radius</a></li>
<li><a href="/tokens/transition">Transition</a></li>
<li><a href="/tokens/z-index">Z-index</a></li>
<li><a href="/tokens/more">More Tokens</a></li>
</ul>
</li>
<li>
<h2>Tutorials</h2>
<ul>
<li><a href="/tutorials/integrating-with-laravel">Integrating with Laravel</a></li>
<li><a href="/tutorials/integrating-with-nextjs">Integrating with NextJS</a></li>
<li><a href="/tutorials/integrating-with-rails">Integrating with Rails</a></li>
</ul>
</li>
</ul>

81
docs/_sidebar.md Normal file
View File

@@ -0,0 +1,81 @@
- Getting Started
- [Overview](/)
- [Installation](/getting-started/installation)
- [Usage](/getting-started/usage)
- [Themes](/getting-started/themes)
- [Customizing](/getting-started/customizing)
- Resources
- [Community](/resources/community)
- [Contributing](/resources/contributing)
- [Changelog](/resources/changelog)
- Components
- [Alert](/components/alert)
- [Avatar](/components/avatar)
- [Badge](/components/badge)
- [Breadcrumb](/components/breadcrumb)
- [Breadcrumb Item](/components/breadcrumb-item)
- [Button](/components/button)
- [Button Group](/components/button-group)
- [Card](/components/card)
- [Checkbox](/components/checkbox)
- [Color Picker](/components/color-picker)
- [Context Menu](/components/context-menu)
- [Details](/components/details)
- [Dialog](/components/dialog)
- [Divider](/components/divider)
- [Drawer](/components/drawer)
- [Dropdown](/components/dropdown)
- [Form](/components/form)
- [Icon](/components/icon)
- [Icon Button](/components/icon-button)
- [Image Comparer](/components/image-comparer)
- [Input](/components/input)
- [Menu](/components/menu)
- [Menu Item](/components/menu-item)
- [Menu Label](/components/menu-label)
- [Progress Bar](/components/progress-bar)
- [Progress Ring](/components/progress-ring)
- [QR Code](/components/qr-code)
- [Radio](/components/radio)
- [Radio Group](/components/radio-group)
- [Range](/components/range)
- [Rating](/components/rating)
- [Select](/components/select)
- [Skeleton](/components/skeleton)
- [Spinner](/components/spinner)
- [Switch](/components/switch)
- [Tab Group](/components/tab-group)
- [Tab](/components/tab)
- [Tab Panel](/components/tab-panel)
- [Tag](/components/tag)
- [Textarea](/components/textarea)
- [Tooltip](/components/tooltip)
<!--plop:component-->
- Utilities
- [Animated Image](/components/animated-image)
- [Animation](/components/animation)
- [Format Bytes](/components/format-bytes)
- [Format Date](/components/format-date)
- [Format Number](/components/format-number)
- [Include](/components/include)
- [Mutation Observer](/components/mutation-observer)
- [Relative Time](/components/relative-time)
- [Resize Observer](/components/resize-observer)
- [Responsive Media](/components/responsive-media)
- Design Tokens
- [Typography](/tokens/typography)
- [Color](/tokens/color)
- [Spacing](/tokens/spacing)
- [Elevation](/tokens/elevation)
- [Border Radius](/tokens/border-radius)
- [Transition](/tokens/transition)
- [Z-index](/tokens/z-index)
- Tutorials
- [Integrating with Laravel](/tutorials/integrating-with-laravel)
- [Integrating with NextJS](/tutorials/integrating-with-nextjs)
- [Integrating with Rails](/tutorials/integrating-with-rails)

View File

@@ -1,35 +0,0 @@
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;
};

View File

@@ -1,64 +0,0 @@
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;
};

View File

@@ -1,71 +0,0 @@
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;
});
};

View File

@@ -1,138 +0,0 @@
let count = 1;
function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* 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 = `
<button type="button"
title="Show HTML code"
class="code-preview__button code-preview__button--html"
>
HTML
</button>
`;
const reactButton = `
<button type="button" title="Show React code" class="code-preview__button code-preview__button--react">
React
</button>
`;
const codePenButton = `
<button type="button" class="code-preview__button code-preview__button--codepen" title="Edit on CodePen">
<svg
width="138"
height="26"
viewBox="0 0 138 26"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
</svg>
</button>
`;
const codePreview = `
<div class="code-preview ${isExpanded ? 'code-preview--expanded' : ''}">
<div class="code-preview__preview">
${code.textContent}
<div class="code-preview__resizer">
<sl-icon name="grip-vertical"></sl-icon>
</div>
</div>
<div class="code-preview__source-group" id="${sourceGroupId}">
<div class="code-preview__source code-preview__source--html" ${reactCode ? 'data-flavor="html"' : ''}>
<pre><code class="language-html">${escapeHtml(code.textContent)}</code></pre>
</div>
${
reactCode
? `
<div class="code-preview__source code-preview__source--react" data-flavor="react">
<pre><code class="language-jsx">${escapeHtml(reactCode.textContent)}</code></pre>
</div>
`
: ''
}
</div>
<div class="code-preview__buttons">
<button
type="button"
class="code-preview__button code-preview__toggle"
aria-expanded="${isExpanded ? 'true' : 'false'}"
aria-controls="${sourceGroupId}"
>
Source
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
${reactCode ? ` ${htmlButton} ${reactButton} ` : ''}
${noCodePen ? '' : codePenButton}
</div>
</div>
`;
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;
};

View File

@@ -1,23 +0,0 @@
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;
};

View File

@@ -1,41 +0,0 @@
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;
};

View File

@@ -1,63 +0,0 @@
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 `<pre>`. For example, a code field
* tagged with "html:preview" will be rendered as `<pre class="language-html preview">`.
*
* 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;
};

View File

@@ -1,67 +0,0 @@
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 `<div role="alert" class="callout callout--${type}">`;
}
return '</div>\n';
}
});
});
// Asides
markdown.use(markdownItContainer, 'aside', {
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
return `<aside>`;
}
return '</aside>\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 `<details>\n<summary><span>${markdown.utils.escapeHtml(m[1])}</span></summary>\n`;
}
return '</details>\n';
}
});
// Replace [#1234] with a link to GitHub issues
markdownItReplaceIt.replacements.push({
name: 'github-issues',
re: /\[#([0-9]+)\]/gs,
sub: '<a href="https://github.com/shoelace-style/shoelace/issues/$1">#$1</a>',
html: true,
default: true
});
module.exports = markdown;

View File

@@ -1,26 +0,0 @@
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);
};

View File

@@ -1,19 +0,0 @@
/**
* @typedef {object} Replacement
* @property {string | RegExp} pattern
* @property {string} replacement
*/
/**
* @typedef {Array<Replacement>} Replacements
*/
/**
* @param {Document} content
* @param {Replacements} replacements
*/
module.exports = function (content, replacements) {
replacements.forEach(replacement => {
content.body.innerHTML = content.body.innerHTML.replaceAll(replacement.pattern, replacement.replacement);
});
};

View File

@@ -1,21 +0,0 @@
/**
* 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;
};

View File

@@ -1,16 +0,0 @@
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;
};

View File

@@ -1,42 +0,0 @@
/**
* 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 <li>
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;
};

View File

@@ -1,23 +0,0 @@
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;
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 688 KiB

View File

@@ -1,11 +0,0 @@
<svg width="733" xmlns="http://www.w3.org/2000/svg" height="733">
<circle cy="366.5" cx="366.5" r="366.5"/>
<circle cy="366.5" cx="366.5" r="336.5" fill="#fede58"/>
<path d="M325 665c-121-21-194-115-212-233v-8l-25-1-1-18h481c6 13 10 27 13 41 13 94-38 146-114 193-45 23-93 29-142 26z"/>
<path d="M372 647c52-6 98-28 138-62 28-25 46-56 51-87 4-20 1-57-5-70l-423-1c-2 56 39 118 74 157 31 34 72 54 116 63 11 2 38 2 49 0z" fill="#871945"/>
<path d="M76 342c-13-26-13-57-9-85 6-27 18-52 35-68 21-20 50-23 77-18 15 4 28 12 39 23 18 17 30 40 36 67 4 20 4 41 0 60l-6 21z"/>
<path d="M234 323c5-6 6-40 2-58-3-16-4-16-10-10-14 14-38 14-52 0-15-18-12-41 6-55 3-3 5-5 5-6-1-4-22-8-34-7-42 4-57.6 40-66.2 77-3 17-1 53 4 59H234z" fill="#fff"/>
<path d="M378 343c-2-3-6-20-7-29-5-28-1-57 11-83 15-30 41-52 72-60 29-7 57 0 82 15 26 17 45 49 50 82 2 12 2 33 0 45-1 10-5 26-8 30z"/>
<path d="M565 324c4-5 5-34 4-50-2-14-6-24-8-24-1 0-3 2-6 5-17 17-47 13-58-9-7-16-4-31 8-43 4-4 7-8 7-9 0 0-4-2-8-3-51-17-105 20-115 80-3 15 0 43 3 53z" fill="#fff"/>
<path d="M504 590s-46 40-105 53c-66 15-114-7-114-7s14-76 93-95c76-18 126 49 126 49z" fill="#f9bedd"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,23 +0,0 @@
<svg width="160" height="45" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<rect x="2" y="2" width="156" height="40" rx="16" fill="#F9F9F9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.425 11.174c.604 1.114.233 2.53-.83 3.164l-6.986 4.166a.378.378 0 00-.18.325v6.748c0 .134.069.258.18.325l5.714 3.407c.11.066.244.066.354 0l5.714-3.407a.378.378 0 00.18-.325V21.29l-4.986 2.936c-1.067.628-2.416.231-3.015-.886-.6-1.118-.22-2.532.846-3.16l7.008-4.127c2.048-1.206 4.576.345 4.576 2.806v6.718c0 1.803-.924 3.467-2.42 4.36l-5.713 3.407a4.596 4.596 0 01-4.734 0l-5.714-3.408C18.924 29.044 18 27.38 18 25.576V18.83c0-1.803.924-3.467 2.42-4.36l6.985-4.165c1.063-.634 2.415-.245 3.02.87z" fill="url(#paint0_linear)"/>
<path fill="#F9F9F9" d="M47 12.5h95v-1H47z"/>
<path d="M52.538 27.752c2.744 0 4.844-1.876 4.844-5.152 0-3.108-2.1-5.152-4.844-5.152s-4.802 2.002-4.802 5.152c0 3.29 2.058 5.152 4.802 5.152zm0-1.554c-1.736 0-2.912-1.316-2.912-3.598 0-2.226 1.162-3.598 2.912-3.598s2.954 1.4 2.954 3.598c0 2.31-1.218 3.598-2.954 3.598zm7.89 4.158V27.22c0-.196-.013-.378-.055-.658.434.7 1.022 1.19 2.17 1.19 1.736 0 3.066-1.414 3.066-3.626 0-2.17-1.19-3.682-2.996-3.682-1.078 0-1.806.476-2.24 1.204.042-.28.056-.462.056-.672v-.308H58.72v9.688h1.708zm1.695-3.948c-1.036 0-1.764-.938-1.764-2.31 0-1.414.742-2.296 1.764-2.296 1.092 0 1.75.952 1.75 2.296 0 1.372-.7 2.31-1.75 2.31zm7.866 1.344c1.848 0 3.052-1.078 3.192-2.478h-1.736c-.112.714-.714 1.134-1.456 1.134-1.036 0-1.722-.826-1.736-1.904h4.97v-.378c0-2.226-1.204-3.682-3.29-3.682-1.988 0-3.43 1.47-3.43 3.626 0 2.366 1.442 3.682 3.486 3.682zm-1.75-4.48c.098-.896.756-1.554 1.68-1.554.924 0 1.526.63 1.554 1.554H68.24zm8.006 4.228v-4.004c0-.952.616-1.694 1.456-1.694.798 0 1.288.63 1.288 1.638v4.06h1.708v-4.312c0-1.68-.896-2.744-2.408-2.744-1.078 0-1.722.518-2.1 1.204.042-.266.056-.476.056-.672v-.308h-1.708V27.5h1.708zm8.911-7.868h1.792V17.84h-1.792v1.792zm.042 1.036V27.5h1.708v-6.832h-1.708zm5.097 6.832v-4.004c0-.952.616-1.694 1.456-1.694.798 0 1.288.63 1.288 1.638v4.06h1.708v-4.312c0-1.68-.896-2.744-2.408-2.744-1.078 0-1.722.518-2.1 1.204.042-.266.056-.476.056-.672v-.308h-1.708V27.5h1.708zm13.238.252c1.526 0 2.52-.658 2.982-1.54-.07.322-.098.644-.098.98v.308h1.68v-5.222h-4.34v1.554h2.66v.07c0 1.4-1.134 2.296-2.59 2.296-1.792 0-3.024-1.372-3.024-3.598s1.26-3.598 3.066-3.598c1.302 0 2.17.756 2.296 1.736h1.89c-.182-1.904-1.764-3.29-4.214-3.29-2.954 0-4.928 2.128-4.928 5.152 0 3.136 1.848 5.152 4.62 5.152zm6.063-8.12h1.792V17.84h-1.792v1.792zm.042 1.036V27.5h1.708v-6.832h-1.708zm6.413 6.958c.434 0 .84-.07 1.008-.126v-1.288c-.168.028-.35.042-.518.042-.728 0-1.008-.42-1.008-1.134v-3.094h1.68v-1.358h-1.68v-2.464h-1.708v2.464h-1.554v1.358h1.554v3.346c0 1.526.77 2.254 2.226 2.254zm3.961 2.73V27.22c0-.196-.014-.378-.056-.658.434.7 1.022 1.19 2.17 1.19 1.736 0 3.066-1.414 3.066-3.626 0-2.17-1.19-3.682-2.996-3.682-1.078 0-1.806.476-2.24 1.204.042-.28.056-.462.056-.672v-.308h-1.708v9.688h1.708zm1.694-3.948c-1.036 0-1.764-.938-1.764-2.31 0-1.414.742-2.296 1.764-2.296 1.092 0 1.75.952 1.75 2.296 0 1.372-.7 2.31-1.75 2.31zm7.88 1.344c2.058 0 3.514-1.372 3.514-3.64 0-2.24-1.456-3.668-3.514-3.668-2.044 0-3.5 1.428-3.5 3.668 0 2.268 1.442 3.64 3.5 3.64zm0-1.344c-1.064 0-1.764-.84-1.764-2.296 0-1.484.728-2.31 1.764-2.31 1.05 0 1.778.826 1.778 2.31 0 1.456-.714 2.296-1.778 2.296zm7.551 1.344c1.26 0 1.876-.686 2.142-1.19-.056.238-.056.42-.056.658v.28h1.708v-9.8h-1.708v3.276c0 .21 0 .42.056.672-.392-.658-1.05-1.204-2.114-1.204-1.596 0-3.15 1.218-3.15 3.668 0 2.408 1.316 3.64 3.122 3.64zm.406-1.344c-1.022 0-1.792-.896-1.792-2.31 0-1.358.77-2.296 1.792-2.296s1.778.896 1.778 2.296c0 1.372-.756 2.31-1.778 2.31z" fill="#12100C"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="33.806" y1="13.629" x2="22.389" y2="30.86" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFB45B"/>
<stop offset="1" stop-color="#FF8A00"/>
</linearGradient>
<filter id="filter0_d" x="0" y=".5" width="160" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy=".5"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -41,7 +41,7 @@
<ellipse id="Oval" fill="#2F2E41" fill-rule="nonzero" cx="103.34581" cy="426.732" rx="10.90314" ry="4.08868"></ellipse>
<circle id="Oval" fill="#FFFFFF" fill-rule="nonzero" cx="86.99113" cy="358.86008" r="14.71922"></circle>
<circle id="Oval" fill="#3F3D56" fill-rule="nonzero" cx="86.99113" cy="358.86008" r="4.90642"></circle>
<path d="M44.12401,329.71183 C40.64653,314.13804 51.76268,298.4014 68.95262,294.56302 C86.14256,290.72464 102.89683,300.23813 106.37431,315.81192 C109.85179,331.38571 98.45939,337.12961 81.26945,340.96792 C64.07951,344.80623 47.60154,345.28568 44.12401,329.71183 Z" id="Path" fill="#12a5e9" fill-rule="nonzero"></path>
<path d="M44.12401,329.71183 C40.64653,314.13804 51.76268,298.4014 68.95262,294.56302 C86.14256,290.72464 102.89683,300.23813 106.37431,315.81192 C109.85179,331.38571 98.45939,337.12961 81.26945,340.96792 C64.07951,344.80623 47.60154,345.28568 44.12401,329.71183 Z" id="Path" fill="#E6E6E6" fill-rule="nonzero"></path>
<ellipse id="Oval" fill="#2F2E41" fill-rule="nonzero" transform="translate(110.988725, 213.490755) rotate(-69.082170) translate(-110.988725, -213.490755) " cx="110.988725" cy="213.490755" rx="21.53369" ry="6.76007"></ellipse>
<circle id="Oval" fill="#2F2E41" fill-rule="nonzero" transform="translate(71.181568, 246.012788) rotate(-80.782520) translate(-71.181568, -246.012788) " cx="71.1815681" cy="246.012788" r="43.06735"></circle>
<rect id="Rectangle" fill="#2F2E41" fill-rule="nonzero" x="51.55595" y="279.81244" width="13.08374" height="23.44171"></rect>
@@ -53,7 +53,7 @@
<path d="M29.40483,205.96134 C25.92735,190.38755 37.0435,174.65088 54.23344,170.8125 C71.42338,166.97412 88.17766,176.48758 91.65513,192.06138 C95.1326,207.63518 83.74022,213.37903 66.55028,217.21738 C49.36034,221.05573 32.88231,221.53519 29.40483,205.96134 Z" id="Path" fill="#0ea5e9" fill-rule="nonzero"></path>
<ellipse id="Oval" fill="#2F2E41" fill-rule="nonzero" transform="translate(22.673401, 226.029366) rotate(-64.625740) translate(-22.673401, -226.029366) " cx="22.6734006" cy="226.029366" rx="6.76007" ry="21.53368"></ellipse>
<path d="M50.02702,261.54972 C50.02702,265.76487 60.88029,274.08829 72.92357,274.08829 C84.96685,274.08829 96.25871,262.22129 96.25871,258.0062 C96.25871,253.79111 84.96678,258.82395 72.92357,258.82395 C60.88036,258.82395 50.02702,257.33457 50.02702,261.54972 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M113.52376,81.56869 C111.867655,81.5704977 110.525568,82.9125851 110.52376,84.56869 L110.52376,208.56869 C110.525568,210.224795 111.867655,211.566882 113.52376,211.56869 L400.52376,211.56869 C402.179865,211.566882 403.521952,210.224795 403.52376,208.56869 L403.52376,84.56869 C403.521952,82.9125851 402.179865,81.5704977 400.52376,81.56869 L113.52376,81.56869 Z" id="Path" fill="#fbc024" fill-rule="nonzero"></path>
<path d="M113.52376,81.56869 C111.867655,81.5704977 110.525568,82.9125851 110.52376,84.56869 L110.52376,208.56869 C110.525568,210.224795 111.867655,211.566882 113.52376,211.56869 L400.52376,211.56869 C402.179865,211.566882 403.521952,210.224795 403.52376,208.56869 L403.52376,84.56869 C403.521952,82.9125851 402.179865,81.5704977 400.52376,81.56869 L113.52376,81.56869 Z" id="Path" fill="#0ea5e9" fill-rule="nonzero"></path>
<circle id="Oval" fill="#FFFFFF" fill-rule="nonzero" cx="191.01816" cy="146.56869" r="29.1211"></circle>
<path d="M256.7436,142.69417 C254.884983,142.724723 253.39428,144.240132 253.39428,146.099 C253.39428,147.957868 254.884983,149.473277 256.7436,149.50383 L348.68926,149.50383 C349.906316,149.524778 351.042001,148.894505 351.668119,147.850647 C352.294237,146.806789 352.31556,145.508108 351.72405,144.444258 C351.132539,143.380407 350.018157,142.713189 348.80107,142.69417 C348.763797,142.693537 348.726527,142.693537 348.68926,142.69417 L256.7436,142.69417 Z" id="b71acdfd-6a55-428e-917a-53f192cb0203" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M256.7436,122.96697 C254.884983,122.997523 253.39428,124.512932 253.39428,126.3718 C253.39428,128.230668 254.884983,129.746077 256.7436,129.77663 L302.65917,129.77663 C303.876249,129.797613 305.011971,129.167347 305.638111,128.123473 C306.26425,127.0796 306.285572,125.78089 305.694037,124.717025 C305.102503,123.65316 303.988082,122.985951 302.77097,122.96697 C302.733703,122.966337 302.696437,122.966337 302.65917,122.96697 L256.7436,122.96697 Z" id="ad4fbcfa-41b0-45f9-a593-23b6dc3fe165" fill="#FFFFFF" fill-rule="nonzero"></path>
@@ -67,13 +67,13 @@
<circle id="Oval" fill="#3F3D56" fill-rule="nonzero" cx="725.31949" cy="403.22896" r="4.90642"></circle>
<ellipse id="Oval" fill="#2F2E41" fill-rule="nonzero" transform="translate(771.918005, 384.148727) rotate(-53.549900) translate(-771.918005, -384.148727) " cx="771.918005" cy="384.148727" rx="21.53368" ry="6.76007"></ellipse>
<path d="M705.39698,436.33866 C705.39698,432.86466 714.34198,426.00466 724.26778,426.00466 C734.19358,426.00466 743.50006,435.78516 743.50006,439.25914 C743.50006,442.73312 734.19351,438.58514 724.26778,438.58514 C714.34205,438.58514 705.39698,439.81268 705.39698,436.33866 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero" transform="translate(724.448520, 433.325266) rotate(-180.000000) translate(-724.448520, -433.325266) "></path>
<path d="M847.1767,378.54172 L611.43117,345.86064 C607.604361,345.32541 604.932725,341.793834 605.45868,337.96574 L631.96057,146.79396 C632.495641,142.967058 636.027329,140.295337 639.85547,140.82147 L875.60098,173.50256 C879.427885,174.037626 882.099609,177.569318 881.57347,181.39746 L855.0716,372.56924 C854.536375,376.396051 851.004794,379.067687 847.1767,378.54172 L847.1767,378.54172 Z" id="Path" fill="#9358ff" fill-rule="nonzero"></path>
<path d="M847.1767,378.54172 L611.43117,345.86064 C607.604361,345.32541 604.932725,341.793834 605.45868,337.96574 L631.96057,146.79396 C632.495641,142.967058 636.027329,140.295337 639.85547,140.82147 L875.60098,173.50256 C879.427885,174.037626 882.099609,177.569318 881.57347,181.39746 L855.0716,372.56924 C854.536375,376.396051 851.004794,379.067687 847.1767,378.54172 L847.1767,378.54172 Z" id="Path" fill="#0ea5e9" fill-rule="nonzero"></path>
<path d="M762.72231,318.87957 L642.36784,302.19498 C642.216871,302.176045 642.067969,302.14324 641.92302,302.09698 L712.51355,211.39072 C713.394132,210.238632 714.82651,209.649483 716.262854,209.8486 C717.699198,210.047717 718.917295,211.004295 719.45127,212.35248 L748.48059,283.8148 L749.87186,287.23459 L762.72231,318.87957 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<polygon id="Path" fill="#000000" fill-rule="nonzero" opacity="0.2" points="762.722 318.879 721.63 313.183 745.864 286.679 747.609 284.77 748.481 283.815 749.872 287.235"></polygon>
<path d="M829.73481,328.16942 L725.63807,313.73863 L749.87186,287.23463 L751.61612,285.32515 L783.19533,250.78503 C784.29885,249.735613 785.796059,249.204111 787.314261,249.322828 C788.832463,249.441546 790.228856,250.199316 791.15584,251.40751 C791.271244,251.575507 791.375823,251.750689 791.46894,251.93199 L829.73481,328.16942 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<circle id="Oval" fill="#FFFFFF" fill-rule="nonzero" cx="764.18602" cy="224.18353" r="18"></circle>
<rect id="Rectangle" fill="#FFFFFF" fill-rule="nonzero" transform="translate(653.262165, 167.923576) rotate(7.892770) translate(-653.262165, -167.923576) " x="642.262129" y="156.923541" width="22.0000711" height="22.0000711"></rect>
<path d="M768.18655,374.08068 C771.66403,358.50689 760.54788,342.77022 743.35794,338.93184 C726.168,335.09346 709.41373,344.60692 705.93625,360.18071 C702.45877,375.7545 713.85117,381.49837 731.04111,385.33671 C748.23105,389.17505 764.70908,389.6545 768.18655,374.08068 Z" id="Path" fill="#12a5e9" fill-rule="nonzero"></path>
<rect id="Rectangle" fill="#3F3D56" fill-rule="nonzero" transform="translate(653.262165, 167.923576) rotate(7.892770) translate(-653.262165, -167.923576) " x="642.262129" y="156.923541" width="22.0000711" height="22.0000711"></rect>
<path d="M768.18655,374.08068 C771.66403,358.50689 760.54788,342.77022 743.35794,338.93184 C726.168,335.09346 709.41373,344.60692 705.93625,360.18071 C702.45877,375.7545 713.85117,381.49837 731.04111,385.33671 C748.23105,389.17505 764.70908,389.6545 768.18655,374.08068 Z" id="Path" fill="#F2F2F2" fill-rule="nonzero"></path>
<ellipse id="Oval" fill="#2F2E41" fill-rule="nonzero" transform="translate(735.010556, 473.249892) rotate(-4.181640) translate(-735.010556, -473.249892) " cx="735.010556" cy="473.249892" rx="10.90314" ry="4.08868"></ellipse>
</g>
</g>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,32 +0,0 @@
<svg width="673" height="739" viewBox="0 0 673 739" xmlns="http://www.w3.org/2000/svg">
<g fill-rule="nonzero" fill="none">
<path d="M467 149.805c-46.62-7.44-99.71-11.41-155-11.41-50.6 0-99.35 3.32-142.98 9.58.01-.67.02-1.34.05-2.01C171.475 65.082 237.996.903 318.914 1.398c80.917.494 146.649 65.48 148.066 146.387.01.68.02 1.35.02 2.02Z" fill="#0BA5E9"/>
<path d="M337.55 1.342a149.047 149.047 0 0 0-168.93 143.229c-.031.67-.04 1.34-.05 2.01 12.961-1.86 26.384-3.454 40.164-4.784 3.478-71.745 57.64-130.8 128.816-140.455Z" opacity=".1" fill="#FFF"/>
<path d="M532.18 161.625a600.121 600.121 0 0 0-65.2-13.84 943.364 943.364 0 0 0-108.74-10.45 1133.608 1133.608 0 0 0-83.01-.34 973.29 973.29 0 0 0-106.16 8.97 624.292 624.292 0 0 0-77.25 15.66C32.61 177.995 0 199.935 0 223.395s32.61 45.4 91.82 61.77c41.64 11.52 92.98 19.37 148.92 22.97 23.09 1.5 46.96 2.26 71.26 2.26 24.38 0 48.33-.77 71.49-2.27 50.91-3.29 98.01-10.1 137.43-20 .21-.06.41-.11.62-.16 2.66-.66 5.28-1.35 7.87-2.04.93-.26 1.85-.51 2.77-.76a.978.978 0 0 1 .16-.05c.88-.24 1.75-.49 2.62-.73 1.74-.5 3.46-.99 5.15-1.5.08-.02.15-.04.22-.06 1.47-.44 2.91-.88 4.34-1.32 1.17-.37 2.33-.73 3.48-1.1.84-.27 1.67-.54 2.49-.81.6-.2 1.19-.39 1.77-.59.79-.26 1.58-.53 2.36-.8.33-.11.66-.22.98-.34.75-.25 1.48-.51 2.21-.77.79-.28 1.58-.57 2.36-.85.65-.23 1.3-.47 1.94-.71.54-.21 1.07-.41 1.61-.61 1.47-.55 2.91-1.12 4.33-1.68.71-.29 1.42-.57 2.12-.86.69-.28 1.39-.57 2.07-.86 1.12-.47 2.22-.94 3.3-1.41.52-.24 1.05-.47 1.56-.69.39-.18.77-.35 1.16-.53.28-.12.56-.25.83-.38 1.01-.46 2.01-.93 2.99-1.4 3.76-1.8 7.27-3.64 10.53-5.52 20.45-11.71 31.24-24.7 31.24-38.2 0-23.46-32.61-45.4-91.82-61.77Zm-.54 121.62c-41.69 11.53-93.17 19.38-149.26 22.95-22.81 1.45-46.39 2.2-70.38 2.2-23.91 0-47.41-.74-70.15-2.19-56.18-3.56-107.74-11.41-149.49-22.96C34.09 267.125 2 245.875 2 223.395c0-1.986.252-3.965.74-5.89 5.1-20.28 36.47-39.26 89.62-53.96a623.806 623.806 0 0 1 76.66-15.57 976.027 976.027 0 0 1 106.8-9c11.92-.39 23.98-.583 36.18-.58 15.41 0 30.65.31 45.63.91a941.367 941.367 0 0 1 109.37 10.5 598.858 598.858 0 0 1 64.64 13.74c53.14 14.7 84.5 33.67 89.61 53.94.496 1.93.75 3.916.75 5.91 0 22.48-32.09 43.73-90.36 59.85Z" fill="#3F3D56"/>
<path d="M623.43 224.305c0 13.36-11.01 26-30.67 37.29-3.27 1.88-6.79 3.72-10.53 5.52-.98.47-1.98.94-2.99 1.4-.27.13-.55.26-.83.38-.39.18-.77.35-1.16.53-.51.22-1.04.45-1.56.69-1.08.47-2.18.94-3.3 1.41-.68.29-1.38.58-2.07.86-.7.29-1.41.57-2.12.86-1.42.56-2.86 1.13-4.33 1.68-.54.2-1.07.4-1.61.61-.64.24-1.29.48-1.94.71-.78.28-1.57.57-2.36.85-.73.26-1.46.52-2.21.77-.32.12-.65.23-.98.34-.78.27-1.57.54-2.36.8-.58.2-1.17.39-1.77.59-.82.27-1.65.54-2.49.81-1.15.37-2.31.73-3.48 1.1-1.43.44-2.87.88-4.34 1.32-.07.02-.14.04-.22.06-1.69.51-3.41 1-5.15 1.5-.87.24-1.74.49-2.62.73a.978.978 0 0 0-.16.05c-.92.25-1.84.5-2.77.76-2.58.68-5.21 1.37-7.87 2.04-.21.05-.41.1-.62.16-38.35 9.58-85.4 16.56-137.47 19.93-22.81 1.47-46.59 2.25-71.02 2.25-24.65 0-48.63-.79-71.62-2.29-137.24-8.95-239.38-43.03-239.38-83.71.01-2.475.388-4.936 1.12-7.3.06.17.12.33.19.5 14.27 37.48 115.54 67.77 246.94 75.16 20.13 1.14 40.98 1.73 62.32 1.73 21.43 0 42.36-.6 62.57-1.74 131.29-7.42 232.46-37.72 246.68-75.17.24-.6.45-1.2.63-1.8a25.304 25.304 0 0 1 1.55 8.62ZM91.67 213.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217Z" fill="#3F3D56"/>
<path d="M162.67 260.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217ZM531.67 213.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217ZM460.67 260.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217ZM311.67 282.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217Z" fill="#3F3D56"/>
<circle fill="#2F2E41" cx="336.978" cy="450.705" r="42.012"/>
<path fill="#2F2E41" d="m300.555 488.547 20.447-10.24 5.715 11.413-20.447 10.24z"/>
<ellipse fill="#2F2E41" transform="rotate(-56.601 300.086 492.946)" cx="300.086" cy="492.946" rx="3.989" ry="10.636"/>
<path fill="#2F2E41" d="m347.239 489.72 5.715-11.412 20.447 10.24-5.715 11.412z"/>
<ellipse fill="#2F2E41" transform="rotate(-33.399 373.87 492.946)" cx="373.87" cy="492.946" rx="10.636" ry="3.989"/>
<circle fill="#FFF" cx="334.037" cy="440.429" r="14.359"/>
<ellipse fill="#3F3D56" transform="rotate(-45 334.135 434.282)" cx="334.135" cy="434.282" rx="4.766" ry="4.8"/>
<path d="M370.12 405c.632-15.553-12.773-28.727-29.941-29.425-17.168-.697-31.597 11.346-32.229 26.9-.632 15.553 11.302 19.087 28.47 19.785 17.167.697 33.068-1.706 33.7-17.26Z" fill="#0BA5E9"/>
<ellipse fill="#2F2E41" transform="rotate(-40.645 380.654 456.766)" cx="380.654" cy="456.766" rx="6.594" ry="21.006"/>
<ellipse fill="#2F2E41" transform="rotate(-49.355 293.42 456.766)" cx="293.419" cy="456.766" rx="21.006" ry="6.594"/>
<path d="M348.517 467.262a9.572 9.572 0 1 1-18.836 3.428l-.003-.018c-.942-5.202 3.08-7.043 8.282-7.985 5.203-.942 9.615-.628 10.557 4.575Z" fill="#FFF"/>
<path d="M266 495.395a2 2 0 0 1-2-2v-118a2 2 0 0 1 4 0v118a2 2 0 0 1-2 2ZM236 601.395a2 2 0 0 1-2-2v-86a2 2 0 0 1 4 0v86a2 2 0 0 1-2 2ZM313 530.395a2 2 0 0 1-2-2v-118a2 2 0 0 1 4 0v118a2 2 0 0 1-2 2ZM284 615.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM325 369.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM225 390.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM399 395.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM395 545.395a2 2 0 0 1-2-2v-58a2 2 0 0 1 4 0v58a2 2 0 0 1-2 2ZM355 596.395a2 2 0 0 1-2-2v-86a2 2 0 0 1 4 0v86a2 2 0 0 1-2 2ZM363 449.395a2 2 0 0 1-2-2v-118a2 2 0 0 1 4 0v118a2 2 0 0 1-2 2Z" fill="#CCC"/>
<ellipse fill="#2F2E41" transform="rotate(-39.938 594.37 683.981)" cx="594.369" cy="683.981" rx="6.76" ry="21.534"/>
<circle fill="#2F2E41" transform="rotate(-71.565 548.562 676.503)" cx="548.562" cy="676.503" r="43.067"/>
<path fill="#2F2E41" d="M553.707 710.303h13.084v23.442h-13.084zM527.54 710.303h13.084v23.442H527.54z"/>
<ellipse fill="#2F2E41" cx="555.888" cy="734.017" rx="10.903" ry="4.089"/>
<ellipse fill="#2F2E41" cx="529.72" cy="733.472" rx="10.903" ry="4.089"/>
<path d="M535.04 622.366c3.845-15.487 20.82-24.6 37.914-20.356 17.094 4.245 27.834 20.24 23.989 35.727-3.846 15.487-16.604 15.537-33.698 11.293-17.094-4.245-32.051-11.177-28.206-26.664Z" fill="#0BA5E9"/>
<ellipse fill="#2F2E41" transform="rotate(-64.626 500.054 656.52)" cx="500.054" cy="656.52" rx="6.76" ry="21.534"/>
<circle fill="#FFF" cx="542.124" cy="667.416" r="14.359"/>
<circle fill="#3F3D56" cx="536.222" cy="662.269" r="4.786"/>
<circle fill="#FFF" cx="542" cy="697.395" r="6"/>
<path d="M671.531 738.395h-236a1 1 0 1 1 0-2h236a1 1 0 0 1 0 2Z" fill="#3F3D56"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,203 @@
.code-block {
position: relative;
border-radius: 3px;
background-color: rgb(var(--sl-color-neutral-50));
margin-bottom: 1.5rem;
}
.code-block__preview {
position: relative;
border: solid 1px rgb(var(--sl-color-neutral-200));
border-bottom: none;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background-color: rgb(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-block__preview--dragging:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: ew-resize;
}
.code-block__resizer {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1.75rem;
font-size: 20px;
color: rgb(var(--sl-color-neutral-600));
background-color: rgb(var(--sl-color-neutral-0));
border-left: solid 1px rgb(var(--sl-color-neutral-200));
border-top-right-radius: 3px;
cursor: ew-resize;
transition: 250ms background-color;
}
@media screen and (max-width: 600px) {
.code-block__preview {
padding-right: 1.5rem;
}
.code-block__resizer {
display: none;
}
}
.code-block__source {
border: solid 1px rgb(var(--sl-color-neutral-200));
border-bottom: none;
border-radius: 0 !important;
margin: 0;
display: none;
}
.code-block--expanded .code-block__source {
display: block;
}
.code-block__buttons {
position: relative;
border: solid 1px rgb(var(--sl-color-neutral-200));
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
display: flex;
}
.code-block__button {
flex: 0 0 auto;
height: 2.5rem;
min-width: 2.5rem;
border: none;
border-radius: 0;
background: rgb(var(--sl-color-neutral-0));
font: inherit;
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
color: rgb(var(--sl-color-neutral-600));
padding: 0 1rem;
cursor: pointer;
}
.code-block__button:not(:last-of-type) {
border-right: solid 1px rgb(var(--sl-color-neutral-200));
}
.code-block__button--codepen {
display: flex;
place-items: center;
width: 6rem;
}
.code-block__button:first-of-type {
border-bottom-left-radius: 3px;
}
.code-block__button:last-of-type {
border-bottom-right-radius: 3px;
}
.code-block__button:hover,
.code-block__button:active {
box-shadow: 0 0 0 1px rgb(var(--sl-color-primary-400));
border-right-color: transparent;
background-color: rgb(var(--sl-color-primary-50));
color: rgb(var(--sl-color-primary-700));
z-index: 1;
}
.code-block__button:focus-visible {
outline: none;
color: rgb(var(--sl-color-primary-600));
border-color: rgb(var(--sl-color-primary-400));
border-right-color: transparent;
background-color: rgb(var(--sl-color-primary-50));
box-shadow: 0 0 0 1px rgb(var(--sl-color-primary-400)), var(--sl-focus-ring);
z-index: 2;
}
.code-block__toggle {
position: relative;
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
width: 100%;
color: rgb(var(--sl-color-neutral-600));
cursor: pointer;
-webkit-appearance: none;
}
.code-block__toggle svg {
width: 1em;
height: 1em;
margin-left: 0.25rem;
}
.code-block--expanded .code-block__toggle svg {
transform: rotate(180deg);
}
/* Copy button styles */
.markdown-section .docsify-copy-code-button {
top: 4px;
right: 4px;
background-color: rgb(var(--sl-color-neutral-600));
border-radius: var(--sl-border-radius-medium);
font-family: var(--sl-font-sans);
font-size: var(--sl-font-size-x-small);
font-weight: var(--sl-font-weight-semibold);
text-transform: uppercase;
padding: 8px;
user-select: none;
transition: 0.1s all;
}
.markdown-section .docsify-copy-code-button.copied {
animation: pulse 0.75s;
--pulse-color: rgb(var(--sl-color-neutral-600));
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 var(--pulse-color);
}
70% {
box-shadow: 0 0 0 0.5rem transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
.markdown-section .docsify-copy-code-button .label {
transition: none;
}
.markdown-section .docsify-copy-code-button .success,
.markdown-section .docsify-copy-code-button .error {
display: none;
}
.markdown-section .docsify-copy-code-button:focus-visible {
box-shadow: 0 0 0 3px rgb(var(--sl-color-neutral-500) / 50%);
}
.markdown-section .docsify-copy-code-button:active {
background-color: rgb(var(--sl-color-neutral-600));
transform: scale(0.92);
}

View File

@@ -0,0 +1,208 @@
(() => {
let count = 1;
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
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 wrap(el, wrapper) {
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
}
window.$docsify.plugins.push((hook, vm) => {
// Convert code blocks to previews
hook.afterEach(function (html, next) {
const domParser = new DOMParser();
const doc = domParser.parseFromString(html, 'text/html');
const codePenButton = `
<button type="button" class="code-block__button code-block__button--codepen" title="Edit on CodePen">
<svg
width="138"
height="26"
viewBox="0 0 138 26"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
</svg>
</button>
`;
[...doc.querySelectorAll('code[class^="lang-"]')].map(code => {
if (code.classList.contains('preview')) {
const pre = code.closest('pre');
const preId = `code-block-preview-${count}`;
const toggleId = `code-block-toggle-${count}`;
pre.id = preId;
pre.classList.add('code-block__source');
pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, ''));
pre.setAttribute('aria-labelledby', toggleId);
const codeBlock = `
<div class="code-block">
<div class="code-block__preview">
${code.textContent}
<div class="code-block__resizer">
<sl-icon name="grip-vertical"></sl-icon>
</div>
</div>
${pre.outerHTML}
<div class="code-block__buttons">
<button type="button" class="code-block__button code-block__toggle" aria-expanded="false" aria-controls="${preId}">
View Source
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
${!code.classList.contains('no-codepen') ? codePenButton : ''}
</div>
</div>
`;
pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body);
count++;
}
});
next(doc.body.innerHTML);
});
// After the page is done loading, force scripts in previews to execute
hook.doneEach(() => {
[...document.querySelectorAll('.code-block__preview script')].map(script => runScript(script));
});
// Horizontal resizing
hook.doneEach(() => {
[...document.querySelectorAll('.code-block__preview')].map(preview => {
const resizer = preview.querySelector('.code-block__resizer');
let startX;
let startWidth;
const dragStart = event => {
startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
preview.classList.add('code-block__preview--dragging');
event.preventDefault();
document.documentElement.addEventListener('mousemove', dragMove, false);
document.documentElement.addEventListener('touchmove', dragMove, false);
document.documentElement.addEventListener('mouseup', dragStop, false);
document.documentElement.addEventListener('touchend', dragStop, false);
};
const dragMove = event => {
setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX);
};
const dragStop = event => {
preview.classList.remove('code-block__preview--dragging');
document.documentElement.removeEventListener('mousemove', dragMove, false);
document.documentElement.removeEventListener('touchmove', dragMove, false);
document.documentElement.removeEventListener('mouseup', dragStop, false);
document.documentElement.removeEventListener('touchend', dragStop, false);
};
const setWidth = width => (preview.style.width = width + 'px');
resizer.addEventListener('mousedown', dragStart);
resizer.addEventListener('touchstart', dragStart);
}, false);
});
});
// Open in CodePen
document.addEventListener('click', event => {
const button = event.target.closest('button');
if (button?.classList.contains('code-block__button--codepen')) {
const codeBlock = button.closest('.code-block');
const html = codeBlock.querySelector('.code-block__source > code').textContent;
const version = sessionStorage.getItem('sl-version');
const form = document.createElement('form');
form.action = 'https://codepen.io/pen/define';
form.method = 'POST';
form.target = '_blank';
// Docs: https://blog.codepen.io/documentation/prefill/
const data = {
title: '',
description: '',
tags: ['shoelace', 'web components'],
editors: '100',
head: `<meta name="viewport" content="width=device-width">`,
css_external: ``,
js_external: ``,
js_module: true,
html:
`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/themes/light.css">\n` +
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
`\n` +
html,
css: `body {\n font: 16px sans-serif;\n}`,
js: ``
};
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = JSON.stringify(data);
form.append(input);
document.body.append(form);
form.submit();
form.remove();
}
});
// Expand and collapse code blocks
document.addEventListener('click', event => {
const toggle = event.target.closest('.code-block__toggle');
if (toggle) {
const codeBlock = event.target.closest('.code-block');
codeBlock.classList.toggle('code-block--expanded');
event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-block--expanded'));
}
});
// Show pulse when copying
document.addEventListener('click', event => {
const button = event.target.closest('.docsify-copy-code-button');
if (button) {
button.classList.remove('copied');
requestAnimationFrame(() => {
button.addEventListener('animationend', () => button.classList.remove('copied'), { once: true });
button.classList.add('copied');
});
}
});
})();

View File

@@ -0,0 +1,512 @@
(() => {
const isDev = location.hostname === 'localhost';
const isNext = location.hostname === 'next.shoelace.style';
const customElements = fetch('/dist/custom-elements.json')
.then(res => res.json())
.catch(err => console.error(err));
function createPropsTable(props) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Reflects</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
${props
.map(prop => {
const hasAttribute = !!prop.attribute;
const isAttributeDifferent = prop.attribute !== prop.name;
let attributeInfo = '';
if (!hasAttribute) {
attributeInfo = `<br><small>(property only)</small>`;
} else if (isAttributeDifferent) {
attributeInfo = `
<br>
<sl-tooltip content="This attribute is different than the property">
<small>
<code class="nowrap">
${escapeHtml(prop.attribute)}
</code>
</small>
</sl-tooltip>`;
}
return `
<tr>
<td>
<code class="nowrap">${escapeHtml(prop.name)}</code>
${attributeInfo}
</td>
<td>
${escapeHtml(prop.description)}
</td>
<td style="text-align: center;">${
prop.reflects ? '<sl-icon label="yes" name="check"></sl-icon>' : ''
}</td>
<td>${prop.type?.text ? `<code>${escapeHtml(prop.type?.text || '')}</code>` : '-'}</td>
<td>${prop.default ? `<code>${escapeHtml(prop.default)}</code>` : '-'}</td>
</tr>
`;
})
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createEventsTable(events) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Event Detail</th>
</tr>
</thead>
<tbody>
${events
.map(
event => `
<tr>
<td><code class="nowrap">${escapeHtml(event.name)}</code></td>
<td>${escapeHtml(event.description)}</td>
<td>${event.type?.text ? `<code>${escapeHtml(event.type?.text)}` : '-'}</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createMethodsTable(methods) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Arguments</th>
</tr>
</thead>
<tbody>
${methods
.map(
method => `
<tr>
<td class="nowrap"><code>${escapeHtml(method.name)}</code></td>
<td>${escapeHtml(method.description)}</td>
<td>
${
method.parameters?.length
? `
<code>${escapeHtml(
method.parameters.map(param => `${param.name}: ${param.type.text}`).join(', ')
)}</code>
`
: '-'
}
</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createSlotsTable(slots) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${slots
.map(
slot => `
<tr>
<td class="nowrap">${slot.name ? `<code>${escapeHtml(slot.name)}</code>` : '(default)'}</td>
<td>${escapeHtml(slot.description)}</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createCustomPropertiesTable(styles) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${styles
.map(
style => `
<tr>
<td><code>${escapeHtml(style.name)}</code></td>
<td>${escapeHtml(style.description)}</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createPartsTable(parts) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${parts
.map(
part => `
<tr>
<td class="nowrap"><code>${escapeHtml(part.name)}</code></td>
<td>${escapeHtml(part.description)}</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createAnimationsTable(animations) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${animations
.map(
animation => `
<tr>
<td class="nowrap"><code>${escapeHtml(animation.name)}</code></td>
<td>${escapeHtml(animation.description)}</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createDependenciesList(targetComponent, allComponents) {
const ul = document.createElement('ul');
const dependencies = [];
// Recursively fetch subdependencies
function getDependencies(tag) {
const component = allComponents.find(c => c.tagName === tag);
if (!component || !Array.isArray(component.dependencies)) {
return [];
}
component.dependencies?.map(tag => {
if (!dependencies.includes(tag)) {
dependencies.push(tag);
}
getDependencies(tag);
});
}
getDependencies(targetComponent);
dependencies.sort().map(tag => {
const li = document.createElement('li');
li.innerHTML = `<code>&lt;${tag}&gt;</code>`;
ul.appendChild(li);
});
return ul.outerHTML;
}
function escapeHtml(html) {
return (html + '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/`(.*?)`/g, '<code>$1</code>');
}
function getAllComponents(metadata) {
const allComponents = [];
metadata.modules?.map(module => {
module.declarations?.map(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');
allComponents.push(declaration);
}
});
});
return allComponents;
}
function getComponent(metadata, tagName) {
return getAllComponents(metadata).find(component => component.tagName === tagName);
}
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
hook.mounted(async function () {
const metadata = await customElements;
const target = document.querySelector('.app-name');
// Add version
const version = document.createElement('div');
version.classList.add('sidebar-version');
version.textContent = isDev ? 'Development' : isNext ? 'Next' : metadata.package.version;
target.appendChild(version);
// Store version for reuse
sessionStorage.setItem('sl-version', metadata.package.version);
// Add repo buttons
const buttons = document.createElement('div');
buttons.classList.add('sidebar-buttons');
buttons.innerHTML = `
<sl-button size="small" class="repo-button repo-button--sponsor" href="https://github.com/sponsors/claviska" target="_blank">
<sl-icon name="heart"></sl-icon> Sponsor
</sl-button>
<sl-button size="small" class="repo-button repo-button--github" href="https://github.com/shoelace-style/shoelace/stargazers" target="_blank">
<sl-icon name="github"></sl-icon> <span class="github-star-count">Star</span>
</sl-button>
<sl-button size="small" class="repo-button repo-button--twitter" href="https://twitter.com/shoelace_style" target="_blank">
<sl-icon name="twitter"></sl-icon> Follow
</sl-button>
`;
target.appendChild(buttons);
});
hook.beforeEach(async function (content, next) {
const metadata = await customElements;
// Replace %VERSION% placeholders
content = content.replace(/%VERSION%/g, metadata.package.version);
// Handle [component-header] tags
content = content.replace(/\[component-header:([a-z-]+)\]/g, (match, tag) => {
const component = getComponent(metadata, tag);
let result = '';
if (!component) {
console.error('Component not found in metadata: ' + tag);
return next(content);
}
let badgeType = 'neutral';
if (component.status === 'stable') badgeType = 'primary';
if (component.status === 'experimental') badgeType = 'warning';
if (component.status === 'planned') badgeType = 'neutral';
if (component.status === 'deprecated') badgeType = 'danger';
result += `
<div class="component-header">
<div class="component-header__tag">
<code>&lt;${component.tagName}&gt; | ${component.name}</code>
</div>
<div class="component-header__info">
<sl-badge type="neutral" pill>
Since ${component.since || '?'}
</sl-badge>
<sl-badge type="${badgeType}" pill style="text-transform: capitalize;">
${component.status}
</sl-badge>
</div>
</div>
`;
return result.replace(/^ +| +$/gm, '');
});
// Handle [component-metadata] tags
content = content.replace(/\[component-metadata:([a-z-]+)\]/g, (match, tag) => {
const component = getComponent(metadata, tag);
let result = '';
if (!component) {
console.error('Component not found in metadata: ' + tag);
return next(content);
}
// Remove members that are private or don't have a description
const members = component.members?.filter(member => member.description && member.privacy !== 'private');
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
const props = members?.filter(prop => {
// Look for a corresponding attribute
const attribute = component.attributes?.find(attr => attr.fieldName === prop.name);
if (attribute) {
prop.attribute = attribute.name || attribute.fieldName;
}
return prop.kind === 'field' && prop.privacy !== 'private';
});
if (props?.length) {
result += `
## Properties
${createPropsTable(props)}
`;
}
if (component.events?.length) {
result += `
## Events
${createEventsTable(component.events)}
`;
}
if (methods?.length) {
result += `
## Methods
${createMethodsTable(methods)}
`;
}
if (component.slots?.length) {
result += `
## Slots
${createSlotsTable(component.slots)}
`;
}
if (component.cssProperties?.length) {
result += `
## CSS Custom Properties
${createCustomPropertiesTable(component.cssProperties)}
`;
}
if (component.cssParts?.length) {
result += `
## CSS Parts
${createPartsTable(component.cssParts)}
`;
}
if (component.animations?.length) {
result += `
## Animations
${createAnimationsTable(component.animations)}
Learn how to [customize animations](/getting-started/customizing#animations).
`;
}
if (component.dependencies?.length) {
result += `
## Dependencies
This component imports the following dependencies.
${createDependenciesList(component.tagName, getAllComponents(metadata))}
`;
}
if (component.path) {
/* prettier-ignore */
result += `
## Importing
<sl-tab-group>
<sl-tab slot="nav" panel="cdn" active>CDN</sl-tab>
<sl-tab slot="nav" panel="bundler">Bundler</sl-tab>
<sl-tab slot="nav" panel="react">React</sl-tab>
<sl-tab-panel name="cdn">\n
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace):
\`\`\`js
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="bundler">\n
To import this component using [a bundler](/getting-started/installation#bundling):
\`\`\`js
import '@shoelace-style/shoelace/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="react">\n
To import this component using [\`@shoelace-style/react\`](https://www.npmjs.com/package/@shoelace-style/react):
\`\`\`js
import '@shoelace-style/react/dist/${component.tagName.replace(/^sl-/, '')}';
\`\`\`
</sl-tab-panel>
</sl-tab-group>
`;
}
// Strip whitespace so markdown doesn't process things as code blocks
return result.replace(/^ +| +$/gm, '');
});
next(content);
});
// Wrap tables so we can scroll them horizontally when needed
hook.doneEach(function () {
const content = document.querySelector('.content');
const tables = [...content.querySelectorAll('table')];
tables.map(table => {
table.outerHTML = `
<div class="table-wrapper">
${table.outerHTML}
</div>
`;
});
});
});
})();

View File

@@ -0,0 +1,24 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
//
// Docsify generates pages dynamically and asynchronously, so when a reload happens, the scroll position can't be
// be restored immediately. This plugin waits until Docsify loads the page and then restores it.
//
window.$docsify.plugins.push((hook, vm) => {
hook.ready(() => {
// Restore
const scrollTop = sessionStorage.getItem('bs-scroll');
if (scrollTop) {
document.documentElement.scrollTop = scrollTop;
}
// Remember
document.addEventListener('scroll', event => {
sessionStorage.setItem('bs-scroll', document.documentElement.scrollTop);
});
});
});
})();

1310
docs/assets/plugins/search/lunr.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
body.site-search-visible {
overflow: hidden;
}
.sidebar .search-box {
margin: 1.25rem 26px;
}
.sidebar .search-box kbd {
margin-top: 2px;
}
/* Site search */
.site-search {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.site-search[hidden] {
display: none;
}
.site-search__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgb(var(--sl-overlay-background-color) / var(--sl-overlay-opacity));
z-index: -1;
}
.site-search__panel {
display: flex;
flex-direction: column;
max-width: 460px;
max-height: calc(100vh - 20rem);
background-color: rgb(var(--sl-surface-base-alt));
border-radius: var(--sl-border-radius-large);
box-shadow: var(--sl-shadow-x-large);
margin: 10rem auto;
}
@media screen and (max-width: 900px) {
.site-search__panel {
max-width: 100%;
max-height: calc(92vh - 120px); /* allow iOS browser chrome */
margin: 4vh var(--sl-spacing-medium);
}
}
.site-search__input::part(base) {
border: none;
background: transparent;
border-radius: var(--sl-border-radius-large);
}
.site-search__input:focus-within::part(base) {
outline: none;
box-shadow: none;
}
.site-search__input {
--sl-input-height-large: 4rem;
}
.site-search__body {
flex: 1 1 auto;
overflow: auto;
}
.site-search--has-results .site-search__body {
border-top: solid 1px rgb(var(--sl-color-neutral-200));
}
.site-search__results {
display: none;
line-height: var(--sl-line-height-dense);
list-style: none;
padding: var(--sl-spacing-x-small) 0;
margin: 0;
}
.site-search--has-results .site-search__results {
display: block;
}
.site-search__results a {
display: block;
text-decoration: none;
padding: var(--sl-spacing-x-small) var(--sl-spacing-large);
}
.site-search__results li a:hover,
.site-search__results li a:hover small {
background-color: rgb(var(--sl-color-neutral-100));
}
.site-search__results li[aria-selected='true'] a,
.site-search__results li[aria-selected='true'] a small,
.site-search__results li[aria-selected='true'] a sl-icon {
outline: none;
color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-color-primary-600));
}
.site-search__results h3 {
font-weight: var(--sl-font-weight-semibold);
margin: 0;
}
.site-search__results small {
display: block;
color: rgb(var(--sl-color-neutral-600));
}
.site-search__result {
padding: 0;
margin: 0;
}
.site-search__result a {
display: flex;
align-items: center;
gap: var(--sl-spacing-medium);
}
.site-search__result-icon {
flex: 0 0 auto;
display: flex;
color: rgb(var(--sl-color-neutral-400));
font-size: var(--sl-font-size-x-large);
}
.site-search__result-description {
flex: 1 1 auto;
}
.site-search__empty {
display: none;
border-top: solid 1px rgb(var(--sl-color-neutral-200));
text-align: center;
padding: var(--sl-spacing-x-large);
}
.site-search--no-results .site-search__empty {
display: block;
}
.site-search__footer {
display: flex;
justify-content: center;
gap: var(--sl-spacing-large);
border-top: solid 1px rgb(var(--sl-color-neutral-200));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
padding: var(--sl-spacing-medium);
}
.site-search__footer small {
color: rgb(var(--sl-color-neutral-700));
}
@media screen and (max-width: 900px) {
.site-search__footer {
display: none;
}
}

View File

@@ -0,0 +1,293 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
// Append the search box to the sidebar
hook.mounted(function () {
const appName = document.querySelector('.sidebar .app-name');
const searchBox = document.createElement('div');
searchBox.classList.add('search-box');
searchBox.innerHTML = `
<sl-input
type="search"
placeholder="Search"
clearable
pill
>
<sl-icon slot="prefix" name="search"></sl-icon>
<kbd slot="suffix" title="Press / to search">/</kbd>
</sl-input>
`;
const searchBoxInput = searchBox.querySelector('sl-input');
appName.insertAdjacentElement('afterend', searchBox);
// Show the search panel when the search is clicked
searchBoxInput.addEventListener('mousedown', event => {
event.preventDefault();
show();
});
// Show the search panel when a key is pressed
searchBoxInput.addEventListener('keydown', event => {
if (event.key === 'Tab') {
return;
}
// Pass the character that was typed through to the search input
if (event.key.length === 1) {
event.preventDefault();
input.value = event.key;
show();
}
});
});
// Append the search panel to the body
const siteSearch = document.createElement('div');
siteSearch.classList.add('site-search');
siteSearch.hidden = true;
siteSearch.innerHTML = `
<div class="site-search__overlay"></div>
<div class="site-search__panel">
<header class="site-search__header">
<sl-input
class="site-search__input"
type="search"
placeholder="Search this site"
size="large"
clearable
>
<sl-icon slot="prefix" name="search"></sl-icon>
</sl-input>
</header>
<div class="site-search__body">
<ul class="site-search__results"></ul>
<div class="site-search__empty">No results found.</div>
</div>
<footer class="site-search__footer">
<small><kbd>↑</kbd> <kbd>↓</kbd> navigate</small>
<small><kbd>↲</kbd> select</small>
<small><kbd>esc</kbd> close</small>
</footer>
</div>
`;
document.body.append(siteSearch);
const searchButtons = [...document.querySelectorAll('[data-site-search]')];
const overlay = siteSearch.querySelector('.site-search__overlay');
const panel = siteSearch.querySelector('.site-search__panel');
const input = siteSearch.querySelector('.site-search__input');
const results = siteSearch.querySelector('.site-search__results');
const animationDuration = 150;
const searchDebounce = 200;
let isShowing = false;
let searchTimeout;
let searchIndex;
let map;
// Load search data
const searchData = fetch('../../../search.json')
.then(res => res.json())
.then(data => {
searchIndex = lunr.Index.load(data.searchIndex);
map = data.map;
});
async function show() {
isShowing = true;
document.body.classList.add('site-search-visible');
siteSearch.hidden = false;
input.focus();
updateResults();
await Promise.all([
panel.animate(
[
{ opacity: 0, transform: 'scale(.9)' },
{ opacity: 1, transform: 'scale(1)' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished
]);
document.addEventListener('mousedown', handleDocumentMouseDown);
document.addEventListener('keydown', handleDocumentKeyDown);
document.addEventListener('focusin', handleDocumentFocusIn);
}
async function hide() {
isShowing = false;
document.body.classList.remove('site-search-visible');
await Promise.all([
panel.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(.9)' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished
]);
siteSearch.hidden = true;
input.value = '';
updateResults();
document.removeEventListener('mousedown', handleDocumentMouseDown);
document.removeEventListener('keydown', handleDocumentKeyDown);
document.removeEventListener('focusin', handleDocumentFocusIn);
}
function handleInput() {
// Debounce search queries
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
}
function handleDocumentFocusIn(event) {
// Close when focus leaves the panel
if (event.target.closest('.site-search__panel') !== panel) {
hide();
}
}
function handleDocumentMouseDown(event) {
// Close when clicking outside of the panel
if (event.target.closest('.site-search__overlay') === overlay) {
hide();
}
}
function handleDocumentKeyDown(event) {
// Close when pressing escape
if (event.key === 'Escape') {
event.preventDefault();
hide();
return;
}
// Handle keyboard selections
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
event.preventDefault();
const currentEl = results.querySelector('[aria-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.map(item => {
if (item === nextEl) {
item.setAttribute('aria-selected', 'true');
nextEl.scrollIntoView({ block: 'nearest' });
} else {
item.setAttribute('aria-selected', 'false');
}
});
return;
}
}
async function updateResults(query = '') {
try {
await searchIndex;
const hasQuery = query.length > 0;
let matches = hasQuery ? searchIndex.search(`${query}`) : [];
// Fall back to a fuzzy search if no matches are found
if (matches.length === 0 && hasQuery) {
matches = searchIndex.search(`${query}~2`);
}
let hasResults = hasQuery && matches.length > 0;
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
results.innerHTML = '';
matches.map((match, index) => {
const page = map[match.ref];
const li = document.createElement('li');
const a = document.createElement('a');
let icon = 'file-text';
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';
a.href = $docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
a.innerHTML = `
<div class="site-search__result-icon">
<sl-icon name="${icon}" aria-hidden="true"></sl-icon>
</div>
<div class="site-search__result__details">
<h3>${page.title}</h3>
<small>${page.url}</small>
</div>
`;
li.classList.add('site-search__result');
li.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
li.appendChild(a);
results.appendChild(li);
});
} catch {
// Ignore query errors as the user types
}
}
// Show the search panel slash is pressed outside of a form element
document.addEventListener('keydown', event => {
if (
!isShowing &&
event.key === '/' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
show();
}
});
input.addEventListener('sl-input', handleInput);
// Close when a result is selected
results.addEventListener('click', event => {
if (event.target.closest('a')) {
hide();
}
});
});
})();

View File

@@ -0,0 +1,29 @@
.theme-picker {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 30;
}
.theme-picker:not(:defined) {
display: none;
}
.theme-picker sl-menu-label {
white-space: nowrap;
}
.theme-picker sl-menu-label kbd {
margin-left: 0.5rem;
}
@media screen and (max-width: 768px) {
.theme-picker {
top: 0.5rem;
right: 0.5rem;
}
.theme-picker sl-menu-label {
display: none;
}
}

View File

@@ -0,0 +1,84 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
hook.mounted(function () {
function getTheme() {
return localStorage.getItem('theme') || 'auto';
}
function isDark() {
if (theme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
return theme === 'dark';
}
}
function setTheme(newTheme) {
const noTransitions = Object.assign(document.createElement('style'), {
textContent: '* { transition: none !important; }'
});
theme = newTheme;
localStorage.setItem('theme', theme);
// Update the UI
[...menu.querySelectorAll('sl-menu-item')].map(item => (item.checked = item.getAttribute('value') === theme));
menuIcon.name = isDark() ? 'moon' : 'sun';
// Toggle the dark mode class without transitions
document.body.appendChild(noTransitions);
requestAnimationFrame(() => {
document.documentElement.classList.toggle('sl-theme-dark', isDark());
requestAnimationFrame(() => document.body.removeChild(noTransitions));
});
}
let theme = getTheme();
// Generate the theme picker dropdown
const dropdown = document.createElement('sl-dropdown');
dropdown.classList.add('theme-picker');
dropdown.innerHTML = `
<sl-button size="small" pill slot="trigger" caret>
<sl-icon name="sun" label="Select Theme"></sl-icon>
</sl-button>
<sl-menu>
<sl-menu-label>Toggle <kbd>\\</kbd></sl-menu-label>
<sl-menu-item value="light">Light</sl-menu-item>
<sl-menu-item value="dark">Dark</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="auto">Auto</sl-menu-item>
</sl-menu>
`;
document.querySelector('.sidebar-toggle').insertAdjacentElement('afterend', dropdown);
// Listen for selections
const menu = dropdown.querySelector('sl-menu');
const menuIcon = dropdown.querySelector('sl-icon');
menu.addEventListener('sl-select', event => setTheme(event.detail.item.value));
// Update the theme when the preference changes
window.matchMedia('(prefers-color-scheme: dark)').addListener(event => setTheme(theme));
// Toggle themes when pressing backslash
document.addEventListener('keydown', event => {
if (
event.key === '\\' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
setTheme(isDark() ? 'light' : 'dark');
show();
}
});
// Set the intial theme and sync the UI
setTheme(theme);
});
});
})();

View File

@@ -1,249 +0,0 @@
(() => {
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 <html>.');
}
// 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 =
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/shoelace.js"></script>\n` +
`\n${htmlExample}`;
jsTemplate = '';
}
// React templates
if (isReact) {
htmlTemplate = '<div id="root"></div>';
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(<App />, 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: `<meta name="viewport" content="width=device-width">`,
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);
})();

View File

@@ -1,272 +0,0 @@
//
// 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);
})();

View File

@@ -1,384 +0,0 @@
(() => {
// 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 = `
<div class="search__overlay"></div>
<dialog id="search-dialog" class="search__dialog">
<div class="search__content">
<div class="search__header">
<div id="search-combobox" class="search__input-wrapper">
<sl-icon name="search"></sl-icon>
<input
id="search-input"
class="search__input"
type="search"
placeholder="Search"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
enterkeyhint="go"
spellcheck="false"
maxlength="100"
role="combobox"
aria-autocomplete="list"
aria-expanded="true"
aria-controls="search-listbox"
aria-haspopup="listbox"
aria-activedescendant
>
<button type="button" class="search__clear-button" aria-label="Clear entry" tabindex="-1" hidden>
<sl-icon name="x-circle-fill"></sl-icon>
</button>
</div>
</div>
<div class="search__body">
<ul
id="search-listbox"
class="search__results"
role="listbox"
aria-label="Search results"
></ul>
<div class="search__empty">No matching pages</div>
</div>
<footer class="search__footer">
<small><kbd>↑</kbd> <kbd>↓</kbd> Navigate</small>
<small><kbd>↲</kbd> Select</small>
<small><kbd>Esc</kbd> Close</small>
</footer>
</div>
</dialog>
`;
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 <dialog> 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 = `
<div class="search__result-icon" aria-hidden="true">
<sl-icon name="${icon}"></sl-icon>
</div>
<div class="search__result__details">
<div class="search__result-title"></div>
<div class="search__result-description"></div>
<div class="search__result-url"></div>
</div>
`;
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());
});
})();

View File

@@ -1,29 +0,0 @@
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);
})();

View File

@@ -1,173 +0,0 @@
/* 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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,347 +0,0 @@
/* 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;
}
}

234
docs/components/alert.md Normal file
View File

@@ -0,0 +1,234 @@
# Alert
[component-header:sl-alert]
Alerts are used to display important messages either inline or as toast notifications.
```html preview
<sl-alert open>
<sl-icon slot="icon" name="info-circle"></sl-icon>
This is a standard alert. You can customize its content and even the icon.
</sl-alert>
```
?> Alerts will not be visible if the `open` attribute is not present.
## Examples
### Types
Set the `type` attribute to change the alert's type.
```html preview
<sl-alert type="primary" open>
<sl-icon slot="icon" name="info-circle"></sl-icon>
<strong>This is super informative</strong><br>
You can tell by how pretty the alert is.
</sl-alert>
<br>
<sl-alert type="success" open>
<sl-icon slot="icon" name="check2-circle"></sl-icon>
<strong>Your changes have been saved</strong><br>
You can safely exit the app now.
</sl-alert>
<br>
<sl-alert type="neutral" open>
<sl-icon slot="icon" name="gear"></sl-icon>
<strong>Your settings have been updated</strong><br>
Settings will take affect on next login.
</sl-alert>
<br>
<sl-alert type="warning" open>
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
<strong>Your session has ended</strong><br>
Please login again to continue.
</sl-alert>
<br>
<sl-alert type="danger" open>
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
<strong>Your account has been deleted</strong><br>
We're very sorry to see you go!
</sl-alert>
```
### Closable
Add the `closable` attribute to show a close button that will hide the alert.
```html preview
<sl-alert type="primary" open closable class="alert-closable">
<sl-icon slot="icon" name="info-circle"></sl-icon>
You can close this alert any time!
</sl-alert>
<script>
const alert = document.querySelector('.alert-closable');
alert.addEventListener('sl-after-hide', () => {
setTimeout(() => alert.open = true, 2000);
});
</script>
```
### Without Icons
Icons are optional. Simply omit the `icon` slot if you don't want them.
```html preview
<sl-alert type="primary" open>
Nothing fancy here, just a simple alert.
</sl-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
<div class="alert-duration">
<sl-button type="primary">Show Alert</sl-button>
<sl-alert type="primary" duration="3000" closable>
<sl-icon slot="icon" name="info-circle"></sl-icon>
This alert will automatically hide itself after three seconds, unless you interact with it.
</sl-alert>
</div>
<script>
const container = document.querySelector('.alert-duration');
const button = container.querySelector('sl-button');
const alert = container.querySelector('sl-alert');
button.addEventListener('click', () => alert.show());
</script>
<style>
.alert-duration sl-alert {
margin-top: var(--sl-spacing-medium);
}
</style>
```
### 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
<div class="alert-toast">
<sl-button type="primary">Primary</sl-button>
<sl-button type="success">Success</sl-button>
<sl-button type="neutral">Neutral</sl-button>
<sl-button type="warning">Warning</sl-button>
<sl-button type="danger">Danger</sl-button>
<sl-alert type="primary" duration="3000" closable>
<sl-icon slot="icon" name="info-circle"></sl-icon>
<strong>This is super informative</strong><br>
You can tell by how pretty the alert is.
</sl-alert>
<sl-alert type="success" duration="3000" closable>
<sl-icon slot="icon" name="check2-circle"></sl-icon>
<strong>Your changes have been saved</strong><br>
You can safely exit the app now.
</sl-alert>
<sl-alert type="neutral" duration="3000" closable>
<sl-icon slot="icon" name="gear"></sl-icon>
<strong>Your settings have been updated</strong><br>
Settings will take affect on next login.
</sl-alert>
<sl-alert type="warning" duration="3000" closable>
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
<strong>Your session has ended</strong><br>
Please login again to continue.
</sl-alert>
<sl-alert type="danger" duration="3000" closable>
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
<strong>Your account has been deleted</strong><br>
We're very sorry to see you go!
</sl-alert>
</div>
<script>
const container = document.querySelector('.alert-toast');
['primary', 'success', 'neutral', 'warning', 'danger'].map(type => {
const button = container.querySelector(`sl-button[type="${type}"]`);
const alert = container.querySelector(`sl-alert[type="${type}"]`);
button.addEventListener('click', () => alert.toast());
});
</script>
```
### 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
<div class="alert-toast-wrapper">
<sl-button type="primary">Create Toast</sl-button>
</div>
<script>
const container = document.querySelector('.alert-toast-wrapper');
const button = container.querySelector('sl-button');
let count = 0;
// Always escape HTML for text arguments!
function escapeHtml(html) {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
}
// Custom function to emit toast notifications
function notify(message, type = 'primary', icon = 'info-circle', duration = 3000) {
const alert = Object.assign(document.createElement('sl-alert'), {
type: type,
closable: true,
duration: duration,
innerHTML: `
<sl-icon name="${icon}" slot="icon"></sl-icon>
${escapeHtml(message)}
`
});
document.body.append(alert);
return alert.toast();
}
button.addEventListener('click', () => {
notify(`This is custom toast #${++count}`);
});
</script>
```
### 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;
}
```
?> 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.
[component-metadata:sl-alert]

View File

@@ -0,0 +1,63 @@
# Animated Image
[component-header:sl-animated-image]
A component for displaying animated GIFs and WEBPs that play and pause on interaction.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
></sl-animated-image>
```
## Examples
### WEBP Images
Both GIF and WEBP images are supported.
```html preview
<sl-animated-image
src="/assets/images/tie.webp"
alt="Animation of a shoe being tied"
></sl-animated-image>
```
### Setting a Width and Height
To set a custom size, apply a width and/or height to the host element.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
style="width: 150px; height: 200px;"
>
</sl-animated-image>
```
### 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
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
class="animated-image-custom-control-box"
></sl-animated-image>
<style>
.animated-image-custom-control-box::part(control-box) {
top: auto;
right: auto;
bottom: 1rem;
left: 1rem;
background-color: deeppink;
color: white;
}
</style>
```
[component-metadata:sl-animated-image]

View File

@@ -1,13 +1,12 @@
---
meta:
title: Animation
description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes.
layout: component
---
# Animation
To animate an element, wrap it in `<sl-animation>` 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.
[component-header:sl-animation]
```html:preview
Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. Powered by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
To animate an element, wrap it in `<sl-animation>` and set an animation `name`. The animation not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options.
```html preview
<div class="animation-overview">
<sl-animation name="bounce" duration="2000" play><div class="box"></div></sl-animation>
<sl-animation name="jello" duration="2000" play><div class="box"></div></sl-animation>
@@ -20,50 +19,13 @@ To animate an element, wrap it in `<sl-animation>` and set an animation `name`.
display: inline-block;
width: 100px;
height: 100px;
background-color: var(--sl-color-primary-600);
background-color: rgb(var(--sl-color-primary-600));
margin: 1.5rem;
}
</style>
```
```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 = () => (
<>
<div class="animation-overview">
<SlAnimation name="bounce" duration={2000} play>
<div class="box" />
</SlAnimation>
<SlAnimation name="jello" duration={2000} play>
<div class="box" />
</SlAnimation>
<SlAnimation name="heartBeat" duration={2000} play>
<div class="box" />
</SlAnimation>
<SlAnimation name="flip" duration={2000} play>
<div class="box" />
</SlAnimation>
</div>
<style>{css}</style>
</>
);
```
:::tip
The animation will only be applied to the first child element found in `<sl-animation>`.
:::
?> The animation will only be applied to the first child element found in `<sl-animation>`.
## Examples
@@ -71,7 +33,7 @@ The animation will only be applied to the first child element found in `<sl-anim
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
```html preview
<div class="animation-sandbox">
<sl-animation name="bounce" easing="ease-in-out" duration="2000" play>
<div class="box"></div>
@@ -80,7 +42,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
<div class="controls">
<sl-select label="Animation" value="bounce"></sl-select>
<sl-select label="Easing" value="linear"></sl-select>
<sl-input label="Playback Rate" type="number" min="0" max="2" step=".25" value="1"></sl-input>
<sl-range min="0" max="2" step=".5" value="1"></sl-range>
</div>
</div>
@@ -91,36 +53,37 @@ This example demonstrates all of the baked-in animations and easings. Animations
const animation = container.querySelector('sl-animation');
const animationName = container.querySelector('.controls sl-select:nth-child(1)');
const easingName = container.querySelector('.controls sl-select:nth-child(2)');
const playbackRate = container.querySelector('sl-input[type="number"]');
const playbackRate = container.querySelector('sl-range');
const animations = getAnimationNames();
const easings = getEasingNames();
animations.map(name => {
const option = Object.assign(document.createElement('sl-option'), {
const menuItem = Object.assign(document.createElement('sl-menu-item'), {
textContent: name,
value: name
});
animationName.appendChild(option);
animationName.appendChild(menuItem);
});
easings.map(name => {
const option = Object.assign(document.createElement('sl-option'), {
const menuItem = Object.assign(document.createElement('sl-menu-item'), {
textContent: name,
value: name
});
easingName.appendChild(option);
easingName.appendChild(menuItem);
});
animationName.addEventListener('sl-change', () => (animation.name = animationName.value));
easingName.addEventListener('sl-change', () => (animation.easing = easingName.value));
playbackRate.addEventListener('sl-input', () => (animation.playbackRate = playbackRate.value));
animationName.addEventListener('sl-change', () => animation.name = animationName.value);
easingName.addEventListener('sl-change', () => animation.easing = easingName.value);
playbackRate.addEventListener('sl-change', () => animation.playbackRate = playbackRate.value);
playbackRate.tooltipFormatter = val => `Playback Rate = ${val}`;
</script>
<style>
.animation-sandbox .box {
width: 100px;
height: 100px;
background-color: var(--sl-color-primary-600);
background-color: rgb(var(--sl-color-primary-600));
}
.animation-sandbox .controls {
@@ -138,7 +101,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
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
```html preview
<div class="animation-scroll">
<sl-animation name="jackInTheBox" duration="2000" iterations="1"><div class="box"></div></sl-animation>
</div>
@@ -166,66 +129,16 @@ Use an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/
display: inline-block;
width: 100px;
height: 100px;
background-color: var(--sl-color-primary-600);
}
background-color: rgb(var(--sl-color-primary-600));
}
</style>
```
```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 (
<>
<div class="animation-scroll">
<SlAnimation ref={animation} name="jackInTheBox" duration={2000} iterations={1}>
<div ref={box} class="box" />
</SlAnimation>
</div>
<style>{css}</style>
</>
);
};
```
### 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
```html preview
<div class="animation-keyframes">
<sl-animation easing="ease-in-out" duration="2000" play>
<div class="box"></div>
@@ -256,63 +169,19 @@ Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/
.animation-keyframes .box {
width: 100px;
height: 100px;
background-color: var(--sl-color-primary-600);
background-color: rgb(var(--sl-color-primary-600));
}
</style>
```
```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 = () => (
<>
<div class="animation-keyframes">
<SlAnimation
easing="ease-in-out"
duration={2000}
play
keyframes={[
{
offset: 0,
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
fillMode: 'both',
transformOrigin: 'center center',
transform: 'rotate(0)'
},
{
offset: 1,
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
fillMode: 'both',
transformOrigin: 'center center',
transform: 'rotate(90deg)'
}
]}
>
<div class="box" />
</SlAnimation>
</div>
<style>{css}</style>
</>
);
```
### 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
```html preview
<div class="animation-form">
<sl-animation name="rubberBand" duration="1000" iterations="1">
<sl-button variant="primary">Click me</sl-button>
<sl-button type="primary">Click me</sl-button>
</sl-animation>
</div>
@@ -327,22 +196,4 @@ Animations won't play until you apply the `play` attribute. You can omit it init
</script>
```
```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 (
<div class="animation-form">
<SlAnimation name="rubberBand" duration={1000} iterations={1} play={play} onSlFinish={() => setPlay(false)}>
<SlButton variant="primary" onClick={() => setPlay(true)}>
Click me
</SlButton>
</SlAnimation>
</div>
);
};
```
[component-metadata:sl-animation]

86
docs/components/avatar.md Normal file
View File

@@ -0,0 +1,86 @@
# Avatar
[component-header:sl-avatar]
Avatars are used to represent a person or object.
```html preview
<sl-avatar></sl-avatar>
```
## Examples
### Images
To use an image for the avatar, set the `image` and `alt` attributes. This will take priority and be shown over initials and icons.
```html preview
<sl-avatar
image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"
alt="Gray tabby kitten looking down"
></sl-avatar>
```
### 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
<sl-avatar initials="SL"></sl-avatar>
```
### 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
<sl-avatar>
<sl-icon slot="icon" name="image"></sl-icon>
</sl-avatar>
<sl-avatar>
<sl-icon slot="icon" name="archive"></sl-icon>
</sl-avatar>
<sl-avatar>
<sl-icon slot="icon" name="briefcase"></sl-icon>
</sl-avatar>
```
### Shapes
Avatars can be shaped using the `shape` attribute.
```html preview
<sl-avatar shape="square"></sl-avatar>
<sl-avatar shape="rounded"></sl-avatar>
<sl-avatar shape="circle"></sl-avatar>
```
### Avatar Groups
You can group avatars with a few lines of CSS.
```html preview
<div class="avatar-group">
<sl-avatar image="https://images.unsplash.com/photo-1490150028299-bf57d78394e0?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=256&h=256&q=80&crop=right"></sl-avatar>
<sl-avatar image="https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=256&h=256&crop=left&q=80"></sl-avatar>
<sl-avatar image="https://images.unsplash.com/photo-1456439663599-95b042d50252?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=256&h=256&crop=left&q=80"></sl-avatar>
<sl-avatar image="https://images.unsplash.com/flagged/photo-1554078875-e37cb8b0e27d?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=256&h=256&crop=top&q=80"></sl-avatar>
</div>
<style>
.avatar-group sl-avatar:not(:first-of-type) {
margin-left: -1rem;
}
.avatar-group sl-avatar::part(base) {
border: solid 2px rgb(var(--sl-color-neutral-0));
}
</style>
```
[component-metadata:sl-avatar]

90
docs/components/badge.md Normal file
View File

@@ -0,0 +1,90 @@
# Badge
[component-header:sl-badge]
Badges are used to draw attention and display statuses or counts.
```html preview
<sl-badge>Badge</sl-badge>
```
## Examples
### Types
Set the `type` attribute to change the badge's type.
```html preview
<sl-badge type="primary">Primary</sl-badge>
<sl-badge type="success">Success</sl-badge>
<sl-badge type="neutral">Neutral</sl-badge>
<sl-badge type="warning">Warning</sl-badge>
<sl-badge type="danger">Danger</sl-badge>
```
### Pill Badges
Use the `pill` attribute to give badges rounded edges.
```html preview
<sl-badge type="primary" pill>Primary</sl-badge>
<sl-badge type="success" pill>Success</sl-badge>
<sl-badge type="neutral" pill>Neutral</sl-badge>
<sl-badge type="warning" pill>Warning</sl-badge>
<sl-badge type="danger" pill>Danger</sl-badge>
```
### Pulsating Badges
Use the `pulse` attribute to draw attention to the badge with a subtle animation.
```html preview
<div class="badge-pulse">
<sl-badge type="primary" pill pulse>1</sl-badge>
<sl-badge type="success" pill pulse>1</sl-badge>
<sl-badge type="neutral" pill pulse>1</sl-badge>
<sl-badge type="warning" pill pulse>1</sl-badge>
<sl-badge type="danger" pill pulse>1</sl-badge>
</div>
<style>
.badge-pulse sl-badge:not(:last-of-type) {
margin-right: 1rem;
}
</style>
```
### 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
<sl-button>
Requests
<sl-badge pill>30</sl-badge>
</sl-button>
<sl-button style="margin-left: 1rem;">
Warnings
<sl-badge type="warning" pill>8</sl-badge>
</sl-button>
<sl-button style="margin-left: 1rem;">
Errors
<sl-badge type="danger" pill>6</sl-badge>
</sl-button>
```
### With Menu Items
When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly.
```html preview
<sl-menu style="max-width: 240px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-label>Messages</sl-menu-label>
<sl-menu-item>Comments <sl-badge slot="suffix" type="neutral" pill>4</sl-badge></sl-menu-item>
<sl-menu-item>Replies <sl-badge slot="suffix" type="neutral" pill>12</sl-badge></sl-menu-item>
</sl-menu>
```
[component-metadata:sl-badge]

View File

@@ -0,0 +1,20 @@
# Breadcrumb Item
[component-header:sl-breadcrumb-item]
Breadcrumb Items are used inside [breadcrumbs](/components/breadcrumb) to represent different links.
```html preview
<sl-breadcrumb>
<sl-breadcrumb-item>
<sl-icon slot="prefix" name="house"></sl-icon>
Home
</sl-breadcrumb-item>
<sl-breadcrumb-item>Clothing</sl-breadcrumb-item>
<sl-breadcrumb-item>Shirts</sl-breadcrumb-item>
</sl-breadcrumb>
```
?> Additional demonstrations can be found in the [breadcrumb examples](/components/breadcrumb).
[component-metadata:sl-breadcrumb-item]

View File

@@ -0,0 +1,132 @@
# Breadcrumb
[component-header:sl-breadcrumb]
Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
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
<sl-breadcrumb>
<sl-breadcrumb-item>Catalog</sl-breadcrumb-item>
<sl-breadcrumb-item>Clothing</sl-breadcrumb-item>
<sl-breadcrumb-item>Women's</sl-breadcrumb-item>
<sl-breadcrumb-item>Shirts &amp; Tops</sl-breadcrumb-item>
</sl-breadcrumb>
```
## 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
<sl-breadcrumb>
<sl-breadcrumb-item href="https://example.com/home">
Homepage
</sl-breadcrumb-item>
<sl-breadcrumb-item href="https://example.com/home/services">
Our Services
</sl-breadcrumb-item>
<sl-breadcrumb-item href="https://example.com/home/services/digital">
Digital Media
</sl-breadcrumb-item>
<sl-breadcrumb-item href="https://example.com/home/services/digital/web-design">
Web Design
</sl-breadcrumb-item>
</sl-breadcrumb>
```
### 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
<sl-breadcrumb>
<sl-icon name="dot" slot="separator"></sl-icon>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
</sl-breadcrumb>
<br>
<sl-breadcrumb>
<sl-icon name="arrow-right" slot="separator"></sl-icon>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
</sl-breadcrumb>
<br>
<sl-breadcrumb>
<span slot="separator">/</span>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
</sl-breadcrumb>
```
### Prefixes
Use the `prefix` slot to add content before any breadcrumb item.
```html preview
<sl-breadcrumb>
<sl-breadcrumb-item>
<sl-icon slot="prefix" name="house"></sl-icon>
Home
</sl-breadcrumb-item>
<sl-breadcrumb-item>Articles</sl-breadcrumb-item>
<sl-breadcrumb-item>Traveling</sl-breadcrumb-item>
</sl-breadcrumb>
```
### Suffixes
Use the `suffix` slot to add content after any breadcrumb item.
```html preview
<sl-breadcrumb>
<sl-breadcrumb-item>Documents</sl-breadcrumb-item>
<sl-breadcrumb-item>Policies</sl-breadcrumb-item>
<sl-breadcrumb-item>
Security
<sl-icon slot="suffix" name="shield-lock"></sl-icon>
</sl-breadcrumb-item>
</sl-breadcrumb>
```
### With Dropdowns
Dropdown menus can be placed in a prefix or suffix slot to provide additional options.
```html preview
<sl-breadcrumb>
<sl-breadcrumb-item>Homepage</sl-breadcrumb-item>
<sl-breadcrumb-item>Our Services</sl-breadcrumb-item>
<sl-breadcrumb-item>Digital Media</sl-breadcrumb-item>
<sl-breadcrumb-item>
Web Design
<sl-dropdown slot="suffix">
<sl-button slot="trigger" size="small" circle>
<sl-icon label="More options" name="three-dots"></sl-icon>
</sl-button>
<sl-menu>
<sl-menu-item checked>Web Design</sl-menu-item>
<sl-menu-item>Web Development</sl-menu-item>
<sl-menu-item>Marketing</sl-menu-item>
</sl-menu>
</sl-dropdown>
</sl-breadcrumb-item>
</sl-breadcrumb>
```
[component-metadata:sl-breadcrumb]

View File

@@ -0,0 +1,221 @@
# Button Group
[component-header:sl-button-group]
Button groups can be used to group related buttons into sections.
```html preview
<sl-button-group>
<sl-button>Left</sl-button>
<sl-button>Center</sl-button>
<sl-button>Right</sl-button>
</sl-button-group>
```
## Examples
### Button Sizes
All button sizes are supported, but avoid mixing sizes within the same button group.
```html preview
<sl-button-group>
<sl-button size="small">Left</sl-button>
<sl-button size="small">Center</sl-button>
<sl-button size="small">Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button size="medium">Left</sl-button>
<sl-button size="medium">Center</sl-button>
<sl-button size="medium">Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button size="large">Left</sl-button>
<sl-button size="large">Center</sl-button>
<sl-button size="large">Right</sl-button>
</sl-button-group>
```
### Theme Buttons
Theme buttons are supported through the button's `type` attribute.
```html preview
<sl-button-group>
<sl-button type="primary">Left</sl-button>
<sl-button type="primary">Center</sl-button>
<sl-button type="primary">Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button type="success">Left</sl-button>
<sl-button type="success">Center</sl-button>
<sl-button type="success">Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button type="neutral">Left</sl-button>
<sl-button type="neutral">Center</sl-button>
<sl-button type="neutral">Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button type="warning">Left</sl-button>
<sl-button type="warning">Center</sl-button>
<sl-button type="warning">Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button type="danger">Left</sl-button>
<sl-button type="danger">Center</sl-button>
<sl-button type="danger">Right</sl-button>
</sl-button-group>
```
### Pill Buttons
Pill buttons are supported through the button's `pill` attribute.
```html preview
<sl-button-group>
<sl-button size="small" pill>Left</sl-button>
<sl-button size="small" pill>Center</sl-button>
<sl-button size="small" pill>Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button size="medium" pill>Left</sl-button>
<sl-button size="medium" pill>Center</sl-button>
<sl-button size="medium" pill>Right</sl-button>
</sl-button-group>
<br><br>
<sl-button-group>
<sl-button size="large" pill>Left</sl-button>
<sl-button size="large" pill>Center</sl-button>
<sl-button size="large" pill>Right</sl-button>
</sl-button-group>
```
### Dropdowns in Button Groups
Dropdowns can be placed inside button groups as long as the trigger is an `<sl-button>` element.
```html preview
<sl-button-group>
<sl-button>Button</sl-button>
<sl-button>Button</sl-button>
<sl-dropdown>
<sl-button slot="trigger" caret>Dropdown</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
<sl-menu-item>Item 2</sl-menu-item>
<sl-menu-item>Item 3</sl-menu-item>
</sl-menu>
</sl-dropdown>
</sl-button-group>
```
### Split Buttons
Create a split button using a button and a dropdown.
```html preview
<sl-button-group>
<sl-button type="primary">Save</sl-button>
<sl-dropdown placement="bottom-end">
<sl-button slot="trigger" type="primary" caret></sl-button>
<sl-menu>
<sl-menu-item>Save</sl-menu-item>
<sl-menu-item>Save as&hellip;</sl-menu-item>
<sl-menu-item>Save all</sl-menu-item>
</sl-menu>
</sl-dropdown>
</sl-button-group>
```
### Tooltips in Button Groups
Buttons can be wrapped in tooltips to provide more detail when the user interacts with them.
```html preview
<sl-button-group>
<sl-tooltip content="I'm on the left">
<sl-button>Left</sl-button>
</sl-tooltip>
<sl-tooltip content="I'm in the middle">
<sl-button>Center</sl-button>
</sl-tooltip>
<sl-tooltip content="I'm on the right">
<sl-button>Right</sl-button>
</sl-tooltip>
</sl-button-group>
```
### Toolbar Example
Create interactive toolbars with button groups.
```html preview
<div class="button-group-toolbar">
<sl-button-group label="History">
<sl-tooltip content="Undo">
<sl-button><sl-icon name="arrow-counterclockwise"></sl-icon></sl-button>
</sl-tooltip>
<sl-tooltip content="Redo">
<sl-button><sl-icon name="arrow-clockwise"></sl-icon></sl-button>
</sl-tooltip>
</sl-button-group>
<sl-button-group label="Formatting">
<sl-tooltip content="Bold">
<sl-button><sl-icon name="type-bold"></sl-icon></sl-button>
</sl-tooltip>
<sl-tooltip content="Italic">
<sl-button><sl-icon name="type-italic"></sl-icon></sl-button>
</sl-tooltip>
<sl-tooltip content="Underline">
<sl-button><sl-icon name="type-underline"></sl-icon></sl-button>
</sl-tooltip>
</sl-button-group>
<sl-button-group label="Alignment">
<sl-tooltip content="Align Left">
<sl-button><sl-icon name="justify-left"></sl-icon></sl-button>
</sl-tooltip>
<sl-tooltip content="Align Center">
<sl-button><sl-icon name="justify"></sl-icon></sl-button>
</sl-tooltip>
<sl-tooltip content="Align Right">
<sl-button><sl-icon name="justify-right"></sl-icon></sl-button>
</sl-tooltip>
</sl-button-group>
</div>
<style>
.button-group-toolbar sl-button-group:not(:last-of-type) {
margin-right: var(--sl-spacing-x-small);
}
</style>
```
[component-metadata:sl-button-group]

240
docs/components/button.md Normal file
View File

@@ -0,0 +1,240 @@
# Button
[component-header:sl-button]
Buttons represent actions that are available to the user.
```html preview
<sl-button>Button</sl-button>
```
## Examples
### Types
Use the `type` attribute to set the button's type.
```html preview
<sl-button type="default">Default</sl-button>
<sl-button type="primary">Primary</sl-button>
<sl-button type="success">Success</sl-button>
<sl-button type="neutral">Neutral</sl-button>
<sl-button type="warning">Warning</sl-button>
<sl-button type="danger">Danger</sl-button>
```
### Sizes
Use the `size` attribute to change a button's size.
```html preview
<sl-button size="small">Small</sl-button>
<sl-button size="medium">Medium</sl-button>
<sl-button size="large">Large</sl-button>
```
### Outline Buttons
Use the `outline` attribute to draw outlined buttons with transparent backgrounds.
```html preview
<sl-button type="default" outline>Default</sl-button>
<sl-button type="primary" outline>Primary</sl-button>
<sl-button type="success" outline>Success</sl-button>
<sl-button type="neutral" outline>Neutral</sl-button>
<sl-button type="warning" outline>Warning</sl-button>
<sl-button type="danger" outline>Danger</sl-button>
```
### Pill Buttons
Use the `pill` attribute to give buttons rounded edges.
```html preview
<sl-button size="small" pill>Small</sl-button>
<sl-button size="medium" pill>Medium</sl-button>
<sl-button size="large" pill>Large</sl-button>
```
### Circle Buttons
Use the `circle` attribute to create circular icon buttons.
```html preview
<sl-button type="default" size="small" circle><sl-icon name="gear"></sl-icon></sl-button>
<sl-button type="default" size="medium" circle><sl-icon name="gear"></sl-icon></sl-button>
<sl-button type="default" size="large" circle><sl-icon name="gear"></sl-icon></sl-button>
```
### Text Buttons
Use the `text` type to create text buttons that share the same size as regular buttons but don't have backgrounds or borders.
```html preview
<sl-button type="text" size="small">Text</sl-button>
<sl-button type="text" size="medium">Text</sl-button>
<sl-button type="text" size="large">Text</sl-button>
```
### 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 `<a>` under the hood. This gives you all the default link behavior the browser provides (e.g. <kbd>CMD/CTRL/SHIFT + CLICK</kbd>) and exposes the `target` and `download` attributes.
```html preview
<sl-button href="https://example.com/">Link</sl-button>
<sl-button href="https://example.com/" target="_blank">New Window</sl-button>
<sl-button href="/assets/images/wordmark.svg" download="shoelace.svg">Download</sl-button>
<sl-button href="https://example.com/" disabled>Disabled</sl-button>
```
?> 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 setting its `width`. This is useful for making buttons span the full width of their container on smaller screens.
```html preview
<sl-button type="default" size="small" style="width: 100%; margin-bottom: 1rem;">Small</sl-button>
<sl-button type="default" size="medium" style="width: 100%; margin-bottom: 1rem;">Medium</sl-button>
<sl-button type="default" size="large" style="width: 100%;">Large</sl-button>
```
### Prefix and Suffix Icons
Use the `prefix` and `suffix` slots to add icons.
```html preview
<sl-button type="default" size="small">
<sl-icon slot="prefix" name="gear"></sl-icon>
Settings
</sl-button>
<sl-button type="default" size="small">
<sl-icon slot="suffix" name="arrow-counterclockwise"></sl-icon>
Refresh
</sl-button>
<sl-button type="default" size="small">
<sl-icon slot="prefix" name="link-45deg"></sl-icon>
<sl-icon slot="suffix" name="box-arrow-up-right"></sl-icon>
Open
</sl-button>
<br><br>
<sl-button type="default">
<sl-icon slot="prefix" name="gear"></sl-icon>
Settings
</sl-button>
<sl-button type="default">
<sl-icon slot="suffix" name="arrow-counterclockwise"></sl-icon>
Refresh
</sl-button>
<sl-button type="default">
<sl-icon slot="prefix" name="link-45deg"></sl-icon>
<sl-icon slot="suffix" name="box-arrow-up-right"></sl-icon>
Open
</sl-button>
<br><br>
<sl-button type="default" size="large">
<sl-icon slot="prefix" name="gear"></sl-icon>
Settings
</sl-button>
<sl-button type="default" size="large">
<sl-icon slot="suffix" name="arrow-counterclockwise"></sl-icon>
Refresh
</sl-button>
<sl-button type="default" size="large">
<sl-icon slot="prefix" name="link-45deg"></sl-icon>
<sl-icon slot="suffix" name="box-arrow-up-right"></sl-icon>
Open
</sl-button>
```
### Caret
Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover.
```html preview
<sl-button size="small" caret>Small</sl-button>
<sl-button size="medium" caret>Medium</sl-button>
<sl-button size="large" caret>Large</sl-button>
```
### Loading
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. Clicks will be suppressed until the loading state is removed.
```html preview
<sl-button type="default" loading>Default</sl-button>
<sl-button type="primary" loading>Primary</sl-button>
<sl-button type="success" loading>Success</sl-button>
<sl-button type="neutral" loading>Neutral</sl-button>
<sl-button type="warning" loading>Warning</sl-button>
<sl-button type="danger" loading>Danger</sl-button>
```
### Disabled
Use the `disabled` attribute to disable a button. Clicks will be suppressed until the disabled state is removed.
```html preview
<sl-button type="default" disabled>Default</sl-button>
<sl-button type="primary" disabled>Primary</sl-button>
<sl-button type="success" disabled>Success</sl-button>
<sl-button type="neutral" disabled>Neutral</sl-button>
<sl-button type="warning" disabled>Warning</sl-button>
<sl-button type="danger" disabled>Danger</sl-button>
```
### 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 type attribute instead of a class (e.g. `sl-button[type="primary"]`).
```html preview
<sl-button class="pink">Pink Button</sl-button>
<style>
sl-button.pink::part(base) {
/* Set design tokens for height and border width */
--sl-input-height-medium: 48px;
--sl-input-border-width: 4px;
border-radius: 0;
background-color: #ff1493;
border-top-color: #ff7ac1;
border-left-color: #ff7ac1;
border-bottom-color: #ad005c;
border-right-color: #ad005c;
color: white;
font-size: 1.125rem;
box-shadow: 0 2px 10px #0002;
transition: var(--sl-transition-medium) transform ease, var(--sl-transition-medium) border ease;
}
sl-button.pink::part(base):hover {
transform: scale(1.05) rotate(-1deg);
}
sl-button.pink::part(base):active {
border-top-color: #ad005c;
border-right-color: #ff7ac1;
border-bottom-color: #ff7ac1;
border-left-color: #ad005c;
transform: scale(1.05) rotate(-1deg) translateY(2px);
}
sl-button.pink::part(base):focus-visible {
outline: dashed 2px deeppink;
outline-offset: 4px;
}
</style>
```
[component-metadata:sl-button]

144
docs/components/card.md Normal file
View File

@@ -0,0 +1,144 @@
# Card
[component-header:sl-card]
Cards can be used to group related subjects in a container.
```html preview
<sl-card class="card-overview">
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
>
<strong>Mittens</strong><br>
This kitten is as cute as he is playful. Bring him home today!<br>
<small>6 weeks old</small>
<div slot="footer">
<sl-button type="primary" pill>More Info</sl-button>
<sl-rating></sl-rating>
</div>
</sl-card>
<style>
.card-overview {
max-width: 300px;
}
.card-overview small {
color: rgb(var(--sl-color-neutral-500));
}
.card-overview [slot="footer"] {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
## Examples
## Basic Card
Basic cards aren't very exciting, but they can display any content you want them to.
```html preview
<sl-card class="card-basic">
This is just a basic card. No image, no header, and no footer. Just your content.
</sl-card>
<style>
.card-basic {
max-width: 300px;
}
</style>
```
## Card with Header
Headers can be used to display titles and more.
```html preview
<sl-card class="card-header">
<div slot="header">
Header Title
<sl-icon-button name="gear"></sl-icon-button>
</div>
This card has a header. You can put all sorts of things in it!
</sl-card>
<style>
.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);
}
</style>
```
## Card with Footer
Footers can be used to display actions, summaries, or other relevant content.
```html preview
<sl-card class="card-footer">
This card has a footer. You can put all sorts of things in it!
<div slot="footer">
<sl-rating></sl-rating>
<sl-button slot="footer" type="primary">Preview</sl-button>
</div>
</sl-card>
<style>
.card-footer {
max-width: 300px;
}
.card-footer [slot="footer"] {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
## Images
Cards accept an `image` slot. The image is displayed atop the card and stretches to fit.
```html preview
<sl-card class="card-image">
<img
slot="image"
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
alt="A kitten walks towards camera on top of pallet."
>
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
</sl-card>
<style>
.card-image {
max-width: 300px;
}
</style>
```
[component-metadata:sl-card]

View File

@@ -0,0 +1,39 @@
# Checkbox
[component-header:sl-checkbox]
Checkboxes allow the user to toggle an option on or off.
```html preview
<sl-checkbox>Checkbox</sl-checkbox>
```
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
## Examples
### Checked
Use the `checked` attribute to activate the checkbox.
```html preview
<sl-checkbox checked>Checked</sl-checkbox>
```
### Indeterminate
Use the `indeterminate` attribute to make the checkbox indeterminate.
```html preview
<sl-checkbox indeterminate>Indeterminate</sl-checkbox>
```
### Disabled
Use the `disabled` attribute to disable the checkbox.
```html preview
<sl-checkbox disabled>Disabled</sl-checkbox>
```
[component-metadata:sl-checkbox]

View File

@@ -0,0 +1,52 @@
# Color Picker
[component-header:sl-color-picker]
Color pickers allow the user to select a color.
```html preview
<sl-color-picker></sl-color-picker>
```
## Examples
### Opacity
Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, or HSLA based on `format`.
```html preview
<sl-color-picker opacity></sl-color-picker>
```
### Formats
Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, and `hsl`. 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
<sl-color-picker format="hex" value="#4a90e2"></sl-color-picker>
<sl-color-picker format="rgb" value="rgb(80, 227, 194)"></sl-color-picker>
<sl-color-picker format="hsl" value="hsl(290, 87%, 47%)"></sl-color-picker>
```
### Sizes
Use the `size` attribute to change the color picker's trigger size.
```html preview
<sl-color-picker size="small"></sl-color-picker>
<sl-color-picker size="medium"></sl-color-picker>
<sl-color-picker size="large"></sl-color-picker>
```
### Inline
The color picker can be rendered inline instead of in a dropdown using the `inline` attribute.
```html preview
<sl-color-picker inline></sl-color-picker>
```
[component-metadata:sl-color-picker]

View File

@@ -0,0 +1,140 @@
# Context Menu
[component-header:sl-context-menu]
Context menus offer additional options through a menu that opens at the pointer's location, usually activated by a right-click.
Context menus are designed to work with [menus](/components/menu) and [menu items](/components/menu-item). The menu must include `slot="menu"`. Other content you provide will be part of the context menu's target area.
```html preview
<sl-context-menu>
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
## Examples
### Handling Selections
The [menu component](/components/menu) emits an `sl-select` event when a menu item is selected. You can use this to handle selections. The selected item will be available in `event.detail.item`.
```html preview
<div class="context-menu-selections">
<sl-context-menu>
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
</div>
<script>
const container = document.querySelector('.context-menu-selections');
const menu = container.querySelector('sl-menu');
const result = container.querySelector('.result');
menu.addEventListener('sl-select', event => {
console.log(`You selected: ${event.detail.item.value}`);
});
</script>
```
### Inline
The context menu uses `display: contents`, so it will assume the shape of the content you slot in.
```html preview
<sl-context-menu>
<span style="background: rgb(var(--sl-color-neutral-100)); padding: .5rem 1rem;">
Right-click here
</span>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
### Placement
The preferred placement of the context menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the menu remains in the viewport.
```html preview
<sl-context-menu placement="top-end">
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
### Detecting the Target Item
A single context menu can wrap a number of items. To detect the item that activated the context menu...
TODO
```html preview
<div class="context-menu-detecting">
<sl-context-menu>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
</div>
<style>
.context-menu-detecting ul {
max-width: 300px;
list-style: none;
padding: 0;
margin: 0;
}
.context-menu-detecting li {
background: rgb(var(--sl-color-neutral-100));
padding: .5rem 1rem;
margin: 0 0 2px 0;
}
</style>
```
[component-metadata:sl-context-menu]

View File

@@ -0,0 +1,65 @@
# Details
[component-header:sl-details]
Details show a brief summary and expand to show additional content.
```html preview
<sl-details summary="Toggle Me">
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.
</sl-details>
```
## Examples
### Disabled
Use the `disable` attribute to prevent the details from expanding.
```html preview
<sl-details summary="Disabled" disabled>
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.
</sl-details>
```
### 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
<div class="details-group-example">
<sl-details summary="First" open>
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.
</sl-details>
<sl-details summary="Second">
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.
</sl-details>
<sl-details summary="Third">
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.
</sl-details>
</div>
<script>
const container = document.querySelector('.details-group-example');
// Close all other details when one is shown
container.addEventListener('sl-show', event => {
[...container.querySelectorAll('sl-details')].map(details => (details.open = event.target === details));
});
</script>
<style>
.details-group-example sl-details:not(:last-of-type) {
margin-bottom: var(--sl-spacing-2x-small);
}
</style>
```
[component-metadata:sl-details]

133
docs/components/dialog.md Normal file
View File

@@ -0,0 +1,133 @@
# Dialog
[component-header:sl-dialog]
Dialogs, sometimes called "modals", appear above the page and require the user's immediate attention.
```html preview
<sl-dialog label="Dialog" class="dialog-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-dialog>
<sl-button>Open Dialog</sl-button>
<script>
const dialog = document.querySelector('.dialog-overview');
const openButton = dialog.nextElementSibling;
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
openButton.addEventListener('click', () => dialog.show());
closeButton.addEventListener('click', () => dialog.hide());
</script>
```
## UX Tips
- Use a dialog when you immediately require the user's attention, e.g. confirming a destructive action.
- Always provide an obvious way for the user to dismiss the dialog.
- Don't nest dialogs. It almost always leads to a poor experience for the user.
## Examples
### Custom Width
Use the `--width` custom property to set the dialog's width.
```html preview
<sl-dialog label="Dialog" class="dialog-width" style="--width: 50vw;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-dialog>
<sl-button>Open Dialog</sl-button>
<script>
const dialog = document.querySelector('.dialog-width');
const openButton = dialog.nextElementSibling;
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
openButton.addEventListener('click', () => dialog.show());
closeButton.addEventListener('click', () => dialog.hide());
</script>
```
### 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
<sl-dialog label="Dialog" class="dialog-scrolling">
<div style="height: 150vh; border: dashed 2px rgb(var(--sl-color-neutral-200)); padding: 0 1rem;">
<p>Scroll down and give it a try! 👇</p>
</div>
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-dialog>
<sl-button>Open Dialog</sl-button>
<script>
const dialog = document.querySelector('.dialog-scrolling');
const openButton = dialog.nextElementSibling;
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
openButton.addEventListener('click', () => dialog.show());
closeButton.addEventListener('click', () => dialog.hide());
</script>
```
### Preventing the Dialog from Closing
By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the <kbd>Escape</kbd> 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.
```html preview
<sl-dialog label="Dialog" class="dialog-deny-close">
This dialog will not close unless you use the button below.
<sl-button slot="footer" type="primary">Save &amp; Close</sl-button>
</sl-dialog>
<sl-button>Open Dialog</sl-button>
<script>
const dialog = document.querySelector('.dialog-deny-close');
const openButton = dialog.nextElementSibling;
const saveButton = dialog.querySelector('sl-button[slot="footer"]');
openButton.addEventListener('click', () => dialog.show());
saveButton.addEventListener('click', () => dialog.hide());
dialog.addEventListener('sl-request-close', event => event.preventDefault());
</script>
```
### Customizing Initial Focus
By default, the dialog's panel will gain focus when opened. This allows the first tab press to focus on the first tabbable element within the dialog. To set focus on a different element, listen for and cancel the `sl-initial-focus` event.
```html preview
<sl-dialog label="Dialog" class="dialog-focus">
<sl-input placeholder="I will have focus when the dialog is opened"></sl-input>
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-dialog>
<sl-button>Open Dialog</sl-button>
<script>
const dialog = document.querySelector('.dialog-focus');
const input = dialog.querySelector('sl-input');
const openButton = dialog.nextElementSibling;
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
openButton.addEventListener('click', () => dialog.show());
closeButton.addEventListener('click', () => dialog.hide());
dialog.addEventListener('sl-initial-focus', event => {
event.preventDefault();
input.focus({ preventScroll: true });
});
</script>
```
[component-metadata:sl-dialog]

View File

@@ -0,0 +1,71 @@
# Divider
[component-header:sl-divider]
Dividers are used to visually separate or group elements.
```html preview
<sl-divider></sl-divider>
```
## Examples
### Width
Use the `--width` custom property to change the width of the divider.
```html preview
<sl-divider style="--width: 4px;"></sl-divider>
```
### Color
Use the `--color` custom property to change the color of the divider.
```html preview
<sl-divider style="--color: tomato;"></sl-divider>
```
### Spacing
Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements.
```html preview
<div style="text-align: center;">
Above
<sl-divider style="--spacing: 2rem;"></sl-divider>
Below
</div>
```
### 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
<div style="display: flex; align-items: center; height: 2rem;">
First
<sl-divider vertical></sl-divider>
Middle
<sl-divider vertical></sl-divider>
Last
</div>
```
### Menu Dividers
Use dividers in [menus](/components/menu) to visually group menu items.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item value="1">Option 1</sl-menu-item>
<sl-menu-item value="2">Option 2</sl-menu-item>
<sl-menu-item value="3">Option 3</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="4">Option 4</sl-menu-item>
<sl-menu-item value="5">Option 5</sl-menu-item>
<sl-menu-item value="6">Option 6</sl-menu-item>
</sl-menu>
```
[component-metadata:sl-divider]

222
docs/components/drawer.md Normal file
View File

@@ -0,0 +1,222 @@
# Drawer
[component-header:sl-drawer]
Drawers slide in from a container to expose additional options and information.
```html preview
<sl-drawer label="Drawer" class="drawer-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-overview');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
## 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
<sl-drawer label="Drawer" placement="start" class="drawer-placement-start">
This drawer slides in from the start.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-placement-start');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
### Slide in From Top
To make the drawer slide in from the top, set the `placement` attribute to `top`.
```html preview
<sl-drawer label="Drawer" placement="top" class="drawer-placement-top">
This drawer slides in from the top.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-placement-top');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
### Slide in From Bottom
To make the drawer slide in from the bottom, set the `placement` attribute to `bottom`.
```html preview
<sl-drawer label="Drawer" placement="bottom" class="drawer-placement-bottom">
This drawer slides in from the bottom.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-placement-bottom');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
### Contained to an Element
By default, the drawer slides out of its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport. To make the drawer slide out of its parent element, add the `contained` attribute and `position: relative` to the parent.
```html preview
<div
style="position: relative; border: solid 2px rgb(var(--sl-panel-border-color)); height: 300px; padding: 1rem; margin-bottom: 1rem;"
>
The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer opens.
<sl-drawer label="Drawer" contained class="drawer-contained" style="--size: 50%;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
</div>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-contained');
const openButton = drawer.parentElement.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
### 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
<sl-drawer label="Drawer" class="drawer-custom-size" style="--size: 50vw;">
This drawer is always 50% of the viewport.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-custom-size');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
### 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
<sl-drawer label="Drawer" class="drawer-scrolling">
<div style="height: 150vh; border: dashed 2px rgb(var(--sl-color-neutral-200)); padding: 0 1rem;">
<p>Scroll down and give it a try! 👇</p>
</div>
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-scrolling');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
### Preventing the Drawer from Closing
By default, drawers will close when the user clicks the close button, clicks the overlay, or presses the <kbd>Escape</kbd> 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.
```html preview
<sl-drawer label="Drawer" class="drawer-deny-close">
This dialog will not close unless you use the button below.
<sl-button slot="footer" type="primary">Save &amp; Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-deny-close');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
drawer.addEventListener('sl-request-close', event => event.preventDefault());
</script>
```
### Customizing Initial Focus
By default, the drawer's panel will gain focus when opened. This allows the first tab press to focus on the first tabbable element within the drawer. To set focus on a different element, listen for and cancel the `sl-initial-focus` event.
```html preview
<sl-drawer label="Drawer" class="drawer-focus">
<sl-input placeholder="I will have focus when the drawer is opened"></sl-input>
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-focus');
const input = drawer.querySelector('sl-input');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
drawer.addEventListener('sl-initial-focus', event => {
event.preventDefault();
input.focus({ preventScroll: true });
});
</script>
```
[component-metadata:sl-drawer]

151
docs/components/dropdown.md Normal file
View File

@@ -0,0 +1,151 @@
# Dropdown
[component-header:sl-dropdown]
Dropdowns expose additional content that "drops down" in a panel.
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) and [select](/components/select)). The API gives you complete control over showing, hiding, and positioning the panel.
```html preview
<sl-dropdown>
<sl-button slot="trigger" caret>Dropdown</sl-button>
<sl-menu>
<sl-menu-item>Dropdown Item 1</sl-menu-item>
<sl-menu-item>Dropdown Item 2</sl-menu-item>
<sl-menu-item>Dropdown Item 3</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item checked>Checked</sl-menu-item>
<sl-menu-item disabled>Disabled</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>
Prefix
<sl-icon slot="prefix" name="gift"></sl-icon>
</sl-menu-item>
<sl-menu-item>
Suffix Icon
<sl-icon slot="suffix" name="heart"></sl-icon>
</sl-menu-item>
</sl-menu>
</sl-dropdown>
```
## Examples
### Getting the Selected Item
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` 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
<div class="dropdown-selection">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection');
const dropdown = container.querySelector('sl-dropdown');
dropdown.addEventListener('sl-select', event => {
const selectedItem = event.detail.item;
console.log(selectedItem.value);
});
</script>
```
### 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
<sl-dropdown placement="top-start">
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item>Cut</sl-menu-item>
<sl-menu-item>Copy</sl-menu-item>
<sl-menu-item>Paste</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>Find</sl-menu-item>
<sl-menu-item>Replace</sl-menu-item>
</sl-menu>
</sl-dropdown>
```
### Distance
The distance from the panel to the trigger can be customized using the `distance` attribute. This value is specified in pixels.
```html preview
<sl-dropdown distance="30">
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item>Cut</sl-menu-item>
<sl-menu-item>Copy</sl-menu-item>
<sl-menu-item>Paste</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>Find</sl-menu-item>
<sl-menu-item>Replace</sl-menu-item>
</sl-menu>
</sl-dropdown>
```
### Skidding
The offset of the panel along the trigger can be customized using the `skidding` attribute. This value is specified in pixels.
```html preview
<sl-dropdown skidding="30">
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item>Cut</sl-menu-item>
<sl-menu-item>Copy</sl-menu-item>
<sl-menu-item>Paste</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>Find</sl-menu-item>
<sl-menu-item>Replace</sl-menu-item>
</sl-menu>
</sl-dropdown>
```
### 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, 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
<div class="dropdown-hoist">
<sl-dropdown>
<sl-button slot="trigger" caret>No Hoist</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
<sl-menu-item>Item 2</sl-menu-item>
<sl-menu-item>Item 3</sl-menu-item>
</sl-menu>
</sl-dropdown>
<sl-dropdown hoist>
<sl-button slot="trigger" caret>Hoist</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
<sl-menu-item>Item 2</sl-menu-item>
<sl-menu-item>Item 3</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<style>
.dropdown-hoist {
border: solid 2px rgb(var(--sl-panel-border-color));
padding: var(--sl-spacing-medium);
overflow: hidden;
}
</style>
```
[component-metadata:sl-dropdown]

215
docs/components/form.md Normal file
View File

@@ -0,0 +1,215 @@
# Form
[component-header:sl-form]
Forms collect data that can easily be processed and sent to a server.
All Shoelace components make use of a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate markup, styles, and behavior. One caveat of this approach is that native `<form>` elements will not recognize Shoelace form controls.
This component solves that problem by serializing _both_ Shoelace form controls and native form controls when the form is submitted. The resulting form data is exposed in the `sl-submit` event as a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object in `event.detail.formData`. You can also find an array of form controls in `event.detail.formControls`.
Shoelace forms don't make use of `action` and `method` attributes and they don't submit the same way as native forms. To handle submission, you need to listen for the `sl-submit` event as shown in the example below and make an XHR request with the resulting form data.
```html preview
<sl-form class="form-overview">
<sl-input name="name" type="text" label="Name"></sl-input>
<br>
<sl-select name="favorite" label="Select your favorite">
<sl-menu-item value="birds">Birds</sl-menu-item>
<sl-menu-item value="cats">Cats</sl-menu-item>
<sl-menu-item value="dogs">Dogs</sl-menu-item>
</sl-select>
<br>
<sl-checkbox name="agree" value="yes">
I totally agree
</sl-checkbox>
<br><br>
<sl-button submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.form-overview');
// Watch for the slSubmit event
form.addEventListener('sl-submit', event => {
const formData = event.detail.formData;
let output = '';
//
// Example 1: Post data to a server and wait for a JSON response
//
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
console.log('Success:', result);
})
.catch(error => {
console.error('Error:', error);
});
//
// Example 2: Output all form control names + values
//
for (const entry of formData.entries()) {
output += `${entry[0]}: ${entry[1]}\n`;
}
alert(output);
//
// Example 3: Get all form controls that were serialized as
// an array of HTML elements
//
console.log(event.detail.formControls);
});
</script>
```
## Form Control Validation
Client-side validation can be enabled through the browser's [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) for many form controls. You can enable it using props such as `required`, `pattern`, `minlength`, and `maxlength`. As the user interacts with the form control, the `invalid` attribute will reflect its validity based on its current value and the constraints that have been defined.
When a form control is invalid, the containing form will not be submitted. Instead, the browser will show the user a relevant error message. If you don't want to use client-side validation, you can suppress this behavior by adding `novalidate` to the `<sl-form>` element.
All form controls support validation, but not all validation props are available for every component. Refer to a component's documentation to see which validation props it supports.
!> Client-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. **You should always validate and sanitize user input on the server!**
### Required Fields
To make a field required, use the `required` prop. The form will not be submitted if a required form control is empty.
```html preview
<sl-form class="input-validation-required">
<sl-input name="name" label="Name" required></sl-input>
<br>
<sl-select label="Favorite Animal" clearable required>
<sl-menu-item value="birds">Birds</sl-menu-item>
<sl-menu-item value="cats">Cats</sl-menu-item>
<sl-menu-item value="dogs">Dogs</sl-menu-item>
<sl-menu-item value="other">Other</sl-menu-item>
</sl-select>
<br>
<sl-textarea name="comment" label="Comment" required></sl-textarea>
<br>
<sl-checkbox required>Check me before submitting</sl-checkbox>
<br><br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-required');
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
</script>
```
### Input Patterns
To restrict a value to a specific [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern), use the `pattern` attribute. This example only allows the letters A-Z, so the form will not submit if a number or symbol is entered. This only works with `<sl-input>` elements.
```html preview
<sl-form class="input-validation-pattern">
<sl-input name="letters" required label="Letters" pattern="[A-Za-z]+"></sl-input>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-pattern');
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
</script>
```
### Input Types
Some input types will automatically trigger constraints, such as `email` and `url`.
```html preview
<sl-form class="input-validation-type">
<sl-input type="email" label="Email" placeholder="you@example.com" required></sl-input>
<br>
<sl-input type="url" label="URL" placeholder="https://example.com/" required></sl-input>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-type');
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
</script>
```
### Custom Validation
To create a custom validation error, use the `setCustomValidity` method. The form will not be submitted when this method is called with anything other than an empty string, and its message will be shown by the browser as the validation error. To make the input valid again, call the method a second time with an empty string as the argument.
```html preview
<sl-form class="input-validation-custom">
<sl-input label="Type 'shoelace'" required></sl-input>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-custom');
const input = form.querySelector('sl-input');
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
input.addEventListener('sl-input', () => {
if (input.value === 'shoelace') {
input.setCustomValidity('');
} else {
input.setCustomValidity('Hey, you\'re supposed to type \'shoelace\' before submitting this!');
}
});
</script>
```
### Custom Validation Styles
The `invalid` attribute reflects the form control's validity, so you can style invalid fields using the `[invalid]` selector. The example below demonstrates how you can give erroneous fields a different appearance. Type something other than "shoelace" to demonstrate this.
```html preview
<sl-input class="custom-input" required pattern="shoelace">
<small slot="help-text">Please enter "shoelace" to continue</small>
</sl-input>
<style>
.custom-input[invalid]:not([disabled])::part(label),
.custom-input[invalid]:not([disabled])::part(help-text) {
color: rgb(var(--sl-color-danger-600));
}
.custom-input[invalid]:not([disabled])::part(base) {
border-color: rgb(var(--sl-color-danger-500));
}
.custom-input[invalid]:focus-within::part(base) {
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-danger-500) / var(--sl-focus-ring-alpha));
}
</style>
```
### Third-party Validation
To opt out of the browser's built-in validation and use your own, add the `novalidate` attribute to the form. This will ignore all constraints and prevent the browser from showing its own warnings when form controls are invalid.
Remember that the `invalid` attribute on form controls reflects validity as defined by the [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). You can set it initially, but the `invalid` attribute will update as the user interacts with the form control. As such, you should not rely on it to set invalid styles using a custom validation library.
Instead, toggle a class and target it in your stylesheet as shown below.
```html
<sl-form novalidate>
<sl-input class="invalid"></sl-input>
</sl-form>
<style>
sl-input.invalid {
...
}
</style>
```
[component-metadata:sl-form]

View File

@@ -0,0 +1,58 @@
# Format Bytes
[component-header:sl-format-bytes]
Formats a number as a human readable bytes value.
```html preview
<div class="format-bytes-overview">
The file is <sl-format-bytes value="1000"></sl-format-bytes> in size.
<br><br>
<sl-input type="number" value="1000" label="Number to Format" style="max-width: 180px;"></sl-input>
</div>
<script>
const container = document.querySelector('.format-bytes-overview');
const formatter = container.querySelector('sl-format-bytes');
const input = container.querySelector('sl-input');
input.addEventListener('sl-input', () => formatter.value = input.value || 0);
</script>
```
## Examples
### Formatting Bytes
Set the `value` attribute to a number to get the value in bytes.
```html preview
<sl-format-bytes value="12"></sl-format-bytes><br>
<sl-format-bytes value="1200"></sl-format-bytes><br>
<sl-format-bytes value="1200000"></sl-format-bytes><br>
<sl-format-bytes value="1200000000"></sl-format-bytes>
```
### Formatting Bits
To get the value in bits, set the `unit` attribute to `bits`.
```html preview
<sl-format-bytes value="12" unit="bits"></sl-format-bytes><br>
<sl-format-bytes value="1200" unit="bits"></sl-format-bytes><br>
<sl-format-bytes value="1200000" unit="bits"></sl-format-bytes><br>
<sl-format-bytes value="1200000000" unit="bits"></sl-format-bytes>
```
### Localization
Use the `locale` attribute to set the number formatting locale.
```html preview
<sl-format-bytes value="12" locale="de"></sl-format-bytes><br>
<sl-format-bytes value="1200" locale="de"></sl-format-bytes><br>
<sl-format-bytes value="1200000" locale="de"></sl-format-bytes><br>
<sl-format-bytes value="1200000000" locale="de"></sl-format-bytes>
```
[component-metadata:sl-format-bytes]

View File

@@ -0,0 +1,64 @@
# Format Date
[component-header:sl-format-date]
Formats a date/time using the specified locale and options.
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
<!-- Shoelace 2 release date 🎉 -->
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
```
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.
?> 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
<!-- Human-readable date -->
<sl-format-date month="long" day="numeric" year="numeric"></sl-format-date><br>
<!-- Time -->
<sl-format-date hour="numeric" minute="numeric"></sl-format-date><br>
<!-- Weekday -->
<sl-format-date weekday="long"></sl-format-date><br>
<!-- Month -->
<sl-format-date month="long"></sl-format-date><br>
<!-- Year -->
<sl-format-date year="numeric"></sl-format-date><br>
<!-- No formatting options -->
<sl-format-date></sl-format-date>
```
### 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
<sl-format-date hour="numeric" minute="numeric" hour-format="12"></sl-format-date><br>
<sl-format-date hour="numeric" minute="numeric" hour-format="24"></sl-format-date>
```
### Localization
Use the `locale` attribute to set the date/time formatting locale.
```html preview
English: <sl-format-date locale="en"></sl-format-date><br>
French: <sl-format-date locale="fr"></sl-format-date><br>
Russian: <sl-format-date locale="ru"></sl-format-date><br>
```
[component-metadata:sl-format-date]

View File

@@ -0,0 +1,61 @@
# Format Number
[component-header:sl-format-number]
Formats a number using the specified locale and options.
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
<div class="format-number-overview">
<sl-format-number value="1000"></sl-format-number>
<br><br>
<sl-input type="number" value="1000" label="Number to Format" style="max-width: 180px;"></sl-input>
</div>
<script>
const container = document.querySelector('.format-number-overview');
const formatter = container.querySelector('sl-format-number');
const input = container.querySelector('sl-input');
input.addEventListener('sl-input', () => formatter.value = input.value || 0);
</script>
```
## Examples
### Percentages
To get the value as a percent, set the `type` attribute to `percent`.
```html preview
<sl-format-number type="percent" value="0"></sl-format-number><br>
<sl-format-number type="percent" value=".25"></sl-format-number><br>
<sl-format-number type="percent" value=".50"></sl-format-number><br>
<sl-format-number type="percent" value=".75"></sl-format-number><br>
<sl-format-number type="percent" value="1"></sl-format-number>
```
### Localization
Use the `locale` attribute to set the number formatting locale.
```html preview
English: <sl-format-number value="2000" locale="en" minimum-fraction-digits="2"></sl-format-number><br>
German: <sl-format-number value="2000" locale="de" minimum-fraction-digits="2"></sl-format-number><br>
Russian: <sl-format-number value="2000" locale="ru" minimum-fraction-digits="2"></sl-format-number>
```
### 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 `locale` to ensure the the number is formatted correctly for the target locale.
```html preview
<sl-format-number type="currency" currency="USD" value="2000" locale="en-US"></sl-format-number><br>
<sl-format-number type="currency" currency="GBP" value="2000" locale="en-GB"></sl-format-number><br>
<sl-format-number type="currency" currency="EUR" value="2000" locale="de"></sl-format-number><br>
<sl-format-number type="currency" currency="RUB" value="2000" locale="ru"></sl-format-number><br>
<sl-format-number type="currency" currency="CNY" value="2000" locale="zh-cn"></sl-format-number>
```
[component-metadata:sl-format-number]

View File

@@ -0,0 +1,78 @@
# Icon Button
[component-header:sl-icon-button]
Icons buttons are simple, icon-only buttons that can be used for actions and in toolbars.
For a full list of icons that come bundled with Shoelace, refer to the [icon component](/components/icon).
```html preview
<sl-icon-button name="gear" label="Settings"></sl-icon-button>
```
## Examples
### Sizes
Icon buttons inherit their parent element's `font-size`.
```html preview
<sl-icon-button name="pencil" label="Edit" style="font-size: 1.5rem;"></sl-icon-button>
<sl-icon-button name="pencil" label="Edit" style="font-size: 2rem;"></sl-icon-button>
<sl-icon-button name="pencil" label="Edit" style="font-size: 2.5rem;"></sl-icon-button>
```
### 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
<div class="icon-button-color">
<sl-icon-button name="type-bold" label="Bold"></sl-icon-button>
<sl-icon-button name="type-italic" label="Italic"></sl-icon-button>
<sl-icon-button name="type-underline" label="Underline"></sl-icon-button>
</div>
<style>
.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;
}
</style>
```
### Link Buttons
Use the `href` attribute to convert the button to a link.
```html preview
<sl-icon-button name="gear" label="Settings" href="https://example.com" target="_blank"></sl-icon-button>
```
### Icon Button with Tooltip
Wrap a tooltip around an icon button to provide contextual information to the user.
```html preview
<sl-tooltip content="Settings">
<sl-icon-button name="gear" label="Settings"></sl-icon-button>
</sl-tooltip>
```
### Disabled
Use the `disabled` attribute to disable the icon button.
```html preview
<sl-icon-button name="gear" label="Settings" disabled></sl-icon-button>
```
[component-metadata:sl-icon-button]

View File

@@ -1,19 +1,12 @@
---
meta:
title: Icon
description: Icons are symbols that can be used to represent various options within an application.
layout: component
---
# Icon
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.
[component-header:sl-icon]
:::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.
:::
Icons are symbols that can be used to represent various options within an application.
## Default Icons
Shoelace comes bundled with over 1,300 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.
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.
Click or tap on an icon below to copy its name and use it like this.
```html
<sl-icon name="icon-name-here"></sl-icon>
@@ -25,97 +18,28 @@ All available icons in the `default` icon library are shown below. Click or tap
<sl-icon slot="prefix" name="search"></sl-icon>
</sl-input>
<sl-select value="outline">
<sl-option value="outline">Outlined</sl-option>
<sl-option value="fill">Filled</sl-option>
<sl-option value="all">All icons</sl-option>
<sl-menu-item value="outline">Outlined</sl-menu-item>
<sl-menu-item value="fill">Filled</sl-menu-item>
<sl-menu-item value="all">All icons</sl-menu-item>
</sl-select>
</div>
<div class="icon-list"></div>
<input type="text" class="icon-copy-input" aria-hidden="true" tabindex="-1">
<input type="text" class="icon-copy-input">
</div>
## Examples
### Colors
Icons inherit their color from the current text color. Thus, you can set the `color` property on the `<sl-icon>` element or an ancestor to change the color.
```html:preview
<div style="color: #4a90e2;">
<sl-icon name="exclamation-triangle"></sl-icon>
<sl-icon name="archive"></sl-icon>
<sl-icon name="battery-charging"></sl-icon>
<sl-icon name="bell"></sl-icon>
</div>
<div style="color: #9013fe;">
<sl-icon name="clock"></sl-icon>
<sl-icon name="cloud"></sl-icon>
<sl-icon name="download"></sl-icon>
<sl-icon name="file-earmark"></sl-icon>
</div>
<div style="color: #417505;">
<sl-icon name="flag"></sl-icon>
<sl-icon name="heart"></sl-icon>
<sl-icon name="image"></sl-icon>
<sl-icon name="lightning"></sl-icon>
</div>
<div style="color: #f5a623;">
<sl-icon name="mic"></sl-icon>
<sl-icon name="search"></sl-icon>
<sl-icon name="star"></sl-icon>
<sl-icon name="trash"></sl-icon>
</div>
```
{% raw %}
```jsx:react
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
<div style={{ color: '#4a90e2' }}>
<SlIcon name="exclamation-triangle"></SlIcon>
<SlIcon name="archive"></SlIcon>
<SlIcon name="battery-charging"></SlIcon>
<SlIcon name="bell"></SlIcon>
</div>
<div style={{ color: '#9013fe' }}>
<SlIcon name="clock"></SlIcon>
<SlIcon name="cloud"></SlIcon>
<SlIcon name="download"></SlIcon>
<SlIcon name="file-earmark"></SlIcon>
</div>
<div style={{ color: '#417505' }}>
<SlIcon name="flag"></SlIcon>
<SlIcon name="heart"></SlIcon>
<SlIcon name="image"></SlIcon>
<SlIcon name="lightning"></SlIcon>
</div>
<div style={{ color: '#f5a623' }}>
<SlIcon name="mic"></SlIcon>
<SlIcon name="search"></SlIcon>
<SlIcon name="star"></SlIcon>
<SlIcon name="trash"></SlIcon>
</div>
</>
);
```
{% 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
```html preview
<div style="font-size: 32px;">
<sl-icon name="exclamation-triangle"></sl-icon>
<sl-icon name="archive"></sl-icon>
<sl-icon name="battery-charging"></sl-icon>
<sl-icon name="bell"></sl-icon>
<sl-icon name="clock"></sl-icon>
<sl-icon name="cloud"></sl-icon>
<sl-icon name="download"></sl-icon>
<sl-icon name="file-earmark"></sl-icon>
<sl-icon name="flag"></sl-icon>
@@ -126,70 +50,18 @@ Icons are sized relative to the current font size. To change their size, set the
<sl-icon name="search"></sl-icon>
<sl-icon name="star"></sl-icon>
<sl-icon name="trash"></sl-icon>
<sl-icon name="x-circle"></sl-icon>
</div>
```
{% raw %}
```jsx:react
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<div style={{ fontSize: '32px' }}>
<SlIcon name="exclamation-triangle" />
<SlIcon name="archive" />
<SlIcon name="battery-charging" />
<SlIcon name="bell" />
<SlIcon name="clock" />
<SlIcon name="cloud" />
<SlIcon name="download" />
<SlIcon name="file-earmark" />
<SlIcon name="flag" />
<SlIcon name="heart" />
<SlIcon name="image" />
<SlIcon name="lightning" />
<SlIcon name="mic" />
<SlIcon name="search" />
<SlIcon name="star" />
<SlIcon name="trash" />
</div>
);
```
{% endraw %}
### Labels
For non-decorative icons, use the `label` attribute to announce it to assistive devices.
```html:preview
<sl-icon name="star-fill" label="Add to favorites"></sl-icon>
```
```jsx:react
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => <SlIcon name="star-fill" label="Add to favorites" />;
```
### 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
<sl-icon src="https://shoelace.style/assets/images/shoe.svg" style="font-size: 8rem;"></sl-icon>
```html preview
<sl-icon src="/assets/images/shoe.svg" style="font-size: 8rem;"></sl-icon>
```
{% raw %}
```jsx:react
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => <SlIcon src="https://shoelace.style/assets/images/shoe.svg" style={{ fontSize: '8rem' }}></SlIcon>;
```
{% endraw %}
## Icon Libraries
You can register additional icons to use with the `<sl-icon>` 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.
@@ -230,7 +102,7 @@ This will register the [Boxicons](https://boxicons.com/) library using the jsDel
Icons in this library are licensed under the [Creative Commons 4.0 License](https://github.com/atisawd/boxicons#license).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -241,7 +113,7 @@ Icons in this library are licensed under the [Creative Commons 4.0 License](http
if (name.substring(0, 4) === 'bxl-') folder = 'logos';
return `https://cdn.jsdelivr.net/npm/boxicons@2.0.5/svg/${folder}/${name}.svg`;
},
mutator: svg => svg.setAttribute('fill', 'currentColor')
mutator:svg => svg.setAttribute('fill', 'currentColor')
});
</script>
@@ -252,14 +124,14 @@ Icons in this library are licensed under the [Creative Commons 4.0 License](http
<sl-icon library="boxicons" name="bx-save"></sl-icon>
<sl-icon library="boxicons" name="bx-server"></sl-icon>
<sl-icon library="boxicons" name="bx-wine"></sl-icon>
<br />
<br>
<sl-icon library="boxicons" name="bxs-bot"></sl-icon>
<sl-icon library="boxicons" name="bxs-cookie"></sl-icon>
<sl-icon library="boxicons" name="bxs-joystick"></sl-icon>
<sl-icon library="boxicons" name="bxs-save"></sl-icon>
<sl-icon library="boxicons" name="bxs-server"></sl-icon>
<sl-icon library="boxicons" name="bxs-wine"></sl-icon>
<br />
<br>
<sl-icon library="boxicons" name="bxl-apple"></sl-icon>
<sl-icon library="boxicons" name="bxl-chrome"></sl-icon>
<sl-icon library="boxicons" name="bxl-edge"></sl-icon>
@@ -269,27 +141,27 @@ Icons in this library are licensed under the [Creative Commons 4.0 License](http
</div>
```
### Lucide
### Feather Icons
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.
This will register the [Feather Icons](https://feathericons.com/) library using the jsDelivr CDN.
Icons in this library are licensed under the [MIT License](https://github.com/lucide-icons/lucide/blob/master/LICENSE).
Icons in this library are licensed under the [MIT License](https://github.com/feathericons/feather/blob/master/LICENSE).
```html:preview
```html preview
<div style="font-size: 24px;">
<sl-icon library="lucide" name="feather"></sl-icon>
<sl-icon library="lucide" name="pie-chart"></sl-icon>
<sl-icon library="lucide" name="settings"></sl-icon>
<sl-icon library="lucide" name="map-pin"></sl-icon>
<sl-icon library="lucide" name="printer"></sl-icon>
<sl-icon library="lucide" name="shopping-cart"></sl-icon>
<sl-icon library="feather" name="feather"></sl-icon>
<sl-icon library="feather" name="pie-chart"></sl-icon>
<sl-icon library="feather" name="settings"></sl-icon>
<sl-icon library="feather" name="map-pin"></sl-icon>
<sl-icon library="feather" name="printer"></sl-icon>
<sl-icon library="feather" name="shopping-cart"></sl-icon>
</div>
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
registerIconLibrary('lucide', {
resolver: name => `https://cdn.jsdelivr.net/npm/lucide-static@0.16.29/icons/${name}.svg`
registerIconLibrary('feather', {
resolver: name => `https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/icons/${name}.svg`
});
</script>
```
@@ -300,7 +172,7 @@ This will register the [Font Awesome Free](https://fontawesome.com/) library usi
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
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -323,20 +195,20 @@ Icons in this library are licensed under the [Font Awesome Free License](https:/
<sl-icon library="fa" name="far-hdd"></sl-icon>
<sl-icon library="fa" name="far-heart"></sl-icon>
<sl-icon library="fa" name="far-star"></sl-icon>
<br />
<br>
<sl-icon library="fa" name="fas-archive"></sl-icon>
<sl-icon library="fa" name="fas-book"></sl-icon>
<sl-icon library="fa" name="fas-chess-knight"></sl-icon>
<sl-icon library="fa" name="fas-dice"></sl-icon>
<sl-icon library="fa" name="fas-pizza-slice"></sl-icon>
<sl-icon library="fa" name="fas-scroll"></sl-icon>
<br />
<br>
<sl-icon library="fa" name="fab-apple"></sl-icon>
<sl-icon library="fa" name="fab-chrome"></sl-icon>
<sl-icon library="fa" name="fab-edge"></sl-icon>
<sl-icon library="fa" name="fab-firefox"></sl-icon>
<sl-icon library="fa" name="fab-opera"></sl-icon>
<sl-icon library="fa" name="fab-microsoft"></sl-icon>
<sl-icon library="fa" name="fab-microsoft"></sl-icon>
</div>
```
@@ -346,22 +218,22 @@ This will register the [Heroicons](https://heroicons.com/) library using the jsD
Icons in this library are licensed under the [MIT License](https://github.com/tailwindlabs/heroicons/blob/master/LICENSE).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
registerIconLibrary('heroicons', {
resolver: name => `https://cdn.jsdelivr.net/npm/heroicons@2.0.1/24/outline/${name}.svg`
resolver: name => `https://cdn.jsdelivr.net/npm/heroicons@0.4.2/outline/${name}.svg`
});
</script>
<div style="font-size: 24px;">
<sl-icon library="heroicons" name="chat-bubble-left"></sl-icon>
<sl-icon library="heroicons" name="chat"></sl-icon>
<sl-icon library="heroicons" name="cloud"></sl-icon>
<sl-icon library="heroicons" name="cog"></sl-icon>
<sl-icon library="heroicons" name="document-text"></sl-icon>
<sl-icon library="heroicons" name="gift"></sl-icon>
<sl-icon library="heroicons" name="speaker-wave"></sl-icon>
<sl-icon library="heroicons" name="volume-up"></sl-icon>
</div>
```
@@ -371,7 +243,7 @@ This will register the [Iconoir](https://iconoir.com/) library using the jsDeliv
Icons in this library are licensed under the [MIT License](https://github.com/lucaburgio/iconoir/blob/master/LICENSE).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -396,7 +268,7 @@ This will register the [Ionicons](https://ionicons.com/) library using the jsDel
Icons in this library are licensed under the [MIT License](https://github.com/ionic-team/ionicons/blob/master/LICENSE).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -418,14 +290,14 @@ Icons in this library are licensed under the [MIT License](https://github.com/io
<sl-icon library="ionicons" name="chatbubble"></sl-icon>
<sl-icon library="ionicons" name="settings"></sl-icon>
<sl-icon library="ionicons" name="warning"></sl-icon>
<br />
<br>
<sl-icon library="ionicons" name="alarm-outline"></sl-icon>
<sl-icon library="ionicons" name="american-football-outline"></sl-icon>
<sl-icon library="ionicons" name="bug-outline"></sl-icon>
<sl-icon library="ionicons" name="chatbubble-outline"></sl-icon>
<sl-icon library="ionicons" name="settings-outline"></sl-icon>
<sl-icon library="ionicons" name="warning-outline"></sl-icon>
<br />
<br>
<sl-icon library="ionicons" name="alarm-sharp"></sl-icon>
<sl-icon library="ionicons" name="american-football-sharp"></sl-icon>
<sl-icon library="ionicons" name="bug-sharp"></sl-icon>
@@ -441,7 +313,7 @@ This will register the [Jam Icons](https://jam-icons.com/) library using the jsD
Icons in this library are licensed under the [MIT License](https://github.com/michaelampr/jam/blob/master/LICENSE).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -458,7 +330,7 @@ Icons in this library are licensed under the [MIT License](https://github.com/mi
<sl-icon library="jam" name="leaf"></sl-icon>
<sl-icon library="jam" name="picture"></sl-icon>
<sl-icon library="jam" name="set-square"></sl-icon>
<br />
<br>
<sl-icon library="jam" name="calendar-f"></sl-icon>
<sl-icon library="jam" name="camera-f"></sl-icon>
<sl-icon library="jam" name="filter-f"></sl-icon>
@@ -474,7 +346,7 @@ This will register the [Material Icons](https://material.io/resources/icons/?sty
Icons in this library are licensed under the [Apache 2.0 License](https://github.com/google/material-design-icons/blob/master/LICENSE).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -494,14 +366,14 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
<sl-icon library="material" name="volume_up"></sl-icon>
<sl-icon library="material" name="settings"></sl-icon>
<sl-icon library="material" name="shopping_basket"></sl-icon>
<br />
<br>
<sl-icon library="material" name="notifications_round"></sl-icon>
<sl-icon library="material" name="email_round"></sl-icon>
<sl-icon library="material" name="delete_round"></sl-icon>
<sl-icon library="material" name="volume_up_round"></sl-icon>
<sl-icon library="material" name="settings_round"></sl-icon>
<sl-icon library="material" name="shopping_basket_round"></sl-icon>
<br />
<br>
<sl-icon library="material" name="notifications_sharp"></sl-icon>
<sl-icon library="material" name="email_sharp"></sl-icon>
<sl-icon library="material" name="delete_sharp"></sl-icon>
@@ -517,7 +389,7 @@ This will register the [Remix Icon](https://remixicon.com/) library using the js
Icons in this library are licensed under the [Apache 2.0 License](https://github.com/Remix-Design/RemixIcon/blob/master/License).
```html:preview
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
@@ -538,45 +410,13 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
<sl-icon library="remixicon" name="development/bug-line"></sl-icon>
<sl-icon library="remixicon" name="media/image-line"></sl-icon>
<sl-icon library="remixicon" name="system/alert-line"></sl-icon>
<br />
<br>
<sl-icon library="remixicon" name="business/cloud-fill"></sl-icon>
<sl-icon library="remixicon" name="design/brush-fill"></sl-icon>
<sl-icon library="remixicon" name="business/pie-chart-fill"></sl-icon>
<sl-icon library="remixicon" name="development/bug-fill"></sl-icon>
<sl-icon library="remixicon" name="media/image-fill"></sl-icon>
<sl-icon library="remixicon" name="system/alert-fill"></sl-icon>
</div>
```
### 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
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
registerIconLibrary('tabler', {
resolver: name => `https://cdn.jsdelivr.net/npm/@tabler/icons@1.68.0/icons/${name}.svg`
});
</script>
<div style="font-size: 24px;">
<sl-icon library="tabler" name="alert-triangle"></sl-icon>
<sl-icon library="tabler" name="arrow-back"></sl-icon>
<sl-icon library="tabler" name="at"></sl-icon>
<sl-icon library="tabler" name="ball-baseball"></sl-icon>
<sl-icon library="tabler" name="cake"></sl-icon>
<sl-icon library="tabler" name="files"></sl-icon>
<br />
<sl-icon library="tabler" name="keyboard"></sl-icon>
<sl-icon library="tabler" name="moon"></sl-icon>
<sl-icon library="tabler" name="pig"></sl-icon>
<sl-icon library="tabler" name="printer"></sl-icon>
<sl-icon library="tabler" name="ship"></sl-icon>
<sl-icon library="tabler" name="toilet-paper"></sl-icon>
<sl-icon library="remixicon" name="system/alert-fill"></sl-icon>
</div>
```
@@ -586,16 +426,14 @@ This will register the [Unicons](https://iconscout.com/unicons) library using th
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
```html preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
registerIconLibrary('unicons', {
resolver: name => {
const match = name.match(/^(.*?)(-s)?$/);
return `https://cdn.jsdelivr.net/npm/@iconscout/unicons@3.0.3/svg/${match[2] === '-s' ? 'solid' : 'line'}/${
match[1]
}.svg`;
return `https://cdn.jsdelivr.net/npm/@iconscout/unicons@3.0.3/svg/${match[2] === '-s' ? 'solid' : 'line'}/${match[1]}.svg`;
},
mutator: svg => svg.setAttribute('fill', 'currentColor')
});
@@ -608,12 +446,12 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
<sl-icon library="unicons" name="polygon"></sl-icon>
<sl-icon library="unicons" name="rocket"></sl-icon>
<sl-icon library="unicons" name="star"></sl-icon>
<br />
<br>
<sl-icon library="unicons" name="clock-s"></sl-icon>
<sl-icon library="unicons" name="graph-bar-s"></sl-icon>
<sl-icon library="unicons" name="padlock-s"></sl-icon>
<sl-icon library="unicons" name="polygon-s"></sl-icon>
<sl-icon library="unicons" name="rocket-s"></sl-icon>
<sl-icon library="unicons" name="rocket-s"></sl-icon>
<sl-icon library="unicons" name="star-s"></sl-icon>
</div>
```
@@ -634,37 +472,6 @@ This example will load the same set of icons from the jsDelivr CDN instead of yo
</script>
```
#### 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 `<use>` elements located in the `<sl-icon>` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements. For this reason, sprite sheets should only be used if you're self-hosting them.
:::
```html:preview
<script type="module">
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
registerIconLibrary('sprite', {
resolver: name => `/assets/images/sprite.svg#${name}`,
mutator: svg => svg.setAttribute('fill', 'currentColor'),
spriteSheet: true
});
</script>
<div style="font-size: 24px;">
<sl-icon library="sprite" name="clock"></sl-icon>
<sl-icon library="sprite" name="speedometer"></sl-icon>
</div>
```
### 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.
@@ -683,21 +490,8 @@ If you want to change the icons Shoelace uses internally, you can register an ic
<!-- Supporting scripts and styles for the search utility -->
<script>
function wrapWithTooltip(item) {
const tooltip = document.createElement('sl-tooltip');
tooltip.content = item.getAttribute('data-name');
// Close open tooltips
document.querySelectorAll('.icon-list sl-tooltip[open]').forEach(tooltip => tooltip.hide());
// Wrap it with a tooltip and trick it into showing up
item.parentNode.insertBefore(tooltip, item);
tooltip.appendChild(item);
requestAnimationFrame(() => tooltip.dispatchEvent(new MouseEvent('mouseover')));
}
fetch('/dist/assets/icons/icons.json')
.then(res => res.json())
.then(res => res.json())
.then(icons => {
const container = document.querySelector('.icon-search');
const input = container.querySelector('sl-input');
@@ -715,27 +509,23 @@ If you want to change the icons Shoelace uses internally, you can register an ic
item.setAttribute('data-name', i.name);
item.setAttribute('data-terms', [i.name, i.title, ...(i.tags || []), ...(i.categories || [])].join(' '));
item.innerHTML = `
<svg width="1em" height="1em" fill="currentColor">
<use href="/assets/images/sprite.svg#${i.name}"></use>
</svg>
<svg width="1em" height="1em">
<use xlink:href="/assets/icons/sprite.svg#${i.name}"></use>
</svg>
`;
list.appendChild(item);
// Wrap it with a tooltip the first time the mouse lands on it. We do this instead of baking them into the DOM
// to improve this page's performance. See: https://github.com/shoelace-style/shoelace/issues/1122
item.addEventListener('mouseover', () => wrapWithTooltip(item), { once: true });
const tooltip = document.createElement('sl-tooltip');
tooltip.content = i.name;
tooltip.appendChild(item);
list.appendChild(tooltip);
// Copy on click
item.addEventListener('click', () => {
const tooltip = item.closest('sl-tooltip');
copyInput.value = i.name;
copyInput.select();
document.execCommand('copy');
if (tooltip) {
tooltip.content = 'Copied!';
setTimeout(() => tooltip.content = i.name, 1000);
}
tooltip.content = 'Copied!';
setTimeout(() => tooltip.content = i.name, 1000);
});
});
@@ -756,19 +546,19 @@ If you want to change the icons Shoelace uses internally, you can register an ic
});
// Sort by type and remember preference
const iconType = sessionStorage.getItem('sl-icon:type') || 'outline';
const iconType = localStorage.getItem('sl-icon:type') || 'outline';
select.value = iconType;
list.setAttribute('data-type', select.value);
select.addEventListener('sl-change', () => {
list.setAttribute('data-type', select.value);
sessionStorage.setItem('sl-icon:type', select.value);
localStorage.setItem('sl-icon:type', select.value);
});
});
</script>
<style>
.icon-search {
border: solid 1px var(--sl-panel-border-color);
border: solid 1px rgb(var(--sl-panel-border-color));
border-radius: var(--sl-border-radius-medium);
padding: var(--sl-spacing-medium);
}
@@ -824,8 +614,8 @@ If you want to change the icons Shoelace uses internally, you can register an ic
}
.icon-list-item:hover {
background-color: var(--sl-color-primary-50);
color: var(--sl-color-primary-600);
background-color: rgb(var(--sl-color-primary-50));
color: rgb(var(--sl-color-primary-600));
}
.icon-list[data-type="outline"] .icon-list-item[data-name$="-fill"] {
@@ -864,6 +654,8 @@ If you want to change the icons Shoelace uses internally, you can register an ic
@media screen and (max-width: 500px) {
.icon-list {
grid-template-columns: repeat(4, 1fr);
}
}
}
</style>
[component-metadata:sl-icon]

View File

@@ -0,0 +1,29 @@
# Image Comparer
[component-header:sl-image-comparer]
Compare visual differences between similar photos with a sliding panel.
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
<sl-image-comparer>
<img slot="before" src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80&sat=-100&bri=-5" alt="Grayscale version of kittens in a basket looking around.">
<img slot="after" src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80" alt="Color version of kittens in a basket looking around.">
</sl-image-comparer>
```
## 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
<sl-image-comparer position="25">
<img slot="before" src="https://images.unsplash.com/photo-1520903074185-8eca362b3dce?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1200&q=80" alt="A person sitting on bricks wearing untied boots.">
<img slot="after" src="https://images.unsplash.com/photo-1520640023173-50a135e35804?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80" alt="A person sitting on a yellow curb tying shoelaces on a boot.">
</sl-image-comparer>
```
[component-metadata:sl-image-comparer]

View File

@@ -1,22 +1,15 @@
---
meta:
title: Include
description: Includes give you the power to embed external HTML files into the page.
layout: component
---
# Include
[component-header:sl-include]
Includes give you the power to embed external HTML files into the page.
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 `<sl-include>` element's default slot so it can be easily accessed and styled through the light DOM.
```html:preview
<sl-include src="https://shoelace.style/assets/examples/include.html"></sl-include>
```
```jsx:react
import SlInclude from '@shoelace-style/shoelace/dist/react/include';
const App = () => <SlInclude src="https://shoelace.style/assets/examples/include.html" />;
```html preview no-codepen
<sl-include src="/assets/examples/include.html"></sl-include>
```
## Examples
@@ -28,21 +21,19 @@ When an include file loads successfully, the `sl-load` event will be emitted. Yo
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
<sl-include src="https://shoelace.style/assets/examples/include.html"></sl-include>
<sl-include src="/assets/examples/include.html"></sl-include>
<script>
const include = document.querySelector('sl-include');
include.addEventListener('sl-load', event => {
if (event.eventPhase === Event.AT_TARGET) {
console.log('Success');
}
include.addEventListener('sl-load', () => {
console.log('Success');
});
include.addEventListener('sl-error', event => {
if (event.eventPhase === Event.AT_TARGET) {
console.log('Error', event.detail.status);
}
console.log('Error', event.detail.status);
});
</script>
```
[component-metadata:sl-include]

141
docs/components/input.md Normal file
View File

@@ -0,0 +1,141 @@
# Input
[component-header:sl-input]
Inputs collect data from the user.
```html preview
<sl-input></sl-input>
```
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
## Examples
### Placeholders
Use the `placeholder` attribute to add a placeholder.
```html preview
<sl-input placeholder="Type something"></sl-input>
```
### Clearable
Add the `clearable` attribute to add a clear button when the input has content.
```html preview
<sl-input placeholder="Clearable" clearable></sl-input>
```
### Toggle Password
Add the `toggle-password` attribute to add a toggle button that will show the password when activated.
```html preview
<sl-input type="password" placeholder="Password Toggle" size="small" toggle-password></sl-input>
<br>
<sl-input type="password" placeholder="Password Toggle" size="medium" toggle-password></sl-input>
<br>
<sl-input type="password" placeholder="Password Toggle" size="large" toggle-password></sl-input>
```
### Filled Inputs
Add the `filled` attribute to draw a filled input.
```html preview
<sl-input placeholder="Type something" filled></sl-input>
```
### Pill
Use the `pill` attribute to give inputs rounded edges.
```html preview
<sl-input placeholder="Small" size="small" pill></sl-input>
<br>
<sl-input placeholder="Medium" size="medium" pill></sl-input>
<br>
<sl-input placeholder="Large" size="large" pill></sl-input>
```
### Input Types
The `type` attribute controls the type of input the browser renders.
```html preview
<sl-input type="email" Placeholder="Email"></sl-input>
<br>
<sl-input type="number" Placeholder="Number"></sl-input>
<br>
<sl-input type="date" Placeholder="Date"></sl-input>
```
### Disabled
Use the `disabled` attribute to disable an input.
```html preview
<sl-input placeholder="Disabled" size="small" disabled></sl-input>
<br>
<sl-input placeholder="Disabled" size="medium" disabled></sl-input>
<br>
<sl-input placeholder="Disabled" size="large" disabled></sl-input>
```
### Sizes
Use the `size` attribute to change an input's size.
```html preview
<sl-input placeholder="Small" size="small"></sl-input>
<br>
<sl-input placeholder="Medium" size="medium"></sl-input>
<br>
<sl-input placeholder="Large" size="large"></sl-input>
```
### Prefix & Suffix Icons
Use the `prefix` and `suffix` slots to add icons.
```html preview
<sl-input placeholder="Small" size="small">
<sl-icon name="house" slot="prefix"></sl-icon>
<sl-icon name="chat" slot="suffix"></sl-icon>
</sl-input>
<br>
<sl-input placeholder="Medium" size="medium">
<sl-icon name="house" slot="prefix"></sl-icon>
<sl-icon name="chat" slot="suffix"></sl-icon>
</sl-input>
<br>
<sl-input placeholder="Large" size="large">
<sl-icon name="house" slot="prefix"></sl-icon>
<sl-icon name="chat" slot="suffix"></sl-icon>
</sl-input>
```
### Labels
Use the `label` attribute to give the input an accessible label. For labels that contain HTML, use the `label` slot instead.
```html preview
<sl-input label="What is your name?"></sl-input>
```
### 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
<sl-input
label="Nickname"
help-text="What would you like people to call you?"
></sl-input>
```
[component-metadata:sl-input]

View File

@@ -0,0 +1,105 @@
# Menu Item
[component-header:sl-menu-item]
Menu items provide options for the user to pick from in a menu.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item checked>Checked</sl-menu-item>
<sl-menu-item disabled>Disabled</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>
Prefix Icon
<sl-icon slot="prefix" name="gift"></sl-icon>
</sl-menu-item>
<sl-menu-item>
Suffix Icon
<sl-icon slot="suffix" name="heart"></sl-icon>
</sl-menu-item>
</sl-menu>
```
## Examples
### Checked
Use the `checked` attribute to draw menu items in a checked state.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item checked>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
</sl-menu>
```
### Disabled
Add the `disabled` attribute to disable the menu item so it cannot be selected.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item disabled>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
</sl-menu>
```
### Prefix & Suffix
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>
<sl-icon slot="prefix" name="house"></sl-icon>
Home
</sl-menu-item>
<sl-menu-item>
<sl-icon slot="prefix" name="envelope"></sl-icon>
Messages
<sl-badge slot="suffix" type="primary" pill>12</sl-badge>
</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>
<sl-icon slot="prefix" name="gear"></sl-icon>
Settings
</sl-menu-item>
</sl-menu>
```
### 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
<sl-menu class="menu-value" style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item value="opt-1">Option 1</sl-menu-item>
<sl-menu-item value="opt-2">Option 2</sl-menu-item>
<sl-menu-item value="opt-3">Option 3</sl-menu-item>
</sl-menu>
<script>
const menu = document.querySelector('.menu-value');
menu.addEventListener('sl-select', event => {
const item = event.detail.item;
// Toggle checked state
item.checked = !item.checked;
// Log value
console.log(`Selected value: ${item.value}`);
});
</script>
```
[component-metadata:sl-menu-item]

View File

@@ -0,0 +1,23 @@
# Menu Label
[component-header:sl-menu-label]
Menu labels are used to describe a group of menu items.
```html preview
<sl-menu
style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);"
>
<sl-menu-label>Fruits</sl-menu-label>
<sl-menu-item value="apple">Apple</sl-menu-item>
<sl-menu-item value="banana">Banana</sl-menu-item>
<sl-menu-item value="orange">Orange</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-label>Vegetables</sl-menu-label>
<sl-menu-item value="broccoli">Broccoli</sl-menu-item>
<sl-menu-item value="carrot">Carrot</sl-menu-item>
<sl-menu-item value="zucchini">Zucchini</sl-menu-item>
</sl-menu>
```
[component-metadata:sl-menu-label]

23
docs/components/menu.md Normal file
View File

@@ -0,0 +1,23 @@
# Menu
[component-header:sl-menu]
Menus provide a list of options for the user to choose from.
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
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
```
?> 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 `<nav>` and `<a>` elements instead.
[component-metadata:sl-menu]

View File

@@ -0,0 +1,104 @@
# Mutation Observer
[component-header:sl-mutation-observer]
The Mutation Observer component offers a thin, declarative interface to the [`MutationObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
The mutation observer will report changes to the content it wraps through the `sl-mutation` event. When emitted, a collection of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects will be attached to `event.detail` that contains information about how it changed.
```html preview
<div class="mutation-overview">
<sl-mutation-observer attr>
<sl-button type="primary">Click to mutate</sl-button>
</sl-mutation-observer>
<br>
👆 Click the button and watch the console
<script>
const container = document.querySelector('.mutation-overview');
const mutationObserver = container.querySelector('sl-mutation-observer');
const button = container.querySelector('sl-button');
const types = ['primary', 'success', 'neutral', 'warning', 'danger'];
let clicks = 0;
// Change the button's type attribute
button.addEventListener('click', () => {
clicks++;
button.setAttribute('type', types[clicks % types.length]);
});
// Log mutations
mutationObserver.addEventListener('sl-mutation', event => {
console.log(event.detail);
});
</script>
<style>
.mutation-overview sl-button {
margin-bottom: 1rem;
}
</style>
</div>
```
?> When you create a mutation observer, you must indicate what changes it should respond to by including at least one of `attr`, `child-list`, or `char-data`. If you don't specify at least one of these attributes, no mutation events will be emitted.
## Examples
### Child List
Use the `child-list` attribute to watch for new child elements that are added or removed.
```html preview
<div class="mutation-child-list">
<sl-mutation-observer child-list>
<div class="buttons">
<sl-button type="primary">Add button</sl-button>
</div>
</sl-mutation-observer>
👆 Add and remove buttons and watch the console
<script>
const container = document.querySelector('.mutation-child-list');
const mutationObserver = container.querySelector('sl-mutation-observer');
const buttons = container.querySelector('.buttons');
const button = container.querySelector('sl-button[type="primary"]');
let i = 0;
// Add a button
button.addEventListener('click', () => {
const button = document.createElement('sl-button');
button.textContent = ++i;
buttons.append(button);
});
// Remove a button
buttons.addEventListener('click', event => {
const target = event.target.closest('sl-button:not([type="primary"])');
event.stopPropagation();
if (target) {
target.remove();
}
});
// Log mutations
mutationObserver.addEventListener('sl-mutation', event => {
console.log(event.detail);
});
</script>
<style>
.mutation-child-list .buttons {
display: flex;
gap: .25rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
</style>
</div>
```
[component-metadata:sl-mutation-observer]

View File

@@ -0,0 +1,68 @@
# Progress Bar
[component-header:sl-progress-bar]
Progress bars are used to show the status of an ongoing operation.
```html preview
<sl-progress-bar value="50"></sl-progress-bar>
```
## Examples
### Custom Height
Use the `--height` custom property to set the progress bar's height.
```html preview
<sl-progress-bar value="50" style="--height: 6px;"></sl-progress-bar>
```
### Labels
Use the `label` attribute to label the progress bar and tell assistive devices how to announce it.
```html preview
<sl-progress-bar value="50" label="Upload progress"></sl-progress-bar>
```
### Showing Values
Use the default slot to show a value.
```html preview
<sl-progress-bar value="50" class="progress-bar-values">50%</sl-progress-bar>
<br>
<sl-button circle><sl-icon name="dash"></sl-icon></sl-button>
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
<script>
const progressBar = document.querySelector('.progress-bar-values');
const subtractButton = progressBar.nextElementSibling.nextElementSibling;
const addButton = subtractButton.nextElementSibling;
addButton.addEventListener('click', () => {
const value = Math.min(100, progressBar.value + 10);
progressBar.value = value;
progressBar.textContent = `${value}%`;
});
subtractButton.addEventListener('click', () => {
const value = Math.max(0, progressBar.value - 10)
progressBar.value = value;
progressBar.textContent = `${value}%`;
});
</script>
```
### Indeterminate
The `indeterminate` attribute can be used to inform the user that the operation is pending, but its status cannot currently be determined. In this state, `value` is ignored and the label, if present, will not be shown.
```html preview
<sl-progress-bar indeterminate></sl-progress-bar>
```
[component-metadata:sl-progress-bar]

View File

@@ -0,0 +1,82 @@
# Progress Ring
[component-header:sl-progress-ring]
Progress rings are used to show the progress of a determinate operation in a circular fashion.
```html preview
<sl-progress-ring value="25"></sl-progress-ring>
```
## Examples
### Size
Use the `--size` custom property to set the diameter of the progress ring.
```html preview
<sl-progress-ring value="50" style="--size: 200px;"></sl-progress-ring>
```
### Track Width
Use the `--track-width` custom property to set the width of the progress ring's track.
```html preview
<sl-progress-ring value="50" style="--track-width: 10px;"></sl-progress-ring>
```
### Colors
To change the color, use the `--track-color` and `--indicator-color` custom properties.
```html preview
<sl-progress-ring
value="50"
style="
--track-color: pink;
--indicator-color: deeppink;
"
></sl-progress-ring>
```
### Labels
Use the `label` attribute to label the progress ring and tell assistive devices how to announce it.
```html preview
<sl-progress-ring value="50" label="Upload progress"></sl-progress-ring>
```
### Showing Values
Use the default slot to show a label.
```html preview
<sl-progress-ring value="50" class="progress-ring-values" style="margin-bottom: .5rem;">50%</sl-progress-ring>
<br>
<sl-button circle><sl-icon name="dash"></sl-icon></sl-button>
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
<script>
const progressRing = document.querySelector('.progress-ring-values');
const subtractButton = progressRing.nextElementSibling.nextElementSibling;
const addButton = subtractButton.nextElementSibling;
addButton.addEventListener('click', () => {
const value = Math.min(100, progressRing.value + 10);
progressRing.value = value;
progressRing.textContent = `${value}%`;
});
subtractButton.addEventListener('click', () => {
const value = Math.max(0, progressRing.value - 10)
progressRing.value = value;
progressRing.textContent = `${value}%`;
});
</script>
```
[component-metadata:sl-progress-ring]

View File

@@ -0,0 +1,84 @@
# QR Code
[component-header:sl-qr-code]
Generates a [QR code](https://www.qrcode.com/) and renders it using the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API).
QR codes are useful for providing small pieces of information to users who can quickly scan them with a smartphone. Most smartphones have built-in QR code scanners, so simply pointing the camera at a QR code will decode it and allow the user to visit a website, dial a phone number, read a message, etc.
```html preview
<div class="qr-overview">
<sl-qr-code value="https://shoelace.style/" label="Scan this code to visit Shoelace on the web!"></sl-qr-code>
<br>
<sl-input maxlength="255" clearable></sl-input>
</div>
<script>
const container = document.querySelector('.qr-overview');
const qrCode = container.querySelector('sl-qr-code');
const input = container.querySelector('sl-input');
input.value = qrCode.value;
input.addEventListener('sl-input', () => qrCode.value = input.value);
</script>
<style>
.qr-overview {
max-width: 256px;
}
.qr-overview sl-input {
margin-top: 1rem;
}
</style>
```
## Examples
### Colors
Use the `fill` and `background` attributes to modify the QR code's colors. You should always ensure good contrast for optimal compatibility with QR code scanners.
```html preview
<sl-qr-code value="https://shoelace.style/" fill="deeppink" background="white"></sl-qr-code>
```
### Size
Use the `size` attribute to change the size of the QR code.
```html preview
<sl-qr-code value="https://shoelace.style/" size="64"></sl-qr-code>
```
### Radius
Create a rounded effect with the `radius` attribute.
```html preview
<sl-qr-code value="https://shoelace.style/" radius="0.5"></sl-qr-code>
```
### Error Correction
QR codes can be rendered with various levels of [error correction](https://www.qrcode.com/en/about/error_correction.html) that can be set using the `error-correction` attribute. This example generates four codes with the same value using different error correction levels.
```html preview
<div class="qr-error-correction">
<sl-qr-code value="https://shoelace.style/" error-correction="L"></sl-qr-code>
<sl-qr-code value="https://shoelace.style/" error-correction="M"></sl-qr-code>
<sl-qr-code value="https://shoelace.style/" error-correction="Q"></sl-qr-code>
<sl-qr-code value="https://shoelace.style/" error-correction="H"></sl-qr-code>
</div>
<style>
.qr-error-correction {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>
```
[component-metadata:sl-qr-code]

View File

@@ -0,0 +1,29 @@
# Radio Group
[component-header:sl-radio-group]
Radio Groups are used to group multiple radios so they function as a single control.
```html preview
<sl-radio-group label="Select an item">
<sl-radio value="1" checked>Item 1</sl-radio>
<sl-radio value="2">Item 2</sl-radio>
<sl-radio value="3">Item 3</sl-radio>
</sl-radio-group>
```
## Examples
### Showing the Fieldset
You can show a fieldset and legend that wraps the radio group using the `fieldset` attribute.
```html preview
<sl-radio-group label="Select an item" fieldset>
<sl-radio value="1" checked>Item 1</sl-radio>
<sl-radio value="2">Item 2</sl-radio>
<sl-radio value="3">Item 3</sl-radio>
</sl-radio-group>
```
[component-metadata:sl-radio-group]

34
docs/components/radio.md Normal file
View File

@@ -0,0 +1,34 @@
# Radio
[component-header:sl-radio]
Radios allow the user to select one option from a group of many.
Radios are designed to be used with [radio groups](/components/radio-group). As such, all of the examples on this page utilize them to demonstrate their correct usage.
```html preview
<sl-radio-group label="Select an option" no-fieldset>
<sl-radio value="1" checked>Option 1</sl-radio>
<sl-radio value="2">Option 2</sl-radio>
<sl-radio value="3">Option 3</sl-radio>
</sl-radio-group>
```
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
## Examples
### Disabled
Use the `disabled` attribute to disable a radio.
```html preview
<sl-radio-group label="Select an option" no-fieldset>
<sl-radio value="1" checked>Option 1</sl-radio>
<sl-radio value="2">Option 2</sl-radio>
<sl-radio value="3">Option 3</sl-radio>
<sl-radio value="4" disabled>Disabled</sl-radio>
</sl-radio-group>
```
[component-metadata:sl-radio]

Some files were not shown because too many files have changed in this diff Show More