@ashley-shrok/viewmodel-shell 5.1.2 → 6.0.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.
@@ -29,6 +29,21 @@
29
29
  --vms-error: #c2453d;
30
30
  --vms-error-glow: rgba(194, 69, 61, 0.10);
31
31
  --vms-warning: #8a630d;
32
+ /* Warning is the ONE tone with a polarity split. --vms-warning is deliberately
33
+ DARK on light themes because it doubles as a TEXT/border color (.vms-text--warning,
34
+ button/badge outline) and warning-colored TEXT on a light surface must be dark to
35
+ clear WCAG-AA — a bright yellow text on white is unreadable. But a warning FILL has
36
+ no such constraint: the standard design-system pattern (Bootstrap/Material) is a
37
+ BRIGHT yellow fill with a DARK foreground. --vms-warning-fill + --vms-on-warning-fill
38
+ are that pair — used on every SOLID warning fill (toast, primary badge/button, the
39
+ toned steps marker) so warning actually reads as a yellow that grabs attention on
40
+ light themes, while staying AA (dark #2a2205 on #e0a823 = 7.37:1). Theme-independent
41
+ (a solid amber fill reads the same on any page surface); dark themes need no override
42
+ — their --vms-warning is already #e0a823, so this ALSO fixes the white-on-bright-amber
43
+ fill that failed AA on dark themes (2.14:1). Tints (section/table/chip) keep the base
44
+ --vms-warning as their own consistent family. */
45
+ --vms-warning-fill: #e0a823;
46
+ --vms-on-warning-fill: #2a2205;
32
47
  --vms-priority-high: #d76410;
33
48
  --vms-success: #2da359;
34
49
  --vms-info: #2277dd;
@@ -473,6 +488,32 @@ body:has(.vms-page--fill) { margin: 0; }
473
488
  text-transform: uppercase;
474
489
  letter-spacing: 0.05em;
475
490
  }
491
+ /* Per-tile tone (StatItem.tone) — a toned stat becomes a subtly tinted chip
492
+ (surface tint + colored border, the SAME color-mix formula as Section tone, so
493
+ it inherits that surface's already-AA-cleared text contrast) so an unhealthy
494
+ stat reads at a glance. Only toned items get the chip surface; neutral items
495
+ stay byte-identical to today's inline stat. --_tone resolves per tone below. */
496
+ .vms-stat-bar__item--tone-danger { --_tone: var(--vms-error); }
497
+ .vms-stat-bar__item--tone-warning { --_tone: var(--vms-warning); }
498
+ .vms-stat-bar__item--tone-success { --_tone: var(--vms-success); }
499
+ .vms-stat-bar__item--tone-info { --_tone: var(--vms-info); }
500
+ .vms-stat-bar__item--toned {
501
+ align-items: center;
502
+ gap: var(--vms-space-2xs);
503
+ padding: var(--vms-space-2xs) var(--vms-space-sm);
504
+ border: 1px solid var(--_tone);
505
+ border-radius: var(--vms-radius-md);
506
+ background: color-mix(in srgb, var(--_tone) 14%, var(--vms-surface));
507
+ }
508
+ /* The tint + border carry the status (the Section-tone model); the value + label
509
+ stay NEUTRAL and readable rather than tone-colored — tone text on its own 14%
510
+ tint fails WCAG-AA (success bottomed at 2.79:1 across the light themes), while
511
+ neutral text on the tint clears it exactly as Section tone's neutral text does. */
512
+ .vms-stat-bar__item--toned .vms-stat-bar__value { color: var(--vms-text); }
513
+ /* Label strengthens from muted to full text on a toned chip: muted on the 14%
514
+ tint bottoms at 4.21:1 (just under the 4.5 AA-text bar), and an emphasized chip
515
+ reads better with a solid label anyway. Full text on the tint is 11.14:1. */
516
+ .vms-stat-bar__item--toned .vms-stat-bar__label { color: var(--vms-text); }
476
517
 
477
518
  /* ── Form ── */
