@ashley-shrok/viewmodel-shell 6.2.1 → 6.3.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.d.ts +10 -0
- package/dist/browser.js +50 -0
- package/dist/index.d.ts +44 -1
- package/dist/server.js +17 -2
- package/package.json +1 -1
- package/styles/default.css +51 -0
package/dist/browser.d.ts
CHANGED
|
@@ -240,4 +240,14 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
240
240
|
* conveyed by color alone. The stepper is NOT focusable and is NOT
|
|
241
241
|
* `role="progressbar"` (that's a continuous %). All text via textContent. */
|
|
242
242
|
private steps;
|
|
243
|
+
/** TrackerNode — a status/heat strip: a tight horizontal row of discrete
|
|
244
|
+
* colored cells, one per time bucket. The framework owns ALL appearance and
|
|
245
|
+
* a11y (never on the wire): the hairline gap, the intrinsic
|
|
246
|
+
* shrink-to-a-min-then-scroll overflow, and the baked colorblind-safe palette
|
|
247
|
+
* (success=blue / danger=red / warning=amber / muted=gray) via the
|
|
248
|
+
* .vms-tracker__cell--{state} classes. A cell with a `label` carries it as both
|
|
249
|
+
* the native tooltip AND aria-label (meaning as text, not color-only). A cell
|
|
250
|
+
* with an `action` becomes a role="button" tabstop with Enter/Space activation
|
|
251
|
+
* (Space suppresses page scroll), mirroring TableRow.action / SectionNode.action. */
|
|
252
|
+
private tracker;
|
|
243
253
|
}
|
package/dist/browser.js
CHANGED
|
@@ -508,6 +508,7 @@ export class BrowserAdapter {
|
|
|
508
508
|
case "chart": return this.chart(n, parent);
|
|
509
509
|
case "breadcrumb": return this.breadcrumb(n, parent, on);
|
|
510
510
|
case "steps": return this.steps(n, parent);
|
|
511
|
+
case "tracker": return this.tracker(n, parent, on);
|
|
511
512
|
default: {
|
|
512
513
|
// Fail loud, not silent (AGENTS.md: "Nothing important fails quietly").
|
|
513
514
|
// Runtime trees are server-controlled JSON, so an unknown/forward-version
|
|
@@ -3491,4 +3492,53 @@ export class BrowserAdapter {
|
|
|
3491
3492
|
});
|
|
3492
3493
|
parent.appendChild(ol);
|
|
3493
3494
|
}
|
|
3495
|
+
/** TrackerNode — a status/heat strip: a tight horizontal row of discrete
|
|
3496
|
+
* colored cells, one per time bucket. The framework owns ALL appearance and
|
|
3497
|
+
* a11y (never on the wire): the hairline gap, the intrinsic
|
|
3498
|
+
* shrink-to-a-min-then-scroll overflow, and the baked colorblind-safe palette
|
|
3499
|
+
* (success=blue / danger=red / warning=amber / muted=gray) via the
|
|
3500
|
+
* .vms-tracker__cell--{state} classes. A cell with a `label` carries it as both
|
|
3501
|
+
* the native tooltip AND aria-label (meaning as text, not color-only). A cell
|
|
3502
|
+
* with an `action` becomes a role="button" tabstop with Enter/Space activation
|
|
3503
|
+
* (Space suppresses page scroll), mirroring TableRow.action / SectionNode.action. */
|
|
3504
|
+
tracker(n, parent, on) {
|
|
3505
|
+
const strip = document.createElement("div");
|
|
3506
|
+
strip.className = "vms-tracker";
|
|
3507
|
+
if (n.id != null)
|
|
3508
|
+
strip.id = n.id;
|
|
3509
|
+
// The strip is a graphical status summary — expose it as an img group with a
|
|
3510
|
+
// label so a screen reader announces it as one thing; per-cell state is on
|
|
3511
|
+
// each cell's aria-label.
|
|
3512
|
+
strip.setAttribute("role", "img");
|
|
3513
|
+
strip.setAttribute("aria-label", "status tracker");
|
|
3514
|
+
for (const cell of n.cells) {
|
|
3515
|
+
const state = cell.state ?? "muted";
|
|
3516
|
+
const el = document.createElement("div");
|
|
3517
|
+
el.className = `vms-tracker__cell vms-tracker__cell--${state}`;
|
|
3518
|
+
// label → native tooltip + aria-label (non-color channel). When absent,
|
|
3519
|
+
// the state name is still the a11y fallback so a cell is never color-only.
|
|
3520
|
+
const aria = cell.label != null && cell.label !== "" ? cell.label : state;
|
|
3521
|
+
el.setAttribute("aria-label", aria);
|
|
3522
|
+
if (cell.label != null && cell.label !== "")
|
|
3523
|
+
el.title = cell.label;
|
|
3524
|
+
if (cell.action) {
|
|
3525
|
+
const action = cell.action;
|
|
3526
|
+
el.classList.add("vms-tracker__cell--clickable");
|
|
3527
|
+
el.tabIndex = 0;
|
|
3528
|
+
el.setAttribute("role", "button");
|
|
3529
|
+
el.addEventListener("click", () => { on(action); });
|
|
3530
|
+
el.addEventListener("keydown", (e) => {
|
|
3531
|
+
if (e.key === "Enter") {
|
|
3532
|
+
on(action);
|
|
3533
|
+
}
|
|
3534
|
+
else if (e.key === " " || e.key === "Spacebar") {
|
|
3535
|
+
e.preventDefault(); // suppress page scroll
|
|
3536
|
+
on(action);
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
strip.appendChild(el);
|
|
3541
|
+
}
|
|
3542
|
+
parent.appendChild(strip);
|
|
3543
|
+
}
|
|
3494
3544
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface Adapter {
|
|
|
103
103
|
* concept). */
|
|
104
104
|
reload?(): void;
|
|
105
105
|
}
|
|
106
|
-
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode | DividerNode | FitsNode | EmptyStateNode | BadgeNode | ChartNode | BreadcrumbNode | StepsNode;
|
|
106
|
+
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode | DividerNode | FitsNode | EmptyStateNode | BadgeNode | ChartNode | BreadcrumbNode | StepsNode | TrackerNode;
|
|
107
107
|
export interface PageNode {
|
|
108
108
|
type: "page";
|
|
109
109
|
title?: string;
|
|
@@ -1087,6 +1087,49 @@ export interface StepsNode {
|
|
|
1087
1087
|
* left, connector running down, descriptions beside each step). */
|
|
1088
1088
|
orientation?: "horizontal" | "vertical";
|
|
1089
1089
|
}
|
|
1090
|
+
/** One bucket in a TrackerNode strip — a single time slot's status. Carries only
|
|
1091
|
+
* display data: the semantic state, an optional hover label, and an optional
|
|
1092
|
+
* click-through action. */
|
|
1093
|
+
export interface TrackerCell {
|
|
1094
|
+
/** Semantic status for this bucket. A CLOSED set specific to a status strip:
|
|
1095
|
+
* `success` (good), `danger` (bad), `warning` (soft flag), `muted` (no data /
|
|
1096
|
+
* no run this slot — the universal honest-uncertainty convention). Omitted =
|
|
1097
|
+
* `muted`. NOTE the color axis differs from the rest of the framework on
|
|
1098
|
+
* PURPOSE: a dense, color-ONLY strip must survive colorblindness by color
|
|
1099
|
+
* alone, so the framework bakes a colorblind-safe palette where `success`
|
|
1100
|
+
* renders BLUE (not the global green success tone), `danger` red, `warning`
|
|
1101
|
+
* amber, `muted` gray — no "colorblind mode" needed. Only the rendered color
|
|
1102
|
+
* diverges; the state NAME stays semantic on the wire (agent-legible). `info`
|
|
1103
|
+
* is intentionally NOT a member — it would collide with `success`=blue and has
|
|
1104
|
+
* no meaning in a pass/fail history. */
|
|
1105
|
+
state?: "success" | "danger" | "warning" | "muted";
|
|
1106
|
+
/** Optional hover label for this bucket (e.g. "2026-07-15 14:22 UTC · Success").
|
|
1107
|
+
* Rendered as the cell's native tooltip AND its aria-label, so the strip's
|
|
1108
|
+
* meaning is carried by TEXT, not color alone — the a11y + agent-legibility
|
|
1109
|
+
* channel a color-only glyph strip could never provide. */
|
|
1110
|
+
label?: string;
|
|
1111
|
+
/** Optional click-through: dispatches this action when the bucket is clicked or
|
|
1112
|
+
* keyboard-activated (Enter / Space — Space preventDefaults page scroll). Makes
|
|
1113
|
+
* the cell a `role="button"` tabstop. Per-bucket identity is encoded in the
|
|
1114
|
+
* action name (e.g. `open-run-4021`), consistent with `TableRow.action` — no
|
|
1115
|
+
* context field. Omitted = a non-interactive status swatch (no tabstop). */
|
|
1116
|
+
action?: ActionEvent;
|
|
1117
|
+
}
|
|
1118
|
+
/** A status tracker / heat strip — a tight horizontal row of discrete colored
|
|
1119
|
+
* cells, one per time bucket, where color encodes each bucket's semantic status.
|
|
1120
|
+
* This is the "uptime strip" / "sentinel history" primitive (industry precedent:
|
|
1121
|
+
* Tremor Tracker, Grafana Status History, Statuspage), NOT a numeric value
|
|
1122
|
+
* sparkline (a tiny line chart — a separate, chart-family concern, deliberately
|
|
1123
|
+
* not built here). The framework owns ALL appearance and a11y: the hairline gap,
|
|
1124
|
+
* the intrinsic shrink-to-a-min-then-scroll overflow (zero viewport
|
|
1125
|
+
* breakpoints), and the baked colorblind-safe palette (see TrackerCell.state).
|
|
1126
|
+
* Bucket count is simply `cells.length` (60 slots, 24 hourly, 7 daily…). */
|
|
1127
|
+
export interface TrackerNode {
|
|
1128
|
+
type: "tracker";
|
|
1129
|
+
id?: string;
|
|
1130
|
+
/** Ordered buckets, oldest → newest (rendered left → right). */
|
|
1131
|
+
cells: TrackerCell[];
|
|
1132
|
+
}
|
|
1090
1133
|
export interface ShellOptions {
|
|
1091
1134
|
endpoint: string;
|
|
1092
1135
|
actionEndpoint: string;
|
package/dist/server.js
CHANGED
|
@@ -224,6 +224,20 @@ function collectActions(node, enclosingForm, out) {
|
|
|
224
224
|
}
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
|
+
case "tracker": {
|
|
228
|
+
// A TrackerCell can carry an optional per-bucket click-through action
|
|
229
|
+
// (per-bucket identity in the name, like row.action). Each is a
|
|
230
|
+
// dispatch-bearing descendant, so the uniqueness collector MUST descend
|
|
231
|
+
// into the cells — otherwise tracker actions are silently exempt from the
|
|
232
|
+
// one-name-one-operation rule (the missed-walk failure class). Guard the
|
|
233
|
+
// optional action the way the empty-state / breadcrumb arms do.
|
|
234
|
+
const tracker = node;
|
|
235
|
+
for (const cell of tracker.cells) {
|
|
236
|
+
if (cell.action)
|
|
237
|
+
recordAction(cell.action, enclosingForm, out);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
227
241
|
// Nodes with no dispatch-bearing actions of their own:
|
|
228
242
|
// text, link, image, stat-bar, progress, copy-button, badge, chart, steps
|
|
229
243
|
// (breadcrumb crumb actions ARE recorded above via the "breadcrumb" arm)
|
|
@@ -394,8 +408,9 @@ function walkForSectionAction(node, outerInteractive) {
|
|
|
394
408
|
return;
|
|
395
409
|
}
|
|
396
410
|
// Leaf-like nodes (field, checkbox, button, text, link, image, stat-bar,
|
|
397
|
-
// tabs, progress, table, copy-button, badge, chart, breadcrumb, steps
|
|
398
|
-
// no SectionNode descendants —
|
|
411
|
+
// tabs, progress, table, copy-button, badge, chart, breadcrumb, steps,
|
|
412
|
+
// tracker) carry no SectionNode descendants — TrackerNode cells hold plain
|
|
413
|
+
// { state, label, action } records; TableNode rows hold strings + per-row controls,
|
|
399
414
|
// not sections; ChartNode (CHART-05) is a childless/action-free data leaf;
|
|
400
415
|
// BreadcrumbNode/StepsNode (NAV-01..03) hold plain { label, ... } records,
|
|
401
416
|
// not ViewNode children, so no recursion is needed here (deliberate, not a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.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 — a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/styles/default.css
CHANGED
|
@@ -1724,3 +1724,54 @@ button.vms-breadcrumb__link {
|
|
|
1724
1724
|
transform: translateX(-1px);
|
|
1725
1725
|
}
|
|
1726
1726
|
}
|
|
1727
|
+
|
|
1728
|
+
/* ── Status tracker / heat strip (TrackerNode) ──
|
|
1729
|
+
A tight horizontal row of discrete colored cells, one per time bucket (uptime
|
|
1730
|
+
strip / sentinel history). The palette is a FIXED, colorblind-safe scheme —
|
|
1731
|
+
blue=good / red=bad / amber=warn / gray=no-data — deliberately NOT the theme's
|
|
1732
|
+
success(green)/danger(red) tones: a dense, color-ONLY strip has to survive
|
|
1733
|
+
colorblindness by color alone (verified separable under deuteranopia /
|
|
1734
|
+
protanopia / tritanopia), so "blue means good" is baked in and needs no mode.
|
|
1735
|
+
It is intentionally the SAME across all themes for a11y consistency (only the
|
|
1736
|
+
no-data gray adapts to the surface). Overflow is intrinsic: cells flex-shrink
|
|
1737
|
+
to a min, then the strip scrolls (zero viewport breakpoints). Overridable via
|
|
1738
|
+
the --vms-tracker-* seam like any other token. */
|
|
1739
|
+
:root {
|
|
1740
|
+
/* Fixed palette, verified separable (deltaE >= 28 worst-pair) under normal +
|
|
1741
|
+
deuteranopia + protanopia + tritanopia. fail is a deep red and warn a bright
|
|
1742
|
+
amber ON PURPOSE — under red-green colorblindness red and amber converge in
|
|
1743
|
+
hue, so they're separated by LIGHTNESS (deep vs bright), which the achromatic
|
|
1744
|
+
channel preserves. pass(blue)/fail is the huge, always-obvious pair. */
|
|
1745
|
+
--vms-tracker-pass: #4a9eff; /* good — blue anchor */
|
|
1746
|
+
--vms-tracker-fail: #cc2936; /* bad — deep red */
|
|
1747
|
+
--vms-tracker-warn: #f2c94c; /* warn — bright amber */
|
|
1748
|
+
/* no-data recedes into the surface, adapting per theme (light→light gray, dark→dark gray) */
|
|
1749
|
+
--vms-tracker-nodata: color-mix(in srgb, var(--vms-text-muted) 42%, var(--vms-surface));
|
|
1750
|
+
}
|
|
1751
|
+
.vms-tracker {
|
|
1752
|
+
display: flex;
|
|
1753
|
+
gap: 1px; /* hairline separation — cells read as discrete, not one smeared bar */
|
|
1754
|
+
align-items: stretch;
|
|
1755
|
+
width: 100%;
|
|
1756
|
+
overflow-x: auto; /* shrink-to-a-min-then-scroll (see cell min-width) */
|
|
1757
|
+
border-radius: var(--vms-radius);
|
|
1758
|
+
}
|
|
1759
|
+
.vms-tracker__cell {
|
|
1760
|
+
flex: 1 1 0;
|
|
1761
|
+
min-width: 3px; /* legibility floor; once cells hit it the strip scrolls instead of vanishing */
|
|
1762
|
+
height: 2rem;
|
|
1763
|
+
background: var(--vms-tracker-nodata);
|
|
1764
|
+
}
|
|
1765
|
+
.vms-tracker__cell--success { background: var(--vms-tracker-pass); }
|
|
1766
|
+
.vms-tracker__cell--danger { background: var(--vms-tracker-fail); }
|
|
1767
|
+
.vms-tracker__cell--warning { background: var(--vms-tracker-warn); }
|
|
1768
|
+
.vms-tracker__cell--muted { background: var(--vms-tracker-nodata); }
|
|
1769
|
+
/* clickable cell: gentle brighten on hover (NO border), keyboard focus ring for a11y */
|
|
1770
|
+
.vms-tracker__cell--clickable { cursor: pointer; }
|
|
1771
|
+
.vms-tracker__cell--clickable:hover { filter: brightness(1.32); }
|
|
1772
|
+
.vms-tracker__cell--clickable:focus-visible {
|
|
1773
|
+
outline: 2px solid var(--vms-accent);
|
|
1774
|
+
outline-offset: 1px;
|
|
1775
|
+
position: relative; /* lift the ring above neighbors */
|
|
1776
|
+
z-index: 1;
|
|
1777
|
+
}
|