@adea-ai/ui 0.73.0 → 0.75.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.
@@ -1 +1 @@
1
- {"version":3,"file":"appearance-choices.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/appearance-editor/appearance-choices.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAc/D,wBAAgB,WAAW,CAAC,KAAK,EAAE,qBAAqB,kCAyCvD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,kCAyDzD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,kCA6BxD"}
1
+ {"version":3,"file":"appearance-choices.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/appearance-editor/appearance-choices.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAc/D,wBAAgB,WAAW,CAAC,KAAK,EAAE,qBAAqB,kCAyCvD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,kCA6DzD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,kCA6BxD"}
@@ -5,8 +5,9 @@ import { RadioGroup } from "@kobalte/core/radio-group";
5
5
  //#region src/components/composites/appearance-editor/appearance-choices.tsx
6
6
  var _tmpl$ = /*#__PURE__*/ template(`<span class="block h-20 w-full">`);
7
7
  var _tmpl$2 = /*#__PURE__*/ template(`<span>`);
8
- var _tmpl$3 = /*#__PURE__*/ template(`<span class="block size-full rounded-full"aria-hidden=true>`);
9
- var _tmpl$4 = /*#__PURE__*/ template(`<span class=sr-only>`);
8
+ var _tmpl$3 = /*#__PURE__*/ template(`<div class="col-span-3 grid grid-cols-3 gap-2 sm:col-span-1 sm:flex sm:gap-2">`);
9
+ var _tmpl$4 = /*#__PURE__*/ template(`<span class="block size-full rounded-full"aria-hidden=true>`);
10
+ var _tmpl$5 = /*#__PURE__*/ template(`<span class=sr-only>`);
10
11
  var MODES = [
11
12
  {
12
13
  value: "system",
@@ -113,7 +114,7 @@ function AccentChoices(props) {
113
114
  },
114
115
  orientation: "horizontal",
115
116
  "aria-label": "Accent",
116
- "class": "flex flex-wrap items-center gap-2",
117
+ "class": "grid grid-cols-3 items-center gap-2 sm:flex sm:flex-wrap sm:justify-end",
117
118
  onChange: (accent) => props.onChange({ accent: accent === "custom" ? props.customAccentValue ?? "" : accent }),
118
119
  get children() {
119
120
  return [
@@ -126,33 +127,37 @@ function AccentChoices(props) {
126
127
  })];
127
128
  }
128
129
  }),
