@adea-ai/ui 0.123.0 → 0.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/NOTICE +18 -0
  2. package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts +13 -0
  3. package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts.map +1 -0
  4. package/dist/components/layout/split-layout/resize-controller-core-retry.js +63 -0
  5. package/dist/components/layout/split-layout/resize-controller-core-retry.js.map +1 -0
  6. package/dist/components/layout/split-layout/resize-controller-core.d.ts +13 -0
  7. package/dist/components/layout/split-layout/resize-controller-core.d.ts.map +1 -0
  8. package/dist/components/layout/split-layout/resize-controller-core.js +63 -0
  9. package/dist/components/layout/split-layout/resize-controller-core.js.map +1 -0
  10. package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts +3 -0
  11. package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts.map +1 -0
  12. package/dist/components/layout/split-layout/resize-controller-retry-2.js +2 -0
  13. package/dist/components/layout/split-layout/resize-controller-retry.d.ts +2 -0
  14. package/dist/components/layout/split-layout/resize-controller-retry.d.ts.map +1 -0
  15. package/dist/components/layout/split-layout/resize-controller-retry.js +2 -0
  16. package/dist/components/layout/split-layout/resize-controller.d.ts +2 -0
  17. package/dist/components/layout/split-layout/resize-controller.d.ts.map +1 -0
  18. package/dist/components/layout/split-layout/resize-controller.js +2 -0
  19. package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -1
  20. package/dist/components/layout/split-layout/split-layout.js +171 -60
  21. package/dist/components/layout/split-layout/split-layout.js.map +1 -1
  22. package/dist/components/theme/appearance-panel.js +1 -1
  23. package/dist/components/theme/index.d.ts +1 -1
  24. package/dist/components/theme/index.d.ts.map +1 -1
  25. package/dist/components/theme/index.js +5 -2
  26. package/dist/components/theme/theme-context.d.ts +13 -0
  27. package/dist/components/theme/theme-context.d.ts.map +1 -0
  28. package/dist/components/theme/theme-context.js +41 -0
  29. package/dist/components/theme/theme-context.js.map +1 -0
  30. package/dist/components/theme/theme-document.d.ts +76 -0
  31. package/dist/components/theme/theme-document.d.ts.map +1 -0
  32. package/dist/components/theme/theme-document.js +93 -0
  33. package/dist/components/theme/theme-document.js.map +1 -0
  34. package/dist/components/theme/theme-persistence.d.ts +50 -0
  35. package/dist/components/theme/theme-persistence.d.ts.map +1 -0
  36. package/dist/components/theme/theme-persistence.js +86 -0
  37. package/dist/components/theme/theme-persistence.js.map +1 -0
  38. package/dist/components/theme/theme-provider.d.ts +43 -75
  39. package/dist/components/theme/theme-provider.d.ts.map +1 -1
  40. package/dist/components/theme/theme-provider.js +50 -116
  41. package/dist/components/theme/theme-provider.js.map +1 -1
  42. package/dist/components/theme/theme-state.d.ts +142 -0
  43. package/dist/components/theme/theme-state.d.ts.map +1 -0
  44. package/dist/components/theme/theme-state.js +104 -0
  45. package/dist/components/theme/theme-state.js.map +1 -0
  46. package/dist/components/theme/theme-toggle.js +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -3
  50. package/dist/lib/appearance-font-settings.d.ts +1 -1
  51. package/dist/lib/appearance-font-settings.d.ts.map +1 -1
  52. package/dist/lib/appearance-font-settings.js.map +1 -1
  53. package/dist/lib/themes.d.ts +48 -0
  54. package/dist/lib/themes.d.ts.map +1 -1
  55. package/dist/lib/themes.js +150 -36
  56. package/dist/lib/themes.js.map +1 -1
  57. package/dist/r/registry.json +46 -0
  58. package/dist/r/split-layout.json +26 -0
  59. package/dist/r/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
  60. package/dist/r/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
  61. package/dist/r/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
  62. package/dist/r/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
  63. package/dist/r/src/components/layout/split-layout/resize-controller.tsx +1 -0
  64. package/dist/r/src/components/layout/split-layout/split-layout.tsx +197 -49
  65. package/dist/r/src/components/theme/index.ts +3 -0
  66. package/dist/r/src/components/theme/theme-context.ts +43 -0
  67. package/dist/r/src/components/theme/theme-document.ts +168 -0
  68. package/dist/r/src/components/theme/theme-persistence.ts +140 -0
  69. package/dist/r/src/components/theme/theme-provider.tsx +126 -253
  70. package/dist/r/src/components/theme/theme-state.ts +225 -0
  71. package/dist/r/src/lib/appearance-font-settings.ts +1 -1
  72. package/dist/r/src/lib/themes.ts +181 -40
  73. package/dist/r/theming.json +20 -0
  74. package/package.json +1 -1
  75. package/registry.json +46 -0
  76. package/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
  77. package/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
  78. package/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
  79. package/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
  80. package/src/components/layout/split-layout/resize-controller.tsx +1 -0
  81. package/src/components/layout/split-layout/split-layout.tsx +197 -49
  82. package/src/components/theme/index.ts +3 -0
  83. package/src/components/theme/theme-context.ts +43 -0
  84. package/src/components/theme/theme-document.ts +168 -0
  85. package/src/components/theme/theme-persistence.ts +140 -0
  86. package/src/components/theme/theme-provider.tsx +126 -253
  87. package/src/components/theme/theme-state.ts +225 -0
  88. package/src/index.ts +3 -0
  89. package/src/lib/appearance-font-settings.ts +1 -1
  90. package/src/lib/themes.ts +181 -40
@@ -0,0 +1,168 @@
1
+ import {
2
+ applyAppearanceFontSettings,
3
+ normalizeAppearanceEditorFontSettings,
4
+ } from '#lib/appearance-font-settings'
5
+ import {
6
+ accentVariables,
7
+ themeCssVariables,
8
+ type ThemeAppearance,
9
+ type ThemeVariant,
10
+ } from '#lib/themes'
11
+ import { resolvedDensity, type ThemeSelection } from './theme-state'
12
+
13
+ /**
14
+ * The document half of the theme provider: the single writer of `<html>`.
15
+ *
16
+ * Written against the few members of an element it uses, so the rules can be tested
17
+ * on a stand-in and the provider's effect stays a one-line call.
18
+ */
19
+ export type ThemeRoot = Pick<
20
+ HTMLElement,
21
+ 'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'
22
+ >
23
+
24
+ export type ThemeDocumentState = {
25
+ /** What the document shows: the committed selection, or a preview laid over it. */
26
+ selection: ThemeSelection
27
+ appearance: ThemeAppearance
28
+ variant: ThemeVariant
29
+ }
30
+
31
+ export type ThemeDocumentOptions = {
32
+ /**
33
+ * Rewrites the custom properties just before they are written: the variant's
34
+ * roles with the accent laid over them. It sees every property of the pass and
35
+ * must return the full map; a name it drops is not written.
36
+ */
37
+ transformTokens?: (tokens: Record<string, string>) => Record<string, string>
38
+ }
39
+
40
+ /**
41
+ * The custom properties a state paints: the variant's roles, with the accent
42
+ * selection laid over them, then `transformTokens`.
43
+ *
44
+ * The accent is applied *here* rather than left to the `[data-accent]` blocks in
45
+ * `theme.css`, because the properties are written inline and an inline declaration
46
+ * outranks every stylesheet selector however specific it is. The blocks still exist
47
+ * and are still correct — they are what styles the window before this runs — but
48
+ * they cannot win from here, so leaving the accent to them meant the attribute was
49
+ * set, the rules were present, and nothing changed on screen.
50
+ */
51
+ export function themeDocumentTokens(
52
+ state: ThemeDocumentState,
53
+ options: ThemeDocumentOptions = {}
54
+ ): { tokens: Record<string, string>; accented: boolean } {
55
+ const properties = { ...themeCssVariables(state.variant) }
56
+ const accent = accentVariables(state.selection.accent, state.appearance, state.selection)
57
+ if (accent) Object.assign(properties, accent)
58
+ return {
59
+ tokens: options.transformTokens ? options.transformTokens(properties) : properties,
60
+ accented: accent !== undefined,
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Write a state onto a root and report which custom properties it owns.
66
+ *
67
+ * Pass the previous call's return as `previous` and any palette property that pass
68
+ * wrote and this one does not is removed. For the catalogue the two sets are the
69
+ * same, so nothing is ever removed; it matters for a host variant that carries
70
+ * `tokens` the catalogue does not, which would otherwise stay inline, beating the
71
+ * stylesheet, after the user switched away from it.
72
+ */
73
+ export function applyThemeToRoot(
74
+ root: ThemeRoot,
75
+ state: ThemeDocumentState,
76
+ options: ThemeDocumentOptions = {},
77
+ previous?: ReadonlySet<string>
78
+ ): ReadonlySet<string> {
79
+ const { tokens, accented } = themeDocumentTokens(state, options)
80
+
81
+ for (const [name, value] of Object.entries(tokens)) {
82
+ root.style.setProperty(name, value)
83
+ }
84
+ if (previous) {
85
+ for (const name of previous) {
86
+ if (!(name in tokens)) root.style.removeProperty(name)
87
+ }
88
+ }
89
+
90
+ root.classList.toggle('dark', state.appearance === 'dark')
91
+ root.dataset['theme'] = state.variant.id
92
+ root.dataset['appearance'] = state.appearance
93
+ root.style.colorScheme = state.appearance
94
+
95
+ if (!accented) root.removeAttribute('data-accent')
96
+ else root.dataset['accent'] = state.selection.accent
97
+
98
+ applyAppearanceFontSettings(
99
+ root,
100
+ normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings
101
+ )
102
+
103
+ // Density is a pure token shift, so unlike the colour axes there is nothing to
104
+ // compute: the attribute selects a block in `theme.css`. It is applied here for
105
+ // the same reason as the others — this is the one place that knows what the
106
+ // current selection is, and it is the only writer of these attributes.
107
+ if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')
108
+ else root.dataset['density'] = resolvedDensity(state.selection)
109
+
110
+ return new Set(Object.keys(tokens))
111
+ }
112
+
113
+ /** What {@link applyWithoutTransitions} needs of a document. */
114
+ export type TransitionDocument = {
115
+ head: { appendChild(node: unknown): unknown }
116
+ createElement(tag: 'style'): {
117
+ textContent: string | null
118
+ offsetHeight?: number
119
+ remove(): void
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Run `apply` with every CSS transition switched off for the frame that carries
125
+ * it, so a theme switch cannot animate page-wide.
126
+ *
127
+ * Without this, flipping the custom properties on `<html>` starts a transition on
128
+ * every element that declares one, and each fades from the old palette to the new
129
+ * over its own duration: a theme change that looks like a rendering fault. The
130
+ * rule is removed two frames later, after the browser has committed the new styles
131
+ * with transitions off, and `restore` runs it early (the provider calls it when it
132
+ * is disposed mid-switch).
133
+ *
134
+ * `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls
135
+ * back to running at once where there is none, which leaves transitions on rather
136
+ * than off.
137
+ */
138
+ export function applyWithoutTransitions(
139
+ doc: TransitionDocument,
140
+ apply: () => void,
141
+ schedule: (callback: () => void) => void = defaultSchedule
142
+ ): () => void {
143
+ const style = doc.createElement('style')
144
+ style.textContent = '*{transition:none !important}'
145
+ doc.head.appendChild(style)
146
+ // Reading a layout property forces the browser to commit the rule before the
147
+ // properties change; without it the two can be batched into one style recalc.
148
+ void style.offsetHeight
149
+ let removed = false
150
+ const restore = () => {
151
+ if (removed) return
152
+ removed = true
153
+ style.remove()
154
+ }
155
+ try {
156
+ apply()
157
+ } catch (error) {
158
+ restore()
159
+ throw error
160
+ }
161
+ schedule(() => schedule(restore))
162
+ return restore
163
+ }
164
+
165
+ function defaultSchedule(callback: () => void): void {
166
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)
167
+ else callback()
168
+ }
@@ -0,0 +1,140 @@
1
+ import { accentPresets } from '#lib/tokens'
2
+ import { normalizeAppearanceEditorFontSettings } from '#lib/appearance-font-settings'
3
+ import { defaultDarkThemeId, defaultLightThemeId, isThemeAccentId, themeById } from '#lib/themes'
4
+ import { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state'
5
+
6
+ /**
7
+ * Where a selection is kept between sessions.
8
+ *
9
+ * The provider never touches storage itself; it asks a `ThemePersistence`. The
10
+ * default is {@link localStoragePersistence}, which is what the provider always did.
11
+ * A host that stores the preference in a different shape, under a versioned
12
+ * envelope, or somewhere that is not `localStorage` supplies its own and the
13
+ * resolution and application rules stay the shared ones.
14
+ *
15
+ * The contract is deliberately narrow, so that an implementation cannot make the
16
+ * provider less safe than it was:
17
+ *
18
+ * - `read` is synchronous and runs once, when the provider is created. It may
19
+ * repair or quarantine what it finds, and it must not throw: the provider
20
+ * treats a throw as "nothing stored" and starts from the defaults.
21
+ * - `write` runs only from `setSelection`. Never on mount, never from `preview`,
22
+ * never as a side effect of reading, so the first write to a store is always a
23
+ * user's decision. A throw is swallowed, because a full or blocked store must
24
+ * not break the switch.
25
+ */
26
+ export type ThemePersistence = {
27
+ read(): Partial<ThemeSelection> | undefined
28
+ write(next: ThemeSelection): void
29
+ }
30
+
31
+ /**
32
+ * The preference store, or nothing.
33
+ *
34
+ * Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a
35
+ * privacy mode, a denied site-data permission — merely *reading* the `localStorage`
36
+ * global throws ("Access is denied", `SecurityError`), so even the `typeof` guard
37
+ * has to sit inside the `try` rather than in front of it. Without a store the
38
+ * preference lives only in the provider's signal for the session.
39
+ */
40
+ function preferenceStore(): Storage | undefined {
41
+ return typeof localStorage === 'undefined' ? undefined : localStorage
42
+ }
43
+
44
+ /** The stored preference, validated against the catalogue. */
45
+ function readSelection(storageKey: string): ThemeSelection {
46
+ try {
47
+ const raw = preferenceStore()?.getItem(storageKey)
48
+ if (!raw) return defaultThemeSelection
49
+ const parsed = JSON.parse(raw) as Partial<ThemeSelection>
50
+ const fonts = normalizeAppearanceEditorFontSettings(parsed.fonts, parsed.font).settings
51
+ return {
52
+ appearance:
53
+ parsed.appearance === 'light' ||
54
+ parsed.appearance === 'dark' ||
55
+ parsed.appearance === 'system'
56
+ ? parsed.appearance
57
+ : defaultThemeSelection.appearance,
58
+ // An unknown id would leave the document with no theme at all, so a stale
59
+ // preference from an older catalogue falls back rather than applying nothing.
60
+ lightThemeId:
61
+ themeById(parsed.lightThemeId ?? '')?.appearance === 'light'
62
+ ? (parsed.lightThemeId as string)
63
+ : defaultLightThemeId,
64
+ darkThemeId:
65
+ themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'
66
+ ? (parsed.darkThemeId as string)
67
+ : defaultDarkThemeId,
68
+ // A theme accent is kept whenever it names a slot, even one the stored pair
69
+ // cannot offer: the id is role-shaped so it survives a theme switch, and an
70
+ // unoffered slot already resolves to the theme's own primary when applied.
71
+ accent:
72
+ typeof parsed.accent === 'string' &&
73
+ (accentPresets.some((preset) => preset.id === parsed.accent) ||
74
+ isThemeAccentId(parsed.accent))
75
+ ? parsed.accent
76
+ : defaultThemeSelection.accent,
77
+ font: fonts.ui.family,
78
+ fonts,
79
+ // Every axis has to be read back here, or it is lost twice: the provider's
80
+ // document effect removes the `data-density` that `themeScript` set for the first paint, and
81
+ // the next `setSelection` rewrites the store without it.
82
+ density: resolvedDensity(parsed as ThemeSelection),
83
+ }
84
+ } catch {
85
+ return defaultThemeSelection
86
+ }
87
+ }
88
+
89
+ /**
90
+ * The default persistence: one JSON document in `localStorage` under `key`.
91
+ *
92
+ * `read` is the provider's original `readSelection`, moved here unchanged — every
93
+ * stored id is validated against the catalogue, so a stale preference falls back to
94
+ * a default rather than reaching the document. `key` may be a function so a
95
+ * provider whose `storageKey` prop changes writes to the key it is now given.
96
+ *
97
+ * Blocked, full or absent storage is survivable in both directions: a read answers
98
+ * with the defaults and a write is dropped, so the preference simply lasts the
99
+ * session.
100
+ */
101
+ export function localStoragePersistence(key: string | (() => string)): ThemePersistence {
102
+ const keyOf = () => (typeof key === 'function' ? key() : key)
103
+ return {
104
+ read: () => readSelection(keyOf()),
105
+ write(next) {
106
+ try {
107
+ preferenceStore()?.setItem(keyOf(), JSON.stringify(next))
108
+ } catch {
109
+ // A full, blocked or unavailable store must not break the switch: the
110
+ // preference simply does not survive the reload.
111
+ }
112
+ },
113
+ }
114
+ }
115
+
116
+ /**
117
+ * What a persistence knows, or nothing.
118
+ *
119
+ * The seam's `read` must not throw, and the provider does not rely on that: a host
120
+ * persistence is code the design system has not seen, and a theme provider that
121
+ * fails to mount because a store misbehaved has turned a lost preference into a
122
+ * blank application.
123
+ */
124
+ export function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined {
125
+ try {
126
+ return persistence.read()
127
+ } catch {
128
+ return undefined
129
+ }
130
+ }
131
+
132
+ /** Hand a selection to the store; a store that throws loses the preference, not the switch. */
133
+ export function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void {
134
+ try {
135
+ persistence.write(next)
136
+ } catch {
137
+ // Same contract as a full or blocked `localStorage`: the choice holds for the
138
+ // session and does not survive the reload.
139
+ }
140
+ }