@lar-works/design-system 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,5 +21,8 @@ import { PlusIcon } from '@lar-works/design-system/icons';
21
21
  </DesignSystemProvider>;
22
22
  ```
23
23
 
24
+ A website that uses the marketing showcase (`Hero`, device frames, `ServerUnit`…) imports
25
+ `@lar-works/design-system/showcase.css` instead of `styles.css`.
26
+
24
27
  Apps compose these components and never style anything themselves; `@lar-works/config`'s lint rules enforce it.
25
28
  `@lar-works/design-system/testing` has helpers for apps' jsdom test setup.
@@ -1,9 +1,9 @@
1
1
  export type ThemePreference = 'system' | 'light' | 'dark';
2
2
  export interface ThemeController {
3
3
  /** Applies the stored preference before the first render, so the page never flashes the other theme. */
4
- initialize(): void;
4
+ initialize: () => void;
5
5
  /** The current preference and a setter that stores and applies it, kept in step across tabs. */
6
- useTheme(): readonly [ThemePreference, (value: ThemePreference) => void];
6
+ useTheme: () => readonly [ThemePreference, (value: ThemePreference) => void];
7
7
  }
8
8
  export declare function isThemePreference(value: string): value is ThemePreference;
9
9
  /**
@@ -38,9 +38,7 @@ function createThemeController(storageKey) {
38
38
  }
39
39
  return {
40
40
  initialize: () => applyThemePreference(read()),
41
- useTheme() {
42
- return [useSyncExternalStore(subscribe, read, () => "system"), set];
43
- }
41
+ useTheme: () => [useSyncExternalStore(subscribe, read, () => "system"), set]
44
42
  };
45
43
  }
46
44
  function applyThemePreference(preference) {
@@ -1 +1 @@
1
- {"version":3,"file":"Theme.js","names":[],"sources":["../../src/foundations/Theme.ts"],"sourcesContent":["import { useSyncExternalStore } from 'react';\n\nexport type ThemePreference = 'system' | 'light' | 'dark';\n\nexport interface ThemeController {\n /** Applies the stored preference before the first render, so the page never flashes the other theme. */\n initialize(): void;\n /** The current preference and a setter that stores and applies it, kept in step across tabs. */\n useTheme(): readonly [ThemePreference, (value: ThemePreference) => void];\n}\n\nexport function isThemePreference(value: string): value is ThemePreference {\n return value === 'system' || value === 'light' || value === 'dark';\n}\n\n/** The page backgrounds, which Safari and installed web apps extend into their own chrome. */\nconst themeColors = { light: '#fcfcfc', dark: '#0a0a0a' } as const;\n\n/**\n * The light/dark preference of one app, stored under `storageKey` in localStorage. The preference sets `.light` or\n * `.dark` on <html> (nothing follows the system) and points the page's `theme-color` metas at the matching background.\n */\nexport function createThemeController(storageKey: string): ThemeController {\n const changeEvent = `${storageKey}-change`;\n\n function read(): ThemePreference {\n const stored = window.localStorage.getItem(storageKey);\n\n return stored && isThemePreference(stored) ? stored : 'system';\n }\n\n function set(preference: ThemePreference): void {\n window.localStorage.setItem(storageKey, preference);\n applyThemePreference(preference);\n window.dispatchEvent(new Event(changeEvent));\n }\n\n function subscribe(onChange: () => void): () => void {\n function handleStorageChange(event: StorageEvent): void {\n if (event.key !== storageKey) return;\n\n applyThemePreference(read());\n onChange();\n }\n\n window.addEventListener(changeEvent, onChange);\n window.addEventListener('storage', handleStorageChange);\n\n return () => {\n window.removeEventListener(changeEvent, onChange);\n window.removeEventListener('storage', handleStorageChange);\n };\n }\n\n return {\n initialize: () => applyThemePreference(read()),\n useTheme() {\n const preference = useSyncExternalStore(subscribe, read, () => 'system' as const);\n\n return [preference, set] as const;\n },\n };\n}\n\nfunction applyThemePreference(preference: ThemePreference): void {\n document.documentElement.classList.remove('light', 'dark');\n\n if (preference !== 'system') document.documentElement.classList.add(preference);\n\n for (const meta of document.querySelectorAll<HTMLMetaElement>('meta[name=\"theme-color\"]')) {\n const scheme = meta.getAttribute('media')?.includes('dark') ? 'dark' : 'light';\n\n meta.content = themeColors[preference === 'system' ? scheme : preference];\n }\n}\n"],"mappings":";;AAWA,SAAgB,kBAAkB,OAAyC;CACvE,OAAO,UAAU,YAAY,UAAU,WAAW,UAAU;AAChE;;AAGA,MAAM,cAAc;CAAE,OAAO;CAAW,MAAM;AAAU;;;;;AAMxD,SAAgB,sBAAsB,YAAqC;CACvE,MAAM,cAAc,GAAG,WAAW;CAElC,SAAS,OAAwB;EAC7B,MAAM,SAAS,OAAO,aAAa,QAAQ,UAAU;EAErD,OAAO,UAAU,kBAAkB,MAAM,IAAI,SAAS;CAC1D;CAEA,SAAS,IAAI,YAAmC;EAC5C,OAAO,aAAa,QAAQ,YAAY,UAAU;EAClD,qBAAqB,UAAU;EAC/B,OAAO,cAAc,IAAI,MAAM,WAAW,CAAC;CAC/C;CAEA,SAAS,UAAU,UAAkC;EACjD,SAAS,oBAAoB,OAA2B;GACpD,IAAI,MAAM,QAAQ,YAAY;GAE9B,qBAAqB,KAAK,CAAC;GAC3B,SAAS;EACb;EAEA,OAAO,iBAAiB,aAAa,QAAQ;EAC7C,OAAO,iBAAiB,WAAW,mBAAmB;EAEtD,aAAa;GACT,OAAO,oBAAoB,aAAa,QAAQ;GAChD,OAAO,oBAAoB,WAAW,mBAAmB;EAC7D;CACJ;CAEA,OAAO;EACH,kBAAkB,qBAAqB,KAAK,CAAC;EAC7C,WAAW;GAGP,OAAO,CAFY,qBAAqB,WAAW,YAAY,QAE9C,GAAG,GAAG;EAC3B;CACJ;AACJ;AAEA,SAAS,qBAAqB,YAAmC;CAC7D,SAAS,gBAAgB,UAAU,OAAO,SAAS,MAAM;CAEzD,IAAI,eAAe,UAAU,SAAS,gBAAgB,UAAU,IAAI,UAAU;CAE9E,KAAK,MAAM,QAAQ,SAAS,iBAAkC,4BAA0B,GAAG;EACvF,MAAM,SAAS,KAAK,aAAa,OAAO,CAAC,EAAE,SAAS,MAAM,IAAI,SAAS;EAEvE,KAAK,UAAU,YAAY,eAAe,WAAW,SAAS;CAClE;AACJ"}
1
+ {"version":3,"file":"Theme.js","names":[],"sources":["../../src/foundations/Theme.ts"],"sourcesContent":["import { useSyncExternalStore } from 'react';\n\nexport type ThemePreference = 'system' | 'light' | 'dark';\n\nexport interface ThemeController {\n /** Applies the stored preference before the first render, so the page never flashes the other theme. */\n initialize: () => void;\n /** The current preference and a setter that stores and applies it, kept in step across tabs. */\n useTheme: () => readonly [ThemePreference, (value: ThemePreference) => void];\n}\n\nexport function isThemePreference(value: string): value is ThemePreference {\n return value === 'system' || value === 'light' || value === 'dark';\n}\n\n/** The page backgrounds, which Safari and installed web apps extend into their own chrome. */\nconst themeColors = { light: '#fcfcfc', dark: '#0a0a0a' } as const;\n\n/**\n * The light/dark preference of one app, stored under `storageKey` in localStorage. The preference sets `.light` or\n * `.dark` on <html> (nothing follows the system) and points the page's `theme-color` metas at the matching background.\n */\nexport function createThemeController(storageKey: string): ThemeController {\n const changeEvent = `${storageKey}-change`;\n\n function read(): ThemePreference {\n const stored = window.localStorage.getItem(storageKey);\n\n return stored && isThemePreference(stored) ? stored : 'system';\n }\n\n function set(preference: ThemePreference): void {\n window.localStorage.setItem(storageKey, preference);\n applyThemePreference(preference);\n window.dispatchEvent(new Event(changeEvent));\n }\n\n function subscribe(onChange: () => void): () => void {\n function handleStorageChange(event: StorageEvent): void {\n if (event.key !== storageKey) return;\n\n applyThemePreference(read());\n onChange();\n }\n\n window.addEventListener(changeEvent, onChange);\n window.addEventListener('storage', handleStorageChange);\n\n return () => {\n window.removeEventListener(changeEvent, onChange);\n window.removeEventListener('storage', handleStorageChange);\n };\n }\n\n return {\n initialize: () => applyThemePreference(read()),\n useTheme: () => [useSyncExternalStore(subscribe, read, () => 'system' as const), set] as const,\n };\n}\n\nfunction applyThemePreference(preference: ThemePreference): void {\n document.documentElement.classList.remove('light', 'dark');\n\n if (preference !== 'system') document.documentElement.classList.add(preference);\n\n for (const meta of document.querySelectorAll<HTMLMetaElement>('meta[name=\"theme-color\"]')) {\n const scheme = meta.getAttribute('media')?.includes('dark') ? 'dark' : 'light';\n\n meta.content = themeColors[preference === 'system' ? scheme : preference];\n }\n}\n"],"mappings":";;AAWA,SAAgB,kBAAkB,OAAyC;CACvE,OAAO,UAAU,YAAY,UAAU,WAAW,UAAU;AAChE;;AAGA,MAAM,cAAc;CAAE,OAAO;CAAW,MAAM;AAAU;;;;;AAMxD,SAAgB,sBAAsB,YAAqC;CACvE,MAAM,cAAc,GAAG,WAAW;CAElC,SAAS,OAAwB;EAC7B,MAAM,SAAS,OAAO,aAAa,QAAQ,UAAU;EAErD,OAAO,UAAU,kBAAkB,MAAM,IAAI,SAAS;CAC1D;CAEA,SAAS,IAAI,YAAmC;EAC5C,OAAO,aAAa,QAAQ,YAAY,UAAU;EAClD,qBAAqB,UAAU;EAC/B,OAAO,cAAc,IAAI,MAAM,WAAW,CAAC;CAC/C;CAEA,SAAS,UAAU,UAAkC;EACjD,SAAS,oBAAoB,OAA2B;GACpD,IAAI,MAAM,QAAQ,YAAY;GAE9B,qBAAqB,KAAK,CAAC;GAC3B,SAAS;EACb;EAEA,OAAO,iBAAiB,aAAa,QAAQ;EAC7C,OAAO,iBAAiB,WAAW,mBAAmB;EAEtD,aAAa;GACT,OAAO,oBAAoB,aAAa,QAAQ;GAChD,OAAO,oBAAoB,WAAW,mBAAmB;EAC7D;CACJ;CAEA,OAAO;EACH,kBAAkB,qBAAqB,KAAK,CAAC;EAC7C,gBAAgB,CAAC,qBAAqB,WAAW,YAAY,QAAiB,GAAG,GAAG;CACxF;AACJ;AAEA,SAAS,qBAAqB,YAAmC;CAC7D,SAAS,gBAAgB,UAAU,OAAO,SAAS,MAAM;CAEzD,IAAI,eAAe,UAAU,SAAS,gBAAgB,UAAU,IAAI,UAAU;CAE9E,KAAK,MAAM,QAAQ,SAAS,iBAAkC,4BAA0B,GAAG;EACvF,MAAM,SAAS,KAAK,aAAa,OAAO,CAAC,EAAE,SAAS,MAAM,IAAI,SAAS;EAEvE,KAAK,UAAU,YAAY,eAAe,WAAW,SAAS;CAClE;AACJ"}
@@ -1,5 +1,5 @@
1
1
  import { tabbable } from "../foundations/KeyboardFocus.js";
2
- import { useEffect, useState } from "react";
2
+ import { useEffect, useId, useState } from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  import { MenuIcon, XIcon } from "lucide-react";
5
5
  // src/layout/Site.tsx
@@ -30,6 +30,7 @@ function SiteShell({ skipLabel, header, footer, children }) {
30
30
  }
31
31
  function SiteHeader({ brand, links, actions, utilities, menuLabel, closeMenuLabel, navigationLabel, onNavigate }) {
32
32
  const [open, setOpen] = useState(false);
33
+ const menuId = useId();
33
34
  function follow(href) {
34
35
  setOpen(false);
35
36
  onNavigate(href);
@@ -57,14 +58,14 @@ function SiteHeader({ brand, links, actions, utilities, menuLabel, closeMenuLabe
57
58
  ...tabbable,
58
59
  type: "button",
59
60
  "aria-expanded": open,
60
- "aria-controls": "site-menu",
61
+ "aria-controls": menuId,
61
62
  "aria-label": open ? closeMenuLabel : menuLabel,
62
63
  onClick: () => setOpen((value) => !value),
63
64
  className: "text-base-800 hover:bg-base-200 -mr-2 inline-flex size-9 items-center justify-center rounded-md transition-colors md:hidden [&_svg]:size-5",
64
65
  children: open ? /* @__PURE__ */ jsx(XIcon, {}) : /* @__PURE__ */ jsx(MenuIcon, {})
65
66
  }),
66
67
  open ? /* @__PURE__ */ jsxs("nav", {
67
- id: "site-menu",
68
+ id: menuId,
68
69
  "aria-label": navigationLabel,
69
70
  className: "glass-menu absolute inset-x-3 top-[calc(100%+0.5rem)] flex flex-col gap-1 rounded-2xl p-2 md:hidden",
70
71
  children: [links.map((link) => /* @__PURE__ */ jsx(NavLink, {
@@ -1 +1 @@
1
- {"version":3,"file":"Site.js","names":[],"sources":["../../src/layout/Site.tsx"],"sourcesContent":["import { MenuIcon, XIcon } from 'lucide-react';\nimport { type ReactNode, useEffect, useState } from 'react';\n\nimport { tabbable } from '../foundations/KeyboardFocus';\n\nexport interface SiteLink {\n label: string;\n href: string;\n current?: boolean;\n external?: boolean;\n}\n\ninterface SiteShellProps {\n skipLabel: string;\n header: ReactNode;\n footer: ReactNode;\n children: ReactNode;\n}\n\n/** The website frame: a glass header that stays on top while the document scrolls under it, then the footer. */\nexport function SiteShell({ skipLabel, header, footer, children }: SiteShellProps) {\n const scrolled = useScrolled();\n\n return (\n <div className=\"flex min-h-dvh min-w-0 flex-col overflow-x-clip pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)]\">\n <a\n href=\"#content\"\n className=\"bg-base-100 sr-only z-[60] rounded-md px-3 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3\"\n >\n {skipLabel}\n </a>\n <div\n data-scrolled={scrolled || undefined}\n className=\"glass-bar data-scrolled:border-base-300 sticky top-0 z-50 shrink-0 border-b border-transparent pt-[env(safe-area-inset-top)] transition-[border-color] duration-300\"\n >\n {header}\n </div>\n <main id=\"content\" className=\"flex-1\">\n {children}\n </main>\n {footer}\n </div>\n );\n}\n\ninterface SiteHeaderProps {\n brand: ReactNode;\n links: readonly SiteLink[];\n actions: ReactNode;\n /** Secondary controls, such as the theme switch: in the bar on wide screens and in the menu on narrow ones. */\n utilities?: ReactNode;\n menuLabel: string;\n closeMenuLabel: string;\n navigationLabel: string;\n onNavigate: (href: string) => void;\n}\n\nexport function SiteHeader({\n brand,\n links,\n actions,\n utilities,\n menuLabel,\n closeMenuLabel,\n navigationLabel,\n onNavigate,\n}: SiteHeaderProps) {\n const [open, setOpen] = useState(false);\n\n function follow(href: string) {\n setOpen(false);\n onNavigate(href);\n }\n\n return (\n <header className=\"mx-auto flex h-14 w-full max-w-[1200px] items-center gap-6 px-5 sm:px-8\">\n {brand}\n <nav aria-label={navigationLabel} className=\"hidden items-center gap-1 md:flex\">\n {links.map((link) => (\n <NavLink key={link.href} link={link} onNavigate={follow} />\n ))}\n </nav>\n <div className=\"ml-auto flex items-center gap-2 max-sm:[&>a]:hidden\">\n {utilities ? <div className=\"hidden md:contents\">{utilities}</div> : null}\n {actions}\n </div>\n <button\n {...tabbable}\n type=\"button\"\n aria-expanded={open}\n aria-controls=\"site-menu\"\n aria-label={open ? closeMenuLabel : menuLabel}\n onClick={() => setOpen((value) => !value)}\n className=\"text-base-800 hover:bg-base-200 -mr-2 inline-flex size-9 items-center justify-center rounded-md transition-colors md:hidden [&_svg]:size-5\"\n >\n {open ? <XIcon /> : <MenuIcon />}\n </button>\n {open ? (\n <nav\n id=\"site-menu\"\n aria-label={navigationLabel}\n className=\"glass-menu absolute inset-x-3 top-[calc(100%+0.5rem)] flex flex-col gap-1 rounded-2xl p-2 md:hidden\"\n >\n {links.map((link) => (\n <NavLink key={link.href} link={link} onNavigate={follow} large />\n ))}\n {utilities ? <div className=\"border-base-300 mt-1 border-t px-3 pt-3 pb-1\">{utilities}</div> : null}\n </nav>\n ) : null}\n </header>\n );\n}\n\nfunction NavLink({ link, onNavigate, large }: { link: SiteLink; onNavigate: (href: string) => void; large?: boolean }) {\n return (\n <a\n {...tabbable}\n href={link.href}\n aria-current={link.current ? 'page' : undefined}\n {...(link.external ? { target: '_blank', rel: 'noreferrer' } : {})}\n onClick={(event) => {\n if (link.external || event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n\n event.preventDefault();\n onNavigate(link.href);\n }}\n className={`text-base-700 hover:text-base-content aria-[current=page]:text-base-content focus-visible:outline-primary rounded-md font-medium transition-colors focus-visible:outline-2 ${large ? 'hover:bg-base-200 px-3 py-2.5 text-base' : 'px-2.5 py-1.5 text-sm'}`}\n >\n {link.label}\n </a>\n );\n}\n\ninterface SiteFooterProps {\n brand: ReactNode;\n tagline: string;\n columns: readonly { title: string; links: readonly SiteLink[] }[];\n legal: string;\n note: ReactNode;\n onNavigate: (href: string) => void;\n}\n\nexport function SiteFooter({ brand, tagline, columns, legal, note, onNavigate }: SiteFooterProps) {\n return (\n <footer className=\"border-base-300 bg-base-50 border-t\">\n <div className=\"mx-auto grid w-full max-w-[1200px] grid-cols-2 gap-x-6 gap-y-10 px-5 pt-14 pb-10 sm:px-8 md:grid-cols-[1.4fr_repeat(4,1fr)]\">\n <div className=\"col-span-2 flex flex-col gap-3 md:col-span-1\">\n {brand}\n <p className=\"text-base-700 max-w-64 text-sm leading-6\">{tagline}</p>\n </div>\n {columns.map((column) => (\n <div key={column.title} className=\"flex flex-col gap-3\">\n <h2 className=\"text-base-800 text-xs font-semibold tracking-[0.06em] uppercase\">\n {column.title}\n </h2>\n <ul className=\"flex flex-col gap-2\">\n {column.links.map((link) => (\n <li key={link.href}>\n <a\n {...tabbable}\n href={link.href}\n {...(link.external ? { target: '_blank', rel: 'noreferrer' } : {})}\n onClick={(event) => {\n if (link.external || !link.href.startsWith('/')) return;\n\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0)\n return;\n\n event.preventDefault();\n onNavigate(link.href);\n }}\n className=\"text-base-700 hover:text-base-content text-sm transition-colors\"\n >\n {link.label}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n <div className=\"mx-auto w-full max-w-[1200px] px-5 sm:px-8\">\n <div className=\"border-base-300/70 text-base-700 flex flex-col gap-2 border-t py-6 text-xs sm:flex-row sm:items-center sm:justify-between\">\n <span>{legal}</span>\n <span>{note}</span>\n </div>\n </div>\n </footer>\n );\n}\n\nfunction useScrolled() {\n const [scrolled, setScrolled] = useState(false);\n\n useEffect(() => {\n const update = () => setScrolled(window.scrollY > 4);\n\n update();\n window.addEventListener('scroll', update, { passive: true });\n\n return () => window.removeEventListener('scroll', update);\n }, []);\n\n return scrolled;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Site.js","names":[],"sources":["../../src/layout/Site.tsx"],"sourcesContent":["import { MenuIcon, XIcon } from 'lucide-react';\nimport { type ReactNode, useEffect, useId, useState } from 'react';\n\nimport { tabbable } from '../foundations/KeyboardFocus';\n\nexport interface SiteLink {\n label: string;\n href: string;\n current?: boolean;\n external?: boolean;\n}\n\ninterface SiteShellProps {\n skipLabel: string;\n header: ReactNode;\n footer: ReactNode;\n children: ReactNode;\n}\n\n/** The website frame: a glass header that stays on top while the document scrolls under it, then the footer. */\nexport function SiteShell({ skipLabel, header, footer, children }: SiteShellProps) {\n const scrolled = useScrolled();\n\n return (\n <div className=\"flex min-h-dvh min-w-0 flex-col overflow-x-clip pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)]\">\n <a\n href=\"#content\"\n className=\"bg-base-100 sr-only z-[60] rounded-md px-3 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3\"\n >\n {skipLabel}\n </a>\n <div\n data-scrolled={scrolled || undefined}\n className=\"glass-bar data-scrolled:border-base-300 sticky top-0 z-50 shrink-0 border-b border-transparent pt-[env(safe-area-inset-top)] transition-[border-color] duration-300\"\n >\n {header}\n </div>\n <main id=\"content\" className=\"flex-1\">\n {children}\n </main>\n {footer}\n </div>\n );\n}\n\ninterface SiteHeaderProps {\n brand: ReactNode;\n links: readonly SiteLink[];\n actions: ReactNode;\n /** Secondary controls, such as the theme switch: in the bar on wide screens and in the menu on narrow ones. */\n utilities?: ReactNode;\n menuLabel: string;\n closeMenuLabel: string;\n navigationLabel: string;\n onNavigate: (href: string) => void;\n}\n\nexport function SiteHeader({\n brand,\n links,\n actions,\n utilities,\n menuLabel,\n closeMenuLabel,\n navigationLabel,\n onNavigate,\n}: SiteHeaderProps) {\n const [open, setOpen] = useState(false);\n const menuId = useId();\n\n function follow(href: string) {\n setOpen(false);\n onNavigate(href);\n }\n\n return (\n <header className=\"mx-auto flex h-14 w-full max-w-[1200px] items-center gap-6 px-5 sm:px-8\">\n {brand}\n <nav aria-label={navigationLabel} className=\"hidden items-center gap-1 md:flex\">\n {links.map((link) => (\n <NavLink key={link.href} link={link} onNavigate={follow} />\n ))}\n </nav>\n <div className=\"ml-auto flex items-center gap-2 max-sm:[&>a]:hidden\">\n {utilities ? <div className=\"hidden md:contents\">{utilities}</div> : null}\n {actions}\n </div>\n <button\n {...tabbable}\n type=\"button\"\n aria-expanded={open}\n aria-controls={menuId}\n aria-label={open ? closeMenuLabel : menuLabel}\n onClick={() => setOpen((value) => !value)}\n className=\"text-base-800 hover:bg-base-200 -mr-2 inline-flex size-9 items-center justify-center rounded-md transition-colors md:hidden [&_svg]:size-5\"\n >\n {open ? <XIcon /> : <MenuIcon />}\n </button>\n {open ? (\n <nav\n id={menuId}\n aria-label={navigationLabel}\n className=\"glass-menu absolute inset-x-3 top-[calc(100%+0.5rem)] flex flex-col gap-1 rounded-2xl p-2 md:hidden\"\n >\n {links.map((link) => (\n <NavLink key={link.href} link={link} onNavigate={follow} large />\n ))}\n {utilities ? <div className=\"border-base-300 mt-1 border-t px-3 pt-3 pb-1\">{utilities}</div> : null}\n </nav>\n ) : null}\n </header>\n );\n}\n\nfunction NavLink({ link, onNavigate, large }: { link: SiteLink; onNavigate: (href: string) => void; large?: boolean }) {\n return (\n <a\n {...tabbable}\n href={link.href}\n aria-current={link.current ? 'page' : undefined}\n {...(link.external ? { target: '_blank', rel: 'noreferrer' } : {})}\n onClick={(event) => {\n if (link.external || event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n\n event.preventDefault();\n onNavigate(link.href);\n }}\n className={`text-base-700 hover:text-base-content aria-[current=page]:text-base-content focus-visible:outline-primary rounded-md font-medium transition-colors focus-visible:outline-2 ${large ? 'hover:bg-base-200 px-3 py-2.5 text-base' : 'px-2.5 py-1.5 text-sm'}`}\n >\n {link.label}\n </a>\n );\n}\n\ninterface SiteFooterProps {\n brand: ReactNode;\n tagline: string;\n columns: readonly { title: string; links: readonly SiteLink[] }[];\n legal: string;\n note: ReactNode;\n onNavigate: (href: string) => void;\n}\n\nexport function SiteFooter({ brand, tagline, columns, legal, note, onNavigate }: SiteFooterProps) {\n return (\n <footer className=\"border-base-300 bg-base-50 border-t\">\n <div className=\"mx-auto grid w-full max-w-[1200px] grid-cols-2 gap-x-6 gap-y-10 px-5 pt-14 pb-10 sm:px-8 md:grid-cols-[1.4fr_repeat(4,1fr)]\">\n <div className=\"col-span-2 flex flex-col gap-3 md:col-span-1\">\n {brand}\n <p className=\"text-base-700 max-w-64 text-sm leading-6\">{tagline}</p>\n </div>\n {columns.map((column) => (\n <div key={column.title} className=\"flex flex-col gap-3\">\n <h2 className=\"text-base-800 text-xs font-semibold tracking-[0.06em] uppercase\">\n {column.title}\n </h2>\n <ul className=\"flex flex-col gap-2\">\n {column.links.map((link) => (\n <li key={link.href}>\n <a\n {...tabbable}\n href={link.href}\n {...(link.external ? { target: '_blank', rel: 'noreferrer' } : {})}\n onClick={(event) => {\n if (link.external || !link.href.startsWith('/')) return;\n\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0)\n return;\n\n event.preventDefault();\n onNavigate(link.href);\n }}\n className=\"text-base-700 hover:text-base-content text-sm transition-colors\"\n >\n {link.label}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n <div className=\"mx-auto w-full max-w-[1200px] px-5 sm:px-8\">\n <div className=\"border-base-300/70 text-base-700 flex flex-col gap-2 border-t py-6 text-xs sm:flex-row sm:items-center sm:justify-between\">\n <span>{legal}</span>\n <span>{note}</span>\n </div>\n </div>\n </footer>\n );\n}\n\nfunction useScrolled() {\n const [scrolled, setScrolled] = useState(false);\n\n useEffect(() => {\n const update = () => setScrolled(window.scrollY > 4);\n\n update();\n window.addEventListener('scroll', update, { passive: true });\n\n return () => window.removeEventListener('scroll', update);\n }, []);\n\n return scrolled;\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ interface ActionModalProps {
4
4
  actionColor?: 'danger' | 'primary';
5
5
  actionDisabled?: boolean;
6
6
  actionLabel: string;
7
- children: ReactNode;
7
+ children?: ReactNode;
8
8
  bodyLayout?: 'scroll' | 'fill';
9
9
  description: string;
10
10
  footerLeading?: ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"ActionModal.js","names":[],"sources":["../../src/overlays/ActionModal.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { Button } from '../actions/Button';\nimport { useDesignSystemLabels } from '../foundations/DesignSystemProvider';\nimport { Modal } from './Modal';\n\ninterface ActionModalProps {\n accessibleTitle?: string;\n actionColor?: 'danger' | 'primary';\n actionDisabled?: boolean;\n actionLabel: string;\n children: ReactNode;\n bodyLayout?: 'scroll' | 'fill';\n description: string;\n footerLeading?: ReactNode;\n headerActions?: ReactNode;\n loading: boolean;\n onAction: () => void;\n onClose: () => void;\n sidebar?: ReactNode;\n noValidate?: boolean;\n title: string;\n}\n\nexport function ActionModal({\n accessibleTitle,\n actionColor = 'primary',\n actionDisabled = false,\n actionLabel,\n children,\n bodyLayout,\n description,\n footerLeading,\n headerActions,\n loading,\n onAction,\n onClose,\n sidebar,\n noValidate,\n title,\n}: ActionModalProps) {\n const labels = useDesignSystemLabels();\n\n return (\n <Modal loading={loading} onClose={onClose}>\n <Modal.Content\n title={title}\n accessibleTitle={accessibleTitle}\n description={description}\n sidebar={sidebar}\n headerActions={headerActions}\n >\n <Modal.Form\n noValidate={noValidate}\n onSubmit={(event) => {\n event.preventDefault();\n\n if (!loading && !actionDisabled) {\n onAction();\n }\n }}\n >\n <Modal.Body layout={bodyLayout}>{children}</Modal.Body>\n <Modal.Footer leading={footerLeading}>\n <Button type=\"button\" disabled={loading} onClick={onClose} variant=\"secondary\">\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n loading={loading}\n disabled={loading || actionDisabled}\n variant={actionColor === 'danger' ? 'destructive' : 'primary'}\n >\n {actionLabel}\n </Button>\n </Modal.Footer>\n </Modal.Form>\n </Modal.Content>\n </Modal>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,YAAY,EACxB,iBACA,cAAc,WACd,iBAAiB,OACjB,aACA,UACA,YACA,aACA,eACA,eACA,SACA,UACA,SACA,SACA,YACA,SACiB;CACjB,MAAM,SAAS,sBAAsB;CAErC,OACI,oBAAC,OAAD;EAAgB;EAAkB;EAC9B,UAAA,oBAAC,MAAM,SAAP;GACW;GACU;GACJ;GACJ;GACM;GAEf,UAAA,qBAAC,MAAM,MAAP;IACgB;IACZ,WAAW,UAAU;KACjB,MAAM,eAAe;KAErB,IAAI,CAAC,WAAW,CAAC,gBACb,SAAS;IAEjB;IARJ,UAAA,CAUI,oBAAC,MAAM,MAAP;KAAY,QAAQ;KAAa;IAAqB,CAAA,GACtD,qBAAC,MAAM,QAAP;KAAc,SAAS;KAAvB,UAAA,CACI,oBAAC,QAAD;MAAQ,MAAK;MAAS,UAAU;MAAS,SAAS;MAAS,SAAQ;MAC9D,UAAA,OAAO;KACJ,CAAA,GACR,oBAAC,QAAD;MACI,MAAK;MACI;MACT,UAAU,WAAW;MACrB,SAAS,gBAAgB,WAAW,gBAAgB;MAEnD,UAAA;KACG,CAAA,CACE;IACN,CAAA,CAAA;;EACD,CAAA;CACZ,CAAA;AAEf"}
1
+ {"version":3,"file":"ActionModal.js","names":[],"sources":["../../src/overlays/ActionModal.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { Button } from '../actions/Button';\nimport { useDesignSystemLabels } from '../foundations/DesignSystemProvider';\nimport { Modal } from './Modal';\n\ninterface ActionModalProps {\n accessibleTitle?: string;\n actionColor?: 'danger' | 'primary';\n actionDisabled?: boolean;\n actionLabel: string;\n children?: ReactNode;\n bodyLayout?: 'scroll' | 'fill';\n description: string;\n footerLeading?: ReactNode;\n headerActions?: ReactNode;\n loading: boolean;\n onAction: () => void;\n onClose: () => void;\n sidebar?: ReactNode;\n noValidate?: boolean;\n title: string;\n}\n\nexport function ActionModal({\n accessibleTitle,\n actionColor = 'primary',\n actionDisabled = false,\n actionLabel,\n children,\n bodyLayout,\n description,\n footerLeading,\n headerActions,\n loading,\n onAction,\n onClose,\n sidebar,\n noValidate,\n title,\n}: ActionModalProps) {\n const labels = useDesignSystemLabels();\n\n return (\n <Modal loading={loading} onClose={onClose}>\n <Modal.Content\n title={title}\n accessibleTitle={accessibleTitle}\n description={description}\n sidebar={sidebar}\n headerActions={headerActions}\n >\n <Modal.Form\n noValidate={noValidate}\n onSubmit={(event) => {\n event.preventDefault();\n\n if (!loading && !actionDisabled) {\n onAction();\n }\n }}\n >\n <Modal.Body layout={bodyLayout}>{children}</Modal.Body>\n <Modal.Footer leading={footerLeading}>\n <Button type=\"button\" disabled={loading} onClick={onClose} variant=\"secondary\">\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n loading={loading}\n disabled={loading || actionDisabled}\n variant={actionColor === 'danger' ? 'destructive' : 'primary'}\n >\n {actionLabel}\n </Button>\n </Modal.Footer>\n </Modal.Form>\n </Modal.Content>\n </Modal>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,YAAY,EACxB,iBACA,cAAc,WACd,iBAAiB,OACjB,aACA,UACA,YACA,aACA,eACA,eACA,SACA,UACA,SACA,SACA,YACA,SACiB;CACjB,MAAM,SAAS,sBAAsB;CAErC,OACI,oBAAC,OAAD;EAAgB;EAAkB;EAC9B,UAAA,oBAAC,MAAM,SAAP;GACW;GACU;GACJ;GACJ;GACM;GAEf,UAAA,qBAAC,MAAM,MAAP;IACgB;IACZ,WAAW,UAAU;KACjB,MAAM,eAAe;KAErB,IAAI,CAAC,WAAW,CAAC,gBACb,SAAS;IAEjB;IARJ,UAAA,CAUI,oBAAC,MAAM,MAAP;KAAY,QAAQ;KAAa;IAAqB,CAAA,GACtD,qBAAC,MAAM,QAAP;KAAc,SAAS;KAAvB,UAAA,CACI,oBAAC,QAAD;MAAQ,MAAK;MAAS,UAAU;MAAS,SAAS;MAAS,SAAQ;MAC9D,UAAA,OAAO;KACJ,CAAA,GACR,oBAAC,QAAD;MACI,MAAK;MACI;MACT,UAAU,WAAW;MACrB,SAAS,gBAAgB,WAAW,gBAAgB;MAEnD,UAAA;KACG,CAAA,CACE;IACN,CAAA,CAAA;;EACD,CAAA;CACZ,CAAA;AAEf"}
@@ -10,7 +10,7 @@ function ProductIndex({ products, label, onNavigate }) {
10
10
  "aria-label": label,
11
11
  className: "mx-auto flex max-w-[880px] flex-wrap justify-center gap-3",
12
12
  children: products.map((product) => /* @__PURE__ */ jsx("li", {
13
- className: "w-full sm:w-[calc(50%-0.375rem)] lg:w-[calc(50%-0.375rem)]",
13
+ className: "w-full sm:w-[calc(50%-0.375rem)]",
14
14
  children: /* @__PURE__ */ jsxs("a", {
15
15
  ...tabbable,
16
16
  href: product.href,
@@ -1 +1 @@
1
- {"version":3,"file":"ProductBand.js","names":[],"sources":["../../src/showcase/ProductBand.tsx"],"sourcesContent":["import { ArrowRightIcon } from 'lucide-react';\nimport { type CSSProperties, type ReactNode, useId } from 'react';\n\nimport { tabbable } from '../foundations/KeyboardFocus';\nimport { Heading, Text } from '../typography/Typography';\n\nexport interface ProductEntry {\n name: string;\n icon: ReactNode;\n status: ReactNode;\n summary: string;\n href: string;\n}\n\n/** The family of products under a hero, one compact entry each, wrapping as the family grows. */\nexport function ProductIndex({\n products,\n label,\n onNavigate,\n}: {\n products: readonly ProductEntry[];\n label: string;\n onNavigate: (href: string) => void;\n}) {\n return (\n <ul aria-label={label} className=\"mx-auto flex max-w-[880px] flex-wrap justify-center gap-3\">\n {products.map((product) => (\n <li key={product.href} className=\"w-full sm:w-[calc(50%-0.375rem)] lg:w-[calc(50%-0.375rem)]\">\n <a\n {...tabbable}\n href={product.href}\n onClick={(event) => {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n\n event.preventDefault();\n onNavigate(product.href);\n }}\n className=\"group/entry border-base-300 bg-base-100/70 hover:border-base-400 focus-visible:outline-primary dark:bg-base-100/50 flex h-full items-center gap-3 rounded-2xl border px-4 py-3 text-left shadow-xs/5 backdrop-blur transition-[border-color,background-color] focus-visible:outline-2\"\n >\n {product.icon}\n <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-base-content text-[15px] font-semibold tracking-[-0.01em]\">\n {product.name}\n </span>\n {product.status}\n </span>\n <span className=\"text-base-700 text-sm leading-5\">{product.summary}</span>\n </span>\n <span className=\"text-base-600 inline-flex transition-transform duration-200 group-hover/entry:translate-x-0.5 [&_svg]:size-4\">\n <ArrowRightIcon />\n </span>\n </a>\n </li>\n ))}\n </ul>\n );\n}\n\ninterface ProductBandProps {\n /** The band's colour world: a hue and how strongly it shows. */\n hue: number;\n chroma?: number;\n icon: ReactNode;\n name: string;\n status: ReactNode;\n title: string;\n description: string;\n actions: ReactNode;\n media: ReactNode;\n /** Puts the media on the left. */\n reverse?: boolean;\n /** A short line under the actions, such as requirements. */\n note?: ReactNode;\n}\n\n/** One product's own space on a page that presents several: its colour, its devices, its way in. */\nexport function ProductBand({\n hue,\n chroma = 0.13,\n icon,\n name,\n status,\n title,\n description,\n actions,\n media,\n reverse,\n note,\n}: ProductBandProps) {\n const id = useId();\n\n return (\n <article\n aria-labelledby={id}\n className=\"border-base-300 bg-base-100 dark:bg-base-100/70 relative isolate overflow-hidden rounded-[2rem] border\"\n style={{ '--lar-band-hue': hue, '--lar-band-chroma': chroma } as CSSProperties}\n >\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-0 -z-10 bg-[radial-gradient(70%_90%_at_var(--lar-band-x)_50%,oklch(0.6_var(--lar-band-chroma)_var(--lar-band-hue)/0.24),transparent_70%)] dark:bg-[radial-gradient(70%_90%_at_var(--lar-band-x)_50%,oklch(0.42_var(--lar-band-chroma)_var(--lar-band-hue)/0.45),transparent_70%)]\"\n style={{ '--lar-band-x': reverse ? '20%' : '80%' } as CSSProperties}\n />\n {/* On narrow screens the text wrapper dissolves, so the product's picture sits right under its\n heading, before the description and actions. */}\n <div\n className={`grid items-center gap-5 p-6 sm:p-10 lg:gap-12 lg:p-12 ${reverse ? 'lg:grid-cols-[minmax(0,8fr)_minmax(0,4fr)]' : 'lg:grid-cols-[minmax(0,4fr)_minmax(0,8fr)]'}`}\n >\n <div className={`contents lg:flex lg:flex-col lg:gap-5 ${reverse ? 'lg:order-2' : ''}`}>\n <div className=\"order-1 flex items-center gap-3 lg:order-none\">\n {icon}\n <Text as=\"span\" variant=\"label\">\n {name}\n </Text>\n {status}\n </div>\n <Heading\n id={id}\n as=\"h2\"\n variant=\"feature\"\n className=\"order-2 lg:order-none lg:text-[1.875rem] lg:leading-[1.15]\"\n >\n {title}\n </Heading>\n <Text variant=\"lead\" tone=\"muted\" className=\"order-4 lg:order-none\">\n {description}\n </Text>\n <div className=\"order-5 mt-1 flex flex-col gap-3 lg:order-none\">\n <div className=\"flex flex-wrap items-center gap-3 max-sm:flex-col max-sm:[&>a]:w-full\">\n {actions}\n </div>\n {note ? (\n <Text variant=\"caption\" tone=\"muted\" className=\"dark:text-[#96969f]\">\n {note}\n </Text>\n ) : null}\n </div>\n </div>\n <div className={`order-3 my-3 lg:my-0 ${reverse ? 'lg:order-1' : 'lg:order-none'}`}>{media}</div>\n </div>\n </article>\n );\n}\n\n/** Product bands one after another, as many as the family has. */\nexport function ProductBands({ children }: { children: ReactNode }) {\n return <div className=\"flex flex-col gap-6 sm:gap-8\">{children}</div>;\n}\n"],"mappings":";;;;;;;AAeA,SAAgB,aAAa,EACzB,UACA,OACA,cAKD;CACC,OACI,oBAAC,MAAD;EAAI,cAAY;EAAO,WAAU;EAC5B,UAAA,SAAS,KAAK,YACX,oBAAC,MAAD;GAAuB,WAAU;GAC7B,UAAA,qBAAC,KAAD;IACI,GAAI;IACJ,MAAM,QAAQ;IACd,UAAU,UAAU;KAChB,IAAI,MAAM,WAAW,MAAM,WAAW,MAAM,YAAY,MAAM,WAAW,GAAG;KAE5E,MAAM,eAAe;KACrB,WAAW,QAAQ,IAAI;IAC3B;IACA,WAAU;IATd,UAAA;KAWK,QAAQ;KACT,qBAAC,QAAD;MAAM,WAAU;MAAhB,UAAA,CACI,qBAAC,QAAD;OAAM,WAAU;OAAhB,UAAA,CACI,oBAAC,QAAD;QAAM,WAAU;QACX,UAAA,QAAQ;OACP,CAAA,GACL,QAAQ,MACP;MACN,CAAA,GAAA,oBAAC,QAAD;OAAM,WAAU;OAAmC,UAAA,QAAQ;MAAc,CAAA,CACvE;;KACN,oBAAC,QAAD;MAAM,WAAU;MACZ,UAAA,oBAAC,gBAAD,CAAiB,CAAA;KACf,CAAA;IACP;;EACH,GA1BK,QAAQ,IA0Bb,CACP;CACD,CAAA;AAEZ;;AAoBA,SAAgB,YAAY,EACxB,KACA,SAAS,KACT,MACA,MACA,QACA,OACA,aACA,SACA,OACA,SACA,QACiB;CACjB,MAAM,KAAK,MAAM;CAEjB,OACI,qBAAC,WAAD;EACI,mBAAiB;EACjB,WAAU;EACV,OAAO;GAAE,kBAAkB;GAAK,qBAAqB;EAAO;EAHhE,UAAA,CAKI,oBAAC,OAAD;GACI,eAAY;GACZ,WAAU;GACV,OAAO,EAAE,gBAAgB,UAAU,QAAQ,MAAM;EACpD,CAAA,GAGD,qBAAC,OAAD;GACI,WAAW,yDAAyD,UAAU,+CAA+C;GADjI,UAAA,CAGI,qBAAC,OAAD;IAAK,WAAW,yCAAyC,UAAU,eAAe;IAAlF,UAAA;KACI,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA;OACK;OACD,oBAAC,MAAD;QAAM,IAAG;QAAO,SAAQ;QACnB,UAAA;OACC,CAAA;OACL;MACA;;KACL,oBAAC,SAAD;MACQ;MACJ,IAAG;MACH,SAAQ;MACR,WAAU;MAET,UAAA;KACI,CAAA;KACT,oBAAC,MAAD;MAAM,SAAQ;MAAO,MAAK;MAAQ,WAAU;MACvC,UAAA;KACC,CAAA;KACN,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACI,oBAAC,OAAD;OAAK,WAAU;OACV,UAAA;MACA,CAAA,GACJ,OACG,oBAAC,MAAD;OAAM,SAAQ;OAAU,MAAK;OAAQ,WAAU;OAC1C,UAAA;MACC,CAAA,IACN,IACH;;IACJ;GACL,CAAA,GAAA,oBAAC,OAAD;IAAK,WAAW,wBAAwB,UAAU,eAAe;IAAoB,UAAA;GAAW,CAAA,CAC/F;EACA,CAAA,CAAA;;AAEjB;;AAGA,SAAgB,aAAa,EAAE,YAAqC;CAChE,OAAO,oBAAC,OAAD;EAAK,WAAU;EAAgC;CAAc,CAAA;AACxE"}
1
+ {"version":3,"file":"ProductBand.js","names":[],"sources":["../../src/showcase/ProductBand.tsx"],"sourcesContent":["import { ArrowRightIcon } from 'lucide-react';\nimport { type CSSProperties, type ReactNode, useId } from 'react';\n\nimport { tabbable } from '../foundations/KeyboardFocus';\nimport { Heading, Text } from '../typography/Typography';\n\nexport interface ProductEntry {\n name: string;\n icon: ReactNode;\n status: ReactNode;\n summary: string;\n href: string;\n}\n\n/** The family of products under a hero, one compact entry each, wrapping as the family grows. */\nexport function ProductIndex({\n products,\n label,\n onNavigate,\n}: {\n products: readonly ProductEntry[];\n label: string;\n onNavigate: (href: string) => void;\n}) {\n return (\n <ul aria-label={label} className=\"mx-auto flex max-w-[880px] flex-wrap justify-center gap-3\">\n {products.map((product) => (\n <li key={product.href} className=\"w-full sm:w-[calc(50%-0.375rem)]\">\n <a\n {...tabbable}\n href={product.href}\n onClick={(event) => {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n\n event.preventDefault();\n onNavigate(product.href);\n }}\n className=\"group/entry border-base-300 bg-base-100/70 hover:border-base-400 focus-visible:outline-primary dark:bg-base-100/50 flex h-full items-center gap-3 rounded-2xl border px-4 py-3 text-left shadow-xs/5 backdrop-blur transition-[border-color,background-color] focus-visible:outline-2\"\n >\n {product.icon}\n <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-base-content text-[15px] font-semibold tracking-[-0.01em]\">\n {product.name}\n </span>\n {product.status}\n </span>\n <span className=\"text-base-700 text-sm leading-5\">{product.summary}</span>\n </span>\n <span className=\"text-base-600 inline-flex transition-transform duration-200 group-hover/entry:translate-x-0.5 [&_svg]:size-4\">\n <ArrowRightIcon />\n </span>\n </a>\n </li>\n ))}\n </ul>\n );\n}\n\ninterface ProductBandProps {\n /** The band's colour world: a hue and how strongly it shows. */\n hue: number;\n chroma?: number;\n icon: ReactNode;\n name: string;\n status: ReactNode;\n title: string;\n description: string;\n actions: ReactNode;\n media: ReactNode;\n /** Puts the media on the left. */\n reverse?: boolean;\n /** A short line under the actions, such as requirements. */\n note?: ReactNode;\n}\n\n/** One product's own space on a page that presents several: its colour, its devices, its way in. */\nexport function ProductBand({\n hue,\n chroma = 0.13,\n icon,\n name,\n status,\n title,\n description,\n actions,\n media,\n reverse,\n note,\n}: ProductBandProps) {\n const id = useId();\n\n return (\n <article\n aria-labelledby={id}\n className=\"border-base-300 bg-base-100 dark:bg-base-100/70 relative isolate overflow-hidden rounded-[2rem] border\"\n style={{ '--lar-band-hue': hue, '--lar-band-chroma': chroma } as CSSProperties}\n >\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-0 -z-10 bg-[radial-gradient(70%_90%_at_var(--lar-band-x)_50%,oklch(0.6_var(--lar-band-chroma)_var(--lar-band-hue)/0.24),transparent_70%)] dark:bg-[radial-gradient(70%_90%_at_var(--lar-band-x)_50%,oklch(0.42_var(--lar-band-chroma)_var(--lar-band-hue)/0.45),transparent_70%)]\"\n style={{ '--lar-band-x': reverse ? '20%' : '80%' } as CSSProperties}\n />\n {/* On narrow screens the text wrapper dissolves, so the product's picture sits right under its\n heading, before the description and actions. */}\n <div\n className={`grid items-center gap-5 p-6 sm:p-10 lg:gap-12 lg:p-12 ${reverse ? 'lg:grid-cols-[minmax(0,8fr)_minmax(0,4fr)]' : 'lg:grid-cols-[minmax(0,4fr)_minmax(0,8fr)]'}`}\n >\n <div className={`contents lg:flex lg:flex-col lg:gap-5 ${reverse ? 'lg:order-2' : ''}`}>\n <div className=\"order-1 flex items-center gap-3 lg:order-none\">\n {icon}\n <Text as=\"span\" variant=\"label\">\n {name}\n </Text>\n {status}\n </div>\n <Heading\n id={id}\n as=\"h2\"\n variant=\"feature\"\n className=\"order-2 lg:order-none lg:text-[1.875rem] lg:leading-[1.15]\"\n >\n {title}\n </Heading>\n <Text variant=\"lead\" tone=\"muted\" className=\"order-4 lg:order-none\">\n {description}\n </Text>\n <div className=\"order-5 mt-1 flex flex-col gap-3 lg:order-none\">\n <div className=\"flex flex-wrap items-center gap-3 max-sm:flex-col max-sm:[&>a]:w-full\">\n {actions}\n </div>\n {note ? (\n <Text variant=\"caption\" tone=\"muted\" className=\"dark:text-[#96969f]\">\n {note}\n </Text>\n ) : null}\n </div>\n </div>\n <div className={`order-3 my-3 lg:my-0 ${reverse ? 'lg:order-1' : 'lg:order-none'}`}>{media}</div>\n </div>\n </article>\n );\n}\n\n/** Product bands one after another, as many as the family has. */\nexport function ProductBands({ children }: { children: ReactNode }) {\n return <div className=\"flex flex-col gap-6 sm:gap-8\">{children}</div>;\n}\n"],"mappings":";;;;;;;AAeA,SAAgB,aAAa,EACzB,UACA,OACA,cAKD;CACC,OACI,oBAAC,MAAD;EAAI,cAAY;EAAO,WAAU;EAC5B,UAAA,SAAS,KAAK,YACX,oBAAC,MAAD;GAAuB,WAAU;GAC7B,UAAA,qBAAC,KAAD;IACI,GAAI;IACJ,MAAM,QAAQ;IACd,UAAU,UAAU;KAChB,IAAI,MAAM,WAAW,MAAM,WAAW,MAAM,YAAY,MAAM,WAAW,GAAG;KAE5E,MAAM,eAAe;KACrB,WAAW,QAAQ,IAAI;IAC3B;IACA,WAAU;IATd,UAAA;KAWK,QAAQ;KACT,qBAAC,QAAD;MAAM,WAAU;MAAhB,UAAA,CACI,qBAAC,QAAD;OAAM,WAAU;OAAhB,UAAA,CACI,oBAAC,QAAD;QAAM,WAAU;QACX,UAAA,QAAQ;OACP,CAAA,GACL,QAAQ,MACP;MACN,CAAA,GAAA,oBAAC,QAAD;OAAM,WAAU;OAAmC,UAAA,QAAQ;MAAc,CAAA,CACvE;;KACN,oBAAC,QAAD;MAAM,WAAU;MACZ,UAAA,oBAAC,gBAAD,CAAiB,CAAA;KACf,CAAA;IACP;;EACH,GA1BK,QAAQ,IA0Bb,CACP;CACD,CAAA;AAEZ;;AAoBA,SAAgB,YAAY,EACxB,KACA,SAAS,KACT,MACA,MACA,QACA,OACA,aACA,SACA,OACA,SACA,QACiB;CACjB,MAAM,KAAK,MAAM;CAEjB,OACI,qBAAC,WAAD;EACI,mBAAiB;EACjB,WAAU;EACV,OAAO;GAAE,kBAAkB;GAAK,qBAAqB;EAAO;EAHhE,UAAA,CAKI,oBAAC,OAAD;GACI,eAAY;GACZ,WAAU;GACV,OAAO,EAAE,gBAAgB,UAAU,QAAQ,MAAM;EACpD,CAAA,GAGD,qBAAC,OAAD;GACI,WAAW,yDAAyD,UAAU,+CAA+C;GADjI,UAAA,CAGI,qBAAC,OAAD;IAAK,WAAW,yCAAyC,UAAU,eAAe;IAAlF,UAAA;KACI,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA;OACK;OACD,oBAAC,MAAD;QAAM,IAAG;QAAO,SAAQ;QACnB,UAAA;OACC,CAAA;OACL;MACA;;KACL,oBAAC,SAAD;MACQ;MACJ,IAAG;MACH,SAAQ;MACR,WAAU;MAET,UAAA;KACI,CAAA;KACT,oBAAC,MAAD;MAAM,SAAQ;MAAO,MAAK;MAAQ,WAAU;MACvC,UAAA;KACC,CAAA;KACN,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACI,oBAAC,OAAD;OAAK,WAAU;OACV,UAAA;MACA,CAAA,GACJ,OACG,oBAAC,MAAD;OAAM,SAAQ;OAAU,MAAK;OAAQ,WAAU;OAC1C,UAAA;MACC,CAAA,IACN,IACH;;IACJ;GACL,CAAA,GAAA,oBAAC,OAAD;IAAK,WAAW,wBAAwB,UAAU,eAAe;IAAoB,UAAA;GAAW,CAAA,CAC/F;EACA,CAAA,CAAA;;AAEjB;;AAGA,SAAgB,aAAa,EAAE,YAAqC;CAChE,OAAO,oBAAC,OAAD;EAAK,WAAU;EAAgC;CAAc,CAAA;AACxE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lar-works/design-system",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lar's design system: tokens, base styles and every visual component the Lar web apps use.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,7 +25,8 @@
25
25
  "types": "./dist/testing.d.ts",
26
26
  "default": "./dist/testing.js"
27
27
  },
28
- "./styles.css": "./src/styles.css"
28
+ "./styles.css": "./src/styles.css",
29
+ "./showcase.css": "./src/showcase.css"
29
30
  },
30
31
  "files": [
31
32
  "dist",
@@ -51,12 +52,12 @@
51
52
  "tailwindcss": "^4.3.0"
52
53
  },
53
54
  "devDependencies": {
54
- "@lar-works/config": "0.1.0",
55
+ "@lar-works/config": "0.1.1",
55
56
  "@storybook/addon-a11y": "10.6.6",
56
57
  "@storybook/addon-docs": "10.6.6",
57
58
  "@storybook/addon-themes": "10.6.6",
58
59
  "@storybook/react-vite": "10.6.6",
59
- "@tailwindcss/vite": "^4.3.5",
60
+ "@tailwindcss/vite": "^4.3.3",
60
61
  "@testing-library/jest-dom": "^7.0.1",
61
62
  "@testing-library/react": "^16.3.3",
62
63
  "@testing-library/user-event": "^14.6.8",
@@ -68,7 +69,7 @@
68
69
  "react": "^19.3.1",
69
70
  "react-dom": "^19.3.1",
70
71
  "storybook": "10.6.6",
71
- "tailwindcss": "^4.3.5",
72
+ "tailwindcss": "^4.3.3",
72
73
  "typescript": "^7.0.2",
73
74
  "vite": "npm:@voidzero-dev/vite-plus-core@1.5.0",
74
75
  "vite-plus": "1.5.0"
@@ -4,9 +4,9 @@ export type ThemePreference = 'system' | 'light' | 'dark';
4
4
 
5
5
  export interface ThemeController {
6
6
  /** Applies the stored preference before the first render, so the page never flashes the other theme. */
7
- initialize(): void;
7
+ initialize: () => void;
8
8
  /** The current preference and a setter that stores and applies it, kept in step across tabs. */
9
- useTheme(): readonly [ThemePreference, (value: ThemePreference) => void];
9
+ useTheme: () => readonly [ThemePreference, (value: ThemePreference) => void];
10
10
  }
11
11
 
12
12
  export function isThemePreference(value: string): value is ThemePreference {
@@ -54,11 +54,7 @@ export function createThemeController(storageKey: string): ThemeController {
54
54
 
55
55
  return {
56
56
  initialize: () => applyThemePreference(read()),
57
- useTheme() {
58
- const preference = useSyncExternalStore(subscribe, read, () => 'system' as const);
59
-
60
- return [preference, set] as const;
61
- },
57
+ useTheme: () => [useSyncExternalStore(subscribe, read, () => 'system' as const), set] as const,
62
58
  };
63
59
  }
64
60
 
@@ -1,8 +1,5 @@
1
1
  @import 'tailwindcss';
2
2
  @import '@ninna-ui/core/theme/presets/minimal.css';
3
- @import '../showcase/devices.css';
4
- @import '../showcase/stage.css';
5
- @import '../showcase/server-unit.css';
6
3
 
7
4
  /* Dark follows an explicit .dark class, or the system while neither .light nor .dark is set, so dark:
8
5
  utilities match the palette in both cases. */
@@ -1,5 +1,5 @@
1
1
  import { MenuIcon, XIcon } from 'lucide-react';
2
- import { type ReactNode, useEffect, useState } from 'react';
2
+ import { type ReactNode, useEffect, useId, useState } from 'react';
3
3
 
4
4
  import { tabbable } from '../foundations/KeyboardFocus';
5
5
 
@@ -66,6 +66,7 @@ export function SiteHeader({
66
66
  onNavigate,
67
67
  }: SiteHeaderProps) {
68
68
  const [open, setOpen] = useState(false);
69
+ const menuId = useId();
69
70
 
70
71
  function follow(href: string) {
71
72
  setOpen(false);
@@ -88,7 +89,7 @@ export function SiteHeader({
88
89
  {...tabbable}
89
90
  type="button"
90
91
  aria-expanded={open}
91
- aria-controls="site-menu"
92
+ aria-controls={menuId}
92
93
  aria-label={open ? closeMenuLabel : menuLabel}
93
94
  onClick={() => setOpen((value) => !value)}
94
95
  className="text-base-800 hover:bg-base-200 -mr-2 inline-flex size-9 items-center justify-center rounded-md transition-colors md:hidden [&_svg]:size-5"
@@ -97,7 +98,7 @@ export function SiteHeader({
97
98
  </button>
98
99
  {open ? (
99
100
  <nav
100
- id="site-menu"
101
+ id={menuId}
101
102
  aria-label={navigationLabel}
102
103
  className="glass-menu absolute inset-x-3 top-[calc(100%+0.5rem)] flex flex-col gap-1 rounded-2xl p-2 md:hidden"
103
104
  >
@@ -9,7 +9,7 @@ interface ActionModalProps {
9
9
  actionColor?: 'danger' | 'primary';
10
10
  actionDisabled?: boolean;
11
11
  actionLabel: string;
12
- children: ReactNode;
12
+ children?: ReactNode;
13
13
  bodyLayout?: 'scroll' | 'fill';
14
14
  description: string;
15
15
  footerLeading?: ReactNode;
@@ -25,7 +25,7 @@ export function ProductIndex({
25
25
  return (
26
26
  <ul aria-label={label} className="mx-auto flex max-w-[880px] flex-wrap justify-center gap-3">
27
27
  {products.map((product) => (
28
- <li key={product.href} className="w-full sm:w-[calc(50%-0.375rem)] lg:w-[calc(50%-0.375rem)]">
28
+ <li key={product.href} className="w-full sm:w-[calc(50%-0.375rem)]">
29
29
  <a
30
30
  {...tabbable}
31
31
  href={product.href}
@@ -0,0 +1,7 @@
1
+ /* styles.css plus the marketing showcase (device frames, stage, server unit): the import for a website. */
2
+ @import './foundations/theme.css';
3
+ @import './showcase/devices.css';
4
+ @import './showcase/stage.css';
5
+ @import './showcase/server-unit.css';
6
+
7
+ @source './';
package/src/styles.css CHANGED
@@ -1,5 +1,7 @@
1
1
  /* Everything an app needs from the design system in one import: Tailwind, ninna-ui's preset, Lar's tokens and
2
- base styles, and the components' utilities, which Tailwind finds by scanning this package's sources. */
2
+ base styles, and the components' utilities, which Tailwind finds by scanning this package's sources. The
3
+ marketing showcase is left out; a site that uses it imports showcase.css instead. */
3
4
  @import './foundations/theme.css';
4
5
 
5
6
  @source './';
7
+ @source not './showcase';