@pipelex/mthds-form 0.14.1 → 0.15.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/README.md CHANGED
@@ -4,19 +4,22 @@ The input form for MTHDS methods, as a library: a headless kernel that maps the
4
4
 
5
5
  It exists because "the form" is one problem that keeps being solved separately. A method declares its inputs; something has to decide what widget each input deserves, whether the Run button may light up, what shape the values take on the wire, and what a validation failure says to a person. That logic belongs in one place with tests around it, not copied into each surface that happens to need a form.
6
6
 
7
- ## Three entry points
7
+ ## Four entry points
8
8
 
9
9
  ```ts
10
10
  import { buildRunFields, computeReadiness } from '@pipelex/mthds-form'; // headless
11
11
  import { FieldRenderer } from '@pipelex/mthds-form/react'; // the controls
12
12
  import { GenerativePage } from '@pipelex/mthds-form/generative'; // a produced layout
13
+ import { assembleBrand } from '@pipelex/mthds-form/brand'; // a brand, held to the theme contract
13
14
  ```
14
15
 
15
16
  `.` is the kernel: no React, no design system, no framework. Its only runtime dependency is `ajv`, which the gate validates through. It runs in a browser, in Node, in a worker.
16
17
 
17
18
  `./react` is the control set — one control per field kind behind a single dispatch point, styled with Tailwind classes over standard shadcn/ui tokens. `react` and `react-dom` are optional peer dependencies, so a consumer that only wants the kernel never installs them.
18
19
 
19
- `./generative` renders a **layout** — a data file a model wrote once for a method, naming paths in the same descriptor and restating nothing about what a field is. It is the only entry that carries json-render and zod, so a host that renders an ordinary form pays for neither. When there is no layout, or the one on file no longer fits the method, the page falls back to the kernel's own form. See [docs/generative-ui.md](docs/generative-ui.md).
20
+ `./generative` renders a **layout** — a data file a model wrote once for a method, naming paths in the same descriptor and restating nothing about what a field is. It is the only entry that carries json-render, and with `./brand` the only one that carries zod, so a host that renders an ordinary form pays for neither. When there is no layout, or the one on file no longer fits the method, the page falls back to the kernel's own form. See [docs/generative-ui.md](docs/generative-ui.md).
21
+
22
+ `./brand` holds a **brand** (a product's colours, radius, typefaces and logos, as a DTCG token file and a manifest) to the theme contract the controls are styled against, and compiles it into a stylesheet scoped to one class. It renders nothing, fetches nothing and depends on zod alone. See [docs/brand.md](docs/brand.md).
20
23
 
21
24
  `mthds` is an ordinary dependency rather than a peer, but it is **types only**: the wire types of `pipe_io_contracts` belong to the MTHDS standard, so this package re-exports the standard's declarations instead of restating them. Every import of it is an `import type` and is erased at build, so it costs an install entry and no shipped bytes. It is not a peer because pnpm does not install an unmet one, and an absent `mthds` degrades the re-exported types silently rather than failing. See [docs/dependency-budget.md](docs/dependency-budget.md).
22
25
 
@@ -0,0 +1,285 @@
1
+ import { z } from 'zod';
2
+ import { B as BrandManifest } from '../manifest-DUeAWeqa.js';
3
+ export { b as brandManifestSchema } from '../manifest-DUeAWeqa.js';
4
+
5
+ /**
6
+ * The brand contract, as data: which token of a brand's `tokens.json` sets
7
+ * which custom property, and what that property paints.
8
+ *
9
+ * It is the theme contract seen from the other side. `src/styles/theme.css`
10
+ * states it as a working default and `docs/theming.md` in prose; this is the
11
+ * same list as a table a build can read, and a brand is exactly these tokens
12
+ * and no others - the validator refuses a file that adds one, because a token
13
+ * nothing consumes is a value with no way to be looked at.
14
+ *
15
+ * The colour and radius rows are not a second list kept beside `theme.css`:
16
+ * they are held to it, by `__tests__/contract.test.ts` over the source and by
17
+ * `scripts/assert-bundle.mjs` over the build. The two typography rows are the
18
+ * exception, and on purpose. `--font-sans` and `--font-mono` are the names of
19
+ * Tailwind's own theme variables, which the prebuilt sheet already declares,
20
+ * so `theme.css` does not define them: it is unlayered, and a value there
21
+ * would override the typeface of every host that loads it. The stock
22
+ * typeface is therefore the host's own, and only a brand's scope sets these
23
+ * two. See docs/brand.md.
24
+ */
25
+ type ContractTokenType = 'color' | 'dimension' | 'fontFamily';
26
+ interface ContractToken {
27
+ /** The DTCG token id, `color.background`. */
28
+ id: string;
29
+ /** The custom property it sets, `--background`. */
30
+ variable: string;
31
+ type: ContractTokenType;
32
+ /** What the property paints, in the words a producer is told. */
33
+ paints: string;
34
+ }
35
+ /** The colour tokens, in the order `theme.css` states them. */
36
+ declare const COLOR_TOKEN_NAMES: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "secondary", "secondary-foreground", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "border", "input", "ring"];
37
+ type ColorTokenName = (typeof COLOR_TOKEN_NAMES)[number];
38
+ declare function isColorTokenName(name: string): name is ColorTokenName;
39
+ declare const BRAND_CONTRACT: readonly ContractToken[];
40
+ /**
41
+ * The pairs that must clear WCAG AA in both modes, as they render: the canvas
42
+ * opaque, the ink blended over it by its alpha.
43
+ */
44
+ declare const CONTRAST_PAIRS: readonly {
45
+ foreground: ColorTokenName;
46
+ background: ColorTokenName;
47
+ }[];
48
+ /** The minimum contrast ratio those pairs must reach: WCAG 2 AA for normal text. */
49
+ declare const MIN_CONTRAST = 4.5;
50
+ /** The custom property a token sets, or a loud failure for a token the contract does not name. */
51
+ declare function contractVariable(id: string): string;
52
+
53
+ declare const srgbColorSchema: z.ZodObject<{
54
+ colorSpace: z.ZodLiteral<"srgb">;
55
+ components: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
56
+ alpha: z.ZodNumber;
57
+ hex: z.ZodOptional<z.ZodString>;
58
+ }, z.core.$strict>;
59
+ type SrgbColor = z.infer<typeof srgbColorSchema>;
60
+ declare const colorValueSchema: z.ZodUnion<readonly [z.ZodObject<{
61
+ colorSpace: z.ZodLiteral<"srgb">;
62
+ components: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
63
+ alpha: z.ZodNumber;
64
+ hex: z.ZodOptional<z.ZodString>;
65
+ }, z.core.$strict>, z.ZodString]>;
66
+ type ColorValue = z.infer<typeof colorValueSchema>;
67
+ declare const colorTokenSchema: z.ZodObject<{
68
+ $value: z.ZodUnion<readonly [z.ZodObject<{
69
+ colorSpace: z.ZodLiteral<"srgb">;
70
+ components: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
71
+ alpha: z.ZodNumber;
72
+ hex: z.ZodOptional<z.ZodString>;
73
+ }, z.core.$strict>, z.ZodString]>;
74
+ $description: z.ZodString;
75
+ $extensions: z.ZodOptional<z.ZodObject<{
76
+ mode: z.ZodObject<{
77
+ dark: z.ZodUnion<readonly [z.ZodObject<{
78
+ colorSpace: z.ZodLiteral<"srgb">;
79
+ components: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
80
+ alpha: z.ZodNumber;
81
+ hex: z.ZodOptional<z.ZodString>;
82
+ }, z.core.$strict>, z.ZodString]>;
83
+ }, z.core.$strict>;
84
+ }, z.core.$strict>>;
85
+ }, z.core.$strict>;
86
+ type ColorToken = z.infer<typeof colorTokenSchema>;
87
+ declare const dimensionTokenSchema: z.ZodObject<{
88
+ $value: z.ZodObject<{
89
+ value: z.ZodNumber;
90
+ unit: z.ZodEnum<{
91
+ rem: "rem";
92
+ px: "px";
93
+ }>;
94
+ }, z.core.$strict>;
95
+ $description: z.ZodString;
96
+ }, z.core.$strict>;
97
+ type DimensionToken = z.infer<typeof dimensionTokenSchema>;
98
+ declare const fontFamilyTokenSchema: z.ZodObject<{
99
+ $value: z.ZodArray<z.ZodString>;
100
+ $description: z.ZodString;
101
+ }, z.core.$strict>;
102
+ type FontFamilyToken = z.infer<typeof fontFamilyTokenSchema>;
103
+ /** The token file, typed: the contract's groups and tokens, nothing else. */
104
+ interface BrandTokens {
105
+ color: {
106
+ $type: 'color';
107
+ } & Record<ColorTokenName, ColorToken>;
108
+ radius: {
109
+ $type: 'dimension';
110
+ base: DimensionToken;
111
+ };
112
+ font: {
113
+ $type: 'fontFamily';
114
+ sans: FontFamilyToken;
115
+ mono: FontFamilyToken;
116
+ };
117
+ }
118
+ type TokensValidation = {
119
+ ok: true;
120
+ tokens: BrandTokens;
121
+ } | {
122
+ ok: false;
123
+ problems: string[];
124
+ };
125
+ type ColorMode = 'light' | 'dark';
126
+ /** `#rrggbb` for a colour, its alpha dropped: each channel the byte the compiler writes. */
127
+ declare function colorHex(color: SrgbColor): string;
128
+ /**
129
+ * Whether a colour compiles to the hex. The hex is what a brand ships for the
130
+ * colour, so any slack here would let a stated accent ship as its neighbour.
131
+ */
132
+ declare function colorIsHex(color: SrgbColor, hex: string): boolean;
133
+ /** The colour a token resolves to in a mode, through its aliases; null when a chain is broken. */
134
+ declare function resolveColor(tokens: BrandTokens, name: ColorTokenName, mode: ColorMode): SrgbColor | null;
135
+ /** WCAG 2 contrast ratio between two colours, taken as opaque. */
136
+ declare function contrastRatio(a: SrgbColor, b: SrgbColor): number;
137
+ /** Every problem with the file at once, so a producer's repair round sees the whole list. */
138
+ declare function validateBrandTokens(doc: unknown): TokensValidation;
139
+
140
+ declare const statedFactsSchema: z.ZodObject<{
141
+ accent: z.ZodOptional<z.ZodObject<{
142
+ light: z.ZodOptional<z.ZodString>;
143
+ dark: z.ZodOptional<z.ZodString>;
144
+ }, z.core.$strict>>;
145
+ logo: z.ZodOptional<z.ZodObject<{
146
+ onLight: z.ZodOptional<z.ZodURL>;
147
+ onDark: z.ZodOptional<z.ZodURL>;
148
+ }, z.core.$strict>>;
149
+ }, z.core.$strict>;
150
+ type StatedFacts = z.infer<typeof statedFactsSchema>;
151
+ /**
152
+ * Where the brand does not carry what the person stated, in the words a repair
153
+ * round is told. Only the modes and canvases that were stated are checked.
154
+ */
155
+ declare function statedFactProblems(stated: StatedFacts | undefined, manifest: BrandManifest, tokens: BrandTokens): string[];
156
+
157
+ /**
158
+ * A brand's `provenance.json`: who produced it, on which model, from which
159
+ * contract and which facts.
160
+ *
161
+ * A brand follows the rules a captured layout follows: produced by a pass,
162
+ * committed with its provenance, and titled by what made it and by nothing
163
+ * else. The provenance is a record, so it is never rewritten to make a check
164
+ * pass. A brand's directory is named by `brandProducerId`, the same recipe a
165
+ * layout fixture's id uses, so the tree and the record cannot disagree about
166
+ * who made a brand.
167
+ */
168
+ /** How a brand came to be. */
169
+ declare const BRAND_PRODUCERS: readonly ["pipelex-method", "claude-code-subagent", "claude-code-session"];
170
+ type BrandProducer = (typeof BRAND_PRODUCERS)[number];
171
+ declare const brandProvenanceSchema: z.ZodObject<{
172
+ producer: z.ZodEnum<{
173
+ "pipelex-method": "pipelex-method";
174
+ "claude-code-subagent": "claude-code-subagent";
175
+ "claude-code-session": "claude-code-session";
176
+ }>;
177
+ model: z.ZodString;
178
+ date: z.ZodString;
179
+ brief: z.ZodString;
180
+ contractHash: z.ZodString;
181
+ seed: z.ZodOptional<z.ZodString>;
182
+ stated: z.ZodOptional<z.ZodObject<{
183
+ accent: z.ZodOptional<z.ZodObject<{
184
+ light: z.ZodOptional<z.ZodString>;
185
+ dark: z.ZodOptional<z.ZodString>;
186
+ }, z.core.$strict>>;
187
+ logo: z.ZodOptional<z.ZodObject<{
188
+ onLight: z.ZodOptional<z.ZodURL>;
189
+ onDark: z.ZodOptional<z.ZodURL>;
190
+ }, z.core.$strict>>;
191
+ }, z.core.$strict>>;
192
+ siteFacts: z.ZodOptional<z.ZodString>;
193
+ rounds: z.ZodOptional<z.ZodNumber>;
194
+ }, z.core.$strict>;
195
+ type BrandProvenance = z.infer<typeof brandProvenanceSchema>;
196
+ /** The id a brand's directory is named by: `producer--model[--seeded]`. */
197
+ declare function brandProducerId(provenance: Pick<BrandProvenance, 'producer' | 'model' | 'seed'>): string;
198
+ /**
199
+ * The scope class a brand's stylesheet sets its tokens on: `brand-<brand>-<producerId>`,
200
+ * with every character outside `[a-z0-9-]` replaced, because a model id
201
+ * carries dots and a dot ends a class selector.
202
+ *
203
+ * The replacement folds: `claude-4.8-opus` and `claude-4-8-opus` share a
204
+ * scope, and so do two ids that differ only in case. A set of brands loaded on
205
+ * one page must therefore check its scopes are distinct, or the later
206
+ * stylesheet silently takes the earlier brand's place.
207
+ */
208
+ declare function brandScope(brand: string, producerId: string): string;
209
+
210
+ /**
211
+ * One brand's three files in - the manifest, the tokens, the provenance - and
212
+ * either the brand, validated and compiled, or every problem with it.
213
+ *
214
+ * This is the verdict on whether a brand is VALID under the contract as it is
215
+ * now: the manifest, the tokens with their contrast, and the facts the person
216
+ * stated. It is a hard verdict, and a brand that fails it is not compiled.
217
+ * Whether a brand was produced against the current brief is a different
218
+ * question and never a reason to refuse one - an older brand stays a valid
219
+ * brand - so the provenance's contract hash is recorded here and judged by
220
+ * the producer, not by this function.
221
+ *
222
+ * The problems come in the order a producer would want to be told: the
223
+ * manifest and the provenance first, because they are cheap and their errors
224
+ * exact, then the tokens, then the stated facts, which can only be checked
225
+ * against files that parsed. Each names the file and the path.
226
+ */
227
+ interface BrandSource {
228
+ /** The brand's directory name: `pipelex`. */
229
+ brand: string;
230
+ /** The producer's directory name under it: `pipelex-method--claude-4.8-opus`. */
231
+ producerId: string;
232
+ manifest: unknown;
233
+ tokens: unknown;
234
+ provenance: unknown;
235
+ }
236
+ interface AssembledBrand extends BrandProvenance {
237
+ brand: string;
238
+ producerId: string;
239
+ manifest: BrandManifest;
240
+ tokens: BrandTokens;
241
+ /** The class a page root carries; the stylesheet is scoped to it. */
242
+ scope: string;
243
+ /** The compiled stylesheet. */
244
+ css: string;
245
+ }
246
+ type AssembleResult = {
247
+ ok: true;
248
+ brand: AssembledBrand;
249
+ } | {
250
+ ok: false;
251
+ problems: string[];
252
+ };
253
+ declare function assembleBrand(source: BrandSource): AssembleResult;
254
+
255
+ /**
256
+ * A validated `tokens.json` in, the brand's stylesheet out.
257
+ *
258
+ * What comes out is exactly the contract, scoped: a `.<scope>` block setting
259
+ * every custom property the contract names, and a `.dark .<scope>` block
260
+ * restating every colour with its dark value. The `.dark` ancestor is the
261
+ * package's own dark-mode convention (`theme.css`, the `dark` variant), so a
262
+ * branded page inside a dark pane picks its dark values up with nothing else
263
+ * to wire.
264
+ *
265
+ * The scope block also sets `font-family: var(--font-sans)`. Tailwind's
266
+ * preflight resolves the page's typeface once, at `<html>`, so a scope that
267
+ * only set `--font-sans` lower in the tree would change no text; declaring the
268
+ * family where the scope is makes the brand's typeface reach everything under
269
+ * it - the produced page and the plain form alike - while a page with no brand
270
+ * keeps the host's own typeface untouched.
271
+ *
272
+ * The contract is closed - a fixed set of tokens, colours in sRGB only, one
273
+ * length, two font stacks - so compiling a file the validator accepted is this
274
+ * function and no dependency. It takes the file as `validateBrandTokens`
275
+ * returned it; anything else is the caller's mistake, and `assembleBrand` is
276
+ * the route that cannot make it. The output stays comparable with what a
277
+ * standard DTCG tool writes for the same file: `__tests__/terrazzo.test.ts`
278
+ * compiles every committed brand through Terrazzo and compares the
279
+ * declarations.
280
+ */
281
+ declare function compileBrand(tokens: BrandTokens, scope: string): string;
282
+ /** Every custom property a compiled brand sets on its scope, in contract order. */
283
+ declare const BRAND_VARIABLES: readonly string[];
284
+
285
+ export { type AssembleResult, type AssembledBrand, BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, BrandManifest, type BrandProducer, type BrandProvenance, type BrandSource, type BrandTokens, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, type ColorMode, type ColorToken, type ColorTokenName, type ColorValue, type ContractToken, type ContractTokenType, type DimensionToken, type FontFamilyToken, MIN_CONTRAST, type SrgbColor, type StatedFacts, type TokensValidation, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, isColorTokenName, resolveColor, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };