@ashley-shrok/viewmodel-shell 2.0.0 → 3.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.
package/dist/browser.js CHANGED
@@ -353,7 +353,7 @@ export class BrowserAdapter {
353
353
  this.sectionKeyCounter.set(baseKey, ordinal + 1);
354
354
  const finalKey = `${baseKey}:${ordinal}`;
355
355
  const el = document.createElement("details");
356
- el.className = `vms-section vms-section--collapsible${n.variant === "card" ? " vms-section--card" : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}`;
356
+ el.className = `vms-section vms-section--collapsible${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}`;
357
357
  el.dataset.sectionKey = finalKey;
358
358
  // Initial render is always closed — the post-render restore loop in
359
359
  // render() re-applies `open=true` for keys the user had open before.
@@ -377,7 +377,7 @@ export class BrowserAdapter {
377
377
  // action — see validateSectionAction in server.ts.
378
378
  if (n.link) {
379
379
  const a = document.createElement("a");
380
- a.className = `vms-section vms-section--linked${n.variant === "card" ? " vms-section--card" : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}`;
380
+ a.className = `vms-section vms-section--linked${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}`;
381
381
  a.href = n.link.url;
382
382
  // Mirror LinkNode's external-attribute pattern (browser.ts ~line 666)
383
383
  // byte-for-byte: target=_blank + rel=noopener noreferrer when external.
@@ -421,7 +421,7 @@ export class BrowserAdapter {
421
421
  return;
422
422
  }
423
423
  const el = document.createElement("section");
424
- el.className = `vms-section${n.variant === "card" ? " vms-section--card" : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.action ? " vms-section--clickable" : ""}`;
424
+ el.className = `vms-section${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.action ? " vms-section--clickable" : ""}`;
425
425
  if (n.heading) {
426
426
  const h = document.createElement("h2");
427
427
  h.className = "vms-section__heading";
@@ -480,7 +480,7 @@ export class BrowserAdapter {
480
480
  }
481
481
  listItem(n, parent, on) {
482
482
  const li = document.createElement("li");
483
- li.className = `vms-list-item${n.variant ? ` vms-list-item--${n.variant}` : ""}`;
483
+ li.className = `vms-list-item${n.state ? ` vms-list-item--${n.state}` : ""}${n.tone ? ` vms-list-item--${n.tone}` : ""}`;
484
484
  if (n.id)
485
485
  li.dataset.id = n.id;
486
486
  this.kids(n.children, li, on);
@@ -769,7 +769,7 @@ export class BrowserAdapter {
769
769
  button(n, parent, on) {
770
770
  const btn = document.createElement("button");
771
771
  btn.type = "button";
772
- btn.className = `vms-button${n.variant ? ` vms-button--${n.variant}` : ""}`;
772
+ btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}`;
773
773
  btn.textContent = n.label;
774
774
  btn.addEventListener("click", () => {
775
775
  // pendingLabel: instant client-side feedback. Swap text + add
@@ -786,15 +786,17 @@ export class BrowserAdapter {
786
786
  }
787
787
  text(n, parent) {
788
788
  const el = document.createElement(n.style === "pre" ? "pre" : "span");
789
- el.className = `vms-text${n.style ? ` vms-text--${n.style}` : ""}`;
789
+ el.className = `vms-text${n.style ? ` vms-text--${n.style}` : ""}${n.tone ? ` vms-text--${n.tone}` : ""}`;
790
790
  el.textContent = n.value;
791
791
  parent.appendChild(el);
792
792
  }
793
793
  link(n, parent) {
794
794
  const a = document.createElement("a");
795
- a.className = "vms-link";
795
+ a.className = n.active ? "vms-link vms-link--active" : "vms-link";
796
796
  a.href = n.href;
797
797
  a.textContent = n.label;
798
+ if (n.active)
799
+ a.setAttribute("aria-current", "page");
798
800
  if (n.external) {
799
801
  a.target = "_blank";
800
802
  a.rel = "noopener noreferrer";
@@ -1006,8 +1008,10 @@ export class BrowserAdapter {
1006
1008
  n.rows.forEach(row => {
1007
1009
  const tr = document.createElement("tr");
1008
1010
  let rowClass = "vms-table__row";
1009
- if (row.variant)
1010
- rowClass += ` vms-table__row--${row.variant}`;
1011
+ if (row.state)
1012
+ rowClass += ` vms-table__row--${row.state}`;
1013
+ if (row.tone)
1014
+ rowClass += ` vms-table__row--${row.tone}`;
1011
1015
  if (row.action)
1012
1016
  rowClass += " vms-table__row--clickable";
1013
1017
  tr.className = rowClass;
@@ -1133,7 +1137,7 @@ export class BrowserAdapter {
1133
1137
  copyButton(n, parent) {
1134
1138
  const btn = document.createElement("button");
1135
1139
  btn.type = "button";
1136
- btn.className = `vms-button${n.variant ? ` vms-button--${n.variant}` : ""}`;
1140
+ btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}`;
1137
1141
  btn.textContent = n.label ?? "Copy";
1138
1142
  btn.addEventListener("click", () => {
1139
1143
  const write = navigator.clipboard?.writeText(n.text);
package/dist/index.d.ts CHANGED
@@ -88,8 +88,10 @@ export interface PageNode {
88
88
  export interface SectionNode {
89
89
  type: "section";
90
90
  heading?: string;
91
- /** Section surface variant. Omitted = current behavior (no modifier class). "card" emits .vms-section--card. Closed union (D-03). */
91
+ /** Section surface variant — the structural KIND of the section's surface (the single meaning of "variant" framework-wide). Omitted = current behavior (no modifier class). "card" emits .vms-section--card. Closed union (D-03). */
92
92
  variant?: "card";
93
+ /** Semantic intent/severity tone — the universal status color axis (orthogonal to `variant`; a section can be a `card` AND `tone:"warning"`). Emits .vms-section--{tone} (subtle tinted surface + colored border, reusing the --vms-error/-warning/-success/-info tokens). Omitted = neutral. Closed union. */
94
+ tone?: "danger" | "warning" | "success" | "info";
93
95
  /** Layout preset arranging direct children. Omitted or "stack" = current vertical flow (no modifier class). "split" (equal 2-up), "cards" (uniform grid), "sidebar" (thin + wide app shell), "row" (left-aligned wrapping horizontal row; items hug content), "switcher" (N equal items flipping all-row ↔ all-stack atomically at a content-width `threshold` — the negative-flex-basis primitive a grid cannot express; distinct from `cards` auto-fit which passes through intermediate column counts) emit .vms-section--{value}. Closed union (D-01/D-02; sidebar D-28; row D-30; switcher SWITCH-01). */
94
96
  layout?: "stack" | "split" | "cards" | "sidebar" | "row" | "switcher";
95
97
  /** Main-axis arrangement for `layout:"row"` (the cluster primitive) — maps to `justify-content`. Omitted = no class → the row default (`flex-start`, left-pack) holds = byte-identical to today. Closed union copied verbatim from Jetpack Compose `Arrangement` ∩ Flutter `MainAxisAlignment` (ALIGN-01). Emits .vms-arrange--{value}. */
@@ -186,8 +188,10 @@ export interface ListNode {
186
188
  export interface ListItemNode {
187
189
  type: "list-item";
188
190
  id?: string;
189
- /** Appended as a BEM modifier: vms-list-item--{variant} */
190
- variant?: string;
191
+ /** Row lifecycle/selection STATE (NOT severity — that's `tone`). A freeform, app-extensible token; the framework ships styling for `active` (selected), `done`, `disabled`, `high` (priority), `running`, `moving`. Appended as a BEM modifier: vms-list-item--{state}. Orthogonal to `tone` (a row can be `state:"active"` AND `tone:"danger"`). */
192
+ state?: string;
193
+ /** Semantic intent/severity — the universal status tone axis (closed). Emits .vms-list-item--{tone} (colored accent border, reusing the shared tokens). Omitted = neutral. */
194
+ tone?: "danger" | "warning" | "success" | "info";
191
195
  children: ViewNode[];
192
196
  }
193
197
  export interface FormNode {
@@ -251,7 +255,12 @@ export interface ButtonNode {
251
255
  type: "button";
252
256
  label: string;
253
257
  action: ActionEvent;
254
- variant?: "primary" | "secondary" | "danger";
258
+ /** Visual emphasis (how loud) — `primary` = filled, `secondary` = outline. Orthogonal to `tone` and `size`. Emits .vms-button--{emphasis}. Omitted = the neutral default button. Closed union. */
259
+ emphasis?: "primary" | "secondary";
260
+ /** Semantic intent/severity (what it means) — the universal status color axis, orthogonal to `emphasis`. A destructive primary button is `emphasis:"primary"` + `tone:"danger"`. Emits .vms-button--{tone}. Omitted = neutral. Closed union. */
261
+ tone?: "danger" | "warning" | "success" | "info";
262
+ /** Box geometry (padding + font), orthogonal to color/emphasis — the one axis no design system bakes into variant. Emits .vms-button--{size}. Omitted = the default (md) size. Closed union. */
263
+ size?: "sm" | "lg";
255
264
  /** Transient label shown from click until the dispatch resolves (response
256
265
  * arrives or dispatch errors). Mirrors `CopyButtonNode.copiedLabel`'s
257
266
  * lifecycle pattern at a different beat: shown DURING the round-trip
@@ -263,7 +272,10 @@ export interface ButtonNode {
263
272
  export interface TextNode {
264
273
  type: "text";
265
274
  value: string;
266
- style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "error" | "warning" | "pre";
275
+ /** Typography role only (NOT color) emits .vms-text--{style}. Semantic color moved to `tone` (the old `error`/`warning` style values are now `tone:"danger"`/`tone:"warning"`). Closed union. */
276
+ style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "pre";
277
+ /** Semantic intent/severity color — the universal status tone axis, orthogonal to `style` (a heading can be `tone:"danger"`). Emits .vms-text--{tone}; the tone color wins over a `style` color via source order. Omitted = default text color. Closed union. */
278
+ tone?: "danger" | "warning" | "success" | "info";
267
279
  }
268
280
  export interface LinkNode {
269
281
  type: "link";
@@ -271,6 +283,11 @@ export interface LinkNode {
271
283
  href: string;
272
284
  /** true = open outside current app context (browser: new tab + noopener) */
273
285
  external?: boolean;
286
+ /** true = this link points at the current location (nav "you are here").
287
+ * Emits `.vms-link--active` + `aria-current="page"`. Server-owned: the
288
+ * backend decides which nav item is active from its route/state, exactly
289
+ * like every other view decision — there is no client-side route matching. */
290
+ active?: boolean;
274
291
  }
275
292
  export interface ImageNode {
276
293
  type: "image";
@@ -352,7 +369,10 @@ export interface TableRow {
352
369
  * cell (right). A previous version typed this as `ButtonNode[]` and called
353
370
  * the button renderer blindly, silently dropping non-button entries. */
354
371
  actions?: (ButtonNode | CheckboxNode)[];
355
- variant?: string;
372
+ /** Row lifecycle STATE (NOT severity — that's `tone`). A freeform, app-extensible token; the framework ships styling for `done`, `disabled`, `running`. Emits .vms-table__row--{state}. Orthogonal to `tone`. */
373
+ state?: string;
374
+ /** Semantic intent/severity — the universal status tone axis (closed). Emits .vms-table__row--{tone} (subtle tinted row background, reusing the shared tokens). Omitted = neutral. */
375
+ tone?: "danger" | "warning" | "success" | "info";
356
376
  }
357
377
  export interface TablePagination {
358
378
  /** 1-based current page. */
@@ -405,11 +425,12 @@ export interface CopyButtonNode {
405
425
  label?: string;
406
426
  /** Ephemeral label shown after a successful copy, reverts after ~1.5 s. Adapter default: "Copied!". */
407
427
  copiedLabel?: string;
408
- /** Visual variant — mirrors ButtonNode.variant (issue #14). Adapter emits
409
- * `vms-button vms-button--{variant}` (browser) / variant-tinted text
410
- * (TUI), so a copy-button can read distinctly from neighboring default
411
- * buttons. Closed union; omitted = current behavior (no modifier). */
412
- variant?: "primary" | "secondary" | "danger";
428
+ /** Visual emphasis — mirrors ButtonNode.emphasis. `primary` = filled, `secondary` = outline. Emits .vms-button--{emphasis}. Closed union. */
429
+ emphasis?: "primary" | "secondary";
430
+ /** Semantic intent/severity mirrors ButtonNode.tone. Emits .vms-button--{tone}. Closed union. */
431
+ tone?: "danger" | "warning" | "success" | "info";
432
+ /** Box geometry mirrors ButtonNode.size. Emits .vms-button--{size}. Omitted = md. Closed union. */
433
+ size?: "sm" | "lg";
413
434
  }
414
435
  /**
415
436
  * The SwiftUI `ViewThatFits` port. The renderer picks the FIRST child whose
package/dist/tui.js CHANGED
@@ -855,14 +855,21 @@ const STYLE_ATTRS = {
855
855
  body: {},
856
856
  muted: { fg: "#888888" },
857
857
  strikethrough: { attributes: 16 /* STRIKETHROUGH */, fg: "#888888" },
858
- error: { fg: "#ff5555" },
859
- warning: { fg: "#e0a823" },
860
858
  pre: { fg: "#cccccc" },
861
859
  };
860
+ // Universal semantic-tone → foreground color, shared by text / buttons / list items.
861
+ const TONE_FG = {
862
+ danger: "#ff5555",
863
+ warning: "#e0a823",
864
+ success: "#5fd75f",
865
+ info: "#5fafff",
866
+ };
862
867
  function TextView({ node }) {
863
868
  const style = node.style ?? "body";
864
869
  const attrs = STYLE_ATTRS[style];
865
- return (_jsx("text", { ...(attrs.attributes != null ? { attributes: attrs.attributes } : {}), ...(attrs.fg != null ? { fg: attrs.fg } : {}), children: node.value }));
870
+ // tone color wins over the style's fg (mirrors the browser source-order rule).
871
+ const fg = node.tone ? TONE_FG[node.tone] : attrs.fg;
872
+ return (_jsx("text", { ...(attrs.attributes != null ? { attributes: attrs.attributes } : {}), ...(fg != null ? { fg } : {}), children: node.value }));
866
873
  }
867
874
  // ── link ──────────────────────────────────────────────────────────────────
868
875
  // External links emit a real OSC 8 hyperlink (\x1b]8;;url\x07label\x1b]8;;\x07);
@@ -896,13 +903,16 @@ function ImageView({ node }) {
896
903
  }
897
904
  // ── list / list-item ──────────────────────────────────────────────────────
898
905
  // A top-level `list` (direct child of page) is a pane on its own; nested
899
- // lists scroll as part of their containing section. list-item variants
900
- // ("done", "active", …) are mapped to text colors.
906
+ // lists scroll as part of their containing section. list-item `state`
907
+ // ("done", "active", …) maps to a text color; semantic `tone` (danger/…)
908
+ // uses the shared TONE_FG and wins over state.
901
909
  const LIST_ITEM_FG = {
902
910
  done: "#88cc88",
903
911
  active: "#88aaff",
904
- error: "#ff5555",
905
- muted: "#888888",
912
+ running: "#88aaff",
913
+ moving: "#88aaff",
914
+ disabled: "#888888",
915
+ high: "#d76410",
906
916
  };
907
917
  function ListView({ node, ctx }) {
908
918
  const isPane = ctx.isTopLevel;
@@ -924,9 +934,9 @@ function ListView({ node, ctx }) {
924
934
  return (_jsx("scrollbox", { focused: focused, focusable: isPaneFocusable, borderColor: focused ? "#88aaff" : "#555555", focusedBorderColor: "#88aaff", flexGrow: 1, flexShrink: 1, children: _jsx("box", { flexDirection: "column", gap: 0, children: node.children.map((child, i) => renderNode(child, childCtx, i)) }) }));
925
935
  }
926
936
  function ListItemView({ node, ctx }) {
927
- const fg = node.variant ? LIST_ITEM_FG[node.variant] : undefined;
937
+ const fg = node.tone ? TONE_FG[node.tone] : node.state ? LIST_ITEM_FG[node.state] : undefined;
928
938
  const childCtx = { ...ctx, isTopLevel: false };
929
- // The variant tint applies to text children inside this item via inherited
939
+ // The tint applies to text children inside this item via inherited
930
940
  // color (OpenTUI <text fg> wins per-element, so this only affects items
931
941
  // whose children don't override). For B2 we just thread the children
932
942
  // through unchanged; full per-variant styling lands in B3 (with the focus +
@@ -1022,8 +1032,8 @@ function TableView({ node, ctx }) {
1022
1032
  // ── minimum-viable text surface for the rest of the node set ───────────────
1023
1033
  // Same as B1 — text only, no interactivity. Full widgets land in B3/B4.
1024
1034
  function ButtonView({ node, ctx }) {
1025
- const fg = node.variant === "danger" ? "#ff5555"
1026
- : node.variant === "primary" ? "#88aaff"
1035
+ const fg = node.tone ? TONE_FG[node.tone]
1036
+ : node.emphasis === "primary" ? "#88aaff"
1027
1037
  : undefined;
1028
1038
  // 0.8.0 (#11) — pendingLabel: when set + this button's key matches the
1029
1039
  // adapter's pendingButtonKey, render the pending label instead of the
@@ -1128,8 +1138,8 @@ function CopyButtonView({ node, ctx }) {
1128
1138
  // 0.9.0 (#14): variant coloring — mirrors ButtonView's fg derivation
1129
1139
  // verbatim. Adapter-medium-adaptation parity: the browser uses CSS
1130
1140
  // classes; the TUI uses ANSI fg colors. Same semantic mapping.
1131
- const fg = node.variant === "danger" ? "#ff5555"
1132
- : node.variant === "primary" ? "#88aaff"
1141
+ const fg = node.tone ? TONE_FG[node.tone]
1142
+ : node.emphasis === "primary" ? "#88aaff"
1133
1143
  : undefined;
1134
1144
  return (_jsxs("text", { ...(fg != null ? { fg } : {}), onMouseDown: () => ctx.copy(node.text), children: ["[ ", label, " ]"] }));
1135
1145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "2.0.0",
3
+ "version": "3.0.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",
@@ -235,6 +235,11 @@ body {
235
235
  flex-direction: row;
236
236
  flex-wrap: wrap;
237
237
  align-items: center;
238
+ /* Horizontal clusters (navbars, toolbars, header rows) need more breathing
239
+ room between siblings than the tight vertical --vms-space-sm rhythm. This
240
+ is the one place the row preset DOES redeclare gap (D-30 left it inherited;
241
+ nav links at 0.75rem read cramped). */
242
+ gap: var(--vms-space-lg);
238
243
  }
239
244
  .vms-page--row > .vms-page__title,
240
245
  .vms-section--row > .vms-section__heading { flex: 0 0 100%; }
@@ -323,6 +328,19 @@ body {
323
328
  padding: var(--vms-space-md);
324
329
  }
325
330
 
331
+ /* ── Section tone — universal status axis (orthogonal to variant:card): a tinted
332
+ surface + solid tone-colored border, reusing the shared semantic tokens.
333
+ The tint is mixed into the OPAQUE surface (not transparent) so it reads on
334
+ dark themes too — a translucent layer over a dark surface is invisible, which
335
+ is why this must mix with var(--vms-surface). Composes with --card (a card
336
+ reads as warning/danger/etc; border-radius + padding come from --card) and
337
+ also tints a bare section. The solid tone border is the always-visible signal
338
+ on any background. ── */
339
+ .vms-section--danger { background: color-mix(in srgb, var(--vms-error) 14%, var(--vms-surface)); border: 1px solid var(--vms-error); }
340
+ .vms-section--warning { background: color-mix(in srgb, var(--vms-warning) 14%, var(--vms-surface)); border: 1px solid var(--vms-warning); }
341
+ .vms-section--success { background: color-mix(in srgb, var(--vms-success) 14%, var(--vms-surface)); border: 1px solid var(--vms-success); }
342
+ .vms-section--info { background: color-mix(in srgb, var(--vms-info) 14%, var(--vms-surface)); border: 1px solid var(--vms-info); }
343
+
326
344
  /* ── Section: clickable (1.4.0 — SectionNode.action click-anywhere primitive) ──
327
345
  Mirrors the 1.1.0 .vms-table__row--clickable idiom one level up the tree.
328
346
  Hover uses a 1px accent-dim ring (rather than a background swap — cards
@@ -492,8 +510,15 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
492
510
  }
493
511
  .vms-checkbox__label { margin-left: var(--vms-space-xs); font-size: var(--vms-text-base); }
494
512
 
495
- /* ── Buttons ── */
513
+ /* ── Buttons three orthogonal axes: emphasis (fill) × tone (color) × size
514
+ (geometry). `--_btn-tone` carries the semantic color: it defaults to accent
515
+ and the tone classes override it, so an emphasis fills/outlines in whatever
516
+ tone is set (emphasis:primary + tone:danger = a filled RED button). Box
517
+ metrics (padding/font) live ONLY on the base (= md) and the size classes —
518
+ never on emphasis or tone — so a primary and a danger button are the SAME
519
+ size. ── */
496
520
  .vms-button {
521
+ --_btn-tone: var(--vms-accent);
497
522
  background: var(--vms-surface-2);
498
523
  border: 1px solid var(--vms-border);
499
524
  border-radius: var(--vms-radius);
@@ -502,32 +527,37 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
502
527
  font-family: var(--vms-font-body);
503
528
  font-size: var(--vms-text-base);
504
529
  padding: var(--vms-space-sm) var(--vms-space-md);
530
+ align-self: flex-start;
505
531
  transition: background var(--vms-t), border-color var(--vms-t), transform var(--vms-t);
506
532
  }
507
533
  .vms-button:hover { border-color: var(--vms-accent-dim); }
508
534
  .vms-button:active { transform: scale(0.98); }
535
+
536
+ /* tone — semantic color. Standalone (no emphasis) = colored text + border on the
537
+ neutral surface; combined with an emphasis it recolors the fill/outline. */
538
+ .vms-button--danger { --_btn-tone: var(--vms-error); color: var(--vms-error); border-color: var(--vms-error); }
539
+ .vms-button--warning { --_btn-tone: var(--vms-warning); color: var(--vms-warning); border-color: var(--vms-warning); }
540
+ .vms-button--success { --_btn-tone: var(--vms-success); color: var(--vms-success); border-color: var(--vms-success); }
541
+ .vms-button--info { --_btn-tone: var(--vms-info); color: var(--vms-info); border-color: var(--vms-info); }
542
+
543
+ /* emphasis — fill treatment, colored by --_btn-tone (declared AFTER tone so the
544
+ filled text color / outline color wins on combination). */
509
545
  .vms-button--primary {
510
- background: var(--vms-accent);
511
- border-color: var(--vms-accent);
546
+ background: var(--_btn-tone);
547
+ border-color: var(--_btn-tone);
512
548
  color: #fff;
513
- padding: var(--vms-space-sm) var(--vms-space-lg);
514
- align-self: flex-start;
515
549
  }
516
550
  .vms-button--primary:hover { filter: brightness(1.1); }
517
551
  .vms-button--secondary {
518
552
  background: transparent;
519
- border-color: var(--vms-accent-dim);
520
- color: var(--vms-accent);
521
- font-size: var(--vms-text-sm);
522
- padding: var(--vms-space-2xs) var(--vms-space-sm);
553
+ border-color: var(--_btn-tone);
554
+ color: var(--_btn-tone);
523
555
  }
524
- .vms-button--secondary:hover { border-color: var(--vms-accent); background: var(--vms-accent-glow); }
525
- .vms-button--danger {
526
- background: transparent;
527
- border: 1px solid var(--vms-border);
528
- color: var(--vms-error);
529
- }
530
- .vms-button--danger:hover { background: var(--vms-error-glow); border-color: var(--vms-error); }
556
+ .vms-button--secondary:hover { background: var(--vms-accent-glow); }
557
+
558
+ /* size — the ONLY axis that touches box metrics (md = base, no class). */
559
+ .vms-button--sm { padding: var(--vms-space-2xs) var(--vms-space-sm); font-size: var(--vms-text-sm); }
560
+ .vms-button--lg { padding: var(--vms-space-sm) var(--vms-space-lg); font-size: var(--vms-text-lg); }
531
561
 
532
562
  /* ── Pending state (0.8.0 / issue #11) ──
533
563
  Applied by BrowserAdapter when a ButtonNode with `pendingLabel` is clicked.
@@ -594,20 +624,23 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
594
624
  }
595
625
  .vms-list-item:hover { border-color: var(--vms-accent-dim); }
596
626
 
597
- /* Common variants — apps can add more by writing .vms-list-item--{name} rules.
598
- Variant accents go on border-left and are preserved through hover. */
627
+ /* State (lifecycle)done/high are framework-styled; apps can add more
628
+ .vms-list-item--{state} rules. The `high` priority accent goes on border-left. */
599
629
  .vms-list-item--done { background: var(--vms-done-bg); border-color: transparent; opacity: 0.75; }
600
- .vms-list-item--critical { border-left: 3px solid var(--vms-error); background: rgba(224, 90, 90, 0.04); }
630
+ .vms-list-item--disabled { opacity: 0.55; }
601
631
  .vms-list-item--high { border-left: 3px solid var(--vms-priority-high); }
632
+ .vms-list-item--high:hover { border-left-color: var(--vms-priority-high); }
633
+
634
+ /* Tone (severity) — the universal status axis (was the old critical/warning/...
635
+ variants; critical → danger). Colored left accent, preserved through hover. */
636
+ .vms-list-item--danger { border-left: 3px solid var(--vms-error); background: color-mix(in srgb, var(--vms-error) 4%, transparent); }
602
637
  .vms-list-item--warning { border-left: 3px solid var(--vms-warning); }
603
638
  .vms-list-item--success { border-left: 3px solid var(--vms-success); }
604
639
  .vms-list-item--info { border-left: 3px solid var(--vms-info); }
605
-
606
- .vms-list-item--critical:hover { border-left-color: var(--vms-error); }
607
- .vms-list-item--high:hover { border-left-color: var(--vms-priority-high); }
608
- .vms-list-item--warning:hover { border-left-color: var(--vms-warning); }
609
- .vms-list-item--success:hover { border-left-color: var(--vms-success); }
610
- .vms-list-item--info:hover { border-left-color: var(--vms-info); }
640
+ .vms-list-item--danger:hover { border-left-color: var(--vms-error); }
641
+ .vms-list-item--warning:hover { border-left-color: var(--vms-warning); }
642
+ .vms-list-item--success:hover { border-left-color: var(--vms-success); }
643
+ .vms-list-item--info:hover { border-left-color: var(--vms-info); }
611
644
 
612
645
  /* Selection state for master-detail / list-detail patterns (D-27). The
613
646
  currently-selected row. Stronger than the semantic hints (selection is
@@ -626,9 +659,13 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
626
659
  .vms-text--body { line-height: 1.6; }
627
660
  .vms-text--muted { color: var(--vms-text-muted); font-size: var(--vms-text-sm); }
628
661
  .vms-text--strikethrough { color: var(--vms-done-text); text-decoration: line-through; }
629
- .vms-text--error { color: var(--vms-error); font-size: var(--vms-text-base); }
630
- .vms-text--warning { color: var(--vms-warning); font-size: var(--vms-text-base); }
631
662
  .vms-text--pre { font-family: var(--vms-font-mono); white-space: pre; }
663
+ /* tone — semantic color axis (was the old style:"error"/"warning"). Placed after
664
+ the typography classes so a tone color wins over a style color (e.g. muted). */
665
+ .vms-text--danger { color: var(--vms-error); }
666
+ .vms-text--warning { color: var(--vms-warning); }
667
+ .vms-text--success { color: var(--vms-success); }
668
+ .vms-text--info { color: var(--vms-info); }
632
669
 
633
670
  /* ── Link ── */
634
671
  .vms-link {
@@ -642,6 +679,13 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
642
679
  align-self: flex-start;
643
680
  }
644
681
  .vms-link:hover { border-bottom-color: var(--vms-accent); }
682
+ /* Active / current nav item ("you are here"). Solid underline + stronger
683
+ weight so the current location reads at a glance; server-owned via
684
+ LinkNode.active (emits aria-current="page" too). */
685
+ .vms-link--active {
686
+ border-bottom-color: var(--vms-accent);
687
+ font-weight: 600;
688
+ }
645
689
 
646
690
  /* ── Image ── */
647
691
  .vms-image { max-width: 100%; height: auto; display: block; }
@@ -719,9 +763,9 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
719
763
  padding: var(--vms-space-sm) var(--vms-space-lg);
720
764
  border-top: 1px solid var(--vms-border);
721
765
  }
722
- /* Buttons inside the footer ignore the framework's default align-self so
723
- they sit naturally on the footer row. */
724
- .vms-modal__footer .vms-button--primary { align-self: auto; padding: var(--vms-space-sm) var(--vms-space-md); }
766
+ /* Buttons inside the footer ignore the base align-self:flex-start so they sit
767
+ naturally on the right-aligned footer row (all emphasis/tone variants). */
768
+ .vms-modal__footer .vms-button { align-self: auto; }
725
769
 
726
770
  /* ── Table ── */
727
771
  .vms-table-wrapper {
@@ -768,16 +812,18 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
768
812
  outline: 2px solid var(--vms-accent);
769
813
  outline-offset: -2px;
770
814
  }
771
- /* Row status variantstints derive from theme vars (color-mix), so a
772
- custom :root theme recolors them automatically; no app CSS, no literals. */
815
+ /* Row STATE (lifecycle)done/disabled/running framework-styled; apps can add
816
+ more .vms-table__row--{state} rules. */
773
817
  .vms-table__row--done { opacity: 0.6; }
774
818
  .vms-table__row--disabled { opacity: 0.55; color: var(--vms-text-muted); }
775
819
  .vms-table__row--disabled.vms-table__row--clickable { cursor: default; }
776
820
  .vms-table__row--disabled.vms-table__row--clickable:hover { background: transparent; }
777
- .vms-table__row--success { background: color-mix(in srgb, var(--vms-success) 8%, transparent); }
778
821
  .vms-table__row--running { background: color-mix(in srgb, var(--vms-accent) 8%, transparent); }
822
+ /* Row TONE (severity) — the universal status axis (was the old critical/danger;
823
+ critical → danger). Tints derive from theme vars (color-mix), so a custom
824
+ :root theme recolors them automatically; no app CSS, no literals. */
825
+ .vms-table__row--success { background: color-mix(in srgb, var(--vms-success) 8%, transparent); }
779
826
  .vms-table__row--warning { background: color-mix(in srgb, var(--vms-warning) 9%, transparent); }
780
- .vms-table__row--critical,
781
827
  .vms-table__row--danger { background: color-mix(in srgb, var(--vms-error) 9%, transparent); }
782
828
  .vms-table__td { padding: var(--vms-space-sm) var(--vms-space-md); vertical-align: middle; }
783
829
  .vms-table__link { color: var(--vms-accent); text-decoration: none; }