@dxos/ui-theme 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/ui-theme",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A set of design system tokens to use with DXOS UI.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -38,7 +38,7 @@
38
38
  "@fontsource-variable/inter": "5.2.8",
39
39
  "@fontsource-variable/jetbrains-mono": "5.0.21",
40
40
  "@fontsource-variable/nunito": "5.2.7",
41
- "@fontsource/poiret-one": "^5.0.20",
41
+ "@fontsource/poiret-one": "^5.2.8",
42
42
  "@tailwindcss/forms": "^0.5.9",
43
43
  "@tailwindcss/postcss": "^4.3.3",
44
44
  "@tailwindcss/vite": "^4.3.3",
@@ -50,8 +50,8 @@
50
50
  "tailwind-scrollbar": "^4.0.0",
51
51
  "tailwind-variants": "^3.2.2",
52
52
  "tailwindcss": "^4.3.0",
53
- "@dxos/node-std": "0.13.0",
54
- "@dxos/ui-types": "0.13.0"
53
+ "@dxos/node-std": "0.14.0",
54
+ "@dxos/ui-types": "0.14.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@storybook/react-vite": "^10.6.0",
package/src/main.css CHANGED
@@ -18,8 +18,10 @@
18
18
  * Tailwind CSS regeneration, which invalidates `main.css` through its
19
19
  * import graph and produces a 3–5-ping HMR storm per icon-sprite write
20
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.
21
+ * in). The entry that imports this file declares the scan paths instead:
22
+ * `workspace.css` in this repo, or the stylesheet `ThemePlugin` generates for
23
+ * an installed copy, since paths relative to this file would reach the
24
+ * consumer's whole `node_modules`.
23
25
  */
24
26
  @import 'tailwindcss' source(none);
25
27
 
@@ -49,45 +51,6 @@
49
51
  /* Third-party integrations */
50
52
  @import './css/integrations/codemirror.css';
51
53
 
52
- /**
53
- * With `source(none)` on the Tailwind import above, these are the *only*
54
- * paths Tailwind scans for utility classes. Cover every workspace source
55
- * directory so lazy-loaded plugin / UI / SDK chunks don't surface new
56
- * utilities mid-session, and root-level HTML entrypoints (index.html,
57
- * internal.html, …) are visible too.
58
- *
59
- * Paths are relative to this file (`packages/ui/ui-theme/src/main.css`):
60
- * ../../../ → `packages/`
61
- * ../../../../tools/ → `tools/`
62
- *
63
- * Tailwind ignores `node_modules` and common build/output dirs by default,
64
- * so a broad workspace glob is safe *for scanning*.
65
- *
66
- * NOTE: Tailwind's scanner respects `.gitignore` (so `dist/` is never
67
- * scanned), but it hands Vite a coarse `dir-dependency` glob and Vite
68
- * re-expands that itself, ignoring only `node_modules`. That re-expansion
69
- * sweeps `dist/**` `.d.ts` files into the watch set and causes a `main.css`
70
- * HMR storm on every package rebuild. Build outputs are excluded from the
71
- * dev watcher in `plugins/ThemePlugin.ts` (`server.watch.ignored`) rather
72
- * than here, because `@source not` does NOT propagate to the glob Vite
73
- * re-expands.
74
- */
75
- @source '../../../**/*.{ts,tsx,html}';
76
- @source '../../../../tools/**/*.{ts,tsx,html}';
77
-
78
- /* Plain stylesheets outside this build (Next's theme, plugin overrides): scanning them keeps the theme variables they read (`var(--color-*)`) from being tree-shaken once no utility class names them. */
79
- @source '../../../**/src/**/*.css';
80
-
81
- /**
82
- * Exclude generated/static data files that have no Tailwind classes but are
83
- * large enough to dominate scan time. The oxide scanner tokenizes every byte;
84
- * a single 5MB geo-data file can pin Vite at 100% CPU for tens of seconds on
85
- * a cold start.
86
- */
87
- @source not '../../../ui/react-ui-geo/data/**';
88
- @source not '../../../ui/solid-ui-geo/data/**';
89
- @source not '../../../apps/composer-app/src/plugins/welcome/components/Welcome/hero-image.ts';
90
-
91
54
  /**
92
55
  * Force-emit CSS variables for the full color palette for all:
93
56
  * --color-{hue}-{shade}
@@ -7,8 +7,8 @@
7
7
  import tailwindcssPostcss from '@tailwindcss/postcss';
8
8
  import tailwindcssVite from '@tailwindcss/vite';
9
9
  import autoprefixer from 'autoprefixer';
10
- import { existsSync, readFileSync } from 'node:fs';
11
- import { resolve } from 'node:path';
10
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
11
+ import { dirname, join, resolve, sep } from 'node:path';
12
12
  import postcssImport from 'postcss-import';
13
13
  import postcssNesting from 'postcss-nesting';
14
14
  import { type HtmlTagDescriptor, type Plugin, type UserConfig } from 'vite';
@@ -82,19 +82,61 @@ const skipHotUpdateInBundledDev = (plugin: Plugin): Plugin => {
82
82
 
83
83
  export type ThemePluginOptions = {
84
84
  srcCssPath?: string;
85
+ /** Extra Tailwind scan globs (absolute, or relative to the Vite root) for a consumer of the published package. */
86
+ content?: string[];
85
87
  virtualFileId?: string;
86
88
  verbose?: boolean;
87
89
  };
88
90
 
91
+ const toCssString = (path: string): string => JSON.stringify(path.split(sep).join('/'));
92
+
93
+ /**
94
+ * Writes the theme entry for a consumer of the published package and returns its path.
95
+ * `workspace.css` cannot serve them: its scan paths are relative to this package, which from inside
96
+ * `node_modules` reaches every installed package, and Tailwind walks all of it before answering.
97
+ */
98
+ const writeConsumerTheme = ({
99
+ packageRoot,
100
+ root,
101
+ outPath,
102
+ content,
103
+ }: {
104
+ packageRoot: string;
105
+ root: string;
106
+ outPath: string;
107
+ content: string[];
108
+ }): string => {
109
+ // The scope this package was installed into, and the consumer's own, which differ under pnpm's isolated layout.
110
+ const scopeDirs = new Set([dirname(packageRoot), resolve(root, 'node_modules/@dxos')]);
111
+ const sources = [
112
+ // Tailwind skips `node_modules` below an explicit base, so this covers only the consumer's own files.
113
+ join(root, '**/*.{ts,tsx,js,jsx,html}'),
114
+ ...[...scopeDirs].map((scopeDir) => join(scopeDir, '*/dist/lib/**/*.mjs')),
115
+ ...content.map((glob) => resolve(root, glob)),
116
+ ];
117
+ const css = [
118
+ `@import ${toCssString(join(packageRoot, 'src/main.css'))};`,
119
+ ...sources.map((source) => `@source ${toCssString(source)};`),
120
+ '',
121
+ ].join('\n');
122
+
123
+ // Rewriting identical content would bump the mtime, which Tailwind reads as a reason to rebuild.
124
+ if (!existsSync(outPath) || readFileSync(outPath, 'utf-8') !== css) {
125
+ mkdirSync(dirname(outPath), { recursive: true });
126
+ writeFileSync(outPath, css);
127
+ }
128
+
129
+ return outPath;
130
+ };
131
+
89
132
  /**
90
133
  * Vite plugin to configure theme.
91
134
  * Returns the official Tailwind Vite plugin (persistent incremental scanner) alongside the theme plugin.
92
135
  */
