@unpunnyfuns/swatchbook-switcher 0.20.5 → 0.50.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
@@ -15,26 +15,32 @@ npm install @unpunnyfuns/swatchbook-switcher
15
15
  ## Usage
16
16
 
17
17
  ```tsx
18
- import { ThemeSwitcher } from '@unpunnyfuns/swatchbook-switcher';
18
+ import { ThemeSwitcher, type SwitcherPreset } from '@unpunnyfuns/swatchbook-switcher';
19
+ import '@unpunnyfuns/swatchbook-switcher/style.css';
19
20
 
20
21
  <ThemeSwitcher
21
22
  axes={axes}
22
23
  presets={presets}
23
- themes={themes}
24
- activeAxes={{ mode: 'Dark' }}
25
- colorFormat="oklch"
24
+ permutations={permutations}
25
+ activeTuple={{ mode: 'Dark' }}
26
+ defaults={{ mode: 'Light', brand: 'Default', contrast: 'Normal' }}
27
+ lastApplied={null}
26
28
  onAxisChange={(axis, context) => {
27
29
  document.documentElement.setAttribute(`data-sb-${axis}`, context);
28
30
  }}
29
- onColorFormatChange={(format) => {
30
- document.documentElement.setAttribute('data-sb-color-format', format);
31
+ onPresetApply={(preset: SwitcherPreset) => {
32
+ for (const [axis, value] of Object.entries(preset.axes)) {
33
+ document.documentElement.setAttribute(`data-sb-${axis}`, value);
34
+ }
31
35
  }}
32
36
  />;
33
37
  ```
34
38
 
35
- The component is pure — it doesn't read or write to storage, and it doesn't bake in any particular way of applying the active tuple. The caller decides how to translate `onAxisChange` into DOM updates, routing changes, or global state.
39
+ The component is pure — it doesn't read or write to storage, and it doesn't bake in any particular way of applying the active tuple. The caller decides how to translate `onAxisChange` / `onPresetApply` into DOM updates, routing changes, or global state.
36
40
 
37
- `SwitcherAxis` / `SwitcherPreset` / `SwitcherTheme` are the accepted shapes and are cross-compatible with `Project.axes` / `.presets` / `.themes` from `@unpunnyfuns/swatchbook-core` pass them through directly.
41
+ Color-format selection isn't part of the switcher hosts that need it slot a `<ColorFormatSelector>` (or any other custom node) into the optional `footer` prop.
42
+
43
+ `SwitcherAxis` / `SwitcherPreset` / `SwitcherPermutation` are the accepted shapes and are cross-compatible with `Project.axes` / `.presets` / `.permutations` from `@unpunnyfuns/swatchbook-core` — pass them through directly.
38
44
 
39
45
  ## Credits
40
46
 
package/dist/index.d.mts CHANGED
@@ -19,7 +19,7 @@ interface SwitcherPreset {
19
19
  axes: Partial<Record<string, string>>;
20
20
  description?: string;
21
21
  }