478
519
  .vms-form { display: flex; flex-direction: column; gap: var(--vms-space-sm); align-items: stretch; }
@@ -574,6 +615,231 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
574
615
  overflow: auto;
575
616
  }
576
617
 
618
+ /* Lookup / reference picker (FieldNode inputType="lookup" / "lookup-multiple",
619
+ Phase 21) — an editable combobox whose listbox popup floats over the page.
620
+ Built entirely from the existing .vms-field__* tokens: no new colors, no new
621
+ custom properties, so every theme recolors it automatically. */
622
+ .vms-field--lookup { position: relative; }
623
+ .vms-field__popup {
624
+ /* Anchored to .vms-field--lookup's positioning context, floating over what
625
+ follows so the popup never reflows the form when it opens. */
626
+ position: absolute;
627
+ top: 100%;
628
+ left: 0;
629
+ right: 0;
630
+ z-index: 20;
631
+ margin-top: var(--vms-space-2xs);
632
+ max-height: 16rem;
633
+ overflow-y: auto;
634
+ background: var(--vms-surface);
635
+ border: 1px solid var(--vms-border);
636
+ border-radius: var(--vms-radius);
637
+ /* A literal rgba, matching the toast overlay's shadow (the house precedent
638
+ for a floating surface) — there is no --vms-shadow token, and inventing
639
+ one would mean touching all 12 themes for a drop shadow. */
640
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24);
641
+ color-scheme: var(--vms-color-scheme);
642
+ }
643
+ /* The popup is rendered ALWAYS and hidden when closed (aria-controls must stay
644
+ valid while hidden). This rule is LOAD-BEARING: the UA's [hidden] default is
645
+ display:none, but the block above would otherwise win on specificity and the
646
+ popup would be permanently visible. */
647
+ .vms-field__popup[hidden] { display: none; }
648
+ /* The lookup's aria-live status regions and its assistive hint: available to
649
+ assistive tech, invisible on screen. Deliberately NOT display:none and NOT
650
+ visibility:hidden — both REMOVE the element from the accessibility tree, so a
651
+ live region styled that way is never announced and the hint is never read.
652
+ This is the standard visually-hidden clip; it must stay clipped-but-rendered. */
653
+ .vms-field__live {
654
+ position: absolute;
655
+ width: 1px;
656
+ height: 1px;
657
+ margin: -1px;
658
+ padding: 0;
659
+ border: 0;
660
+ overflow: hidden;
661
+ clip: rect(0 0 0 0);
662
+ clip-path: inset(50%);
663
+ white-space: nowrap;
664
+ }
665
+ .vms-field__option {
666
+ padding: var(--vms-space-xs) var(--vms-space-md);
667
+ font-size: var(--vms-text-md);
668
+ line-height: var(--vms-control-line);
669
+ color: var(--vms-text);
670
+ cursor: pointer;
671
+ /* §7 item 33 (reflow) — option text WRAPS rather than truncating: a
672
+ candidate is a person's or record's name, not a status word, and at high
673
+ magnification a truncated option is unreadable. The border delimits a
674
+ wrapped option so it never reads as two separate suggestions (GOV.UK's 12x
675
+ tester: "the words just looked like separate suggestions that didn't make
676
+ much sense"). */
677
+ overflow-wrap: anywhere;
678
+ border-bottom: 1px solid var(--vms-border);
679
+ }
680
+ .vms-field__option:last-child { border-bottom: none; }
681
+ .vms-field__option:hover { background: var(--vms-surface-2); }
682
+ /* The active (aria-activedescendant) option. DOM focus stays in the input, so
683
+ :focus cannot express this — the highlight must be driven off aria-selected,
684
+ which is the same fact the live region announces. */
685
+ .vms-field__option[aria-selected="true"] {
686
+ background: var(--vms-accent-glow);
687
+ color: var(--vms-text);
688
+ }
689
+
690
+ /* ── lookup chips (Phase 21, LOOK-03; BOTH modes since 21-14 / D2a) ──
691
+ The selection, rendered OUTSIDE the combobox as chip(s) — for `lookup` AND
692
+ `lookup-multiple`, from ONE implementation. The only difference between the
693
+ two is arity (single replaces on pick, multi appends), which lives in
694
+ browser.ts and has no expression here at all: a chip is a chip.
695
+
696
+ 🚨 We diverge from SLDS deliberately and it is worth knowing why, because the
697
+ survey says the opposite: Salesforce renders single-select's selection INSIDE
698
+ the input and ships no pill element for single at all. 21-13 followed them,
699
+ and the operator found that model has NOWHERE TO CLICK TO TYPE — the pill was
700
+ the input, so clicking in just appended to the name. Their model is coherent
701
+ given clear-then-search; ours is coherent given always-typeable. (There is no
702
+ `.vms-field--lookup-selected` any more; do not resurrect it.)
703
+
704
+ Structure is role=list/listitem + a real <button> (design §7 item 24) — see
705
+ browser.ts. This is presentation only; nothing here is on the wire.
706
+
707
+ ⚠️ CONTRAST: the chip's fg/bg pair is NOT covered by the fixed 13-pair
708
+ check:aa-contrast gate, so it is hand-measured across the shipped default +
709
+ all 12 themes: 10.63:1 worst (dark-*), 13.60:1 (light-*), vs a 4.5:1 text bar.
710
+ Single-select reuses this exact pair rather than inventing a token, so it adds
711
+ no new pair to measure. Re-measure if --_chip-tone ever stops being
712
+ --vms-text. */
713
+ .vms-field__chips {
714
+ display: flex;
715
+ flex-wrap: wrap;
716
+ gap: var(--vms-space-2xs);
717
+ /* Reset the <ul>. NOTE this is exactly why browser.ts sets role="list"
718
+ EXPLICITLY: `list-style: none` strips the implicit list/listitem roles in
719
+ Safari/VoiceOver, so a styled <ul> silently stops being a list — the
720
+ accessibility regression is caused BY this line and repaired in the
721
+ renderer. Do not "clean up" that seemingly-redundant role. */
722
+ list-style: none;
723
+ margin: 0;
724
+ padding: 0;
725
+ }
726
+ .vms-field__chip {
727
+ /* The .vms-badge private-tone technique (default.css, .vms-badge): ONE private
728
+ custom property holds the working color and the fill, text and border all
729
+ derive from it, so EVERY THEME RECOLORS AUTOMATICALLY — --vms-text and
730
+ --vms-surface are theme-owned. Retoning a chip would be one line. */
731
+ --_chip-tone: var(--vms-text);
732
+ display: inline-flex;
733
+ align-items: center;
734
+ /* Hug content in ANY parent layout. Carried over from .vms-badge's comment
735
+ (written in 5.1.2 for exactly this failure mode) because the chip hits it
736
+ IDENTICALLY: a chip is semantically inline, but as a flex item it is
737
+ BLOCKIFIED (inline-flex → flex) and the chip row's align-items would then
738
+ stretch it. A definite cross-size opts out of that stretch —
739
+ align-items:stretch only stretches items whose cross size is `auto`.
740
+ Deliberately NOT align-self:start, which would top-anchor the chip since a
741
+ child's align-self beats the parent's align-items. */
742
+ width: fit-content;
743
+ max-width: 100%;
744
+ gap: var(--vms-space-2xs);
745
+ font-size: var(--vms-text-xs);
746
+ font-weight: 600;
747
+ /* Not the badge's `1`: a chip's text WRAPS (below), and line-height:1 makes
748
+ wrapped lines collide. */
749
+ line-height: 1.35;
750
+ /* Tighter on the right — the remove button carries its own hit area. */
751
+ padding: 0.2em 0.3em 0.2em 0.6em;
752
+ border-radius: 999px;
753
+ /* 🚨 DELIBERATELY NOT `white-space: nowrap` — the ONE thing not copied from
754
+ .vms-badge, and the easiest to reintroduce by "consistency". Design §7 item
755
+ 33: chip text must WRAP WITHOUT TRUNCATION. A badge is a short status word;
756
+ a chip holds a PERSON'S NAME. GOV.UK's 12x-magnification tester on the
757
+ truncation failure: "Some larger country names may take over two lines. It
758
+ wasn't always clear to him that this had happened — the words just looked
759
+ like separate suggestions that didn't make much sense." The pill border-
760
+ radius + the surface knockout are what delimit a wrapped chip so it never
761
+ reads as two. (The popup's options carry the same rule and their own
762
+ delimiting border — see .vms-field__option.) */
763
+ overflow-wrap: anywhere;
764
+ /* The polarity-adaptive knockout — the house technique, used 18x in this file.
765
+ Mixing TOWARD --vms-surface (never toward white/black) auto-adapts to light
766
+ AND dark themes with ZERO per-theme deepening.
767
+ ⚠️ PROVISIONAL MIX RATIO — 12%, not the badge's 16%, and Plan 21-09's
768
+ hand-check owns the final number. Two reasons it must not simply inherit
769
+ 16%: (1) the fg/bg pair here is a NEW one the fixed 13-pair check:aa-contrast
770
+ gate does NOT auto-cover; (2) the badge's 16% was tuned for a small badge on
771
+ a PAGE surface. This is deliberately conservative — the tone is --vms-text
772
+ (near-maximal contrast against a 12% tint of ITSELF in the surface), not the
773
+ badge's --vms-text-muted, because a chip carries CONTENT (a name) rather
774
+ than muted status. The chip's background is opaque, so the field's input
775
+ backdrop does not enter the contrast pair. */
776
+ background: color-mix(in srgb, var(--_chip-tone) 12%, var(--vms-surface));
777
+ color: var(--_chip-tone);
778
+ border: 1px solid transparent;
779
+ }
780
+ /* §7 item 31 — the two-step Backspace's ARMED chip: the first press highlights
781
+ the last chip rather than deleting it. This is the VISUAL half only; the
782
+ announcement is the half that matters (browser.ts), because an arm that were
783
+ only a CSS class would leave an AT user pressing Backspace, hearing nothing,
784
+ pressing again, and deleting something they were never told about. Border, not
785
+ a fill swap, so it cannot disturb the contrast pair above. */
786
+ .vms-field__chip--armed { border-color: var(--_chip-tone); }
787
+ .vms-field__chip-remove {
788
+ display: inline-flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ /* Never let the button shrink or wrap away from its label — the chip's text
792
+ wraps; its remove affordance must not. */
793
+ flex: none;
794
+ align-self: flex-start;
795
+ width: 1.4em;
796
+ height: 1.4em;
797
+ padding: 0;
798
+ border: 0;
799
+ border-radius: 999px;
800
+ background: transparent;
801
+ /* Inherit the chip's tone rather than re-deriving it, so a retoned chip keeps
802
+ a consistent remove button for free. */
803
+ color: inherit;
804
+ font-family: inherit;
805
+ font-size: 1.1em;
806
+ line-height: 1;
807
+ cursor: pointer;
808
+ }
809
+ .vms-field__chip-remove:hover {
810
+ background: color-mix(in srgb, var(--_chip-tone) 22%, transparent);
811
+ }
812
+ /* The chip is INTERACTIVE where the badge is inert, so its remove button needs a
813
+ visible focus ring: it is a REAL focusable button (§7 item 26 gives the chips a
814
+ roving tabindex precisely so DOM focus lands here), and it is reachable by
815
+ keyboard alone. :focus-visible, so a mouse click doesn't ring it.
816
+
817
+ 🚨 DELIBERATELY --_chip-tone, NOT --vms-accent (which is what
818
+ .vms-field__input:focus uses, and what this rule used before Plan 21-09's
819
+ hand-check). Measured, not assumed: --vms-accent clears the 3:1 non-text bar
820
+ (SC 1.4.11) against --vms-surface on all 13 targets, but only BARELY on
821
+ light-amber/light-green/light-teal (3.34 / 3.23 / 3.28). The chip's fill is a
822
+ TINTED surface, and that tint eats the entire remaining margin — the accent
823
+ ring measures 2.63 / 2.54 / 2.58 against the chip fill on those three themes:
824
+ a FAILING focus indicator on a keyboard-only affordance.
825
+
826
+ The mix ratio is NOT the lever, which is the non-obvious part: the ring still
827
+ fails at a 4% tint and only reaches 3.10:1 at 2% — a chip with no visible fill
828
+ at all. No chip worth rendering can rescue the accent here.
829
+
830
+ --_chip-tone is the structural fix and needs no per-theme rule: it is the same
831
+ polarity-adaptive knockout pair as the chip's TEXT (mixing toward
832
+ --vms-surface), so it inherits that pair's measured headroom — 10.63:1 worst
833
+ (dark-*), 13.60:1 (light-*), vs a 3:1 bar. It is also what the chip's own
834
+ private-tone technique already does everywhere else: .vms-field__chip--armed
835
+ borders with it and this button's `color` inherits it. The accent ring was the
836
+ outlier, not the convention. Re-measure if --_chip-tone ever stops being
837
+ --vms-text. */
838
+ .vms-field__chip-remove:focus-visible {
839
+ outline: 2px solid var(--_chip-tone);
840
+ outline-offset: 1px;
841
+ }
842
+
577
843
  /* Form-collected checkbox (FieldNode inputType="checkbox") — inline label. */
