@dxos/ui-theme 0.8.4-staging.ac66bdf99f → 0.9.1-main.c7dcc2e112

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 (169) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/index.mjs +89 -867
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/node-esm/index.mjs +89 -867
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/plugin/node-cjs/main.css +279 -120
  10. package/dist/plugin/node-cjs/main.css.map +3 -3
  11. package/dist/plugin/node-cjs/meta.json +1 -1
  12. package/dist/plugin/node-cjs/plugins/ThemePlugin.cjs +82 -10
  13. package/dist/plugin/node-cjs/plugins/ThemePlugin.cjs.map +3 -3
  14. package/dist/plugin/node-esm/main.css +279 -120
  15. package/dist/plugin/node-esm/main.css.map +3 -3
  16. package/dist/plugin/node-esm/meta.json +1 -1
  17. package/dist/plugin/node-esm/plugins/ThemePlugin.mjs +84 -12
  18. package/dist/plugin/node-esm/plugins/ThemePlugin.mjs.map +3 -3
  19. package/dist/types/src/Theme.stories.d.ts.map +1 -1
  20. package/dist/types/src/defs.d.ts +1 -1
  21. package/dist/types/src/defs.d.ts.map +1 -1
  22. package/dist/types/src/fragments/density.d.ts +2 -2
  23. package/dist/types/src/fragments/density.d.ts.map +1 -1
  24. package/dist/types/src/fragments/hover.d.ts +0 -1
  25. package/dist/types/src/fragments/hover.d.ts.map +1 -1
  26. package/dist/types/src/index.d.ts +0 -1
  27. package/dist/types/src/index.d.ts.map +1 -1
  28. package/dist/types/src/plugins/ThemePlugin.d.ts +3 -2
  29. package/dist/types/src/plugins/ThemePlugin.d.ts.map +1 -1
  30. package/dist/types/src/util/hash-styles.d.ts +12 -2
  31. package/dist/types/src/util/hash-styles.d.ts.map +1 -1
  32. package/dist/types/src/util/mx.d.ts +0 -36
  33. package/dist/types/src/util/mx.d.ts.map +1 -1
  34. package/dist/types/src/util/size.d.ts.map +1 -1
  35. package/dist/types/src/util/valence.d.ts +8 -2
  36. package/dist/types/src/util/valence.d.ts.map +1 -1
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +14 -21
  39. package/src/Theme.stories.tsx +94 -27
  40. package/src/css/DESIGN_SYSTEM.md +187 -0
  41. package/src/css/base/base.css +2 -2
  42. package/src/css/components/button.css +57 -12
  43. package/src/css/components/card.css +14 -0
  44. package/src/css/components/checkbox.css +3 -3
  45. package/src/css/components/focus.css +12 -12
  46. package/src/css/components/link.css +4 -1
  47. package/src/css/components/panel.css +45 -45
  48. package/src/css/components/state.css +76 -0
  49. package/src/css/components/state.md +101 -0
  50. package/src/css/components/surface.css +86 -11
  51. package/src/css/components/tag.css +25 -24
  52. package/src/css/integrations/codemirror.css +4 -3
  53. package/src/css/integrations/tldraw.css +1 -1
  54. package/src/css/layout/main.css +6 -0
  55. package/src/css/layout/size.css +17 -4
  56. package/src/css/theme/animation.css +31 -0
  57. package/src/css/theme/palette.css +34 -0
  58. package/src/css/theme/semantic.css +190 -68
  59. package/src/css/theme/spacing.css +30 -13
  60. package/src/css/theme/styles.css +172 -119
  61. package/src/css/utilities.css +42 -0
  62. package/src/defs.ts +3 -1
  63. package/src/fragments/AUDIT.md +0 -2
  64. package/src/fragments/density.ts +34 -7
  65. package/src/fragments/hover.ts +0 -2
  66. package/src/index.ts +1 -1
  67. package/src/main.css +68 -9
  68. package/src/plugins/ThemePlugin.ts +102 -14
  69. package/src/plugins/main.css +10 -7
  70. package/src/util/hash-styles.ts +54 -42
  71. package/src/util/mx.ts +1 -126
  72. package/src/util/valence.ts +15 -5
  73. package/dist/types/src/theme/components/avatar.d.ts +0 -21
  74. package/dist/types/src/theme/components/avatar.d.ts.map +0 -1
  75. package/dist/types/src/theme/components/breadcrumb.d.ts +0 -9
  76. package/dist/types/src/theme/components/breadcrumb.d.ts.map +0 -1
  77. package/dist/types/src/theme/components/button.d.ts +0 -15
  78. package/dist/types/src/theme/components/button.d.ts.map +0 -1
  79. package/dist/types/src/theme/components/card.d.ts +0 -12
  80. package/dist/types/src/theme/components/card.d.ts.map +0 -1
  81. package/dist/types/src/theme/components/dialog.d.ts +0 -17
  82. package/dist/types/src/theme/components/dialog.d.ts.map +0 -1
  83. package/dist/types/src/theme/components/focus.d.ts +0 -6
  84. package/dist/types/src/theme/components/focus.d.ts.map +0 -1
  85. package/dist/types/src/theme/components/icon-button.d.ts +0 -8
  86. package/dist/types/src/theme/components/icon-button.d.ts.map +0 -1
  87. package/dist/types/src/theme/components/icon.d.ts +0 -10
  88. package/dist/types/src/theme/components/icon.d.ts.map +0 -1
  89. package/dist/types/src/theme/components/index.d.ts +0 -27
  90. package/dist/types/src/theme/components/index.d.ts.map +0 -1
  91. package/dist/types/src/theme/components/input.d.ts +0 -115
  92. package/dist/types/src/theme/components/input.d.ts.map +0 -1
  93. package/dist/types/src/theme/components/link.d.ts +0 -7
  94. package/dist/types/src/theme/components/link.d.ts.map +0 -1
  95. package/dist/types/src/theme/components/list.d.ts +0 -14
  96. package/dist/types/src/theme/components/list.d.ts.map +0 -1
  97. package/dist/types/src/theme/components/main.d.ts +0 -28
  98. package/dist/types/src/theme/components/main.d.ts.map +0 -1
  99. package/dist/types/src/theme/components/menu.d.ts +0 -13
  100. package/dist/types/src/theme/components/menu.d.ts.map +0 -1
  101. package/dist/types/src/theme/components/message.d.ts +0 -12
  102. package/dist/types/src/theme/components/message.d.ts.map +0 -1
  103. package/dist/types/src/theme/components/popover.d.ts +0 -11
  104. package/dist/types/src/theme/components/popover.d.ts.map +0 -1
  105. package/dist/types/src/theme/components/scroll-area.d.ts +0 -32
  106. package/dist/types/src/theme/components/scroll-area.d.ts.map +0 -1
  107. package/dist/types/src/theme/components/select.d.ts +0 -13
  108. package/dist/types/src/theme/components/select.d.ts.map +0 -1
  109. package/dist/types/src/theme/components/separator.d.ts +0 -8
  110. package/dist/types/src/theme/components/separator.d.ts.map +0 -1
  111. package/dist/types/src/theme/components/skeleton.d.ts +0 -7
  112. package/dist/types/src/theme/components/skeleton.d.ts.map +0 -1
  113. package/dist/types/src/theme/components/splitter.d.ts +0 -4
  114. package/dist/types/src/theme/components/splitter.d.ts.map +0 -1
  115. package/dist/types/src/theme/components/status.d.ts +0 -9
  116. package/dist/types/src/theme/components/status.d.ts.map +0 -1
  117. package/dist/types/src/theme/components/tag.d.ts +0 -7
  118. package/dist/types/src/theme/components/tag.d.ts.map +0 -1
  119. package/dist/types/src/theme/components/toast.d.ts +0 -12
  120. package/dist/types/src/theme/components/toast.d.ts.map +0 -1
  121. package/dist/types/src/theme/components/toolbar.d.ts +0 -11
  122. package/dist/types/src/theme/components/toolbar.d.ts.map +0 -1
  123. package/dist/types/src/theme/components/tooltip.d.ts +0 -8
  124. package/dist/types/src/theme/components/tooltip.d.ts.map +0 -1
  125. package/dist/types/src/theme/components/treegrid.d.ts +0 -10
  126. package/dist/types/src/theme/components/treegrid.d.ts.map +0 -1
  127. package/dist/types/src/theme/index.d.ts +0 -4
  128. package/dist/types/src/theme/index.d.ts.map +0 -1
  129. package/dist/types/src/theme/primitives/column.d.ts +0 -29
  130. package/dist/types/src/theme/primitives/column.d.ts.map +0 -1
  131. package/dist/types/src/theme/primitives/index.d.ts +0 -3
  132. package/dist/types/src/theme/primitives/index.d.ts.map +0 -1
  133. package/dist/types/src/theme/primitives/panel.d.ts +0 -13
  134. package/dist/types/src/theme/primitives/panel.d.ts.map +0 -1
  135. package/dist/types/src/theme/theme.d.ts +0 -5
  136. package/dist/types/src/theme/theme.d.ts.map +0 -1
  137. package/src/css/components/selected.css +0 -30
  138. package/src/theme/components/avatar.ts +0 -95
  139. package/src/theme/components/breadcrumb.ts +0 -29
  140. package/src/theme/components/button.ts +0 -48
  141. package/src/theme/components/card.ts +0 -102
  142. package/src/theme/components/dialog.ts +0 -61
  143. package/src/theme/components/focus.ts +0 -33
  144. package/src/theme/components/icon-button.ts +0 -18
  145. package/src/theme/components/icon.ts +0 -28
  146. package/src/theme/components/index.ts +0 -30
  147. package/src/theme/components/input.ts +0 -171
  148. package/src/theme/components/link.ts +0 -25
  149. package/src/theme/components/list.ts +0 -46
  150. package/src/theme/components/main.ts +0 -34
  151. package/src/theme/components/menu.ts +0 -50
  152. package/src/theme/components/message.ts +0 -40
  153. package/src/theme/components/popover.ts +0 -41
  154. package/src/theme/components/scroll-area.ts +0 -115
  155. package/src/theme/components/select.ts +0 -52
  156. package/src/theme/components/separator.ts +0 -24
  157. package/src/theme/components/skeleton.ts +0 -23
  158. package/src/theme/components/splitter.ts +0 -20
  159. package/src/theme/components/status.ts +0 -32
  160. package/src/theme/components/tag.ts +0 -23
  161. package/src/theme/components/toast.ts +0 -53
  162. package/src/theme/components/toolbar.ts +0 -35
  163. package/src/theme/components/tooltip.ts +0 -27
  164. package/src/theme/components/treegrid.ts +0 -37
  165. package/src/theme/index.ts +0 -7
  166. package/src/theme/primitives/column.ts +0 -71
  167. package/src/theme/primitives/index.ts +0 -6
  168. package/src/theme/primitives/panel.ts +0 -43
  169. package/src/theme/theme.ts +0 -87
