@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.
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts +35 -0
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts.map +1 -0
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js +33 -0
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js.map +1 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts +54 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts.map +1 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.js +30 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.js.map +1 -0
- package/dist/Lightbox/Lightbox.d.ts +57 -0
- package/dist/Lightbox/Lightbox.d.ts.map +1 -0
- package/dist/Lightbox/Lightbox.js +50 -0
- package/dist/Lightbox/Lightbox.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.mdx +70 -0
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +118 -0
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.test.tsx +53 -0
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx +90 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.mdx +48 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +161 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.test.tsx +80 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.tsx +107 -0
- package/src/Lightbox/Lightbox.mdx +62 -0
- package/src/Lightbox/Lightbox.stories.tsx +133 -0
- package/src/Lightbox/Lightbox.test.tsx +135 -0
- package/src/Lightbox/Lightbox.tsx +143 -0
- package/src/icons.storyHelpers.tsx +22 -0
- package/src/index.ts +9 -0
- 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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
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;
|
|
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
|
|
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
|
|
47
|
-
"@charcuterie/tokens": "1.
|
|
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
|
+
})
|