@ashley-shrok/viewmodel-shell 1.12.0 → 2.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
@@ -368,29 +368,6 @@ export class BrowserAdapter {
368
368
  parent.appendChild(el);
369
369
  return;
370
370
  }
371
- // 1.11.0 — flyout:true overlay disclosure. The hover/focus sibling of
372
- // collapsible's inline <details>: heading => focusable <button> trigger,
373
- // children => an absolutely-positioned panel revealed on :hover /
374
- // :focus-within (pure CSS — see default.css; no JS, no listeners, no
375
- // round-tripped open state). Precedence: collapsible (checked above) wins,
376
- // so reaching here means collapsible is not set; flyout in turn wins over
377
- // link/action below. Omitted/false renders byte-identical to <section>.
378
- if (n.flyout === true) {
379
- const el = document.createElement("div");
380
- el.className = `vms-section vms-section--flyout${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}` : ""}`;
381
- const trigger = document.createElement("button");
382
- trigger.type = "button";
383
- trigger.className = "vms-section__trigger";
384
- // Headingless fallback label — mirrors collapsible's "Show details".
385
- trigger.textContent = n.heading ?? "Menu";
386
- el.appendChild(trigger);
387
- const panel = document.createElement("div");
388
- panel.className = "vms-section__panel";
389
- this.kids(n.children, panel, on);
390
- el.appendChild(panel);
391
- parent.appendChild(el);
392
- return;
393
- }
394
371
  // 1.5.0 — SectionNode.link URL-wrapper variant (issue #21). When set,
395
372
  // emit a wrapping <a href> element instead of <section> so every native
396
373
  // browser link affordance works for free (middle-click / Ctrl/Cmd-click
@@ -815,9 +792,11 @@ export class BrowserAdapter {
815
792
  }
816
793
  link(n, parent) {
817
794
  const a = document.createElement("a");
818
- a.className = "vms-link";
795
+ a.className = n.active ? "vms-link vms-link--active" : "vms-link";
819
796
  a.href = n.href;
820
797
  a.textContent = n.label;
798
+ if (n.active)
799
+ a.setAttribute("aria-current", "page");
821
800
  if (n.external) {
822
801
  a.target = "_blank";
823
802
  a.rel = "noopener noreferrer";
package/dist/index.d.ts CHANGED
@@ -106,28 +106,6 @@ export interface SectionNode {
106
106
  id?: string;
107
107
  /** When true, the section renders as a native `<details>`/`<summary>` disclosure widget (closed by default). Aesthetic, client-side primitive — the open/closed state is DOM-local and the server does NOT round-trip it (same conceptual model as draft text values in unsubmitted form inputs). The browser adapter snapshots `<details>.open` before each re-render and restores it after, keyed by `id ?? heading ?? "vms-section-anon"` (disambiguated by per-render ordinal); a re-key drops the preserved state (the documented escape hatch for rare server-driven expansion). The summary label is the section's `heading`; a headingless collapsible section uses the fallback string `"Show details"`. If a section needs to start open, do not mark it collapsible. Omitted/false = today's `<section>` rendering, byte-identical. */
108
108
  collapsible?: boolean;
109
- /** When true, the section renders as an OVERLAY disclosure ("flyout") — the
110
- * hover/focus sibling of `collapsible`'s inline `<details>` reveal. The
111
- * `heading` becomes a focusable `<button class="vms-section__trigger">`
112
- * trigger; the `children` are wrapped in a `<div class="vms-section__panel">`
113
- * that is absolutely positioned and revealed on `:hover` / `:focus-within`
114
- * (pure CSS — no JavaScript, no state machine). Hover (desktop), tap-to-focus
115
- * (touch), and keyboard Tab-to-trigger (a11y) all reveal it for free; it hides
116
- * on blur / pointer-leave. Unlike `collapsible`, the open state is EPHEMERAL
117
- * (driven by `:hover`/`:focus-within`), so it is NOT round-tripped and NOT
118
- * snapshotted across re-renders — there is nothing to preserve.
119
- *
120
- * Use a flyout (overlay) rather than `collapsible` (inline) when the revealed
121
- * content should float over siblings instead of pushing them — e.g. a menu
122
- * inside a `layout:"row"` bar, where an inline disclosure would shove the bar
123
- * open. A headingless flyout uses the fallback trigger label `"Menu"`.
124
- *
125
- * Mutually exclusive with the other section modes; the renderer resolves a
126
- * fixed precedence and never combines them: `collapsible` > `flyout` > `link`
127
- * > `action`. So `collapsible: true` wins if both are set, and a flyout
128
- * section ignores `link`/`action`. Omitted/false = today's `<section>`
129
- * rendering, byte-identical (no class drift, no extra elements). */
130
- flyout?: boolean;
131
109
  /** Click-anywhere section dispatch primitive — mirrors `TableRow.action` (1.1.0)
132
110
  * at the section level. When set, the renderer makes the entire section
133
111
  * clickable AND keyboard-activatable (Enter / Space — Space preventDefaults
@@ -293,6 +271,11 @@ export interface LinkNode {
293
271
  href: string;
294
272
  /** true = open outside current app context (browser: new tab + noopener) */
295
273
  external?: boolean;
274
+ /** true = this link points at the current location (nav "you are here").
275
+ * Emits `.vms-link--active` + `aria-current="page"`. Server-owned: the
276
+ * backend decides which nav item is active from its route/state, exactly
277
+ * like every other view decision — there is no client-side route matching. */
278
+ active?: boolean;
296
279
  }
297
280
  export interface ImageNode {
298
281
  type: "image";
package/dist/tui.js CHANGED
@@ -808,8 +808,6 @@ function layoutProps(layout, _sidebarFraction) {
808
808
  return { flexDirection: "row", flexWrap: "wrap", gap: 1 };
809
809
  case "row":
810
810
  // 1.11.0 — left-aligned wrapping horizontal row; items hug content.
811
- // (Section flyout has no TUI overlay; it degrades to a plain labeled
812
- // section — SectionView ignores the flag and renders children inline.)
813
811
  return { flexDirection: "row", flexWrap: "wrap", gap: 1 };
814
812
  case "sidebar":
815
813
  return { flexDirection: "row", gap: 1, flexGrow: 1, flexShrink: 1 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "1.12.0",
3
+ "version": "2.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 — a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -226,7 +226,7 @@ body {
226
226
 
227
227
  /* ── Layout preset: row (D-30 / 1.11.0) — left-aligned wrapping horizontal row;
228
228
  children hug their content (no forced grow), wrap to the next line on overflow.
229
- The general horizontal-row primitive — a navbar composes from this + flyout.
229
+ The general horizontal-row primitive — a navbar composes from this.
230
230
  Title/heading takes its own full-width row, like the other presets. gap
231
231
  inherited from .vms-page/.vms-section — NOT redeclared. ── */
232
232
  .vms-page--row,
@@ -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%; }
@@ -373,57 +378,6 @@ body {
373
378
  outline-offset: 2px;
374
379
  }
375
380
 
376
- /* ── Section: flyout (1.11.0 — overlay disclosure, the hover/focus sibling of
377
- collapsible) ── The heading renders as a focusable .vms-section__trigger button;
378
- the children are wrapped in an absolutely-positioned .vms-section__panel revealed
379
- on :hover / :focus-within. Pure CSS — hover (desktop), tap-to-focus (touch), and
380
- Tab-to-trigger (keyboard a11y) all reveal it; it hides on blur / pointer-leave.
381
- No JS, no round-tripped open state. The panel overlays (out of flow) so a flyout
382
- inside a .vms-section--row bar does not reflow its siblings. */
383
- .vms-section--flyout {
384
- position: relative;
385
- flex-direction: column; /* trigger stacks above the (absolute) panel; wrapper hugs the trigger */
386
- align-items: flex-start;
387
- }
388
- .vms-section__trigger {
389
- font-size: var(--vms-text-xs);
390
- letter-spacing: 0.08em;
391
- text-transform: uppercase;
392
- color: var(--vms-text-muted);
393
- cursor: pointer;
394
- background: none;
395
- border: none;
396
- padding: 0;
397
- font-family: inherit;
398
- }
399
- .vms-section__trigger:focus-visible {
400
- outline: 2px solid var(--vms-accent);
401
- outline-offset: 2px;
402
- }
403
- .vms-section__panel {
404
- position: absolute;
405
- top: 100%;
406
- left: 0;
407
- z-index: 50;
408
- min-width: max-content;
409
- display: flex;
410
- flex-direction: column;
411
- gap: var(--vms-space-sm);
412
- margin-top: var(--vms-space-xs);
413
- padding: var(--vms-space-sm);
414
- background: var(--vms-surface);
415
- border: 1px solid var(--vms-border);
416
- border-radius: var(--vms-radius);
417
- visibility: hidden;
418
- opacity: 0;
419
- transition: opacity var(--vms-t);
420
- }
421
- .vms-section--flyout:hover > .vms-section__panel,
422
- .vms-section--flyout:focus-within > .vms-section__panel {
423
- visibility: visible;
424
- opacity: 1;
425
- }
426
-
427
381
  /* ── Stat bar ── */
428
382
  .vms-stat-bar { display: flex; gap: var(--vms-space-lg); flex-wrap: wrap; }
429
383
  .vms-stat-bar__item { display: flex; align-items: baseline; gap: var(--vms-space-xs); }
@@ -693,6 +647,13 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
693
647
  align-self: flex-start;
694
648
  }
695
649
  .vms-link:hover { border-bottom-color: var(--vms-accent); }
650
+ /* Active / current nav item ("you are here"). Solid underline + stronger
651
+ weight so the current location reads at a glance; server-owned via
652
+ LinkNode.active (emits aria-current="page" too). */
653
+ .vms-link--active {
654
+ border-bottom-color: var(--vms-accent);
655
+ font-weight: 600;
656
+ }
696
657
 
697
658
  /* ── Image ── */
698
659
  .vms-image { max-width: 100%; height: auto; display: block; }