@pipelex/mthds-form 0.29.0 → 0.30.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.
@@ -361,7 +361,10 @@ interface AccentRoles {
361
361
  accent: string;
362
362
  /** A pale accent fill (deep on a dark page), behind accent ink: a pill, a badge. */
363
363
  accentFill: string;
364
- /** The accent as text, reading at 4.5:1 or more on the page and on `accentFill`. */
364
+ /**
365
+ * The accent as text, reading at 4.5:1 or more on the page, on `accentFill`
366
+ * and on every other surface the host named (`deriveBrandRoles`' `surfaces`).
367
+ */
365
368
  accentInk: string;
366
369
  }
367
370
  /** The header band's roles, present when the header is set. */
@@ -385,8 +388,17 @@ type BrandRoles = (ActionRoles | Absent<ActionRoles>) & (AccentRoles | Absent<Ac
385
388
  * The roles a page paints with, from up to three seeds and the colour of the
386
389
  * page they render on. Throws when a seed or the page is not a `#rrggbb`
387
390
  * colour: a host validates what a person typed before it reaches here.
391
+ *
392
+ * `surfaces` are the other colours accent text sits on besides the page and
393
+ * the pale fill, each `#rrggbb`: a style's panel and the fields inside it
394
+ * (`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.
388
400
  */
389
- declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: string): BrandRoles;
401
+ declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: string, surfaces?: readonly string[]): BrandRoles;
390
402
  /**
391
403
  * Whether the button colour sits below 3:1 against the page, so a button may
392
404
  * be hard to see in that theme. Only a colour the person set is judged: with
@@ -395,11 +407,12 @@ declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: st
395
407
  declare function isActionFaint(seeds: BrandSeeds | null | undefined, page: string): boolean;
396
408
  /**
397
409
  * Whether the accent had to be darkened or lightened to become ink on the
398
- * page, so links read in a shade of it rather than in the colour itself. Only
410
+ * page (and on the `surfaces`, as `deriveBrandRoles` takes them), so links read
411
+ * in a shade of it rather than in the colour itself. Only
399
412
  * a colour the person set is judged, and the accent counts as set when it
400
413
  * follows a button colour the person set: with neither, the answer is `false`.
401
414
  */
402
- declare function isAccentAdjusted(seeds: BrandSeeds | null | undefined, page: string): boolean;
415
+ declare function isAccentAdjusted(seeds: BrandSeeds | null | undefined, page: string, surfaces?: readonly string[]): boolean;
403
416
  /**
404
417
  * The seeds a dark theme uses: each dark colour where one is set, and the
405
418
  * light one where it is not. Colour by colour, before the accent follows the
@@ -411,4 +424,156 @@ declare function resolveDarkSeeds(colors: BrandSeeds | null | undefined, darkCol
411
424
  /** Pure black or pure white, whichever reads better on a `#rrggbb` fill. */
412
425
  declare function readableTextOn(fill: string): TextOnFill;
413
426
 
414
- export { type AssembleResult, type AssembledBrand, BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, 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 SrgbColor, type StatedFacts, type TextOnFill, type TokensValidation, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, deriveBrandRoles, isAccentAdjusted, isActionFaint, isColorTokenName, readableTextOn, resolveColor, resolveDarkSeeds, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
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 };
@@ -201,10 +201,11 @@ function resolveColor(tokens, name, mode) {
201
201
  current = target;
202
202
  }
203
203
  }
204
+ function linearChannel(channel) {
205
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
206
+ }
204
207
  function luminance(color) {
205
- const [r, g, b] = color.components.map(
206
- (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
207
- );
208
+ const [r, g, b] = color.components.map(linearChannel);
208
209
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
209
210
  }
210
211
  function contrastRatio(a, b) {
@@ -486,20 +487,28 @@ function assembleBrand(source) {
486
487
  var MIN_CONTROL_CONTRAST = 3;
487
488
  var ACCENT_FILL_STRENGTH = 0.12;
488
489
  var SEARCH_STEPS = 32;
489
- function deriveBrandRoles(seeds, page) {
490
+ function deriveBrandRoles(seeds, page, surfaces = []) {
490
491
  const canvas = parseHex(page, "The page colour");
492
+ const extreme = textOn(canvas) === "#000000" ? BLACK : WHITE;
493
+ const others = surfaces.map((surface, index) => parseHex(surface, `Surface ${index + 1}`));
491
494
  const action = seeds?.action == null ? null : parseHex(seeds.action, "The action seed");
492
495
  const accent = seeds?.accent == null ? action : parseHex(seeds.accent, "The accent seed");
493
496
  const header = seeds?.header == null ? null : parseHex(seeds.header, "The header seed");
494
497
  const actionRoles = action === null ? {} : { action: colorHex(action), onAction: textOn(action) };
495
- const accentRoles = accent === null ? {} : accentRolesOf(accent, canvas);
498
+ const accentRoles = accent === null ? {} : accentRolesOf(accent, canvas, extreme, others);
496
499
  const headerRoles = header === null ? {} : { header: colorHex(header), onHeader: textOn(header) };
497
500
  return { ...actionRoles, ...accentRoles, ...headerRoles };
498
501
  }
499
- function accentRolesOf(accent, canvas) {
500
- const extreme = textOn(canvas) === "#000000" ? BLACK : WHITE;
502
+ function accentRolesOf(accent, canvas, extreme, surfaces) {
503
+ for (const surface of surfaces) {
504
+ if (contrastRatio(extreme, surface) < MIN_CONTRAST) {
505
+ throw new Error(
506
+ `No accent ink can read on ${colorHex(surface)}: not even ${colorHex(extreme)}, the page's own text extreme, reaches ${MIN_CONTRAST}:1 there.`
507
+ );
508
+ }
509
+ }
501
510
  const accentFill = paleFill(accent, canvas, extreme);
502
- const accentInk = inkFor(accent, [canvas, accentFill], extreme);
511
+ const accentInk = inkFor(accent, [canvas, accentFill, ...surfaces], extreme);
503
512
  return {
504
513
  accent: colorHex(accent),
505
514
  accentFill: colorHex(accentFill),
@@ -511,8 +520,8 @@ function isActionFaint(seeds, page) {
511
520
  if (seeds?.action == null) return false;
512
521
  return contrastRatio(parseHex(seeds.action, "The action seed"), canvas) < MIN_CONTROL_CONTRAST;
513
522
  }
514
- function isAccentAdjusted(seeds, page) {
515
- const roles = deriveBrandRoles(seeds, page);
523
+ function isAccentAdjusted(seeds, page, surfaces = []) {
524
+ const roles = deriveBrandRoles(seeds, page, surfaces);
516
525
  return roles.accentInk !== void 0 && roles.accentInk !== roles.accent;
517
526
  }
518
527
  function resolveDarkSeeds(colors, darkColors) {
@@ -594,6 +603,260 @@ function fromHsl(hue, saturation, lightness) {
594
603
  return [r + offset, g + offset, b + offset];
595
604
  }
596
605
 
597
- export { BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, MIN_CONTRAST, MIN_CONTROL_CONTRAST, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, deriveBrandRoles, isAccentAdjusted, isActionFaint, isColorTokenName, readableTextOn, resolveColor, resolveDarkSeeds, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
606
+ // src/brand/styles.ts
607
+ var SANS_FALLBACK = 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
608
+ var SERIF_FALLBACK = 'ui-serif, Georgia, Cambria, "Times New Roman", serif';
609
+ var WEIGHTS = [300, 400, 500, 600, 700];
610
+ function face(id, family, category, use) {
611
+ const fallback = category === "serif" ? SERIF_FALLBACK : SANS_FALLBACK;
612
+ return {
613
+ id,
614
+ family,
615
+ category,
616
+ weights: WEIGHTS,
617
+ fallback,
618
+ stack: `"${family}", ${fallback}`,
619
+ use
620
+ };
621
+ }
622
+ var TYPEFACES = deepFreeze([
623
+ face("inter", "Inter", "sans-serif", "body"),
624
+ face("dm-sans", "DM Sans", "sans-serif", "body"),
625
+ face("plus-jakarta-sans", "Plus Jakarta Sans", "sans-serif", "body"),
626
+ face("manrope", "Manrope", "sans-serif", "body"),
627
+ face("ibm-plex-sans", "IBM Plex Sans", "sans-serif", "body"),
628
+ face("source-sans-3", "Source Sans 3", "sans-serif", "body"),
629
+ face("work-sans", "Work Sans", "sans-serif", "body"),
630
+ face("space-grotesk", "Space Grotesk", "sans-serif", "body"),
631
+ face("newsreader", "Newsreader", "serif", "heading")
632
+ ]);
633
+ var FACES = new Map(TYPEFACES.map((candidate) => [candidate.id, candidate]));
634
+ function isTypeface(value) {
635
+ return typeof value === "string" && FACES.get(value)?.use === "body";
636
+ }
637
+ function faceOf(id) {
638
+ return FACES.get(id);
639
+ }
640
+ var role = (name, more = {}) => ({
641
+ role: name,
642
+ ...more
643
+ });
644
+ var DEFINITIONS = {
645
+ // The host's own look, named: it sets nothing, and a team's typeface is its
646
+ // body and its headings.
647
+ quiet: {
648
+ name: "Quiet",
649
+ direction: "Calm and precise: light titles, hairline panels, generous air, nothing decorated.",
650
+ bodyFace: null,
651
+ headingFace: "body",
652
+ roles: {}
653
+ },
654
+ soft: {
655
+ name: "Soft",
656
+ direction: "Friendly and rounded: generous corners, panels that float on a soft shadow with no outline, warm medium-weight titles.",
657
+ bodyFace: "dm-sans",
658
+ headingFace: "body",
659
+ roles: {
660
+ headingWeight: 500,
661
+ headingTracking: "-0.015em",
662
+ radius: 12,
663
+ panel: {
664
+ light: {
665
+ fill: role("card"),
666
+ edge: null,
667
+ edgeWidth: 1,
668
+ shadow: "0 1px 2px rgb(16 24 40 / 0.05), 0 12px 32px -10px rgb(16 24 40 / 0.16)",
669
+ field: role("card")
670
+ },
671
+ // On a dark page a shadow barely shows, so the panel is lifted by its
672
+ // fill instead, the card a little toward the foreground, with a faint
673
+ // highlight along its top edge where light would catch it, and its
674
+ // fields sink back to the page's own background.
675
+ dark: {
676
+ fill: role("card", { toward: { role: "foreground", share: 0.07 } }),
677
+ edge: null,
678
+ edgeWidth: 1,
679
+ shadow: "inset 0 1px 0 rgb(255 255 255 / 0.07), 0 2px 4px rgb(0 0 0 / 0.55), 0 24px 56px -12px rgb(0 0 0 / 0.95)",
680
+ field: role("background")
681
+ }
682
+ },
683
+ // A short shadow in the button colour on a light page, kept within the
684
+ // gap a host leaves under its button. On a dark page the panel is lifted
685
+ // by its fill, and a glow in a light button colour reads as a smear.
686
+ actionShadow: {
687
+ light: { x: 0, y: 6, blur: 14, spread: -6, color: role("primary", { alpha: 0.5 }) },
688
+ dark: "none"
689
+ },
690
+ density: 1.08
691
+ }
692
+ },
693
+ editorial: {
694
+ name: "Editorial",
695
+ direction: "Considered and literary: serif display titles, fine hairlines, wide margins and a reading rhythm.",
696
+ bodyFace: "source-sans-3",
697
+ headingFace: "newsreader",
698
+ roles: {
699
+ headingWeight: 500,
700
+ headingTracking: "-0.01em",
701
+ radius: 2,
702
+ // Hairlines all the way in: the fields inside the panel take its own
703
+ // fill, so each reads as its fine edge rather than as a grey block.
704
+ panel: {
705
+ light: {
706
+ fill: role("card"),
707
+ edge: role("border"),
708
+ edgeWidth: 1,
709
+ shadow: null,
710
+ field: role("card")
711
+ },
712
+ dark: {
713
+ fill: role("card"),
714
+ edge: role("border"),
715
+ edgeWidth: 1,
716
+ shadow: null,
717
+ field: role("card")
718
+ }
719
+ },
720
+ actionShadow: { light: "none", dark: "none" },
721
+ density: 1.25
722
+ }
723
+ },
724
+ bold: {
725
+ name: "Bold",
726
+ direction: "Confident and graphic: heavy, tightly set titles, solid panels with firm edges, a compact rhythm.",
727
+ bodyFace: "space-grotesk",
728
+ headingFace: "body",
729
+ roles: {
730
+ headingWeight: 700,
731
+ headingTracking: "-0.03em",
732
+ radius: 6,
733
+ // One firm edge around a solid panel, softer on a dark page. The panel
734
+ // is the muted fill, which the host's fields all but match, so fields
735
+ // set into it take the card on a light page and the page's background
736
+ // on a dark one.
737
+ panel: {
738
+ light: {
739
+ fill: role("muted"),
740
+ edge: role("foreground", { alpha: 0.62 }),
741
+ edgeWidth: 2,
742
+ shadow: null,
743
+ field: role("card")
744
+ },
745
+ dark: {
746
+ fill: role("muted"),
747
+ edge: role("foreground", { alpha: 0.3 }),
748
+ edgeWidth: 2,
749
+ shadow: null,
750
+ field: role("background")
751
+ }
752
+ },
753
+ actionShadow: { light: "none", dark: "none" },
754
+ density: 0.85
755
+ }
756
+ }
757
+ };
758
+ var APP_STYLE_IDS = Object.freeze([
759
+ "quiet",
760
+ "soft",
761
+ "editorial",
762
+ "bold"
763
+ ]);
764
+ function isAppStyle(value) {
765
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(DEFINITIONS, value);
766
+ }
767
+ function deriveStyleRoles(style, typeface) {
768
+ const definition = DEFINITIONS[isAppStyle(style) ? style : "quiet"];
769
+ const roles = {};
770
+ const body = isTypeface(typeface) ? faceOf(typeface) : definition.bodyFace === null ? void 0 : faceOf(definition.bodyFace);
771
+ if (body) roles.bodyFace = body;
772
+ const heading = definition.headingFace === "body" ? body : faceOf(definition.headingFace);
773
+ if (heading) roles.headingFace = heading;
774
+ return copyData({ ...roles, ...definition.roles });
775
+ }
776
+ var APP_STYLES = deepFreeze(
777
+ Object.fromEntries(
778
+ APP_STYLE_IDS.map((id) => [
779
+ id,
780
+ {
781
+ id,
782
+ name: DEFINITIONS[id].name,
783
+ direction: DEFINITIONS[id].direction,
784
+ roles: deriveStyleRoles(id)
785
+ }
786
+ ])
787
+ )
788
+ );
789
+ function copyData(value) {
790
+ if (Array.isArray(value)) return value.map(copyData);
791
+ if (value !== null && typeof value === "object") {
792
+ return Object.fromEntries(
793
+ Object.entries(value).map(([key, inner]) => [key, copyData(inner)])
794
+ );
795
+ }
796
+ return value;
797
+ }
798
+ function deepFreeze(value) {
799
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
800
+ for (const inner of Object.values(value)) deepFreeze(inner);
801
+ Object.freeze(value);
802
+ }
803
+ return value;
804
+ }
805
+ function resolveRoleColor(color, palette, behind) {
806
+ const of = (name) => {
807
+ const value = palette[name];
808
+ if (!isColorTokenName(name) || value === void 0) {
809
+ throw new Error(`The palette has no ${name} to resolve a style's colour against.`);
810
+ }
811
+ return parseHex(value, `The palette's ${name}`);
812
+ };
813
+ let rgb = of(color.role).components;
814
+ if (color.toward) {
815
+ const share = clampUnit(color.toward.share);
816
+ rgb = mixOklab(rgb, of(color.toward.role).components, share);
817
+ }
818
+ if (color.alpha !== void 0 && color.alpha < 1) {
819
+ if (behind === void 0) {
820
+ throw new Error(`A translucent ${color.role} has no one colour: say what it lies over.`);
821
+ }
822
+ const under = parseHex(behind, "The colour behind").components;
823
+ const alpha = clampUnit(color.alpha);
824
+ rgb = rgb.map((channel, index) => under[index] + (channel - under[index]) * alpha);
825
+ }
826
+ return colorHex({ components: rgb});
827
+ }
828
+ function clampUnit(value) {
829
+ return Math.min(1, Math.max(0, value));
830
+ }
831
+ function mixOklab(a, b, share) {
832
+ const [l1, a1, b1] = toOklab(a);
833
+ const [l2, a2, b2] = toOklab(b);
834
+ const at = (from, to) => from + (to - from) * share;
835
+ return fromOklab([at(l1, l2), at(a1, a2), at(b1, b2)]).map(clampUnit);
836
+ }
837
+ var fromLinear = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
838
+ function toOklab(rgb) {
839
+ const [r, g, b] = rgb.map(linearChannel);
840
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
841
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
842
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
843
+ return [
844
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
845
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
846
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
847
+ ];
848
+ }
849
+ function fromOklab([lightness, a, b]) {
850
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
851
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
852
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
853
+ return [
854
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
855
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
856
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
857
+ ].map((channel) => fromLinear(clampUnit(channel)));
858
+ }
859
+
860
+ export { APP_STYLES, APP_STYLE_IDS, BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, MIN_CONTRAST, MIN_CONTROL_CONTRAST, TYPEFACES, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, deriveBrandRoles, deriveStyleRoles, isAccentAdjusted, isActionFaint, isAppStyle, isColorTokenName, isTypeface, readableTextOn, resolveColor, resolveDarkSeeds, resolveRoleColor, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
598
861
  //# sourceMappingURL=index.js.map
599
862
  //# sourceMappingURL=index.js.map