anentrypoint-design 1.0.9 → 1.0.11

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.
@@ -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
- .app-topbar > .app-topbar-theme { flex: 0 0 auto; }
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, String(v)]) }) }),
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 || '(none)'],
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
- state.body = await page(h0, instance);
79
- state.error = null;
87
+ body = await page(h0, instance);
80
88
  } catch (e) {
81
- state.error = String(e && e.stack || e);
82
- state.body = Panel({ title: 'error', children: pre(state.error) });
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
  }
@@ -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
- var(--os-bg-0);
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,6 +366,8 @@ 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;
357
371
  }
358
372
  .ds-247420 .wm-win.wm-focused {
359
373
  border-color: color-mix(in oklab, var(--os-fg) 28%, transparent);
@@ -364,6 +378,8 @@ html, body {
364
378
  background: var(--os-bg-2);
365
379
  border: none;
366
380
  padding: var(--space-2) var(--space-2-75);
381
+ display: flex;
382
+ align-items: center;
367
383
  gap: var(--space-2-5);
368
384
  transition: background 120ms ease;
369
385
  }
@@ -442,7 +458,12 @@ html, body {
442
458
  linear-gradient(135deg, transparent 0 45%, currentColor 45% 55%, transparent 55% 100%),
443
459
  linear-gradient(135deg, transparent 0 70%, currentColor 70% 80%, transparent 80% 100%);
444
460
  }
445
- .ds-247420 .wm-body { background: var(--os-bg-1); }
461
+ .ds-247420 .wm-body {
462
+ background: var(--os-bg-1);
463
+ flex: 1 1 auto;
464
+ min-height: 0;
465
+ overflow: auto;
466
+ }
446
467
 
447
468
  .os-side-rail {
448
469
  position: fixed;
@@ -593,6 +614,18 @@ html, body {
593
614
  .app-frame { width: 100%; height: 100%; border: 0; background: var(--os-bg-0); }
594
615
  .app-iframe { width: 100%; height: 100%; border: 0; display: block; }
595
616
  .app-iframe.web { background: #ffffff; } /* intentional: white canvas for embedded external web content that expects white */
617
+ /* Browser-pane empty state (about:blank): a bare framed iframe with no
618
+ * content is a large dead rectangle -- this placeholder fills that space
619
+ * with a real affordance until a navigation lands. Hidden the instant the
620
+ * pane's own url stops being about:blank (docs/browser-pane.js). */
621
+ .bp-empty {
622
+ width: 100%; height: 100%;
623
+ display: flex; align-items: center; justify-content: center;
624
+ color: var(--os-fg-3);
625
+ font: var(--fs-sm) var(--os-font);
626
+ background: var(--os-bg-1);
627
+ text-align: center;
628
+ }
596
629
  .app-text-cursor { }
597
630
 
598
631
  @media (prefers-reduced-motion: no-preference) {
@@ -936,7 +969,11 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
936
969
  .ds-247420 .tb-sess-badge {
937
970
  display: inline-flex; align-items: center; justify-content: center;
938
971
  min-width: 16px; height: 14px; padding: 0 var(--space-1);
939
- border-radius: var(--radius-sm, var(--r-0));
972
+ /* A neutral window-COUNT chip, not a notification/alert badge -- a small
973
+ * corner radius (--r-0, 4px) reads as a rectangular count pill, distinct
974
+ * from the fully-round .tb-sess-dot presence indicator elsewhere in this
975
+ * chip, so the two don't get visually conflated. */
976
+ border-radius: var(--r-0);
940
977
  background: color-mix(in oklab, var(--fg) 14%, transparent);
941
978
  font-size: var(--fs-pico); font-weight: 600; line-height: 1;
942
979
  }
@@ -1071,6 +1108,10 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
1071
1108
  .ds-247420 .tb-sessions-card-actions { display: flex; gap: var(--space-1-75); margin-top: var(--space-1); }
1072
1109
  .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
1110
  .ds-247420 .tb-sessions-card-actions button:hover { background: color-mix(in oklab, var(--fg) 6%, transparent); }
1111
+ /* Baseline (non-hover) danger tint so the destructive action reads as
1112
+ * distinct at rest, not only on hover -- otherwise Delete is visually
1113
+ * identical to Active/Export until the pointer happens to land on it. */
1114
+ .ds-247420 .tb-sessions-card-actions button.danger { color: var(--color-danger); border-color: color-mix(in oklab, var(--color-danger) 40%, transparent); }
1074
1115
  .ds-247420 .tb-sessions-card-actions button.danger:hover { background: var(--color-danger); color: var(--on-color); border-color: var(--color-danger); }
1075
1116
  .ds-247420 .tb-sessions-empty-mid { padding: 40px; text-align: center; opacity: 0.6; font-size: var(--fs-tiny); }
1076
1117
 
@@ -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
- C.Btn({ key: 'b4', variant: 'danger', children: 'Danger' }),
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. */
@@ -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