@close-by/clay 0.2.1 → 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 +6 -4
- package/src/index.ts +71 -1
- package/src/tailwind.ts +238 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@close-by/clay",
|
|
3
|
-
"version": "0.2.
|
|
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.2.
|
|
29
|
-
"@close-by/clay-components": "0.2.
|
|
30
|
-
"@close-by/clay-server": "0.2.
|
|
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
|
-
|
|
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 {
|
package/src/tailwind.ts
ADDED
|
@@ -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
|
+
}
|