@ashley-shrok/viewmodel-shell 5.2.0 → 6.1.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/dist/browser.js CHANGED
@@ -954,8 +954,8 @@ export class BrowserAdapter {
954
954
  parent.appendChild(el);
955
955
  }
956
956
  list(n, parent, on) {
957
- const ul = document.createElement("ul");
958
- ul.className = "vms-list";
957
+ const ul = document.createElement(n.ordered ? "ol" : "ul");
958
+ ul.className = `vms-list${n.ordered ? " vms-list--ordered" : ""}`;
959
959
  if (n.id)
960
960
  ul.id = n.id;
961
961
  this.kids(n.children, ul, on);
@@ -2880,10 +2880,12 @@ export class BrowserAdapter {
2880
2880
  bar.className = "vms-stat-bar";
2881
2881
  n.stats.forEach(stat => {
2882
2882
  const item = document.createElement("div");
2883
- item.className = "vms-stat-bar__item";
2883
+ item.className = stat.tone
2884
+ ? `vms-stat-bar__item vms-stat-bar__item--toned vms-stat-bar__item--tone-${stat.tone}`
2885
+ : "vms-stat-bar__item";
2884
2886
  const val = document.createElement("span");
2885
2887
  val.className = "vms-stat-bar__value";
2886
- val.textContent = String(stat.value);
2888
+ val.textContent = stat.value;
2887
2889
  const lbl = document.createElement("span");
2888
2890
  lbl.className = "vms-stat-bar__label";
2889
2891
  lbl.textContent = stat.label;
@@ -3405,7 +3407,9 @@ export class BrowserAdapter {
3405
3407
  n.steps.forEach((step, i) => {
3406
3408
  const state = i < n.current ? "done" : i === n.current ? "current" : "upcoming";
3407
3409
  const li = document.createElement("li");
3408
- li.className = `vms-steps__step vms-steps__step--${state}`;
3410
+ li.className = step.tone
3411
+ ? `vms-steps__step vms-steps__step--${state} vms-steps__step--toned vms-steps__step--tone-${step.tone}`
3412
+ : `vms-steps__step vms-steps__step--${state}`;
3409
3413
  if (state === "current")
3410
3414
  li.setAttribute("aria-current", "step");
3411
3415
  // Connector — CSS-drawn line marker-center to marker-center, behind the
package/dist/index.d.ts CHANGED
@@ -233,6 +233,14 @@ export interface SectionNode {
233
233
  export interface ListNode {
234
234
  type: "list";
235
235
  id?: string;
236
+ /** When true, this is an ORDERED list — renders `<ol>` (numbered) instead of
237
+ * `<ul>`. The semantic element gives real ordinal meaning (a11y: screen
238
+ * readers announce position/count; agent-legibility: the order is structural,
239
+ * not baked into item text). The visible "1." / "2." markers come from a CSS
240
+ * counter (`.vms-list--ordered`), not native list markers, because `.vms-list`
241
+ * is a flex column with `list-style:none` — so numbering survives the styled
242
+ * list-item layout. Omitted/false = `<ul>` (byte-identical to today). */
243
+ ordered?: boolean;
236
244
  children: ViewNode[];
237
245
  }
238
246
  export interface ListItemNode {
@@ -722,7 +730,21 @@ export interface StatBarNode {
722
730
  type: "stat-bar";
723
731
  stats: Array<{
724
732
  label: string;
725
- value: string | number;
733
+ /** The stat's value. Typed `string` (not `string | number`) so the TS and
734
+ * .NET backends emit BYTE-IDENTICAL wire — a bare number would serialize as
735
+ * JSON `12` in TS but the .NET twin's `string Value` can only emit `"12"`,
736
+ * a real cross-backend drift the parity suite never exercised. Format the
737
+ * number server-side (`String(n)`, `n.toFixed(2)`, `$${n}`) — the value is
738
+ * display text, and any formatting an app wants is richer than a bare
739
+ * number anyway. (Narrowed in 6.0.0 — see CHANGELOG; the field was unused.) */
740
+ value: string;
741
+ /** Optional semantic status tone for this tile — the universal status color
742
+ * axis (same closed set as Section/Button/ListItem/TableRow). Renders the
743
+ * tile as a subtly tinted chip (surface tint + colored border, reusing the
744
+ * --vms-error/-warning/-success/-info tokens), so an unhealthy stat reads at
745
+ * a glance rather than via one small line of text. Omitted = today's neutral
746
+ * inline stat. Closed union. */
747
+ tone?: "danger" | "warning" | "success" | "info";
726
748
  }>;
727
749
  }
728
750
  export interface TabsNode {
@@ -993,6 +1015,16 @@ export interface StepItem {
993
1015
  label: string;
994
1016
  /** Optional one-line supporting text shown beside/under the label. */
995
1017
  description?: string;
1018
+ /** Optional semantic status tone for this step — the universal status color
1019
+ * axis (same closed set as Section/Button/ListItem/TableRow). ORTHOGONAL to
1020
+ * the done/current/upcoming state the framework DERIVES from `current`: tone
1021
+ * overlays a semantic color onto the marker (e.g. a failed/blocked stage read
1022
+ * as `danger`, a stage needing attention as `warning`) regardless of its
1023
+ * position in the sequence. Omitted = the derived state's default appearance
1024
+ * (accent for done/current, muted for upcoming). Because tone is app-authored
1025
+ * status reinforced by the step's own label text, it is NOT color-only (same
1026
+ * posture as Section tone). Closed union. */
1027
+ tone?: "danger" | "warning" | "success" | "info";
996
1028
  }
997
1029
  /** A discrete step / stepper / wizard progress indicator — an ordered list of
998
1030
  * stages with a single 0-based `current` index. Per-step status DERIVES from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "5.2.0",
3
+ "version": "6.1.0",
4
4
  "description": "A server-driven UI framework where the wire format is structured enough that agents can build full-stack apps without ever opening a browser and all UI tests are pure unit tests with no browser runtime. Server returns a JSON tree of typed nodes; a thin TypeScript adapter renders it to DOM. Backend-agnostic \u2014 a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,6 +31,8 @@
31
31
  "scripts": {
32
32
  "build": "tsc -b tsconfig.tui.json",
33
33
  "check:core-globals": "node scripts/check-core-platform-globals.mjs",
34
+ "check:test-types": "tsc -p tsconfig.test.json --noEmit",
35
+ "check:demo-types": "node scripts/check-demo-types.mjs",
34
36
  "check:aa-contrast": "node scripts/check-aa-contrast.mjs",
35
37
  "check:no-demo-style": "node scripts/check-no-demo-style.mjs",
36
38
  "check:theme-byte-identity": "node scripts/check-theme-byte-identity.mjs",
@@ -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; }
@@ -875,6 +916,16 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
875
916
  border-color: var(--_btn-tone);
876
917
  color: #fff;
877
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
+ }
878
929
  .vms-button--primary:hover { filter: brightness(1.1); }
879
930
  .vms-button--secondary {
880
931
  background: transparent;
@@ -992,6 +1043,22 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
992
1043
  .vms-list-item--success:hover { border-left-color: var(--vms-success); }
993
1044
  .vms-list-item--info:hover { border-left-color: var(--vms-info); }
994
1045
 
1046
+ /* Ordered list — <ol>. `.vms-list` sets list-style:none for the styled
1047
+ flex-column layout, so native markers never show; a CSS counter supplies the
1048
+ number as the first flex child of each item (spaced by the item's own gap).
1049
+ tabular-nums + right-aligned min-width keeps multi-digit numbers aligned. */
1050
+ .vms-list--ordered { counter-reset: vms-li; }
1051
+ .vms-list--ordered > .vms-list-item { counter-increment: vms-li; }
1052
+ .vms-list--ordered > .vms-list-item::before {
1053
+ content: counter(vms-li) ".";
1054
+ flex: 0 0 auto;
1055
+ min-width: 1.5em;
1056
+ text-align: right;
1057
+ font-variant-numeric: tabular-nums;
1058
+ font-weight: 600;
1059
+ color: var(--vms-text-muted);
1060
+ }
1061
+
995
1062
  /* Selection state for master-detail / list-detail patterns (D-27). The
996
1063
  currently-selected row. Stronger than the semantic hints (selection is
997
1064
  the primary state) and stable through hover. Themable: uses only seam
@@ -1328,7 +1395,10 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
1328
1395
  success/info are deepened (color-mix w/ black) so white at 14px also clears
1329
1396
  4.5:1 (their pure tokens sit at 3.2 / 4.4:1). */
1330
1397
  .vms-toast--danger { background: var(--vms-error); color: #fff; }
1331
- .vms-toast--warning { background: var(--vms-warning); color: #fff; }
1398
+ /* Warning uses the bright fill + dark foreground (see --vms-warning-fill): white on
1399
+ the dark #8a630d was muddy on light and white on the bright #e0a823 fails AA on dark
1400
+ (2.14:1). Dark text on the bright fill is 7.37:1 and reads as an attention-grabbing yellow. */
1401
+ .vms-toast--warning { background: var(--vms-warning-fill); color: var(--vms-on-warning-fill); }
1332
1402
  .vms-toast--success { background: color-mix(in srgb, var(--vms-success) 80%, #000); color: #fff; }
1333
1403
  .vms-toast--info { background: color-mix(in srgb, var(--vms-info) 88%, #000); color: #fff; }
1334
1404
  /* fade-out applied just before removal. */
@@ -1403,6 +1473,14 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
1403
1473
  border-color: var(--_badge-tone);
1404
1474
  color: #fff;
1405
1475
  }
1476
+ /* Warning polarity exception: a filled warning badge uses the bright fill + dark text
1477
+ (white on warning fails AA; see --vms-warning-fill). Border matches the fill (no rim),
1478
+ like every other primary tone. */
1479
+ .vms-badge--warning.vms-badge--primary {
1480
+ background: var(--vms-warning-fill);
1481
+ border-color: var(--vms-warning-fill);
1482
+ color: var(--vms-on-warning-fill);
1483
+ }
1406
1484
  .vms-badge--secondary {
1407
1485
  background: transparent;
1408
1486
  border-color: var(--_badge-tone);
@@ -1530,6 +1608,52 @@ button.vms-breadcrumb__link {
1530
1608
  }
1531
1609
  .vms-steps__label { color: var(--vms-text); font-weight: 600; }
1532
1610
  .vms-steps__description { color: var(--vms-text-muted); font-size: var(--vms-text-xs); }
1611
+ /* Per-step tone (StepItem.tone) — overlays the universal status color onto the
1612
+ marker, ORTHOGONAL to the derived done/current/upcoming treatment: the tone
1613
+ recolors whatever the state already does (filled states fill in the tone;
1614
+ upcoming stays outlined but in the tone), so a failed/blocked stage reads red
1615
+ without losing the reached/not-reached distinction. Two-class selectors
1616
+ outspecify the base state rules. The knockout glyph stays --vms-surface
1617
+ (polarity-adaptive, >=3:1 on the filled tone tokens across all themes — a
1618
+ graphical indicator whose state also rides aria-label + glyph + label text). */
1619
+ .vms-steps__step--tone-danger { --_tone: var(--vms-error); }
1620
+ .vms-steps__step--tone-warning { --_tone: var(--vms-warning); }
1621
+ .vms-steps__step--tone-success { --_tone: var(--vms-success); }
1622
+ .vms-steps__step--tone-info { --_tone: var(--vms-info); }
1623
+ /* Tone recolors the DERIVED treatment: filled states (done/current) fill in the tone;
1624
+ upcoming stays outlined but in the tone — preserving the reached/not-reached read while
1625
+ overlaying status color. Two-class selectors outspecify the base state rules. */
1626
+ .vms-steps__step--toned.vms-steps__step--done .vms-steps__marker,
1627
+ .vms-steps__step--toned.vms-steps__step--current .vms-steps__marker {
1628
+ background: var(--_tone);
1629
+ border-color: var(--_tone);
1630
+ color: var(--vms-surface);
1631
+ }
1632
+ .vms-steps__step--toned.vms-steps__step--upcoming .vms-steps__marker {
1633
+ border-color: var(--_tone);
1634
+ color: var(--_tone);
1635
+ }
1636
+ .vms-steps__step--toned.vms-steps__step--current .vms-steps__marker {
1637
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--_tone) 30%, transparent);
1638
+ }
1639
+ /* Warning FILLED (done/current): bright fill + dark glyph, no rim (see --vms-warning-fill). */
1640
+ .vms-steps__step--tone-warning.vms-steps__step--done .vms-steps__marker,
1641
+ .vms-steps__step--tone-warning.vms-steps__step--current .vms-steps__marker {
1642
+ background: var(--vms-warning-fill);
1643
+ border-color: var(--vms-warning-fill);
1644
+ color: var(--vms-on-warning-fill);
1645
+ }
1646
+ /* Warning UPCOMING (outline): try the BRIGHT amber for the ring + number rather than the
1647
+ dark --vms-warning. Bright amber as a line/digit on white is ~2.14:1 (below the strict
1648
+ bar), so this is a judgment call on visual legibility — the step's state is multi-channel
1649
+ (aria-label + number + position), so color is not its sole carrier. */
1650
+ .vms-steps__step--tone-warning.vms-steps__step--upcoming .vms-steps__marker {
1651
+ border-color: var(--vms-warning-fill);
1652
+ color: var(--vms-warning-fill);
1653
+ }
1654
+ .vms-steps__step--tone-warning.vms-steps__step--current .vms-steps__marker {
1655
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--vms-warning-fill) 35%, transparent);
1656
+ }
1533
1657
 
1534
1658
  /* Connector — the OUTGOING segment from this marker toward the next (hidden on
1535
1659
  the last step). Colored by the step's own state: accent once the step is done,