diff --git a/.gitignore b/.gitignore index b6f26d43d..3d9a6f9fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .cache +_site docs/dist docs/search.json dist diff --git a/README.md b/README.md index 621663108..4c16605ae 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,7 @@ Once you've cloned the repo, run the following command. npm start ``` -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. +This will spin up the dev server. After the initial build, a browser will open automatically. There is currently no hot module reloading (HMR), as browser's don't provide a way to reregister custom elements, but most changes to the source will reload the browser automatically. ### Building diff --git a/cspell.json b/cspell.json index e511c3dd4..aa386632c 100644 --- a/cspell.json +++ b/cspell.json @@ -81,6 +81,7 @@ "labelledby", "Laravel", "LaViska", + "linkify", "listbox", "listitem", "litelement", @@ -105,6 +106,7 @@ "ParamagicDev", "peta", "petabit", + "prismjs", "progressbar", "radiogroup", "Railsbyte", @@ -127,6 +129,7 @@ "Segoe", "semibold", "slotchange", + "smartquotes", "spacebar", "stylesheet", "Tabbable", diff --git a/docs/_includes/component.njk b/docs/_includes/component.njk new file mode 100644 index 000000000..8d65f7f85 --- /dev/null +++ b/docs/_includes/component.njk @@ -0,0 +1,347 @@ +{% extends "default.njk" %} + +{# Find the component based on the `tag` front matter #} +{% set component = getComponent('sl-' + page.fileSlug) %} + +{% block content %} + {# Determine the badge variant #} + {% if component.status == 'stable' %} + {% set badgeVariant = 'primary' %} + {% elseif component.status == 'experimental' %} + {% set badgeVariant = 'warning' %} + {% elseif component.status == 'planned' %} + {% set badgeVariant = 'neutral' %} + {% elseif component.status == 'deprecated' %} + {% set badgeVariant = 'danger' %} + {% else %} + {% set badgeVariant = 'neutral' %} + {% endif %} + + {# Header #} +
+

{{ component.name | classNameToComponentName }}

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

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

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

Importing

+

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

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

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

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

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

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

+ To import this component using a bundler: +

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

+ To import this component as a React component: +

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

Slots

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

Learn more about using slots.

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

Properties

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

Learn more about attributes and properties.

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

Events

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

Learn more about events.

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

Methods

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

Learn more about methods.

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

Custom Properties

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

Learn more about customizing CSS custom properties.

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

Parts

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

Learn more about customizing CSS parts.

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

Animations

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

Learn more about customizing animations.

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

Dependencies

+ +

This component automatically imports the following dependencies.

+ + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/docs/_includes/default.njk b/docs/_includes/default.njk new file mode 100644 index 000000000..96af3ff55 --- /dev/null +++ b/docs/_includes/default.njk @@ -0,0 +1,143 @@ + + + + {# Metadata #} + + + + {{ meta.title }} + + {# Stylesheets #} + + + + + {# Favicons #} + + + {# Twitter Cards #} + + + + + {# OpenGraph #} + + + + + + {# Shoelace #} + + + + + {# Set the initial theme and menu states here to prevent flashing #} + + + + + Skip to main content + + + {# Menu toggle #} + + + {# Icon toolbar #} +
+ {# GitHub #} + + + + + {# Twitter #} + + + + + {# Theme toggle #} + +
+ + + + {# Content #} +
+ +
+ {% if toc %} + + {% endif %} + +
+ {% block content %} + {{ content | safe }} + {% endblock %} +
+
+
+ + {# Scripts #} + + + + + + diff --git a/docs/_includes/sidebar.njk b/docs/_includes/sidebar.njk new file mode 100644 index 000000000..9f3b280fe --- /dev/null +++ b/docs/_includes/sidebar.njk @@ -0,0 +1,65 @@ + diff --git a/docs/_utilities/active-links.cjs b/docs/_utilities/active-links.cjs new file mode 100644 index 000000000..7a998054e --- /dev/null +++ b/docs/_utilities/active-links.cjs @@ -0,0 +1,35 @@ +function normalizePathname(pathname) { + // Remove /index.html + if (pathname.endsWith('/index.html')) { + pathname = pathname.replace(/\/index\.html/, ''); + } + + // Remove trailing slashes + return pathname.replace(/\/$/, ''); +} + +/** + * Adds a class name to links that are currently active. + */ +module.exports = function (doc, options) { + options = { + className: 'active-link', // the class to add to active links + pathname: undefined, // the current pathname to compare + within: 'body', // element containing the target links + ...options + }; + + const within = doc.querySelector(options.within); + + if (!within) { + return doc; + } + + within.querySelectorAll('a').forEach(link => { + if (normalizePathname(options.pathname) === normalizePathname(link.pathname)) { + link.classList.add(options.className); + } + }); + + return doc; +}; diff --git a/docs/_utilities/anchor-headings.cjs b/docs/_utilities/anchor-headings.cjs new file mode 100644 index 000000000..7e45031c4 --- /dev/null +++ b/docs/_utilities/anchor-headings.cjs @@ -0,0 +1,56 @@ +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'); + const slug = createSlug(heading.textContent ?? '') ?? ''; + let id = slug; + let suffix = 0; + + // If we can't generate a slug, skip this heading + if (!slug) return; + + // Skip heading levels we don't care about + if (!options.levels?.includes(heading.tagName.toLowerCase())) { + return; + } + + // Ensure the id is unique + while (doc.getElementById(id) !== null) { + id = `${slug}-${++suffix}`; + } + + if (hasAnchor || !id) return; + + heading.setAttribute('id', id); + anchor.setAttribute('href', `#${encodeURIComponent(id)}`); + anchor.setAttribute('aria-label', `Direct link to "${heading.textContent}"`); + + if (options.className) { + heading.classList.add(options.className); + } + + // Append the anchor + heading.append(anchor); + }); + + return doc; +}; diff --git a/docs/_utilities/cem.cjs b/docs/_utilities/cem.cjs new file mode 100644 index 000000000..c156e5989 --- /dev/null +++ b/docs/_utilities/cem.cjs @@ -0,0 +1,74 @@ +// +// TODO - switch to local dist +// +const customElementsManifest = require('@shoelace-style/shoelace/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.map(component => { + const dependencies = []; + + // Recursively fetch sub-dependencies + function getDependencies(tag) { + const component = allComponents.find(c => c.tagName === tag); + if (!component || !Array.isArray(component.dependencies)) { + return; + } + + component.dependencies?.forEach(dependentTag => { + if (!dependencies.includes(dependentTag)) { + dependencies.push(dependentTag); + } + getDependencies(dependentTag); + }); + } + + getDependencies(component.tagName); + + component.dependencies = dependencies.sort(); + }); + + // Sort by name + return allComponents.sort((a, b) => { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); +}; diff --git a/docs/_utilities/code-previews.cjs b/docs/_utilities/code-previews.cjs new file mode 100644 index 000000000..f1362b202 --- /dev/null +++ b/docs/_utilities/code-previews.cjs @@ -0,0 +1,153 @@ +let count = 1; + +function escapeHtml(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +/** + * Turns code fields with the :preview suffix into interactive code previews. + */ +module.exports = function (doc, options) { + options = { + within: 'body', // the element containing the code fields to convert + ...options + }; + + const within = doc.querySelector(options.within); + if (!within) { + return doc; + } + + within.querySelectorAll('[class*=":preview"]').forEach(code => { + const pre = code.closest('pre'); + if (!pre) { + return; + } + const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null; + const reactCode = adjacentPre?.querySelector('code[class$="react"]'); + const sourceGroupId = `code-preview-source-group-${count}`; + const toggleId = `code-preview-toggle-${count}`; + const isExpanded = code.getAttribute('class').includes(':expanded'); + const noCodePen = code.getAttribute('class').includes(':no-codepen'); + + count++; + + const htmlButton = ` + + `; + + const reactButton = ` + + `; + + const codePenButton = ` + + `; + + const codePreview = ` +
+
+ ${code.textContent} +
+ +
+
+ +
+
+
${escapeHtml(code.textContent)}
+
+ + ${ + reactCode + ? ` +
+
${escapeHtml(reactCode.textContent)}
+
+ ` + : '' + } +
+ +
+ + + ${reactCode ? ` ${htmlButton} ${reactButton} ` : ''} + + ${noCodePen ? '' : codePenButton} +
+
+ `; + + pre.insertAdjacentHTML('afterend', codePreview); + pre.remove(); + + if (adjacentPre) { + adjacentPre.remove(); + } + }); + + // Wrap code preview scripts in anonymous functions so they don't run in the global scope + doc.querySelectorAll('.code-preview__preview script').forEach(script => { + if (script.type === 'module') { + // Modules are already scoped + script.textContent = script.innerHTML; + } else { + // Wrap non-modules in an anonymous function so they don't run in the global scope + script.textContent = `(() => { ${script.innerHTML} })();`; + } + }); + + return doc; +}; + +function getAdjacentExample(name, pre) { + let currentPre = pre.nextElementSibling; + + while (currentPre?.tagName.toLowerCase() === 'pre') { + if (currentPre?.getAttribute('class').indexOf(name) > -1) { + return currentPre; + } + + currentPre = currentPre.nextElementSibling; + } + + return null; +} diff --git a/docs/_utilities/copy-code-buttons.cjs b/docs/_utilities/copy-code-buttons.cjs new file mode 100644 index 000000000..306164b24 --- /dev/null +++ b/docs/_utilities/copy-code-buttons.cjs @@ -0,0 +1,26 @@ +/** + * 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('button'); + button.setAttribute('type', 'button'); + button.classList.add('copy-code-button'); + button.setAttribute('aria-label', 'Copy'); + button.innerHTML = ` + + + + + + `; + + pre.append(button); + }); + + return doc; +}; diff --git a/docs/_utilities/external-links.cjs b/docs/_utilities/external-links.cjs new file mode 100644 index 000000000..4d7c2ef13 --- /dev/null +++ b/docs/_utilities/external-links.cjs @@ -0,0 +1,41 @@ +const { isExternalLink } = require('./strings.cjs'); + +/** + * Transforms external links to make them safer and optionally add a target. The provided doc should be a document + * object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations. + */ +module.exports = function (doc, options) { + options = { + className: 'external-link', // the class name to add to links + noopener: true, // sets rel="noopener" + noreferrer: true, // sets rel="noreferrer" + ignore: link => false, // callback function to filter links that should be ignored + within: 'body', // element that contains the target links + target: '', // sets the target attribute + ...options + }; + + const within = doc.querySelector(options.within); + + if (within) { + within.querySelectorAll('a').forEach(link => { + if (isExternalLink(link) && !options.ignore(link)) { + link.classList.add(options.className); + + const rel = []; + if (options.noopener) rel.push('noopener'); + if (options.noreferrer) rel.push('noreferrer'); + + if (rel.length) { + link.setAttribute('rel', rel.join(' ')); + } + + if (options.target) { + link.setAttribute('target', options.target); + } + } + }); + } + + return doc; +}; diff --git a/docs/_utilities/highlight-code.cjs b/docs/_utilities/highlight-code.cjs new file mode 100644 index 000000000..bb4c01f09 --- /dev/null +++ b/docs/_utilities/highlight-code.cjs @@ -0,0 +1,63 @@ +const Prism = require('prismjs'); +const PrismLoader = require('prismjs/components/index.js'); + +PrismLoader('diff'); +PrismLoader.silent = true; + +/** Highlights a code string. */ +function highlight(code, language) { + const alias = language.replace(/^diff-/, ''); + const isDiff = /^diff-/i.test(language); + + // Auto-load the target language + if (!Prism.languages[alias]) { + PrismLoader(alias); + + if (!Prism.languages[alias]) { + throw new Error(`Unsupported language for code highlighting: "${language}"`); + } + } + + // Register diff-* languages to use the diff grammar + if (isDiff) { + Prism.languages[language] = Prism.languages.diff; + } + + return Prism.highlight(code, Prism.languages[language], language); +} + +/** + * Highlights all code fields that have a language parameter. If the language has a colon in its name, the first chunk + * will be the language used and additional chunks will be applied as classes to the `
`. For example, a code field
+ * tagged with "html:preview" will be rendered as `
`.
+ *
+ * The provided doc should be a document object provided by JSDOM. The same document will be returned with the
+ * appropriate DOM manipulations.
+ */
+module.exports = function (doc) {
+  doc.querySelectorAll('pre > code[class]').forEach(code => {
+    // Look for class="language-*" and split colons into separate classes
+    code.classList.forEach(className => {
+      if (className.startsWith('language-')) {
+        //
+        // We use certain suffixes to indicate code previews, expanded states, etc. The class might look something like
+        // this:
+        //
+        //  class="language-html:preview:expanded"
+        //
+        // The language will always come first, so we need to drop the "language-" prefix and everything after the first
+        // color to get the highlighter language.
+        //
+        const language = className.replace(/^language-/, '').split(':')[0];
+
+        try {
+          code.innerHTML = highlight(code.textContent ?? '', language);
+        } catch (err) {
+          // Language not found, skip it
+        }
+      }
+    });
+  });
+
+  return doc;
+};
diff --git a/docs/_utilities/markdown.cjs b/docs/_utilities/markdown.cjs
new file mode 100644
index 000000000..4a73e8f39
--- /dev/null
+++ b/docs/_utilities/markdown.cjs
@@ -0,0 +1,67 @@
+const MarkdownIt = require('markdown-it');
+const markdownItContainer = require('markdown-it-container');
+const markdownItIns = require('markdown-it-ins');
+const markdownItKbd = require('markdown-it-kbd');
+const markdownItMark = require('markdown-it-mark');
+const markdownItReplaceIt = require('markdown-it-replace-it');
+
+const markdown = MarkdownIt({
+  html: true,
+  xhtmlOut: false,
+  breaks: false,
+  langPrefix: 'language-',
+  linkify: false,
+  typographer: false
+});
+
+// Third-party plugins
+markdown.use(markdownItContainer);
+markdown.use(markdownItIns);
+markdown.use(markdownItKbd);
+markdown.use(markdownItMark);
+markdown.use(markdownItReplaceIt);
+
+// Callouts
+['tip', 'warning', 'danger'].forEach(type => {
+  markdown.use(markdownItContainer, type, {
+    render: function (tokens, idx) {
+      if (tokens[idx].nesting === 1) {
+        return `\n';
+    }
+  });
+});
+
+// Asides
+markdown.use(markdownItContainer, 'aside', {
+  render: function (tokens, idx) {
+    if (tokens[idx].nesting === 1) {
+      return `\n';
+  }
+});
+
+// Details
+markdown.use(markdownItContainer, 'details', {
+  validate: params => params.trim().match(/^details\s+(.*)$/),
+  render: (tokens, idx) => {
+    const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
+    if (tokens[idx].nesting === 1) {
+      return `
\n${markdown.utils.escapeHtml(m[1])}\n`; + } + return '
\n'; + } +}); + +// Replace [#1234] with a link to GitHub issues +markdownItReplaceIt.replacements.push({ + name: 'github-issues', + re: /\[#([0-9]+)\]/gs, + sub: '#$1', + html: true, + default: true +}); + +module.exports = markdown; diff --git a/docs/_utilities/prettier.cjs b/docs/_utilities/prettier.cjs new file mode 100644 index 000000000..d58e7ada2 --- /dev/null +++ b/docs/_utilities/prettier.cjs @@ -0,0 +1,26 @@ +const { format } = require('prettier'); + +/** Formats markup using prettier. */ +module.exports = function (content, options) { + options = { + arrowParens: 'avoid', + bracketSpacing: true, + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + bracketSameLine: false, + jsxSingleQuote: false, + parser: 'html', + printWidth: 120, + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'none', + useTabs: false, + ...options + }; + + return format(content, options); +} diff --git a/docs/_utilities/scrolling-tables.cjs b/docs/_utilities/scrolling-tables.cjs new file mode 100644 index 000000000..148248dbe --- /dev/null +++ b/docs/_utilities/scrolling-tables.cjs @@ -0,0 +1,21 @@ +/** + * Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM. + * The same document will be returned with the appropriate DOM manipulations. + */ +module.exports = function (doc, options) { + const tables = [...doc.querySelectorAll('table')]; + + options = { + className: 'table-scroll', // the class name to add to the table's container + ...options + }; + + tables.forEach(table => { + const div = doc.createElement('div'); + div.classList.add(options.className); + table.insertAdjacentElement('beforebegin', div); + div.append(table); + }); + + return doc; +}; diff --git a/docs/_utilities/strings.cjs b/docs/_utilities/strings.cjs new file mode 100644 index 000000000..6831d66d9 --- /dev/null +++ b/docs/_utilities/strings.cjs @@ -0,0 +1,16 @@ +const slugify = require('slugify'); + +/** Creates a slug from an arbitrary string of text. */ +module.exports.createSlug = function (text) { + return slugify(String(text), { + remove: /[^\w|\s]/g, + lower: true + }); +}; + +/** Determines whether or not a link is external. */ +module.exports.isExternalLink = function (link) { + // We use the "internal" hostname when initializing JSDOM so we know that those are local links + if (!link.hostname || link.hostname === 'internal') return false; + return true; +}; diff --git a/docs/_utilities/table-of-contents.cjs b/docs/_utilities/table-of-contents.cjs new file mode 100644 index 000000000..1ac04fd31 --- /dev/null +++ b/docs/_utilities/table-of-contents.cjs @@ -0,0 +1,42 @@ +/** + * Generates an in-page table of contents based on headings. + */ +module.exports = function (doc, options) { + options = { + levels: ['h2'], // headings to include (they must have an id) + container: 'nav', // the container to append links to + listItem: true, // if true, links will be wrapped in
  • + within: 'body', // the element containing the headings to summarize + ...options + }; + + const container = doc.querySelector(options.container); + const within = doc.querySelector(options.within); + const headingSelector = options.levels.map(h => `${h}[id]`).join(', '); + + if (!container || !within) { + return doc; + } + + within.querySelectorAll(headingSelector).forEach(heading => { + const listItem = doc.createElement('li'); + const link = doc.createElement('a'); + const level = heading.tagName.slice(1); + + link.href = `#${heading.id}`; + link.textContent = heading.textContent; + + if (options.listItem) { + // List item + link + listItem.setAttribute('data-level', level); + listItem.append(link); + container.append(listItem); + } else { + // Link only + link.setAttribute('data-level', level); + container.append(link); + } + }); + + return doc; +}; diff --git a/docs/_utilities/typography.cjs b/docs/_utilities/typography.cjs new file mode 100644 index 000000000..c4f17c4d2 --- /dev/null +++ b/docs/_utilities/typography.cjs @@ -0,0 +1,23 @@ +const smartquotes = require('smartquotes'); + +smartquotes.replacements.push([/\-\-\-/g, '\u2014']); // em dash +smartquotes.replacements.push([/\-\-/g, '\u2013']); // en dash +smartquotes.replacements.push([/\.\.\./g, '\u2026']); // ellipsis +smartquotes.replacements.push([/\(c\)/gi, '\u00A9']); // copyright +smartquotes.replacements.push([/\(r\)/gi, '\u00AE']); // registered trademark +smartquotes.replacements.push([/\?!/g, '\u2048']); // ?! +smartquotes.replacements.push([/!!/g, '\u203C']); // !! +smartquotes.replacements.push([/\?\?/g, '\u2047']); // ?? +smartquotes.replacements.push([/([0-9]\s?)\-(\s?[0-9])/g, '$1\u2013$2']); // number ranges use en dash + +/** + * Improves typography by adding smart quotes and similar corrections within the specified element(s). + * + * The provided doc should be a document object provided by JSDOM. The same document will be returned with the + * appropriate DOM manipulations. + */ +module.exports = function (doc, selector = 'body') { + const elements = [...doc.querySelectorAll(selector)]; + elements.forEach(el => smartquotes.element(el)); + return doc; +}; diff --git a/docs/assets/icons/sprite.svg b/docs/assets/icons/sprite.svg deleted file mode 100644 index 61f2720db..000000000 --- a/docs/assets/icons/sprite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/assets/images/gitpod.svg b/docs/assets/images/gitpod.svg new file mode 100644 index 000000000..a30505a69 --- /dev/null +++ b/docs/assets/images/gitpod.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/undraw-taken.svg b/docs/assets/images/undraw-taken.svg new file mode 100644 index 000000000..3b523849e --- /dev/null +++ b/docs/assets/images/undraw-taken.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/scripts/code-previews.js b/docs/assets/scripts/code-previews.js new file mode 100644 index 000000000..ee8fbdbad --- /dev/null +++ b/docs/assets/scripts/code-previews.js @@ -0,0 +1,247 @@ +(() => { + function convertModuleLinks(html) { + html = html + .replace(/@shoelace-style\/shoelace/g, `https://cdn.skypack.dev/@shoelace-style/shoelace@${shoelaceVersion}`) + .replace(/from 'react'/g, `from 'https://cdn.skypack.dev/react@${reactVersion}'`) + .replace(/from "react"/g, `from "https://cdn.skypack.dev/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'); + } + + const shoelaceVersion = document.documentElement.getAttribute('data-shoelace-version'); + const reactVersion = '18.2.0'; + let flavor = getFlavor(); + let count = 1; + + // We need the version to open + if (!shoelaceVersion) { + throw new Error('The data-shoelace-version attribute is missing from .'); + } + + // Sync flavor UI on page load + 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'); + } + }); + + // + // Resizing previews + // + [...document.querySelectorAll('.code-preview__preview')].forEach(preview => { + const resizer = preview.querySelector('.code-preview__resizer'); + let startX; + let startWidth; + + function dragStart(event) { + startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX; + startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10); + preview.classList.add('code-preview__preview--dragging'); + event.preventDefault(); + document.documentElement.addEventListener('mousemove', dragMove); + document.documentElement.addEventListener('touchmove', dragMove); + document.documentElement.addEventListener('mouseup', dragStop); + document.documentElement.addEventListener('touchend', dragStop); + } + + function dragMove(event) { + setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX); + } + + 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); + } + + function setWidth(width) { + preview.style.width = `${width}px`; + } + + resizer.addEventListener('mousedown', dragStart); + resizer.addEventListener('touchstart', dragStart, { passive: true }); + }, false); + + // + // 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) { + const toggle = codeBlock.querySelector('.code-preview__toggle'); + + if (toggle) { + codeBlock.classList.toggle('code-preview--expanded', force === undefined ? undefined : force); + event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-preview--expanded')); + } + } + + // + // Open in CodePen + // + document.addEventListener('click', event => { + const button = event.target.closest('button'); + + if (button?.classList.contains('code-preview__button--codepen')) { + const codeBlock = button.closest('.code-preview'); + const htmlExample = codeBlock.querySelector('.code-preview__source--html > pre > code')?.textContent; + const reactExample = codeBlock.querySelector('.code-preview__source--react > pre > code')?.textContent; + const isReact = flavor === 'react' && typeof reactExample === 'string'; + const theme = document.documentElement.classList.contains('sl-theme-dark') ? 'dark' : 'light'; + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const isDark = theme === 'dark' || (theme === 'auto' && prefersDark); + const editors = isReact ? '0010' : '1000'; + let htmlTemplate = ''; + let jsTemplate = ''; + let cssTemplate = ''; + + const form = document.createElement('form'); + form.action = 'https://codepen.io/pen/define'; + form.method = 'POST'; + form.target = '_blank'; + + // HTML templates + if (!isReact) { + htmlTemplate = + `\n` + + `\n${htmlExample}`; + jsTemplate = ''; + } + + // React templates + if (isReact) { + htmlTemplate = '
    '; + jsTemplate = + `import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` + + `import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` + + `import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${shoelaceVersion}/dist/utilities/base-path';\n` + + `\n` + + `// Set the base path for Shoelace assets\n` + + `setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${shoelaceVersion}/dist/')\n` + + `\n${convertModuleLinks(reactExample)}\n` + + `\n` + + `ReactDOM.render(, document.getElementById('root'));`; + } + + // CSS templates + cssTemplate = + `@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${shoelaceVersion}/dist/themes/${ + isDark ? 'dark' : 'light' + }.css';\n` + + '\n' + + 'body {\n' + + ' font: 16px sans-serif;\n' + + ' background-color: var(--sl-color-neutral-0);\n' + + ' color: var(--sl-color-neutral-900);\n' + + ' padding: 1rem;\n' + + '}'; + + // Docs: https://blog.codepen.io/documentation/prefill/ + const data = { + title: '', + description: '', + tags: ['shoelace', 'web components'], + editors, + head: ``, + html_classes: `sl-theme-${isDark ? 'dark' : 'light'}`, + css_external: ``, + js_external: ``, + js_module: true, + js_pre_processor: isReact ? 'babel' : 'none', + html: htmlTemplate, + css: cssTemplate, + js: jsTemplate + }; + + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'data'; + input.value = JSON.stringify(data); + form.append(input); + + document.documentElement.append(form); + form.submit(); + form.remove(); + } + }); +})(); diff --git a/docs/assets/scripts/docs.js b/docs/assets/scripts/docs.js new file mode 100644 index 000000000..369135106 --- /dev/null +++ b/docs/assets/scripts/docs.js @@ -0,0 +1,216 @@ +// +// 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 isSidebarOpen() { + return document.documentElement.classList.contains('sidebar-open'); + } + + function isSidebarVisible() { + return sidebar.getBoundingClientRect().x >= 0; + } + + function toggleSidebar(force) { + const isOpen = typeof force === 'boolean' ? force : !isSidebarOpen(); + return document.documentElement.classList.toggle('sidebar-open', isOpen); + } + + function updateInert() { + sidebar.inert = !isSidebarVisible(); + } + + const menuToggle = document.getElementById('menu-toggle'); + const sidebar = document.getElementById('sidebar'); + + // Toggle the menu + if (menuToggle) { + menuToggle.addEventListener('click', toggleSidebar); + } + + // Update the sidebar's inert state when the window resizes and when the sidebar transitions + window.addEventListener('resize', () => toggleSidebar(false)); + sidebar.addEventListener('transitionend', updateInert); + + // 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 switcher +// +(() => { + function toggleTheme() { + const isDark = !document.documentElement.classList.contains('sl-theme-dark'); + document.documentElement.classList.toggle('sl-theme-dark', isDark); + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + } + + // Toggle the theme + const themeToggle = document.getElementById('theme-toggle'); + + if (themeToggle) { + themeToggle.addEventListener('click', toggleTheme); + } + + // Toggle with backslash + document.addEventListener('keydown', event => { + if ( + event.key === '\\' && + !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) + ) { + event.preventDefault(); + toggleTheme(); + } + }); +})(); + +// +// 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(); + }); +})(); + +// +// Copy code buttons +// +(() => { + document.addEventListener('click', event => { + const button = event.target.closest('.copy-code-button'); + const pre = button?.closest('pre'); + const code = pre?.querySelector('code'); + const copyIcon = button?.querySelector('.copy-code-button__copy-icon'); + const copiedIcon = button?.querySelector('.copy-code-button__copied-icon'); + + if (button && code) { + navigator.clipboard.writeText(code.innerText); + copyIcon.style.display = 'none'; + copiedIcon.style.display = 'inline'; + button.classList.add('copy-code-button--copied'); + + setTimeout(() => { + copyIcon.style.display = 'inline'; + copiedIcon.style.display = 'none'; + button.classList.remove('copy-code-button--copied'); + }, 1000); + } + }); +})(); + +// +// 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 +// +(() => { + const links = [...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() { + // 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 + links.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); + } + }); +})(); diff --git a/docs/assets/scripts/search.js b/docs/assets/scripts/search.js new file mode 100644 index 000000000..f524724cb --- /dev/null +++ b/docs/assets/scripts/search.js @@ -0,0 +1,369 @@ +(() => { + // Append the search dialog to the body + const siteSearch = document.createElement('div'); + const scrollbarWidth = Math.abs(window.innerWidth - document.documentElement.clientWidth); + + siteSearch.classList.add('search'); + siteSearch.innerHTML = ` +
    + +
    +
    +
    + + + +
    +
    +
    +
      +
      No matching pages
      +
      +
      + Navigate + Select + Esc Close +
      +
      +
      + `; + + const overlay = siteSearch.querySelector('.search__overlay'); + const dialog = siteSearch.querySelector('.search__dialog'); + const input = siteSearch.querySelector('.search__input'); + const clearButton = siteSearch.querySelector('.search__clear-button'); + const results = siteSearch.querySelector('.search__results'); + const version = document.documentElement.getAttribute('data-shoelace-version'); + const animationDuration = 150; + const searchDebounce = 50; + let isShowing = false; + let searchTimeout; + let searchIndex; + let map; + + const loadSearchIndex = new Promise(resolve => { + const key = `search_${version}`; + const cache = localStorage.getItem(key); + const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame; + + // Cleanup older search indices (everything before this version) + try { + const items = { ...localStorage }; + + Object.keys(items).forEach(k => { + if (key > k) { + localStorage.removeItem(k); + } + }); + } catch { + /* do nothing */ + } + + // Look for a cached index + try { + if (cache) { + const data = JSON.parse(cache); + + searchIndex = window.lunr.Index.load(data.searchIndex); + map = data.map; + + return resolve(); + } + } catch { + /* do nothing */ + } + + // Wait until idle to fetch the index + wait(() => { + fetch('/assets/search.json') + .then(res => res.json()) + .then(data => { + if (!window.lunr) { + console.error('The Lunr search client has not yet been loaded.'); + } + + searchIndex = window.lunr.Index.load(data.searchIndex); + map = data.map; + + // Cache the search index for this version + if (version) { + try { + localStorage.setItem(key, JSON.stringify(data)); + } catch (err) { + console.warn(`Unable to cache the search index: ${err}`); + } + } + + resolve(); + }); + }); + }); + + async function show() { + isShowing = true; + document.body.append(siteSearch); + document.body.classList.add('search-visible'); + document.body.style.setProperty('--docs-search-scroll-lock-size', `${scrollbarWidth}px`); + clearButton.hidden = true; + requestAnimationFrame(() => input.focus()); + updateResults(); + + dialog.showModal(); + + await Promise.all([ + dialog.animate( + [ + { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' }, + { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' } + ], + { duration: animationDuration } + ).finished, + overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished + ]); + + dialog.addEventListener('mousedown', handleMouseDown); + dialog.addEventListener('keydown', handleKeyDown); + } + + async function hide() { + isShowing = false; + + await Promise.all([ + dialog.animate( + [ + { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' }, + { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' } + ], + { duration: animationDuration } + ).finished, + overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished + ]); + + dialog.close(); + + input.blur(); // otherwise Safari will scroll to the bottom of the page on close + input.value = ''; + document.body.classList.remove('search-visible'); + document.body.style.removeProperty('--docs-search-scroll-lock-size'); + siteSearch.remove(); + updateResults(); + + dialog.removeEventListener('mousedown', handleMouseDown); + dialog.removeEventListener('keydown', handleKeyDown); + } + + function handleInput() { + clearButton.hidden = input.value === ''; + + // Debounce search queries + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce); + } + + function handleClear() { + clearButton.hidden = true; + input.value = ''; + input.focus(); + updateResults(); + } + + function handleMouseDown(event) { + if (!event.target.closest('.search__content')) { + hide(); + } + } + + function handleKeyDown(event) { + // Close when pressing escape + if (event.key === 'Escape') { + event.preventDefault(); // prevent from closing immediately so it can animate + event.stopImmediatePropagation(); + hide(); + return; + } + + // Handle keyboard selections + if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) { + event.preventDefault(); + + const currentEl = results.querySelector('[data-selected="true"]'); + const items = [...results.querySelectorAll('li')]; + const index = items.indexOf(currentEl); + let nextEl; + + if (items.length === 0) { + return; + } + + switch (event.key) { + case 'ArrowUp': + nextEl = items[Math.max(0, index - 1)]; + break; + case 'ArrowDown': + nextEl = items[Math.min(items.length - 1, index + 1)]; + break; + case 'Home': + nextEl = items[0]; + break; + case 'End': + nextEl = items[items.length - 1]; + break; + case 'Enter': + currentEl?.querySelector('a')?.click(); + break; + } + + // Update the selected item + items.forEach(item => { + if (item === nextEl) { + input.setAttribute('aria-activedescendant', item.id); + item.setAttribute('data-selected', 'true'); + nextEl.scrollIntoView({ block: 'nearest' }); + } else { + item.setAttribute('data-selected', 'false'); + } + }); + } + } + + async function updateResults(query = '') { + try { + await loadSearchIndex; + + const hasQuery = query.length > 0; + const searchTerms = query + .split(' ') + .map((term, index, arr) => { + // Search API: https://lunrjs.com/guides/searching.html + if (index === arr.length - 1) { + // The last term is not mandatory and 1x fuzzy. We also duplicate it with a wildcard to match partial words + // as the user types. + return `${term}~1 ${term}*`; + } else { + // All other terms are mandatory and 1x fuzzy + return `+${term}~1`; + } + }) + .join(' '); + const matches = hasQuery ? searchIndex.search(searchTerms) : []; + const hasResults = hasQuery && matches.length > 0; + + siteSearch.classList.toggle('search--has-results', hasQuery && hasResults); + siteSearch.classList.toggle('search--no-results', hasQuery && !hasResults); + + input.setAttribute('aria-activedescendant', ''); + results.innerHTML = ''; + + matches.forEach((match, index) => { + const page = map[match.ref]; + const li = document.createElement('li'); + const a = document.createElement('a'); + const displayTitle = page.title ?? ''; + const displayDescription = page.description ?? ''; + const displayUrl = page.url.replace(/^\//, ''); + let icon = 'file-text'; + + a.setAttribute('role', 'option'); + a.setAttribute('id', `search-result-item-${match.ref}`); + + if (page.url.includes('getting-started/')) { + icon = 'lightbulb'; + } + if (page.url.includes('resources/')) { + icon = 'book'; + } + if (page.url.includes('components/')) { + icon = 'puzzle'; + } + if (page.url.includes('tokens/')) { + icon = 'palette2'; + } + if (page.url.includes('utilities/')) { + icon = 'wrench'; + } + if (page.url.includes('tutorials/')) { + icon = 'joystick'; + } + + li.classList.add('search__result'); + li.setAttribute('role', 'option'); + li.setAttribute('id', `search-result-item-${match.ref}`); + li.setAttribute('data-selected', index === 0 ? 'true' : 'false'); + + a.href = page.url; + a.innerHTML = ` + +
      +
      +
      +
      +
      + `; + a.querySelector('.search__result-title').textContent = displayTitle; + a.querySelector('.search__result-description').textContent = displayDescription; + a.querySelector('.search__result-url').textContent = displayUrl; + + li.appendChild(a); + results.appendChild(li); + }); + } catch { + // Ignore query errors as the user types + } + } + + // Show the search dialog when clicking on data-plugin="search" + document.addEventListener('click', event => { + const searchButton = event.target.closest('[data-plugin="search"]'); + if (searchButton) { + show(); + } + }); + + // Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element + document.addEventListener('keydown', event => { + if ( + !isShowing && + (event.key === '/' || (event.key === 'k' && (event.metaKey || event.ctrlKey))) && + !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) + ) { + event.preventDefault(); + show(); + } + }); + + input.addEventListener('input', handleInput); + clearButton.addEventListener('click', handleClear); + + // Close when a result is selected + results.addEventListener('click', event => { + if (event.target.closest('a')) { + hide(); + } + }); +})(); diff --git a/docs/assets/styles/code-previews.css b/docs/assets/styles/code-previews.css new file mode 100644 index 000000000..0fa5efacc --- /dev/null +++ b/docs/assets/styles/code-previews.css @@ -0,0 +1,173 @@ +/* Interactive code blocks */ +.code-preview { + position: relative; + border-radius: 3px; + background-color: var(--sl-color-neutral-50); + margin-bottom: 1.5rem; +} + +.code-preview__preview { + position: relative; + border: solid 1px var(--sl-color-neutral-200); + border-bottom: none; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + background-color: var(--sl-color-neutral-0); + min-width: 20rem; + max-width: 100%; + padding: 1.5rem 3.25rem 1.5rem 1.5rem; +} + +/* Block the preview while dragging to prevent iframes from intercepting drag events */ +.code-preview__preview--dragging:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: ew-resize; +} + +.code-preview__resizer { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 1.75rem; + font-size: 20px; + color: var(--sl-color-neutral-600); + background-color: var(--sl-color-neutral-0); + border-left: solid 1px var(--sl-color-neutral-200); + border-top-right-radius: 3px; + cursor: ew-resize; +} + +@media screen and (max-width: 600px) { + .code-preview__preview { + padding-right: 1.5rem; + } + + .code-preview__resizer { + display: none; + } +} + +.code-preview__source { + border: solid 1px var(--sl-color-neutral-200); + border-bottom: none; + border-radius: 0 !important; + display: none; +} + +.code-preview--expanded .code-preview__source { + display: block; +} + +.code-preview__source pre { + margin: 0; +} + +.code-preview__buttons { + position: relative; + border: solid 1px var(--sl-color-neutral-200); + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + display: flex; +} + +.code-preview__button { + flex: 0 0 auto; + height: 2.5rem; + min-width: 2.5rem; + border: none; + border-radius: 0; + background: var(--sl-color-neutral-0); + font: inherit; + font-size: 0.7rem; + font-weight: 500; + text-transform: uppercase; + color: var(--sl-color-neutral-600); + padding: 0 1rem; + cursor: pointer; +} + +.code-preview__button:not(:last-of-type) { + border-right: solid 1px var(--sl-color-neutral-200); +} + +.code-preview__button--html, +.code-preview__button--react { + width: 70px; + display: flex; + place-items: center; + justify-content: center; +} + +.code-preview__button--selected { + font-weight: 700; + color: var(--sl-color-primary-600); +} + +.code-preview__button--codepen { + display: flex; + place-items: center; + width: 6rem; +} + +.code-preview__button:first-of-type { + border-bottom-left-radius: 3px; +} + +.code-preview__button:last-of-type { + border-bottom-right-radius: 3px; +} + +.code-preview__button:hover, +.code-preview__button:active { + box-shadow: 0 0 0 1px var(--sl-color-primary-400); + border-right-color: transparent; + background-color: var(--sl-color-primary-50); + color: var(--sl-color-primary-600); + z-index: 1; +} + +.code-preview__button:focus-visible { + outline: none; + outline: var(--sl-focus-ring); + z-index: 2; +} + +.code-preview__toggle { + position: relative; + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; + width: 100%; + color: var(--sl-color-neutral-600); + cursor: pointer; +} + +.code-preview__toggle svg { + width: 1em; + height: 1em; + margin-left: 0.25rem; +} + +.code-preview--expanded .code-preview__toggle svg { + transform: rotate(180deg); +} + +/* We can apply data-flavor="html|react" to any element on the page to toggle it when the flavor changes */ +.flavor-html [data-flavor]:not([data-flavor='html']) { + display: none; +} + +.flavor-react [data-flavor]:not([data-flavor='react']) { + display: none; +} diff --git a/docs/assets/styles/docs.css b/docs/assets/styles/docs.css index 1341846af..403e2ae36 100644 --- a/docs/assets/styles/docs.css +++ b/docs/assets/styles/docs.css @@ -1,13 +1,105 @@ -html { - box-sizing: border-box; +:root { + --docs-background-color: var(--sl-color-neutral-0); + --docs-border-color: var(--sl-color-neutral-200); + --docs-border-width: 1px; + --docs-border-radius: var(--sl-border-radius-medium); + --docs-content-max-width: 860px; + --docs-sidebar-width: 320px; + --docs-sidebar-transition-speed: 250ms; + --docs-content-toc-max-width: 260px; + --docs-content-padding: 2rem; + --docs-content-vertical-spacing: 2rem; + --docs-search-overlay-background: rgb(0 0 0 / 0.2); + --docs-skip-to-main-width: 200px; } +/* Light theme */ +:root { + color-scheme: normal; + + --sl-color-primary-50: var(--sl-color-sky-50); + --sl-color-primary-100: var(--sl-color-sky-100); + --sl-color-primary-200: var(--sl-color-sky-200); + --sl-color-primary-300: var(--sl-color-sky-300); + --sl-color-primary-400: var(--sl-color-sky-400); + --sl-color-primary-500: var(--sl-color-sky-500); + --sl-color-primary-600: var(--sl-color-sky-600); + --sl-color-primary-700: var(--sl-color-sky-700); + --sl-color-primary-800: var(--sl-color-sky-800); + --sl-color-primary-900: var(--sl-color-sky-900); + --sl-color-primary-950: var(--sl-color-sky-950); + + --docs-overlay-color: hsl(240 3.8% 46.1% / 33%); + --docs-shadow-x-small: 0 1px 2px hsl(240 3.8% 46.1% / 12%); + --docs-shadow-small: 0 1px 2px hsl(240 3.8% 46.1% / 24%); + --docs-shadow-medium: 0 2px 4px hsl(240 3.8% 46.1% / 24%); + --docs-shadow-large: 0 2px 8px hsl(240 3.8% 46.1% / 24%); + --docs-shadow-x-large: 0 4px 16px hsl(240 3.8% 46.1% / 24%); +} + +/* Dark theme */ +.sl-theme-dark { + color-scheme: dark; + + --docs-overlay-color: hsl(0 0% 0% / 66%); + --docs-shadow-x-small: 0 1px 2px rgb(0 0 0 / 36%); + --docs-shadow-small: 0 1px 2px rgb(0 0 0 / 48%); + --docs-shadow-medium: 0 2px 4px rgb(0 0 0 / 48%); + --docs-shadow-large: 0 2px 8px rgb(0 0 0 / 48%); + --docs-shadow-x-large: 0 4px 16px rgb(0 0 0 / 48%); +} + +/* Utils */ +html.sl-theme-dark .only-light, +html:not(.sl-theme-dark) .only-dark { + display: none !important; +} + +.visually-hidden:not(:focus-within) { + position: absolute !important; + width: 1px !important; + height: 1px !important; + clip: rect(0 0 0 0) !important; + clip-path: inset(50%) !important; + border: none !important; + overflow: hidden !important; + white-space: nowrap !important; + padding: 0 !important; +} + +.nowrap { + white-space: nowrap; +} + +@media screen and (max-width: 900px) { + :root { + --docs-content-padding: 1rem; + } +} + +/* Bare styles */ *, *:before, *:after { box-sizing: inherit; } +a:focus, +button:focus { + outline: none; +} + +a:focus-visible, +button:focus-visible { + outline: var(--sl-focus-ring); + outline-offset: var(--sl-focus-ring-offset); +} + +::selection { + background-color: var(--sl-color-primary-200); + color: var(--sl-color-neutral-900); +} + /* Show custom elements only after they're registered */ :not(:defined), :not(:defined) * { @@ -19,132 +111,1051 @@ html { transition: 0.1s opacity; } -body { - font-family: var(--sl-font-sans); - font-size: var(--sl-font-size-medium); - font-weight: var(--sl-font-weight-normal); - letter-spacing: var(--sl-letter-spacing-normal); - background-color: var(--sl-color-neutral-0); - color: var(--sl-color-neutral-900); +html { + height: 100%; + box-sizing: border-box; line-height: var(--sl-line-height-normal); + padding: 0; + margin: 0; +} + +body { + height: 100%; + font: 16px var(--sl-font-sans); + font-weight: var(--sl-font-weight-normal); + background-color: var(--docs-background-color); + line-height: var(--sl-line-height-normal); + color: var(--sl-color-neutral-900); + padding: 0; + margin: 0; + overflow-x: hidden; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +/* Common elements */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--sl-font-sans); + font-weight: var(--sl-font-weight-semibold); + margin: 3rem 0 1.5rem 0; +} + +h1:first-of-type { + margin-top: 1rem; +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 1.875rem; + border-bottom: solid var(--docs-border-width) var(--docs-border-color); +} + +h3 { + font-size: 1.625rem; +} + +h4 { + font-size: 1.375rem; +} + +h5 { + font-size: 1.125rem; +} + +h6 { + font-size: 0.875rem; +} + +p { + margin-block-end: 1.5em; +} + +img { + max-width: 100%; + border-radius: var(--docs-border-radius); +} + +.callout img, +details img { + width: 100%; + margin-left: 0; + margin-right: 0; +} + +details pre { + border: solid var(--docs-border-width) var(--docs-border-color); } a { - color: var(--sl-color-primary-600); + color: var(--sl-color-primary-700); } a:hover { - color: var(--sl-color-primary-700); + color: var(--sl-color-primary-800); +} + +abbr[title] { + text-decoration: none; + border-bottom: dashed 1px var(--sl-color-primary-700); + cursor: help; +} + +em { + font-style: italic; } strong { font-weight: var(--sl-font-weight-bold); } -/* Sidebar */ -.sidebar { - background-color: var(--sl-color-neutral-0); - border-right: solid 1px var(--sl-color-neutral-200); +code { + font-family: var(--sl-font-mono); + font-size: 0.9125em; + background-color: rgba(0 0 0 / 0.025); + background-blend-mode: darken; + border-radius: var(--docs-border-radius); + padding: 0.125em 0.25em; } -.sidebar .app-name { - padding: 0 1.5rem; +.sl-theme-dark code { + background-color: rgba(255 255 255 / 0.03); +} + +kbd { + background: var(--sl-color-neutral-100); + border: solid 1px var(--sl-color-neutral-200); + box-shadow: inset 0 1px 0 0 var(--sl-color-neutral-0), inset 0 -1px 0 0 var(--sl-color-neutral-200); + font-family: var(--sl-font-mono); + border-radius: var(--docs-border-radius); + color: var(--sl-color-neutral-800); + padding: 0.125em 0.4em; +} + +ins { + background-color: var(--sl-color-green-200); + color: var(--sl-color-green-900); + border-radius: var(--docs-border-radius); + text-decoration: none; + padding: 0.125em 0.25em; +} + +s { + background-color: var(--sl-color-red-200); + color: var(--sl-color-red-900); + border-radius: var(--docs-border-radius); + text-decoration: none; + padding: 0.125em 0.25em; +} + +mark { + background-color: var(--sl-color-yellow-200); + border-radius: var(--docs-border-radius); + padding: 0.125em 0.25em; +} + +hr { + border: none; + border-bottom: solid var(--docs-border-width) var(--docs-border-color); + margin: calc(var(--docs-content-vertical-spacing) * 2) 0; +} + +/* Blockquotes */ +blockquote { + position: relative; + font-family: var(--sl-font-serif); + font-size: 1.33rem; + font-style: italic; + color: var(--sl-color-neutral-700); + background-color: var(--sl-color-neutral-100); + border-radius: var(--docs-border-radius); + border-left: solid 6px var(--sl-color-neutral-300); + padding: 1.5rem; + margin: 0 0 1.5rem 0; +} + +blockquote > :first-child { + margin-top: 0; +} + +blockquote > :last-child { + margin-bottom: 0; +} + +/* Lists */ +ul, +ol { + padding: 0; + margin: 0 0 var(--docs-content-vertical-spacing) 2rem; +} + +ul { + list-style: disc; +} + +li { + padding: 0; + margin: 0 0 0.25rem 0; +} + +li ul, +li ol { + margin-top: 0.25rem; +} + +ul ul:last-child, +ul ol:last-child, +ol ul:last-child, +ol ol:last-child { + margin-bottom: 0; +} + +/* Anchor headings */ +.anchor-heading { + position: relative; + color: inherit; + text-decoration: none; +} + +.anchor-heading a { + text-decoration: none; + color: inherit; +} + +.anchor-heading a::after { + content: '#'; + color: var(--sl-color-primary-700); + margin-inline: 0.5rem; + opacity: 0; + transition: 100ms opacity; +} + +.anchor-heading:hover a::after, +.anchor-heading:focus-within a::after { + opacity: 1; +} + +/* External links */ +.external-link__icon { + width: 0.75em; + height: 0.75em; + vertical-align: 0; + margin-left: 0.25em; + margin-right: 0.125em; +} + +/* Tables */ +table { + max-width: 100%; + border: none; + border-collapse: collapse; + color: inherit; + margin-bottom: var(--docs-content-vertical-spacing); +} + +table tr { + border-bottom: solid var(--docs-border-width) var(--docs-border-color); +} + +table th { + font-weight: var(--sl-font-weight-semibold); + text-align: left; +} + +table td, +table th { + line-height: var(--sl-line-height-normal); + padding: 1rem 1rem; +} + +table th p:first-child, +table td p:first-child { + margin-top: 0; +} + +table th p:last-child, +table td p:last-child { + margin-bottom: 0; +} + +.table-scroll { + max-width: 100%; + overflow-x: auto; +} + +th.table-name, +th.table-event-detail { + min-width: 15ch; +} + +th.table-description { + min-width: 50ch !important; + max-width: 70ch; +} + +/* Code blocks */ +pre { + position: relative; + background-color: var(--sl-color-neutral-50); + font-family: var(--sl-font-mono); + color: var(--sl-color-neutral-900); + border-radius: var(--docs-border-radius); + padding: 1rem; + white-space: pre; +} + +.sl-theme-dark pre { + background-color: var(--sl-color-neutral-50); +} + +pre:not(:last-child) { + margin-bottom: 1.5rem; +} + +pre > code { + display: block; + background: none !important; + border-radius: 0; + hyphens: none; + tab-size: 2; + white-space: pre; + padding: 1rem; + margin: -1rem; + overflow: auto; +} + +pre .token.comment { + color: var(--sl-color-neutral-600); +} + +pre .token.prolog, +pre .token.doctype, +pre .token.cdata, +pre .token.operator, +pre .token.punctuation { + color: var(--sl-color-neutral-700); +} + +.namespace { + opacity: 0.7; +} + +pre .token.property, +pre .token.keyword, +pre .token.tag, +pre .token.url { + color: var(--sl-color-blue-700); +} + +pre .token.symbol, +pre .token.deleted { + color: var(--sl-color-red-700); +} + +pre .token.boolean, +pre .token.constant, +pre .token.selector, +pre .token.attr-name, +pre .token.string, +pre .token.char, +pre .token.builtin, +pre .token.inserted { + color: var(--sl-color-emerald-700); +} + +pre .token.atrule, +pre .token.attr-value, +pre .token.number, +pre .token.variable { + color: var(--sl-color-violet-700); +} + +pre .token.function, +pre .token.class-name, +pre .token.regex { + color: var(--sl-color-orange-700); +} + +pre .token.important { + color: var(--sl-color-red-700); +} + +pre .token.important, +pre .token.bold { + font-weight: bold; +} + +pre .token.italic { + font-style: italic; +} + +/* Copy code button */ +.copy-code-button { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: var(--sl-color-neutral-0); + border-radius: calc(var(--docs-border-radius) * 0.875); + border: solid 1px var(--sl-color-neutral-200); + color: var(--sl-color-neutral-800); + text-transform: uppercase; + padding: 0.5rem; + margin: 0; + cursor: pointer; + transition: 100ms opacity, 100ms scale; +} + +.copy-code-button svg { + width: 1rem; + height: 1rem; +} + +pre .copy-code-button { + opacity: 0; + scale: 0.9; +} + +pre:hover .copy-code-button, +.copy-code-button:focus-visible { + opacity: 1; + scale: 1; +} + +pre:hover .copy-code-button:hover, +pre:hover .copy-code-button--copied { + background: var(--sl-color-neutral-200); + border-color: var(--sl-color-neutral-300); + color: var(--sl-color-neutral-900); +} + +/* Callouts */ +.callout { + position: relative; + background-color: var(--sl-color-neutral-100); + border-left: solid 4px var(--sl-color-neutral-500); + border-radius: var(--docs-border-radius); + color: var(--sl-color-neutral-800); + padding: 1.5rem 1.5rem 1.5rem 2rem; + margin-bottom: var(--docs-content-vertical-spacing); +} + +.callout > :first-child { + margin-top: 0; +} + +.callout > :last-child { + margin-bottom: 0; +} + +.callout--tip { + background-color: var(--sl-color-primary-100); + border-left-color: var(--sl-color-primary-600); + color: var(--sl-color-primary-800); +} + +.callout::before { + content: ''; + position: absolute; + top: calc(50% - 0.8rem); + left: calc(-0.8rem - 2px); + width: 1.6rem; + height: 1.6rem; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--sl-font-serif); + font-weight: var(--sl-font-weight-bold); + color: var(--sl-color-neutral-0); + clip-path: circle(50% at 50% 50%); +} + +.callout--tip::before { + content: 'i'; + font-style: italic; + background-color: var(--sl-color-primary-600); +} + +.callout--warning { + background-color: var(--sl-color-warning-100); + border-left-color: var(--sl-color-warning-600); + color: var(--sl-color-warning-800); +} + +.callout--warning::before { + content: '!'; + background-color: var(--sl-color-warning-600); +} + +.callout--danger { + background-color: var(--sl-color-danger-100); + border-left-color: var(--sl-color-danger-600); + color: var(--sl-color-danger-800); +} + +.callout--danger::before { + content: '‼'; + background-color: var(--sl-color-danger-600); +} + +.callout + .callout { + margin-top: calc(-0.5 * var(--docs-content-vertical-spacing)); +} + +.callout a { + color: inherit; +} + +/* Aside */ +.content aside { + float: right; + min-width: 300px; + max-width: 50%; + background: var(--sl-color-neutral-100); + border-radius: var(--docs-border-radius); + padding: 1rem; + margin-left: 1rem; +} + +.content aside > :first-child { + margin-top: 0; +} + +.content aside > :last-child { + margin-bottom: 0; +} + +@media screen and (max-width: 600px) { + .content aside { + float: none; + width: calc(100% + (var(--docs-content-padding) * 2)); + max-width: none; + margin: var(--docs-content-vertical-spacing) calc(-1 * var(--docs-content-padding)); + } +} + +/* Details */ +.content details { + background-color: var(--sl-color-neutral-100); + border-radius: var(--docs-border-radius); + padding: 1rem; + margin: 0 0 var(--docs-content-vertical-spacing) 0; +} + +.content details summary { + font-weight: var(--sl-font-weight-semibold); + border-radius: var(--docs-border-radius); + padding: 1rem; + margin: -1rem; + cursor: pointer; + user-select: none; +} + +.content details summary span { + padding-left: 0.5rem; +} + +.content details[open] summary { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + margin-bottom: 1rem; +} + +.content details[open] summary:focus-visible { + border-radius: var(--docs-border-radius); +} + +.content details > :last-child { + margin-bottom: 0; +} + +.content details > :nth-child(2) { + margin-top: 0; +} + +.content details + details { + margin-top: calc(-1 * var(--docs-content-vertical-spacing) + (2 * var(--docs-border-width))); +} + +/* Sidebar */ +#sidebar { + position: fixed; + flex: 0; + top: 0; + left: 0; + bottom: 0; + z-index: 20; + width: var(--docs-sidebar-width); + background-color: var(--docs-background-color); + border-right: solid var(--docs-border-width) var(--docs-border-color); + border-radius: 0; + padding: 2rem; + margin: 0; + overflow: auto; + scrollbar-width: thin; + transition: var(--docs-sidebar-transition-speed) translate ease-in-out; +} + +#sidebar::-webkit-scrollbar { + width: 4px; +} + +#sidebar::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 9999px; +} + +#sidebar:hover::-webkit-scrollbar-thumb { + background: var(--sl-color-neutral-400); +} + +#sidebar:hover::-webkit-scrollbar-track { + background: var(--sl-color-neutral-100); +} + +#sidebar > header { + margin-bottom: 1.5rem; +} + +#sidebar > header h1 { + margin: 0; +} + +#sidebar > header a { + display: block; +} + +#sidebar nav a { + text-decoration: none; +} + +#sidebar nav h2 { + font-size: var(--sl-font-size-medium); + font-weight: var(--sl-font-weight-semibold); + border-bottom: solid var(--docs-border-width) var(--docs-border-color); + margin: 1.5rem 0 0.5rem 0; +} + +#sidebar ul { + padding: 0; + margin: 0; +} + +#sidebar ul li { + list-style: none; + padding: 0; + margin: 0.125rem 0.5rem; +} + +#sidebar ul ul ul { + margin-left: 0.75rem; +} + +#sidebar ul li a { + line-height: 1.33; + color: inherit; + display: inline-block; + padding: 0; +} + +#sidebar ul li a:not(.active-link):hover { + color: var(--sl-color-primary-700); +} + +#sidebar nav .active-link { + color: var(--sl-color-primary-700); + border-bottom: dashed 1px var(--sl-color-primary-700); +} + +#sidebar > header img { + display: block; + width: 100%; + height: auto; + margin: 0 auto; +} + +@media screen and (max-width: 900px) { + #sidebar { + translate: -100%; + } + + .sidebar-open #sidebar { + translate: 0; + } } .sidebar-version { font-size: var(--sl-font-size-x-small); - font-weight: var(--sl-font-weight-normal); color: var(--sl-color-neutral-500); text-align: right; - padding: 0 var(--sl-spacing-small); - margin: -1.25rem 0 0.6rem 0; + margin-top: -0.5rem; + margin-right: 1rem; + margin-bottom: -0.5rem; } .sidebar-buttons { display: flex; justify-content: space-between; - text-align: center; - margin-top: 0; } -/* Sidebar toggle */ -.sidebar-toggle { - top: 0.25rem; - left: 0.25rem; - width: 2rem; - height: 2rem; - border-radius: var(--sl-border-radius-medium); - background-color: var(--sl-color-neutral-0); - padding: 0.5rem; +/* Main content */ +main { + position: relative; + padding: var(--docs-content-vertical-spacing) var(--docs-content-padding) + calc(var(--docs-content-vertical-spacing) * 2) var(--docs-content-padding); + margin-left: var(--docs-sidebar-width); } -.sidebar-toggle:hover .sidebar-toggle-button { - opacity: 1; +.sidebar-open .content { + margin-left: 0; } -.sidebar-toggle:active .sidebar-toggle-button span { - background-color: var(--sl-color-primary-600); -} - -.sidebar-toggle:focus { - outline: var(--sl-focus-ring); - outline-offset: var(--sl-focus-ring-offset); -} - -.sidebar-toggle span:last-child { +.content__body > :last-child { margin-bottom: 0; } -@media screen and (max-width: 768px) { - body.close .sidebar-toggle { +@media screen and (max-width: 900px) { + main { + margin-left: 0; + } +} + +/* Component layouts */ +.content { + display: grid; + grid-template-columns: 100%; + gap: 2rem; + position: relative; + max-width: var(--docs-content-max-width); + margin: 0 auto; +} + +.content--with-toc { + /* There's a 2rem gap, so we need to remove it from the column */ + grid-template-columns: calc(75% - 2rem) min(25%, var(--docs-content-toc-max-width)); + max-width: calc(var(--docs-content-max-width) + var(--docs-content-toc-max-width)); +} + +.content__body { + order: 1; + width: 100%; +} + +.content:not(.content--with-toc) .content__toc { + display: none; +} + +.content__toc { + order: 2; + display: flex; + flex-direction: column; + margin-top: 0; +} + +.content__toc ul { + position: sticky; + top: 5rem; + max-height: calc(100vh - 6rem); + font-size: var(--sl-font-size-small); + line-height: 1.33; + border-left: solid 1px var(--sl-color-neutral-200); + list-style: none; + padding: 1rem 0; + margin: 0; + padding-left: 1rem; + overflow-y: auto; +} + +.content__toc li { + padding: 0 0 0 0.5rem; + margin: 0; +} + +.content__toc li[data-level='3'] { + margin-left: 1rem; +} + +/* We don't use them, but just in case */ +.content__toc li[data-level='4'], +.content__toc li[data-level='5'], +.content__toc li[data-level='6'] { + margin-left: 2rem; +} + +.content__toc li:not(:last-of-type) { + margin-bottom: 0.6rem; +} + +.content__toc a { + color: var(--sl-color-neutral-700); + text-decoration: none; +} + +.content__toc a:hover { + color: var(--sl-color-primary-700); +} + +.content__toc a.active { + color: var(--sl-color-primary-700); + border-bottom: dashed 1px var(--sl-color-primary-700); +} + +.content__toc .top a { + font-weight: var(--sl-font-weight-semibold); + color: var(--sl-color-neutral-500); +} + +@media screen and (max-width: 1024px) { + .content { + grid-template-columns: 100%; + gap: 0; + } + + .content__toc { + position: relative; + order: 1; + } + + .content__toc ul { + display: flex; + justify-content: start; + gap: 1rem 1.5rem; + position: static; + border: none; + border-bottom: solid 1px var(--sl-color-neutral-200); + border-radius: 0; + padding: 1rem 1.5rem 1rem 0.5rem; /* extra right padding to hide the fade effect */ + margin-top: 1rem; + overflow-x: auto; + } + + .content__toc ul::after { + content: ''; + position: absolute; + top: 0; + bottom: 1rem; /* don't cover the scrollbar */ + right: 0; width: 2rem; - background: none; + background: linear-gradient(90deg, rgba(0 0 0 / 0) 0%, var(--sl-color-neutral-0) 100%); + } + + .content__toc li { + white-space: nowrap; + } + + .content__toc li:not(:last-of-type) { + margin-bottom: 0; + } + + .content__toc [data-level]:not([data-level='2']) { + display: none; + } + + .content__body { + order: 2; + } +} + +/* Menu toggle */ +#menu-toggle { + display: none; + position: fixed; + z-index: 30; + top: 0.25rem; + left: 0.25rem; + height: auto; + width: auto; + color: black; + border: none; + border-radius: 50%; + background: var(--sl-color-neutral-0); + padding: 0.5rem; + margin: 0; + cursor: pointer; + transition: 250ms scale ease, 250ms rotate ease; +} + +@media screen and (max-width: 900px) { + #menu-toggle { + display: flex; + } +} + +.sl-theme-dark #menu-toggle { + color: white; +} + +#menu-toggle:hover { + scale: 1.1; +} + +#menu-toggle svg { + width: 1.25rem; + height: 1.25rem; +} + +html.sidebar-open #menu-toggle { + rotate: 180deg; +} + +/* Skip to main content */ +#skip-to-main { + position: fixed; + top: 0.25rem; + left: calc(50% - var(--docs-skip-to-main-width) / 2); + z-index: 100; + width: var(--docs-skip-to-main-width); + text-align: center; + text-decoration: none; + border-radius: 9999px; + background: var(--sl-color-neutral-0); + color: var(--sl-color-neutral-1000); + padding: 0.5rem; +} + +/* Icon toolbar */ +#icon-toolbar { + display: flex; + position: fixed; + top: 0; + right: 1rem; + z-index: 10; + background: var(--sl-color-neutral-800); + border-bottom-left-radius: calc(var(--docs-border-radius) * 2); + border-bottom-right-radius: calc(var(--docs-border-radius) * 2); + padding: 0.125rem 0.25rem; +} + +#icon-toolbar button, +#icon-toolbar a { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + width: auto; + height: auto; + background: transparent; + border: none; + border-radius: var(--docs-border-radius); + font-size: 1.25rem; + color: var(--sl-color-neutral-0); + text-decoration: none; + padding: 0.5rem; + margin: 0; + cursor: pointer; + transition: 250ms scale ease; +} + +.sl-theme-dark #icon-toolbar { + background: var(--sl-color-neutral-200); +} + +.sl-theme-dark #icon-toolbar button, +.sl-theme-dark #icon-toolbar a { + color: var(--sl-color-neutral-1000); +} + +#icon-toolbar button:hover, +#icon-toolbar a:hover { + scale: 1.1; +} + +#icon-toolbar a:not(:last-child), +#icon-toolbar button:not(:last-child) { + margin-right: 0.25rem; +} + +@media screen and (max-width: 900px) { + #icon-toolbar { + right: 0; + border-bottom-right-radius: 0; + } + + #icon-toolbar button, + #icon-toolbar a { + font-size: 1rem; padding: 0.5rem; } } -/* Sidebar nav */ -.sidebar-nav { - padding: 0 1rem; +/* Sidebar addons */ +#sidebar-addons:not(:empty) { + margin-bottom: var(--docs-content-vertical-spacing); } -.sidebar-nav li { - line-height: 1; - padding: 0; -} +/* Print styles */ +@media print { + a:not(.anchor-heading)[href]::after { + content: ' (' attr(href) ')'; + } -.sidebar-nav a { - color: inherit; - text-decoration: none; - line-height: 1.5em; - padding-top: 0.25em; - padding-bottom: 0.25em; -} + details, + pre, + .callout { + border: solid var(--docs-border-width) var(--docs-border-color); + } -.sidebar-nav li.collapse > a, -.sidebar-nav li.active > a { - color: var(--sl-color-primary-600); -} + details summary { + list-style: none; + } -.sidebar li > p { - font-weight: var(--sl-font-weight-bold); - border-bottom: solid 1px var(--sl-color-neutral-200); - margin: 0 0.75rem 0.5rem 0; -} + details summary span { + padding-left: 0; + } -.sidebar ul li ul { - padding-left: 0.5rem; - margin: 0 0.75rem 1.5rem 0; -} + details summary::marker, + details summary::-webkit-details-marker { + display: none; + } -.sidebar ul ul ul { - padding: 0; - margin: 0 0 0 0.5rem; -} + .callout::before { + border: solid var(--docs-border-width) var(--docs-border-color); + } -.sidebar ul ul ul li { - list-style: disc; - margin-left: 1.5rem; + .component-page__navigation, + .copy-code-button, + .code-preview__buttons, + .code-preview__resizer { + display: none !important; + } + + .flavor-html .code-preview__source--html, + .flavor-react .code-preview__source--react { + display: block !important; + } + + .flavor-html .code-preview__source--html > pre, + .flavor-react .code-preview__source--react > pre { + border: none; + } + + .code-preview__source-group { + border-bottom: solid 1px var(--sl-color-neutral-200); + border-bottom-left-radius: var(--sl-border-radius-medium); + border-bottom-right-radius: var(--sl-border-radius-medium); + } + + #sidebar { + display: none; + } + + #content { + margin-left: 0; + } + + #menu-toggle, + #icon-toolbar, + .external-link__icon { + display: none; + } } /* Splash */ @@ -157,6 +1168,11 @@ strong { min-width: 420px; } +.splash-start h1 { + font-size: var(--sl-font-size-large); + font-weight: var(--sl-font-weight-normal); +} + .splash-end { display: flex; align-items: flex-end; @@ -173,12 +1189,12 @@ strong { max-width: 22rem; } -.markdown-section .splash-start h1:first-of-type { +.splash-start h1:first-of-type { font-size: var(--sl-font-size-large); margin: 0 0 0.5rem 0; } -@media screen and (max-width: 1040px) { +@media screen and (max-width: 1280px) { .splash { display: block; } @@ -203,340 +1219,30 @@ strong { } } -/* Content */ -.content { - padding-top: 0; +/* Repo buttons */ +.repo-button--sponsor sl-icon { + color: var(--sl-color-pink-600); } -.markdown-section { - max-width: 860px; -} - -.anchor span { - color: var(--sl-color-neutral-1000); -} - -.markdown-section blockquote { - position: relative; - border-left: solid 4px var(--sl-color-neutral-200); - font-style: italic; - padding: 1rem 1.5rem; - margin: 0 0 1rem 0; -} - -.markdown-section blockquote p:first-child { - margin-top: 0; -} - -.markdown-section blockquote p:last-child { - margin-bottom: 0; -} - -.markdown-section ul { - padding: 0 0 0 1.5rem; - margin: 0 0 1rem 0; -} - -.markdown-section ul ul { - margin-bottom: 0; -} - -.docsify-pagination-container { - border-top-color: var(--sl-color-neutral-200) !important; -} - -.pagination-item-label, -.pagination-item-subtitle, -.pagination-item-title { - opacity: 1 !important; -} - -.markdown-section h1, -.markdown-section h2, -.markdown-section h3, -.markdown-section h4, -.markdown-section h5, -.markdown-section h6 { - font-weight: var(--sl-font-weight-normal); - margin: 0 0 1em 0; -} - -.markdown-section h1 { - font-size: var(--sl-font-size-2x-large); -} - -.markdown-section h2 { - font-size: var(--sl-font-size-x-large); - border-bottom: solid 1px var(--sl-color-neutral-200); - margin-top: 2rem; -} - -.markdown-section h3 { - font-size: var(--sl-font-size-large); -} - -.markdown-section h4 { - font-size: var(--sl-font-size-medium); -} - -.markdown-section h5 { - font-size: var(--sl-font-size-small); -} - -.markdown-section h6 { - font-size: var(--sl-font-size-x-small); -} - -.markdown-section pre { - font-family: var(--sl-font-mono); -} - -.markdown-section h1:first-of-type { - margin-bottom: 0; -} - -.markdown-section code { - font-family: var(--sl-font-mono); - font-size: 87.5%; - background-color: var(--sl-color-neutral-50); - border-radius: var(--sl-border-radius-small); - padding: 2px 4px; -} - -kbd, -.markdown-section kbd { - font-family: var(--sl-font-mono); - font-size: 87.5%; - background-color: var(--sl-color-neutral-50); - border-radius: var(--sl-border-radius-small); - border: solid 1px var(--sl-color-neutral-200); - box-shadow: inset 0 1px 0 var(--sl-color-neutral-0); - padding: 2px 5px; -} - -/* Code blocks */ -.markdown-section pre { - position: relative; - background-color: var(--sl-color-neutral-50); - border-radius: var(--sl-border-radius-medium); -} - -.markdown-section pre > code { - display: block; - background: none; - border-radius: 0; - color: var(--sl-color-neutral-900); - padding: var(--sl-spacing-medium); - overflow: auto; - hyphens: none; - tab-size: 2; -} - -.markdown-section pre .token.comment { - color: var(--sl-color-neutral-500); -} - -.markdown-section pre .token.prolog, -.markdown-section pre .token.doctype, -.markdown-section pre .token.cdata, -.markdown-section pre .token.operator { +.repo-button--github sl-icon { color: var(--sl-color-neutral-700); } -.markdown-section pre .token.punctuation { - color: var(--sl-color-neutral-700); -} - -.namespace { - opacity: 0.7; -} - -.markdown-section pre .token.property, -.markdown-section pre .token.keyword, -.markdown-section pre .token.tag, -.markdown-section pre .token.url { - color: var(--sl-color-sky-800); -} - -.markdown-section pre .token.symbol, -.markdown-section pre .token.deleted { - color: var(--sl-color-pink-700); -} - -.markdown-section pre .token.boolean, -.markdown-section pre .token.constant, -.markdown-section pre .token.selector, -.markdown-section pre .token.attr-name, -.markdown-section pre .token.string, -.markdown-section pre .token.char, -.markdown-section pre .token.builtin, -.markdown-section pre .token.inserted { - color: var(--sl-color-emerald-700); -} - -.markdown-section pre .token.atrule, -.markdown-section pre .token.attr-value, -.markdown-section pre .token.number, -.markdown-section pre .token.variable { - color: var(--sl-color-violet-700); -} - -.markdown-section pre .token.function, -.markdown-section pre .token.class-name, -.markdown-section pre .token.regex { - color: var(--sl-color-orange-700); -} - -.markdown-section pre .token.important { - color: var(--sl-color-red-700); -} - -.markdown-section pre .token.important, -.markdown-section pre .token.bold { - font-weight: bold; -} - -.markdown-section pre .token.italic { - font-style: italic; -} - -/* Tables */ -.table-wrapper { - overflow-x: auto; -} - -@media screen and (max-width: 1200px) { - .table-wrapper table { - min-width: 800px; - } -} - -.markdown-section table { - display: table; - margin-bottom: 1.5rem; -} - -.markdown-section tr { - border: none; -} - -.markdown-section tr:nth-child(2n) { - background: transparent; -} - -.markdown-section th { - border: none; - font-weight: var(--sl-font-weight-semibold); - text-align: left; -} - -.markdown-section td { - border-top: solid 1px var(--sl-color-neutral-200); - border-bottom: solid 1px var(--sl-color-neutral-200); - border-left: none; - border-right: none; - padding: 0.75rem 0.5rem; -} - -.markdown-section table .nowrap { - white-space: nowrap; -} - -.markdown-section table sl-tooltip code { - border-bottom: dashed 1px var(--sl-color-neutral-300); - cursor: help; -} - -.markdown-section .metadata-table { - margin-bottom: 0rem; -} - -/* Iframes */ -.markdown-section iframe { - border: none; -} - -/* Tips & Warnings */ -.markdown-section p.tip, -.markdown-section p.warn { - position: relative; - background-color: var(--sl-color-neutral-50); - border-left: solid 4px transparent; - border-radius: var(--sl-border-radius-medium); - padding-left: 1.5rem; -} - -.markdown-section p.tip:before, -.markdown-section p.warn:before { - content: '!'; - border-radius: 100%; - color: var(--sl-color-neutral-0); - font-size: 14px; - font-weight: bold; - left: -12px; - line-height: 20px; - position: absolute; - height: 20px; - width: 20px; - text-align: center; - top: calc(50% - 10px); -} - -.markdown-section p.warn { - border-left-color: var(--sl-color-primary-600); -} - -.markdown-section p.warn:before { - background-color: var(--sl-color-primary-600); -} - -.markdown-section p.tip { - border-left-color: var(--sl-color-danger-600); -} - -.markdown-section p.tip:before { - background-color: var(--sl-color-danger-600); -} - -.markdown-section p.tip code, -.markdown-section p.warn code { - background-color: var(--sl-color-neutral-100); - white-space: nowrap; -} - -/* Sponsorship callouts */ -.sponsor-callout { - background: var(--sl-color-warning-100); - border-left: solid 3px var(--sl-color-warning-300); - border-radius: var(--sl-border-radius-medium); - padding: 0.5rem 1.5rem; -} - -@media screen and (max-width: 950px) { - .sponsor-callout__secondary-label { - display: none; - } +.repo-button--twitter sl-icon { + color: var(--sl-color-sky-500); } /* Component headers */ -.component-header { - margin-top: -1rem; +.component-header h1 { + margin-bottom: 0; } .component-header__tag { - border-bottom: none; - padding: 0; - margin: 0.75rem 0 0.25rem 0; + margin-top: -0.5rem; + margin-bottom: 0.5rem; } -.component-header__summary { - font-size: var(--sl-font-size-large); - line-height: 1.6; - border-top: solid 1px var(--sl-color-neutral-200); - margin-top: 2rem; -} - -.markdown-section .component-header__tag code { +.component-header__tag code { background: none; color: var(--sl-color-neutral-600); font-size: var(--sl-font-size-large); @@ -545,7 +1251,13 @@ kbd, } .component-header__info { - margin-bottom: 0.5rem; + margin-bottom: var(--sl-spacing-x-large); +} + +.component-summary { + font-size: var(--sl-font-size-large); + line-height: 1.6; + margin: 2rem 0; } /* Repo buttons */ @@ -661,9 +1373,3 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code { grid-column-start: span 6; } } - -.not-found-image { - display: block; - max-width: 460px; - margin: 2rem 0; -} diff --git a/docs/assets/styles/search.css b/docs/assets/styles/search.css new file mode 100644 index 000000000..c5a7d17e5 --- /dev/null +++ b/docs/assets/styles/search.css @@ -0,0 +1,347 @@ +/* Search plugin */ +:root, +:root.sl-theme-dark { + --docs-search-box-background: var(--sl-color-neutral-0); + --docs-search-box-border-width: 1px; + --docs-search-box-border-color: var(--sl-color-neutral-300); + --docs-search-box-color: var(--sl-color-neutral-600); + --docs-search-dialog-background: var(--sl-color-neutral-0); + --docs-search-border-width: var(--docs-border-width); + --docs-search-border-color: var(--docs-border-color); + --docs-search-text-color: var(--sl-color-neutral-900); + --docs-search-text-color-muted: var(--sl-color-neutral-500); + --docs-search-font-weight-normal: var(--sl-font-weight-normal); + --docs-search-font-weight-semibold: var(--sl-font-weight-semibold); + --docs-search-border-radius: calc(2 * var(--docs-border-radius)); + --docs-search-accent-color: var(--sl-color-primary-600); + --docs-search-icon-color: var(--sl-color-neutral-500); + --docs-search-icon-color-active: var(--sl-color-neutral-600); + --docs-search-shadow: var(--docs-shadow-x-large); + --docs-search-result-background-hover: var(--sl-color-neutral-100); + --docs-search-result-color-hover: var(--sl-color-neutral-900); + --docs-search-result-background-active: var(--sl-color-primary-600); + --docs-search-result-color-active: var(--sl-color-neutral-0); + --docs-search-focus-ring: var(--sl-focus-ring); + --docs-search-overlay-background: rgb(0 0 0 / 0.33); +} + +:root.sl-theme-dark { + --docs-search-overlay-background: rgb(71 71 71 / 0.33); +} + +body.search-visible { + padding-right: var(--docs-search-scroll-lock-size) !important; + overflow: hidden !important; +} + +/* Search box */ +.search-box { + flex: 1 1 auto; + display: flex; + align-items: center; + width: 100%; + border: none; + border-radius: 9999px; + background: var(--docs-search-box-background); + border: solid var(--docs-search-box-border-width) var(--docs-search-box-border-color); + font: inherit; + color: var(--docs-search-box-color); + padding: 0.75rem 1rem; + margin: var(--sl-spacing-large) 0; + cursor: pointer; +} + +.search-box span { + flex: 1 1 auto; + width: 1rem; + height: 1rem; + text-align: left; + line-height: 1; + margin: 0 0.75rem; +} + +.search-box:focus { + outline: none; +} + +.search-box:focus-visible { + outline: var(--docs-search-focus-ring); +} + +/* Site search */ +.search { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; +} + +.search[hidden] { + display: none; +} + +.search__overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--docs-search-overlay-background); + z-index: -1; +} + +.search__dialog { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + background: transparent; + border: none; + padding: 0; + margin: 0; +} + +.search__dialog:focus { + outline: none; +} + +.search__dialog::backdrop { + display: none; +} + +/* Fixes an iOS Safari 16.4 bug that draws the parent element's border radius incorrectly when showing/hiding results */ +.search__header { + background-color: var(--docs-search-dialog-background); + border-radius: var(--docs-search-border-radius); +} + +.search--has-results .search__header { + border-top-left-radius: var(--docs-search-border-radius); + border-top-right-radius: var(--docs-search-border-radius); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.search__content { + display: flex; + flex-direction: column; + width: 100%; + max-width: 500px; + max-height: calc(100vh - 20rem); + background-color: var(--docs-search-dialog-background); + border-radius: var(--docs-search-border-radius); + box-shadow: var(--docs-search-shadow); + padding: 0; + margin: 10rem auto; +} + +@media screen and (max-width: 900px) { + .search__content { + max-width: calc(100% - 2rem); + max-height: calc(90svh); + margin: 4vh 1rem; + } +} + +.search__input-wrapper { + display: flex; + align-items: center; +} + +.search__input-wrapper sl-icon { + width: 1.5rem; + height: 1.5rem; + flex: 0 0 auto; + color: var(--docs-search-icon-color); + margin: 0 1.5rem; +} + +.search__clear-button { + display: flex; + background: none; + border: none; + font: inherit; + padding: 0; + margin: 0; + cursor: pointer; +} + +.search__clear-button[hidden] { + display: none; +} + +.search__clear-button:active sl-icon { + color: var(--docs-search-icon-color-active); +} + +.search__input { + flex: 1 1 auto; + min-width: 0; + border: none; + font: inherit; + font-size: 1.5rem; + font-weight: var(--docs-search-font-weight-normal); + color: var(--docs-search-text-color); + background: transparent; + padding: 1rem 0; + margin: 0; +} + +.search__input::placeholder { + color: var(--docs-search-text-color-muted); +} + +.search__input::-webkit-search-decoration, +.search__input::-webkit-search-cancel-button, +.search__input::-webkit-search-results-button, +.search__input::-webkit-search-results-decoration { + display: none; +} + +.search__input:focus, +.search__input:focus-visible { + outline: none; +} + +.search__body { + flex: 1 1 auto; + overflow: auto; +} + +.search--has-results .search__body { + border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); +} + +.search__results { + display: none; + line-height: 1.2; + list-style: none; + padding: 0.5rem 0; + margin: 0; +} + +.search--has-results .search__results { + display: block; +} + +.search__results a { + display: block; + text-decoration: none; + padding: 0.5rem 1.5rem; +} + +.search__results a:focus-visible { + outline: var(--docs-search-focus-ring); +} + +.search__results li a:hover, +.search__results li a:hover small { + background-color: var(--docs-search-result-background-hover); + color: var(--docs-search-result-color-hover); +} + +.search__results li[data-selected='true'] a, +.search__results li[data-selected='true'] a * { + outline: none; + background-color: var(--docs-search-result-background-active); + color: var(--docs-search-result-color-active); +} + +.search__results h3 { + font-weight: var(--docs-search-font-weight-semibold); + margin: 0; +} + +.search__results small { + display: block; + color: var(--docs-search-text-color-muted); +} + +.search__result { + padding: 0; + margin: 0; +} + +.search__result a { + display: flex; + align-items: center; + gap: 1rem; +} + +.search__result-icon { + flex: 0 0 auto; + display: flex; + color: var(--docs-search-text-color-muted); +} + +.search__result-icon sl-icon { + font-size: 1.5rem; +} + +.search__result__details { + width: calc(100% - 3rem); +} + +.search__result-title, +.search__result-description, +.search__result-url { + max-width: 400px; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.search__result-title { + font-size: 1.2rem; + font-weight: var(--docs-search-font-weight-semibold); + color: var(--docs-search-accent-color); +} + +.search__result-description { + font-size: 0.875rem; + color: var(--docs-search-text-color); +} + +.search__result-url { + font-size: 0.875rem; + color: var(--docs-search-text-color-muted); +} + +.search__empty { + display: none; + border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); + text-align: center; + color: var(--docs-search-text-color-muted); + padding: 2rem; +} + +.search--no-results .search__empty { + display: block; +} + +.search__footer { + display: flex; + justify-content: center; + gap: 2rem; + border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; + padding: 1rem; +} + +.search__footer small { + color: var(--docs-search-text-color-muted); +} + +.search__footer small kbd:last-of-type { + margin-right: 0.25rem; +} + +@media screen and (max-width: 900px) { + .search__footer { + display: none; + } +} diff --git a/docs/eleventy.config.cjs b/docs/eleventy.config.cjs new file mode 100644 index 000000000..30742369a --- /dev/null +++ b/docs/eleventy.config.cjs @@ -0,0 +1,217 @@ +/* eslint-disable no-invalid-this */ +const fs = require('fs'); +const path = require('path'); +const lunr = require('lunr'); +const { capitalCase } = require('change-case'); +const { JSDOM } = require('jsdom'); +const { customElementsManifest, getAllComponents } = require('./_utilities/cem.cjs'); +const shoelaceFlavoredMarkdown = require('./_utilities/markdown.cjs'); +const activeLinks = require('./_utilities/active-links.cjs'); +const anchorHeadings = require('./_utilities/anchor-headings.cjs'); +const codePreviews = require('./_utilities/code-previews.cjs'); +const copyCodeButtons = require('./_utilities/copy-code-buttons.cjs'); +const externalLinks = require('./_utilities/external-links.cjs'); +const highlightCodeBlocks = require('./_utilities/highlight-code.cjs'); +const tableOfContents = require('./_utilities/table-of-contents.cjs'); +const prettier = require('./_utilities/prettier.cjs'); +const scrollingTables = require('./_utilities/scrolling-tables.cjs'); +const typography = require('./_utilities/typography.cjs'); + +const assetsDir = 'assets'; +const allComponents = getAllComponents(); +let hasBuiltSearchIndex = false; + +module.exports = function (eleventyConfig) { + // + // Global data + // + eleventyConfig.addGlobalData('baseUrl', 'https://shoelace.style/'); // the production URL + eleventyConfig.addGlobalData('layout', 'default'); // make 'default' the default layout + eleventyConfig.addGlobalData('toc', true); // enable the table of contents + eleventyConfig.addGlobalData('meta', { + title: 'Shoelace', + description: 'A forward-thinking library of web components.', + image: 'images/og-image.png', + version: customElementsManifest.package.version, + components: allComponents + }); + + // + // Layout aliases + // + eleventyConfig.addLayoutAlias('default', 'default.njk'); + + // + // Copy assets + // + eleventyConfig.addPassthroughCopy(assetsDir); + eleventyConfig.setServerPassthroughCopyBehavior('passthrough'); // emulates passthrough copy during --serve + + // + // Functions + // + + // Generates a URL relative to the site's root + eleventyConfig.addNunjucksGlobal('rootUrl', (value = '', absolute = false) => { + value = path.join('/', value); + return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value; + }); + + // Generates a URL relative to the site's asset directory + eleventyConfig.addNunjucksGlobal('assetUrl', (value = '', absolute = false) => { + value = path.join(`/${assetsDir}`, value); + return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value; + }); + + // Fetches a specific component's metadata + eleventyConfig.addNunjucksGlobal('getComponent', tagName => { + const component = allComponents.find(c => c.tagName === tagName); + if (!component) { + throw new Error( + `Unable to find a component called "${tagName}". Make sure the file name is the same as the component's tag ` + + `name (minus the sl- prefix).` + ); + } + return component; + }); + + // + // Custom markdown syntaxes + // + eleventyConfig.setLibrary('md', shoelaceFlavoredMarkdown); + + // + // Filters + // + eleventyConfig.addFilter('markdown', content => { + return shoelaceFlavoredMarkdown.render(content); + }); + + eleventyConfig.addFilter('markdownInline', content => { + return shoelaceFlavoredMarkdown.renderInline(content); + }); + + eleventyConfig.addFilter('classNameToComponentName', className => { + let name = capitalCase(className.replace(/^Sl/, '')); + if (name === 'Qr Code') name = 'QR Code'; // manual override + return name; + }); + + eleventyConfig.addFilter('removeSlPrefix', tagName => { + return tagName.replace(/^sl-/, ''); + }); + + // + // Transforms + // + eleventyConfig.addTransform('html-transform', function (content) { + // Parse the template and get a Document object + const doc = new JSDOM(content, { + // We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily + // identify which ones are internal and which ones are external. + url: `https://internal/` + }).window.document; + + // DOM transforms + activeLinks(doc, { pathname: this.page.url }); + anchorHeadings(doc, { + within: '#content .content__body', + levels: ['h2', 'h3', 'h4', 'h5'] + }); + tableOfContents(doc, { + levels: ['h2', 'h3'], + container: '#content .content__toc > ul', + within: '#content .content__body' + }); + codePreviews(doc); + externalLinks(doc, { target: '_blank' }); + highlightCodeBlocks(doc); + scrollingTables(doc); + copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks + typography(doc, '#content'); + + // Serialize the Document object to an HTML string and prepend the doctype + content = `\n${doc.documentElement.outerHTML}`; + + // String transforms + content = prettier(content); + + return content; + }); + + // + // Build a search index + // + eleventyConfig.on('eleventy.after', async ({ results }) => { + // We only want to build the search index on the first run so all pages get indexed. + if (hasBuiltSearchIndex) { + return; + } + + const map = {}; + const searchIndexFilename = path.join(eleventyConfig.dir.output, assetsDir, 'search.json'); + const lunrFilename = path.join(eleventyConfig.dir.output, assetsDir, 'scripts/lunr.js'); + const searchIndex = lunr(function () { + // The search index uses these field names extensively, so shortening them can save some serious bytes. The + // initial index file went from 468 KB => 401 KB by using single-character names! + this.ref('id'); // id + this.field('t', { boost: 50 }); // title + this.field('h', { boost: 25 }); // headings + this.field('c'); // content + + results.forEach((result, index) => { + const url = path.join('/', path.relative(eleventyConfig.dir.output, result.outputPath)); + const doc = new JSDOM(result.content, { + // We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily + // identify which ones are internal and which ones are external. + url: `https://internal/` + }).window.document; + const content = doc.querySelector('#content'); + + // Get title and headings + const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim(); + const headings = [...content.querySelectorAll('h1, h2, h3, h4')] + .map(heading => heading.textContent) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + + // Remove code blocks and whitespace from content + [...content.querySelectorAll('code[class|=language]')].forEach(code => code.remove()); + const textContent = content.textContent.replace(/\s+/g, ' ').trim(); + + // Update the index and map + this.add({ id: index, t: title, h: headings, c: textContent }); + map[index] = { title, url }; + }); + }); + + // Copy the Lunr search client and write the index + fs.copyFileSync('../node_modules/lunr/lunr.min.js', lunrFilename); + fs.writeFileSync(searchIndexFilename, JSON.stringify({ searchIndex, map }), 'utf-8'); + + hasBuiltSearchIndex = true; + }); + + // + // Dev server options (see https://www.11ty.dev/docs/dev-server/#options) + // + eleventyConfig.setServerOptions({ + domDiff: false, // disable dom diffing so custom elements don't break on reload, + port: 4000, // if port 4000 is taken, 11ty will use the next one available + watch: [] // additional files to watch that will trigger server updates (array of paths or globs) + }); + + // + // 11ty config + // + return { + dir: { + input: 'pages', + output: '../_site', + includes: '../_includes' // resolved relative to the input dir + }, + markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files + templateEngineOverride: ['njk'] // just Nunjucks and then markdown + }; +}; diff --git a/docs/pages/404.md b/docs/pages/404.md new file mode 100644 index 000000000..085c8d42a --- /dev/null +++ b/docs/pages/404.md @@ -0,0 +1,19 @@ +--- +meta: + title: Page Not Found + description: "The page you were looking for couldn't be found." +permalink: 404.html +toc: false +--- + +
      + +# Page Not Found + +![A UFO takes one of the little worker monsters](/assets/images/undraw-taken.svg) + +The page you were looking for couldn't be found. + +Press [[/]] to search, or [head back to the homepage](/). + +
      diff --git a/docs/pages/components/alert.md b/docs/pages/components/alert.md new file mode 100644 index 000000000..eb9ba05fb --- /dev/null +++ b/docs/pages/components/alert.md @@ -0,0 +1,436 @@ +--- +meta: + title: Alert + description: Alerts are used to display important messages inline or as toast notifications. +layout: component +--- + +```html:preview + + + This is a standard alert. You can customize its content and even the icon. + +``` + +```jsx:react +import { SlAlert, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + This is a standard alert. You can customize its content and even the icon. + +); +``` + +:::tip +Alerts will not be visible if the `open` attribute is not present. +::: + +## Examples + +### Variants + +Set the `variant` attribute to change the alert's variant. + +```html:preview + + + This is super informative
      + You can tell by how pretty the alert is. +
      + +
      + + + + Your changes have been saved
      + You can safely exit the app now. +
      + +
      + + + + Your settings have been updated
      + Settings will take affect on next login. +
      + +
      + + + + Your session has ended
      + Please login again to continue. +
      + +
      + + + + Your account has been deleted
      + We're very sorry to see you go! +
      +``` + +```jsx:react +import { SlAlert, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + This is super informative +
      + You can tell by how pretty the alert is. +
      + +
      + + + + Your changes have been saved +
      + You can safely exit the app now. +
      + +
      + + + + Your settings have been updated +
      + Settings will take affect on next login. +
      + +
      + + + + Your session has ended +
      + Please login again to continue. +
      + +
      + + + + Your account has been deleted +
      + We're very sorry to see you go! +
      + +); +``` + +### Closable + +Add the `closable` attribute to show a close button that will hide the alert. + +```html:preview + + + You can close this alert any time! + + + +``` + +```jsx:react +import { useState } from 'react'; +import { SlAlert, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [open, setOpen] = useState(true); + + function handleHide() { + setOpen(false); + setTimeout(() => setOpen(true), 2000); + } + + return ( + + + You can close this alert any time! + + ); +}; +``` + +### Without Icons + +Icons are optional. Simply omit the `icon` slot if you don't want them. + +```html:preview + Nothing fancy here, just a simple alert. +``` + +```jsx:react +import { SlAlert } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Nothing fancy here, just a simple alert. + +); +``` + +### Duration + +Set the `duration` attribute to automatically hide an alert after a period of time. This is useful for alerts that don't require acknowledgement. + +```html:preview +
      + Show Alert + + + + This alert will automatically hide itself after three seconds, unless you interact with it. + +
      + + + + +``` + +```jsx:react +import { useState } from 'react'; +import { SlAlert, SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .alert-duration sl-alert { + margin-top: var(--sl-spacing-medium); + } +`; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> +
      + setOpen(true)}> + Show Alert + + + setOpen(false)}> + + This alert will automatically hide itself after three seconds, unless you interact with it. + +
      + + + + ); +}; +``` + +### Toast Notifications + +To display an alert as a toast notification, or "toast", create the alert and call its `toast()` method. This will move the alert out of its position in the DOM and into [the toast stack](#the-toast-stack) where it will be shown. Once dismissed, it will be removed from the DOM completely. To reuse a toast, store a reference to it and call `toast()` again later on. + +You should always use the `closable` attribute so users can dismiss the notification. It's also common to set a reasonable `duration` when the notification doesn't require acknowledgement. + +```html:preview +
      + Primary + Success + Neutral + Warning + Danger + + + + This is super informative
      + You can tell by how pretty the alert is. +
      + + + + Your changes have been saved
      + You can safely exit the app now. +
      + + + + Your settings have been updated
      + Settings will take affect on next login. +
      + + + + Your session has ended
      + Please login again to continue. +
      + + + + Your account has been deleted
      + We're very sorry to see you go! +
      +
      + + +``` + +```jsx:react +import { useRef } from 'react'; +import { SlAlert, SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +function showToast(alert) { + alert.toast(); +} + +const App = () => { + const primary = useRef(null); + const success = useRef(null); + const neutral = useRef(null); + const warning = useRef(null); + const danger = useRef(null); + + return ( + <> + primary.current.toast()}> + Primary + + + success.current.toast()}> + Success + + + neutral.current.toast()}> + Neutral + + + warning.current.toast()}> + Warning + + + danger.current.toast()}> + Danger + + + + + This is super informative +
      + You can tell by how pretty the alert is. +
      + + + + Your changes have been saved +
      + You can safely exit the app now. +
      + + + + Your settings have been updated +
      + Settings will take affect on next login. +
      + + + + Your session has ended +
      + Please login again to continue. +
      + + + + Your account has been deleted +
      + We're very sorry to see you go! +
      + + ); +}; +``` + +### Creating Toasts Imperatively + +For convenience, you can create a utility that emits toast notifications with a function call rather than composing them in your HTML. To do this, generate the alert with JavaScript, append it to the body, and call the `toast()` method as shown in the example below. + +```html:preview +
      + Create Toast +
      + + +``` + +### The Toast Stack + +The toast stack is a fixed position singleton element created and managed internally by the alert component. It will be added and removed from the DOM as needed when toasts are shown. When more than one toast is visible, they will stack vertically in the toast stack. + +By default, the toast stack is positioned at the top-right of the viewport. You can change its position by targeting `.sl-toast-stack` in your stylesheet. To make toasts appear at the top-left of the viewport, for example, use the following styles. + +```css +.sl-toast-stack { + left: 0; + right: auto; +} +``` + +:::tip +By design, it is not possible to show toasts in more than one stack simultaneously. Such behavior is confusing and makes for a poor user experience. +::: diff --git a/docs/pages/components/animated-image.md b/docs/pages/components/animated-image.md new file mode 100644 index 000000000..67b07b197 --- /dev/null +++ b/docs/pages/components/animated-image.md @@ -0,0 +1,130 @@ +--- +meta: + title: Animated Image + description: A component for displaying animated GIFs and WEBPs that play and pause on interaction. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + +); +``` + +:::tip +This component uses `` to draw freeze frames, so images are subject to [cross-origin restrictions](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image). +::: + +## Examples + +### WEBP Images + +Both GIF and WEBP images are supported. + +```html:preview + +``` + +```jsx:react +import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + +); +``` + +### Setting a Width and Height + +To set a custom size, apply a width and/or height to the host element. + +```html:preview + + +``` + +{% raw %} + +```jsx:react +import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + +); +``` + +{% endraw %} + +### Customizing the Control Box + +You can change the appearance and location of the control box by targeting the `control-box` part in your styles. + +```html:preview + + + +``` + +```jsx:react +import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .animated-image-custom-control-box::part(control-box) { + top: auto; + right: auto; + bottom: 1rem; + left: 1rem; + background-color: deeppink; + border: none; + color: pink; + } +`; + +const App = () => ( + <> + + + + +); +``` diff --git a/docs/pages/components/animation.md b/docs/pages/components/animation.md new file mode 100644 index 000000000..1d405353c --- /dev/null +++ b/docs/pages/components/animation.md @@ -0,0 +1,347 @@ +--- +meta: + title: Animation + description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. +layout: component +--- + +To animate an element, wrap it in `` and set an animation `name`. The animation will not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options. + +```html:preview +
      +
      +
      +
      +
      +
      + + +``` + +```jsx:react +import { SlAnimation } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .animation-overview .box { + display: inline-block; + width: 100px; + height: 100px; + background-color: var(--sl-color-primary-600); + margin: 1.5rem; + } +`; + +const App = () => ( + <> +
      + +
      + + +
      + + +
      + + +
      + +
      + + + +); +``` + +:::tip +The animation will only be applied to the first child element found in ``. +::: + +## Examples + +### Animations & Easings + +This example demonstrates all of the baked-in animations and easings. Animations are based on those found in the popular [Animate.css](https://animate.style/) library. + +```html:preview +
      + +
      +
      + +
      + + + +
      +
      + + + + +``` + +### Using Intersection Observer + +Use an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to control the animation when an element enters or exits the viewport. For example, scroll the box below in and out of your screen. The animation stops when the box exits the viewport and restarts each time it enters the viewport. + +```html:preview +
      +
      +
      + + + + +``` + +```jsx:react +import { useEffect, useRef, useState } from 'react'; +import { SlAnimation } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .animation-scroll { + height: calc(100vh + 100px); + } + + .animation-scroll .box { + display: inline-block; + width: 100px; + height: 100px; + background-color: var(--sl-color-primary-600); + } +`; + +const App = () => { + const animation = useRef(null); + const box = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + animation.current.play = true; + } else { + animation.current.play = false; + animation.current.currentTime = 0; + } + }); + + if (box.current) { + observer.observe(box.current); + } + }, [box]); + + return ( + <> +
      + +
      + +
      + + + + ); +}; +``` + +### Custom Keyframe Formats + +Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats) to build custom animations. + +```html:preview +
      + +
      +
      +
      + + + + +``` + +```jsx:react +import { SlAnimation } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .animation-keyframes .box { + width: 100px; + height: 100px; + background-color: var(--sl-color-primary-600); + } +`; + +const App = () => ( + <> +
      + +
      + +
      + + + +); +``` + +### Playing Animations on Demand + +Animations won't play until you apply the `play` attribute. You can omit it initially, then apply it on demand such as after a user interaction. In this example, the button will animate once every time the button is clicked. + +```html:preview +
      + + Click me + +
      + + +``` + +```jsx:react +import { useState } from 'react'; +import { SlAnimation, SlButton } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [play, setPlay] = useState(false); + + return ( +
      + setPlay(false)}> + setPlay(true)}> + Click me + + +
      + ); +}; +``` diff --git a/docs/pages/components/avatar.md b/docs/pages/components/avatar.md new file mode 100644 index 000000000..80df7c2f5 --- /dev/null +++ b/docs/pages/components/avatar.md @@ -0,0 +1,207 @@ +--- +meta: + title: Avatar + description: Avatars are used to represent a person or object. +layout: component +--- + +By default, a generic icon will be shown. You can personalize avatars by adding custom icons, initials, and images. You should always provide a `label` for assistive devices. + +```html:preview + +``` + +```jsx:react +import { SlAvatar } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ; +``` + +## Examples + +### Images + +To use an image for the avatar, set the `image` and `label` attributes. This will take priority and be shown over initials and icons. +Avatar images can be lazily loaded by setting the `loading` attribute to `lazy`. + +```html:preview + + +``` + +```jsx:react +import { SlAvatar } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + +); +``` + +### Initials + +When you don't have an image to use, you can set the `initials` attribute to show something more personalized than an icon. + +```html:preview + +``` + +```jsx:react +import { SlAvatar } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ; +``` + +### Custom Icons + +When no image or initials are set, an icon will be shown. The default avatar shows a generic "user" icon, but you can customize this with the `icon` slot. + +```html:preview + + + + + + + + + + + +``` + +```jsx:react +import { SlAvatar, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + + + + + + + + + + +); +``` + +### Shapes + +Avatars can be shaped using the `shape` attribute. + +```html:preview + + + +``` + +```jsx:react +import { SlAvatar, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + + +); +``` + +### Avatar Groups + +You can group avatars with a few lines of CSS. + +```html:preview +
      + + + + + + + +
      + + +``` + +```jsx:react +import { SlAvatar, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .avatar-group sl-avatar:not(:first-of-type) { + margin-left: -1rem; + } + + .avatar-group sl-avatar::part(base) { + border: solid 2px var(--sl-color-neutral-0); + } +`; + +const App = () => ( + <> +
      + + + + + + + +
      + + + +); +``` diff --git a/docs/pages/components/badge.md b/docs/pages/components/badge.md new file mode 100644 index 000000000..1d8c06537 --- /dev/null +++ b/docs/pages/components/badge.md @@ -0,0 +1,230 @@ +--- +meta: + title: Badge + description: Badges are used to draw attention and display statuses or counts. +layout: component +--- + +```html:preview +Badge +``` + +```jsx:react +import { SlBadge } from '@shoelace-style/shoelace/dist/react'; + +const App = () => Badge; +``` + +## Examples + +### Variants + +Set the `variant` attribute to change the badge's variant. + +```html:preview +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import { SlBadge } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + Primary + Success + Neutral + Warning + Danger + +); +``` + +### Pill Badges + +Use the `pill` attribute to give badges rounded edges. + +```html:preview +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import { SlBadge } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + Primary + + + Success + + + Neutral + + + Warning + + + Danger + + +); +``` + +### Pulsating Badges + +Use the `pulse` attribute to draw attention to the badge with a subtle animation. + +```html:preview +
      + 1 + 1 + 1 + 1 + 1 +
      + + +``` + +```jsx:react +import { SlBadge } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .badge-pulse sl-badge:not(:last-of-type) { + margin-right: 1rem; + } +`; + +const App = () => ( + <> +
      + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + +
      + + + +); +``` + +### With Buttons + +One of the most common use cases for badges is attaching them to buttons. To make this easier, badges will be automatically positioned at the top-right when they're a child of a button. + +```html:preview + + Requests + 30 + + + + Warnings + 8 + + + + Errors + 6 + +``` + +{% raw %} + +```jsx:react +import { SlBadge, SlButton } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + Requests + 30 + + + + Warnings + + 8 + + + + + Errors + + 6 + + + +); +``` + +{% endraw %} + +### With Menu Items + +When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly. + +```html:preview + + Messages + Comments 4 + Replies 12 + +``` + +{% raw %} + +```jsx:react +import { SlBadge, SlButton, SlMenu, SlMenuItem, SlMenuLabel } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Messages + + Comments + + 4 + + + + Replies + + 12 + + + +); +``` + +{% endraw %} diff --git a/docs/pages/components/breadcrumb-item.md b/docs/pages/components/breadcrumb-item.md new file mode 100644 index 000000000..61d182921 --- /dev/null +++ b/docs/pages/components/breadcrumb-item.md @@ -0,0 +1,36 @@ +--- +meta: + title: Breadcrumb Item + description: Breadcrumb Items are used inside breadcrumbs to represent different links. +layout: component +--- + +```html:preview + + + + Home + + Clothing + Shirts + +``` + +```jsx:react +import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + + Home + + Clothing + Shirts + +); +``` + +:::tip +Additional demonstrations can be found in the [breadcrumb examples](/components/breadcrumb). +::: diff --git a/docs/pages/components/breadcrumb.md b/docs/pages/components/breadcrumb.md new file mode 100644 index 000000000..888861f5b --- /dev/null +++ b/docs/pages/components/breadcrumb.md @@ -0,0 +1,251 @@ +--- +meta: + title: Breadcrumb + description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy. +layout: component +--- + +Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation. + +```html:preview + + Catalog + Clothing + Women's + Shirts & Tops + +``` + +```jsx:react +import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Catalog + Clothing + Women's + Shirts & Tops + +); +``` + +## Examples + +### Breadcrumb Links + +By default, breadcrumb items are rendered as buttons so you can use them to navigate single-page applications. In this case, you'll need to add event listeners to handle clicks. + +For websites, you'll probably want to use links instead. You can make any breadcrumb item a link by applying an `href` attribute to it. Now, when the user activates it, they'll be taken to the corresponding page — no event listeners required. + +```html:preview + + Homepage + + Our Services + + Digital Media + + Web Design + +``` + +```jsx:react +import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Homepage + + Our Services + + Digital Media + + Web Design + +); +``` + +### Custom Separators + +Use the `separator` slot to change the separator that goes between breadcrumb items. Icons work well, but you can also use text or an image. + +```html:preview + + + First + Second + Third + + +
      + + + + First + Second + Third + + +
      + + + / + First + Second + Third + +``` + +```jsx:react +import '@shoelace-style/shoelace/dist/components/icon/icon.js'; +import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + First + Second + Third + + +
      + + + + First + Second + Third + + +
      + + + / + First + Second + Third + + +); +``` + +### Prefixes + +Use the `prefix` slot to add content before any breadcrumb item. + +```html:preview + + + + Home + + Articles + Traveling + +``` + +```jsx:react +import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + + Home + + Articles + Traveling + +); +``` + +### Suffixes + +Use the `suffix` slot to add content after any breadcrumb item. + +```html:preview + + Documents + Policies + + Security + + + +``` + +```jsx:react +import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Documents + Policies + + Security + + + +); +``` + +### With Dropdowns + +Dropdown menus can be placed in a prefix or suffix slot to provide additional options. + +```html:preview + + Homepage + Our Services + Digital Media + + Web Design + + + + + + Web Design + Web Development + Marketing + + + + +``` + +```jsx:react +import { + SlBreadcrumb, + SlBreadcrumbItem, + SlButton, + SlDropdown, + SlIcon, + SlMenu, + SlMenuItem +} from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Homepage + Our Services + Digital Media + + Web Design + + + + + + + Web Design + + Web Development + Marketing + + + + +); +``` diff --git a/docs/pages/components/button-group.md b/docs/pages/components/button-group.md new file mode 100644 index 000000000..fbba3ae2a --- /dev/null +++ b/docs/pages/components/button-group.md @@ -0,0 +1,494 @@ +--- +meta: + title: Button Group + description: Button groups can be used to group related buttons into sections. +layout: component +--- + +```html:preview + + Left + Center + Right + +``` + +```jsx:react +import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Left + Center + Right + +); +``` + +## Examples + +### Button Sizes + +All button sizes are supported, but avoid mixing sizes within the same button group. + +```html:preview + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

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

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

      + + + Left + Center + Right + + +

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

      + + + Left + Center + Right + + +

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

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

      + + + + Settings + + + + + Refresh + + + + + + Open + +``` + +```jsx:react +import { SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + Settings + + + + + Refresh + + + + + + Open + + +
      +
      + + + + Settings + + + + + Refresh + + + + + + Open + + +
      +
      + + + + Settings + + + + + Refresh + + + + + + Open + + +); +``` + +### Caret + +Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover. + +```html:preview +Small +Medium +Large +``` + +```jsx:react +import { SlButton } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + Small + + + Medium + + + Large + + +); +``` + +### Loading + +Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. Clicks will be suppressed until the loading state is removed. + +```html:preview +Default +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import { SlButton } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + Default + + + Primary + + + Success + + + Neutral + + + Warning + + + Danger + + +); +``` + +### Disabled + +Use the `disabled` attribute to disable a button. Clicks will be suppressed until the disabled state is removed. + +```html:preview +Default +Primary +Success +Neutral +Warning +Danger +``` + +```jsx:react +import { SlButton } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + Default + + + + Primary + + + + Success + + + + Neutral + + + + Warning + + + + Danger + + +); +``` + +### Styling Buttons + +This example demonstrates how to style buttons using a custom class. This is the recommended approach if you need to add additional variations. To customize an existing variation, modify the selector to target the button's `variant` attribute instead of a class (e.g. `sl-button[variant="primary"]`). + +```html:preview +Pink Button + + +``` diff --git a/docs/pages/components/card.md b/docs/pages/components/card.md new file mode 100644 index 000000000..6899cdd14 --- /dev/null +++ b/docs/pages/components/card.md @@ -0,0 +1,302 @@ +--- +meta: + title: Card + description: Cards can be used to group related subjects in a container. +layout: component +--- + +```html:preview + + A kitten sits patiently between a terracotta pot and decorative grasses. + + Mittens
      + This kitten is as cute as he is playful. Bring him home today!
      + 6 weeks old + +
      + More Info + +
      +
      + + +``` + +```jsx:react +import { SlButton, SlCard, SlRating } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .card-overview { + max-width: 300px; + } + + .card-overview small { + color: var(--sl-color-neutral-500); + } + + .card-overview [slot="footer"] { + display: flex; + justify-content: space-between; + align-items: center; + } +`; + +const App = () => ( + <> + + A kitten sits patiently between a terracotta pot and decorative grasses. + Mittens +
      + This kitten is as cute as he is playful. Bring him home today! +
      + 6 weeks old +
      + + More Info + + +
      +
      + + + +); +``` + +## Examples + +### Basic Card + +Basic cards aren't very exciting, but they can display any content you want them to. + +```html:preview + + This is just a basic card. No image, no header, and no footer. Just your content. + + + +``` + +```jsx:react +import { SlCard } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .card-basic { + max-width: 300px; + } +`; + +const App = () => ( + <> + + This is just a basic card. No image, no header, and no footer. Just your content. + + + + +); +``` + +### Card with Header + +Headers can be used to display titles and more. + +```html:preview + +
      + Header Title + +
      + + This card has a header. You can put all sorts of things in it! +
      + + +``` + +```jsx:react +import { SlCard, SlIconButton } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .card-header { + max-width: 300px; + } + + .card-header [slot="header"] { + display: flex; + align-items: center; + justify-content: space-between; + } + + .card-header h3 { + margin: 0; + } + + .card-header sl-icon-button { + font-size: var(--sl-font-size-medium); + } +`; + +const App = () => ( + <> + +
      + Header Title + +
      + This card has a header. You can put all sorts of things in it! +
      + + + +); +``` + +### Card with Footer + +Footers can be used to display actions, summaries, or other relevant content. + +```html:preview + + This card has a footer. You can put all sorts of things in it! + +
      + + Preview +
      +
      + + +``` + +```jsx:react +import { SlButton, SlCard, SlRating } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .card-footer { + max-width: 300px; + } + + .card-footer [slot="footer"] { + display: flex; + justify-content: space-between; + align-items: center; + } +`; + +const App = () => ( + <> + + This card has a footer. You can put all sorts of things in it! +
      + + + Preview + +
      +
      + + + +); +``` + +### Images + +Cards accept an `image` slot. The image is displayed atop the card and stretches to fit. + +```html:preview + + A kitten walks towards camera on top of pallet. + This is a kitten, but not just any kitten. This kitten likes walking along pallets. + + + +``` + +```jsx:react +import { SlCard } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .card-image { + max-width: 300px; + } +`; + +const App = () => ( + <> + + A kitten walks towards camera on top of pallet. + This is a kitten, but not just any kitten. This kitten likes walking along pallets. + + + + +); +``` diff --git a/docs/pages/components/carousel-item.md b/docs/pages/components/carousel-item.md new file mode 100644 index 000000000..42755baed --- /dev/null +++ b/docs/pages/components/carousel-item.md @@ -0,0 +1,84 @@ +--- +meta: + title: Carousel Item + description: A carousel item represent a slide within a carousel. +layout: component +--- + +```html:preview + + + The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash + + + A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash + + + The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash + + + A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash + + + A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash + + + A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash + + + The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash + + + A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash + + + A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash + + +); +``` + +:::tip +Additional demonstrations can be found in the [carousel examples](/components/carousel). +::: diff --git a/docs/pages/components/carousel.md b/docs/pages/components/carousel.md new file mode 100644 index 000000000..2258a3e0b --- /dev/null +++ b/docs/pages/components/carousel.md @@ -0,0 +1,1238 @@ +--- +meta: + title: Carousel + description: Carousels display an arbitrary number of content slides along a horizontal or vertical axis. +layout: component +--- + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +); +``` + +## Examples + +### Pagination + +Use the `pagination` attribute to show the total number of slides and the current slide as a set of interactive dots. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Navigation + +Use the `navigation` attribute to show previous and next buttons. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Looping + +By default, the carousel will not advanced beyond the first and last slides. You can change this behavior and force the carousel to "wrap" with the `loop` attribute. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Autoplay + +The carousel will automatically advance when the `autoplay` attribute is used. To change how long a slide is shown before advancing, set `autoplay-interval` to the desired number of milliseconds. For best results, use the `loop` attribute when autoplay is enabled. Note that autoplay will pause while the user interacts with the carousel. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +); +``` + +### Mouse Dragging + +The carousel uses [scroll snap](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap) to position slides at various snap positions. This allows users to scroll through the slides very naturally, especially on touch devices. Unfortunately, desktop users won't be able to click and drag with a mouse, which can feel unnatural. Adding the `mouse-dragging` attribute can help with this. + +This example is best demonstrated using a mouse. Try clicking and dragging the slide to move it. Then toggle the switch and try again. + +```html:preview +
      + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + Enable mouse dragging +
      + + +``` + +```jsx:react +import { useState } from 'react'; +import { SlCarousel, SlCarouselItem, SlDivider, SlSwitch } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [isEnabled, setIsEnabled] = useState(false); + + return ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + setIsEnabled(!isEnabled)}> + Enable mouse dragging + + + ); +}; +``` + +### Multiple Slides Per View + +The `slides-per-view` attribute makes it possible to display multiple slides at a time. You can also use the `slides-per-move` attribute to advance more than once slide at a time, if desired. + +```html:preview + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + +``` + +{% raw %} + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + +); +``` + +{% endraw %} + +### Adding and Removing Slides + +The content of the carousel can be changed by adding or removing carousel items. The carousel will update itself automatically. + +```html:preview + + Slide 1 + Slide 2 + Slide 3 + + + + + + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .dynamic-carousel { + --aspect-ratio: 3 / 2; + } + + .dynamic-carousel ~ .carousel-options { + display: flex; + justify-content: center; + margin-top: var(--sl-spacing-large); + } + + .dynamic-carousel sl-carousel-item { + flex: 0 0 100%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: var(--sl-font-size-2x-large); + } +`; + +const App = () => { + const [slides, setSlides] = useState(['#204ed8', '#be133d', '#6e28d9']); + const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']; + + const addSlide = () => { + setSlides([...slides, getRandomColor()]); + }; + + const removeSlide = () => { + setSlides(slides.slice(0, -1)); + }; + + return ( + <> + + {slides.map((color, i) => ( + + Slide {i} + + ))} + + +
      + Add slide + Remove slide +
      + + + + ); +}; +``` + +{% endraw %} + +### Vertical Scrolling + +Setting the `orientation` attribute to `vertical` will render the carousel in a vertical layout. If the content of your slides vary in height, you will need to set amn explicit `height` or `max-height` on the carousel using CSS. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +``` + +```jsx:react +import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .vertical { + max-height: 400px; + } + + .vertical::part(base) { + grid-template-areas: 'slides slides pagination'; + } + + .vertical::part(pagination) { + flex-direction: column; + } + + .vertical::part(navigation) { + transform: rotate(90deg); + display: flex; + } +`; + +const App = () => ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + +); +``` + +### Aspect Ratio + +Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + + 1/1 + 3/2 + 16/9 + + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import { SlCarousel, SlCarouselItem, SlDivider, SlSelect, SlOption } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [aspectRatio, setAspectRatio] = useState('3/2'); + + return ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + + + + setAspectRatio(event.target.value)} + > + 1 / 1 + 3 / 2 + 16 / 9 + + + + + ); +}; +``` + +{% endraw %} + +### Scroll Hint + +Use the `--scroll-hint` custom property to add inline padding in horizontal carousels and block padding in vertical carousels. This will make the closest slides slightly visible, hinting that there are more items in the carousel. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import { SlCarousel, SlCarouselItem, SlDivider, SlRange } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +); +``` + +{% endraw %} + +### Gallery Example + +The carousel has a robust API that makes it possible to extend and customize. This example syncs the active slide with a set of thumbnails, effectively creating a gallery-style carousel. + +```html:preview + + + The sun shines on the mountains and trees (by Adam Kool on Unsplash) + + + A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) + + + The sun is setting over a lavender field (by Leonard Cotte on Unsplash) + + + A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) + + + A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) + + + +
      +
      + Thumbnail by 1 + Thumbnail by 2 + Thumbnail by 3 + Thumbnail by 4 + Thumbnail by 5 +
      +
      + + + + +``` + +```jsx:react +import { useRef } from 'react'; +import { SlCarousel, SlCarouselItem, SlDivider, SlRange } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + .carousel-thumbnails { + --slide-aspect-ratio: 3 / 2; + } + + .thumbnails { + display: flex; + justify-content: center; + } + + .thumbnails__scroller { + display: flex; + gap: var(--sl-spacing-small); + overflow-x: auto; + scrollbar-width: none; + scroll-behavior: smooth; + scroll-padding: var(--sl-spacing-small); + } + + .thumbnails__scroller::-webkit-scrollbar { + display: none; + } + + .thumbnails__image { + width: 64px; + height: 64px; + object-fit: cover; + + opacity: 0.3; + will-change: opacity; + transition: 250ms opacity; + + cursor: pointer; + } + + .thumbnails__image.active { + opacity: 1; + } +`; + +const images = [ + { + src: '/assets/examples/carousel/mountains.jpg', + alt: 'The sun shines on the mountains and trees (by Adam Kool on Unsplash' + }, + { + src: '/assets/examples/carousel/waterfall.jpg', + alt: 'A waterfall in the middle of a forest (by Thomas Kelly on Unsplash' + }, + { + src: '/assets/examples/carousel/sunset.jpg', + alt: 'The sun is setting over a lavender field (by Leonard Cotte on Unsplash' + }, + { + src: '/assets/examples/carousel/field.jpg', + alt: 'A field of grass with the sun setting in the background (by Sapan Patel on Unsplash' + }, + { + src: '/assets/examples/carousel/valley.jpg', + alt: 'A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash' + } +]; + +const App = () => { + const carouselRef = useRef(); + const thumbnailsRef = useRef(); + const [currentSlide, setCurrentSlide] = useState(0); + + useEffect(() => { + const thumbnails = Array.from(thumbnailsRef.current.querySelectorAll('.thumbnails__image')); + + thumbnails[currentSlide]..scrollIntoView({ + block: 'nearest' + }); + }, [currentSlide]); + + const handleThumbnailClick = (index) => { + carouselRef.current.goToSlide(index); + } + + const handleSlideChange = (event) => { + const slideIndex = e.detail.index; + setCurrentSlide(slideIndex); + } + + return ( + <> + + {images.map({ src, alt }) => ( + + {alt} + + )} + + +
      +
      + {images.map({ src, alt }, i) => ( + {`Thumbnail handleThumbnailClick(i)} + src={src} + /> + )} +
      +
      + + + ); +}; +``` diff --git a/docs/pages/components/checkbox.md b/docs/pages/components/checkbox.md new file mode 100644 index 000000000..af1af5863 --- /dev/null +++ b/docs/pages/components/checkbox.md @@ -0,0 +1,163 @@ +--- +meta: + title: Checkbox + description: Checkboxes allow the user to toggle an option on or off. +layout: component +--- + +```html:preview +Checkbox +``` + +```jsx:react +import { SlCheckbox } from '@shoelace-style/shoelace/dist/react'; + +const App = () => Checkbox; +``` + +:::tip +This component works with standard `
      ` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. +::: + +## Examples + +### Checked + +Use the `checked` attribute to activate the checkbox. + +```html:preview +Checked +``` + +```jsx:react +import { SlCheckbox } from '@shoelace-style/shoelace/dist/react'; + +const App = () => Checked; +``` + +### Indeterminate + +Use the `indeterminate` attribute to make the checkbox indeterminate. + +```html:preview +Indeterminate +``` + +```jsx:react +import { SlCheckbox } from '@shoelace-style/shoelace/dist/react'; + +const App = () => Indeterminate; +``` + +### Disabled + +Use the `disabled` attribute to disable the checkbox. + +```html:preview +Disabled +``` + +```jsx:react +import { SlCheckbox } from '@shoelace-style/shoelace/dist/react'; + +const App = () => Disabled; +``` + +### Sizes + +Use the `size` attribute to change a checkbox's size. + +```html:preview +Small +
      +Medium +
      +Large +``` + +```jsx:react +import { SlCheckbox } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + Small +
      + Medium +
      + Large + +); +``` + +### Custom Validity + +Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string. + +```html:preview + + Check me +
      + Submit +
      + +``` + +{% raw %} + +```jsx:react +import { useEffect, useRef } from 'react'; +import { SlButton, SlCheckbox } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const checkbox = useRef(null); + const errorMessage = `Don't forget to check me!`; + + function handleChange() { + checkbox.current.setCustomValidity(checkbox.current.checked ? '' : errorMessage); + } + + function handleSubmit(event) { + event.preventDefault(); + alert('All fields are valid!'); + } + + useEffect(() => { + checkbox.current.setCustomValidity(errorMessage); + }, []); + + return ( +
      + + Check me + +
      + + Submit + +
      + ); +}; +``` + +{% endraw %} diff --git a/docs/pages/components/color-picker.md b/docs/pages/components/color-picker.md new file mode 100644 index 000000000..50fc139e7 --- /dev/null +++ b/docs/pages/components/color-picker.md @@ -0,0 +1,140 @@ +--- +meta: + title: Color Picker + description: Color pickers allow the user to select a color. +layout: component +--- + +```html:preview + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ; +``` + +:::tip +This component works with standard `
      ` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. +::: + +## Examples + +### Initial Value + +Use the `value` attribute to set an initial value for the color picker. + +```html:preview + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ; +``` + +### Opacity + +Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA, or HSVA based on `format`. + +```html:preview + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ; +``` + +### Formats + +Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl`, and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option. + +To prevent users from toggling the format themselves, add the `no-format-toggle` attribute. + +```html:preview + + + + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + + + +); +``` + +### Swatches + +Use the `swatches` attribute to add convenient presets to the color picker. Any format the color picker can parse is acceptable (including CSS color names), but each value must be separated by a semicolon (`;`). Alternatively, you can pass an array of color values to this property using JavaScript. + +```html:preview + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + +); +``` + +### Sizes + +Use the `size` attribute to change the color picker's trigger size. + +```html:preview + + + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + + +); +``` + +### Inline + +The color picker can be rendered inline instead of in a dropdown using the `inline` attribute. + +```html:preview + +``` + +```jsx:react +import { SlColorPicker } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ; +``` diff --git a/docs/pages/components/details.md b/docs/pages/components/details.md new file mode 100644 index 000000000..c0da4c425 --- /dev/null +++ b/docs/pages/components/details.md @@ -0,0 +1,134 @@ +--- +meta: + title: Details + description: Details show a brief summary and expand to show additional content. +layout: component +--- + + + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +``` + +```jsx:react +import { SlDetails } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +); +``` + +## Examples + +### Disabled + +Use the `disable` attribute to prevent the details from expanding. + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +``` + +```jsx:react +import { SlDetails } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +); +``` + +### Customizing the Summary Icon + +Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. To disable the animation, override the `rotate` property on the `summary-icon` part as shown below. + +```html:preview + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + + + +``` + +```jsx:react +import { SlDetails, SlIcon } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + sl-details.custom-icon::part(summary-icon) { + /* Disable the expand/collapse animation */ + rotate: none; + } +`; + +const App = () => ( + <> + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. + + + + +); +``` + +### Grouping Details + +Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `sl-show` event. + +```html:preview +
      + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +
      + + + + +``` diff --git a/docs/pages/components/dialog.md b/docs/pages/components/dialog.md new file mode 100644 index 000000000..718723491 --- /dev/null +++ b/docs/pages/components/dialog.md @@ -0,0 +1,321 @@ +--- +meta: + title: Dialog + description: 'Dialogs, sometimes called "modals", appear above the page and require the user''s immediate attention.' +layout: component +--- + + + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Dialog + + +``` + +```jsx:react +import { useState } from 'react'; +import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +## Examples + +### Custom Width + +Use the `--width` custom property to set the dialog's width. + +```html:preview + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Close + + +Open Dialog + + +``` + +{% raw %} + +```jsx:react +import { useState } from 'react'; +import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + setOpen(false)}> + Close + + + + setOpen(true)}>Open Dialog + + ); +}; +``` + +{% endraw %} + +### Scrolling + +By design, a dialog's height will never exceed that of the viewport. As such, dialogs will not scroll with the page ensuring the header and footer are always accessible to the user. + +```html:preview + +
      +

      Scroll down and give it a try! 👇

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

      Scroll down and give it a try! 👇

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

      Scroll down and give it a try! 👇

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

      Scroll down and give it a try! 👇

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

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

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