@pipelex/mthds-form 0.30.0 → 0.32.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,12 +4,13 @@ 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
- ## Four entry points
7
+ ## Five 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 { layoutCheckProblems } from '@pipelex/mthds-form/generative/headless'; // producing and checking one, on a server
13
14
  import { assembleBrand } from '@pipelex/mthds-form/brand'; // a brand, held to the theme contract
14
15
  ```
15
16
 
@@ -17,7 +18,9 @@ import { assembleBrand } from '@pipelex/mthds-form/brand'; // a brand, held to t
17
18
 
18
19
  `./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.
19
20
 
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
+ `./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 and `./generative/headless` are the only entries that carry json-render, and with `./brand` the only ones that carry 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).
22
+
23
+ `./generative/headless` is the half of that layer that renders nothing: the designer method's catalog and brief, the prompt pin, the JSONL compiler and the checks a layout is held to. It carries `@json-render/core` and `zod` but no React and no `'use client'` directive, so a host producing layouts calls it on its server, in a server action or a route handler; `./generative` re-exports all of it beside the page.
21
24
 
22
25
  `./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 also turns up to three seed colours a person picked into colour roles whose text always reads (`deriveBrandRoles`). It renders nothing, fetches nothing and depends on zod alone. See [docs/brand.md](docs/brand.md).
23
26
 
@@ -1,54 +1,8 @@
1
+ import { C as ColorTokenName } from '../styles-Bn8byNdo.js';
2
+ export { A as APP_STYLES, a as APP_STYLE_IDS, b as ActionShadow, c as AppStyle, d as AppStyleId, B as BRAND_CONTRACT, e as BodyTypefaceId, f as COLOR_TOKEN_NAMES, g as CONTRAST_PAIRS, h as ContractToken, i as ContractTokenType, j as ContrastPair, M as MIN_CONTRAST, k as MIN_CONTROL_CONTRAST, P as PanelRoles, R as RoleColor, S as StyleRoles, T as TYPEFACES, l as Typeface, m as TypefaceId, n as contractVariable, o as deriveStyleRoles, p as isAppStyle, q as isColorTokenName, r as isTypeface, s as resolveRoleColor } from '../styles-Bn8byNdo.js';
1
3
  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;
4
+ import { B as BrandManifest } from '../manifest-D3DLyNEv.js';
5
+ export { b as brandManifestSchema } from '../manifest-D3DLyNEv.js';
52
6
 
53
7
  declare const srgbColorSchema: z.ZodObject<{
54
8
  colorSpace: z.ZodLiteral<"srgb">;
@@ -284,55 +238,6 @@ declare function compileBrand(tokens: BrandTokens, scope: string): string;
284
238
  /** Every custom property a compiled brand sets on its scope, in contract order. */
285
239
  declare const BRAND_VARIABLES: readonly string[];
286
240
 
287
- /**
288
- * Up to three seed colours in, the colour roles a page paints with out.
289
- *
290
- * A team brands its pages with at most three colours, each optional, each a
291
- * `#rrggbb` a person picked: the button colour (`action`), the accent
292
- * (`accent`) and the header band (`header`). A person picks a colour for how
293
- * it looks, never for how text reads on it, so nothing here assumes a seed is
294
- * readable. What this derives is everything a page needs around the seeds,
295
- * with every text role readable by construction:
296
- *
297
- * - **Fills are the seeds, exactly.** `action`, `accent` and `header` come
298
- * back as they were given, in lower case, so the button and the band are the
299
- * colours the person chose.
300
- * - **Text on a fill is pure black or pure white**, whichever contrasts more
301
- * (`onAction`, `onHeader`). One of the two reaches at least 4.58:1 on any
302
- * colour, so every label clears WCAG AA. Only the two extremes carry that
303
- * guarantee: a warm near-black or an off-white falls below 4.5:1 on some
304
- * mid-tone, so no other colour is ever returned for text.
305
- * - **The accent as text is ink** (`accentInk`): the accent darkened on a
306
- * light page or lightened on a dark one, keeping its hue and saturation,
307
- * just until it reads at 4.5:1 on the page and on the pale accent fill, the
308
- * two backgrounds a host draws accent text on. An accent that already reads
309
- * is its own ink.
310
- * - **The pale accent fill** (`accentFill`) is the accent laid over the page
311
- * at a low strength: a pale tint on a light page, a deep one on a dark page.
312
- *
313
- * **An unset seed produces no role.** The roles a seed paints are present
314
- * when it is set and absent when it is not, so a host keeps its own token for
315
- * whatever the team left unset: the team's colours where it chose some, and
316
- * the host's palette everywhere else. The one seed that reaches further is the
317
- * button colour, which the accent follows when the accent is unset, so a red
318
- * button never sits beside links in another hue. With no seed at all, there
319
- * is no colour role to return.
320
- *
321
- * The page colour is an argument because every text role is measured against
322
- * it: a host renders a light and a dark theme, and it calls this once per
323
- * theme with that theme's page colour and that theme's seeds. Which seeds a
324
- * theme uses is the host's to decide; `resolveDarkSeeds` is the rule for a
325
- * dark set that falls back, colour by colour, to the light one.
326
- *
327
- * It returns roles, never custom property names: each host maps the roles onto
328
- * its own tokens. See docs/brand.md § "Three seed colours".
329
- */
330
- /**
331
- * The contrast a control's outline needs against what surrounds it: WCAG 2's
332
- * level for a user interface component (1.4.11). A button colour below it
333
- * against the page is a button whose shape is hard to see.
334
- */
335
- declare const MIN_CONTROL_CONTRAST = 3;
336
241
  /**
337
242
  * Up to three seed colours, each `#rrggbb` in any case, or unset. `null` and
338
243
  * `undefined` both mean unset, so a host can pass a team's stored colours as
@@ -392,11 +297,12 @@ type BrandRoles = (ActionRoles | Absent<ActionRoles>) & (AccentRoles | Absent<Ac
392
297
  * `surfaces` are the other colours accent text sits on besides the page and
393
298
  * the pale fill, each `#rrggbb`: a style's panel and the fields inside it
394
299
  * (`resolveRoleColor` turns a style's role colour into one), or a canvas
395
- * behind the page. The ink is moved until it reads on all of them. When
396
- * there is an accent to derive, a surface on which not even the page's own
397
- * text extreme, pure black on a light page or pure white on a dark one,
398
- * reaches 4.5:1 throws, since no ink moving the page's way could read there.
399
- * With no accent, no ink is derived, and the surfaces are only parsed.
300
+ * behind the page. The ink is moved until it reads on all of them, and an
301
+ * accent that already reads on every one is its own ink. When there is an
302
+ * accent to derive and no shade of it reads on the page, the pale fill and
303
+ * every surface at once, which a surface on the far side of the page's
304
+ * lightness can cause, it throws, naming them. With no accent, no ink is
305
+ * derived, and the surfaces are only parsed.
400
306
  */
401
307
  declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: string, surfaces?: readonly string[]): BrandRoles;
402
308
  /**
@@ -424,156 +330,4 @@ declare function resolveDarkSeeds(colors: BrandSeeds | null | undefined, darkCol
424
330
  /** Pure black or pure white, whichever reads better on a `#rrggbb` fill. */
425
331
  declare function readableTextOn(fill: string): TextOnFill;
426
332
 