578
844
  .vms-field--checkbox { flex-direction: row; align-items: center; gap: var(--vms-space-xs); }
579
845
  .vms-field--checkbox .vms-field__input {
@@ -650,6 +916,16 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
650
916
  border-color: var(--_btn-tone);
651
917
  color: #fff;
652
918
  }
919
+ /* Warning is the polarity exception: a filled warning button uses the bright fill +
920
+ dark label (white on warning fails AA — 3.2/2.14:1 light/dark; see --vms-warning-fill).
921
+ Border matches the fill (no rim) — same as every other primary tone, which is the
922
+ conventional filled-warning look (cf. Bootstrap .btn-warning). The dark label + filled
923
+ shape identify the control; a dark rim read as out-of-place next to danger/success/info. */
924
+ .vms-button--warning.vms-button--primary {
925
+ background: var(--vms-warning-fill);
926
+ border-color: var(--vms-warning-fill);
927
+ color: var(--vms-on-warning-fill);
928
+ }
653
929
  .vms-button--primary:hover { filter: brightness(1.1); }
654
930
  .vms-button--secondary {
655
931
  background: transparent;
@@ -1103,7 +1379,10 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
1103
1379
  success/info are deepened (color-mix w/ black) so white at 14px also clears
1104
1380
  4.5:1 (their pure tokens sit at 3.2 / 4.4:1). */
1105
1381
  .vms-toast--danger { background: var(--vms-error); color: #fff; }
1106
- .vms-toast--warning { background: var(--vms-warning); color: #fff; }
1382
+ /* Warning uses the bright fill + dark foreground (see --vms-warning-fill): white on
1383
+ the dark #8a630d was muddy on light and white on the bright #e0a823 fails AA on dark
1384
+ (2.14:1). Dark text on the bright fill is 7.37:1 and reads as an attention-grabbing yellow. */
1385
+ .vms-toast--warning { background: var(--vms-warning-fill); color: var(--vms-on-warning-fill); }
1107
1386
  .vms-toast--success { background: color-mix(in srgb, var(--vms-success) 80%, #000); color: #fff; }
1108
1387
  .vms-toast--info { background: color-mix(in srgb, var(--vms-info) 88%, #000); color: #fff; }
1109
1388
  /* fade-out applied just before removal. */
@@ -1178,6 +1457,14 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
1178
1457
  border-color: var(--_badge-tone);
1179
1458
  color: #fff;
1180
1459
  }
1460
+ /* Warning polarity exception: a filled warning badge uses the bright fill + dark text
1461
+ (white on warning fails AA; see --vms-warning-fill). Border matches the fill (no rim),
1462
+ like every other primary tone. */
1463
+ .vms-badge--warning.vms-badge--primary {
1464
+ background: var(--vms-warning-fill);
1465
+ border-color: var(--vms-warning-fill);
1466
+ color: var(--vms-on-warning-fill);
1467
+ }
1181
1468
  .vms-badge--secondary {
1182
1469
  background: transparent;
1183
1470
  border-color: var(--_badge-tone);
@@ -1305,6 +1592,52 @@ button.vms-breadcrumb__link {
1305
1592
  }
1306
1593
  .vms-steps__label { color: var(--vms-text); font-weight: 600; }
1307
1594
  .vms-steps__description { color: var(--vms-text-muted); font-size: var(--vms-text-xs); }
1595
+ /* Per-step tone (StepItem.tone) — overlays the universal status color onto the
1596
+ marker, ORTHOGONAL to the derived done/current/upcoming treatment: the tone
1597
+ recolors whatever the state already does (filled states fill in the tone;
1598
+ upcoming stays outlined but in the tone), so a failed/blocked stage reads red
1599
+ without losing the reached/not-reached distinction. Two-class selectors
1600
+ outspecify the base state rules. The knockout glyph stays --vms-surface
1601
+ (polarity-adaptive, >=3:1 on the filled tone tokens across all themes — a
1602
+ graphical indicator whose state also rides aria-label + glyph + label text). */
1603
+ .vms-steps__step--tone-danger { --_tone: var(--vms-error); }
1604
+ .vms-steps__step--tone-warning { --_tone: var(--vms-warning); }
1605
+ .vms-steps__step--tone-success { --_tone: var(--vms-success); }
1606
+ .vms-steps__step--tone-info { --_tone: var(--vms-info); }
1607
+ /* Tone recolors the DERIVED treatment: filled states (done/current) fill in the tone;
1608
+ upcoming stays outlined but in the tone — preserving the reached/not-reached read while
1609
+ overlaying status color. Two-class selectors outspecify the base state rules. */
1610
+ .vms-steps__step--toned.vms-steps__step--done .vms-steps__marker,
1611
+ .vms-steps__step--toned.vms-steps__step--current .vms-steps__marker {
1612
+ background: var(--_tone);
1613
+ border-color: var(--_tone);
1614
+ color: var(--vms-surface);
1615
+ }
1616
+ .vms-steps__step--toned.vms-steps__step--upcoming .vms-steps__marker {
1617
+ border-color: var(--_tone);
1618
+ color: var(--_tone);
1619
+ }
1620
+ .vms-steps__step--toned.vms-steps__step--current .vms-steps__marker {
1621
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--_tone) 30%, transparent);
1622
+ }
1623
+ /* Warning FILLED (done/current): bright fill + dark glyph, no rim (see --vms-warning-fill). */
1624
+ .vms-steps__step--tone-warning.vms-steps__step--done .vms-steps__marker,
1625
+ .vms-steps__step--tone-warning.vms-steps__step--current .vms-steps__marker {
1626
+ background: var(--vms-warning-fill);
1627
+ border-color: var(--vms-warning-fill);
1628
+ color: var(--vms-on-warning-fill);
1629
+ }
1630
+ /* Warning UPCOMING (outline): try the BRIGHT amber for the ring + number rather than the
1631
+ dark --vms-warning. Bright amber as a line/digit on white is ~2.14:1 (below the strict
1632
+ bar), so this is a judgment call on visual legibility — the step's state is multi-channel
1633
+ (aria-label + number + position), so color is not its sole carrier. */
1634
+ .vms-steps__step--tone-warning.vms-steps__step--upcoming .vms-steps__marker {
1635
+ border-color: var(--vms-warning-fill);
1636
+ color: var(--vms-warning-fill);
1637
+ }
1638
+ .vms-steps__step--tone-warning.vms-steps__step--current .vms-steps__marker {
1639
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--vms-warning-fill) 35%, transparent);
1640
+ }
1308
1641
 
1309
1642
  /* Connector — the OUTGOING segment from this marker toward the next (hidden on
1310
1643
  the last step). Colored by the step's own state: accent once the step is done,