@ashley-shrok/viewmodel-shell 1.10.0 → 1.11.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 +23 -0
- package/dist/index.d.ts +26 -4
- package/dist/tui.js +5 -0
- package/package.json +1 -1
- package/styles/default.css +66 -0
package/dist/browser.js
CHANGED
|
@@ -265,6 +265,29 @@ export class BrowserAdapter {
|
|
|
265
265
|
parent.appendChild(el);
|
|
266
266
|
return;
|
|
267
267
|
}
|
|
268
|
+
// 1.11.0 — flyout:true overlay disclosure. The hover/focus sibling of
|
|
269
|
+
// collapsible's inline <details>: heading => focusable <button> trigger,
|
|
270
|
+
// children => an absolutely-positioned panel revealed on :hover /
|
|
271
|
+
// :focus-within (pure CSS — see default.css; no JS, no listeners, no
|
|
272
|
+
// round-tripped open state). Precedence: collapsible (checked above) wins,
|
|
273
|
+
// so reaching here means collapsible is not set; flyout in turn wins over
|
|
274
|
+
// link/action below. Omitted/false renders byte-identical to <section>.
|
|
275
|
+
if (n.flyout === true) {
|
|
276
|
+
const el = document.createElement("div");
|
|
277
|
+
el.className = `vms-section vms-section--flyout${n.variant === "card" ? " vms-section--card" : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}`;
|
|
278
|
+
const trigger = document.createElement("button");
|
|
279
|
+
trigger.type = "button";
|
|
280
|
+
trigger.className = "vms-section__trigger";
|
|
281
|
+
// Headingless fallback label — mirrors collapsible's "Show details".
|
|
282
|
+
trigger.textContent = n.heading ?? "Menu";
|
|
283
|
+
el.appendChild(trigger);
|
|
284
|
+
const panel = document.createElement("div");
|
|
285
|
+
panel.className = "vms-section__panel";
|
|
286
|
+
this.kids(n.children, panel, on);
|
|
287
|
+
el.appendChild(panel);
|
|
288
|
+
parent.appendChild(el);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
268
291
|
// 1.5.0 — SectionNode.link URL-wrapper variant (issue #21). When set,
|
|
269
292
|
// emit a wrapping <a href> element instead of <section> so every native
|
|
270
293
|
// browser link affordance works for free (middle-click / Ctrl/Cmd-click
|
package/dist/index.d.ts
CHANGED
|
@@ -69,8 +69,8 @@ export interface PageNode {
|
|
|
69
69
|
title?: string;
|
|
70
70
|
/** Density of global spacing. Omitted or "comfortable" = current behavior (no modifier class). "compact" emits .vms-page--compact. Closed union (D-03). */
|
|
71
71
|
density?: "comfortable" | "compact";
|
|
72
|
-
/** 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) emit .vms-page--{value}. Closed union (D-01/D-02; sidebar D-28). */
|
|
73
|
-
layout?: "stack" | "split" | "cards" | "sidebar";
|
|
72
|
+
/** 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) emit .vms-page--{value}. Closed union (D-01/D-02; sidebar D-28; row D-30). */
|
|
73
|
+
layout?: "stack" | "split" | "cards" | "sidebar" | "row";
|
|
74
74
|
/** Page-shell max-width override. Omitted = framework default cap (`--vms-page-max`, 1080px). "wide" = `--vms-page-max-wide` (1440px default), for data-heavy pages with wide tables. "full" = uncapped (max-width: none), for full-bleed dashboards. TUI ignores this (terminals fill naturally). Closed union (D-13 / issue #13). */
|
|
75
75
|
width?: "wide" | "full";
|
|
76
76
|
children: ViewNode[];
|
|
@@ -80,12 +80,34 @@ export interface SectionNode {
|
|
|
80
80
|
heading?: string;
|
|
81
81
|
/** Section surface variant. Omitted = current behavior (no modifier class). "card" emits .vms-section--card. Closed union (D-03). */
|
|
82
82
|
variant?: "card";
|
|
83
|
-
/** 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) emit .vms-section--{value}. Closed union (D-01/D-02; sidebar D-28). */
|
|
84
|
-
layout?: "stack" | "split" | "cards" | "sidebar";
|
|
83
|
+
/** 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) emit .vms-section--{value}. Closed union (D-01/D-02; sidebar D-28; row D-30). */
|
|
84
|
+
layout?: "stack" | "split" | "cards" | "sidebar" | "row";
|
|
85
85
|
/** Optional stable preservation key for the renderer's collapsible-section open-state snapshot. Used only when `collapsible: true`. Provide when `heading` isn't unique within a page or is absent — otherwise the renderer falls back to `heading ?? "vms-section-anon"`, disambiguated by per-render ordinal. Omitted = use the heading fallback. */
|
|
86
86
|
id?: string;
|
|
87
87
|
/** 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. */
|
|
88
88
|
collapsible?: boolean;
|
|
89
|
+
/** When true, the section renders as an OVERLAY disclosure ("flyout") — the
|
|
90
|
+
* hover/focus sibling of `collapsible`'s inline `<details>` reveal. The
|
|
91
|
+
* `heading` becomes a focusable `<button class="vms-section__trigger">`
|
|
92
|
+
* trigger; the `children` are wrapped in a `<div class="vms-section__panel">`
|
|
93
|
+
* that is absolutely positioned and revealed on `:hover` / `:focus-within`
|
|
94
|
+
* (pure CSS — no JavaScript, no state machine). Hover (desktop), tap-to-focus
|
|
95
|
+
* (touch), and keyboard Tab-to-trigger (a11y) all reveal it for free; it hides
|
|
96
|
+
* on blur / pointer-leave. Unlike `collapsible`, the open state is EPHEMERAL
|
|
97
|
+
* (driven by `:hover`/`:focus-within`), so it is NOT round-tripped and NOT
|
|
98
|
+
* snapshotted across re-renders — there is nothing to preserve.
|
|
99
|
+
*
|
|
100
|
+
* Use a flyout (overlay) rather than `collapsible` (inline) when the revealed
|
|
101
|
+
* content should float over siblings instead of pushing them — e.g. a menu
|
|
102
|
+
* inside a `layout:"row"` bar, where an inline disclosure would shove the bar
|
|
103
|
+
* open. A headingless flyout uses the fallback trigger label `"Menu"`.
|
|
104
|
+
*
|
|
105
|
+
* Mutually exclusive with the other section modes; the renderer resolves a
|
|
106
|
+
* fixed precedence and never combines them: `collapsible` > `flyout` > `link`
|
|
107
|
+
* > `action`. So `collapsible: true` wins if both are set, and a flyout
|
|
108
|
+
* section ignores `link`/`action`. Omitted/false = today's `<section>`
|
|
109
|
+
* rendering, byte-identical (no class drift, no extra elements). */
|
|
110
|
+
flyout?: boolean;
|
|
89
111
|
/** Click-anywhere section dispatch primitive — mirrors `TableRow.action` (1.1.0)
|
|
90
112
|
* at the section level. When set, the renderer makes the entire section
|
|
91
113
|
* clickable AND keyboard-activatable (Enter / Space — Space preventDefaults
|
package/dist/tui.js
CHANGED
|
@@ -781,6 +781,11 @@ function layoutProps(layout, _sidebarFraction) {
|
|
|
781
781
|
return { flexDirection: "row", gap: 1, flexGrow: 1, flexShrink: 1 };
|
|
782
782
|
case "cards":
|
|
783
783
|
return { flexDirection: "row", flexWrap: "wrap", gap: 1 };
|
|
784
|
+
case "row":
|
|
785
|
+
// 1.11.0 — left-aligned wrapping horizontal row; items hug content.
|
|
786
|
+
// (Section flyout has no TUI overlay; it degrades to a plain labeled
|
|
787
|
+
// section — SectionView ignores the flag and renders children inline.)
|
|
788
|
+
return { flexDirection: "row", flexWrap: "wrap", gap: 1 };
|
|
784
789
|
case "sidebar":
|
|
785
790
|
return { flexDirection: "row", gap: 1, flexGrow: 1, flexShrink: 1 };
|
|
786
791
|
case "stack":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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
|
@@ -213,6 +213,21 @@ body {
|
|
|
213
213
|
.vms-page--sidebar > .vms-page__title,
|
|
214
214
|
.vms-section--sidebar > .vms-section__heading { flex: 0 0 100%; }
|
|
215
215
|
|
|
216
|
+
/* ── Layout preset: row (D-30 / 1.11.0) — left-aligned wrapping horizontal row;
|
|
217
|
+
children hug their content (no forced grow), wrap to the next line on overflow.
|
|
218
|
+
The general horizontal-row primitive — a navbar composes from this + flyout.
|
|
219
|
+
Title/heading takes its own full-width row, like the other presets. gap
|
|
220
|
+
inherited from .vms-page/.vms-section — NOT redeclared. ── */
|
|
221
|
+
.vms-page--row,
|
|
222
|
+
.vms-section--row {
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: row;
|
|
225
|
+
flex-wrap: wrap;
|
|
226
|
+
align-items: center;
|
|
227
|
+
}
|
|
228
|
+
.vms-page--row > .vms-page__title,
|
|
229
|
+
.vms-section--row > .vms-section__heading { flex: 0 0 100%; }
|
|
230
|
+
|
|
216
231
|
/* ── Section variant: card (THEME-04) — grouped surface, existing seam vars ── */
|
|
217
232
|
.vms-section--card {
|
|
218
233
|
background: var(--vms-surface);
|
|
@@ -271,6 +286,57 @@ body {
|
|
|
271
286
|
outline-offset: 2px;
|
|
272
287
|
}
|
|
273
288
|
|
|
289
|
+
/* ── Section: flyout (1.11.0 — overlay disclosure, the hover/focus sibling of
|
|
290
|
+
collapsible) ── The heading renders as a focusable .vms-section__trigger button;
|
|
291
|
+
the children are wrapped in an absolutely-positioned .vms-section__panel revealed
|
|
292
|
+
on :hover / :focus-within. Pure CSS — hover (desktop), tap-to-focus (touch), and
|
|
293
|
+
Tab-to-trigger (keyboard a11y) all reveal it; it hides on blur / pointer-leave.
|
|
294
|
+
No JS, no round-tripped open state. The panel overlays (out of flow) so a flyout
|
|
295
|
+
inside a .vms-section--row bar does not reflow its siblings. */
|
|
296
|
+
.vms-section--flyout {
|
|
297
|
+
position: relative;
|
|
298
|
+
flex-direction: column; /* trigger stacks above the (absolute) panel; wrapper hugs the trigger */
|
|
299
|
+
align-items: flex-start;
|
|
300
|
+
}
|
|
301
|
+
.vms-section__trigger {
|
|
302
|
+
font-size: var(--vms-text-xs);
|
|
303
|
+
letter-spacing: 0.08em;
|
|
304
|
+
text-transform: uppercase;
|
|
305
|
+
color: var(--vms-text-muted);
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
background: none;
|
|
308
|
+
border: none;
|
|
309
|
+
padding: 0;
|
|
310
|
+
font-family: inherit;
|
|
311
|
+
}
|
|
312
|
+
.vms-section__trigger:focus-visible {
|
|
313
|
+
outline: 2px solid var(--vms-accent);
|
|
314
|
+
outline-offset: 2px;
|
|
315
|
+
}
|
|
316
|
+
.vms-section__panel {
|
|
317
|
+
position: absolute;
|
|
318
|
+
top: 100%;
|
|
319
|
+
left: 0;
|
|
320
|
+
z-index: 50;
|
|
321
|
+
min-width: max-content;
|
|
322
|
+
display: flex;
|
|
323
|
+
flex-direction: column;
|
|
324
|
+
gap: var(--vms-space-sm);
|
|
325
|
+
margin-top: var(--vms-space-xs);
|
|
326
|
+
padding: var(--vms-space-sm);
|
|
327
|
+
background: var(--vms-surface);
|
|
328
|
+
border: 1px solid var(--vms-border);
|
|
329
|
+
border-radius: var(--vms-radius);
|
|
330
|
+
visibility: hidden;
|
|
331
|
+
opacity: 0;
|
|
332
|
+
transition: opacity var(--vms-t);
|
|
333
|
+
}
|
|
334
|
+
.vms-section--flyout:hover > .vms-section__panel,
|
|
335
|
+
.vms-section--flyout:focus-within > .vms-section__panel {
|
|
336
|
+
visibility: visible;
|
|
337
|
+
opacity: 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
274
340
|
/* ── Stat bar ── */
|
|
275
341
|
.vms-stat-bar { display: flex; gap: var(--vms-space-lg); flex-wrap: wrap; }
|
|
276
342
|
.vms-stat-bar__item { display: flex; align-items: baseline; gap: var(--vms-space-xs); }
|