427
- /**
428
- * The app styles and the typefaces, as data, and the roles a team's choice of
429
- * the two sets.
430
- *
431
- * A style is a small set of design values a team picks from a closed list, on
432
- * top of its own colours and logo: how its headings, its panels, its corners,
433
- * its call to action and its rhythm look. A typeface is picked from a short
434
- * list beside it, and replaces the style's body face, and its heading face
435
- * wherever the style's headings follow the body. Neither adds a colour: every
436
- * fill, edge and ink a style uses is a role the theme contract already has,
437
- * named here by that role (`RoleColor`), or a shadow.
438
- *
439
- * `deriveStyleRoles` returns roles, never custom property names, the way
440
- * `deriveBrandRoles` does for the seed colours: each host maps them onto its
441
- * own tokens, and the kernel's controls and the generative layer read the few
442
- * optional tokens docs/theming.md lists, each with today's look as its
443
- * fallback. A role a choice does not set is not a key, so a host keeps its own
444
- * value there; `quiet` with no typeface sets nothing at all, which is what
445
- * makes "no style" and "the default style" one state.
446
- *
447
- * See docs/brand.md § "App styles and typefaces".
448
- */
449
- /** The faces of the closed list, all on Google Fonts under the SIL Open Font License. */
450
- type TypefaceId = 'inter' | 'dm-sans' | 'plus-jakarta-sans' | 'manrope' | 'ibm-plex-sans' | 'source-sans-3' | 'work-sans' | 'space-grotesk' | 'newsreader';
451
- /** The faces a team may pick as its typeface: every face but a style's own heading face. */
452
- type BodyTypefaceId = Exclude<TypefaceId, 'newsreader'>;
453
- interface Typeface {
454
- id: TypefaceId;
455
- /** The family name, as a stylesheet and a font service name it. */
456
- family: string;
457
- category: 'sans-serif' | 'serif';
458
- /** The weights a host loads it in: from the host's own light titles (300) to the boldest a style sets (700). */
459
- weights: readonly number[];
460
- /** The generic stack that renders while the face loads, or when it never does. */
461
- fallback: string;
462
- /** The face first, then its fallback: a whole `font-family` value. */
463
- stack: string;
464
- /** Whether a team may pick it (`body`), or a style uses it for its headings only (`heading`). */
465
- use: 'body' | 'heading';
466
- }
467
- /**
468
- * Every face a style or a team can set. The body faces are the list a team
469
- * picks from; Newsreader is the `editorial` style's heading face, listed so a
470
- * host loads it, and is not a team's choice. Frozen, like `APP_STYLES`: a
471
- * process serves many teams, so the catalogue one of them reads cannot be
472
- * one another has edited.
473
- */
474
- declare const TYPEFACES: readonly Typeface[];
475
- /** Whether `value` names a face a team may pick: a body face of `TYPEFACES`. */
476
- declare function isTypeface(value: unknown): value is BodyTypefaceId;
477
- /**
478
- * A colour a style paints with, named by the role of the theme contract it
479
- * is: never a colour of its own, so a team's palette and the host's theme
480
- * stay the only sources of colour, and their readability guarantees hold.
481
- */
482
- interface RoleColor {
483
- role: ColorTokenName;
484
- /** Mixed toward another role, in Oklab, by `share` (0 to 1): the card a little toward the foreground. */
485
- toward?: {
486
- role: ColorTokenName;
487
- share: number;
488
- };
489
- /** Its opacity over what lies behind it, 0 to 1; absent, opaque. */
490
- alpha?: number;
491
- }
492
- /** How a style draws a panel - a host's composer, a designed page's rail or card - in one theme. */
493
- interface PanelRoles {
494
- fill: RoleColor;
495
- /** The edge's colour, or `null` for no visible edge (the width is kept, so a panel's box does not move). */
496
- edge: RoleColor | null;
497
- /** The edge's width, in CSS pixels. */
498
- edgeWidth: number;
499
- /** A CSS `box-shadow` in a neutral ink, or `null` for none. */
500
- shadow: string | null;
501
- /**
502
- * The fill of the fields set into the panel, where the host's field fill
503
- * would sink into it; absent, the host's own.
504
- */
505
- field?: RoleColor;
506
- }
507
- /** The call to action's shadow: a CSS shadow's geometry, in pixels, tinted with a role. */
508
- interface ActionShadow {
509
- x: number;
510
- y: number;
511
- blur: number;
512
- spread: number;
513
- color: RoleColor;
514
- }
515
- /**
516
- * The roles a style and a typeface set. An absent role is not a key: a host
517
- * keeps its own value there.
518
- */
519
- interface StyleRoles {
520
- /** The face of everything on the page. */
521
- bodyFace?: Typeface;
522
- /** The face of the page's titles and its sections' and panels' headings. */
523
- headingFace?: Typeface;
524
- headingWeight?: number;
525
- /** The headings' letter spacing, in `em`. */
526
- headingTracking?: string;
527
- /** A control's corner radius, in CSS pixels; a panel takes one and a half of it. */
528
- radius?: number;
529
- /** The panel treatment in each theme: a shadow that reads on a light page is lost on a dark one. */
530
- panel?: {
531
- light: PanelRoles;
532
- dark: PanelRoles;
533
- };
534
- /**
535
- * The call to action's shadow in each theme, or `none` for a plain fill: a
536
- * shadow in the button colour that lifts a button on a light page smears
537
- * under it on a dark one.
538
- */
539
- actionShadow?: {
540
- light: ActionShadow | 'none';
541
- dark: ActionShadow | 'none';
542
- };
543
- /** The rhythm: a multiple of the host's own margins, paddings and gaps around the form. Never a control's size. */
544
- density?: number;
545
- }
546
- type AppStyleId = 'quiet' | 'soft' | 'editorial' | 'bold';
547
- interface AppStyle {
548
- id: AppStyleId;
549
- name: string;
550
- /** The direction, in one sentence, as a designer reads it. */
551
- direction: string;
552
- /** The roles the style sets when the team picks no typeface. */
553
- roles: StyleRoles;
554
- }
555
- declare const APP_STYLE_IDS: readonly AppStyleId[];
556
- /** Whether `value` names one of the app styles. */
557
- declare function isAppStyle(value: unknown): value is AppStyleId;
558
- /**
559
- * The roles a team's choice sets: its style and its typeface, each a stored
560
- * id or nothing. An unknown or absent style is `quiet`, which sets nothing;
561
- * an unknown typeface, or one a team may not pick, is no typeface, so the
562
- * style's own face (or the host's) stays. The team's typeface replaces the
563
- * style's body face, and its heading face wherever the style's headings
564
- * follow the body.
565
- */
566
- declare function deriveStyleRoles(style: string | null | undefined, typeface?: string | null): StyleRoles;
567
- /** The styles, each with its own roles: what it sets for a team that picked no typeface. */
568
- declare const APP_STYLES: Readonly<Record<AppStyleId, AppStyle>>;
569
- /**
570
- * A role colour as a `#rrggbb` hex over the palette it names, to measure a
571
- * surface a style draws: the ink a team's accent needs on a style's panel
572
- * (`deriveBrandRoles`' `surfaces`). The mix is Oklab's, as CSS `color-mix()`
573
- * computes it. A translucent colour is laid over `behind`; without it, it
574
- * throws, since a translucent colour has no one value. Throws, too, for a
575
- * role the palette does not hold or a palette colour that is not a hex.
576
- */
577
- declare function resolveRoleColor(color: RoleColor, palette: Partial<Record<ColorTokenName, string>>, behind?: string): string;
578
-
579
- export { APP_STYLES, APP_STYLE_IDS, type ActionShadow, type AppStyle, type AppStyleId, type AssembleResult, type AssembledBrand, BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, type BodyTypefaceId, BrandManifest, type BrandProducer, type BrandProvenance, type BrandRoles, type BrandSeeds, 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, MIN_CONTROL_CONTRAST, type PanelRoles, type RoleColor, type SrgbColor, type StatedFacts, type StyleRoles, TYPEFACES, type TextOnFill, type TokensValidation, type Typeface, type TypefaceId, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, deriveBrandRoles, deriveStyleRoles, isAccentAdjusted, isActionFaint, isAppStyle, isColorTokenName, isTypeface, readableTextOn, resolveColor, resolveDarkSeeds, resolveRoleColor, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
333
+ export { type AssembleResult, type AssembledBrand, BRAND_PRODUCERS, BRAND_VARIABLES, BrandManifest, type BrandProducer, type BrandProvenance, type BrandRoles, type BrandSeeds, type BrandSource, type BrandTokens, type ColorMode, type ColorToken, ColorTokenName, type ColorValue, type DimensionToken, type FontFamilyToken, type SrgbColor, type StatedFacts, type TextOnFill, type TokensValidation, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contrastRatio, deriveBrandRoles, isAccentAdjusted, isActionFaint, readableTextOn, resolveColor, resolveDarkSeeds, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };