voidforge-build 23.11.4 → 23.12.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/dist/.claude/agents/batman-qa.md +1 -0
- package/dist/.claude/agents/galadriel-frontend.md +2 -0
- package/dist/.claude/agents/kusanagi-devops.md +4 -0
- package/dist/.claude/agents/lucius-config.md +6 -0
- package/dist/.claude/agents/samwise-accessibility.md +4 -0
- package/dist/.claude/agents/silver-surfer-herald.md +13 -4
- package/dist/.claude/commands/architect.md +9 -0
- package/dist/.claude/commands/assemble.md +4 -1
- package/dist/.claude/commands/assess.md +13 -1
- package/dist/.claude/commands/audit-docs.md +106 -0
- package/dist/.claude/commands/deploy.md +29 -1
- package/dist/.claude/commands/engage.md +19 -1
- package/dist/.claude/commands/gauntlet.md +23 -4
- package/dist/.claude/commands/imagine.md +15 -0
- package/dist/.claude/commands/sentinel.md +15 -0
- package/dist/.claude/commands/ux.md +36 -0
- package/dist/.claude/commands/void.md +1 -0
- package/dist/CHANGELOG.md +65 -0
- package/dist/CLAUDE.md +9 -0
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/AI_INTELLIGENCE.md +33 -0
- package/dist/docs/methods/ASSEMBLER.md +31 -2
- package/dist/docs/methods/BUILD_PROTOCOL.md +2 -0
- package/dist/docs/methods/CAMPAIGN.md +46 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +194 -0
- package/dist/docs/methods/DOC_AUDIT.md +92 -0
- package/dist/docs/methods/FORGE_KEEPER.md +16 -5
- package/dist/docs/methods/GAUNTLET.md +38 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +57 -0
- package/dist/docs/methods/QA_ENGINEER.md +21 -0
- package/dist/docs/methods/RELEASE_MANAGER.md +27 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +12 -1
- package/dist/docs/methods/SUB_AGENTS.md +54 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +13 -0
- package/dist/docs/methods/TESTING.md +19 -0
- package/dist/docs/patterns/README.md +3 -0
- package/dist/docs/patterns/ai-eval.ts +63 -0
- package/dist/docs/patterns/daemon-process.ts +90 -0
- package/dist/docs/patterns/database-migration.ts +65 -0
- package/dist/docs/patterns/deploy-preflight.ts +85 -2
- package/dist/docs/patterns/design-tokens.ts +338 -0
- package/dist/docs/patterns/error-message-categorization.tsx +376 -0
- package/dist/wizard/lib/patterns/daemon-process.d.ts +2 -1
- package/dist/wizard/lib/patterns/daemon-process.js +89 -1
- package/package.json +2 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Semantic Design Tokens (primitive -> semantic -> CSS custom properties)
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - NO raw color or type values in components. Components reference SEMANTIC
|
|
6
|
+
* tokens only (e.g., `surface.raised`, `text.muted`, `font.size.body`) —
|
|
7
|
+
* never primitives (e.g., `#4f46e5`, `gray-200`, `16px`).
|
|
8
|
+
* - Two layers: PRIMITIVES (the raw palette/scale — every hex, every step of
|
|
9
|
+
* the type scale) and SEMANTIC roles (what a thing MEANS — `accent`,
|
|
10
|
+
* `danger`, `border.subtle`). Semantic roles map TO primitives.
|
|
11
|
+
* - Tokens are emitted as CSS custom properties (`--vf-color-accent`) so a
|
|
12
|
+
* theme override is a `:root[data-theme=...]` block — not a recompile.
|
|
13
|
+
* - A palette or type pivot becomes a TOKEN edit (re-point semantic roles at
|
|
14
|
+
* different primitives), not a component-by-component rewrite.
|
|
15
|
+
*
|
|
16
|
+
* The trap (field report #351, #3): a rebrand asked for a new accent color and
|
|
17
|
+
* a tighter type scale. Components had hardcoded `#4f46e5` and `text-[15px]`
|
|
18
|
+
* inline in 60+ files. The "one color change" turned into a 60-file grep-and-
|
|
19
|
+
* replace that missed five spots (shipped a two-tone accent for a week) and
|
|
20
|
+
* couldn't support a dark theme at all because the values had no indirection.
|
|
21
|
+
* Scoping all color/type to semantic tokens makes the pivot a single edit.
|
|
22
|
+
*
|
|
23
|
+
* Agents: Galadriel (design system), Legolas (code), Samwise (a11y contrast)
|
|
24
|
+
*
|
|
25
|
+
* Framework adaptations:
|
|
26
|
+
* React/Next.js: This file — emit CSS vars into a <style> at the root, read
|
|
27
|
+
* them via Tailwind theme extension or plain `var(--vf-...)`.
|
|
28
|
+
* Tailwind: map semantic tokens into `theme.extend.colors` /
|
|
29
|
+
* `theme.extend.fontSize` as `var(--vf-...)` so `bg-accent` resolves to the
|
|
30
|
+
* token (see the Tailwind block at the bottom).
|
|
31
|
+
* Vue/Svelte: same CSS-var output; bind `data-theme` on a root element.
|
|
32
|
+
* Django/Rails templates: emit the `:root` block from `tokensToCss()` into a
|
|
33
|
+
* server-rendered <style> tag in the base layout; components use `var(...)`.
|
|
34
|
+
*
|
|
35
|
+
* Pairs with /docs/patterns/component.tsx (components consume tokens, never
|
|
36
|
+
* raw values) and /docs/patterns/combobox.tsx (a11y-critical surfaces).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// ── Layer 1: Primitives (the raw palette + scales) ───────────────────────────
|
|
40
|
+
// These are the ONLY place raw values live. Name them by what they ARE
|
|
41
|
+
// (a swatch index, a scale step), never by what they're FOR. A primitive
|
|
42
|
+
// never appears directly in a component.
|
|
43
|
+
|
|
44
|
+
export const primitives = {
|
|
45
|
+
color: {
|
|
46
|
+
// Neutral ramp
|
|
47
|
+
white: '#ffffff',
|
|
48
|
+
black: '#0a0a0a',
|
|
49
|
+
gray50: '#f9fafb',
|
|
50
|
+
gray100: '#f3f4f6',
|
|
51
|
+
gray200: '#e5e7eb',
|
|
52
|
+
gray500: '#6b7280',
|
|
53
|
+
gray700: '#374151',
|
|
54
|
+
gray900: '#111827',
|
|
55
|
+
// Brand ramp
|
|
56
|
+
indigo500: '#6366f1',
|
|
57
|
+
indigo600: '#4f46e5',
|
|
58
|
+
indigo700: '#4338ca',
|
|
59
|
+
// Status ramps
|
|
60
|
+
red500: '#ef4444',
|
|
61
|
+
red600: '#dc2626',
|
|
62
|
+
green500: '#22c55e',
|
|
63
|
+
amber500: '#f59e0b',
|
|
64
|
+
},
|
|
65
|
+
// Type scale — a modular scale, not arbitrary pixels.
|
|
66
|
+
fontSize: {
|
|
67
|
+
xs: '0.75rem',
|
|
68
|
+
sm: '0.875rem',
|
|
69
|
+
base: '1rem',
|
|
70
|
+
lg: '1.125rem',
|
|
71
|
+
xl: '1.5rem',
|
|
72
|
+
'2xl': '2rem',
|
|
73
|
+
},
|
|
74
|
+
fontWeight: {
|
|
75
|
+
regular: '400',
|
|
76
|
+
medium: '500',
|
|
77
|
+
semibold: '600',
|
|
78
|
+
bold: '700',
|
|
79
|
+
},
|
|
80
|
+
lineHeight: {
|
|
81
|
+
tight: '1.2',
|
|
82
|
+
normal: '1.5',
|
|
83
|
+
relaxed: '1.7',
|
|
84
|
+
},
|
|
85
|
+
fontFamily: {
|
|
86
|
+
sans: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
87
|
+
mono: 'ui-monospace, "SF Mono", "Cascadia Code", monospace',
|
|
88
|
+
},
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
type PrimitiveColor = keyof typeof primitives.color;
|
|
92
|
+
type PrimitiveFontSize = keyof typeof primitives.fontSize;
|
|
93
|
+
type PrimitiveFontWeight = keyof typeof primitives.fontWeight;
|
|
94
|
+
type PrimitiveLineHeight = keyof typeof primitives.lineHeight;
|
|
95
|
+
type PrimitiveFontFamily = keyof typeof primitives.fontFamily;
|
|
96
|
+
|
|
97
|
+
// ── Layer 2: Semantic tokens (what a thing MEANS) ────────────────────────────
|
|
98
|
+
// A semantic token is a NAME FOR A ROLE that points at a primitive. Components
|
|
99
|
+
// reference these. Re-point them to pivot the whole product. The shape is the
|
|
100
|
+
// contract — a theme override must provide every role, so the type system
|
|
101
|
+
// guarantees no role is left un-themed.
|
|
102
|
+
|
|
103
|
+
export type SemanticTokens = {
|
|
104
|
+
color: {
|
|
105
|
+
'bg.canvas': PrimitiveColor; // page background
|
|
106
|
+
'bg.raised': PrimitiveColor; // cards, popovers
|
|
107
|
+
'text.default': PrimitiveColor;
|
|
108
|
+
'text.muted': PrimitiveColor;
|
|
109
|
+
'text.on-accent': PrimitiveColor; // text placed on top of `accent`
|
|
110
|
+
'border.subtle': PrimitiveColor;
|
|
111
|
+
accent: PrimitiveColor; // primary action / brand
|
|
112
|
+
'accent.hover': PrimitiveColor;
|
|
113
|
+
danger: PrimitiveColor;
|
|
114
|
+
success: PrimitiveColor;
|
|
115
|
+
warning: PrimitiveColor;
|
|
116
|
+
};
|
|
117
|
+
type: {
|
|
118
|
+
'size.caption': PrimitiveFontSize;
|
|
119
|
+
'size.body': PrimitiveFontSize;
|
|
120
|
+
'size.heading': PrimitiveFontSize;
|
|
121
|
+
'size.display': PrimitiveFontSize;
|
|
122
|
+
'weight.body': PrimitiveFontWeight;
|
|
123
|
+
'weight.emphasis': PrimitiveFontWeight;
|
|
124
|
+
'leading.body': PrimitiveLineHeight;
|
|
125
|
+
'leading.heading': PrimitiveLineHeight;
|
|
126
|
+
'family.ui': PrimitiveFontFamily;
|
|
127
|
+
'family.code': PrimitiveFontFamily;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ── Worked example, step 1: define the default (light) theme ─────────────────
|
|
132
|
+
// Every semantic role points at a primitive. This is the whole brand surface —
|
|
133
|
+
// change these mappings and the product re-skins. Note: no hex here, only
|
|
134
|
+
// references into `primitives`.
|
|
135
|
+
|
|
136
|
+
export const lightTheme: SemanticTokens = {
|
|
137
|
+
color: {
|
|
138
|
+
'bg.canvas': 'white',
|
|
139
|
+
'bg.raised': 'gray50',
|
|
140
|
+
'text.default': 'gray900',
|
|
141
|
+
'text.muted': 'gray500',
|
|
142
|
+
'text.on-accent': 'white',
|
|
143
|
+
'border.subtle': 'gray200',
|
|
144
|
+
accent: 'indigo600',
|
|
145
|
+
'accent.hover': 'indigo700',
|
|
146
|
+
danger: 'red600',
|
|
147
|
+
success: 'green500',
|
|
148
|
+
warning: 'amber500',
|
|
149
|
+
},
|
|
150
|
+
type: {
|
|
151
|
+
'size.caption': 'sm',
|
|
152
|
+
'size.body': 'base',
|
|
153
|
+
'size.heading': 'xl',
|
|
154
|
+
'size.display': '2xl',
|
|
155
|
+
'weight.body': 'regular',
|
|
156
|
+
'weight.emphasis': 'semibold',
|
|
157
|
+
'leading.body': 'normal',
|
|
158
|
+
'leading.heading': 'tight',
|
|
159
|
+
'family.ui': 'sans',
|
|
160
|
+
'family.code': 'mono',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// ── Worked example, step 2: a theme override (dark) ──────────────────────────
|
|
165
|
+
// Because color roles are indirection, a dark theme is just a different
|
|
166
|
+
// primitive mapping. The type scale is unchanged here — override only what
|
|
167
|
+
// differs. A full rebrand would supply a new `primitives.color` ramp AND a new
|
|
168
|
+
// mapping; both still live in tokens, never in components.
|
|
169
|
+
|
|
170
|
+
export const darkTheme: SemanticTokens = {
|
|
171
|
+
color: {
|
|
172
|
+
'bg.canvas': 'black',
|
|
173
|
+
'bg.raised': 'gray900',
|
|
174
|
+
'text.default': 'gray50',
|
|
175
|
+
'text.muted': 'gray500',
|
|
176
|
+
'text.on-accent': 'white',
|
|
177
|
+
'border.subtle': 'gray700',
|
|
178
|
+
accent: 'indigo500', // lighter accent reads better on dark canvas
|
|
179
|
+
'accent.hover': 'indigo600',
|
|
180
|
+
danger: 'red500',
|
|
181
|
+
success: 'green500',
|
|
182
|
+
warning: 'amber500',
|
|
183
|
+
},
|
|
184
|
+
type: lightTheme.type, // type scale is theme-invariant in this example
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ── Emit: semantic tokens -> CSS custom properties ───────────────────────────
|
|
188
|
+
// `--vf-color-<role>` and `--vf-type-<role>`, with `.` and braces flattened to
|
|
189
|
+
// `-`. The variable VALUE is the resolved primitive, so consumers never touch
|
|
190
|
+
// primitives directly. Emit one block per theme keyed by `data-theme`.
|
|
191
|
+
|
|
192
|
+
const CSS_VAR_PREFIX = '--vf';
|
|
193
|
+
|
|
194
|
+
function cssVarName(group: 'color' | 'type', role: string): string {
|
|
195
|
+
// 'accent.hover' -> '--vf-color-accent-hover'
|
|
196
|
+
const safeRole = role.replace(/[.\s]+/g, '-');
|
|
197
|
+
return `${CSS_VAR_PREFIX}-${group}-${safeRole}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveColor(role: PrimitiveColor): string {
|
|
201
|
+
return primitives.color[role];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveType(
|
|
205
|
+
tokenKey: keyof SemanticTokens['type'],
|
|
206
|
+
ref: string,
|
|
207
|
+
): string {
|
|
208
|
+
if (tokenKey.startsWith('size.')) return primitives.fontSize[ref as PrimitiveFontSize];
|
|
209
|
+
if (tokenKey.startsWith('weight.')) return primitives.fontWeight[ref as PrimitiveFontWeight];
|
|
210
|
+
if (tokenKey.startsWith('leading.')) return primitives.lineHeight[ref as PrimitiveLineHeight];
|
|
211
|
+
if (tokenKey.startsWith('family.')) return primitives.fontFamily[ref as PrimitiveFontFamily];
|
|
212
|
+
throw new Error(`Unknown type token group: ${tokenKey}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Build the `--vf-*` declarations for one theme as `key: value` lines. */
|
|
216
|
+
export function themeToDeclarations(tokens: SemanticTokens): Record<string, string> {
|
|
217
|
+
const out: Record<string, string> = {};
|
|
218
|
+
for (const [role, ref] of Object.entries(tokens.color)) {
|
|
219
|
+
out[cssVarName('color', role)] = resolveColor(ref as PrimitiveColor);
|
|
220
|
+
}
|
|
221
|
+
for (const [role, ref] of Object.entries(tokens.type)) {
|
|
222
|
+
out[cssVarName('type', role)] = resolveType(
|
|
223
|
+
role as keyof SemanticTokens['type'],
|
|
224
|
+
ref as string,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render every theme into a single CSS string: `:root` carries the default
|
|
232
|
+
* theme, `[data-theme="<name>"]` carries each override. Drop this into a
|
|
233
|
+
* <style> tag (React: dangerouslySetInnerHTML; templates: server-render).
|
|
234
|
+
*/
|
|
235
|
+
export function tokensToCss(themes: {
|
|
236
|
+
default: SemanticTokens;
|
|
237
|
+
overrides?: Record<string, SemanticTokens>;
|
|
238
|
+
}): string {
|
|
239
|
+
const block = (selector: string, decls: Record<string, string>): string => {
|
|
240
|
+
const body = Object.entries(decls)
|
|
241
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
242
|
+
.join('\n');
|
|
243
|
+
return `${selector} {\n${body}\n}`;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const parts = [block(':root', themeToDeclarations(themes.default))];
|
|
247
|
+
for (const [name, theme] of Object.entries(themes.overrides ?? {})) {
|
|
248
|
+
parts.push(block(`[data-theme="${name}"]`, themeToDeclarations(theme)));
|
|
249
|
+
}
|
|
250
|
+
return parts.join('\n\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Worked example, step 3: consume tokens in a component ─────────────────────
|
|
254
|
+
// The component references SEMANTIC tokens via `var(--vf-...)` — never a hex,
|
|
255
|
+
// never a pixel literal. Swapping `data-theme` on any ancestor re-themes it
|
|
256
|
+
// with zero component changes. This is the payoff: the pivot lives in tokens.
|
|
257
|
+
|
|
258
|
+
import type { CSSProperties, PropsWithChildren } from 'react';
|
|
259
|
+
|
|
260
|
+
const tokenColor = (role: keyof SemanticTokens['color']): string =>
|
|
261
|
+
`var(${cssVarName('color', role)})`;
|
|
262
|
+
const tokenType = (role: keyof SemanticTokens['type']): string =>
|
|
263
|
+
`var(${cssVarName('type', role)})`;
|
|
264
|
+
|
|
265
|
+
export function CalloutCard({ children }: PropsWithChildren): JSX.Element {
|
|
266
|
+
const style: CSSProperties = {
|
|
267
|
+
background: tokenColor('bg.raised'),
|
|
268
|
+
color: tokenColor('text.default'),
|
|
269
|
+
border: `1px solid ${tokenColor('border.subtle')}`,
|
|
270
|
+
borderRadius: 8,
|
|
271
|
+
padding: '1rem 1.25rem',
|
|
272
|
+
fontFamily: tokenType('family.ui'),
|
|
273
|
+
fontSize: tokenType('size.body'),
|
|
274
|
+
lineHeight: tokenType('leading.body'),
|
|
275
|
+
};
|
|
276
|
+
const ctaStyle: CSSProperties = {
|
|
277
|
+
background: tokenColor('accent'),
|
|
278
|
+
color: tokenColor('text.on-accent'),
|
|
279
|
+
fontWeight: tokenType('weight.emphasis'),
|
|
280
|
+
border: 'none',
|
|
281
|
+
borderRadius: 6,
|
|
282
|
+
padding: '0.5rem 0.875rem',
|
|
283
|
+
marginTop: '0.75rem',
|
|
284
|
+
cursor: 'pointer',
|
|
285
|
+
};
|
|
286
|
+
return (
|
|
287
|
+
<section style={style}>
|
|
288
|
+
{children}
|
|
289
|
+
{/* No #hex, no 15px — only token references. A rebrand never touches this file. */}
|
|
290
|
+
<button type="button" style={ctaStyle}>
|
|
291
|
+
Continue
|
|
292
|
+
</button>
|
|
293
|
+
</section>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// To mount the themes once at the app root (Next.js: in a Server Component
|
|
298
|
+
// layout, or a root <style> in app/layout.tsx):
|
|
299
|
+
//
|
|
300
|
+
// const css = tokensToCss({ default: lightTheme, overrides: { dark: darkTheme } });
|
|
301
|
+
// // <style dangerouslySetInnerHTML={{ __html: css }} />
|
|
302
|
+
// // then: <html data-theme={prefersDark ? 'dark' : undefined}> ... </html>
|
|
303
|
+
//
|
|
304
|
+
// produces, e.g.:
|
|
305
|
+
// :root { --vf-color-accent: #4f46e5; --vf-type-size-body: 1rem; ... }
|
|
306
|
+
// [data-theme="dark"] { --vf-color-accent: #6366f1; ... }
|
|
307
|
+
|
|
308
|
+
// ── Tailwind adaptation ──────────────────────────────────────────────────────
|
|
309
|
+
// Map semantic tokens into the Tailwind theme as `var(--vf-...)`, so utility
|
|
310
|
+
// classes (`bg-accent`, `text-muted`, `text-body`) resolve to tokens and a
|
|
311
|
+
// theme swap still flows through `data-theme`. Components keep using semantic
|
|
312
|
+
// utilities — never `bg-[#4f46e5]`.
|
|
313
|
+
//
|
|
314
|
+
// // tailwind.config.ts
|
|
315
|
+
// export default {
|
|
316
|
+
// theme: {
|
|
317
|
+
// extend: {
|
|
318
|
+
// colors: {
|
|
319
|
+
// canvas: 'var(--vf-color-bg-canvas)',
|
|
320
|
+
// raised: 'var(--vf-color-bg-raised)',
|
|
321
|
+
// accent: { DEFAULT: 'var(--vf-color-accent)', hover: 'var(--vf-color-accent-hover)' },
|
|
322
|
+
// muted: 'var(--vf-color-text-muted)',
|
|
323
|
+
// },
|
|
324
|
+
// fontSize: {
|
|
325
|
+
// caption: 'var(--vf-type-size-caption)',
|
|
326
|
+
// body: 'var(--vf-type-size-body)',
|
|
327
|
+
// heading: 'var(--vf-type-size-heading)',
|
|
328
|
+
// },
|
|
329
|
+
// },
|
|
330
|
+
// },
|
|
331
|
+
// };
|
|
332
|
+
//
|
|
333
|
+
// Lint guardrail (field report #351): forbid raw hex/px in component source so
|
|
334
|
+
// the indirection can't be bypassed. Stylelint: `color-no-hex` + a custom
|
|
335
|
+
// `declaration-property-value-disallowed-list` for `font-size: /\d+px/`; for
|
|
336
|
+
// className strings, an ESLint `no-restricted-syntax` rule banning Tailwind
|
|
337
|
+
// arbitrary-value brackets in color/size utilities (e.g. `bg-[#...]`,
|
|
338
|
+
// `text-[15px]`). The token layer only pays off if primitives can't leak past it.
|