@charcuterie/ui 1.0.1 → 1.1.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.
Files changed (32) hide show
  1. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts +35 -0
  2. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts.map +1 -0
  3. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js +33 -0
  4. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js.map +1 -0
  5. package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts +54 -0
  6. package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts.map +1 -0
  7. package/dist/ColorSchemeToggle/ColorSchemeToggle.js +30 -0
  8. package/dist/ColorSchemeToggle/ColorSchemeToggle.js.map +1 -0
  9. package/dist/Lightbox/Lightbox.d.ts +57 -0
  10. package/dist/Lightbox/Lightbox.d.ts.map +1 -0
  11. package/dist/Lightbox/Lightbox.js +50 -0
  12. package/dist/Lightbox/Lightbox.js.map +1 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -0
  16. package/dist/index.js.map +1 -1
  17. package/package.json +3 -3
  18. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.mdx +70 -0
  19. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +118 -0
  20. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.test.tsx +53 -0
  21. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx +90 -0
  22. package/src/ColorSchemeToggle/ColorSchemeToggle.mdx +48 -0
  23. package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +161 -0
  24. package/src/ColorSchemeToggle/ColorSchemeToggle.test.tsx +80 -0
  25. package/src/ColorSchemeToggle/ColorSchemeToggle.tsx +107 -0
  26. package/src/Lightbox/Lightbox.mdx +62 -0
  27. package/src/Lightbox/Lightbox.stories.tsx +133 -0
  28. package/src/Lightbox/Lightbox.test.tsx +135 -0
  29. package/src/Lightbox/Lightbox.tsx +143 -0
  30. package/src/icons.storyHelpers.tsx +22 -0
  31. package/src/index.ts +9 -0
  32. package/src/sourceRules.test.ts +17 -1
@@ -0,0 +1,35 @@
1
+ import type { ColorSchemeApplier, ColorSchemePersistence, ColorSchemeResolver } from "@charcuterie/logic";
2
+ import type { ReactNode } from "react";
3
+ import type { ColorSchemeIcons, ColorSchemeToggleProps } from "../ColorSchemeToggle/ColorSchemeToggle.tsx";
4
+ export type ColorSchemeSwitcherProps = Pick<ColorSchemeToggleProps, "appearance" | "className" | "isDisabled" | "label" | "order" | "size"> & {
5
+ /** Override where the resolved scheme is written. Default `<html>`. */
6
+ apply?: ColorSchemeApplier;
7
+ icons: ColorSchemeIcons;
8
+ /** Override persistence. Default `localStorage` under `storageKey`. */
9
+ persistence?: ColorSchemePersistence;
10
+ /**
11
+ * Override the OS resolver. Default `matchMedia`. This is the seam
12
+ * the image-viewer passes Electron `nativeTheme` into, and a
13
+ * future React-Native app passes `Appearance` — the whole reason
14
+ * the browser dependency is isolated to this one layer.
15
+ */
16
+ resolver?: ColorSchemeResolver;
17
+ storageKey?: string;
18
+ };
19
+ /**
20
+ * The connected colour-scheme switcher — Layer 3, and the only
21
+ * layer that touches the browser.
22
+ *
23
+ * It is a thin wrapper: it wires `useColorScheme` to the three
24
+ * browser defaults from `@charcuterie/logic/browser`
25
+ * (`matchMedia` + `localStorage` + `data-scheme` on `<html>`), and
26
+ * renders the presentational `ColorSchemeToggle`. Every seam is
27
+ * overridable, so a non-browser host swaps `resolver`/`persistence`/
28
+ * `apply` and the same component drives Electron or React-Native.
29
+ *
30
+ * The seams are built once (lazy `useState`) so the `matchMedia`
31
+ * query and the core survive re-renders and StrictMode's double
32
+ * mount.
33
+ */
34
+ export declare const ColorSchemeSwitcher: ({ apply, icons, order, persistence, resolver, storageKey, ...toggleProps }: ColorSchemeSwitcherProps) => ReactNode;
35
+ //# sourceMappingURL=ColorSchemeSwitcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorSchemeSwitcher.d.ts","sourceRoot":"","sources":["../../src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,oBAAoB,CAAA;AAO3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,4CAA4C,CAAA;AAGnD,MAAM,MAAM,wBAAwB,GAAG,IAAI,CACzC,sBAAsB,EACpB,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,OAAO,GACP,OAAO,GACP,MAAM,CACT,GAAG;IACF,uEAAuE;IACvE,KAAK,CAAC,EAAE,kBAAkB,CAAA;IAC1B,KAAK,EAAE,gBAAgB,CAAA;IACvB,uEAAuE;IACvE,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAAI,4EAQjC,wBAAwB,KAAG,SAsB7B,CAAA"}
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useColorScheme } from "@charcuterie/logic";
3
+ import { dataSchemeApplier, localStoragePersistence, matchMediaResolver, } from "@charcuterie/logic/browser";
4
+ import { useState } from "react";
5
+ import { ColorSchemeToggle } from "../ColorSchemeToggle/ColorSchemeToggle.js";
6
+ /**
7
+ * The connected colour-scheme switcher — Layer 3, and the only
8
+ * layer that touches the browser.
9
+ *
10
+ * It is a thin wrapper: it wires `useColorScheme` to the three
11
+ * browser defaults from `@charcuterie/logic/browser`
12
+ * (`matchMedia` + `localStorage` + `data-scheme` on `<html>`), and
13
+ * renders the presentational `ColorSchemeToggle`. Every seam is
14
+ * overridable, so a non-browser host swaps `resolver`/`persistence`/
15
+ * `apply` and the same component drives Electron or React-Native.
16
+ *
17
+ * The seams are built once (lazy `useState`) so the `matchMedia`
18
+ * query and the core survive re-renders and StrictMode's double
19
+ * mount.
20
+ */
21
+ export const ColorSchemeSwitcher = ({ apply, icons, order, persistence, resolver, storageKey, ...toggleProps }) => {
22
+ const [seams] = useState(() => ({
23
+ apply: apply ?? dataSchemeApplier(),
24
+ persistence: persistence ?? localStoragePersistence(storageKey),
25
+ resolver: resolver ?? matchMediaResolver(),
26
+ }));
27
+ const { cycle, mode } = useColorScheme({
28
+ ...seams,
29
+ order,
30
+ });
31
+ return (_jsx(ColorSchemeToggle, { ...toggleProps, icons: icons, mode: mode, onCycle: cycle, order: order }));
32
+ };
33
+ //# sourceMappingURL=ColorSchemeSwitcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorSchemeSwitcher.js","sourceRoot":"","sources":["../../src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx"],"names":[],"mappings":";AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAMhC,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA4C,CAAA;AA0B9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,KAAK,EACL,KAAK,EACL,KAAK,EACL,WAAW,EACX,QAAQ,EACR,UAAU,EACV,GAAG,WAAW,EACW,EAAa,EAAE;IACxC,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9B,KAAK,EAAE,KAAK,IAAI,iBAAiB,EAAE;QACnC,WAAW,EACT,WAAW,IAAI,uBAAuB,CAAC,UAAU,CAAC;QACpD,QAAQ,EAAE,QAAQ,IAAI,kBAAkB,EAAE;KAC3C,CAAC,CAAC,CAAA;IAEH,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,cAAc,CAAC;QACrC,GAAG,KAAK;QACR,KAAK;KACN,CAAC,CAAA;IAEF,OAAO,CACL,KAAC,iBAAiB,OACZ,WAAW,EACf,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,KAAK,EACd,KAAK,EAAE,KAAK,GACZ,CACH,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,54 @@
1
+ import type { ColorSchemeMode } from "@charcuterie/logic";
2
+ import type { ControlSize } from "@charcuterie/tokens";
3
+ import type { ReactNode } from "react";
4
+ import type { IntentAppearance } from "../intentStyles.ts";
5
+ /**
6
+ * One `ReactNode` per mode. The library ships **no icons** — lucide
7
+ * (ISC) is the fleet recommendation — so an app passes its own
8
+ * `Sun` / `Moon` / `Monitor`, exactly as `IconButton` takes its
9
+ * glyph as `children`
10
+ * ([decision](../../../../docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md)).
11
+ */
12
+ export type ColorSchemeIcons = Record<ColorSchemeMode, ReactNode>;
13
+ export type ColorSchemeToggleProps = {
14
+ appearance?: IntentAppearance;
15
+ className?: string;
16
+ icons: ColorSchemeIcons;
17
+ isDisabled?: boolean;
18
+ /**
19
+ * Builds the accessible name from the current and next mode.
20
+ * Default announces the current mode and where a press goes —
21
+ * "Colour scheme: dark. Activate to switch to system." — so the
22
+ * name updates as the control cycles and
23
+ * `getByRole("button", { name: /colour scheme/i })` resolves.
24
+ */
25
+ label?: (mode: ColorSchemeMode, nextMode: ColorSchemeMode) => string;
26
+ mode: ColorSchemeMode;
27
+ /**
28
+ * Called with the next mode on activation. **Controlled**: this
29
+ * component stores nothing — it renders `mode` and reports intent,
30
+ * which is what keeps it free of `matchMedia`/`document` and
31
+ * usable in any runtime. `<ColorSchemeSwitcher>` wires this to the
32
+ * `useColorScheme` hook; a plain consumer wires it to its own
33
+ * state.
34
+ */
35
+ onCycle: (nextMode: ColorSchemeMode) => void;
36
+ order?: readonly ColorSchemeMode[];
37
+ size?: ControlSize;
38
+ };
39
+ /**
40
+ * The cycling colour-scheme button — light → dark → system → light.
41
+ *
42
+ * Layer 2 of the three: presentational and controlled, with no
43
+ * browser coupling. It is an `IconButton`, so it inherits the one
44
+ * rule that makes the fleet agent-drivable — a glyph is not a name,
45
+ * and this control's name states the current mode rather than
46
+ * leaving a screen reader to read a sun path as nothing.
47
+ *
48
+ * A *segmented* all-three-visible form was considered and deferred
49
+ * (owner chose cycling-only, 2026-08-03); `icons` and `order` are
50
+ * shaped so a `ColorSchemePicker` could join later without changing
51
+ * this API.
52
+ */
53
+ export declare const ColorSchemeToggle: ({ appearance, className, icons, isDisabled, label, mode, onCycle, order, size, }: ColorSchemeToggleProps) => ReactNode;
54
+ //# sourceMappingURL=ColorSchemeToggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorSchemeToggle.d.ts","sourceRoot":"","sources":["../../src/ColorSchemeToggle/ColorSchemeToggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAKzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CACnC,eAAe,EACf,SAAS,CACV,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,gBAAgB,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CACN,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,eAAe,KACtB,MAAM,CAAA;IACX,IAAI,EAAE,eAAe,CAAA;IACrB;;;;;;;OAOG;IACH,OAAO,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,IAAI,CAAA;IAC5C,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IAClC,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAcD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,GAAI,kFAU/B,sBAAsB,KAAG,SAiB3B,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DEFAULT_COLOR_SCHEME_ORDER, nextColorSchemeMode, } from "@charcuterie/logic";
3
+ import { IconButton } from "../IconButton/IconButton.js";
4
+ const MODE_LABEL = {
5
+ dark: "Dark",
6
+ light: "Light",
7
+ system: "System",
8
+ };
9
+ const defaultLabel = (mode, nextMode) => `Colour scheme: ${MODE_LABEL[mode]}. Activate to switch to ${MODE_LABEL[nextMode]}.`;
10
+ /**
11
+ * The cycling colour-scheme button — light → dark → system → light.
12
+ *
13
+ * Layer 2 of the three: presentational and controlled, with no
14
+ * browser coupling. It is an `IconButton`, so it inherits the one
15
+ * rule that makes the fleet agent-drivable — a glyph is not a name,
16
+ * and this control's name states the current mode rather than
17
+ * leaving a screen reader to read a sun path as nothing.
18
+ *
19
+ * A *segmented* all-three-visible form was considered and deferred
20
+ * (owner chose cycling-only, 2026-08-03); `icons` and `order` are
21
+ * shaped so a `ColorSchemePicker` could join later without changing
22
+ * this API.
23
+ */
24
+ export const ColorSchemeToggle = ({ appearance = "ghost", className, icons, isDisabled = false, label = defaultLabel, mode, onCycle, order = DEFAULT_COLOR_SCHEME_ORDER, size = "md", }) => {
25
+ const nextMode = nextColorSchemeMode(mode, order);
26
+ return (_jsx(IconButton, { appearance: appearance, className: className, isDisabled: isDisabled, label: label(mode, nextMode), onClick: () => {
27
+ onCycle(nextMode);
28
+ }, size: size, children: icons[mode] }));
29
+ };
30
+ //# sourceMappingURL=ColorSchemeToggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorSchemeToggle.js","sourceRoot":"","sources":["../../src/ColorSchemeToggle/ColorSchemeToggle.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,oBAAoB,CAAA;AAI3B,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA8B,CAAA;AA6CzD,MAAM,UAAU,GAAoC;IAClD,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACjB,CAAA;AAED,MAAM,YAAY,GAAG,CACnB,IAAqB,EACrB,QAAyB,EACzB,EAAE,CACF,kBAAkB,UAAU,CAAC,IAAI,CAAC,2BAA2B,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAA;AAEtF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAChC,UAAU,GAAG,OAAO,EACpB,SAAS,EACT,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,GAAG,YAAY,EACpB,IAAI,EACJ,OAAO,EACP,KAAK,GAAG,0BAA0B,EAClC,IAAI,GAAG,IAAI,GACY,EAAa,EAAE;IACtC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAEjD,OAAO,CACL,KAAC,UAAU,IACT,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,EAC5B,OAAO,EAAE,GAAG,EAAE;YACZ,OAAO,CAAC,QAAQ,CAAC,CAAA;QACnB,CAAC,EACD,IAAI,EAAE,IAAI,YAET,KAAK,CAAC,IAAI,CAAC,GACD,CACd,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from "react";
2
+ export type LightboxProps = {
3
+ /**
4
+ * Required, and the whole reason this is not a bare `<img>` in a
5
+ * dialog. It names both the enlarged image and — via `heading`'s
6
+ * default below — the dialog itself, and a poster grid of unnamed
7
+ * images is unnavigable rather than merely imperfect. Same rule
8
+ * `MediaTile` makes.
9
+ */
10
+ alt: string;
11
+ /** Shown under the enlarged image. A year, a source, a filename. */
12
+ caption?: ReactNode;
13
+ /** Applied to the built-in trigger button, not the dialog. */
14
+ className?: string;
15
+ /**
16
+ * The dialog's accessible name and visible heading. Defaults to
17
+ * `alt`, which is right when the thumbnail's alt is already the
18
+ * title ("THE OUTFIT poster"); pass it when the two should differ.
19
+ */
20
+ heading?: string;
21
+ /**
22
+ * **Controlled** open state. Omit for the uncontrolled default,
23
+ * where the trigger owns it through `useVisibility`. Present means
24
+ * the caller owns it and the built-in trigger becomes a plain
25
+ * button that only *reports* intent through `onOpenChange`.
26
+ */
27
+ isOpen?: boolean;
28
+ onOpenChange?: (isOpen: boolean) => void;
29
+ src: string;
30
+ /**
31
+ * The clickable thumbnail — a smaller `<img>`, usually. Omitted in
32
+ * the controlled, trigger-elsewhere case, where something else in
33
+ * the page opens the lightbox and this renders only the overlay.
34
+ */
35
+ thumbnail?: ReactNode;
36
+ };
37
+ /**
38
+ * A thumbnail that opens its own full-size view — the one image
39
+ * interaction three apps hand-roll and none get the focus trap or
40
+ * the scroll lock right. It is a **skin over `Modal`**, not a second
41
+ * dialog: `Modal` owns `showModal()`, the top layer, Escape, the
42
+ * `::backdrop` scrim and `lockScrollBehind`, and everything that
43
+ * makes those correct in every browser the fleet runs. What this
44
+ * adds is the image-specific part — a `cursor-zoom-in` trigger and
45
+ * an `object-contain` image clamped to the viewport so a 2:3 poster
46
+ * and a 16:9 still both land whole.
47
+ *
48
+ * ### Controlled and not, one component
49
+ *
50
+ * `useVisibility` runs unconditionally — hooks cannot be conditional
51
+ * — but is *consulted* only when `isOpen` is absent. A caller that
52
+ * passes `isOpen` owns the truth; the internal store is then an
53
+ * unread two-field object, which is cheaper than splitting this into
54
+ * two components that share a Modal body.
55
+ */
56
+ export declare const Lightbox: ({ alt, caption, className, heading, isOpen, onOpenChange, src, thumbnail, }: LightboxProps) => ReactNode;
57
+ //# sourceMappingURL=Lightbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Lightbox.d.ts","sourceRoot":"","sources":["../../src/Lightbox/Lightbox.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,QAAQ,GAAI,6EAStB,aAAa,KAAG,SAwElB,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useVisibility } from "@charcuterie/logic";
3
+ import { Modal } from "../Modal/Modal.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ /**
6
+ * A thumbnail that opens its own full-size view — the one image
7
+ * interaction three apps hand-roll and none get the focus trap or
8
+ * the scroll lock right. It is a **skin over `Modal`**, not a second
9
+ * dialog: `Modal` owns `showModal()`, the top layer, Escape, the
10
+ * `::backdrop` scrim and `lockScrollBehind`, and everything that
11
+ * makes those correct in every browser the fleet runs. What this
12
+ * adds is the image-specific part — a `cursor-zoom-in` trigger and
13
+ * an `object-contain` image clamped to the viewport so a 2:3 poster
14
+ * and a 16:9 still both land whole.
15
+ *
16
+ * ### Controlled and not, one component
17
+ *
18
+ * `useVisibility` runs unconditionally — hooks cannot be conditional
19
+ * — but is *consulted* only when `isOpen` is absent. A caller that
20
+ * passes `isOpen` owns the truth; the internal store is then an
21
+ * unread two-field object, which is cheaper than splitting this into
22
+ * two components that share a Modal body.
23
+ */
24
+ export const Lightbox = ({ alt, caption, className, heading, isOpen, onOpenChange, src, thumbnail, }) => {
25
+ const isControlled = isOpen !== undefined;
26
+ const { hide, isVisible, show } = useVisibility();
27
+ const isShown = isControlled ? isOpen : isVisible;
28
+ const requestOpen = () => {
29
+ if (!isControlled) {
30
+ show();
31
+ }
32
+ onOpenChange?.(true);
33
+ };
34
+ const requestClose = () => {
35
+ if (!isControlled) {
36
+ hide();
37
+ }
38
+ onOpenChange?.(false);
39
+ };
40
+ return (_jsxs(_Fragment, { children: [thumbnail === undefined ? null : (_jsx("button", { "aria-label": `Enlarge ${heading ?? alt}`, className: toClassName(
41
+ // `cursor-zoom-in` is the affordance a bare `<img>`
42
+ // never had, and the focus ring is the fleet's standard
43
+ // — the same one `MediaTile`'s link wears.
44
+ "group block cursor-zoom-in rounded-md outline-offset-2 focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-focus-ring", className), onClick: requestOpen, type: "button", children: thumbnail })), _jsx(Modal, { heading: heading ?? alt, isVisible: isShown, onClose: requestClose, size: "xl", children: _jsxs("figure", { className: "flex flex-col items-center gap-3", children: [_jsx("img", { alt: alt,
45
+ // `object-contain`, not `cover`: the enlarged view must
46
+ // show the whole image, and the clamp is `dvh` for the
47
+ // same address-bar reason `Modal`'s is.
48
+ className: "max-h-[75dvh] w-auto max-w-full rounded-md object-contain", src: src }), caption ? (_jsx("figcaption", { className: "text-center text-content-muted text-sm", children: caption })) : null] }) })] }));
49
+ };
50
+ //# sourceMappingURL=Lightbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Lightbox.js","sourceRoot":"","sources":["../../src/Lightbox/Lightbox.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAoB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAsC/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EACvB,GAAG,EACH,OAAO,EACP,SAAS,EACT,OAAO,EACP,MAAM,EACN,YAAY,EACZ,GAAG,EACH,SAAS,GACK,EAAa,EAAE;IAC7B,MAAM,YAAY,GAAG,MAAM,KAAK,SAAS,CAAA;IAEzC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,aAAa,EAAE,CAAA;IAEjD,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAEjD,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;QAED,YAAY,EAAE,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;QAED,YAAY,EAAE,CAAC,KAAK,CAAC,CAAA;IACvB,CAAC,CAAA;IAED,OAAO,CACL,8BACG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAChC,+BAMc,WAAW,OAAO,IAAI,GAAG,EAAE,EACvC,SAAS,EAAE,WAAW;gBACpB,oDAAoD;gBACpD,wDAAwD;gBACxD,2CAA2C;gBAC3C,uKAAuK,EACvK,SAAS,CACV,EACD,OAAO,EAAE,WAAW,EACpB,IAAI,EAAC,QAAQ,YAEZ,SAAS,GACH,CACV,EAED,KAAC,KAAK,IACJ,OAAO,EAAE,OAAO,IAAI,GAAG,EACvB,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAC,IAAI,YAET,kBAAQ,SAAS,EAAC,kCAAkC,aAClD,cACE,GAAG,EAAE,GAAG;4BACR,wDAAwD;4BACxD,uDAAuD;4BACvD,wCAAwC;4BACxC,SAAS,EAAC,2DAA2D,EACrE,GAAG,EAAE,GAAG,GACR,EAED,OAAO,CAAC,CAAC,CAAC,CACT,qBAAY,SAAS,EAAC,wCAAwC,YAC3D,OAAO,GACG,CACd,CAAC,CAAC,CAAC,IAAI,IACD,GACH,IACP,CACJ,CAAA;AACH,CAAC,CAAA"}
package/dist/index.d.ts CHANGED
@@ -40,6 +40,10 @@ export type { ButtonProps } from "./Button/Button.tsx";
40
40
  export { Button } from "./Button/Button.tsx";
41
41
  export type { CardPadding, CardProps, CardSurface, } from "./Card/Card.tsx";
42
42
  export { Card } from "./Card/Card.tsx";
43
+ export type { ColorSchemeSwitcherProps } from "./ColorSchemeSwitcher/ColorSchemeSwitcher.tsx";
44
+ export { ColorSchemeSwitcher } from "./ColorSchemeSwitcher/ColorSchemeSwitcher.tsx";
45
+ export type { ColorSchemeIcons, ColorSchemeToggleProps, } from "./ColorSchemeToggle/ColorSchemeToggle.tsx";
46
+ export { ColorSchemeToggle } from "./ColorSchemeToggle/ColorSchemeToggle.tsx";
43
47
  export type { BadgeSize } from "./controlStyles.ts";
44
48
  export { BADGE_SIZE_CLASS, CONTROL_SIZE_CLASS, DOT_SIZE_CLASS, ICON_CONTROL_SIZE_CLASS, MIN_TOUCH_TARGET_CLASS, SPINNER_SIZE_CLASS, } from "./controlStyles.ts";
45
49
  export type { EmptyStateProps, EmptyStateSize, } from "./EmptyState/EmptyState.tsx";
@@ -54,6 +58,8 @@ export type { IconButtonProps } from "./IconButton/IconButton.tsx";
54
58
  export { IconButton } from "./IconButton/IconButton.tsx";
55
59
  export type { IntentAppearance } from "./intentStyles.ts";
56
60
  export { DISABLED_CLASS, FOCUS_RING_CLASS, INTENT_APPEARANCE_CLASS, INTENT_CONTENT_CLASS, INTENT_HOVER_CLASS, INTENT_SOLID_FILL_CLASS, } from "./intentStyles.ts";
61
+ export type { LightboxProps } from "./Lightbox/Lightbox.tsx";
62
+ export { Lightbox } from "./Lightbox/Lightbox.tsx";
57
63
  export type { LiveStatusIndicatorProps, LiveStatusIndicatorSize, } from "./LiveStatusIndicator/LiveStatusIndicator.tsx";
58
64
  export { LiveStatusIndicator } from "./LiveStatusIndicator/LiveStatusIndicator.tsx";
59
65
  export type { LogLine, LogViewerProps, } from "./LogViewer/LogViewer.tsx";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,YAAY,EACV,qBAAqB,EACrB,aAAa,EACb,cAAc,GACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,YAAY,EACV,UAAU,EACV,SAAS,GACV,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzC,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,YAAY,EACV,WAAW,EACX,SAAS,EACT,WAAW,GACZ,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EACV,eAAe,EACf,cAAc,GACf,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AACxD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAC9D,YAAY,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,+CAA+C,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAA;AACnF,YAAY,EACV,OAAO,EACP,cAAc,GACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,YAAY,EACV,cAAc,EACd,cAAc,GACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAC7D,YAAY,EACV,QAAQ,EACR,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,YAAY,EACV,UAAU,EACV,SAAS,GACV,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAC/C,YAAY,EACV,gBAAgB,EAChB,eAAe,GAChB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AACvC,YAAY,EACV,qBAAqB,EACrB,aAAa,GACd,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAA;AAC1E,YAAY,EACV,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,YAAY,EACV,aAAa,EACb,aAAa,GACd,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAClD,YAAY,EACV,wBAAwB,EACxB,aAAa,GACd,MAAM,+CAA+C,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAA;AACnF,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAC/C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,OAAO,EACP,cAAc,EACd,eAAe,EACf,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzC,YAAY,EACV,WAAW,EACX,gBAAgB,GACjB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -35,6 +35,8 @@ export { Alert } from "./Alert/Alert.js";
35
35
  export { Badge } from "./Badge/Badge.js";
36
36
  export { Button } from "./Button/Button.js";
37
37
  export { Card } from "./Card/Card.js";
38
+ export { ColorSchemeSwitcher } from "./ColorSchemeSwitcher/ColorSchemeSwitcher.js";
39
+ export { ColorSchemeToggle } from "./ColorSchemeToggle/ColorSchemeToggle.js";
38
40
  export { BADGE_SIZE_CLASS, CONTROL_SIZE_CLASS, DOT_SIZE_CLASS, ICON_CONTROL_SIZE_CLASS, MIN_TOUCH_TARGET_CLASS, SPINNER_SIZE_CLASS, } from "./controlStyles.js";
39
41
  export { EmptyState } from "./EmptyState/EmptyState.js";
40
42
  export { Field } from "./Field/Field.js";
@@ -42,6 +44,7 @@ export { FieldGroup } from "./Field/FieldGroup.js";
42
44
  export { FileDropZone } from "./FileDropZone/FileDropZone.js";
43
45
  export { IconButton } from "./IconButton/IconButton.js";
44
46
  export { DISABLED_CLASS, FOCUS_RING_CLASS, INTENT_APPEARANCE_CLASS, INTENT_CONTENT_CLASS, INTENT_HOVER_CLASS, INTENT_SOLID_FILL_CLASS, } from "./intentStyles.js";
47
+ export { Lightbox } from "./Lightbox/Lightbox.js";
45
48
  export { LiveStatusIndicator } from "./LiveStatusIndicator/LiveStatusIndicator.js";
46
49
  export { LogViewer } from "./LogViewer/LogViewer.js";
47
50
  export { MediaTile } from "./MediaTile/MediaTile.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAwB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAK1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAKtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAE3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AAKvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAO1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAO1B,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAyB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA2C,CAAA;AAE7E,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAwB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAKtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAE3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AAKvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAO1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAO1B,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAyB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcuterie/ui",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,8 +43,8 @@
43
43
  "typecheck": "tsc --noEmit"
44
44
  },
45
45
  "dependencies": {
46
- "@charcuterie/logic": "1.0.1",
47
- "@charcuterie/tokens": "1.0.0",
46
+ "@charcuterie/logic": "1.1.0",
47
+ "@charcuterie/tokens": "1.1.0",
48
48
  "@floating-ui/react": "^0.27.20"
49
49
  },
50
50
  "peerDependencies": {
@@ -0,0 +1,70 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as ColorSchemeSwitcherStories from "./ColorSchemeSwitcher.stories";
4
+
5
+ <Meta of={ColorSchemeSwitcherStories} />
6
+
7
+ # ColorSchemeSwitcher
8
+
9
+ The connected switcher — **Layer 3, and the only layer that touches the browser.** It wires
10
+ the [`useColorScheme`](/) hook to the three defaults from `@charcuterie/logic/browser`
11
+ (`matchMedia` + `localStorage` + `data-scheme` on `<html>`) and renders the presentational
12
+ [`ColorSchemeToggle`](?path=/docs/components-colorschemetoggle--docs). Drop it in and the app
13
+ follows the OS, remembers the user's choice, and re-themes with zero React re-render.
14
+
15
+ <Canvas of={ColorSchemeSwitcherStories.Default} />
16
+
17
+ ## Props
18
+
19
+ <Controls />
20
+
21
+ ## Every seam is overridable — that is the point
22
+
23
+ `resolver`, `persistence`, and `apply` all default to the browser implementations but can be
24
+ replaced. Isolating the browser dependency to this one component is what lets a **non-browser
25
+ host** reuse the entire stack unchanged.
26
+
27
+ ### Electron (`nativeTheme`) — copy-paste
28
+
29
+ The image-viewer is Electron, which owns the OS appearance through `nativeTheme`. Pass it as
30
+ the resolver and nothing else changes:
31
+
32
+ ```tsx
33
+ import { ColorSchemeSwitcher } from "@charcuterie/ui";
34
+ import type { ColorSchemeResolver } from "@charcuterie/logic";
35
+ import { nativeTheme } from "electron";
36
+ import { Monitor, Moon, Sun } from "lucide-react";
37
+
38
+ const nativeThemeResolver: ColorSchemeResolver = {
39
+ get: () => (nativeTheme.shouldUseDarkColors ? "dark" : "light"),
40
+ subscribe: (listener) => {
41
+ nativeTheme.on("updated", listener);
42
+ return () => nativeTheme.off("updated", listener);
43
+ },
44
+ };
45
+
46
+ <ColorSchemeSwitcher
47
+ resolver={nativeThemeResolver}
48
+ icons={{ light: <Sun />, dark: <Moon />, system: <Monitor /> }}
49
+ />;
50
+ ```
51
+
52
+ `persistence` and `apply` keep their browser defaults in an Electron **renderer** (it has
53
+ `localStorage` and a `document`). Only the resolver needs replacing, because only the OS
54
+ answer comes from outside the DOM.
55
+
56
+ ### React-Native — the shape
57
+
58
+ RN has no `data-scheme` and no `matchMedia`. Replace **all three** seams: a resolver over
59
+ `Appearance.getColorScheme()` / `Appearance.addChangeListener`, a persistence over
60
+ `AsyncStorage`, and an `apply` that sets a theme **context value** instead of a DOM
61
+ attribute. `ColorSchemeToggle` renders as-is; only the seams differ.
62
+
63
+ ## First paint
64
+
65
+ Following the OS/persisted choice means `<html data-scheme>` must be set **before first
66
+ paint**, or the page flashes the wrong theme for a frame. `@charcuterie/tokens` ships the
67
+ inline snippet — `buildFirstPaintScript(variant, { storageKey })`, emitted to
68
+ `dist/first-paint-script.html`. Paste it into `<head>` before any stylesheet, and keep its
69
+ `storageKey` equal to the one `localStoragePersistence(key)` uses — the shared default is
70
+ `DEFAULT_COLOR_SCHEME_STORAGE_KEY` (`"charcuterie-scheme"`), exported from both packages.
@@ -0,0 +1,118 @@
1
+ import type {
2
+ ColorSchemePersistence,
3
+ ColorSchemeResolver,
4
+ ResolvedColorScheme,
5
+ } from "@charcuterie/logic"
6
+ import type { Meta, StoryObj } from "@storybook/react"
7
+ import { useRef, useState } from "react"
8
+ import type { ColorSchemeIcons } from "../ColorSchemeToggle/ColorSchemeToggle.tsx"
9
+ import {
10
+ MonitorIcon,
11
+ MoonIcon,
12
+ SunIcon,
13
+ } from "../icons.storyHelpers.tsx"
14
+ import { ColorSchemeSwitcher } from "./ColorSchemeSwitcher.tsx"
15
+
16
+ const ICONS: ColorSchemeIcons = {
17
+ dark: <MoonIcon />,
18
+ light: <SunIcon />,
19
+ system: <MonitorIcon />,
20
+ }
21
+
22
+ /**
23
+ * A controllable stand-in for `matchMedia` — the same shape Electron
24
+ * `nativeTheme` and RN `Appearance` satisfy. The stories inject it
25
+ * instead of the real one so the board never reads the CI machine's
26
+ * OS setting and never writes `localStorage`.
27
+ */
28
+ const makeFakeResolver = (
29
+ initial: ResolvedColorScheme,
30
+ ): ColorSchemeResolver => ({
31
+ get: () => initial,
32
+ subscribe: () => () => {},
33
+ })
34
+
35
+ const inMemoryPersistence = (): ColorSchemePersistence => {
36
+ let value:
37
+ | Parameters<ColorSchemePersistence["write"]>[0]
38
+ | null = null
39
+
40
+ return {
41
+ read: () => value,
42
+ write: (mode) => {
43
+ value = mode
44
+ },
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Drives a **scoped** demo: the switcher writes `data-scheme` onto
50
+ * this panel rather than `<html>`, so the surrounding Storybook
51
+ * chrome is untouched and the effect is contained and visible. In a
52
+ * real app the default `apply` targets `<html>`.
53
+ */
54
+ const ScopedSwitcherDemo = () => {
55
+ const panelRef = useRef<HTMLDivElement>(null)
56
+
57
+ const [seams] = useState(() => ({
58
+ persistence: inMemoryPersistence(),
59
+ resolver: makeFakeResolver("dark"),
60
+ }))
61
+
62
+ return (
63
+ <div
64
+ className="inline-flex items-center gap-3 rounded-lg border border-border-subtle bg-surface-base p-3 text-content-primary"
65
+ data-scheme="dark"
66
+ ref={panelRef}
67
+ >
68
+ <ColorSchemeSwitcher
69
+ apply={(resolvedScheme) => {
70
+ panelRef.current?.setAttribute(
71
+ "data-scheme",
72
+ resolvedScheme,
73
+ )
74
+ }}
75
+ icons={ICONS}
76
+ persistence={seams.persistence}
77
+ resolver={seams.resolver}
78
+ />
79
+
80
+ <span className="text-sm text-content-secondary">
81
+ This panel follows the switcher.
82
+ </span>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ const meta = {
88
+ title: "Components/ColorSchemeSwitcher",
89
+ component: ColorSchemeSwitcher,
90
+ parameters: { layout: "padded" },
91
+ // The three injected seams are functions/objects, not values a
92
+ // Controls panel can render — `react-docgen` sees them as opaque
93
+ // names from `@charcuterie/logic`. Disabled explicitly rather than
94
+ // left to fall through to the object control.
95
+ argTypes: {
96
+ apply: { control: false },
97
+ persistence: { control: false },
98
+ resolver: { control: false },
99
+ },
100
+ args: {
101
+ icons: ICONS,
102
+ },
103
+ } satisfies Meta<typeof ColorSchemeSwitcher>
104
+
105
+ export default meta
106
+
107
+ type Story = StoryObj<typeof meta>
108
+
109
+ /**
110
+ * Connected: the switcher builds its own state through
111
+ * `useColorScheme` and applies the resolved scheme. Here the OS
112
+ * resolver and persistence are injected fakes and the applier is
113
+ * scoped to the demo panel, which is exactly how a non-browser host
114
+ * (Electron `nativeTheme`) wires it — by replacing those seams.
115
+ */
116
+ export const Default: Story = {
117
+ render: () => <ScopedSwitcherDemo />,
118
+ }
@@ -0,0 +1,53 @@
1
+ import { composeStories } from "@storybook/react"
2
+ import { expect, userEvent, within } from "storybook/test"
3
+ import { test } from "vitest"
4
+
5
+ import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
6
+ import { mountStory } from "../mountStory.testHelpers.ts"
7
+ import { expectAgentDrivable } from "../testing/index.ts"
8
+ import * as stories from "./ColorSchemeSwitcher.stories.tsx"
9
+
10
+ const { Default } = composeStories(stories)
11
+
12
+ /**
13
+ * The connected switcher is agent-drivable and cycles like the
14
+ * toggle — the extra thing proven here is that the resolved scheme
15
+ * reaches the applier: the demo panel's `data-scheme` tracks it.
16
+ */
17
+ test("it renders a named, drivable button", async () => {
18
+ const { canvas } = await mountStory(Default)
19
+
20
+ expectAgentDrivable(canvas, {
21
+ name: /colour scheme/i,
22
+ role: "button",
23
+ })
24
+ })
25
+
26
+ test("system resolves through the injected resolver to the panel", async () => {
27
+ const { canvasElement } = await mountStory(Default)
28
+
29
+ const panel = canvasElement.querySelector("[data-scheme]")
30
+
31
+ // The injected resolver answers "dark", mode starts on system.
32
+ expect(panel?.getAttribute("data-scheme")).toBe("dark")
33
+ })
34
+
35
+ test("cycling to a concrete mode re-applies the scheme", async () => {
36
+ const { canvasElement } = await mountStory(Default)
37
+
38
+ const canvas = within(canvasElement)
39
+
40
+ const button = expectAgentDrivable(canvas, {
41
+ name: /colour scheme/i,
42
+ role: "button",
43
+ })
44
+
45
+ const panel = canvasElement.querySelector("[data-scheme]")
46
+
47
+ // system → light: resolved scheme is now light regardless of OS.
48
+ await userEvent.click(button)
49
+
50
+ expect(panel?.getAttribute("data-scheme")).toBe("light")
51
+
52
+ await expectNoAxeViolations(canvasElement)
53
+ })