@shortwind/cli 0.1.0-beta.10 → 0.1.0-beta.12
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 +1 -1
- package/dist/{bench-BGTQAha8.js → bench-BGO3pupw.js} +111 -29
- package/dist/bench-BGO3pupw.js.map +1 -0
- package/dist/bin.js +11 -4
- package/dist/bin.js.map +1 -1
- package/dist/{catalog.generated-B_ds7MPV.js → catalog.generated-BdZstlkf.js} +3 -3
- package/dist/catalog.generated-BdZstlkf.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/dist/bench-BGTQAha8.js.map +0 -1
- package/dist/catalog.generated-B_ds7MPV.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The Shortwind command-line tool. Provides the `shortwind` command: `init`, `add`
|
|
|
10
10
|
npx @shortwind/cli@beta init # beta: published on the `beta` tag
|
|
11
11
|
```
|
|
12
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,
|
|
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, and generates `skills/shortwind/SKILL.md`. On Vite it also patches the plugin into your config; on Next/Astro it prints the one-line snippet to paste. Run it with `--yes` (default preset) or `--preset <name>` to skip the interactive prompt in CI and agent sessions.
|
|
14
14
|
|
|
15
15
|
Install it to get the `shortwind` command in scripts:
|
|
16
16
|
|
|
@@ -5,11 +5,11 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
6
6
|
import { PLACEHOLDER_SHA, RECIPE_SHA_HEX_LENGTH, buildRegistry, isReservedRecipeName, normalizeRecipeBody, parseRecipeFile, renderSkillMarkdown } from "@shortwind/core";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
+
import { findTailwindEntryCssFiles, loadRegistryFromDir, modeForFile, syncSourceDirectiveToFile, transformContent } from "@shortwind/tailwind";
|
|
8
9
|
import { glob } from "tinyglobby";
|
|
9
10
|
import chokidar from "chokidar";
|
|
10
11
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
11
12
|
import cl100k_base from "js-tiktoken/ranks/cl100k_base";
|
|
12
|
-
import { loadRegistryFromDir, modeForFile, transformContent } from "@shortwind/tailwind";
|
|
13
13
|
//#region src/fingerprint.ts
|
|
14
14
|
const HEADER_PATTERN = /^\/\*\s*shortwind:\s+(\S+)@(\S+)\s+sha:([^\s*]+)(?:\s+—\s+DO NOT EDIT THIS LINE)?\s*\*\/\s*$/;
|
|
15
15
|
function extractHeader(source) {
|
|
@@ -58,6 +58,9 @@ function sealRecipeFile(source, family, version) {
|
|
|
58
58
|
}
|
|
59
59
|
//#endregion
|
|
60
60
|
//#region src/detect.ts
|
|
61
|
+
function skillAdapterFor(bundler) {
|
|
62
|
+
return bundler === "unknown" ? void 0 : bundler;
|
|
63
|
+
}
|
|
61
64
|
function parsePackageJson(pkgPath) {
|
|
62
65
|
let parsed;
|
|
63
66
|
try {
|
|
@@ -165,7 +168,7 @@ const BUNDLED_ORIGIN = "bundled:@shortwind/catalog";
|
|
|
165
168
|
function bundledSource() {
|
|
166
169
|
let cache = null;
|
|
167
170
|
const load = () => {
|
|
168
|
-
cache ??= import("./catalog.generated-
|
|
171
|
+
cache ??= import("./catalog.generated-BdZstlkf.js");
|
|
169
172
|
return cache;
|
|
170
173
|
};
|
|
171
174
|
return {
|
|
@@ -203,9 +206,26 @@ function fileSource(origin) {
|
|
|
203
206
|
}
|
|
204
207
|
};
|
|
205
208
|
}
|
|
209
|
+
const FETCH_RETRIES = 2;
|
|
210
|
+
const RETRY_BASE_DELAY_MS = 500;
|
|
211
|
+
function isTransientFetchError(err) {
|
|
212
|
+
return err instanceof DOMException && err.name === "TimeoutError" || err instanceof TypeError;
|
|
213
|
+
}
|
|
206
214
|
function httpSource(origin) {
|
|
207
215
|
const base = origin.replace(/\/+$/, "");
|
|
208
|
-
const get = (url) =>
|
|
216
|
+
const get = async (url) => {
|
|
217
|
+
let lastErr;
|
|
218
|
+
for (let attempt = 0; attempt <= FETCH_RETRIES; attempt++) {
|
|
219
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, RETRY_BASE_DELAY_MS * attempt));
|
|
220
|
+
try {
|
|
221
|
+
return await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!isTransientFetchError(err)) throw err;
|
|
224
|
+
lastErr = err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw lastErr;
|
|
228
|
+
};
|
|
209
229
|
let presetsCache = null;
|
|
210
230
|
return {
|
|
211
231
|
origin,
|
|
@@ -415,6 +435,21 @@ async function scaffoldTheme(cwd) {
|
|
|
415
435
|
action: "created"
|
|
416
436
|
};
|
|
417
437
|
}
|
|
438
|
+
const THEME_COLOR_TOKENS = new Set([...THEME_BLOCK.matchAll(/--color-([\w-]+)\s*:/g)].map((m) => m[1] ?? ""));
|
|
439
|
+
const COLOR_UTILITY_RE = /^(?:bg|text|border|ring|outline|fill|stroke|divide|accent|caret|decoration|shadow|from|via|to|placeholder)-(.+)$/;
|
|
440
|
+
function referencedThemeTokens(flattened) {
|
|
441
|
+
const out = /* @__PURE__ */ new Set();
|
|
442
|
+
for (const utilities of Object.values(flattened)) for (const raw of utilities) {
|
|
443
|
+
const m = (raw.split(":").pop() ?? raw).match(COLOR_UTILITY_RE);
|
|
444
|
+
if (!m) continue;
|
|
445
|
+
const name = (m[1] ?? "").replace(/\/.*$/, "");
|
|
446
|
+
if (THEME_COLOR_TOKENS.has(name)) out.add(name);
|
|
447
|
+
}
|
|
448
|
+
return [...out].sort();
|
|
449
|
+
}
|
|
450
|
+
function findMissingThemeTokens(css, flattened) {
|
|
451
|
+
return referencedThemeTokens(flattened).filter((name) => !new RegExp(`--(?:color-)?${name}\\s*:`).test(css));
|
|
452
|
+
}
|
|
418
453
|
function isTailwindV4(cwd) {
|
|
419
454
|
try {
|
|
420
455
|
const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
@@ -437,21 +472,22 @@ const VITE_CONFIGS = [
|
|
|
437
472
|
const VITE_SNIPPET = [
|
|
438
473
|
`import { shortwind } from "@shortwind/vite";`,
|
|
439
474
|
`// add shortwind() to the Vite plugins array — it runs in the pre phase,`,
|
|
440
|
-
`// before Tailwind's scan
|
|
441
|
-
`//
|
|
475
|
+
`// before Tailwind's scan. strict: true fails the build when a recipe`,
|
|
476
|
+
`// token leaks unexpanded (recommended; drop it to demote to a warning):`,
|
|
477
|
+
`// plugins: [shortwind({ strict: true }), tailwindcss(), react()]`
|
|
442
478
|
].join("\n");
|
|
443
479
|
async function wireBundler(cwd, bundler) {
|
|
444
480
|
if (bundler === "vite") return wireVite(cwd);
|
|
445
481
|
if (bundler === "next") return {
|
|
446
482
|
configPath: null,
|
|
447
483
|
action: "manual",
|
|
448
|
-
snippet: `import { withShortwind } from "@shortwind/next";\n// wrap your Next config: export default withShortwind(nextConfig);`,
|
|
484
|
+
snippet: `import { withShortwind } from "@shortwind/next";\n// withShortwind is curried — wrap your Next config. strict: true fails\n// the build when a recipe token leaks unexpanded (recommended):\n// export default withShortwind({ strict: true })(nextConfig);`,
|
|
449
485
|
reason: "Next config wiring is manual"
|
|
450
486
|
};
|
|
451
487
|
if (bundler === "astro") return {
|
|
452
488
|
configPath: null,
|
|
453
489
|
action: "manual",
|
|
454
|
-
snippet: `import shortwind from "@shortwind/astro";\n// add to integrations: integrations: [shortwind()]`,
|
|
490
|
+
snippet: `import shortwind from "@shortwind/astro";\n// add to integrations — strict: true fails the build when a recipe\n// token leaks unexpanded (recommended):\n// integrations: [shortwind({ strict: true })]`,
|
|
455
491
|
reason: "Astro config wiring is manual"
|
|
456
492
|
};
|
|
457
493
|
return {
|
|
@@ -499,25 +535,33 @@ function addImport(source, line) {
|
|
|
499
535
|
//#endregion
|
|
500
536
|
//#region src/agents-file.ts
|
|
501
537
|
const MARKER = "skills/shortwind/SKILL.md";
|
|
538
|
+
const DYNAMIC_MARKER = "expandClassList";
|
|
502
539
|
function line(skillRel) {
|
|
503
540
|
return `For UI, prefer Shortwind \`@recipe\` class names (e.g. \`@card\`, \`@btn-primary\`, \`@row\`) over raw Tailwind where a recipe fits — full catalog in \`${skillRel}\`.`;
|
|
504
541
|
}
|
|
542
|
+
function dynamicLine() {
|
|
543
|
+
return "Never build a recipe name dynamically (variable, prop, concatenation) — it silently won't expand. For a runtime choice between recipes use the `rc()`/`expandClassList` helper, and turn on `strict: true` in the Shortwind adapter config to fail the build on leaked `@tokens` — worked snippets under \"Dynamic classes\" in the SKILL doc above.";
|
|
544
|
+
}
|
|
505
545
|
const CANDIDATES = ["AGENTS.md", "CLAUDE.md"];
|
|
506
546
|
async function wireAgentsInstructions(cwd, skillPath) {
|
|
507
547
|
const pointer = line(path.relative(cwd, skillPath).split(path.sep).join("/"));
|
|
548
|
+
const dynamic = dynamicLine();
|
|
508
549
|
let touched = null;
|
|
509
550
|
for (const name of CANDIDATES) {
|
|
510
551
|
const file = path.join(cwd, name);
|
|
511
552
|
if (!existsSync(file)) continue;
|
|
512
553
|
const current = await readFile(file, "utf8");
|
|
513
|
-
|
|
554
|
+
const missing = [];
|
|
555
|
+
if (!current.includes(MARKER)) missing.push(pointer);
|
|
556
|
+
if (!current.includes(DYNAMIC_MARKER)) missing.push(dynamic);
|
|
557
|
+
if (missing.length === 0) {
|
|
514
558
|
touched ??= {
|
|
515
559
|
path: file,
|
|
516
560
|
action: "skipped"
|
|
517
561
|
};
|
|
518
562
|
continue;
|
|
519
563
|
}
|
|
520
|
-
await writeFile(file, current + (current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n") +
|
|
564
|
+
await writeFile(file, current + (current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n") + missing.join("\n") + "\n");
|
|
521
565
|
return {
|
|
522
566
|
path: file,
|
|
523
567
|
action: "appended"
|
|
@@ -525,7 +569,7 @@ async function wireAgentsInstructions(cwd, skillPath) {
|
|
|
525
569
|
}
|
|
526
570
|
if (touched) return touched;
|
|
527
571
|
const target = path.join(cwd, "AGENTS.md");
|
|
528
|
-
await writeFile(target, `# AGENTS.md\n\n${pointer}\n`);
|
|
572
|
+
await writeFile(target, `# AGENTS.md\n\n${pointer}\n${dynamic}\n`);
|
|
529
573
|
return {
|
|
530
574
|
path: target,
|
|
531
575
|
action: "created"
|
|
@@ -553,7 +597,15 @@ async function init(options) {
|
|
|
553
597
|
installError = err instanceof Error ? err.message : String(err);
|
|
554
598
|
}
|
|
555
599
|
const recipesDir = path.join(cwd, "recipes");
|
|
556
|
-
|
|
600
|
+
let copied;
|
|
601
|
+
try {
|
|
602
|
+
copied = await copyRecipes(source, families, recipesDir);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
const done = families.filter((f) => existsSync(path.join(recipesDir, `${f}.css`)));
|
|
605
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
606
|
+
throw new Error(`init aborted while copying recipe families (${done.length}/${families.length} copied${done.length > 0 ? `: ${done.join(", ")}` : ""}) — ${reason}\nThe project is incomplete (no config/SKILL.md/theme yet). Re-run the same init command to resume; already-copied families are skipped.`);
|
|
607
|
+
}
|
|
608
|
+
const { installed, skipped } = copied;
|
|
557
609
|
await updateLockfile(recipesDir, registry, installed);
|
|
558
610
|
const configPath = path.join(cwd, "shortwind.config.json");
|
|
559
611
|
await writeConfig(configPath, {
|
|
@@ -562,11 +614,17 @@ async function init(options) {
|
|
|
562
614
|
});
|
|
563
615
|
const vscodePath = path.join(cwd, ".vscode", "settings.json");
|
|
564
616
|
await wireVscodeClassRegex(vscodePath);
|
|
565
|
-
const huskyPath = path.join(cwd, ".husky", "pre-commit");
|
|
566
|
-
await installHuskyHook(huskyPath);
|
|
617
|
+
const huskyPath = await installHuskyHook(cwd, path.join(cwd, ".husky", "pre-commit"));
|
|
567
618
|
const skillPath = path.join(cwd, "skills", "shortwind", "SKILL.md");
|
|
568
|
-
await writeSkillMd(skillPath, recipesDir, families);
|
|
619
|
+
const skillRegistry = await writeSkillMd(skillPath, recipesDir, families, shape.bundler);
|
|
569
620
|
const theme = await scaffoldTheme(cwd);
|
|
621
|
+
let missingThemeTokens = [];
|
|
622
|
+
if (theme.action === "skipped" && theme.themePath && skillRegistry) missingThemeTokens = findMissingThemeTokens(await readFile(theme.themePath, "utf8"), skillRegistry.flattened);
|
|
623
|
+
const safelistCssPaths = [];
|
|
624
|
+
if (shape.bundler !== "vite" && shape.bundler !== "astro" && skillRegistry) for (const file of findTailwindEntryCssFiles(cwd)) {
|
|
625
|
+
syncSourceDirectiveToFile(file, skillRegistry);
|
|
626
|
+
safelistCssPaths.push(file);
|
|
627
|
+
}
|
|
570
628
|
const bundlerConfig = await wireBundler(cwd, shape.bundler);
|
|
571
629
|
const agentsFile = await wireAgentsInstructions(cwd, skillPath);
|
|
572
630
|
return {
|
|
@@ -583,6 +641,8 @@ async function init(options) {
|
|
|
583
641
|
skillPath,
|
|
584
642
|
themePath: theme.themePath,
|
|
585
643
|
themeAction: theme.action,
|
|
644
|
+
safelistCssPaths,
|
|
645
|
+
missingThemeTokens,
|
|
586
646
|
bundlerConfigPath: bundlerConfig.configPath,
|
|
587
647
|
bundlerConfigAction: bundlerConfig.action,
|
|
588
648
|
...bundlerConfig.snippet ? { bundlerConfigSnippet: bundlerConfig.snippet } : {},
|
|
@@ -721,18 +781,20 @@ async function wireVscodeClassRegex(vscodePath) {
|
|
|
721
781
|
parse(next);
|
|
722
782
|
await writeFile(vscodePath, next.endsWith("\n") ? next : next + "\n");
|
|
723
783
|
}
|
|
724
|
-
const HUSKY_LINE = "npx shortwind build";
|
|
725
|
-
async function installHuskyHook(huskyPath) {
|
|
784
|
+
const HUSKY_LINE = "npx @shortwind/cli build";
|
|
785
|
+
async function installHuskyHook(cwd, huskyPath) {
|
|
786
|
+
if (!existsSync(path.join(cwd, ".git"))) return null;
|
|
726
787
|
await mkdir(path.dirname(huskyPath), { recursive: true });
|
|
727
788
|
if (!existsSync(huskyPath)) {
|
|
728
789
|
await writeFile(huskyPath, `${HUSKY_LINE}\n`, { mode: 493 });
|
|
729
|
-
return;
|
|
790
|
+
return huskyPath;
|
|
730
791
|
}
|
|
731
792
|
const current = await readFile(huskyPath, "utf8");
|
|
732
|
-
if (current.includes(HUSKY_LINE)) return;
|
|
733
|
-
await writeFile(huskyPath, current.endsWith("\n") ? current + HUSKY_LINE + "\n" : current + "\nnpx shortwind build\n", { mode: 493 });
|
|
793
|
+
if (current.includes(HUSKY_LINE)) return huskyPath;
|
|
794
|
+
await writeFile(huskyPath, current.endsWith("\n") ? current + HUSKY_LINE + "\n" : current + "\nnpx @shortwind/cli build\n", { mode: 493 });
|
|
795
|
+
return huskyPath;
|
|
734
796
|
}
|
|
735
|
-
async function writeSkillMd(skillPath, recipesDir, families) {
|
|
797
|
+
async function writeSkillMd(skillPath, recipesDir, families, bundler) {
|
|
736
798
|
const allRecipes = [];
|
|
737
799
|
const guidance = {};
|
|
738
800
|
const problems = [];
|
|
@@ -749,10 +811,15 @@ async function writeSkillMd(skillPath, recipesDir, families) {
|
|
|
749
811
|
if (problems.length > 0 || !resolved.ok) {
|
|
750
812
|
const all = resolved.ok ? problems : [...problems, ...resolved.errors.map((e) => e.message)];
|
|
751
813
|
console.warn(`[shortwind] SKILL.md not generated — recipe errors:\n ${all.join("\n ")}`);
|
|
752
|
-
return;
|
|
814
|
+
return null;
|
|
753
815
|
}
|
|
754
816
|
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
755
|
-
|
|
817
|
+
const adapter = skillAdapterFor(bundler);
|
|
818
|
+
await writeFile(skillPath, renderSkillMarkdown(resolved.value, {
|
|
819
|
+
order: families,
|
|
820
|
+
...adapter ? { adapter } : {}
|
|
821
|
+
}));
|
|
822
|
+
return resolved.value;
|
|
756
823
|
}
|
|
757
824
|
//#endregion
|
|
758
825
|
//#region src/project.ts
|
|
@@ -827,7 +894,11 @@ async function regenerateSkillMd(cwd, config) {
|
|
|
827
894
|
}
|
|
828
895
|
const { mkdir } = await import("node:fs/promises");
|
|
829
896
|
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
830
|
-
|
|
897
|
+
const adapter = skillAdapterFor(detectProject(cwd).bundler);
|
|
898
|
+
await writeFile(skillPath, renderSkillMarkdown(resolved.value, {
|
|
899
|
+
order: families,
|
|
900
|
+
...adapter ? { adapter } : {}
|
|
901
|
+
}));
|
|
831
902
|
return skillPath;
|
|
832
903
|
}
|
|
833
904
|
/**
|
|
@@ -1145,8 +1216,13 @@ async function build(options) {
|
|
|
1145
1216
|
if (errors.length > 0) throw new BuildError(errors);
|
|
1146
1217
|
const resolved = buildRegistry(allRecipes, { guidance });
|
|
1147
1218
|
if (!resolved.ok) throw new BuildError(resolved.errors);
|
|
1219
|
+
const { bundler } = detectProject(cwd);
|
|
1220
|
+
const adapter = skillAdapterFor(bundler);
|
|
1148
1221
|
const skillPath = path.join(cwd, config.outputPath);
|
|
1149
|
-
const next = renderSkillMarkdown(resolved.value, {
|
|
1222
|
+
const next = renderSkillMarkdown(resolved.value, {
|
|
1223
|
+
order: families,
|
|
1224
|
+
...adapter ? { adapter } : {}
|
|
1225
|
+
});
|
|
1150
1226
|
const current = existsSync(skillPath) ? readFileSync(skillPath, "utf8") : null;
|
|
1151
1227
|
let changed = false;
|
|
1152
1228
|
if (current !== next) {
|
|
@@ -1154,10 +1230,16 @@ async function build(options) {
|
|
|
1154
1230
|
await writeFile(skillPath, next);
|
|
1155
1231
|
changed = true;
|
|
1156
1232
|
}
|
|
1233
|
+
const safelistCssPaths = [];
|
|
1234
|
+
if (bundler !== "vite" && bundler !== "astro") for (const file of findTailwindEntryCssFiles(cwd)) {
|
|
1235
|
+
if (syncSourceDirectiveToFile(file, resolved.value)) changed = true;
|
|
1236
|
+
safelistCssPaths.push(file);
|
|
1237
|
+
}
|
|
1157
1238
|
return {
|
|
1158
1239
|
changed,
|
|
1159
1240
|
families,
|
|
1160
|
-
skillPath
|
|
1241
|
+
skillPath,
|
|
1242
|
+
safelistCssPaths
|
|
1161
1243
|
};
|
|
1162
1244
|
}
|
|
1163
1245
|
//#endregion
|
|
@@ -1511,7 +1593,7 @@ const DEFAULT_RECIPES_CSS = {
|
|
|
1511
1593
|
"dialog.css": "/* shortwind: dialog@0.0.1 sha:000000 */\n\n/* @guide\n A modal is three layers: @dialog-overlay (dimmed backdrop), @dialog (the\n centering wrapper), and @dialog-content (the panel). Structure the panel with\n @dialog-header and @dialog-footer. Don't put content styling on @dialog\n itself — it's only the positioner.\n*/\n\n/* Modal dialog wrapper — covers the viewport, centers content. */\n@recipe dialog {\n fixed inset-0 z-50 flex items-center justify-center p-4\n}\n\n/* Dimmed overlay behind the dialog. */\n@recipe dialog-overlay {\n fixed inset-0 z-40 bg-black/50\n}\n\n/* Dialog content panel. */\n@recipe dialog-content {\n relative z-50 w-full max-w-md rounded-lg border border-border bg-popover text-popover-foreground p-6 shadow-xl\n}\n\n/* Dialog header region with title. */\n@recipe dialog-header {\n mb-4 flex flex-col gap-1\n}\n\n/* Dialog footer with right-aligned actions. */\n@recipe dialog-footer {\n mt-6 flex items-center justify-end gap-2\n}\n",
|
|
1512
1594
|
"empty.css": "/* shortwind: empty@0.0.1 sha:000000 */\n\n/* @guide\n @empty is the container for a no-data state; fill it with @empty-icon,\n @empty-title, and @empty-description. These are slots for that pattern — use\n @heading-* / @body from the text family for ordinary copy.\n*/\n\n/* Empty-state container. */\n@recipe empty {\n flex flex-col items-center justify-center gap-3 rounded-md border border-dashed border-border p-8 text-center\n}\n\n/* Empty-state icon slot. */\n@recipe empty-icon {\n flex h-12 w-12 items-center justify-center rounded-full bg-muted text-muted-foreground\n}\n\n/* Empty-state title text. */\n@recipe empty-title {\n text-base font-semibold text-foreground\n}\n\n/* Empty-state supporting description. */\n@recipe empty-description {\n max-w-sm text-sm text-muted-foreground\n}\n",
|
|
1513
1595
|
"feedback.css": "/* shortwind: feedback@0.0.1 sha:000000 */\n\n/* @guide\n Inline messages use @alert (neutral) or a tone variant @alert-success/\n warning/danger/info — one tone each. @callout is a left-accent inline note,\n @toast is a floating notification, @banner spans the full viewport width.\n*/\n\n/* Default informational alert. */\n@recipe alert {\n flex items-start gap-3 rounded-md border border-border bg-card p-4 text-sm text-card-foreground\n}\n\n/* Success alert. */\n@recipe alert-success {\n flex items-start gap-3 rounded-md border border-green-200 bg-green-50 p-4 text-sm text-green-900 dark:border-green-900 dark:bg-green-950 dark:text-green-100\n}\n\n/* Warning alert. */\n@recipe alert-warning {\n flex items-start gap-3 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100\n}\n\n/* Danger alert. */\n@recipe alert-danger {\n flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive\n}\n\n/* Informational alert. */\n@recipe alert-info {\n flex items-start gap-3 rounded-md border border-primary/30 bg-primary/10 p-4 text-sm text-primary\n}\n\n/* Inline callout — flush left edge accent. */\n@recipe callout {\n border-l-4 border-primary bg-primary/10 p-4 text-sm text-foreground\n}\n\n/* Floating toast notification. */\n@recipe toast {\n pointer-events-auto flex items-start gap-3 rounded-md border border-border bg-popover p-4 text-sm text-popover-foreground shadow-lg\n}\n\n/* Full-width banner spanning the viewport. */\n@recipe banner {\n w-full bg-primary px-4 py-2 text-center text-sm font-medium text-primary-foreground\n}\n",
|
|
1514
|
-
"form.css": "/* shortwind: form@0.0.
|
|
1596
|
+
"form.css": "/* shortwind: form@0.0.2 sha:000000 */\n\n/* @guide\n Wrap each label+control+message in @field (use @field-error for the invalid\n state); group related fields with @fieldset. Controls are bare: @input,\n @textarea, @select, @checkbox, @radio, plus @input-error for invalid text\n and @input-shell for the transparent input shell. Helper text is\n @help. There is no @form-group (use @field), @form-input (use @input),\n @form-helper (use @help) or @form-checkbox (use @checkbox); the field label\n recipe is @label, in the text family.\n*/\n\n/* Text input field. */\n@recipe input {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Dinachi-style input shell — transparent background, h-9, file/\n placeholder/selection/aria-invalid/focus-visible states baked in. */\n@recipe input-shell {\n flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40\n}\n\n/* Input in error state. */\n@recipe input-error {\n @input border-destructive focus:border-destructive focus:outline-destructive\n}\n\n/* Multi-line textarea. */\n@recipe textarea {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Native select control. */\n@recipe select {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Checkbox input. */\n@recipe checkbox {\n h-4 w-4 rounded border-input text-primary focus:outline-2 focus:outline-offset-2 focus:outline-ring\n}\n\n/* Radio input. */\n@recipe radio {\n h-4 w-4 border-input text-primary focus:outline-2 focus:outline-offset-2 focus:outline-ring\n}\n\n/* Form field wrapper — label + input + help/error. */\n@recipe field {\n flex flex-col gap-1.5\n}\n\n/* Form field in error state. */\n@recipe field-error {\n flex flex-col gap-1.5\n}\n\n/* Grouped form section with optional legend. */\n@recipe fieldset {\n flex flex-col gap-4 rounded-md border border-border p-4\n}\n\n/* Field-level helper text. */\n@recipe help {\n text-xs text-muted-foreground\n}\n",
|
|
1515
1597
|
"icon.css": "/* shortwind: icon@0.0.1 sha:000000 */\n\n/* @guide\n Size an icon with @icon-sm/md/lg (16/20/24px) — these set width and height\n only; add @icon-muted for secondary color. They're for SVG/icon elements, not\n to be confused with @btn-icon (the icon button in the button family).\n*/\n\n/* Small icon — 16px. */\n@recipe icon-sm {\n h-4 w-4 shrink-0\n}\n\n/* Default icon size — 20px. */\n@recipe icon-md {\n h-5 w-5 shrink-0\n}\n\n/* Large icon — 24px. */\n@recipe icon-lg {\n h-6 w-6 shrink-0\n}\n\n/* Icon with muted color. */\n@recipe icon-muted {\n text-muted-foreground\n}\n",
|
|
1516
1598
|
"layout.css": "/* shortwind: layout@0.0.1 sha:000000 */\n\n/* @guide\n Composition primitives. @stack-* stacks children vertically (flex-col);\n @row* lays them out horizontally (flex-row). Choose the gap with the size\n suffix (xs/sm/md/lg on stacks). Use @grid-2/3/4 only for true multi-column\n grids, @center to center on both axes, @full to fill the parent. Common\n slips: there is no @flex-row (use @row) or @flex-col (use a @stack-*), and\n the grids are @grid-3, not @grid-cols-3.\n*/\n\n/* Vertical stack with extra-small gap. */\n@recipe stack-xs {\n flex flex-col gap-1\n}\n\n/* Vertical stack with small gap. */\n@recipe stack-sm {\n flex flex-col gap-2\n}\n\n/* Vertical stack with medium gap. */\n@recipe stack-md {\n flex flex-col gap-4\n}\n\n/* Vertical stack with large gap. */\n@recipe stack-lg {\n flex flex-col gap-8\n}\n\n/* Horizontal row with default gap and centered items. */\n@recipe row {\n flex flex-row items-center gap-2\n}\n\n/* Horizontal row with space between children. */\n@recipe row-between {\n flex flex-row items-center justify-between gap-2\n}\n\n/* Horizontal row aligned to the end. */\n@recipe row-end {\n flex flex-row items-center justify-end gap-2\n}\n\n/* Two-column responsive grid. */\n@recipe grid-2 {\n grid grid-cols-1 gap-4 sm:grid-cols-2\n}\n\n/* Three-column responsive grid. */\n@recipe grid-3 {\n grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\n}\n\n/* Four-column responsive grid. */\n@recipe grid-4 {\n grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\n}\n\n/* Center content horizontally and vertically. */\n@recipe center {\n flex items-center justify-center\n}\n\n/* Fill the available width and height. */\n@recipe full {\n h-full w-full\n}\n",
|
|
1517
1599
|
"list.css": "/* shortwind: list@0.0.1 sha:000000 */\n\n/* @guide\n @list wraps a stack of @list-item rows; use @list-bordered for divided rows.\n Definition lists are separate: @dl with @dt (term) and @dd (description).\n For site navigation reach for the navigation family (@nav), not @list.\n*/\n\n/* Vertical list with default gap. */\n@recipe list {\n flex flex-col gap-1\n}\n\n/* Single list item. */\n@recipe list-item {\n flex items-center gap-2 rounded-md px-3 py-2 text-sm text-foreground\n}\n\n/* List with dividing borders between items. */\n@recipe list-bordered {\n divide-y divide-border rounded-md border border-border\n}\n\n/* Definition list container. */\n@recipe dl {\n grid grid-cols-1 gap-2 sm:grid-cols-3 sm:gap-4\n}\n\n/* Definition term. */\n@recipe dt {\n text-sm font-medium text-muted-foreground\n}\n\n/* Definition description. */\n@recipe dd {\n text-sm text-foreground sm:col-span-2\n}\n",
|
|
@@ -1519,9 +1601,9 @@ const DEFAULT_RECIPES_CSS = {
|
|
|
1519
1601
|
"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",
|
|
1520
1602
|
"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",
|
|
1521
1603
|
"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",
|
|
1522
|
-
"surface.css": "/* shortwind: surface@0.0.
|
|
1604
|
+
"surface.css": "/* shortwind: surface@0.0.2 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",
|
|
1523
1605
|
"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",
|
|
1524
|
-
"text.css": "/* shortwind: text@0.0.
|
|
1606
|
+
"text.css": "/* shortwind: text@0.0.2 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), @eyebrow (uppercase kicker above a\n heading). Use @label for form labels and @link for inline links. Don't\n append a -text suffix: it's @body not @body-text, @muted not @muted-text,\n @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/* Eyebrow — uppercase kicker above a heading. */\n@recipe eyebrow {\n font-mono text-xs font-medium uppercase tracking-[0.2em] 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",
|
|
1525
1607
|
"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"
|
|
1526
1608
|
};
|
|
1527
1609
|
//#endregion
|
|
@@ -2365,4 +2447,4 @@ function formatBenchTable(result) {
|
|
|
2365
2447
|
//#endregion
|
|
2366
2448
|
export { buildHeaderLine as A, cliVersion as C, createRegistrySource as D, writeLockfile as E, verifyFetchedFamily as F, extractHeader as M, rewriteHeaderSha as N, resolvePresetFamilies as O, sealRecipeFile as P, DEFAULT_REGISTRY as S, readLockfile as T, NewFamilyError as _, formatFindingsText as a, add as b, UpgradeError as c, BuildError as d, build as f, reseal as g, preset as h, extractClassUsages as i, computeBodySha as j, detectProject 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, newFamily as v, init as w, renameFamilyInSource as x, remove as y };
|
|
2367
2449
|
|
|
2368
|
-
//# sourceMappingURL=bench-
|
|
2450
|
+
//# sourceMappingURL=bench-BGO3pupw.js.map
|