package/src/main.css CHANGED
@@ -11,7 +11,17 @@
11
11
  */
12
12
  @layer properties, theme, dx-tokens, user-tokens, base, tw-base, dx-base, components, tw-components, dx-components, utilities;
13
13
 
14
- @import 'tailwindcss';
14
+ /**
15
+ * `source(none)` disables Tailwind 4's automatic content discovery (which
16
+ * scans the project root, including `publicDir`). With auto-detection on,
17
+ * every write to `public/icons.svg` from `vite-plugin-icons` triggers a
18
+ * Tailwind CSS regeneration, which invalidates `main.css` through its
19
+ * import graph and produces a 3–5-ping HMR storm per icon-sprite write
20
+ * during cold-start (icons land in tight bursts as plugin chunks stream
21
+ * in). Explicit `@source` directives below replace the auto-scan with a
22
+ * stable, source-only set.
23
+ */
24
+ @import 'tailwindcss' source(none);
15
25
 
16
26
  /* @theme extensions */
17
27
  @import './css/theme/animation.css';
@@ -28,6 +38,7 @@
28
38
 
29
39
  /* @layer dx-components (components) */
30
40
  @import './css/components/button.css';
41
+ @import './css/components/card.css';
31
42
  @import './css/components/checkbox.css';
32
43
  @import './css/components/dialog.css';
