@close-by/clay 0.2.0 → 0.2.2

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": "@close-by/clay",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "NiceGUI-like ui.* facade for Clay apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,8 +25,10 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@close-by/clay-core": "0.1.0",
29
- "@close-by/clay-components": "0.1.0",
30
- "@close-by/clay-server": "0.1.0"
28
+ "@close-by/clay-core": "0.2.2",
29
+ "@close-by/clay-components": "0.2.2",
30
+ "@close-by/clay-server": "0.2.2",
31
+ "@tailwindcss/cli": "^4.3.3",
32
+ "tailwindcss": "^4.3.3"
31
33
  }
32
34
  }
package/src/index.ts CHANGED
@@ -383,8 +383,23 @@ import {
383
383
  type NavFromPagesOptions,
384
384
  } from './pages';
385
385
  import { resolveClayClientDir } from './client-dir';
386
+ import {
387
+ detectClayTailwindContent,
388
+ ensureClayTailwindCss,
389
+ mergeClayCss,
390
+ type ClayTailwindOptions,
391
+ } from './tailwind';
392
+
386
393
 
387
394
  export { resolveClayClientDir } from './client-dir';
395
+ export {
396
+ detectClayTailwindContent,
397
+ ensureClayTailwindCss,
398
+ mergeClayCss,
399
+ type ClayTailwindOptions,
400
+ type ClayTailwindResult,
401
+ } from './tailwind';
402
+
388
403
 
389
404
  export type {
390
405
  ButtonProps,
@@ -1333,8 +1348,61 @@ export function page(path: string, fn: () => void, options?: PageOptions): void
1333
1348
  export type RunConfig = ClayServerConfig & {
1334
1349
  /** Global dashboard shell; wraps every page unless `ui.page(..., { shell: false })`. */
1335
1350
  app?: AppProps;
1351
+ /**
1352
+ * Auto-build Tailwind utilities from app modules and inject after the client CSS.
1353
+ *
1354
+ * - `undefined` — on when `pages/`, `src/`, or `app/` exists under cwd
1355
+ * - `true` — same auto-detect
1356
+ * - `false` — off
1357
+ * - object — custom `content` paths, `watch`, `appendCss`, …
1358
+ *
1359
+ * Scans `className` / `.classes(...)` strings; no app-level Tailwind config required.
1360
+ */
1361
+ tailwind?: boolean | (Omit<ClayTailwindOptions, 'content'> & { content?: string[] });
1336
1362
  };
1337
1363
 
1364
+ const pathDelimiter = process.platform === 'win32' ? ';' : ':';
1365
+
1366
+ function applyClayTailwind(cfg: RunConfig): RunConfig {
1367
+ if (cfg.tailwind === false || process.env.CLAY_NO_TAILWIND === '1') {
1368
+ return { ...cfg, tailwind: false };
1369
+ }
1370
+
1371
+ const tw = cfg.tailwind;
1372
+ const cwd = process.cwd();
1373
+ const envContent = process.env.CLAY_TAILWIND_CONTENT?.split(pathDelimiter).filter(Boolean) ?? [];
1374
+
1375
+ const opts =
1376
+ tw === undefined || tw === true
1377
+ ? {
1378
+ content: envContent.length ? envContent : detectClayTailwindContent(cwd),
1379
+ watch: process.env.CLAY_TAILWIND_WATCH === '1',
1380
+ }
1381
+ : {
1382
+ content: tw.content?.length
1383
+ ? tw.content
1384
+ : envContent.length
1385
+ ? envContent
1386
+ : detectClayTailwindContent(cwd),
1387
+ watch: tw.watch ?? process.env.CLAY_TAILWIND_WATCH === '1',
1388
+ appendCss: tw.appendCss,
1389
+ outFile: tw.outFile,
1390
+ cwd: tw.cwd ?? cwd,
1391
+ };
1392
+
1393
+ if (!opts.content.length) return cfg;
1394
+
1395
+ const { cssPath } = ensureClayTailwindCss({
1396
+ content: opts.content,
1397
+ cwd: opts.cwd ?? cwd,
1398
+ watch: opts.watch,
1399
+ appendCss: opts.appendCss,
1400
+ outFile: opts.outFile,
1401
+ });
1402
+
1403
+ return { ...cfg, css: mergeClayCss(cfg.css, cssPath) };
1404
+ }
1405
+
1338
1406
  let runCalled = false;
1339
1407
 
1340
1408
  /** Whether `ui.run` has been invoked in this process (used by the `clay` CLI). */
@@ -1374,7 +1442,9 @@ export function run(rootOrConfig: PageFn | RunConfig = {}, config: RunConfig = {
1374
1442
  cfg = { ...cfg, clientDir: resolveClayClientDir() };
1375
1443
  }
1376
1444
 
1377
- const { app: appProps, ...serverConfig } = cfg;
1445
+ cfg = applyClayTailwind(cfg);
1446
+
1447
+ const { app: appProps, tailwind: _tailwind, ...serverConfig } = cfg;
1378
1448
  if (appProps) {
1379
1449
  setPageWrapper((pageFn) => app(appProps, pageFn));
1380
1450
  } else {
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Zero-config Tailwind for Clay apps: scan page modules, emit utilities CSS,
3
+ * inject via `ui.run({ css })` / CLI.
4
+ *
5
+ * Uses `@tailwindcss/cli` (utilities + theme only — client already ships base/preflight).
6
+ */
7
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
8
+ import { createRequire } from 'module';
9
+ import { dirname, isAbsolute, join, resolve } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ export type ClayTailwindOptions = {
13
+ /**
14
+ * Directories/files to scan for `className` / `.classes(...)` strings.
15
+ * Absolute or cwd-relative. Prefer app dirs — never the repo root with `node_modules`.
16
+ */
17
+ content: string[];
18
+ /** Working directory for relative paths. Default `process.cwd()`. */
19
+ cwd?: string;
20
+ /** Output CSS path. Default `<cwd>/.clay/tailwind.css`. */
21
+ outFile?: string;
22
+ /** Keep `@tailwindcss/cli --watch` running after the first build. */
23
+ watch?: boolean;
24
+ /** Extra CSS appended after the generated input (token overrides, keyframes). */
25
+ appendCss?: string;
26
+ };
27
+
28
+ export type ClayTailwindResult = {
29
+ cssPath: string;
30
+ /** Stop the background watch process (if any). */
31
+ stop: () => void;
32
+ };
33
+
34
+ const THEME_SNIPPET = `
35
+ /* Token utilities for app-scanned classes (vars come from the Clay client). */
36
+ @theme inline {
37
+ --color-background: var(--background);
38
+ --color-foreground: var(--foreground);
39
+ --color-card: var(--card);
40
+ --color-card-foreground: var(--card-foreground);
41
+ --color-popover: var(--popover);
42
+ --color-popover-foreground: var(--popover-foreground);
43
+ --color-primary: var(--primary);
44
+ --color-primary-foreground: var(--primary-foreground);
45
+ --color-secondary: var(--secondary);
46
+ --color-secondary-foreground: var(--secondary-foreground);
47
+ --color-muted: var(--muted);
48
+ --color-muted-foreground: var(--muted-foreground);
49
+ --color-accent: var(--accent);
50
+ --color-accent-foreground: var(--accent-foreground);
51
+ --color-destructive: var(--destructive);
52
+ --color-destructive-foreground: var(--destructive-foreground);
53
+ --color-border: var(--border);
54
+ --color-input: var(--input);
55
+ --color-ring: var(--ring);
56
+ --color-chart-1: var(--chart-1);
57
+ --color-chart-2: var(--chart-2);
58
+ --color-chart-3: var(--chart-3);
59
+ --color-chart-4: var(--chart-4);
60
+ --color-chart-5: var(--chart-5);
61
+ --color-sidebar: var(--sidebar);
62
+ --color-sidebar-foreground: var(--sidebar-foreground);
63
+ --color-sidebar-primary: var(--sidebar-primary);
64
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
65
+ --color-sidebar-accent: var(--sidebar-accent);
66
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
67
+ --color-sidebar-border: var(--sidebar-border);
68
+ --color-sidebar-ring: var(--sidebar-ring);
69
+ --radius-sm: calc(var(--radius) - 4px);
70
+ --radius-md: calc(var(--radius) - 2px);
71
+ --radius-lg: var(--radius);
72
+ --radius-xl: calc(var(--radius) + 4px);
73
+ }
74
+ `.trim();
75
+
76
+ function resolveContentPaths(content: string[], cwd: string): string[] {
77
+ const out: string[] = [];
78
+ for (const raw of content) {
79
+ const p = isAbsolute(raw) ? raw : resolve(cwd, raw);
80
+ if (existsSync(p)) out.push(p);
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /** Guess scan roots under `cwd` (`pages/`, `src/`, `app/`). */
86
+ export function detectClayTailwindContent(cwd = process.cwd()): string[] {
87
+ const candidates = ['pages', 'src', 'app'];
88
+ return candidates.map((d) => join(cwd, d)).filter((p) => existsSync(p));
89
+ }
90
+
91
+ function resolveTailwindcssRoot(): string {
92
+ const require = createRequire(import.meta.url);
93
+ const tryPkg = (req: NodeJS.Require): string | null => {
94
+ try {
95
+ return dirname(req.resolve('tailwindcss/package.json'));
96
+ } catch {
97
+ return null;
98
+ }
99
+ };
100
+ const fromUi = tryPkg(require);
101
+ if (fromUi) return fromUi;
102
+ try {
103
+ const rootReq = createRequire(
104
+ join(fileURLToPath(new URL('.', import.meta.url)), '../../../package.json'),
105
+ );
106
+ const fromRoot = tryPkg(rootReq);
107
+ if (fromRoot) return fromRoot;
108
+ } catch {
109
+ // ignore
110
+ }
111
+ throw new Error(
112
+ 'tailwindcss package not found. Install with: bun add tailwindcss @tailwindcss/cli',
113
+ );
114
+ }
115
+
116
+ function writeInputCss(inputFile: string, sources: string[], appendCss?: string): void {
117
+ mkdirSync(dirname(inputFile), { recursive: true });
118
+ const twRoot = resolveTailwindcssRoot();
119
+ // Absolute CSS imports so builds work even when `.clay/` is outside node_modules.
120
+ const themeCss = join(twRoot, 'theme.css');
121
+ const utilitiesCss = join(twRoot, 'utilities.css');
122
+ const sourceLines = sources.map((s) => `@source ${JSON.stringify(s)};`).join('\n');
123
+ const body = [
124
+ '/* Generated by @close-by/clay — do not edit */',
125
+ `@import ${JSON.stringify(themeCss)} layer(theme);`,
126
+ `@import ${JSON.stringify(utilitiesCss)} layer(utilities);`,
127
+ '',
128
+ sourceLines,
129
+ '',
130
+ THEME_SNIPPET,
131
+ appendCss ? `\n${appendCss.trim()}\n` : '',
132
+ ].join('\n');
133
+ writeFileSync(inputFile, body, 'utf8');
134
+ }
135
+
136
+ function resolveTailwindCliEntry(): string {
137
+ const require = createRequire(import.meta.url);
138
+ const tryPkg = (req: NodeJS.Require): string | null => {
139
+ try {
140
+ const pkgJsonPath = req.resolve('@tailwindcss/cli/package.json');
141
+ return join(dirname(pkgJsonPath), 'dist/index.mjs');
142
+ } catch {
143
+ return null;
144
+ }
145
+ };
146
+
147
+ const fromUi = tryPkg(require);
148
+ if (fromUi && existsSync(fromUi)) return fromUi;
149
+
150
+ try {
151
+ const rootReq = createRequire(
152
+ join(fileURLToPath(new URL('.', import.meta.url)), '../../../package.json'),
153
+ );
154
+ const fromRoot = tryPkg(rootReq);
155
+ if (fromRoot && existsSync(fromRoot)) return fromRoot;
156
+ } catch {
157
+ // ignore
158
+ }
159
+
160
+ throw new Error(
161
+ 'Tailwind CLI not found. Install with: bun add tailwindcss @tailwindcss/cli\n' +
162
+ '(shipped with @close-by/clay — ensure the package is installed).',
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Build (and optionally watch) Tailwind CSS for Clay page modules.
168
+ * Returns a stylesheet path suitable for `ui.run({ css })`.
169
+ */
170
+ export function ensureClayTailwindCss(options: ClayTailwindOptions): ClayTailwindResult {
171
+ const cwd = options.cwd ?? process.cwd();
172
+ const sources = resolveContentPaths(options.content, cwd);
173
+ if (sources.length === 0) {
174
+ throw new Error(
175
+ 'Clay Tailwind: no content paths found. Pass content: ["./pages"] or create a pages/ directory.',
176
+ );
177
+ }
178
+
179
+ const outFile = options.outFile
180
+ ? isAbsolute(options.outFile)
181
+ ? options.outFile
182
+ : resolve(cwd, options.outFile)
183
+ : join(cwd, '.clay', 'tailwind.css');
184
+ const inputFile = join(dirname(outFile), 'tailwind.input.css');
185
+ mkdirSync(dirname(outFile), { recursive: true });
186
+ writeInputCss(inputFile, sources, options.appendCss);
187
+
188
+ const cliEntry = resolveTailwindCliEntry();
189
+ const buildArgs = [cliEntry, '-i', inputFile, '-o', outFile];
190
+
191
+ const built = Bun.spawnSync({
192
+ cmd: [process.execPath, ...buildArgs],
193
+ cwd,
194
+ stdout: 'pipe',
195
+ stderr: 'pipe',
196
+ });
197
+ if (built.exitCode !== 0) {
198
+ const err = built.stderr.toString() || built.stdout.toString() || `exit ${built.exitCode}`;
199
+ throw new Error(`Clay Tailwind build failed:\n${err}`);
200
+ }
201
+ if (!existsSync(outFile)) {
202
+ throw new Error(`Clay Tailwind build produced no file at ${outFile}`);
203
+ }
204
+
205
+ let child: ReturnType<typeof Bun.spawn> | null = null;
206
+ if (options.watch) {
207
+ child = Bun.spawn({
208
+ cmd: [process.execPath, ...buildArgs, '--watch'],
209
+ cwd,
210
+ stdout: 'ignore',
211
+ stderr: 'inherit',
212
+ stdin: 'ignore',
213
+ });
214
+ }
215
+
216
+ return {
217
+ cssPath: outFile,
218
+ stop: () => {
219
+ if (!child) return;
220
+ try {
221
+ child.kill();
222
+ } catch {
223
+ // already exited
224
+ }
225
+ child = null;
226
+ },
227
+ };
228
+ }
229
+
230
+ /** Merge a Tailwind stylesheet into existing `css` config (append). */
231
+ export function mergeClayCss(
232
+ existing: string | string[] | undefined,
233
+ extra: string | string[],
234
+ ): string[] {
235
+ const a = existing == null ? [] : Array.isArray(existing) ? existing : [existing];
236
+ const b = Array.isArray(extra) ? extra : [extra];
237
+ return [...a, ...b].map((p) => p.trim()).filter(Boolean);
238
+ }