@ashley-shrok/viewmodel-shell 3.4.0 → 3.5.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/agent-skill.md +3 -1
- package/dist/browser.d.ts +18 -0
- package/dist/browser.js +62 -0
- package/dist/index.d.ts +62 -2
- package/dist/index.js +10 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +32 -3
- package/package.json +2 -2
- package/styles/default.css +114 -0
package/agent-skill.md
CHANGED
|
@@ -91,7 +91,8 @@ Optional success-path fields, which may appear alone or alongside `vm`/`state`:
|
|
|
91
91
|
{ "sideEffects": [
|
|
92
92
|
{ "type": "set-local-storage", "key": "auth_jwt", "value": "eyJ..." },
|
|
93
93
|
{ "type": "set-session-storage", "key": "draft_id", "value": "42" },
|
|
94
|
-
{ "type": "download", "url": "/api/invoices/42/pdf", "filename": "invoice-42.pdf" }
|
|
94
|
+
{ "type": "download", "url": "/api/invoices/42/pdf", "filename": "invoice-42.pdf" },
|
|
95
|
+
{ "type": "toast", "message": "Ticket #4012 closed.", "tone": "success" }
|
|
95
96
|
] }
|
|
96
97
|
```
|
|
97
98
|
|
|
@@ -100,6 +101,7 @@ Optional success-path fields, which may appear alone or alongside `vm`/`state`:
|
|
|
100
101
|
| `set-local-storage` | Write `key`/`value` to platform localStorage (or your agent's equivalent persistent store). |
|
|
101
102
|
| `set-session-storage` | Same as above for session-scoped storage. |
|
|
102
103
|
| `download` | Fetch `url` (re-presenting your auth headers — see *Auth*) and save the bytes. Filename precedence: `Content-Disposition` > side-effect `filename` > URL basename > `"download"`. |
|
|
104
|
+
| `toast` | Show a transient confirmation: `message` (required) + optional `tone` (`danger`\|`warning`\|`success`\|`info`) + `durationMs`. A UX nicety — **fail-quiet**: an agent/adapter with no toast surface simply ignores it (nothing to persist or act on), so it carries no state and needs no acknowledgement. |
|
|
103
105
|
|
|
104
106
|
**Forward-compat rule — silently ignore unknown verbs.** A future minor release may add new verbs. If you see a `type` you do not recognize, skip it; do not error. Honor or ignore per your policy.
|
|
105
107
|
|
package/dist/browser.d.ts
CHANGED
|
@@ -12,6 +12,17 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
12
12
|
storage(scope: "local" | "session", key: string, value: string): void;
|
|
13
13
|
saveFile(data: Blob, filename: string, _contentType: string): void;
|
|
14
14
|
setBusy(active: boolean): void;
|
|
15
|
+
/** Transient confirmation toast. Lazily creates/reuses a single fixed-corner
|
|
16
|
+
* host region (.vms-toast-region) appended to <body> so toasts stack and
|
|
17
|
+
* survive the container's innerHTML wipe on each render(); appends a
|
|
18
|
+
* .vms-toast element (+ tone modifier) and auto-removes it after
|
|
19
|
+
* durationMs (default 4000), with a brief fade-out. This is the ONLY place
|
|
20
|
+
* toast DOM lives — core stays platform-agnostic (it just calls this verb).
|
|
21
|
+
* Fail-quiet by absence is the core's concern (it optional-chains the call). */
|
|
22
|
+
toast(message: string, opts?: {
|
|
23
|
+
tone?: string;
|
|
24
|
+
durationMs?: number;
|
|
25
|
+
}): void;
|
|
15
26
|
private unloadHandler;
|
|
16
27
|
setPreventUnload(active: boolean): void;
|
|
17
28
|
transport(input: string, init: {
|
|
@@ -96,4 +107,11 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
96
107
|
* concept. */
|
|
97
108
|
private table;
|
|
98
109
|
private copyButton;
|
|
110
|
+
/** EmptyStateNode — a centered "nothing here" block: a heading, an optional
|
|
111
|
+
* message, then the optional CTA ButtonNode (rendered via the shared button
|
|
112
|
+
* renderer so it dispatches like any other button). */
|
|
113
|
+
private emptyState;
|
|
114
|
+
/** BadgeNode — a compact inline status pill / count. Leaf node: label text +
|
|
115
|
+
* tone/emphasis modifier classes. */
|
|
116
|
+
private badge;
|
|
99
117
|
}
|
package/dist/browser.js
CHANGED
|
@@ -162,6 +162,38 @@ export class BrowserAdapter {
|
|
|
162
162
|
setBusy(active) {
|
|
163
163
|
this.container.classList.toggle("vms-busy", active);
|
|
164
164
|
}
|
|
165
|
+
/** Transient confirmation toast. Lazily creates/reuses a single fixed-corner
|
|
166
|
+
* host region (.vms-toast-region) appended to <body> so toasts stack and
|
|
167
|
+
* survive the container's innerHTML wipe on each render(); appends a
|
|
168
|
+
* .vms-toast element (+ tone modifier) and auto-removes it after
|
|
169
|
+
* durationMs (default 4000), with a brief fade-out. This is the ONLY place
|
|
170
|
+
* toast DOM lives — core stays platform-agnostic (it just calls this verb).
|
|
171
|
+
* Fail-quiet by absence is the core's concern (it optional-chains the call). */
|
|
172
|
+
toast(message, opts) {
|
|
173
|
+
let region = document.querySelector(".vms-toast-region");
|
|
174
|
+
if (!region) {
|
|
175
|
+
region = document.createElement("div");
|
|
176
|
+
region.className = "vms-toast-region";
|
|
177
|
+
document.body.appendChild(region);
|
|
178
|
+
}
|
|
179
|
+
const el = document.createElement("div");
|
|
180
|
+
el.className = `vms-toast${opts?.tone ? ` vms-toast--${opts.tone}` : ""}`;
|
|
181
|
+
el.setAttribute("role", "status");
|
|
182
|
+
el.setAttribute("aria-live", "polite");
|
|
183
|
+
el.textContent = message;
|
|
184
|
+
region.appendChild(el);
|
|
185
|
+
const duration = opts?.durationMs ?? 4000;
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
el.classList.add("vms-toast--leaving");
|
|
188
|
+
// Remove after the fade-out transition; a short fixed delay keeps it
|
|
189
|
+
// simple (no transitionend bookkeeping). Clean up the region if it empties.
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
el.remove();
|
|
192
|
+
if (region && region.childElementCount === 0)
|
|
193
|
+
region.remove();
|
|
194
|
+
}, 200);
|
|
195
|
+
}, duration);
|
|
196
|
+
}
|
|
165
197
|
unloadHandler = null;
|
|
166
198
|
setPreventUnload(active) {
|
|
167
199
|
if (active && this.unloadHandler == null) {
|
|
@@ -240,6 +272,8 @@ export class BrowserAdapter {
|
|
|
240
272
|
case "copy-button": return this.copyButton(n, parent);
|
|
241
273
|
case "divider": return this.divider(n, parent);
|
|
242
274
|
case "fits": return this.fits(n, parent, on);
|
|
275
|
+
case "empty-state": return this.emptyState(n, parent, on);
|
|
276
|
+
case "badge": return this.badge(n, parent);
|
|
243
277
|
default: {
|
|
244
278
|
// Fail loud, not silent (AGENTS.md: "Nothing important fails quietly").
|
|
245
279
|
// Runtime trees are server-controlled JSON, so an unknown/forward-version
|
|
@@ -1293,4 +1327,32 @@ export class BrowserAdapter {
|
|
|
1293
1327
|
});
|
|
1294
1328
|
parent.appendChild(btn);
|
|
1295
1329
|
}
|
|
1330
|
+
/** EmptyStateNode — a centered "nothing here" block: a heading, an optional
|
|
1331
|
+
* message, then the optional CTA ButtonNode (rendered via the shared button
|
|
1332
|
+
* renderer so it dispatches like any other button). */
|
|
1333
|
+
emptyState(n, parent, on) {
|
|
1334
|
+
const el = document.createElement("div");
|
|
1335
|
+
el.className = "vms-empty-state";
|
|
1336
|
+
const heading = document.createElement("div");
|
|
1337
|
+
heading.className = "vms-empty-state__heading";
|
|
1338
|
+
heading.textContent = n.heading;
|
|
1339
|
+
el.appendChild(heading);
|
|
1340
|
+
if (n.message != null && n.message !== "") {
|
|
1341
|
+
const msg = document.createElement("div");
|
|
1342
|
+
msg.className = "vms-empty-state__message";
|
|
1343
|
+
msg.textContent = n.message;
|
|
1344
|
+
el.appendChild(msg);
|
|
1345
|
+
}
|
|
1346
|
+
if (n.action)
|
|
1347
|
+
this.button(n.action, el, on);
|
|
1348
|
+
parent.appendChild(el);
|
|
1349
|
+
}
|
|
1350
|
+
/** BadgeNode — a compact inline status pill / count. Leaf node: label text +
|
|
1351
|
+
* tone/emphasis modifier classes. */
|
|
1352
|
+
badge(n, parent) {
|
|
1353
|
+
const span = document.createElement("span");
|
|
1354
|
+
span.className = `vms-badge${n.tone ? ` vms-badge--${n.tone}` : ""}${n.emphasis ? ` vms-badge--${n.emphasis}` : ""}`;
|
|
1355
|
+
span.textContent = n.label;
|
|
1356
|
+
parent.appendChild(span);
|
|
1357
|
+
}
|
|
1296
1358
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -62,8 +62,20 @@ export interface Adapter {
|
|
|
62
62
|
* none), so a rapid checkbox click during an in-flight round-trip never
|
|
63
63
|
* visually flips the box. Fail-quiet by absence (TUI has no equivalent). */
|
|
64
64
|
setBusy?(active: boolean): void;
|
|
65
|
+
/** Show a transient confirmation toast, driven by a `{ type: "toast" }`
|
|
66
|
+
* ShellSideEffect. `BrowserAdapter` stacks toasts in a single fixed-corner
|
|
67
|
+
* host region and auto-dismisses each after `opts.durationMs` (default
|
|
68
|
+
* ~4000ms). FAIL-QUIET BY ABSENCE — modeled on setPreventUnload/setBusy,
|
|
69
|
+
* NOT on navigate/storage/saveFile: a dropped toast is a missed UX nicety,
|
|
70
|
+
* never a correctness/security bug, so the core MUST NOT call failCapability
|
|
71
|
+
* when this verb is absent (non-browser targets like the TUI simply have no
|
|
72
|
+
* toast surface and the effect is a no-op). */
|
|
73
|
+
toast?(message: string, opts?: {
|
|
74
|
+
tone?: string;
|
|
75
|
+
durationMs?: number;
|
|
76
|
+
}): void;
|
|
65
77
|
}
|
|
66
|
-
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode | DividerNode | FitsNode;
|
|
78
|
+
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode | DividerNode | FitsNode | EmptyStateNode | BadgeNode;
|
|
67
79
|
export interface PageNode {
|
|
68
80
|
type: "page";
|
|
69
81
|
title?: string;
|
|
@@ -488,6 +500,47 @@ export interface CopyButtonNode {
|
|
|
488
500
|
* = content-width (the default hug). Orthogonal to emphasis/tone/size. */
|
|
489
501
|
width?: "auto" | "full";
|
|
490
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* A first-class "nothing here" presentation — the empty-state primitive
|
|
505
|
+
* (MUI/Ant `<Empty>`, the standard zero-data placeholder). A centered block
|
|
506
|
+
* with a required heading, an optional supporting message, and an optional
|
|
507
|
+
* call-to-action ButtonNode (e.g. "Create your first ticket"). No icon field —
|
|
508
|
+
* the framework ships no icon set.
|
|
509
|
+
*
|
|
510
|
+
* The `action` ButtonNode carries a real action name and is a dispatch-bearing
|
|
511
|
+
* descendant, so EVERY tree-walk (action-name uniqueness collector, section
|
|
512
|
+
* shape validator) descends into it — exactly like `modal.footer` / `fits`
|
|
513
|
+
* candidates / `section.action`. A missed walk would silently exempt the CTA
|
|
514
|
+
* from the one-name-one-operation rule.
|
|
515
|
+
*/
|
|
516
|
+
export interface EmptyStateNode {
|
|
517
|
+
type: "empty-state";
|
|
518
|
+
/** The primary line — what's missing / what to do. Required. */
|
|
519
|
+
heading: string;
|
|
520
|
+
/** Optional supporting line below the heading. */
|
|
521
|
+
message?: string;
|
|
522
|
+
/** Optional call-to-action button (e.g. "Add the first item"). */
|
|
523
|
+
action?: ButtonNode;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* A compact status pill / count — the badge primitive (MUI/Ant `<Badge>`,
|
|
527
|
+
* `<Tag>`). A leaf inline node (no children, no action) for a short label or
|
|
528
|
+
* count that appears inside text/section/list/table contexts. Appearance is
|
|
529
|
+
* the universal `tone` (semantic color) + `emphasis` (filled vs outline) axes,
|
|
530
|
+
* matching the rest of the framework's appearance vocabulary.
|
|
531
|
+
*/
|
|
532
|
+
export interface BadgeNode {
|
|
533
|
+
type: "badge";
|
|
534
|
+
/** The pill text — a short label or count (e.g. "New", "3", "Beta"). */
|
|
535
|
+
label: string;
|
|
536
|
+
/** Semantic intent/severity — the universal status tone axis. Emits
|
|
537
|
+
* .vms-badge--{tone}. Omitted = neutral. Closed union. */
|
|
538
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
539
|
+
/** Visual weight — `primary` = filled solid tone, `secondary` = outline.
|
|
540
|
+
* Mirrors ButtonNode's emphasis semantics. Emits .vms-badge--{emphasis}.
|
|
541
|
+
* Omitted = the default filled tint. Closed union. */
|
|
542
|
+
emphasis?: "primary" | "secondary";
|
|
543
|
+
}
|
|
491
544
|
/**
|
|
492
545
|
* The SwiftUI `ViewThatFits` port. The renderer picks the FIRST child whose
|
|
493
546
|
* intrinsic size FITS the available container (no axis overflow), else the next,
|
|
@@ -563,7 +616,7 @@ export interface ShellOptions {
|
|
|
563
616
|
pollInterval?: number;
|
|
564
617
|
}
|
|
565
618
|
export interface ShellSideEffect {
|
|
566
|
-
/** "set-local-storage" | "set-session-storage" | "download" — unknown types are silently ignored. */
|
|
619
|
+
/** "set-local-storage" | "set-session-storage" | "download" | "toast" — unknown types are silently ignored. */
|
|
567
620
|
type: string;
|
|
568
621
|
/** For "set-local-storage" / "set-session-storage": the storage key. */
|
|
569
622
|
key?: string;
|
|
@@ -574,6 +627,13 @@ export interface ShellSideEffect {
|
|
|
574
627
|
/** For "download": optional filename hint. Response Content-Disposition wins
|
|
575
628
|
* when present; this is the fallback before the URL basename. */
|
|
576
629
|
filename?: string;
|
|
630
|
+
/** For "toast": the text shown in the transient confirmation. Required for a
|
|
631
|
+
* toast effect (the shell guards `message != null` before routing). */
|
|
632
|
+
message?: string;
|
|
633
|
+
/** For "toast": optional semantic tone — "danger" | "warning" | "success" | "info". */
|
|
634
|
+
tone?: string;
|
|
635
|
+
/** For "toast": optional auto-dismiss delay in ms (adapter default ~4000). */
|
|
636
|
+
durationMs?: number;
|
|
577
637
|
}
|
|
578
638
|
/** One entry in the structured error payload returned on `ok: false`. */
|
|
579
639
|
export interface ErrorEntry {
|
package/dist/index.js
CHANGED
|
@@ -290,6 +290,16 @@ export class ViewModelShell {
|
|
|
290
290
|
// storage, and a slow download MUST NOT delay the user-visible update.
|
|
291
291
|
void this.download(effect.url, effect.filename);
|
|
292
292
|
}
|
|
293
|
+
else if (effect.type === "toast" && effect.message != null) {
|
|
294
|
+
// FAIL-QUIET (cf. setPreventUnload/setBusy): a toast is a UX nicety,
|
|
295
|
+
// not a correctness/security guarantee, so an adapter without the
|
|
296
|
+
// capability simply drops it — NO failCapability, no onError. The
|
|
297
|
+
// optional-chaining call is the entire contract.
|
|
298
|
+
adapter.toast?.(effect.message, {
|
|
299
|
+
tone: effect.tone,
|
|
300
|
+
durationMs: effect.durationMs,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
293
303
|
}
|
|
294
304
|
// 0.14.0 — apply the unload guard before the redirect/render branch so it's
|
|
295
305
|
// in place (or cleared) consistently across both branches. A server that
|
package/dist/server.d.ts
CHANGED
|
@@ -87,6 +87,14 @@ export declare const shellSideEffect: {
|
|
|
87
87
|
* The conditional spread keeps `filename` ABSENT (not undefined) on the
|
|
88
88
|
* JSON wire, matching the .NET WhenWritingNull null-omission contract. */
|
|
89
89
|
download: (url: string, filename?: string) => ShellSideEffect;
|
|
90
|
+
/** Transient confirmation toast (a UX nicety, fail-quiet by absence — see
|
|
91
|
+
* Adapter.toast). `message` is required; `tone`/`durationMs` are optional and
|
|
92
|
+
* kept ABSENT (not undefined) from the JSON wire via conditional spread,
|
|
93
|
+
* matching the .NET WhenWritingNull null-omission contract. */
|
|
94
|
+
toast: (message: string, opts?: {
|
|
95
|
+
tone?: string;
|
|
96
|
+
durationMs?: number;
|
|
97
|
+
}) => ShellSideEffect;
|
|
90
98
|
};
|
|
91
99
|
/**
|
|
92
100
|
* Mount the canonical VMS agent skill markdown as an HTTP handler.
|
package/dist/server.js
CHANGED
|
@@ -190,8 +190,18 @@ function collectActions(node, enclosingForm, out) {
|
|
|
190
190
|
collectActions(child, enclosingForm, out);
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
|
+
case "empty-state": {
|
|
194
|
+
// EmptyStateNode.action is an optional ButtonNode carrying a real action
|
|
195
|
+
// name. It is a dispatch-bearing descendant, so the uniqueness collector
|
|
196
|
+
// MUST descend into it — otherwise the CTA is silently exempt from the
|
|
197
|
+
// one-name-one-operation rule (the 3.3.0 missed-walk failure class).
|
|
198
|
+
const es = node;
|
|
199
|
+
if (es.action)
|
|
200
|
+
collectActions(es.action, enclosingForm, out);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
193
203
|
// Nodes with no dispatch-bearing actions of their own:
|
|
194
|
-
// text, link, image, stat-bar, progress, copy-button
|
|
204
|
+
// text, link, image, stat-bar, progress, copy-button, badge
|
|
195
205
|
default:
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
@@ -346,9 +356,18 @@ function walkForSectionAction(node, outerInteractive) {
|
|
|
346
356
|
walkForSectionAction(child, outerInteractive);
|
|
347
357
|
return;
|
|
348
358
|
}
|
|
359
|
+
case "empty-state": {
|
|
360
|
+
// EmptyStateNode.action is a ButtonNode (no SectionNode descendants), but
|
|
361
|
+
// descend for consistency with every other walk so a future shape can't
|
|
362
|
+
// slip an interactive section past this validator.
|
|
363
|
+
const es = node;
|
|
364
|
+
if (es.action)
|
|
365
|
+
walkForSectionAction(es.action, outerInteractive);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
349
368
|
// Leaf-like nodes (field, checkbox, button, text, link, image, stat-bar,
|
|
350
|
-
// tabs, progress, table, copy-button) carry no SectionNode
|
|
351
|
-
// TableNode rows hold strings + per-row controls, not sections.
|
|
369
|
+
// tabs, progress, table, copy-button, badge) carry no SectionNode
|
|
370
|
+
// descendants — TableNode rows hold strings + per-row controls, not sections.
|
|
352
371
|
default:
|
|
353
372
|
return;
|
|
354
373
|
}
|
|
@@ -427,6 +446,16 @@ export const shellSideEffect = {
|
|
|
427
446
|
* The conditional spread keeps `filename` ABSENT (not undefined) on the
|
|
428
447
|
* JSON wire, matching the .NET WhenWritingNull null-omission contract. */
|
|
429
448
|
download: (url, filename) => ({ type: "download", url, ...(filename != null ? { filename } : {}) }),
|
|
449
|
+
/** Transient confirmation toast (a UX nicety, fail-quiet by absence — see
|
|
450
|
+
* Adapter.toast). `message` is required; `tone`/`durationMs` are optional and
|
|
451
|
+
* kept ABSENT (not undefined) from the JSON wire via conditional spread,
|
|
452
|
+
* matching the .NET WhenWritingNull null-omission contract. */
|
|
453
|
+
toast: (message, opts) => ({
|
|
454
|
+
type: "toast",
|
|
455
|
+
message,
|
|
456
|
+
...(opts?.tone != null ? { tone: opts.tone } : {}),
|
|
457
|
+
...(opts?.durationMs != null ? { durationMs: opts.durationMs } : {}),
|
|
458
|
+
}),
|
|
430
459
|
};
|
|
431
460
|
// ─── Canonical agent skill mount helper (1.6.0 / 1.5.0) ──────────────────────
|
|
432
461
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"bin": {
|
|
29
|
-
"vms-tui": "
|
|
29
|
+
"vms-tui": "dist/tui-cli.js"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -b tsconfig.tui.json",
|
package/styles/default.css
CHANGED
|
@@ -977,3 +977,117 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
977
977
|
cursor: pointer;
|
|
978
978
|
font-size: var(--vms-text-lg);
|
|
979
979
|
}
|
|
980
|
+
|
|
981
|
+
/* ── Toast (side-effect-driven transient confirmation) ──
|
|
982
|
+
The BrowserAdapter lazily creates ONE .vms-toast-region pinned to the
|
|
983
|
+
bottom-right corner and stacks .vms-toast elements into it; each auto-removes
|
|
984
|
+
after its durationMs with a brief fade. The region is click-through
|
|
985
|
+
(pointer-events:none) so it never blocks the UI under it; individual toasts
|
|
986
|
+
re-enable pointer events for their own content. Framework-owned styling —
|
|
987
|
+
this is the framework owning its presentation, not app CSS. */
|
|
988
|
+
.vms-toast-region {
|
|
989
|
+
position: fixed;
|
|
990
|
+
bottom: var(--vms-space-md);
|
|
991
|
+
right: var(--vms-space-md);
|
|
992
|
+
z-index: 1100; /* above .vms-modal-backdrop (1000) */
|
|
993
|
+
display: flex;
|
|
994
|
+
flex-direction: column;
|
|
995
|
+
gap: var(--vms-space-xs);
|
|
996
|
+
align-items: flex-end;
|
|
997
|
+
pointer-events: none;
|
|
998
|
+
max-width: min(24rem, calc(100vw - 2 * var(--vms-space-md)));
|
|
999
|
+
}
|
|
1000
|
+
.vms-toast {
|
|
1001
|
+
pointer-events: auto;
|
|
1002
|
+
/* Neutral toast = high-contrast inverted chip (the snackbar pattern): the
|
|
1003
|
+
theme's text color as the surface, the theme's surface as the text. This
|
|
1004
|
+
stands clear of the page background it sits on in EITHER theme — a
|
|
1005
|
+
surface-colored toast was nearly invisible against the page surface. */
|
|
1006
|
+
background: var(--vms-text);
|
|
1007
|
+
color: var(--vms-surface);
|
|
1008
|
+
border: 1px solid transparent;
|
|
1009
|
+
border-radius: var(--vms-radius);
|
|
1010
|
+
font-size: var(--vms-text-base);
|
|
1011
|
+
font-weight: 500;
|
|
1012
|
+
padding: var(--vms-space-sm) var(--vms-space-md);
|
|
1013
|
+
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.30);
|
|
1014
|
+
opacity: 1;
|
|
1015
|
+
transition: opacity var(--vms-t), transform var(--vms-t);
|
|
1016
|
+
}
|
|
1017
|
+
/* tone — a FULLY filled chip in the status color (not a thin accent rail), so
|
|
1018
|
+
an important toast (especially danger) is impossible to miss. error/warning
|
|
1019
|
+
are dark enough for white body text to clear WCAG-AA on their pure tokens;
|
|
1020
|
+
success/info are deepened (color-mix w/ black) so white at 14px also clears
|
|
1021
|
+
4.5:1 (their pure tokens sit at 3.2 / 4.4:1). */
|
|
1022
|
+
.vms-toast--danger { background: var(--vms-error); color: #fff; }
|
|
1023
|
+
.vms-toast--warning { background: var(--vms-warning); color: #fff; }
|
|
1024
|
+
.vms-toast--success { background: color-mix(in srgb, var(--vms-success) 80%, #000); color: #fff; }
|
|
1025
|
+
.vms-toast--info { background: color-mix(in srgb, var(--vms-info) 88%, #000); color: #fff; }
|
|
1026
|
+
/* fade-out applied just before removal. */
|
|
1027
|
+
.vms-toast--leaving { opacity: 0; transform: translateY(0.25rem); }
|
|
1028
|
+
|
|
1029
|
+
/* ── Empty state (first-class "nothing here" presentation) ──
|
|
1030
|
+
A centered block: heading, optional message, optional CTA button. */
|
|
1031
|
+
.vms-empty-state {
|
|
1032
|
+
display: flex;
|
|
1033
|
+
flex-direction: column;
|
|
1034
|
+
align-items: center;
|
|
1035
|
+
justify-content: center;
|
|
1036
|
+
gap: var(--vms-space-sm);
|
|
1037
|
+
text-align: center;
|
|
1038
|
+
padding: var(--vms-space-xl) var(--vms-space-md);
|
|
1039
|
+
color: var(--vms-text-muted);
|
|
1040
|
+
}
|
|
1041
|
+
.vms-empty-state__heading {
|
|
1042
|
+
font-size: var(--vms-text-lg);
|
|
1043
|
+
font-weight: 600;
|
|
1044
|
+
color: var(--vms-text);
|
|
1045
|
+
}
|
|
1046
|
+
.vms-empty-state__message {
|
|
1047
|
+
font-size: var(--vms-text-base);
|
|
1048
|
+
color: var(--vms-text-muted);
|
|
1049
|
+
max-width: min(40ch, 100%);
|
|
1050
|
+
}
|
|
1051
|
+
/* The CTA button inside an empty state centers (override the button's default
|
|
1052
|
+
align-self:flex-start) so it sits under the centered heading/message. */
|
|
1053
|
+
.vms-empty-state .vms-button { align-self: center; }
|
|
1054
|
+
|
|
1055
|
+
/* ── Badge (compact inline status pill / count) ──
|
|
1056
|
+
Leaf inline element. Default = neutral filled tint; tone recolors it; emphasis
|
|
1057
|
+
switches between a solid fill (primary) and an outline (secondary). */
|
|
1058
|
+
.vms-badge {
|
|
1059
|
+
--_badge-tone: var(--vms-text-muted);
|
|
1060
|
+
display: inline-flex;
|
|
1061
|
+
align-items: center;
|
|
1062
|
+
/* Center the pill on the surrounding text instead of baselining it (an
|
|
1063
|
+
inline-flex pill otherwise hangs below the baseline of adjacent text). */
|
|
1064
|
+
vertical-align: middle;
|
|
1065
|
+
gap: var(--vms-space-2xs);
|
|
1066
|
+
font-size: var(--vms-text-xs);
|
|
1067
|
+
font-weight: 600;
|
|
1068
|
+
line-height: 1;
|
|
1069
|
+
padding: 0.2em 0.6em;
|
|
1070
|
+
border-radius: 999px;
|
|
1071
|
+
white-space: nowrap;
|
|
1072
|
+
/* default (no tone, no emphasis): subtle neutral tint pill. */
|
|
1073
|
+
background: color-mix(in srgb, var(--_badge-tone) 16%, var(--vms-surface));
|
|
1074
|
+
color: var(--_badge-tone);
|
|
1075
|
+
border: 1px solid transparent;
|
|
1076
|
+
}
|
|
1077
|
+
/* tone — the universal status color, sets the pill's working color. */
|
|
1078
|
+
.vms-badge--danger { --_badge-tone: var(--vms-error); }
|
|
1079
|
+
.vms-badge--warning { --_badge-tone: var(--vms-warning); }
|
|
1080
|
+
.vms-badge--success { --_badge-tone: var(--vms-success); }
|
|
1081
|
+
.vms-badge--info { --_badge-tone: var(--vms-info); }
|
|
1082
|
+
/* emphasis — primary = solid filled tone (white text); secondary = outline.
|
|
1083
|
+
Declared after tone so the chosen --_badge-tone drives both. */
|
|
1084
|
+
.vms-badge--primary {
|
|
1085
|
+
background: var(--_badge-tone);
|
|
1086
|
+
border-color: var(--_badge-tone);
|
|
1087
|
+
color: #fff;
|
|
1088
|
+
}
|
|
1089
|
+
.vms-badge--secondary {
|
|
1090
|
+
background: transparent;
|
|
1091
|
+
border-color: var(--_badge-tone);
|
|
1092
|
+
color: var(--_badge-tone);
|
|
1093
|
+
}
|