33
44
  @import './css/components/focus.css';
@@ -35,7 +46,7 @@
35
46
  @import './css/components/panel.css';
36
47
  @import './css/components/link.css';
37
48
  @import './css/components/scrollbar.css';
38
- @import './css/components/selected.css';
49
+ @import './css/components/state.css';
39
50
  @import './css/components/surface.css';
40
51
  @import './css/components/tag.css';
41
52
  @import './css/components/text.css';
@@ -54,15 +65,63 @@
54
65
  @import './css/integrations/tldraw.css';
55
66
 
56
67
  /**
57
- * Force-emit CSS variables for the full color palette for all:
68
+ * With `source(none)` on the Tailwind import above, these are the *only*
69
+ * paths Tailwind scans for utility classes. Cover every workspace source
70
+ * directory so lazy-loaded plugin / UI / SDK chunks don't surface new
71
+ * utilities mid-session, and root-level HTML entrypoints (index.html,
72
+ * internal.html, …) are visible too.
73
+ *
74
+ * Paths are relative to this file (`packages/ui/ui-theme/src/main.css`):
75
+ * ../../../ → `packages/`
76
+ * ../../../../tools/ → `tools/`
77
+ *
78
+ * Tailwind ignores `node_modules` and common build/output dirs by default,
79
+ * so a broad workspace glob is safe *for scanning*.
80
+ *
81
+ * NOTE: Tailwind's scanner respects `.gitignore` (so `dist/` is never
82
+ * scanned), but it hands Vite a coarse `dir-dependency` glob and Vite
83
+ * re-expands that itself, ignoring only `node_modules`. That re-expansion
84
+ * sweeps `dist/**` `.d.ts` files into the watch set and causes a `main.css`
85
+ * HMR storm on every package rebuild. Build outputs are excluded from the
86
+ * dev watcher in `plugins/ThemePlugin.ts` (`server.watch.ignored`) rather
87
+ * than here, because `@source not` does NOT propagate to the glob Vite
88
+ * re-expands.
89
+ */
90
+ @source '../../../**/*.{ts,tsx,html}';
91
+ @source '../../../../tools/**/*.{ts,tsx,html}';
92
+
93
+ /**
94
+ * Exclude generated/static data files that have no Tailwind classes but are
95
+ * large enough to dominate scan time. The oxide scanner tokenizes every byte;
96
+ * a single 5MB geo-data file can pin Vite at 100% CPU for tens of seconds on
97
+ * a cold start.
98
+ */
99
+ @source not '../../../ui/react-ui-geo/data/**';
100
+ @source not '../../../ui/solid-ui-geo/data/**';
101
+ @source not '../../../apps/composer-app/src/plugins/welcome/components/Welcome/hero-image.ts';
102
+
103
+ /**
104
+ * Force-emit CSS variables for the full color palette for all:
58
105
  * --color-{hue}-{shade}
59
106
  * --color-{hue}-{style}
60
107
  */
