@adea-ai/ui 0.27.0 → 0.29.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,7 +1,24 @@
1
1
  import { type Accessor, type JSX, type ParentProps } from 'solid-js';
2
- export type Theme = 'light' | 'dark' | 'system';
3
- export type ResolvedTheme = 'light' | 'dark';
2
+ import { type AppearanceMode, type AppearancePreferencesV2, type ResolvedAppearance } from './appearance';
3
+ export type Theme = AppearanceMode;
4
+ export type ResolvedTheme = ResolvedAppearance;
5
+ export type { AppearanceMode, AppearancePreferencesV2, ResolvedAppearance } from './appearance';
4
6
  type ThemeContextValue = {
7
+ /** The persisted v2 appearance preferences. */
8
+ preferences: Accessor<AppearancePreferencesV2>;
9
+ /** Mode after combining the user choice with the OS report. */
10
+ resolvedMode: Accessor<ResolvedAppearance>;
11
+ /** The active theme variant id (`documentElement.dataset.theme`). */
12
+ variantId: Accessor<string>;
13
+ /** Surface after reduced-transparency and capability resolution. */
14
+ effectiveSurface: Accessor<'opaque' | 'frosted' | 'translucent'>;
15
+ /** True while the OS or the user forces opaque surfaces. */
16
+ reduceTransparencyActive: Accessor<boolean>;
17
+ /** Persist a field update immediately (Zeron's `SavePolicy::Immediate`). */
18
+ update: (patch: Partial<AppearancePreferencesV2>) => void;
19
+ /** Apply a draft against the visible app without persisting it. */
20
+ preview: (preferences: AppearancePreferencesV2 | undefined) => void;
21
+ /** Legacy single-mode seam (settings toggle and existing consumers). */
5
22
  theme: Accessor<Theme>;
6
23
  resolvedTheme: Accessor<ResolvedTheme>;
7
24
  setTheme: (theme: Theme) => void;
@@ -9,19 +26,27 @@ type ThemeContextValue = {
9
26
  type ThemeProviderProps = ParentProps<{
10
27
  defaultTheme?: Theme;
11
28
  disableTransitionOnChange?: boolean;
29
+ /**
30
+ * Whether the host window can render OS translucency. The web lane keeps
31
+ * the default `false` and renders tokenized frosted surfaces instead of
32
+ * pretending to OS vibrancy.
33
+ */
34
+ nativeTranslucency?: boolean;
12
35
  }>;
13
36
  /**
14
- * The shared Solid theme provider. It owns the same contract the previous
15
- * provider exposed to the token layer: a `dark` class on the document element
16
- * plus a `color-scheme` style, persisted in `localStorage` and defaulting to
17
- * the system preference.
37
+ * The shared Solid appearance provider. It owns the versioned appearance
38
+ * preference (mode, independent light/dark themes, accent, surface) on top of
39
+ * the contract the previous provider exposed to the token layer: a `dark`
40
+ * class on the document element plus a `color-scheme` style, persisted in
41
+ * `localStorage` and defaulting to the system preference. The legacy single
42
+ * `theme` key migrates on read and is never deleted.
18
43
  */
19
44
  export declare function ThemeProvider(props: ThemeProviderProps): JSX.Element;
20
45
  export declare function useTheme(): ThemeContextValue;
21
46
  /**
22
- * Pre-paint theme restore for server-rendered documents. Rendered in the
23
- * document head, it applies the stored or system theme before first paint so
24
- * hydration never shows the wrong palette.
47
+ * Pre-paint appearance restore for server-rendered documents. Rendered in the
48
+ * document head, it resolves the stored (or legacy) preference against the OS
49
+ * and applies the palette before first paint so hydration never shows the
50
+ * wrong theme.
25
51
  */
26
52
  export declare function ThemeScript(): JSX.Element;
27
- export {};
@@ -1,72 +1,85 @@
1
- import { createComponent, template } from "solid-js/web";
1
+ import { DARK_QUERY, REDUCED_TRANSPARENCY_QUERY, appearanceThemeScript, applyAppearanceToDocument, defaultAppearancePreferences, readAppearancePreferences, resolveAppearanceState, writeAppearancePreferences } from "./appearance.js";
2
+ import { createComponent, effect, template } from "solid-js/web";
2
3
  import { createContext, createEffect, createSignal, onCleanup, useContext } from "solid-js";
3
4
  //#region src/components/theme-provider.tsx
4
5
  var _tmpl$ = /*#__PURE__*/ template(`<script>`);
5
- var STORAGE_KEY = "theme";
6
- var DARK_QUERY = "(prefers-color-scheme: dark)";
7
6
  var ThemeContext = createContext();
8
- function readStoredTheme() {
9
- if (typeof window === "undefined") return "system";
10
- try {
11
- const stored = window.localStorage.getItem(STORAGE_KEY);
12
- return stored === "light" || stored === "dark" || stored === "system" ? stored : "system";
13
- } catch {
14
- return "system";
15
- }
16
- }
17
7
  function systemPrefersDark() {
18
- return typeof window !== "undefined" && window.matchMedia(DARK_QUERY).matches;
8
+ return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
19
9
  }
20
- /**
21
- * Applies the resolved theme to the document and suppresses transitions for the
22
- * frame that carries the change, so a theme switch cannot animate page-wide.
23
- */
24
- function applyTheme(theme, disableTransitionOnChange) {
25
- if (typeof document === "undefined") return;
26
- const root = document.documentElement;
27
- let restoreTransitions;
28
- if (disableTransitionOnChange) {
29
- const style = document.createElement("style");
30
- style.appendChild(document.createTextNode("*{transition:none !important}"));
31
- document.head.appendChild(style);
32
- style.offsetHeight;
33
- restoreTransitions = () => style.remove();
34
- }
35
- root.classList.toggle("dark", theme === "dark");
36
- root.style.colorScheme = theme;
37
- if (restoreTransitions) requestAnimationFrame(() => requestAnimationFrame(() => restoreTransitions?.()));
10
+ function systemReducesTransparency() {
11
+ return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-transparency: reduce)").matches;
38
12
  }
39
13
  /**
40
- * The shared Solid theme provider. It owns the same contract the previous
41
- * provider exposed to the token layer: a `dark` class on the document element
42
- * plus a `color-scheme` style, persisted in `localStorage` and defaulting to
43
- * the system preference.
14
+ * The shared Solid appearance provider. It owns the versioned appearance
15
+ * preference (mode, independent light/dark themes, accent, surface) on top of
16
+ * the contract the previous provider exposed to the token layer: a `dark`
17
+ * class on the document element plus a `color-scheme` style, persisted in
18
+ * `localStorage` and defaulting to the system preference. The legacy single
19
+ * `theme` key migrates on read and is never deleted.
44
20
  */
45
21
  function ThemeProvider(props) {
46
- const [theme, setThemeSignal] = createSignal(props.defaultTheme ?? readStoredTheme());
22
+ const storage = typeof window === "undefined" ? void 0 : window.localStorage;
23
+ const stored = readAppearancePreferences(storage);
24
+ const initial = stored === defaultAppearancePreferences && props.defaultTheme ? {
25
+ ...stored,
26
+ mode: props.defaultTheme
27
+ } : stored;
28
+ const [preferences, setPreferences] = createSignal(initial);
47
29
  const [prefersDark, setPrefersDark] = createSignal(systemPrefersDark());
48
- const resolvedTheme = () => {
49
- const current = theme();
50
- if (current === "system") return prefersDark() ? "dark" : "light";
51
- return current;
52
- };
30
+ const [osReducedTransparency, setOsReducedTransparency] = createSignal(systemReducesTransparency());
31
+ const nativeTranslucency = () => props.nativeTranslucency ?? false;
53
32
  if (typeof window !== "undefined") {
54
- const media = window.matchMedia(DARK_QUERY);
55
- const onChange = (event) => setPrefersDark(event.matches);
56
- media.addEventListener("change", onChange);
57
- onCleanup(() => media.removeEventListener("change", onChange));
33
+ const darkMedia = window.matchMedia(DARK_QUERY);
34
+ const onDarkChange = (event) => setPrefersDark(event.matches);
35
+ darkMedia.addEventListener("change", onDarkChange);
36
+ onCleanup(() => darkMedia.removeEventListener("change", onDarkChange));
37
+ const transparencyMedia = window.matchMedia(REDUCED_TRANSPARENCY_QUERY);
38
+ const onTransparencyChange = (event) => setOsReducedTransparency(event.matches);
39
+ transparencyMedia.addEventListener("change", onTransparencyChange);
40
+ onCleanup(() => transparencyMedia.removeEventListener("change", onTransparencyChange));
58
41
  }
42
+ const resolvedState = () => resolveAppearanceState(preferences(), {
43
+ systemAppearance: prefersDark() ? "dark" : "light",
44
+ osReducedTransparency: osReducedTransparency(),
45
+ nativeTranslucency: nativeTranslucency()
46
+ });
47
+ const persist = (next) => {
48
+ setPreferences(next);
49
+ writeAppearancePreferences(storage, next);
50
+ };
59
51
  createEffect(() => {
60
- applyTheme(resolvedTheme(), props.disableTransitionOnChange ?? true);
52
+ applyResolved(resolvedState(), props.disableTransitionOnChange ?? true);
61
53
  });
62
- const setTheme = (next) => {
63
- setThemeSignal(next);
64
- try {
65
- window.localStorage.setItem(STORAGE_KEY, next);
66
- } catch {}
54
+ const update = (patch) => {
55
+ persist({
56
+ ...preferences(),
57
+ ...patch
58
+ });
59
+ };
60
+ const preview = (draft) => {
61
+ if (!draft) {
62
+ applyResolved(resolvedState(), props.disableTransitionOnChange ?? true);
63
+ return;
64
+ }
65
+ applyResolved(resolveAppearanceState(draft, {
66
+ systemAppearance: prefersDark() ? "dark" : "light",
67
+ osReducedTransparency: osReducedTransparency(),
68
+ nativeTranslucency: nativeTranslucency()
69
+ }), props.disableTransitionOnChange ?? true);
67
70
  };
71
+ const theme = () => preferences().mode;
72
+ const resolvedTheme = () => resolvedState().resolvedMode;
73
+ const setTheme = (next) => update({ mode: next });
68
74
  return createComponent(ThemeContext.Provider, {
69
75
  value: {
76
+ preferences,
77
+ resolvedMode: resolvedTheme,
78
+ variantId: () => resolvedState().variant.id,
79
+ effectiveSurface: () => resolvedState().effectiveSurface,
80
+ reduceTransparencyActive: () => resolvedState().reduceTransparencyActive,
81
+ update,
82
+ preview,
70
83
  theme,
71
84
  resolvedTheme,
72
85
  setTheme
@@ -76,20 +89,38 @@ function ThemeProvider(props) {
76
89
  }
77
90
  });
78
91
  }
92
+ /**
93
+ * Applies the resolved theme to the document and suppresses transitions for the
94
+ * frame that carries the change, so a theme switch cannot animate page-wide.
95
+ */
96
+ function applyResolved(state, disableTransitionOnChange) {
97
+ if (typeof document === "undefined") return;
98
+ let restoreTransitions;
99
+ if (disableTransitionOnChange) {
100
+ const style = document.createElement("style");
101
+ style.appendChild(document.createTextNode("*{transition:none !important}"));
102
+ document.head.appendChild(style);
103
+ style.offsetHeight;
104
+ restoreTransitions = () => style.remove();
105
+ }
106
+ applyAppearanceToDocument(document, state);
107
+ if (restoreTransitions) requestAnimationFrame(() => requestAnimationFrame(() => restoreTransitions?.()));
108
+ }
79
109
  function useTheme() {
80
110
  const context = useContext(ThemeContext);
81
111
  if (!context) throw new Error("useTheme must be used within a ThemeProvider.");
82
112
  return context;
83
113
  }
84
114
  /**
85
- * Pre-paint theme restore for server-rendered documents. Rendered in the
86
- * document head, it applies the stored or system theme before first paint so
87
- * hydration never shows the wrong palette.
115
+ * Pre-paint appearance restore for server-rendered documents. Rendered in the
116
+ * document head, it resolves the stored (or legacy) preference against the OS
117
+ * and applies the palette before first paint so hydration never shows the
118
+ * wrong theme.
88
119
  */
89
120
  function ThemeScript() {
90
121
  return (() => {
91
122
  var _el$ = _tmpl$();
92
- _el$.innerHTML = "(function(){try{var t=localStorage.getItem('theme')||'system';var d=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);var r=document.documentElement;r.classList.toggle('dark',d);r.style.colorScheme=d?'dark':'light'}catch(e){}})();";
123
+ effect(() => _el$.innerHTML = appearanceThemeScript());
93
124
  return _el$;
94
125
  })();
95
126
  }
@@ -0,0 +1,12 @@
1
+ import type { ThemeVariant } from './appearance';
2
+ /** The three-dot preview for a theme variant (background, primary, card). */
3
+ export declare function ThemeSwatch(props: {
4
+ class?: string;
5
+ variant: ThemeVariant;
6
+ }): import("solid-js").JSX.Element;
7
+ /** Single-color swatch (accent presets and custom accents). */
8
+ export declare function ColorSwatch(props: {
9
+ class?: string;
10
+ color: string;
11
+ label: string;
12
+ }): import("solid-js").JSX.Element;
@@ -0,0 +1,66 @@
1
+ import { className, createComponent, effect, insert, setAttribute, setStyleProperty, template } from "solid-js/web";
2
+ import { For } from "solid-js";
3
+ //#region src/components/theme-swatch.tsx
4
+ var _tmpl$ = /*#__PURE__*/ template(`<span aria-hidden=true>`);
5
+ var _tmpl$2 = /*#__PURE__*/ template(`<span class=theme-swatch__dot>`);
6
+ var _tmpl$3 = /*#__PURE__*/ template(`<span role=img><span class=theme-swatch__dot>`);
7
+ /** The three-dot preview for a theme variant (background, primary, card). */
8
+ function ThemeSwatch(props) {
9
+ const swatches = () => [
10
+ {
11
+ role: "Background",
12
+ color: props.variant.colors.background
13
+ },
14
+ {
15
+ role: "Primary",
16
+ color: props.variant.colors.primary
17
+ },
18
+ {
19
+ role: "Card",
20
+ color: props.variant.colors.card
21
+ }
22
+ ];
23
+ return (() => {
24
+ var _el$ = _tmpl$();
25
+ insert(_el$, createComponent(For, {
26
+ get each() {
27
+ return swatches();
28
+ },
29
+ children: (swatch) => (() => {
30
+ var _el$2 = _tmpl$2();
31
+ effect((_p$) => {
32
+ var _v$ = swatch.role, _v$2 = swatch.color;
33
+ _v$ !== _p$.e && setAttribute(_el$2, "title", _p$.e = _v$);
34
+ _v$2 !== _p$.t && setStyleProperty(_el$2, "background-color", _p$.t = _v$2);
35
+ return _p$;
36
+ }, {
37
+ e: void 0,
38
+ t: void 0
39
+ });
40
+ return _el$2;
41
+ })()
42
+ }));
43
+ effect(() => className(_el$, props.class));
44
+ return _el$;
45
+ })();
46
+ }
47
+ /** Single-color swatch (accent presets and custom accents). */
48
+ function ColorSwatch(props) {
49
+ return (() => {
50
+ var _el$3 = _tmpl$3(), _el$4 = _el$3.firstChild;
51
+ effect((_p$) => {
52
+ var _v$3 = props.class, _v$4 = props.label, _v$5 = props.color;
53
+ _v$3 !== _p$.e && className(_el$3, _p$.e = _v$3);
54
+ _v$4 !== _p$.t && setAttribute(_el$3, "aria-label", _p$.t = _v$4);
55
+ _v$5 !== _p$.a && setStyleProperty(_el$4, "background-color", _p$.a = _v$5);
56
+ return _p$;
57
+ }, {
58
+ e: void 0,
59
+ t: void 0,
60
+ a: void 0
61
+ });
62
+ return _el$3;
63
+ })();
64
+ }
65
+ //#endregion
66
+ export { ColorSwatch, ThemeSwatch };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { Toggle } from './components/ui/toggle';
4
4
  export { ToggleGroup, ToggleGroupItem } from './components/ui/toggle-group';
5
5
  export { RadioGroup, RadioGroupItem } from './components/ui/radio-group';
6
6
  export { ThemeToggle } from './components/theme-toggle';
7
+ export { ColorSwatch, ThemeSwatch } from './components/theme-swatch';
7
8
  export { ThemeProvider, ThemeScript, useTheme, type ResolvedTheme, type Theme, } from './components/theme-provider';
8
9
  export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerSwipeHandle, DrawerTitle, DrawerTrigger, } from './components/ui/drawer';
9
10
  export { AccountDrawer, type AccountDrawerProps } from './components/account-drawer';
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { ThemeToggle } from "./components/theme-toggle.js";
6
6
  import { AccountDrawer } from "./components/account-drawer.js";
7
7
  import { OnScreenControls } from "./components/on-screen-controls.js";
8
8
  import { SceneSettings } from "./components/scene-settings.js";
9
+ import { ColorSwatch, ThemeSwatch } from "./components/theme-swatch.js";
9
10
  import { Card, CardContent } from "./components/ui/card.js";
10
11
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./components/ui/dialog.js";
11
12
  import { RadioGroup, RadioGroupItem } from "./components/ui/radio-group.js";
@@ -17,4 +18,4 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./comp
17
18
  import { VersionDialog } from "./components/version-dialog.js";
18
19
  import { WorkspaceBrand } from "./components/workspace-brand.js";
19
20
  import { WorkspaceLogo } from "./components/workspace-logo.js";
20
- export { AccountDrawer, Button, Card, CardContent, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerSwipeHandle, DrawerTitle, DrawerTrigger, OnScreenControls, RadioGroup, RadioGroupItem, SceneSettings, Spinner, Switch, ThemeProvider, ThemeScript, ThemeToggle, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, VersionDialog, WorkspaceBrand, WorkspaceLogo, buttonVariants, cn, useTheme };
21
+ export { AccountDrawer, Button, Card, CardContent, ColorSwatch, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerSwipeHandle, DrawerTitle, DrawerTrigger, OnScreenControls, RadioGroup, RadioGroupItem, SceneSettings, Spinner, Switch, ThemeProvider, ThemeScript, ThemeSwatch, ThemeToggle, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, VersionDialog, WorkspaceBrand, WorkspaceLogo, buttonVariants, cn, useTheme };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Shared UI kit for Adea: Kobalte/corvu-backed Solid primitives, workspace shells, and theme styles.",
5
5
  "keywords": [
6
6
  "components",
@@ -58,6 +58,7 @@
58
58
  },
59
59
  "scripts": {
60
60
  "lint": "oxlint",
61
+ "test": "bun test tests",
61
62
  "build": "vite build && tsc -p tsconfig.json --emitDeclarationOnly",
62
63
  "dev": "vite build --watch",
63
64
  "typecheck": "tsc -p tsconfig.json --noEmit"