@agjs/tsforge 0.1.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/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THEMED COMPONENT MATERIALIZATION — the harness holds tested primitive STRUCTURE
|
|
3
|
+
* and a set of VIBE themes; the model calls one tool (`scaffold_ui`) to get a
|
|
4
|
+
* coherent component set without authoring (or re-authoring) any of it. Two layers,
|
|
5
|
+
* both hardcoded:
|
|
6
|
+
*
|
|
7
|
+
* 1. tokens — a design-token block (`:root` palette + `--radius`) written into
|
|
8
|
+
* `index.css`. THE big lever: every primitive references semantic classes
|
|
9
|
+
* (`bg-card`, `rounded-md`), so swapping the token preset reskins the whole set
|
|
10
|
+
* coherently (minimal = neutral; warm = amber + round; futuristic = cool-neon +
|
|
11
|
+
* sharp). One block decides ~90% of the look.
|
|
12
|
+
* 2. deltas — optional per-component extra classes, merged via `cn(...)` into the
|
|
13
|
+
* primitive's root for finer per-vibe identity. STRUCTURE is invariant; only
|
|
14
|
+
* these classes vary, and most are empty (the tokens carry the vibe).
|
|
15
|
+
*
|
|
16
|
+
* The SDK philosophy applied to UI: tested structure + a parameterized surface. The
|
|
17
|
+
* model writes app logic, never plumbing. All primitives are dependency-free (plain
|
|
18
|
+
* HTML elements + cva), so a scaffolded project needs no extra Radix install and the
|
|
19
|
+
* set stays offline-testable. They land under `src/components/ui/` (eslint-ignored,
|
|
20
|
+
* vite tree-shakes unused ones from the bundle).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The vibes the model can request. */
|
|
24
|
+
export type ThemeName = "minimal" | "warm" | "futuristic";
|
|
25
|
+
|
|
26
|
+
/** The primitives `scaffold_ui` can materialize. */
|
|
27
|
+
export type ComponentName =
|
|
28
|
+
// primitives (atoms)
|
|
29
|
+
| "button"
|
|
30
|
+
| "card"
|
|
31
|
+
| "input"
|
|
32
|
+
| "label"
|
|
33
|
+
| "textarea"
|
|
34
|
+
| "select"
|
|
35
|
+
| "badge"
|
|
36
|
+
| "separator"
|
|
37
|
+
| "table"
|
|
38
|
+
// composition blocks (molecules) — the view chrome the model otherwise re-rolls
|
|
39
|
+
| "app-shell"
|
|
40
|
+
| "page-header"
|
|
41
|
+
| "field"
|
|
42
|
+
| "form-actions"
|
|
43
|
+
| "toolbar"
|
|
44
|
+
| "empty-state";
|
|
45
|
+
|
|
46
|
+
export const COMPONENT_NAMES: readonly ComponentName[] = [
|
|
47
|
+
"button",
|
|
48
|
+
"card",
|
|
49
|
+
"input",
|
|
50
|
+
"label",
|
|
51
|
+
"textarea",
|
|
52
|
+
"select",
|
|
53
|
+
"badge",
|
|
54
|
+
"separator",
|
|
55
|
+
"table",
|
|
56
|
+
"app-shell",
|
|
57
|
+
"page-header",
|
|
58
|
+
"field",
|
|
59
|
+
"form-actions",
|
|
60
|
+
"toolbar",
|
|
61
|
+
"empty-state",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export const THEME_NAMES: readonly ThemeName[] = [
|
|
65
|
+
"minimal",
|
|
66
|
+
"warm",
|
|
67
|
+
"futuristic",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export interface ITheme {
|
|
71
|
+
name: ThemeName;
|
|
72
|
+
/** The `index.css` token block (Tailwind import + `:root` + `@theme`). */
|
|
73
|
+
tokens: string;
|
|
74
|
+
/** Per-component extra classes merged into the root via `cn`. Missing ⇒ none. */
|
|
75
|
+
deltas: Partial<Record<ComponentName, string>>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── token presets (the vibe lever) ──────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function tokenBlock(vars: string): string {
|
|
81
|
+
return `@import "tailwindcss";
|
|
82
|
+
|
|
83
|
+
@custom-variant dark (&:is(.dark *));
|
|
84
|
+
|
|
85
|
+
:root {
|
|
86
|
+
${vars}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@theme inline {
|
|
90
|
+
--radius-sm: calc(var(--radius) - 2px);
|
|
91
|
+
--radius-md: var(--radius);
|
|
92
|
+
--radius-lg: calc(var(--radius) + 2px);
|
|
93
|
+
--color-background: var(--background);
|
|
94
|
+
--color-foreground: var(--foreground);
|
|
95
|
+
--color-card: var(--card);
|
|
96
|
+
--color-card-foreground: var(--card-foreground);
|
|
97
|
+
--color-primary: var(--primary);
|
|
98
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
99
|
+
--color-secondary: var(--secondary);
|
|
100
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
101
|
+
--color-muted: var(--muted);
|
|
102
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
103
|
+
--color-accent: var(--accent);
|
|
104
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
105
|
+
--color-destructive: var(--destructive);
|
|
106
|
+
--color-border: var(--border);
|
|
107
|
+
--color-input: var(--input);
|
|
108
|
+
--color-ring: var(--ring);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@layer base {
|
|
112
|
+
* {
|
|
113
|
+
@apply border-border;
|
|
114
|
+
}
|
|
115
|
+
body {
|
|
116
|
+
@apply bg-background text-foreground;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const VARS_MINIMAL = ` --radius: 0.5rem;
|
|
123
|
+
--background: oklch(1 0 0);
|
|
124
|
+
--foreground: oklch(0.145 0 0);
|
|
125
|
+
--card: oklch(1 0 0);
|
|
126
|
+
--card-foreground: oklch(0.145 0 0);
|
|
127
|
+
--primary: oklch(0.205 0 0);
|
|
128
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
129
|
+
--secondary: oklch(0.97 0 0);
|
|
130
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
131
|
+
--muted: oklch(0.97 0 0);
|
|
132
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
133
|
+
--accent: oklch(0.97 0 0);
|
|
134
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
135
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
136
|
+
--border: oklch(0.922 0 0);
|
|
137
|
+
--input: oklch(0.922 0 0);
|
|
138
|
+
--ring: oklch(0.708 0 0);`;
|
|
139
|
+
|
|
140
|
+
const VARS_WARM = ` --radius: 0.875rem;
|
|
141
|
+
--background: oklch(0.99 0.01 85);
|
|
142
|
+
--foreground: oklch(0.24 0.03 50);
|
|
143
|
+
--card: oklch(0.98 0.02 85);
|
|
144
|
+
--card-foreground: oklch(0.24 0.03 50);
|
|
145
|
+
--primary: oklch(0.66 0.16 50);
|
|
146
|
+
--primary-foreground: oklch(0.99 0.01 85);
|
|
147
|
+
--secondary: oklch(0.94 0.04 80);
|
|
148
|
+
--secondary-foreground: oklch(0.34 0.05 50);
|
|
149
|
+
--muted: oklch(0.95 0.03 85);
|
|
150
|
+
--muted-foreground: oklch(0.52 0.04 55);
|
|
151
|
+
--accent: oklch(0.9 0.07 75);
|
|
152
|
+
--accent-foreground: oklch(0.32 0.06 50);
|
|
153
|
+
--destructive: oklch(0.58 0.22 25);
|
|
154
|
+
--border: oklch(0.89 0.04 80);
|
|
155
|
+
--input: oklch(0.89 0.04 80);
|
|
156
|
+
--ring: oklch(0.66 0.16 50);`;
|
|
157
|
+
|
|
158
|
+
const VARS_FUTURISTIC = ` --radius: 0rem;
|
|
159
|
+
--background: oklch(0.16 0.02 265);
|
|
160
|
+
--foreground: oklch(0.96 0.01 260);
|
|
161
|
+
--card: oklch(0.21 0.03 265);
|
|
162
|
+
--card-foreground: oklch(0.96 0.01 260);
|
|
163
|
+
--primary: oklch(0.72 0.18 195);
|
|
164
|
+
--primary-foreground: oklch(0.16 0.02 265);
|
|
165
|
+
--secondary: oklch(0.27 0.03 265);
|
|
166
|
+
--secondary-foreground: oklch(0.96 0.01 260);
|
|
167
|
+
--muted: oklch(0.27 0.03 265);
|
|
168
|
+
--muted-foreground: oklch(0.72 0.02 260);
|
|
169
|
+
--accent: oklch(0.32 0.06 300);
|
|
170
|
+
--accent-foreground: oklch(0.96 0.01 260);
|
|
171
|
+
--destructive: oklch(0.62 0.24 18);
|
|
172
|
+
--border: oklch(0.4 0.05 265);
|
|
173
|
+
--input: oklch(0.3 0.04 265);
|
|
174
|
+
--ring: oklch(0.72 0.18 195);`;
|
|
175
|
+
|
|
176
|
+
export const THEMES: Record<ThemeName, ITheme> = {
|
|
177
|
+
minimal: { name: "minimal", tokens: tokenBlock(VARS_MINIMAL), deltas: {} },
|
|
178
|
+
warm: {
|
|
179
|
+
name: "warm",
|
|
180
|
+
tokens: tokenBlock(VARS_WARM),
|
|
181
|
+
deltas: { card: "shadow-sm", button: "shadow-sm" },
|
|
182
|
+
},
|
|
183
|
+
futuristic: {
|
|
184
|
+
name: "futuristic",
|
|
185
|
+
tokens: tokenBlock(VARS_FUTURISTIC),
|
|
186
|
+
deltas: {
|
|
187
|
+
button: "uppercase tracking-wide",
|
|
188
|
+
card: "backdrop-blur-sm",
|
|
189
|
+
input: "font-mono",
|
|
190
|
+
textarea: "font-mono",
|
|
191
|
+
select: "font-mono",
|
|
192
|
+
label: "uppercase tracking-wider text-xs",
|
|
193
|
+
badge: "uppercase tracking-wide",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ─── primitive STRUCTURE (invariant) — `$DELTA` becomes the theme's classes ─────
|
|
199
|
+
|
|
200
|
+
const PRIMITIVES: Record<ComponentName, string> = {
|
|
201
|
+
button: `import { cva, type VariantProps } from "class-variance-authority";
|
|
202
|
+
import { cn } from "@/lib/utils";
|
|
203
|
+
|
|
204
|
+
const buttonVariants = cva(
|
|
205
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 $DELTA",
|
|
206
|
+
{
|
|
207
|
+
variants: {
|
|
208
|
+
variant: {
|
|
209
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
210
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
211
|
+
destructive: "bg-destructive text-white hover:bg-destructive/90",
|
|
212
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
213
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
214
|
+
},
|
|
215
|
+
size: { default: "h-9 px-4 py-2", sm: "h-8 px-3", lg: "h-10 px-6", icon: "size-9" },
|
|
216
|
+
},
|
|
217
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
export interface IButtonProps
|
|
222
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
223
|
+
VariantProps<typeof buttonVariants> {}
|
|
224
|
+
|
|
225
|
+
export function Button({
|
|
226
|
+
className,
|
|
227
|
+
variant,
|
|
228
|
+
size,
|
|
229
|
+
...props
|
|
230
|
+
}: IButtonProps): React.JSX.Element {
|
|
231
|
+
return (
|
|
232
|
+
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
`,
|
|
236
|
+
card: `import { cn } from "@/lib/utils";
|
|
237
|
+
|
|
238
|
+
export function Card({
|
|
239
|
+
className,
|
|
240
|
+
...props
|
|
241
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
className={cn(
|
|
245
|
+
"rounded-lg border bg-card text-card-foreground p-6 $DELTA",
|
|
246
|
+
className
|
|
247
|
+
)}
|
|
248
|
+
{...props}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
`,
|
|
253
|
+
input: `import { cn } from "@/lib/utils";
|
|
254
|
+
|
|
255
|
+
export function Input({
|
|
256
|
+
className,
|
|
257
|
+
...props
|
|
258
|
+
}: React.InputHTMLAttributes<HTMLInputElement>): React.JSX.Element {
|
|
259
|
+
return (
|
|
260
|
+
<input
|
|
261
|
+
className={cn(
|
|
262
|
+
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 $DELTA",
|
|
263
|
+
className
|
|
264
|
+
)}
|
|
265
|
+
{...props}
|
|
266
|
+
/>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
`,
|
|
270
|
+
label: `import { cn } from "@/lib/utils";
|
|
271
|
+
|
|
272
|
+
export function Label({
|
|
273
|
+
className,
|
|
274
|
+
...props
|
|
275
|
+
}: React.LabelHTMLAttributes<HTMLLabelElement>): React.JSX.Element {
|
|
276
|
+
return (
|
|
277
|
+
<label
|
|
278
|
+
className={cn(
|
|
279
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 $DELTA",
|
|
280
|
+
className
|
|
281
|
+
)}
|
|
282
|
+
{...props}
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
`,
|
|
287
|
+
textarea: `import { cn } from "@/lib/utils";
|
|
288
|
+
|
|
289
|
+
export function Textarea({
|
|
290
|
+
className,
|
|
291
|
+
...props
|
|
292
|
+
}: React.TextareaHTMLAttributes<HTMLTextAreaElement>): React.JSX.Element {
|
|
293
|
+
return (
|
|
294
|
+
<textarea
|
|
295
|
+
className={cn(
|
|
296
|
+
"flex min-h-16 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 $DELTA",
|
|
297
|
+
className
|
|
298
|
+
)}
|
|
299
|
+
{...props}
|
|
300
|
+
/>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
`,
|
|
304
|
+
select: `import { cn } from "@/lib/utils";
|
|
305
|
+
|
|
306
|
+
export function Select({
|
|
307
|
+
className,
|
|
308
|
+
...props
|
|
309
|
+
}: React.SelectHTMLAttributes<HTMLSelectElement>): React.JSX.Element {
|
|
310
|
+
return (
|
|
311
|
+
<select
|
|
312
|
+
className={cn(
|
|
313
|
+
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 $DELTA",
|
|
314
|
+
className
|
|
315
|
+
)}
|
|
316
|
+
{...props}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
`,
|
|
321
|
+
badge: `import { cva, type VariantProps } from "class-variance-authority";
|
|
322
|
+
import { cn } from "@/lib/utils";
|
|
323
|
+
|
|
324
|
+
const badgeVariants = cva(
|
|
325
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-medium $DELTA",
|
|
326
|
+
{
|
|
327
|
+
variants: {
|
|
328
|
+
variant: {
|
|
329
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
330
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
331
|
+
outline: "text-foreground",
|
|
332
|
+
destructive: "border-transparent bg-destructive text-white",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
defaultVariants: { variant: "default" },
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
export interface IBadgeProps
|
|
340
|
+
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
341
|
+
VariantProps<typeof badgeVariants> {}
|
|
342
|
+
|
|
343
|
+
export function Badge({
|
|
344
|
+
className,
|
|
345
|
+
variant,
|
|
346
|
+
...props
|
|
347
|
+
}: IBadgeProps): React.JSX.Element {
|
|
348
|
+
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
349
|
+
}
|
|
350
|
+
`,
|
|
351
|
+
separator: `import { cn } from "@/lib/utils";
|
|
352
|
+
|
|
353
|
+
export function Separator({
|
|
354
|
+
className,
|
|
355
|
+
...props
|
|
356
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
|
|
357
|
+
return (
|
|
358
|
+
<div
|
|
359
|
+
role="separator"
|
|
360
|
+
className={cn("shrink-0 bg-border h-px w-full $DELTA", className)}
|
|
361
|
+
{...props}
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
`,
|
|
366
|
+
table: `import { cn } from "@/lib/utils";
|
|
367
|
+
|
|
368
|
+
export function Table({
|
|
369
|
+
className,
|
|
370
|
+
...props
|
|
371
|
+
}: React.TableHTMLAttributes<HTMLTableElement>): React.JSX.Element {
|
|
372
|
+
return (
|
|
373
|
+
<div className="relative w-full overflow-auto $DELTA">
|
|
374
|
+
<table className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function TableHeader({
|
|
380
|
+
className,
|
|
381
|
+
...props
|
|
382
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
|
|
383
|
+
return <thead className={cn("[&_tr]:border-b", className)} {...props} />;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function TableBody({
|
|
387
|
+
className,
|
|
388
|
+
...props
|
|
389
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
|
|
390
|
+
return <tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function TableRow({
|
|
394
|
+
className,
|
|
395
|
+
...props
|
|
396
|
+
}: React.HTMLAttributes<HTMLTableRowElement>): React.JSX.Element {
|
|
397
|
+
return (
|
|
398
|
+
<tr
|
|
399
|
+
className={cn("border-b transition-colors hover:bg-muted/50", className)}
|
|
400
|
+
{...props}
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function TableHead({
|
|
406
|
+
className,
|
|
407
|
+
...props
|
|
408
|
+
}: React.ThHTMLAttributes<HTMLTableCellElement>): React.JSX.Element {
|
|
409
|
+
return (
|
|
410
|
+
<th
|
|
411
|
+
className={cn(
|
|
412
|
+
"h-10 px-2 text-left align-middle font-medium text-muted-foreground",
|
|
413
|
+
className
|
|
414
|
+
)}
|
|
415
|
+
{...props}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function TableCell({
|
|
421
|
+
className,
|
|
422
|
+
...props
|
|
423
|
+
}: React.TdHTMLAttributes<HTMLTableCellElement>): React.JSX.Element {
|
|
424
|
+
return <td className={cn("p-2 align-middle", className)} {...props} />;
|
|
425
|
+
}
|
|
426
|
+
`,
|
|
427
|
+
// ─── composition blocks (molecules) ────────────────────────────────────────
|
|
428
|
+
"app-shell": `import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
|
429
|
+
import { cn } from "@/lib/utils";
|
|
430
|
+
|
|
431
|
+
export interface INavItem {
|
|
432
|
+
to: string;
|
|
433
|
+
label: string;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface IAppShellProps {
|
|
437
|
+
title: string;
|
|
438
|
+
nav: readonly INavItem[];
|
|
439
|
+
children?: React.ReactNode;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function AppShell({
|
|
443
|
+
title,
|
|
444
|
+
nav,
|
|
445
|
+
children,
|
|
446
|
+
}: IAppShellProps): React.JSX.Element {
|
|
447
|
+
const { pathname } = useLocation();
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<div className={cn("flex min-h-screen bg-background $DELTA")}>
|
|
451
|
+
<aside className="w-56 shrink-0 border-r border-border bg-card p-4">
|
|
452
|
+
<h1 className="mb-6 text-xl font-bold text-foreground">{title}</h1>
|
|
453
|
+
<nav className="flex flex-col gap-1">
|
|
454
|
+
{nav.map((item) => {
|
|
455
|
+
const active =
|
|
456
|
+
pathname === item.to ||
|
|
457
|
+
(item.to !== "/" && pathname.startsWith(\`\${item.to}/\`));
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<Link
|
|
461
|
+
key={item.to}
|
|
462
|
+
to={item.to}
|
|
463
|
+
className={cn(
|
|
464
|
+
"rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
465
|
+
active
|
|
466
|
+
? "bg-primary text-primary-foreground"
|
|
467
|
+
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
468
|
+
)}
|
|
469
|
+
>
|
|
470
|
+
{item.label}
|
|
471
|
+
</Link>
|
|
472
|
+
);
|
|
473
|
+
})}
|
|
474
|
+
</nav>
|
|
475
|
+
</aside>
|
|
476
|
+
<main className="flex-1 p-6">{children ?? <Outlet />}</main>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
`,
|
|
481
|
+
"page-header": `import { cn } from "@/lib/utils";
|
|
482
|
+
|
|
483
|
+
export interface IPageHeaderProps {
|
|
484
|
+
title: string;
|
|
485
|
+
description?: string;
|
|
486
|
+
children?: React.ReactNode;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function PageHeader({
|
|
490
|
+
title,
|
|
491
|
+
description,
|
|
492
|
+
children,
|
|
493
|
+
}: IPageHeaderProps): React.JSX.Element {
|
|
494
|
+
return (
|
|
495
|
+
<div className={cn("mb-6 flex items-center justify-between gap-4 $DELTA")}>
|
|
496
|
+
<div>
|
|
497
|
+
<h1 className="text-2xl font-semibold text-foreground">{title}</h1>
|
|
498
|
+
{description !== undefined && (
|
|
499
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
{children !== undefined && (
|
|
503
|
+
<div className="flex items-center gap-2">{children}</div>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
`,
|
|
509
|
+
field: `import { cn } from "@/lib/utils";
|
|
510
|
+
import { Label } from "@/components/ui/label";
|
|
511
|
+
|
|
512
|
+
export interface IFieldProps {
|
|
513
|
+
label: string;
|
|
514
|
+
htmlFor?: string;
|
|
515
|
+
error?: string;
|
|
516
|
+
children: React.ReactNode;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function Field({
|
|
520
|
+
label,
|
|
521
|
+
htmlFor,
|
|
522
|
+
error,
|
|
523
|
+
children,
|
|
524
|
+
}: IFieldProps): React.JSX.Element {
|
|
525
|
+
return (
|
|
526
|
+
<div className={cn("flex flex-col gap-1.5 $DELTA")}>
|
|
527
|
+
<Label htmlFor={htmlFor}>{label}</Label>
|
|
528
|
+
{children}
|
|
529
|
+
{error !== undefined && (
|
|
530
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
`,
|
|
536
|
+
"form-actions": `import { cn } from "@/lib/utils";
|
|
537
|
+
|
|
538
|
+
export function FormActions({
|
|
539
|
+
className,
|
|
540
|
+
...props
|
|
541
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
|
|
542
|
+
return (
|
|
543
|
+
<div
|
|
544
|
+
className={cn("flex items-center justify-end gap-2 pt-2 $DELTA", className)}
|
|
545
|
+
{...props}
|
|
546
|
+
/>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
`,
|
|
550
|
+
toolbar: `import { cn } from "@/lib/utils";
|
|
551
|
+
|
|
552
|
+
export function Toolbar({
|
|
553
|
+
className,
|
|
554
|
+
...props
|
|
555
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
|
|
556
|
+
return (
|
|
557
|
+
<div
|
|
558
|
+
className={cn(
|
|
559
|
+
"mb-4 flex flex-wrap items-center gap-2 $DELTA",
|
|
560
|
+
className
|
|
561
|
+
)}
|
|
562
|
+
{...props}
|
|
563
|
+
/>
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
`,
|
|
567
|
+
"empty-state": `import { cn } from "@/lib/utils";
|
|
568
|
+
|
|
569
|
+
export interface IEmptyStateProps {
|
|
570
|
+
title: string;
|
|
571
|
+
description?: string;
|
|
572
|
+
children?: React.ReactNode;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function EmptyState({
|
|
576
|
+
title,
|
|
577
|
+
description,
|
|
578
|
+
children,
|
|
579
|
+
}: IEmptyStateProps): React.JSX.Element {
|
|
580
|
+
return (
|
|
581
|
+
<div
|
|
582
|
+
className={cn(
|
|
583
|
+
"flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border p-12 text-center $DELTA"
|
|
584
|
+
)}
|
|
585
|
+
>
|
|
586
|
+
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
587
|
+
{description !== undefined && (
|
|
588
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
589
|
+
)}
|
|
590
|
+
{children}
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
`,
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
/** The relative path a primitive is written to (the shadcn convention). */
|
|
598
|
+
export function componentPath(name: ComponentName): string {
|
|
599
|
+
return `src/components/ui/${name}.tsx`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Parse/validate a requested theme; undefined if not a known vibe. */
|
|
603
|
+
export function asThemeName(value: unknown): ThemeName | undefined {
|
|
604
|
+
return THEME_NAMES.find((t) => t === value);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Keep only the valid, de-duplicated component names from a requested list. */
|
|
608
|
+
export function asComponentNames(value: unknown): ComponentName[] {
|
|
609
|
+
if (!Array.isArray(value)) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return COMPONENT_NAMES.filter((c) => value.includes(c));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Materialize the requested primitives under a theme: a path→content map the
|
|
618
|
+
* scaffolder writes. Always includes `src/index.css` (the theme's token block) plus
|
|
619
|
+
* one file per component, with the theme's per-component delta baked into the
|
|
620
|
+
* structure (an empty/missing delta collapses cleanly — no stray double space).
|
|
621
|
+
*/
|
|
622
|
+
export function materializeComponents(
|
|
623
|
+
theme: ThemeName,
|
|
624
|
+
components: readonly ComponentName[]
|
|
625
|
+
): Record<string, string> {
|
|
626
|
+
const t = THEMES[theme];
|
|
627
|
+
const out: Record<string, string> = { "src/index.css": t.tokens };
|
|
628
|
+
|
|
629
|
+
for (const name of components) {
|
|
630
|
+
const delta = t.deltas[name] ?? "";
|
|
631
|
+
|
|
632
|
+
out[componentPath(name)] = PRIMITIVES[name]
|
|
633
|
+
.replace(" $DELTA", delta.length > 0 ? ` ${delta}` : "")
|
|
634
|
+
.replace("$DELTA", delta);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return out;
|
|
638
|
+
}
|