61
- @source inline("bg-{neutral,red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose}-{50,100,200,300,400,500,600,700,800,900,950}");
62
- @source inline("bg-{neutral,red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose}-{fill,surface,surface-text,text,border}");
63
- @source inline("border-{neutral,red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose}-border");
64
- @source inline("bg-{info,success,warning,error}-{fill,surface,surface-text,text,border}");
65
- @source inline("border-{info,success,warning,error}-border");
108
+ @source inline('bg-{neutral,red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose}-{50,100,200,300,400,500,600,700,800,900,950}');
109
+ @source inline('bg-{neutral,red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose}-{bg,bg-hover,fg,surface,text,border}');
110
+ @source inline('bg-{primary,secondary}-{50,100,200,300,400,500,600,700,800,900,950}');
111
+ @source inline('border-{neutral,red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose}-border');
112
+ @source inline('bg-{info,success,warning,error}-{bg,surface,fg,text,border}');
113
+ @source inline('border-{info,success,warning,error}-border');
114
+
115
+ /**
116
+ * Force-emit tokens consumed by web components / .pcss files outside the
117
+ * Tailwind source-scan graph (e.g. lit-grid's dx-grid.pcss uses var(--color-grid-line)).
118
+ */
119
+ @source inline('bg-grid-{surface,fg,line,highlight,comment,comment-active,selection-surface,selection-fg}');
120
+ /*
121
+ * Sidebar surfaces alias each other (e.g. header-surface -> l0-surface); force-emit so the vars
122
+ * exist in apps that use the alias but not the underlying utility (e.g. devtools).
123
+ */
124
+ @source inline('bg-{l0,r0,l1,r1}-surface');
66
125
 
67
126
  /**
68
127
  * Plugins must come after all imports.
@@ -84,4 +143,4 @@
84
143
  /** Supports mouse/trackpad (Web) */
85
144
  @custom-variant hover-hover (@media (hover: hover));
86
145
  /** Active navigation */
87
- @custom-variant is-current (&[aria-current]:not([aria-current="false"]));
146
+ @custom-variant is-current (&[aria-current]:not([aria-current='false']));
@@ -4,10 +4,11 @@
4
4
 
5
5
  /* eslint-disable no-console */
6
6
 
7
- import tailwindcss from '@tailwindcss/postcss';
7
+ import tailwindcssPostcss from '@tailwindcss/postcss';
8
+ import tailwindcssVite from '@tailwindcss/vite';
8
9
  import autoprefixer from 'autoprefixer';
9
10
  import { existsSync, readFileSync } from 'node:fs';
10
- import { dirname, resolve } from 'node:path';
11
+ import { resolve } from 'node:path';
11
12
  import postcssImport from 'postcss-import';
12
13
  import postcssNesting from 'postcss-nesting';
13
14
  import { type HtmlTagDescriptor, type Plugin, type UserConfig } from 'vite';
