@shortwind/cli 0.1.0-beta.0 → 0.1.0-beta.1
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/README.md +32 -0
- package/dist/{bench-a_9WmuOE.js → bench-NKKDz3ld.js} +285 -8
- package/dist/bench-NKKDz3ld.js.map +1 -0
- package/dist/bin.js +36 -2
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/dist/bench-a_9WmuOE.js.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @shortwind/cli
|
|
2
|
+
|
|
3
|
+
The Shortwind command-line tool. Provides the `shortwind` command: `init`, `add`, `remove`, `upgrade`, `dev`, `build`, `lint`, `ls`, `preset`, `bench`.
|
|
4
|
+
|
|
5
|
+
[Shortwind](https://shortwind.dev) is a token-efficient class layer for Tailwind: you write short `@recipe` names in `class=`/`className=` and they expand to full Tailwind class clusters at build time.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @shortwind/cli@beta init # beta: published on the `beta` tag
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`init` is the one command you need — it detects your bundler, installs the right adapter, copies the recipe catalog into `./recipes/`, scaffolds a default theme, wires the plugin into your config, and generates `skills/shortwind/SKILL.md`.
|
|
14
|
+
|
|
15
|
+
Install it to get the `shortwind` command in scripts:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i -D @shortwind/cli@beta
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Common commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
shortwind init --preset app # starter | app | content | all | none
|
|
25
|
+
shortwind add table dialog # add families on demand
|
|
26
|
+
shortwind dev # watch recipes/, regenerate SKILL.md
|
|
27
|
+
shortwind build # one-shot regenerate SKILL.md
|
|
28
|
+
shortwind lint # validate recipe usage, naming, conflicts
|
|
29
|
+
shortwind bench # measure token savings
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Docs: <https://shortwind.dev>
|
|
@@ -2,11 +2,11 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
5
|
-
import { buildRegistry, parseRecipeFile, renderSkillMarkdown } from "@shortwind/core";
|
|
5
|
+
import { buildRegistry, isReservedRecipeName, parseRecipeFile, renderSkillMarkdown } from "@shortwind/core";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import chokidar from "chokidar";
|
|
9
8
|
import { glob } from "tinyglobby";
|
|
9
|
+
import chokidar from "chokidar";
|
|
10
10
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
11
11
|
import cl100k_base from "js-tiktoken/ranks/cl100k_base";
|
|
12
12
|
import { loadRegistryFromDir, transformContent } from "@shortwind/tailwind";
|
|
@@ -207,6 +207,221 @@ async function writeLockfile(recipesDir, lock) {
|
|
|
207
207
|
};
|
|
208
208
|
await writeFile(lockPath(recipesDir), JSON.stringify(sorted, null, 2) + "\n");
|
|
209
209
|
}
|
|
210
|
+
const THEME_BLOCK = `/* shortwind:theme — default tokens for the recipe catalog. Edit freely. */
|
|
211
|
+
@custom-variant dark (&:is(.dark *));
|
|
212
|
+
|
|
213
|
+
:root {
|
|
214
|
+
--radius: 0.625rem;
|
|
215
|
+
--background: oklch(1 0 0);
|
|
216
|
+
--foreground: oklch(0.145 0 0);
|
|
217
|
+
--card: oklch(1 0 0);
|
|
218
|
+
--card-foreground: oklch(0.145 0 0);
|
|
219
|
+
--popover: oklch(1 0 0);
|
|
220
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
221
|
+
--primary: oklch(0.205 0 0);
|
|
222
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
223
|
+
--secondary: oklch(0.97 0 0);
|
|
224
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
225
|
+
--muted: oklch(0.97 0 0);
|
|
226
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
227
|
+
--accent: oklch(0.97 0 0);
|
|
228
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
229
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
230
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
231
|
+
--border: oklch(0.922 0 0);
|
|
232
|
+
--input: oklch(0.922 0 0);
|
|
233
|
+
--ring: oklch(0.708 0 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.dark {
|
|
237
|
+
--background: oklch(0.145 0 0);
|
|
238
|
+
--foreground: oklch(0.985 0 0);
|
|
239
|
+
--card: oklch(0.205 0 0);
|
|
240
|
+
--card-foreground: oklch(0.985 0 0);
|
|
241
|
+
--popover: oklch(0.205 0 0);
|
|
242
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
243
|
+
--primary: oklch(0.922 0 0);
|
|
244
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
245
|
+
--secondary: oklch(0.269 0 0);
|
|
246
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
247
|
+
--muted: oklch(0.269 0 0);
|
|
248
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
249
|
+
--accent: oklch(0.269 0 0);
|
|
250
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
251
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
252
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
253
|
+
--border: oklch(1 0 0 / 10%);
|
|
254
|
+
--input: oklch(1 0 0 / 15%);
|
|
255
|
+
--ring: oklch(0.556 0 0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@theme inline {
|
|
259
|
+
--color-background: var(--background);
|
|
260
|
+
--color-foreground: var(--foreground);
|
|
261
|
+
--color-card: var(--card);
|
|
262
|
+
--color-card-foreground: var(--card-foreground);
|
|
263
|
+
--color-popover: var(--popover);
|
|
264
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
265
|
+
--color-primary: var(--primary);
|
|
266
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
267
|
+
--color-secondary: var(--secondary);
|
|
268
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
269
|
+
--color-muted: var(--muted);
|
|
270
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
271
|
+
--color-accent: var(--accent);
|
|
272
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
273
|
+
--color-destructive: var(--destructive);
|
|
274
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
275
|
+
--color-border: var(--border);
|
|
276
|
+
--color-input: var(--input);
|
|
277
|
+
--color-ring: var(--ring);
|
|
278
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
279
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
280
|
+
--radius-lg: var(--radius);
|
|
281
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@layer base {
|
|
285
|
+
body {
|
|
286
|
+
@apply bg-background text-foreground;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/* end shortwind theme */
|
|
290
|
+
`;
|
|
291
|
+
const TAILWIND_IMPORT_RE = /@import\s+["']tailwindcss["'][^;\n]*;?/;
|
|
292
|
+
async function scaffoldTheme(cwd) {
|
|
293
|
+
const cssFiles = await glob(["**/*.css"], {
|
|
294
|
+
cwd,
|
|
295
|
+
absolute: true,
|
|
296
|
+
onlyFiles: true,
|
|
297
|
+
ignore: [
|
|
298
|
+
"**/node_modules/**",
|
|
299
|
+
"**/dist/**",
|
|
300
|
+
"**/.next/**",
|
|
301
|
+
"**/.output/**",
|
|
302
|
+
"recipes/**"
|
|
303
|
+
]
|
|
304
|
+
});
|
|
305
|
+
for (const file of cssFiles) {
|
|
306
|
+
const source = await readFile(file, "utf8");
|
|
307
|
+
if (!TAILWIND_IMPORT_RE.test(source)) continue;
|
|
308
|
+
if (source.includes("/* shortwind:theme")) return {
|
|
309
|
+
themePath: file,
|
|
310
|
+
action: "skipped",
|
|
311
|
+
reason: "already scaffolded"
|
|
312
|
+
};
|
|
313
|
+
if (/--background\s*:/.test(source) || /@theme\b/.test(source)) return {
|
|
314
|
+
themePath: file,
|
|
315
|
+
action: "skipped",
|
|
316
|
+
reason: "project already defines a theme"
|
|
317
|
+
};
|
|
318
|
+
const m = source.match(TAILWIND_IMPORT_RE);
|
|
319
|
+
const at = (m.index ?? 0) + m[0].length;
|
|
320
|
+
await writeFile(file, source.slice(0, at) + "\n\n" + THEME_BLOCK + source.slice(at));
|
|
321
|
+
return {
|
|
322
|
+
themePath: file,
|
|
323
|
+
action: "injected"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (!isTailwindV4(cwd)) return {
|
|
327
|
+
themePath: null,
|
|
328
|
+
action: "skipped",
|
|
329
|
+
reason: "no Tailwind v4 CSS entry found"
|
|
330
|
+
};
|
|
331
|
+
const target = path.join(cwd, "src", "index.css");
|
|
332
|
+
if (existsSync(target)) return {
|
|
333
|
+
themePath: target,
|
|
334
|
+
action: "skipped",
|
|
335
|
+
reason: "src/index.css exists without a tailwindcss import"
|
|
336
|
+
};
|
|
337
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
338
|
+
await writeFile(target, `@import "tailwindcss";\n\n${THEME_BLOCK}`);
|
|
339
|
+
return {
|
|
340
|
+
themePath: target,
|
|
341
|
+
action: "created"
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function isTailwindV4(cwd) {
|
|
345
|
+
try {
|
|
346
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
347
|
+
const m = (pkg.devDependencies?.["tailwindcss"] ?? pkg.dependencies?.["tailwindcss"] ?? "").match(/(\d+)/);
|
|
348
|
+
return m ? Number(m[1]) >= 4 : false;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/bundler-config.ts
|
|
355
|
+
const VITE_CONFIGS = [
|
|
356
|
+
"vite.config.ts",
|
|
357
|
+
"vite.config.mts",
|
|
358
|
+
"vite.config.cts",
|
|
359
|
+
"vite.config.js",
|
|
360
|
+
"vite.config.mjs",
|
|
361
|
+
"vite.config.cjs"
|
|
362
|
+
];
|
|
363
|
+
const VITE_SNIPPET = [
|
|
364
|
+
`import { shortwind } from "@shortwind/vite";`,
|
|
365
|
+
`// add shortwind() to the Vite plugins array — it runs in the pre phase,`,
|
|
366
|
+
`// before Tailwind's scan:`,
|
|
367
|
+
`// plugins: [shortwind(), tailwindcss(), react()]`
|
|
368
|
+
].join("\n");
|
|
369
|
+
async function wireBundler(cwd, bundler) {
|
|
370
|
+
if (bundler === "vite") return wireVite(cwd);
|
|
371
|
+
if (bundler === "next") return {
|
|
372
|
+
configPath: null,
|
|
373
|
+
action: "manual",
|
|
374
|
+
snippet: `import { withShortwind } from "@shortwind/next";\n// wrap your Next config: export default withShortwind(nextConfig);`,
|
|
375
|
+
reason: "Next config wiring is manual"
|
|
376
|
+
};
|
|
377
|
+
if (bundler === "astro") return {
|
|
378
|
+
configPath: null,
|
|
379
|
+
action: "manual",
|
|
380
|
+
snippet: `import shortwind from "@shortwind/astro";\n// add to integrations: integrations: [shortwind()]`,
|
|
381
|
+
reason: "Astro config wiring is manual"
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
configPath: null,
|
|
385
|
+
action: "skipped",
|
|
386
|
+
reason: "no supported bundler detected"
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async function wireVite(cwd) {
|
|
390
|
+
const configPath = VITE_CONFIGS.map((f) => path.join(cwd, f)).find((p) => existsSync(p));
|
|
391
|
+
if (!configPath) return {
|
|
392
|
+
configPath: null,
|
|
393
|
+
action: "manual",
|
|
394
|
+
snippet: VITE_SNIPPET,
|
|
395
|
+
reason: "no vite config found"
|
|
396
|
+
};
|
|
397
|
+
const source = await readFile(configPath, "utf8");
|
|
398
|
+
if (/@shortwind\/vite/.test(source)) return {
|
|
399
|
+
configPath,
|
|
400
|
+
action: "skipped",
|
|
401
|
+
reason: "plugin already wired"
|
|
402
|
+
};
|
|
403
|
+
const pluginsMatch = source.match(/plugins\s*:\s*\[/);
|
|
404
|
+
if (!pluginsMatch) return {
|
|
405
|
+
configPath,
|
|
406
|
+
action: "manual",
|
|
407
|
+
snippet: VITE_SNIPPET,
|
|
408
|
+
reason: "no plugins array found"
|
|
409
|
+
};
|
|
410
|
+
const withImport = addImport(source, `import { shortwind } from "@shortwind/vite";`);
|
|
411
|
+
const at = withImport.indexOf(pluginsMatch[0]) + pluginsMatch[0].length;
|
|
412
|
+
await writeFile(configPath, withImport.slice(0, at) + "shortwind(), " + withImport.slice(at));
|
|
413
|
+
return {
|
|
414
|
+
configPath,
|
|
415
|
+
action: "patched"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function addImport(source, line) {
|
|
419
|
+
const importRe = /^[ \t]*import[\s\S]*?from\s+["'][^"']+["'];?[ \t]*$/gm;
|
|
420
|
+
let lastEnd = -1;
|
|
421
|
+
for (const m of source.matchAll(importRe)) lastEnd = (m.index ?? 0) + m[0].length;
|
|
422
|
+
if (lastEnd === -1) return `${line}\n${source}`;
|
|
423
|
+
return source.slice(0, lastEnd) + `\n${line}` + source.slice(lastEnd);
|
|
424
|
+
}
|
|
210
425
|
//#endregion
|
|
211
426
|
//#region src/init.ts
|
|
212
427
|
const DEFAULT_REGISTRY = "https://shortwind.dev/registry";
|
|
@@ -233,6 +448,8 @@ async function init(options) {
|
|
|
233
448
|
await installHuskyHook(huskyPath);
|
|
234
449
|
const skillPath = path.join(cwd, "skills", "shortwind", "SKILL.md");
|
|
235
450
|
await writeSkillMd(skillPath, recipesDir, families);
|
|
451
|
+
const theme = await scaffoldTheme(cwd);
|
|
452
|
+
const bundlerConfig = await wireBundler(cwd, shape.bundler);
|
|
236
453
|
return {
|
|
237
454
|
packageManager: shape.packageManager,
|
|
238
455
|
preset: options.preset,
|
|
@@ -244,7 +461,12 @@ async function init(options) {
|
|
|
244
461
|
configPath,
|
|
245
462
|
vscodePath,
|
|
246
463
|
huskyPath,
|
|
247
|
-
skillPath
|
|
464
|
+
skillPath,
|
|
465
|
+
themePath: theme.themePath,
|
|
466
|
+
themeAction: theme.action,
|
|
467
|
+
bundlerConfigPath: bundlerConfig.configPath,
|
|
468
|
+
bundlerConfigAction: bundlerConfig.action,
|
|
469
|
+
...bundlerConfig.snippet ? { bundlerConfigSnippet: bundlerConfig.snippet } : {}
|
|
248
470
|
};
|
|
249
471
|
}
|
|
250
472
|
async function resolveFamilies(preset, source) {
|
|
@@ -595,6 +817,43 @@ function collectBrokenDependents(recipesDir, removedRecipeNames) {
|
|
|
595
817
|
return out;
|
|
596
818
|
}
|
|
597
819
|
//#endregion
|
|
820
|
+
//#region src/commands/new.ts
|
|
821
|
+
var NewFamilyError = class extends Error {
|
|
822
|
+
constructor(message) {
|
|
823
|
+
super(message);
|
|
824
|
+
this.name = "NewFamilyError";
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
function template(family) {
|
|
828
|
+
return [
|
|
829
|
+
`/* shortwind: ${family}@0.0.1 sha:000000 */`,
|
|
830
|
+
``,
|
|
831
|
+
`/* @guide`,
|
|
832
|
+
` TODO: one or two lines on when to reach for these recipes, and which`,
|
|
833
|
+
` easy-to-confuse name to prefer. */`,
|
|
834
|
+
``,
|
|
835
|
+
`/* TODO: describe this recipe. */`,
|
|
836
|
+
`@recipe ${family} {`,
|
|
837
|
+
` p-4`,
|
|
838
|
+
`}`,
|
|
839
|
+
``
|
|
840
|
+
].join("\n");
|
|
841
|
+
}
|
|
842
|
+
async function newFamily(options) {
|
|
843
|
+
assertValidFamilyName(options.family);
|
|
844
|
+
const cwd = path.resolve(options.cwd);
|
|
845
|
+
const config = await readConfig(cwd);
|
|
846
|
+
const recipesDir = path.join(cwd, config.recipesDir);
|
|
847
|
+
const familyPath = path.join(recipesDir, `${options.family}.css`);
|
|
848
|
+
if (existsSync(familyPath) && !options.force) throw new NewFamilyError(`${path.join(config.recipesDir, `${options.family}.css`)} already exists (use --force to overwrite)`);
|
|
849
|
+
await mkdir(recipesDir, { recursive: true });
|
|
850
|
+
await writeFile(familyPath, template(options.family));
|
|
851
|
+
return {
|
|
852
|
+
familyPath,
|
|
853
|
+
skillPath: await regenerateSkillMd(cwd, config)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
598
857
|
//#region src/commands/preset.ts
|
|
599
858
|
async function preset(options) {
|
|
600
859
|
const cwd = path.resolve(options.cwd);
|
|
@@ -1038,7 +1297,7 @@ const DEFAULT_RECIPES_CSS = {
|
|
|
1038
1297
|
"navigation.css": "/* shortwind: navigation@0.0.1 sha:000000 */\n\n/* @guide\n @nav is the container; links are @nav-link with @nav-link-active for the\n current page. Tabs mirror that pair: @tab and @tab-active. Use @breadcrumb\n for trail navigation. Active and inactive are separate recipes — swap the\n whole class rather than combining them.\n*/\n\n/* Top-level nav container. */\n@recipe nav {\n flex items-center gap-1\n}\n\n/* Inactive nav link with hover/focus states. */\n@recipe nav-link {\n inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n\n/* Active nav link. */\n@recipe nav-link-active {\n inline-flex items-center gap-2 rounded-md bg-muted px-3 py-1.5 text-sm font-medium text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n\n/* Breadcrumb trail container. */\n@recipe breadcrumb {\n flex items-center gap-1.5 text-sm text-muted-foreground\n}\n\n/* Inactive tab control. */\n@recipe tab {\n inline-flex items-center gap-2 border-b-2 border-transparent px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n\n/* Active tab control. */\n@recipe tab-active {\n inline-flex items-center gap-2 border-b-2 border-primary px-3 py-2 text-sm font-medium text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n",
|
|
1039
1298
|
"progress.css": "/* shortwind: progress@0.0.1 sha:000000 */\n\n/* @guide\n A bar is two pieces: @progress-track (the background) wrapping @progress-bar\n (the fill). For an indeterminate state use @spinner instead — it's a\n standalone loader, not a bar.\n*/\n\n/* Progress bar track (background). */\n@recipe progress-track {\n h-2 w-full overflow-hidden rounded-full bg-muted\n}\n\n/* Progress bar fill. */\n@recipe progress-bar {\n h-full rounded-full bg-primary transition-all\n}\n\n/* Indeterminate loading spinner. */\n@recipe spinner {\n inline-block h-4 w-4 animate-spin rounded-full border-2 border-border border-t-primary\n}\n",
|
|
1040
1299
|
"skeleton.css": "/* shortwind: skeleton@0.0.1 sha:000000 */\n\n/* @guide\n Match the skeleton to the shape it stands in for: @skeleton (block),\n @skeleton-text (a text line), @skeleton-circle (avatar/icon). Size block and\n text skeletons with raw width/height utilities.\n*/\n\n/* Default rectangular skeleton placeholder. */\n@recipe skeleton {\n animate-pulse rounded-md bg-muted\n}\n\n/* Single-line text skeleton. */\n@recipe skeleton-text {\n h-4 w-full animate-pulse rounded bg-muted\n}\n\n/* Circular skeleton (avatar/icon). */\n@recipe skeleton-circle {\n h-10 w-10 animate-pulse rounded-full bg-muted\n}\n",
|
|
1041
|
-
"surface.css": "/* shortwind: surface@0.0.1 sha:000000 */\n\n/* @guide\n @surface / @surface-muted / @surface-accent set a background+foreground pair\n for a region — one per section. @
|
|
1300
|
+
"surface.css": "/* shortwind: surface@0.0.1 sha:000000 */\n\n/* @guide\n @surface / @surface-muted / @surface-accent set a background+foreground pair\n for a region — one per section. @wrapper (or @wrapper-tight for prose)\n centers and width-caps content; there is no @wrapper-lg, set a different cap\n with max-w-* yourself. (Note: @container is reserved for Tailwind's\n container-query utility, so the content wrapper is @wrapper.) @divider-h and\n @divider-v are hairline rules.\n*/\n\n/* Default page/section surface. */\n@recipe surface {\n bg-background text-foreground\n}\n\n/* Muted surface — secondary background. */\n@recipe surface-muted {\n bg-muted text-foreground\n}\n\n/* Accent surface — soft brand background. */\n@recipe surface-accent {\n bg-accent text-accent-foreground\n}\n\n/* Centered content wrapper with a max width. */\n@recipe wrapper {\n mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8\n}\n\n/* Narrow content wrapper for prose. */\n@recipe wrapper-tight {\n mx-auto w-full max-w-3xl px-4 sm:px-6\n}\n\n/* Horizontal divider line. */\n@recipe divider-h {\n shrink-0 h-px w-full bg-border\n}\n\n/* Vertical divider line. */\n@recipe divider-v {\n shrink-0 h-full w-px bg-border\n}\n",
|
|
1042
1301
|
"table.css": "/* shortwind: table@0.0.1 sha:000000 */\n\n/* @guide\n Wrap the table in @table-container for horizontal overflow, then put @table\n (or @table-zebra for striped rows) on the <table>. Cells are @th (header) and\n @td (body); add @tr-hover to a <tr> for row highlighting.\n*/\n\n/* Scroll container for a wide table — keeps overflow horizontal. */\n@recipe table-container {\n w-full overflow-x-auto rounded-md border border-border\n}\n\n/* Data table base. */\n@recipe table {\n w-full border-collapse text-left text-sm text-foreground\n}\n\n/* Table header cell. */\n@recipe th {\n border-b border-border px-3 py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground\n}\n\n/* Table body cell. */\n@recipe td {\n border-b border-border px-3 py-2\n}\n\n/* Row hover state. */\n@recipe tr-hover {\n transition-colors hover:bg-muted\n}\n\n/* Table with zebra striping on alternating rows. */\n@recipe table-zebra {\n w-full border-collapse text-left text-sm text-foreground [&_tbody_tr:nth-child(odd)]:bg-muted\n}\n",
|
|
1043
1302
|
"text.css": "/* shortwind: text@0.0.1 sha:000000 */\n\n/* @guide\n Headings are sized by weight, not HTML level: @heading-xl/lg/md/sm — there\n is no @h1..@h6. Body copy: @body (default), @lead (intro paragraphs), @muted\n (secondary), @caption (fine print). Use @label for form labels and @link for\n inline links. Don't append a -text suffix: it's @body not @body-text, @muted\n not @muted-text, @link not @link-text.\n*/\n\n/* Top-level page heading. */\n@recipe heading-xl {\n text-4xl font-bold tracking-tight text-foreground\n}\n\n/* Large section heading. */\n@recipe heading-lg {\n text-2xl font-semibold tracking-tight text-foreground\n}\n\n/* Medium heading. */\n@recipe heading-md {\n text-xl font-semibold text-foreground\n}\n\n/* Small heading. */\n@recipe heading-sm {\n text-base font-semibold text-foreground\n}\n\n/* Default body text. */\n@recipe body {\n text-sm leading-6 text-foreground\n}\n\n/* Lead paragraph — larger body copy for hero/intro sections. */\n@recipe lead {\n text-lg leading-relaxed text-muted-foreground\n}\n\n/* Muted secondary text. */\n@recipe muted {\n text-sm text-muted-foreground\n}\n\n/* Form label text. */\n@recipe label {\n text-sm font-medium text-foreground\n}\n\n/* Caption — small supporting text. */\n@recipe caption {\n text-xs text-muted-foreground\n}\n\n/* Inline link with hover/focus states. */\n@recipe link {\n text-primary underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n",
|
|
1044
1303
|
"tooltip.css": "/* shortwind: tooltip@0.0.1 sha:000000 */\n\n/* @guide\n @tooltip is the floating label bubble — it styles appearance only, so pair it\n with your own positioning.\n*/\n\n/* Floating tooltip bubble. */\n@recipe tooltip {\n pointer-events-none z-50 rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md\n}\n"
|
|
@@ -1162,7 +1421,7 @@ const CORPUS_FILES = {
|
|
|
1162
1421
|
<div className="h-8 w-8 rounded-full bg-primary/20" />
|
|
1163
1422
|
</div>
|
|
1164
1423
|
</header>
|
|
1165
|
-
<main className="@
|
|
1424
|
+
<main className="@wrapper @stack-lg py-8">
|
|
1166
1425
|
<div className="@grid-3">
|
|
1167
1426
|
<div className="@card-elevated">Card 1</div>
|
|
1168
1427
|
<div className="@card-elevated">Card 2</div>
|
|
@@ -1186,7 +1445,8 @@ const ALL_RULES = [
|
|
|
1186
1445
|
"recipe/bad-suffix-order",
|
|
1187
1446
|
"recipe/conflicting-intent",
|
|
1188
1447
|
"recipe/dynamic-class",
|
|
1189
|
-
"recipe/no-sibling-overlap"
|
|
1448
|
+
"recipe/no-sibling-overlap",
|
|
1449
|
+
"recipe/reserved-name"
|
|
1190
1450
|
];
|
|
1191
1451
|
const DEFAULT_CONTENT = ["src/**/*.{html,js,jsx,ts,tsx,vue,svelte,astro,md,mdx}"];
|
|
1192
1452
|
async function lint(options) {
|
|
@@ -1198,6 +1458,7 @@ async function lint(options) {
|
|
|
1198
1458
|
const { registry, parseFindings } = loadRegistry(recipesDir, enabledRules);
|
|
1199
1459
|
findings.push(...parseFindings);
|
|
1200
1460
|
findings.push(...checkRecipeNames(registry, recipesDir, enabledRules));
|
|
1461
|
+
findings.push(...checkReservedNames(registry, recipesDir, enabledRules));
|
|
1201
1462
|
const files = await glob(options.content ?? DEFAULT_CONTENT, {
|
|
1202
1463
|
cwd,
|
|
1203
1464
|
absolute: true,
|
|
@@ -1387,6 +1648,22 @@ function checkRecipeNames(registry, recipesDir, enabledRules) {
|
|
|
1387
1648
|
}
|
|
1388
1649
|
return findings;
|
|
1389
1650
|
}
|
|
1651
|
+
function checkReservedNames(registry, recipesDir, enabledRules) {
|
|
1652
|
+
if (!enabledRules.has("recipe/reserved-name")) return [];
|
|
1653
|
+
const findings = [];
|
|
1654
|
+
for (const recipes of Object.values(registry.families)) for (const recipe of recipes) {
|
|
1655
|
+
if (!isReservedRecipeName(recipe.name)) continue;
|
|
1656
|
+
findings.push({
|
|
1657
|
+
rule: "recipe/reserved-name",
|
|
1658
|
+
severity: "error",
|
|
1659
|
+
file: path.join(recipesDir, recipe.sourceFile),
|
|
1660
|
+
line: recipe.sourceLine,
|
|
1661
|
+
column: 1,
|
|
1662
|
+
message: `recipe @${recipe.name} collides with a reserved Tailwind @-utility; rename it`
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
return findings;
|
|
1666
|
+
}
|
|
1390
1667
|
function checkUsageSuffixOrder(file, tokens, registry) {
|
|
1391
1668
|
const findings = [];
|
|
1392
1669
|
for (const token of tokens) {
|
|
@@ -1831,6 +2108,6 @@ function formatBenchTable(result) {
|
|
|
1831
2108
|
return lines.join("\n");
|
|
1832
2109
|
}
|
|
1833
2110
|
//#endregion
|
|
1834
|
-
export {
|
|
2111
|
+
export { extractHeader as A, readLockfile as C, detectProject as D, resolvePresetFamilies as E, rewriteHeaderSha as M, sealRecipeFile as N, buildHeaderLine as O, init as S, createRegistrySource as T, newFamily as _, formatFindingsText as a, renameFamilyInSource as b, UpgradeError as c, BuildError as d, build as f, NewFamilyError as g, preset as h, extractClassUsages as i, normalizeBody as j, computeBodySha as k, upgrade as l, ls as m, formatBenchTable as n, lint as o, formatLsText as p, ALL_RULES as r, verify as s, bench as t, dev as u, remove as v, writeLockfile as w, DEFAULT_REGISTRY as x, add as y };
|
|
1835
2112
|
|
|
1836
|
-
//# sourceMappingURL=bench-
|
|
2113
|
+
//# sourceMappingURL=bench-NKKDz3ld.js.map
|