anentrypoint-design 1.0.10 → 1.0.12
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/247420.css +179 -8
- package/dist/247420.js +56 -30
- package/dist/index.html +29 -3
- package/package.json +1 -1
- package/src/components/chat/message.js +1 -1
- package/src/components/content/charts.js +26 -5
- package/src/components/shell/atoms.js +3 -2
- package/src/components/shell/icons.js +13 -1
- package/src/components/theme-toggle.js +18 -0
- package/src/css/app-shell/chat-polish.css +18 -0
- package/src/css/app-shell/hero-content.css +38 -5
- package/src/css/app-shell/kits-appended.css +56 -2
- package/src/css/app-shell/primitives.css +4 -0
- package/src/css/app-shell/row-print.css +13 -0
- package/src/css/app-shell/sidebar-misc.css +15 -0
- package/src/css/app-shell/topbar.css +35 -1
- package/src/kits/os/freddie/pages-core.js +13 -3
- package/src/kits/os/freddie/pages-os.js +2 -2
- package/src/kits/os/freddie-dashboard.js +16 -5
- package/src/kits/os/theme.css +109 -7
- package/src/kits/os/wm.js +41 -2
- package/src/page-html/client-script.js +18 -2
- package/src/page-html/page-styles.js +10 -0
- package/types/components.d.ts +1 -1
|
@@ -80,7 +80,23 @@ body.canvas-host { background: transparent !important; }
|
|
|
80
80
|
.app-topbar > .brand { flex: 0 0 auto; }
|
|
81
81
|
.app-topbar > .app-search { flex: 1 1 auto; }
|
|
82
82
|
.app-topbar > nav { margin-left: auto; }
|
|
83
|
-
|
|
83
|
+
/* Utility cluster grouping: the theme toggle sits after the repo-info nav
|
|
84
|
+
links with no visual separation between the two groups, so the row read
|
|
85
|
+
as one undifferentiated strip of controls. A thin inset rule plus extra
|
|
86
|
+
left gap reads as "these are a distinct utility group", matching the
|
|
87
|
+
subtle divider already used between .app-status segments. */
|
|
88
|
+
.app-topbar > .app-topbar-theme {
|
|
89
|
+
flex: 0 0 auto;
|
|
90
|
+
margin-left: var(--space-2);
|
|
91
|
+
padding-left: var(--space-3);
|
|
92
|
+
position: relative;
|
|
93
|
+
}
|
|
94
|
+
.app-topbar > .app-topbar-theme::before {
|
|
95
|
+
content: ''; position: absolute; left: 0; top: 50%;
|
|
96
|
+
transform: translateY(-50%);
|
|
97
|
+
width: 1px; height: 18px;
|
|
98
|
+
background: var(--rule-strong);
|
|
99
|
+
}
|
|
84
100
|
.app-topbar nav { display: flex; gap: var(--space-1); font-size: var(--fs-sm); flex-wrap: wrap; }
|
|
85
101
|
.app-topbar nav a { flex: 0 0 auto; }
|
|
86
102
|
.app-topbar nav a {
|
|
@@ -171,6 +187,15 @@ body.canvas-host { background: transparent !important; }
|
|
|
171
187
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
172
188
|
min-width: 0; flex: 0 1 auto;
|
|
173
189
|
}
|
|
190
|
+
/* Ancestor trail segments previously had no rule of their own and simply
|
|
191
|
+
inherited .app-crumb's base --fg-3 — the same tone the separators sit on
|
|
192
|
+
top of (before their own 0.5 opacity dims them further). That leaves only
|
|
193
|
+
two visually distinct tiers in practice (dim separator, bold leaf) rather
|
|
194
|
+
than three, since the ancestor tone reads as "default text colour", not a
|
|
195
|
+
deliberate middle step. --fg-3 is the crumb bar's OWN base, so explicitly
|
|
196
|
+
restating it here is the anchor for the 3-tier read: separator (--fg-3 at
|
|
197
|
+
0.5 opacity, dimmest) < ancestor (--fg-3, mid) < leaf (--fg bold, brightest). */
|
|
198
|
+
.app-crumb > span:not(.sep):not(.leaf):not(.crumb-right) { color: var(--fg-3); }
|
|
174
199
|
.app-crumb .leaf { color: var(--fg); font-weight: 600; flex: 0 1 auto; }
|
|
175
200
|
.app-crumb .crumb-right { margin-left: auto; color: var(--fg-3); flex: 0 0 auto; }
|
|
176
201
|
|
|
@@ -189,6 +214,15 @@ body.canvas-host { background: transparent !important; }
|
|
|
189
214
|
.app-side-shell {
|
|
190
215
|
background: var(--bg); overflow-y: auto; min-height: 0;
|
|
191
216
|
scrollbar-width: thin; scrollbar-color: var(--rule-strong) transparent;
|
|
217
|
+
/* Bottom-edge fade: a rail longer than the viewport otherwise shows only
|
|
218
|
+
the thin scrollbar track as its sole "there is more below" cue. Mirrors
|
|
219
|
+
the horizontal fade already used by .os-instances (src/kits/os/theme.css)
|
|
220
|
+
for its own overflow edge, oriented top-to-bottom for this vertical
|
|
221
|
+
scroller. The top edge stays fully opaque (rows start flush under the
|
|
222
|
+
topbar with no need to hint upward scroll on initial load); only the
|
|
223
|
+
trailing 24px softens toward transparent. */
|
|
224
|
+
-webkit-mask-image: linear-gradient(to bottom, black calc(100% - 24px), transparent);
|
|
225
|
+
mask-image: linear-gradient(to bottom, black calc(100% - 24px), transparent);
|
|
192
226
|
}
|
|
193
227
|
.app-side-shell::-webkit-scrollbar { width: 8px; }
|
|
194
228
|
.app-side-shell::-webkit-scrollbar-thumb { background: var(--rule-strong); border-radius: var(--r-pill, 999px); }
|
|
@@ -44,6 +44,16 @@ export function makeCorePages(ctx) {
|
|
|
44
44
|
const tools = h0.pi.tools.size;
|
|
45
45
|
const skills = h0.pi.skills.size;
|
|
46
46
|
const health = (typeof h0.pi.health === 'function') ? h0.pi.health() : { ok: true };
|
|
47
|
+
// Epoch-ms fields (ts, or any *At/*Time-suffixed key) render as raw
|
|
48
|
+
// 13-digit numbers otherwise -- unreadable and gives no sense of
|
|
49
|
+
// recency. Format as a locale timestamp; anything that doesn't
|
|
50
|
+
// parse as a plausible epoch-ms value falls through to String(v)
|
|
51
|
+
// unchanged so this never mangles a genuine small integer.
|
|
52
|
+
const isEpochMsKey = (k) => k === 'ts' || /(At|Time)$/.test(k);
|
|
53
|
+
const fmtHealthValue = (k, v) => {
|
|
54
|
+
if (isEpochMsKey(k) && typeof v === 'number' && v > 1e12) return new Date(v).toLocaleString();
|
|
55
|
+
return String(v);
|
|
56
|
+
};
|
|
47
57
|
return [
|
|
48
58
|
Hero({ title: 'assistant', body: 'open js agent harness — in-page agent runtime.', accent: h0.version || 'web' }),
|
|
49
59
|
Kpi({ items: [[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']] }),
|
|
@@ -55,7 +65,7 @@ export function makeCorePages(ctx) {
|
|
|
55
65
|
['set api key', 'keys tab -> click chip to set value'],
|
|
56
66
|
['add cron', 'cron tab -> form'],
|
|
57
67
|
] }) }),
|
|
58
|
-
Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k,
|
|
68
|
+
Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k, fmtHealthValue(k, v)]) }) }),
|
|
59
69
|
];
|
|
60
70
|
},
|
|
61
71
|
async sessions(h0) {
|
|
@@ -79,7 +89,7 @@ export function makeCorePages(ctx) {
|
|
|
79
89
|
Kpi({ items: [[list.length, 'sessions']] }),
|
|
80
90
|
Panel({ title: 'recent sessions', count: list.length, children: list.length === 0
|
|
81
91
|
? EmptyState({ text: 'no sessions yet — open chat and send a message', glyph: Icon('thread') })
|
|
82
|
-
: Table({ headers: ['id', 'title', 'platform', 'model', 'cwd', 'skill', ''], rows }) }),
|
|
92
|
+
: Table({ headers: ['id', 'title', 'platform', 'model', 'cwd', 'skill', ''], striped: true, rows }) }),
|
|
83
93
|
];
|
|
84
94
|
},
|
|
85
95
|
async agents(h0) {
|
|
@@ -88,7 +98,7 @@ export function makeCorePages(ctx) {
|
|
|
88
98
|
Kpi({ items: [[a.count || 0, 'active'], [a.turns || 0, 'turns']] }),
|
|
89
99
|
Panel({ title: 'agent overview', children: Receipt({ rows: [
|
|
90
100
|
['total turns', String(a.turns || 0)],
|
|
91
|
-
['active session', a.active || '
|
|
101
|
+
['active session', a.active || '—'],
|
|
92
102
|
['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
|
|
93
103
|
] }) }),
|
|
94
104
|
];
|
|
@@ -14,7 +14,7 @@ export function makeOsPages(ctx) {
|
|
|
14
14
|
Kpi({ items: [[list.length, 'instances'], [activeId || '—', 'active']] }),
|
|
15
15
|
Panel({ title: 'instances', count: list.length, children: list.length === 0
|
|
16
16
|
? EmptyState({ text: 'no instances', glyph: Icon('square') })
|
|
17
|
-
: Table({ headers: ['id', 'active', 'shells', 'windows'],
|
|
17
|
+
: Table({ headers: ['id', 'active', 'shells', 'windows'], striped: true,
|
|
18
18
|
rows: list.map(i => [i.id, i.id === activeId ? Icon('circle') : '', String((i.shells || []).length), String((i.windows || []).length)]) }) }),
|
|
19
19
|
];
|
|
20
20
|
},
|
|
@@ -25,7 +25,7 @@ export function makeOsPages(ctx) {
|
|
|
25
25
|
Kpi({ items: [[wins.length, 'windows'], [focused ? (focused.id || focused.title || '?') : '—', 'focused']] }),
|
|
26
26
|
Panel({ title: 'windows', count: wins.length, children: wins.length === 0
|
|
27
27
|
? EmptyState({ text: 'no windows open', glyph: Icon('square') })
|
|
28
|
-
: Table({ headers: ['id', 'title', 'min', 'max', 'pos'],
|
|
28
|
+
: Table({ headers: ['id', 'title', 'min', 'max', 'pos'], striped: true,
|
|
29
29
|
rows: wins.map(w => [w.id || '?', w.title || '', w.min ? Icon('circle') : '', w.max ? Icon('circle') : '',
|
|
30
30
|
(w.el ? `${w.el.offsetLeft},${w.el.offsetTop} ${w.el.offsetWidth}×${w.el.offsetHeight}` : '')]) }) }),
|
|
31
31
|
];
|
|
@@ -19,6 +19,13 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces, loading
|
|
|
19
19
|
const state = { active: 'home', ts: new Date().toLocaleTimeString(), body: null, error: null };
|
|
20
20
|
let host = instance.host || null;
|
|
21
21
|
const allRoutes = osSurfaces ? [...ROUTES, ...OS_ROUTE_DEFS] : ROUTES;
|
|
22
|
+
// Bumped on every setActive() and captured by loadActive() before its
|
|
23
|
+
// first await; a resolving page whose generation no longer matches the
|
|
24
|
+
// live one is a stale in-flight nav (user clicked a second page before
|
|
25
|
+
// the first's async page() resolved) and must not overwrite state.body —
|
|
26
|
+
// otherwise the crumb/side (synchronous) shows the new page while the
|
|
27
|
+
// main content silently keeps whichever page happened to resolve last.
|
|
28
|
+
let navGeneration = 0;
|
|
22
29
|
|
|
23
30
|
async function ensureHost() {
|
|
24
31
|
if (host) return host;
|
|
@@ -27,7 +34,7 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces, loading
|
|
|
27
34
|
return host;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
function setActive(p) { state.active = p; rerender(); }
|
|
37
|
+
function setActive(p) { state.active = p; navGeneration++; rerender(); }
|
|
31
38
|
if (typeof window !== 'undefined') window.__fd_nav = setActive;
|
|
32
39
|
|
|
33
40
|
function rerender() { webjsx.applyDiff(root, view()); loadActive(); }
|
|
@@ -72,15 +79,19 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces, loading
|
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
async function loadActive() {
|
|
82
|
+
const myGeneration = navGeneration;
|
|
83
|
+
let body, error = null;
|
|
75
84
|
try {
|
|
76
85
|
const h0 = await ensureHost();
|
|
77
86
|
const page = PAGES[state.active] || PAGES.home;
|
|
78
|
-
|
|
79
|
-
state.error = null;
|
|
87
|
+
body = await page(h0, instance);
|
|
80
88
|
} catch (e) {
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
error = String(e && e.stack || e);
|
|
90
|
+
body = Panel({ title: 'error', children: pre(error) });
|
|
83
91
|
}
|
|
92
|
+
if (myGeneration !== navGeneration) return; // superseded by a later nav click
|
|
93
|
+
state.body = body;
|
|
94
|
+
state.error = error;
|
|
84
95
|
state.ts = new Date().toLocaleTimeString();
|
|
85
96
|
webjsx.applyDiff(root, view());
|
|
86
97
|
}
|
package/src/kits/os/theme.css
CHANGED
|
@@ -105,12 +105,24 @@ html, body {
|
|
|
105
105
|
* (barely above the base tone, no imagery/texture asset to fetch or theme)
|
|
106
106
|
* gives the desktop depth without competing with window chrome — scoped to
|
|
107
107
|
* .ds-247420 .wm-root specifically (the actual desktop viewport BEHIND
|
|
108
|
-
* windows), not the page background used by bare/non-OS surfaces.
|
|
108
|
+
* windows), not the page background used by bare/non-OS surfaces.
|
|
109
|
+
* A faint repeating dot-grid is layered on top of the wash: the radial wash
|
|
110
|
+
* alone reads as a barely-perceptible tint over a large empty area (the
|
|
111
|
+
* reported "huge empty dark canvas" below a single default window), while a
|
|
112
|
+
* grid of low-opacity dots gives the eye actual anchor points across that
|
|
113
|
+
* space without competing with window chrome or requiring an image asset. */
|
|
109
114
|
.ds-247420 .wm-root {
|
|
110
|
-
background
|
|
115
|
+
/* Split into background-image/-color longhands, NOT the `background`
|
|
116
|
+
shorthand: some engines fail to parse a multi-layer gradient list
|
|
117
|
+
shorthand-form when any layer's color-stop uses color-mix() — the
|
|
118
|
+
whole declaration is dropped and the canvas silently renders flat.
|
|
119
|
+
The longhand form parses these same layers correctly. */
|
|
120
|
+
background-image:
|
|
121
|
+
radial-gradient(circle, color-mix(in oklab, var(--os-fg) 6%, transparent) 1px, transparent 1px),
|
|
111
122
|
radial-gradient(ellipse 120% 80% at 20% -10%, color-mix(in oklab, var(--os-accent) 6%, transparent), transparent 60%),
|
|
112
|
-
radial-gradient(ellipse 100% 70% at 100% 100%, color-mix(in oklab, var(--os-accent) 4%, transparent), transparent 55%)
|
|
113
|
-
|
|
123
|
+
radial-gradient(ellipse 100% 70% at 100% 100%, color-mix(in oklab, var(--os-accent) 4%, transparent), transparent 55%);
|
|
124
|
+
background-color: var(--os-bg-0);
|
|
125
|
+
background-size: 28px 28px, auto, auto;
|
|
114
126
|
}
|
|
115
127
|
@media (prefers-reduced-transparency: reduce) {
|
|
116
128
|
.ds-247420 .wm-root { background: var(--os-bg-0); }
|
|
@@ -354,7 +366,65 @@ html, body {
|
|
|
354
366
|
border-radius: var(--radius-lg, var(--r-3));
|
|
355
367
|
box-shadow: var(--shadow-2);
|
|
356
368
|
overflow: hidden;
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-direction: column;
|
|
371
|
+
/* Open/maximize/restore transitions: transform+opacity are interpolable
|
|
372
|
+
* (unlike wm-min's `display:none`, which cannot transition at all — a
|
|
373
|
+
* real minimize/restore animation needs wm.js to defer the display swap
|
|
374
|
+
* until this transition ends, tracked as its own follow-up). A freshly
|
|
375
|
+
* mounted .wm-win starts from CSS-animation-in via @starting-style so it
|
|
376
|
+
* scales/fades into place instead of snapping to full size, and every
|
|
377
|
+
* maximize/restore toggle (a real geometry change, not a display swap)
|
|
378
|
+
* eases instead of jumping. */
|
|
379
|
+
transition: opacity 160ms ease, transform 160ms cubic-bezier(0.2, 0.8, 0.3, 1), box-shadow 160ms ease;
|
|
380
|
+
}
|
|
381
|
+
@supports (transition-behavior: allow-discrete) {
|
|
382
|
+
.ds-247420 .wm-win {
|
|
383
|
+
transition: opacity 160ms ease, transform 160ms cubic-bezier(0.2, 0.8, 0.3, 1), box-shadow 160ms ease;
|
|
384
|
+
@starting-style {
|
|
385
|
+
opacity: 0;
|
|
386
|
+
transform: scale(0.96);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
@media (prefers-reduced-motion: reduce) {
|
|
391
|
+
.ds-247420 .wm-win { transition: none; }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* Minimize/restore/close: wm.js's animateMinimize()/animateClose() (wm.js)
|
|
395
|
+
* apply these classes BEFORE the real wm-min toggle / el.remove(), holding
|
|
396
|
+
* them for exactly the duration below so the display:none / DOM-removal
|
|
397
|
+
* that neither CSS nor `transition` can animate happens only after the
|
|
398
|
+
* visual scale+fade has actually played out. Direction: minimize collapses
|
|
399
|
+
* toward the taskbar (down+shrink); close shrinks in place; restore is the
|
|
400
|
+
* minimize keyframe in reverse. Not scoped to .ds-247420 -- these classes
|
|
401
|
+
* are always renderWindow()-authored, own their state fully. */
|
|
402
|
+
.wm-win.wm-minimizing {
|
|
403
|
+
transition: opacity 200ms ease, transform 200ms cubic-bezier(0.4, 0, 1, 1);
|
|
404
|
+
opacity: 0;
|
|
405
|
+
transform: scale(0.85) translateY(24px);
|
|
406
|
+
pointer-events: none;
|
|
407
|
+
}
|
|
408
|
+
.wm-win.wm-restoring {
|
|
409
|
+
transition: opacity 200ms ease, transform 200ms cubic-bezier(0.2, 0.8, 0.3, 1);
|
|
410
|
+
animation: wm-restore-in 200ms cubic-bezier(0.2, 0.8, 0.3, 1);
|
|
411
|
+
}
|
|
412
|
+
@keyframes wm-restore-in {
|
|
413
|
+
from { opacity: 0; transform: scale(0.85) translateY(24px); }
|
|
414
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
357
415
|
}
|
|
416
|
+
.wm-win.wm-closing {
|
|
417
|
+
transition: opacity 200ms ease, transform 200ms cubic-bezier(0.4, 0, 1, 1);
|
|
418
|
+
opacity: 0;
|
|
419
|
+
transform: scale(0.92);
|
|
420
|
+
pointer-events: none;
|
|
421
|
+
}
|
|
422
|
+
@media (prefers-reduced-motion: reduce) {
|
|
423
|
+
.wm-win.wm-minimizing, .wm-win.wm-restoring, .wm-win.wm-closing {
|
|
424
|
+
transition: none; animation: none; opacity: 1; transform: none;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
358
428
|
.ds-247420 .wm-win.wm-focused {
|
|
359
429
|
border-color: color-mix(in oklab, var(--os-fg) 28%, transparent);
|
|
360
430
|
box-shadow: inset var(--space-1) 0 0 var(--os-accent), var(--shadow-3);
|
|
@@ -364,6 +434,8 @@ html, body {
|
|
|
364
434
|
background: var(--os-bg-2);
|
|
365
435
|
border: none;
|
|
366
436
|
padding: var(--space-2) var(--space-2-75);
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
367
439
|
gap: var(--space-2-5);
|
|
368
440
|
transition: background 120ms ease;
|
|
369
441
|
}
|
|
@@ -380,7 +452,12 @@ html, body {
|
|
|
380
452
|
letter-spacing: 0.005em;
|
|
381
453
|
text-align: center;
|
|
382
454
|
font-weight: 600;
|
|
383
|
-
|
|
455
|
+
/* 0.72, not 0.65: measured live against the unfocused titlebar's actual
|
|
456
|
+
* background (rgb(77,77,82)) the opacity-blended effective contrast at
|
|
457
|
+
* 0.65 was 4.25:1 -- below WCAG AA's 4.5:1 floor for normal text. 0.72
|
|
458
|
+
* clears it with margin (~4.8:1) while staying visually dimmer than the
|
|
459
|
+
* focused window's full-opacity title. */
|
|
460
|
+
opacity: 0.72;
|
|
384
461
|
transition: opacity 120ms ease;
|
|
385
462
|
/* No text-transform here: the titlebar renders the window's own dynamic
|
|
386
463
|
* title (renderWindow({title}) / setTitle(t) in wm.js) — a user- or
|
|
@@ -442,7 +519,12 @@ html, body {
|
|
|
442
519
|
linear-gradient(135deg, transparent 0 45%, currentColor 45% 55%, transparent 55% 100%),
|
|
443
520
|
linear-gradient(135deg, transparent 0 70%, currentColor 70% 80%, transparent 80% 100%);
|
|
444
521
|
}
|
|
445
|
-
.ds-247420 .wm-body {
|
|
522
|
+
.ds-247420 .wm-body {
|
|
523
|
+
background: var(--os-bg-1);
|
|
524
|
+
flex: 1 1 auto;
|
|
525
|
+
min-height: 0;
|
|
526
|
+
overflow: auto;
|
|
527
|
+
}
|
|
446
528
|
|
|
447
529
|
.os-side-rail {
|
|
448
530
|
position: fixed;
|
|
@@ -593,6 +675,18 @@ html, body {
|
|
|
593
675
|
.app-frame { width: 100%; height: 100%; border: 0; background: var(--os-bg-0); }
|
|
594
676
|
.app-iframe { width: 100%; height: 100%; border: 0; display: block; }
|
|
595
677
|
.app-iframe.web { background: #ffffff; } /* intentional: white canvas for embedded external web content that expects white */
|
|
678
|
+
/* Browser-pane empty state (about:blank): a bare framed iframe with no
|
|
679
|
+
* content is a large dead rectangle -- this placeholder fills that space
|
|
680
|
+
* with a real affordance until a navigation lands. Hidden the instant the
|
|
681
|
+
* pane's own url stops being about:blank (docs/browser-pane.js). */
|
|
682
|
+
.bp-empty {
|
|
683
|
+
width: 100%; height: 100%;
|
|
684
|
+
display: flex; align-items: center; justify-content: center;
|
|
685
|
+
color: var(--os-fg-3);
|
|
686
|
+
font: var(--fs-sm) var(--os-font);
|
|
687
|
+
background: var(--os-bg-1);
|
|
688
|
+
text-align: center;
|
|
689
|
+
}
|
|
596
690
|
.app-text-cursor { }
|
|
597
691
|
|
|
598
692
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -936,7 +1030,11 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
|
|
|
936
1030
|
.ds-247420 .tb-sess-badge {
|
|
937
1031
|
display: inline-flex; align-items: center; justify-content: center;
|
|
938
1032
|
min-width: 16px; height: 14px; padding: 0 var(--space-1);
|
|
939
|
-
|
|
1033
|
+
/* A neutral window-COUNT chip, not a notification/alert badge -- a small
|
|
1034
|
+
* corner radius (--r-0, 4px) reads as a rectangular count pill, distinct
|
|
1035
|
+
* from the fully-round .tb-sess-dot presence indicator elsewhere in this
|
|
1036
|
+
* chip, so the two don't get visually conflated. */
|
|
1037
|
+
border-radius: var(--r-0);
|
|
940
1038
|
background: color-mix(in oklab, var(--fg) 14%, transparent);
|
|
941
1039
|
font-size: var(--fs-pico); font-weight: 600; line-height: 1;
|
|
942
1040
|
}
|
|
@@ -1071,6 +1169,10 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
|
|
|
1071
1169
|
.ds-247420 .tb-sessions-card-actions { display: flex; gap: var(--space-1-75); margin-top: var(--space-1); }
|
|
1072
1170
|
.ds-247420 .tb-sessions-card-actions button { flex: 1; height: 24px; font-size: var(--fs-nano); padding: 0 var(--space-2); border-radius: var(--radius-sm, var(--r-0)); border: 1px solid color-mix(in oklab, var(--fg) 16%, transparent); background: transparent; color: inherit; cursor: pointer; font-family: inherit; }
|
|
1073
1171
|
.ds-247420 .tb-sessions-card-actions button:hover { background: color-mix(in oklab, var(--fg) 6%, transparent); }
|
|
1172
|
+
/* Baseline (non-hover) danger tint so the destructive action reads as
|
|
1173
|
+
* distinct at rest, not only on hover -- otherwise Delete is visually
|
|
1174
|
+
* identical to Active/Export until the pointer happens to land on it. */
|
|
1175
|
+
.ds-247420 .tb-sessions-card-actions button.danger { color: var(--color-danger); border-color: color-mix(in oklab, var(--color-danger) 40%, transparent); }
|
|
1074
1176
|
.ds-247420 .tb-sessions-card-actions button.danger:hover { background: var(--color-danger); color: var(--on-color); border-color: var(--color-danger); }
|
|
1075
1177
|
.ds-247420 .tb-sessions-empty-mid { padding: 40px; text-align: center; opacity: 0.6; font-size: var(--fs-tiny); }
|
|
1076
1178
|
|
package/src/kits/os/wm.js
CHANGED
|
@@ -226,11 +226,11 @@ export function renderWindow(opts = {}) {
|
|
|
226
226
|
},
|
|
227
227
|
setFocused(v) { applyFocused(el, v); },
|
|
228
228
|
setMaximized(v) { applyMaximized(el, v); },
|
|
229
|
-
setMinimized(v) {
|
|
229
|
+
setMinimized(v) { animateMinimize(el, v); },
|
|
230
230
|
setInstanceId(id) { if (id) el.dataset.instanceId = id; else delete el.dataset.instanceId; },
|
|
231
231
|
setZIndex(z) { el.style.zIndex = String(z); },
|
|
232
232
|
getBounds() { return { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight }; },
|
|
233
|
-
dispose() { if (closeArmTimer) clearTimeout(closeArmTimer); el
|
|
233
|
+
dispose() { if (closeArmTimer) clearTimeout(closeArmTimer); animateClose(el); },
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -252,3 +252,42 @@ function setBodyContent(host, body) {
|
|
|
252
252
|
function applyFocused(el, v) { el.classList.toggle('wm-focused', !!v); }
|
|
253
253
|
function applyMaximized(el, v) { el.classList.toggle('wm-max', !!v); }
|
|
254
254
|
function applyMinimized(el, v) { el.classList.toggle('wm-min', !!v); }
|
|
255
|
+
|
|
256
|
+
function reducedMotion() {
|
|
257
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// wm.css's `.wm-win.wm-min{display:none}` cannot be CSS-transitioned (display
|
|
261
|
+
// has no interpolable intermediate value), so a real minimize animation needs
|
|
262
|
+
// the display swap deferred until a scale+fade transition actually finishes.
|
|
263
|
+
// `.wm-minimizing`/`.wm-restoring` (theme.css) carry the transform+opacity
|
|
264
|
+
// keyframes; this only sequences when `wm-min` itself flips. 220ms fallback
|
|
265
|
+
// timer guards against a transitionend that never fires (element removed
|
|
266
|
+
// mid-transition, browser tab backgrounded and rAF/transitions paused, etc.)
|
|
267
|
+
// so a window can never get stuck invisible-but-not-display:none.
|
|
268
|
+
function animateMinimize(el, v) {
|
|
269
|
+
if (reducedMotion()) { applyMinimized(el, v); return; }
|
|
270
|
+
if (v) {
|
|
271
|
+
el.classList.add('wm-minimizing');
|
|
272
|
+
const done = () => { el.classList.remove('wm-minimizing'); applyMinimized(el, true); };
|
|
273
|
+
el.addEventListener('transitionend', done, { once: true });
|
|
274
|
+
setTimeout(done, 220);
|
|
275
|
+
} else {
|
|
276
|
+
applyMinimized(el, false);
|
|
277
|
+
el.classList.add('wm-restoring');
|
|
278
|
+
setTimeout(() => el.classList.remove('wm-restoring'), 220);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Same display-can't-transition problem as minimize, but for the terminal
|
|
283
|
+
// close path: el.remove() used to happen synchronously, so a window vanished
|
|
284
|
+
// instantly with no close animation at all (the literal gap named in the
|
|
285
|
+
// "no animation on open/close" request). Fade+scale out, then remove.
|
|
286
|
+
function animateClose(el) {
|
|
287
|
+
if (!el.isConnected) return;
|
|
288
|
+
if (reducedMotion()) { el.remove(); return; }
|
|
289
|
+
el.classList.add('wm-closing');
|
|
290
|
+
const done = () => el.remove();
|
|
291
|
+
el.addEventListener('transitionend', done, { once: true });
|
|
292
|
+
setTimeout(done, 220);
|
|
293
|
+
}
|
|
@@ -35,9 +35,15 @@ function heroNode(hero) {
|
|
|
35
35
|
hero.body || hero.subheading,
|
|
36
36
|
hero.accent ? h('span', { class: 'ds-hero-accent' }, ' ' + hero.accent) : null,
|
|
37
37
|
) : null,
|
|
38
|
+
// Every hero CTA is a plain <a href> that navigates away from this page
|
|
39
|
+
// (GitHub, the portfolio site, ...), never an in-page action -- so the
|
|
40
|
+
// trailing arrow is appended here once, at the render layer, rather than
|
|
41
|
+
// hand-typed per label in content yaml where it could drift out of sync
|
|
42
|
+
// with the "navigates away = arrow" rule used elsewhere (row ctas,
|
|
43
|
+
// signin's in-page submit buttons).
|
|
38
44
|
Array.isArray(hero.ctas) && hero.ctas.length
|
|
39
45
|
? h('div', { class: 'ds-hero-actions' }, ...hero.ctas.map((c, i) =>
|
|
40
|
-
h('a', { key: i, class: i === 0 ? 'btn btn-accent' : 'btn btn-ghost', href: c.href || '#' }, c.label || c.cta || 'go')))
|
|
46
|
+
h('a', { key: i, class: i === 0 ? 'btn btn-accent' : 'btn btn-ghost', href: c.href || '#' }, (c.label || c.cta || 'go') + ' ->')))
|
|
41
47
|
: null,
|
|
42
48
|
badgeRow,
|
|
43
49
|
);
|
|
@@ -56,8 +62,18 @@ function showcaseNode(showcase) {
|
|
|
56
62
|
C.Btn({ key: 'b1', variant: 'primary', children: 'Primary' }),
|
|
57
63
|
C.Btn({ key: 'b2', variant: 'default', children: 'Default' }),
|
|
58
64
|
C.Btn({ key: 'b3', variant: 'ghost', children: 'Ghost' }),
|
|
59
|
-
|
|
65
|
+
// .ds-showcase-btn-danger-group gives Danger its own visually separated
|
|
66
|
+
// cluster (extra leading gap + a hairline divider) instead of sitting in
|
|
67
|
+
// the same uniform-gap row as Primary -- two similarly-weighted filled
|
|
68
|
+
// buttons side by side otherwise compete for "the one action to take"
|
|
69
|
+
// with nothing marking Danger as the deliberately-set-apart one.
|
|
70
|
+
h('span', { key: 'b4-group', class: 'ds-showcase-btn-danger-group' },
|
|
71
|
+
C.Btn({ key: 'b4', variant: 'danger', children: 'Danger' })),
|
|
60
72
|
);
|
|
73
|
+
// Hue documents STATUS, not decoration: green = shipping/live, blue = pre-
|
|
74
|
+
// release/in-flux, purple = newly added. Badge (not a bespoke pill) carries
|
|
75
|
+
// the '0 violations' count so every status indicator in this row is one of
|
|
76
|
+
// the two real chip-family components, never a one-off styled span.
|
|
61
77
|
const chipRow = h('div', { class: 'ds-showcase-row' },
|
|
62
78
|
C.Chip({ key: 'c1', tone: 'green', children: 'Live' }),
|
|
63
79
|
C.Chip({ key: 'c2', tone: 'blue', children: 'Beta' }),
|
|
@@ -106,6 +106,16 @@ a.row:hover .ds-row-arrow { opacity: 1 }
|
|
|
106
106
|
letter-spacing: 0.06em; color: var(--fg-3, #6b6b6b);
|
|
107
107
|
}
|
|
108
108
|
.ds-showcase-row { display: flex; flex-wrap: wrap; gap: var(--space-2, 8px); align-items: center; }
|
|
109
|
+
/* Sets Danger apart from Primary/Default/Ghost in the button showcase row: an
|
|
110
|
+
extra left margin beyond the row's own gap, plus a hairline divider, so a
|
|
111
|
+
danger action never reads as just one more same-weight choice beside the
|
|
112
|
+
primary action. */
|
|
113
|
+
.ds-showcase-btn-danger-group {
|
|
114
|
+
display: inline-flex; align-items: center;
|
|
115
|
+
margin-left: var(--space-3, 16px);
|
|
116
|
+
padding-left: var(--space-3, 16px);
|
|
117
|
+
border-left: 1px solid var(--rule, rgba(0,0,0,.12));
|
|
118
|
+
}
|
|
109
119
|
/* Card-grid layout (panel.layout === 'cards'): a browsable gallery of tiles
|
|
110
120
|
instead of a dense text-row list, opt-in per panel via home.yaml's
|
|
111
121
|
kits.layout: cards. */
|
package/types/components.d.ts
CHANGED
|
@@ -267,7 +267,7 @@ export declare function Lede(props?: LedeProps): VNode;
|
|
|
267
267
|
*/
|
|
268
268
|
export interface DotProps {
|
|
269
269
|
/** @default 'on' */
|
|
270
|
-
tone?: 'on' | 'live' | (string & {});
|
|
270
|
+
tone?: 'on' | 'live' | 'warn' | (string & {});
|
|
271
271
|
}
|
|
272
272
|
export declare function Dot(props?: DotProps): VNode;
|
|
273
273
|
|