From 0cb9a54ff9f4da735abcfce79b1fd115e7fba83b Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 16 Mar 2026 09:04:10 +0100 Subject: [PATCH 01/72] =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 29 + docs/components/src/page-toc/page-toc.css | 4 + docs/components/src/page-toc/page-toc.ts | 3 + docs/components/tsconfig.json | 20 + docs/components/tsdown.config.ts | 9 + docs/website/eleventy.config.js | 39 + docs/website/package.json | 18 + docs/website/src/_includes/base.njk | 37 + docs/website/src/_includes/sidebar.njk | 43 + docs/website/src/_includes/toc.njk | 34 + .../website/src/components/actions/actions.md | 10 + docs/website/src/components/actions/button.md | 86 ++ docs/website/src/components/components.md | 9 + docs/website/src/css/main.css | 290 +++++ .../src/getting-started/getting-started.md | 9 + .../src/getting-started/introduction.md | 32 + docs/website/src/index.md | 14 + package.json | 2 + yarn.lock | 1002 ++++++++++++++++- 19 files changed, 1682 insertions(+), 8 deletions(-) create mode 100644 docs/components/package.json create mode 100644 docs/components/src/page-toc/page-toc.css create mode 100644 docs/components/src/page-toc/page-toc.ts create mode 100644 docs/components/tsconfig.json create mode 100644 docs/components/tsdown.config.ts create mode 100644 docs/website/eleventy.config.js create mode 100644 docs/website/package.json create mode 100644 docs/website/src/_includes/base.njk create mode 100644 docs/website/src/_includes/sidebar.njk create mode 100644 docs/website/src/_includes/toc.njk create mode 100644 docs/website/src/components/actions/actions.md create mode 100644 docs/website/src/components/actions/button.md create mode 100644 docs/website/src/components/components.md create mode 100644 docs/website/src/css/main.css create mode 100644 docs/website/src/getting-started/getting-started.md create mode 100644 docs/website/src/getting-started/introduction.md create mode 100644 docs/website/src/index.md diff --git a/docs/components/package.json b/docs/components/package.json new file mode 100644 index 0000000000..55ba146d2d --- /dev/null +++ b/docs/components/package.json @@ -0,0 +1,29 @@ +{ + "name": "@sl-design-system/docs-components", + "private": true, + "type": "module", + "version": "0.0.0", + "description": "Web components used in the documentation of the SL Design System", + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/sl-design-system/components.git" + }, + "scripts": { + "build": "tsdown" + }, + "exports": { + "./page-toc": "./dist/page-toc/page-toc.js", + "./package.json": "./package.json" + }, + "devDependencies": { + "@typescript/native-preview": "^7.0.0-dev.20260315.1", + "lit": "^3.3.2", + "tsdown": "^0.21.2", + "typescript": "^5.9.3", + "wireit": "^0.14.12" + }, + "peerDependencies": { + "lit": "^3.3.2" + } +} diff --git a/docs/components/src/page-toc/page-toc.css b/docs/components/src/page-toc/page-toc.css new file mode 100644 index 0000000000..4cef8a0ce4 --- /dev/null +++ b/docs/components/src/page-toc/page-toc.css @@ -0,0 +1,4 @@ +:host { + display: flex; + flex-direction: column; +} diff --git a/docs/components/src/page-toc/page-toc.ts b/docs/components/src/page-toc/page-toc.ts new file mode 100644 index 0000000000..42535e5412 --- /dev/null +++ b/docs/components/src/page-toc/page-toc.ts @@ -0,0 +1,3 @@ +export class PageToc extends LitElement { + +} \ No newline at end of file diff --git a/docs/components/tsconfig.json b/docs/components/tsconfig.json new file mode 100644 index 0000000000..77b838a84e --- /dev/null +++ b/docs/components/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts new file mode 100644 index 0000000000..ee70f1f88e --- /dev/null +++ b/docs/components/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + dts: { + tsgo: true, + }, + exports: true, + // ...config options +}) diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js new file mode 100644 index 0000000000..6b89ce36e7 --- /dev/null +++ b/docs/website/eleventy.config.js @@ -0,0 +1,39 @@ +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import eleventyNavigationPlugin from '@11ty/eleventy-navigation'; +import markdownItAnchor from 'markdown-it-anchor'; + +const require = createRequire(import.meta.url); +const themePath = dirname(require.resolve('@sl-design-system/sanoma-learning/package.json')); + +export default function (eleventyConfig) { + eleventyConfig.addPlugin(eleventyNavigationPlugin); + + eleventyConfig.amendLibrary('md', mdLib => { + mdLib.use(markdownItAnchor, { + permalink: markdownItAnchor.permalink.headerLink(), + level: [2, 3] + }); + }); + + eleventyConfig.addPassthroughCopy({ 'src/css': 'css' }); + + eleventyConfig.addPassthroughCopy({ + [join(themePath, 'light.css')]: 'theme/light.css', + [join(themePath, 'global.css')]: 'theme/global.css', + [join(themePath, 'fonts.css')]: 'theme/fonts.css', + [join(themePath, 'fonts')]: 'theme/fonts' + }); + + return { + dir: { + input: 'src', + output: 'dist', + includes: '_includes', + data: '_data' + }, + templateFormats: ['njk', 'md'], + markdownTemplateEngine: 'njk', + htmlTemplateEngine: 'njk' + }; +} diff --git a/docs/website/package.json b/docs/website/package.json new file mode 100644 index 0000000000..526968eeb2 --- /dev/null +++ b/docs/website/package.json @@ -0,0 +1,18 @@ +{ + "name": "@sl-design-system/website-v2", + "private": true, + "type": "module", + "scripts": { + "start": "eleventy --serve", + "build": "eleventy" + }, + "dependencies": { + "@11ty/eleventy": "^3.0.0", + "@11ty/eleventy-navigation": "^0.3.5", + "@sl-design-system/sanoma-learning": "workspace:*", + "markdown-it-anchor": "^9.0.0" + }, + "devDependencies": { + "lit": "^3.3.2" + } +} diff --git a/docs/website/src/_includes/base.njk b/docs/website/src/_includes/base.njk new file mode 100644 index 0000000000..7b5064bf5c --- /dev/null +++ b/docs/website/src/_includes/base.njk @@ -0,0 +1,37 @@ + + + + + + {{ title }} | SL Design System + + + + + + + +
+ + +
+

{{ title }}

+ {{ content | safe }} +
+ + +
+ + {% include "toc.njk" %} + + diff --git a/docs/website/src/_includes/sidebar.njk b/docs/website/src/_includes/sidebar.njk new file mode 100644 index 0000000000..0ba22f6861 --- /dev/null +++ b/docs/website/src/_includes/sidebar.njk @@ -0,0 +1,43 @@ +{%- macro renderNav(entries, activePage) -%} + +{%- endmacro -%} + +{%- set navPages = collections.all | eleventyNavigation -%} +{{ renderNav(navPages, page.url) }} diff --git a/docs/website/src/_includes/toc.njk b/docs/website/src/_includes/toc.njk new file mode 100644 index 0000000000..c45dc2dc48 --- /dev/null +++ b/docs/website/src/_includes/toc.njk @@ -0,0 +1,34 @@ + diff --git a/docs/website/src/components/actions/actions.md b/docs/website/src/components/actions/actions.md new file mode 100644 index 0000000000..6eacc5e15b --- /dev/null +++ b/docs/website/src/components/actions/actions.md @@ -0,0 +1,10 @@ +--- +title: Actions +layout: base.njk +eleventyNavigation: + key: Actions + parent: Components + order: 1 +--- + +Action components allow users to trigger operations or navigate. diff --git a/docs/website/src/components/actions/button.md b/docs/website/src/components/actions/button.md new file mode 100644 index 0000000000..53ac4d48d8 --- /dev/null +++ b/docs/website/src/components/actions/button.md @@ -0,0 +1,86 @@ +--- +title: Button +layout: base.njk +eleventyNavigation: + key: Button + parent: Actions + order: 1 +--- + +A button initiates an action when clicked, like redirecting to a new page or submitting a form. It is a key element for interaction and action. + +## Usage + +Use buttons for the most important actions you want users to take. Use clear, descriptive labels that explain what happens when the button is clicked. + +```html +Click me +``` + +### Variants + +Buttons come in several variants to indicate the level of emphasis: + +- **Primary** — For the main action on a page. Use sparingly. +- **Secondary** — For supporting actions. This is the default. +- **Ghost** — For low-emphasis actions that blend with surrounding content. +- **Danger** — For destructive actions like deleting data. + +```html +Save +Cancel +Learn more +Delete +``` + +### Sizes + +Buttons are available in three sizes: + +- `sm` — Small, for compact UIs +- `md` — Medium, the default +- `lg` — Large, for prominent actions + +```html +Small +Medium +Large +``` + +## Accessibility + +The button component renders a native ` + + +
+ + = 0 ? `result-${this.activeIndex}` : nothing} + aria-controls="search-results" + placeholder="Search documentation..." + role="combobox" + aria-expanded="true" + aria-autocomplete="list" + > + +
    +
  • + +
    + Button + Buttons represent actions that are available to the user. + docs/components/button +
    +
  • +
  • + +
    + Dialog + + Dialogs, sometimes called "modals", appear above the page content. + + docs/components/dialog +
    +
  • +
  • + +
    + Drawer + Drawers slide in from a container to expose additional options. + docs/components/drawer +
    +
  • +
  • + +
    + Visual Tests + A page to visually test component styles against native styles. + docs/resources/visual-tests +
    +
  • +
+
+
+ `; + } + + #open(): void { + this.dialog?.showModal(); + this.activeIndex = -1; + + requestAnimationFrame(() => { + this.renderRoot.querySelector('sl-search-field')?.focus(); + }); + } + + #onBackdropClick(event: MouseEvent): void { + const dialog = this.dialog; + + if (!dialog || dialog !== event.composedPath()[0]) { + return; + } + + const rect = dialog.getBoundingClientRect(); + + if ( + event.clientY < rect.top || + event.clientY > rect.bottom || + event.clientX < rect.left || + event.clientX > rect.right + ) { + dialog.close(); + } + } + + #onDialogKeydown(event: KeyboardEvent): void { + if (event.key === 'Escape') { + event.preventDefault(); + this.dialog?.close(); + return; + } + + const items = this.renderRoot.querySelectorAll('.results li'); + + if (!items.length) { + return; + } + + if (event.key === 'ArrowDown') { + event.preventDefault(); + this.activeIndex = this.activeIndex < items.length - 1 ? this.activeIndex + 1 : 0; + items[this.activeIndex]?.scrollIntoView({ block: 'nearest' }); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + this.activeIndex = this.activeIndex > 0 ? this.activeIndex - 1 : items.length - 1; + items[this.activeIndex]?.scrollIntoView({ block: 'nearest' }); + } else if ((event.key === 'Enter' || event.key === ' ') && this.activeIndex >= 0) { + event.preventDefault(); + items[this.activeIndex]?.dispatchEvent(new Event('click', { bubbles: true })); + } + } +} diff --git a/docs/components/src/sidebar/sidebar.css b/docs/components/src/sidebar/sidebar.css new file mode 100644 index 0000000000..3d10ff084a --- /dev/null +++ b/docs/components/src/sidebar/sidebar.css @@ -0,0 +1,72 @@ +:host { + background: var(--sl-elevation-surface-raised-sunken); + block-size: 100dvh; + display: flex; + flex-direction: column; + inset-block-start: 0; + overflow: hidden; + position: sticky; +} + +header { + padding: var(--sl-size-300); + padding-block-end: var(--sl-size-200); +} + +header a { + display: block; + text-decoration: none; +} + +header img { + block-size: 2rem; + display: block; + inline-size: auto; +} + +.logo-dark { + display: none; +} + +:host-context([data-color-scheme='dark']) .logo-light { + display: none; +} + +:host-context([data-color-scheme='dark']) .logo-dark { + display: block; +} + +doc-search { + padding-inline: var(--sl-size-300); +} + +.body { + flex: 1; + overflow-y: auto; + padding-block-end: var(--sl-size-300); + padding-inline: var(--sl-size-300); +} + +footer { + align-items: center; + /* stylelint-disable-next-line custom-property-pattern */ + border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-default); + display: flex; + justify-content: space-between; + padding: var(--sl-size-200) var(--sl-size-300); +} + +footer a { + align-items: center; + color: var(--sl-color-foreground-plain); + display: inline-flex; + text-decoration: none; +} + +footer a:hover { + color: var(--sl-color-foreground-accent-blue-bold); +} + +footer sl-icon { + font-size: var(--sl-size-300); +} diff --git a/docs/components/src/sidebar/sidebar.spec.ts b/docs/components/src/sidebar/sidebar.spec.ts new file mode 100644 index 0000000000..f3553c2412 --- /dev/null +++ b/docs/components/src/sidebar/sidebar.spec.ts @@ -0,0 +1,99 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it } from 'vitest'; + +// Use dynamic import from dist to avoid CSS module resolution issues in browser tests +const { Sidebar } = await import('@sl-design-system/doc-components/sidebar/sidebar'); + +try { + customElements.define('doc-sidebar', Sidebar); +} catch { + /* empty */ +} + +describe('doc-sidebar', () => { + let el: InstanceType; + + beforeEach(async () => { + el = await fixture(html``); + }); + + describe('structure', () => { + it('should render a header with a link', () => { + const link = el.renderRoot.querySelector('header a'); + + expect(link).to.exist; + expect(link).to.have.attribute('href', '/'); + expect(link).to.have.attribute('aria-label', 'SL Design System'); + }); + + it('should render logo images in the header', () => { + const lightLogo = el.renderRoot.querySelector('header img.logo-light'); + const darkLogo = el.renderRoot.querySelector('header img.logo-dark'); + + expect(lightLogo).to.exist; + expect(lightLogo).to.have.attribute('src', '/assets/logo-black.svg'); + expect(darkLogo).to.exist; + expect(darkLogo).to.have.attribute('src', '/assets/logo.svg'); + }); + + it('should render a body section with a site-nav', () => { + const body = el.renderRoot.querySelector('.body'); + const siteNav = el.renderRoot.querySelector('doc-site-nav'); + + expect(body).to.exist; + expect(siteNav).to.exist; + }); + + it('should render a slot inside the site-nav', () => { + const slot = el.renderRoot.querySelector('doc-site-nav slot'); + + expect(slot).to.exist; + }); + + it('should render a footer', () => { + const footer = el.renderRoot.querySelector('footer'); + + expect(footer).to.exist; + }); + + it('should render a GitHub link in the footer', () => { + const link = el.renderRoot.querySelector('footer a'); + + expect(link).to.exist; + expect(link).to.have.attribute('href', 'https://github.com/sl-design-system/components'); + expect(link).to.have.attribute('target', '_blank'); + expect(link).to.have.attribute('rel', 'noopener noreferrer'); + }); + + it('should render a GitHub icon in the footer link', () => { + const icon = el.renderRoot.querySelector('footer a sl-icon'); + + expect(icon).to.exist; + expect(icon).to.have.attribute('name', 'fab-github'); + }); + + it('should render a theme switch in the footer', () => { + const themeSwitch = el.renderRoot.querySelector('footer doc-theme-switch'); + + expect(themeSwitch).to.exist; + }); + }); + + describe('slotted content', () => { + beforeEach(async () => { + el = await fixture(html` + + Navigation content + + `); + }); + + it('should slot content into the site-nav', () => { + const slot = el.renderRoot.querySelector('doc-site-nav slot') as HTMLSlotElement; + const assignedNodes = slot?.assignedNodes({ flatten: true }); + + expect(assignedNodes?.length).to.be.greaterThan(0); + }); + }); +}); diff --git a/docs/components/src/sidebar/sidebar.stories.ts b/docs/components/src/sidebar/sidebar.stories.ts new file mode 100644 index 0000000000..28796a08d7 --- /dev/null +++ b/docs/components/src/sidebar/sidebar.stories.ts @@ -0,0 +1,52 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type Story = StoryObj; + +try { + const { Sidebar } = await import('./sidebar.js'); + customElements.define('doc-sidebar', Sidebar); +} catch { + /* empty */ +} + +export default { + title: 'Sidebar', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen' + }, + render: () => html` + +
+ + + + + + + + + + + + + +
+

Page Title

+

Main content area. The sidebar should remain fixed while this content scrolls.

+
+
+ ` +} satisfies Meta; + +export const Basic: Story = {}; diff --git a/docs/components/src/sidebar/sidebar.ts b/docs/components/src/sidebar/sidebar.ts new file mode 100644 index 0000000000..4638920ed2 --- /dev/null +++ b/docs/components/src/sidebar/sidebar.ts @@ -0,0 +1,51 @@ +import { faGithub } from '@fortawesome/free-brands-svg-icons'; +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Icon } from '@sl-design-system/icon'; +import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; +import { Search } from '../search/search.js'; +import { SiteNav } from '../site-nav/site-nav.js'; +import { ThemeSwitch } from '../theme-switch/theme-switch.js'; +import styles from './sidebar.css' with { type: 'css' }; + +Icon.register(faGithub); + +export class Sidebar extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'doc-search': Search, + 'doc-site-nav': SiteNav, + 'doc-theme-switch': ThemeSwitch, + 'sl-icon': Icon + }; + } + + /** @internal */ + static styles: CSSResultGroup = styles; + + render(): TemplateResult { + return html` +
+ + SL Design System + SL Design System + +
+ + + +
+ + + +
+ + + `; + } +} diff --git a/docs/components/src/site-nav/nav-group.css b/docs/components/src/site-nav/nav-group.css new file mode 100644 index 0000000000..54a7ec50af --- /dev/null +++ b/docs/components/src/site-nav/nav-group.css @@ -0,0 +1,17 @@ +:host { + display: block; +} + +:host(:not(:first-of-type)) { + margin-block-start: var(--sl-size-300); +} + +h2 { + color: var(--sl-color-foreground-plain); + font: var(--sl-text-new-body-sm); + font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; + font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); + margin: 0; + padding-block: var(--sl-size-075); + padding-inline: var(--sl-size-100); +} diff --git a/docs/components/src/site-nav/nav-group.spec.ts b/docs/components/src/site-nav/nav-group.spec.ts new file mode 100644 index 0000000000..aac9b52048 --- /dev/null +++ b/docs/components/src/site-nav/nav-group.spec.ts @@ -0,0 +1,112 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it } from 'vitest'; + +const { NavGroup } = await import('@sl-design-system/doc-components/site-nav/site-nav'); + +try { + customElements.define('doc-nav-group', NavGroup); +} catch { + /* empty */ +} + +describe('doc-nav-group', () => { + let el: InstanceType; + + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(NavGroup); + }); + + it('should not have a heading property', () => { + expect(el.heading).to.be.undefined; + }); + + it('should not render a heading element when none is set', () => { + const h2 = el.renderRoot.querySelector('h2'); + + expect(h2).to.not.exist; + }); + + it('should render a slot for child items', () => { + const slot = el.renderRoot.querySelector('slot'); + + expect(slot).to.exist; + }); + }); + + describe('with heading', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should have the heading property set', () => { + expect(el.heading).to.equal('Getting Started'); + }); + + it('should render an h2 element', () => { + const h2 = el.renderRoot.querySelector('h2'); + + expect(h2).to.exist; + }); + + it('should display the heading text', () => { + const h2 = el.renderRoot.querySelector('h2'); + + expect(h2).to.have.text('Getting Started'); + }); + + it('should still render a slot', () => { + const slot = el.renderRoot.querySelector('slot'); + + expect(slot).to.exist; + }); + }); + + describe('updating heading', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should update the heading when the property changes', async () => { + el.heading = 'Updated'; + await el.updateComplete; + + const h2 = el.renderRoot.querySelector('h2'); + + expect(h2).to.have.text('Updated'); + }); + + it('should remove the heading when set to undefined', async () => { + el.heading = undefined; + await el.updateComplete; + + const h2 = el.renderRoot.querySelector('h2'); + + expect(h2).to.not.exist; + }); + }); + + describe('with slotted content', () => { + beforeEach(async () => { + el = await fixture(html` + + Child content + + `); + }); + + it('should slot the child content', () => { + const slot = el.renderRoot.querySelector('slot') as HTMLSlotElement, + assigned = slot.assignedElements({ flatten: true }); + + expect(assigned).to.have.length(1); + expect(assigned[0]).to.have.class('test-child'); + }); + }); +}); diff --git a/docs/components/src/site-nav/nav-group.ts b/docs/components/src/site-nav/nav-group.ts new file mode 100644 index 0000000000..e89074b5f8 --- /dev/null +++ b/docs/components/src/site-nav/nav-group.ts @@ -0,0 +1,32 @@ +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; +import { property } from 'lit/decorators.js'; +import styles from './nav-group.css' with { type: 'css' }; + +export class NavGroup extends LitElement { + /** @internal */ + static styles: CSSResultGroup = styles; + + /** The section heading text. */ + @property() heading?: string; + + override connectedCallback(): void { + super.connectedCallback(); + + this.setAttribute('role', 'group'); + } + + override render(): TemplateResult { + return html` + ${this.heading ? html`

${this.heading}

` : nothing} + + `; + } + + override updated(): void { + if (this.heading) { + this.setAttribute('aria-label', this.heading); + } else { + this.removeAttribute('aria-label'); + } + } +} diff --git a/docs/components/src/site-nav/nav-item.css b/docs/components/src/site-nav/nav-item.css new file mode 100644 index 0000000000..2de82a90c6 --- /dev/null +++ b/docs/components/src/site-nav/nav-item.css @@ -0,0 +1,131 @@ +/* stylelint-disable custom-property-pattern */ + +:host { + --_bg: transparent; + --_bg-mix: transparent; + --_bg-opacity: 0; + --_color: var(--sl-color-foreground-plain); + --_font-weight: inherit; + --nav-indent: var(--sl-size-200); + + display: block; + outline: none; +} + +:host(:hover) { + --_bg-mix: var(--sl-color-background-accent-grey-subtlest); + --_bg-opacity: var(--sl-opacity-interactive-plain-hover); +} + +:host(:active) { + --_bg-opacity: var(--sl-opacity-interactive-plain-active); +} + +:host([aria-current='page']) { + --_bg: var(--sl-color-background-selected-subtlest); + --_bg-mix: var(--sl-color-background-selected-subtlest); + --_bg-opacity: 0; + --_color: var(--sl-color-foreground-accent-blue-bold); + --_font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); +} + +:host([aria-current='page']:hover) { + --_bg-mix: var(--sl-color-background-selected-bold); + --_bg-opacity: var(--sl-opacity-interactive-plain-hover); +} + +:host([aria-current='page']:active) { + --_bg-opacity: var(--sl-opacity-interactive-plain-active); +} + +:host(:focus-visible) summary, +:host(:focus-visible) .leaf { + outline: var(--sl-size-borderWidth-focusRing) solid var(--sl-color-border-focused); + outline-offset: calc(var(--sl-size-borderWidth-focusRing) * -1); +} + +details { + border: none; +} + +summary { + align-items: center; + background: color-mix(in srgb, var(--_bg), var(--_bg-mix) calc(100% * var(--_bg-opacity))); + border-radius: var(--sl-size-borderRadius-md); + color: var(--_color); + cursor: pointer; + display: flex; + font-weight: var(--_font-weight); + gap: var(--sl-size-100); + list-style: none; + padding-block: var(--sl-size-075); + padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); + position: relative; +} + +summary::-webkit-details-marker { + display: none; +} + +summary a { + color: inherit; + flex: 1; + text-decoration: none; +} + +summary .label { + flex: 1; +} + +.active { + background: var(--sl-color-background-selected-bold); + block-size: var(--sl-size-300); + border-radius: var(--sl-size-150); + inline-size: var(--sl-size-050); + inset-block-start: 50%; + inset-inline-start: calc(-1 * (var(--sl-size-050) + var(--sl-size-050))); + position: absolute; + translate: 0 -50%; +} + +:host([data-level='0']) .active { + display: none; +} + +.chevron { + margin-inline-start: auto; + transition: transform 0.15s ease; +} + +details[open] > summary .chevron { + transform: rotate(90deg); +} + +details[open] > slot { + border-inline-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + display: block; + margin-inline-start: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent) + 0.5em); +} + +.leaf { + align-items: center; + background: color-mix(in srgb, var(--_bg), var(--_bg-mix) calc(100% * var(--_bg-opacity))); + border-radius: var(--sl-size-borderRadius-md); + color: var(--_color); + display: flex; + font-weight: var(--_font-weight); + gap: var(--sl-size-100); + padding-block: var(--sl-size-075); + padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); + position: relative; + text-decoration: none; +} + +@media (prefers-reduced-motion: no-preference) { + :host(:where(:active, :focus-visible, :hover)) summary, + :host(:where(:active, :focus-visible, :hover)) .leaf { + transition-duration: var(--sl-animation-button-duration); + transition-property: background, color; + transition-timing-function: var(--sl-animation-button-easing); + } +} diff --git a/docs/components/src/site-nav/nav-item.spec.ts b/docs/components/src/site-nav/nav-item.spec.ts new file mode 100644 index 0000000000..326d976e78 --- /dev/null +++ b/docs/components/src/site-nav/nav-item.spec.ts @@ -0,0 +1,381 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it } from 'vitest'; + +const { NavItem } = await import('@sl-design-system/doc-components/site-nav/site-nav'); + +try { + customElements.define('doc-nav-item', NavItem); +} catch { + /* empty */ +} + +describe('doc-nav-item', () => { + let el: InstanceType; + + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(NavItem); + }); + + it('should not have a heading', () => { + expect(el.heading).to.be.undefined; + }); + + it('should not have an href', () => { + expect(el.href).to.be.undefined; + }); + + it('should not have an icon', () => { + expect(el.icon).to.be.undefined; + }); + + it('should not be active', () => { + expect(el.active).to.be.false; + }); + + it('should not be open', () => { + expect(el.open).to.be.false; + }); + + it('should not be expandable', () => { + expect(el.expandable).to.be.false; + }); + + it('should have level 0', () => { + expect(el.level).to.equal(0); + }); + }); + + describe('leaf item', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render a link', () => { + const link = el.renderRoot.querySelector('a.leaf'); + + expect(link).to.exist; + }); + + it('should have the correct href', () => { + const link = el.renderRoot.querySelector('a.leaf'); + + expect(link).to.have.attribute('href', '/'); + }); + + it('should display the heading text', () => { + const link = el.renderRoot.querySelector('a.leaf'); + + expect(link).to.have.trimmed.text('Home'); + }); + + it('should not render a details element', () => { + const details = el.renderRoot.querySelector('details'); + + expect(details).to.not.exist; + }); + + it('should not render a chevron icon', () => { + const chevron = el.renderRoot.querySelector('.chevron'); + + expect(chevron).to.not.exist; + }); + + it('should not have aria-current when not active', () => { + const link = el.renderRoot.querySelector('a.leaf'); + + expect(link).to.not.have.attribute('aria-current'); + }); + }); + + describe('active leaf item', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should have the active attribute reflected', () => { + expect(el).to.have.attribute('active'); + }); + + it('should set aria-current="page" on the link', () => { + const link = el.renderRoot.querySelector('a.leaf'); + + expect(link).to.have.attribute('aria-current', 'page'); + }); + + it('should remove aria-current when active is set to false', async () => { + el.active = false; + await el.updateComplete; + + const link = el.renderRoot.querySelector('a.leaf'); + + expect(link).to.not.have.attribute('aria-current'); + }); + }); + + describe('item with icon', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render an sl-icon element', () => { + const icon = el.renderRoot.querySelector('sl-icon'); + + expect(icon).to.exist; + }); + + it('should set the icon name', () => { + const icon = el.renderRoot.querySelector('sl-icon'); + + expect(icon).to.have.attribute('name', 'far-book'); + }); + + it('should not render an icon when icon is not set', async () => { + el.icon = undefined; + await el.updateComplete; + + const icon = el.renderRoot.querySelector('a.leaf sl-icon'); + + expect(icon).to.not.exist; + }); + }); + + describe('expandable item', () => { + beforeEach(async () => { + el = await fixture(html` + + + + + `); + }); + + it('should be expandable', () => { + expect(el.expandable).to.be.true; + }); + + it('should render a details element', () => { + const details = el.renderRoot.querySelector('details'); + + expect(details).to.exist; + }); + + it('should render a summary', () => { + const summary = el.renderRoot.querySelector('summary'); + + expect(summary).to.exist; + }); + + it('should display the heading in the summary', () => { + const summary = el.renderRoot.querySelector('summary'); + + expect(summary).to.have.trimmed.text('Guides'); + }); + + it('should render a chevron icon', () => { + const chevron = el.renderRoot.querySelector('.chevron'); + + expect(chevron).to.exist; + }); + + it('should have the chevron with name far-chevron-right', () => { + const chevron = el.renderRoot.querySelector('.chevron'); + + expect(chevron).to.have.attribute('name', 'far-chevron-right'); + }); + + it('should be collapsed by default', () => { + const details = el.renderRoot.querySelector('details'); + + expect(details).to.not.have.attribute('open'); + expect(el.open).to.be.false; + }); + + it('should expand when open is set to true', async () => { + el.open = true; + await el.updateComplete; + + const details = el.renderRoot.querySelector('details'); + + expect(details).to.have.attribute('open'); + }); + + it('should collapse when open is set to false', async () => { + el.open = true; + await el.updateComplete; + + el.open = false; + await el.updateComplete; + + const details = el.renderRoot.querySelector('details'); + + expect(details).to.not.have.attribute('open'); + }); + + it('should update the open property when details is toggled', async () => { + const details = el.renderRoot.querySelector('details')!; + + details.open = true; + details.dispatchEvent(new Event('toggle')); + await el.updateComplete; + + expect(el.open).to.be.true; + }); + + it('should reflect the open attribute', async () => { + el.open = true; + await el.updateComplete; + + expect(el).to.have.attribute('open'); + }); + + it('should render a slot for child items', () => { + const slot = el.renderRoot.querySelector('slot'); + + expect(slot).to.exist; + }); + + it('should render a span label when no href is set', () => { + const span = el.renderRoot.querySelector('summary .label'); + + expect(span).to.exist; + expect(span).to.have.text('Guides'); + }); + + it('should not render a link in the summary when no href is set', () => { + const link = el.renderRoot.querySelector('summary a'); + + expect(link).to.not.exist; + }); + }); + + describe('expandable item with href', () => { + beforeEach(async () => { + el = await fixture(html` + + + + `); + }); + + it('should render a link inside the summary', () => { + const link = el.renderRoot.querySelector('summary a'); + + expect(link).to.exist; + }); + + it('should have the correct href on the summary link', () => { + const link = el.renderRoot.querySelector('summary a'); + + expect(link).to.have.attribute('href', '/section/'); + }); + + it('should display the heading in the summary link', () => { + const link = el.renderRoot.querySelector('summary a'); + + expect(link).to.have.text('Section'); + }); + + it('should not render a span label', () => { + const span = el.renderRoot.querySelector('summary .label'); + + expect(span).to.not.exist; + }); + }); + + describe('expandable item with icon', () => { + beforeEach(async () => { + el = await fixture(html` + + + + `); + }); + + it('should render the icon in the summary', () => { + const icon = el.renderRoot.querySelector('summary sl-icon:not(.chevron)'); + + expect(icon).to.exist; + }); + + it('should set the correct icon name', () => { + const icon = el.renderRoot.querySelector('summary sl-icon:not(.chevron)'); + + expect(icon).to.have.attribute('name', 'far-code-branch'); + }); + + it('should render both the item icon and the chevron', () => { + const icons = el.renderRoot.querySelectorAll('summary sl-icon'); + + expect(icons).to.have.length(2); + }); + }); + + describe('nesting levels', () => { + let root: InstanceType, + child: InstanceType, + grandchild: InstanceType; + + beforeEach(async () => { + root = await fixture(html` + + + + + + `); + child = root.querySelector('doc-nav-item')!; + grandchild = child.querySelector('doc-nav-item')!; + }); + + it('should have level 0 for the root item', () => { + expect(root.level).to.equal(0); + }); + + it('should have level 1 for the child item', () => { + expect(child.level).to.equal(1); + }); + + it('should have level 2 for the grandchild item', () => { + expect(grandchild.level).to.equal(2); + }); + + it('should set --nav-level to 0 on the root', () => { + expect(root.style.getPropertyValue('--nav-level')).to.equal('0'); + }); + + it('should set --nav-level to 1 on the child', () => { + expect(child.style.getPropertyValue('--nav-level')).to.equal('1'); + }); + + it('should set --nav-level to 2 on the grandchild', () => { + expect(grandchild.style.getPropertyValue('--nav-level')).to.equal('2'); + }); + }); + + describe('initially open with children', () => { + beforeEach(async () => { + el = await fixture(html` + + + + `); + }); + + it('should render with details open', () => { + const details = el.renderRoot.querySelector('details'); + + expect(details).to.have.attribute('open'); + }); + + it('should have the open property set', () => { + expect(el.open).to.be.true; + }); + }); +}); diff --git a/docs/components/src/site-nav/nav-item.ts b/docs/components/src/site-nav/nav-item.ts new file mode 100644 index 0000000000..35c034f36a --- /dev/null +++ b/docs/components/src/site-nav/nav-item.ts @@ -0,0 +1,118 @@ +import { faChevronRight } from '@fortawesome/pro-regular-svg-icons'; +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Icon } from '@sl-design-system/icon'; +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import styles from './nav-item.css' with { type: 'css' }; + +Icon.register(faChevronRight); + +export class NavItem extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'sl-icon': Icon + }; + } + + /** @internal */ + static styles: CSSResultGroup = styles; + + /** The display text for this nav item. */ + @property() heading?: string; + + /** The URL this item links to. */ + @property() href?: string; + + /** The icon name (for top-level items). */ + @property() icon?: string; + + /** Whether this is the active/current page. */ + @property({ type: Boolean, reflect: true }) active = false; + + /** Whether the children are expanded. */ + @property({ type: Boolean, reflect: true }) open = false; + + /** @internal Whether this item has child nav items. */ + @state() expandable = false; + + /** @internal The nesting level (0-based), computed from DOM. */ + @state() level = 0; + + override connectedCallback(): void { + super.connectedCallback(); + + this.setAttribute('role', 'treeitem'); + + // Compute nesting level by counting ancestor nav-item elements + let level = 0, + parent = this.parentElement; + + while (parent) { + if (parent.localName === this.localName) { + level++; + } + parent = parent.parentElement; + } + + this.level = level; + this.dataset.level = String(level); + this.style.setProperty('--nav-level', String(level)); + + // Pre-detect expandability from light DOM children + this.expandable = !!this.querySelector(this.localName); + } + + override updated(): void { + // Sync ARIA states with properties + if (this.expandable) { + this.setAttribute('aria-expanded', String(this.open)); + } else { + this.removeAttribute('aria-expanded'); + } + + if (this.active) { + this.setAttribute('aria-current', 'page'); + } else { + this.removeAttribute('aria-current'); + } + } + + override render(): TemplateResult { + if (this.expandable) { + return html` +
+ + ${this.active ? html`` : nothing} + ${this.icon ? html`` : nothing} + ${this.href + ? html`${this.heading}` + : html`${this.heading}`} + + + +
+ `; + } + + return html` + + ${this.active ? html`` : nothing} + ${this.icon ? html`` : nothing} ${this.heading} + + + `; + } + + #onSlotChange(event: Event): void { + const slot = event.target as HTMLSlotElement, + children = slot.assignedElements({ flatten: true }); + + this.expandable = children.some(child => child.localName === this.localName); + } + + #onToggle(event: Event): void { + this.open = (event.target as HTMLDetailsElement).open; + } +} diff --git a/docs/components/src/site-nav/site-nav.css b/docs/components/src/site-nav/site-nav.css new file mode 100644 index 0000000000..9891b76e5c --- /dev/null +++ b/docs/components/src/site-nav/site-nav.css @@ -0,0 +1,8 @@ +:host { + display: block; +} + +nav { + display: flex; + flex-direction: column; +} diff --git a/docs/components/src/site-nav/site-nav.spec.ts b/docs/components/src/site-nav/site-nav.spec.ts new file mode 100644 index 0000000000..574198ba62 --- /dev/null +++ b/docs/components/src/site-nav/site-nav.spec.ts @@ -0,0 +1,60 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it } from 'vitest'; + +const { SiteNav, NavGroup, NavItem } = await import('@sl-design-system/doc-components/site-nav/site-nav'); + +try { + customElements.define('doc-site-nav', SiteNav); + customElements.define('doc-nav-group', NavGroup); + customElements.define('doc-nav-item', NavItem); +} catch { + /* empty */ +} + +describe('doc-site-nav', () => { + let el: InstanceType; + + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(SiteNav); + }); + + it('should render a nav element', () => { + const nav = el.renderRoot.querySelector('nav'); + + expect(nav).to.exist; + }); + + it('should have an aria-label on the nav', () => { + const nav = el.renderRoot.querySelector('nav'); + + expect(nav).to.have.attribute('aria-label', 'Site navigation'); + }); + + it('should render a slot for children', () => { + const slot = el.renderRoot.querySelector('slot'); + + expect(slot).to.exist; + }); + + it('should slot nav-group children', async () => { + el = await fixture(html` + + + + + + `); + + const slot = el.renderRoot.querySelector('slot') as HTMLSlotElement, + assigned = slot.assignedElements({ flatten: true }); + + expect(assigned).to.have.length(1); + expect(assigned[0].localName).to.equal('doc-nav-group'); + }); +}); diff --git a/docs/components/src/site-nav/site-nav.stories.ts b/docs/components/src/site-nav/site-nav.stories.ts new file mode 100644 index 0000000000..bac9059075 --- /dev/null +++ b/docs/components/src/site-nav/site-nav.stories.ts @@ -0,0 +1,174 @@ +import { + faBook, + faBookmark, + faCircleQuestion, + faCodeBranch, + faEnvelope, + faFileLines, + faHome, + faInfoCircle, + faLayerGroup, + faShieldCheck +} from '@fortawesome/pro-regular-svg-icons'; +import { Icon } from '@sl-design-system/icon'; +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +Icon.register( + faBook, + faBookmark, + faCircleQuestion, + faCodeBranch, + faEnvelope, + faFileLines, + faHome, + faInfoCircle, + faLayerGroup, + faShieldCheck +); + +type Props = Record; +type Story = StoryObj; + +try { + const { SiteNav } = await import('./site-nav.js'); + const { NavGroup } = await import('./nav-group.js'); + const { NavItem } = await import('./nav-item.js'); + customElements.define('doc-site-nav', SiteNav); + customElements.define('doc-nav-group', NavGroup); + customElements.define('doc-nav-item', NavItem); +} catch { + /* empty */ +} + +function onNavClick(event: Event): void { + const nav = event.currentTarget as HTMLElement, + target = (event.target as HTMLElement).closest('doc-nav-item'); + + if (!target || target.querySelector('doc-nav-item')) { + return; + } + + event.preventDefault(); + nav.querySelectorAll('doc-nav-item[active]').forEach(item => item.removeAttribute('active')); + target.setAttribute('active', ''); +} + +export default { + title: 'Site Navigation', + parameters: { + layout: 'padded' + }, + render: () => { + return html` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; + } +} satisfies Meta; + +export const Basic: Story = {}; + +export const Collapsed: Story = { + render: () => { + return html` + + + + + + + + + + + + + + `; + } +}; + +export const FlatItems: Story = { + render: () => { + return html` + + + + + + + + + `; + } +}; + +export const ActiveItem: Story = { + render: () => { + return html` + + + + + + + + + + + + + + + + + + `; + } +}; diff --git a/docs/components/src/site-nav/site-nav.ts b/docs/components/src/site-nav/site-nav.ts index 9057ebd7cf..a74edc8ece 100644 --- a/docs/components/src/site-nav/site-nav.ts +++ b/docs/components/src/site-nav/site-nav.ts @@ -1,8 +1,136 @@ -import { LitElement, type TemplateResult, html } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; +import { NavItem } from './nav-item.js'; +import styles from './site-nav.css' with { type: 'css' }; + +export { NavGroup } from './nav-group.js'; +export { NavItem } from './nav-item.js'; export class SiteNav extends LitElement { - render(): TemplateResult { - return html`Hello world`; + /** @internal */ + static styles: CSSResultGroup = styles; + + /** Returns all visible (not inside a collapsed parent) nav-items in DOM order. */ + #getVisibleItems(): NavItem[] { + return Array.from(this.querySelectorAll('*')) + .filter((el): el is NavItem => el instanceof NavItem) + .filter(item => { + let parent = item.parentElement; + + while (parent && parent !== this) { + if (parent instanceof NavItem && parent.expandable && !parent.open) { + return false; + } + parent = parent.parentElement; + } + + return true; + }); + } + + #focusItem(items: NavItem[], index: number): void { + items.forEach((item, i) => { + item.tabIndex = i === index ? 0 : -1; + }); + items[index]?.focus(); + } + + #onKeyDown = (event: KeyboardEvent): void => { + const items = this.#getVisibleItems(), + current = items.indexOf(document.activeElement as NavItem); + + if (current === -1 && !['Home', 'End'].includes(event.key)) { + return; + } + + let handled = true; + + switch (event.key) { + case 'ArrowDown': + this.#focusItem(items, Math.min(current + 1, items.length - 1)); + break; + case 'ArrowUp': + this.#focusItem(items, Math.max(current - 1, 0)); + break; + case 'ArrowRight': + if (items[current]?.expandable && !items[current].open) { + items[current].open = true; + } else if (items[current]?.expandable && items[current].open) { + // Move to first child + const next = current + 1; + if (next < items.length) { + this.#focusItem(items, next); + } + } + break; + case 'ArrowLeft': + if (items[current]?.expandable && items[current].open) { + items[current].open = false; + } else { + // Move to parent nav-item + let parent = items[current]?.parentElement; + while (parent && parent !== this) { + if (parent instanceof NavItem) { + const parentIdx = items.indexOf(parent); + if (parentIdx >= 0) { + this.#focusItem(items, parentIdx); + } + break; + } + parent = parent.parentElement; + } + } + break; + case 'Home': + this.#focusItem(items, 0); + break; + case 'End': + this.#focusItem(items, items.length - 1); + break; + case 'Enter': + case ' ': + if (items[current]?.href) { + window.location.href = items[current].href!; + } else if (items[current]?.expandable) { + items[current].open = !items[current].open; + } + break; + default: + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + override connectedCallback(): void { + super.connectedCallback(); + + this.addEventListener('keydown', this.#onKeyDown); + + // Set up roving tabindex after children are parsed + requestAnimationFrame(() => this.#initTabIndex()); + } + + override disconnectedCallback(): void { + this.removeEventListener('keydown', this.#onKeyDown); + + super.disconnectedCallback(); + } + + #initTabIndex(): void { + const items = this.#getVisibleItems(); + items.forEach((item, i) => { + item.tabIndex = i === 0 ? 0 : -1; + }); + } + + override render(): TemplateResult { + return html` + + `; } } diff --git a/docs/components/src/theme-switch/theme-switch.spec.ts b/docs/components/src/theme-switch/theme-switch.spec.ts new file mode 100644 index 0000000000..5644f8b56a --- /dev/null +++ b/docs/components/src/theme-switch/theme-switch.spec.ts @@ -0,0 +1,138 @@ +import { type Switch } from '@sl-design-system/switch'; +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { type ThemeSwitch } from './theme-switch.js'; + +// Register the component for testing +const { ThemeSwitch: ThemeSwitchClass } = await import('./theme-switch.js'); + +try { + customElements.define('doc-theme-switch', ThemeSwitchClass); +} catch { + /* empty */ +} + +describe('doc-theme-switch', () => { + let el: ThemeSwitch; + + describe('defaults', () => { + beforeEach(async () => { + document.documentElement.removeAttribute('data-color-scheme'); + + el = await fixture(html``); + }); + + it('should render a switch', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl).to.exist; + }); + + it('should have a sun icon for the off state', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl).to.have.attribute('icon-off', 'fas-sun-bright'); + }); + + it('should have a moon icon for the on state', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl).to.have.attribute('icon-on', 'fas-moon-stars'); + }); + + it('should have an aria-label on the switch', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl).to.have.attribute('aria-label', 'Switch between light and dark mode'); + }); + + it('should set the data-color-scheme attribute on the document', () => { + expect(document.documentElement).to.have.attribute('data-color-scheme'); + }); + + it('should have a medium switch by default', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl).to.not.have.attribute('size'); + }); + }); + + describe('light mode', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should have the color-scheme attribute set to light', () => { + expect(el).to.have.attribute('color-scheme', 'light'); + }); + + it('should not have the switch checked', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl?.checked).to.not.be.ok; + }); + + it('should set data-color-scheme to light on the document', () => { + expect(document.documentElement).to.have.attribute('data-color-scheme', 'light'); + }); + }); + + describe('dark mode', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should have the color-scheme attribute set to dark', () => { + expect(el).to.have.attribute('color-scheme', 'dark'); + }); + + it('should have the switch checked', () => { + const switchEl = el.renderRoot.querySelector('sl-switch'); + + expect(switchEl?.checked).to.be.true; + }); + + it('should set data-color-scheme to dark on the document', () => { + expect(document.documentElement).to.have.attribute('data-color-scheme', 'dark'); + }); + }); + + describe('toggling', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should switch to dark mode when the switch is toggled', async () => { + const switchEl = el.renderRoot.querySelector('sl-switch')!; + + switchEl.click(); + await el.updateComplete; + + expect(el.colorScheme).to.equal('dark'); + expect(document.documentElement).to.have.attribute('data-color-scheme', 'dark'); + }); + + it('should switch back to light mode when toggled again', async () => { + const switchEl = el.renderRoot.querySelector('sl-switch')!; + + switchEl.click(); + await el.updateComplete; + + switchEl.click(); + await el.updateComplete; + + expect(el.colorScheme).to.equal('light'); + expect(document.documentElement).to.have.attribute('data-color-scheme', 'light'); + }); + + it('should reflect the color-scheme attribute when toggled', async () => { + const switchEl = el.renderRoot.querySelector('sl-switch')!; + + switchEl.click(); + await el.updateComplete; + + expect(el).to.have.attribute('color-scheme', 'dark'); + }); + }); +}); diff --git a/docs/components/src/theme-switch/theme-switch.stories.ts b/docs/components/src/theme-switch/theme-switch.stories.ts new file mode 100644 index 0000000000..6a39e06c6c --- /dev/null +++ b/docs/components/src/theme-switch/theme-switch.stories.ts @@ -0,0 +1,36 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { type ThemeSwitch } from './theme-switch.js'; + +type Props = Pick; +type Story = StoryObj; + +try { + const { ThemeSwitch } = await import('./theme-switch.js'); + customElements.define('doc-theme-switch', ThemeSwitch); +} catch { + /* empty */ +} + +export default { + title: 'Theme Switch', + tags: ['autodocs'], + args: { + colorScheme: 'light' + }, + argTypes: { + colorScheme: { + control: 'inline-radio', + options: ['light', 'dark'] + } + }, + render: ({ colorScheme }) => html`` +} satisfies Meta; + +export const Light: Story = {}; + +export const Dark: Story = { + args: { + colorScheme: 'dark' + } +}; diff --git a/docs/components/src/theme-switch/theme-switch.ts b/docs/components/src/theme-switch/theme-switch.ts new file mode 100644 index 0000000000..3a5c7d8bfc --- /dev/null +++ b/docs/components/src/theme-switch/theme-switch.ts @@ -0,0 +1,62 @@ +import { faMoonStars, faSunBright } from '@fortawesome/pro-solid-svg-icons'; +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Icon } from '@sl-design-system/icon'; +import { Switch } from '@sl-design-system/switch'; +import { LitElement, type TemplateResult, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +export type ColorScheme = 'light' | 'dark'; + +Icon.register(faMoonStars, faSunBright); + +export class ThemeSwitch extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'sl-switch': Switch + }; + } + + /** The current color scheme. */ + @property({ reflect: true, attribute: 'color-scheme' }) colorScheme: ColorScheme = 'light'; + + connectedCallback(): void { + super.connectedCallback(); + + if (!this.hasAttribute('color-scheme')) { + this.colorScheme = this.#getPreferredColorScheme(); + } + + this.#applyColorScheme(); + } + + render(): TemplateResult { + return html` + + `; + } + + #onChange(): void { + this.colorScheme = this.colorScheme === 'light' ? 'dark' : 'light'; + this.#applyColorScheme(); + } + + #applyColorScheme(): void { + document.documentElement.setAttribute('data-color-scheme', this.colorScheme); + + const themeLink = document.getElementById('theme-css') as HTMLLinkElement | null; + if (themeLink) { + themeLink.href = `/theme/${this.colorScheme}.css`; + } + } + + #getPreferredColorScheme(): ColorScheme { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } +} diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index df959760f8..70dec063cd 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -44,8 +44,11 @@ export default defineConfig({ tsgo: true, }, entry: [ - 'src/nav/nav.ts', - 'src/page-toc/page-toc.ts' + 'src/page-toc/page-toc.ts', + 'src/search/search.ts', + 'src/sidebar/sidebar.ts', + 'src/site-nav/site-nav.ts', + 'src/theme-switch/theme-switch.ts' ], exports: true, platform: 'browser', diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index 696ec6a952..d02cd2792c 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -19,11 +19,13 @@ export default function (eleventyConfig) { }); eleventyConfig.addPassthroughCopy({ 'src/css': 'css' }); + eleventyConfig.addPassthroughCopy({ 'src/assets': 'assets' }); eleventyConfig.addWatchTarget('../components/dist/**/*.(css|js)'); eleventyConfig.addPassthroughCopy({ [join(themePath, 'light.css')]: 'theme/light.css', + [join(themePath, 'dark.css')]: 'theme/dark.css', [join(themePath, 'global.css')]: 'theme/global.css', [join(themePath, 'fonts.css')]: 'theme/fonts.css', [join(themePath, 'fonts')]: 'theme/fonts' diff --git a/docs/website/package.json b/docs/website/package.json index a7cfb97268..2d0e8e6e8e 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -12,7 +12,7 @@ "@sl-design-system/doc-components": "workspace:^", "@sl-design-system/sanoma-learning": "workspace:*", "@webcomponents/scoped-custom-element-registry": "^0.0.10", - "markdown-it-anchor": "^9.0.0" + "markdown-it-anchor": "^9.2.0" }, "devDependencies": { "esbuild": "^0.25.0", diff --git a/docs/website/src/_includes/base.njk b/docs/website/src/_includes/base.njk index e3aa697f5c..7907440a0b 100644 --- a/docs/website/src/_includes/base.njk +++ b/docs/website/src/_includes/base.njk @@ -4,20 +4,26 @@ {{ title }} | SL Design System + - + +
- +

{{ title }}

diff --git a/docs/website/src/_includes/sidebar.njk b/docs/website/src/_includes/sidebar.njk index 0ba22f6861..355fc80211 100644 --- a/docs/website/src/_includes/sidebar.njk +++ b/docs/website/src/_includes/sidebar.njk @@ -1,43 +1,49 @@ -{%- macro renderNav(entries, activePage) -%} - + + {%- endfor -%} {%- endmacro -%} {%- set navPages = collections.all | eleventyNavigation -%} -{{ renderNav(navPages, page.url) }} + + {%- for entry in navPages -%} + {%- if entry.children | length -%} + + {{ renderItems(entry.children, page.url) }} + + {%- else -%} + + {%- endif -%} + {%- endfor -%} + diff --git a/docs/website/src/assets/logo-black.svg b/docs/website/src/assets/logo-black.svg new file mode 100644 index 0000000000..c697850c88 --- /dev/null +++ b/docs/website/src/assets/logo-black.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/website/src/assets/logo.svg b/docs/website/src/assets/logo.svg new file mode 100644 index 0000000000..e9ca6e2005 --- /dev/null +++ b/docs/website/src/assets/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/website/src/css/main.css b/docs/website/src/css/main.css index 8f7f68b298..747db156bb 100644 --- a/docs/website/src/css/main.css +++ b/docs/website/src/css/main.css @@ -23,18 +23,18 @@ body { .site-layout { display: grid; - grid-template-columns: 1fr; grid-template-areas: - "sidebar" - "content" - "toc"; + 'sidebar' + 'content' + 'toc'; + grid-template-columns: 1fr; min-block-size: 100dvh; } -@media (min-width: 1024px) { +@media (width >= 1024px) { .site-layout { + grid-template-areas: 'sidebar content toc'; grid-template-columns: 280px 1fr 220px; - grid-template-areas: "sidebar content toc"; } } @@ -42,98 +42,8 @@ body { Sidebar ============================================ */ -.sidebar { +doc-sidebar { grid-area: sidebar; - background: var(--sl-elevation-surface-raised-sunken); - padding: var(--sl-size-300); - overflow-y: auto; -} - -@media (min-width: 1024px) { - .sidebar { - position: sticky; - inset-block-start: 0; - block-size: 100dvh; - } -} - -.sidebar__header { - margin-block-end: var(--sl-size-300); -} - -.sidebar__logo { - font: var(--sl-text-new-heading-md); - font-family: Roboto, var(--sl-text-new-typeset-fontFamily-heading), sans-serif; - font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); - color: var(--sl-color-foreground-plain); - text-decoration: none; -} - -/* Navigation list */ - -.nav-list { - list-style: none; - padding: 0; - margin: 0; -} - -.nav-list .nav-list { - padding-inline-start: var(--sl-size-200); -} - -.nav-item { - margin: 0; -} - -/* Links and summaries */ - -.nav-link { - display: block; - padding-block: var(--sl-size-075); - padding-inline: var(--sl-size-100); - color: var(--sl-color-foreground-plain); - text-decoration: none; - border-radius: var(--sl-size-borderRadius-md); - font: var(--sl-text-new-body-sm); - font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; -} - -a.nav-link:hover, -.nav-link a:hover { - background: var(--sl-color-background-accent-grey-subtlest); -} - -.nav-link.is-active, -.nav-link.is-active a { - color: var(--sl-color-foreground-accent-blue-bold); - font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); -} - -/* Details/Summary for collapsible sections */ - -details { - border: none; -} - -details > summary { - cursor: pointer; - list-style: none; -} - -details > summary::-webkit-details-marker { - display: none; -} - -details > summary::before { - content: "▸"; - display: inline-block; - inline-size: var(--sl-size-200); - font-size: var(--sl-size-150); - transition: transform 0.15s ease; -} - -details[open] > summary::before { - transform: rotate(90deg); } /* ============================================ @@ -142,40 +52,38 @@ details[open] > summary::before { .content { grid-area: content; - padding: var(--sl-size-400); - max-inline-size: 48rem; inline-size: 100%; + max-inline-size: 48rem; + padding: var(--sl-size-400); } .content h1 { + color: var(--sl-color-foreground-plain); font: var(--sl-text-new-heading-2xl); font-family: Roboto, var(--sl-text-new-typeset-fontFamily-heading), sans-serif; font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); margin-block-end: var(--sl-size-300); - color: var(--sl-color-foreground-plain); } .content h2 { + color: var(--sl-color-foreground-plain); font: var(--sl-text-new-heading-xl); font-family: Roboto, var(--sl-text-new-typeset-fontFamily-heading), sans-serif; font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); - margin-block-start: var(--sl-size-500); - margin-block-end: var(--sl-size-200); - color: var(--sl-color-foreground-plain); + margin-block: var(--sl-size-500) var(--sl-size-200); } .content h3 { + color: var(--sl-color-foreground-plain); font: var(--sl-text-new-heading-lg); font-family: Roboto, var(--sl-text-new-typeset-fontFamily-heading), sans-serif; font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); - margin-block-start: var(--sl-size-400); - margin-block-end: var(--sl-size-150); - color: var(--sl-color-foreground-plain); + margin-block: var(--sl-size-400) var(--sl-size-150); } .content p { - margin-block-end: var(--sl-size-200); line-height: 1.6; + margin-block-end: var(--sl-size-200); } .content ul, @@ -199,17 +107,17 @@ details[open] > summary::before { .content code { background: var(--sl-color-background-accent-grey-subtlest); - padding-inline: var(--sl-size-050); border-radius: var(--sl-size-borderRadius-sm); font-size: 0.9em; + padding-inline: var(--sl-size-050); } .content pre { background: var(--sl-color-background-accent-grey-subtlest); - padding: var(--sl-size-200); border-radius: var(--sl-size-borderRadius-md); - overflow-x: auto; margin-block-end: var(--sl-size-200); + overflow-x: auto; + padding: var(--sl-size-200); } .content pre code { @@ -221,8 +129,8 @@ details[open] > summary::before { .content :is(h2, h3) a.header-anchor { color: var(--sl-color-foreground-accent-grey-subtle); - text-decoration: none; margin-inline-start: var(--sl-size-075); + text-decoration: none; } .content :is(h2, h3):hover a.header-anchor { @@ -234,37 +142,37 @@ details[open] > summary::before { ============================================ */ .toc { - grid-area: toc; display: none; + grid-area: toc; } -@media (min-width: 1024px) { +@media (width >= 1024px) { .toc { display: block; } } .toc__container { - position: sticky; inset-block-start: var(--sl-size-400); padding: var(--sl-size-300); + position: sticky; } .toc__title { + color: var(--sl-color-foreground-accent-grey-subtle); font: var(--sl-text-new-body-sm); font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); - text-transform: uppercase; letter-spacing: 0.05em; - color: var(--sl-color-foreground-accent-grey-subtle); margin-block-end: var(--sl-size-150); + text-transform: uppercase; } .toc__list { + border-inline-start: var(--sl-size-borderWidth-subtle) solid var(--sl-color-background-accent-grey-subtle); list-style: none; - padding: 0; margin: 0; - border-inline-start: var(--sl-size-borderWidth-subtle) solid var(--sl-color-background-accent-grey-subtle); + padding: 0; } .toc__item { @@ -276,13 +184,13 @@ details[open] > summary::before { } .toc__link { + color: var(--sl-color-foreground-accent-grey-subtle); display: block; + font: var(--sl-text-new-body-sm); + font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; padding-block: var(--sl-size-050); padding-inline-start: var(--sl-size-150); - color: var(--sl-color-foreground-accent-grey-subtle); text-decoration: none; - font: var(--sl-text-new-body-sm); - font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; } .toc__link:hover { diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index f35930e731..577a186e71 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -1,4 +1,10 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc'; +import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar'; +import { NavGroup, NavItem, SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav'; customElements.define('doc-page-toc', PageToc); +customElements.define('doc-sidebar', Sidebar); +customElements.define('doc-site-nav', SiteNav); +customElements.define('doc-nav-group', NavGroup); +customElements.define('doc-nav-item', NavItem); diff --git a/vitest.config.ts b/vitest.config.ts index 18ed741ae6..801693f3dc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -47,6 +47,30 @@ export default defineConfig({ }, setupFiles: 'vitest.setup.ts' } + }, + { + extends: true, + test: { + name: 'docs', + include: ['docs/components/**/*.spec.ts'], + browser: { + enabled: true, + headless: true, + provider: playwright({ + contextOptions: { + locale: 'en', + reducedMotion: 'reduce' + } + }), + instances: [ + { + browser: 'chromium' + } + ], + viewport: { width: 1024, height: 768 } + }, + setupFiles: 'vitest.setup.ts' + } } ] } diff --git a/yarn.lock b/yarn.lock index 354ea6f96d..99bc012c53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6290,6 +6290,8 @@ __metadata: "@open-wc/scoped-elements": "npm:^3.0.6" "@roenlie/vite-plugin-import-css-sheet": "npm:^0.0.7" "@sl-design-system/icon": "workspace:^" + "@sl-design-system/search-field": "workspace:^" + "@sl-design-system/switch": "workspace:^" "@storybook/addon-a11y": "npm:^10.3.1" "@storybook/addon-docs": "npm:^10.3.1" "@storybook/addon-vitest": "npm:^10.3.1" @@ -6836,7 +6838,7 @@ __metadata: languageName: unknown linkType: soft -"@sl-design-system/search-field@npm:^0.2.4, @sl-design-system/search-field@workspace:packages/components/search-field": +"@sl-design-system/search-field@npm:^0.2.4, @sl-design-system/search-field@workspace:^, @sl-design-system/search-field@workspace:packages/components/search-field": version: 0.0.0-use.local resolution: "@sl-design-system/search-field@workspace:packages/components/search-field" dependencies: @@ -6917,7 +6919,7 @@ __metadata: languageName: unknown linkType: soft -"@sl-design-system/switch@npm:^1.1.7, @sl-design-system/switch@workspace:packages/components/switch": +"@sl-design-system/switch@npm:^1.1.7, @sl-design-system/switch@workspace:^, @sl-design-system/switch@workspace:packages/components/switch": version: 0.0.0-use.local resolution: "@sl-design-system/switch@workspace:packages/components/switch" dependencies: From 9eca31bfca35f700de44f55d91c537224ac0d1d3 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Sun, 22 Mar 2026 11:54:46 +0100 Subject: [PATCH 09/72] =?UTF-8?q?=F0=9F=8E=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/src/sidebar/sidebar.css | 63 ++++++++-------- docs/components/src/sidebar/sidebar.ts | 12 ++- docs/components/src/site-nav/nav-group.css | 6 +- docs/components/tsdown.config.ts | 6 +- docs/website/src/_includes/base.njk | 9 +-- docs/website/src/_includes/toc.njk | 34 --------- docs/website/src/css/main.css | 75 +++---------------- docs/website/src/index.md | 14 ---- .../introduction.md => introduction/about.md} | 8 +- .../getting-started/for-designers.md | 10 +++ .../getting-started/for-developers.md | 10 +++ .../getting-started/getting-started.md | 3 +- docs/website/src/introduction/introduction.md | 8 ++ 13 files changed, 86 insertions(+), 172 deletions(-) delete mode 100644 docs/website/src/index.md rename docs/website/src/{getting-started/introduction.md => introduction/about.md} (93%) create mode 100644 docs/website/src/introduction/getting-started/for-designers.md create mode 100644 docs/website/src/introduction/getting-started/for-developers.md rename docs/website/src/{ => introduction}/getting-started/getting-started.md (56%) create mode 100644 docs/website/src/introduction/introduction.md diff --git a/docs/components/src/sidebar/sidebar.css b/docs/components/src/sidebar/sidebar.css index 3d10ff084a..c14c0cf38f 100644 --- a/docs/components/src/sidebar/sidebar.css +++ b/docs/components/src/sidebar/sidebar.css @@ -8,43 +8,42 @@ position: sticky; } -header { - padding: var(--sl-size-300); - padding-block-end: var(--sl-size-200); +:host-context([data-color-scheme='dark']) .logo-light { + display: none; } -header a { +:host-context([data-color-scheme='dark']) .logo-dark { display: block; - text-decoration: none; } -header img { - block-size: 2rem; - display: block; - inline-size: auto; -} +header { + padding: var(--sl-size-300); + padding-block-end: var(--sl-size-200); -.logo-dark { - display: none; -} + a { + display: block; + text-decoration: none; + } -:host-context([data-color-scheme='dark']) .logo-light { - display: none; + img { + block-size: 2rem; + display: block; + inline-size: auto; + } } -:host-context([data-color-scheme='dark']) .logo-dark { - display: block; +.logo-dark { + display: none; } doc-search { padding-inline: var(--sl-size-300); } -.body { +doc-site-nav { flex: 1; overflow-y: auto; - padding-block-end: var(--sl-size-300); - padding-inline: var(--sl-size-300); + padding: var(--sl-size-200) var(--sl-size-300); } footer { @@ -54,19 +53,19 @@ footer { display: flex; justify-content: space-between; padding: var(--sl-size-200) var(--sl-size-300); -} -footer a { - align-items: center; - color: var(--sl-color-foreground-plain); - display: inline-flex; - text-decoration: none; -} + a { + align-items: center; + color: var(--sl-color-foreground-plain); + display: inline-flex; + text-decoration: none; -footer a:hover { - color: var(--sl-color-foreground-accent-blue-bold); -} + &:hover { + color: var(--sl-color-foreground-accent-blue-bold); + } + } -footer sl-icon { - font-size: var(--sl-size-300); + sl-icon { + font-size: var(--sl-size-300); + } } diff --git a/docs/components/src/sidebar/sidebar.ts b/docs/components/src/sidebar/sidebar.ts index 4638920ed2..4cf517089e 100644 --- a/docs/components/src/sidebar/sidebar.ts +++ b/docs/components/src/sidebar/sidebar.ts @@ -27,18 +27,16 @@ export class Sidebar extends ScopedElementsMixin(LitElement) { return html`
- SL Design System - SL Design System + SL Design System + SL Design System
-
- - - -
+ + +
- +
- {% include "toc.njk" %} diff --git a/docs/website/src/_includes/toc.njk b/docs/website/src/_includes/toc.njk index b0c54e4ad5..f8e8f9518f 100644 --- a/docs/website/src/_includes/toc.njk +++ b/docs/website/src/_includes/toc.njk @@ -1,35 +1 @@ - diff --git a/docs/website/src/css/main.css b/docs/website/src/css/main.css index 747db156bb..9c1066a204 100644 --- a/docs/website/src/css/main.css +++ b/docs/website/src/css/main.css @@ -1,15 +1,3 @@ -/* ============================================ - Site Layout — 3-column grid on wide screens - ============================================ */ - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - html { background: var(--sl-elevation-surface-base-default); color: var(--sl-color-foreground-plain); @@ -18,6 +6,7 @@ html { } body { + margin: 0; min-block-size: 100dvh; } @@ -62,7 +51,8 @@ doc-sidebar { font: var(--sl-text-new-heading-2xl); font-family: Roboto, var(--sl-text-new-typeset-fontFamily-heading), sans-serif; font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); - margin-block-end: var(--sl-size-300); + margin: 0; + text-box-trim: trim-both; } .content h2 { @@ -137,62 +127,15 @@ doc-sidebar { color: var(--sl-color-foreground-accent-blue-bold); } -/* ============================================ - Table of Contents (right sidebar) - ============================================ */ - -.toc { +doc-page-toc { + align-self: start; display: none; grid-area: toc; -} + inset-block-start: 0; + padding-block-start: var(--sl-size-400); + position: sticky; -@media (width >= 1024px) { - .toc { + @media (width >= 1024px) { display: block; } } - -.toc__container { - inset-block-start: var(--sl-size-400); - padding: var(--sl-size-300); - position: sticky; -} - -.toc__title { - color: var(--sl-color-foreground-accent-grey-subtle); - font: var(--sl-text-new-body-sm); - font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; - font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); - letter-spacing: 0.05em; - margin-block-end: var(--sl-size-150); - text-transform: uppercase; -} - -.toc__list { - border-inline-start: var(--sl-size-borderWidth-subtle) solid var(--sl-color-background-accent-grey-subtle); - list-style: none; - margin: 0; - padding: 0; -} - -.toc__item { - margin: 0; -} - -.toc__item--nested { - padding-inline-start: var(--sl-size-150); -} - -.toc__link { - color: var(--sl-color-foreground-accent-grey-subtle); - display: block; - font: var(--sl-text-new-body-sm); - font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; - padding-block: var(--sl-size-050); - padding-inline-start: var(--sl-size-150); - text-decoration: none; -} - -.toc__link:hover { - color: var(--sl-color-foreground-accent-blue-bold); -} diff --git a/docs/website/src/index.md b/docs/website/src/index.md deleted file mode 100644 index 6df7181950..0000000000 --- a/docs/website/src/index.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: SL Design System -layout: base.njk -eleventyNavigation: - key: Home - order: 0 ---- - -Welcome to the **SL Design System** documentation. - -Use the sidebar to navigate to the different sections: - -- **Getting Started** — Learn how to get up and running -- **Components** — Browse the available UI components diff --git a/docs/website/src/getting-started/introduction.md b/docs/website/src/introduction/about.md similarity index 93% rename from docs/website/src/getting-started/introduction.md rename to docs/website/src/introduction/about.md index 0279d9bf9e..82f21e3d50 100644 --- a/docs/website/src/getting-started/introduction.md +++ b/docs/website/src/introduction/about.md @@ -1,10 +1,10 @@ --- -title: Introduction +title: About layout: base.njk eleventyNavigation: - key: Introduction - parent: Getting Started - order: 1 + key: About + parent: Introduction + order: 0 --- The **SL Design System** is a collection of reusable web components built with [Lit](https://lit.dev/). It provides a consistent, accessible, and themeable set of UI building blocks for Sanoma Learning applications. diff --git a/docs/website/src/introduction/getting-started/for-designers.md b/docs/website/src/introduction/getting-started/for-designers.md new file mode 100644 index 0000000000..1288d67504 --- /dev/null +++ b/docs/website/src/introduction/getting-started/for-designers.md @@ -0,0 +1,10 @@ +--- +title: For Designers +layout: base.njk +eleventyNavigation: + key: For Designers + parent: Getting Started + order: 0 +--- + +Getting started for designers. diff --git a/docs/website/src/introduction/getting-started/for-developers.md b/docs/website/src/introduction/getting-started/for-developers.md new file mode 100644 index 0000000000..4baf033dfc --- /dev/null +++ b/docs/website/src/introduction/getting-started/for-developers.md @@ -0,0 +1,10 @@ +--- +title: For Developers +layout: base.njk +eleventyNavigation: + key: For Developers + parent: Getting Started + order: 1 +--- + +Getting started for developers. diff --git a/docs/website/src/getting-started/getting-started.md b/docs/website/src/introduction/getting-started/getting-started.md similarity index 56% rename from docs/website/src/getting-started/getting-started.md rename to docs/website/src/introduction/getting-started/getting-started.md index 3a850d3dba..e3ee1efe1b 100644 --- a/docs/website/src/getting-started/getting-started.md +++ b/docs/website/src/introduction/getting-started/getting-started.md @@ -3,7 +3,8 @@ title: Getting Started layout: base.njk eleventyNavigation: key: Getting Started + parent: Introduction order: 1 --- -This section contains guides to help you get started with the SL Design System. +How to get started. diff --git a/docs/website/src/introduction/introduction.md b/docs/website/src/introduction/introduction.md new file mode 100644 index 0000000000..c453a02c7f --- /dev/null +++ b/docs/website/src/introduction/introduction.md @@ -0,0 +1,8 @@ +--- +title: Introduction +layout: base.njk +eleventyNavigation: + key: Introduction + order: 0 +permalink: false +--- From eb42f4bb1063e8ea4cb6fdc073af1f524eb79e2e Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Sun, 22 Mar 2026 12:30:15 +0100 Subject: [PATCH 10/72] =?UTF-8?q?=F0=9F=9A=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/website/eleventy.config.js | 37 +++++++++++++++++++ docs/website/package.json | 2 + docs/website/src/_includes/sidebar.njk | 3 +- .../{about.md => documentation.md} | 5 ++- docs/website/src/js/icons.js | 5 +++ docs/website/src/js/main.js | 5 ++- 6 files changed, 52 insertions(+), 5 deletions(-) rename docs/website/src/introduction/{about.md => documentation.md} (95%) create mode 100644 docs/website/src/js/icons.js diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index d02cd2792c..f4e59e3e87 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -1,3 +1,4 @@ +import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import * as esbuild from 'esbuild'; @@ -32,6 +33,42 @@ export default function (eleventyConfig) { }); eleventyConfig.on('eleventy.before', async () => { + // Scan pages for icon names in eleventyNavigation frontmatter + const icons = new Set(); + const files = readdirSync('src', { recursive: true }); + + for (const file of files) { + if (typeof file !== 'string' || !file.endsWith('.md')) continue; + + const content = readFileSync(join('src', file), 'utf-8'), + frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/), + iconMatch = frontmatterMatch?.[1].match(/^\s+icon:\s*(\S+)/m); + + if (iconMatch) { + icons.add(iconMatch[1]); + } + } + + // Generate icon registration module + let iconsJs = '// Auto-generated from page frontmatter icons\n'; + + if (icons.size > 0) { + const importNames = [...icons].map( + name => + 'fa' + + name + .split('-') + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') + ); + + iconsJs += `import { ${importNames.join(', ')} } from '@fortawesome/pro-regular-svg-icons';\n`; + iconsJs += `import { Icon } from '@sl-design-system/icon';\n\n`; + iconsJs += `Icon.register(${importNames.join(', ')});\n`; + } + + writeFileSync('src/js/icons.js', iconsJs); + await esbuild.build({ entryPoints: ['src/js/main.js'], bundle: true, diff --git a/docs/website/package.json b/docs/website/package.json index 2d0e8e6e8e..6cb046ccb2 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -9,7 +9,9 @@ "dependencies": { "@11ty/eleventy": "^3.0.0", "@11ty/eleventy-navigation": "^0.3.5", + "@fortawesome/pro-regular-svg-icons": "^7.2.0", "@sl-design-system/doc-components": "workspace:^", + "@sl-design-system/icon": "workspace:^", "@sl-design-system/sanoma-learning": "workspace:*", "@webcomponents/scoped-custom-element-registry": "^0.0.10", "markdown-it-anchor": "^9.2.0" diff --git a/docs/website/src/_includes/sidebar.njk b/docs/website/src/_includes/sidebar.njk index 355fc80211..8a5ef0842b 100644 --- a/docs/website/src/_includes/sidebar.njk +++ b/docs/website/src/_includes/sidebar.njk @@ -20,7 +20,7 @@ @@ -42,6 +42,7 @@ {%- endif -%} diff --git a/docs/website/src/introduction/about.md b/docs/website/src/introduction/documentation.md similarity index 95% rename from docs/website/src/introduction/about.md rename to docs/website/src/introduction/documentation.md index 82f21e3d50..b01707f564 100644 --- a/docs/website/src/introduction/about.md +++ b/docs/website/src/introduction/documentation.md @@ -1,10 +1,11 @@ --- -title: About +title: Documentation layout: base.njk eleventyNavigation: - key: About + key: Documentation parent: Introduction order: 0 + icon: book --- The **SL Design System** is a collection of reusable web components built with [Lit](https://lit.dev/). It provides a consistent, accessible, and themeable set of UI building blocks for Sanoma Learning applications. diff --git a/docs/website/src/js/icons.js b/docs/website/src/js/icons.js new file mode 100644 index 0000000000..d647dd3194 --- /dev/null +++ b/docs/website/src/js/icons.js @@ -0,0 +1,5 @@ +// Auto-generated from page frontmatter icons +import { faBook } from '@fortawesome/pro-regular-svg-icons'; +import { Icon } from '@sl-design-system/icon'; + +Icon.register(faBook); diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index 577a186e71..1c095e740f 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -1,10 +1,11 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; +import './icons.js'; import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc'; import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar'; import { NavGroup, NavItem, SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav'; +customElements.define('doc-nav-group', NavGroup); +customElements.define('doc-nav-item', NavItem); customElements.define('doc-page-toc', PageToc); customElements.define('doc-sidebar', Sidebar); customElements.define('doc-site-nav', SiteNav); -customElements.define('doc-nav-group', NavGroup); -customElements.define('doc-nav-item', NavItem); From dcf96afe065ace27babf8489c03141af86df3446 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Sun, 22 Mar 2026 19:42:06 +0100 Subject: [PATCH 11/72] =?UTF-8?q?=F0=9F=94=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/src/site-nav/nav-group.css | 6 ++ docs/components/src/site-nav/nav-item.css | 79 ++++++++----------- docs/components/src/site-nav/nav-item.ts | 2 +- .../src/api-reference/api-reference.md | 7 ++ docs/website/src/api-reference/button.md | 10 +++ docs/website/src/css/main.css | 14 +++- docs/website/src/design-tokens/border.md | 10 +++ .../src/design-tokens/design-tokens.md | 7 ++ docs/website/src/introduction/faq.md | 9 +++ .../getting-started/getting-started.md | 3 +- docs/website/src/js/icons.js | 4 +- 11 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 docs/website/src/api-reference/api-reference.md create mode 100644 docs/website/src/api-reference/button.md create mode 100644 docs/website/src/design-tokens/border.md create mode 100644 docs/website/src/design-tokens/design-tokens.md create mode 100644 docs/website/src/introduction/faq.md diff --git a/docs/components/src/site-nav/nav-group.css b/docs/components/src/site-nav/nav-group.css index d9c9913f33..c8a5b04e26 100644 --- a/docs/components/src/site-nav/nav-group.css +++ b/docs/components/src/site-nav/nav-group.css @@ -13,3 +13,9 @@ h2 { margin: 0; padding: var(--sl-size-075) var(--sl-size-100); } + +slot { + display: flex; + flex-direction: column; + gap: var(--sl-size-025); +} diff --git a/docs/components/src/site-nav/nav-item.css b/docs/components/src/site-nav/nav-item.css index 2de82a90c6..2bae3c84b1 100644 --- a/docs/components/src/site-nav/nav-item.css +++ b/docs/components/src/site-nav/nav-item.css @@ -1,19 +1,17 @@ /* stylelint-disable custom-property-pattern */ :host { - --_bg: transparent; - --_bg-mix: transparent; - --_bg-opacity: 0; - --_color: var(--sl-color-foreground-plain); - --_font-weight: inherit; + --_bg-color: transparent; + --_bg-mix-color: var(--sl-color-background-neutral-interactive-plain); + --_bg-opacity: var(--sl-opacity-interactive-plain-idle); --nav-indent: var(--sl-size-200); + color: var(--sl-color-foreground-plain); display: block; outline: none; } :host(:hover) { - --_bg-mix: var(--sl-color-background-accent-grey-subtlest); --_bg-opacity: var(--sl-opacity-interactive-plain-hover); } @@ -22,24 +20,14 @@ } :host([aria-current='page']) { - --_bg: var(--sl-color-background-selected-subtlest); - --_bg-mix: var(--sl-color-background-selected-subtlest); - --_bg-opacity: 0; - --_color: var(--sl-color-foreground-accent-blue-bold); - --_font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); -} - -:host([aria-current='page']:hover) { - --_bg-mix: var(--sl-color-background-selected-bold); - --_bg-opacity: var(--sl-opacity-interactive-plain-hover); -} + --_bg-color: var(--sl-color-background-selected-subtlest); + --_bg-mix-color: var(--sl-color-background-selected-interactive-plain); -:host([aria-current='page']:active) { - --_bg-opacity: var(--sl-opacity-interactive-plain-active); + color: var(--sl-color-foreground-accent-blue-bold); } :host(:focus-visible) summary, -:host(:focus-visible) .leaf { +:host(:focus-visible) a { outline: var(--sl-size-borderWidth-focusRing) solid var(--sl-color-border-focused); outline-offset: calc(var(--sl-size-borderWidth-focusRing) * -1); } @@ -52,7 +40,7 @@ summary { align-items: center; background: color-mix(in srgb, var(--_bg), var(--_bg-mix) calc(100% * var(--_bg-opacity))); border-radius: var(--sl-size-borderRadius-md); - color: var(--_color); + color: inherit; cursor: pointer; display: flex; font-weight: var(--_font-weight); @@ -61,29 +49,29 @@ summary { padding-block: var(--sl-size-075); padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); position: relative; -} -summary::-webkit-details-marker { - display: none; -} + &::-webkit-details-marker { + display: none; + } -summary a { - color: inherit; - flex: 1; - text-decoration: none; -} + a { + color: inherit; + flex: 1; + text-decoration: none; + } -summary .label { - flex: 1; + .label { + flex: 1; + } } .active { background: var(--sl-color-background-selected-bold); block-size: var(--sl-size-300); border-radius: var(--sl-size-150); - inline-size: var(--sl-size-050); + inline-size: calc(var(--sl-size-075) - var(--sl-size-010)); inset-block-start: 50%; - inset-inline-start: calc(-1 * (var(--sl-size-050) + var(--sl-size-050))); + inset-inline-start: calc(-1 * (var(--sl-size-125) - var(--sl-size-010))); position: absolute; translate: 0 -50%; } @@ -103,29 +91,26 @@ details[open] > summary .chevron { details[open] > slot { border-inline-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); - display: block; + display: flex; + flex-direction: column; + gap: var(--sl-size-025); margin-inline-start: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent) + 0.5em); + padding-inline-start: var(--sl-size-075); } -.leaf { +a { align-items: center; - background: color-mix(in srgb, var(--_bg), var(--_bg-mix) calc(100% * var(--_bg-opacity))); - border-radius: var(--sl-size-borderRadius-md); - color: var(--_color); + background: color-mix(in srgb, var(--_bg-color), var(--_bg-mix-color) calc(100% * var(--_bg-opacity))); + border-radius: var(--sl-size-borderRadius-default); + color: inherit; display: flex; - font-weight: var(--_font-weight); gap: var(--sl-size-100); padding-block: var(--sl-size-075); padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); position: relative; text-decoration: none; -} -@media (prefers-reduced-motion: no-preference) { - :host(:where(:active, :focus-visible, :hover)) summary, - :host(:where(:active, :focus-visible, :hover)) .leaf { - transition-duration: var(--sl-animation-button-duration); - transition-property: background, color; - transition-timing-function: var(--sl-animation-button-easing); + @media (prefers-reduced-motion: no-preference) { + transition: background 200ms ease-in-out; } } diff --git a/docs/components/src/site-nav/nav-item.ts b/docs/components/src/site-nav/nav-item.ts index 35c034f36a..a79c5fee79 100644 --- a/docs/components/src/site-nav/nav-item.ts +++ b/docs/components/src/site-nav/nav-item.ts @@ -97,7 +97,7 @@ export class NavItem extends ScopedElementsMixin(LitElement) { } return html` - + ${this.active ? html`` : nothing} ${this.icon ? html`` : nothing} ${this.heading} diff --git a/docs/website/src/api-reference/api-reference.md b/docs/website/src/api-reference/api-reference.md new file mode 100644 index 0000000000..18afc22e86 --- /dev/null +++ b/docs/website/src/api-reference/api-reference.md @@ -0,0 +1,7 @@ +--- +title: API Reference +layout: base.njk +eleventyNavigation: + key: API Reference + order: 3 +--- diff --git a/docs/website/src/api-reference/button.md b/docs/website/src/api-reference/button.md new file mode 100644 index 0000000000..4a9c99c160 --- /dev/null +++ b/docs/website/src/api-reference/button.md @@ -0,0 +1,10 @@ +--- +title: Button +layout: base.njk +eleventyNavigation: + key: Button + parent: API Reference + order: 1 +--- + +Button API. diff --git a/docs/website/src/css/main.css b/docs/website/src/css/main.css index 9c1066a204..caa1237fe5 100644 --- a/docs/website/src/css/main.css +++ b/docs/website/src/css/main.css @@ -1,3 +1,7 @@ +@view-transition { + navigation: auto; +} + html { background: var(--sl-elevation-surface-base-default); color: var(--sl-color-foreground-plain); @@ -16,12 +20,14 @@ body { 'sidebar' 'content' 'toc'; - grid-template-columns: 1fr; min-block-size: 100dvh; -} -@media (width >= 1024px) { - .site-layout { + @media (width >= 640px) { + grid-template-areas: 'sidebar content'; + grid-template-columns: 280px 1fr; + } + + @media (width >= 1024px) { grid-template-areas: 'sidebar content toc'; grid-template-columns: 280px 1fr 220px; } diff --git a/docs/website/src/design-tokens/border.md b/docs/website/src/design-tokens/border.md new file mode 100644 index 0000000000..e9a956b0e1 --- /dev/null +++ b/docs/website/src/design-tokens/border.md @@ -0,0 +1,10 @@ +--- +title: Border +layout: base.njk +eleventyNavigation: + key: Border + parent: Design tokens + order: 1 +--- + +Border design tokens. diff --git a/docs/website/src/design-tokens/design-tokens.md b/docs/website/src/design-tokens/design-tokens.md new file mode 100644 index 0000000000..f25c68ccd5 --- /dev/null +++ b/docs/website/src/design-tokens/design-tokens.md @@ -0,0 +1,7 @@ +--- +title: Design tokens +layout: base.njk +eleventyNavigation: + key: Design tokens + order: 1 +--- diff --git a/docs/website/src/introduction/faq.md b/docs/website/src/introduction/faq.md new file mode 100644 index 0000000000..3419b659d8 --- /dev/null +++ b/docs/website/src/introduction/faq.md @@ -0,0 +1,9 @@ +--- +title: FAQ +layout: base.njk +eleventyNavigation: + key: FAQ + parent: Introduction + order: 1 + icon: circle-question +--- diff --git a/docs/website/src/introduction/getting-started/getting-started.md b/docs/website/src/introduction/getting-started/getting-started.md index e3ee1efe1b..2601fcd90b 100644 --- a/docs/website/src/introduction/getting-started/getting-started.md +++ b/docs/website/src/introduction/getting-started/getting-started.md @@ -4,7 +4,8 @@ layout: base.njk eleventyNavigation: key: Getting Started parent: Introduction - order: 1 + order: 2 + icon: rocket --- How to get started. diff --git a/docs/website/src/js/icons.js b/docs/website/src/js/icons.js index d647dd3194..86c7605efd 100644 --- a/docs/website/src/js/icons.js +++ b/docs/website/src/js/icons.js @@ -1,5 +1,5 @@ // Auto-generated from page frontmatter icons -import { faBook } from '@fortawesome/pro-regular-svg-icons'; +import { faBook, faCircleQuestion, faRocket } from '@fortawesome/pro-regular-svg-icons'; import { Icon } from '@sl-design-system/icon'; -Icon.register(faBook); +Icon.register(faBook, faCircleQuestion, faRocket); From 8f21736227588bbd909f6631c3f4d39f08f7d301 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Sun, 22 Mar 2026 19:58:26 +0100 Subject: [PATCH 12/72] =?UTF-8?q?=F0=9F=8C=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/src/sidebar/sidebar.css | 4 ++-- docs/components/src/site-nav/nav-item.css | 18 +++++++++++++----- docs/components/src/site-nav/nav-item.ts | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/components/src/sidebar/sidebar.css b/docs/components/src/sidebar/sidebar.css index c14c0cf38f..a107c9d3f2 100644 --- a/docs/components/src/sidebar/sidebar.css +++ b/docs/components/src/sidebar/sidebar.css @@ -49,7 +49,7 @@ doc-site-nav { footer { align-items: center; /* stylelint-disable-next-line custom-property-pattern */ - border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-default); + border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); display: flex; justify-content: space-between; padding: var(--sl-size-200) var(--sl-size-300); @@ -66,6 +66,6 @@ footer { } sl-icon { - font-size: var(--sl-size-300); + inline-size: var(--sl-size-300); } } diff --git a/docs/components/src/site-nav/nav-item.css b/docs/components/src/site-nav/nav-item.css index 2bae3c84b1..7deb2602b4 100644 --- a/docs/components/src/site-nav/nav-item.css +++ b/docs/components/src/site-nav/nav-item.css @@ -38,8 +38,8 @@ details { summary { align-items: center; - background: color-mix(in srgb, var(--_bg), var(--_bg-mix) calc(100% * var(--_bg-opacity))); - border-radius: var(--sl-size-borderRadius-md); + background: color-mix(in srgb, var(--_bg-color), var(--_bg-mix-color) calc(100% * var(--_bg-opacity))); + border-radius: var(--sl-size-borderRadius-default); color: inherit; cursor: pointer; display: flex; @@ -50,6 +50,10 @@ summary { padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); position: relative; + @media (prefers-reduced-motion: no-preference) { + transition: background 200ms ease-in-out; + } + &::-webkit-details-marker { display: none; } @@ -85,8 +89,12 @@ summary { transition: transform 0.15s ease; } -details[open] > summary .chevron { - transform: rotate(90deg); +details[open] > summary { + margin-block-end: var(--sl-size-025); + + .chevron { + transform: rotate(90deg); + } } details[open] > slot { @@ -98,7 +106,7 @@ details[open] > slot { padding-inline-start: var(--sl-size-075); } -a { +.leaf { align-items: center; background: color-mix(in srgb, var(--_bg-color), var(--_bg-mix-color) calc(100% * var(--_bg-opacity))); border-radius: var(--sl-size-borderRadius-default); diff --git a/docs/components/src/site-nav/nav-item.ts b/docs/components/src/site-nav/nav-item.ts index a79c5fee79..35c034f36a 100644 --- a/docs/components/src/site-nav/nav-item.ts +++ b/docs/components/src/site-nav/nav-item.ts @@ -97,7 +97,7 @@ export class NavItem extends ScopedElementsMixin(LitElement) { } return html` - + ${this.active ? html`` : nothing} ${this.icon ? html`` : nothing} ${this.heading} From 4279d1e76404d563e15ad3488f503d938fa419d5 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Sun, 22 Mar 2026 20:26:30 +0100 Subject: [PATCH 13/72] =?UTF-8?q?=F0=9F=8F=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/website/src/components/actions/actions.md | 1 + docs/website/src/components/form/form.md | 11 +++++++++++ docs/website/src/components/form/text-field.md | 10 ++++++++++ docs/website/src/components/grid/grid.md | 11 +++++++++++ docs/website/src/components/layout/layout.md | 11 +++++++++++ docs/website/src/components/navigation/navigation.md | 11 +++++++++++ docs/website/src/components/overlay/overlay.md | 11 +++++++++++ docs/website/src/components/status/status.md | 11 +++++++++++ docs/website/src/components/utilities/utilities.md | 11 +++++++++++ docs/website/src/design-tokens/border.md | 1 + docs/website/src/design-tokens/color.md | 11 +++++++++++ docs/website/src/js/icons.js | 4 ++-- 12 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 docs/website/src/components/form/form.md create mode 100644 docs/website/src/components/form/text-field.md create mode 100644 docs/website/src/components/grid/grid.md create mode 100644 docs/website/src/components/layout/layout.md create mode 100644 docs/website/src/components/navigation/navigation.md create mode 100644 docs/website/src/components/overlay/overlay.md create mode 100644 docs/website/src/components/status/status.md create mode 100644 docs/website/src/components/utilities/utilities.md create mode 100644 docs/website/src/design-tokens/color.md diff --git a/docs/website/src/components/actions/actions.md b/docs/website/src/components/actions/actions.md index 6eacc5e15b..7e72377581 100644 --- a/docs/website/src/components/actions/actions.md +++ b/docs/website/src/components/actions/actions.md @@ -5,6 +5,7 @@ eleventyNavigation: key: Actions parent: Components order: 1 + icon: bolt --- Action components allow users to trigger operations or navigate. diff --git a/docs/website/src/components/form/form.md b/docs/website/src/components/form/form.md new file mode 100644 index 0000000000..5347c46436 --- /dev/null +++ b/docs/website/src/components/form/form.md @@ -0,0 +1,11 @@ +--- +title: Form +layout: base.njk +eleventyNavigation: + key: Form + parent: Components + order: 1 + icon: input-pipe +--- + +Form components allow users to input and submit data. diff --git a/docs/website/src/components/form/text-field.md b/docs/website/src/components/form/text-field.md new file mode 100644 index 0000000000..63e3dbda22 --- /dev/null +++ b/docs/website/src/components/form/text-field.md @@ -0,0 +1,10 @@ +--- +title: Text field +layout: base.njk +eleventyNavigation: + key: Text field + parent: Form + order: 1 +--- + +Text field components allow users to input and edit text. diff --git a/docs/website/src/components/grid/grid.md b/docs/website/src/components/grid/grid.md new file mode 100644 index 0000000000..698cd0ab5c --- /dev/null +++ b/docs/website/src/components/grid/grid.md @@ -0,0 +1,11 @@ +--- +title: Grid +layout: base.njk +eleventyNavigation: + key: Grid + parent: Components + order: 2 + icon: table-cells +--- + +Grid components allow users to create powerful data tables. diff --git a/docs/website/src/components/layout/layout.md b/docs/website/src/components/layout/layout.md new file mode 100644 index 0000000000..b2c9d640a8 --- /dev/null +++ b/docs/website/src/components/layout/layout.md @@ -0,0 +1,11 @@ +--- +title: Layout +layout: base.njk +eleventyNavigation: + key: Layout + parent: Components + order: 3 + icon: table-layout +--- + +Layout components allow users to create flexible and responsive layouts. diff --git a/docs/website/src/components/navigation/navigation.md b/docs/website/src/components/navigation/navigation.md new file mode 100644 index 0000000000..a1bc3ea719 --- /dev/null +++ b/docs/website/src/components/navigation/navigation.md @@ -0,0 +1,11 @@ +--- +title: Navigation +layout: base.njk +eleventyNavigation: + key: Navigation + parent: Components + order: 3 + icon: compass +--- + +Navigation components allow users to create flexible and responsive navigation menus. diff --git a/docs/website/src/components/overlay/overlay.md b/docs/website/src/components/overlay/overlay.md new file mode 100644 index 0000000000..31c55f5d34 --- /dev/null +++ b/docs/website/src/components/overlay/overlay.md @@ -0,0 +1,11 @@ +--- +title: Overlay +layout: base.njk +eleventyNavigation: + key: Overlay + parent: Components + order: 4 + icon: layer-group +--- + +Overlay components allow users to create flexible and responsive overlay elements. diff --git a/docs/website/src/components/status/status.md b/docs/website/src/components/status/status.md new file mode 100644 index 0000000000..226fca0207 --- /dev/null +++ b/docs/website/src/components/status/status.md @@ -0,0 +1,11 @@ +--- +title: Status +layout: base.njk +eleventyNavigation: + key: Status + parent: Components + order: 5 + icon: alarm-clock +--- + +Status components allow users to create flexible and responsive status indicators. diff --git a/docs/website/src/components/utilities/utilities.md b/docs/website/src/components/utilities/utilities.md new file mode 100644 index 0000000000..593f1c81f6 --- /dev/null +++ b/docs/website/src/components/utilities/utilities.md @@ -0,0 +1,11 @@ +--- +title: Utilities +layout: base.njk +eleventyNavigation: + key: Utilities + parent: Components + order: 6 + icon: wrench +--- + +Utilities components allow users to create flexible and reusable utility classes. diff --git a/docs/website/src/design-tokens/border.md b/docs/website/src/design-tokens/border.md index e9a956b0e1..ee1a6efc7f 100644 --- a/docs/website/src/design-tokens/border.md +++ b/docs/website/src/design-tokens/border.md @@ -5,6 +5,7 @@ eleventyNavigation: key: Border parent: Design tokens order: 1 + icon: border-all --- Border design tokens. diff --git a/docs/website/src/design-tokens/color.md b/docs/website/src/design-tokens/color.md new file mode 100644 index 0000000000..1b18cf70c0 --- /dev/null +++ b/docs/website/src/design-tokens/color.md @@ -0,0 +1,11 @@ +--- +title: Color +layout: base.njk +eleventyNavigation: + key: Color + parent: Design tokens + order: 2 + icon: palette +--- + +Color design tokens. diff --git a/docs/website/src/js/icons.js b/docs/website/src/js/icons.js index 86c7605efd..55ad060742 100644 --- a/docs/website/src/js/icons.js +++ b/docs/website/src/js/icons.js @@ -1,5 +1,5 @@ // Auto-generated from page frontmatter icons -import { faBook, faCircleQuestion, faRocket } from '@fortawesome/pro-regular-svg-icons'; +import { faBorderAll, faPalette, faBook, faCircleQuestion, faBolt, faInputPipe, faTableCells, faTableLayout, faCompass, faLayerGroup, faAlarmClock, faWrench, faRocket } from '@fortawesome/pro-regular-svg-icons'; import { Icon } from '@sl-design-system/icon'; -Icon.register(faBook, faCircleQuestion, faRocket); +Icon.register(faBorderAll, faPalette, faBook, faCircleQuestion, faBolt, faInputPipe, faTableCells, faTableLayout, faCompass, faLayerGroup, faAlarmClock, faWrench, faRocket); From b03858aa44683d04f7e27ea5fce22621e893f022 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 23 Mar 2026 11:46:50 +0100 Subject: [PATCH 14/72] =?UTF-8?q?=F0=9F=90=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/website/src/components/form/date-field.md | 10 ++++++++++ docs/website/src/components/form/text-field.md | 2 +- docs/website/src/components/form/time-field.md | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/website/src/components/form/date-field.md create mode 100644 docs/website/src/components/form/time-field.md diff --git a/docs/website/src/components/form/date-field.md b/docs/website/src/components/form/date-field.md new file mode 100644 index 0000000000..7aed72a3dd --- /dev/null +++ b/docs/website/src/components/form/date-field.md @@ -0,0 +1,10 @@ +--- +title: Date field +layout: base.njk +eleventyNavigation: + key: Date field + parent: Form + order: 1 +--- + +Date field components allow users to input and edit dates. diff --git a/docs/website/src/components/form/text-field.md b/docs/website/src/components/form/text-field.md index 63e3dbda22..2f9f5afcea 100644 --- a/docs/website/src/components/form/text-field.md +++ b/docs/website/src/components/form/text-field.md @@ -4,7 +4,7 @@ layout: base.njk eleventyNavigation: key: Text field parent: Form - order: 1 + order: 2 --- Text field components allow users to input and edit text. diff --git a/docs/website/src/components/form/time-field.md b/docs/website/src/components/form/time-field.md new file mode 100644 index 0000000000..f5b9a67199 --- /dev/null +++ b/docs/website/src/components/form/time-field.md @@ -0,0 +1,10 @@ +--- +title: Time field +layout: base.njk +eleventyNavigation: + key: Time field + parent: Form + order: 3 +--- + +Time field components allow users to input and edit times. From 222b1a64ad96c781c48db8ee57f4febd2f88e680 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 23 Mar 2026 19:08:30 +0100 Subject: [PATCH 15/72] =?UTF-8?q?=F0=9F=93=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/.storybook/main.ts | 3 +- docs/components/package.json | 15 ++- docs/components/src/search/search.stories.ts | 6 +- .../components/src/sidebar/sidebar.stories.ts | 3 +- docs/components/src/site-nav/nav-group.css | 31 +++++ .../components/src/site-nav/nav-group.spec.ts | 85 ++++++++++++- .../src/site-nav/nav-group.stories.ts | 93 ++++++++++++++ docs/components/src/site-nav/nav-group.ts | 40 +++++- docs/components/src/site-nav/nav-item.css | 31 +++-- .../src/site-nav/nav-item.stories.ts | 115 ++++++++++++++++++ docs/components/src/site-nav/nav-item.ts | 4 +- .../src/site-nav/site-nav.stories.ts | 9 +- .../src/theme-switch/theme-switch.stories.ts | 4 +- docs/website/eleventy.config.js | 12 +- docs/website/src/_includes/toc.njk | 1 - .../api-reference/api-reference.md | 0 .../src/{ => content}/api-reference/button.md | 0 .../components/actions/actions.md | 0 .../components/actions/button.md | 0 .../{ => content}/components/components.md | 0 .../components/form/date-field.md | 0 .../src/{ => content}/components/form/form.md | 0 .../components/form/text-field.md | 0 .../components/form/time-field.md | 0 .../src/{ => content}/components/grid/grid.md | 0 .../{ => content}/components/layout/layout.md | 0 .../components/navigation/navigation.md | 0 .../components/overlay/overlay.md | 0 .../{ => content}/components/status/status.md | 0 .../components/utilities/utilities.md | 0 .../src/{ => content}/design-tokens/border.md | 0 .../src/{ => content}/design-tokens/color.md | 0 .../design-tokens/design-tokens.md | 0 .../introduction/documentation.md | 0 .../src/{ => content}/introduction/faq.md | 0 .../getting-started/for-designers.md | 0 .../getting-started/for-developers.md | 0 .../getting-started/getting-started.md | 0 .../introduction/introduction.md | 0 .../src/{_includes => includes}/base.njk | 0 .../src/{_includes => includes}/sidebar.njk | 0 yarn.lock | 2 + 42 files changed, 410 insertions(+), 44 deletions(-) create mode 100644 docs/components/src/site-nav/nav-group.stories.ts create mode 100644 docs/components/src/site-nav/nav-item.stories.ts delete mode 100644 docs/website/src/_includes/toc.njk rename docs/website/src/{ => content}/api-reference/api-reference.md (100%) rename docs/website/src/{ => content}/api-reference/button.md (100%) rename docs/website/src/{ => content}/components/actions/actions.md (100%) rename docs/website/src/{ => content}/components/actions/button.md (100%) rename docs/website/src/{ => content}/components/components.md (100%) rename docs/website/src/{ => content}/components/form/date-field.md (100%) rename docs/website/src/{ => content}/components/form/form.md (100%) rename docs/website/src/{ => content}/components/form/text-field.md (100%) rename docs/website/src/{ => content}/components/form/time-field.md (100%) rename docs/website/src/{ => content}/components/grid/grid.md (100%) rename docs/website/src/{ => content}/components/layout/layout.md (100%) rename docs/website/src/{ => content}/components/navigation/navigation.md (100%) rename docs/website/src/{ => content}/components/overlay/overlay.md (100%) rename docs/website/src/{ => content}/components/status/status.md (100%) rename docs/website/src/{ => content}/components/utilities/utilities.md (100%) rename docs/website/src/{ => content}/design-tokens/border.md (100%) rename docs/website/src/{ => content}/design-tokens/color.md (100%) rename docs/website/src/{ => content}/design-tokens/design-tokens.md (100%) rename docs/website/src/{ => content}/introduction/documentation.md (100%) rename docs/website/src/{ => content}/introduction/faq.md (100%) rename docs/website/src/{ => content}/introduction/getting-started/for-designers.md (100%) rename docs/website/src/{ => content}/introduction/getting-started/for-developers.md (100%) rename docs/website/src/{ => content}/introduction/getting-started/getting-started.md (100%) rename docs/website/src/{ => content}/introduction/introduction.md (100%) rename docs/website/src/{_includes => includes}/base.njk (100%) rename docs/website/src/{_includes => includes}/sidebar.njk (100%) diff --git a/docs/components/.storybook/main.ts b/docs/components/.storybook/main.ts index 56a36781ac..4ff5d24a21 100644 --- a/docs/components/.storybook/main.ts +++ b/docs/components/.storybook/main.ts @@ -13,8 +13,7 @@ function getAbsolutePath(value: string) { const config: StorybookConfig = { stories: [ - '../src/**/*.mdx', - '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)' + '../src/**/*.stories.ts' ], addons: [ getAbsolutePath('@storybook/addon-vitest'), diff --git a/docs/components/package.json b/docs/components/package.json index 54ad57711e..7c0ea7d7ff 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -19,9 +19,22 @@ }, "scripts": { "build": "tsdown", - "start": "storybook dev -p 6009", + "start": "wireit", "watch": "tsdown --watch" }, + "wireit": { + "start": { + "command": "storybook dev -p 6009 --no-open", + "service": { + "readyWhen": { + "lineMatches": "Storybook ready!" + } + }, + "files": [ + ".storybook/main.ts" + ] + } + }, "dependencies": { "@open-wc/scoped-elements": "^3.0.6", "@sl-design-system/icon": "workspace:^", diff --git a/docs/components/src/search/search.stories.ts b/docs/components/src/search/search.stories.ts index f8e95c90e0..047cfcc22d 100644 --- a/docs/components/src/search/search.stories.ts +++ b/docs/components/src/search/search.stories.ts @@ -1,10 +1,10 @@ import { type Meta, type StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; +import { Search } from './search.js'; type Story = StoryObj; try { - const { Search } = await import('./search.js'); customElements.define('doc-search', Search); } catch { /* empty */ @@ -12,10 +12,6 @@ try { export default { title: 'Search', - tags: ['autodocs'], - parameters: { - layout: 'padded' - }, render: () => html` + + + + + + `; + } +} satisfies Meta; + +export const Basic: Story = {}; + +export const WithLink: Story = { + args: { + href: '#' + } +}; + +export const Collapsible: Story = { + args: { + collapsible: true + } +}; + +export const Collapsed: Story = { + args: { + collapsible: true, + collapsed: true + } +}; + +export const MultipleGroups: Story = { + render: () => { + return html` + +
+ + + + + + + + + + + + + +
+ `; + } +}; diff --git a/docs/components/src/site-nav/nav-group.ts b/docs/components/src/site-nav/nav-group.ts index e89074b5f8..2207d8ba40 100644 --- a/docs/components/src/site-nav/nav-group.ts +++ b/docs/components/src/site-nav/nav-group.ts @@ -1,14 +1,35 @@ +import { faChevronDown } from '@fortawesome/pro-regular-svg-icons'; +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Icon } from '@sl-design-system/icon'; import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import styles from './nav-group.css' with { type: 'css' }; -export class NavGroup extends LitElement { +Icon.register(faChevronDown); + +export class NavGroup extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'sl-icon': Icon + }; + } + /** @internal */ static styles: CSSResultGroup = styles; + /** Whether this group can be collapsed. */ + @property({ type: Boolean, reflect: true }) collapsible?: boolean; + + /** Whether this group is collapsed. Only applies when collapsible is true. */ + @property({ type: Boolean, reflect: true }) collapsed?: boolean; + /** The section heading text. */ @property() heading?: string; + /** Optional URL for the heading. */ + @property() href?: string; + override connectedCallback(): void { super.connectedCallback(); @@ -17,7 +38,16 @@ export class NavGroup extends LitElement { override render(): TemplateResult { return html` - ${this.heading ? html`

${this.heading}

` : nothing} + ${this.heading + ? html` +

+ ${this.href ? html`${this.heading}` : this.heading} + ${this.collapsible + ? html`` + : nothing} +

+ ` + : nothing} `; } @@ -29,4 +59,10 @@ export class NavGroup extends LitElement { this.removeAttribute('aria-label'); } } + + #onHeadingClick(): void { + if (this.collapsible) { + this.collapsed = !this.collapsed; + } + } } diff --git a/docs/components/src/site-nav/nav-item.css b/docs/components/src/site-nav/nav-item.css index 7deb2602b4..809fd09f92 100644 --- a/docs/components/src/site-nav/nav-item.css +++ b/docs/components/src/site-nav/nav-item.css @@ -11,14 +11,6 @@ outline: none; } -:host(:hover) { - --_bg-opacity: var(--sl-opacity-interactive-plain-hover); -} - -:host(:active) { - --_bg-opacity: var(--sl-opacity-interactive-plain-active); -} - :host([aria-current='page']) { --_bg-color: var(--sl-color-background-selected-subtlest); --_bg-mix-color: var(--sl-color-background-selected-interactive-plain); @@ -58,6 +50,14 @@ summary { display: none; } + &:hover { + --_bg-opacity: var(--sl-opacity-interactive-plain-hover); + } + + &:active { + --_bg-opacity: var(--sl-opacity-interactive-plain-active); + } + a { color: inherit; flex: 1; @@ -75,7 +75,7 @@ summary { border-radius: var(--sl-size-150); inline-size: calc(var(--sl-size-075) - var(--sl-size-010)); inset-block-start: 50%; - inset-inline-start: calc(-1 * (var(--sl-size-125) - var(--sl-size-010))); + inset-inline-start: calc(-1 * (var(--sl-size-150) - var(--sl-size-010))); position: absolute; translate: 0 -50%; } @@ -103,7 +103,7 @@ details[open] > slot { flex-direction: column; gap: var(--sl-size-025); margin-inline-start: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent) + 0.5em); - padding-inline-start: var(--sl-size-075); + padding-inline-start: var(--sl-size-100); } .leaf { @@ -113,12 +113,19 @@ details[open] > slot { color: inherit; display: flex; gap: var(--sl-size-100); - padding-block: var(--sl-size-075); - padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); + padding: var(--sl-size-075) var(--sl-size-100); position: relative; text-decoration: none; @media (prefers-reduced-motion: no-preference) { transition: background 200ms ease-in-out; } + + &:hover { + --_bg-opacity: var(--sl-opacity-interactive-plain-hover); + } + + &:active { + --_bg-opacity: var(--sl-opacity-interactive-plain-active); + } } diff --git a/docs/components/src/site-nav/nav-item.stories.ts b/docs/components/src/site-nav/nav-item.stories.ts new file mode 100644 index 0000000000..2bda668f33 --- /dev/null +++ b/docs/components/src/site-nav/nav-item.stories.ts @@ -0,0 +1,115 @@ +import { faBook, faBookmark, faCodeBranch, faFileLines, faHome } from '@fortawesome/pro-regular-svg-icons'; +import { Icon } from '@sl-design-system/icon'; +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { NavGroup } from './nav-group.js'; +import { NavItem } from './nav-item.js'; + +Icon.register(faBook, faBookmark, faCodeBranch, faFileLines, faHome); + +type Props = Pick; +type Story = StoryObj; + +try { + customElements.define('doc-nav-group', NavGroup); + customElements.define('doc-nav-item', NavItem); +} catch { + /* empty */ +} + +export default { + title: 'Site Navigation/Nav Item', + args: { + heading: 'Documentation', + href: '#', + icon: 'far-book' + }, + argTypes: { + icon: { + control: 'text' + } + }, + render: ({ active, heading, href, icon }) => { + return html` + + + + + `; + } +} satisfies Meta; + +export const Basic: Story = {}; + +export const Active: Story = { + args: { + active: true + } +}; + +export const WithoutIcon: Story = { + args: { + icon: undefined + } +}; + +export const Expandable: Story = { + args: { + heading: 'Guides', + href: undefined, + icon: 'far-bookmark' + }, + render: ({ active, heading, icon, open }) => { + return html` + + + + + + + + + `; + } +}; + +export const ExpandableOpen: Story = { + args: { + heading: 'Guides', + href: undefined, + icon: 'far-bookmark', + open: true + }, + render: Expandable.render +}; + +export const Nested: Story = { + render: () => { + return html` + + + + + + + + + + + + + `; + } +}; diff --git a/docs/components/src/site-nav/nav-item.ts b/docs/components/src/site-nav/nav-item.ts index 35c034f36a..062a9d6ada 100644 --- a/docs/components/src/site-nav/nav-item.ts +++ b/docs/components/src/site-nav/nav-item.ts @@ -29,10 +29,10 @@ export class NavItem extends ScopedElementsMixin(LitElement) { @property() icon?: string; /** Whether this is the active/current page. */ - @property({ type: Boolean, reflect: true }) active = false; + @property({ type: Boolean, reflect: true }) active?: boolean; /** Whether the children are expanded. */ - @property({ type: Boolean, reflect: true }) open = false; + @property({ type: Boolean, reflect: true }) open?: boolean; /** @internal Whether this item has child nav items. */ @state() expandable = false; diff --git a/docs/components/src/site-nav/site-nav.stories.ts b/docs/components/src/site-nav/site-nav.stories.ts index bac9059075..fc77af80e9 100644 --- a/docs/components/src/site-nav/site-nav.stories.ts +++ b/docs/components/src/site-nav/site-nav.stories.ts @@ -13,6 +13,9 @@ import { import { Icon } from '@sl-design-system/icon'; import { type Meta, type StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; +import { NavGroup } from './nav-group.js'; +import { NavItem } from './nav-item.js'; +import { SiteNav } from './site-nav.js'; Icon.register( faBook, @@ -31,9 +34,6 @@ type Props = Record; type Story = StoryObj; try { - const { SiteNav } = await import('./site-nav.js'); - const { NavGroup } = await import('./nav-group.js'); - const { NavItem } = await import('./nav-item.js'); customElements.define('doc-site-nav', SiteNav); customElements.define('doc-nav-group', NavGroup); customElements.define('doc-nav-item', NavItem); @@ -56,9 +56,6 @@ function onNavClick(event: Event): void { export default { title: 'Site Navigation', - parameters: { - layout: 'padded' - }, render: () => { return html` - - - + + + `; @@ -101,13 +101,13 @@ export const Nested: Story = { - - - - + + + + - + `; diff --git a/docs/components/src/site-nav/nav-item.ts b/docs/components/src/site-nav/nav-item.ts index b2fef8c274..9a09a5967e 100644 --- a/docs/components/src/site-nav/nav-item.ts +++ b/docs/components/src/site-nav/nav-item.ts @@ -19,6 +19,12 @@ export class NavItem extends ScopedElementsMixin(LitElement) { /** @internal */ static styles: CSSResultGroup = styles; + /** Whether this is the active/current page. */ + @property({ type: Boolean, reflect: true }) active?: boolean; + + /** @internal Whether this item has child nav items. */ + @state() expandable = false; + /** The display text for this nav item. */ @property() heading?: string; @@ -28,18 +34,12 @@ export class NavItem extends ScopedElementsMixin(LitElement) { /** The icon name (for top-level items). */ @property() icon?: string; - /** Whether this is the active/current page. */ - @property({ type: Boolean, reflect: true }) active?: boolean; + /** @internal The nesting level (0-based), computed from DOM. */ + @state() level = 0; /** Whether the children are expanded. */ @property({ type: Boolean, reflect: true }) open?: boolean; - /** @internal Whether this item has child nav items. */ - @state() expandable = false; - - /** @internal The nesting level (0-based), computed from DOM. */ - @state() level = 0; - override connectedCallback(): void { super.connectedCallback(); @@ -65,18 +65,17 @@ export class NavItem extends ScopedElementsMixin(LitElement) { } override updated(): void { - // Sync ARIA states with properties - if (this.expandable) { - this.setAttribute('aria-expanded', Boolean(this.open).toString()); - } else { - this.removeAttribute('aria-expanded'); - } - if (this.active) { this.setAttribute('aria-current', 'page'); } else { this.removeAttribute('aria-current'); } + + if (this.expandable) { + this.setAttribute('aria-expanded', Boolean(this.open).toString()); + } else { + this.removeAttribute('aria-expanded'); + } } override render(): TemplateResult { @@ -91,7 +90,7 @@ export class NavItem extends ScopedElementsMixin(LitElement) { : html`${this.heading}`} - + `; } @@ -105,14 +104,20 @@ export class NavItem extends ScopedElementsMixin(LitElement) { `; } - #onSlotChange(event: Event): void { - const slot = event.target as HTMLSlotElement, - children = slot.assignedElements({ flatten: true }); + #onClick(event: Event): void { + const link = event.composedPath().find(el => el instanceof HTMLElement && el.matches('a.leaf')); + if (!link) { + return; + } + } + + #onSlotChange(event: Event & { target: HTMLSlotElement }): void { + const children = event.target.assignedElements({ flatten: true }); this.expandable = children.some(child => child.localName === this.localName); } - #onToggle(event: Event): void { - this.open = (event.target as HTMLDetailsElement).open; + #onToggle(event: Event & { target: HTMLDetailsElement }): void { + this.open = event.target.open; } } diff --git a/docs/components/src/site-nav/site-nav.ts b/docs/components/src/site-nav/site-nav.ts index a74edc8ece..f54851cc70 100644 --- a/docs/components/src/site-nav/site-nav.ts +++ b/docs/components/src/site-nav/site-nav.ts @@ -2,39 +2,34 @@ import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit' import { NavItem } from './nav-item.js'; import styles from './site-nav.css' with { type: 'css' }; -export { NavGroup } from './nav-group.js'; -export { NavItem } from './nav-item.js'; - export class SiteNav extends LitElement { /** @internal */ static styles: CSSResultGroup = styles; - /** Returns all visible (not inside a collapsed parent) nav-items in DOM order. */ - #getVisibleItems(): NavItem[] { - return Array.from(this.querySelectorAll('*')) - .filter((el): el is NavItem => el instanceof NavItem) - .filter(item => { - let parent = item.parentElement; + override connectedCallback(): void { + super.connectedCallback(); - while (parent && parent !== this) { - if (parent instanceof NavItem && parent.expandable && !parent.open) { - return false; - } - parent = parent.parentElement; - } + this.addEventListener('keydown', this.#onKeydown); - return true; - }); + // Set up roving tabindex after children are parsed + requestAnimationFrame(() => this.#initTabIndex()); } - #focusItem(items: NavItem[], index: number): void { - items.forEach((item, i) => { - item.tabIndex = i === index ? 0 : -1; - }); - items[index]?.focus(); + override disconnectedCallback(): void { + this.removeEventListener('keydown', this.#onKeydown); + + super.disconnectedCallback(); + } + + override render(): TemplateResult { + return html` + + `; } - #onKeyDown = (event: KeyboardEvent): void => { + #onKeydown = (event: KeyboardEvent): void => { const items = this.#getVisibleItems(), current = items.indexOf(document.activeElement as NavItem); @@ -104,19 +99,29 @@ export class SiteNav extends LitElement { } }; - override connectedCallback(): void { - super.connectedCallback(); + /** Returns all visible (not inside a collapsed parent) nav-items in DOM order. */ + #getVisibleItems(): NavItem[] { + return Array.from(this.querySelectorAll('*')) + .filter((el): el is NavItem => el instanceof NavItem) + .filter(item => { + let parent = item.parentElement; - this.addEventListener('keydown', this.#onKeyDown); + while (parent && parent !== this) { + if (parent instanceof NavItem && parent.expandable && !parent.open) { + return false; + } + parent = parent.parentElement; + } - // Set up roving tabindex after children are parsed - requestAnimationFrame(() => this.#initTabIndex()); + return true; + }); } - override disconnectedCallback(): void { - this.removeEventListener('keydown', this.#onKeyDown); - - super.disconnectedCallback(); + #focusItem(items: NavItem[], index: number): void { + items.forEach((item, i) => { + item.tabIndex = i === index ? 0 : -1; + }); + items[index]?.focus(); } #initTabIndex(): void { @@ -125,12 +130,4 @@ export class SiteNav extends LitElement { item.tabIndex = i === 0 ? 0 : -1; }); } - - override render(): TemplateResult { - return html` - - `; - } } diff --git a/docs/website/src/content/api-reference/api-reference.md b/docs/website/src/content/api-reference/api-reference.md index 18afc22e86..1904707a28 100644 --- a/docs/website/src/content/api-reference/api-reference.md +++ b/docs/website/src/content/api-reference/api-reference.md @@ -4,4 +4,6 @@ layout: base.njk eleventyNavigation: key: API Reference order: 3 + collapsible: true + collapsed: true --- From 1ffe76c66e893eeb7ae56de36eff2a8ac829c40c Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 25 Mar 2026 08:57:31 +0100 Subject: [PATCH 19/72] =?UTF-8?q?=F0=9F=8C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 5 +- docs/components/src/site-nav/nav-item.css | 77 +++++++++++-------- .../src/site-nav/nav-item.stories.ts | 16 ++-- docs/components/src/site-nav/nav-item.ts | 38 +++++---- .../src/site-nav/site-nav.stories.ts | 2 +- docs/components/src/site-nav/site-nav.ts | 43 +++++++++++ docs/components/tsdown.config.ts | 2 + docs/website/src/js/main.js | 4 +- 8 files changed, 124 insertions(+), 63 deletions(-) diff --git a/docs/components/package.json b/docs/components/package.json index 9de583ee5c..ce48849197 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -13,6 +13,8 @@ "./page-toc/page-toc": "./dist/page-toc/page-toc.js", "./search/search": "./dist/search/search.js", "./sidebar/sidebar": "./dist/sidebar/sidebar.js", + "./site-nav/nav-group": "./dist/site-nav/nav-group.js", + "./site-nav/nav-item": "./dist/site-nav/nav-item.js", "./site-nav/site-nav": "./dist/site-nav/site-nav.js", "./theme-switch/theme-switch": "./dist/theme-switch/theme-switch.js", "./package.json": "./package.json" @@ -61,9 +63,6 @@ "lit": "^3.3.2" }, "inlinedDependencies": { - "@floating-ui/core": "1.7.5", - "@floating-ui/dom": "1.7.6", - "@floating-ui/utils": "0.2.11", "@fortawesome/free-brands-svg-icons": "7.2.0", "@fortawesome/pro-regular-svg-icons": "7.2.0", "@fortawesome/pro-solid-svg-icons": "7.2.0" diff --git a/docs/components/src/site-nav/nav-item.css b/docs/components/src/site-nav/nav-item.css index 809fd09f92..f57c2a4c2d 100644 --- a/docs/components/src/site-nav/nav-item.css +++ b/docs/components/src/site-nav/nav-item.css @@ -11,13 +11,17 @@ outline: none; } -:host([aria-current='page']) { +:host([active]) { --_bg-color: var(--sl-color-background-selected-subtlest); --_bg-mix-color: var(--sl-color-background-selected-interactive-plain); color: var(--sl-color-foreground-accent-blue-bold); } +:host([data-level='0']) .active { + display: none; +} + :host(:focus-visible) summary, :host(:focus-visible) a { outline: var(--sl-size-borderWidth-focusRing) solid var(--sl-color-border-focused); @@ -26,21 +30,31 @@ details { border: none; + + &[open] { + summary { + margin-block-end: var(--sl-size-025); + } + + .chevron { + transform: rotate(90deg); + } + + .subtree { + display: flex; + } + } } summary { align-items: center; background: color-mix(in srgb, var(--_bg-color), var(--_bg-mix-color) calc(100% * var(--_bg-opacity))); border-radius: var(--sl-size-borderRadius-default); - color: inherit; cursor: pointer; display: flex; - font-weight: var(--_font-weight); gap: var(--sl-size-100); - list-style: none; padding-block: var(--sl-size-075); padding-inline: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent)) var(--sl-size-100); - position: relative; @media (prefers-reduced-motion: no-preference) { transition: background 200ms ease-in-out; @@ -63,25 +77,6 @@ summary { flex: 1; text-decoration: none; } - - .label { - flex: 1; - } -} - -.active { - background: var(--sl-color-background-selected-bold); - block-size: var(--sl-size-300); - border-radius: var(--sl-size-150); - inline-size: calc(var(--sl-size-075) - var(--sl-size-010)); - inset-block-start: 50%; - inset-inline-start: calc(-1 * (var(--sl-size-150) - var(--sl-size-010))); - position: absolute; - translate: 0 -50%; -} - -:host([data-level='0']) .active { - display: none; } .chevron { @@ -89,24 +84,40 @@ summary { transition: transform 0.15s ease; } -details[open] > summary { - margin-block-end: var(--sl-size-025); - - .chevron { - transform: rotate(90deg); - } +.label { + flex: 1; } -details[open] > slot { +.subtree { border-inline-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); - display: flex; + display: none; flex-direction: column; gap: var(--sl-size-025); margin-inline-start: calc(var(--sl-size-100) + var(--nav-level, 0) * var(--nav-indent) + 0.5em); padding-inline-start: var(--sl-size-100); + position: relative; +} + +[part='active'] { + background: var(--sl-elevation-surface-base-default); + border-radius: var(--sl-size-150); + inline-size: calc(var(--sl-size-075) - var(--sl-size-010)); + inset-block: anchor(top) anchor(bottom); + inset-inline-start: calc(-1 * (var(--sl-size-050) - var(--sl-size-010))); + position: absolute; + transition: inset 200ms ease-in-out; + + &::before { + background: var(--sl-color-background-selected-bold); + border-radius: var(--sl-size-150); + content: ''; + inline-size: 100%; + inset-block: var(--sl-size-050); + position: absolute; + } } -.leaf { +[part='leaf'] { align-items: center; background: color-mix(in srgb, var(--_bg-color), var(--_bg-mix-color) calc(100% * var(--_bg-opacity))); border-radius: var(--sl-size-borderRadius-default); diff --git a/docs/components/src/site-nav/nav-item.stories.ts b/docs/components/src/site-nav/nav-item.stories.ts index dd67c6eef7..2bda668f33 100644 --- a/docs/components/src/site-nav/nav-item.stories.ts +++ b/docs/components/src/site-nav/nav-item.stories.ts @@ -72,9 +72,9 @@ export const Expandable: Story = { - - - + + + `; @@ -101,13 +101,13 @@ export const Nested: Story = { - - - - + + + + - + `; diff --git a/docs/components/src/site-nav/nav-item.ts b/docs/components/src/site-nav/nav-item.ts index 9a09a5967e..3924f1d4a5 100644 --- a/docs/components/src/site-nav/nav-item.ts +++ b/docs/components/src/site-nav/nav-item.ts @@ -45,6 +45,17 @@ export class NavItem extends ScopedElementsMixin(LitElement) { this.setAttribute('role', 'treeitem'); + const style = document.createElement('style'); + style.innerText = ` + doc-nav-item:has(> doc-nav-item[active])::part(active) { + position-anchor: --active; + } + doc-nav-item > doc-nav-item[active]::part(leaf) { + anchor-name: --active; + } + `; + this.append(style); + // Compute nesting level by counting ancestor nav-item elements let level = 0, parent = this.parentElement; @@ -65,12 +76,6 @@ export class NavItem extends ScopedElementsMixin(LitElement) { } override updated(): void { - if (this.active) { - this.setAttribute('aria-current', 'page'); - } else { - this.removeAttribute('aria-current'); - } - if (this.expandable) { this.setAttribute('aria-expanded', Boolean(this.open).toString()); } else { @@ -83,34 +88,33 @@ export class NavItem extends ScopedElementsMixin(LitElement) { return html`
- ${this.active ? html`` : nothing} ${this.icon ? html`` : nothing} ${this.href ? html`${this.heading}` : html`${this.heading}`} - +
+ + +
`; } return html` - - ${this.active ? html`` : nothing} + ${this.icon ? html`` : nothing} ${this.heading} `; } - #onClick(event: Event): void { - const link = event.composedPath().find(el => el instanceof HTMLElement && el.matches('a.leaf')); - if (!link) { - return; - } - } - #onSlotChange(event: Event & { target: HTMLSlotElement }): void { const children = event.target.assignedElements({ flatten: true }); diff --git a/docs/components/src/site-nav/site-nav.stories.ts b/docs/components/src/site-nav/site-nav.stories.ts index fc77af80e9..2e2c517e03 100644 --- a/docs/components/src/site-nav/site-nav.stories.ts +++ b/docs/components/src/site-nav/site-nav.stories.ts @@ -63,7 +63,7 @@ export default { max-inline-size: 280px; } - + diff --git a/docs/components/src/site-nav/site-nav.ts b/docs/components/src/site-nav/site-nav.ts index f54851cc70..e75d54e90f 100644 --- a/docs/components/src/site-nav/site-nav.ts +++ b/docs/components/src/site-nav/site-nav.ts @@ -2,6 +2,8 @@ import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit' import { NavItem } from './nav-item.js'; import styles from './site-nav.css' with { type: 'css' }; +const insideStorybook = location.pathname.startsWith('/iframe.html'); + export class SiteNav extends LitElement { /** @internal */ static styles: CSSResultGroup = styles; @@ -11,11 +13,14 @@ export class SiteNav extends LitElement { this.addEventListener('keydown', this.#onKeydown); + navigation.addEventListener('navigate', this.#onNavigate); + // Set up roving tabindex after children are parsed requestAnimationFrame(() => this.#initTabIndex()); } override disconnectedCallback(): void { + navigation.removeEventListener('navigate', this.#onNavigate); this.removeEventListener('keydown', this.#onKeydown); super.disconnectedCallback(); @@ -99,6 +104,44 @@ export class SiteNav extends LitElement { } }; + #onNavigate = (event: NavigateEvent): void => { + // Return early if the event can't be intercepted or is a download request + if (!event.canIntercept || event.downloadRequest !== null) { + return; + } + + // Return early if the navigation is just a hash change on the same page + if (!insideStorybook && event.hashChange) { + return; + } + + // Remove the active state from the currently active item, if any + this.querySelector('doc-nav-item[active]')?.removeAttribute('active'); + + // Add the active state to the clicked nav item (if any) + event.sourceElement?.getRootNode()?.host?.setAttribute('active', ''); + + // In Storybook, just update the URL and let Storybook handle the rest + if (insideStorybook) { + return; + } + + event.intercept({ + async handler() { + const response = await fetch(new URL(event.destination.url)), + text = await response.text(), + doc = new DOMParser().parseFromString(text, 'text/html'), + newContent = doc.querySelector('main.content'), + currentContent = document.querySelector('main.content'); + + if (newContent && currentContent) { + document.title = doc.title; + currentContent.innerHTML = newContent.innerHTML; + } + } + }); + }; + /** Returns all visible (not inside a collapsed parent) nav-items in DOM order. */ #getVisibleItems(): NavItem[] { return Array.from(this.querySelectorAll('*')) diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index ac5b7699d7..d2d9ae9fac 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -49,6 +49,8 @@ export default defineConfig({ 'src/page-toc/page-toc.ts', 'src/search/search.ts', 'src/sidebar/sidebar.ts', + 'src/site-nav/nav-group.ts', + 'src/site-nav/nav-item.ts', 'src/site-nav/site-nav.ts', 'src/theme-switch/theme-switch.ts' ], diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index 1c095e740f..d6d2cf060d 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -2,7 +2,9 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-regi import './icons.js'; import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc'; import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar'; -import { NavGroup, NavItem, SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav'; +import { NavGroup } from '@sl-design-system/doc-components/site-nav/nav-group'; +import { NavItem } from '@sl-design-system/doc-components/site-nav/nav-item'; +import { SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav'; customElements.define('doc-nav-group', NavGroup); customElements.define('doc-nav-item', NavItem); From a87a3ea6a3af57029feabdc3942ff6e99fb17278 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 30 Mar 2026 14:15:17 +0200 Subject: [PATCH 20/72] =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 16 +- docs/website/package.json | 4 +- package.json | 10 +- tools/vitest-browser-lit/package.json | 4 +- yarn.lock | 966 ++++++++++---------------- 5 files changed, 384 insertions(+), 616 deletions(-) diff --git a/docs/components/package.json b/docs/components/package.json index ce48849197..33cad7c5a6 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -45,17 +45,17 @@ }, "devDependencies": { "@roenlie/vite-plugin-import-css-sheet": "^0.0.7", - "@storybook/addon-a11y": "^10.3.1", - "@storybook/addon-docs": "^10.3.1", - "@storybook/addon-vitest": "^10.3.1", - "@storybook/web-components": "^10.3.1", - "@storybook/web-components-vite": "^10.3.1", - "@tsdown/css": "^0.21.4", + "@storybook/addon-a11y": "^10.3.3", + "@storybook/addon-docs": "^10.3.3", + "@storybook/addon-vitest": "^10.3.3", + "@storybook/web-components": "^10.3.3", + "@storybook/web-components-vite": "^10.3.3", + "@tsdown/css": "^0.21.7", "@typescript/native-preview": "^7.0.0-dev.20260315.1", "lit": "^3.3.2", "sass-embedded": "^1.98.0", - "storybook": "^10.3.1", - "tsdown": "^0.21.4", + "storybook": "^10.3.3", + "tsdown": "^0.21.7", "typescript": "^5.9.3", "wireit": "^0.14.12" }, diff --git a/docs/website/package.json b/docs/website/package.json index 6cb046ccb2..f06b17dcd6 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -7,8 +7,8 @@ "build": "eleventy" }, "dependencies": { - "@11ty/eleventy": "^3.0.0", - "@11ty/eleventy-navigation": "^0.3.5", + "@11ty/eleventy": "^3.1.5", + "@11ty/eleventy-navigation": "^1.0.5", "@fortawesome/pro-regular-svg-icons": "^7.2.0", "@sl-design-system/doc-components": "workspace:^", "@sl-design-system/icon": "workspace:^", diff --git a/package.json b/package.json index 7d3f1bbae2..9931a9e915 100644 --- a/package.json +++ b/package.json @@ -469,9 +469,9 @@ "@types/chai-dom": "^1.11.3", "@types/sinon": "^21.0.0", "@types/sinon-chai": "^4.0.0", - "@vitest/browser-playwright": "~4.0.17", - "@vitest/coverage-v8": "~4.0.17", - "@vitest/ui": "~4.0.17", + "@vitest/browser-playwright": "~4.1.2", + "@vitest/coverage-v8": "~4.1.2", + "@vitest/ui": "~4.1.2", "@webcomponents/scoped-custom-element-registry": "^0.0.10", "chai": "^6.2.2", "chai-datetime": "^1.8.1", @@ -488,8 +488,8 @@ "storybook": "^10.3.3", "stylelint": "^16.19.1", "typescript": "^5.5.4", - "vite": "^8.0.2", - "vitest": "~4.0.17", + "vite": "^8.0.3", + "vitest": "~4.1.2", "wireit": "^0.14.12" } } diff --git a/tools/vitest-browser-lit/package.json b/tools/vitest-browser-lit/package.json index ab6ecbb5ba..5855667796 100644 --- a/tools/vitest-browser-lit/package.json +++ b/tools/vitest-browser-lit/package.json @@ -28,8 +28,8 @@ "vitest": ">=2.1.0" }, "devDependencies": { - "@vitest/browser": "^4.0.17", + "@vitest/browser": "^4.1.2", "lit": "^3.3.2", - "vitest": "^4.0.17" + "vitest": "^4.1.2" } } diff --git a/yarn.lock b/yarn.lock index 562ae55d79..7a400a1549 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,7 +5,7 @@ __metadata: version: 8 cacheKey: 10c0 -"@11ty/dependency-tree-esm@npm:^2.0.0": +"@11ty/dependency-tree-esm@npm:^2.0.4": version: 2.0.4 resolution: "@11ty/dependency-tree-esm@npm:2.0.4" dependencies: @@ -24,7 +24,7 @@ __metadata: languageName: node linkType: hard -"@11ty/dependency-tree@npm:^4.0.0": +"@11ty/dependency-tree@npm:^4.0.2": version: 4.0.2 resolution: "@11ty/dependency-tree@npm:4.0.2" dependencies: @@ -112,7 +112,16 @@ __metadata: languageName: node linkType: hard -"@11ty/eleventy-plugin-bundle@npm:^3.0.6": +"@11ty/eleventy-navigation@npm:^1.0.5": + version: 1.0.5 + resolution: "@11ty/eleventy-navigation@npm:1.0.5" + dependencies: + dependency-graph: "npm:^1.0.0" + checksum: 10c0/cfaead17d0e391582d5151bd0fb3f18be26339baf75a8c0ad13dfa6a5bfef4fa38166889893573e9671397288d4acf405d3586786fed35dd2d7004443f3c3e26 + languageName: node + linkType: hard + +"@11ty/eleventy-plugin-bundle@npm:^3.0.7": version: 3.0.7 resolution: "@11ty/eleventy-plugin-bundle@npm:3.0.7" dependencies: @@ -196,46 +205,46 @@ __metadata: languageName: node linkType: hard -"@11ty/eleventy@npm:^3.0.0": - version: 3.1.2 - resolution: "@11ty/eleventy@npm:3.1.2" +"@11ty/eleventy@npm:^3.1.5": + version: 3.1.5 + resolution: "@11ty/eleventy@npm:3.1.5" dependencies: - "@11ty/dependency-tree": "npm:^4.0.0" - "@11ty/dependency-tree-esm": "npm:^2.0.0" + "@11ty/dependency-tree": "npm:^4.0.2" + "@11ty/dependency-tree-esm": "npm:^2.0.4" "@11ty/eleventy-dev-server": "npm:^2.0.8" - "@11ty/eleventy-plugin-bundle": "npm:^3.0.6" + "@11ty/eleventy-plugin-bundle": "npm:^3.0.7" "@11ty/eleventy-utils": "npm:^2.0.7" "@11ty/lodash-custom": "npm:^4.17.21" - "@11ty/posthtml-urls": "npm:^1.0.1" - "@11ty/recursive-copy": "npm:^4.0.2" + "@11ty/posthtml-urls": "npm:^1.0.2" + "@11ty/recursive-copy": "npm:^4.0.4" "@sindresorhus/slugify": "npm:^2.2.1" bcp-47-normalize: "npm:^2.3.0" chokidar: "npm:^3.6.0" - debug: "npm:^4.4.1" + debug: "npm:^4.4.3" dependency-graph: "npm:^1.0.0" entities: "npm:^6.0.1" filesize: "npm:^10.1.6" gray-matter: "npm:^4.0.3" iso-639-1: "npm:^3.1.5" - js-yaml: "npm:^4.1.0" + js-yaml: "npm:^4.1.1" kleur: "npm:^4.1.5" - liquidjs: "npm:^10.21.1" - luxon: "npm:^3.6.1" - markdown-it: "npm:^14.1.0" + liquidjs: "npm:^10.25.0" + luxon: "npm:^3.7.2" + markdown-it: "npm:^14.1.1" minimist: "npm:^1.2.8" - moo: "npm:^0.5.2" + moo: "npm:0.5.2" node-retrieve-globals: "npm:^6.0.1" nunjucks: "npm:^3.2.4" - picomatch: "npm:^4.0.2" + picomatch: "npm:^4.0.3" please-upgrade-node: "npm:^3.2.0" - posthtml: "npm:^0.16.6" + posthtml: "npm:^0.16.7" posthtml-match-helper: "npm:^2.0.3" - semver: "npm:^7.7.2" - slugify: "npm:^1.6.6" - tinyglobby: "npm:^0.2.14" + semver: "npm:^7.7.4" + slugify: "npm:^1.6.8" + tinyglobby: "npm:^0.2.15" bin: eleventy: cmd.cjs - checksum: 10c0/3c1356c5a90e79fb8c481a667cbda892ec60b66be07d9a6a9727616864ddee9df01f5d44a02205002fd0361f1587c682008effc5cbc44ea32611a7b989c7bbaf + checksum: 10c0/538f005653ded77c77505d37ab3ef5557a33bfcd28df48a9a62ab6b6f052cf9b13cad871ab96fb7758a4b77de20e1d3d662e11cedf83d13558b21511c06629a0 languageName: node linkType: hard @@ -246,27 +255,27 @@ __metadata: languageName: node linkType: hard -"@11ty/posthtml-urls@npm:^1.0.1": - version: 1.0.2 - resolution: "@11ty/posthtml-urls@npm:1.0.2" +"@11ty/posthtml-urls@npm:^1.0.2": + version: 1.0.3 + resolution: "@11ty/posthtml-urls@npm:1.0.3" dependencies: evaluate-value: "npm:^2.0.0" http-equiv-refresh: "npm:^2.0.1" list-to-array: "npm:^1.1.0" parse-srcset: "npm:^1.0.2" - checksum: 10c0/cf4c269875488dc4d53f422f0de75aee9fb5c218517f687fb2e5705bf1cf08afaafda13865e65a41790619329cc581455de2569f356a5e69befcf925874ea580 + checksum: 10c0/61e1316826f19319c6af44092888c17d2bdebdc81b91af756d1c77e475131f35aee0fa265d121a858f6fbc80fe61c42085323d9196f83165df2f3e9b6672229d languageName: node linkType: hard -"@11ty/recursive-copy@npm:^4.0.2": - version: 4.0.3 - resolution: "@11ty/recursive-copy@npm:4.0.3" +"@11ty/recursive-copy@npm:^4.0.4": + version: 4.0.4 + resolution: "@11ty/recursive-copy@npm:4.0.4" dependencies: errno: "npm:^1.0.0" junk: "npm:^3.1.0" - maximatch: "npm:^0.1.0" + minimatch: "npm:^3.1.5" slash: "npm:^3.0.0" - checksum: 10c0/faf71eb57c3555f4b47bfe47cbfdf979c642ea2462a2afcaedb9091f5647fe1fa24f3de3882c0b8f05468d6d60b3dbf2901a57c13e21bc1c5e70b78b2e9b2525 + checksum: 10c0/9d9d87aefd5c66da569ce92af06790961ed317f18e452fac5bd8f49f750d44839575a5f7c87d278e9b81a24c8c020a6595f00d0c31b5b8c1ed08b02b8a6cc2af languageName: node linkType: hard @@ -957,17 +966,17 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:8.0.0-rc.2": - version: 8.0.0-rc.2 - resolution: "@babel/generator@npm:8.0.0-rc.2" +"@babel/generator@npm:8.0.0-rc.3": + version: 8.0.0-rc.3 + resolution: "@babel/generator@npm:8.0.0-rc.3" dependencies: - "@babel/parser": "npm:^8.0.0-rc.2" - "@babel/types": "npm:^8.0.0-rc.2" + "@babel/parser": "npm:^8.0.0-rc.3" + "@babel/types": "npm:^8.0.0-rc.3" "@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/trace-mapping": "npm:^0.3.28" "@types/jsesc": "npm:^2.5.0" jsesc: "npm:^3.0.2" - checksum: 10c0/ffaf6d16c0b60968f25823d006177e5b0021a2fb2d463f9b8ae70b34a48fe530f1395dce6301989424024fffcdd2251f7357cf95dfebd96d3c637daddc5eb1aa + checksum: 10c0/43363ccd8c5e18ea4c1c198f17158acabb4074f12ae64c5772f97fe58b9a377de445d9fd0b403812bc10dadff44fe356a07f6017f25732b021bba3654241a78e languageName: node linkType: hard @@ -1146,17 +1155,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^8.0.0-rc.2": - version: 8.0.0-rc.2 - resolution: "@babel/helper-string-parser@npm:8.0.0-rc.2" - checksum: 10c0/74b5107ed84c99948651a52ebd880d91f0c307fcf0273b75c79552b2c5469c27573640a5187c87df636e96c247a43c57be70bdac1ce06c29a30aefd499952121 +"@babel/helper-string-parser@npm:^8.0.0-rc.3": + version: 8.0.0-rc.3 + resolution: "@babel/helper-string-parser@npm:8.0.0-rc.3" + checksum: 10c0/204a59f279c9fb3ea7ea5a817712f3e9099a67711ecf7f314c98d700037ea50920c2a311f94529aa3fd163517068283db0e0e14c08832b29ae45e7c1969b3ff7 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:8.0.0-rc.2, @babel/helper-validator-identifier@npm:^8.0.0-rc.2": - version: 8.0.0-rc.2 - resolution: "@babel/helper-validator-identifier@npm:8.0.0-rc.2" - checksum: 10c0/9a1687e18bfb50728ae38b1dac889c1a3bc2c53bdf4c1632533b1b0672cc272c087507a2a7c60c3af20d58bd645e169dd685f892d2a62d580e759e26d67e8788 +"@babel/helper-validator-identifier@npm:8.0.0-rc.3, @babel/helper-validator-identifier@npm:^8.0.0-rc.3": + version: 8.0.0-rc.3 + resolution: "@babel/helper-validator-identifier@npm:8.0.0-rc.3" + checksum: 10c0/03236675006da83b8530ef95896042d5246989e2fdc8283a60882a14c7ce86dc18db6a6b12f18b638d6722adc5f1e721142889a331e12a6f7c0fba3e307fdc7f languageName: node linkType: hard @@ -1195,18 +1204,18 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:8.0.0-rc.2, @babel/parser@npm:^8.0.0-beta.4, @babel/parser@npm:^8.0.0-rc.2": - version: 8.0.0-rc.2 - resolution: "@babel/parser@npm:8.0.0-rc.2" +"@babel/parser@npm:8.0.0-rc.3, @babel/parser@npm:^8.0.0-beta.4, @babel/parser@npm:^8.0.0-rc.3": + version: 8.0.0-rc.3 + resolution: "@babel/parser@npm:8.0.0-rc.3" dependencies: - "@babel/types": "npm:^8.0.0-rc.2" + "@babel/types": "npm:^8.0.0-rc.3" bin: parser: ./bin/babel-parser.js - checksum: 10c0/704ddbc1fce338e5b8df6327c1a7eeb1a554d136f89738135a8be5f5e2e854bd3f05eb3946b9d7b6814491bcd677175496076657696674eaecbed0e582749b2e + checksum: 10c0/666f954d5744261e4fbfa32170ca0034bb96d624d1c0936eb6a5a76e196773e93b480a99c87621cc35bee00015fd27318a5bd0542efd747cb0499ad5d3e58b75 languageName: node linkType: hard -"@babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.2, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": +"@babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.2, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": version: 7.29.0 resolution: "@babel/parser@npm:7.29.0" dependencies: @@ -2062,13 +2071,13 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:8.0.0-rc.2, @babel/types@npm:^8.0.0-rc.2": - version: 8.0.0-rc.2 - resolution: "@babel/types@npm:8.0.0-rc.2" +"@babel/types@npm:8.0.0-rc.3, @babel/types@npm:^8.0.0-rc.3": + version: 8.0.0-rc.3 + resolution: "@babel/types@npm:8.0.0-rc.3" dependencies: - "@babel/helper-string-parser": "npm:^8.0.0-rc.2" - "@babel/helper-validator-identifier": "npm:^8.0.0-rc.2" - checksum: 10c0/8b372115aa4ee3f55541e19887683655e32b78b64579d2402119920af3512594a2be820e05db4a3100cb38db3da3a7bdcc1beb7d031a4ab0129f28d858f129bd + "@babel/helper-string-parser": "npm:^8.0.0-rc.3" + "@babel/helper-validator-identifier": "npm:^8.0.0-rc.3" + checksum: 10c0/ffc29e2453bb3c25defbc49b243e09e10374bad571c7f27d9fed09a92f43a25624c55edfed5c8d4e45cac0b39efa040e9b6b2aef7cd7076f04311c31a2a2c8bf languageName: node linkType: hard @@ -4293,13 +4302,6 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.115.0": - version: 0.115.0 - resolution: "@oxc-project/types@npm:0.115.0" - checksum: 10c0/47fc31eb3fb3fcf4119955339f92ba2003f9b445834c1a28ed945cd6b9cd833c7ba66fca88aa5277336c2c55df300a593bc67970e544691eceaa486ebe12cb58 - languageName: node - linkType: hard - "@oxc-project/types@npm:=0.122.0": version: 0.122.0 resolution: "@oxc-project/types@npm:0.122.0" @@ -4788,9 +4790,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.11" +"@rolldown/binding-android-arm64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.12" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -4802,16 +4804,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.9" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.11" +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.12" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -4823,16 +4818,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.9" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.11" +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.12" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -4844,16 +4832,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.9" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.11" +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.12" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -4865,16 +4846,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.9" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.11" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.12" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -4886,16 +4860,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.9" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.11" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.12" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -4907,16 +4874,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.9" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.11" +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.12" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -4928,44 +4888,23 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.9" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.11" +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.12" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.9" - conditions: os=linux & cpu=ppc64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.11" - conditions: os=linux & cpu=s390x & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.9" +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.12" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.11" +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.12" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -4977,16 +4916,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.9" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.11" +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.12" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -4998,16 +4930,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.9" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.11" +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.12" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -5019,16 +4944,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.9" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.11" +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.12" dependencies: "@napi-rs/wasm-runtime": "npm:^1.1.1" conditions: cpu=wasm32 @@ -5044,18 +4962,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.9" - dependencies: - "@napi-rs/wasm-runtime": "npm:^1.1.1" - conditions: cpu=wasm32 - languageName: node - linkType: hard - -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.11" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.12" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -5067,16 +4976,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.9" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.11" +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.12" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5088,17 +4990,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.9" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/pluginutils@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.11" - checksum: 10c0/ed20f15c0d78bb3e82f1cb1924ed4b489c026e76cc28ed861609101c75790effa1e2e0fed37ee1b22ceec83aee8ab59098a0d5d3d1b62baa1b44753f88a5e4c6 +"@rolldown/pluginutils@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.12" + checksum: 10c0/f785d1180ea4876bf6a6a67135822808d1c07f902409524ff1088779f7d5318f6e603d281fb107a5145c1ca54b7cabebd359629ec474ebbc2812f2cf53db4023 languageName: node linkType: hard @@ -5109,13 +5004,6 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.9" - checksum: 10c0/fca488fb96b134ccf95b42632b6112b4abb8b3a9688f166fbd627410def2538ee201953717d234ddecbff62dfe4edc4e72c657b01a9d0750134608d767eea5fd - languageName: node - linkType: hard - "@rollup/plugin-json@npm:^6.1.0": version: 6.1.0 resolution: "@rollup/plugin-json@npm:6.1.0" @@ -5791,17 +5679,17 @@ __metadata: "@sl-design-system/icon": "workspace:^" "@sl-design-system/search-field": "workspace:^" "@sl-design-system/switch": "workspace:^" - "@storybook/addon-a11y": "npm:^10.3.1" - "@storybook/addon-docs": "npm:^10.3.1" - "@storybook/addon-vitest": "npm:^10.3.1" - "@storybook/web-components": "npm:^10.3.1" - "@storybook/web-components-vite": "npm:^10.3.1" - "@tsdown/css": "npm:^0.21.4" + "@storybook/addon-a11y": "npm:^10.3.3" + "@storybook/addon-docs": "npm:^10.3.3" + "@storybook/addon-vitest": "npm:^10.3.3" + "@storybook/web-components": "npm:^10.3.3" + "@storybook/web-components-vite": "npm:^10.3.3" + "@tsdown/css": "npm:^0.21.7" "@typescript/native-preview": "npm:^7.0.0-dev.20260315.1" lit: "npm:^3.3.2" sass-embedded: "npm:^1.98.0" - storybook: "npm:^10.3.1" - tsdown: "npm:^0.21.4" + storybook: "npm:^10.3.3" + tsdown: "npm:^0.21.7" typescript: "npm:^5.9.3" wireit: "npm:^0.14.12" peerDependencies: @@ -6150,9 +6038,9 @@ __metadata: "@types/chai-dom": "npm:^1.11.3" "@types/sinon": "npm:^21.0.0" "@types/sinon-chai": "npm:^4.0.0" - "@vitest/browser-playwright": "npm:~4.0.17" - "@vitest/coverage-v8": "npm:~4.0.17" - "@vitest/ui": "npm:~4.0.17" + "@vitest/browser-playwright": "npm:~4.1.2" + "@vitest/coverage-v8": "npm:~4.1.2" + "@vitest/ui": "npm:~4.1.2" "@webcomponents/scoped-custom-element-registry": "npm:^0.0.10" chai: "npm:^6.2.2" chai-datetime: "npm:^1.8.1" @@ -6169,8 +6057,8 @@ __metadata: storybook: "npm:^10.3.3" stylelint: "npm:^16.19.1" typescript: "npm:^5.5.4" - vite: "npm:^8.0.2" - vitest: "npm:~4.0.17" + vite: "npm:^8.0.3" + vitest: "npm:~4.1.2" wireit: "npm:^0.14.12" languageName: unknown linkType: soft @@ -6617,9 +6505,9 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/vitest-browser-lit@workspace:tools/vitest-browser-lit" dependencies: - "@vitest/browser": "npm:^4.0.17" + "@vitest/browser": "npm:^4.1.2" lit: "npm:^3.3.2" - vitest: "npm:^4.0.17" + vitest: "npm:^4.1.2" peerDependencies: "@vitest/browser": ">=2.1.0" lit: ">3.0.0" @@ -6631,8 +6519,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/website-v2@workspace:docs/website" dependencies: - "@11ty/eleventy": "npm:^3.0.0" - "@11ty/eleventy-navigation": "npm:^0.3.5" + "@11ty/eleventy": "npm:^3.1.5" + "@11ty/eleventy-navigation": "npm:^1.0.5" "@fortawesome/pro-regular-svg-icons": "npm:^7.2.0" "@sl-design-system/doc-components": "workspace:^" "@sl-design-system/icon": "workspace:^" @@ -6690,7 +6578,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:^10.3.1, @storybook/addon-a11y@npm:^10.3.3": +"@storybook/addon-a11y@npm:^10.3.3": version: 10.3.3 resolution: "@storybook/addon-a11y@npm:10.3.3" dependencies: @@ -6702,7 +6590,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-docs@npm:^10.3.1, @storybook/addon-docs@npm:^10.3.3": +"@storybook/addon-docs@npm:^10.3.3": version: 10.3.3 resolution: "@storybook/addon-docs@npm:10.3.3" dependencies: @@ -6730,7 +6618,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-vitest@npm:^10.3.1, @storybook/addon-vitest@npm:^10.3.3": +"@storybook/addon-vitest@npm:^10.3.3": version: 10.3.3 resolution: "@storybook/addon-vitest@npm:10.3.3" dependencies: @@ -6896,7 +6784,7 @@ __metadata: languageName: node linkType: hard -"@storybook/web-components-vite@npm:^10.3.1, @storybook/web-components-vite@npm:^10.3.3": +"@storybook/web-components-vite@npm:^10.3.3": version: 10.3.3 resolution: "@storybook/web-components-vite@npm:10.3.3" dependencies: @@ -6908,7 +6796,7 @@ __metadata: languageName: node linkType: hard -"@storybook/web-components@npm:10.3.3, @storybook/web-components@npm:^10.3.1, @storybook/web-components@npm:^10.3.3": +"@storybook/web-components@npm:10.3.3, @storybook/web-components@npm:^10.3.3": version: 10.3.3 resolution: "@storybook/web-components@npm:10.3.3" dependencies: @@ -7086,20 +6974,20 @@ __metadata: languageName: node linkType: hard -"@tsdown/css@npm:^0.21.4": - version: 0.21.4 - resolution: "@tsdown/css@npm:0.21.4" +"@tsdown/css@npm:^0.21.7": + version: 0.21.7 + resolution: "@tsdown/css@npm:0.21.7" dependencies: lightningcss: "npm:^1.32.0" postcss-load-config: "npm:^6.0.1" - rolldown: "npm:1.0.0-rc.9" + rolldown: "npm:1.0.0-rc.12" peerDependencies: postcss: ^8.4.0 postcss-import: ^16.0.0 postcss-modules: ^6.0.0 sass: "*" sass-embedded: "*" - tsdown: 0.21.4 + tsdown: 0.21.7 peerDependenciesMeta: postcss: optional: true @@ -7111,7 +6999,7 @@ __metadata: optional: true sass-embedded: optional: true - checksum: 10c0/f71a7e92846de98cd54769899de9e4a878304a377216664185ebd71a0fa134633fdf5dafc9ad07bdd46b038d3393f4601034cbedef85d11af46e1677b3712e3c + checksum: 10c0/9ebda2b1fa8157b540fb8b434af1527cd040ecde5acac6318e7a612a13644cbd93b327079436e84d4886ebfda257121a22dbc0499380d1e72f47df4512d27f6b languageName: node linkType: hard @@ -8209,80 +8097,62 @@ __metadata: languageName: node linkType: hard -"@vitest/browser-playwright@npm:~4.0.17": - version: 4.0.18 - resolution: "@vitest/browser-playwright@npm:4.0.18" +"@vitest/browser-playwright@npm:~4.1.2": + version: 4.1.2 + resolution: "@vitest/browser-playwright@npm:4.1.2" dependencies: - "@vitest/browser": "npm:4.0.18" - "@vitest/mocker": "npm:4.0.18" - tinyrainbow: "npm:^3.0.3" + "@vitest/browser": "npm:4.1.2" + "@vitest/mocker": "npm:4.1.2" + tinyrainbow: "npm:^3.1.0" peerDependencies: playwright: "*" - vitest: 4.0.18 + vitest: 4.1.2 peerDependenciesMeta: playwright: optional: false - checksum: 10c0/505fafe6f957d020b74914ed328de57cba0be65ff82810da85297523776a0d7389669660e58734a416fc09ce262632b4d2cf257a9e8ab1115b695d133bba7bb5 - languageName: node - linkType: hard - -"@vitest/browser@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/browser@npm:4.0.18" - dependencies: - "@vitest/mocker": "npm:4.0.18" - "@vitest/utils": "npm:4.0.18" - magic-string: "npm:^0.30.21" - pixelmatch: "npm:7.1.0" - pngjs: "npm:^7.0.0" - sirv: "npm:^3.0.2" - tinyrainbow: "npm:^3.0.3" - ws: "npm:^8.18.3" - peerDependencies: - vitest: 4.0.18 - checksum: 10c0/6b7bda92fa2e8c68de3e51c97322161484c3f1dd7a7417cdeabb4f1d98eab7dba96c156ac4282ea537c58d55cc0e5959abb4b9d90d3823b3cc3071c3f7460633 + checksum: 10c0/701a750a16059be20dddb6884e9aaad43002e1d08da94df31b0dba9abed33d0c3faba8b1c56b7da25b61b0faab1e72597cbcedd2b969f4f6139b2e17a3fd4d06 languageName: node linkType: hard -"@vitest/browser@npm:^4.0.17": - version: 4.1.0 - resolution: "@vitest/browser@npm:4.1.0" +"@vitest/browser@npm:4.1.2, @vitest/browser@npm:^4.1.2": + version: 4.1.2 + resolution: "@vitest/browser@npm:4.1.2" dependencies: "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.0" - "@vitest/utils": "npm:4.1.0" + "@vitest/mocker": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" magic-string: "npm:^0.30.21" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" - tinyrainbow: "npm:^3.0.3" + tinyrainbow: "npm:^3.1.0" ws: "npm:^8.19.0" peerDependencies: - vitest: 4.1.0 - checksum: 10c0/33b35cea63f392b6afafb6636bebe7ff0d234b1c120ec74a97462c7a7cbdbc67f415a5f0f95651f4074d53bfe12d4ff3ae8f16ba79045226df6365c77f950e18 + vitest: 4.1.2 + checksum: 10c0/8ff656df7c3796f24b38800f42cc59902b15196556ef1df1cf931faf0b095db9677109c2e855ed8915c36bc6aae804b4c53e22c069c749ed2b7e16d8eefddde5 languageName: node linkType: hard -"@vitest/coverage-v8@npm:~4.0.17": - version: 4.0.18 - resolution: "@vitest/coverage-v8@npm:4.0.18" +"@vitest/coverage-v8@npm:~4.1.2": + version: 4.1.2 + resolution: "@vitest/coverage-v8@npm:4.1.2" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.0.18" - ast-v8-to-istanbul: "npm:^0.3.10" + "@vitest/utils": "npm:4.1.2" + ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" istanbul-reports: "npm:^3.2.0" - magicast: "npm:^0.5.1" + magicast: "npm:^0.5.2" obug: "npm:^2.1.1" - std-env: "npm:^3.10.0" - tinyrainbow: "npm:^3.0.3" + std-env: "npm:^4.0.0-rc.1" + tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.0.18 - vitest: 4.0.18 + "@vitest/browser": 4.1.2 + vitest: 4.1.2 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d + checksum: 10c0/2f4488efb34a5d9e3a70631ba263e153eecba8ec0da52cb874cdc674c88369061706572b9fc0c302376fd1de58aedc86a2f9f75e5a521084e31a1446c85f2c40 languageName: node linkType: hard @@ -8299,55 +8169,36 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/expect@npm:4.0.18" +"@vitest/expect@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/expect@npm:4.1.2" dependencies: - "@standard-schema/spec": "npm:^1.0.0" + "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.0.18" - "@vitest/utils": "npm:4.0.18" - chai: "npm:^6.2.1" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/123b0aa111682e82ec5289186df18037b1a1768700e468ee0f9879709aaa320cf790463c15c0d8ee10df92b402f4394baf5d27797e604d78e674766d87bcaadc - languageName: node - linkType: hard - -"@vitest/mocker@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/mocker@npm:4.0.18" - dependencies: - "@vitest/spy": "npm:4.0.18" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.21" - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - checksum: 10c0/fb0a257e7e167759d4ad228d53fa7bad2267586459c4a62188f2043dd7163b4b02e1e496dc3c227837f776e7d73d6c4343613e89e7da379d9d30de8260f1ee4b + "@vitest/spy": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/e238c833b5555d31b074545807956d5e874a1ef725525ecc99f1885b71b230b2127d40d8d142a7253666b8565d5806723853e85e0e99265520ec7506fdc5890c languageName: node linkType: hard -"@vitest/mocker@npm:4.1.0": - version: 4.1.0 - resolution: "@vitest/mocker@npm:4.1.0" +"@vitest/mocker@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/mocker@npm:4.1.2" dependencies: - "@vitest/spy": "npm:4.1.0" + "@vitest/spy": "npm:4.1.2" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10c0/f61d3df6461008eb1e62ba465172207b29bd0d9866ff6bc88cd40fc99cd5d215ad89e2894ba6de87068e33f75de903b28a65ccc6074edf3de1fbead6a4a369cc + checksum: 10c0/f23094f3c7e1e5af42e6a468f0815c1ecdcab85cb3a56ab6f3f214a9808a40271467d4352cae972482b9738cc31c62c7312d8b0da227d6ea03d2b3aacb8d385f languageName: node linkType: hard @@ -8360,42 +8211,34 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/pretty-format@npm:4.0.18" - dependencies: - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/0086b8c88eeca896d8e4b98fcdef452c8041a1b63eb9e85d3e0bcc96c8aa76d8e9e0b6990ebb0bb0a697c4ebab347e7735888b24f507dbff2742ddce7723fd94 - languageName: node - linkType: hard - -"@vitest/pretty-format@npm:4.1.0": - version: 4.1.0 - resolution: "@vitest/pretty-format@npm:4.1.0" +"@vitest/pretty-format@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/pretty-format@npm:4.1.2" dependencies: - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/638077f53b5f24ff2d4bc062e69931fa718141db28ddafe435de98a402586b82e8c3cadfc580c0ad233d7f0203aa22d866ac2adca98b83038dbd5423c3d7fe27 + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/6f57519c707e6a3d1ff8630ca87ce78fda9bf7bb33f6e4a0c775a8b510f2a6cee109849e2cdb736b0280681c567bd03e4cff724cbf0962950c9ff81377f0b2bc languageName: node linkType: hard -"@vitest/runner@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/runner@npm:4.0.18" +"@vitest/runner@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/runner@npm:4.1.2" dependencies: - "@vitest/utils": "npm:4.0.18" + "@vitest/utils": "npm:4.1.2" pathe: "npm:^2.0.3" - checksum: 10c0/fdb4afa411475133c05ba266c8092eaf1e56cbd5fb601f92ec6ccb9bab7ca52e06733ee8626599355cba4ee71cb3a8f28c84d3b69dc972e41047edc50229bc01 + checksum: 10c0/35654a87bd27983443adc24d68529d624f7d70e0386176741dc5bcc4188b86a70af2c512405d7e97aa45c16d83e1c8566c1f99c8440430f95557275f18612d21 languageName: node linkType: hard -"@vitest/snapshot@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/snapshot@npm:4.0.18" +"@vitest/snapshot@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/snapshot@npm:4.1.2" dependencies: - "@vitest/pretty-format": "npm:4.0.18" + "@vitest/pretty-format": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/d3bfefa558db9a69a66886ace6575eb96903a5ba59f4d9a5d0fecb4acc2bb8dbb443ef409f5ac1475f2e1add30bd1d71280f98912da35e89c75829df9e84ea43 + checksum: 10c0/6d20e92386937afddbc81344211e554b83a559e20fb10c1deb0b1c3532994dc9fc62d816706ac835bdb737eb1ab02e9c0bc9de80dd8316060e1e0aaa447ba48f languageName: node linkType: hard @@ -8408,34 +8251,27 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/spy@npm:4.0.18" - checksum: 10c0/6de537890b3994fcadb8e8d8ac05942320ae184f071ec395d978a5fba7fa928cbb0c5de85af86a1c165706c466e840de8779eaff8c93450c511c7abaeb9b8a4e - languageName: node - linkType: hard - -"@vitest/spy@npm:4.1.0": - version: 4.1.0 - resolution: "@vitest/spy@npm:4.1.0" - checksum: 10c0/363776bbffda45af76ff500deacb9b1a35ad8b889462c1be9ebe5f29578ce1dd2c4bd7858c8188614a7db9699a5c802b7beb72e5a18ab5130a70326817961446 +"@vitest/spy@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/spy@npm:4.1.2" + checksum: 10c0/2b5888d536d3e2083c5f8939763e6d780c2c03cc60e1ab45f9d04eacf14467acb9724cae1c4778e4c06426d49d04517e190122882953054a4b13fda44780bb14 languageName: node linkType: hard -"@vitest/ui@npm:~4.0.17": - version: 4.0.18 - resolution: "@vitest/ui@npm:4.0.18" +"@vitest/ui@npm:~4.1.2": + version: 4.1.2 + resolution: "@vitest/ui@npm:4.1.2" dependencies: - "@vitest/utils": "npm:4.0.18" + "@vitest/utils": "npm:4.1.2" fflate: "npm:^0.8.2" - flatted: "npm:^3.3.3" + flatted: "npm:^3.4.2" pathe: "npm:^2.0.3" sirv: "npm:^3.0.2" tinyglobby: "npm:^0.2.15" - tinyrainbow: "npm:^3.0.3" + tinyrainbow: "npm:^3.1.0" peerDependencies: - vitest: 4.0.18 - checksum: 10c0/25ed45b52948101ba5c641aeb84126ce695d067984b8430f797498f7e3aeee53c5392e9e14290744a68b5114cfe933e84675b85340204cdbd118079217985157 + vitest: 4.1.2 + checksum: 10c0/9fad46c544f754e29739995fb00edcaa4567ff03d46ce347471f5fa6b905167ba1e11a00e96d2dcfe9c3271e18b54e52294728a04288fcc2b9438a753ff538d2 languageName: node linkType: hard @@ -8450,24 +8286,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/utils@npm:4.0.18" - dependencies: - "@vitest/pretty-format": "npm:4.0.18" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/4a3c43c1421eb90f38576926496f6c80056167ba111e63f77cf118983902673737a1a38880b890d7c06ec0a12475024587344ee502b3c43093781533022f2aeb - languageName: node - linkType: hard - -"@vitest/utils@npm:4.1.0": - version: 4.1.0 - resolution: "@vitest/utils@npm:4.1.0" +"@vitest/utils@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/utils@npm:4.1.2" dependencies: - "@vitest/pretty-format": "npm:4.1.0" + "@vitest/pretty-format": "npm:4.1.2" convert-source-map: "npm:^2.0.0" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/222afbdef4f680a554bb6c3d946a4a879a441ebfb8597295cb7554d295e0e2624f3d4c2920b5767bbb8961a9f8a16756270ffc84032f5ea432cdce080ccab050 + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/d96475e0703b6e5208c6c0f570c1235278cbac3f3913a9aa4203a3e617c9eaca85a184bfd5d13cf366b84754df787ab8bc85242c5e0c63105ee7176c186a2136 languageName: node linkType: hard @@ -9511,14 +9337,14 @@ __metadata: languageName: node linkType: hard -"ast-v8-to-istanbul@npm:^0.3.10": - version: 0.3.10 - resolution: "ast-v8-to-istanbul@npm:0.3.10" +"ast-v8-to-istanbul@npm:^1.0.0": + version: 1.0.0 + resolution: "ast-v8-to-istanbul@npm:1.0.0" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.31" estree-walker: "npm:^3.0.3" - js-tokens: "npm:^9.0.1" - checksum: 10c0/8a7a07c04f8f130b8a5abb76cdb31cce06a8eb4b7d4abbe207bc721132127ae332e857b96aa415ac43ec2c6c9312508210c598f61a7de2d0e3db5615e6b03183 + js-tokens: "npm:^10.0.0" + checksum: 10c0/35e57b754ba63287358094d4f7ae8de2de27286fb4e76a1fbf28b2e67e3b670b59c3f511882473d0fd2cdbaa260062e3cd4f216b724c70032e2b09e5cebbd618 languageName: node linkType: hard @@ -10386,7 +10212,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:>1.9.0, chai@npm:^6.2.1, chai@npm:^6.2.2": +"chai@npm:>1.9.0, chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 @@ -12379,7 +12205,7 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.0.0, es-module-lexer@npm:^1.5.0, es-module-lexer@npm:^1.7.0": +"es-module-lexer@npm:^1.0.0, es-module-lexer@npm:^1.5.0": version: 1.7.0 resolution: "es-module-lexer@npm:1.7.0" checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b @@ -13189,7 +13015,7 @@ __metadata: languageName: node linkType: hard -"expect-type@npm:^1.2.2": +"expect-type@npm:^1.3.0": version: 1.3.0 resolution: "expect-type@npm:1.3.0" checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd @@ -13696,10 +13522,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.7, flatted@npm:^3.2.9, flatted@npm:^3.3.3": - version: 3.3.3 - resolution: "flatted@npm:3.3.3" - checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538 +"flatted@npm:^3.2.7, flatted@npm:^3.2.9, flatted@npm:^3.3.3, flatted@npm:^3.4.2": + version: 3.4.2 + resolution: "flatted@npm:3.4.2" + checksum: 10c0/a65b67aae7172d6cdf63691be7de6c5cd5adbdfdfe2e9da1a09b617c9512ed794037741ee53d93114276bff3f93cd3b0d97d54f9b316e1e4885dde6e9ffdf7ed languageName: node linkType: hard @@ -14060,12 +13886,12 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.13.6": - version: 4.13.6 - resolution: "get-tsconfig@npm:4.13.6" +"get-tsconfig@npm:^4.13.7": + version: 4.13.7 + resolution: "get-tsconfig@npm:4.13.7" dependencies: resolve-pkg-maps: "npm:^1.0.0" - checksum: 10c0/bab6937302f542f97217cbe7cbbdfa7e85a56a377bc7a73e69224c1f0b7c9ae8365918e55752ae8648265903f506c1705f63c0de1d4bab1ec2830fef3e539a1a + checksum: 10c0/1118eb7e9b27bce0b9b6f042e98f0d067e26dfa1ca32bc4b56e892b615b57a5a4af9e6f801c7b0611a4afef2e31c4941be4c6026e0e6a480aaf1ddaf261113d5 languageName: node linkType: hard @@ -16116,6 +15942,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -16123,13 +15956,6 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^9.0.1": - version: 9.0.1 - resolution: "js-tokens@npm:9.0.1" - checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e - languageName: node - linkType: hard - "js-yaml@npm:^3.13.1, js-yaml@npm:^3.6.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -16849,15 +16675,15 @@ __metadata: languageName: node linkType: hard -"liquidjs@npm:^10.21.1, liquidjs@npm:^10.7.0": - version: 10.25.0 - resolution: "liquidjs@npm:10.25.0" +"liquidjs@npm:^10.25.0, liquidjs@npm:^10.7.0": + version: 10.25.2 + resolution: "liquidjs@npm:10.25.2" dependencies: commander: "npm:^10.0.0" bin: liquid: bin/liquid.js liquidjs: bin/liquid.js - checksum: 10c0/0d31f774e6723a929c71454b9c71d3864c44750cc695ae7d831293ebc65df58217bb70a55448260f2b14640b71396493f4a471c9194e82207e42ffcb96d7f5a6 + checksum: 10c0/e032a65abaca217a7b7c594c72ed5e1a3f105488b79e7748e816b396b339d8d7838d9f5e7d2c165f27bc2b49929ac171381fc0b76370803bafb89c2f01dcf27b languageName: node linkType: hard @@ -17199,7 +17025,7 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.3.0, luxon@npm:^3.6.1": +"luxon@npm:^3.3.0, luxon@npm:^3.7.2": version: 3.7.2 resolution: "luxon@npm:3.7.2" checksum: 10c0/ed8f0f637826c08c343a29dd478b00628be93bba6f068417b1d8896b61cb61c6deacbe1df1e057dbd9298334044afa150f9aaabbeb3181418ac8520acfdc2ae2 @@ -17224,14 +17050,14 @@ __metadata: languageName: node linkType: hard -"magicast@npm:^0.5.1": - version: 0.5.1 - resolution: "magicast@npm:0.5.1" +"magicast@npm:^0.5.2": + version: 0.5.2 + resolution: "magicast@npm:0.5.2" dependencies: - "@babel/parser": "npm:^7.28.5" - "@babel/types": "npm:^7.28.5" + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" source-map-js: "npm:^1.2.1" - checksum: 10c0/a00bbf3688b9b3e83c10b3bfe3f106cc2ccbf20c4f2dc1c9020a10556dfe0a6a6605a445ee8e86a6e2b484ec519a657b5e405532684f72678c62e4c0d32f962c + checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 languageName: node linkType: hard @@ -17356,9 +17182,9 @@ __metadata: languageName: node linkType: hard -"markdown-it@npm:^14.1.0": - version: 14.1.0 - resolution: "markdown-it@npm:14.1.0" +"markdown-it@npm:^14.1.0, markdown-it@npm:^14.1.1": + version: 14.1.1 + resolution: "markdown-it@npm:14.1.1" dependencies: argparse: "npm:^2.0.1" entities: "npm:^4.4.0" @@ -17368,7 +17194,7 @@ __metadata: uc.micro: "npm:^2.1.0" bin: markdown-it: bin/markdown-it.mjs - checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4 + checksum: 10c0/c67f2a4c8069a307c78d8c15104bbcb15a2c6b17f4c904364ca218ec2eccf76a397eba1ea05f5ac5de72c4b67fcf115d422d22df0bfb86a09b663f55b9478d4f languageName: node linkType: hard @@ -18250,12 +18076,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.0, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" +"minimatch@npm:^3.0.0, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2, minimatch@npm:^3.1.5": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 languageName: node linkType: hard @@ -18477,7 +18303,7 @@ __metadata: languageName: node linkType: hard -"moo@npm:^0.5.2": +"moo@npm:0.5.2, moo@npm:^0.5.2": version: 0.5.2 resolution: "moo@npm:0.5.2" checksum: 10c0/a9d9ad8198a51fe35d297f6e9fdd718298ca0b39a412e868a0ebd92286379ab4533cfc1f1f34516177f5129988ab25fe598f78e77c84e3bfe0d4a877b56525a8 @@ -19912,7 +19738,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:4.0.3, picomatch@npm:^4.0.2, picomatch@npm:^4.0.3": +"picomatch@npm:4.0.3": version: 4.0.3 resolution: "picomatch@npm:4.0.3" checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 @@ -19926,6 +19752,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 + languageName: node + linkType: hard + "pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -19952,17 +19785,6 @@ __metadata: languageName: node linkType: hard -"pixelmatch@npm:7.1.0": - version: 7.1.0 - resolution: "pixelmatch@npm:7.1.0" - dependencies: - pngjs: "npm:^7.0.0" - bin: - pixelmatch: bin/pixelmatch - checksum: 10c0/ff069f92edaa841ac9b58b0ab74e1afa1f3b5e770eea0218c96bac1da4e752f5f6b79a0f9c4ba6b02afb955d39b8c78bcc3cc884f8122b67a1f2efbbccbe1a73 - languageName: node - linkType: hard - "pkce-challenge@npm:^5.0.0": version: 5.0.1 resolution: "pkce-challenge@npm:5.0.1" @@ -20602,13 +20424,13 @@ __metadata: languageName: node linkType: hard -"posthtml@npm:^0.16.6": - version: 0.16.6 - resolution: "posthtml@npm:0.16.6" +"posthtml@npm:^0.16.6, posthtml@npm:^0.16.7": + version: 0.16.7 + resolution: "posthtml@npm:0.16.7" dependencies: posthtml-parser: "npm:^0.11.0" posthtml-render: "npm:^3.0.0" - checksum: 10c0/0505cb70ece051206ffa932394181372be6390a974fd2f50e4e6fdd5d11e41feffba9a5f5e22809ca42899f79bd489d53ceac1d7ad0d782db9521b578e5b7f5a + checksum: 10c0/7058d1a3e88126f007476588e3e6275894623ecc6d33a7d9eb38587bad2d3eac86070e92d2c30a7480bd704cf863fc3ac14ba722ff5a6e7d10e01d36d8b1b3a8 languageName: node linkType: hard @@ -21703,24 +21525,25 @@ __metadata: languageName: node linkType: hard -"rolldown-plugin-dts@npm:^0.22.5": - version: 0.22.5 - resolution: "rolldown-plugin-dts@npm:0.22.5" +"rolldown-plugin-dts@npm:^0.23.2": + version: 0.23.2 + resolution: "rolldown-plugin-dts@npm:0.23.2" dependencies: - "@babel/generator": "npm:8.0.0-rc.2" - "@babel/helper-validator-identifier": "npm:8.0.0-rc.2" - "@babel/parser": "npm:8.0.0-rc.2" - "@babel/types": "npm:8.0.0-rc.2" + "@babel/generator": "npm:8.0.0-rc.3" + "@babel/helper-validator-identifier": "npm:8.0.0-rc.3" + "@babel/parser": "npm:8.0.0-rc.3" + "@babel/types": "npm:8.0.0-rc.3" ast-kit: "npm:^3.0.0-beta.1" birpc: "npm:^4.0.0" dts-resolver: "npm:^2.1.3" - get-tsconfig: "npm:^4.13.6" + get-tsconfig: "npm:^4.13.7" obug: "npm:^2.1.1" + picomatch: "npm:^4.0.4" peerDependencies: "@ts-macro/tsc": ^0.3.6 - "@typescript/native-preview": ">=7.0.0-dev.20250601.1" - rolldown: ^1.0.0-rc.3 - typescript: ^5.0.0 || ^6.0.0-beta + "@typescript/native-preview": ">=7.0.0-dev.20260325.1" + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 vue-tsc: ~3.2.0 peerDependenciesMeta: "@ts-macro/tsc": @@ -21731,31 +21554,31 @@ __metadata: optional: true vue-tsc: optional: true - checksum: 10c0/43940457abc0576833a50da2fe90d4993f5b4171910875da1d540954ba14aac9b41e72920caa11c1ffee0e439c0bd5b950706036f56c7220c5ad7cf178559236 + checksum: 10c0/083359757ecc238e6234f3f63fddf90ef40c7a6c917206aa4e6bed6a244121b7104e6707af9bcca23179d82ac56866028b5b3602f4f4f8e26af0368878c5989c languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.11": - version: 1.0.0-rc.11 - resolution: "rolldown@npm:1.0.0-rc.11" +"rolldown@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "rolldown@npm:1.0.0-rc.12" dependencies: "@oxc-project/types": "npm:=0.122.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.11" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.11" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.11" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.11" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.11" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.11" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.11" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.11" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.11" - "@rolldown/pluginutils": "npm:1.0.0-rc.11" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.12" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.12" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.12" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.12" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.12" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.12" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.12" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.12" + "@rolldown/pluginutils": "npm:1.0.0-rc.12" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -21789,7 +21612,7 @@ __metadata: optional: true bin: rolldown: bin/cli.mjs - checksum: 10c0/f92457aa26dac614bbaa92079d05c6a4819054468b46b2f46f68bae4bf42dc2c840a4d89be4ffa2a5821a63cd46157fa167a93e1f0b6671f89c16e3da8e2dbf3 + checksum: 10c0/0c4e5e3cdcdddce282cb2d84e1c98d6ad8d4e452d5c1402e498b35ec1060026e552dd783efc9f4ba876d7c0863b5973edc79b6a546f565e9832dc1077ec18c2c languageName: node linkType: hard @@ -21845,64 +21668,6 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.9": - version: 1.0.0-rc.9 - resolution: "rolldown@npm:1.0.0-rc.9" - dependencies: - "@oxc-project/types": "npm:=0.115.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.9" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.9" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.9" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.9" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.9" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.9" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.9" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.9" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.9" - "@rolldown/pluginutils": "npm:1.0.0-rc.9" - dependenciesMeta: - "@rolldown/binding-android-arm64": - optional: true - "@rolldown/binding-darwin-arm64": - optional: true - "@rolldown/binding-darwin-x64": - optional: true - "@rolldown/binding-freebsd-x64": - optional: true - "@rolldown/binding-linux-arm-gnueabihf": - optional: true - "@rolldown/binding-linux-arm64-gnu": - optional: true - "@rolldown/binding-linux-arm64-musl": - optional: true - "@rolldown/binding-linux-ppc64-gnu": - optional: true - "@rolldown/binding-linux-s390x-gnu": - optional: true - "@rolldown/binding-linux-x64-gnu": - optional: true - "@rolldown/binding-linux-x64-musl": - optional: true - "@rolldown/binding-openharmony-arm64": - optional: true - "@rolldown/binding-wasm32-wasi": - optional: true - "@rolldown/binding-win32-arm64-msvc": - optional: true - "@rolldown/binding-win32-x64-msvc": - optional: true - bin: - rolldown: bin/cli.mjs - checksum: 10c0/d19af14dccf569dc25c0c3c2f1142b7a6f7cec291d55bba80cea71099f89c6d634145bb1b6487626ddd41d578f183f7065ed68067e49d2b964ad6242693b0f79 - languageName: node - linkType: hard - "rollup-plugin-dts@npm:^6.4.0": version: 6.4.0 resolution: "rollup-plugin-dts@npm:6.4.0" @@ -22490,7 +22255,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.4, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4": +"semver@npm:7.7.4, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4": version: 7.7.4 resolution: "semver@npm:7.7.4" bin: @@ -23299,10 +23064,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.10.0": - version: 3.10.0 - resolution: "std-env@npm:3.10.0" - checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f +"std-env@npm:^4.0.0-rc.1": + version: 4.0.0 + resolution: "std-env@npm:4.0.0" + checksum: 10c0/63b1716eae27947adde49e21b7225a0f75fb2c3d410273ae9de8333c07c7d5fc7a0628ae4c8af6b4b49b4274ed46c2bf118ed69b64f1261c9d8213d76ed1c16c languageName: node linkType: hard @@ -23339,7 +23104,7 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^10.3.1, storybook@npm:^10.3.3": +"storybook@npm:^10.3.3": version: 10.3.3 resolution: "storybook@npm:10.3.3" dependencies: @@ -24133,7 +23898,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:0.2.15, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:0.2.15, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -24150,10 +23915,10 @@ __metadata: languageName: node linkType: hard -"tinyrainbow@npm:^3.0.3": - version: 3.0.3 - resolution: "tinyrainbow@npm:3.0.3" - checksum: 10c0/1e799d35cd23cabe02e22550985a3051dc88814a979be02dc632a159c393a998628eacfc558e4c746b3006606d54b00bcdea0c39301133956d10a27aa27e988c +"tinyrainbow@npm:^3.1.0": + version: 3.1.0 + resolution: "tinyrainbow@npm:3.1.0" + checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 languageName: node linkType: hard @@ -24343,9 +24108,9 @@ __metadata: languageName: node linkType: hard -"tsdown@npm:^0.21.4": - version: 0.21.4 - resolution: "tsdown@npm:0.21.4" +"tsdown@npm:^0.21.7": + version: 0.21.7 + resolution: "tsdown@npm:0.21.7" dependencies: ansis: "npm:^4.2.0" cac: "npm:^7.0.0" @@ -24354,22 +24119,22 @@ __metadata: hookable: "npm:^6.1.0" import-without-cache: "npm:^0.2.5" obug: "npm:^2.1.1" - picomatch: "npm:^4.0.3" - rolldown: "npm:1.0.0-rc.9" - rolldown-plugin-dts: "npm:^0.22.5" + picomatch: "npm:^4.0.4" + rolldown: "npm:1.0.0-rc.12" + rolldown-plugin-dts: "npm:^0.23.2" semver: "npm:^7.7.4" tinyexec: "npm:^1.0.4" tinyglobby: "npm:^0.2.15" tree-kill: "npm:^1.2.2" unconfig-core: "npm:^7.5.0" - unrun: "npm:^0.2.32" + unrun: "npm:^0.2.34" peerDependencies: "@arethetypeswrong/core": ^0.18.1 - "@tsdown/css": 0.21.4 - "@tsdown/exe": 0.21.4 + "@tsdown/css": 0.21.7 + "@tsdown/exe": 0.21.7 "@vitejs/devtools": "*" publint: ^0.3.0 - typescript: ^5.0.0 + typescript: ^5.0.0 || ^6.0.0 unplugin-unused: ^0.5.0 peerDependenciesMeta: "@arethetypeswrong/core": @@ -24388,7 +24153,7 @@ __metadata: optional: true bin: tsdown: dist/run.mjs - checksum: 10c0/a2f096ecbba422af569c0e5a7ef57a7aed8b6a3e03e3af21e69df0aadd76148097e6015bb16cc839ff73589fb959fc9f0d997a9453a11269b4c4cd3ce7eec833 + checksum: 10c0/dfb25e50fd64e93bafd2d148eab3bbc6bf2dcdfa7aed47aa875ed12a16202d7b8076e7b9498ef3624bc9dcbc618632bce87a09e70a8afab914079ffe12012cec languageName: node linkType: hard @@ -24899,11 +24664,11 @@ __metadata: languageName: node linkType: hard -"unrun@npm:^0.2.32": - version: 0.2.32 - resolution: "unrun@npm:0.2.32" +"unrun@npm:^0.2.34": + version: 0.2.34 + resolution: "unrun@npm:0.2.34" dependencies: - rolldown: "npm:1.0.0-rc.9" + rolldown: "npm:1.0.0-rc.12" peerDependencies: synckit: ^0.11.11 peerDependenciesMeta: @@ -24911,7 +24676,7 @@ __metadata: optional: true bin: unrun: dist/cli.mjs - checksum: 10c0/c3916b6e82e588aa226b19006ee7b56dfda4f99b36a4e7589af43f5322799a8126331e554dd169667ff2d6929042574ac073276ff200a5936f437c4d8e6edeaa + checksum: 10c0/e6c3c9e56598e4f401c3b14afa2a6ef0cc357975390d2246f4f99b4564e6a7ae1613274f57c6c8f77013fc314720e2b8afeedcb8e0fb1bc07a0963eef964e01c languageName: node linkType: hard @@ -25114,7 +24879,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:7.3.1, vite@npm:^6.0.0 || ^7.0.0": +"vite@npm:7.3.1": version: 7.3.1 resolution: "vite@npm:7.3.1" dependencies: @@ -25169,15 +24934,15 @@ __metadata: languageName: node linkType: hard -"vite@npm:^8.0.2": - version: 8.0.2 - resolution: "vite@npm:8.0.2" +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.3": + version: 8.0.3 + resolution: "vite@npm:8.0.3" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" - picomatch: "npm:^4.0.3" + picomatch: "npm:^4.0.4" postcss: "npm:^8.5.8" - rolldown: "npm:1.0.0-rc.11" + rolldown: "npm:1.0.0-rc.12" tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 @@ -25222,44 +24987,45 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/b271a3c3f8100bab45ee16583cb046aa028f943205b56065b09d3f1851ed8e7068fc6a76e9dc01beca805e8bb1e53f229c4c1c623be87ef1acb00fc002a29cf6 + checksum: 10c0/bed9520358080393a02fe22565b3309b4b3b8f916afe4c97577528f3efb05c1bf4b29f7b552179bc5b3938629e50fbd316231727457411dbc96648fa5c9d14bf languageName: node linkType: hard -"vitest@npm:^4.0.17, vitest@npm:~4.0.17": - version: 4.0.18 - resolution: "vitest@npm:4.0.18" - dependencies: - "@vitest/expect": "npm:4.0.18" - "@vitest/mocker": "npm:4.0.18" - "@vitest/pretty-format": "npm:4.0.18" - "@vitest/runner": "npm:4.0.18" - "@vitest/snapshot": "npm:4.0.18" - "@vitest/spy": "npm:4.0.18" - "@vitest/utils": "npm:4.0.18" - es-module-lexer: "npm:^1.7.0" - expect-type: "npm:^1.2.2" +"vitest@npm:^4.1.2, vitest@npm:~4.1.2": + version: 4.1.2 + resolution: "vitest@npm:4.1.2" + dependencies: + "@vitest/expect": "npm:4.1.2" + "@vitest/mocker": "npm:4.1.2" + "@vitest/pretty-format": "npm:4.1.2" + "@vitest/runner": "npm:4.1.2" + "@vitest/snapshot": "npm:4.1.2" + "@vitest/spy": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" obug: "npm:^2.1.1" pathe: "npm:^2.0.3" picomatch: "npm:^4.0.3" - std-env: "npm:^3.10.0" + std-env: "npm:^4.0.0-rc.1" tinybench: "npm:^2.9.0" tinyexec: "npm:^1.0.2" tinyglobby: "npm:^0.2.15" - tinyrainbow: "npm:^3.0.3" - vite: "npm:^6.0.0 || ^7.0.0" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.18 - "@vitest/browser-preview": 4.0.18 - "@vitest/browser-webdriverio": 4.0.18 - "@vitest/ui": 4.0.18 + "@vitest/browser-playwright": 4.1.2 + "@vitest/browser-preview": 4.1.2 + "@vitest/browser-webdriverio": 4.1.2 + "@vitest/ui": 4.1.2 happy-dom: "*" jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: "@edge-runtime/vm": optional: true @@ -25279,9 +25045,11 @@ __metadata: optional: true jsdom: optional: true + vite: + optional: false bin: vitest: vitest.mjs - checksum: 10c0/b913cd32032c95f29ff08c931f4b4c6fd6d2da498908d6770952c561a1b8d75c62499a1f04cadf82fb89cc0f9a33f29fb5dfdb899f6dbb27686a9d91571be5fa + checksum: 10c0/061fdd0319ba533c926b139b9377a7dbf91e63d815d86fe318a207bd19842b74ca6f6402ea61b26ed9d2924306bdb4d0b13f69c29e2a2a89b3b67602bcccb54c languageName: node linkType: hard @@ -25801,7 +25569,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.1, ws@npm:^8.18.3, ws@npm:^8.19.0": +"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.1, ws@npm:^8.19.0": version: 8.19.0 resolution: "ws@npm:8.19.0" peerDependencies: From 4bc88ab16d1c28f63d75eba8f6dc3e25d7046181 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 30 Mar 2026 19:44:28 +0200 Subject: [PATCH 21/72] =?UTF-8?q?=F0=9F=9A=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 10 +- .../src/copy-button/copy-button.css | 3 + .../src/copy-button/copy-button.spec.ts | 135 +++++++ .../src/copy-button/copy-button.stories.ts | 50 +++ .../components/src/copy-button/copy-button.ts | 62 ++++ docs/components/src/example/example.css | 119 ++++++ docs/components/src/example/example.spec.ts | 340 ++++++++++++++++++ .../components/src/example/example.stories.ts | 135 +++++++ docs/components/src/example/example.ts | 204 +++++++++++ .../src/install-info/install-info.css | 47 +++ .../src/install-info/install-info.spec.ts | 106 ++++++ .../src/install-info/install-info.stories.ts | 33 ++ .../src/install-info/install-info.ts | 31 ++ docs/components/tsdown.config.ts | 3 + docs/website/src/js/main.js | 2 + packages/components/button/src/button.ts | 9 +- yarn.lock | 16 + 17 files changed, 1298 insertions(+), 7 deletions(-) create mode 100644 docs/components/src/copy-button/copy-button.css create mode 100644 docs/components/src/copy-button/copy-button.spec.ts create mode 100644 docs/components/src/copy-button/copy-button.stories.ts create mode 100644 docs/components/src/copy-button/copy-button.ts create mode 100644 docs/components/src/example/example.css create mode 100644 docs/components/src/example/example.spec.ts create mode 100644 docs/components/src/example/example.stories.ts create mode 100644 docs/components/src/example/example.ts create mode 100644 docs/components/src/install-info/install-info.css create mode 100644 docs/components/src/install-info/install-info.spec.ts create mode 100644 docs/components/src/install-info/install-info.stories.ts create mode 100644 docs/components/src/install-info/install-info.ts diff --git a/docs/components/package.json b/docs/components/package.json index 33cad7c5a6..ab54ddcae4 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -10,6 +10,9 @@ "url": "https://github.com/sl-design-system/components.git" }, "exports": { + "./copy-button/copy-button": "./dist/copy-button/copy-button.js", + "./example/example": "./dist/example/example.js", + "./install-info/install-info": "./dist/install-info/install-info.js", "./page-toc/page-toc": "./dist/page-toc/page-toc.js", "./search/search": "./dist/search/search.js", "./sidebar/sidebar": "./dist/sidebar/sidebar.js", @@ -41,7 +44,8 @@ "@open-wc/scoped-elements": "^3.0.6", "@sl-design-system/icon": "workspace:^", "@sl-design-system/search-field": "workspace:^", - "@sl-design-system/switch": "workspace:^" + "@sl-design-system/switch": "workspace:^", + "prismjs": "^1.30.0" }, "devDependencies": { "@roenlie/vite-plugin-import-css-sheet": "^0.0.7", @@ -51,6 +55,7 @@ "@storybook/web-components": "^10.3.3", "@storybook/web-components-vite": "^10.3.3", "@tsdown/css": "^0.21.7", + "@types/prismjs": "^1.26.6", "@typescript/native-preview": "^7.0.0-dev.20260315.1", "lit": "^3.3.2", "sass-embedded": "^1.98.0", @@ -63,6 +68,9 @@ "lit": "^3.3.2" }, "inlinedDependencies": { + "@floating-ui/core": "1.7.5", + "@floating-ui/dom": "1.7.6", + "@floating-ui/utils": "0.2.11", "@fortawesome/free-brands-svg-icons": "7.2.0", "@fortawesome/pro-regular-svg-icons": "7.2.0", "@fortawesome/pro-solid-svg-icons": "7.2.0" diff --git a/docs/components/src/copy-button/copy-button.css b/docs/components/src/copy-button/copy-button.css new file mode 100644 index 0000000000..e7ddcab66a --- /dev/null +++ b/docs/components/src/copy-button/copy-button.css @@ -0,0 +1,3 @@ +:host { + cursor: copy; +} diff --git a/docs/components/src/copy-button/copy-button.spec.ts b/docs/components/src/copy-button/copy-button.spec.ts new file mode 100644 index 0000000000..d64f4592a9 --- /dev/null +++ b/docs/components/src/copy-button/copy-button.spec.ts @@ -0,0 +1,135 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { CopyButton } from './copy-button.js'; + +try { + customElements.define('doc-copy-button', CopyButton); +} catch { + /* empty */ +} + +describe('doc-copy-button', () => { + let el: CopyButton; + + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(CopyButton); + }); + + it('should have a copy icon', () => { + const icon = el.renderRoot.querySelector('sl-icon'); + + expect(icon).to.exist; + expect(icon).to.have.attribute('name', 'far-copy'); + }); + + it('should have ghost fill by default', () => { + expect(el.fill).to.equal('ghost'); + }); + + it('should have the button role', () => { + expect(el).to.have.attribute('role', 'button'); + }); + + it('should have cursor copy style', () => { + expect(getComputedStyle(el).cursor).to.equal('copy'); + }); + + it('should not have a target by default', () => { + expect(el.target).to.be.undefined; + }); + }); + + describe('target', () => { + beforeEach(async () => { + // Create a target element in the document + const target = document.createElement('div'); + target.id = 'copy-target'; + target.textContent = 'Text to copy'; + document.body.appendChild(target); + + el = await fixture(html``); + }); + + afterEach(() => { + document.getElementById('copy-target')?.remove(); + }); + + it('should have the target property set', () => { + expect(el.target).to.equal('copy-target'); + }); + + it('should copy the target text to the clipboard on click', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + el.click(); + await el.updateComplete; + + expect(writeText).toHaveBeenCalledWith('Text to copy'); + + writeText.mockRestore(); + }); + + it('should trim the target text before copying', async () => { + const target = document.getElementById('copy-target')!; + target.textContent = ' padded text '; + + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + el.click(); + await el.updateComplete; + + expect(writeText).toHaveBeenCalledWith('padded text'); + + writeText.mockRestore(); + }); + }); + + describe('no target', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should not attempt to copy when no target is set', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + el.click(); + await el.updateComplete; + + expect(writeText).not.toHaveBeenCalled(); + + writeText.mockRestore(); + }); + }); + + describe('nonexistent target', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should not attempt to copy when the target element does not exist', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + el.click(); + await el.updateComplete; + + expect(writeText).not.toHaveBeenCalled(); + + writeText.mockRestore(); + }); + }); + + describe('fill override', () => { + it('should allow overriding the fill', async () => { + el = await fixture(html``); + + expect(el.fill).to.equal('outline'); + }); + }); +}); diff --git a/docs/components/src/copy-button/copy-button.stories.ts b/docs/components/src/copy-button/copy-button.stories.ts new file mode 100644 index 0000000000..10acfb0a5a --- /dev/null +++ b/docs/components/src/copy-button/copy-button.stories.ts @@ -0,0 +1,50 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { CopyButton } from './copy-button.js'; + +type Props = Pick; +type Story = StoryObj; + +try { + customElements.define('doc-copy-button', CopyButton); +} catch { + /* empty */ +} + +export default { + title: 'Copy Button', + args: { + target: 'copy-target' + }, + argTypes: { + target: { + control: 'text' + }, + fill: { + control: 'select', + options: ['solid', 'outline', 'link', 'ghost'] + } + }, + render: ({ target, fill }) => html` +

This text will be copied to the clipboard.

+ + ` +} satisfies Meta; + +export const Basic: Story = {}; + +export const Outline: Story = { + args: { + fill: 'outline' + } +}; + +export const WithDifferentTarget: Story = { + render: ({ target }) => html` +
const foo = 'bar';
+ + `, + args: { + target: 'code-snippet' + } +}; diff --git a/docs/components/src/copy-button/copy-button.ts b/docs/components/src/copy-button/copy-button.ts new file mode 100644 index 0000000000..850a750cc7 --- /dev/null +++ b/docs/components/src/copy-button/copy-button.ts @@ -0,0 +1,62 @@ +import { faCopy } from '@fortawesome/pro-regular-svg-icons'; +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Button } from '@sl-design-system/button'; +import { Icon } from '@sl-design-system/icon'; +import { Tooltip } from '@sl-design-system/tooltip'; +import { type CSSResultGroup, type TemplateResult, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import styles from './copy-button.css' with { type: 'css' }; + +Icon.register(faCopy); + +export class CopyButton extends ScopedElementsMixin(Button) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'sl-icon': Icon, + 'sl-tooltip': Tooltip + }; + } + + /** @internal */ + static override styles: CSSResultGroup = [Button.styles, styles]; + + /** Cleanup function for the lazy tooltip. */ + #cleanupTooltip?: () => void; + + /** The DOM id of the element whose text content should be copied. */ + @property() target?: string; + + override connectedCallback(): void { + super.connectedCallback(); + + this.fill ??= 'ghost'; + this.addEventListener('click', this.#onClick); + + this.#cleanupTooltip = Tooltip.lazy(this, tooltip => { + tooltip.textContent = 'Copy'; + }); + } + + override disconnectedCallback(): void { + this.removeEventListener('click', this.#onClick); + this.#cleanupTooltip?.(); + + super.disconnectedCallback(); + } + + override render(): TemplateResult { + return html``; + } + + #onClick = async (): Promise => { + if (!this.target) { + return; + } + + const el = document.getElementById(this.target); + if (el) { + await navigator.clipboard.writeText(el.textContent?.trim() ?? ''); + } + }; +} diff --git a/docs/components/src/example/example.css b/docs/components/src/example/example.css new file mode 100644 index 0000000000..1cdcf37271 --- /dev/null +++ b/docs/components/src/example/example.css @@ -0,0 +1,119 @@ +:host { + display: block; +} + +.preview { + border: 1px solid var(--sl-color-border-plain); + border-radius: var(--sl-size-050); + padding: var(--sl-size-200); +} + +.tabs { + display: flex; + gap: var(--sl-size-050); + margin-block-start: var(--sl-size-100); +} + +.panel { + background: var(--sl-color-surface-container-highest); + border-radius: var(--sl-size-050); + display: flex; + margin-block-start: var(--sl-size-100); + position: relative; +} + +pre { + flex: 1; + font: var(--sl-text-body-sm); + font-family: + ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', + 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; + margin: 0; + overflow-x: auto; + padding: var(--sl-size-150); + white-space: pre; +} + +code { + font: inherit; +} + +.copy { + background: none; + border: 1px solid var(--sl-color-border-plain); + border-radius: var(--sl-size-050); + color: var(--sl-color-foreground-plain); + cursor: pointer; + font: var(--sl-text-body-sm); + inset-block-start: var(--sl-size-100); + inset-inline-end: var(--sl-size-100); + padding: var(--sl-size-025) var(--sl-size-100); + position: absolute; + + &:hover { + background: var(--sl-color-surface-container-low); + } +} + +/* stylelint-disable color-no-hex -- Prism syntax highlighting tokens (Okaidia-inspired, scoped to shadow DOM) */ +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #8292a2; +} + +.token.punctuation { + color: #f8f8f2; +} + +.token.namespace { + opacity: 0.7; +} + +.token.property, +.token.tag, +.token.constant, +.token.symbol, +.token.deleted { + color: #f92672; +} + +.token.boolean, +.token.number { + color: #ae81ff; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #a6e22e; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + color: #f8f8f2; +} + +.token.atrule, +.token.attr-value, +.token.function, +.token.class-name { + color: #e6db74; +} + +.token.keyword { + color: #66d9ef; +} + +.token.regex, +.token.important { + color: #fd971f; +} diff --git a/docs/components/src/example/example.spec.ts b/docs/components/src/example/example.spec.ts new file mode 100644 index 0000000000..8e42d83ef0 --- /dev/null +++ b/docs/components/src/example/example.spec.ts @@ -0,0 +1,340 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { type LitElement, html } from 'lit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// Use dynamic import from dist to avoid CSS module resolution issues in browser tests +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const { DocExample: DocExampleClass } = await import('@sl-design-system/doc-components/example/example'); + +try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + customElements.define('doc-example', DocExampleClass); +} catch { + /* empty */ +} + +describe('doc-example', () => { + describe('all languages', () => { + let el: LitElement & { selected?: string }; + + beforeEach(async () => { + el = await fixture(html` + + + + + + `); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(DocExampleClass); + }); + + it('should have html selected by default', () => { + expect(el.selected).to.equal('html'); + }); + + it('should render a preview area', () => { + const preview = el.renderRoot.querySelector('.preview'); + + expect(preview).to.exist; + }); + + it('should render HTML content in the preview', () => { + const preview = el.renderRoot.querySelector('.preview div'); + + expect(preview?.innerHTML).to.contain('

Hello

'); + }); + + it('should have a tablist', () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]'); + + expect(tablist).to.exist; + }); + + it('should have an aria-label on the tablist', () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]'); + + expect(tablist).to.have.attribute('aria-label', 'Code language'); + }); + + it('should have three tabs', () => { + const tabs = el.renderRoot.querySelectorAll('.tabs > *'); + + expect(tabs).to.have.length(3); + }); + + it('should label tabs HTML, CSS, TypeScript', () => { + const tabs = el.renderRoot.querySelectorAll('.tabs > *'); + + expect(tabs[0]).to.have.trimmed.text('HTML'); + expect(tabs[1]).to.have.trimmed.text('CSS'); + expect(tabs[2]).to.have.trimmed.text('TypeScript'); + }); + + it('should mark the selected tab as aria-selected', () => { + const tab = el.renderRoot.querySelector('#tab-html'); + + expect(tab).to.have.attribute('aria-selected', 'true'); + }); + + it('should mark non-selected tabs as not aria-selected', () => { + const tab = el.renderRoot.querySelector('#tab-css'); + + expect(tab).to.have.attribute('aria-selected', 'false'); + }); + + it('should set tabindex 0 on the selected tab', () => { + const tab = el.renderRoot.querySelector('#tab-html'); + + expect(tab).to.have.attribute('tabindex', '0'); + }); + + it('should set tabindex -1 on non-selected tabs', () => { + const tab = el.renderRoot.querySelector('#tab-css'); + + expect(tab).to.have.attribute('tabindex', '-1'); + }); + + it('should have a code panel', () => { + const panel = el.renderRoot.querySelector('[role="tabpanel"]'); + + expect(panel).to.exist; + }); + + it('should link the panel to the selected tab', () => { + const panel = el.renderRoot.querySelector('[role="tabpanel"]'); + + expect(panel).to.have.attribute('aria-labelledby', 'tab-html'); + }); + + it('should contain syntax highlighted code', () => { + const code = el.renderRoot.querySelector('code'); + + expect(code).to.exist; + expect(code?.querySelector('.token')).to.exist; + }); + + it('should have a copy button', () => { + const button = el.renderRoot.querySelector('.copy'); + + expect(button).to.exist; + expect(button).to.have.attribute('aria-label', 'Copy to clipboard'); + }); + + describe('tab switching', () => { + it('should switch to CSS tab on click', async () => { + const tab = el.renderRoot.querySelector('#tab-css'); + tab?.click(); + await el.updateComplete; + + expect(el.selected).to.equal('css'); + + const panel = el.renderRoot.querySelector('[role="tabpanel"]'); + + expect(panel).to.have.attribute('id', 'panel-css'); + }); + + it('should switch to TypeScript tab on click', async () => { + const tab = el.renderRoot.querySelector('#tab-ts'); + tab?.click(); + await el.updateComplete; + + expect(el.selected).to.equal('ts'); + }); + }); + + describe('keyboard navigation', () => { + it('should move to the next tab on ArrowRight', async () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]')!; + + tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); + await el.updateComplete; + + expect(el.selected).to.equal('css'); + }); + + it('should wrap around on ArrowRight from the last tab', async () => { + el.selected = 'ts'; + await el.updateComplete; + + const tablist = el.renderRoot.querySelector('[role="tablist"]')!; + + tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); + await el.updateComplete; + + expect(el.selected).to.equal('html'); + }); + + it('should move to the previous tab on ArrowLeft', async () => { + el.selected = 'css'; + await el.updateComplete; + + const tablist = el.renderRoot.querySelector('[role="tablist"]')!; + + tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); + await el.updateComplete; + + expect(el.selected).to.equal('html'); + }); + + it('should wrap around on ArrowLeft from the first tab', async () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]')!; + + tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); + await el.updateComplete; + + expect(el.selected).to.equal('ts'); + }); + + it('should go to the first tab on Home', async () => { + el.selected = 'ts'; + await el.updateComplete; + + const tablist = el.renderRoot.querySelector('[role="tablist"]')!; + + tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true })); + await el.updateComplete; + + expect(el.selected).to.equal('html'); + }); + + it('should go to the last tab on End', async () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]')!; + + tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true })); + await el.updateComplete; + + expect(el.selected).to.equal('ts'); + }); + }); + + describe('copy', () => { + it('should copy raw code to the clipboard', () => { + const writeText = vi.fn().mockResolvedValue(undefined); + + vi.stubGlobal('navigator', { clipboard: { writeText } }); + + const button = el.renderRoot.querySelector('.copy')!; + button.click(); + + expect(writeText).toHaveBeenCalledWith('

Hello

'); + + vi.unstubAllGlobals(); + }); + }); + }); + + describe('html only', () => { + let el: LitElement & { selected?: string }; + + beforeEach(async () => { + el = await fixture(html` + + + + `); + }); + + it('should render a preview', () => { + const preview = el.renderRoot.querySelector('.preview'); + + expect(preview).to.exist; + }); + + it('should not render a tablist', () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]'); + + expect(tablist).to.not.exist; + }); + + it('should render a code panel without aria-labelledby', () => { + const panel = el.renderRoot.querySelector('[role="tabpanel"]'); + + expect(panel).to.exist; + expect(panel).to.not.have.attribute('aria-labelledby'); + }); + }); + + describe('css only', () => { + let el: LitElement & { selected?: string }; + + beforeEach(async () => { + el = await fixture(html` + + + + `); + }); + + it('should not render a preview', () => { + const preview = el.renderRoot.querySelector('.preview'); + + expect(preview).to.not.exist; + }); + + it('should not render a tablist', () => { + const tablist = el.renderRoot.querySelector('[role="tablist"]'); + + expect(tablist).to.not.exist; + }); + + it('should render a code panel', () => { + const panel = el.renderRoot.querySelector('[role="tabpanel"]'); + + expect(panel).to.exist; + }); + }); + + describe('html and css', () => { + let el: LitElement & { selected?: string }; + + beforeEach(async () => { + el = await fixture(html` + + + + + `); + }); + + it('should render a preview', () => { + const preview = el.renderRoot.querySelector('.preview'); + + expect(preview).to.exist; + }); + + it('should have two tabs', () => { + const tabs = el.renderRoot.querySelectorAll('.tabs > *'); + + expect(tabs).to.have.length(2); + }); + + it('should apply CSS to the preview via adoptedStyleSheets', () => { + const sheets = el.shadowRoot!.adoptedStyleSheets; + + expect(sheets.length).to.be.greaterThan(1); + }); + }); + + describe('dedenting', () => { + let el: LitElement & { selected?: string }; + + beforeEach(async () => { + el = await fixture(html` + + + + `); + }); + + it('should strip common leading whitespace', () => { + const code = el.renderRoot.querySelector('code'); + + expect(code?.textContent).to.contain('const a = 1;'); + expect(code?.textContent).to.not.match(/^\s{12}/); + }); + }); +}); diff --git a/docs/components/src/example/example.stories.ts b/docs/components/src/example/example.stories.ts new file mode 100644 index 0000000000..92e0e8f6a9 --- /dev/null +++ b/docs/components/src/example/example.stories.ts @@ -0,0 +1,135 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { DocExample } from './example.js'; + +type Story = StoryObj; + +try { + customElements.define('doc-example', DocExample); +} catch { + /* empty */ +} + +/** Helper to create a template element with a data-lang attribute. */ +function langTemplate(lang: string, code: string): HTMLTemplateElement { + const template = document.createElement('template'); + + template.dataset['lang'] = lang; + template.innerHTML = code; + + return template; +} + +const exampleHTML = '', + exampleCSS = `.primary { + background: #007bff; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + font-size: 16px; + padding: 8px 16px; +} + +.primary:hover { + background: #0056b3; +}`, + exampleTS = `const button = document.querySelector('.primary'); + +button?.addEventListener('click', () => { + console.log('Button clicked!'); +});`, + cardHTML = `
+

Card title

+

Card content goes here.

+
`, + cardCSS = `.card { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 16px; +} + +.card h3 { + margin-block-start: 0; +}`, + onlyCSS = `:root { + --primary: #007bff; + --secondary: #6c757d; + --font-family: system-ui, sans-serif; +} + +body { + font-family: var(--font-family); + margin: 0; + padding: 16px; +}`, + onlyTS = `interface User { + id: number; + name: string; + email: string; +} + +async function fetchUser(id: number): Promise { + const response = await fetch(\`/api/users/\${id}\`); + + if (!response.ok) { + throw new Error(\`Failed to fetch user \${id}\`); + } + + return response.json() as Promise; +}`, + paragraphHTML = `

Hello world

+

This is a paragraph with bold and italic text.

`; + +export default { + title: 'Example' +} satisfies Meta; + +export const Basic: Story = { + render: () => { + const el = document.createElement('doc-example'); + + el.append(langTemplate('html', exampleHTML), langTemplate('css', exampleCSS), langTemplate('ts', exampleTS)); + + return el; + } +}; + +export const HTMLOnly: Story = { + render: () => { + const el = document.createElement('doc-example'); + + el.append(langTemplate('html', paragraphHTML)); + + return el; + } +}; + +export const HTMLAndCSS: Story = { + render: () => { + const el = document.createElement('doc-example'); + + el.append(langTemplate('html', cardHTML), langTemplate('css', cardCSS)); + + return el; + } +}; + +export const CSSOnly: Story = { + render: () => { + const el = document.createElement('doc-example'); + + el.append(langTemplate('css', onlyCSS)); + + return el; + } +}; + +export const TypeScriptOnly: Story = { + render: () => { + const el = document.createElement('doc-example'); + + el.append(langTemplate('ts', onlyTS)); + + return el; + } +}; diff --git a/docs/components/src/example/example.ts b/docs/components/src/example/example.ts new file mode 100644 index 0000000000..7de79ec638 --- /dev/null +++ b/docs/components/src/example/example.ts @@ -0,0 +1,204 @@ +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Button } from '@sl-design-system/button'; +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; +import { state } from 'lit/decorators.js'; +import Prism from 'prismjs'; +import 'prismjs/components/prism-css.js'; +import 'prismjs/components/prism-typescript.js'; +import styles from './example.css' with { type: 'css' }; + +type Language = 'html' | 'css' | 'ts'; + +const LANGUAGE_LABELS: Record = { + html: 'HTML', + css: 'CSS', + ts: 'TypeScript' +}; + +const PRISM_GRAMMARS: Record = { + html: Prism.languages['markup'], + css: Prism.languages['css'], + ts: Prism.languages['typescript'] +}; + +const PRISM_LANGUAGE_IDS: Record = { + html: 'markup', + css: 'css', + ts: 'typescript' +}; + +/** Dedent code by removing common leading whitespace from all non-empty lines. */ +function dedent(code: string): string { + const lines = code.split('\n'); + + // Remove leading/trailing blank lines + while (lines.length && lines[0].trim() === '') { + lines.shift(); + } + while (lines.length && lines[lines.length - 1].trim() === '') { + lines.pop(); + } + + const indents = lines.filter(l => l.trim().length > 0).map(l => l.match(/^(\s*)/)?.[1].length ?? 0), + minIndent = indents.length ? Math.min(...indents) : 0; + + return minIndent > 0 ? lines.map(l => l.slice(minIndent)).join('\n') : lines.join('\n'); +} + +export class DocExample extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'sl-button': Button + }; + } + + /** @internal */ + static styles: CSSResultGroup = styles; + + /** The available code snippets keyed by language. */ + #snippets = new Map(); + + /** The ordered list of available languages. */ + #languages: Language[] = []; + + /** The currently selected tab. */ + @state() selected?: Language; + + /** A dynamic stylesheet for user-provided preview CSS. */ + #previewSheet = new CSSStyleSheet(); + + override connectedCallback(): void { + super.connectedCallback(); + this.#parseTemplates(); + this.#applyPreviewStyles(); + } + + override render(): TemplateResult { + const hasPreview = this.#snippets.has('html'), + hasTabs = this.#languages.length > 1, + code = this.selected ? this.#snippets.get(this.selected) : undefined; + + return html` + ${hasPreview ? this.#renderPreview() : nothing} ${hasTabs ? this.#renderTabs() : nothing} + ${code !== undefined && this.selected + ? html` +
+
+ +
+ ` + : nothing} + `; + } + + #renderPreview(): TemplateResult { + const htmlCode = this.#snippets.get('html') ?? ''; + + return html` +
+
+
+ `; + } + + #renderTabs(): TemplateResult { + return html` +
+ ${this.#languages.map( + lang => html` + this.#select(lang)} + > + ${LANGUAGE_LABELS[lang]} + + ` + )} +
+ `; + } + + #parseTemplates(): void { + const templates = this.querySelectorAll('template[data-lang]'); + + for (const template of templates) { + const lang = template.dataset['lang'] as Language; + + if (!LANGUAGE_LABELS[lang]) { + continue; + } + + const raw = lang === 'html' ? template.innerHTML : (template.content.textContent ?? ''); + + this.#snippets.set(lang, dedent(raw)); + } + + // Maintain a consistent order: html, css, ts + this.#languages = (['html', 'css', 'ts'] as Language[]).filter(l => this.#snippets.has(l)); + this.selected = this.#languages[0]; + } + + #applyPreviewStyles(): void { + const cssCode = this.#snippets.get('css'); + + if (cssCode) { + this.#previewSheet.replaceSync(`.preview { ${cssCode} }`); + this.shadowRoot!.adoptedStyleSheets = [...this.shadowRoot!.adoptedStyleSheets, this.#previewSheet]; + } + } + + #highlight(code: string, lang: Language): string { + return Prism.highlight(code, PRISM_GRAMMARS[lang], PRISM_LANGUAGE_IDS[lang]); + } + + #select(lang: Language): void { + this.selected = lang; + + void this.updateComplete.then(() => { + this.renderRoot.querySelector(`#tab-${lang}`)?.focus(); + }); + } + + #onTabKeydown(event: KeyboardEvent): void { + const index = this.#languages.indexOf(this.selected!); + let next: number | undefined; + + switch (event.key) { + case 'ArrowRight': + next = (index + 1) % this.#languages.length; + break; + case 'ArrowLeft': + next = (index - 1 + this.#languages.length) % this.#languages.length; + break; + case 'Home': + next = 0; + break; + case 'End': + next = this.#languages.length - 1; + break; + } + + if (next !== undefined) { + event.preventDefault(); + this.#select(this.#languages[next]); + } + } + + async #copy(code: string): Promise { + await navigator.clipboard.writeText(code); + } +} diff --git a/docs/components/src/install-info/install-info.css b/docs/components/src/install-info/install-info.css new file mode 100644 index 0000000000..e83f0ad160 --- /dev/null +++ b/docs/components/src/install-info/install-info.css @@ -0,0 +1,47 @@ +:host { + display: block; +} + +.panel { + align-items: center; + background: var(--sl-elevation-surface-raised-sunken); + border: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + border-radius: var(--sl-size-borderRadius-lg); + display: flex; + gap: var(--sl-size-100); + padding: var(--sl-size-150) var(--sl-size-200); +} + +code { + flex: 1; + font-family: + ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', + 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; + white-space: pre; +} + +.command { + color: var(--sl-color-foreground-accent-teal-plain); +} + +.scope { + color: var(--sl-color-foreground-accent-grey-subtlest); +} + +.package { + color: var(--sl-color-foreground-accent-blue-plain); +} + +.copy { + background: none; + border: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + border-radius: var(--sl-size-borderRadius-default); + color: var(--sl-color-foreground-plain); + cursor: pointer; + font: var(--sl-text-new-body-sm); + padding: var(--sl-size-025) var(--sl-size-100); + + &:hover { + background: var(--sl-elevation-surface-raised-alternative); + } +} diff --git a/docs/components/src/install-info/install-info.spec.ts b/docs/components/src/install-info/install-info.spec.ts new file mode 100644 index 0000000000..244f1aebc2 --- /dev/null +++ b/docs/components/src/install-info/install-info.spec.ts @@ -0,0 +1,106 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// Use dynamic import from dist to avoid CSS module resolution issues in browser tests +const { InstallInfo: InstallInfoClass } = await import('@sl-design-system/doc-components/install-info/install-info'); + +try { + customElements.define('doc-install-info', InstallInfoClass); +} catch { + /* empty */ +} + +describe('doc-install-info', () => { + let el: InstanceType; + + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(InstallInfoClass); + }); + + it('should have a panel', () => { + const panel = el.renderRoot.querySelector('.panel'); + + expect(panel).to.exist; + }); + + it('should show the command', () => { + const command = el.renderRoot.querySelector('.command'); + + expect(command).to.have.trimmed.text('npm install'); + }); + + it('should show the scope', () => { + const scope = el.renderRoot.querySelector('.scope'); + + expect(scope).to.have.trimmed.text('@sl-design-system/'); + }); + + it('should show the package name', () => { + const pkg = el.renderRoot.querySelector('.package'); + + expect(pkg).to.have.trimmed.text('button'); + }); + }); + + describe('syntax highlighting', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should have a command span', () => { + const command = el.renderRoot.querySelector('.command'); + + expect(command).to.exist; + }); + + it('should have a scope span', () => { + const scope = el.renderRoot.querySelector('.scope'); + + expect(scope).to.exist; + }); + + it('should have a package span', () => { + const pkg = el.renderRoot.querySelector('.package'); + + expect(pkg).to.have.trimmed.text('text-field'); + }); + }); + + describe('copy button', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should have a copy button', () => { + const copyBtn = el.renderRoot.querySelector('.copy'); + + expect(copyBtn).to.exist; + }); + + it('should have an aria-label on the copy button', () => { + const copyBtn = el.renderRoot.querySelector('.copy'); + + expect(copyBtn).to.have.attribute('aria-label', 'Copy npm install @sl-design-system/button'); + }); + + it('should copy the command to clipboard when clicked', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + const copyBtn = el.renderRoot.querySelector('.copy')!; + copyBtn.click(); + + await el.updateComplete; + + expect(writeText).toHaveBeenCalledWith('npm install @sl-design-system/button'); + + writeText.mockRestore(); + }); + }); +}); diff --git a/docs/components/src/install-info/install-info.stories.ts b/docs/components/src/install-info/install-info.stories.ts new file mode 100644 index 0000000000..7054d7ad1a --- /dev/null +++ b/docs/components/src/install-info/install-info.stories.ts @@ -0,0 +1,33 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { InstallInfo } from './install-info.js'; + +type Props = Pick; +type Story = StoryObj; + +try { + customElements.define('doc-install-info', InstallInfo); +} catch { + /* empty */ +} + +export default { + title: 'Install Info', + args: { + package: 'button' + }, + argTypes: { + package: { + control: 'text' + } + }, + render: ({ package: pkg }) => html`` +} satisfies Meta; + +export const Basic: Story = {}; + +export const DifferentPackage: Story = { + args: { + package: 'text-field' + } +}; diff --git a/docs/components/src/install-info/install-info.ts b/docs/components/src/install-info/install-info.ts new file mode 100644 index 0000000000..5daa53ba51 --- /dev/null +++ b/docs/components/src/install-info/install-info.ts @@ -0,0 +1,31 @@ +import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import styles from './install-info.css' with { type: 'css' }; + +export class InstallInfo extends LitElement { + /** @internal */ + static styles: CSSResultGroup = styles; + + /** The package name (without the @sl-design-system/ prefix). */ + @property() package?: string; + + override render(): TemplateResult { + const command = `npm install @sl-design-system/${this.package}`; + + return html` +
+ npm install @sl-design-system/${this.package} + +
+ `; + } + + async #copy(text: string): Promise { + await navigator.clipboard.writeText(text); + } +} diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index d2d9ae9fac..ac8abf0904 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -46,6 +46,9 @@ export default defineConfig({ tsgo: true, }, entry: [ + 'src/copy-button/copy-button.ts', + 'src/example/example.ts', + 'src/install-info/install-info.ts', 'src/page-toc/page-toc.ts', 'src/search/search.ts', 'src/sidebar/sidebar.ts', diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index d6d2cf060d..77b765ec62 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -1,11 +1,13 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; import './icons.js'; +import { InstallInfo } from '@sl-design-system/doc-components/install-info/install-info'; import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc'; import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar'; import { NavGroup } from '@sl-design-system/doc-components/site-nav/nav-group'; import { NavItem } from '@sl-design-system/doc-components/site-nav/nav-item'; import { SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav'; +customElements.define('doc-install-info', InstallInfo); customElements.define('doc-nav-group', NavGroup); customElements.define('doc-nav-item', NavItem); customElements.define('doc-page-toc', PageToc); diff --git a/packages/components/button/src/button.ts b/packages/components/button/src/button.ts index 57b24c949e..601fe49e4d 100644 --- a/packages/components/button/src/button.ts +++ b/packages/components/button/src/button.ts @@ -165,12 +165,9 @@ export class Button extends LitElement { } #onUpdate(): void { - const filteredNodes = this.renderRoot - .querySelector('slot') - ?.assignedNodes({ flatten: true }) - .filter(node => { - return node.nodeType === Node.ELEMENT_NODE || (node.textContent && node.textContent.trim().length > 0); - }); + const filteredNodes = ( + this.renderRoot.querySelector('slot')?.assignedNodes({ flatten: true }) ?? Array.from(this.renderRoot.childNodes) + ).filter(node => node.nodeType === Node.ELEMENT_NODE || (node.textContent && node.textContent.trim().length > 0)); let hasIcon = false; diff --git a/yarn.lock b/yarn.lock index 7a400a1549..c9be740432 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5685,8 +5685,10 @@ __metadata: "@storybook/web-components": "npm:^10.3.3" "@storybook/web-components-vite": "npm:^10.3.3" "@tsdown/css": "npm:^0.21.7" + "@types/prismjs": "npm:^1.26.6" "@typescript/native-preview": "npm:^7.0.0-dev.20260315.1" lit: "npm:^3.3.2" + prismjs: "npm:^1.30.0" sass-embedded: "npm:^1.98.0" storybook: "npm:^10.3.3" tsdown: "npm:^0.21.7" @@ -7423,6 +7425,13 @@ __metadata: languageName: node linkType: hard +"@types/prismjs@npm:^1.26.6": + version: 1.26.6 + resolution: "@types/prismjs@npm:1.26.6" + checksum: 10c0/152a27500cb32b114edfb77f9d0dccd03bebc84828d1e92abacaf212b22d3ccdde041ce421dd58b6ec8461bbec7cd76ed5ee773cae4be7ca36a6dd4ddcf0f9e7 + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" @@ -20521,6 +20530,13 @@ __metadata: languageName: node linkType: hard +"prismjs@npm:^1.30.0": + version: 1.30.0 + resolution: "prismjs@npm:1.30.0" + checksum: 10c0/f56205bfd58ef71ccfcbcb691fd0eb84adc96c6ff21b0b69fc6fdcf02be42d6ef972ba4aed60466310de3d67733f6a746f89f2fb79c00bf217406d465b3e8f23 + languageName: node + linkType: hard + "proc-log@npm:^5.0.0": version: 5.0.0 resolution: "proc-log@npm:5.0.0" From 888a99cc9b32442294f7ccd9ee385e2b5224e089 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 31 Mar 2026 09:30:22 +0200 Subject: [PATCH 22/72] =?UTF-8?q?=F0=9F=8E=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cem.yaml | 9 + docs/website/eleventy.config.js | 33 + .../src/content/components/actions/button.md | 2 +- docs/website/src/includes/component.njk | 48 + docs/website/src/utils/manifest.js | 3 + package.json | 2 +- .../accordion/src/accordion-item.ts | 13 +- website/package.json | 2 - yarn.lock | 1377 +---------------- 9 files changed, 185 insertions(+), 1304 deletions(-) create mode 100644 .cem.yaml create mode 100644 docs/website/src/includes/component.njk create mode 100644 docs/website/src/utils/manifest.js diff --git a/.cem.yaml b/.cem.yaml new file mode 100644 index 0000000000..293c0be883 --- /dev/null +++ b/.cem.yaml @@ -0,0 +1,9 @@ +sourceControlRootUrl: 'https://github.com/sl-design-system/components/tree/main' + +generate: + files: + - 'packages/components/*/src/**/*.ts' + exclude: + - 'packages/components/*/src/**/*.spec.ts' + - 'packages/components/*/src/**/*.stories.ts' + output: 'custom-elements.json' diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index 5e60b30c07..d4cd32bed4 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -4,12 +4,15 @@ import { dirname, join } from 'node:path'; import * as esbuild from 'esbuild'; import eleventyNavigationPlugin from '@11ty/eleventy-navigation'; import markdownItAnchor from 'markdown-it-anchor'; +import { getComponents } from './src/utils/manifest.js'; const require = createRequire(import.meta.url); const themePath = dirname(require.resolve('@sl-design-system/sanoma-learning/package.json')); /** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */ export default function (eleventyConfig) { + const allComponents = getComponents(); + eleventyConfig.addPlugin(eleventyNavigationPlugin); eleventyConfig.amendLibrary('md', mdLib => { @@ -32,6 +35,36 @@ export default function (eleventyConfig) { eleventyConfig.addWatchTarget('../components/dist/**/*.(css|js)'); + // Helpers + eleventyConfig.addNunjucksGlobal('getComponent', tagName => { + const component = allComponents.find(c => c.tagName === tagName); + if (!component) { + throw new Error( + `Unable to find "<${tagName}>". Make sure the file name is the same as the tag name (without prefix).` + ); + } + return component; + }); + + eleventyConfig.addCollection('componentPages', function (collectionApi) { + const componentPages = collectionApi.getFilteredByGlob( + path.join(eleventyConfig.directories.input, 'components/**/*.md') + ); + + return componentPages.map(page => { + const componentName = path.basename(page.inputPath, '.md'), + tagName = `sl-${componentName}`, + component = allComponents.find(c => c.tagName === tagName); + + // Add component to the page's data + if (component) { + page.data.component = component; + } + + return page; + }); + }); + eleventyConfig.on('eleventy.before', async () => { // Scan pages for icon names in eleventyNavigation frontmatter const icons = new Set(); diff --git a/docs/website/src/content/components/actions/button.md b/docs/website/src/content/components/actions/button.md index 53ac4d48d8..d4c6462436 100644 --- a/docs/website/src/content/components/actions/button.md +++ b/docs/website/src/content/components/actions/button.md @@ -1,6 +1,6 @@ --- title: Button -layout: base.njk +layout: component.njk eleventyNavigation: key: Button parent: Actions diff --git a/docs/website/src/includes/component.njk b/docs/website/src/includes/component.njk new file mode 100644 index 0000000000..be3adc77d7 --- /dev/null +++ b/docs/website/src/includes/component.njk @@ -0,0 +1,48 @@ + + + + + + {{ title }} | SL Design System + + + + + + + + + +
+ + {% include "sidebar.njk" %} + + +
+
+

{{ title }}

+
+ {% if component.status %} + {{ component.status }} + {% endif %} + {% if component.version %} + v{{ component.version }} + {% endif %} +
+ + {{ content | safe }} +
+ + +
+ + + + diff --git a/docs/website/src/utils/manifest.js b/docs/website/src/utils/manifest.js new file mode 100644 index 0000000000..ae8a906522 --- /dev/null +++ b/docs/website/src/utils/manifest.js @@ -0,0 +1,3 @@ +export function getComponent() { + return []; +} diff --git a/package.json b/package.json index 9931a9e915..0bff5d8778 100644 --- a/package.json +++ b/package.json @@ -457,9 +457,9 @@ "@af-utils/scrollend-polyfill": "^0.0.14", "@changesets/cli": "^2.30.0", "@changesets/get-github-info": "^0.8.0", - "@custom-elements-manifest/analyzer": "^0.11.0", "@faker-js/faker": "^10.3.0", "@lit/localize-tools": "^0.8.1", + "@pwrs/cem": "^0.9.16", "@storybook/addon-a11y": "^10.3.3", "@storybook/addon-docs": "^10.3.3", "@storybook/addon-vitest": "^10.3.3", diff --git a/packages/components/accordion/src/accordion-item.ts b/packages/components/accordion/src/accordion-item.ts index 1dae081f65..3e32b9f36b 100644 --- a/packages/components/accordion/src/accordion-item.ts +++ b/packages/components/accordion/src/accordion-item.ts @@ -6,14 +6,9 @@ import { property } from 'lit/decorators.js'; import styles from './accordion-item.scss.js'; import { type AccordionIconType } from './accordion.js'; -declare global { - interface HTMLElementTagNameMap { - 'sl-accordion-item': AccordionItem; - } -} - /** * An accordion item component. + * @element sl-accordion-item * * @csspart details - Details element of the accordion-item * @csspart summary - Header element of the accordion-item @@ -191,3 +186,9 @@ export class AccordionItem extends LitElement { requestAnimationFrame(() => details.classList.add(state)); } } + +// declare global { +// interface HTMLElementTagNameMap { +// 'sl-accordion-item': AccordionItem; +// } +// } diff --git a/website/package.json b/website/package.json index 48364f0d50..55576eb090 100644 --- a/website/package.json +++ b/website/package.json @@ -174,8 +174,6 @@ "@11ty/eleventy-img": "^3.1.8", "@11ty/eleventy-navigation": "^0.3.5", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", - "@custom-elements-manifest/analyzer": "^0.11.0", - "@custom-elements-manifest/to-markdown": "^0.1.0", "@luncheon/esbuild-plugin-gzip": "^0.1.0", "@types/markdown-it": "^14.1.2", "@web/dev-server": "^0.4.6", diff --git a/yarn.lock b/yarn.lock index c9be740432..b3c5d8efbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2445,52 +2445,6 @@ __metadata: languageName: node linkType: hard -"@custom-elements-manifest/analyzer@npm:^0.11.0": - version: 0.11.0 - resolution: "@custom-elements-manifest/analyzer@npm:0.11.0" - dependencies: - "@custom-elements-manifest/find-dependencies": "npm:^0.0.7" - "@github/catalyst": "npm:^1.6.0" - "@web/config-loader": "npm:0.1.3" - chokidar: "npm:3.5.2" - command-line-args: "npm:5.1.2" - comment-parser: "npm:1.2.4" - custom-elements-manifest: "npm:1.0.0" - debounce: "npm:1.2.1" - globby: "npm:11.0.4" - typescript: "npm:~5.4.2" - bin: - cem: cem.js - custom-elements-manifest: cem.js - checksum: 10c0/8710abf95d232ed1002f5b6170e47500f767705e9e0f31c71778f934de6dcb48a3dbe8f126e212e131615a088408f1c2b1daac505376c20a99af0eb538450c0c - languageName: node - linkType: hard - -"@custom-elements-manifest/find-dependencies@npm:^0.0.7": - version: 0.0.7 - resolution: "@custom-elements-manifest/find-dependencies@npm:0.0.7" - dependencies: - oxc-resolver: "npm:^11.9.0" - rs-module-lexer: "npm:^2.5.1" - checksum: 10c0/442dc5a9cdbeb891ecbfaa47903ea6655092fa1bf497e77fddb6978083e37eb1a665a462412ba30d8a6c5186daa1db989ca70fe9487d04b9cd947320c6b12540 - languageName: node - linkType: hard - -"@custom-elements-manifest/to-markdown@npm:^0.1.0": - version: 0.1.0 - resolution: "@custom-elements-manifest/to-markdown@npm:0.1.0" - dependencies: - mdast-builder: "npm:^1.1.1" - mdast-util-from-markdown: "npm:^1.0.4" - mdast-util-gfm: "npm:^1.0.0" - mdast-util-to-markdown: "npm:^1.0.1" - remark-gfm: "npm:^1.0.0" - remark-stringify: "npm:^9.0.1" - unified: "npm:^9.2.1" - checksum: 10c0/75e95f35d6df467ad8501e130544c97ac322886093dfe6ab10e4ef98ab2dcfb0af9b99aaaccdd23768078e2ac11d512f3ae10be6f7faa0ad2ccc994e6da84e15 - languageName: node - linkType: hard - "@discoveryjs/json-ext@npm:0.6.3": version: 0.6.3 resolution: "@discoveryjs/json-ext@npm:0.6.3" @@ -3200,13 +3154,6 @@ __metadata: languageName: node linkType: hard -"@github/catalyst@npm:^1.6.0": - version: 1.6.0 - resolution: "@github/catalyst@npm:1.6.0" - checksum: 10c0/e21b72959987cb54339d30aea12f14fa72d5d8d7f04a931fccd09c9fc9c36fb113b7846e2cbdc24dc20677ea247dbe7dceca2ca768029bd6c7a2fa25a82d453b - languageName: node - linkType: hard - "@graphql-typed-document-node/core@npm:^3.1.1": version: 3.2.0 resolution: "@graphql-typed-document-node/core@npm:3.2.0" @@ -4309,148 +4256,6 @@ __metadata: languageName: node linkType: hard -"@oxc-resolver/binding-android-arm-eabi@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@oxc-resolver/binding-android-arm64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-android-arm64@npm:11.19.1" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@oxc-resolver/binding-darwin-arm64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.19.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@oxc-resolver/binding-darwin-x64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-darwin-x64@npm:11.19.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@oxc-resolver/binding-freebsd-x64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.19.1" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.19.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.19.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-arm64-gnu@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.19.1" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-arm64-musl@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.19.1" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.19.1" - conditions: os=linux & cpu=ppc64 & libc=glibc - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.19.1" - conditions: os=linux & cpu=riscv64 & libc=glibc - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-riscv64-musl@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.19.1" - conditions: os=linux & cpu=riscv64 & libc=musl - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-s390x-gnu@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.19.1" - conditions: os=linux & cpu=s390x & libc=glibc - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-x64-gnu@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.19.1" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@oxc-resolver/binding-linux-x64-musl@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.19.1" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@oxc-resolver/binding-openharmony-arm64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.19.1" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@oxc-resolver/binding-wasm32-wasi@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.19.1" - dependencies: - "@napi-rs/wasm-runtime": "npm:^1.1.1" - conditions: cpu=wasm32 - languageName: node - linkType: hard - -"@oxc-resolver/binding-win32-arm64-msvc@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.19.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@oxc-resolver/binding-win32-ia32-msvc@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-win32-ia32-msvc@npm:11.19.1" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@oxc-resolver/binding-win32-x64-msvc@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.19.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@parcel/watcher-android-arm64@npm:2.5.0": version: 2.5.0 resolution: "@parcel/watcher-android-arm64@npm:2.5.0" @@ -4770,6 +4575,77 @@ __metadata: languageName: node linkType: hard +"@pwrs/cem-darwin-arm64@npm:0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem-darwin-arm64@npm:0.9.16" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@pwrs/cem-darwin-x64@npm:0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem-darwin-x64@npm:0.9.16" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@pwrs/cem-linux-arm64@npm:0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem-linux-arm64@npm:0.9.16" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@pwrs/cem-linux-x64@npm:0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem-linux-x64@npm:0.9.16" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@pwrs/cem-win32-arm64@npm:0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem-win32-arm64@npm:0.9.16" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@pwrs/cem-win32-x64@npm:0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem-win32-x64@npm:0.9.16" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@pwrs/cem@npm:^0.9.16": + version: 0.9.16 + resolution: "@pwrs/cem@npm:0.9.16" + dependencies: + "@pwrs/cem-darwin-arm64": "npm:0.9.16" + "@pwrs/cem-darwin-x64": "npm:0.9.16" + "@pwrs/cem-linux-arm64": "npm:0.9.16" + "@pwrs/cem-linux-x64": "npm:0.9.16" + "@pwrs/cem-win32-arm64": "npm:0.9.16" + "@pwrs/cem-win32-x64": "npm:0.9.16" + dependenciesMeta: + "@pwrs/cem-darwin-arm64": + optional: true + "@pwrs/cem-darwin-x64": + optional: true + "@pwrs/cem-linux-arm64": + optional: true + "@pwrs/cem-linux-x64": + optional: true + "@pwrs/cem-win32-arm64": + optional: true + "@pwrs/cem-win32-x64": + optional: true + bin: + cem: bin/cem.js + checksum: 10c0/390103c3b292547234ddc0424eb280ade0cc49ddddaa2017548b2fba593481f9d08cb1e93d241f8fce12cfe58303e5d469a5b0fb4caff7bac547428f4ad123c4 + languageName: node + linkType: hard + "@quansync/fs@npm:^1.0.0": version: 1.0.0 resolution: "@quansync/fs@npm:1.0.0" @@ -6028,9 +5904,9 @@ __metadata: "@af-utils/scrollend-polyfill": "npm:^0.0.14" "@changesets/cli": "npm:^2.30.0" "@changesets/get-github-info": "npm:^0.8.0" - "@custom-elements-manifest/analyzer": "npm:^0.11.0" "@faker-js/faker": "npm:^10.3.0" "@lit/localize-tools": "npm:^0.8.1" + "@pwrs/cem": "npm:^0.9.16" "@storybook/addon-a11y": "npm:^10.3.3" "@storybook/addon-docs": "npm:^10.3.3" "@storybook/addon-vitest": "npm:^10.3.3" @@ -6543,8 +6419,6 @@ __metadata: "@11ty/eleventy-img": "npm:^3.1.8" "@11ty/eleventy-navigation": "npm:^0.3.5" "@11ty/eleventy-plugin-syntaxhighlight": "npm:^5.0.0" - "@custom-elements-manifest/analyzer": "npm:^0.11.0" - "@custom-elements-manifest/to-markdown": "npm:^0.1.0" "@luncheon/esbuild-plugin-gzip": "npm:^0.1.0" "@types/markdown-it": "npm:^14.1.2" "@web/dev-server": "npm:^0.4.6" @@ -7157,15 +7031,6 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.0.0": - version: 4.1.9 - resolution: "@types/debug@npm:4.1.9" - dependencies: - "@types/ms": "npm:*" - checksum: 10c0/8b550c47c70cc1af9a58e5c572f2418f30bface5bf5d5afa0d938923978f40be4c55646f1ab260f6f1492ca6ab065d447de23cb3b30d7b38597c2cbf89f4cb21 - languageName: node - linkType: hard - "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -7349,15 +7214,6 @@ __metadata: languageName: node linkType: hard -"@types/mdast@npm:^3.0.0": - version: 3.0.13 - resolution: "@types/mdast@npm:3.0.13" - dependencies: - "@types/unist": "npm:^2" - checksum: 10c0/b328d1622075a67db1d8eac78dcbd55aefb4adaf63206b58abfce902c0ce5232a2674bd0bf961696c9a3765d5fcf145378ce03075bd1690a25adc617650f1228 - languageName: node - linkType: hard - "@types/mdurl@npm:^2": version: 2.0.0 resolution: "@types/mdurl@npm:2.0.0" @@ -7386,13 +7242,6 @@ __metadata: languageName: node linkType: hard -"@types/ms@npm:*": - version: 0.7.32 - resolution: "@types/ms@npm:0.7.32" - checksum: 10c0/16f60d0a2485edfa459e9570aec9135d9ef08dd855630754063f3baf1d1df7a5edd0f249ff9b460a33842181250f51b27b35078b83cf6ec1dccabb4485de19d6 - languageName: node - linkType: hard - "@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.7.4": version: 22.18.10 resolution: "@types/node@npm:22.18.10" @@ -7572,13 +7421,6 @@ __metadata: languageName: node linkType: hard -"@types/unist@npm:^2, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2, @types/unist@npm:^2.0.3": - version: 2.0.8 - resolution: "@types/unist@npm:2.0.8" - checksum: 10c0/2c4685d5258b4f543677d20dce0d72b8235e70b6c859af24fcf445f92dac98ec8a1faa0cfb43307466561fcd9dbd2534a4860000944401ac3314a685b5efe3d7 - languageName: node - linkType: hard - "@types/ws@npm:^7.4.0": version: 7.4.7 resolution: "@types/ws@npm:7.4.7" @@ -8306,15 +8148,6 @@ __metadata: languageName: node linkType: hard -"@web/config-loader@npm:0.1.3": - version: 0.1.3 - resolution: "@web/config-loader@npm:0.1.3" - dependencies: - semver: "npm:^7.3.4" - checksum: 10c0/165376a525f2690b6266229d4e4f82a3e877ed1aaaea0b1131081d10650704f37d5dbdb5c959c4efcf3f4f91ccf97dcdc2b99df233d21317ee25ed7e00afacc6 - languageName: node - linkType: hard - "@web/config-loader@npm:^0.3.0": version: 0.3.1 resolution: "@web/config-loader@npm:0.3.1" @@ -8598,69 +8431,6 @@ __metadata: languageName: node linkType: hard -"@xn-sakina/rml-darwin-arm64@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-darwin-arm64@npm:2.6.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@xn-sakina/rml-darwin-x64@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-darwin-x64@npm:2.6.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@xn-sakina/rml-linux-arm-gnueabihf@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-linux-arm-gnueabihf@npm:2.6.0" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@xn-sakina/rml-linux-arm64-gnu@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-linux-arm64-gnu@npm:2.6.0" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@xn-sakina/rml-linux-arm64-musl@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-linux-arm64-musl@npm:2.6.0" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@xn-sakina/rml-linux-x64-gnu@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-linux-x64-gnu@npm:2.6.0" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@xn-sakina/rml-linux-x64-musl@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-linux-x64-musl@npm:2.6.0" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@xn-sakina/rml-win32-arm64-msvc@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-win32-arm64-msvc@npm:2.6.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@xn-sakina/rml-win32-x64-msvc@npm:2.6.0": - version: 2.6.0 - resolution: "@xn-sakina/rml-win32-x64-msvc@npm:2.6.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -9089,7 +8859,7 @@ __metadata: languageName: node linkType: hard -"array-back@npm:^6.1.2, array-back@npm:^6.2.2": +"array-back@npm:^6.2.2": version: 6.2.2 resolution: "array-back@npm:6.2.2" checksum: 10c0/c98a6e43b669400f58e2fba478336d5d02aac970566ffae3af0cb9b5585ec3811a1e010c76e34fb809a9762e6822a43a9c9a1b99f2a35f43b11a9e198e782818 @@ -9529,13 +9299,6 @@ __metadata: languageName: node linkType: hard -"bail@npm:^1.0.0": - version: 1.0.5 - resolution: "bail@npm:1.0.5" - checksum: 10c0/4cf7d0b5c82fdc69590b3fe85c17c4ec37647681b20875551fd6187a85c122b20178dc118001d3ebd5d0ab3dc0e95637c71f889f481882ee761db43c6b16fa05 - languageName: node - linkType: hard - "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -10182,20 +9945,6 @@ __metadata: languageName: node linkType: hard -"ccount@npm:^1.0.0": - version: 1.1.0 - resolution: "ccount@npm:1.1.0" - checksum: 10c0/9ccfddfa45c8d6d01411b8e30d2ce03c55c33f32a69bdb84ee44d743427cdb01b03159954917023d0dac960c34973ba42626bb9fa883491ebb663a53a6713d43 - languageName: node - linkType: hard - -"ccount@npm:^2.0.0": - version: 2.0.1 - resolution: "ccount@npm:2.0.1" - checksum: 10c0/3939b1664390174484322bc3f45b798462e6c07ee6384cb3d645e0aa2f318502d174845198c1561930e1d431087f74cf1fe291ae9a4722821a9f4ba67e574350 - languageName: node - linkType: hard - "cem-plugin-expanded-types@npm:^1.4.0": version: 1.4.0 resolution: "cem-plugin-expanded-types@npm:1.4.0" @@ -10274,27 +10023,6 @@ __metadata: languageName: node linkType: hard -"character-entities-legacy@npm:^1.0.0": - version: 1.1.4 - resolution: "character-entities-legacy@npm:1.1.4" - checksum: 10c0/ea4ca9c29887335eed86d78fc67a640168342b1274da84c097abb0575a253d1265281a5052f9a863979e952bcc267b4ecaaf4fe233a7e1e0d8a47806c65b96c7 - languageName: node - linkType: hard - -"character-entities@npm:^1.0.0": - version: 1.2.4 - resolution: "character-entities@npm:1.2.4" - checksum: 10c0/ad015c3d7163563b8a0ee1f587fb0ef305ef344e9fd937f79ca51cccc233786a01d591d989d5bf7b2e66b528ac9efba47f3b1897358324e69932f6d4b25adfe1 - languageName: node - linkType: hard - -"character-entities@npm:^2.0.0": - version: 2.0.2 - resolution: "character-entities@npm:2.0.2" - checksum: 10c0/b0c645a45bcc90ff24f0e0140f4875a8436b8ef13b6bcd31ec02cfb2ca502b680362aa95386f7815bdc04b6464d48cf191210b3840d7c04241a149ede591a308 - languageName: node - linkType: hard - "character-parser@npm:^2.2.0": version: 2.2.0 resolution: "character-parser@npm:2.2.0" @@ -10304,13 +10032,6 @@ __metadata: languageName: node linkType: hard -"character-reference-invalid@npm:^1.0.0": - version: 1.1.4 - resolution: "character-reference-invalid@npm:1.1.4" - checksum: 10c0/29f05081c5817bd1e975b0bf61e77b60a40f62ad371d0f0ce0fdb48ab922278bc744d1fbe33771dced751887a8403f265ff634542675c8d7375f6ff4811efd0e - languageName: node - linkType: hard - "chardet@npm:^2.1.1": version: 2.1.1 resolution: "chardet@npm:2.1.1" @@ -10325,25 +10046,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.2": - version: 3.5.2 - resolution: "chokidar@npm:3.5.2" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/e7179a9dc4ce54c1ba660652319039b7ca0817a442dd05a45afcbdefcd4848b4276debfa9cf321798c2c567c6289da14dd48d9a1ee92056a7b526c554cffe129 - languageName: node - linkType: hard - "chokidar@npm:^1.6.0": version: 1.7.0 resolution: "chokidar@npm:1.7.0" @@ -10673,18 +10375,6 @@ __metadata: languageName: node linkType: hard -"command-line-args@npm:5.1.2": - version: 5.1.2 - resolution: "command-line-args@npm:5.1.2" - dependencies: - array-back: "npm:^6.1.2" - find-replace: "npm:^3.0.0" - lodash.camelcase: "npm:^4.3.0" - typical: "npm:^4.0.0" - checksum: 10c0/e8dcb3152b106bd806c038dabb26f1fe0aced6d9ab06f02a80569c572124c1f55ef2686897823d787ab6c3cd1668b0314dd963457d07d063e67679fa3bd0d07d - languageName: node - linkType: hard - "command-line-args@npm:^5.1.1": version: 5.2.1 resolution: "command-line-args@npm:5.2.1" @@ -10765,13 +10455,6 @@ __metadata: languageName: node linkType: hard -"comment-parser@npm:1.2.4": - version: 1.2.4 - resolution: "comment-parser@npm:1.2.4" - checksum: 10c0/ff25ad318ead37621476cf7348dfab0991bc0f249fcc82deec99e9accf941f3dca764fb76ac5570950fbb3642bd9875cfaad9ec57661eec56bbc5c08e85e3dc9 - languageName: node - linkType: hard - "common-path-prefix@npm:^3.0.0": version: 3.0.0 resolution: "common-path-prefix@npm:3.0.0" @@ -11281,13 +10964,6 @@ __metadata: languageName: node linkType: hard -"custom-elements-manifest@npm:1.0.0": - version: 1.0.0 - resolution: "custom-elements-manifest@npm:1.0.0" - checksum: 10c0/6e8e4dcc6ae4f846e92b6ec0ef5c8d155889dae6df7210cbf3e9c47387ffab83fce8fde96a1aaa9766304cb08afa17198da4ef8560745311bdf3ba280fc77b40 - languageName: node - linkType: hard - "custom-event@npm:~1.0.0": version: 1.0.1 resolution: "custom-event@npm:1.0.1" @@ -11342,7 +11018,7 @@ __metadata: languageName: node linkType: hard -"debounce@npm:1.2.1, debounce@npm:^1.2.0": +"debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" checksum: 10c0/6c9320aa0973fc42050814621a7a8a78146c1975799b5b3cc1becf1f77ba9a5aa583987884230da0842a03f385def452fad5d60db97c3d1c8b824e38a8edf500 @@ -11358,7 +11034,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -11398,15 +11074,6 @@ __metadata: languageName: node linkType: hard -"decode-named-character-reference@npm:^1.0.0": - version: 1.0.2 - resolution: "decode-named-character-reference@npm:1.0.2" - dependencies: - character-entities: "npm:^2.0.0" - checksum: 10c0/66a9fc5d9b5385a2b3675c69ba0d8e893393d64057f7dbbb585265bb4fc05ec513d76943b8e5aac7d8016d20eea4499322cbf4cd6d54b466976b78f3a7587a4c - languageName: node - linkType: hard - "decode-uri-component@npm:^0.2.0": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -11590,7 +11257,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0, dequal@npm:^2.0.3": +"dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888 @@ -11650,13 +11317,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0": - version: 5.2.0 - resolution: "diff@npm:5.2.0" - checksum: 10c0/aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 - languageName: node - linkType: hard - "diff@npm:^7.0.0": version: 7.0.0 resolution: "diff@npm:7.0.0" @@ -13217,7 +12877,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.1.1, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -14088,20 +13748,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:11.0.4": - version: 11.0.4 - resolution: "globby@npm:11.0.4" - dependencies: - array-union: "npm:^2.1.0" - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.1.1" - ignore: "npm:^5.1.4" - merge2: "npm:^1.3.0" - slash: "npm:^3.0.0" - checksum: 10c0/de5f828c834baf75e3bd3c629bb3a64d1dfa9965831d0b105b728f9184284c6ba2b0d42e24862b411abc18e6e0af12e60880b3a62e096752de3426f2839f9ef7 - languageName: node - linkType: hard - "globby@npm:^11.0.0, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" @@ -14751,7 +14397,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.4, ignore@npm:^5.2.0": +"ignore@npm:^5.2.0": version: 5.3.1 resolution: "ignore@npm:5.3.1" checksum: 10c0/703f7f45ffb2a27fb2c5a8db0c32e7dee66b33a225d28e8db4e1be6474795f606686a6e3bcc50e1aa12f2042db4c9d4a7d60af3250511de74620fbed052ea4cd @@ -15082,13 +14728,6 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^2.0.0": - version: 2.0.5 - resolution: "is-buffer@npm:2.0.5" - checksum: 10c0/e603f6fced83cf94c53399cff3bda1a9f08e391b872b64a73793b0928be3e5f047f2bcece230edb7632eaea2acdbfcb56c23b33d8a20c820023b230f1485679a - languageName: node - linkType: hard - "is-builtin-module@npm:^3.2.1": version: 3.2.1 resolution: "is-builtin-module@npm:3.2.1" @@ -15319,13 +14958,6 @@ __metadata: languageName: node linkType: hard -"is-hexadecimal@npm:^1.0.0": - version: 1.0.4 - resolution: "is-hexadecimal@npm:1.0.4" - checksum: 10c0/ec4c64e5624c0f240922324bc697e166554f09d3ddc7633fc526084502626445d0a871fbd8cae52a9844e83bd0bb414193cc5a66806d7b2867907003fc70c5ea - languageName: node - linkType: hard - "is-in-ssh@npm:^1.0.0": version: 1.0.0 resolution: "is-in-ssh@npm:1.0.0" @@ -15468,7 +15100,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": +"is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: 10c0/e5c9814cdaa627a9ad0a0964ded0e0491bfd9ace405c49a5d63c88b30a162f1512c069d5b80997893c4d0181eadc3fed02b4ab4b81059aba5620bfcdfdeb9c53 @@ -16314,7 +15946,7 @@ __metadata: languageName: node linkType: hard -"kleur@npm:^4.0.3, kleur@npm:^4.1.5": +"kleur@npm:^4.1.5": version: 4.1.5 resolution: "kleur@npm:4.1.5" checksum: 10c0/e9de6cb49657b6fa70ba2d1448fd3d691a5c4370d8f7bbf1c2f64c24d461270f2117e1b0afe8cb3114f13bbd8e51de158c2a224953960331904e636a5e4c0f2a @@ -16949,20 +16581,6 @@ __metadata: languageName: node linkType: hard -"longest-streak@npm:^2.0.0": - version: 2.0.4 - resolution: "longest-streak@npm:2.0.4" - checksum: 10c0/918fb5104cde537757f44431776d6d828bc091a63ca38a3b3e59a08b88498b4421bf5fd9823ef22b4d186f0234d9943087fa96bd6117d26dedcf6008480fd46a - languageName: node - linkType: hard - -"longest-streak@npm:^3.0.0": - version: 3.1.0 - resolution: "longest-streak@npm:3.1.0" - checksum: 10c0/7c2f02d0454b52834d1bcedef79c557bd295ee71fdabb02d041ff3aa9da48a90b5df7c0409156dedbc4df9b65da18742652aaea4759d6ece01f08971af6a7eaa - languageName: node - linkType: hard - "loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -17207,22 +16825,6 @@ __metadata: languageName: node linkType: hard -"markdown-table@npm:^2.0.0": - version: 2.0.0 - resolution: "markdown-table@npm:2.0.0" - dependencies: - repeat-string: "npm:^1.0.0" - checksum: 10c0/f257e0781ea50eb946919df84bdee4ba61f983971b277a369ca7276f89740fd0e2749b9b187163a42df4c48682b71962d4007215ce3523480028f06c11ddc2e6 - languageName: node - linkType: hard - -"markdown-table@npm:^3.0.0": - version: 3.0.3 - resolution: "markdown-table@npm:3.0.3" - checksum: 10c0/47433a3f31e4637a184e38e873ab1d2fadfb0106a683d466fec329e99a2d8dfa09f091fa42202c6f13ec94aef0199f449a684b28042c636f2edbc1b7e1811dcd - languageName: node - linkType: hard - "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -17256,222 +16858,6 @@ __metadata: languageName: node linkType: hard -"mdast-builder@npm:^1.1.1": - version: 1.1.1 - resolution: "mdast-builder@npm:1.1.1" - dependencies: - "@types/unist": "npm:^2.0.3" - checksum: 10c0/daff384277f59dbe6163b47853ba06573531ae5fd5535b529dbfed4a1a97f8bb5a0b07f897fad29ab9a4c5ad2fdd469cef247a49836c3075aabb4d8e74ad6496 - languageName: node - linkType: hard - -"mdast-util-find-and-replace@npm:^1.1.0": - version: 1.1.1 - resolution: "mdast-util-find-and-replace@npm:1.1.1" - dependencies: - escape-string-regexp: "npm:^4.0.0" - unist-util-is: "npm:^4.0.0" - unist-util-visit-parents: "npm:^3.0.0" - checksum: 10c0/4b9da583e858146a6553155795ef2f0d37b72b8d20487f75895e01fd240a483fbdb97f5aecd218e8ce598be24edb742c5bcbcba2896d172101529376ef390633 - languageName: node - linkType: hard - -"mdast-util-find-and-replace@npm:^2.0.0": - version: 2.2.2 - resolution: "mdast-util-find-and-replace@npm:2.2.2" - dependencies: - "@types/mdast": "npm:^3.0.0" - escape-string-regexp: "npm:^5.0.0" - unist-util-is: "npm:^5.0.0" - unist-util-visit-parents: "npm:^5.0.0" - checksum: 10c0/ce935f4bd4aeab47f91531a7f09dfab89aaeea62ad31029b43185c5b626921357703d8e5093c13073c097fdabfc57cb2f884d7dfad83dbe7239e351375d6797c - languageName: node - linkType: hard - -"mdast-util-from-markdown@npm:^1.0.0, mdast-util-from-markdown@npm:^1.0.4": - version: 1.3.1 - resolution: "mdast-util-from-markdown@npm:1.3.1" - dependencies: - "@types/mdast": "npm:^3.0.0" - "@types/unist": "npm:^2.0.0" - decode-named-character-reference: "npm:^1.0.0" - mdast-util-to-string: "npm:^3.1.0" - micromark: "npm:^3.0.0" - micromark-util-decode-numeric-character-reference: "npm:^1.0.0" - micromark-util-decode-string: "npm:^1.0.0" - micromark-util-normalize-identifier: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - unist-util-stringify-position: "npm:^3.0.0" - uvu: "npm:^0.5.0" - checksum: 10c0/f4e901bf2a2e93fe35a339e0cff581efacce2f7117cd5652e9a270847bd7e2508b3e717b7b4156af54d4f896d63033e06ff9fafbf59a1d46fe17dd5e2a3f7846 - languageName: node - linkType: hard - -"mdast-util-gfm-autolink-literal@npm:^0.1.0": - version: 0.1.3 - resolution: "mdast-util-gfm-autolink-literal@npm:0.1.3" - dependencies: - ccount: "npm:^1.0.0" - mdast-util-find-and-replace: "npm:^1.1.0" - micromark: "npm:^2.11.3" - checksum: 10c0/155665a88a9b11fb5f8b6c5bff1a1e9d30f7381ff8c1864c7ede1eab4e312c51cef1e92e113cda174ebad40181350e555c303fa3293a1dc60b8945818d0af39a - languageName: node - linkType: hard - -"mdast-util-gfm-autolink-literal@npm:^1.0.0": - version: 1.0.3 - resolution: "mdast-util-gfm-autolink-literal@npm:1.0.3" - dependencies: - "@types/mdast": "npm:^3.0.0" - ccount: "npm:^2.0.0" - mdast-util-find-and-replace: "npm:^2.0.0" - micromark-util-character: "npm:^1.0.0" - checksum: 10c0/750e312eae73c3f2e8aa0e8c5232cb1b905357ff37ac236927f1af50cdbee7c2cfe2379b148ac32fa4137eeb3b24601e1bb6135084af926c7cd808867804193f - languageName: node - linkType: hard - -"mdast-util-gfm-strikethrough@npm:^0.2.0": - version: 0.2.3 - resolution: "mdast-util-gfm-strikethrough@npm:0.2.3" - dependencies: - mdast-util-to-markdown: "npm:^0.6.0" - checksum: 10c0/1de00913769c252add1f48fea547121d971ef7a8bfe6a89b775dea38aa319e6b10b6f514b492586aa7e660f8880b5c2390e411302a0b2386ed793f914b9eca71 - languageName: node - linkType: hard - -"mdast-util-gfm-strikethrough@npm:^1.0.0": - version: 1.0.3 - resolution: "mdast-util-gfm-strikethrough@npm:1.0.3" - dependencies: - "@types/mdast": "npm:^3.0.0" - mdast-util-to-markdown: "npm:^1.3.0" - checksum: 10c0/29616b3dfdd33d3cd13f9b3181a8562fa2fbacfcb04a37dba3c690ba6829f0231b145444de984726d9277b2bc90dd7d96fb9df9f6292d5e77d65a8659ee2f52b - languageName: node - linkType: hard - -"mdast-util-gfm-table@npm:^0.1.0": - version: 0.1.6 - resolution: "mdast-util-gfm-table@npm:0.1.6" - dependencies: - markdown-table: "npm:^2.0.0" - mdast-util-to-markdown: "npm:~0.6.0" - checksum: 10c0/a3b3fa2f91a44054dbe7e8a4cba1bcaa35255633da7850ad2688c60d1e1825d5d668774f31689d018d9f04cadc68f6055349048192c89a0e6c2ccb91a7ae7d1f - languageName: node - linkType: hard - -"mdast-util-gfm-table@npm:^1.0.0": - version: 1.0.7 - resolution: "mdast-util-gfm-table@npm:1.0.7" - dependencies: - "@types/mdast": "npm:^3.0.0" - markdown-table: "npm:^3.0.0" - mdast-util-from-markdown: "npm:^1.0.0" - mdast-util-to-markdown: "npm:^1.3.0" - checksum: 10c0/a37a05a936292c4f48394123332d3c034a6e1b15bb3e7f3b94e6bce3260c9184fd388abbc4100827edd5485a6563098306994d15a729bde3c96de7a62ed5720b - languageName: node - linkType: hard - -"mdast-util-gfm-task-list-item@npm:^0.1.0": - version: 0.1.6 - resolution: "mdast-util-gfm-task-list-item@npm:0.1.6" - dependencies: - mdast-util-to-markdown: "npm:~0.6.0" - checksum: 10c0/6b5b5239f031b630cd433cfd0bb30b7258dfac7d49c86a2c937127bc00fda186f798cf2a671507bcfad00f075d2d8779be9c109549052d98f1b4927e6e12d8be - languageName: node - linkType: hard - -"mdast-util-gfm-task-list-item@npm:^1.0.0": - version: 1.0.2 - resolution: "mdast-util-gfm-task-list-item@npm:1.0.2" - dependencies: - "@types/mdast": "npm:^3.0.0" - mdast-util-to-markdown: "npm:^1.3.0" - checksum: 10c0/91fa91f7d1a8797bf129008dab12d23917015ad12df00044e275b4459e8b383fbec6234338953a0089ef9c3a114d0a360c3e652eb0ebf6ece7e7a8fd3b5977c6 - languageName: node - linkType: hard - -"mdast-util-gfm@npm:^0.1.0": - version: 0.1.2 - resolution: "mdast-util-gfm@npm:0.1.2" - dependencies: - mdast-util-gfm-autolink-literal: "npm:^0.1.0" - mdast-util-gfm-strikethrough: "npm:^0.2.0" - mdast-util-gfm-table: "npm:^0.1.0" - mdast-util-gfm-task-list-item: "npm:^0.1.0" - mdast-util-to-markdown: "npm:^0.6.1" - checksum: 10c0/109c5f3e3340c25ecec5fb0b9b1a4137fb0948ffbc38ed4b85d477f3da471c2a475a84f2cb2569663768d6967aedf0f3a18b936ea907d0e34374f4eeaed18c5a - languageName: node - linkType: hard - -"mdast-util-gfm@npm:^1.0.0": - version: 1.0.0 - resolution: "mdast-util-gfm@npm:1.0.0" - dependencies: - mdast-util-gfm-autolink-literal: "npm:^1.0.0" - mdast-util-gfm-strikethrough: "npm:^1.0.0" - mdast-util-gfm-table: "npm:^1.0.0" - mdast-util-gfm-task-list-item: "npm:^1.0.0" - checksum: 10c0/9127b705ccfcce762f430f30b061ad049d0906172eba61b2c7cee52270e5da3516fe7f7c0d73cf562c51c1c4acc530cdc97ccc0ebb5c8797713c62dff040ace6 - languageName: node - linkType: hard - -"mdast-util-phrasing@npm:^3.0.0": - version: 3.0.1 - resolution: "mdast-util-phrasing@npm:3.0.1" - dependencies: - "@types/mdast": "npm:^3.0.0" - unist-util-is: "npm:^5.0.0" - checksum: 10c0/5e00e303652a7581593549dbce20dfb69d687d79a972f7928f6ca1920ef5385bceb737a3d5292ab6d937ed8c67bb59771e80e88f530b78734fe7d155f833e32b - languageName: node - linkType: hard - -"mdast-util-to-markdown@npm:^0.6.0, mdast-util-to-markdown@npm:^0.6.1, mdast-util-to-markdown@npm:~0.6.0": - version: 0.6.5 - resolution: "mdast-util-to-markdown@npm:0.6.5" - dependencies: - "@types/unist": "npm:^2.0.0" - longest-streak: "npm:^2.0.0" - mdast-util-to-string: "npm:^2.0.0" - parse-entities: "npm:^2.0.0" - repeat-string: "npm:^1.0.0" - zwitch: "npm:^1.0.0" - checksum: 10c0/716035b75a50394298eb31acee60a20d06310c7ebf83a3009908714d8c4058d636344932c9c054f1a26e8c6c20e2aafda3b87e003c16037b3e16b2d260a87463 - languageName: node - linkType: hard - -"mdast-util-to-markdown@npm:^1.0.1, mdast-util-to-markdown@npm:^1.3.0": - version: 1.5.0 - resolution: "mdast-util-to-markdown@npm:1.5.0" - dependencies: - "@types/mdast": "npm:^3.0.0" - "@types/unist": "npm:^2.0.0" - longest-streak: "npm:^3.0.0" - mdast-util-phrasing: "npm:^3.0.0" - mdast-util-to-string: "npm:^3.0.0" - micromark-util-decode-string: "npm:^1.0.0" - unist-util-visit: "npm:^4.0.0" - zwitch: "npm:^2.0.0" - checksum: 10c0/9831d14aa6c097750a90c7b87b4e814b040731c30606a794c9b136dc746633dd9ec07154ca97d4fec4eaf732cf89d14643424e2581732d6ee18c9b0e51ff7664 - languageName: node - linkType: hard - -"mdast-util-to-string@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-to-string@npm:2.0.0" - checksum: 10c0/a4231085133cdfec24644b694c13661e5a01d26716be0105b6792889faa04b8030e4abbf72d4be3363098b2b38b2b98f1f1f1f0858eb6580dc04e2aca1436a37 - languageName: node - linkType: hard - -"mdast-util-to-string@npm:^3.0.0, mdast-util-to-string@npm:^3.1.0": - version: 3.2.0 - resolution: "mdast-util-to-string@npm:3.2.0" - dependencies: - "@types/mdast": "npm:^3.0.0" - checksum: 10c0/112f4bf0f6758dcb95deffdcf37afba7eaecdfe2ee13252de031723094d4d55220e147326690a8b91244758e2d678e7aeb1fdd0fa6ef3317c979bc42effd9a21 - languageName: node - linkType: hard - "mdn-data@npm:2.0.28": version: 2.0.28 resolution: "mdn-data@npm:2.0.28" @@ -17593,309 +16979,6 @@ __metadata: languageName: node linkType: hard -"micromark-core-commonmark@npm:^1.0.1": - version: 1.1.0 - resolution: "micromark-core-commonmark@npm:1.1.0" - dependencies: - decode-named-character-reference: "npm:^1.0.0" - micromark-factory-destination: "npm:^1.0.0" - micromark-factory-label: "npm:^1.0.0" - micromark-factory-space: "npm:^1.0.0" - micromark-factory-title: "npm:^1.0.0" - micromark-factory-whitespace: "npm:^1.0.0" - micromark-util-character: "npm:^1.0.0" - micromark-util-chunked: "npm:^1.0.0" - micromark-util-classify-character: "npm:^1.0.0" - micromark-util-html-tag-name: "npm:^1.0.0" - micromark-util-normalize-identifier: "npm:^1.0.0" - micromark-util-resolve-all: "npm:^1.0.0" - micromark-util-subtokenize: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.1" - uvu: "npm:^0.5.0" - checksum: 10c0/b3bf7b7004ce7dbb3ae151dcca4db1d12546f1b943affb2418da4b90b9ce59357373c433ee2eea4c868aee0791dafa355aeed19f5ef2b0acaf271f32f1ecbe6a - languageName: node - linkType: hard - -"micromark-extension-gfm-autolink-literal@npm:~0.5.0": - version: 0.5.7 - resolution: "micromark-extension-gfm-autolink-literal@npm:0.5.7" - dependencies: - micromark: "npm:~2.11.3" - checksum: 10c0/4e56021641200cd88a9e05be531405bba007db9187554e06d0dfb5d8c49df67991322f2f952d6a25bbe3972ef0543a08d7ea00dff7b8577f8f3ca196c6544114 - languageName: node - linkType: hard - -"micromark-extension-gfm-strikethrough@npm:~0.6.5": - version: 0.6.5 - resolution: "micromark-extension-gfm-strikethrough@npm:0.6.5" - dependencies: - micromark: "npm:~2.11.0" - checksum: 10c0/c14e953b833718f56a71a650e9c2958fdb2b91093d7304043443eb64a8287cb8ff776d3cec0d40ca00ccd69357438f3dcac2cc40d3f16e47230cfbce72a1cf51 - languageName: node - linkType: hard - -"micromark-extension-gfm-table@npm:~0.4.0": - version: 0.4.3 - resolution: "micromark-extension-gfm-table@npm:0.4.3" - dependencies: - micromark: "npm:~2.11.0" - checksum: 10c0/0f4be3a1206024845bbc2495ea3b2a255bf5287af3747733d398adf962bfcf6f0c452dc66e268ab84f41b64a2f8113028887034045450bad43a48a8b5583bc14 - languageName: node - linkType: hard - -"micromark-extension-gfm-tagfilter@npm:~0.3.0": - version: 0.3.0 - resolution: "micromark-extension-gfm-tagfilter@npm:0.3.0" - checksum: 10c0/5a81cffbcad7f314ddb3b761c5e2db5a5286e231e68559861da821ee748838cc9323fd22af5cbbe68569e826fa8159f2f2b0d53dc8aecc458ef48b2503a071fb - languageName: node - linkType: hard - -"micromark-extension-gfm-task-list-item@npm:~0.3.0": - version: 0.3.3 - resolution: "micromark-extension-gfm-task-list-item@npm:0.3.3" - dependencies: - micromark: "npm:~2.11.0" - checksum: 10c0/e94e02eb2509a6ced49a6b296a7c503068488da79b5d3a3e4dfe5dcd5abdb95a1f305c087abb4ca3f7c90112ae29d628b30edeadaf53d3eec9dfe338bb678650 - languageName: node - linkType: hard - -"micromark-extension-gfm@npm:^0.3.0": - version: 0.3.3 - resolution: "micromark-extension-gfm@npm:0.3.3" - dependencies: - micromark: "npm:~2.11.0" - micromark-extension-gfm-autolink-literal: "npm:~0.5.0" - micromark-extension-gfm-strikethrough: "npm:~0.6.5" - micromark-extension-gfm-table: "npm:~0.4.0" - micromark-extension-gfm-tagfilter: "npm:~0.3.0" - micromark-extension-gfm-task-list-item: "npm:~0.3.0" - checksum: 10c0/6ed94c6213687b84c7b2dbacf8a50b078c60fd960bc9ddb3ec742fc298b8f7d5dcd8e9ab2a73fb8b423a0b11bf0a1565bc24bf45b45009f2693690277a7675df - languageName: node - linkType: hard - -"micromark-factory-destination@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-factory-destination@npm:1.1.0" - dependencies: - micromark-util-character: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/71ebd9089bf0c9689b98ef42215c04032ae2701ae08c3546b663628553255dca18e5310dbdacddad3acd8de4f12a789835fff30dadc4da3c4e30387a75e6b488 - languageName: node - linkType: hard - -"micromark-factory-label@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-factory-label@npm:1.1.0" - dependencies: - micromark-util-character: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - uvu: "npm:^0.5.0" - checksum: 10c0/5e2cd2d8214bb92a34dfcedf9c7aecf565e3648650a3a6a0495ededf15f2318dd214dc069e3026402792cd5839d395313f8ef9c2e86ca34a8facaa0f75a77753 - languageName: node - linkType: hard - -"micromark-factory-space@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-factory-space@npm:1.1.0" - dependencies: - micromark-util-character: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/3da81187ce003dd4178c7adc4674052fb8befc8f1a700ae4c8227755f38581a4ae963866dc4857488d62d1dc9837606c9f2f435fa1332f62a0f1c49b83c6a822 - languageName: node - linkType: hard - -"micromark-factory-title@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-factory-title@npm:1.1.0" - dependencies: - micromark-factory-space: "npm:^1.0.0" - micromark-util-character: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/cf8c687d1d5c3928846a4791d4a7e2f1d7bdd2397051e20d60f06b7565a48bf85198ab6f85735e997ab3f0cbb80b8b6391f4f7ebc0aae2f2f8c3a08541257bf6 - languageName: node - linkType: hard - -"micromark-factory-whitespace@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-factory-whitespace@npm:1.1.0" - dependencies: - micromark-factory-space: "npm:^1.0.0" - micromark-util-character: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/7248cc4534f9befb38c6f398b6e38efd3199f1428fc214c9cb7ed5b6e9fa7a82c0d8cdfa9bcacde62887c9a7c8c46baf5c318b2ae8f701afbccc8ad702e92dce - languageName: node - linkType: hard - -"micromark-util-character@npm:^1.0.0": - version: 1.2.0 - resolution: "micromark-util-character@npm:1.2.0" - dependencies: - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/3390a675a50731b58a8e5493cd802e190427f10fa782079b455b00f6b54e406e36882df7d4a3bd32b709f7a2c3735b4912597ebc1c0a99566a8d8d0b816e2cd4 - languageName: node - linkType: hard - -"micromark-util-chunked@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-chunked@npm:1.1.0" - dependencies: - micromark-util-symbol: "npm:^1.0.0" - checksum: 10c0/59534cf4aaf481ed58d65478d00eae0080df9b5816673f79b5ddb0cea263e5a9ee9cbb6cc565daf1eb3c8c4ff86fc4e25d38a0577539655cda823a4249efd358 - languageName: node - linkType: hard - -"micromark-util-classify-character@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-classify-character@npm:1.1.0" - dependencies: - micromark-util-character: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/3266453dc0fdaf584e24c9b3c91d1ed180f76b5856699c51fd2549305814fcab7ec52afb4d3e83d002a9115cd2d2b2ffdc9c0b38ed85120822bf515cc00636ec - languageName: node - linkType: hard - -"micromark-util-combine-extensions@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-combine-extensions@npm:1.1.0" - dependencies: - micromark-util-chunked: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/0bc572fab3fe77f533c29aa1b75cb847b9fc9455f67a98623ef9740b925c0b0426ad9f09bbb56f1e844ea9ebada7873d1f06d27f7c979a917692b273c4b69e31 - languageName: node - linkType: hard - -"micromark-util-decode-numeric-character-reference@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-decode-numeric-character-reference@npm:1.1.0" - dependencies: - micromark-util-symbol: "npm:^1.0.0" - checksum: 10c0/64ef2575e3fc2426976c19e16973348f20b59ddd5543f1467ac2e251f29e0a91f12089703d29ae985b0b9a408ee0d72f06d04ed3920811aa2402aabca3bdf9e4 - languageName: node - linkType: hard - -"micromark-util-decode-string@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-decode-string@npm:1.1.0" - dependencies: - decode-named-character-reference: "npm:^1.0.0" - micromark-util-character: "npm:^1.0.0" - micromark-util-decode-numeric-character-reference: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - checksum: 10c0/757a0aaa5ad6c50c7480bd75371d407ac75f5022cd4404aba07adadf1448189502aea9bb7b2d09d25e18745e0abf72b95506b6beb184bcccabe919e48e3a5df7 - languageName: node - linkType: hard - -"micromark-util-encode@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-encode@npm:1.1.0" - checksum: 10c0/9878c9bc96999d45626a7597fffac85348ea842dce75d2417345cbf070a9941c62477bd0963bef37d4f0fd29f2982be6ddf416d62806f00ccb334af9d6ee87e7 - languageName: node - linkType: hard - -"micromark-util-html-tag-name@npm:^1.0.0": - version: 1.2.0 - resolution: "micromark-util-html-tag-name@npm:1.2.0" - checksum: 10c0/15421869678d36b4fe51df453921e8186bff514a14e9f79f32b7e1cdd67874e22a66ad34a7f048dd132cbbbfc7c382ae2f777a2bfd1f245a47705dc1c6d4f199 - languageName: node - linkType: hard - -"micromark-util-normalize-identifier@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-normalize-identifier@npm:1.1.0" - dependencies: - micromark-util-symbol: "npm:^1.0.0" - checksum: 10c0/a9657321a2392584e4d978061882117a84db7d2c2c1c052c0f5d25da089d463edb9f956d5beaf7f5768984b6f72d046d59b5972951ec7bf25397687a62b8278a - languageName: node - linkType: hard - -"micromark-util-resolve-all@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-resolve-all@npm:1.1.0" - dependencies: - micromark-util-types: "npm:^1.0.0" - checksum: 10c0/b5c95484c06e87bbbb60d8430eb030a458733a5270409f4c67892d1274737087ca6a7ca888987430e57cf1dcd44bb16390d3b3936a2bf07f7534ec8f52ce43c9 - languageName: node - linkType: hard - -"micromark-util-sanitize-uri@npm:^1.0.0": - version: 1.2.0 - resolution: "micromark-util-sanitize-uri@npm:1.2.0" - dependencies: - micromark-util-character: "npm:^1.0.0" - micromark-util-encode: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - checksum: 10c0/dbdb98248e9f0408c7a00f1c1cd805775b41d213defd659533835f34b38da38e8f990bf7b3f782e96bffbc549aec9c3ecdab197d4ad5adbfe08f814a70327b6e - languageName: node - linkType: hard - -"micromark-util-subtokenize@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-subtokenize@npm:1.1.0" - dependencies: - micromark-util-chunked: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.0" - uvu: "npm:^0.5.0" - checksum: 10c0/f292b1b162845db50d36255c9d4c4c6d47931fbca3ac98a80c7e536d2163233fd662f8ca0479ee2b80f145c66a1394c7ed17dfce801439741211015e77e3901e - languageName: node - linkType: hard - -"micromark-util-symbol@npm:^1.0.0": - version: 1.1.0 - resolution: "micromark-util-symbol@npm:1.1.0" - checksum: 10c0/10ceaed33a90e6bfd3a5d57053dbb53f437d4809cc11430b5a09479c0ba601577059be9286df4a7eae6e350a60a2575dc9fa9d9872b5b8d058c875e075c33803 - languageName: node - linkType: hard - -"micromark-util-types@npm:^1.0.0, micromark-util-types@npm:^1.0.1": - version: 1.1.0 - resolution: "micromark-util-types@npm:1.1.0" - checksum: 10c0/a9749cb0a12a252ff536baabcb7012421b6fad4d91a5fdd80d7b33dc7b4c22e2d0c4637dfe5b902d00247fe6c9b01f4a24fce6b572b16ccaa4da90e6ce2a11e4 - languageName: node - linkType: hard - -"micromark@npm:^2.11.3, micromark@npm:~2.11.0, micromark@npm:~2.11.3": - version: 2.11.4 - resolution: "micromark@npm:2.11.4" - dependencies: - debug: "npm:^4.0.0" - parse-entities: "npm:^2.0.0" - checksum: 10c0/67307cbacae621ab1eb23e333a5addc7600cf97d3b40cad22fc1c2d03d734d6d9cbc3f5a7e5d655a8c0862a949abe590ab7cfa96be366bfe09e239a94e6eea55 - languageName: node - linkType: hard - -"micromark@npm:^3.0.0": - version: 3.2.0 - resolution: "micromark@npm:3.2.0" - dependencies: - "@types/debug": "npm:^4.0.0" - debug: "npm:^4.0.0" - decode-named-character-reference: "npm:^1.0.0" - micromark-core-commonmark: "npm:^1.0.1" - micromark-factory-space: "npm:^1.0.0" - micromark-util-character: "npm:^1.0.0" - micromark-util-chunked: "npm:^1.0.0" - micromark-util-combine-extensions: "npm:^1.0.0" - micromark-util-decode-numeric-character-reference: "npm:^1.0.0" - micromark-util-encode: "npm:^1.0.0" - micromark-util-normalize-identifier: "npm:^1.0.0" - micromark-util-resolve-all: "npm:^1.0.0" - micromark-util-sanitize-uri: "npm:^1.0.0" - micromark-util-subtokenize: "npm:^1.0.0" - micromark-util-symbol: "npm:^1.0.0" - micromark-util-types: "npm:^1.0.1" - uvu: "npm:^0.5.0" - checksum: 10c0/f243e805d1b3cc699fddae2de0b1492bc82462f1a709d7ae5c82039f88b1e009c959100184717e748be057b5f88603289d5681679a4e6fbabcd037beb34bc744 - languageName: node - linkType: hard - "micromatch@npm:^2.1.5": version: 2.3.11 resolution: "micromatch@npm:2.3.11" @@ -19212,75 +18295,6 @@ __metadata: languageName: node linkType: hard -"oxc-resolver@npm:^11.9.0": - version: 11.19.1 - resolution: "oxc-resolver@npm:11.19.1" - dependencies: - "@oxc-resolver/binding-android-arm-eabi": "npm:11.19.1" - "@oxc-resolver/binding-android-arm64": "npm:11.19.1" - "@oxc-resolver/binding-darwin-arm64": "npm:11.19.1" - "@oxc-resolver/binding-darwin-x64": "npm:11.19.1" - "@oxc-resolver/binding-freebsd-x64": "npm:11.19.1" - "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.19.1" - "@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.19.1" - "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.19.1" - "@oxc-resolver/binding-linux-arm64-musl": "npm:11.19.1" - "@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.19.1" - "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.19.1" - "@oxc-resolver/binding-linux-riscv64-musl": "npm:11.19.1" - "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.19.1" - "@oxc-resolver/binding-linux-x64-gnu": "npm:11.19.1" - "@oxc-resolver/binding-linux-x64-musl": "npm:11.19.1" - "@oxc-resolver/binding-openharmony-arm64": "npm:11.19.1" - "@oxc-resolver/binding-wasm32-wasi": "npm:11.19.1" - "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.19.1" - "@oxc-resolver/binding-win32-ia32-msvc": "npm:11.19.1" - "@oxc-resolver/binding-win32-x64-msvc": "npm:11.19.1" - dependenciesMeta: - "@oxc-resolver/binding-android-arm-eabi": - optional: true - "@oxc-resolver/binding-android-arm64": - optional: true - "@oxc-resolver/binding-darwin-arm64": - optional: true - "@oxc-resolver/binding-darwin-x64": - optional: true - "@oxc-resolver/binding-freebsd-x64": - optional: true - "@oxc-resolver/binding-linux-arm-gnueabihf": - optional: true - "@oxc-resolver/binding-linux-arm-musleabihf": - optional: true - "@oxc-resolver/binding-linux-arm64-gnu": - optional: true - "@oxc-resolver/binding-linux-arm64-musl": - optional: true - "@oxc-resolver/binding-linux-ppc64-gnu": - optional: true - "@oxc-resolver/binding-linux-riscv64-gnu": - optional: true - "@oxc-resolver/binding-linux-riscv64-musl": - optional: true - "@oxc-resolver/binding-linux-s390x-gnu": - optional: true - "@oxc-resolver/binding-linux-x64-gnu": - optional: true - "@oxc-resolver/binding-linux-x64-musl": - optional: true - "@oxc-resolver/binding-openharmony-arm64": - optional: true - "@oxc-resolver/binding-wasm32-wasi": - optional: true - "@oxc-resolver/binding-win32-arm64-msvc": - optional: true - "@oxc-resolver/binding-win32-ia32-msvc": - optional: true - "@oxc-resolver/binding-win32-x64-msvc": - optional: true - checksum: 10c0/8ac4eaffa9c0bcbb9f4f4a2b43786457ec5a68684d8776cb78b5a15ce3d1a79d3e67262aa3c635f98a0c1cd6cd56a31fcb05bffb9a286100056e4ab06b928833 - languageName: node - linkType: hard - "p-event@npm:^4.2.0": version: 4.2.0 resolution: "p-event@npm:4.2.0" @@ -19487,20 +18501,6 @@ __metadata: languageName: node linkType: hard -"parse-entities@npm:^2.0.0": - version: 2.0.0 - resolution: "parse-entities@npm:2.0.0" - dependencies: - character-entities: "npm:^1.0.0" - character-entities-legacy: "npm:^1.0.0" - character-reference-invalid: "npm:^1.0.0" - is-alphanumerical: "npm:^1.0.0" - is-decimal: "npm:^1.0.0" - is-hexadecimal: "npm:^1.0.0" - checksum: 10c0/f85a22c0ea406ff26b53fdc28641f01cc36fa49eb2e3135f02693286c89ef0bcefc2262d99b3688e20aac2a14fd10b75c518583e875c1b9fe3d1f937795e0854 - languageName: node - linkType: hard - "parse-glob@npm:^3.0.4": version: 3.0.4 resolution: "parse-glob@npm:3.0.4" @@ -21290,25 +20290,6 @@ __metadata: languageName: node linkType: hard -"remark-gfm@npm:^1.0.0": - version: 1.0.0 - resolution: "remark-gfm@npm:1.0.0" - dependencies: - mdast-util-gfm: "npm:^0.1.0" - micromark-extension-gfm: "npm:^0.3.0" - checksum: 10c0/929a2328b1a0c63c38cc1678a41089f75f594fb928c02bfcfe967702377ede245fec0ed45a258fe0af421dda547439911260b8621b2ea6819eaa5f6b47d2bb4c - languageName: node - linkType: hard - -"remark-stringify@npm:^9.0.1": - version: 9.0.1 - resolution: "remark-stringify@npm:9.0.1" - dependencies: - mdast-util-to-markdown: "npm:^0.6.0" - checksum: 10c0/3d3b3736f993f94b66f7af60f9d20481e1bd6d262a7c141809d3bb1b3a5eaea3a5f51b56672aad57f0c7d43654448f95254ed4e9fab53964cafe0dce6dfa87ae - languageName: node - linkType: hard - "remove-trailing-separator@npm:^1.0.1": version: 1.1.0 resolution: "remove-trailing-separator@npm:1.1.0" @@ -21336,7 +20317,7 @@ __metadata: languageName: node linkType: hard -"repeat-string@npm:^1.0.0, repeat-string@npm:^1.5.2, repeat-string@npm:^1.6.1": +"repeat-string@npm:^1.5.2, repeat-string@npm:^1.6.1": version: 1.6.1 resolution: "repeat-string@npm:1.6.1" checksum: 10c0/87fa21bfdb2fbdedc44b9a5b118b7c1239bdd2c2c1e42742ef9119b7d412a5137a1d23f1a83dc6bb686f4f27429ac6f542e3d923090b44181bafa41e8ac0174d @@ -21804,42 +20785,6 @@ __metadata: languageName: node linkType: hard -"rs-module-lexer@npm:^2.5.1": - version: 2.6.0 - resolution: "rs-module-lexer@npm:2.6.0" - dependencies: - "@xn-sakina/rml-darwin-arm64": "npm:2.6.0" - "@xn-sakina/rml-darwin-x64": "npm:2.6.0" - "@xn-sakina/rml-linux-arm-gnueabihf": "npm:2.6.0" - "@xn-sakina/rml-linux-arm64-gnu": "npm:2.6.0" - "@xn-sakina/rml-linux-arm64-musl": "npm:2.6.0" - "@xn-sakina/rml-linux-x64-gnu": "npm:2.6.0" - "@xn-sakina/rml-linux-x64-musl": "npm:2.6.0" - "@xn-sakina/rml-win32-arm64-msvc": "npm:2.6.0" - "@xn-sakina/rml-win32-x64-msvc": "npm:2.6.0" - dependenciesMeta: - "@xn-sakina/rml-darwin-arm64": - optional: true - "@xn-sakina/rml-darwin-x64": - optional: true - "@xn-sakina/rml-linux-arm-gnueabihf": - optional: true - "@xn-sakina/rml-linux-arm64-gnu": - optional: true - "@xn-sakina/rml-linux-arm64-musl": - optional: true - "@xn-sakina/rml-linux-x64-gnu": - optional: true - "@xn-sakina/rml-linux-x64-musl": - optional: true - "@xn-sakina/rml-win32-arm64-msvc": - optional: true - "@xn-sakina/rml-win32-x64-msvc": - optional: true - checksum: 10c0/a40d6d5708446ebb55974a4907cb0eac3651b79290b7676361bbc3733fb59c8dabd3d50c1de04b148509b4fddee9cb49a3eaa33994987644f3bb877bb192336c - languageName: node - linkType: hard - "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -21865,7 +20810,7 @@ __metadata: languageName: node linkType: hard -"sade@npm:^1.7.3, sade@npm:~1.8.1": +"sade@npm:~1.8.1": version: 1.8.1 resolution: "sade@npm:1.8.1" dependencies: @@ -22271,7 +21216,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.4, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4": +"semver@npm:7.7.4, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4": version: 7.7.4 resolution: "semver@npm:7.7.4" bin: @@ -24049,13 +22994,6 @@ __metadata: languageName: node linkType: hard -"trough@npm:^1.0.0": - version: 1.0.5 - resolution: "trough@npm:1.0.5" - checksum: 10c0/f036d0d7f9bc7cfe5ee650d70b57bb1f048f3292adf6c81bb9b228e546b2b2e5b74ea04a060d21472108a8cda05ec4814bbe86f87ee35c182c50cb41b5c1810a - languageName: node - linkType: hard - "ts-api-utils@npm:^1.0.1": version: 1.4.3 resolution: "ts-api-utils@npm:1.4.3" @@ -24362,16 +23300,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.4.2": - version: 5.4.5 - resolution: "typescript@npm:5.4.5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A^2.9.2 || ^3.0.0 || ^4.0.0#optional!builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" @@ -24392,16 +23320,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A~5.4.2#optional!builtin": - version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9 - languageName: node - linkType: hard - "typical@npm:^4.0.0": version: 4.0.0 resolution: "typical@npm:4.0.0" @@ -24520,20 +23438,6 @@ __metadata: languageName: node linkType: hard -"unified@npm:^9.2.1": - version: 9.2.2 - resolution: "unified@npm:9.2.2" - dependencies: - bail: "npm:^1.0.0" - extend: "npm:^3.0.0" - is-buffer: "npm:^2.0.0" - is-plain-obj: "npm:^2.0.0" - trough: "npm:^1.0.0" - vfile: "npm:^4.0.0" - checksum: 10c0/a66d71b039c24626802a4664a1f3210f29ab1f75b89fd41933e6ab00561e1ec43a5bec6de32c7ebc86544e5f00ef5836e8fe79a823e81e35825de4e35823eda9 - languageName: node - linkType: hard - "union-value@npm:^1.0.0": version: 1.0.1 resolution: "union-value@npm:1.0.1" @@ -24582,71 +23486,6 @@ __metadata: languageName: node linkType: hard -"unist-util-is@npm:^4.0.0": - version: 4.1.0 - resolution: "unist-util-is@npm:4.1.0" - checksum: 10c0/21ca3d7bacc88853b880b19cb1b133a056c501617d7f9b8cce969cd8b430ed7e1bc416a3a11b02540d5de6fb86807e169d00596108a459d034cf5faec97c055e - languageName: node - linkType: hard - -"unist-util-is@npm:^5.0.0": - version: 5.2.1 - resolution: "unist-util-is@npm:5.2.1" - dependencies: - "@types/unist": "npm:^2.0.0" - checksum: 10c0/a2376910b832bb10653d2167c3cd85b3610a5fd53f5169834c08b3c3a720fae9043d75ad32d727eedfc611491966c26a9501d428ec62467edc17f270feb5410b - languageName: node - linkType: hard - -"unist-util-stringify-position@npm:^2.0.0": - version: 2.0.3 - resolution: "unist-util-stringify-position@npm:2.0.3" - dependencies: - "@types/unist": "npm:^2.0.2" - checksum: 10c0/46fa03f840df173b7f032cbfffdb502fb05b79b3fb5451681c796cf4985d9087a537833f5afb75d55e79b46bbbe4b3d81dd75a1062f9289091c526aebe201d5d - languageName: node - linkType: hard - -"unist-util-stringify-position@npm:^3.0.0": - version: 3.0.3 - resolution: "unist-util-stringify-position@npm:3.0.3" - dependencies: - "@types/unist": "npm:^2.0.0" - checksum: 10c0/14550027825230528f6437dad7f2579a841780318569851291be6c8a970bae6f65a7feb24dabbcfce0e5e68cacae85bf12cbda3f360f7c873b4db602bdf7bb21 - languageName: node - linkType: hard - -"unist-util-visit-parents@npm:^3.0.0": - version: 3.1.1 - resolution: "unist-util-visit-parents@npm:3.1.1" - dependencies: - "@types/unist": "npm:^2.0.0" - unist-util-is: "npm:^4.0.0" - checksum: 10c0/231c80c5ba8e79263956fcaa25ed2a11ad7fe77ac5ba0d322e9d51bbc4238501e3bb52f405e518bcdc5471e27b33eff520db0aa4a3b1feb9fb6e2de6ae385d49 - languageName: node - linkType: hard - -"unist-util-visit-parents@npm:^5.0.0, unist-util-visit-parents@npm:^5.1.1": - version: 5.1.3 - resolution: "unist-util-visit-parents@npm:5.1.3" - dependencies: - "@types/unist": "npm:^2.0.0" - unist-util-is: "npm:^5.0.0" - checksum: 10c0/f6829bfd8f2eddf63a32e2c302cd50978ef0c194b792c6fe60c2b71dfd7232415a3c5941903972543e9d34e6a8ea69dee9ccd95811f4a795495ed2ae855d28d0 - languageName: node - linkType: hard - -"unist-util-visit@npm:^4.0.0": - version: 4.1.2 - resolution: "unist-util-visit@npm:4.1.2" - dependencies: - "@types/unist": "npm:^2.0.0" - unist-util-is: "npm:^5.0.0" - unist-util-visit-parents: "npm:^5.1.1" - checksum: 10c0/56a1f49a4d8e321e75b3c7821d540a45165a031dd06324bb0e8c75e7737bc8d73bdddbf0b0ca82000f9708a4c36861c6ebe88d01f7cf00e925f5d75f13a3a017 - languageName: node - linkType: hard - "universalify@npm:^0.1.0": version: 0.1.2 resolution: "universalify@npm:0.1.2" @@ -24828,20 +23667,6 @@ __metadata: languageName: node linkType: hard -"uvu@npm:^0.5.0": - version: 0.5.6 - resolution: "uvu@npm:0.5.6" - dependencies: - dequal: "npm:^2.0.0" - diff: "npm:^5.0.0" - kleur: "npm:^4.0.3" - sade: "npm:^1.7.3" - bin: - uvu: bin.js - checksum: 10c0/ad32eb5f7d94bdeb71f80d073003f0138e24f61ed68cecc8e15d2f30838f44c9670577bb1775c8fac894bf93d1bc1583d470a9195e49bfa6efa14cc6f4942bff - languageName: node - linkType: hard - "validate-npm-package-license@npm:^3.0.4": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -24873,28 +23698,6 @@ __metadata: languageName: node linkType: hard -"vfile-message@npm:^2.0.0": - version: 2.0.4 - resolution: "vfile-message@npm:2.0.4" - dependencies: - "@types/unist": "npm:^2.0.0" - unist-util-stringify-position: "npm:^2.0.0" - checksum: 10c0/ce50d90e0e5dc8f995f39602dd2404f1756388a54209c983d259b17c15e6f262a53546977a638065bc487d0657799fa96f4c1ba6b2915d9724a4968e9c7ff1c8 - languageName: node - linkType: hard - -"vfile@npm:^4.0.0": - version: 4.2.1 - resolution: "vfile@npm:4.2.1" - dependencies: - "@types/unist": "npm:^2.0.0" - is-buffer: "npm:^2.0.0" - unist-util-stringify-position: "npm:^2.0.0" - vfile-message: "npm:^2.0.0" - checksum: 10c0/4816aecfedc794ba4d3131abff2032ef0e825632cfa8cd20dd9d83819ef260589924f4f3e8fa30e06da2d8e60d7ec8ef7d0af93e0483df62890738258daf098a - languageName: node - linkType: hard - "vite@npm:7.3.1": version: 7.3.1 resolution: "vite@npm:7.3.1" @@ -25821,17 +24624,3 @@ __metadata: checksum: 10c0/6f3638310e89697b0a21a4e7282b2505f26fb32dabb6ca1f09b721279e8512436efff199db0c7e3c7b7d11400dd42c88fa5cffc85984db56b9ac497a0050fe11 languageName: node linkType: hard - -"zwitch@npm:^1.0.0": - version: 1.0.5 - resolution: "zwitch@npm:1.0.5" - checksum: 10c0/26dc7d32e5596824b565db1da9650d00d32659c1211195bef50c25c60820f9c942aa7abefe678fc1ed0b97c1755036ac1bde5f97881d7d0e73e04e02aca56957 - languageName: node - linkType: hard - -"zwitch@npm:^2.0.0": - version: 2.0.4 - resolution: "zwitch@npm:2.0.4" - checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e - languageName: node - linkType: hard From 5b538a13cbd1e70e71e4c2d18448818efd9617f1 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 9 Apr 2026 19:46:55 +0200 Subject: [PATCH 23/72] =?UTF-8?q?=F0=9F=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 23 ++++++++++++----------- docs/components/tsdown.config.ts | 15 ++++++++++++++- docs/website/eleventy.config.js | 6 +++--- docs/website/src/js/main.js | 12 ++++++------ docs/website/src/utils/manifest.js | 2 +- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/docs/components/package.json b/docs/components/package.json index ab54ddcae4..e283b7bf77 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -10,16 +10,16 @@ "url": "https://github.com/sl-design-system/components.git" }, "exports": { - "./copy-button/copy-button": "./dist/copy-button/copy-button.js", - "./example/example": "./dist/example/example.js", - "./install-info/install-info": "./dist/install-info/install-info.js", - "./page-toc/page-toc": "./dist/page-toc/page-toc.js", - "./search/search": "./dist/search/search.js", - "./sidebar/sidebar": "./dist/sidebar/sidebar.js", - "./site-nav/nav-group": "./dist/site-nav/nav-group.js", - "./site-nav/nav-item": "./dist/site-nav/nav-item.js", - "./site-nav/site-nav": "./dist/site-nav/site-nav.js", - "./theme-switch/theme-switch": "./dist/theme-switch/theme-switch.js", + "./copy-button/copy-button.js": "./dist/copy-button/copy-button.js", + "./example/example.js": "./dist/example/example.js", + "./install-info/install-info.js": "./dist/install-info/install-info.js", + "./page-toc/page-toc.js": "./dist/page-toc/page-toc.js", + "./search/search.js": "./dist/search/search.js", + "./sidebar/sidebar.js": "./dist/sidebar/sidebar.js", + "./site-nav/nav-group.js": "./dist/site-nav/nav-group.js", + "./site-nav/nav-item.js": "./dist/site-nav/nav-item.js", + "./site-nav/site-nav.js": "./dist/site-nav/site-nav.js", + "./theme-switch/theme-switch.js": "./dist/theme-switch/theme-switch.js", "./package.json": "./package.json" }, "scripts": { @@ -73,6 +73,7 @@ "@floating-ui/utils": "0.2.11", "@fortawesome/free-brands-svg-icons": "7.2.0", "@fortawesome/pro-regular-svg-icons": "7.2.0", - "@fortawesome/pro-solid-svg-icons": "7.2.0" + "@fortawesome/pro-solid-svg-icons": "7.2.0", + "@open-wc/dedupe-mixin": "2.0.1" } } diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index ac8abf0904..2d052c064f 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -57,7 +57,20 @@ export default defineConfig({ 'src/site-nav/site-nav.ts', 'src/theme-switch/theme-switch.ts' ], - exports: true, + exports: { + customExports(exports) { + return Object.fromEntries( + Object.entries(exports).map(([key, value]) => { + // Skip the root export and keys that already have a file extension (e.g. './foo.js') + if (key === '.' || /\.[^/]+$/.test(key)) { + return [key, value]; + } + + return [`${key}.js`, value]; + }) + ); + } + }, platform: 'browser', plugins: [cssPlugin] }) diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index d4cd32bed4..4a61e79b90 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -1,6 +1,6 @@ import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; -import { dirname, join } from 'node:path'; +import { basename, dirname, join } from 'node:path'; import * as esbuild from 'esbuild'; import eleventyNavigationPlugin from '@11ty/eleventy-navigation'; import markdownItAnchor from 'markdown-it-anchor'; @@ -48,11 +48,11 @@ export default function (eleventyConfig) { eleventyConfig.addCollection('componentPages', function (collectionApi) { const componentPages = collectionApi.getFilteredByGlob( - path.join(eleventyConfig.directories.input, 'components/**/*.md') + join(eleventyConfig.directories.input, 'components/**/*.md') ); return componentPages.map(page => { - const componentName = path.basename(page.inputPath, '.md'), + const componentName = basename(page.inputPath, '.md'), tagName = `sl-${componentName}`, component = allComponents.find(c => c.tagName === tagName); diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index 77b765ec62..0f1409c6d0 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -1,11 +1,11 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; import './icons.js'; -import { InstallInfo } from '@sl-design-system/doc-components/install-info/install-info'; -import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc'; -import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar'; -import { NavGroup } from '@sl-design-system/doc-components/site-nav/nav-group'; -import { NavItem } from '@sl-design-system/doc-components/site-nav/nav-item'; -import { SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav'; +import { InstallInfo } from '@sl-design-system/doc-components/install-info/install-info.js'; +import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc.js'; +import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar.js'; +import { NavGroup } from '@sl-design-system/doc-components/site-nav/nav-group.js'; +import { NavItem } from '@sl-design-system/doc-components/site-nav/nav-item.js'; +import { SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav.js'; customElements.define('doc-install-info', InstallInfo); customElements.define('doc-nav-group', NavGroup); diff --git a/docs/website/src/utils/manifest.js b/docs/website/src/utils/manifest.js index ae8a906522..2d47e0fa40 100644 --- a/docs/website/src/utils/manifest.js +++ b/docs/website/src/utils/manifest.js @@ -1,3 +1,3 @@ -export function getComponent() { +export function getComponents() { return []; } From 53e91a3db852f03137531996bef059417879dc89 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 14 Apr 2026 08:37:30 +0200 Subject: [PATCH 24/72] =?UTF-8?q?=F0=9F=90=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 18 +- .../src/code-example/code-example.css | 22 ++ .../src/code-example/code-example.spec.ts | 58 +++ .../src/code-example/code-example.stories.ts | 40 ++ .../src/code-example/code-example.ts | 37 ++ docs/components/src/example/example.css | 119 ------ docs/components/src/example/example.spec.ts | 340 ----------------- .../components/src/example/example.stories.ts | 135 ------- docs/components/src/example/example.ts | 204 ---------- docs/components/tsdown.config.ts | 2 +- docs/website/eleventy.config.js | 58 ++- docs/website/package.json | 10 +- .../content/api-reference/api-reference.md | 2 +- .../src/content/api-reference/button.md | 2 +- .../src/content/components/actions/actions.md | 2 +- .../src/content/components/actions/button.md | 84 +++-- .../src/content/components/components.md | 2 +- .../src/content/components/form/date-field.md | 2 +- .../src/content/components/form/form.md | 2 +- .../src/content/components/form/text-field.md | 2 +- .../src/content/components/form/time-field.md | 2 +- .../src/content/components/grid/grid.md | 2 +- .../src/content/components/layout/layout.md | 2 +- .../components/navigation/navigation.md | 2 +- .../src/content/components/overlay/overlay.md | 2 +- .../src/content/components/status/status.md | 2 +- .../content/components/utilities/utilities.md | 2 +- .../src/content/design-tokens/border.md | 2 +- .../src/content/design-tokens/color.md | 2 +- .../content/design-tokens/design-tokens.md | 2 +- .../src/content/introduction/documentation.md | 2 +- docs/website/src/content/introduction/faq.md | 2 +- .../getting-started/for-designers.md | 2 +- .../getting-started/for-developers.md | 2 +- .../getting-started/getting-started.md | 2 +- .../src/content/introduction/introduction.md | 2 +- docs/website/src/css/main.css | 80 +++- docs/website/src/includes/base.njk | 20 +- docs/website/src/js/main.js | 8 + docs/website/src/js/theme.js | 3 + docs/website/src/layouts/api-reference.njk | 1 + docs/website/src/layouts/component.njk | 16 + docs/website/src/layouts/design-tokens.njk | 1 + docs/website/src/layouts/docs.njk | 3 + .../website/src/transformers/code-examples.js | 37 ++ .../src/transformers/highlight-code.js | 46 +++ docs/website/src/utils/manifest.js | 90 ++++- docs/website/src/utils/markdown.js | 21 ++ package.json | 2 +- packages/components/button/src/button.ts | 1 + .../components/date-field/src/date-field.ts | 1 + packages/components/grid/src/grid.ts | 2 + .../components/text-field/src/text-field.ts | 1 + .../components/time-field/src/time-field.ts | 1 + yarn.lock | 350 +++++++++--------- 55 files changed, 805 insertions(+), 1052 deletions(-) create mode 100644 docs/components/src/code-example/code-example.css create mode 100644 docs/components/src/code-example/code-example.spec.ts create mode 100644 docs/components/src/code-example/code-example.stories.ts create mode 100644 docs/components/src/code-example/code-example.ts delete mode 100644 docs/components/src/example/example.css delete mode 100644 docs/components/src/example/example.spec.ts delete mode 100644 docs/components/src/example/example.stories.ts delete mode 100644 docs/components/src/example/example.ts create mode 100644 docs/website/src/js/theme.js create mode 100644 docs/website/src/layouts/api-reference.njk create mode 100644 docs/website/src/layouts/component.njk create mode 100644 docs/website/src/layouts/design-tokens.njk create mode 100644 docs/website/src/layouts/docs.njk create mode 100644 docs/website/src/transformers/code-examples.js create mode 100644 docs/website/src/transformers/highlight-code.js create mode 100644 docs/website/src/utils/markdown.js diff --git a/docs/components/package.json b/docs/components/package.json index e283b7bf77..3a4e5f4571 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -10,8 +10,8 @@ "url": "https://github.com/sl-design-system/components.git" }, "exports": { + "./code-example/code-example.js": "./dist/code-example/code-example.js", "./copy-button/copy-button.js": "./dist/copy-button/copy-button.js", - "./example/example.js": "./dist/example/example.js", "./install-info/install-info.js": "./dist/install-info/install-info.js", "./page-toc/page-toc.js": "./dist/page-toc/page-toc.js", "./search/search.js": "./dist/search/search.js", @@ -49,17 +49,17 @@ }, "devDependencies": { "@roenlie/vite-plugin-import-css-sheet": "^0.0.7", - "@storybook/addon-a11y": "^10.3.3", - "@storybook/addon-docs": "^10.3.3", - "@storybook/addon-vitest": "^10.3.3", - "@storybook/web-components": "^10.3.3", - "@storybook/web-components-vite": "^10.3.3", + "@storybook/addon-a11y": "^10.3.5", + "@storybook/addon-docs": "^10.3.5", + "@storybook/addon-vitest": "^10.3.5", + "@storybook/web-components": "^10.3.5", + "@storybook/web-components-vite": "^10.3.5", "@tsdown/css": "^0.21.7", "@types/prismjs": "^1.26.6", - "@typescript/native-preview": "^7.0.0-dev.20260315.1", + "@typescript/native-preview": "^7.0.0-dev.20260413.1", "lit": "^3.3.2", - "sass-embedded": "^1.98.0", - "storybook": "^10.3.3", + "sass-embedded": "^1.99.0", + "storybook": "^10.3.5", "tsdown": "^0.21.7", "typescript": "^5.9.3", "wireit": "^0.14.12" diff --git a/docs/components/src/code-example/code-example.css b/docs/components/src/code-example/code-example.css new file mode 100644 index 0000000000..f48ec856be --- /dev/null +++ b/docs/components/src/code-example/code-example.css @@ -0,0 +1,22 @@ +:host { + border: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + border-radius: var(--sl-size-050); + display: block; + overflow: clip; +} + +slot[name='source']::slotted(pre) { + background: transparent !important; + margin: 0 !important; + padding: 0 !important; +} + +.demo { + padding: var(--sl-size-200); +} + +.source { + background: var(--sl-color-background-accent-grey-subtlest); + border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + padding: var(--sl-size-200); +} diff --git a/docs/components/src/code-example/code-example.spec.ts b/docs/components/src/code-example/code-example.spec.ts new file mode 100644 index 0000000000..ee28faf638 --- /dev/null +++ b/docs/components/src/code-example/code-example.spec.ts @@ -0,0 +1,58 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { type LitElement, html } from 'lit'; +import { beforeEach, describe, expect, it } from 'vitest'; + +// Use dynamic import from dist to avoid CSS module resolution issues in browser tests +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const { DocCodeExample: DocCodeExampleClass } = await import('@sl-design-system/doc-components/code-example/code-example'); + +try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + customElements.define('doc-code-example', DocCodeExampleClass); +} catch { + /* empty */ +} + +describe('doc-code-example', () => { + let el: LitElement; + + beforeEach(async () => { + el = await fixture(html` + + +
<button>Click me</button>
+
+ `); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(DocCodeExampleClass); + }); + + it('should render a preview area', () => { + const preview = el.renderRoot.querySelector('.preview'); + + expect(preview).to.exist; + }); + + it('should render slotted content in the preview', () => { + const slot = el.renderRoot.querySelector('.preview slot'); + + expect(slot).to.exist; + expect(slot?.assignedElements()).to.have.length.greaterThan(0); + }); + + it('should render a source area', () => { + const source = el.renderRoot.querySelector('.source'); + + expect(source).to.exist; + }); + + it('should render the source slot', () => { + const slot = el.renderRoot.querySelector('slot[name="source"]'); + + expect(slot).to.exist; + expect(slot?.assignedElements()).to.have.length.greaterThan(0); + }); +}); diff --git a/docs/components/src/code-example/code-example.stories.ts b/docs/components/src/code-example/code-example.stories.ts new file mode 100644 index 0000000000..b54278bf69 --- /dev/null +++ b/docs/components/src/code-example/code-example.stories.ts @@ -0,0 +1,40 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { CodeExample } from './code-example.js'; + +type Story = StoryObj; + +try { + customElements.define('doc-code-example', CodeExample); +} catch { + /* empty */ +} + +export default { + title: 'Code Example' +} satisfies Meta; + +export const Basic: Story = { + render: () => { + const el = document.createElement('doc-code-example'); + + el.innerHTML = ` + +
<button type="button">Click me</button>
+ `; + + return el; + } +}; + +export const WithSource: Story = { + render: () => { + const el = document.createElement('doc-code-example'); + + el.innerHTML = ` +

Hello world

+
<p>Hello <strong>world</strong></p>
+ `; + + return el; + } +}; diff --git a/docs/components/src/code-example/code-example.ts b/docs/components/src/code-example/code-example.ts new file mode 100644 index 0000000000..d4117d486c --- /dev/null +++ b/docs/components/src/code-example/code-example.ts @@ -0,0 +1,37 @@ +import { type CSSResultGroup, LitElement, type PropertyValues, type TemplateResult, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import styles from './code-example.css' with { type: 'css' }; + +export class CodeExample extends LitElement { + /** @internal */ + static styles: CSSResultGroup = styles; + + /** @internal */ + #internals = this.attachInternals(); + + /** The alignment of the content within the demo area. */ + @property() align?: 'start' | 'center' | 'end' | 'fill'; + + override render(): TemplateResult { + return html` +
+ +
+
+ +
+ `; + } + + updated(changes: PropertyValues): void { + // Remove old value + if (changes.get('align')) { + this.#internals.states.delete(`align-${changes.get('align')}`); + } + + // Add new value + if (this.align) { + this.#internals.states.add(`align-${this.align}`); + } + } +} diff --git a/docs/components/src/example/example.css b/docs/components/src/example/example.css deleted file mode 100644 index 1cdcf37271..0000000000 --- a/docs/components/src/example/example.css +++ /dev/null @@ -1,119 +0,0 @@ -:host { - display: block; -} - -.preview { - border: 1px solid var(--sl-color-border-plain); - border-radius: var(--sl-size-050); - padding: var(--sl-size-200); -} - -.tabs { - display: flex; - gap: var(--sl-size-050); - margin-block-start: var(--sl-size-100); -} - -.panel { - background: var(--sl-color-surface-container-highest); - border-radius: var(--sl-size-050); - display: flex; - margin-block-start: var(--sl-size-100); - position: relative; -} - -pre { - flex: 1; - font: var(--sl-text-body-sm); - font-family: - ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', - 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; - margin: 0; - overflow-x: auto; - padding: var(--sl-size-150); - white-space: pre; -} - -code { - font: inherit; -} - -.copy { - background: none; - border: 1px solid var(--sl-color-border-plain); - border-radius: var(--sl-size-050); - color: var(--sl-color-foreground-plain); - cursor: pointer; - font: var(--sl-text-body-sm); - inset-block-start: var(--sl-size-100); - inset-inline-end: var(--sl-size-100); - padding: var(--sl-size-025) var(--sl-size-100); - position: absolute; - - &:hover { - background: var(--sl-color-surface-container-low); - } -} - -/* stylelint-disable color-no-hex -- Prism syntax highlighting tokens (Okaidia-inspired, scoped to shadow DOM) */ -.token.comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: #8292a2; -} - -.token.punctuation { - color: #f8f8f2; -} - -.token.namespace { - opacity: 0.7; -} - -.token.property, -.token.tag, -.token.constant, -.token.symbol, -.token.deleted { - color: #f92672; -} - -.token.boolean, -.token.number { - color: #ae81ff; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.inserted { - color: #a6e22e; -} - -.token.operator, -.token.entity, -.token.url, -.language-css .token.string, -.style .token.string, -.token.variable { - color: #f8f8f2; -} - -.token.atrule, -.token.attr-value, -.token.function, -.token.class-name { - color: #e6db74; -} - -.token.keyword { - color: #66d9ef; -} - -.token.regex, -.token.important { - color: #fd971f; -} diff --git a/docs/components/src/example/example.spec.ts b/docs/components/src/example/example.spec.ts deleted file mode 100644 index 8e42d83ef0..0000000000 --- a/docs/components/src/example/example.spec.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { fixture } from '@sl-design-system/vitest-browser-lit'; -import { type LitElement, html } from 'lit'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -// Use dynamic import from dist to avoid CSS module resolution issues in browser tests -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -const { DocExample: DocExampleClass } = await import('@sl-design-system/doc-components/example/example'); - -try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - customElements.define('doc-example', DocExampleClass); -} catch { - /* empty */ -} - -describe('doc-example', () => { - describe('all languages', () => { - let el: LitElement & { selected?: string }; - - beforeEach(async () => { - el = await fixture(html` - - - - - - `); - }); - - it('should render', () => { - expect(el).to.exist; - expect(el).to.be.instanceOf(DocExampleClass); - }); - - it('should have html selected by default', () => { - expect(el.selected).to.equal('html'); - }); - - it('should render a preview area', () => { - const preview = el.renderRoot.querySelector('.preview'); - - expect(preview).to.exist; - }); - - it('should render HTML content in the preview', () => { - const preview = el.renderRoot.querySelector('.preview div'); - - expect(preview?.innerHTML).to.contain('

Hello

'); - }); - - it('should have a tablist', () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]'); - - expect(tablist).to.exist; - }); - - it('should have an aria-label on the tablist', () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]'); - - expect(tablist).to.have.attribute('aria-label', 'Code language'); - }); - - it('should have three tabs', () => { - const tabs = el.renderRoot.querySelectorAll('.tabs > *'); - - expect(tabs).to.have.length(3); - }); - - it('should label tabs HTML, CSS, TypeScript', () => { - const tabs = el.renderRoot.querySelectorAll('.tabs > *'); - - expect(tabs[0]).to.have.trimmed.text('HTML'); - expect(tabs[1]).to.have.trimmed.text('CSS'); - expect(tabs[2]).to.have.trimmed.text('TypeScript'); - }); - - it('should mark the selected tab as aria-selected', () => { - const tab = el.renderRoot.querySelector('#tab-html'); - - expect(tab).to.have.attribute('aria-selected', 'true'); - }); - - it('should mark non-selected tabs as not aria-selected', () => { - const tab = el.renderRoot.querySelector('#tab-css'); - - expect(tab).to.have.attribute('aria-selected', 'false'); - }); - - it('should set tabindex 0 on the selected tab', () => { - const tab = el.renderRoot.querySelector('#tab-html'); - - expect(tab).to.have.attribute('tabindex', '0'); - }); - - it('should set tabindex -1 on non-selected tabs', () => { - const tab = el.renderRoot.querySelector('#tab-css'); - - expect(tab).to.have.attribute('tabindex', '-1'); - }); - - it('should have a code panel', () => { - const panel = el.renderRoot.querySelector('[role="tabpanel"]'); - - expect(panel).to.exist; - }); - - it('should link the panel to the selected tab', () => { - const panel = el.renderRoot.querySelector('[role="tabpanel"]'); - - expect(panel).to.have.attribute('aria-labelledby', 'tab-html'); - }); - - it('should contain syntax highlighted code', () => { - const code = el.renderRoot.querySelector('code'); - - expect(code).to.exist; - expect(code?.querySelector('.token')).to.exist; - }); - - it('should have a copy button', () => { - const button = el.renderRoot.querySelector('.copy'); - - expect(button).to.exist; - expect(button).to.have.attribute('aria-label', 'Copy to clipboard'); - }); - - describe('tab switching', () => { - it('should switch to CSS tab on click', async () => { - const tab = el.renderRoot.querySelector('#tab-css'); - tab?.click(); - await el.updateComplete; - - expect(el.selected).to.equal('css'); - - const panel = el.renderRoot.querySelector('[role="tabpanel"]'); - - expect(panel).to.have.attribute('id', 'panel-css'); - }); - - it('should switch to TypeScript tab on click', async () => { - const tab = el.renderRoot.querySelector('#tab-ts'); - tab?.click(); - await el.updateComplete; - - expect(el.selected).to.equal('ts'); - }); - }); - - describe('keyboard navigation', () => { - it('should move to the next tab on ArrowRight', async () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]')!; - - tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); - await el.updateComplete; - - expect(el.selected).to.equal('css'); - }); - - it('should wrap around on ArrowRight from the last tab', async () => { - el.selected = 'ts'; - await el.updateComplete; - - const tablist = el.renderRoot.querySelector('[role="tablist"]')!; - - tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); - await el.updateComplete; - - expect(el.selected).to.equal('html'); - }); - - it('should move to the previous tab on ArrowLeft', async () => { - el.selected = 'css'; - await el.updateComplete; - - const tablist = el.renderRoot.querySelector('[role="tablist"]')!; - - tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); - await el.updateComplete; - - expect(el.selected).to.equal('html'); - }); - - it('should wrap around on ArrowLeft from the first tab', async () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]')!; - - tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); - await el.updateComplete; - - expect(el.selected).to.equal('ts'); - }); - - it('should go to the first tab on Home', async () => { - el.selected = 'ts'; - await el.updateComplete; - - const tablist = el.renderRoot.querySelector('[role="tablist"]')!; - - tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true })); - await el.updateComplete; - - expect(el.selected).to.equal('html'); - }); - - it('should go to the last tab on End', async () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]')!; - - tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true })); - await el.updateComplete; - - expect(el.selected).to.equal('ts'); - }); - }); - - describe('copy', () => { - it('should copy raw code to the clipboard', () => { - const writeText = vi.fn().mockResolvedValue(undefined); - - vi.stubGlobal('navigator', { clipboard: { writeText } }); - - const button = el.renderRoot.querySelector('.copy')!; - button.click(); - - expect(writeText).toHaveBeenCalledWith('

Hello

'); - - vi.unstubAllGlobals(); - }); - }); - }); - - describe('html only', () => { - let el: LitElement & { selected?: string }; - - beforeEach(async () => { - el = await fixture(html` - - - - `); - }); - - it('should render a preview', () => { - const preview = el.renderRoot.querySelector('.preview'); - - expect(preview).to.exist; - }); - - it('should not render a tablist', () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]'); - - expect(tablist).to.not.exist; - }); - - it('should render a code panel without aria-labelledby', () => { - const panel = el.renderRoot.querySelector('[role="tabpanel"]'); - - expect(panel).to.exist; - expect(panel).to.not.have.attribute('aria-labelledby'); - }); - }); - - describe('css only', () => { - let el: LitElement & { selected?: string }; - - beforeEach(async () => { - el = await fixture(html` - - - - `); - }); - - it('should not render a preview', () => { - const preview = el.renderRoot.querySelector('.preview'); - - expect(preview).to.not.exist; - }); - - it('should not render a tablist', () => { - const tablist = el.renderRoot.querySelector('[role="tablist"]'); - - expect(tablist).to.not.exist; - }); - - it('should render a code panel', () => { - const panel = el.renderRoot.querySelector('[role="tabpanel"]'); - - expect(panel).to.exist; - }); - }); - - describe('html and css', () => { - let el: LitElement & { selected?: string }; - - beforeEach(async () => { - el = await fixture(html` - - - - - `); - }); - - it('should render a preview', () => { - const preview = el.renderRoot.querySelector('.preview'); - - expect(preview).to.exist; - }); - - it('should have two tabs', () => { - const tabs = el.renderRoot.querySelectorAll('.tabs > *'); - - expect(tabs).to.have.length(2); - }); - - it('should apply CSS to the preview via adoptedStyleSheets', () => { - const sheets = el.shadowRoot!.adoptedStyleSheets; - - expect(sheets.length).to.be.greaterThan(1); - }); - }); - - describe('dedenting', () => { - let el: LitElement & { selected?: string }; - - beforeEach(async () => { - el = await fixture(html` - - - - `); - }); - - it('should strip common leading whitespace', () => { - const code = el.renderRoot.querySelector('code'); - - expect(code?.textContent).to.contain('const a = 1;'); - expect(code?.textContent).to.not.match(/^\s{12}/); - }); - }); -}); diff --git a/docs/components/src/example/example.stories.ts b/docs/components/src/example/example.stories.ts deleted file mode 100644 index 92e0e8f6a9..0000000000 --- a/docs/components/src/example/example.stories.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { type Meta, type StoryObj } from '@storybook/web-components-vite'; -import { DocExample } from './example.js'; - -type Story = StoryObj; - -try { - customElements.define('doc-example', DocExample); -} catch { - /* empty */ -} - -/** Helper to create a template element with a data-lang attribute. */ -function langTemplate(lang: string, code: string): HTMLTemplateElement { - const template = document.createElement('template'); - - template.dataset['lang'] = lang; - template.innerHTML = code; - - return template; -} - -const exampleHTML = '', - exampleCSS = `.primary { - background: #007bff; - border: none; - border-radius: 4px; - color: white; - cursor: pointer; - font-size: 16px; - padding: 8px 16px; -} - -.primary:hover { - background: #0056b3; -}`, - exampleTS = `const button = document.querySelector('.primary'); - -button?.addEventListener('click', () => { - console.log('Button clicked!'); -});`, - cardHTML = `
-

Card title

-

Card content goes here.

-
`, - cardCSS = `.card { - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 16px; -} - -.card h3 { - margin-block-start: 0; -}`, - onlyCSS = `:root { - --primary: #007bff; - --secondary: #6c757d; - --font-family: system-ui, sans-serif; -} - -body { - font-family: var(--font-family); - margin: 0; - padding: 16px; -}`, - onlyTS = `interface User { - id: number; - name: string; - email: string; -} - -async function fetchUser(id: number): Promise { - const response = await fetch(\`/api/users/\${id}\`); - - if (!response.ok) { - throw new Error(\`Failed to fetch user \${id}\`); - } - - return response.json() as Promise; -}`, - paragraphHTML = `

Hello world

-

This is a paragraph with bold and italic text.

`; - -export default { - title: 'Example' -} satisfies Meta; - -export const Basic: Story = { - render: () => { - const el = document.createElement('doc-example'); - - el.append(langTemplate('html', exampleHTML), langTemplate('css', exampleCSS), langTemplate('ts', exampleTS)); - - return el; - } -}; - -export const HTMLOnly: Story = { - render: () => { - const el = document.createElement('doc-example'); - - el.append(langTemplate('html', paragraphHTML)); - - return el; - } -}; - -export const HTMLAndCSS: Story = { - render: () => { - const el = document.createElement('doc-example'); - - el.append(langTemplate('html', cardHTML), langTemplate('css', cardCSS)); - - return el; - } -}; - -export const CSSOnly: Story = { - render: () => { - const el = document.createElement('doc-example'); - - el.append(langTemplate('css', onlyCSS)); - - return el; - } -}; - -export const TypeScriptOnly: Story = { - render: () => { - const el = document.createElement('doc-example'); - - el.append(langTemplate('ts', onlyTS)); - - return el; - } -}; diff --git a/docs/components/src/example/example.ts b/docs/components/src/example/example.ts deleted file mode 100644 index 7de79ec638..0000000000 --- a/docs/components/src/example/example.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; -import { Button } from '@sl-design-system/button'; -import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; -import { state } from 'lit/decorators.js'; -import Prism from 'prismjs'; -import 'prismjs/components/prism-css.js'; -import 'prismjs/components/prism-typescript.js'; -import styles from './example.css' with { type: 'css' }; - -type Language = 'html' | 'css' | 'ts'; - -const LANGUAGE_LABELS: Record = { - html: 'HTML', - css: 'CSS', - ts: 'TypeScript' -}; - -const PRISM_GRAMMARS: Record = { - html: Prism.languages['markup'], - css: Prism.languages['css'], - ts: Prism.languages['typescript'] -}; - -const PRISM_LANGUAGE_IDS: Record = { - html: 'markup', - css: 'css', - ts: 'typescript' -}; - -/** Dedent code by removing common leading whitespace from all non-empty lines. */ -function dedent(code: string): string { - const lines = code.split('\n'); - - // Remove leading/trailing blank lines - while (lines.length && lines[0].trim() === '') { - lines.shift(); - } - while (lines.length && lines[lines.length - 1].trim() === '') { - lines.pop(); - } - - const indents = lines.filter(l => l.trim().length > 0).map(l => l.match(/^(\s*)/)?.[1].length ?? 0), - minIndent = indents.length ? Math.min(...indents) : 0; - - return minIndent > 0 ? lines.map(l => l.slice(minIndent)).join('\n') : lines.join('\n'); -} - -export class DocExample extends ScopedElementsMixin(LitElement) { - /** @internal */ - static get scopedElements(): ScopedElementsMap { - return { - 'sl-button': Button - }; - } - - /** @internal */ - static styles: CSSResultGroup = styles; - - /** The available code snippets keyed by language. */ - #snippets = new Map(); - - /** The ordered list of available languages. */ - #languages: Language[] = []; - - /** The currently selected tab. */ - @state() selected?: Language; - - /** A dynamic stylesheet for user-provided preview CSS. */ - #previewSheet = new CSSStyleSheet(); - - override connectedCallback(): void { - super.connectedCallback(); - this.#parseTemplates(); - this.#applyPreviewStyles(); - } - - override render(): TemplateResult { - const hasPreview = this.#snippets.has('html'), - hasTabs = this.#languages.length > 1, - code = this.selected ? this.#snippets.get(this.selected) : undefined; - - return html` - ${hasPreview ? this.#renderPreview() : nothing} ${hasTabs ? this.#renderTabs() : nothing} - ${code !== undefined && this.selected - ? html` -
-
- -
- ` - : nothing} - `; - } - - #renderPreview(): TemplateResult { - const htmlCode = this.#snippets.get('html') ?? ''; - - return html` -
-
-
- `; - } - - #renderTabs(): TemplateResult { - return html` -
- ${this.#languages.map( - lang => html` - this.#select(lang)} - > - ${LANGUAGE_LABELS[lang]} - - ` - )} -
- `; - } - - #parseTemplates(): void { - const templates = this.querySelectorAll('template[data-lang]'); - - for (const template of templates) { - const lang = template.dataset['lang'] as Language; - - if (!LANGUAGE_LABELS[lang]) { - continue; - } - - const raw = lang === 'html' ? template.innerHTML : (template.content.textContent ?? ''); - - this.#snippets.set(lang, dedent(raw)); - } - - // Maintain a consistent order: html, css, ts - this.#languages = (['html', 'css', 'ts'] as Language[]).filter(l => this.#snippets.has(l)); - this.selected = this.#languages[0]; - } - - #applyPreviewStyles(): void { - const cssCode = this.#snippets.get('css'); - - if (cssCode) { - this.#previewSheet.replaceSync(`.preview { ${cssCode} }`); - this.shadowRoot!.adoptedStyleSheets = [...this.shadowRoot!.adoptedStyleSheets, this.#previewSheet]; - } - } - - #highlight(code: string, lang: Language): string { - return Prism.highlight(code, PRISM_GRAMMARS[lang], PRISM_LANGUAGE_IDS[lang]); - } - - #select(lang: Language): void { - this.selected = lang; - - void this.updateComplete.then(() => { - this.renderRoot.querySelector(`#tab-${lang}`)?.focus(); - }); - } - - #onTabKeydown(event: KeyboardEvent): void { - const index = this.#languages.indexOf(this.selected!); - let next: number | undefined; - - switch (event.key) { - case 'ArrowRight': - next = (index + 1) % this.#languages.length; - break; - case 'ArrowLeft': - next = (index - 1 + this.#languages.length) % this.#languages.length; - break; - case 'Home': - next = 0; - break; - case 'End': - next = this.#languages.length - 1; - break; - } - - if (next !== undefined) { - event.preventDefault(); - this.#select(this.#languages[next]); - } - } - - async #copy(code: string): Promise { - await navigator.clipboard.writeText(code); - } -} diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index 2d052c064f..9faa4f74b2 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -47,7 +47,7 @@ export default defineConfig({ }, entry: [ 'src/copy-button/copy-button.ts', - 'src/example/example.ts', + 'src/code-example/code-example.ts', 'src/install-info/install-info.ts', 'src/page-toc/page-toc.ts', 'src/search/search.ts', diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index 4a61e79b90..bff987af1e 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -1,26 +1,19 @@ import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { basename, dirname, join } from 'node:path'; +import { parse as HTMLParse } from 'node-html-parser'; import * as esbuild from 'esbuild'; import eleventyNavigationPlugin from '@11ty/eleventy-navigation'; -import markdownItAnchor from 'markdown-it-anchor'; +import { codeExamplesTransformer } from './src/transformers/code-examples.js'; import { getComponents } from './src/utils/manifest.js'; +import { markdown } from './src/utils/markdown.js'; const require = createRequire(import.meta.url); const themePath = dirname(require.resolve('@sl-design-system/sanoma-learning/package.json')); /** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */ -export default function (eleventyConfig) { - const allComponents = getComponents(); - - eleventyConfig.addPlugin(eleventyNavigationPlugin); - - eleventyConfig.amendLibrary('md', mdLib => { - mdLib.use(markdownItAnchor, { - permalink: markdownItAnchor.permalink.headerLink(), - level: [2, 3] - }); - }); +export default async function (eleventyConfig) { + let allComponents = await getComponents(); eleventyConfig.addPassthroughCopy({ 'src/assets': 'assets' }); eleventyConfig.addPassthroughCopy({ 'src/css': 'css' }); @@ -33,9 +26,29 @@ export default function (eleventyConfig) { [join(themePath, 'fonts')]: 'theme/fonts' }); + eleventyConfig.addWatchTarget('../../custom-elements.json'); eleventyConfig.addWatchTarget('../components/dist/**/*.(css|js)'); + eleventyConfig.setWatchThrottleWaitTime(10); // in milliseconds + + eleventyConfig.on('eleventy.beforeWatch', async changes => { + let updateComponents = false; + + for (const change of changes) { + if (change.includes('custom-elements.json') || change.includes('package.json')) { + updateComponents = true; + break; + } + } + + if (updateComponents) { + allComponents = await getComponents(); + } + }); + + eleventyConfig.addPlugin(eleventyNavigationPlugin); + + eleventyConfig.setLibrary('md', markdown); - // Helpers eleventyConfig.addNunjucksGlobal('getComponent', tagName => { const component = allComponents.find(c => c.tagName === tagName); if (!component) { @@ -65,6 +78,18 @@ export default function (eleventyConfig) { }); }); + eleventyConfig.addTransform('component', function (content) { + let doc = HTMLParse(content, { blockTextElements: { code: true } }); + + const transformers = [codeExamplesTransformer()]; + + for (const transformer of transformers) { + transformer.call(this, doc); + } + + return doc.toString(); + }); + eleventyConfig.on('eleventy.before', async () => { // Scan pages for icon names in eleventyNavigation frontmatter const icons = new Set(); @@ -113,10 +138,11 @@ export default function (eleventyConfig) { return { dir: { - input: 'src/content', - output: 'dist', + data: '../data', includes: '../includes', - data: '../data' + input: 'src/content', + layouts: '../layouts', + output: 'dist' }, templateFormats: ['njk', 'md'], markdownTemplateEngine: 'njk', diff --git a/docs/website/package.json b/docs/website/package.json index f06b17dcd6..492ade5295 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -14,10 +14,12 @@ "@sl-design-system/icon": "workspace:^", "@sl-design-system/sanoma-learning": "workspace:*", "@webcomponents/scoped-custom-element-registry": "^0.0.10", - "markdown-it-anchor": "^9.2.0" - }, - "devDependencies": { "esbuild": "^0.25.0", - "lit": "^3.3.2" + "lit": "^3.3.2", + "markdown-it": "^14.1.1", + "markdown-it-anchor": "^9.2.0", + "markdown-it-attrs": "^4.3.1", + "node-html-parser": "^7.1.0", + "prismjs": "^1.30.0" } } diff --git a/docs/website/src/content/api-reference/api-reference.md b/docs/website/src/content/api-reference/api-reference.md index 1904707a28..f5b214d5ce 100644 --- a/docs/website/src/content/api-reference/api-reference.md +++ b/docs/website/src/content/api-reference/api-reference.md @@ -1,6 +1,6 @@ --- title: API Reference -layout: base.njk +layout: api-reference eleventyNavigation: key: API Reference order: 3 diff --git a/docs/website/src/content/api-reference/button.md b/docs/website/src/content/api-reference/button.md index 4a9c99c160..8b00e971b5 100644 --- a/docs/website/src/content/api-reference/button.md +++ b/docs/website/src/content/api-reference/button.md @@ -1,6 +1,6 @@ --- title: Button -layout: base.njk +layout: api-reference eleventyNavigation: key: Button parent: API Reference diff --git a/docs/website/src/content/components/actions/actions.md b/docs/website/src/content/components/actions/actions.md index 7e72377581..150d20c1cd 100644 --- a/docs/website/src/content/components/actions/actions.md +++ b/docs/website/src/content/components/actions/actions.md @@ -1,6 +1,6 @@ --- title: Actions -layout: base.njk +layout: docs eleventyNavigation: key: Actions parent: Components diff --git a/docs/website/src/content/components/actions/button.md b/docs/website/src/content/components/actions/button.md index d4c6462436..ee84a6794b 100644 --- a/docs/website/src/content/components/actions/button.md +++ b/docs/website/src/content/components/actions/button.md @@ -1,6 +1,6 @@ --- title: Button -layout: component.njk +layout: component eleventyNavigation: key: Button parent: Actions @@ -9,31 +9,52 @@ eleventyNavigation: A button initiates an action when clicked, like redirecting to a new page or submitting a form. It is a key element for interaction and action. -## Usage +```html {.example} +Button +``` + +## Examples -Use buttons for the most important actions you want users to take. Use clear, descriptive labels that explain what happens when the button is clicked. +### Variants -```html -Click me +Use the `variant` attribute to indicate the type of action. + +```html {.example} + + Primary + Secondary + Success + Info + Warning + Danger + ``` -### Variants +### Fill -Buttons come in several variants to indicate the level of emphasis: +Use the `fill` attribute to change the button's level of emphasis. -- **Primary** — For the main action on a page. Use sparingly. -- **Secondary** — For supporting actions. This is the default. -- **Ghost** — For low-emphasis actions that blend with surrounding content. -- **Danger** — For destructive actions like deleting data. +```html {.example} + + Solid + Outline + Link + Ghost + +``` -```html -Save -Cancel -Learn more -Delete +### Shape + +Use the `shape` attribute to change the button's shape. + +```html {.example} + + Square + Pill + ``` -### Sizes +### Size Buttons are available in three sizes: @@ -41,10 +62,31 @@ Buttons are available in three sizes: - `md` — Medium, the default - `lg` — Large, for prominent actions -```html -Small -Medium -Large +```html {.example} + + Small + Medium + Large + +``` + +### Icon buttons + +Icon buttons are used for actions that can be represented by an icon, such as "close" or "edit". Always provide a text label for accessibility, either through an `` or using `aria-label`. + +```html {.example} + + + + + + + + + + + Smile! + ``` ## Accessibility diff --git a/docs/website/src/content/components/components.md b/docs/website/src/content/components/components.md index 4ea4947349..8f261d4ab8 100644 --- a/docs/website/src/content/components/components.md +++ b/docs/website/src/content/components/components.md @@ -1,6 +1,6 @@ --- title: Components -layout: base.njk +layout: docs.njk eleventyNavigation: key: Components order: 2 diff --git a/docs/website/src/content/components/form/date-field.md b/docs/website/src/content/components/form/date-field.md index 7aed72a3dd..cdb5ab3d60 100644 --- a/docs/website/src/content/components/form/date-field.md +++ b/docs/website/src/content/components/form/date-field.md @@ -1,6 +1,6 @@ --- title: Date field -layout: base.njk +layout: component eleventyNavigation: key: Date field parent: Form diff --git a/docs/website/src/content/components/form/form.md b/docs/website/src/content/components/form/form.md index 5347c46436..28970ba1ab 100644 --- a/docs/website/src/content/components/form/form.md +++ b/docs/website/src/content/components/form/form.md @@ -1,6 +1,6 @@ --- title: Form -layout: base.njk +layout: docs eleventyNavigation: key: Form parent: Components diff --git a/docs/website/src/content/components/form/text-field.md b/docs/website/src/content/components/form/text-field.md index 2f9f5afcea..a7d153dab4 100644 --- a/docs/website/src/content/components/form/text-field.md +++ b/docs/website/src/content/components/form/text-field.md @@ -1,6 +1,6 @@ --- title: Text field -layout: base.njk +layout: component eleventyNavigation: key: Text field parent: Form diff --git a/docs/website/src/content/components/form/time-field.md b/docs/website/src/content/components/form/time-field.md index f5b9a67199..4fe68ee915 100644 --- a/docs/website/src/content/components/form/time-field.md +++ b/docs/website/src/content/components/form/time-field.md @@ -1,6 +1,6 @@ --- title: Time field -layout: base.njk +layout: component eleventyNavigation: key: Time field parent: Form diff --git a/docs/website/src/content/components/grid/grid.md b/docs/website/src/content/components/grid/grid.md index 698cd0ab5c..8f43317f48 100644 --- a/docs/website/src/content/components/grid/grid.md +++ b/docs/website/src/content/components/grid/grid.md @@ -1,6 +1,6 @@ --- title: Grid -layout: base.njk +layout: docs eleventyNavigation: key: Grid parent: Components diff --git a/docs/website/src/content/components/layout/layout.md b/docs/website/src/content/components/layout/layout.md index b2c9d640a8..8bd28506a6 100644 --- a/docs/website/src/content/components/layout/layout.md +++ b/docs/website/src/content/components/layout/layout.md @@ -1,6 +1,6 @@ --- title: Layout -layout: base.njk +layout: docs eleventyNavigation: key: Layout parent: Components diff --git a/docs/website/src/content/components/navigation/navigation.md b/docs/website/src/content/components/navigation/navigation.md index a1bc3ea719..4f9022392d 100644 --- a/docs/website/src/content/components/navigation/navigation.md +++ b/docs/website/src/content/components/navigation/navigation.md @@ -1,6 +1,6 @@ --- title: Navigation -layout: base.njk +layout: docs eleventyNavigation: key: Navigation parent: Components diff --git a/docs/website/src/content/components/overlay/overlay.md b/docs/website/src/content/components/overlay/overlay.md index 31c55f5d34..71244ed9e5 100644 --- a/docs/website/src/content/components/overlay/overlay.md +++ b/docs/website/src/content/components/overlay/overlay.md @@ -1,6 +1,6 @@ --- title: Overlay -layout: base.njk +layout: docs eleventyNavigation: key: Overlay parent: Components diff --git a/docs/website/src/content/components/status/status.md b/docs/website/src/content/components/status/status.md index 226fca0207..6184a22448 100644 --- a/docs/website/src/content/components/status/status.md +++ b/docs/website/src/content/components/status/status.md @@ -1,6 +1,6 @@ --- title: Status -layout: base.njk +layout: docs eleventyNavigation: key: Status parent: Components diff --git a/docs/website/src/content/components/utilities/utilities.md b/docs/website/src/content/components/utilities/utilities.md index 593f1c81f6..9ed921ed70 100644 --- a/docs/website/src/content/components/utilities/utilities.md +++ b/docs/website/src/content/components/utilities/utilities.md @@ -1,6 +1,6 @@ --- title: Utilities -layout: base.njk +layout: docs eleventyNavigation: key: Utilities parent: Components diff --git a/docs/website/src/content/design-tokens/border.md b/docs/website/src/content/design-tokens/border.md index ee1a6efc7f..4a8a8f60df 100644 --- a/docs/website/src/content/design-tokens/border.md +++ b/docs/website/src/content/design-tokens/border.md @@ -1,6 +1,6 @@ --- title: Border -layout: base.njk +layout: design-tokens eleventyNavigation: key: Border parent: Design tokens diff --git a/docs/website/src/content/design-tokens/color.md b/docs/website/src/content/design-tokens/color.md index 1b18cf70c0..d07a1b3450 100644 --- a/docs/website/src/content/design-tokens/color.md +++ b/docs/website/src/content/design-tokens/color.md @@ -1,6 +1,6 @@ --- title: Color -layout: base.njk +layout: design-tokens eleventyNavigation: key: Color parent: Design tokens diff --git a/docs/website/src/content/design-tokens/design-tokens.md b/docs/website/src/content/design-tokens/design-tokens.md index 307c4cab8a..7318a54e23 100644 --- a/docs/website/src/content/design-tokens/design-tokens.md +++ b/docs/website/src/content/design-tokens/design-tokens.md @@ -1,6 +1,6 @@ --- title: Design tokens -layout: base.njk +layout: design-tokens eleventyNavigation: key: Design tokens order: 1 diff --git a/docs/website/src/content/introduction/documentation.md b/docs/website/src/content/introduction/documentation.md index b01707f564..d24ced06fe 100644 --- a/docs/website/src/content/introduction/documentation.md +++ b/docs/website/src/content/introduction/documentation.md @@ -1,6 +1,6 @@ --- title: Documentation -layout: base.njk +layout: docs eleventyNavigation: key: Documentation parent: Introduction diff --git a/docs/website/src/content/introduction/faq.md b/docs/website/src/content/introduction/faq.md index 3419b659d8..53ef517a09 100644 --- a/docs/website/src/content/introduction/faq.md +++ b/docs/website/src/content/introduction/faq.md @@ -1,6 +1,6 @@ --- title: FAQ -layout: base.njk +layout: docs eleventyNavigation: key: FAQ parent: Introduction diff --git a/docs/website/src/content/introduction/getting-started/for-designers.md b/docs/website/src/content/introduction/getting-started/for-designers.md index 56281253ad..a0a1fa6246 100644 --- a/docs/website/src/content/introduction/getting-started/for-designers.md +++ b/docs/website/src/content/introduction/getting-started/for-designers.md @@ -1,6 +1,6 @@ --- title: For designers -layout: base.njk +layout: docs eleventyNavigation: key: For designers parent: Getting started diff --git a/docs/website/src/content/introduction/getting-started/for-developers.md b/docs/website/src/content/introduction/getting-started/for-developers.md index 968aa51595..b422d1c81e 100644 --- a/docs/website/src/content/introduction/getting-started/for-developers.md +++ b/docs/website/src/content/introduction/getting-started/for-developers.md @@ -1,6 +1,6 @@ --- title: For developers -layout: base.njk +layout: docs eleventyNavigation: key: For developers parent: Getting started diff --git a/docs/website/src/content/introduction/getting-started/getting-started.md b/docs/website/src/content/introduction/getting-started/getting-started.md index cf5b4a732a..96ea96aa0d 100644 --- a/docs/website/src/content/introduction/getting-started/getting-started.md +++ b/docs/website/src/content/introduction/getting-started/getting-started.md @@ -1,6 +1,6 @@ --- title: Getting started -layout: base.njk +layout: docs eleventyNavigation: key: Getting started parent: Introduction diff --git a/docs/website/src/content/introduction/introduction.md b/docs/website/src/content/introduction/introduction.md index c453a02c7f..367993df80 100644 --- a/docs/website/src/content/introduction/introduction.md +++ b/docs/website/src/content/introduction/introduction.md @@ -1,6 +1,6 @@ --- title: Introduction -layout: base.njk +layout: docs eleventyNavigation: key: Introduction order: 0 diff --git a/docs/website/src/css/main.css b/docs/website/src/css/main.css index caa1237fe5..9619861365 100644 --- a/docs/website/src/css/main.css +++ b/docs/website/src/css/main.css @@ -6,7 +6,6 @@ html { background: var(--sl-elevation-surface-base-default); color: var(--sl-color-foreground-plain); font: var(--sl-text-new-body-md); - font-family: Roboto, var(--sl-text-new-typeset-fontFamily-body), sans-serif; } body { @@ -46,6 +45,7 @@ doc-sidebar { ============================================ */ .content { + box-sizing: border-box; grid-area: content; inline-size: 100%; max-inline-size: 48rem; @@ -102,10 +102,10 @@ doc-sidebar { } .content code { - background: var(--sl-color-background-accent-grey-subtlest); + background: var(--sl-color-background-neutral-subtle); border-radius: var(--sl-size-borderRadius-sm); font-size: 0.9em; - padding-inline: var(--sl-size-050); + padding: var(--sl-size-025) var(--sl-size-050); } .content pre { @@ -133,6 +133,11 @@ doc-sidebar { color: var(--sl-color-foreground-accent-blue-bold); } +.component-meta { + display: flex; + gap: var(--sl-size-100); +} + doc-page-toc { align-self: start; display: none; @@ -145,3 +150,72 @@ doc-page-toc { display: block; } } + +/* ============================================ + Prism syntax highlighting + ============================================ */ + +.code-token { + &.code-comment, + &.code-prolog, + &.code-doctype, + &.code-cdata { + color: var(--sl-color-palette-grey-400); + } + + &.code-punctuation { + color: var(--sl-color-palette-grey-500); + } + + &.code-property, + &.code-tag, + &.code-constant, + &.code-symbol, + &.code-deleted { + color: var(--sl-color-palette-green-400); + } + + &.code-attr-name, + &.code-boolean, + &.code-number { + color: var(--sl-color-palette-purple-700); + } + + &.code-selector, + &.code-string, + &.code-char, + &.code-builtin, + &.code-inserted { + color: var(--sl-color-palette-teal-300); + } + + &.code-atrule, + &.code-function, + &.code-class-name { + color: var(--sl-color-palette-yellow-300); + } + + &.code-attr-value { + color: var(--sl-color-palette-blue-300); + } + + &.code-keyword { + color: var(--sl-color-palette-teal-300); + } + + &.code-regex, + &.code-important { + color: var(--sl-color-palette-orange-300); + } + + &.code-operator, + &.code-entity, + &.code-url, + &.code-variable { + color: var(--sl-color-foreground-inverted-plain); + } + + &.code-namespace { + opacity: 0.7; + } +} diff --git a/docs/website/src/includes/base.njk b/docs/website/src/includes/base.njk index be767aa642..08877c984f 100644 --- a/docs/website/src/includes/base.njk +++ b/docs/website/src/includes/base.njk @@ -1,4 +1,5 @@ +{% if hasGeneratedTitle == undefined %}{% set hasGeneratedTitle = true %}{% endif %} @@ -6,7 +7,7 @@ {{ title }} | SL Design System @@ -21,16 +22,31 @@
+ {% if hasSidebar %} {% include "sidebar.njk" %} + {% endif %}
-

{{ title }}

+ {% block header %} + {% if hasGeneratedTitle %} +

{{ title }}

+ {% endif %} + {% endblock %} + + {% block beforeContent %}{% endblock %} + + {% block content %} {{ content | safe }} + {% endblock %} + + {% block afterContent %}{% endblock %}
+ {% if hasTableOfContents %} + {% endif %}
diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index 0f1409c6d0..5d468c5c25 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -1,5 +1,12 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; +import './theme.js'; import './icons.js'; +import '@sl-design-system/button/register.js'; +import '@sl-design-system/button-bar/register.js'; +import '@sl-design-system/badge/register.js'; +import '@sl-design-system/icon/register.js'; +import '@sl-design-system/tooltip/register.js'; +import { CodeExample } from '@sl-design-system/doc-components/code-example/code-example.js'; import { InstallInfo } from '@sl-design-system/doc-components/install-info/install-info.js'; import { PageToc } from '@sl-design-system/doc-components/page-toc/page-toc.js'; import { Sidebar } from '@sl-design-system/doc-components/sidebar/sidebar.js'; @@ -7,6 +14,7 @@ import { NavGroup } from '@sl-design-system/doc-components/site-nav/nav-group.js import { NavItem } from '@sl-design-system/doc-components/site-nav/nav-item.js'; import { SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav.js'; +customElements.define('doc-code-example', CodeExample); customElements.define('doc-install-info', InstallInfo); customElements.define('doc-nav-group', NavGroup); customElements.define('doc-nav-item', NavItem); diff --git a/docs/website/src/js/theme.js b/docs/website/src/js/theme.js new file mode 100644 index 0000000000..5bc6a23e5d --- /dev/null +++ b/docs/website/src/js/theme.js @@ -0,0 +1,3 @@ +import { setup } from '@sl-design-system/sanoma-learning'; + +setup(); diff --git a/docs/website/src/layouts/api-reference.njk b/docs/website/src/layouts/api-reference.njk new file mode 100644 index 0000000000..e1f6774a4f --- /dev/null +++ b/docs/website/src/layouts/api-reference.njk @@ -0,0 +1 @@ +{% extends './docs.njk' %} diff --git a/docs/website/src/layouts/component.njk b/docs/website/src/layouts/component.njk new file mode 100644 index 0000000000..a300fbc7bf --- /dev/null +++ b/docs/website/src/layouts/component.njk @@ -0,0 +1,16 @@ +{% extends './docs.njk' %} +{% set component = getComponent('sl-' + page.fileSlug) %} + +{% block beforeContent %} +
+ <{{ component.tagName }}> + + {{ component.status | capitalize }} + + v{{ component.version }} +
+{% endblock %} diff --git a/docs/website/src/layouts/design-tokens.njk b/docs/website/src/layouts/design-tokens.njk new file mode 100644 index 0000000000..e1f6774a4f --- /dev/null +++ b/docs/website/src/layouts/design-tokens.njk @@ -0,0 +1 @@ +{% extends './docs.njk' %} diff --git a/docs/website/src/layouts/docs.njk b/docs/website/src/layouts/docs.njk new file mode 100644 index 0000000000..680a6d0f97 --- /dev/null +++ b/docs/website/src/layouts/docs.njk @@ -0,0 +1,3 @@ +{% set hasSidebar = true %} +{% set hasTableOfContents = true %} +{% extends "../includes/base.njk" %} diff --git a/docs/website/src/transformers/code-examples.js b/docs/website/src/transformers/code-examples.js new file mode 100644 index 0000000000..26144f1311 --- /dev/null +++ b/docs/website/src/transformers/code-examples.js @@ -0,0 +1,37 @@ +import { parse } from 'node-html-parser'; +import { highlightCode } from './highlight-code.js'; + +export function codeExamplesTransformer(options = {}) { + options = { + container: 'body', + ...options + }; + + return function (doc) { + const container = doc.querySelector(options.container); + if (!container) { + return; + } + + container.querySelectorAll('code.example').forEach(code => { + const pre = code.closest('pre'), + uuid = crypto.randomUUID(), + id = `code-example-${uuid.slice(-12)}`, + demo = pre.textContent; + + const langClass = [...code.classList.values()].find(val => val.startsWith('language-')), + lang = langClass ? langClass.replace(/^language-/, '') : 'plain'; + + code.innerHTML = highlightCode(code.textContent ?? '', lang); + + const codeExample = parse(` + + ${demo} +
${code.innerHTML}
+
+ `); + + pre.replaceWith(codeExample); + }); + }; +} diff --git a/docs/website/src/transformers/highlight-code.js b/docs/website/src/transformers/highlight-code.js new file mode 100644 index 0000000000..59f1749753 --- /dev/null +++ b/docs/website/src/transformers/highlight-code.js @@ -0,0 +1,46 @@ +import Prism from 'prismjs'; +import PrismLoader from 'prismjs/components/index.js'; +import 'prismjs/plugins/custom-class/prism-custom-class.js'; + +PrismLoader('diff'); +PrismLoader.silent = true; +Prism.plugins.customClass.prefix('code-'); + +export function highlightCode(code, language = 'plain') { + const alias = language.replace(/^diff-/, ''), + isDiff = /^diff-/i.test(language); + + if (!Prism.languages[alias]) { + PrismLoader(alias); + if (!Prism.languages[alias]) { + throw new Error(`Unsupported language for code highlighting: "${language}"`); + } + } + + if (isDiff) { + Prism.languages[language] = Prism.languages.diff; + } + + return Prism.highlight(code, Prism.languages[language], language); +} + +export function highlightCodeTransformer(options = {}) { + options = { + container: 'body', + ...options + }; + + return function (doc) { + const container = doc.querySelector(options.container); + if (!container) { + return; + } + + container.querySelectorAll('code[class*="language-"]').forEach(code => { + const langClass = [...code.classList.values()].find(val => val.startsWith('language-')), + lang = langClass ? langClass.replace(/^language-/, '') : 'plain'; + + code.innerHTML = highlightCode(code.textContent ?? '', lang); + }); + }; +} diff --git a/docs/website/src/utils/manifest.js b/docs/website/src/utils/manifest.js index 2d47e0fa40..2bde1f00ff 100644 --- a/docs/website/src/utils/manifest.js +++ b/docs/website/src/utils/manifest.js @@ -1,3 +1,89 @@ -export function getComponents() { - return []; +import { access, readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import manifest from '../../../../custom-elements.json' with { type: 'json' }; + +// Repo root is 4 levels up from this file (docs/website/src/utils/manifest.js) +const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url)); + +/** + * Walks up the directory tree from `fileName` until it finds a `package.json`, + * returning its absolute path, or `null` if none is found before the root. + */ +const findNearestPackageJson = async fileName => { + let currentDir = dirname(fileName); + + while (true) { + const packageJsonPath = join(currentDir, 'package.json'); + + try { + await access(packageJsonPath); + + return packageJsonPath; + } catch { + // If the file doesn't exist, move up to the parent directory + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + // Reached the root directory without finding a package.json + return null; + } + currentDir = parentDir; + } + } +}; + +const getComponentMetadata = async path => { + const packageJsonPath = await findNearestPackageJson(join(repoRoot, path)); + if (!packageJsonPath) { + console.warn(`No package.json found for component at path "${path}".`); + return { status: null, version: null }; + } + + try { + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); + + return { + status: packageJson.status || null, + version: packageJson.version || null + }; + } catch (err) { + console.error(`Error reading package.json for path "${path}":`, err); + + return { status: null, version: null }; + } +}; + +const sortByName = (a, b) => (a.name || '').localeCompare(b.name || ''); + +export async function getComponents() { + const components = []; + + for (const module of manifest.modules || []) { + const { status, version } = await getComponentMetadata(module.path); + + for (const declaration of module.declarations || []) { + if (declaration.customElement) { + const cssParts = declaration.cssParts?.sort(sortByName); + const cssProperties = declaration.cssProperties?.sort(sortByName); + const cssStates = declaration.cssStates?.sort(sortByName); + const events = declaration.events?.sort(sortByName); + const members = declaration.members?.sort(sortByName); + const slots = declaration.slots?.sort(sortByName); + + components.push({ + ...declaration, + cssParts, + cssProperties, + cssStates, + events, + members, + slots, + status, + version + }); + } + } + } + + return components; } diff --git a/docs/website/src/utils/markdown.js b/docs/website/src/utils/markdown.js new file mode 100644 index 0000000000..956a9905e5 --- /dev/null +++ b/docs/website/src/utils/markdown.js @@ -0,0 +1,21 @@ +import markdownit from 'markdown-it'; +import markdownItAttrs from 'markdown-it-attrs'; +import markdownItAnchor from 'markdown-it-anchor'; + +export const markdown = markdownit({ + html: true, + xhtmlOut: false, + breaks: false, + langPrefix: 'language-', + linkify: false, + typographer: false +}); + +markdown.use(markdownItAttrs, { + allowedAttributes: [] +}); + +markdown.use(markdownItAnchor, { + permalink: markdownItAnchor.permalink.headerLink(), + level: [2, 3] +}); diff --git a/package.json b/package.json index 9d300a5d54..4a749ad0ab 100644 --- a/package.json +++ b/package.json @@ -461,7 +461,7 @@ "@changesets/get-github-info": "^0.8.0", "@faker-js/faker": "^10.3.0", "@lit/localize-tools": "^0.8.1", - "@pwrs/cem": "^0.9.16", + "@pwrs/cem": "^0.9.18", "@storybook/addon-a11y": "^10.3.5", "@storybook/addon-docs": "^10.3.5", "@storybook/addon-vitest": "^10.3.5", diff --git a/packages/components/button/src/button.ts b/packages/components/button/src/button.ts index 2656f18c38..8cf67a6f17 100644 --- a/packages/components/button/src/button.ts +++ b/packages/components/button/src/button.ts @@ -28,6 +28,7 @@ export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'info' | 'warn * Foo * ``` * + * @customElement sl-button * @slot default - Text label of the button. Optionally an sl-icon can be added */ export class Button extends ForwardAriaMixin(LitElement) { diff --git a/packages/components/date-field/src/date-field.ts b/packages/components/date-field/src/date-field.ts index 393e746cb9..87bb9d2525 100644 --- a/packages/components/date-field/src/date-field.ts +++ b/packages/components/date-field/src/date-field.ts @@ -35,6 +35,7 @@ type DatePartType = 'day' | 'month' | 'year'; * A form component that allows the user to pick a date from a calendar. * Uses individual spinbutton inputs per date part for improved accessibility. * + * @customElement sl-date-field * @cssState has-focus - Set when the date field has focus. * @cssState has-value - Set when the date field has a value. * @cssState placeholder-shown - Set when the date field is empty and has a placeholder. diff --git a/packages/components/grid/src/grid.ts b/packages/components/grid/src/grid.ts index b84f1d6576..5a604ae4ae 100644 --- a/packages/components/grid/src/grid.ts +++ b/packages/components/grid/src/grid.ts @@ -105,6 +105,8 @@ export type SlStateChangeEvent = CustomEvent<{ grid: Grid }>; * Data grid component. This component is designed to be highly customizable * and can be used to display a wide variety of data. It supports sorting, * filtering, grouping, and more. + * + * @customElement sl-grid */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export class Grid extends ScopedElementsMixin(LitElement) { diff --git a/packages/components/text-field/src/text-field.ts b/packages/components/text-field/src/text-field.ts index e637a01756..08870d13dd 100644 --- a/packages/components/text-field/src/text-field.ts +++ b/packages/components/text-field/src/text-field.ts @@ -28,6 +28,7 @@ let nextUniqueId = 0; /** * Single line text field component. * + * @customElement sl-text-field * @slot prefix - Content shown before the input * @slot input - The slot for the input element * @slot suffix - Content shown after the input diff --git a/packages/components/time-field/src/time-field.ts b/packages/components/time-field/src/time-field.ts index 67a1c99712..04bf36f8a2 100644 --- a/packages/components/time-field/src/time-field.ts +++ b/packages/components/time-field/src/time-field.ts @@ -39,6 +39,7 @@ const timeSeparators = new Map(); * A form component that allows the user to pick a time. * Uses individual spinbutton inputs per time part for improved accessibility. * + * @customElement sl-time-field * @cssState has-focus - Set when the time field has focus. * @cssState has-value - Set when the time field has a value. * @cssState placeholder-shown - Set when the time field is empty and has a placeholder. diff --git a/yarn.lock b/yarn.lock index 385584735d..5dbcb8fc30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4590,58 +4590,58 @@ __metadata: languageName: node linkType: hard -"@pwrs/cem-darwin-arm64@npm:0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem-darwin-arm64@npm:0.9.16" +"@pwrs/cem-darwin-arm64@npm:0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem-darwin-arm64@npm:0.9.18" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@pwrs/cem-darwin-x64@npm:0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem-darwin-x64@npm:0.9.16" +"@pwrs/cem-darwin-x64@npm:0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem-darwin-x64@npm:0.9.18" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@pwrs/cem-linux-arm64@npm:0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem-linux-arm64@npm:0.9.16" +"@pwrs/cem-linux-arm64@npm:0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem-linux-arm64@npm:0.9.18" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@pwrs/cem-linux-x64@npm:0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem-linux-x64@npm:0.9.16" +"@pwrs/cem-linux-x64@npm:0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem-linux-x64@npm:0.9.18" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@pwrs/cem-win32-arm64@npm:0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem-win32-arm64@npm:0.9.16" +"@pwrs/cem-win32-arm64@npm:0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem-win32-arm64@npm:0.9.18" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@pwrs/cem-win32-x64@npm:0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem-win32-x64@npm:0.9.16" +"@pwrs/cem-win32-x64@npm:0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem-win32-x64@npm:0.9.18" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@pwrs/cem@npm:^0.9.16": - version: 0.9.16 - resolution: "@pwrs/cem@npm:0.9.16" +"@pwrs/cem@npm:^0.9.18": + version: 0.9.18 + resolution: "@pwrs/cem@npm:0.9.18" dependencies: - "@pwrs/cem-darwin-arm64": "npm:0.9.16" - "@pwrs/cem-darwin-x64": "npm:0.9.16" - "@pwrs/cem-linux-arm64": "npm:0.9.16" - "@pwrs/cem-linux-x64": "npm:0.9.16" - "@pwrs/cem-win32-arm64": "npm:0.9.16" - "@pwrs/cem-win32-x64": "npm:0.9.16" + "@pwrs/cem-darwin-arm64": "npm:0.9.18" + "@pwrs/cem-darwin-x64": "npm:0.9.18" + "@pwrs/cem-linux-arm64": "npm:0.9.18" + "@pwrs/cem-linux-x64": "npm:0.9.18" + "@pwrs/cem-win32-arm64": "npm:0.9.18" + "@pwrs/cem-win32-x64": "npm:0.9.18" dependenciesMeta: "@pwrs/cem-darwin-arm64": optional: true @@ -4657,7 +4657,7 @@ __metadata: optional: true bin: cem: bin/cem.js - checksum: 10c0/390103c3b292547234ddc0424eb280ade0cc49ddddaa2017548b2fba593481f9d08cb1e93d241f8fce12cfe58303e5d469a5b0fb4caff7bac547428f4ad123c4 + checksum: 10c0/943ac12991b3ec99648b49208ff546afa5a37ad6eb00a95b32d9675b8af136ced5f9f048d938a3c104b936bd243f25ba1b26480c5bb17b18a34e5ac48fd8f4e1 languageName: node linkType: hard @@ -5686,18 +5686,18 @@ __metadata: "@sl-design-system/icon": "workspace:^" "@sl-design-system/search-field": "workspace:^" "@sl-design-system/switch": "workspace:^" - "@storybook/addon-a11y": "npm:^10.3.3" - "@storybook/addon-docs": "npm:^10.3.3" - "@storybook/addon-vitest": "npm:^10.3.3" - "@storybook/web-components": "npm:^10.3.3" - "@storybook/web-components-vite": "npm:^10.3.3" + "@storybook/addon-a11y": "npm:^10.3.5" + "@storybook/addon-docs": "npm:^10.3.5" + "@storybook/addon-vitest": "npm:^10.3.5" + "@storybook/web-components": "npm:^10.3.5" + "@storybook/web-components-vite": "npm:^10.3.5" "@tsdown/css": "npm:^0.21.7" "@types/prismjs": "npm:^1.26.6" - "@typescript/native-preview": "npm:^7.0.0-dev.20260315.1" + "@typescript/native-preview": "npm:^7.0.0-dev.20260413.1" lit: "npm:^3.3.2" prismjs: "npm:^1.30.0" - sass-embedded: "npm:^1.98.0" - storybook: "npm:^10.3.3" + sass-embedded: "npm:^1.99.0" + storybook: "npm:^10.3.5" tsdown: "npm:^0.21.7" typescript: "npm:^5.9.3" wireit: "npm:^0.14.12" @@ -6038,7 +6038,7 @@ __metadata: "@changesets/get-github-info": "npm:^0.8.0" "@faker-js/faker": "npm:^10.3.0" "@lit/localize-tools": "npm:^0.8.1" - "@pwrs/cem": "npm:^0.9.16" + "@pwrs/cem": "npm:^0.9.18" "@storybook/addon-a11y": "npm:^10.3.5" "@storybook/addon-docs": "npm:^10.3.5" "@storybook/addon-vitest": "npm:^10.3.5" @@ -6548,7 +6548,11 @@ __metadata: "@webcomponents/scoped-custom-element-registry": "npm:^0.0.10" esbuild: "npm:^0.25.0" lit: "npm:^3.3.2" + markdown-it: "npm:^14.1.1" markdown-it-anchor: "npm:^9.2.0" + markdown-it-attrs: "npm:^4.3.1" + node-html-parser: "npm:^7.1.0" + prismjs: "npm:^1.30.0" languageName: unknown linkType: soft @@ -6596,7 +6600,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:^10.3.3, @storybook/addon-a11y@npm:^10.3.5": +"@storybook/addon-a11y@npm:^10.3.5": version: 10.3.5 resolution: "@storybook/addon-a11y@npm:10.3.5" dependencies: @@ -6608,7 +6612,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-docs@npm:^10.3.3, @storybook/addon-docs@npm:^10.3.5": +"@storybook/addon-docs@npm:^10.3.5": version: 10.3.5 resolution: "@storybook/addon-docs@npm:10.3.5" dependencies: @@ -6636,7 +6640,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-vitest@npm:^10.3.3, @storybook/addon-vitest@npm:^10.3.5": +"@storybook/addon-vitest@npm:^10.3.5": version: 10.3.5 resolution: "@storybook/addon-vitest@npm:10.3.5" dependencies: @@ -6802,7 +6806,7 @@ __metadata: languageName: node linkType: hard -"@storybook/web-components-vite@npm:^10.3.3, @storybook/web-components-vite@npm:^10.3.5": +"@storybook/web-components-vite@npm:^10.3.5": version: 10.3.5 resolution: "@storybook/web-components-vite@npm:10.3.5" dependencies: @@ -6814,7 +6818,7 @@ __metadata: languageName: node linkType: hard -"@storybook/web-components@npm:10.3.5, @storybook/web-components@npm:^10.3.3, @storybook/web-components@npm:^10.3.5": +"@storybook/web-components@npm:10.3.5, @storybook/web-components@npm:^10.3.5": version: 10.3.5 resolution: "@storybook/web-components@npm:10.3.5" dependencies: @@ -7779,66 +7783,66 @@ __metadata: languageName: node linkType: hard -"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260413.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260413.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260413.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260413.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260413.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260413.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260413.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview@npm:^7.0.0-dev.20260315.1": - version: 7.0.0-dev.20260315.1 - resolution: "@typescript/native-preview@npm:7.0.0-dev.20260315.1" +"@typescript/native-preview@npm:^7.0.0-dev.20260413.1": + version: 7.0.0-dev.20260413.1 + resolution: "@typescript/native-preview@npm:7.0.0-dev.20260413.1" dependencies: - "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260315.1" - "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260315.1" - "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260315.1" - "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260315.1" - "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260315.1" - "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260315.1" - "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260315.1" + "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260413.1" + "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260413.1" + "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260413.1" + "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260413.1" + "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260413.1" + "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260413.1" + "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260413.1" dependenciesMeta: "@typescript/native-preview-darwin-arm64": optional: true @@ -7856,7 +7860,7 @@ __metadata: optional: true bin: tsgo: bin/tsgo.js - checksum: 10c0/d017002bd72e0993d007f661fee11ccdf56a2067edd5f9f81ed4497e652d4e6aada5e6d9cbb480bd3277226fd385355f0269f2c03093cd11e956768a59cd5318 + checksum: 10c0/572b2aa8e07e0716e4a35ffe772d88db757d251ea2f9bb1b5ecdd8efba7de90671e0ac09277ab74aee7e647fa52d25ae23066c6c6e4a69ead885a698bf406f4c languageName: node linkType: hard @@ -13892,7 +13896,7 @@ __metadata: languageName: node linkType: hard -"he@npm:^1.2.0": +"he@npm:1.2.0, he@npm:^1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" bin: @@ -16729,6 +16733,15 @@ __metadata: languageName: node linkType: hard +"markdown-it-attrs@npm:^4.3.1": + version: 4.3.1 + resolution: "markdown-it-attrs@npm:4.3.1" + peerDependencies: + markdown-it: ">= 9.0.0" + checksum: 10c0/78b6ef298ef43f2163e91f4dc25e907a29e3845384837e5d9e1adc5243091f296840c95adfaa56c28d446c972bb705308c2bb783257a7549f574d396a42e152f + languageName: node + linkType: hard + "markdown-it@npm:^13.0.1": version: 13.0.2 resolution: "markdown-it@npm:13.0.2" @@ -17728,6 +17741,16 @@ __metadata: languageName: node linkType: hard +"node-html-parser@npm:^7.1.0": + version: 7.1.0 + resolution: "node-html-parser@npm:7.1.0" + dependencies: + css-select: "npm:^5.1.0" + he: "npm:1.2.0" + checksum: 10c0/8bee2616de374bb6f43b62cc8523f6d923cd199b2b7a42c6cf2ca503f1e89c32fb44195bd5af17d6fde1b992ad62c24465b29437213ae25f7770261fc9819173 + languageName: node + linkType: hard + "node-releases@npm:^2.0.27": version: 2.0.27 resolution: "node-releases@npm:2.0.27" @@ -20871,162 +20894,162 @@ __metadata: languageName: node linkType: hard -"sass-embedded-all-unknown@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-all-unknown@npm:1.98.0" +"sass-embedded-all-unknown@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-all-unknown@npm:1.99.0" dependencies: - sass: "npm:1.98.0" + sass: "npm:1.99.0" conditions: (!cpu=arm | !cpu=arm64 | !cpu=riscv64 | !cpu=x64) languageName: node linkType: hard -"sass-embedded-android-arm64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-android-arm64@npm:1.98.0" +"sass-embedded-android-arm64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-android-arm64@npm:1.99.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"sass-embedded-android-arm@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-android-arm@npm:1.98.0" +"sass-embedded-android-arm@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-android-arm@npm:1.99.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"sass-embedded-android-riscv64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-android-riscv64@npm:1.98.0" +"sass-embedded-android-riscv64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-android-riscv64@npm:1.99.0" conditions: os=android & cpu=riscv64 languageName: node linkType: hard -"sass-embedded-android-x64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-android-x64@npm:1.98.0" +"sass-embedded-android-x64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-android-x64@npm:1.99.0" conditions: os=android & cpu=x64 languageName: node linkType: hard -"sass-embedded-darwin-arm64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-darwin-arm64@npm:1.98.0" +"sass-embedded-darwin-arm64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-darwin-arm64@npm:1.99.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"sass-embedded-darwin-x64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-darwin-x64@npm:1.98.0" +"sass-embedded-darwin-x64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-darwin-x64@npm:1.99.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"sass-embedded-linux-arm64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-arm64@npm:1.98.0" +"sass-embedded-linux-arm64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-arm64@npm:1.99.0" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"sass-embedded-linux-arm@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-arm@npm:1.98.0" +"sass-embedded-linux-arm@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-arm@npm:1.99.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"sass-embedded-linux-musl-arm64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-musl-arm64@npm:1.98.0" +"sass-embedded-linux-musl-arm64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-musl-arm64@npm:1.99.0" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"sass-embedded-linux-musl-arm@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-musl-arm@npm:1.98.0" +"sass-embedded-linux-musl-arm@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-musl-arm@npm:1.99.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"sass-embedded-linux-musl-riscv64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-musl-riscv64@npm:1.98.0" +"sass-embedded-linux-musl-riscv64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-musl-riscv64@npm:1.99.0" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"sass-embedded-linux-musl-x64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-musl-x64@npm:1.98.0" +"sass-embedded-linux-musl-x64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-musl-x64@npm:1.99.0" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"sass-embedded-linux-riscv64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-riscv64@npm:1.98.0" +"sass-embedded-linux-riscv64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-riscv64@npm:1.99.0" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"sass-embedded-linux-x64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-linux-x64@npm:1.98.0" +"sass-embedded-linux-x64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-linux-x64@npm:1.99.0" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"sass-embedded-unknown-all@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-unknown-all@npm:1.98.0" +"sass-embedded-unknown-all@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-unknown-all@npm:1.99.0" dependencies: - sass: "npm:1.98.0" + sass: "npm:1.99.0" conditions: (!os=android | !os=darwin | !os=linux | !os=win32) languageName: node linkType: hard -"sass-embedded-win32-arm64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-win32-arm64@npm:1.98.0" +"sass-embedded-win32-arm64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-win32-arm64@npm:1.99.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"sass-embedded-win32-x64@npm:1.98.0": - version: 1.98.0 - resolution: "sass-embedded-win32-x64@npm:1.98.0" +"sass-embedded-win32-x64@npm:1.99.0": + version: 1.99.0 + resolution: "sass-embedded-win32-x64@npm:1.99.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"sass-embedded@npm:^1.98.0": - version: 1.98.0 - resolution: "sass-embedded@npm:1.98.0" +"sass-embedded@npm:^1.99.0": + version: 1.99.0 + resolution: "sass-embedded@npm:1.99.0" dependencies: "@bufbuild/protobuf": "npm:^2.5.0" colorjs.io: "npm:^0.5.0" immutable: "npm:^5.1.5" rxjs: "npm:^7.4.0" - sass-embedded-all-unknown: "npm:1.98.0" - sass-embedded-android-arm: "npm:1.98.0" - sass-embedded-android-arm64: "npm:1.98.0" - sass-embedded-android-riscv64: "npm:1.98.0" - sass-embedded-android-x64: "npm:1.98.0" - sass-embedded-darwin-arm64: "npm:1.98.0" - sass-embedded-darwin-x64: "npm:1.98.0" - sass-embedded-linux-arm: "npm:1.98.0" - sass-embedded-linux-arm64: "npm:1.98.0" - sass-embedded-linux-musl-arm: "npm:1.98.0" - sass-embedded-linux-musl-arm64: "npm:1.98.0" - sass-embedded-linux-musl-riscv64: "npm:1.98.0" - sass-embedded-linux-musl-x64: "npm:1.98.0" - sass-embedded-linux-riscv64: "npm:1.98.0" - sass-embedded-linux-x64: "npm:1.98.0" - sass-embedded-unknown-all: "npm:1.98.0" - sass-embedded-win32-arm64: "npm:1.98.0" - sass-embedded-win32-x64: "npm:1.98.0" + sass-embedded-all-unknown: "npm:1.99.0" + sass-embedded-android-arm: "npm:1.99.0" + sass-embedded-android-arm64: "npm:1.99.0" + sass-embedded-android-riscv64: "npm:1.99.0" + sass-embedded-android-x64: "npm:1.99.0" + sass-embedded-darwin-arm64: "npm:1.99.0" + sass-embedded-darwin-x64: "npm:1.99.0" + sass-embedded-linux-arm: "npm:1.99.0" + sass-embedded-linux-arm64: "npm:1.99.0" + sass-embedded-linux-musl-arm: "npm:1.99.0" + sass-embedded-linux-musl-arm64: "npm:1.99.0" + sass-embedded-linux-musl-riscv64: "npm:1.99.0" + sass-embedded-linux-musl-x64: "npm:1.99.0" + sass-embedded-linux-riscv64: "npm:1.99.0" + sass-embedded-linux-x64: "npm:1.99.0" + sass-embedded-unknown-all: "npm:1.99.0" + sass-embedded-win32-arm64: "npm:1.99.0" + sass-embedded-win32-x64: "npm:1.99.0" supports-color: "npm:^8.1.1" sync-child-process: "npm:^1.0.2" varint: "npm:^6.0.0" @@ -21069,7 +21092,7 @@ __metadata: optional: true bin: sass: dist/bin/sass.js - checksum: 10c0/c6f337002d5d96bd3e7192bcaec4b0e0516bd2ef232c4f851fd2a1f77234e3b8cc917a2b435eeaa797919a6e42c79a5a0191e4f2d93cd899887fa2d397b86531 + checksum: 10c0/f0fb3c6989dc42327a5bfec6ac881bbf2e564053d7179992ee0a38b463058593930559f67c3b0d0cd8f2c97885e9e9d9c0c9c009cafee3fe77b8021d197b2deb languageName: node linkType: hard @@ -21116,24 +21139,7 @@ __metadata: languageName: node linkType: hard -"sass@npm:1.98.0": - version: 1.98.0 - resolution: "sass@npm:1.98.0" - dependencies: - "@parcel/watcher": "npm:^2.4.1" - chokidar: "npm:^4.0.0" - immutable: "npm:^5.1.5" - source-map-js: "npm:>=0.6.2 <2.0.0" - dependenciesMeta: - "@parcel/watcher": - optional: true - bin: - sass: sass.js - checksum: 10c0/9e91daa20f970fefb364ac31289f070636da7aa7eaeb43e371ea98fa98085a6dbc2d3d058504226a02d07717faf0a4ce8d41b579ecb428c4a9d96b4dc1944a95 - languageName: node - linkType: hard - -"sass@npm:^1.81.0, sass@npm:^1.99.0": +"sass@npm:1.99.0, sass@npm:^1.81.0, sass@npm:^1.99.0": version: 1.99.0 resolution: "sass@npm:1.99.0" dependencies: @@ -22081,7 +22087,7 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^10.3.3, storybook@npm:^10.3.5": +"storybook@npm:^10.3.5": version: 10.3.5 resolution: "storybook@npm:10.3.5" dependencies: From d73c5c335e315f52535fd1895099f19ac5f7b88b Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 14 Apr 2026 09:07:33 +0200 Subject: [PATCH 25/72] =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/content/components/actions/button.md | 26 +++++++++++++++++++ docs/website/src/css/main.css | 12 +-------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/website/src/content/components/actions/button.md b/docs/website/src/content/components/actions/button.md index ee84a6794b..fabb237140 100644 --- a/docs/website/src/content/components/actions/button.md +++ b/docs/website/src/content/components/actions/button.md @@ -70,6 +70,17 @@ Buttons are available in three sizes: ``` +### Disabled + +You can either use the `disabled` attribute to disable a button or set the `aria-disabled="true"` attribute for accessibility. The former will prevent the button from being focusable and will not trigger any events, while the latter will keep the button focusable but will indicate to assistive technologies that the action is unavailable. + +```html {.example} + + Disabled + Aria Disabled + +``` + ### Icon buttons Icon buttons are used for actions that can be represented by an icon, such as "close" or "edit". Always provide a text label for accessibility, either through an `` or using `aria-label`. @@ -89,6 +100,21 @@ Icon buttons are used for actions that can be represented by an icon, such as "c ``` +### Link buttons + +Sometimes you want a link to look like a button. In that case, you can use the `fill="link"` attribute and wrap the button's content in an `` element. + +```html {.example} + + + Link + + + New window + + +``` + ## Accessibility The button component renders a native ` -
<button>Click me</button>
- - `); - }); + let el: CodeExample; - it('should render', () => { - expect(el).to.exist; - expect(el).to.be.instanceOf(DocCodeExampleClass); - }); + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html` + + +
<button>Click me</button>
+
+ `); + }); - it('should render a preview area', () => { - const preview = el.renderRoot.querySelector('.preview'); + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(CodeExample); + }); - expect(preview).to.exist; - }); + it('should render a demo area', () => { + expect(el.renderRoot.querySelector('.demo')).to.exist; + }); - it('should render slotted content in the preview', () => { - const slot = el.renderRoot.querySelector('.preview slot'); + it('should render slotted content in the demo area', () => { + const slot = el.renderRoot.querySelector('.demo slot'); - expect(slot).to.exist; - expect(slot?.assignedElements()).to.have.length.greaterThan(0); - }); + expect(slot).to.exist; + expect(slot?.assignedElements()).to.have.length.greaterThan(0); + }); + + it('should render a source area', () => { + expect(el.renderRoot.querySelector('.source')).to.exist; + }); + + it('should render the source slot', () => { + const slot = el.renderRoot.querySelector('slot[name="source"]'); - it('should render a source area', () => { - const source = el.renderRoot.querySelector('.source'); + expect(slot).to.exist; + expect(slot?.assignedElements()).to.have.length.greaterThan(0); + }); - expect(source).to.exist; + it('should render a copy button', () => { + expect(el.renderRoot.querySelector('doc-copy-button')).to.exist; + }); + + it('should render the copy button with no content when source is not set', () => { + const copyButton = el.renderRoot.querySelector('doc-copy-button'); + + expect(copyButton?.content).to.be.undefined; + }); + + it('should copy the source to the clipboard on click', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + el.renderRoot.querySelector('doc-copy-button')!.click(); + await el.updateComplete; + + expect(writeText).toHaveBeenCalledWith(''); + + writeText.mockRestore(); + }); }); - it('should render the source slot', () => { - const slot = el.renderRoot.querySelector('slot[name="source"]'); + describe('justify', () => { + beforeEach(async () => { + el = await fixture(html``); + }); + + it('should reflect the justify state on the element', async () => { + const demo = el.renderRoot.querySelector('.demo'); - expect(slot).to.exist; - expect(slot?.assignedElements()).to.have.length.greaterThan(0); + expect(demo).to.have.style('justify-content', 'center'); + }); }); }); diff --git a/docs/components/src/code-example/code-example.stories.ts b/docs/components/src/code-example/code-example.stories.ts index b54278bf69..87648614cb 100644 --- a/docs/components/src/code-example/code-example.stories.ts +++ b/docs/components/src/code-example/code-example.stories.ts @@ -1,7 +1,11 @@ +import '@sl-design-system/button/register.js'; import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { CodeExample } from './code-example.js'; -type Story = StoryObj; +type Props = Pick; +type Story = StoryObj; try { customElements.define('doc-code-example', CodeExample); @@ -10,31 +14,48 @@ try { } export default { - title: 'Code Example' -} satisfies Meta; - -export const Basic: Story = { - render: () => { - const el = document.createElement('doc-code-example'); - - el.innerHTML = ` - -
<button type="button">Click me</button>
- `; - - return el; + title: 'Code Example', + argTypes: { + inverted: { + control: { type: 'boolean' } + }, + justify: { + control: { type: 'inline-radio' }, + options: ['start', 'center', 'end', 'stretch'] + }, + showSource: { + control: { type: 'boolean' } + } + }, + render: ({ inverted, justify, showSource }) => html` + + Click me +
<sl-button>Click me</sl-button>
+
+ ` +} satisfies Meta; + +export const Basic: Story = {}; + +export const Inverted: Story = { + args: { + inverted: true + } +}; +export const JustifyCenter: Story = { + args: { + justify: 'center' } }; -export const WithSource: Story = { - render: () => { - const el = document.createElement('doc-code-example'); - - el.innerHTML = ` -

Hello world

-
<p>Hello <strong>world</strong></p>
- `; +export const JustifyStretch: Story = { + args: { + justify: 'stretch' + } +}; - return el; +export const ShowSource: Story = { + args: { + showSource: true } }; diff --git a/docs/components/src/code-example/code-example.ts b/docs/components/src/code-example/code-example.ts index d4117d486c..850f82eefc 100644 --- a/docs/components/src/code-example/code-example.ts +++ b/docs/components/src/code-example/code-example.ts @@ -1,37 +1,65 @@ -import { type CSSResultGroup, LitElement, type PropertyValues, type TemplateResult, html } from 'lit'; -import { property } from 'lit/decorators.js'; +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { Icon } from '@sl-design-system/icon'; +import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { CopyButton } from '../copy-button/copy-button.js'; import styles from './code-example.css' with { type: 'css' }; -export class CodeExample extends LitElement { +export class CodeExample extends ScopedElementsMixin(LitElement) { /** @internal */ - static styles: CSSResultGroup = styles; + static get scopedElements(): ScopedElementsMap { + return { + 'doc-copy-button': CopyButton, + 'sl-icon': Icon + }; + } /** @internal */ - #internals = this.attachInternals(); + static styles: CSSResultGroup = styles; + + /** Whether the demo background should use the inverted background color. */ + @property({ type: Boolean, reflect: true }) inverted?: boolean; /** The alignment of the content within the demo area. */ - @property() align?: 'start' | 'center' | 'end' | 'fill'; + @property() justify?: 'start' | 'center' | 'end' | 'stretch'; + + /** Whether to show the source code by default (without requiring the user to expand it). */ + @property({ type: Boolean, reflect: true, attribute: 'show-source' }) showSource?: boolean; + + /** @internal The source code to be copied to the clipboard. */ + @state() source?: string; override render(): TemplateResult { + const source = html` +
+ + +
+ `; + return html`
-
- -
+ ${this.showSource + ? source + : html` +
+ + Code + + + ${source} +
+ `} `; } - updated(changes: PropertyValues): void { - // Remove old value - if (changes.get('align')) { - this.#internals.states.delete(`align-${changes.get('align')}`); - } + #onSlotChange(event: Event & { target: HTMLSlotElement }): void { + const pre = event.target + .assignedElements({ flatten: true }) + .find((el): el is HTMLPreElement => el.tagName === 'PRE'); - // Add new value - if (this.align) { - this.#internals.states.add(`align-${this.align}`); - } + this.source = pre?.textContent?.trim() ?? ''; } } diff --git a/docs/website/src/content/components/actions/button.md b/docs/website/src/content/components/actions/button.md index 98ba5fbb85..56a25ee55b 100644 --- a/docs/website/src/content/components/actions/button.md +++ b/docs/website/src/content/components/actions/button.md @@ -7,7 +7,7 @@ eleventyNavigation: order: 1 --- -```html {.example} +```html {.example .show-source} Button ``` diff --git a/docs/website/src/transformers/code-examples.js b/docs/website/src/transformers/code-examples.js index 26144f1311..2f1489f509 100644 --- a/docs/website/src/transformers/code-examples.js +++ b/docs/website/src/transformers/code-examples.js @@ -17,7 +17,8 @@ export function codeExamplesTransformer(options = {}) { const pre = code.closest('pre'), uuid = crypto.randomUUID(), id = `code-example-${uuid.slice(-12)}`, - demo = pre.textContent; + demo = pre.textContent, + showSource = code.classList.contains('show-source'); const langClass = [...code.classList.values()].find(val => val.startsWith('language-')), lang = langClass ? langClass.replace(/^language-/, '') : 'plain'; @@ -25,7 +26,7 @@ export function codeExamplesTransformer(options = {}) { code.innerHTML = highlightCode(code.textContent ?? '', lang); const codeExample = parse(` - + ${demo}
${code.innerHTML}
diff --git a/packages/components/button/src/button.scss b/packages/components/button/src/button.scss index 35def75ae4..70bc5b8264 100644 --- a/packages/components/button/src/button.scss +++ b/packages/components/button/src/button.scss @@ -237,6 +237,7 @@ button { color: var(--sl-color-foreground-secondary-bold); cursor: pointer; display: inline-flex; + flex-grow: 1; font: inherit; font-weight: var(--sl-text-new-typeset-fontWeight-semiBold); gap: var(--sl-size-075); From ec18529a3c7641f7c5c34915e8b438ac62580c99 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 15 Apr 2026 13:29:11 +0200 Subject: [PATCH 40/72] =?UTF-8?q?=E2=98=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/website/package.json | 2 + .../src/content/components/actions/button.md | 63 ++++++++++++----- docs/website/src/css/main.css | 70 ++++++++++++------- docs/website/src/js/main.js | 1 + .../website/src/transformers/code-examples.js | 3 +- docs/website/src/utils/markdown.js | 18 ++++- yarn.lock | 16 +++++ 7 files changed, 129 insertions(+), 44 deletions(-) diff --git a/docs/website/package.json b/docs/website/package.json index c0ad05cce5..7d9f407339 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -38,6 +38,8 @@ "lit": "^3.3.2", "markdown-it": "^14.1.1", "markdown-it-attrs": "^4.3.1", + "markdown-it-container": "^4.0.0", + "markdown-it-deflist": "^3.0.0", "node-html-parser": "^7.1.0", "prismjs": "^1.30.0", "slugify": "^1.6.9", diff --git a/docs/website/src/content/components/actions/button.md b/docs/website/src/content/components/actions/button.md index 56a25ee55b..ec849df9ff 100644 --- a/docs/website/src/content/components/actions/button.md +++ b/docs/website/src/content/components/actions/button.md @@ -11,11 +11,36 @@ eleventyNavigation: Button ``` +## Usage + +Buttons should be used in user interfaces when you want to provide users with a clear and actionable way to interact with a webpage, application, or device. + +Buttons are used to trigger specific actions or functions. For example, you can use a "Submit" button in a form to send user input to a server, or a "Save" button to save changes in an application. + +::: info +Do not disable a button by default and leave it up to the user to figure out why the action is unavailable. Instead, provide an explanation using a tooltip or by including helper text next to the button. +::: + ## Examples ### Variants -Use the `variant` attribute to indicate the type of action. +Use the `variant` attribute to indicate the urgency or sentiment of the action. + +Primary +: The most important action on the page; the next step in the main user flow. + +Secondary +: Secondary flows, or when there is no clear hierarchy (e.g. dashboards). + +Success +: Confirming a successful or completed operation. + +Warning +: Actions requiring caution or extra user confirmation. + +Danger +: Irreversible or potentially destructive actions. ```html {.example} @@ -28,9 +53,27 @@ Use the `variant` attribute to indicate the type of action. ``` +There is an additional `inverted` variant that is designed for use on dark backgrounds. + +```html {.example .inverted} +Inverted +``` + ### Fill -Use the `fill` attribute to change the button's level of emphasis. +Use the `fill` attribute to indicate how essential the action is. + +Solid +: Essential actions that move the user forward in the flow. + +Outline +: Important but optional; draws attention without blocking progress. + +Ghost +: Suggestive or secondary actions that shouldn't distract from the main flow. + +Link +: Informational actions, such as "Read more" or "View details". ```html {.example} @@ -70,7 +113,7 @@ Buttons are available in three sizes: ### Disabled -You can either use the `disabled` attribute to disable a button or set the `aria-disabled="true"` attribute for accessibility. The former will prevent the button from being focusable and will not trigger any events, while the latter will keep the button focusable but will indicate to assistive technologies that the action is unavailable. +You can either use the `disabled` attribute to disable a button or set the `aria-disabled="true"` attribute for accessibility. Both will not trigger any events. The former will prevent the button from being focusable, while the latter will keep the button focusable but will indicate to assistive technologies that the action is unavailable. ```html {.example} @@ -117,16 +160,4 @@ Sometimes you want a link to look like a button. In that case, you can use the ` The button component renders a native `'); }); it('should copy the source to the clipboard on click', async () => { const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); - el.renderRoot.querySelector('doc-copy-button')!.click(); + const docCode = el.renderRoot.querySelector('doc-code') as Element & { renderRoot: ShadowRoot }; + const copyButtonHost = docCode?.renderRoot.querySelector('doc-copy-button') as Element & { renderRoot: ShadowRoot }; + copyButtonHost?.renderRoot.querySelector('sl-button')!.click(); await el.updateComplete; expect(writeText).toHaveBeenCalledWith(''); @@ -79,7 +84,7 @@ describe('doc-code-example', () => { it('should reflect the justify state on the element', async () => { const demo = el.renderRoot.querySelector('.demo'); - expect(demo).to.have.style('justify-content', 'center'); + expect(demo).to.have.style('justify-items', 'center'); }); }); }); diff --git a/docs/components/src/code-example/code-example.ts b/docs/components/src/code-example/code-example.ts index 850f82eefc..d6d271fa34 100644 --- a/docs/components/src/code-example/code-example.ts +++ b/docs/components/src/code-example/code-example.ts @@ -1,15 +1,15 @@ import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; import { Icon } from '@sl-design-system/icon'; import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { CopyButton } from '../copy-button/copy-button.js'; +import { property } from 'lit/decorators.js'; +import { Code } from '../code/code.js'; import styles from './code-example.css' with { type: 'css' }; export class CodeExample extends ScopedElementsMixin(LitElement) { /** @internal */ static get scopedElements(): ScopedElementsMap { return { - 'doc-copy-button': CopyButton, + 'doc-code': Code, 'sl-icon': Icon }; } @@ -26,15 +26,11 @@ export class CodeExample extends ScopedElementsMixin(LitElement) { /** Whether to show the source code by default (without requiring the user to expand it). */ @property({ type: Boolean, reflect: true, attribute: 'show-source' }) showSource?: boolean; - /** @internal The source code to be copied to the clipboard. */ - @state() source?: string; - override render(): TemplateResult { const source = html` -
- - -
+ + + `; return html` @@ -54,12 +50,4 @@ export class CodeExample extends ScopedElementsMixin(LitElement) { `} `; } - - #onSlotChange(event: Event & { target: HTMLSlotElement }): void { - const pre = event.target - .assignedElements({ flatten: true }) - .find((el): el is HTMLPreElement => el.tagName === 'PRE'); - - this.source = pre?.textContent?.trim() ?? ''; - } } diff --git a/docs/components/src/code/code.css b/docs/components/src/code/code.css new file mode 100644 index 0000000000..72e1239705 --- /dev/null +++ b/docs/components/src/code/code.css @@ -0,0 +1,27 @@ +:host { + background: var(--sl-color-background-accent-grey-subtlest); + border-radius: var(--sl-size-borderRadius-default); + display: block; + overflow-x: auto; + padding: var(--sl-size-200); + position: relative; +} + +:host(:hover) doc-copy-button, +doc-copy-button:focus-within { + opacity: 1; +} + +::slotted(pre) { + background: transparent !important; + margin: 0 !important; + padding: 0 !important; +} + +doc-copy-button { + inset-block-start: var(--sl-size-100); + inset-inline-end: var(--sl-size-100); + opacity: 0; + position: absolute; + transition: opacity 0.15s; +} diff --git a/docs/components/src/code/code.spec.ts b/docs/components/src/code/code.spec.ts new file mode 100644 index 0000000000..a17ec9dfdd --- /dev/null +++ b/docs/components/src/code/code.spec.ts @@ -0,0 +1,70 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Code } from './code.js'; + +try { + customElements.define('doc-code', Code); +} catch { + /* empty */ +} + +describe('doc-code', () => { + let el: Code; + + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html` + +
const foo = 'bar';
+
+ `); + }); + + it('should render', () => { + expect(el).to.exist; + expect(el).to.be.instanceOf(Code); + }); + + it('should render a default slot', () => { + const slot = el.renderRoot.querySelector('slot:not([name])'); + + expect(slot).to.exist; + expect(slot?.assignedElements()).to.have.length.greaterThan(0); + }); + + it('should render a copy button', () => { + expect(el.renderRoot.querySelector('doc-copy-button')).to.exist; + }); + + it('should set the copy button content from the slotted pre element', () => { + const copyButton = el.renderRoot.querySelector('doc-copy-button'); + + expect(copyButton?.content).to.equal("const foo = 'bar';"); + }); + + it('should copy the source to the clipboard on click', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); + + const copyButtonHost = el.renderRoot.querySelector('doc-copy-button')! as Element & { renderRoot: ShadowRoot }; + copyButtonHost.renderRoot.querySelector('sl-button')!.click(); + await el.updateComplete; + + expect(writeText).toHaveBeenCalledWith("const foo = 'bar';"); + + writeText.mockRestore(); + }); + }); + + describe('without pre element', () => { + beforeEach(async () => { + el = await fixture(html`some text`); + }); + + it('should render a copy button with no content', () => { + const copyButton = el.renderRoot.querySelector('doc-copy-button'); + + expect(copyButton?.content).to.equal(''); + }); + }); +}); diff --git a/docs/components/src/code/code.stories.ts b/docs/components/src/code/code.stories.ts new file mode 100644 index 0000000000..6c19815c06 --- /dev/null +++ b/docs/components/src/code/code.stories.ts @@ -0,0 +1,48 @@ +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { Code } from './code.js'; + +type Story = StoryObj; + +try { + customElements.define('doc-code', Code); +} catch { + /* empty */ +} + +export default { + title: 'Code', + render: () => html` + +
import { LitElement, html } from 'lit';
+
+export class MyElement extends LitElement {
+  override render() {
+    return html\`<p>Hello world!</p>\`;
+  }
+}
+
+ ` +} satisfies Meta; + +export const Basic: Story = {}; + +export const MultiLine: Story = { + render: () => html` + +
<sl-button variant="primary">Click me</sl-button>
+<sl-button variant="default">Cancel</sl-button>
+
+ ` +}; + +export const CSS: Story = { + render: () => html` + +
:host {
+  display: block;
+  color: red;
+}
+
+ ` +}; diff --git a/docs/components/src/code/code.ts b/docs/components/src/code/code.ts new file mode 100644 index 0000000000..f88caca8ed --- /dev/null +++ b/docs/components/src/code/code.ts @@ -0,0 +1,35 @@ +import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; +import { state } from 'lit/decorators.js'; +import { CopyButton } from '../copy-button/copy-button.js'; +import styles from './code.css' with { type: 'css' }; + +export class Code extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'doc-copy-button': CopyButton + }; + } + + /** @internal */ + static override styles: CSSResultGroup = styles; + + /** @internal The source code to be copied to the clipboard. */ + @state() source?: string; + + override render(): TemplateResult { + return html` + + + `; + } + + #onSlotChange(event: Event & { target: HTMLSlotElement }): void { + const pre = event.target + .assignedElements({ flatten: true }) + .find((el): el is HTMLPreElement => el.tagName === 'PRE'); + + this.source = pre?.textContent?.trim() ?? ''; + } +} diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index 5cb50d6cdf..9a5f68526c 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -46,8 +46,9 @@ export default defineConfig({ tsgo: true, }, entry: [ - 'src/copy-button/copy-button.ts', + 'src/code/code.ts', 'src/code-example/code-example.ts', + 'src/copy-button/copy-button.ts', 'src/heading/heading.ts', 'src/install-info/install-info.ts', 'src/page-toc/page-toc.ts', diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index e15d440636..854ba6990d 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -1,11 +1,12 @@ +import eleventyNavigationPlugin from '@11ty/eleventy-navigation'; +import * as esbuild from 'esbuild'; import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { basename, dirname, join, resolve } from 'node:path'; import { parse as HTMLParse } from 'node-html-parser'; -import * as esbuild from 'esbuild'; -import eleventyNavigationPlugin from '@11ty/eleventy-navigation'; import { anchorHeadingsTransformer } from './src/transformers/anchor-headings.js'; import { codeExamplesTransformer } from './src/transformers/code-examples.js'; +import { highlightCodeTransformer } from './src/transformers/highlight-code.js'; import { getComponents } from './src/utils/manifest.js'; import { markdown } from './src/utils/markdown.js'; @@ -82,7 +83,7 @@ export default async function (eleventyConfig) { eleventyConfig.addTransform('component', function (content) { let doc = HTMLParse(content, { blockTextElements: { code: true } }); - const transformers = [anchorHeadingsTransformer(), codeExamplesTransformer()]; + const transformers = [anchorHeadingsTransformer(), codeExamplesTransformer(), highlightCodeTransformer()]; for (const transformer of transformers) { transformer.call(this, doc); diff --git a/docs/website/src/content/components/actions/button.md b/docs/website/src/content/components/actions/button.md index ec849df9ff..d4a06aa494 100644 --- a/docs/website/src/content/components/actions/button.md +++ b/docs/website/src/content/components/actions/button.md @@ -160,4 +160,11 @@ Sometimes you want a link to look like a button. In that case, you can use the ` The button component renders a native ` - +
= 0 ? `result-${this.activeIndex}` : nothing} aria-controls="search-results" placeholder="Search documentation..." role="combobox" aria-expanded="true" - aria-autocomplete="list" - > + aria-autocomplete="list"> -
    -
  • - -
    - Button - Buttons represent actions that are available to the user. - docs/components/button -
    -
  • -
  • - -
    - Dialog - - Dialogs, sometimes called "modals", appear above the page content. - - docs/components/dialog -
    -
  • -
  • - -
    - Drawer - Drawers slide in from a container to expose additional options. - docs/components/drawer -
    -
  • + ${this.#renderResults()} +
+
+ `; + } + + #renderResults(): TemplateResult { + if (this.loading) { + return html`

Loading search index…

`; + } + + if (!this.query) { + return html`

Start typing to search the documentation.

`; + } + + if (this.results.length === 0) { + return html`

No results found for “${this.query}”.

`; + } + + return html` +
    + ${this.results.map( + (result, index) => html`
  • - + class=${index === this.activeIndex ? 'active' : nothing} + aria-selected=${index === this.activeIndex || nothing} + @click=${() => this.#select(index)}> +
    - Visual Tests - A page to visually test component styles against native styles. - docs/resources/visual-tests + ${result.title} + ${result.description + ? html`${result.description}` + : nothing} + ${result.url.replace(/^\//, '')}
  • -
- -
+ ` + )} + `; } @@ -131,11 +158,102 @@ export class Search extends ScopedElementsMixin(LitElement) { this.dialog?.showModal(); this.activeIndex = -1; + void this.#loadIndex(); + requestAnimationFrame(() => { this.renderRoot.querySelector('sl-search-field')?.focus(); }); } + /** Lazily fetches and parses the search index, building the MiniSearch instance once. */ + async #loadIndex(): Promise { + if (this.#index || this.#loadPromise) { + return this.#loadPromise; + } + + this.loading = true; + + this.#loadPromise = (async () => { + try { + const response = await fetch('/search.json'), + data = (await response.json()) as SearchData; + + this.#index = MiniSearch.loadJSON(JSON.stringify(data.searchIndex), { + fields: SEARCH_FIELDS + }); + this.#map = data.map; + } catch { + // Leave the index undefined; searches will simply return no results. + } finally { + this.loading = false; + } + })(); + + return this.#loadPromise; + } + + #onSearch(event: SlSearchEvent): void { + this.#search(event.detail); + } + + #search(query: string): void { + this.query = query.trim(); + + if (!this.query || !this.#index) { + this.results = []; + this.activeIndex = -1; + return; + } + + const matches = this.#index.search(this.query, { + prefix: true, + fuzzy: 0.2, + boost: { t: 20, h: 10, c: 1 } + }); + + // Re-rank so that title matches win: MiniSearch's score doesn't account for + // where in the title a term matches, so boost exact/prefix/word-boundary title hits. + const queryLower = this.query.toLowerCase(), + rankTitle = (title: string): number => { + if (title === queryLower) { + return 3; + } else if (title.startsWith(queryLower)) { + return 2; + } else if ( + new RegExp(`\\b${queryLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`).test(title) + ) { + return 1; + } else { + return 0; + } + }; + + matches.sort((a: SearchResult, b: SearchResult) => { + const rankDiff = + rankTitle((this.#map[b.id]?.title ?? '').toLowerCase()) - + rankTitle((this.#map[a.id]?.title ?? '').toLowerCase()); + + return rankDiff !== 0 ? rankDiff : b.score - a.score; + }); + + this.results = matches + .map(match => this.#map[match.id]) + .filter((page): page is SearchResultItem => !!page?.url); + + // Highlight the first result so Enter navigates to the top match. + this.activeIndex = this.results.length > 0 ? 0 : -1; + } + + #select(index: number): void { + const result = this.results[index]; + if (!result) { + return; + } + + this.dialog?.close(); + window.location.href = result.url; + } + #onBackdropClick(event: MouseEvent): void { const dialog = this.dialog; @@ -162,23 +280,27 @@ export class Search extends ScopedElementsMixin(LitElement) { return; } - const items = this.renderRoot.querySelectorAll('.results li'); - - if (!items.length) { + if (this.results.length === 0) { return; } if (event.key === 'ArrowDown') { event.preventDefault(); - this.activeIndex = this.activeIndex < items.length - 1 ? this.activeIndex + 1 : 0; - items[this.activeIndex]?.scrollIntoView({ block: 'nearest' }); + this.activeIndex = this.activeIndex < this.results.length - 1 ? this.activeIndex + 1 : 0; + this.#scrollActiveIntoView(); } else if (event.key === 'ArrowUp') { event.preventDefault(); - this.activeIndex = this.activeIndex > 0 ? this.activeIndex - 1 : items.length - 1; - items[this.activeIndex]?.scrollIntoView({ block: 'nearest' }); - } else if ((event.key === 'Enter' || event.key === ' ') && this.activeIndex >= 0) { + this.activeIndex = this.activeIndex > 0 ? this.activeIndex - 1 : this.results.length - 1; + this.#scrollActiveIntoView(); + } else if (event.key === 'Enter' && this.activeIndex >= 0) { event.preventDefault(); - items[this.activeIndex]?.dispatchEvent(new Event('click', { bubbles: true })); + this.#select(this.activeIndex); } } + + #scrollActiveIntoView(): void { + this.renderRoot + .querySelectorAll('.results li') + [this.activeIndex]?.scrollIntoView({ block: 'nearest' }); + } } diff --git a/docs/website/eleventy.config.js b/docs/website/eleventy.config.js index a06ae83d96..ffb4c3067f 100644 --- a/docs/website/eleventy.config.js +++ b/docs/website/eleventy.config.js @@ -7,6 +7,7 @@ import { parse as HTMLParse } from 'node-html-parser'; import { anchorHeadingsTransformer } from './src/transformers/anchor-headings.js'; import { codeExamplesTransformer } from './src/transformers/code-examples.js'; import { highlightCodeTransformer } from './src/transformers/highlight-code.js'; +import { searchPlugin } from './src/plugins/search.js'; import { getComponents, getCustomElements } from './src/utils/manifest.js'; import { markdown } from './src/utils/markdown.js'; @@ -105,6 +106,17 @@ export default async function (eleventyConfig) { return doc.toString(); }); + // Build the client-side search index. Registered after the `component` transform + // so headings and other content transforms are already applied. + eleventyConfig.addPlugin( + searchPlugin({ + selectorsToIgnore: ['doc-code-example', 'doc-page-toc', 'pre'], + getTitle: doc => (doc.querySelector('title')?.textContent ?? '').replace(/\s*\|\s*SL Design System\s*$/, ''), + getDescription: doc => doc.querySelector('main.content p')?.textContent ?? '', + getContent: doc => doc.querySelector('main.content')?.textContent ?? '' + }) + ); + eleventyConfig.on('eleventy.before', async () => { // Scan pages for icon names in eleventyNavigation frontmatter const icons = new Set(), diff --git a/docs/website/package.json b/docs/website/package.json index ae2fb2a83b..388a0d3764 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -40,6 +40,7 @@ "markdown-it-attrs": "^4.3.1", "markdown-it-container": "^4.0.0", "markdown-it-deflist": "^3.0.0", + "minisearch": "^7.2.0", "node-html-parser": "^7.1.0", "prismjs": "^1.30.0", "slugify": "^1.6.9", diff --git a/docs/website/src/content/introduction/faq.md b/docs/website/src/content/introduction/faq.md index 53ef517a09..c391252bf3 100644 --- a/docs/website/src/content/introduction/faq.md +++ b/docs/website/src/content/introduction/faq.md @@ -7,3 +7,31 @@ eleventyNavigation: order: 1 icon: circle-question --- + +### The font in the components doesn't match the rest of my application. + +Make sure the font you use in the application has the exact same name as the font used in the theme tokens. If the names align but the fonts still don't load, confirm the font is actually loading — the components use the fonts available to the page, so load them as you normally would. Contact the team if the problem persists. + +### The icons in my components are broken. + +Make sure you have called the `setup()` method as described in the "Initialize the theme" step. This method also initializes the icon set, so icons won't render without it. + +### How do I use the dark mode of the theme? + +For themes that support dark mode, including `all.css` loads both modes and switches automatically based on the system preference. Alternatively, load `light.css` and `dark.css` yourself based on your own conditions (include `base.css` as well). You can also use the SCSS mixins (`@mixin sl-theme-base`, `@mixin sl-theme-light`, `@mixin sl-theme-dark`) to wrap the tokens with your own theme-switching selectors. + +### How do I setup my Bitbucket pipeline to work with the SLDS packages? + +For the Sanoma Learning Bitbucket pipelines there is a common token you can use to authenticate with the GitHub NPM registry: `${SLDESIGNSYSTEMS_GITHUB_NPM_AUTH_TOKEN}`. + +### Which browsers are supported? + +We support the 2 latest versions of the major browsers: Chrome, Edge, Firefox, and Safari. + +### Which versions of Angular are supported? + +We support the 2 latest versions of Angular. Check the Angular documentation for the currently actively supported versions. + +### Do you support SSR? + +Server-side rendering of web components is a hard problem, and the related web standards continue to evolve. At the moment we do not support this, but we may look at it again in the future. diff --git a/docs/website/src/content/introduction/getting-started/for-developers.md b/docs/website/src/content/introduction/getting-started/for-developers.md index b422d1c81e..37abe6c12c 100644 --- a/docs/website/src/content/introduction/getting-started/for-developers.md +++ b/docs/website/src/content/introduction/getting-started/for-developers.md @@ -7,4 +7,92 @@ eleventyNavigation: order: 1 --- -Getting started for developers. +The Sanoma Learning Design System (SLDS) provides a collection of framework-agnostic web components and matching themes for building applications. Because the components are built on web standards, they work in any modern framework — or with no framework at all. This guide walks you through gaining access to the packages, installing them, and getting your first component rendering on screen. + +## Access to packages + +The packages are published to a private registry, so before you can install anything you need to be granted access. SLDS packages are available through two channels, depending on where you work: + +- **Nexus Server**: This is the route for Sanoma Learning product teams, where the packages are mirrored internally. Contact TechOps for the access details and registry configuration. +- **GitHub Packages**: For everyone else, the design system team has to add your account to the organization before you can pull packages. Reach out via their Slack channel or email designsystem@sanoma.com with your company email address, and mention which product you're working on so they can grant the right access. + +Once your account has been approved, you authenticate with the GitHub NPM registry using a personal access token. Generate a token with the `read_packages` permission and add it to your `~/.npmrc`, along with the registry mapping for the `@sl-design-system` scope so npm knows where to look for these packages: + +``` +//npm.pkg.github.com/:_authToken= +@sl-design-system:registry=https://npm.pkg.github.com +``` + +## Installation steps + +With access in place, you can install the packages and wire up a theme. The steps below take you from an empty project to a working component. + +1. **Install the components and a theme** via npm. Each component lives in its own package, so you only install what you actually use. A theme package (for example, Sanoma Learning) supplies the design tokens, fonts hooks, and icon set the components rely on: + + ```bash + npm add @sl-design-system/button @sl-design-system/sanoma-learning + ``` + +2. **Import the theme stylesheet** in your HTML. This loads the CSS custom properties (design tokens) that give the components their look and feel: + + ```html + + ``` + +3. **Initialize the theme** in your JavaScript. Calling `setup()` registers the theme and initializes the icon set, so this step is required for icons to render correctly: + + ```javascript + import { setup } from '@sl-design-system/sanoma-learning'; + + setup(); + ``` + +4. **Install the polyfills.** The components depend on two browser features that aren't yet available everywhere: scoped custom element registries (which let components register their own internal elements without polluting the global registry or clashing with other libraries) and the invoker commands API (which powers declarative interactions such as buttons opening dialogs). The polyfills fill these gaps so the components behave consistently across all supported browsers: + + ```bash + npm add @webcomponents/scoped-custom-element-registry invokers-polyfill + ``` + +5. **Import the polyfills** before any components, so the features are in place by the time the components load: + + ```javascript + import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; + import 'invokers-polyfill'; + ``` + +6. **Register the components** you want to use. Importing a component's `register.js` defines its custom element with the browser: + + ```javascript + import '@sl-design-system/button/register.js'; + ``` + +7. **Use them in your markup** like any other HTML element: + + ```html + Hello world! + ``` + +## Key configuration notes + +A few details are worth keeping in mind as you integrate the components into a real project: + +- **Peer dependencies** are not installed automatically and must be added separately. Check the warnings npm prints during installation to see what each package expects. +- **Fonts** are intentionally not bundled with the design system. Load them through your application's normal font-loading process, making sure the font names match the names used in the theme tokens. +- **Dark mode** can be enabled either by loading `dark.css` alongside the base styles, or by controlling theme switching yourself through the provided SCSS mixins. +- **Angular** projects that report template validation errors for the custom elements should add `CUSTOM_ELEMENTS_SCHEMA` to the relevant module or component. +- **Deprecated tokens**: if you depend on tokens that have since been deprecated, the `light-deprecated.css` and `dark-deprecated.css` stylesheets keep them available while you migrate. + +## Browser & framework support + +The design system targets modern, evergreen environments: + +- **Browsers**: the latest 2 versions of Chrome, Edge, Firefox, and Safari. +- **Angular**: the latest 2 supported versions. +- **SSR**: server-side rendering of the web components is not currently supported. + +## Resources + +To see a full integration in context, take a look at the example apps: + +- [Angular example](https://github.com/sl-design-system/angular-demo) +- [Lit example](https://github.com/sl-design-system/example-design-system-lit-app) diff --git a/docs/website/src/plugins/search.js b/docs/website/src/plugins/search.js new file mode 100644 index 0000000000..7bc1a301e4 --- /dev/null +++ b/docs/website/src/plugins/search.js @@ -0,0 +1,129 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import MiniSearch from 'minisearch'; +import { parse } from 'node-html-parser'; + +function collapseWhitespace(string) { + return string.replace(/\s+/g, ' ').trim(); +} + +/** + * Eleventy plugin that builds a MiniSearch index from the rendered HTML and + * writes it to `search.json` in the output directory. The client-side search + * (see the `doc-search` component) fetches this file and queries it in the browser. + * + * Based on the `searchPlugin` from the Web Awesome documentation. + */ +export function searchPlugin(options = {}) { + options = { + selectorsToIgnore: [], + defaultIcon: 'far-file-lines', + getTitle: doc => doc.querySelector('title')?.textContent ?? '', + getDescription: doc => doc.querySelector('meta[name="description"]')?.getAttribute('content') ?? '', + getHeadings: doc => + [...doc.querySelectorAll('h1, h2, h3, h4, h5, h6, doc-heading')].map(heading => heading.textContent ?? ''), + getContent: doc => doc.querySelector('body')?.textContent ?? '', + ...options + }; + + return function (eleventyConfig) { + // Input paths of templates whose pages should be excluded (via `unlisted: true`). + const excludedInputPaths = new Set(); + + // Indexable pages keyed by URL. Keying by URL (rather than input path) means + // paginated templates contribute one entry per generated page. + const pagesToIndex = new Map(); + + // [urlPrefix, icon] pairs collected from pages that define an `icon` in their + // `eleventyNavigation` front matter (e.g. category sections). A result inherits + // the icon of the longest matching prefix, so component pages show their category icon. + const iconsByPrefix = []; + + eleventyConfig.addPreprocessor('exclude-unlisted-from-search', '*', function (data, content) { + if (data.unlisted) { + excludedInputPaths.add(data.page.inputPath); + } else { + excludedInputPaths.delete(data.page.inputPath); + } + + return content; + }); + + // Collect [urlPrefix, icon] pairs from the navigation. Unlike preprocessors, + // collection items expose both the resolved `url` and the navigation front matter. + eleventyConfig.addCollection('searchIcons', collectionApi => { + for (const item of collectionApi.getAll()) { + const icon = item.data?.eleventyNavigation?.icon; + + if (icon && typeof item.url === 'string') { + iconsByPrefix.push([item.url === '/' ? '/' : item.url.replace(/\/$/, ''), `far-${icon}`]); + } + } + + return []; + }); + + /** Resolves the icon for a page URL by finding the longest matching prefix. */ + function resolveIcon(url) { + for (const [prefix, icon] of iconsByPrefix) { + if (url === prefix || url.startsWith(`${prefix}/`)) { + return icon; + } + } + + return options.defaultIcon; + } + + // Runs after the content transforms (e.g. anchor headings), so the parsed + // DOM matches what is served to the browser. + eleventyConfig.addTransform('search', function (content) { + const outputPath = this.page.outputPath; + + if (typeof outputPath !== 'string' || !outputPath.endsWith('.html') || excludedInputPaths.has(this.page.inputPath)) { + return content; + } + + const doc = parse(content, { + blockTextElements: { script: false, noscript: false, style: false, pre: false, code: false } + }); + + // Drop content that shouldn't be searchable to keep the index small. + options.selectorsToIgnore.forEach(selector => { + doc.querySelectorAll(selector).forEach(el => el.remove()); + }); + + const rawUrl = this.page.url === '/' ? '/' : this.page.url.replace(/\/$/, ''); + + pagesToIndex.set(rawUrl, { + title: collapseWhitespace(options.getTitle(doc)), + description: collapseWhitespace(options.getDescription(doc)), + headings: options.getHeadings(doc).map(collapseWhitespace).join(' '), + content: collapseWhitespace(options.getContent(doc)), + url: rawUrl + }); + + return content; + }); + + eleventyConfig.on('eleventy.after', async ({ directories }) => { + const outputFilename = join(directories.output, 'search.json'); + + // Longest prefixes first so the most specific category icon wins. + iconsByPrefix.sort((a, b) => b[0].length - a[0].length); + + // Short field names keep the serialized index small. + const searchIndex = new MiniSearch({ fields: ['t', 'h', 'c'], storeFields: [] }); + const map = []; + + let id = 0; + for (const [, page] of pagesToIndex) { + searchIndex.add({ id, t: page.title, h: page.headings, c: page.content }); + map[id] = { title: page.title, description: page.description, url: page.url, icon: resolveIcon(page.url) }; + id++; + } + + await mkdir(dirname(outputFilename), { recursive: true }); + await writeFile(outputFilename, JSON.stringify({ searchIndex: searchIndex.toJSON(), map }), 'utf-8'); + }); + }; +} diff --git a/yarn.lock b/yarn.lock index 9dbcc0b881..e4795b4eb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6220,6 +6220,7 @@ __metadata: "@types/prismjs": "npm:^1.26.6" "@typescript/native-preview": "npm:^7.0.0-dev.20260413.1" lit: "npm:^3.3.2" + minisearch: "npm:^7.2.0" prismjs: "npm:^1.30.0" sass-embedded: "npm:^1.99.0" storybook: "npm:^10.3.5" @@ -7091,6 +7092,7 @@ __metadata: markdown-it-attrs: "npm:^4.3.1" markdown-it-container: "npm:^4.0.0" markdown-it-deflist: "npm:^3.0.0" + minisearch: "npm:^7.2.0" node-html-parser: "npm:^7.1.0" prismjs: "npm:^1.30.0" slugify: "npm:^1.6.9" @@ -18074,6 +18076,13 @@ __metadata: languageName: node linkType: hard +"minisearch@npm:^7.2.0": + version: 7.2.0 + resolution: "minisearch@npm:7.2.0" + checksum: 10c0/64efaf30ead2acb19eb8be49c78352c527812dd2927a0981c9d666339d5d206d132b83038d2bfa756f5161cae5d98f15b07cc7e7b7be6b8278d35d2ecdc2628c + languageName: node + linkType: hard + "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" From 1a3fb5e41de67bf5cf80a0c17bdde8392ff7d15f Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Sun, 14 Jun 2026 16:07:22 +0200 Subject: [PATCH 64/72] =?UTF-8?q?=E2=9B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/content/components/actions/actions.md | 23 +++- .../src/content/components/actions/switch.md | 60 ++++++++++ .../content/components/actions/switch.md.bak | 106 ------------------ .../content/components/form/checkbox-group.md | 74 ++++++++++++ .../src/content/components/form/checkbox.md | 65 +++++++++++ .../src/content/components/form/combobox.md | 89 +++++++++++++++ .../src/content/components/form/date-field.md | 68 ++++++++++- .../src/content/components/form/editor.md | 59 ++++++++++ .../src/content/components/form/form-field.md | 68 +++++++++++ .../src/content/components/form/form.md | 78 ++++++++++++- .../content/components/form/number-field.md | 62 ++++++++++ .../content/components/form/radio-group.md | 74 ++++++++++++ .../content/components/form/search-field.md | 64 +++++++++++ .../src/content/components/form/select.md | 90 +++++++++++++++ .../src/content/components/form/text-area.md | 68 +++++++++++ .../src/content/components/form/text-field.md | 70 +++++++++++- .../src/content/components/form/time-field.md | 66 ++++++++++- .../src/content/components/grid/basics.md | 69 ++++++++++++ .../content/components/grid/drag-and-drop.md | 33 ++++++ .../src/content/components/grid/editing.md | 28 +++++ .../src/content/components/grid/filtering.md | 40 +++++++ .../src/content/components/grid/grid.md | 61 +++++++++- .../src/content/components/grid/grouping.md | 35 ++++++ .../src/content/components/grid/pagination.md | 42 +++++++ .../src/content/components/grid/scrolling.md | 37 ++++++ .../src/content/components/grid/selection.md | 51 +++++++++ .../src/content/components/grid/sorting.md | 40 +++++++ .../src/content/components/grid/styling.md | 46 ++++++++ .../content/components/layout/accordion.md | 72 ++++++++++++ .../src/content/components/layout/callout.md | 68 +++++++++++ .../src/content/components/layout/card.md | 85 ++++++++++++++ .../src/content/components/layout/layout.md | 18 ++- .../src/content/components/layout/panel.md | 68 +++++++++++ .../components/navigation/breadcrumbs.md | 92 +++++++++++++++ .../components/navigation/navigation.md | 18 ++- .../components/navigation/paginator.md | 82 ++++++++++++++ .../src/content/components/navigation/tabs.md | 100 +++++++++++++++++ .../src/content/components/navigation/tree.md | 85 ++++++++++++++ .../src/content/components/overlay/dialog.md | 84 ++++++++++++++ .../src/content/components/overlay/drawer.md | 63 +++++++++++ .../components/overlay/message-dialog.md | 64 +++++++++++ .../src/content/components/overlay/overlay.md | 24 +++- .../src/content/components/overlay/popover.md | 62 ++++++++++ .../src/content/components/overlay/tooltip.md | 67 +++++++++++ .../src/content/components/status/badge.md | 72 ++++++++++++ .../components/status/inline-message.md | 74 ++++++++++++ .../content/components/status/progress-bar.md | 61 ++++++++++ .../src/content/components/status/skeleton.md | 61 ++++++++++ .../src/content/components/status/spinner.md | 53 +++++++++ .../src/content/components/status/status.md | 27 ++++- .../src/content/components/status/tag-list.md | 89 +++++++++++++++ .../src/content/components/status/tag.md | 61 ++++++++++ .../content/components/utilities/announcer.md | 55 +++++++++ .../components/utilities/ellipsize-text.md | 43 +++++++ .../components/utilities/emoji-browser.md | 52 +++++++++ .../components/utilities/format-date.md | 68 +++++++++++ .../components/utilities/format-number.md | 81 +++++++++++++ .../content/components/utilities/infotip.md | 76 +++++++++++++ .../content/components/utilities/listbox.md | 99 ++++++++++++++++ .../content/components/utilities/scrollbar.md | 60 ++++++++++ .../content/components/utilities/utilities.md | 39 ++++++- .../components/utilities/virtual-list.md | 85 ++++++++++++++ docs/website/src/js/main.js | 42 +++++++ 63 files changed, 3796 insertions(+), 120 deletions(-) create mode 100644 docs/website/src/content/components/actions/switch.md delete mode 100644 docs/website/src/content/components/actions/switch.md.bak create mode 100644 docs/website/src/content/components/form/checkbox-group.md create mode 100644 docs/website/src/content/components/form/checkbox.md create mode 100644 docs/website/src/content/components/form/combobox.md create mode 100644 docs/website/src/content/components/form/editor.md create mode 100644 docs/website/src/content/components/form/form-field.md create mode 100644 docs/website/src/content/components/form/number-field.md create mode 100644 docs/website/src/content/components/form/radio-group.md create mode 100644 docs/website/src/content/components/form/search-field.md create mode 100644 docs/website/src/content/components/form/select.md create mode 100644 docs/website/src/content/components/form/text-area.md create mode 100644 docs/website/src/content/components/grid/basics.md create mode 100644 docs/website/src/content/components/grid/drag-and-drop.md create mode 100644 docs/website/src/content/components/grid/editing.md create mode 100644 docs/website/src/content/components/grid/filtering.md create mode 100644 docs/website/src/content/components/grid/grouping.md create mode 100644 docs/website/src/content/components/grid/pagination.md create mode 100644 docs/website/src/content/components/grid/scrolling.md create mode 100644 docs/website/src/content/components/grid/selection.md create mode 100644 docs/website/src/content/components/grid/sorting.md create mode 100644 docs/website/src/content/components/grid/styling.md create mode 100644 docs/website/src/content/components/layout/accordion.md create mode 100644 docs/website/src/content/components/layout/callout.md create mode 100644 docs/website/src/content/components/layout/card.md create mode 100644 docs/website/src/content/components/layout/panel.md create mode 100644 docs/website/src/content/components/navigation/breadcrumbs.md create mode 100644 docs/website/src/content/components/navigation/paginator.md create mode 100644 docs/website/src/content/components/navigation/tabs.md create mode 100644 docs/website/src/content/components/navigation/tree.md create mode 100644 docs/website/src/content/components/overlay/dialog.md create mode 100644 docs/website/src/content/components/overlay/drawer.md create mode 100644 docs/website/src/content/components/overlay/message-dialog.md create mode 100644 docs/website/src/content/components/overlay/popover.md create mode 100644 docs/website/src/content/components/overlay/tooltip.md create mode 100644 docs/website/src/content/components/status/badge.md create mode 100644 docs/website/src/content/components/status/inline-message.md create mode 100644 docs/website/src/content/components/status/progress-bar.md create mode 100644 docs/website/src/content/components/status/skeleton.md create mode 100644 docs/website/src/content/components/status/spinner.md create mode 100644 docs/website/src/content/components/status/tag-list.md create mode 100644 docs/website/src/content/components/status/tag.md create mode 100644 docs/website/src/content/components/utilities/announcer.md create mode 100644 docs/website/src/content/components/utilities/ellipsize-text.md create mode 100644 docs/website/src/content/components/utilities/emoji-browser.md create mode 100644 docs/website/src/content/components/utilities/format-date.md create mode 100644 docs/website/src/content/components/utilities/format-number.md create mode 100644 docs/website/src/content/components/utilities/infotip.md create mode 100644 docs/website/src/content/components/utilities/listbox.md create mode 100644 docs/website/src/content/components/utilities/scrollbar.md create mode 100644 docs/website/src/content/components/utilities/virtual-list.md diff --git a/docs/website/src/content/components/actions/actions.md b/docs/website/src/content/components/actions/actions.md index 150d20c1cd..db79cfde0c 100644 --- a/docs/website/src/content/components/actions/actions.md +++ b/docs/website/src/content/components/actions/actions.md @@ -8,4 +8,25 @@ eleventyNavigation: icon: bolt --- -Action components allow users to trigger operations or navigate. +Action components let users trigger operations, make choices or navigate. They are the primary way +users interact with an application. + +## Overview + +[Button](/components/actions/button) +: Triggers an action or navigates. + +[Button bar](/components/actions/button-bar) +: Groups related buttons together in a bar. + +[Menu button](/components/actions/menu-button) +: A button that opens a menu of actions. + +[Toggle button](/components/actions/toggle-button) +: A button that switches between a pressed and unpressed state. + +[Toggle group](/components/actions/toggle-group) +: A set of toggle buttons where one or more can be active. + +[Switch](/components/actions/switch) +: A single on/off toggle. diff --git a/docs/website/src/content/components/actions/switch.md b/docs/website/src/content/components/actions/switch.md new file mode 100644 index 0000000000..137d264a92 --- /dev/null +++ b/docs/website/src/content/components/actions/switch.md @@ -0,0 +1,60 @@ +--- +title: Switch +layout: docs +eleventyNavigation: + key: Switch + parent: Actions +--- + +`` is a single on/off toggle. Use it for settings that take effect immediately, such as +enabling a feature. When the choice is part of a form that is submitted later, a +[checkbox](/components/form/checkbox) is usually more appropriate. + +The label goes in the default slot. + +## Usage + +```html +Enable notifications +``` + +## Examples + +### Basic + +```html {.example .show-source} +Toggle me +``` + +### Checked + +```html {.example .show-source} +On by default +``` + +### Reverse + +Add `reverse` to place the label before the toggle (for example to align it with other controls). + +```html {.example .show-source} +Label on the left +``` + +### Disabled + +```html {.example .show-source .vertical} +Off +On +``` + +### Custom icons + +Use the `icon-on` and `icon-off` attributes to show an icon inside the toggle for each state. + +```html {.example .show-source} + +``` + +## API + +See the [API reference](/api-reference/sl-switch) for all attributes and properties. diff --git a/docs/website/src/content/components/actions/switch.md.bak b/docs/website/src/content/components/actions/switch.md.bak deleted file mode 100644 index e6856e80e3..0000000000 --- a/docs/website/src/content/components/actions/switch.md.bak +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Switch -layout: component -issueNumber: 2315 -storybook: form-switch--basic -eleventyNavigation: - key: Switch - parent: Actions ---- - -```html {.example .show-source} -Dark mode -``` - -## Usage - -Use a switch when users need to toggle a binary setting on or off with immediate effect — no form submission required. Common examples include enabling notifications, activating dark mode, or toggling a feature preference. - -::: info -A switch should always take effect immediately. If the action requires confirmation or a form submit, use a checkbox instead. -::: - -Avoid using switches for contextual UI controls like opening drawers, menus, or panels. In those cases, use a [toggle button](/components/actions/toggle-button/) instead. - -## Examples - -### Checked - -Use the `checked` attribute to set the initial on state, or bind to it to control the state programmatically. - -```html {.example .horizontal} -Unchecked -Checked -``` - -### Size - -Switches are available in three sizes: - -- `sm` — Small, for compact UIs -- `md` — Medium, the default -- `lg` — Large, for prominent settings - -```html {.example .horizontal} -Small -Medium -Large -``` - -### Reverse - -Use the `reverse` attribute to place the toggle after the label instead of before it. - -```html {.example .horizontal} -Label before -Label after -``` - -### Custom icons - -Use the `icon-on` and `icon-off` attributes to replace the default check/cross icons with custom ones. This is useful when the icons themselves communicate the meaning of the on/off states. - -::: info -Custom icons are not rendered at the `sm` size. The handle is too small to display them legibly. -::: - -```html {.example .horizontal} -Theme -Theme -Theme -``` - -### Disabled - -Use the `disabled` attribute to prevent interaction with the switch. - -```html {.example .horizontal} -Unchecked -Checked -``` - -### Without label - -When there is no visible label, provide an accessible name using `aria-label`. - -```html {.example} - -``` - -## Accessibility - -The switch uses a native `` in the light DOM, following the [ARIA switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/examples/switch-checkbox/). This ensures proper keyboard interaction and screen reader announcements out of the box. - -When a text label is provided as slot content, it is automatically associated with the input via `aria-labelledby`. - -Keyboard interaction: - -- `Space` or `Enter` — toggles the switch on or off - -### ARIA - -| Attribute | Description | -| ---------------- | --------------------------------------------------------------------------------------- | -| `aria-label` | Required when there is no visible label; provides an accessible name to screen readers. | -| `aria-labelledby`| Set automatically when slot content is present; associates the label with the input. | -| `aria-checked` | Managed automatically; reflects the current on/off state. | diff --git a/docs/website/src/content/components/form/checkbox-group.md b/docs/website/src/content/components/form/checkbox-group.md new file mode 100644 index 0000000000..9ac7e9b92a --- /dev/null +++ b/docs/website/src/content/components/form/checkbox-group.md @@ -0,0 +1,74 @@ +--- +title: Checkbox group +layout: docs +eleventyNavigation: + key: Checkbox group + parent: Form +--- + +`` groups several [checkboxes](/components/form/checkbox) so they are labelled and +validated together, and so their values are collected into a single array. Use it when the user can +select more than one option from a set. + +Place `` elements in the default slot, each with a `value`. + +## Usage + +```html + + + Cheese + Mushroom + Pepperoni + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + + Option 1 + Option 2 + Option 3 + Option 4 + +``` + +### Preselected values + +Set the group's `value` to an array of the values that should be checked. + +```html {.example .show-source} + + Option 1 + Option 2 + Option 3 + + + +``` + +### Required + +Inside an ``, a required group needs at least one checked option. + +```html {.example .show-source} + + + + Option 1 + Option 2 + Option 3 + + + +``` + +## API + +See the [API reference](/api-reference/sl-checkbox-group) for all attributes and properties. diff --git a/docs/website/src/content/components/form/checkbox.md b/docs/website/src/content/components/form/checkbox.md new file mode 100644 index 0000000000..68d1f5b534 --- /dev/null +++ b/docs/website/src/content/components/form/checkbox.md @@ -0,0 +1,65 @@ +--- +title: Checkbox +layout: docs +eleventyNavigation: + key: Checkbox + parent: Form +--- + +`` is a single on/off choice, for example to accept terms or toggle an option. To offer +several related choices where more than one can be selected, use a +[checkbox group](/components/form/checkbox-group). + +The label goes in the default slot. + +## Usage + +```html +I agree to the terms +``` + +## Examples + +### Basic + +```html {.example .show-source} +Toggle me +``` + +### Checked + +```html {.example .show-source} +Checked +``` + +### Indeterminate + +The `indeterminate` state represents a "partially selected" checkbox, often used for a parent that +controls a group of children. + +```html {.example .show-source} +Indeterminate +``` + +### Disabled + +```html {.example .show-source .vertical} +Unchecked +Checked +``` + +### Required + +Inside an ``, a required checkbox must be checked for the form to be valid. + +```html {.example .show-source} + + + I accept the terms and conditions + + +``` + +## API + +See the [API reference](/api-reference/sl-checkbox) for all attributes and properties. diff --git a/docs/website/src/content/components/form/combobox.md b/docs/website/src/content/components/form/combobox.md new file mode 100644 index 0000000000..c3f274aff8 --- /dev/null +++ b/docs/website/src/content/components/form/combobox.md @@ -0,0 +1,89 @@ +--- +title: Combobox +layout: docs +eleventyNavigation: + key: Combobox + parent: Form +--- + +`` combines a text input with a filterable list of options. As the user types, the list +narrows to matching options. It supports selecting a single value or multiple values, and can +optionally accept custom values that aren't in the list. + +Use a combobox when there are many options to choose from; for a short, fixed set use a +[select](/components/form/select). Provide the options as `` elements inside an +``. + +## Usage + +```html + + + + Netherlands + Belgium + Germany + + + +``` + +## Examples + +### Basic + +Start typing to filter the options. + +```html {.example .show-source} + + + Option 1 + Option 2 + Option 3 + + +``` + +### Selected value + +```html {.example .show-source} + + + Option 1 + Option 2 + Option 3 + + +``` + +### Multiple selection + +Add the `multiple` attribute to let the user select more than one option. Selected options appear as +removable [tags](/components/status/tag). + +```html {.example .show-source} + + + Option 1 + Option 2 + Option 3 + + +``` + +### Custom values + +Add `allow-custom-values` to let the user enter values that aren't in the list. + +```html {.example .show-source} + + + Option 1 + Option 2 + + +``` + +## API + +See the [API reference](/api-reference/sl-combobox) for all attributes and properties. diff --git a/docs/website/src/content/components/form/date-field.md b/docs/website/src/content/components/form/date-field.md index 1ee99598b0..b1df6c0abf 100644 --- a/docs/website/src/content/components/form/date-field.md +++ b/docs/website/src/content/components/form/date-field.md @@ -1,9 +1,73 @@ --- title: Date field -layout: component +layout: docs eleventyNavigation: key: Date field parent: Form --- -Date field components allow users to input and edit dates. +`` is a text input with an attached calendar for selecting a date. Users can either +type a date or pick one from the calendar. For selecting a time, use a +[time field](/components/form/time-field). + +Wrap it in an [``](/components/form/form-field) to add a label, hint and validation +messages. + +## Usage + +```html + + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + +``` + +### Value + +Set the `value` attribute to an ISO date string to preselect a date. + +```html {.example .show-source} + +``` + +### Min and max + +Restrict the selectable range with the `min` and `max` attributes. + +```html {.example .show-source} + +``` + +### Select only + +Add `select-only` to prevent typing, so the date can only be chosen from the calendar. + +```html {.example .show-source} + +``` + +### Week numbers + +Add `show-week-numbers` to display week numbers in the calendar. + +```html {.example .show-source} + +``` + +### Disabled and readonly + +```html {.example .show-source .vertical} + + +``` + +## API + +See the [API reference](/api-reference/sl-date-field) for all attributes and properties. diff --git a/docs/website/src/content/components/form/editor.md b/docs/website/src/content/components/form/editor.md new file mode 100644 index 0000000000..1cf76429fb --- /dev/null +++ b/docs/website/src/content/components/form/editor.md @@ -0,0 +1,59 @@ +--- +title: Editor +layout: docs +eleventyNavigation: + key: Editor + parent: Form +--- + +`` is a rich text editor for formatted content. It provides a toolbar for common +formatting — bold, italic, lists and more — and integrates with `` like any other form +control. + +Wrap it in an [``](/components/form/form-field) to add a label, hint and validation +messages. + +## Usage + +```html + + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + +``` + +### Disabled + +```html {.example .show-source} + +``` + +## Setting and reading content + +The editor's content is available through its `value` property. Set it to pre-fill the editor, and +read it to get the current content. + +```html + + + +``` + +## API + +See the [API reference](/api-reference/sl-editor) for all attributes and properties. diff --git a/docs/website/src/content/components/form/form-field.md b/docs/website/src/content/components/form/form-field.md new file mode 100644 index 0000000000..e21ae8810f --- /dev/null +++ b/docs/website/src/content/components/form/form-field.md @@ -0,0 +1,68 @@ +--- +title: Form field +layout: docs +eleventyNavigation: + key: Form field + parent: Form +--- + +`` wraps a form control and provides the surrounding structure: a label, an optional +hint, and the validation messages. It works with any of the form controls and keeps their labelling +and error handling consistent. + +In most cases you only need the `label` and `hint` attributes; the form field generates the +``, `` and error elements for you. + +## Usage + +```html + + + +``` + +## Examples + +### Label and hint + +The `label` attribute renders the field's label, and `hint` renders helper text below the control. + +```html {.example .show-source} + + + +``` + +### Validation messages + +When a control inside the form field is invalid, the field shows the error message. Wrap the field +in an `` and call `reportValidity()` to trigger validation. + +```html {.example .show-source} + + + + + +``` + +### Custom label, hint and error + +For full control, slot your own ``, `` and `` instead of using the +attributes. This lets you add rich content such as an [infotip](/components/utilities/infotip) in the +label. + +```html {.example .show-source} + + + Username + A unique identifier used for account login. + + + Use letters and numbers only. + +``` + +## API + +See the [API reference](/api-reference/sl-form-field) for all attributes and properties. diff --git a/docs/website/src/content/components/form/form.md b/docs/website/src/content/components/form/form.md index 28970ba1ab..711a382f78 100644 --- a/docs/website/src/content/components/form/form.md +++ b/docs/website/src/content/components/form/form.md @@ -8,4 +8,80 @@ eleventyNavigation: icon: input-pipe --- -Form components allow users to input and submit data. +Form components let users enter and submit data. They share a common foundation: every control +integrates with `` for value collection and validation, and is typically wrapped in an +`` that provides the label, hint and error messages. + +## Overview + +[Form field](/components/form/form-field) +: Wraps a control with a label, hint and validation messages. + +[Text field](/components/form/text-field) +: A single-line text input. + +[Text area](/components/form/text-area) +: A multi-line text input. + +[Number field](/components/form/number-field) +: A numeric input with optional step buttons and formatting. + +[Search field](/components/form/search-field) +: A text input tailored for search, with a clear button. + +[Select](/components/form/select) +: A dropdown for choosing a single option from a list. + +[Combobox](/components/form/combobox) +: A text input combined with a filterable list of options, with single or multiple selection. + +[Checkbox](/components/form/checkbox) +: A single on/off choice. + +[Checkbox group](/components/form/checkbox-group) +: A group of related checkboxes whose values are collected together. + +[Radio group](/components/form/radio-group) +: A set of mutually exclusive choices. + +[Date field](/components/form/date-field) +: An input with a calendar for selecting a date. + +[Time field](/components/form/time-field) +: An input for selecting a time. + +[Editor](/components/form/editor) +: A rich text editor. + +## The form element + +`` ties the individual controls together. It collects their values into a single object, +tracks overall validity, and emits `sl-update-state` and `sl-update-validity` events as the user +interacts with the form. + +```html {.example .show-source} + + + + + + + + +``` + +Give each control a `name` so its value appears in the form's `value` object. Call +`form.reportValidity()` to validate all controls and show their error messages, and `form.reset()` +to restore their initial values. + +## Disabling a form + +Setting the `disabled` attribute on `` disables all the controls inside it at once. + +```html {.example .show-source} + + + + + +``` diff --git a/docs/website/src/content/components/form/number-field.md b/docs/website/src/content/components/form/number-field.md new file mode 100644 index 0000000000..dcbf4b0a44 --- /dev/null +++ b/docs/website/src/content/components/form/number-field.md @@ -0,0 +1,62 @@ +--- +title: Number field +layout: docs +eleventyNavigation: + key: Number field + parent: Form +--- + +`` is an input for numeric values. It supports minimum and maximum bounds, a step +increment, optional step buttons, and locale-aware formatting for currencies, percentages and units. + +Wrap it in an [``](/components/form/form-field) to add a label, hint and validation +messages. + +## Usage + +```html + + + +``` + +## Examples + +### Min, max and step + +```html {.example .show-source} + +``` + +### Step buttons + +Use the `step-buttons` attribute to show increment/decrement buttons, either at the `end` or on both +`edges`. + +```html {.example .show-source .vertical} + + +``` + +### Formatting + +The `format-options` attribute takes the same options as +[`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat), +so you can format the value as a currency, percentage or unit. + +```html {.example .show-source .vertical} + + + +``` + +### Disabled and readonly + +```html {.example .show-source .vertical} + + +``` + +## API + +See the [API reference](/api-reference/sl-number-field) for all attributes and properties. diff --git a/docs/website/src/content/components/form/radio-group.md b/docs/website/src/content/components/form/radio-group.md new file mode 100644 index 0000000000..362624ad0c --- /dev/null +++ b/docs/website/src/content/components/form/radio-group.md @@ -0,0 +1,74 @@ +--- +title: Radio group +layout: docs +eleventyNavigation: + key: Radio group + parent: Form +--- + +`` presents a set of mutually exclusive choices — the user can pick exactly one. Use +it when all the options benefit from being visible at once; if there are many options or space is +limited, use a [select](/components/form/select) instead. + +Place `` elements in the default slot, each with a `value`. + +## Usage + +```html + + + Small + Medium + Large + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + + One + Two + Three + +``` + +### Selected value + +Set the group's `value` to preselect the matching radio. + +```html {.example .show-source} + + One + Two + Three + +``` + +### Horizontal + +Add the `horizontal` attribute to lay the radios out in a row. + +```html {.example .show-source} + + One + Two + Three + +``` + +### Disabled + +```html {.example .show-source} + + One + Two + +``` + +## API + +See the [API reference](/api-reference/sl-radio-group) for all attributes and properties. diff --git a/docs/website/src/content/components/form/search-field.md b/docs/website/src/content/components/form/search-field.md new file mode 100644 index 0000000000..9ac3a59bf4 --- /dev/null +++ b/docs/website/src/content/components/form/search-field.md @@ -0,0 +1,64 @@ +--- +title: Search field +layout: docs +eleventyNavigation: + key: Search field + parent: Form +--- + +`` is a text input tailored for searching. It shows a magnifying-glass icon and a +clear button, and emits a debounced `sl-search` event as the user types (and immediately on Enter). + +## Usage + +```html + +``` + +Always give the field an accessible name with `aria-label` (or a label via +[``](/components/form/form-field)). + +## Examples + +### Basic + +Type to see the clear button appear; clearing emits an `sl-clear` event. + +```html {.example .show-source} + +``` + +### With a value + +```html {.example .show-source} + +``` + +### Custom icon + +Override the default icon through the `prefix` slot. + +```html {.example .show-source} + + + +``` + +### Disabled + +```html {.example .show-source} + +``` + +## Events + +`sl-search` +: Emitted (debounced) as the user types, and immediately when Enter is pressed. The query string is + in `event.detail`. + +`sl-clear` +: Emitted when the field is cleared. + +## API + +See the [API reference](/api-reference/sl-search-field) for all attributes, properties and events. diff --git a/docs/website/src/content/components/form/select.md b/docs/website/src/content/components/form/select.md new file mode 100644 index 0000000000..2e60caabca --- /dev/null +++ b/docs/website/src/content/components/form/select.md @@ -0,0 +1,90 @@ +--- +title: Select +layout: docs +eleventyNavigation: + key: Select + parent: Form +--- + +`` lets the user choose a single option from a dropdown list. Use it when there are more +options than is practical for a [radio group](/components/form/radio-group), or when space is +limited. For free-text filtering, use a [combobox](/components/form/combobox). + +Provide the choices as `` elements in the default slot, and group them with +``. + +## Usage + +```html + + + Netherlands + Belgium + Germany + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + + Option 1 + Option 2 + Option 3 + +``` + +### Selected value + +Set the `value` attribute to preselect the matching option. + +```html {.example .show-source} + + Option 1 + Option 2 + Option 3 + +``` + +### Groups + +Group related options with ``. + +```html {.example .show-source} + + + Option 1 + Option 2 + + + Option 3 + Option 4 + + +``` + +### Clearable + +Add `clearable` to let the user reset the selection. + +```html {.example .show-source} + + Option 1 + Option 2 + +``` + +### Disabled + +```html {.example .show-source} + + Option 1 + +``` + +## API + +See the [API reference](/api-reference/sl-select) for all attributes and properties. diff --git a/docs/website/src/content/components/form/text-area.md b/docs/website/src/content/components/form/text-area.md new file mode 100644 index 0000000000..d9c5a6025c --- /dev/null +++ b/docs/website/src/content/components/form/text-area.md @@ -0,0 +1,68 @@ +--- +title: Text area +layout: docs +eleventyNavigation: + key: Text area + parent: Form +--- + +`` is a multi-line text input for longer, free-form content such as comments or +descriptions. For a single line of text, use a [text field](/components/form/text-field) instead. + +Wrap it in an [``](/components/form/form-field) to add a label, hint and validation +messages. + +## Usage + +```html + + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + +``` + +### Rows + +Use the `rows` attribute to set the initial visible height. + +```html {.example .show-source} + +``` + +### Resize + +The `resize` attribute controls how the user can resize the field: `vertical` (default), `none`, +`horizontal`, `both` or `auto` (grows with the content). + +```html {.example .show-source .vertical} + + +``` + +### Length limits + +Use `minlength` and `maxlength` to constrain the amount of text. + +```html {.example .show-source} + + + +``` + +### Disabled and readonly + +```html {.example .show-source .vertical} + + +``` + +## API + +See the [API reference](/api-reference/sl-text-area) for all attributes and properties. diff --git a/docs/website/src/content/components/form/text-field.md b/docs/website/src/content/components/form/text-field.md index dc0fa2a25c..e2da507f9c 100644 --- a/docs/website/src/content/components/form/text-field.md +++ b/docs/website/src/content/components/form/text-field.md @@ -1,9 +1,75 @@ --- title: Text field -layout: component +layout: docs eleventyNavigation: key: Text field parent: Form --- -Text field components allow users to input and edit text. +`` is a single-line text input. Use it for short, free-form text such as a name, email +address or URL. For longer text, use a [text area](/components/form/text-area); for numbers, use a +[number field](/components/form/number-field). + +Wrap it in an [``](/components/form/form-field) to add a label, hint and validation +messages. + +## Usage + +```html + + + +``` + +## Examples + +### Types + +Set the `type` attribute to `text` (default), `email`, `tel`, `url`, `password` or `number` to get +the appropriate keyboard and built-in validation. + +```html {.example .show-source} + + + +``` + +### Placeholder + +```html {.example .show-source} + +``` + +### Required + +Add `required` so the field must be filled in. Inside an ``, the error message appears when +validation runs. + +```html {.example .show-source} + + + + + +``` + +### Disabled and readonly + +```html {.example .show-source .vertical} + + +``` + +### Prefix and suffix + +Use the `prefix` and `suffix` slots to add icons or text alongside the input. + +```html {.example .show-source} + + + +``` + +## API + +See the [API reference](/api-reference/sl-text-field) for all attributes and properties. diff --git a/docs/website/src/content/components/form/time-field.md b/docs/website/src/content/components/form/time-field.md index 61b2389a1d..acb9707c82 100644 --- a/docs/website/src/content/components/form/time-field.md +++ b/docs/website/src/content/components/form/time-field.md @@ -1,9 +1,71 @@ --- title: Time field -layout: component +layout: docs eleventyNavigation: key: Time field parent: Form --- -Time field components allow users to input and edit times. +`` is an input for selecting a time. It formats the time according to the locale and +can be constrained to a range and to specific step increments. For selecting a date, use a +[date field](/components/form/date-field). + +Wrap it in an [``](/components/form/form-field) to add a label, hint and validation +messages. + +## Usage + +```html + + + +``` + +## Examples + +### Basic + +```html {.example .show-source} + +``` + +### Value + +```html {.example .show-source} + +``` + +### Min and max + +Restrict the selectable range with the `min` and `max` attributes. + +```html {.example .show-source} + +``` + +### Steps + +Use `hour-step` and `minute-step` to control the increments offered. + +```html {.example .show-source} + +``` + +### Locale + +The `locale` attribute changes how the time is formatted (for example 12- vs 24-hour). + +```html {.example .show-source} + +``` + +### Disabled and readonly + +```html {.example .show-source .vertical} + + +``` + +## API + +See the [API reference](/api-reference/sl-time-field) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/basics.md b/docs/website/src/content/components/grid/basics.md new file mode 100644 index 0000000000..b1199e201e --- /dev/null +++ b/docs/website/src/content/components/grid/basics.md @@ -0,0 +1,69 @@ +--- +title: Basics +layout: docs +eleventyNavigation: + key: Grid basics + title: Basics + parent: Grid +--- + +A grid is made up of an `` element with the data set on its `items` property, and one +`` per column. Each column's `path` points to a property on the row data, and +`header` sets the column heading. + +## Columns + +```html + + + + + +``` + +- **`path`** — the (dot-separated) property to display, e.g. `school.name`. +- **`header`** — the column heading; defaults to a humanised version of the path. +- **`grow`** — how much the column grows relative to others (like `flex-grow`); use `grow="0"` for a + fixed, content-sized column. +- **`ellipsize-text`** — truncates overflowing text with an ellipsis. +- **`align`** — aligns the cell content (`start`, `center` or `end`). + +## Custom cell rendering + +For anything beyond plain text, give a column a `renderer` function. Register any custom elements it +uses through `scopedElements`. + +```html + + + +``` + +## Column groups + +Wrap columns in an `` to show a shared header above them. + +```html + + + + + + + +``` + +## API + +See the API reference for [`sl-grid`](/api-reference/sl-grid) and +[`sl-grid-column`](/api-reference/sl-grid-column). diff --git a/docs/website/src/content/components/grid/drag-and-drop.md b/docs/website/src/content/components/grid/drag-and-drop.md new file mode 100644 index 0000000000..7f944b0e9d --- /dev/null +++ b/docs/website/src/content/components/grid/drag-and-drop.md @@ -0,0 +1,33 @@ +--- +title: Drag and drop +layout: docs +eleventyNavigation: + key: Grid drag and drop + title: Drag and drop + parent: Grid +--- + +The grid lets users reorder rows by dragging. The easiest way to enable this is to add an +``, which shows a drag handle and automatically sets the grid's +`draggable-rows` property to `between` (so rows can be dropped between other rows). + +## Reordering rows + +```html + + + + + +``` + +## Controlling drag behaviour + +For finer control you can set the `draggable-rows` property yourself (`between`, `on-top` or +`on-grid`), and use the `dropFilter` property to decide which rows may be dropped where. The grid +dispatches a drop event you can use to update the underlying data. + +## API + +See the API reference for [`sl-grid`](/api-reference/sl-grid) and +[`sl-grid-drag-handle-column`](/api-reference/sl-grid-drag-handle-column). diff --git a/docs/website/src/content/components/grid/editing.md b/docs/website/src/content/components/grid/editing.md new file mode 100644 index 0000000000..5a1526f17b --- /dev/null +++ b/docs/website/src/content/components/grid/editing.md @@ -0,0 +1,28 @@ +--- +title: Editing +layout: docs +eleventyNavigation: + key: Grid editing + title: Editing + parent: Grid +--- + +The grid supports editing cell values inline. Use a column type that renders an editable control, +such as ``, which shows a text field in each cell of that column. + +## Editable columns + +```html + + + + + +``` + +Editing a cell updates the corresponding property on the row's data object, so your underlying data +stays in sync. + +## API + +See the [API reference](/api-reference/sl-grid-text-field-column) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/filtering.md b/docs/website/src/content/components/grid/filtering.md new file mode 100644 index 0000000000..5c1d57fa78 --- /dev/null +++ b/docs/website/src/content/components/grid/filtering.md @@ -0,0 +1,40 @@ +--- +title: Filtering +layout: docs +eleventyNavigation: + key: Grid filtering + title: Filtering + parent: Grid +--- + +Use `` to let users narrow down the rows. The column adds a filter control to +its header; the `mode` attribute controls what kind of control it is. + +## Filter columns + +```html + + + + + +``` + +## Modes + +The `mode` attribute selects the filter control: + +- **`text`** — a text field that matches on the column's value (the default). +- **`select`** — a list of the distinct values to choose from. Use `label-path` when you filter on + an id but want to show a human-readable label. +- **`date`** / **`date-range`** — filter by a single date or a date range. + +You can pre-apply a filter by setting the column's `value` attribute. + +```html + +``` + +## API + +See the [API reference](/api-reference/sl-grid-filter-column) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/grid.md b/docs/website/src/content/components/grid/grid.md index 8f43317f48..b69559b069 100644 --- a/docs/website/src/content/components/grid/grid.md +++ b/docs/website/src/content/components/grid/grid.md @@ -8,4 +8,63 @@ eleventyNavigation: icon: table-cells --- -Grid components allow users to create powerful data tables. +`` is a powerful data table for displaying and interacting with collections of data. It +supports sorting, filtering, grouping, selection, inline editing, pagination, drag-and-drop +reordering and custom styling. + +Because the grid is data-driven, you provide the data through the `items` property (or a +`dataSource` for advanced scenarios) and describe the columns with `` elements. The +examples throughout this section therefore configure the grid from JavaScript. + +```html + + + +``` + +```html + + + + +``` + +## Topics + +This section is organised by feature, mirroring the grid stories in Storybook: + +[Basics](/components/grid/basics) +: Columns, headers, custom renderers and column groups. + +[Sorting](/components/grid/sorting) +: Let users sort the data by column. + +[Filtering](/components/grid/filtering) +: Let users narrow down the rows shown. + +[Grouping](/components/grid/grouping) +: Group rows under collapsible headers. + +[Selection](/components/grid/selection) +: Select one or multiple rows. + +[Editing](/components/grid/editing) +: Edit cell values inline. + +[Pagination](/components/grid/pagination) +: Split the rows across pages. + +[Scrolling](/components/grid/scrolling) +: Make the grid body scroll within a fixed height. + +[Drag and drop](/components/grid/drag-and-drop) +: Reorder rows by dragging. + +[Styling](/components/grid/styling) +: Borders, stripes and custom cell parts. diff --git a/docs/website/src/content/components/grid/grouping.md b/docs/website/src/content/components/grid/grouping.md new file mode 100644 index 0000000000..3b4cb9986b --- /dev/null +++ b/docs/website/src/content/components/grid/grouping.md @@ -0,0 +1,35 @@ +--- +title: Grouping +layout: docs +eleventyNavigation: + key: Grid grouping + title: Grouping + parent: Grid +--- + +Grouping organises rows under collapsible group headers. To enable it, provide the grid with an +`ArrayListDataSource` configured with a `groupBy` path instead of setting `items` directly. + +## Grouping rows + +```html + + + + + + + +``` + +The grid renders a header for each group, which users can collapse and expand. For full control over +how a group header looks, set the grid's `groupHeaderRenderer` property. + +## API + +See the [API reference](/api-reference/sl-grid) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/pagination.md b/docs/website/src/content/components/grid/pagination.md new file mode 100644 index 0000000000..b612e838eb --- /dev/null +++ b/docs/website/src/content/components/grid/pagination.md @@ -0,0 +1,42 @@ +--- +title: Pagination +layout: docs +eleventyNavigation: + key: Grid pagination + title: Pagination + parent: Grid +--- + +For large data sets you can split the rows across pages and pair the grid with a +[paginator](/components/navigation/paginator). Drive both from the same `ArrayListDataSource`: the +data source knows the page size and current page, and the grid renders the current page. + +## Paginated grid + +```html + + + + +``` + +The data source paginates the rows, so the grid only renders the items for the current page. + +## API + +See the [API reference](/api-reference/sl-grid) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/scrolling.md b/docs/website/src/content/components/grid/scrolling.md new file mode 100644 index 0000000000..07202cdbb0 --- /dev/null +++ b/docs/website/src/content/components/grid/scrolling.md @@ -0,0 +1,37 @@ +--- +title: Scrolling +layout: docs +eleventyNavigation: + key: Grid scrolling + title: Scrolling + parent: Grid +--- + +By default the grid grows to fit all of its rows. To keep the header visible and let the body scroll +within a fixed area, constrain the grid's height with CSS. The column headers stay sticky at the top +while the rows scroll. + +## Scrollable grid + +```html + + + + + +``` + +The grid is also virtualized: it only renders the rows that are currently visible, so it stays +performant even with very large data sets. + +## Sticky columns + +Add the `sticky` attribute to a column to keep it pinned while the grid scrolls horizontally. + +```html + +``` + +## API + +See the [API reference](/api-reference/sl-grid) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/selection.md b/docs/website/src/content/components/grid/selection.md new file mode 100644 index 0000000000..b5d7e17f22 --- /dev/null +++ b/docs/website/src/content/components/grid/selection.md @@ -0,0 +1,51 @@ +--- +title: Selection +layout: docs +eleventyNavigation: + key: Grid selection + title: Selection + parent: Grid +--- + +The grid supports selecting one or multiple rows. Add an `` to show +checkboxes, or use the `selects` attribute to enable selection without a dedicated column. + +## Multiple selection + +Adding a selection column automatically enables multiple selection. Use `select-all` to include a +"select all" checkbox in the header. + +```html + + + + + +``` + +## Single selection + +For single selection, use ``, which renders radio buttons. + +```html + + + + +``` + +## Reacting to selection + +Read the current selection from the grid's selection model, and listen for changes. The grid also +dispatches `sl-grid-active-row-change` when the active row changes. + +```js +grid.addEventListener('sl-grid-active-row-change', event => { + console.log('Active row:', event.detail); +}); +``` + +## API + +See the API reference for [`sl-grid-selection-column`](/api-reference/sl-grid-selection-column) and +[`sl-grid-select-column`](/api-reference/sl-grid-select-column). diff --git a/docs/website/src/content/components/grid/sorting.md b/docs/website/src/content/components/grid/sorting.md new file mode 100644 index 0000000000..5ea9020092 --- /dev/null +++ b/docs/website/src/content/components/grid/sorting.md @@ -0,0 +1,40 @@ +--- +title: Sorting +layout: docs +eleventyNavigation: + key: Grid sorting + title: Sorting + parent: Grid +--- + +To let users sort the data, use `` instead of ``. This adds a +clickable, sortable header that cycles through ascending, descending and unsorted. Not every column +has to be sortable — mix sort columns with regular ones as needed. + +## Sortable columns + +```html + + + + + +``` + +When you set the data with the `items` property, the grid sorts it for you. For server-side or +otherwise managed data, provide a `dataSource` instead and the grid will ask it to sort. + +```html + + + +``` + +## API + +See the [API reference](/api-reference/sl-grid-sort-column) for all attributes and properties. diff --git a/docs/website/src/content/components/grid/styling.md b/docs/website/src/content/components/grid/styling.md new file mode 100644 index 0000000000..7577621eac --- /dev/null +++ b/docs/website/src/content/components/grid/styling.md @@ -0,0 +1,46 @@ +--- +title: Styling +layout: docs +eleventyNavigation: + key: Grid styling + title: Styling + parent: Grid +--- + +The grid offers several attributes and CSS parts to adjust its appearance. + +## Borders and stripes + +- **`no-border`** — removes the outer border around the grid. +- **`no-row-border`** — removes the borders between rows. +- **`striped`** — gives rows alternating background colors for easier scanning. + +```html + + + +``` + +## Custom cell parts + +Give a column a `parts` value to add CSS parts to its cells, then target them with `::part()`. A +`parts` function lets you apply parts conditionally based on the row data. + +```html + + + + + +``` + +You can also style whole rows by setting the grid's `itemParts` property to a function that returns +part names per row. + +## API + +See the [API reference](/api-reference/sl-grid) for all attributes and properties. diff --git a/docs/website/src/content/components/layout/accordion.md b/docs/website/src/content/components/layout/accordion.md new file mode 100644 index 0000000000..c9831b5a38 --- /dev/null +++ b/docs/website/src/content/components/layout/accordion.md @@ -0,0 +1,72 @@ +--- +title: Accordion +layout: docs +eleventyNavigation: + key: Accordion + parent: Layout +--- + +`` is a stack of collapsible sections. Use it to organise related content into +sections that the user can expand and collapse, keeping long pages manageable. + +Each section is an `` with a `summary` (the clickable header) and its content in +the default slot. + +## Usage + +```html + + Content of the first section. + Content of the second section. + +``` + +## Examples + +### Basic + +Add the `open` attribute to an item to have it expanded initially. + +```html {.example .show-source} + + + A design system is a collection of reusable components and guidelines that help teams build + consistent products. + + + It improves consistency, speeds up development and makes products easier to maintain. + + + Install the packages, import a theme, and start using the components. + + +``` + +### Single + +Add the `single` attribute so that opening one section automatically closes the others. + +```html {.example .show-source} + + Only one section can be open at a time. + Opening this one closes the others. + And so on. + +``` + +### Icon type + +Use the `icon-type` attribute to switch the expand/collapse indicator between `plusminus` (default) +and `chevron`. + +```html {.example .show-source} + + Uses a chevron indicator. + Uses a chevron indicator. + +``` + +## API + +See the API reference for [`sl-accordion`](/api-reference/sl-accordion) and +[`sl-accordion-item`](/api-reference/sl-accordion-item). diff --git a/docs/website/src/content/components/layout/callout.md b/docs/website/src/content/components/layout/callout.md new file mode 100644 index 0000000000..f4d0b6fcb5 --- /dev/null +++ b/docs/website/src/content/components/layout/callout.md @@ -0,0 +1,68 @@ +--- +title: Callout +layout: docs +eleventyNavigation: + key: Callout + parent: Layout +--- + +`` is a prominent, in-page message that draws attention to important information. Unlike +an [inline message](/components/status/inline-message), a callout is not dismissible and is meant for +information that should stay visible, such as a tip or an important note within content. + +Put the main content in the default slot and an optional heading in the `title` slot. + +## Usage + +```html + +

Good to know

+ The main content of the callout. +
+``` + +## Examples + +### Variants + +The `variant` attribute sets the tone and icon: `info` (default), `success`, `warning` or `danger`. + +```html {.example .show-source .vertical} +An informational callout. +Everything is working as expected. +Be careful with this setting. +This action has serious consequences. +``` + +### With a title + +```html {.example .show-source} + +

Good to know

+ You can combine a title with body content for more detailed callouts. +
+``` + +### Density + +Use the `density` attribute to add more breathing room. It accepts `default` and `relaxed`. + +```html {.example .show-source .vertical} +A default-density callout. +A relaxed callout with more spacing. +``` + +### Custom icon + +Use the `icon` slot to override the default variant icon. + +```html {.example .show-source} + + + This callout uses a custom icon. + +``` + +## API + +See the [API reference](/api-reference/sl-callout) for all attributes and properties. diff --git a/docs/website/src/content/components/layout/card.md b/docs/website/src/content/components/layout/card.md new file mode 100644 index 0000000000..53f4d7fec3 --- /dev/null +++ b/docs/website/src/content/components/layout/card.md @@ -0,0 +1,85 @@ +--- +title: Card +layout: docs +eleventyNavigation: + key: Card + parent: Layout +--- + +`` is a flexible container for a single piece of content, such as an article, product or +result. It provides slots for media, a title, supporting header content, body text and actions, and +can be laid out horizontally or vertically. + +## Slots + +default +: The title of the card. + +`media` +: An image (or other media) for the card. + +`header` +: A subtitle or badges shown near the title. + +`body` +: The body text of the card. + +`actions` +: Actions shown at the bottom — a single button or a button bar. + +## Usage + +```html + + + Card title + A short description of the card's content. + +``` + +## Examples + +### Basic + +By default the card is laid out horizontally, with the media beside the content. + +```html {.example .show-source} + + + Captivating Nyhavn Moments + Travel + + Immerse yourself in the vibrant hues of Nyhavn, Copenhagen's iconic waterfront. + + +``` + +### Vertical + +Set `orientation="vertical"` to stack the media above the content — useful in grids. + +```html {.example .show-source} + + + Architectural Elegance + New + Copenhagen's skyline blends historic landmarks with contemporary design. + +``` + +### With actions + +Use the `actions` slot for buttons at the bottom of the card. + +```html {.example .show-source} + + + Enchanting Copenhagen + Discover the allure of Copenhagen's waterfront gem. + Read more + +``` + +## API + +See the [API reference](/api-reference/sl-card) for all attributes, properties and slots. diff --git a/docs/website/src/content/components/layout/layout.md b/docs/website/src/content/components/layout/layout.md index 8bd28506a6..3c79f3b926 100644 --- a/docs/website/src/content/components/layout/layout.md +++ b/docs/website/src/content/components/layout/layout.md @@ -8,4 +8,20 @@ eleventyNavigation: icon: table-layout --- -Layout components allow users to create flexible and responsive layouts. +Layout components give content structure and grouping. They range from simple containers (panel, +card) to grouping patterns that manage their own content (accordion), and prominent in-page messages +(callout). + +## Overview + +[Card](/components/layout/card) +: A flexible container for a piece of content, with slots for media, a title, body and actions. + +[Panel](/components/layout/panel) +: A container with a heading and actions, optionally collapsible. + +[Accordion](/components/layout/accordion) +: A stack of collapsible sections, of which one or several can be open at a time. + +[Callout](/components/layout/callout) +: A prominent, in-page message that draws attention to important information. diff --git a/docs/website/src/content/components/layout/panel.md b/docs/website/src/content/components/layout/panel.md new file mode 100644 index 0000000000..1184982f46 --- /dev/null +++ b/docs/website/src/content/components/layout/panel.md @@ -0,0 +1,68 @@ +--- +title: Panel +layout: docs +eleventyNavigation: + key: Panel + parent: Layout +--- + +`` is a container with a heading and an area for actions, used to group related content +into a labelled section. It can optionally be collapsed. + +Set the heading with the `heading` attribute (or the `heading` slot for richer content), put the +content in the default slot, and place buttons in the `actions` slot. + +## Usage + +```html + + + + + Panel content goes here. + +``` + +## Examples + +### Basic + +```html {.example .show-source} + + + + + Panel content goes here. + +``` + +### Collapsible + +Add `collapsible` to let the user expand and collapse the panel, and `collapsed` to have it start +collapsed. + +```html {.example .show-source .vertical} + + This panel can be collapsed by clicking its heading. + + + This panel starts collapsed. + +``` + +### Prefix and suffix + +Use the `prefix` and `suffix` slots to add content before or after the heading, such as an icon or a +badge. + +```html {.example .show-source} + + + Active + Panel content goes here. + +``` + +## API + +See the [API reference](/api-reference/sl-panel) for all attributes, properties and slots. diff --git a/docs/website/src/content/components/navigation/breadcrumbs.md b/docs/website/src/content/components/navigation/breadcrumbs.md new file mode 100644 index 0000000000..8940310e68 --- /dev/null +++ b/docs/website/src/content/components/navigation/breadcrumbs.md @@ -0,0 +1,92 @@ +--- +title: Breadcrumbs +layout: docs +eleventyNavigation: + key: Breadcrumbs + parent: Navigation +--- + +`` shows the path to the current page and lets the user navigate back up the +hierarchy. By default it prepends a link to the home page. + +Provide the trail as plain `
` elements in the default slot; the component adds the separators and, +on small screens, collapses the middle of the trail to save space. + +## Usage + +```html + + Products + Books + The Great Gatsby + +``` + +## Examples + +### Basic + +A home link is shown first, followed by the breadcrumbs you provide. + +```html {.example .show-source} + + Lorem + Ipsum + Dolor + +``` + +### Without the home link + +Add `no-home` to omit the automatic home link. + +```html {.example .show-source} + + Lorem + Ipsum + +``` + +### Hide the home label + +Add `hide-home-label` to show only the home icon, without the "Home" text. + +```html {.example .show-source} + + Lorem + Ipsum + +``` + +### Collapsing + +When the trail is too long to fit, the middle items collapse behind a menu so the first and last +remain visible. + +```html {.example .show-source} + + Lorem + Ipsum + Dolor + Sit + Amet + Consectetur + +``` + +### Inverted + +Use the `inverted` attribute on dark backgrounds. + +```html {.example .show-source} +
+ + Lorem + Ipsum + +
+``` + +## API + +See the [API reference](/api-reference/sl-breadcrumbs) for all attributes and properties. diff --git a/docs/website/src/content/components/navigation/navigation.md b/docs/website/src/content/components/navigation/navigation.md index 4f9022392d..7b17004c98 100644 --- a/docs/website/src/content/components/navigation/navigation.md +++ b/docs/website/src/content/components/navigation/navigation.md @@ -8,4 +8,20 @@ eleventyNavigation: icon: compass --- -Navigation components allow users to create flexible and responsive navigation menus. +Navigation components help users move through an application and understand where they are. They +range from showing the current location (breadcrumbs) to switching between views (tabs), browsing +hierarchical data (tree) and moving through large sets of results (paginator). + +## Overview + +[Breadcrumbs](/components/navigation/breadcrumbs) +: Shows the path to the current page and lets the user navigate back up the hierarchy. + +[Tabs](/components/navigation/tabs) +: Switches between related views within the same context. + +[Tree](/components/navigation/tree) +: Displays hierarchical data that can be expanded, collapsed and selected. + +[Paginator](/components/navigation/paginator) +: Splits a large list of items across multiple pages. diff --git a/docs/website/src/content/components/navigation/paginator.md b/docs/website/src/content/components/navigation/paginator.md new file mode 100644 index 0000000000..94fca778ac --- /dev/null +++ b/docs/website/src/content/components/navigation/paginator.md @@ -0,0 +1,82 @@ +--- +title: Paginator +layout: docs +eleventyNavigation: + key: Paginator + parent: Navigation +--- + +`` splits a large list of items across multiple pages and lets the user move between +them. It comes with two companion components: ``, which shows a "showing X–Y of +Z" summary, and ``, which lets the user change how many items appear per +page. + +Configure the paginator with the total number of items (`total-items`), the page size (`page-size`) +and the current page (`page`, zero-based). + +## Usage + +```html + +``` + +The paginator emits an `sl-page-change` event with the new page index whenever the user navigates. + +## Examples + +### Basic + +```html {.example .show-source} + +``` + +### Status + +`` shows which items are currently visible. + +```html {.example .show-source} + +``` + +### Page size selector + +`` lets the user choose the page size from a list. It emits an +`sl-page-size-change` event. + +```html {.example .show-source} + +``` + +### Putting it together + +In a real application you wire the three components together so changing the page or page size keeps +them in sync. + +```html +
+ + + +
+ + +``` + +## API + +See the API reference for [`sl-paginator`](/api-reference/sl-paginator), +[`sl-paginator-status`](/api-reference/sl-paginator-status) and +[`sl-paginator-page-size`](/api-reference/sl-paginator-page-size). diff --git a/docs/website/src/content/components/navigation/tabs.md b/docs/website/src/content/components/navigation/tabs.md new file mode 100644 index 0000000000..91ee0ec486 --- /dev/null +++ b/docs/website/src/content/components/navigation/tabs.md @@ -0,0 +1,100 @@ +--- +title: Tabs +layout: docs +eleventyNavigation: + key: Tabs + parent: Navigation +--- + +`` organises related content into tabs, letting the user switch between views within +the same context. Each `` is paired, in order, with an `` that holds its +content. + +## Usage + +```html + + First tab + Second tab + + Contents of the first tab + Contents of the second tab + +``` + +Mark the initial tab with `selected`, and disable a tab with `disabled`. + +## Examples + +### Basic + +```html {.example .show-source} + + First tab + Second tab + Disabled tab + + Contents of the first tab + Contents of the second tab + Contents of the third tab + +``` + +### Icons, subtitles and badges + +A tab can include an icon (`icon` slot), a `subtitle` slot and a `badge` slot for richer labels. + +```html {.example .show-source} + + + + Overview + Project summary + + + + Tasks + Things to do + 8 + + + Overview content + Tasks content + +``` + +### Vertical + +Add the `vertical` attribute to stack the tabs alongside the panels. + +```html {.example .show-source} + + First tab + Second tab + + Contents of the first tab + Contents of the second tab + +``` + +### Activation + +By default a tab is activated manually (focus a tab, then press Enter or Space). Set +`activation="auto"` to activate a tab as soon as it receives focus. + +```html {.example .show-source} + + First tab + Second tab + Third tab + + Contents of the first tab + Contents of the second tab + Contents of the third tab + +``` + +## API + +See the API reference for [`sl-tab-group`](/api-reference/sl-tab-group), +[`sl-tab`](/api-reference/sl-tab) and [`sl-tab-panel`](/api-reference/sl-tab-panel). diff --git a/docs/website/src/content/components/navigation/tree.md b/docs/website/src/content/components/navigation/tree.md new file mode 100644 index 0000000000..64f04dd9c8 --- /dev/null +++ b/docs/website/src/content/components/navigation/tree.md @@ -0,0 +1,85 @@ +--- +title: Tree +layout: docs +eleventyNavigation: + key: Tree + parent: Navigation +--- + +`` displays hierarchical data as a tree that can be expanded, collapsed and selected. It is +virtualized, so it stays performant even with very large data sets. + +The data is provided through a **data source** rather than slotted markup. Use `FlatTreeDataSource` +when your data is a flat array with a level on each item, or `NestedTreeDataSource` when your data is +already nested with children. + +## Usage + +Set the `dataSource` property to a configured data source. The configuration tells the tree how to +read each item — its id, label, level, and whether it is expandable, expanded or selected. + +```html + + + +``` + +## Nested data + +If your data already has a nested shape, use `NestedTreeDataSource` and tell it how to read each +node's children. + +```js +import { NestedTreeDataSource } from '@sl-design-system/tree'; + +const dataSource = new NestedTreeDataSource(data, { + getId: item => item.id, + getLabel: item => item.name, + getChildren: item => item.children +}); +``` + +## Options + +hideGuides +: Set the `hide-guides` attribute to hide the indentation guide lines. + +renderer +: Provide a `renderer` function to customise how each node is rendered — for example to add icons, + badges or buttons. + +## Selection + +The data source exposes the current `selection` and methods such as `toggle`, `expandAll` and +`collapseAll`. Enable selecting multiple nodes through the data source's selection configuration. + +```js +// React to selection changes +tree.dataSource.addEventListener('sl-selection-change', () => { + console.log([...tree.dataSource.selection]); +}); +``` + +## API + +`` is configured through the `dataSource`, `renderer` and `hide-guides` properties. See the +[source on GitHub](https://github.com/sl-design-system/components/tree/main/packages/components/tree) +for the full API. diff --git a/docs/website/src/content/components/overlay/dialog.md b/docs/website/src/content/components/overlay/dialog.md new file mode 100644 index 0000000000..0306434ed4 --- /dev/null +++ b/docs/website/src/content/components/overlay/dialog.md @@ -0,0 +1,84 @@ +--- +title: Dialog +layout: docs +eleventyNavigation: + key: Dialog + parent: Overlay +--- + +`` is a modal window that appears on top of the page for a focused task, such as +confirming an action or filling in a short form. It has a `title` slot, a body (the default slot) +and slots for action buttons. For simple alert/confirm messages, the +[message dialog](/components/overlay/message-dialog) is more convenient. + +## Opening and closing + +The dialog works with the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API), +so you can open and close it from buttons without writing any JavaScript. Point a button at the +dialog with `commandfor` and use the `--show-modal` and `--close` commands: + +```html {.example .show-source} +Open dialog + + + Dialog title +

This is the body of the dialog. Use it for focused tasks or important messages.

+ Cancel + Confirm +
+``` + +You can also open it from JavaScript by calling `showModal()` on the dialog, and close it with +`close()`. + +## Examples + +### Actions + +Place buttons in the `primary-actions` slot, and less prominent actions in the `secondary-actions` +slot. Add `sl-dialog-close` (or a `--close` command) to a button to close the dialog when it is +clicked. + +```html {.example .show-source} +Open dialog + + + Delete item +

Are you sure you want to delete this item? This action cannot be undone.

+ Help + Cancel + Delete +
+``` + +### Close button + +Add the `close-button` attribute to show a close (×) button in the top corner. + +```html {.example .show-source} +Open dialog + + + With a close button +

This dialog has a close button in the top corner.

+
+``` + +### Disable cancel + +By default the dialog can be dismissed with the Escape key or by clicking the backdrop. Add +`disable-cancel` to require the user to use one of the action buttons. + +```html {.example .show-source} +Open dialog + + + Disable cancel +

You can only close this dialog with the button below.

+ Close +
+``` + +## API + +See the [API reference](/api-reference/sl-dialog) for all attributes and properties. diff --git a/docs/website/src/content/components/overlay/drawer.md b/docs/website/src/content/components/overlay/drawer.md new file mode 100644 index 0000000000..10757a46ed --- /dev/null +++ b/docs/website/src/content/components/overlay/drawer.md @@ -0,0 +1,63 @@ +--- +title: Drawer +layout: docs +eleventyNavigation: + key: Drawer + parent: Overlay +--- + +`` is a panel that slides in from the edge of the screen. Use it for secondary content or +tasks that benefit from more room than a [dialog](/components/overlay/dialog) — such as filters, +details or settings — while keeping the user in context. + +Put a heading in the `title` slot and the panel content in the default slot. + +## Usage + +Open the drawer by calling `showModal()` on it, and close it with `close()`. + +```html +Show drawer + + +

More information

+

The content of the drawer goes here.

+
+ + +``` + +## Examples + +### Attachment + +Use the `attachment` attribute to choose which edge the drawer slides in from. It defaults to +`right`. + +```html + +

Left drawer

+

This drawer slides in from the left.

+
+``` + +### Disable close + +By default the drawer can be dismissed with the Escape key or by clicking the backdrop. Add +`disable-close` to require the user to close it explicitly. + +```html + +

Important

+

This drawer can only be closed with a button inside it.

+
+``` + +## API + +See the [API reference](/api-reference/sl-drawer) for all attributes and properties. diff --git a/docs/website/src/content/components/overlay/message-dialog.md b/docs/website/src/content/components/overlay/message-dialog.md new file mode 100644 index 0000000000..88651dba85 --- /dev/null +++ b/docs/website/src/content/components/overlay/message-dialog.md @@ -0,0 +1,64 @@ +--- +title: Message dialog +layout: docs +eleventyNavigation: + key: Message dialog + parent: Overlay +--- + +The message dialog is a convenience API for showing simple alert and confirmation dialogs — the +design system equivalent of `window.alert()` and `window.confirm()`. Rather than placing an element +in your markup, you call a static method that shows the dialog and returns a promise. + +For richer, custom modals, use the [dialog](/components/overlay/dialog) component instead. + +## Alert + +`MessageDialog.alert()` shows a message with a single OK button. It resolves when the user +dismisses it. + +```js +import { MessageDialog } from '@sl-design-system/message-dialog'; + +await MessageDialog.alert('Your changes have been saved.'); + +// With a custom title +await MessageDialog.alert('Your changes have been saved.', 'Success'); +``` + +## Confirm + +`MessageDialog.confirm()` shows a message with OK and Cancel buttons and resolves to a boolean +indicating the user's choice. + +```js +import { MessageDialog } from '@sl-design-system/message-dialog'; + +const confirmed = await MessageDialog.confirm('Are you sure you want to delete this item?', 'Delete item'); + +if (confirmed) { + // proceed with the deletion +} +``` + +## Custom dialogs + +For full control over the message, title and buttons, use `MessageDialog.show()` with a +configuration object. + +```js +import { MessageDialog } from '@sl-design-system/message-dialog'; + +const result = await MessageDialog.show({ + title: 'Unsaved changes', + message: 'You have unsaved changes. What would you like to do?', + buttons: [ + { text: 'Discard', value: 'discard', variant: 'danger' }, + { text: 'Keep editing', value: 'keep', variant: 'primary' } + ] +}); +``` + +## API + +See the [API reference](/api-reference/sl-message-dialog) for all properties and methods. diff --git a/docs/website/src/content/components/overlay/overlay.md b/docs/website/src/content/components/overlay/overlay.md index 71244ed9e5..4816c5e01b 100644 --- a/docs/website/src/content/components/overlay/overlay.md +++ b/docs/website/src/content/components/overlay/overlay.md @@ -8,4 +8,26 @@ eleventyNavigation: icon: layer-group --- -Overlay components allow users to create flexible and responsive overlay elements. +Overlay components display content on top of the rest of the page. They range from small, +contextual hints (tooltip, popover) to focused, interrupting surfaces (dialog, drawer, message +dialog) and contextual action lists (menu). + +## Overview + +[Dialog](/components/overlay/dialog) +: A modal window for focused tasks, with a title, body and action buttons. + +[Message dialog](/components/overlay/message-dialog) +: A convenience API for simple alert and confirm dialogs. + +[Drawer](/components/overlay/drawer) +: A panel that slides in from the edge of the screen. + +[Popover](/components/overlay/popover) +: A lightweight, anchored container for extra information or actions. + +[Tooltip](/components/overlay/tooltip) +: A small label that appears on hover or focus. + +[Menu](/components/overlay/menu) +: A list of contextual actions. diff --git a/docs/website/src/content/components/overlay/popover.md b/docs/website/src/content/components/overlay/popover.md new file mode 100644 index 0000000000..aa8edc6c6b --- /dev/null +++ b/docs/website/src/content/components/overlay/popover.md @@ -0,0 +1,62 @@ +--- +title: Popover +layout: docs +eleventyNavigation: + key: Popover + parent: Overlay +--- + +`` is a lightweight, anchored container that appears on top of other content to show +extra information or actions. Unlike a [dialog](/components/overlay/dialog) it is non-modal: the rest +of the page stays interactive. Unlike a [tooltip](/components/overlay/tooltip) it can contain rich, +interactive content. + +Anchor the popover to a trigger with the `anchor` attribute (the trigger's `id`), and control its +placement with `position`. + +## Usage + +The popover is built on the native [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API), +so you toggle it by calling `togglePopover()` (or `showPopover()` / `hidePopover()`). + +```html +Toggle popover + + +
Hello! I am a popover.
+

I can hold text, actions, or other rich content.

+
+ + +``` + +## Position + +Use the `position` attribute to place the popover relative to its anchor. Besides `top`, `right`, +`bottom` and `left`, each side also has `-start` and `-end` variants. + +```html + +``` + +## Rich content + +Because the popover is non-modal and accepts any markup, you can include headings, paragraphs and +interactive controls such as buttons. + +```html + +
Filters
+

Refine the results shown in the list.

+ Apply +
+``` + +## API + +See the [API reference](/api-reference/sl-popover) for all attributes and properties. diff --git a/docs/website/src/content/components/overlay/tooltip.md b/docs/website/src/content/components/overlay/tooltip.md new file mode 100644 index 0000000000..d2c302ade0 --- /dev/null +++ b/docs/website/src/content/components/overlay/tooltip.md @@ -0,0 +1,67 @@ +--- +title: Tooltip +layout: docs +eleventyNavigation: + key: Tooltip + parent: Overlay +--- + +`` shows a small label when the user hovers over or focuses an element. Use it for short, +supplementary hints. Because tooltips only appear on hover/focus, never put essential information or +interactive content in them. + +Associate a tooltip with its trigger by giving the trigger an `aria-describedby` (or, for icon-only +controls, `aria-labelledby`) that matches the tooltip's `id`. + +## Usage + +```html +Hover me +This is a tooltip +``` + +## Examples + +### Basic + +Hover or focus the button to reveal the tooltip. + +```html {.example .show-source} +Hover me +This is the tooltip message +``` + +### Positions + +Use the `position` attribute to place the tooltip relative to its trigger. Besides `top`, `right`, +`bottom` and `left`, each side also has `-start` and `-end` variants. + +```html {.example .show-source .horizontal} +Top +Tooltip on top + +Right +Tooltip on the right + +Bottom +Tooltip on the bottom + +Left +Tooltip on the left +``` + +### Labelling an icon-only button + +For a button with only an icon, use `aria-labelledby` so the tooltip also acts as the button's +accessible name. + +```html {.example .show-source} + + + +Settings +``` + +## API + +See the [API reference](/api-reference/sl-tooltip) for all attributes and properties. diff --git a/docs/website/src/content/components/status/badge.md b/docs/website/src/content/components/status/badge.md new file mode 100644 index 0000000000..269d79cee3 --- /dev/null +++ b/docs/website/src/content/components/status/badge.md @@ -0,0 +1,72 @@ +--- +title: Badge +layout: docs +eleventyNavigation: + key: Badge + parent: Status +--- + +`` is a small label used to highlight a status, category or count next to other content. +It draws attention to a short piece of information without taking up much space. + +## Usage + +```html +Status +``` + +## Examples + +### Colors + +Use the `color` attribute to convey meaning. Available colors are `blue`, `green`, `grey`, +`orange`, `purple`, `red`, `teal` and `yellow`. + +```html {.example .show-source .horizontal} +Blue +Green +Grey +Orange +Purple +Red +Teal +Yellow +``` + +### Emphasis + +The `emphasis` attribute switches between a `subtle` (default) and a `bold` appearance. + +```html {.example .show-source .horizontal} +Subtle +Bold +``` + +### Sizes + +Use the `size` attribute to pick `sm`, `md` or `lg`. + +```html {.example .show-source .horizontal} +Small +Medium +Large +``` + +### Count and icon + +A badge can also hold a number or a single icon, which makes it useful as a counter. + +```html {.example .show-source .horizontal} +8 + +100 +``` + +::: info +The `variant` attribute is deprecated. Use the `color` attribute instead — existing variants are +mapped to the matching color. +::: + +## API + +See the [API reference](/api-reference/sl-badge) for all attributes and properties. diff --git a/docs/website/src/content/components/status/inline-message.md b/docs/website/src/content/components/status/inline-message.md new file mode 100644 index 0000000000..446e3a49ad --- /dev/null +++ b/docs/website/src/content/components/status/inline-message.md @@ -0,0 +1,74 @@ +--- +title: Inline message +layout: docs +eleventyNavigation: + key: Inline message + parent: Status +--- + +`` is a prominent, in-page message that informs the user about the result of an +action or the state of a part of the interface. Unlike a toast, it stays in the layout next to the +content it relates to. + +Put the main content in the default slot, and an optional heading in the `title` slot. + +## Usage + +```html + +

Heads up

+ The main content of the message. +
+``` + +## Examples + +### Variants + +The `variant` attribute sets the tone and icon: `info`, `success`, `warning` or `danger`. + +```html {.example .show-source .vertical} +An informational message. +Your changes have been saved. +Your subscription expires soon. +Something went wrong. +``` + +### With a title and details + +Use the `title` slot for a heading and the default slot for richer content. + +```html {.example .show-source} + +

Could not save your changes

+ Please fix the following errors: +
    +
  • Title is required
  • +
  • End date must be after the start date
  • +
+
+``` + +### Indismissible + +By default the message has a close button. Add `indismissible` to remove it for messages that should +stay visible. + +```html {.example .show-source} +This message cannot be dismissed. +``` + +### Custom icon + +Use the `icon` slot to override the default variant icon. + +```html {.example .show-source} + + + This example shows a custom icon. + +``` + +## API + +See the [API reference](/api-reference/sl-inline-message) for all attributes, properties and events. diff --git a/docs/website/src/content/components/status/progress-bar.md b/docs/website/src/content/components/status/progress-bar.md new file mode 100644 index 0000000000..faadaeb8ee --- /dev/null +++ b/docs/website/src/content/components/status/progress-bar.md @@ -0,0 +1,61 @@ +--- +title: Progress bar +layout: docs +eleventyNavigation: + key: Progress bar + parent: Status +--- + +`` shows the progress of a task. Use it when you can measure how far along +something is (for example an upload). When the duration is unknown, use the `indeterminate` state or +a [spinner](/components/status/spinner) instead. + +Set the progress with the `value` attribute (0–100), describe the task with the `label` attribute, +and optionally add helper text in the default slot. + +## Usage + +```html +Uploaded 60% of 100% +``` + +## Examples + +### Basic + +```html {.example .show-source .vertical} +Uploaded 60% of 100% +``` + +### Variants + +The `variant` attribute communicates the outcome of the task: `success`, `warning` or `error`. + +```html {.example .show-source .vertical} +File uploaded +40% of 100% +50% of 100% +``` + +### Colors + +Use the `color` attribute for a decorative, non-semantic color such as `blue`, `green`, `orange`, +`purple`, `red`, `teal` or `yellow`. + +```html {.example .show-source .vertical} + + + +``` + +### Indeterminate + +When you can't measure progress, add the `indeterminate` attribute to show continuous activity. + +```html {.example .show-source .vertical} +This may take a few minutes +``` + +## API + +See the [API reference](/api-reference/sl-progress-bar) for all attributes and properties. diff --git a/docs/website/src/content/components/status/skeleton.md b/docs/website/src/content/components/status/skeleton.md new file mode 100644 index 0000000000..435d7b7056 --- /dev/null +++ b/docs/website/src/content/components/status/skeleton.md @@ -0,0 +1,61 @@ +--- +title: Skeleton +layout: docs +eleventyNavigation: + key: Skeleton + parent: Status +--- + +`` is a placeholder that mimics the shape of content while it is loading. Showing the +rough layout up front reduces the perceived loading time and avoids large layout shifts when the +real content arrives. + +Size a skeleton with regular CSS (`width`/`height` or `inline-size`/`block-size`) so it matches the +content it stands in for. + +## Usage + +```html + +``` + +## Examples + +### Effects + +The `effect` attribute controls the loading animation: `shimmer` (default), `sheen`, `pulse` or +`none`. + +```html {.example .show-source .vertical} + + + + +``` + +### Shapes + +Use `variant="circle"` for avatars and other round content. By default the skeleton is rectangular. + +```html {.example .show-source .horizontal} + + +``` + +### Composing a placeholder + +Combine several skeletons to mirror the layout of the content that is loading. + +```html {.example .show-source} +
+ + + + + +
+``` + +## API + +See the [API reference](/api-reference/sl-skeleton) for all attributes and properties. diff --git a/docs/website/src/content/components/status/spinner.md b/docs/website/src/content/components/status/spinner.md new file mode 100644 index 0000000000..bc2f239a5a --- /dev/null +++ b/docs/website/src/content/components/status/spinner.md @@ -0,0 +1,53 @@ +--- +title: Spinner +layout: docs +eleventyNavigation: + key: Spinner + parent: Status +--- + +`` indicates that something is loading when you don't know how long it will take. For +tasks where you can measure progress, use a [progress bar](/components/status/progress-bar) instead. + +The spinner uses `currentColor`, so it takes the text color of its container — which makes it easy +to use inside buttons and other colored surfaces. + +## Usage + +```html + +``` + +## Examples + +### Sizes + +Use the `size` attribute to choose from `sm`, `md`, `lg`, `xl`, `2xl`, `3xl` and `4xl`. + +```html {.example .show-source .horizontal} + + + + + +``` + +### In a button + +Because the spinner inherits the current text color, it sits naturally inside a button to indicate a +pending action. + +```html {.example .show-source .horizontal} + + + Sending + + + + Sending + +``` + +## API + +See the [API reference](/api-reference/sl-spinner) for all attributes and properties. diff --git a/docs/website/src/content/components/status/status.md b/docs/website/src/content/components/status/status.md index 6184a22448..55f4b34431 100644 --- a/docs/website/src/content/components/status/status.md +++ b/docs/website/src/content/components/status/status.md @@ -8,4 +8,29 @@ eleventyNavigation: icon: alarm-clock --- -Status components allow users to create flexible and responsive status indicators. +Status components communicate the state of the system to the user — whether something is loading, +in progress, succeeded or failed, and how content is categorised. Use them to give timely, clear +feedback so users always know what is happening. + +## Overview + +[Badge](/components/status/badge) +: A small label that highlights a status, category or count. + +[Tag](/components/status/tag) +: A compact, optionally removable label used to represent a keyword, filter or selection. + +[Tag list](/components/status/tag-list) +: Displays a collection of tags, with optional stacking and an overflow counter. + +[Inline message](/components/status/inline-message) +: A prominent, in-page message that informs the user about the result of an action. + +[Progress bar](/components/status/progress-bar) +: Shows the progress of a task, either as a percentage or as an indeterminate activity. + +[Spinner](/components/status/spinner) +: Indicates that something is loading when the duration is unknown. + +[Skeleton](/components/status/skeleton) +: A placeholder that mimics the shape of content while it is loading. diff --git a/docs/website/src/content/components/status/tag-list.md b/docs/website/src/content/components/status/tag-list.md new file mode 100644 index 0000000000..139c5f6bcf --- /dev/null +++ b/docs/website/src/content/components/status/tag-list.md @@ -0,0 +1,89 @@ +--- +title: Tag list +layout: docs +eleventyNavigation: + key: Tag list + parent: Status +--- + +`` displays a collection of [tags](/components/status/tag) together. It handles +overflow for you: tags that don't fit are collapsed behind a counter, and you can optionally stack +them to save space. + +Place `` elements in the default slot. + +## Usage + +```html + + Tag 1 + Tag 2 + Tag 3 + +``` + +## Examples + +### Basic + +When there are more tags than fit on a single line, the remainder is collapsed behind a counter. + +```html {.example .show-source} + + Design + Development + Research + Accessibility + Testing + Documentation + Localization + +``` + +### Variants and sizes + +The `variant` (`neutral` or `info`) and `size` (`md` or `lg`) attributes apply to all tags in the +list. + +```html {.example .show-source .vertical} + + Info 1 + Info 2 + Info 3 + + + Large 1 + Large 2 + Large 3 + +``` + +### Removable + +Make the tags removable through the list. + +```html {.example .show-source} + + Design + Development + Research + +``` + +### Stacked + +Add the `stacked` attribute to overlap the tags into a compact group with a counter. + +```html {.example .show-source} + + Tag 1 + Tag 2 + Tag 3 + Tag 4 + Tag 5 + +``` + +## API + +See the [API reference](/api-reference/sl-tag-list) for all attributes and properties. diff --git a/docs/website/src/content/components/status/tag.md b/docs/website/src/content/components/status/tag.md new file mode 100644 index 0000000000..b94cb36dff --- /dev/null +++ b/docs/website/src/content/components/status/tag.md @@ -0,0 +1,61 @@ +--- +title: Tag +layout: docs +eleventyNavigation: + key: Tag + parent: Status +--- + +`` is a compact label that represents a keyword, filter or selection. Tags are often +removable, so users can clear individual selections. To show several tags together, use the +[tag list](/components/status/tag-list). + +The tag's text goes in the default slot. + +## Usage + +```html +Tag label +``` + +## Examples + +### Variants + +The `variant` attribute switches between the `default` and `info` appearance. + +```html {.example .show-source .horizontal} +Default +Info +``` + +### Sizes + +Use the `size` attribute to choose `md` (default) or `lg`. + +```html {.example .show-source .horizontal} +Medium +Large +``` + +### Removable + +Add the `removable` attribute to show a remove button. The tag emits an event when the user removes +it. + +```html {.example .show-source .horizontal} +Removable +``` + +### Disabled + +Add the `disabled` attribute to make a tag non-interactive. + +```html {.example .show-source .horizontal} +Disabled +Disabled +``` + +## API + +See the [API reference](/api-reference/sl-tag) for all attributes, properties and events. diff --git a/docs/website/src/content/components/utilities/announcer.md b/docs/website/src/content/components/utilities/announcer.md new file mode 100644 index 0000000000..ed537a4745 --- /dev/null +++ b/docs/website/src/content/components/utilities/announcer.md @@ -0,0 +1,55 @@ +--- +title: Announcer +layout: docs +eleventyNavigation: + key: Announcer + parent: Utilities +--- + +The announcer sends messages to assistive technology (such as screen readers) through an ARIA +[live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). +Use it to communicate things that happen visually but would otherwise go unnoticed by screen reader +users — for example "5 results found", "Item added to cart" or "Changes saved". + +There should be a single `` instance on the page (typically in your app shell). All +components and code share that one instance, so you never create it more than once. + +```html + +``` + +::: info +Avoid sending messages too quickly after one another — screen readers may skip messages that arrive +in rapid succession. +::: + +## Sending messages + +### With the `announce` function + +The simplest way to announce something is the `announce` helper. The second argument is the urgency: +`polite` (the default) waits until the user is idle, while `assertive` interrupts immediately. + +```js +import { announce } from '@sl-design-system/announcer'; + +announce('Your changes have been saved.'); +announce('Error: could not save your changes.', 'assertive'); +``` + +### With an event + +You can also announce a message by emitting the `sl-announce` event on `document.body`. This is +handy when you don't want a direct dependency on the announcer in your code. + +```js +document.body.dispatchEvent( + new CustomEvent('sl-announce', { + detail: { message: 'Your changes have been saved.' } + }) +); +``` + +## API + +See the [API reference](/api-reference/sl-announcer) for all attributes, properties and events. diff --git a/docs/website/src/content/components/utilities/ellipsize-text.md b/docs/website/src/content/components/utilities/ellipsize-text.md new file mode 100644 index 0000000000..68d51dfe11 --- /dev/null +++ b/docs/website/src/content/components/utilities/ellipsize-text.md @@ -0,0 +1,43 @@ +--- +title: Ellipsize text +layout: docs +eleventyNavigation: + key: Ellipsize text + parent: Utilities +--- + +`` truncates text that overflows its container with an ellipsis (…) and, when the +text is truncated, shows the full text in a tooltip on hover and focus. It watches its container for +size changes, so the truncation stays correct when the layout changes. + +Use it wherever you have a fixed-width area that may contain text of unpredictable length, such as +table cells, list items or cards. + +## Usage + +The component truncates based on the width of its container, so give it (or a parent) a constrained +width. + +```html + + This is a long text that should be truncated + +``` + +## Examples + +### Basic + +The first line is truncated and the full text is available in a tooltip. The second example has +plenty of room, so no ellipsis or tooltip is added. + +```html {.example .show-source} + + This is a long text that should be truncated + +Short text +``` + +## API + +See the [API reference](/api-reference/sl-ellipsize-text) for all attributes and properties. diff --git a/docs/website/src/content/components/utilities/emoji-browser.md b/docs/website/src/content/components/utilities/emoji-browser.md new file mode 100644 index 0000000000..afcaad025d --- /dev/null +++ b/docs/website/src/content/components/utilities/emoji-browser.md @@ -0,0 +1,52 @@ +--- +title: Emoji browser +layout: docs +eleventyNavigation: + key: Emoji browser + parent: Utilities +--- + +`` is a searchable emoji picker. It shows emojis grouped by category, supports +searching by name, and can highlight a set of frequently used emojis at the top. + +The emoji data is loaded at runtime, so you must tell the component where to find it through the +`base-url` attribute. + +## Usage + +```html + +``` + +Make sure the emoji data files are served from the URL you pass to `base-url`. + +## Examples + +### Frequently used + +Pass a space-separated list of emojis to `frequently-used` to show them in a dedicated section at +the top. + +```html + +``` + +### Initial search query + +Use the `query` attribute to open the browser with a search already applied. + +```html + +``` + +### Locale + +Use the `locale` attribute to localize the emoji names used for searching and labelling. + +```html + +``` + +## API + +See the [API reference](/api-reference/sl-emoji-browser) for all attributes, properties and events. diff --git a/docs/website/src/content/components/utilities/format-date.md b/docs/website/src/content/components/utilities/format-date.md new file mode 100644 index 0000000000..9f3efbd988 --- /dev/null +++ b/docs/website/src/content/components/utilities/format-date.md @@ -0,0 +1,68 @@ +--- +title: Format date +layout: docs +eleventyNavigation: + key: Format date + parent: Utilities +--- + +`` formats a date and/or time according to a locale. It is a thin wrapper around the +native [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) +API, so anything you can do with `Intl.DateTimeFormat` you can do declaratively in your markup. + +The `date` attribute accepts anything the `Date` constructor understands — an ISO string, a +timestamp, or a `Date` object set via the property. + +## Usage + +```html + +``` + +If no valid date is set, the slotted content is shown instead, which makes it easy to provide a +fallback message. + +## Examples + +### Date and time style + +Use the `date-style` and `time-style` attributes for quick, locale-aware formatting. They accept +`full`, `long`, `medium` and `short`, and can be combined. + +```html {.example .show-source} + + + +``` + +### Individual fields + +For full control, set individual fields such as `weekday`, `year`, `month`, `day`, `hour` and +`minute`. These cannot be combined with `date-style`/`time-style`. + +```html {.example .show-source} + +``` + +### Locales + +Set the `locale` attribute to format the same date for a different language and region. + +```html {.example .show-source} + + + + +``` + +### Fallback + +When the date is missing or invalid, the slotted content is rendered instead. + +```html {.example .show-source} +No date available +``` + +## API + +See the [API reference](/api-reference/sl-format-date) for all attributes and properties. diff --git a/docs/website/src/content/components/utilities/format-number.md b/docs/website/src/content/components/utilities/format-number.md new file mode 100644 index 0000000000..60f7b018bd --- /dev/null +++ b/docs/website/src/content/components/utilities/format-number.md @@ -0,0 +1,81 @@ +--- +title: Format number +layout: docs +eleventyNavigation: + key: Format number + parent: Utilities +--- + +`` formats a number according to a locale. It is a thin wrapper around the native +[`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) +API, so it can format plain numbers, currencies, percentages and units. + +Set the value with the `number` attribute. If the value is not a valid number, the slotted content +is shown instead. + +## Usage + +```html + +``` + +## Examples + +### Decimal + +By default the number is formatted as a decimal, using grouping separators appropriate for the +locale. + +```html {.example .show-source} + +``` + +### Currency + +Set `number-style="currency"` together with a `currency` code (an ISO 4217 code such as `EUR` or +`USD`). + +```html {.example .show-source} + + +``` + +### Percent + +With `number-style="percent"` the value is multiplied by 100 and a percent sign is appended, so +`0.1` becomes `10%`. + +```html {.example .show-source} + +``` + +### Unit + +Use `number-style="unit"` with a `unit` (such as `meter` or `kilometer-per-hour`) and an optional +`unit-display` of `short`, `long` or `narrow`. + +```html {.example .show-source} + +``` + +### Locales + +Set the `locale` attribute to change the grouping and decimal separators. + +```html {.example .show-source} + + + +``` + +### Fallback + +When the value is not a valid number, the slotted content is rendered instead. + +```html {.example .show-source} +Not available +``` + +## API + +See the [API reference](/api-reference/sl-format-number) for all attributes and properties. diff --git a/docs/website/src/content/components/utilities/infotip.md b/docs/website/src/content/components/utilities/infotip.md new file mode 100644 index 0000000000..3bc618644e --- /dev/null +++ b/docs/website/src/content/components/utilities/infotip.md @@ -0,0 +1,76 @@ +--- +title: Infotip +layout: docs +eleventyNavigation: + key: Infotip + parent: Utilities +--- + +`` is an info icon button that reveals additional information in a popover. Use it to +offer optional, supporting context next to a label or piece of content — without cluttering the +interface. + +The content of the popover is slotted, so it can be plain text or richer markup. + +## Usage + +```html +This field requires a unique identifier used for account login. +``` + +## Examples + +### Basic + +Activate the icon button to toggle the popover. + +```html {.example .show-source} +This field requires a unique identifier used for account login. +``` + +### Custom icon + +Use the `icon` slot to replace the default info icon. + +```html {.example .show-source} + + + You can change the trigger icon through the icon slot. + +``` + +### Rich content + +The default slot accepts any markup, not just text. + +```html {.example .show-source} + + Password requirements +
    +
  • At least 8 characters
  • +
  • One uppercase letter
  • +
  • One number or special character
  • +
+
+``` + +### In a form label + +The infotip is designed to be used in the `infotip` slot of ``, so it sits neatly next to +the label text of a form field. + +```html + + + Username + A unique identifier used for account login. + + + +``` + +## API + +`` exposes a default slot for the popover content and an `icon` slot for the trigger +icon. See the [source on GitHub](https://github.com/sl-design-system/components/tree/main/packages/components/infotip) +for the full API. diff --git a/docs/website/src/content/components/utilities/listbox.md b/docs/website/src/content/components/utilities/listbox.md new file mode 100644 index 0000000000..15ebbcb095 --- /dev/null +++ b/docs/website/src/content/components/utilities/listbox.md @@ -0,0 +1,99 @@ +--- +title: Listbox +layout: docs +eleventyNavigation: + key: Listbox + parent: Utilities +--- + +`` is a scrollable container for a list of selectable options. It is a low-level +building block used by components such as select and combobox, but it can also be used on its own +wherever you need a list of options. + +Options are provided either as slotted `` (and ``) elements, or +declaratively through the `options` property for very large, data-driven lists. + +## Usage + +```html + + Option 1 + Option 2 + Option 3 + +``` + +## Examples + +### Basic + +Use `` for each item; add the `selected` attribute to mark the current selection and +`disabled` to make an option unavailable. + +```html {.example .show-source} + + Option 1 + Option 2 + Option 3 + +``` + +### Groups and dividers + +Group related options with ``, and separate options with a horizontal rule. + +```html {.example .show-source} + + + Option 1 + Option 2 + + I am not in a group +
+ + Option 3 + Option 4 + +
+``` + +### Emphasis + +Use the `emphasis` attribute (`subtle` or `bold`) to change how the selected option is highlighted. + +```html {.example .show-source} + + Option 1 + Option 2 + Option 3 + +``` + +### Large, data-driven lists + +For very large lists, pass an array to the `options` property instead of slotting elements. The +listbox virtualizes the options so only the visible ones are rendered. Use the `option*Path` +properties to tell the listbox how to read each item. + +```html + + + +``` + +## API + +`` works together with `` and ``. See the +[source on GitHub](https://github.com/sl-design-system/components/tree/main/packages/components/listbox) +for the full API. diff --git a/docs/website/src/content/components/utilities/scrollbar.md b/docs/website/src/content/components/utilities/scrollbar.md new file mode 100644 index 0000000000..33bc50b1d1 --- /dev/null +++ b/docs/website/src/content/components/utilities/scrollbar.md @@ -0,0 +1,60 @@ +--- +title: Scrollbar +layout: docs +eleventyNavigation: + key: Scrollbar + parent: Utilities +--- + +`` renders a custom, themable scrollbar that controls a separate scrolling container. +It is intended for components that need a scrollbar that looks consistent across browsers and +platforms, such as the grid. + +::: info +When in doubt, **always** prefer the native scrollbar. Only reach for `` when you +specifically need a custom one. +::: + +## Usage + +Point the scrollbar at the scroll container using the `scroller` attribute, which takes the `id` of +the element to control. The container itself usually hides its native scrollbar with +`scrollbar-width: none`. + +```html +
+
…wide content…
+
+ +``` + +## Examples + +### Horizontal + +By default the scrollbar is horizontal. Drag the thumb to scroll the linked container. + +```html {.example .show-source} +
+
+
+ +``` + +### Vertical + +Add the `vertical` attribute for a vertical scrollbar, and place it alongside a vertically +scrolling container. + +```html +
+
+
…tall content…
+
+ +
+``` + +## API + +See the [API reference](/api-reference/sl-scrollbar) for all attributes and properties. diff --git a/docs/website/src/content/components/utilities/utilities.md b/docs/website/src/content/components/utilities/utilities.md index 9ed921ed70..197d53ab70 100644 --- a/docs/website/src/content/components/utilities/utilities.md +++ b/docs/website/src/content/components/utilities/utilities.md @@ -8,4 +8,41 @@ eleventyNavigation: icon: wrench --- -Utilities components allow users to create flexible and reusable utility classes. +Utilities are small, focused helpers that solve a single, common problem. Some are purely +behavioural (like sending screen reader announcements), some format data (dates and numbers +according to a locale), and some are low-level building blocks used by other components (like the +listbox or virtual list). + +Unlike the larger components, utilities are usually combined with your own markup or other +components rather than used on their own. + +## Overview + +[Format date](/components/utilities/format-date) +: Format a date and/or time according to a locale, built on top of `Intl.DateTimeFormat`. + +[Format number](/components/utilities/format-number) +: Format a number, currency, percentage or unit according to a locale, built on top of + `Intl.NumberFormat`. + +[Ellipsize text](/components/utilities/ellipsize-text) +: Truncate text that overflows its container and reveal the full text in a tooltip. + +[Infotip](/components/utilities/infotip) +: An info icon button that reveals additional information in a popover. + +[Listbox](/components/utilities/listbox) +: A scrollable container of selectable options, used internally by components such as select and + combobox. + +[Virtual list](/components/utilities/virtual-list) +: Efficiently render very large lists by only rendering the items that are currently visible. + +[Scrollbar](/components/utilities/scrollbar) +: A custom, themable scrollbar for a separate scrolling container. + +[Announcer](/components/utilities/announcer) +: Send messages to assistive technology through an ARIA live region. + +[Emoji browser](/components/utilities/emoji-browser) +: A searchable emoji picker. diff --git a/docs/website/src/content/components/utilities/virtual-list.md b/docs/website/src/content/components/utilities/virtual-list.md new file mode 100644 index 0000000000..873b561855 --- /dev/null +++ b/docs/website/src/content/components/utilities/virtual-list.md @@ -0,0 +1,85 @@ +--- +title: Virtual list +layout: docs +eleventyNavigation: + key: Virtual list + parent: Utilities +--- + +`` efficiently renders very large lists by only rendering the items that are +currently visible (plus a small overscan buffer). This keeps the DOM small and scrolling smooth, +even with tens of thousands of items. + +You provide the data through the `items` property and a `renderItem` function that returns the +markup for a single item. + +## Usage + +Because `items` and `renderItem` are properties (not attributes), the virtual list is configured +from JavaScript. + +```html + + + +``` + +## Options + +estimateSize +: The estimated size (in pixels) of a single item. A good estimate improves the accuracy of the + scrollbar before items are measured. + +gap +: The gap, in pixels, between items. + +overscan +: The number of extra items to render outside the visible area, which makes fast scrolling smoother. + +## Scrolling programmatically + +Use the `scrollToIndex` method to jump to a specific item. + +```js +document.querySelector('sl-virtual-list').scrollToIndex(5000, { align: 'start' }); +``` + +## Styling + +Items are exposed through the `item` CSS part (and any parts you add in `renderItem`), so you can +style them from the outside. + +```css +sl-virtual-list::part(item) { + align-items: center; + border-block-end: 1px solid var(--sl-color-border-plain); + display: flex; + gap: var(--sl-size-100); + padding: var(--sl-size-150); +} +``` + +## API + +`` is configured through the `items`, `renderItem`, `estimateSize`, `gap` and +`overscan` properties. See the +[source on GitHub](https://github.com/sl-design-system/components/tree/main/packages/components/virtual-list) +for the full API. diff --git a/docs/website/src/js/main.js b/docs/website/src/js/main.js index 0f7a09a26b..7ed6fa1b02 100644 --- a/docs/website/src/js/main.js +++ b/docs/website/src/js/main.js @@ -1,4 +1,5 @@ import '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js'; +import 'invokers-polyfill'; import './theme.js'; import './icons.js'; import { @@ -35,20 +36,56 @@ import { faBold as fasBold, faGear as fasGear, faItalic as fasItalic, + faMoonStars as fasMoonStars, + faSunBright as fasSunBright, faUnderline as fasUnderline } from '@fortawesome/pro-solid-svg-icons'; +import '@sl-design-system/accordion/register.js'; import '@sl-design-system/button/register.js'; import '@sl-design-system/button-bar/register.js'; import '@sl-design-system/badge/register.js'; +import '@sl-design-system/breadcrumbs/register.js'; import '@sl-design-system/callout/register.js'; +import '@sl-design-system/card/register.js'; +import '@sl-design-system/checkbox/register.js'; +import '@sl-design-system/combobox/register.js'; +import '@sl-design-system/date-field/register.js'; import '@sl-design-system/dialog/register.js'; +import '@sl-design-system/drawer/register.js'; +import '@sl-design-system/editor/register.js'; +import '@sl-design-system/ellipsize-text/register.js'; +import '@sl-design-system/form/register.js'; +import '@sl-design-system/format-date/register.js'; +import '@sl-design-system/format-number/register.js'; import '@sl-design-system/icon/register.js'; import { Icon } from '@sl-design-system/icon'; +import '@sl-design-system/infotip/register.js'; +import '@sl-design-system/inline-message/register.js'; +import '@sl-design-system/listbox/register.js'; import '@sl-design-system/menu/register.js'; +import '@sl-design-system/message-dialog/register.js'; +import '@sl-design-system/number-field/register.js'; +import '@sl-design-system/paginator/register.js'; +import '@sl-design-system/panel/register.js'; +import '@sl-design-system/popover/register.js'; +import '@sl-design-system/progress-bar/register.js'; +import '@sl-design-system/radio-group/register.js'; +import '@sl-design-system/scrollbar/register.js'; +import '@sl-design-system/search-field/register.js'; +import '@sl-design-system/select/register.js'; +import '@sl-design-system/skeleton/register.js'; +import '@sl-design-system/spinner/register.js'; import '@sl-design-system/switch/register.js'; +import '@sl-design-system/tabs/register.js'; +import '@sl-design-system/tag/register.js'; +import '@sl-design-system/text-area/register.js'; +import '@sl-design-system/text-field/register.js'; +import '@sl-design-system/time-field/register.js'; +import '@sl-design-system/tree/register.js'; import '@sl-design-system/toggle-button/register.js'; import '@sl-design-system/toggle-group/register.js'; import '@sl-design-system/tooltip/register.js'; +import { setup } from '@sl-design-system/sanoma-learning'; import { Code } from '@sl-design-system/doc-components/code/code.js'; import { Code as CodeBlock } from '@sl-design-system/doc-components/code-block/code-block.js'; import { CodeExample } from '@sl-design-system/doc-components/code-example/code-example.js'; @@ -62,6 +99,9 @@ import { NavGroup } from '@sl-design-system/doc-components/site-nav/nav-group.js import { NavItem } from '@sl-design-system/doc-components/site-nav/nav-item.js'; import { SiteNav } from '@sl-design-system/doc-components/site-nav/site-nav.js'; +// Register the theme's system icons (e.g. the infotip's `info` icon) and config. +setup(); + // Icons used in the documentation templates Icon.register(faBug, faCircleExclamation, faCode, faFileLines); @@ -94,6 +134,8 @@ Icon.register( fasBold, fasGear, fasItalic, + fasMoonStars, + fasSunBright, fasUnderline ); From 04fd7a4dd8f5cdd9ce7ad76c24a80e0196ecd882 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 15 Jun 2026 10:13:01 +0200 Subject: [PATCH 65/72] =?UTF-8?q?=F0=9F=8E=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/package.json | 1 + .../src/command-palette/command-palette.css | 154 +++++++++++ .../command-palette/command-palette.spec.ts | 199 ++++++++++++++ .../command-palette.stories.ts | 55 ++++ .../src/command-palette/command-palette.ts | 256 ++++++++++++++++++ docs/components/tsdown.config.ts | 10 +- docs/website/package.json | 2 +- package.json | 2 +- yarn.lock | 60 ++-- 9 files changed, 703 insertions(+), 36 deletions(-) create mode 100644 docs/components/src/command-palette/command-palette.css create mode 100644 docs/components/src/command-palette/command-palette.spec.ts create mode 100644 docs/components/src/command-palette/command-palette.stories.ts create mode 100644 docs/components/src/command-palette/command-palette.ts diff --git a/docs/components/package.json b/docs/components/package.json index 742e0e1cc6..e45c4740ea 100644 --- a/docs/components/package.json +++ b/docs/components/package.json @@ -13,6 +13,7 @@ "./code-block/code-block.js": "./dist/code-block/code-block.js", "./code-example/code-example.js": "./dist/code-example/code-example.js", "./code/code.js": "./dist/code/code.js", + "./command-palette/command-palette.js": "./dist/command-palette/command-palette.js", "./copy-button/copy-button.js": "./dist/copy-button/copy-button.js", "./heading/heading.js": "./dist/heading/heading.js", "./install-info/install-info.js": "./dist/install-info/install-info.js", diff --git a/docs/components/src/command-palette/command-palette.css b/docs/components/src/command-palette/command-palette.css new file mode 100644 index 0000000000..cc633a48a1 --- /dev/null +++ b/docs/components/src/command-palette/command-palette.css @@ -0,0 +1,154 @@ +/* stylelint-disable custom-property-pattern */ + +:host { + display: contents; +} + +dialog { + background: var(--sl-elevation-surface-raised-default); + border: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + border-radius: var(--sl-size-borderRadius-lg); + box-shadow: var(--sl-elevation-shadow-300, 0 16px 40px rgb(0 0 0 / 18%)); + flex-direction: column; + inline-size: min(580px, calc(100dvw - var(--sl-size-600))); + margin: 15vh auto auto; + max-block-size: 70dvh; + overflow: visible; + padding: 0; + + &[open] { + display: flex; + + &::backdrop { + background: var(--sl-color-blanket-plain); + opacity: 1; + + @starting-style { + opacity: 0; + } + } + } + + &::backdrop { + opacity: 0; + transition: opacity 0.2s ease-in-out; + transition-behavior: allow-discrete; + } +} + +.search { + align-items: center; + border-block-end: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); + display: flex; + flex: none; + gap: var(--sl-size-150); + padding: var(--sl-size-200) var(--sl-size-250); + + sl-icon { + color: var(--sl-color-foreground-subtlest); + flex: none; + font-size: var(--sl-size-225, 18px); + } + + input { + appearance: none; + background: transparent; + border: 0; + color: var(--sl-color-foreground-plain); + flex: 1; + font: var(--sl-text-body-lg, inherit); + margin: 0; + min-inline-size: 0; + outline: none; + padding: 0; + + &::placeholder { + color: var(--sl-color-foreground-subtlest); + } + } +} + +.message { + color: var(--sl-color-foreground-subtle); + font: var(--sl-text-body-md); + margin: 0; + padding: var(--sl-size-300) var(--sl-size-250); + text-align: center; +} + +.results { + display: flex; + flex-direction: column; + gap: var(--sl-size-025, 2px); + list-style: none; + margin: 0; + overflow-y: auto; + padding: var(--sl-size-150); + + li { + align-items: center; + border-radius: var(--sl-size-borderRadius-default); + color: var(--sl-color-foreground-plain); + cursor: pointer; + display: flex; + font: var(--sl-text-body-md); + gap: var(--sl-size-150); + padding: var(--sl-size-150) var(--sl-size-200); + + &.active { + background: var( + --sl-color-background-input-interactive, + var(--sl-color-background-accent-blue-subtlest) + ); + } + + sl-icon { + color: var(--sl-color-foreground-subtle); + flex: none; + font-size: var(--sl-size-200); + } + + .label { + flex: 1; + min-inline-size: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.legend { + align-items: center; + border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-subtlest); + color: var(--sl-color-foreground-subtlest); + display: flex; + flex: none; + font: var(--sl-text-body-sm); + gap: var(--sl-size-250); + padding: var(--sl-size-150) var(--sl-size-250); + + span { + align-items: center; + display: inline-flex; + gap: var(--sl-size-075, 6px); + } + + kbd { + align-items: center; + background: var(--sl-elevation-surface-raised-sunken); + border: var(--sl-size-borderWidth-default) solid var(--sl-color-border-default); + border-radius: var(--sl-size-borderRadius-sm); + color: var(--sl-color-foreground-subtle); + display: inline-flex; + font: var(--sl-text-body-sm); + justify-content: center; + line-height: 1; + min-inline-size: var(--sl-size-225, 18px); + padding: var(--sl-size-050) var(--sl-size-075, 6px); + + sl-icon { + font-size: var(--sl-size-150); + } + } +} diff --git a/docs/components/src/command-palette/command-palette.spec.ts b/docs/components/src/command-palette/command-palette.spec.ts new file mode 100644 index 0000000000..7774fd9986 --- /dev/null +++ b/docs/components/src/command-palette/command-palette.spec.ts @@ -0,0 +1,199 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { type Command } from './command-palette.js'; + +// Use dynamic import from dist to avoid CSS module resolution issues in browser tests + +const { CommandPalette: CommandPaletteClass } = + await import('@sl-design-system/doc-components/command-palette/command-palette.js'); + +try { + customElements.define('doc-command-palette', CommandPaletteClass); +} catch { + /* empty */ +} + +const commands = [ + { id: 'home', label: 'Go to home', keywords: ['start'] }, + { id: 'components', label: 'Browse components' }, + { id: 'tokens', label: 'Design tokens' } +]; + +describe('doc-command-palette', () => { + let el: InstanceType; + + beforeEach(async () => { + el = await fixture(html``); + }); + + describe('dialog', () => { + it('should render a dialog', () => { + expect(el.renderRoot.querySelector('dialog')).to.exist; + }); + + it('should not be open by default', () => { + expect(el.renderRoot.querySelector('dialog')!.open).to.be.false; + }); + + it('should use a plain input instead of sl-search-field', () => { + expect(el.renderRoot.querySelector('input')).to.exist; + expect(el.renderRoot.querySelector('sl-search-field')).not.to.exist; + }); + + it('should render a legend with keyboard hints', () => { + const legend = el.renderRoot.querySelector('.legend'); + + expect(legend).to.exist; + expect(legend!.querySelectorAll('kbd').length).to.be.greaterThan(0); + }); + + it('should open when show() is called and focus the input', async () => { + el.show(); + + expect(el.dialog!.open).to.be.true; + + await new Promise(resolve => requestAnimationFrame(resolve)); + + expect(el.renderRoot.activeElement).to.equal(el.input); + }); + + it('should close when close() is called', () => { + el.show(); + expect(el.dialog!.open).to.be.true; + + el.close(); + + expect(el.dialog!.open).to.be.false; + }); + }); + + describe('commands', () => { + it('should render all commands by default', () => { + expect(el.renderRoot.querySelectorAll('.results li').length).to.equal(commands.length); + }); + + it('should highlight the first command when opened', () => { + el.show(); + + expect(el.activeIndex).to.equal(0); + }); + + it('should filter commands by the query', async () => { + el.query = 'browse'; + await el.updateComplete; + + const items = el.renderRoot.querySelectorAll('.results li'); + + expect(items.length).to.equal(1); + expect(items[0].textContent?.trim()).to.equal('Browse components'); + }); + + it('should match against keywords', async () => { + el.query = 'start'; + await el.updateComplete; + + const items = el.renderRoot.querySelectorAll('.results li'); + + expect(items.length).to.equal(1); + expect(items[0].textContent?.trim()).to.equal('Go to home'); + }); + + it('should show a message when there are no results', async () => { + el.query = 'nonexistent'; + await el.updateComplete; + + expect(el.renderRoot.querySelector('.message')).to.exist; + expect(el.renderRoot.querySelectorAll('.results li').length).to.equal(0); + }); + + it('should emit doc-command-select when a command is clicked', () => { + const onSelect = vi.fn<(event: CustomEvent) => void>(); + el.addEventListener('doc-command-select', onSelect); + + el.show(); + el.renderRoot.querySelector('.results li')!.click(); + + expect(onSelect).toHaveBeenCalledOnce(); + expect(onSelect.mock.calls[0][0].detail.id).to.equal('home'); + }); + }); + + describe('keyboard', () => { + it('should open on Cmd+K', () => { + const showModalSpy = vi.spyOn(el.dialog!, 'showModal'); + + document.dispatchEvent( + new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true }) + ); + + expect(showModalSpy).toHaveBeenCalled(); + }); + + it('should open on Ctrl+K', () => { + const showModalSpy = vi.spyOn(el.dialog!, 'showModal'); + + document.dispatchEvent( + new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }) + ); + + expect(showModalSpy).toHaveBeenCalled(); + }); + + it('should move the active index with the arrow keys', async () => { + el.show(); + expect(el.activeIndex).to.equal(0); + + el.dialog!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + await el.updateComplete; + expect(el.activeIndex).to.equal(1); + + el.dialog!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); + await el.updateComplete; + expect(el.activeIndex).to.equal(0); + }); + + it('should wrap around when navigating past the ends', async () => { + el.show(); + + el.dialog!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); + await el.updateComplete; + + expect(el.activeIndex).to.equal(commands.length - 1); + }); + + it('should select the active command on Enter', () => { + const onSelect = vi.fn<(event: CustomEvent) => void>(); + el.addEventListener('doc-command-select', onSelect); + + el.show(); + el.dialog!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + + expect(onSelect).toHaveBeenCalledOnce(); + expect(onSelect.mock.calls[0][0].detail.id).to.equal('home'); + }); + + it('should close on Escape', () => { + el.show(); + expect(el.dialog!.open).to.be.true; + + el.dialog!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); + + expect(el.dialog!.open).to.be.false; + }); + }); + + describe('cleanup', () => { + it('should remove the keydown listener on disconnect', () => { + const showModalSpy = vi.spyOn(el.dialog!, 'showModal'); + + el.remove(); + + document.dispatchEvent( + new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true }) + ); + + expect(showModalSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/docs/components/src/command-palette/command-palette.stories.ts b/docs/components/src/command-palette/command-palette.stories.ts new file mode 100644 index 0000000000..698c886457 --- /dev/null +++ b/docs/components/src/command-palette/command-palette.stories.ts @@ -0,0 +1,55 @@ +import { + faClockRotateLeft, + faCodeBranch, + faHouse, + faMoon, + faPalette, + faShapes +} from '@fortawesome/pro-regular-svg-icons'; +import { Icon } from '@sl-design-system/icon'; +import { type Meta, type StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import { type Command, CommandPalette } from './command-palette.js'; + +type Story = StoryObj; + +Icon.register(faClockRotateLeft, faCodeBranch, faHouse, faMoon, faPalette, faShapes); + +try { + customElements.define('doc-command-palette', CommandPalette); +} catch { + /* empty */ +} + +const commands: Command[] = [ + { id: 'home', label: 'Go to home', icon: 'far-house', keywords: ['start', 'index'] }, + { id: 'components', label: 'Browse components', icon: 'far-shapes' }, + { id: 'tokens', label: 'Design tokens', icon: 'far-palette' }, + { id: 'theme', label: 'Toggle theme', icon: 'far-moon', keywords: ['dark', 'light'] }, + { + id: 'github', + label: 'Open on GitHub', + icon: 'far-code-branch', + keywords: ['repository', 'source'] + }, + { + id: 'changelog', + label: 'View changelog', + icon: 'far-clock-rotate-left', + keywords: ['releases'] + } +]; + +export default { + title: 'Command palette', + render: () => html` + + + ` +} satisfies Meta; + +export const Basic: Story = {}; diff --git a/docs/components/src/command-palette/command-palette.ts b/docs/components/src/command-palette/command-palette.ts new file mode 100644 index 0000000000..6bf8a9d285 --- /dev/null +++ b/docs/components/src/command-palette/command-palette.ts @@ -0,0 +1,256 @@ +import { faArrowTurnDownLeft, faMagnifyingGlass } from '@fortawesome/pro-regular-svg-icons'; +import { + type ScopedElementsMap, + ScopedElementsMixin +} from '@open-wc/scoped-elements/lit-element.js'; +import { Icon } from '@sl-design-system/icon'; +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; +import styles from './command-palette.css' with { type: 'css' }; + +Icon.register(faArrowTurnDownLeft, faMagnifyingGlass); + +/** A single command shown in the palette. */ +export interface Command { + /** Unique identifier, also used as the DOM id of the option. */ + id: string; + + /** The text shown for the command. */ + label: string; + + /** Optional icon name (e.g. `far-file-lines`) shown before the label. */ + icon?: string; + + /** Optional extra terms used when filtering, in addition to the label. */ + keywords?: string[]; + + /** Optional URL navigated to when the command is selected. */ + url?: string; +} + +/** Fired when a command is selected. */ +export type DocCommandSelectEvent = CustomEvent; + +/** + * A visually lightweight command palette modal. Type to filter a list of commands, navigate with + * the arrow keys and select with Enter. A legend with keyboard hints is shown at the bottom. + * + * @fires doc-command-select - Fired with the selected command. + */ +export class CommandPalette extends ScopedElementsMixin(LitElement) { + /** @internal */ + static get scopedElements(): ScopedElementsMap { + return { + 'sl-icon': Icon + }; + } + + /** @internal */ + static styles: CSSResultGroup = styles; + + @query('dialog') dialog?: HTMLDialogElement; + + @query('input') input?: HTMLInputElement; + + /** The commands available in the palette. */ + @property({ type: Array }) commands: Command[] = []; + + /** Index of the currently highlighted command, or -1 if none. */ + @state() activeIndex = -1; + + /** The current query string. */ + @state() query = ''; + + override connectedCallback(): void { + super.connectedCallback(); + + document.addEventListener('keydown', this.#onKeydown); + } + + override disconnectedCallback(): void { + document.removeEventListener('keydown', this.#onKeydown); + + super.disconnectedCallback(); + } + + render(): TemplateResult { + const results = this.#filter(); + + return html` + + + + ${this.#renderResults(results)} + +
+ to navigate + to select + esc to close +
+
+ `; + } + + #renderResults(results: Command[]): TemplateResult { + if (results.length === 0) { + return html` +

No results found${this.query ? html`for “${this.query}”` : nothing}.

+ `; + } + + return html` +
    + ${results.map( + (command, index) => html` +
  • this.#select(command)} + @pointermove=${() => (this.activeIndex = index)}> + ${command.icon ? html`` : nothing} + ${command.label} +
  • + ` + )} +
+ `; + } + + /** Opens the palette and focuses the input. */ + show(): void { + this.dialog?.showModal(); + this.query = ''; + this.activeIndex = this.commands.length > 0 ? 0 : -1; + + requestAnimationFrame(() => this.input?.focus()); + } + + /** Closes the palette. */ + close(): void { + this.dialog?.close(); + } + + #onBackdropClick(event: MouseEvent): void { + const dialog = this.dialog; + + if (!dialog || dialog !== event.composedPath()[0]) { + return; + } + + const rect = dialog.getBoundingClientRect(); + + if ( + event.clientY < rect.top || + event.clientY > rect.bottom || + event.clientX < rect.left || + event.clientX > rect.right + ) { + dialog.close(); + } + } + + #onDialogKeydown(event: KeyboardEvent): void { + if (event.key === 'Escape') { + event.preventDefault(); + this.close(); + return; + } + + const results = this.#filter(); + + if (results.length === 0) { + return; + } + + if (event.key === 'ArrowDown') { + event.preventDefault(); + this.activeIndex = this.activeIndex < results.length - 1 ? this.activeIndex + 1 : 0; + this.#scrollActiveIntoView(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + this.activeIndex = this.activeIndex > 0 ? this.activeIndex - 1 : results.length - 1; + this.#scrollActiveIntoView(); + } else if (event.key === 'Enter' && this.activeIndex >= 0) { + event.preventDefault(); + + const command = results[this.activeIndex]; + if (command) { + this.#select(command); + } + } + } + + #onInput(event: Event): void { + this.query = (event.target as HTMLInputElement).value; + this.activeIndex = this.#filter().length > 0 ? 0 : -1; + } + + #onKeydown = (event: KeyboardEvent): void => { + if (event.key === 'k' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + this.show(); + } + }; + + /** Filters the commands by the current query. */ + #filter(): Command[] { + const query = this.query.trim().toLowerCase(); + + if (!query) { + return this.commands; + } + + return this.commands.filter(command => { + const haystack = [command.label, ...(command.keywords ?? [])].join(' ').toLowerCase(); + + return haystack.includes(query); + }); + } + + #select(command: Command): void { + this.close(); + + this.dispatchEvent( + new CustomEvent('doc-command-select', { + bubbles: true, + composed: true, + detail: command + }) + ); + + if (command.url) { + window.location.href = command.url; + } + } + + #scrollActiveIntoView(): void { + const items = this.renderRoot.querySelectorAll('.results li'); + + items[this.activeIndex]?.scrollIntoView({ block: 'nearest' }); + } +} diff --git a/docs/components/tsdown.config.ts b/docs/components/tsdown.config.ts index 6de7670b60..82107196b3 100644 --- a/docs/components/tsdown.config.ts +++ b/docs/components/tsdown.config.ts @@ -1,13 +1,14 @@ import { readFileSync } from 'node:fs'; import { URL } from 'node:url'; -import { defineConfig } from 'tsdown' +import { defineConfig } from 'tsdown'; // This plugin handles CSS imports with { type: 'css' } and converts them to CSSStyleSheet const cssPlugin = { name: 'css-stylesheet', transform(code, id) { // Check if this is a CSS import with type: 'css' attribute - const cssImportRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+\.css)['"]\s+with\s+\{\s*type:\s*['"]css['"]\s*\}/g; + const cssImportRegex = + /import\s+(\w+)\s+from\s+['"]([^'"]+\.css)['"]\s+with\s+\{\s*type:\s*['"]css['"]\s*\}/g; let transformedCode = code, hasTransformations = false; @@ -43,12 +44,13 @@ export default defineConfig({ onlyBundle: false }, dts: { - tsgo: true, + tsgo: true }, entry: [ 'src/code/code.ts', 'src/code-block/code-block.ts', 'src/code-example/code-example.ts', + 'src/command-palette/command-palette.ts', 'src/copy-button/copy-button.ts', 'src/heading/heading.ts', 'src/install-info/install-info.ts', @@ -78,4 +80,4 @@ export default defineConfig({ hash: false, platform: 'browser', plugins: [cssPlugin] -}) +}); diff --git a/docs/website/package.json b/docs/website/package.json index 388a0d3764..061cae3d36 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -32,7 +32,7 @@ "@11ty/eleventy": "^4.0.0-alpha.7", "@11ty/eleventy-navigation": "^1.0.5", "@fortawesome/pro-regular-svg-icons": "^7.2.0", - "@pwrs/cem": "^0.9.19", + "@pwrs/cem": "^0.10.6", "@webcomponents/scoped-custom-element-registry": "^0.0.10", "esbuild": "^0.25.0", "lit": "^3.3.2", diff --git a/package.json b/package.json index 3cdd74a5ad..4967d6dccd 100644 --- a/package.json +++ b/package.json @@ -398,7 +398,7 @@ "@faker-js/faker": "^10.4.0", "@lit/localize-tools": "^0.8.1", "@playwright/test": "^1.60.0", - "@pwrs/cem": "^0.9.18", + "@pwrs/cem": "^0.10.6", "@storybook/addon-a11y": "^10.4.1", "@storybook/addon-docs": "^10.4.1", "@storybook/addon-vitest": "^10.4.1", diff --git a/yarn.lock b/yarn.lock index e4795b4eb8..5163f545f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5130,58 +5130,58 @@ __metadata: languageName: node linkType: hard -"@pwrs/cem-darwin-arm64@npm:0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem-darwin-arm64@npm:0.9.19" +"@pwrs/cem-darwin-arm64@npm:0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem-darwin-arm64@npm:0.10.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@pwrs/cem-darwin-x64@npm:0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem-darwin-x64@npm:0.9.19" +"@pwrs/cem-darwin-x64@npm:0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem-darwin-x64@npm:0.10.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@pwrs/cem-linux-arm64@npm:0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem-linux-arm64@npm:0.9.19" +"@pwrs/cem-linux-arm64@npm:0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem-linux-arm64@npm:0.10.6" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@pwrs/cem-linux-x64@npm:0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem-linux-x64@npm:0.9.19" +"@pwrs/cem-linux-x64@npm:0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem-linux-x64@npm:0.10.6" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@pwrs/cem-win32-arm64@npm:0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem-win32-arm64@npm:0.9.19" +"@pwrs/cem-win32-arm64@npm:0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem-win32-arm64@npm:0.10.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@pwrs/cem-win32-x64@npm:0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem-win32-x64@npm:0.9.19" +"@pwrs/cem-win32-x64@npm:0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem-win32-x64@npm:0.10.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@pwrs/cem@npm:^0.9.18, @pwrs/cem@npm:^0.9.19": - version: 0.9.19 - resolution: "@pwrs/cem@npm:0.9.19" +"@pwrs/cem@npm:^0.10.6": + version: 0.10.6 + resolution: "@pwrs/cem@npm:0.10.6" dependencies: - "@pwrs/cem-darwin-arm64": "npm:0.9.19" - "@pwrs/cem-darwin-x64": "npm:0.9.19" - "@pwrs/cem-linux-arm64": "npm:0.9.19" - "@pwrs/cem-linux-x64": "npm:0.9.19" - "@pwrs/cem-win32-arm64": "npm:0.9.19" - "@pwrs/cem-win32-x64": "npm:0.9.19" + "@pwrs/cem-darwin-arm64": "npm:0.10.6" + "@pwrs/cem-darwin-x64": "npm:0.10.6" + "@pwrs/cem-linux-arm64": "npm:0.10.6" + "@pwrs/cem-linux-x64": "npm:0.10.6" + "@pwrs/cem-win32-arm64": "npm:0.10.6" + "@pwrs/cem-win32-x64": "npm:0.10.6" dependenciesMeta: "@pwrs/cem-darwin-arm64": optional: true @@ -5197,7 +5197,7 @@ __metadata: optional: true bin: cem: bin/cem.js - checksum: 10c0/4dc5bb9e43ce000058da108a4b78e36f169ab041ec2a3e3476279087965538f46c4384197f01b130869b068bbebf00d998e1ec356fb161def5e597c93ec5b957 + checksum: 10c0/1d0464850f0a4a4d826b3d84c7c5f017a39b1c278ebfe81ae73aab12faad985772ff2bcc76a134d6830dde1b20b94670cab9fd20f25c4b698adf08e0ba9d946b languageName: node linkType: hard @@ -6581,7 +6581,7 @@ __metadata: "@faker-js/faker": "npm:^10.4.0" "@lit/localize-tools": "npm:^0.8.1" "@playwright/test": "npm:^1.60.0" - "@pwrs/cem": "npm:^0.9.18" + "@pwrs/cem": "npm:^0.10.6" "@storybook/addon-a11y": "npm:^10.4.1" "@storybook/addon-docs": "npm:^10.4.1" "@storybook/addon-vitest": "npm:^10.4.1" @@ -7084,7 +7084,7 @@ __metadata: "@11ty/eleventy": "npm:^4.0.0-alpha.7" "@11ty/eleventy-navigation": "npm:^1.0.5" "@fortawesome/pro-regular-svg-icons": "npm:^7.2.0" - "@pwrs/cem": "npm:^0.9.19" + "@pwrs/cem": "npm:^0.10.6" "@webcomponents/scoped-custom-element-registry": "npm:^0.0.10" esbuild: "npm:^0.25.0" lit: "npm:^3.3.2" From 6d08290bb86a2767fab7d2d558b88748789a5afd Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 15 Jun 2026 11:38:31 +0200 Subject: [PATCH 66/72] =?UTF-8?q?=F0=9F=8C=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/command-palette/command-palette.css | 4 +- .../src/command-palette/command-palette.ts | 8 ++-- docs/components/tsdown.config.ts | 41 +------------------ 3 files changed, 8 insertions(+), 45 deletions(-) diff --git a/docs/components/src/command-palette/command-palette.css b/docs/components/src/command-palette/command-palette.css index cc633a48a1..45aaa2383f 100644 --- a/docs/components/src/command-palette/command-palette.css +++ b/docs/components/src/command-palette/command-palette.css @@ -118,9 +118,9 @@ dialog { } } -.legend { +footer { align-items: center; - border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-subtlest); + border-block-start: var(--sl-size-borderWidth-default) solid var(--sl-color-border-plain); color: var(--sl-color-foreground-subtlest); display: flex; flex: none; diff --git a/docs/components/src/command-palette/command-palette.ts b/docs/components/src/command-palette/command-palette.ts index 6bf8a9d285..62ac03853b 100644 --- a/docs/components/src/command-palette/command-palette.ts +++ b/docs/components/src/command-palette/command-palette.ts @@ -103,11 +103,11 @@ export class CommandPalette extends ScopedElementsMixin(LitElement) { ${this.#renderResults(results)} -