@@ -40,8 +41,9 @@ export type ThemePluginOptions = {
40
41
 
41
42
  /**
42
43
  * Vite plugin to configure theme.
44
+ * Returns the official Tailwind Vite plugin (persistent incremental scanner) alongside the theme plugin.
43
45
  */
44
- export const ThemePlugin = (options: ThemePluginOptions): Plugin => {
46
+ export const ThemePlugin = (options: ThemePluginOptions): Plugin[] => {
45
47
  // Prefer source CSS if available (monorepo dev), fall back to dist for installed package.
46
48
  const srcThemePath = resolve(import.meta.dirname, ROOT, 'src/main.css');
47
49
  const distThemePath = resolve(import.meta.dirname, '../main.css');
@@ -53,23 +55,65 @@ export const ThemePlugin = (options: ThemePluginOptions): Plugin => {
53
55
  const darkModeScriptPath = resolve(pluginsDir, 'dark-mode.ts');
54
56
  const mainCssPath = resolve(pluginsDir, 'main.css');
55
57
 
56
- const config: ThemePluginOptions = {
57
- srcCssPath: isMonorepo ? srcThemePath : distThemePath,
58
- virtualFileId: '@dxos-theme',
59
- ...options,
58
+ const config = {
59
+ srcCssPath: options.srcCssPath ?? (isMonorepo ? srcThemePath : distThemePath),
60
+ virtualFileId: options.virtualFileId ?? '@dxos-theme',
61
+ verbose: options.verbose,
60
62
  };
61
63
 
62
- // Derive project root from the source location so Tailwind scans all packages.
63
- const base = isMonorepo ? resolve(dirname(srcThemePath), ROOT) : undefined;
64
-
65
64
  if (process.env.DEBUG || options.verbose) {
66
65
  console.log('ThemePlugin:\n', JSON.stringify(config, null, 2));
67
66
  }
68
67
 
69
- return {
68
+ // Trailing-edge debounce handle for theme CSS reloads (see `handleHotUpdate`).
69
+ let themeReloadTimer: ReturnType<typeof setTimeout> | undefined;
70
+
71
+ const themePlugin: Plugin = {
70
72
  name: 'vite-plugin-dxos-ui-theme',
71
73
  config: (): UserConfig => {
72
74
  return {
75
+ server: {
76
+ watch: {
77
+ // Stop build outputs from driving HMR — they are the root of the
78
+ // `main.css` HMR storm.
79
+ //
80
+ // Tailwind's `@source` scanning (see `src/main.css`) registers its
81
+ // scanned source files as Vite watch dependencies of the compiled
82
+ // theme CSS. Tailwind's own scanner respects `.gitignore` (and the
83
+ // `@source not` directives), so it never *scans* `dist/`. BUT the
84
+ // scanner hands Vite a coarse `dir-dependency` glob — e.g.
85
+ // `{**/*.html,**/*.ts,**/*.tsx}` — and Vite re-expands that glob
86
+ // itself, ignoring only `node_modules` (not `.gitignore`, not the
87
+ // `@source not` negations). The re-expansion therefore sweeps in
88
+ // every `packages/*/dist/**/*.d.ts` (`.d.ts` matches `**/*.ts`),
89
+ // making each emitted declaration file a watch-dependency of
90
+ // `main.css`. A single package rebuild emits dozens of `.d.ts` in a
91
+ // tight burst, and each write re-invalidates the theme — 40+ HMR
92
+ // pings for `main.css` in one second, repeating on every rebuild.
93
+ //
94
+ // Ignoring build outputs in the watcher is also semantically
95
+ // correct: in dev the workspace resolves `@dxos/*` via the `source`
96
+ // export condition (see `vite-plugin-import-source`), so `dist/`
97
+ // is never consumed at runtime and its churn should never trigger
98
+ // HMR. Vite concatenates these patterns with its built-in ignores
99
+ // (`**/node_modules/**`, `**/.git/**`, …), so this is purely
100
+ // additive.
101
+ //
102
+ // `<root>/.claude/**` covers agent worktrees checked out under the
103
+ // repo root (`.claude/worktrees/<name>/packages/**`): they are full
104
+ // source copies, so the glob re-expansion above sweeps them in and
105
+ // every agent-side edit burst or checkout invalidates the theme in
106
+ // the user's dev server. The pattern is anchored at the resolved
107
+ // repo root (not `**/.claude/**`) because chokidar matches against
108
+ // absolute paths — a bare pattern would match *everything* when the
109
+ // dev server itself runs from inside a worktree whose path contains
110
+ // a `.claude` segment. `*.log` covers runtime log sinks (e.g.
111
+ // vite-plugin-log's `app.log` in the app root), which are appended
112
+ // continuously at runtime and must never feed back into the
113
+ // watcher.
114
+ ignored: ['**/dist/**', '**/out/**', '**/*.log', `${resolve(import.meta.dirname, ROOT, '.claude')}/**`],
115
+ },
116
+ },
73
117
  css: {
74
118
  postcss: {
75
119
  plugins: [
@@ -77,9 +121,11 @@ export const ThemePlugin = (options: ThemePluginOptions): Plugin => {
77
121
  postcssImport(),
78
122
  // Processes CSS nesting syntax.
79
123
  postcssNesting(),
80
- // Processes Tailwind directives and generates utilities from scanned content.
81
- // base points to project root so all packages are scanned (not just ui-theme).
82
- tailwindcss(base !== undefined ? { base } : {}),
124
+ // Resolves @reference/@apply in `.pcss` files (e.g. lit-grid, lit-ui), which the
125
+ // @tailwindcss/vite plugin skips its transform filter only matches `.css`.
126
+ // Theme `.css` files are compiled by @tailwindcss/vite first (enforce: 'pre'),
127
+ // so this plugin's quick-bail check passes them through untouched.
128
+ tailwindcssPostcss(),
83
129
  // Adds vendor prefixes.
84
130
  autoprefixer,
85
131
  ],
@@ -92,6 +138,42 @@ export const ThemePlugin = (options: ThemePluginOptions): Plugin => {
92
138
  return config.srcCssPath;
93
139
  }
94
140
  },
141
+ hotUpdate({ type, file, modules }) {
142
+ // Direct edits to CSS (the theme source or its imports) keep Vite's
143
+ // default immediate update for instant feedback while authoring styles.
144
+ if (this.environment.name !== 'client' || type !== 'update' || file.endsWith('.css')) {
145
+ return;
146
+ }
147
+
148
+ // Every content file Tailwind scans is registered as a dependency of the
149
+ // theme CSS — Vite models it as a file-only entry node whose importer is
150
+ // `main.css` — so each source-file save invalidates `main.css` and
151
+ // re-runs the monorepo-wide Tailwind scan. During an edit wave that
152
+ // serializes one full scan per save. Drop the theme-dep entries from the
153
+ // update (the changed module itself still hot-updates immediately) and
154
+ // reload the theme CSS once on the trailing edge of a quiet window, so a
155
+ // wave costs at most one scan.
156
+ const isThemeDep = (mod: (typeof modules)[number]): boolean =>
157
+ mod.file === config.srcCssPath ||
158
+ (mod.id === null &&
159
+ mod.importers.size > 0 &&
160
+ [...mod.importers].every((importer) => importer.file === config.srcCssPath));
161
+ if (!modules.some(isThemeDep)) {
162
+ return;
163
+ }
164
+
165
+ const environment = this.environment;
166
+ clearTimeout(themeReloadTimer);
167
+ themeReloadTimer = setTimeout(() => {
168
+ for (const mod of environment.moduleGraph.getModulesByFile(config.srcCssPath) ?? []) {
169
+ environment.reloadModule(mod).catch(() => {
170
+ // Server may be mid-restart; the next edit reschedules the reload.
171
+ });
172
+ }
173
+ }, 300);
174
+
175
+ return modules.filter((mod) => !isThemeDep(mod));
176
+ },
95
177
  transformIndexHtml: () => {
96
178
  // Apply .dark class to <html> synchronously before any scripts run, so that
97
179
  // the critical CSS html.dark rules apply on the very first paint.
@@ -122,4 +204,10 @@ export const ThemePlugin = (options: ThemePluginOptions): Plugin => {
122
204
  return [darkModeTag, layersTag, criticalTag];
123
205
  },
124
206
  };
207
+
208
+ // The Tailwind Vite plugins are `enforce: 'pre'`, so they compile theme CSS (resolving
209
+ // `@import 'tailwindcss'`, @source, @plugin, @theme) before the postcss chain runs —
210
+ // postcss-import never sees the raw Tailwind directives. Scan roots come from the @source
211
+ // directives in main.css (relative to that file); no project-root base is needed.
212
+ return [...tailwindcssVite(), themePlugin];
125
213
  };
@@ -24,22 +24,25 @@ body {
24
24
  }
25
25
 
26
26
  /* Pre-CSS color fallbacks so text is readable before and during Vite HMR. */
27
- /* Values are intentionally kept in sync with @theme tokens in semantic.css. */
27
+ /* Values are intentionally kept in sync with @theme tokens in semantic.css:
28
+ * color ← --color-base-fg = light-dark(neutral-950, neutral-150)
29
+ * background-color ← --color-base-surface = elevation-3 = light-dark(neutral-125, neutral-850)
30
+ * The literals carry the ramp tint (chroma 0.001, hue 190) so first paint matches the loaded theme. */
28
31
  :root {
29
32
  color-scheme: light;
30
33
  }
31
34
  html {
32
- color: oklch(0.145 0 0);
33
- background-color: oklch(0.985 0 0);
35
+ color: oklch(0.145 0.001 190);
36
+ background-color: oklch(0.92 0.001 190);
34
37
  }
35
38
  html.dark {
36
39
  color-scheme: dark;
37
- color: oklch(0.985 0 0);
38
- background-color: oklch(0.145 0 0);
40
+ color: oklch(0.905 0.001 190);
41
+ background-color: oklch(0.237 0.001 190);
39
42
  }
40
43
  @media (prefers-color-scheme: dark) {
41
44
  html:not(.dark) {
42
- color: oklch(0.985 0 0);
43
- background-color: oklch(0.145 0 0);
45
+ color: oklch(0.905 0.001 190);
46
+ background-color: oklch(0.237 0.001 190);
44
47
  }
45
48
  }
@@ -6,21 +6,23 @@ import { type ChromaticPalette } from '@dxos/ui-types';
6
6
 
7
7
  export type Hue = ChromaticPalette | 'neutral';
8
8
 
9
- // TODO(burdon): Reconcile with ui-theme/theme/roles.css
9
+ /**
10
+ * See theme.css
11
+ */
10
12
  export type ColorStyles = {
11
13
  hue: Hue;
12
- fill: string; // -fill
13
- surface: string; // -surface
14
- surfaceText: string; // -surface-text
15
- text: string; // -text
16
- border: string; // -border
14
+ bg: string;
15
+ surface: string;
16
+ fg: string;
17
+ text: string;
18
+ border: string;
17
19
  };
18
20
 
19
21
  const neutral: ColorStyles = {
20
22
  hue: 'neutral',
21
- fill: 'bg-neutral-fill',
23
+ bg: 'bg-neutral-bg',
22
24
  surface: 'bg-neutral-surface',
23
- surfaceText: 'text-neutral-surface-text',
25
+ fg: 'text-neutral-fg',
24
26
  text: 'text-neutral-text',
25
27
  border: 'border-neutral-border',
26
28
  };
@@ -30,137 +32,137 @@ const neutral: ColorStyles = {
30
32
  const styles: ColorStyles[] = [
31
33
  {
32
34
  hue: 'red',
33
- fill: 'bg-red-fill',
35
+ bg: 'bg-red-bg',
34
36
  surface: 'bg-red-surface',
35
- surfaceText: 'text-red-surface-text',
37
+ fg: 'text-red-fg',
36
38
  text: 'text-red-text',
37
39
  border: 'border-red-border',
38
40
  },
39
41
  {
40
42
  hue: 'orange',
41
- fill: 'bg-orange-fill',
43
+ bg: 'bg-orange-bg',
42
44
  surface: 'bg-orange-surface',
43
- surfaceText: 'text-orange-surface-text',
45
+ fg: 'text-orange-fg',
44
46
  text: 'text-orange-text',
45
47
  border: 'border-orange-border',
46
48
  },
47
49
  {
48
50
  hue: 'amber',
49
- fill: 'bg-amber-fill',
51
+ bg: 'bg-amber-bg',
50
52
  surface: 'bg-amber-surface',
51
- surfaceText: 'text-amber-surface-text',
53
+ fg: 'text-amber-fg',
52
54
  text: 'text-amber-text',
53
55
  border: 'border-amber-border',
54
56
  },
55
57
  {
56
58
  hue: 'yellow',
57
- fill: 'bg-yellow-fill',
59
+ bg: 'bg-yellow-bg',
58
60
  surface: 'bg-yellow-surface',
59
- surfaceText: 'text-yellow-surface-text',
61
+ fg: 'text-yellow-fg',
60
62
  text: 'text-yellow-text',
61
63
  border: 'border-yellow-border',
62
64
  },
63
65
  {
64
66
  hue: 'lime',
65
- fill: 'bg-lime-fill',
67
+ bg: 'bg-lime-bg',
66
68
  surface: 'bg-lime-surface',
67
- surfaceText: 'text-lime-surface-text',
69
+ fg: 'text-lime-fg',
68
70
  text: 'text-lime-text',
69
71
  border: 'border-lime-border',
70
72
  },
71
73
  {
72
74
  hue: 'green',
73
- fill: 'bg-green-fill',
75
+ bg: 'bg-green-bg',
74
76
  surface: 'bg-green-surface',
75
- surfaceText: 'text-green-surface-text',
77
+ fg: 'text-green-fg',
76
78
  text: 'text-green-text',
77
79
  border: 'border-green-border',
78
80
  },
79
81
  {
80
82
  hue: 'emerald',
81
- fill: 'bg-emerald-fill',
83
+ bg: 'bg-emerald-bg',
82
84
  surface: 'bg-emerald-surface',
83
- surfaceText: 'text-emerald-surface-text',
85
+ fg: 'text-emerald-fg',
84
86
  text: 'text-emerald-text',
85
87
  border: 'border-emerald-border',
86
88
  },
87
89
  {
88
90
  hue: 'teal',
89
- fill: 'bg-teal-fill',
91
+ bg: 'bg-teal-bg',
90
92
  surface: 'bg-teal-surface',
91
- surfaceText: 'text-teal-surface-text',
93
+ fg: 'text-teal-fg',
92
94
  text: 'text-teal-text',
93
95
  border: 'border-teal-border',
94
96
  },
95
97
  {
96
98
  hue: 'cyan',
97
- fill: 'bg-cyan-fill',
99
+ bg: 'bg-cyan-bg',
98
100
  surface: 'bg-cyan-surface',
99
- surfaceText: 'text-cyan-surface-text',
101
+ fg: 'text-cyan-fg',
100
102
  text: 'text-cyan-text',
101
103
  border: 'border-cyan-border',
102
104
  },
103
105
  {
104
106
  hue: 'sky',
105
- fill: 'bg-sky-fill',
107
+ bg: 'bg-sky-bg',
106
108
  surface: 'bg-sky-surface',
107
- surfaceText: 'text-sky-surface-text',
109
+ fg: 'text-sky-fg',
108
110
  text: 'text-sky-text',
109
111
  border: 'border-sky-border',
110
112
  },
111
113
  {
112
114
  hue: 'blue',
113
- fill: 'bg-blue-fill',
115
+ bg: 'bg-blue-bg',
114
116
  surface: 'bg-blue-surface',
115
- surfaceText: 'text-blue-surface-text',
117
+ fg: 'text-blue-fg',
116
118
  text: 'text-blue-text',
117
119
  border: 'border-blue-border',
118
120
  },
119
121
  {
120
122
  hue: 'indigo',
121
- fill: 'bg-indigo-fill',
123
+ bg: 'bg-indigo-bg',
122
124
  surface: 'bg-indigo-surface',
123
- surfaceText: 'text-indigo-surface-text',
125
+ fg: 'text-indigo-fg',
124
126
  text: 'text-indigo-text',
125
127
  border: 'border-indigo-border',
126
128
  },
127
129
  {
128
130
  hue: 'violet',
129
- fill: 'bg-violet-fill',
131
+ bg: 'bg-violet-bg',
130
132
  surface: 'bg-violet-surface',
131
- surfaceText: 'text-violet-surface-text',
133
+ fg: 'text-violet-fg',
132
134
  text: 'text-violet-text',
133
135
  border: 'border-violet-border',
134
136
  },
135
137
  {
136
138
  hue: 'purple',
137
- fill: 'bg-purple-fill',
139
+ bg: 'bg-purple-bg',
138
140
  surface: 'bg-purple-surface',
139
- surfaceText: 'text-purple-surface-text',
141
+ fg: 'text-purple-fg',
140
142
  text: 'text-purple-text',
141
143
  border: 'border-purple-border',
142
144
  },
143
145
  {
144
146
  hue: 'fuchsia',
145
- fill: 'bg-fuchsia-fill',
147
+ bg: 'bg-fuchsia-bg',
146
148
  surface: 'bg-fuchsia-surface',
147
- surfaceText: 'text-fuchsia-surface-text',
149
+ fg: 'text-fuchsia-fg',
148
150
  text: 'text-fuchsia-text',
149
151
  border: 'border-fuchsia-border',
150
152
  },
151
153
  {
152
154
  hue: 'pink',
153
- fill: 'bg-pink-fill',
155
+ bg: 'bg-pink-bg',
154
156
  surface: 'bg-pink-surface',
155
- surfaceText: 'text-pink-surface-text',
157
+ fg: 'text-pink-fg',
156
158
  text: 'text-pink-text',
157
159
  border: 'border-pink-border',
158
160
  },
159
161
  {
160
162
  hue: 'rose',
161
- fill: 'bg-rose-fill',
163
+ bg: 'bg-rose-bg',
162
164
  surface: 'bg-rose-surface',
163
- surfaceText: 'text-rose-surface-text',
165
+ fg: 'text-rose-fg',
164
166
  text: 'text-rose-text',
165
167
  border: 'border-rose-border',
166
168
  },
@@ -171,6 +173,16 @@ export const palette = {
171
173
  hues: styles,
172
174
  };
173
175
 
176
+ const validHues: ReadonlySet<Hue> = new Set<Hue>([neutral.hue, ...styles.map((s) => s.hue)]);
177
+
178
+ /**
179
+ * Normalise an arbitrary string into a known `Hue`, falling back to `'neutral'` when the
180
+ * input doesn't match one of the catalogued palette entries. Useful when accepting hue
181
+ * values from user-authored data (e.g. ECHO objects, plugin settings) that need to be
182
+ * forwarded to a hue-keyed prop like `Tag`'s `palette`.
183
+ */
184
+ export const toHue = (hue: string | undefined): Hue => (hue && validHues.has(hue as Hue) ? (hue as Hue) : 'neutral');
185
+
174
186
  // TODO(burdon): Rename getClassNames.
175
187
  export const getStyles = (hue: string): ColorStyles => {
176
188
  return styles.find((color) => color.hue === hue) || neutral;