22
- interface SwitcherTheme {
22
+ interface SwitcherPermutation {
23
23
  name: string;
24
24
  input: Record<string, string>;
25
25
  }
@@ -30,8 +30,8 @@ interface ThemeSwitcherProps {
30
30
  axes: readonly SwitcherAxis[];
31
31
  /** Saved preset snapshots rendered above the axes, if any. */
32
32
  presets?: readonly SwitcherPreset[];
33
- /** Full theme list — only consulted for the "modified since preset applied" dot. */
34
- themes?: readonly SwitcherTheme[];
33
+ /** Full permutation list — only consulted for the "modified since preset applied" dot. */
34
+ permutations?: readonly SwitcherPermutation[];
35
35
  /** Active axis tuple, keyed by axis name. */
36
36
  activeTuple: Readonly<Record<string, string>>;
37
37
  /** Default tuple used to fill in omitted preset axes. */
@@ -62,7 +62,7 @@ interface ThemeSwitcherProps {
62
62
  declare function ThemeSwitcher({
63
63
  axes,
64
64
  presets,
65
- themes,
65
+ permutations,
66
66
  activeTuple,
67
67
  defaults,
68
68
  lastApplied,
@@ -72,5 +72,5 @@ declare function ThemeSwitcher({
72
72
  footer
73
73
  }: ThemeSwitcherProps): ReactElement;
74
74
  //#endregion
75
- export { type SwitcherAxis, type SwitcherPreset, type SwitcherTheme, ThemeSwitcher, type ThemeSwitcherProps };
75
+ export { type SwitcherAxis, type SwitcherPermutation, type SwitcherPreset, ThemeSwitcher, type ThemeSwitcherProps };
76
76
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -14,7 +14,7 @@ import React from "react";
14
14
  * the menu. Uses classic JSX so it survives embedding in Storybook's
15
15
  * manager bundle (which doesn't expose `react/jsx-runtime`).
16
16
  */
17
- function ThemeSwitcher({ axes, presets = [], themes = [], activeTuple, defaults, lastApplied, onAxisChange, onPresetApply, onKeyDown, footer }) {
17
+ function ThemeSwitcher({ axes, presets = [], permutations = [], activeTuple, defaults, lastApplied, onAxisChange, onPresetApply, onKeyDown, footer }) {
18
18
  return /* @__PURE__ */ React.createElement("div", {
19
19
  role: "menu",
20
20
  tabIndex: -1,
@@ -24,7 +24,7 @@ function ThemeSwitcher({ axes, presets = [], themes = [], activeTuple, defaults,
24
24
  }, presets.length > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(PresetsSection, {
25
25
  presets,
26
26
  axes,
27
- themes,
27
+ permutations,
28
28
  defaults,
29
29
  activeTuple,
30
30
  lastApplied,
@@ -77,11 +77,11 @@ function AxisSection({ axis, active, onSelect }) {
77
77
  /**
78
78
  * Treat the `{ name: 'theme', source: 'synthetic' }` axis — the one core
79
79
  * fabricates for single-theme projects with no resolver — as a special case
80
- * that reads as "Theme". Authored single-axis resolvers keep their real
80
+ * that reads as "Permutation". Authored single-axis resolvers keep their real
81
81
  * name (e.g. `mode`, `brand`).
82
82
  */
83
83
  function displayLabelFor(axis) {
84
- if (axis.source === "synthetic" && axis.name === "theme") return "Theme";
84
+ if (axis.source === "synthetic" && axis.name === "theme") return "Permutation";
85
85
  return axis.name;
86
86
  }
87
87
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/ThemeSwitcher.tsx"],"sourcesContent":["import cx from 'clsx';\nimport React, { type KeyboardEvent, type ReactElement } from 'react';\nimport './ThemeSwitcher.css';\nimport type { SwitcherAxis, SwitcherPreset, SwitcherTheme } from '#/types.ts';\n\nexport interface ThemeSwitcherProps {\n /** Project axes the UI renders one section per. */\n axes: readonly SwitcherAxis[];\n /** Saved preset snapshots rendered above the axes, if any. */\n presets?: readonly SwitcherPreset[];\n /** Full theme list — only consulted for the \"modified since preset applied\" dot. */\n themes?: readonly SwitcherTheme[];\n /** Active axis tuple, keyed by axis name. */\n activeTuple: Readonly<Record<string, string>>;\n /** Default tuple used to fill in omitted preset axes. */\n defaults: Readonly<Record<string, string>>;\n /** Name of the most recently applied preset, or null. Drives the \"modified\" dot. */\n lastApplied: string | null;\n /** Receives an axis name + new context. */\n onAxisChange(axisName: string, next: string): void;\n /** Called with the full preset object when the user clicks a preset pill. */\n onPresetApply(preset: SwitcherPreset): void;\n /** Optional key handler, usually used by consumers to close a popover on Escape. */\n onKeyDown?(event: KeyboardEvent<HTMLDivElement>): void;\n /** Host-specific content rendered after the axes (e.g. the Storybook addon's color-format picker). */\n footer?: ReactElement | null;\n}\n\n/**\n * Popover body for the swatchbook theme switcher. Renders preset pills\n * (when the project ships any) and one row per axis. Color-format\n * selection is specific to the Storybook addon (it toggles how blocks\n * stringify colors); hosts that need it slot\n * `<ColorFormatSelector>` into the `footer` prop rather than it being\n * baked into every consumer.\n *\n * Consumers own the trigger + positioning — the switcher just draws\n * the menu. Uses classic JSX so it survives embedding in Storybook's\n * manager bundle (which doesn't expose `react/jsx-runtime`).\n */\nexport function ThemeSwitcher({\n axes,\n presets = [],\n themes = [],\n activeTuple,\n defaults,\n lastApplied,\n onAxisChange,\n onPresetApply,\n onKeyDown,\n footer,\n}: ThemeSwitcherProps): ReactElement {\n return (\n <div\n role=\"menu\"\n tabIndex={-1}\n className=\"sb-switcher\"\n onKeyDown={onKeyDown}\n data-testid=\"swatchbook-switcher\"\n >\n {presets.length > 0 && (\n <>\n <PresetsSection\n presets={presets}\n axes={axes}\n themes={themes}\n defaults={defaults}\n activeTuple={activeTuple}\n lastApplied={lastApplied}\n onApply={onPresetApply}\n />\n <div className=\"sb-switcher__divider\" />\n </>\n )}\n\n {axes.map((axis) => (\n <AxisSection\n key={`axis-${axis.name}`}\n axis={axis}\n active={activeTuple[axis.name] ?? axis.default}\n onSelect={(next) => onAxisChange(axis.name, next)}\n />\n ))}\n\n {footer && (\n <>\n <div className=\"sb-switcher__divider\" />\n {footer}\n </>\n )}\n </div>\n );\n}\n\ninterface OptionPillProps {\n label: string;\n active: boolean;\n title?: string;\n onClick(): void;\n trailing?: ReactElement | null;\n}\n\nfunction OptionPill({ label, active, title, onClick, trailing }: OptionPillProps): ReactElement {\n return (\n <button\n type=\"button\"\n title={title}\n onClick={onClick}\n // Skip focus on mouse click so host themes that paint a :focus\n // border-color don't stick it on the previously-clicked pill.\n // Keyboard tabbing still lands focus normally; only preventDefault\n // on mousedown blocks the implicit focus-on-click behavior.\n onMouseDown={(event) => event.preventDefault()}\n className={cx('sb-switcher__pill', active && 'sb-switcher__pill--active')}\n >\n {label}\n {trailing ?? null}\n </button>\n );\n}\n\ninterface PresetsSectionProps {\n presets: readonly SwitcherPreset[];\n axes: readonly SwitcherAxis[];\n themes: readonly SwitcherTheme[];\n defaults: Readonly<Record<string, string>>;\n activeTuple: Readonly<Record<string, string>>;\n lastApplied: string | null;\n onApply(preset: SwitcherPreset): void;\n}\n\nfunction PresetsSection({\n presets,\n axes,\n defaults,\n activeTuple,\n lastApplied,\n onApply,\n}: PresetsSectionProps): ReactElement {\n return (\n <div>\n <div className=\"sb-switcher__section-label\">Presets</div>\n <div className=\"sb-switcher__section-body\">\n {presets.map((preset) => {\n const tuple = presetTuple(preset, axes, defaults);\n const matches = tuplesEqual(tuple, activeTuple, axes);\n const modified = !matches && preset.name === lastApplied;\n const title = preset.description ? `${preset.name} — ${preset.description}` : preset.name;\n return (\n <OptionPill\n key={`preset/${preset.name}`}\n label={preset.name}\n active={matches}\n title={title}\n onClick={() => onApply(preset)}\n trailing={\n modified ? <span aria-hidden className=\"sb-switcher__pill-modified\" /> : null\n }\n />\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface AxisSectionProps {\n axis: SwitcherAxis;\n active: string;\n onSelect(next: string): void;\n}\n\nfunction AxisSection({ axis, active, onSelect }: AxisSectionProps): ReactElement {\n return (\n <div className=\"sb-switcher__axis-row\">\n <div className=\"sb-switcher__axis-label\" title={axis.description}>\n {displayLabelFor(axis)}\n </div>\n <div className=\"sb-switcher__axis-pills\">\n {axis.contexts.map((ctx) => (\n <OptionPill\n key={`${axis.name}/${ctx}`}\n label={ctx}\n active={ctx === active}\n onClick={() => onSelect(ctx)}\n />\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * Treat the `{ name: 'theme', source: 'synthetic' }` axis — the one core\n * fabricates for single-theme projects with no resolver — as a special case\n * that reads as \"Theme\". Authored single-axis resolvers keep their real\n * name (e.g. `mode`, `brand`).\n */\nfunction displayLabelFor(axis: SwitcherAxis): string {\n if (axis.source === 'synthetic' && axis.name === 'theme') return 'Theme';\n return axis.name;\n}\n\n/**\n * Compose a preset's sanitized partial tuple with the axis defaults, so\n * applying a preset that only names some axes leaves the omitted ones at\n * their defaults (not blank). Mirrors the addon preview decorator's own\n * fallback logic so what the switcher sends out matches what the host\n * honors.\n */\nfunction presetTuple(\n preset: SwitcherPreset,\n axes: readonly SwitcherAxis[],\n defaults: Readonly<Record<string, string>>,\n): Record<string, string> {\n const out: Record<string, string> = { ...defaults };\n for (const axis of axes) {\n const candidate = preset.axes[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n return out;\n}\n\nfunction tuplesEqual(\n a: Readonly<Record<string, string>>,\n b: Readonly<Record<string, string>>,\n axes: readonly SwitcherAxis[],\n): boolean {\n for (const axis of axes) {\n if (a[axis.name] !== b[axis.name]) return false;\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/ThemeSwitcher.tsx"],"sourcesContent":["import cx from 'clsx';\nimport React, { type KeyboardEvent, type ReactElement } from 'react';\nimport './ThemeSwitcher.css';\nimport type { SwitcherAxis, SwitcherPreset, SwitcherPermutation } from '#/types.ts';\n\nexport interface ThemeSwitcherProps {\n /** Project axes the UI renders one section per. */\n axes: readonly SwitcherAxis[];\n /** Saved preset snapshots rendered above the axes, if any. */\n presets?: readonly SwitcherPreset[];\n /** Full permutation list — only consulted for the \"modified since preset applied\" dot. */\n permutations?: readonly SwitcherPermutation[];\n /** Active axis tuple, keyed by axis name. */\n activeTuple: Readonly<Record<string, string>>;\n /** Default tuple used to fill in omitted preset axes. */\n defaults: Readonly<Record<string, string>>;\n /** Name of the most recently applied preset, or null. Drives the \"modified\" dot. */\n lastApplied: string | null;\n /** Receives an axis name + new context. */\n onAxisChange(axisName: string, next: string): void;\n /** Called with the full preset object when the user clicks a preset pill. */\n onPresetApply(preset: SwitcherPreset): void;\n /** Optional key handler, usually used by consumers to close a popover on Escape. */\n onKeyDown?(event: KeyboardEvent<HTMLDivElement>): void;\n /** Host-specific content rendered after the axes (e.g. the Storybook addon's color-format picker). */\n footer?: ReactElement | null;\n}\n\n/**\n * Popover body for the swatchbook theme switcher. Renders preset pills\n * (when the project ships any) and one row per axis. Color-format\n * selection is specific to the Storybook addon (it toggles how blocks\n * stringify colors); hosts that need it slot\n * `<ColorFormatSelector>` into the `footer` prop rather than it being\n * baked into every consumer.\n *\n * Consumers own the trigger + positioning — the switcher just draws\n * the menu. Uses classic JSX so it survives embedding in Storybook's\n * manager bundle (which doesn't expose `react/jsx-runtime`).\n */\nexport function ThemeSwitcher({\n axes,\n presets = [],\n permutations = [],\n activeTuple,\n defaults,\n lastApplied,\n onAxisChange,\n onPresetApply,\n onKeyDown,\n footer,\n}: ThemeSwitcherProps): ReactElement {\n return (\n <div\n role=\"menu\"\n tabIndex={-1}\n className=\"sb-switcher\"\n onKeyDown={onKeyDown}\n data-testid=\"swatchbook-switcher\"\n >\n {presets.length > 0 && (\n <>\n <PresetsSection\n presets={presets}\n axes={axes}\n permutations={permutations}\n defaults={defaults}\n activeTuple={activeTuple}\n lastApplied={lastApplied}\n onApply={onPresetApply}\n />\n <div className=\"sb-switcher__divider\" />\n </>\n )}\n\n {axes.map((axis) => (\n <AxisSection\n key={`axis-${axis.name}`}\n axis={axis}\n active={activeTuple[axis.name] ?? axis.default}\n onSelect={(next) => onAxisChange(axis.name, next)}\n />\n ))}\n\n {footer && (\n <>\n <div className=\"sb-switcher__divider\" />\n {footer}\n </>\n )}\n </div>\n );\n}\n\ninterface OptionPillProps {\n label: string;\n active: boolean;\n title?: string;\n onClick(): void;\n trailing?: ReactElement | null;\n}\n\nfunction OptionPill({ label, active, title, onClick, trailing }: OptionPillProps): ReactElement {\n return (\n <button\n type=\"button\"\n title={title}\n onClick={onClick}\n // Skip focus on mouse click so host permutations that paint a :focus\n // border-color don't stick it on the previously-clicked pill.\n // Keyboard tabbing still lands focus normally; only preventDefault\n // on mousedown blocks the implicit focus-on-click behavior.\n onMouseDown={(event) => event.preventDefault()}\n className={cx('sb-switcher__pill', active && 'sb-switcher__pill--active')}\n >\n {label}\n {trailing ?? null}\n </button>\n );\n}\n\ninterface PresetsSectionProps {\n presets: readonly SwitcherPreset[];\n axes: readonly SwitcherAxis[];\n permutations: readonly SwitcherPermutation[];\n defaults: Readonly<Record<string, string>>;\n activeTuple: Readonly<Record<string, string>>;\n lastApplied: string | null;\n onApply(preset: SwitcherPreset): void;\n}\n\nfunction PresetsSection({\n presets,\n axes,\n defaults,\n activeTuple,\n lastApplied,\n onApply,\n}: PresetsSectionProps): ReactElement {\n return (\n <div>\n <div className=\"sb-switcher__section-label\">Presets</div>\n <div className=\"sb-switcher__section-body\">\n {presets.map((preset) => {\n const tuple = presetTuple(preset, axes, defaults);\n const matches = tuplesEqual(tuple, activeTuple, axes);\n const modified = !matches && preset.name === lastApplied;\n const title = preset.description ? `${preset.name} — ${preset.description}` : preset.name;\n return (\n <OptionPill\n key={`preset/${preset.name}`}\n label={preset.name}\n active={matches}\n title={title}\n onClick={() => onApply(preset)}\n trailing={\n modified ? <span aria-hidden className=\"sb-switcher__pill-modified\" /> : null\n }\n />\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface AxisSectionProps {\n axis: SwitcherAxis;\n active: string;\n onSelect(next: string): void;\n}\n\nfunction AxisSection({ axis, active, onSelect }: AxisSectionProps): ReactElement {\n return (\n <div className=\"sb-switcher__axis-row\">\n <div className=\"sb-switcher__axis-label\" title={axis.description}>\n {displayLabelFor(axis)}\n </div>\n <div className=\"sb-switcher__axis-pills\">\n {axis.contexts.map((ctx) => (\n <OptionPill\n key={`${axis.name}/${ctx}`}\n label={ctx}\n active={ctx === active}\n onClick={() => onSelect(ctx)}\n />\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * Treat the `{ name: 'theme', source: 'synthetic' }` axis — the one core\n * fabricates for single-theme projects with no resolver — as a special case\n * that reads as \"Permutation\". Authored single-axis resolvers keep their real\n * name (e.g. `mode`, `brand`).\n */\nfunction displayLabelFor(axis: SwitcherAxis): string {\n if (axis.source === 'synthetic' && axis.name === 'theme') return 'Permutation';\n return axis.name;\n}\n\n/**\n * Compose a preset's sanitized partial tuple with the axis defaults, so\n * applying a preset that only names some axes leaves the omitted ones at\n * their defaults (not blank). Mirrors the addon preview decorator's own\n * fallback logic so what the switcher sends out matches what the host\n * honors.\n */\nfunction presetTuple(\n preset: SwitcherPreset,\n axes: readonly SwitcherAxis[],\n defaults: Readonly<Record<string, string>>,\n): Record<string, string> {\n const out: Record<string, string> = { ...defaults };\n for (const axis of axes) {\n const candidate = preset.axes[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n return out;\n}\n\nfunction tuplesEqual(\n a: Readonly<Record<string, string>>,\n b: Readonly<Record<string, string>>,\n axes: readonly SwitcherAxis[],\n): boolean {\n for (const axis of axes) {\n if (a[axis.name] !== b[axis.name]) return false;\n }\n return true;\n}\n"],"mappings":"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"}
package/dist/style.css CHANGED
@@ -52,6 +52,11 @@
52
52
  line-height: 18px;
53
53
  }
54
54
 
55
+ .sb-switcher__pill:focus-visible {
56
+ outline: 2px solid var(--swatchbook-accent-bg, #1d4ed8);
57
+ outline-offset: 1px;
58
+ }
59
+
55
60
  .sb-switcher__pill--active {
56
61
  background: #007aff1f;
57
62
  border-color: #007aff73;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unpunnyfuns/swatchbook-switcher",
3
- "version": "0.20.5",
3
+ "version": "0.50.0",
4
4
  "description": "Framework-agnostic theme-switcher UI for swatchbook — axis pills, preset pills, color-format selector. Consumed by the Storybook addon toolbar and any React host that knows how to set data-attributes on the document.",
5
5
  "license": "MIT",
6
6
  "author": "unpunnyfuns <unpunnyfuns@gmail.com>",