129
- createComponent(For, {
130
- get each() {
131
- return props.accentOptions;
132
- },
133
- children: (option) => createComponent(RadioGroup.Item, {
134
- get value() {
135
- return option.id;
130
+ (() => {
131
+ var _el$3 = _tmpl$3();
132
+ insert(_el$3, createComponent(For, {
133
+ get each() {
134
+ return props.accentOptions;
136
135
  },
137
- "class": "rounded-full focus-within:ring-3 focus-within:ring-ring/50",
138
- get children() {
139
- return [createComponent(RadioGroup.ItemInput, {}), createComponent(RadioGroup.ItemLabel, {
140
- "class": "block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary",
141
- get children() {
142
- return [(() => {
143
- var _el$3 = _tmpl$3();
144
- effect((_$p) => setStyleProperty(_el$3, "background-color", (props.draft.mode === "system" ? props.resolvedAppearance : props.draft.mode) === "light" ? option.light : option.dark));
145
- return _el$3;
146
- })(), (() => {
147
- var _el$4 = _tmpl$4();
148
- insert(_el$4, () => option.label);
149
- return _el$4;
150
- })()];
151
- }
152
- })];
153
- }
154
- })
155
- }),
136
+ children: (option) => createComponent(RadioGroup.Item, {
137
+ get value() {
138
+ return option.id;
139
+ },
140
+ "class": "rounded-full focus-within:ring-3 focus-within:ring-ring/50",
141
+ get children() {
142
+ return [createComponent(RadioGroup.ItemInput, {}), createComponent(RadioGroup.ItemLabel, {
143
+ "class": "block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary",
144
+ get children() {
145
+ return [(() => {
146
+ var _el$4 = _tmpl$4();
147
+ effect((_$p) => setStyleProperty(_el$4, "background-color", (props.draft.mode === "system" ? props.resolvedAppearance : props.draft.mode) === "light" ? option.light : option.dark));
148
+ return _el$4;
149
+ })(), (() => {
150
+ var _el$5 = _tmpl$5();
151
+ insert(_el$5, () => option.label);
152
+ return _el$5;
153
+ })()];
154
+ }
155
+ })];
156
+ }
157
+ })
158
+ }));
159
+ return _el$3;
160
+ })(),
156
161
  createComponent(RadioGroup.Item, {
157
162
  value: "custom",
158
163
  get children() {
@@ -1 +1 @@
1
- {"version":3,"file":"appearance-choices.js","names":["RadioGroup","Radio","For","Show","AppearanceEditorProps","ThemeMiniature","ThemeMiniatureSplit","MODES","value","label","const","SURFACES","ModeChoices","props","_$createComponent","draft","mode","disabled","saving","orientation","onChange","option","find","candidate","children","each","Item","data-mode","ItemInput","ItemLabel","_el$","_tmpl$","_$insert","when","fallback","theme","_$memo","lightTheme","darkTheme","light","dark","_el$2","_tmpl$2","AccentChoices","custom","accent","accentOptions","some","id","accentSelection","customAccentValue","_el$3","_tmpl$3","_$effect","_$p","_$setStyleProperty","resolvedAppearance","_el$4","_tmpl$4","GlassChoices","surface","surfaceCapability","frosted","_$createComponent","Radio","_$memo","_$setStyleProperty"],"sources":["../../../../src/components/composites/appearance-editor/appearance-choices.tsx"],"sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2026 Wing\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n// Pinned mode-card, accent-swatch, and glass-chip composition. Kobalte replaces\n// the app port's manual roving focus; it does not replace the donor hierarchy.\nimport { RadioGroup as Radio } from '@kobalte/core/radio-group'\nimport { For, Show } from 'solid-js'\nimport type { AppearanceEditorProps } from './appearance-types'\nimport { ThemeMiniature, ThemeMiniatureSplit } from './theme-preview'\n\nconst MODES = [\n { value: 'system', label: 'System' },\n { value: 'light', label: 'Light' },\n { value: 'dark', label: 'Dark' },\n] as const\nconst SURFACES = [\n { value: 'theme', label: 'Theme default' },\n { value: 'frosted', label: 'Frosted' },\n { value: 'opaque', label: 'Opaque' },\n] as const\n\nexport function ModeChoices(props: AppearanceEditorProps) {\n return (\n <Radio\n value={props.draft.mode}\n disabled={props.saving}\n orientation=\"horizontal\"\n aria-label=\"Appearance mode\"\n onChange={(mode) => {\n const option = MODES.find((candidate) => candidate.value === mode)\n if (option) props.onChange({ mode: option.value })\n }}\n class=\"grid grid-cols-1 gap-3 px-4 py-4 sm:grid-cols-3\"\n >\n <For each={MODES}>\n {(option) => (\n <Radio.Item\n value={option.value}\n data-mode={option.value}\n class=\"min-w-0 rounded-lg focus-within:ring-3 focus-within:ring-ring/50\"\n >\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex cursor-pointer flex-col gap-2 rounded-lg border p-2 text-center text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\">\n <span class=\"block h-20 w-full\">\n <Show\n when={option.value === 'system'}\n fallback={\n <ThemeMiniature\n theme={option.value === 'light' ? props.lightTheme : props.darkTheme}\n />\n }\n >\n <ThemeMiniatureSplit light={props.lightTheme} dark={props.darkTheme} />\n </Show>\n </span>\n <span>{option.label}</span>\n </Radio.ItemLabel>\n </Radio.Item>\n )}\n </For>\n </Radio>\n )\n}\n\nexport function AccentChoices(props: AppearanceEditorProps) {\n const custom = () =>\n props.draft.accent !== 'theme' &&\n !props.accentOptions.some((option) => option.id === props.draft.accent)\n const accentSelection = () => (custom() ? 'custom' : props.draft.accent)\n return (\n <Radio\n value={accentSelection()}\n disabled={props.saving}\n orientation=\"horizontal\"\n aria-label=\"Accent\"\n class=\"flex flex-wrap items-center gap-2\"\n onChange={(accent) =>\n props.onChange({\n accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,\n })\n }\n >\n <Radio.Item value=\"theme\">\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary\">\n Theme default\n </Radio.ItemLabel>\n </Radio.Item>\n <For each={props.accentOptions}>\n {(option) => (\n <Radio.Item\n value={option.id}\n class=\"rounded-full focus-within:ring-3 focus-within:ring-ring/50\"\n >\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary\">\n <span\n class=\"block size-full rounded-full\"\n aria-hidden=\"true\"\n style={{\n 'background-color':\n (props.draft.mode === 'system'\n ? props.resolvedAppearance\n : props.draft.mode) === 'light'\n ? option.light\n : option.dark,\n }}\n />\n <span class=\"sr-only\">{option.label}</span>\n </Radio.ItemLabel>\n </Radio.Item>\n )}\n </For>\n <Radio.Item value=\"custom\">\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary\">\n Custom\n </Radio.ItemLabel>\n </Radio.Item>\n </Radio>\n )\n}\n\nexport function GlassChoices(props: AppearanceEditorProps) {\n return (\n <Radio\n value={props.draft.surface}\n disabled={props.saving}\n orientation=\"horizontal\"\n aria-label=\"Glass\"\n class=\"flex flex-wrap gap-1\"\n onChange={(surface) => {\n const option = SURFACES.find((candidate) => candidate.value === surface)\n if (option) props.onChange({ surface: option.value })\n }}\n >\n <For each={SURFACES}>\n {(option) => (\n <Radio.Item\n value={option.value}\n disabled={option.value === 'frosted' && !props.surfaceCapability.frosted}\n class=\"rounded-md focus-within:ring-3 focus-within:ring-ring/50\"\n >\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\">\n {option.label}\n </Radio.ItemLabel>\n </Radio.Item>\n )}\n </For>\n </Radio>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"appearance-choices.js","names":["RadioGroup","Radio","For","Show","AppearanceEditorProps","ThemeMiniature","ThemeMiniatureSplit","MODES","value","label","const","SURFACES","ModeChoices","props","_$createComponent","draft","mode","disabled","saving","orientation","onChange","option","find","candidate","children","each","Item","data-mode","ItemInput","ItemLabel","_el$","_tmpl$","_$insert","when","fallback","theme","_$memo","lightTheme","darkTheme","light","dark","_el$2","_tmpl$2","AccentChoices","custom","accent","accentOptions","some","id","accentSelection","customAccentValue","_el$3","_tmpl$3","_el$4","_tmpl$4","_$effect","_$p","_$setStyleProperty","resolvedAppearance","_el$5","_tmpl$5","GlassChoices","surface","surfaceCapability","frosted","_$createComponent","Radio","_$memo","_$setStyleProperty"],"sources":["../../../../src/components/composites/appearance-editor/appearance-choices.tsx"],"sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2026 Wing\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n// Pinned mode-card, accent-swatch, and glass-chip composition. Kobalte replaces\n// the app port's manual roving focus; it does not replace the donor hierarchy.\nimport { RadioGroup as Radio } from '@kobalte/core/radio-group'\nimport { For, Show } from 'solid-js'\nimport type { AppearanceEditorProps } from './appearance-types'\nimport { ThemeMiniature, ThemeMiniatureSplit } from './theme-preview'\n\nconst MODES = [\n { value: 'system', label: 'System' },\n { value: 'light', label: 'Light' },\n { value: 'dark', label: 'Dark' },\n] as const\nconst SURFACES = [\n { value: 'theme', label: 'Theme default' },\n { value: 'frosted', label: 'Frosted' },\n { value: 'opaque', label: 'Opaque' },\n] as const\n\nexport function ModeChoices(props: AppearanceEditorProps) {\n return (\n <Radio\n value={props.draft.mode}\n disabled={props.saving}\n orientation=\"horizontal\"\n aria-label=\"Appearance mode\"\n onChange={(mode) => {\n const option = MODES.find((candidate) => candidate.value === mode)\n if (option) props.onChange({ mode: option.value })\n }}\n class=\"grid grid-cols-1 gap-3 px-4 py-4 sm:grid-cols-3\"\n >\n <For each={MODES}>\n {(option) => (\n <Radio.Item\n value={option.value}\n data-mode={option.value}\n class=\"min-w-0 rounded-lg focus-within:ring-3 focus-within:ring-ring/50\"\n >\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex cursor-pointer flex-col gap-2 rounded-lg border p-2 text-center text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\">\n <span class=\"block h-20 w-full\">\n <Show\n when={option.value === 'system'}\n fallback={\n <ThemeMiniature\n theme={option.value === 'light' ? props.lightTheme : props.darkTheme}\n />\n }\n >\n <ThemeMiniatureSplit light={props.lightTheme} dark={props.darkTheme} />\n </Show>\n </span>\n <span>{option.label}</span>\n </Radio.ItemLabel>\n </Radio.Item>\n )}\n </For>\n </Radio>\n )\n}\n\nexport function AccentChoices(props: AppearanceEditorProps) {\n const custom = () =>\n props.draft.accent !== 'theme' &&\n !props.accentOptions.some((option) => option.id === props.draft.accent)\n const accentSelection = () => (custom() ? 'custom' : props.draft.accent)\n return (\n <Radio\n value={accentSelection()}\n disabled={props.saving}\n orientation=\"horizontal\"\n aria-label=\"Accent\"\n /* Narrow hosts wrap the swatches into a three-column grid so the row\n control stays beside its label; wide hosts keep one inline row. */\n class=\"grid grid-cols-3 items-center gap-2 sm:flex sm:flex-wrap sm:justify-end\"\n onChange={(accent) =>\n props.onChange({\n accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,\n })\n }\n >\n <Radio.Item value=\"theme\">\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary\">\n Theme default\n </Radio.ItemLabel>\n </Radio.Item>\n <div class=\"col-span-3 grid grid-cols-3 gap-2 sm:col-span-1 sm:flex sm:gap-2\">\n <For each={props.accentOptions}>\n {(option) => (\n <Radio.Item\n value={option.id}\n class=\"rounded-full focus-within:ring-3 focus-within:ring-ring/50\"\n >\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary\">\n <span\n class=\"block size-full rounded-full\"\n aria-hidden=\"true\"\n style={{\n 'background-color':\n (props.draft.mode === 'system'\n ? props.resolvedAppearance\n : props.draft.mode) === 'light'\n ? option.light\n : option.dark,\n }}\n />\n <span class=\"sr-only\">{option.label}</span>\n </Radio.ItemLabel>\n </Radio.Item>\n )}\n </For>\n </div>\n <Radio.Item value=\"custom\">\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary\">\n Custom\n </Radio.ItemLabel>\n </Radio.Item>\n </Radio>\n )\n}\n\nexport function GlassChoices(props: AppearanceEditorProps) {\n return (\n <Radio\n value={props.draft.surface}\n disabled={props.saving}\n orientation=\"horizontal\"\n aria-label=\"Glass\"\n class=\"flex flex-wrap gap-1\"\n onChange={(surface) => {\n const option = SURFACES.find((candidate) => candidate.value === surface)\n if (option) props.onChange({ surface: option.value })\n }}\n >\n <For each={SURFACES}>\n {(option) => (\n <Radio.Item\n value={option.value}\n disabled={option.value === 'frosted' && !props.surfaceCapability.frosted}\n class=\"rounded-md focus-within:ring-3 focus-within:ring-ring/50\"\n >\n <Radio.ItemInput />\n <Radio.ItemLabel class=\"flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\">\n {option.label}\n </Radio.ItemLabel>\n </Radio.Item>\n )}\n </For>\n </Radio>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"appearance-editor.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/appearance-editor/appearance-editor.tsx"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAIvF;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,kCAqJ5D;AAED;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,kCAyB9D"}
1
+ {"version":3,"file":"appearance-editor.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/appearance-editor/appearance-editor.tsx"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAIvF;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,kCAmJ5D;AAED;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,kCAyB9D"}
@@ -14,7 +14,7 @@ var _tmpl$2 = /*#__PURE__*/ template(`<p role=alert class="mt-1 text-xs text-for
14
14
  var _tmpl$3 = /*#__PURE__*/ template(`<div class="px-4 py-3"><label class="mb-2 block text-xs font-medium">Custom accent`);
15
15
  var _tmpl$4 = /*#__PURE__*/ template(`<p role=status class="px-4 py-2 text-xs text-muted-foreground">`);
16
16
  var _tmpl$5 = /*#__PURE__*/ template(`<p role=status class="px-4 pt-3 text-xs text-muted-foreground">`);
17
- var _tmpl$6 = /*#__PURE__*/ template(`<div data-appearance-editor><div class="divide-y divide-border rounded-lg border"></div><div class="flex flex-wrap items-center gap-2 px-4 py-4"><div class="ml-auto flex gap-2">`);
17
+ var _tmpl$6 = /*#__PURE__*/ template(`<div data-appearance-editor><div class="divide-y divide-border rounded-lg border"></div><div class="sticky bottom-0 z-10 mt-2 flex flex-wrap items-center gap-2 border-t bg-background px-4 py-3"><div class="ml-auto flex gap-2">`);
18
18
  /**
19
19
  * A live editor whose persistence and preview authority belong to its host.
20
20
  * No ThemeProvider is required, and no catalogue, renderer engine or storage is
@@ -78,7 +78,6 @@ function AppearanceEditor(props) {
78
78
  onSelect: (darkThemeId) => props.onChange({ darkThemeId })
79
79
  }), null);
80
80
  insert(_el$3, createComponent(SettingsRow, {
81
- stacked: true,
82
81
  title: "Accent",
83
82
  get icon() {
84
83
  return createComponent(SlidersHorizontal, {});
@@ -1 +1 @@
1
- {"version":3,"file":"appearance-editor.js","names":["EyeOff","Palette","PanelsTopLeft","SlidersHorizontal","Show","createUniqueId","cn","Button","Input","Switch","Popover","PopoverContent","PopoverDescription","PopoverTitle","PopoverTrigger","AppearanceEditorProps","AppearancePopoverProps","ModeChoices","AccentChoices","GlassChoices","SettingsRow","ThemeRow","AppearanceEditor","props","errorId","saveReasonId","custom","draft","accent","accentOptions","some","option","id","accentDescription","name","find","label","glassDescription","surface","surfaceCapability","themeDefaultDescription","_el$","_tmpl$6","_el$3","firstChild","_el$9","nextSibling","_el$0","_$insert","_$createComponent","when","recoveryNotice","children","_el$2","_tmpl$","appearance","selectedId","lightThemeId","preview","lightTheme","themes","disabled","saving","onSelect","onChange","darkThemeId","darkTheme","stacked","title","icon","description","_el$4","_tmpl$3","_el$5","_$setAttribute","value","aria-invalid","customAccentError","aria-describedby","undefined","onInput","event","currentTarget","_el$6","_tmpl$2","_$memo","frosted","reason","_el$7","_tmpl$4","checked","reduceTransparency","onManageThemes","type","size","variant","onClick","saveDisabledReason","_el$8","_tmpl$5","onReset","onCancel","aria-busy","onSave","_$effect","_$className","class","AppearancePopover","open","modal","onOpenChange","onOpen","onDismiss","as","_$createComponent","_$memo","_$className"],"sources":["../../../../src/components/composites/appearance-editor/appearance-editor.tsx"],"sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2026 Wing\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n/*\n * Composition substantially translated from Zeron settings/appearance.rs and\n * settings/widgets.rs, pinned at 30a9a9537c5ec96226c87f4bf349b6f77c5dfb59,\n * preserving Adea's existing appearance-surface.tsx port: System/Light/Dark\n * miniatures, compact independent theme rows, accent swatches and helper copy,\n * glass chips, and theme library affordance. Kobalte replaces the port's manual\n * radio keyboard model. The controlled draft and explicit action ports replace\n * GPUI globals and app-specific storage/native authority. Save/Cancel/Reset and\n * custom accent/reduced-transparency are accepted Adea divergences (#425).\n */\nimport { EyeOff, Palette, PanelsTopLeft, SlidersHorizontal } from 'lucide-solid'\nimport { Show, createUniqueId } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../../ui/button/button'\nimport { Input } from '../../ui/input/input'\n\nimport { Switch } from '../../ui/switch/switch'\nimport {\n Popover,\n PopoverContent,\n PopoverDescription,\n PopoverTitle,\n PopoverTrigger,\n} from '../../ui/popover/popover'\nimport type { AppearanceEditorProps, AppearancePopoverProps } from './appearance-types'\nimport { ModeChoices, AccentChoices, GlassChoices } from './appearance-choices'\nimport { SettingsRow, ThemeRow } from './appearance-rows'\n\n/**\n * A live editor whose persistence and preview authority belong to its host.\n * No ThemeProvider is required, and no catalogue, renderer engine or storage is\n * imported. Hosts normalize unknown IDs and custom colors, report recovery,\n * snapshot on open, preview on changes, commit on Save, and restore on dismissal.\n * Both theme rows remain mounted when mode changes. Typeface/density axes are\n * deliberately absent from Adea #425's accepted surface.\n */\nexport function AppearanceEditor(props: AppearanceEditorProps) {\n const errorId = createUniqueId()\n const saveReasonId = createUniqueId()\n const custom = () =>\n props.draft.accent !== 'theme' &&\n !props.accentOptions.some((option) => option.id === props.draft.accent)\n const accentDescription = () => {\n if (props.draft.accent === 'theme') return \"Theme default · Uses the palette's intended color.\"\n const name = props.accentOptions.find((option) => option.id === props.draft.accent)?.label\n return `${name ?? 'Custom'} · Controls, glyphs, selections, code, and activity.`\n }\n const glassDescription = () =>\n props.draft.surface === 'opaque'\n ? 'Solid surfaces for every theme.'\n : props.draft.surface === 'frosted'\n ? 'Theme-colored glass where supported.'\n : (props.surfaceCapability.themeDefaultDescription ?? \"Uses this theme's default surface.\")\n return (\n <div data-appearance-editor class={cn('min-w-0', props.class)}>\n <Show when={props.recoveryNotice}>\n <p role=\"status\" class=\"px-4 py-2 text-sm text-muted-foreground\">\n {props.recoveryNotice}\n </p>\n </Show>\n <ModeChoices {...props} />\n <div class=\"divide-y divide-border rounded-lg border\">\n <ThemeRow\n appearance=\"light\"\n selectedId={props.draft.lightThemeId}\n preview={props.lightTheme}\n themes={props.themes}\n disabled={props.saving}\n onSelect={(lightThemeId) => props.onChange({ lightThemeId })}\n />\n <ThemeRow\n appearance=\"dark\"\n selectedId={props.draft.darkThemeId}\n preview={props.darkTheme}\n themes={props.themes}\n disabled={props.saving}\n onSelect={(darkThemeId) => props.onChange({ darkThemeId })}\n />\n <SettingsRow\n stacked\n title=\"Accent\"\n icon={<SlidersHorizontal />}\n description={accentDescription()}\n >\n <AccentChoices {...props} />\n </SettingsRow>\n <Show when={custom()}>\n <div class=\"px-4 py-3\">\n <label class=\"mb-2 block text-xs font-medium\" for={`${errorId}-input`}>\n Custom accent\n </label>\n <Input\n id={`${errorId}-input`}\n value={props.draft.accent}\n disabled={props.saving}\n aria-invalid={!!props.customAccentError}\n aria-describedby={props.customAccentError ? errorId : undefined}\n onInput={(event) => props.onChange({ accent: event.currentTarget.value })}\n />\n <Show when={props.customAccentError}>\n <p id={errorId} role=\"alert\" class=\"mt-1 text-xs text-foreground\">\n {props.customAccentError}\n </p>\n </Show>\n </div>\n </Show>\n <SettingsRow title=\"Glass\" icon={<PanelsTopLeft />} description={glassDescription()}>\n <GlassChoices {...props} />\n </SettingsRow>\n <Show when={!props.surfaceCapability.frosted && props.surfaceCapability.reason}>\n <p role=\"status\" class=\"px-4 py-2 text-xs text-muted-foreground\">\n {props.surfaceCapability.reason}\n </p>\n </Show>\n <SettingsRow\n title=\"Reduce transparency\"\n icon={<EyeOff />}\n description=\"Prefer solid surfaces, including during live preview.\"\n >\n <Switch\n checked={props.draft.reduceTransparency}\n disabled={props.saving}\n onChange={(reduceTransparency: boolean) => props.onChange({ reduceTransparency })}\n aria-label=\"Reduce transparency\"\n />\n </SettingsRow>\n <SettingsRow\n title=\"Theme library\"\n icon={<Palette />}\n description={\n props.onManageThemes\n ? 'Manage themes through the application.'\n : 'Theme import is unavailable.'\n }\n >\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={!props.onManageThemes || props.saving}\n onClick={() => props.onManageThemes?.()}\n >\n Manage themes\n </Button>\n </SettingsRow>\n </div>\n <Show when={props.saveDisabledReason}>\n <p id={saveReasonId} role=\"status\" class=\"px-4 pt-3 text-xs text-muted-foreground\">\n {props.saveDisabledReason}\n </p>\n </Show>\n <div class=\"flex flex-wrap items-center gap-2 px-4 py-4\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n disabled={props.saving}\n onClick={() => props.onReset()}\n >\n Reset\n </Button>\n <div class=\"ml-auto flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={props.saving}\n onClick={() => props.onCancel()}\n >\n Cancel\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={props.saving || !!props.customAccentError || !!props.saveDisabledReason}\n aria-busy={props.saving}\n aria-describedby={props.saveDisabledReason ? saveReasonId : undefined}\n onClick={() => props.onSave()}\n >\n {props.saving ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\n/** Modal anchoring/dismissal and nested-menu behavior come from Kobalte.\n * Its measured available height also bounds scrolling, keeping footer actions\n * reachable when a narrow popup begins below its trigger. */\nexport function AppearancePopover(props: AppearancePopoverProps) {\n return (\n <Popover\n open={props.open}\n modal\n onOpenChange={(open) => (open ? props.onOpen() : props.onDismiss())}\n >\n <PopoverTrigger\n as={Button}\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Appearance settings\"\n >\n <Palette />\n </PopoverTrigger>\n <PopoverContent class=\"w-lg max-w-[calc(100vw-2rem)] max-h-(--kb-popper-content-available-height) overflow-y-auto\">\n <PopoverTitle>Appearance</PopoverTitle>\n <PopoverDescription>\n Changes preview immediately. Save keeps them; Cancel restores the previous appearance.\n </PopoverDescription>\n <AppearanceEditor {...props} />\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"appearance-editor.js","names":["EyeOff","Palette","PanelsTopLeft","SlidersHorizontal","Show","createUniqueId","cn","Button","Input","Switch","Popover","PopoverContent","PopoverDescription","PopoverTitle","PopoverTrigger","AppearanceEditorProps","AppearancePopoverProps","ModeChoices","AccentChoices","GlassChoices","SettingsRow","ThemeRow","AppearanceEditor","props","errorId","saveReasonId","custom","draft","accent","accentOptions","some","option","id","accentDescription","name","find","label","glassDescription","surface","surfaceCapability","themeDefaultDescription","_el$","_tmpl$6","_el$3","firstChild","_el$9","nextSibling","_el$0","_$insert","_$createComponent","when","recoveryNotice","children","_el$2","_tmpl$","appearance","selectedId","lightThemeId","preview","lightTheme","themes","disabled","saving","onSelect","onChange","darkThemeId","darkTheme","title","icon","description","_el$4","_tmpl$3","_el$5","_$setAttribute","value","aria-invalid","customAccentError","aria-describedby","undefined","onInput","event","currentTarget","_el$6","_tmpl$2","_$memo","frosted","reason","_el$7","_tmpl$4","checked","reduceTransparency","onManageThemes","type","size","variant","onClick","saveDisabledReason","_el$8","_tmpl$5","onReset","onCancel","aria-busy","onSave","_$effect","_$className","class","AppearancePopover","open","modal","onOpenChange","onOpen","onDismiss","as","_$createComponent","_$memo","_$className"],"sources":["../../../../src/components/composites/appearance-editor/appearance-editor.tsx"],"sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2026 Wing\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n/*\n * Composition substantially translated from Zeron settings/appearance.rs and\n * settings/widgets.rs, pinned at 30a9a9537c5ec96226c87f4bf349b6f77c5dfb59,\n * preserving Adea's existing appearance-surface.tsx port: System/Light/Dark\n * miniatures, compact independent theme rows, accent swatches and helper copy,\n * glass chips, and theme library affordance. Kobalte replaces the port's manual\n * radio keyboard model. The controlled draft and explicit action ports replace\n * GPUI globals and app-specific storage/native authority. Save/Cancel/Reset and\n * custom accent/reduced-transparency are accepted Adea divergences (#425).\n */\nimport { EyeOff, Palette, PanelsTopLeft, SlidersHorizontal } from 'lucide-solid'\nimport { Show, createUniqueId } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../../ui/button/button'\nimport { Input } from '../../ui/input/input'\n\nimport { Switch } from '../../ui/switch/switch'\nimport {\n Popover,\n PopoverContent,\n PopoverDescription,\n PopoverTitle,\n PopoverTrigger,\n} from '../../ui/popover/popover'\nimport type { AppearanceEditorProps, AppearancePopoverProps } from './appearance-types'\nimport { ModeChoices, AccentChoices, GlassChoices } from './appearance-choices'\nimport { SettingsRow, ThemeRow } from './appearance-rows'\n\n/**\n * A live editor whose persistence and preview authority belong to its host.\n * No ThemeProvider is required, and no catalogue, renderer engine or storage is\n * imported. Hosts normalize unknown IDs and custom colors, report recovery,\n * snapshot on open, preview on changes, commit on Save, and restore on dismissal.\n * Both theme rows remain mounted when mode changes. Typeface/density axes are\n * deliberately absent from Adea #425's accepted surface.\n */\nexport function AppearanceEditor(props: AppearanceEditorProps) {\n const errorId = createUniqueId()\n const saveReasonId = createUniqueId()\n const custom = () =>\n props.draft.accent !== 'theme' &&\n !props.accentOptions.some((option) => option.id === props.draft.accent)\n const accentDescription = () => {\n if (props.draft.accent === 'theme') return \"Theme default · Uses the palette's intended color.\"\n const name = props.accentOptions.find((option) => option.id === props.draft.accent)?.label\n return `${name ?? 'Custom'} · Controls, glyphs, selections, code, and activity.`\n }\n const glassDescription = () =>\n props.draft.surface === 'opaque'\n ? 'Solid surfaces for every theme.'\n : props.draft.surface === 'frosted'\n ? 'Theme-colored glass where supported.'\n : (props.surfaceCapability.themeDefaultDescription ?? \"Uses this theme's default surface.\")\n return (\n <div data-appearance-editor class={cn('min-w-0', props.class)}>\n <Show when={props.recoveryNotice}>\n <p role=\"status\" class=\"px-4 py-2 text-sm text-muted-foreground\">\n {props.recoveryNotice}\n </p>\n </Show>\n <ModeChoices {...props} />\n <div class=\"divide-y divide-border rounded-lg border\">\n <ThemeRow\n appearance=\"light\"\n selectedId={props.draft.lightThemeId}\n preview={props.lightTheme}\n themes={props.themes}\n disabled={props.saving}\n onSelect={(lightThemeId) => props.onChange({ lightThemeId })}\n />\n <ThemeRow\n appearance=\"dark\"\n selectedId={props.draft.darkThemeId}\n preview={props.darkTheme}\n themes={props.themes}\n disabled={props.saving}\n onSelect={(darkThemeId) => props.onChange({ darkThemeId })}\n />\n <SettingsRow title=\"Accent\" icon={<SlidersHorizontal />} description={accentDescription()}>\n <AccentChoices {...props} />\n </SettingsRow>\n <Show when={custom()}>\n <div class=\"px-4 py-3\">\n <label class=\"mb-2 block text-xs font-medium\" for={`${errorId}-input`}>\n Custom accent\n </label>\n <Input\n id={`${errorId}-input`}\n value={props.draft.accent}\n disabled={props.saving}\n aria-invalid={!!props.customAccentError}\n aria-describedby={props.customAccentError ? errorId : undefined}\n onInput={(event) => props.onChange({ accent: event.currentTarget.value })}\n />\n <Show when={props.customAccentError}>\n <p id={errorId} role=\"alert\" class=\"mt-1 text-xs text-foreground\">\n {props.customAccentError}\n </p>\n </Show>\n </div>\n </Show>\n <SettingsRow title=\"Glass\" icon={<PanelsTopLeft />} description={glassDescription()}>\n <GlassChoices {...props} />\n </SettingsRow>\n <Show when={!props.surfaceCapability.frosted && props.surfaceCapability.reason}>\n <p role=\"status\" class=\"px-4 py-2 text-xs text-muted-foreground\">\n {props.surfaceCapability.reason}\n </p>\n </Show>\n <SettingsRow\n title=\"Reduce transparency\"\n icon={<EyeOff />}\n description=\"Prefer solid surfaces, including during live preview.\"\n >\n <Switch\n checked={props.draft.reduceTransparency}\n disabled={props.saving}\n onChange={(reduceTransparency: boolean) => props.onChange({ reduceTransparency })}\n aria-label=\"Reduce transparency\"\n />\n </SettingsRow>\n <SettingsRow\n title=\"Theme library\"\n icon={<Palette />}\n description={\n props.onManageThemes\n ? 'Manage themes through the application.'\n : 'Theme import is unavailable.'\n }\n >\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={!props.onManageThemes || props.saving}\n onClick={() => props.onManageThemes?.()}\n >\n Manage themes\n </Button>\n </SettingsRow>\n </div>\n <Show when={props.saveDisabledReason}>\n <p id={saveReasonId} role=\"status\" class=\"px-4 pt-3 text-xs text-muted-foreground\">\n {props.saveDisabledReason}\n </p>\n </Show>\n {/* Sticky so Save/Cancel stay reachable in a tall scroll container: the\n editor scrolls with its host panel, and actions parked at the bottom\n of a long form were unreachable without scrolling past every row. */}\n <div class=\"sticky bottom-0 z-10 mt-2 flex flex-wrap items-center gap-2 border-t bg-background px-4 py-3\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n disabled={props.saving}\n onClick={() => props.onReset()}\n >\n Reset\n </Button>\n <div class=\"ml-auto flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={props.saving}\n onClick={() => props.onCancel()}\n >\n Cancel\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={props.saving || !!props.customAccentError || !!props.saveDisabledReason}\n aria-busy={props.saving}\n aria-describedby={props.saveDisabledReason ? saveReasonId : undefined}\n onClick={() => props.onSave()}\n >\n {props.saving ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\n/** Modal anchoring/dismissal and nested-menu behavior come from Kobalte.\n * Its measured available height also bounds scrolling, keeping footer actions\n * reachable when a narrow popup begins below its trigger. */\nexport function AppearancePopover(props: AppearancePopoverProps) {\n return (\n <Popover\n open={props.open}\n modal\n onOpenChange={(open) => (open ? props.onOpen() : props.onDismiss())}\n >\n <PopoverTrigger\n as={Button}\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Appearance settings\"\n >\n <Palette />\n </PopoverTrigger>\n <PopoverContent class=\"w-lg max-w-[calc(100vw-2rem)] max-h-(--kb-popper-content-available-height) overflow-y-auto\">\n <PopoverTitle>Appearance</PopoverTitle>\n <PopoverDescription>\n Changes preview immediately. Save keeps them; Cancel restores the previous appearance.\n </PopoverDescription>\n <AppearanceEditor {...props} />\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
@@ -86,6 +86,19 @@ export type ThemeProviderProps = {
86
86
  storageKey?: string;
87
87
  /** Start from this instead of the stored preference. */
88
88
  initial?: Partial<ThemeSelection>;
89
+ /**
90
+ * The selection, owned by the caller.
91
+ *
92
+ * For a consumer that keeps the state itself — a settings store, or a workshop
93
+ * whose selection lives in Storybook globals — rather than handing it to this
94
+ * provider's signal. Each change lands here and the document follows; nothing is
95
+ * written to the store, because the caller is the one who persists. A picker
96
+ * below the provider still works, and its change is the caller's to read back.
97
+ *
98
+ * Absent, the provider is the whole owner: it reads `storageKey` on mount,
99
+ * seeds from `initial`, and persists every `setSelection`.
100
+ */
101
+ selection?: Partial<ThemeSelection>;
89
102
  };
90
103
  export declare function ThemeProvider(props: ThemeProviderProps): JSX.Element;
91
104
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../../src/components/theme/theme-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAWnC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAA;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,2EAA2E;IAC3E,UAAU,EAAE,eAAe,GAAG,QAAQ,CAAA;IACtC,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAED,eAAO,MAAM,qBAAqB,EAAE,cAOnC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,SAAS,CAAA;AAEpD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,cAAc,GAAG,YAAY,CAEvE;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,SAAS,EAAE,MAAM,cAAc,CAAA;IAC/B,iEAAiE;IACjE,kBAAkB,EAAE,MAAM,eAAe,CAAA;IACzC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,YAAY,CAAA;IAC3B,qCAAqC;IACrC,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;IACtD,gEAAgE;IAChE,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;CAChC,CAAA;AAID,wBAAgB,QAAQ,IAAI,iBAAiB,CAI5C;AAsCD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACtB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;CAClC,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,eA6FtD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,UAAU,SAAoB,GAAG,MAAM,CAElE"}
1
+ {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../../src/components/theme/theme-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAWnC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAA;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,2EAA2E;IAC3E,UAAU,EAAE,eAAe,GAAG,QAAQ,CAAA;IACtC,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAED,eAAO,MAAM,qBAAqB,EAAE,cAOnC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,SAAS,CAAA;AAEpD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,cAAc,GAAG,YAAY,CAEvE;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,SAAS,EAAE,MAAM,cAAc,CAAA;IAC/B,iEAAiE;IACjE,kBAAkB,EAAE,MAAM,eAAe,CAAA;IACzC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,YAAY,CAAA;IAC3B,qCAAqC;IACrC,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;IACtD,gEAAgE;IAChE,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;CAChC,CAAA;AAID,wBAAgB,QAAQ,IAAI,iBAAiB,CAI5C;AAsCD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACtB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IACjC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;CACpC,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,eAsGtD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,UAAU,SAAoB,GAAG,MAAM,CAElE"}
@@ -49,13 +49,21 @@ function ThemeProvider(props) {
49
49
  const [local] = splitProps(props, [
50
50
  "children",
51
51
  "storageKey",
52
- "initial"
52
+ "initial",
53
+ "selection"
53
54
  ]);
54
55
  const storageKey = () => local.storageKey ?? "adea-appearance";
55
56
  const [selection, setSelectionState] = createSignal({
56
57
  ...readSelection(storageKey()),
57
58
  ...local.initial
58
59
  });
60
+ createEffect(() => {
61
+ const incoming = local.selection;
62
+ if (incoming) setSelectionState((current) => ({
63
+ ...current,
64
+ ...incoming
65
+ }));
66
+ });
59
67
  const [systemAppearance, setSystemAppearance] = createSignal("light");
60
68
  if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
61
69
  const query = window.matchMedia("(prefers-color-scheme: dark)");
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.js","names":["JSX","createContext","createEffect","createMemo","createSignal","onCleanup","splitProps","useContext","accentPresets","fontOptions","accentVariables","builtinThemes","defaultDarkThemeId","defaultLightThemeId","themeById","themeCssVariables","ThemeAppearance","ThemeVariant","ThemeSelection","appearance","lightThemeId","darkThemeId","accent","font","density","ThemeDensity","defaultThemeSelection","resolvedDensity","selection","ThemeContextValue","resolvedAppearance","variant","setSelection","patch","Partial","themes","ThemeContext","useTheme","value","Error","readSelection","storageKey","localStorage","raw","getItem","parsed","JSON","parse","some","preset","id","option","ThemeProviderProps","children","Element","initial","ThemeProvider","props","local","setSelectionState","systemAppearance","setSystemAppearance","window","matchMedia","query","matches","onChange","event","MediaQueryListEvent","addEventListener","removeEventListener","preference","document","root","documentElement","active","properties","Object","assign","name","entries","style","setProperty","classList","toggle","dataset","colorScheme","removeAttribute","next","setItem","stringify","_$createComponent","Provider","inlineScriptLiteral","replace","character","charCodeAt","toString","padStart","themeScript","_$createComponent"],"sources":["../../../src/components/theme/theme-provider.tsx"],"sourcesContent":["import type { JSX } from 'solid-js'\nimport {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n onCleanup,\n splitProps,\n useContext,\n} from 'solid-js'\nimport { accentPresets, fontOptions } from '#lib/tokens'\nimport {\n accentVariables,\n builtinThemes,\n defaultDarkThemeId,\n defaultLightThemeId,\n themeById,\n themeCssVariables,\n type ThemeAppearance,\n type ThemeVariant,\n} from '#lib/themes'\n\n/**\n * ThemeProvider.\n *\n * Owns the appearance preference and applies it to the document. One provider near\n * the root; everything below reads it.\n *\n * ## What a preference is\n *\n * Four independent choices, and keeping them independent is the point:\n *\n * - **appearance** — light, dark, or follow the system.\n * - **a theme per appearance** — so a user can run Catppuccin Mocha by night and\n * Solarized Light by day without re-choosing when the system flips.\n * - **an accent** — adea's six presets, which re-colour the interactive roles on\n * top of whichever theme is active.\n * - **a font** — the interface face.\n *\n * Four axes rather than one list of presets, because they compose: a user who wants\n * Nord with a violet accent and the system font should not need someone to author\n * that combination.\n *\n * ## Why the variants are applied as inline properties\n *\n * The catalogue is data, and a theme is applied by writing its roles onto\n * `<html>` as custom properties. That is what makes an imported palette work\n * without a hand-written `[data-theme]` block per theme, and it is why a settings\n * UI can show a preview by reading the same object rather than by loading a\n * stylesheet. `theme.css` keeps the default theme's values so the first paint is\n * correct before any script runs.\n */\nexport type ThemeSelection = {\n /** `system` follows `prefers-color-scheme` and updates when it changes. */\n appearance: ThemeAppearance | 'system'\n /** The variant to use when the resolved appearance is light. */\n lightThemeId: string\n /** The variant to use when the resolved appearance is dark. */\n darkThemeId: string\n /** An `accentPresets` id, or `theme` for the variant's own primary. */\n accent: string\n /** A `fontOptions` id, or `space-grotesk` for the default. */\n font: string\n /**\n * `comfortable` is the system's default geometry; `compact` moves the control\n * ladder down one rung.\n *\n * Optional, and defaulted on read, so a preference stored before density existed\n * keeps working and `ThemeSelection` stays assignable without it.\n */\n density?: ThemeDensity\n}\n\nexport const defaultThemeSelection: ThemeSelection = {\n appearance: 'system',\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n accent: 'theme',\n font: 'space-grotesk',\n density: 'comfortable',\n}\n\n/**\n * The two density rungs.\n *\n * `comfortable` is the geometry in `theme.css`, so it is the absence of an\n * attribute rather than a value: a document that ships no `data-density` at all\n * renders correctly, which is what keeps this additive.\n */\nexport type ThemeDensity = 'comfortable' | 'compact'\n\n/**\n * A stored selection's density, defaulted.\n *\n * The stored value is read through here rather than trusted, because a preference\n * written before density existed has no `density` key and a hand-edited store may\n * hold anything.\n */\nexport function resolvedDensity(selection: ThemeSelection): ThemeDensity {\n return selection.density === 'compact' ? 'compact' : 'comfortable'\n}\n\nexport type ThemeContextValue = {\n /** The preference as stored. */\n selection: () => ThemeSelection\n /** What the preference resolves to right now, after `system`. */\n resolvedAppearance: () => ThemeAppearance\n /** The variant in use. */\n variant: () => ThemeVariant\n /** Update part of the preference. */\n setSelection: (patch: Partial<ThemeSelection>) => void\n /** The catalogue, so a picker does not import it separately. */\n themes: readonly ThemeVariant[]\n}\n\nconst ThemeContext = createContext<ThemeContextValue>()\n\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext)\n if (!value) throw new Error('useTheme must be called inside a <ThemeProvider>.')\n return value\n}\n\n/** The stored preference, validated against the catalogue. */\nfunction readSelection(storageKey: string): ThemeSelection {\n if (typeof localStorage === 'undefined') return defaultThemeSelection\n try {\n const raw = localStorage.getItem(storageKey)\n if (!raw) return defaultThemeSelection\n const parsed = JSON.parse(raw) as Partial<ThemeSelection>\n return {\n appearance:\n parsed.appearance === 'light' ||\n parsed.appearance === 'dark' ||\n parsed.appearance === 'system'\n ? parsed.appearance\n : defaultThemeSelection.appearance,\n // An unknown id would leave the document with no theme at all, so a stale\n // preference from an older catalogue falls back rather than applying nothing.\n lightThemeId:\n themeById(parsed.lightThemeId ?? '')?.appearance === 'light'\n ? (parsed.lightThemeId as string)\n : defaultLightThemeId,\n darkThemeId:\n themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'\n ? (parsed.darkThemeId as string)\n : defaultDarkThemeId,\n accent: accentPresets.some((preset) => preset.id === parsed.accent)\n ? (parsed.accent as string)\n : defaultThemeSelection.accent,\n font: fontOptions.some((option) => option.id === parsed.font)\n ? (parsed.font as string)\n : defaultThemeSelection.font,\n }\n } catch {\n return defaultThemeSelection\n }\n}\n\nexport type ThemeProviderProps = {\n children?: JSX.Element\n /** Where the preference is persisted. Distinct per application. */\n storageKey?: string\n /** Start from this instead of the stored preference. */\n initial?: Partial<ThemeSelection>\n}\n\nexport function ThemeProvider(props: ThemeProviderProps) {\n const [local] = splitProps(props, ['children', 'storageKey', 'initial'])\n const storageKey = () => local.storageKey ?? 'adea-appearance'\n\n const [selection, setSelectionState] = createSignal<ThemeSelection>({\n ...readSelection(storageKey()),\n ...local.initial,\n })\n\n const [systemAppearance, setSystemAppearance] = createSignal<ThemeAppearance>('light')\n\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n const query = window.matchMedia('(prefers-color-scheme: dark)')\n setSystemAppearance(query.matches ? 'dark' : 'light')\n const onChange = (event: MediaQueryListEvent) =>\n setSystemAppearance(event.matches ? 'dark' : 'light')\n query.addEventListener('change', onChange)\n onCleanup(() => query.removeEventListener('change', onChange))\n }\n\n const resolvedAppearance = createMemo<ThemeAppearance>(() => {\n const preference = selection().appearance\n if (preference === 'system') return systemAppearance()\n return preference\n })\n\n const variant = createMemo<ThemeVariant>(() => {\n const id = resolvedAppearance() === 'dark' ? selection().darkThemeId : selection().lightThemeId\n return themeById(id) ?? builtinThemes[0]!\n })\n\n createEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const active = variant()\n\n // The variant's roles, as properties. `--primary` and `--ring` are written here\n // too, and an accent selection is layered over them below.\n //\n // The accent is applied *here* rather than left to the `[data-accent]` blocks in\n // `theme.css`, because this loop writes inline custom properties and an inline\n // declaration outranks every stylesheet selector however specific it is. The\n // blocks still exist and are still correct — they are what styles the window\n // before this runs — but they cannot win from here, so leaving the accent to\n // them meant the attribute was set, the rules were present, and nothing\n // changed on screen.\n const properties = { ...themeCssVariables(active) }\n const accent = accentVariables(selection().accent, resolvedAppearance())\n if (accent) Object.assign(properties, accent)\n\n for (const [name, value] of Object.entries(properties)) {\n root.style.setProperty(name, value)\n }\n\n root.classList.toggle('dark', resolvedAppearance() === 'dark')\n root.dataset['theme'] = active.id\n root.dataset['appearance'] = resolvedAppearance()\n root.style.colorScheme = resolvedAppearance()\n\n if (selection().accent === 'theme') root.removeAttribute('data-accent')\n else root.dataset['accent'] = selection().accent\n\n if (selection().font === 'space-grotesk') root.removeAttribute('data-font')\n else root.dataset['font'] = selection().font\n\n // Density is a pure token shift, so unlike the colour axes there is nothing to\n // compute: the attribute selects a block in `theme.css`. It is applied here for\n // the same reason as the others — this is the one place that knows what the\n // current selection is, and it is the only writer of these attributes.\n if (resolvedDensity(selection()) === 'comfortable') root.removeAttribute('data-density')\n else root.dataset['density'] = resolvedDensity(selection())\n })\n\n const value: ThemeContextValue = {\n selection,\n resolvedAppearance,\n variant,\n themes: builtinThemes,\n setSelection(patch) {\n const next = { ...selection(), ...patch }\n setSelectionState(next)\n if (typeof localStorage !== 'undefined') {\n try {\n localStorage.setItem(storageKey(), JSON.stringify(next))\n } catch {\n // A full or unavailable store must not break the switch: the preference\n // simply does not survive the reload.\n }\n }\n },\n }\n\n return <ThemeContext.Provider value={value}>{local.children}</ThemeContext.Provider>\n}\n\n/**\n * A string as a JavaScript literal that is also safe inside an inline `<script>`.\n *\n * `JSON.stringify` produces a correct JavaScript literal, and it is **not** safe\n * here: it leaves `<` and `>` alone, so a value containing `</script>` ends the tag\n * early and everything after it is parsed as HTML. That is the classic inline-script\n * injection, and it is a real one even though the value is a developer-supplied prop\n * rather than user input — a prop can come from configuration, and a key that happens\n * to contain a closing tag should not be able to execute.\n *\n * The escape is also correct JavaScript: `\\u003c` is `<` to the parser, so the value\n * is byte-identical at runtime and inert to the HTML tokenizer. `\\u2028` and `\\u2029`\n * are escaped for the neighbouring reason — they are legal inside a JSON string and\n * were, before ES2019, line terminators to a JavaScript parser.\n *\n * CodeQL flags the unescaped form as `js/bad-code-sanitization`, which is how this\n * was found; `tests/theme-script.test.ts` pins it so it cannot come back.\n */\nexport function inlineScriptLiteral(value: string): string {\n return JSON.stringify(value).replace(\n /[<>\\u2028\\u2029]/g,\n (character) => `\\\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`\n )\n}\n\n/**\n * The no-flash script.\n *\n * Rendered inline in `<head>` before any stylesheet that matters. Without it a dark\n * preference paints the light theme first and then swaps, which is the flash every\n * themed application has to solve and the one thing a provider cannot do from\n * inside the body.\n *\n * Deliberately tiny and dependency-free: it reads the stored preference, resolves\n * `system`, and sets the class. It does not apply the variant's roles — the default\n * theme in `theme.css` covers the first paint, and the provider writes the rest on\n * mount.\n *\n * The script reads two values out of `localStorage` and writes them to `dataset`,\n * which is a *property* assignment rather than code: a stored accent or font id can\n * set an attribute and cannot execute. The one value that reaches the source as text\n * is `storageKey`, and it goes through {@link inlineScriptLiteral} for the reason\n * documented there.\n */\nexport function themeScript(storageKey = 'adea-appearance'): string {\n return `(function(){try{var s=localStorage.getItem(${inlineScriptLiteral(storageKey)});var p=s?JSON.parse(s):{};var a=p.appearance||'system';var d=a==='dark'||(a==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);var r=document.documentElement;if(d)r.classList.add('dark');r.style.colorScheme=d?'dark':'light';if(p.accent&&p.accent!=='theme')r.dataset.accent=p.accent;if(p.font&&p.font!=='space-grotesk')r.dataset.font=p.font;if(p.density==='compact')r.dataset.density='compact';}catch(e){}})();`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme-provider.js","names":["JSX","createContext","createEffect","createMemo","createSignal","onCleanup","splitProps","useContext","accentPresets","fontOptions","accentVariables","builtinThemes","defaultDarkThemeId","defaultLightThemeId","themeById","themeCssVariables","ThemeAppearance","ThemeVariant","ThemeSelection","appearance","lightThemeId","darkThemeId","accent","font","density","ThemeDensity","defaultThemeSelection","resolvedDensity","selection","ThemeContextValue","resolvedAppearance","variant","setSelection","patch","Partial","themes","ThemeContext","useTheme","value","Error","readSelection","storageKey","localStorage","raw","getItem","parsed","JSON","parse","some","preset","id","option","ThemeProviderProps","children","Element","initial","ThemeProvider","props","local","setSelectionState","incoming","current","systemAppearance","setSystemAppearance","window","matchMedia","query","matches","onChange","event","MediaQueryListEvent","addEventListener","removeEventListener","preference","document","root","documentElement","active","properties","Object","assign","name","entries","style","setProperty","classList","toggle","dataset","colorScheme","removeAttribute","next","setItem","stringify","_$createComponent","Provider","inlineScriptLiteral","replace","character","charCodeAt","toString","padStart","themeScript","_$createComponent"],"sources":["../../../src/components/theme/theme-provider.tsx"],"sourcesContent":["import type { JSX } from 'solid-js'\nimport {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n onCleanup,\n splitProps,\n useContext,\n} from 'solid-js'\nimport { accentPresets, fontOptions } from '#lib/tokens'\nimport {\n accentVariables,\n builtinThemes,\n defaultDarkThemeId,\n defaultLightThemeId,\n themeById,\n themeCssVariables,\n type ThemeAppearance,\n type ThemeVariant,\n} from '#lib/themes'\n\n/**\n * ThemeProvider.\n *\n * Owns the appearance preference and applies it to the document. One provider near\n * the root; everything below reads it.\n *\n * ## What a preference is\n *\n * Four independent choices, and keeping them independent is the point:\n *\n * - **appearance** — light, dark, or follow the system.\n * - **a theme per appearance** — so a user can run Catppuccin Mocha by night and\n * Solarized Light by day without re-choosing when the system flips.\n * - **an accent** — adea's six presets, which re-colour the interactive roles on\n * top of whichever theme is active.\n * - **a font** — the interface face.\n *\n * Four axes rather than one list of presets, because they compose: a user who wants\n * Nord with a violet accent and the system font should not need someone to author\n * that combination.\n *\n * ## Why the variants are applied as inline properties\n *\n * The catalogue is data, and a theme is applied by writing its roles onto\n * `<html>` as custom properties. That is what makes an imported palette work\n * without a hand-written `[data-theme]` block per theme, and it is why a settings\n * UI can show a preview by reading the same object rather than by loading a\n * stylesheet. `theme.css` keeps the default theme's values so the first paint is\n * correct before any script runs.\n */\nexport type ThemeSelection = {\n /** `system` follows `prefers-color-scheme` and updates when it changes. */\n appearance: ThemeAppearance | 'system'\n /** The variant to use when the resolved appearance is light. */\n lightThemeId: string\n /** The variant to use when the resolved appearance is dark. */\n darkThemeId: string\n /** An `accentPresets` id, or `theme` for the variant's own primary. */\n accent: string\n /** A `fontOptions` id, or `space-grotesk` for the default. */\n font: string\n /**\n * `comfortable` is the system's default geometry; `compact` moves the control\n * ladder down one rung.\n *\n * Optional, and defaulted on read, so a preference stored before density existed\n * keeps working and `ThemeSelection` stays assignable without it.\n */\n density?: ThemeDensity\n}\n\nexport const defaultThemeSelection: ThemeSelection = {\n appearance: 'system',\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n accent: 'theme',\n font: 'space-grotesk',\n density: 'comfortable',\n}\n\n/**\n * The two density rungs.\n *\n * `comfortable` is the geometry in `theme.css`, so it is the absence of an\n * attribute rather than a value: a document that ships no `data-density` at all\n * renders correctly, which is what keeps this additive.\n */\nexport type ThemeDensity = 'comfortable' | 'compact'\n\n/**\n * A stored selection's density, defaulted.\n *\n * The stored value is read through here rather than trusted, because a preference\n * written before density existed has no `density` key and a hand-edited store may\n * hold anything.\n */\nexport function resolvedDensity(selection: ThemeSelection): ThemeDensity {\n return selection.density === 'compact' ? 'compact' : 'comfortable'\n}\n\nexport type ThemeContextValue = {\n /** The preference as stored. */\n selection: () => ThemeSelection\n /** What the preference resolves to right now, after `system`. */\n resolvedAppearance: () => ThemeAppearance\n /** The variant in use. */\n variant: () => ThemeVariant\n /** Update part of the preference. */\n setSelection: (patch: Partial<ThemeSelection>) => void\n /** The catalogue, so a picker does not import it separately. */\n themes: readonly ThemeVariant[]\n}\n\nconst ThemeContext = createContext<ThemeContextValue>()\n\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext)\n if (!value) throw new Error('useTheme must be called inside a <ThemeProvider>.')\n return value\n}\n\n/** The stored preference, validated against the catalogue. */\nfunction readSelection(storageKey: string): ThemeSelection {\n if (typeof localStorage === 'undefined') return defaultThemeSelection\n try {\n const raw = localStorage.getItem(storageKey)\n if (!raw) return defaultThemeSelection\n const parsed = JSON.parse(raw) as Partial<ThemeSelection>\n return {\n appearance:\n parsed.appearance === 'light' ||\n parsed.appearance === 'dark' ||\n parsed.appearance === 'system'\n ? parsed.appearance\n : defaultThemeSelection.appearance,\n // An unknown id would leave the document with no theme at all, so a stale\n // preference from an older catalogue falls back rather than applying nothing.\n lightThemeId:\n themeById(parsed.lightThemeId ?? '')?.appearance === 'light'\n ? (parsed.lightThemeId as string)\n : defaultLightThemeId,\n darkThemeId:\n themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'\n ? (parsed.darkThemeId as string)\n : defaultDarkThemeId,\n accent: accentPresets.some((preset) => preset.id === parsed.accent)\n ? (parsed.accent as string)\n : defaultThemeSelection.accent,\n font: fontOptions.some((option) => option.id === parsed.font)\n ? (parsed.font as string)\n : defaultThemeSelection.font,\n }\n } catch {\n return defaultThemeSelection\n }\n}\n\nexport type ThemeProviderProps = {\n children?: JSX.Element\n /** Where the preference is persisted. Distinct per application. */\n storageKey?: string\n /** Start from this instead of the stored preference. */\n initial?: Partial<ThemeSelection>\n /**\n * The selection, owned by the caller.\n *\n * For a consumer that keeps the state itself — a settings store, or a workshop\n * whose selection lives in Storybook globals — rather than handing it to this\n * provider's signal. Each change lands here and the document follows; nothing is\n * written to the store, because the caller is the one who persists. A picker\n * below the provider still works, and its change is the caller's to read back.\n *\n * Absent, the provider is the whole owner: it reads `storageKey` on mount,\n * seeds from `initial`, and persists every `setSelection`.\n */\n selection?: Partial<ThemeSelection>\n}\n\nexport function ThemeProvider(props: ThemeProviderProps) {\n const [local] = splitProps(props, ['children', 'storageKey', 'initial', 'selection'])\n const storageKey = () => local.storageKey ?? 'adea-appearance'\n\n const [selection, setSelectionState] = createSignal<ThemeSelection>({\n ...readSelection(storageKey()),\n ...local.initial,\n })\n\n // The controlled half of the contract. A patch from the owner replaces the\n // matching axes; the stored preference is untouched, since the owner is the one\n // that persists. Reading `local.selection` — a prop getter, not a destructured\n // value — is what keeps this subscribed for the provider's lifetime.\n createEffect(() => {\n const incoming = local.selection\n if (incoming) setSelectionState((current) => ({ ...current, ...incoming }))\n })\n\n const [systemAppearance, setSystemAppearance] = createSignal<ThemeAppearance>('light')\n\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n const query = window.matchMedia('(prefers-color-scheme: dark)')\n setSystemAppearance(query.matches ? 'dark' : 'light')\n const onChange = (event: MediaQueryListEvent) =>\n setSystemAppearance(event.matches ? 'dark' : 'light')\n query.addEventListener('change', onChange)\n onCleanup(() => query.removeEventListener('change', onChange))\n }\n\n const resolvedAppearance = createMemo<ThemeAppearance>(() => {\n const preference = selection().appearance\n if (preference === 'system') return systemAppearance()\n return preference\n })\n\n const variant = createMemo<ThemeVariant>(() => {\n const id = resolvedAppearance() === 'dark' ? selection().darkThemeId : selection().lightThemeId\n return themeById(id) ?? builtinThemes[0]!\n })\n\n createEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const active = variant()\n\n // The variant's roles, as properties. `--primary` and `--ring` are written here\n // too, and an accent selection is layered over them below.\n //\n // The accent is applied *here* rather than left to the `[data-accent]` blocks in\n // `theme.css`, because this loop writes inline custom properties and an inline\n // declaration outranks every stylesheet selector however specific it is. The\n // blocks still exist and are still correct — they are what styles the window\n // before this runs — but they cannot win from here, so leaving the accent to\n // them meant the attribute was set, the rules were present, and nothing\n // changed on screen.\n const properties = { ...themeCssVariables(active) }\n const accent = accentVariables(selection().accent, resolvedAppearance())\n if (accent) Object.assign(properties, accent)\n\n for (const [name, value] of Object.entries(properties)) {\n root.style.setProperty(name, value)\n }\n\n root.classList.toggle('dark', resolvedAppearance() === 'dark')\n root.dataset['theme'] = active.id\n root.dataset['appearance'] = resolvedAppearance()\n root.style.colorScheme = resolvedAppearance()\n\n if (selection().accent === 'theme') root.removeAttribute('data-accent')\n else root.dataset['accent'] = selection().accent\n\n if (selection().font === 'space-grotesk') root.removeAttribute('data-font')\n else root.dataset['font'] = selection().font\n\n // Density is a pure token shift, so unlike the colour axes there is nothing to\n // compute: the attribute selects a block in `theme.css`. It is applied here for\n // the same reason as the others — this is the one place that knows what the\n // current selection is, and it is the only writer of these attributes.\n if (resolvedDensity(selection()) === 'comfortable') root.removeAttribute('data-density')\n else root.dataset['density'] = resolvedDensity(selection())\n })\n\n const value: ThemeContextValue = {\n selection,\n resolvedAppearance,\n variant,\n themes: builtinThemes,\n setSelection(patch) {\n const next = { ...selection(), ...patch }\n setSelectionState(next)\n if (typeof localStorage !== 'undefined') {\n try {\n localStorage.setItem(storageKey(), JSON.stringify(next))\n } catch {\n // A full or unavailable store must not break the switch: the preference\n // simply does not survive the reload.\n }\n }\n },\n }\n\n return <ThemeContext.Provider value={value}>{local.children}</ThemeContext.Provider>\n}\n\n/**\n * A string as a JavaScript literal that is also safe inside an inline `<script>`.\n *\n * `JSON.stringify` produces a correct JavaScript literal, and it is **not** safe\n * here: it leaves `<` and `>` alone, so a value containing `</script>` ends the tag\n * early and everything after it is parsed as HTML. That is the classic inline-script\n * injection, and it is a real one even though the value is a developer-supplied prop\n * rather than user input — a prop can come from configuration, and a key that happens\n * to contain a closing tag should not be able to execute.\n *\n * The escape is also correct JavaScript: `\\u003c` is `<` to the parser, so the value\n * is byte-identical at runtime and inert to the HTML tokenizer. `\\u2028` and `\\u2029`\n * are escaped for the neighbouring reason — they are legal inside a JSON string and\n * were, before ES2019, line terminators to a JavaScript parser.\n *\n * CodeQL flags the unescaped form as `js/bad-code-sanitization`, which is how this\n * was found; `tests/theme-script.test.ts` pins it so it cannot come back.\n */\nexport function inlineScriptLiteral(value: string): string {\n return JSON.stringify(value).replace(\n /[<>\\u2028\\u2029]/g,\n (character) => `\\\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`\n )\n}\n\n/**\n * The no-flash script.\n *\n * Rendered inline in `<head>` before any stylesheet that matters. Without it a dark\n * preference paints the light theme first and then swaps, which is the flash every\n * themed application has to solve and the one thing a provider cannot do from\n * inside the body.\n *\n * Deliberately tiny and dependency-free: it reads the stored preference, resolves\n * `system`, and sets the class. It does not apply the variant's roles — the default\n * theme in `theme.css` covers the first paint, and the provider writes the rest on\n * mount.\n *\n * The script reads two values out of `localStorage` and writes them to `dataset`,\n * which is a *property* assignment rather than code: a stored accent or font id can\n * set an attribute and cannot execute. The one value that reaches the source as text\n * is `storageKey`, and it goes through {@link inlineScriptLiteral} for the reason\n * documented there.\n */\nexport function themeScript(storageKey = 'adea-appearance'): string {\n return `(function(){try{var s=localStorage.getItem(${inlineScriptLiteral(storageKey)});var p=s?JSON.parse(s):{};var a=p.appearance||'system';var d=a==='dark'||(a==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);var r=document.documentElement;if(d)r.classList.add('dark');r.style.colorScheme=d?'dark':'light';if(p.accent&&p.accent!=='theme')r.dataset.accent=p.accent;if(p.font&&p.font!=='space-grotesk')r.dataset.font=p.font;if(p.density==='compact')r.dataset.density='compact';}catch(e){}})();`\n}\n"],"mappings":"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"}
@@ -93,7 +93,9 @@ export function AccentChoices(props: AppearanceEditorProps) {
93
93
  disabled={props.saving}
94
94
  orientation="horizontal"
95
95
  aria-label="Accent"
96
- class="flex flex-wrap items-center gap-2"
96
+ /* Narrow hosts wrap the swatches into a three-column grid so the row
97
+ control stays beside its label; wide hosts keep one inline row. */
98
+ class="grid grid-cols-3 items-center gap-2 sm:flex sm:flex-wrap sm:justify-end"
97
99
  onChange={(accent) =>
98
100
  props.onChange({
99
101
  accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,
@@ -106,31 +108,33 @@ export function AccentChoices(props: AppearanceEditorProps) {
106
108
  Theme default
107
109
  </Radio.ItemLabel>
108
110
  </Radio.Item>
109
- <For each={props.accentOptions}>
110
- {(option) => (
111
- <Radio.Item
112
- value={option.id}
113
- class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
114
- >
115
- <Radio.ItemInput />
116
- <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
117
- <span
118
- class="block size-full rounded-full"
119
- aria-hidden="true"
120
- style={{
121
- 'background-color':
122
- (props.draft.mode === 'system'
123
- ? props.resolvedAppearance
124
- : props.draft.mode) === 'light'
125
- ? option.light
126
- : option.dark,
127
- }}
128
- />
129
- <span class="sr-only">{option.label}</span>
130
- </Radio.ItemLabel>
131
- </Radio.Item>
132
- )}
133
- </For>
111
+ <div class="col-span-3 grid grid-cols-3 gap-2 sm:col-span-1 sm:flex sm:gap-2">
112
+ <For each={props.accentOptions}>
113
+ {(option) => (
114
+ <Radio.Item
115
+ value={option.id}
116
+ class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
117
+ >
118
+ <Radio.ItemInput />
119
+ <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
120
+ <span
121
+ class="block size-full rounded-full"
122
+ aria-hidden="true"
123
+ style={{
124
+ 'background-color':
125
+ (props.draft.mode === 'system'
126
+ ? props.resolvedAppearance
127
+ : props.draft.mode) === 'light'
128
+ ? option.light
129
+ : option.dark,
130
+ }}
131
+ />
132
+ <span class="sr-only">{option.label}</span>
133
+ </Radio.ItemLabel>
134
+ </Radio.Item>
135
+ )}
136
+ </For>
137
+ </div>
134
138
  <Radio.Item value="custom">
135
139
  <Radio.ItemInput />
136
140
  <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
@@ -99,12 +99,7 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
99
99
  disabled={props.saving}
100
100
  onSelect={(darkThemeId) => props.onChange({ darkThemeId })}
101
101
  />
102
- <SettingsRow
103
- stacked
104
- title="Accent"
105
- icon={<SlidersHorizontal />}
106
- description={accentDescription()}
107
- >
102
+ <SettingsRow title="Accent" icon={<SlidersHorizontal />} description={accentDescription()}>
108
103
  <AccentChoices {...props} />
109
104
  </SettingsRow>
110
105
  <Show when={custom()}>
@@ -172,7 +167,10 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
172
167
  {props.saveDisabledReason}
173
168
  </p>
174
169
  </Show>
175
- <div class="flex flex-wrap items-center gap-2 px-4 py-4">
170
+ {/* Sticky so Save/Cancel stay reachable in a tall scroll container: the
171
+ editor scrolls with its host panel, and actions parked at the bottom
172
+ of a long form were unreachable without scrolling past every row. */}
173
+ <div class="sticky bottom-0 z-10 mt-2 flex flex-wrap items-center gap-2 border-t bg-background px-4 py-3">
176
174
  <Button
177
175
  type="button"
178
176
  size="sm"
@@ -163,10 +163,23 @@ export type ThemeProviderProps = {
163
163
  storageKey?: string
164
164
  /** Start from this instead of the stored preference. */
165
165
  initial?: Partial<ThemeSelection>
166
+ /**
167
+ * The selection, owned by the caller.
168
+ *
169
+ * For a consumer that keeps the state itself — a settings store, or a workshop
170
+ * whose selection lives in Storybook globals — rather than handing it to this
171
+ * provider's signal. Each change lands here and the document follows; nothing is
172
+ * written to the store, because the caller is the one who persists. A picker
173
+ * below the provider still works, and its change is the caller's to read back.
174
+ *
175
+ * Absent, the provider is the whole owner: it reads `storageKey` on mount,
176
+ * seeds from `initial`, and persists every `setSelection`.
177
+ */
178
+ selection?: Partial<ThemeSelection>
166
179
  }
167
180
 
168
181
  export function ThemeProvider(props: ThemeProviderProps) {
169
- const [local] = splitProps(props, ['children', 'storageKey', 'initial'])
182
+ const [local] = splitProps(props, ['children', 'storageKey', 'initial', 'selection'])
170
183
  const storageKey = () => local.storageKey ?? 'adea-appearance'
171
184
 
172
185
  const [selection, setSelectionState] = createSignal<ThemeSelection>({
@@ -174,6 +187,15 @@ export function ThemeProvider(props: ThemeProviderProps) {
174
187
  ...local.initial,
175
188
  })
176
189
 
190
+ // The controlled half of the contract. A patch from the owner replaces the
191
+ // matching axes; the stored preference is untouched, since the owner is the one
192
+ // that persists. Reading `local.selection` — a prop getter, not a destructured
193
+ // value — is what keeps this subscribed for the provider's lifetime.
194
+ createEffect(() => {
195
+ const incoming = local.selection
196
+ if (incoming) setSelectionState((current) => ({ ...current, ...incoming }))
197
+ })
198
+
177
199
  const [systemAppearance, setSystemAppearance] = createSignal<ThemeAppearance>('light')
178
200
 
179
201
  if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -93,7 +93,9 @@ export function AccentChoices(props: AppearanceEditorProps) {
93
93
  disabled={props.saving}
94
94
  orientation="horizontal"
95
95
  aria-label="Accent"
96
- class="flex flex-wrap items-center gap-2"
96
+ /* Narrow hosts wrap the swatches into a three-column grid so the row
97
+ control stays beside its label; wide hosts keep one inline row. */
98
+ class="grid grid-cols-3 items-center gap-2 sm:flex sm:flex-wrap sm:justify-end"
97
99
  onChange={(accent) =>
98
100
  props.onChange({
99
101
  accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,
@@ -106,31 +108,33 @@ export function AccentChoices(props: AppearanceEditorProps) {
106
108
  Theme default
107
109
  </Radio.ItemLabel>
108
110
  </Radio.Item>
109
- <For each={props.accentOptions}>
110
- {(option) => (
111
- <Radio.Item
112
- value={option.id}
113
- class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
114
- >
115
- <Radio.ItemInput />
116
- <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
117
- <span
118
- class="block size-full rounded-full"
119
- aria-hidden="true"
120
- style={{
121
- 'background-color':
122
- (props.draft.mode === 'system'
123
- ? props.resolvedAppearance
124
- : props.draft.mode) === 'light'
125
- ? option.light
126
- : option.dark,
127
- }}
128
- />
129
- <span class="sr-only">{option.label}</span>
130
- </Radio.ItemLabel>
131
- </Radio.Item>
132
- )}
133
- </For>
111
+ <div class="col-span-3 grid grid-cols-3 gap-2 sm:col-span-1 sm:flex sm:gap-2">
112
+ <For each={props.accentOptions}>
113
+ {(option) => (
114
+ <Radio.Item
115
+ value={option.id}
116
+ class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
117
+ >
118
+ <Radio.ItemInput />
119
+ <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
120
+ <span
121
+ class="block size-full rounded-full"
122
+ aria-hidden="true"
123
+ style={{
124
+ 'background-color':
125
+ (props.draft.mode === 'system'
126
+ ? props.resolvedAppearance
127
+ : props.draft.mode) === 'light'
128
+ ? option.light
129
+ : option.dark,
130
+ }}
131
+ />
132
+ <span class="sr-only">{option.label}</span>
133
+ </Radio.ItemLabel>
134
+ </Radio.Item>
135
+ )}
136
+ </For>
137
+ </div>
134
138
  <Radio.Item value="custom">
135
139
  <Radio.ItemInput />
136
140
  <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
@@ -99,12 +99,7 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
99
99
  disabled={props.saving}
100
100
  onSelect={(darkThemeId) => props.onChange({ darkThemeId })}
101
101
  />
102
- <SettingsRow
103
- stacked
104
- title="Accent"
105
- icon={<SlidersHorizontal />}
106
- description={accentDescription()}
107
- >
102
+ <SettingsRow title="Accent" icon={<SlidersHorizontal />} description={accentDescription()}>
108
103
  <AccentChoices {...props} />
109
104
  </SettingsRow>
110
105
  <Show when={custom()}>
@@ -172,7 +167,10 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
172
167
  {props.saveDisabledReason}
173
168
  </p>
174
169
  </Show>
175
- <div class="flex flex-wrap items-center gap-2 px-4 py-4">
170
+ {/* Sticky so Save/Cancel stay reachable in a tall scroll container: the
171
+ editor scrolls with its host panel, and actions parked at the bottom
172
+ of a long form were unreachable without scrolling past every row. */}
173
+ <div class="sticky bottom-0 z-10 mt-2 flex flex-wrap items-center gap-2 border-t bg-background px-4 py-3">
176
174
  <Button
177
175
  type="button"
178
176
  size="sm"
@@ -163,10 +163,23 @@ export type ThemeProviderProps = {
163
163
  storageKey?: string
164
164
  /** Start from this instead of the stored preference. */
165
165
  initial?: Partial<ThemeSelection>
166
+ /**
167
+ * The selection, owned by the caller.
168
+ *
169
+ * For a consumer that keeps the state itself — a settings store, or a workshop
170
+ * whose selection lives in Storybook globals — rather than handing it to this
171
+ * provider's signal. Each change lands here and the document follows; nothing is
172
+ * written to the store, because the caller is the one who persists. A picker
173
+ * below the provider still works, and its change is the caller's to read back.
174
+ *
175
+ * Absent, the provider is the whole owner: it reads `storageKey` on mount,
176
+ * seeds from `initial`, and persists every `setSelection`.
177
+ */
178
+ selection?: Partial<ThemeSelection>
166
179
  }
167
180
 
168
181
  export function ThemeProvider(props: ThemeProviderProps) {
169
- const [local] = splitProps(props, ['children', 'storageKey', 'initial'])
182
+ const [local] = splitProps(props, ['children', 'storageKey', 'initial', 'selection'])
170
183
  const storageKey = () => local.storageKey ?? 'adea-appearance'
171
184
 
172
185
  const [selection, setSelectionState] = createSignal<ThemeSelection>({
@@ -174,6 +187,15 @@ export function ThemeProvider(props: ThemeProviderProps) {
174
187
  ...local.initial,
175
188
  })
176
189
 
190
+ // The controlled half of the contract. A patch from the owner replaces the
191
+ // matching axes; the stored preference is untouched, since the owner is the one
192
+ // that persists. Reading `local.selection` — a prop getter, not a destructured
193
+ // value — is what keeps this subscribed for the provider's lifetime.
194
+ createEffect(() => {
195
+ const incoming = local.selection
196
+ if (incoming) setSelectionState((current) => ({ ...current, ...incoming }))
197
+ })
198
+
177
199
  const [systemAppearance, setSystemAppearance] = createSignal<ThemeAppearance>('light')
178
200
 
179
201
  if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {