@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 +3 -24
- package/dist/index.d.ts +5 -22
- package/dist/tui.js +0 -2
- package/package.json +1 -1
- package/styles/default.css +13 -52
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.
|
|
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",
|
package/styles/default.css
CHANGED
|
@@ -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
|
|
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; }
|