93
136
  export const ThemePlugin = (options: ThemePluginOptions): Plugin[] => {
94
- // Prefer source CSS if available (monorepo dev), fall back to dist for installed package.
95
- const srcThemePath = resolve(import.meta.dirname, ROOT, 'src/main.css');
96
- const distThemePath = resolve(import.meta.dirname, 'main.css');
97
- const isMonorepo = existsSync(srcThemePath);
137
+ // `src` is published too, so only the location tells a workspace checkout from an installed copy.
138
+ const packageRoot = realpathSync(resolve(import.meta.dirname, ROOT));
139
+ const isMonorepo = !packageRoot.split(sep).includes('node_modules');
98
140
 
99
141
  // Static assets shipped via "files": ["src"] in package.json.
100
142
  // Both monorepo and installed package resolve to the same src/plugins/ directory.
@@ -103,15 +145,12 @@ export const ThemePlugin = (options: ThemePluginOptions): Plugin[] => {
103
145
  const mainCssPath = resolve(pluginsDir, 'main.css');
104
146
 
105
147
  const config = {
106
- srcCssPath: options.srcCssPath ?? (isMonorepo ? srcThemePath : distThemePath),
148
+ // Installed copies get theirs from `configResolved`, which knows the consumer's root.
149
+ srcCssPath: options.srcCssPath ?? (isMonorepo ? resolve(packageRoot, 'src/workspace.css') : ''),
107
150
  virtualFileId: options.virtualFileId ?? '@dxos-theme',
108
151
  verbose: options.verbose,
109
152
  };
110
153
 
111
- if (process.env.DEBUG || options.verbose) {
112
- console.log('ThemePlugin:\n', JSON.stringify(config, null, 2));
113
- }
114
-
115
154
  // Set under `vite dev --experimentalBundle`; see the guard in `hotUpdate`.
116
155
  let bundledDev = false;
117
156
 
@@ -130,6 +169,18 @@ export const ThemePlugin = (options: ThemePluginOptions): Plugin[] => {
130
169
  name: 'vite-plugin-dxos-ui-theme',
131
170
  configResolved: (resolved) => {
132
171
  bundledDev = resolved.experimental.bundledDev === true;
172
+ if (!config.srcCssPath) {
173
+ config.srcCssPath = writeConsumerTheme({
174
+ packageRoot,
175
+ root: resolved.root,
176
+ outPath: resolve(resolved.cacheDir, '..', '.dxos-ui-theme.css'),
177
+ content: options.content ?? [],
178
+ });
179
+ }
180
+
181
+ if (process.env.DEBUG || options.verbose) {
182
+ console.log('ThemePlugin:\n', JSON.stringify(config, null, 2));
183
+ }
133
184
  },
134
185
  config: (): UserConfig => {
135
186
  return {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * In-repo theme entry: the theme plus the workspace scan paths.
3
+ * Installed copies of this package never load it; `ThemePlugin` generates their entry instead.
4
+ */
5
+
6
+ @import './main.css';
7
+
8
+ /**
9
+ * With `source(none)` on the Tailwind import in `main.css`, these are the *only*
10
+ * paths Tailwind scans for utility classes. Cover every workspace source
11
+ * directory so lazy-loaded plugin / UI / SDK chunks don't surface new
12
+ * utilities mid-session, and root-level HTML entrypoints (index.html,
13
+ * internal.html, …) are visible too.
14
+ *
15
+ * Paths are relative to this file (`packages/ui/ui-theme/src/workspace.css`):
16
+ * ../../../ → `packages/`
17
+ * ../../../../tools/ → `tools/`
18
+ *
19
+ * Tailwind ignores `node_modules` and common build/output dirs by default,
20
+ * so a broad workspace glob is safe *for scanning*.
21
+ *
22
+ * NOTE: Tailwind's scanner respects `.gitignore` (so `dist/` is never
23
+ * scanned), but it hands Vite a coarse `dir-dependency` glob and Vite
24
+ * re-expands that itself, ignoring only `node_modules`. That re-expansion
25
+ * sweeps `dist/**` `.d.ts` files into the watch set and causes a theme
26
+ * HMR storm on every package rebuild. Build outputs are excluded from the
27
+ * dev watcher in `plugins/ThemePlugin.ts` (`server.watch.ignored`) rather
28
+ * than here, because `@source not` does NOT propagate to the glob Vite
29
+ * re-expands.
30
+ */
31
+ @source '../../../**/*.{ts,tsx,html}';
32
+ @source '../../../../tools/**/*.{ts,tsx,html}';
33
+
34
+ /* Plain stylesheets outside this build (Next's theme, plugin overrides): scanning them keeps the theme variables they read (`var(--color-*)`) from being tree-shaken once no utility class names them. */
35
+ @source '../../../**/src/**/*.css';
36
+
37
+ /**
38
+ * Exclude generated/static data files that have no Tailwind classes but are
39
+ * large enough to dominate scan time. The oxide scanner tokenizes every byte;
40
+ * a single 5MB geo-data file can pin Vite at 100% CPU for tens of seconds on
41
+ * a cold start.
42
+ */
43
+ @source not '../../../ui/react-ui-geo/data/**';
44
+ @source not '../../../ui/solid-ui-geo/data/**';
45
+ @source not '../../../apps/composer-app/src/plugins/welcome/components/Welcome/hero-image.ts';