anentrypoint-design 0.0.381 → 0.0.382
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 +228 -162
- package/dist/247420.js +24 -24
- package/package.json +1 -1
- package/src/components/content/avatar.js +31 -0
- package/src/components/content/charts.js +50 -0
- package/src/components/content/cli.js +46 -0
- package/src/components/content/feedback.js +77 -0
- package/src/components/content/fields.js +136 -0
- package/src/components/content/hero.js +146 -0
- package/src/components/content/lists.js +85 -0
- package/src/components/content/panel.js +79 -0
- package/src/components/content/row.js +115 -0
- package/src/components/content/table.js +97 -0
- package/src/components/content/views.js +81 -0
- package/src/components/content.js +29 -861
- package/src/components/editor-primitives/batch.js +60 -0
- package/src/components/editor-primitives/chrome.js +146 -0
- package/src/components/editor-primitives/collapse.js +46 -0
- package/src/components/editor-primitives/context-menu.js +127 -0
- package/src/components/editor-primitives/diagnostics.js +44 -0
- package/src/components/editor-primitives/focus-trap.js +26 -0
- package/src/components/editor-primitives/json-viewer.js +123 -0
- package/src/components/editor-primitives/layout.js +89 -0
- package/src/components/editor-primitives/modals.js +89 -0
- package/src/components/editor-primitives/pager.js +74 -0
- package/src/components/editor-primitives/property-grid.js +57 -0
- package/src/components/editor-primitives/shared.js +18 -0
- package/src/components/editor-primitives/split-panel.js +101 -0
- package/src/components/editor-primitives/toast.js +59 -0
- package/src/components/editor-primitives/tree.js +75 -0
- package/src/components/editor-primitives.js +35 -1048
- package/src/components/overlay-primitives/approval-prompt.js +38 -0
- package/src/components/overlay-primitives/auth-modal.js +89 -0
- package/src/components/overlay-primitives/command-palette.js +101 -0
- package/src/components/overlay-primitives/emoji-picker.js +103 -0
- package/src/components/overlay-primitives/floating.js +146 -0
- package/src/components/overlay-primitives/full-screen.js +45 -0
- package/src/components/overlay-primitives/menus.js +131 -0
- package/src/components/overlay-primitives/popover.js +51 -0
- package/src/components/overlay-primitives/roving-menu.js +73 -0
- package/src/components/overlay-primitives/settings-popover.js +85 -0
- package/src/components/overlay-primitives/tooltip.js +55 -0
- package/src/components/overlay-primitives.js +33 -855
- package/src/css/app-shell/chat-polish.css +39 -32
- package/src/css/app-shell/data-density.css +25 -21
- package/src/css/app-shell/files.css +37 -29
- package/src/css/app-shell/kits-appended.css +73 -50
- package/src/css/app-shell/responsive.css +47 -29
- package/src/kits/os/freddie-dashboard.css +2 -2
|
@@ -1,865 +1,33 @@
|
|
|
1
1
|
// Content blocks: Panel, Row, RowLink, Section, Hero, Install, Receipt,
|
|
2
2
|
// Changelog, WorksList, WritingList, Manifesto, Kpi, Table, HomeView,
|
|
3
3
|
// ProjectView, Form. Pure factories.
|
|
4
|
-
|
|
5
|
-
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
6
|
-
import { Btn, Heading, Lede, Dot, Icon, Chip } from './shell.js';
|
|
7
|
-
const h = webjsx.createElement;
|
|
8
|
-
|
|
9
|
-
// Avatar — generic identity disc: an image when `src` resolves, else a
|
|
10
|
-
// letter fallback derived from `name`/`fallback`. Kit previously only had
|
|
11
|
-
// scoped one-offs (chat.js `.chat-avatar`, community.js `.cm-user-avatar`)
|
|
12
|
-
// duplicating this same letter-fallback logic; this is the reusable version.
|
|
13
|
-
// avatarInitial — the single shared letter-fallback computation behind
|
|
14
|
-
// Avatar and every custom-colored avatar wrapper (community.js's voice/user/
|
|
15
|
-
// member rows use their own `--avatar-bg` CSS-variable styling and can't
|
|
16
|
-
// drop in the Avatar element directly, but still want the SAME fallback
|
|
17
|
-
// text, not their own independently-drifting .slice(0,n).toUpperCase()).
|
|
18
|
-
// Empty-guards to '?' identically everywhere it's used.
|
|
19
|
-
export function avatarInitial(name, count = 1) {
|
|
20
|
-
return name ? String(name).trim().slice(0, count).toUpperCase() || '?' : '?';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Avatar — the single letter-fallback/image avatar primitive. `initialsCount`
|
|
24
|
-
// (default 1) controls how many leading characters of `name` become the
|
|
25
|
-
// fallback letters when no `src`/`fallback` is given (community.js's
|
|
26
|
-
// pill-shaped ServerIcon wants 2); `shape` ('circle' default, or 'square')
|
|
27
|
-
// covers non-circular consumers without each hand-rolling its own
|
|
28
|
-
// .slice(0,n).toUpperCase() (previously duplicated across 5+ call sites in
|
|
29
|
-
// community.js and chat.js with drifting char-counts/empty-guards).
|
|
30
|
-
export function Avatar({ name, src, fallback, size = 'md', shape = 'circle', initialsCount = 1, key } = {}) {
|
|
31
|
-
const letter = fallback != null ? fallback : avatarInitial(name, initialsCount);
|
|
32
|
-
const cls = 'ds-avatar ds-avatar-' + size + (shape === 'square' ? ' ds-avatar-square' : '');
|
|
33
|
-
if (src) return h('img', { key, class: cls, src, alt: name || '', loading: 'lazy' });
|
|
34
|
-
return h('span', { key, class: cls, 'aria-hidden': !!name, role: name ? 'img' : undefined, 'aria-label': name || undefined }, letter);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function Panel({ title, count, right, style = '', class: className = '', children, kind, id }) {
|
|
38
|
-
const cls = 'panel' + (kind ? ' panel-' + kind : '') + (className ? ' ' + className : '');
|
|
39
|
-
return h('div', { class: cls, style, id: id || null },
|
|
40
|
-
title != null ? h('div', { class: 'panel-head' },
|
|
41
|
-
h('span', {}, title),
|
|
42
|
-
right != null ? right : (count != null ? h('span', {}, String(count)) : null)
|
|
43
|
-
) : null,
|
|
44
|
-
h('div', { class: 'panel-body' }, ...(Array.isArray(children) ? children : [children]))
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Card — semantic alias of Panel; behaves identically.
|
|
49
|
-
export const Card = Panel;
|
|
50
|
-
|
|
51
|
-
// Split a title string around case-insensitive matches of `highlight`, wrapping
|
|
52
|
-
// hits in <mark class="ds-hl">. Every segment is a keyed span so the children
|
|
53
|
-
// array never mixes keyed VElements with bare strings (webjsx applyDiff crashes
|
|
54
|
-
// on mixed keying).
|
|
55
|
-
function highlightTitle(title, highlight) {
|
|
56
|
-
const text = String(title);
|
|
57
|
-
const needle = String(highlight).toLowerCase();
|
|
58
|
-
if (!needle) return text;
|
|
59
|
-
const lower = text.toLowerCase();
|
|
60
|
-
const segs = [];
|
|
61
|
-
let pos = 0, n = 0;
|
|
62
|
-
while (pos <= text.length) {
|
|
63
|
-
const hit = lower.indexOf(needle, pos);
|
|
64
|
-
if (hit === -1) break;
|
|
65
|
-
if (hit > pos) segs.push(h('span', { key: 'hs' + n++ }, text.slice(pos, hit)));
|
|
66
|
-
segs.push(h('mark', { key: 'hs' + n++, class: 'ds-hl' }, text.slice(hit, hit + needle.length)));
|
|
67
|
-
pos = hit + needle.length;
|
|
68
|
-
}
|
|
69
|
-
if (!segs.length) return text;
|
|
70
|
-
if (pos < text.length) segs.push(h('span', { key: 'hs' + n++ }, text.slice(pos)));
|
|
71
|
-
return segs;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded, highlight, actions, detail }) {
|
|
75
|
-
// `rank` is an alias for `code` (the leading monospace index); callers use
|
|
76
|
-
// either name. `rail` renders a thin colour bar at the row's leading edge as
|
|
77
|
-
// a status indicator (tone: green | purple | flame | <any token>).
|
|
78
|
-
const codeVal = code != null ? code : rank;
|
|
79
|
-
// Support legacy active/selected props for backward compatibility
|
|
80
|
-
const isActive = state === 'active' || (state === 'default' && (active || selected));
|
|
81
|
-
const isLink = kind === 'link' || (href != null && !onClick);
|
|
82
|
-
const isButton = !isLink && !!onClick;
|
|
83
|
-
const stateCls = state === 'disabled' ? ' row-state-disabled' : (state === 'error' ? ' row-state-error' : '');
|
|
84
|
-
// With no leading/code, the title would otherwise land in the narrow code
|
|
85
|
-
// column and wrap; `row-nocode` collapses that column so the title gets the
|
|
86
|
-
// full width (meta still pinned right).
|
|
87
|
-
const noLead = codeVal == null && leading == null;
|
|
88
|
-
const cls = 'row' + (isActive ? ' active' : '') + stateCls + (cols ? ' row-grid' : '') + (noLead && !cols ? ' row-nocode' : '') + (rail ? ' rail-' + rail : '');
|
|
89
|
-
const isDisabled = state === 'disabled';
|
|
90
|
-
const props = { key, class: cls, style: cols ? `${style ? style + ';' : ''}grid-template-columns:${cols}` : style };
|
|
91
|
-
if (isLink) {
|
|
92
|
-
props.href = href || '#';
|
|
93
|
-
if (target) props.target = target;
|
|
94
|
-
} else if (isButton && !isDisabled) {
|
|
95
|
-
// Clickable div needs button semantics + keyboard activation for a11y parity.
|
|
96
|
-
// A disabled row is inert: no click, no button role, no tab stop.
|
|
97
|
-
props.onclick = onClick;
|
|
98
|
-
props.role = 'button';
|
|
99
|
-
props.tabindex = '0';
|
|
100
|
-
props.onkeydown = (e) => {
|
|
101
|
-
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(e); }
|
|
102
|
-
};
|
|
103
|
-
// When the row is a disclosure toggle (host passes a boolean `expanded`),
|
|
104
|
-
// announce its open/closed state so AT users hear "expanded/collapsed".
|
|
105
|
-
// Omitted entirely for plain action buttons (expanded === undefined).
|
|
106
|
-
if (expanded === true || expanded === false) props['aria-expanded'] = expanded ? 'true' : 'false';
|
|
107
|
-
}
|
|
108
|
-
if (isDisabled) props['aria-disabled'] = 'true';
|
|
109
|
-
if (isActive && (isLink || isButton)) props['aria-current'] = isActive ? 'page' : null;
|
|
110
|
-
// `highlight` wraps case-insensitive matches in the title in <mark class="ds-hl">.
|
|
111
|
-
// The segments live inside a single wrapper span so the title's child list
|
|
112
|
-
// never mixes keyed and unkeyed siblings.
|
|
113
|
-
const titleNode = (highlight && typeof title === 'string')
|
|
114
|
-
? h('span', {}, ...[].concat(highlightTitle(title, highlight)))
|
|
115
|
-
: title;
|
|
116
|
-
// `actions` render ONLY when the row is expanded, as a sibling action strip
|
|
117
|
-
// inside the row container; each button stops propagation so it never fires
|
|
118
|
-
// the row onClick.
|
|
119
|
-
const actionRow = (expanded === true && Array.isArray(actions) && actions.length)
|
|
120
|
-
? h('span', { class: 'row-actions', role: 'group', 'aria-label': 'row actions' },
|
|
121
|
-
...actions.map((a, i) => h('button', {
|
|
122
|
-
key: 'ract' + i,
|
|
123
|
-
type: 'button',
|
|
124
|
-
class: 'row-act',
|
|
125
|
-
title: a.title || a.label,
|
|
126
|
-
'aria-label': a.title || a.label,
|
|
127
|
-
onclick: (e) => { e.stopPropagation(); a.onClick && a.onClick(e); },
|
|
128
|
-
onkeydown: (e) => { e.stopPropagation(); },
|
|
129
|
-
}, a.label)))
|
|
130
|
-
: null;
|
|
131
|
-
// Color is not the only status channel: emit a visually-hidden word for the
|
|
132
|
-
// meaningful rail tones (error/subagent) so AT and color-blind users get the
|
|
133
|
-
// state. green is the unremarkable default - announcing "ok" everywhere would
|
|
134
|
-
// be AT noise - so it emits nothing.
|
|
135
|
-
const railWord = rail === 'flame' ? 'error' : rail === 'purple' ? 'subagent' : null;
|
|
136
|
-
// `detail` renders as a sibling block AFTER the title/meta children (its own
|
|
137
|
-
// line via flex-basis:100% in .ds-row-detail), not inside the title span.
|
|
138
|
-
// The same `highlight` search term that marks matches in the collapsed
|
|
139
|
-
// title previously stopped applying the moment a row expanded - the
|
|
140
|
-
// expanded body (often the ONLY place a match beyond the 220-char title
|
|
141
|
-
// window is actually visible) rendered as plain unmarked text.
|
|
142
|
-
const detailNode = (highlight && typeof detail === 'string')
|
|
143
|
-
? h('span', {}, ...[].concat(highlightTitle(detail, highlight)))
|
|
144
|
-
: detail;
|
|
145
|
-
return h(isLink ? 'a' : 'div', props,
|
|
146
|
-
railWord ? h('span', { class: 'sr-only' }, railWord) : null,
|
|
147
|
-
leading != null ? leading : (codeVal != null ? h('span', { class: 'code' }, codeVal) : null),
|
|
148
|
-
h('span', { class: 'title', title: typeof title === 'string' ? title : undefined }, titleNode, sub ? h('span', { class: 'sub', title: typeof sub === 'string' ? sub : undefined }, sub) : null),
|
|
149
|
-
trailing != null ? trailing : (meta != null ? h('span', { class: 'meta' }, meta) : null),
|
|
150
|
-
actionRow,
|
|
151
|
-
detail != null ? h('pre', { class: 'ds-row-detail' }, detailNode) : null);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function RowLink({ code, title, sub, meta, href = '#', key, target }) {
|
|
155
|
-
return Row({ code, title, sub, meta, href, kind: 'link', key, target });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// PanelFromItems — the shared 'items[] -> RowLink wrapped in Panel' mapper
|
|
159
|
-
// every portfolio consumer theme.mjs (zellous/wireweave/thebird/247420) had
|
|
160
|
-
// hand-rolled identically: items.map((it,i) => RowLink({code, title, sub,
|
|
161
|
-
// meta, href})) inside a titled Panel. `keyPrefix` seeds each row's stable
|
|
162
|
-
// key (`${keyPrefix}${i}`), matching the consumer convention of a
|
|
163
|
-
// one-letter-per-section prefix (e.g. 'f' for features, 'm' for modules).
|
|
164
|
-
// Field aliasing mirrors the union of shapes actually hand-rolled downstream:
|
|
165
|
-
// title reads `title` then `name`; sub reads `sub` then `desc`; code falls
|
|
166
|
-
// back to a zero-padded 1-based index when the item carries none. `heading`/
|
|
167
|
-
// `count`/`style`/`kind` pass through to Panel unchanged.
|
|
168
|
-
export function PanelFromItems({ heading, items = [], keyPrefix = 'i', count, style, kind, emptyText } = {}) {
|
|
169
|
-
if (!items || !items.length) return emptyText != null ? h('div', { class: 'empty' }, emptyText) : null;
|
|
170
|
-
const rows = items.map((it, i) => {
|
|
171
|
-
const codeVal = it.code != null ? it.code : (it.rank != null ? it.rank : String(i + 1).padStart(2, '0'));
|
|
172
|
-
return RowLink({
|
|
173
|
-
key: keyPrefix + i,
|
|
174
|
-
code: codeVal,
|
|
175
|
-
title: it.title != null ? it.title : it.name,
|
|
176
|
-
sub: it.sub != null ? it.sub : (it.desc != null ? it.desc : ''),
|
|
177
|
-
meta: it.meta != null ? it.meta : '',
|
|
178
|
-
href: it.href || '#'
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
return Panel({ title: heading, count, style, kind, children: rows });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function Section({ title, eyebrow, children, id }) {
|
|
185
|
-
return h('section', { class: 'ds-section', id: id || null },
|
|
186
|
-
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
187
|
-
title ? h('h3', {}, title) : null,
|
|
188
|
-
...(Array.isArray(children) ? children : [children])
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function Hero({ eyebrow, title, body, accent, actions, badges }) {
|
|
193
|
-
// Eyebrow + title share the title grid-area so the named-area layout stays
|
|
194
|
-
// intact; body occupies the wide left column, badges + actions stack in
|
|
195
|
-
// the narrow right column so it carries real visual weight instead of
|
|
196
|
-
// sitting empty beside the body copy.
|
|
197
|
-
const badgeList = Array.isArray(badges) ? badges.filter(Boolean) : [];
|
|
198
|
-
const badgeRow = badgeList.length
|
|
199
|
-
? h('div', { class: 'ds-hero-stats' }, ...badgeList.map((b, i) =>
|
|
200
|
-
h('span', { key: 'hb' + i, class: 'ds-hero-stat' }, String(b && b.label != null ? b.label : b))))
|
|
201
|
-
: null;
|
|
202
|
-
const actionRow = actions ? h('div', { class: 'ds-hero-actions' }, ...(Array.isArray(actions) ? actions : [actions])) : null;
|
|
203
|
-
const aside = (badgeRow || actionRow) ? h('div', { class: 'ds-hero-aside' }, badgeRow, actionRow) : null;
|
|
204
|
-
return h('div', { class: 'ds-hero' },
|
|
205
|
-
h('div', { class: 'ds-hero-head' },
|
|
206
|
-
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
207
|
-
h('h1', { class: 'ds-hero-title' }, title)
|
|
208
|
-
),
|
|
209
|
-
body ? h('p', { class: 'ds-hero-body' },
|
|
210
|
-
body,
|
|
211
|
-
accent ? h('span', { class: 'ds-hero-accent' }, ' ' + accent) : null
|
|
212
|
-
) : null,
|
|
213
|
-
aside
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// HeroFromPageData — a single factory for the "hero block driven by a page-data
|
|
218
|
-
// object" shape that recurs, independently hand-rolled, across every flatspace
|
|
219
|
-
// consumer theme.mjs (heading/subheading/body/badges/ctas/install all read off
|
|
220
|
-
// a `hero` object parsed from the `__site__` JSON script tag). Consumers differ
|
|
221
|
-
// only in which fields their content YAML populates; this factory renders every
|
|
222
|
-
// field it is given and omits what is absent, so it is a drop-in for the
|
|
223
|
-
// narrowest (heading+body only) or richest (badges+ctas+install) hero shape
|
|
224
|
-
// alike. Returns null on a falsy `hero` so callers can write
|
|
225
|
-
// `HeroFromPageData(page.hero)` unconditionally, matching the existing
|
|
226
|
-
// `!home.hero ? null : ...` guard every hand-rolled version repeats.
|
|
227
4
|
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
? h('div', { class: 'cli' },
|
|
258
|
-
h('span', { class: 'prompt' }, '$'),
|
|
259
|
-
h('span', { class: 'cmd' }, hero.install))
|
|
260
|
-
: null;
|
|
261
|
-
return h('div', { class: 'ds-hero' },
|
|
262
|
-
h('div', { class: 'ds-hero-head' },
|
|
263
|
-
hero.eyebrow ? h('span', { class: 'eyebrow' }, hero.eyebrow) : null,
|
|
264
|
-
h('h1', { class: 'ds-hero-title' }, heading)
|
|
265
|
-
),
|
|
266
|
-
hero.subheading ? h('p', { class: 'ds-hero-body lede' }, hero.subheading) : null,
|
|
267
|
-
hero.body ? h('p', { class: 'ds-hero-body' },
|
|
268
|
-
hero.body,
|
|
269
|
-
hero.accent ? h('span', { class: 'ds-hero-accent' }, ' ' + hero.accent) : null,
|
|
270
|
-
) : null,
|
|
271
|
-
(badgeRow || ctaRow || installRow)
|
|
272
|
-
? h('div', { class: 'ds-hero-aside' }, badgeRow, installRow, ctaRow)
|
|
273
|
-
: null,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function Marquee({ items = [], sep = '/' }) {
|
|
278
|
-
// No items -> no ticker: an empty marquee still paints its border-block
|
|
279
|
-
// rules as an unexplained full-width stripe.
|
|
280
|
-
if (!items.length) return null;
|
|
281
|
-
// Two identical runs make the -50% translate loop seamless. Each text and
|
|
282
|
-
// separator is a keyed span so webjsx applyDiff never sees a primitive
|
|
283
|
-
// sibling beside a keyed VElement.
|
|
284
|
-
const run = (runKey) => items.flatMap((it, i) => [
|
|
285
|
-
h('span', { class: 'ds-marquee-item', key: `${runKey}-i${i}` }, it),
|
|
286
|
-
h('span', { class: 'ds-marquee-sep', key: `${runKey}-s${i}`, 'aria-hidden': 'true' }, sep),
|
|
287
|
-
]);
|
|
288
|
-
return h('div', { class: 'ds-marquee', role: 'marquee' },
|
|
289
|
-
h('div', { class: 'ds-marquee-track' }, ...run('a'), ...run('b'))
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function Install({ cmd, copied, onCopy }) {
|
|
294
|
-
return h('div', { class: 'cli' },
|
|
295
|
-
h('span', { class: 'prompt' }, '$'),
|
|
296
|
-
h('span', { class: 'cmd' }, cmd),
|
|
297
|
-
h('span', { class: 'copy', onclick: () => onCopy && onCopy(cmd) }, copied ? 'copied' : 'copy')
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// CliBlock — the shared 'quickstart.lines[] -> stacked CLI block' renderer
|
|
302
|
-
// every portfolio consumer theme.mjs (zellous/wireweave/247420) had hand-rolled
|
|
303
|
-
// identically: lines.map((l,i) => a div per line holding a prompt span ('$' or
|
|
304
|
-
// '#' for a comment line) and a cmd span, all wrapped in a Panel. This factory
|
|
305
|
-
// targets the multi-line `.ds-cli-block` contract defined in gm-prose.css
|
|
306
|
-
// (`.ds-cli-block` holding `.ds-cli-row` rows — each a prompt+cmd pair — and
|
|
307
|
-
// `.ds-cli-comment` comment rows). `lines` is [{kind, text}] where kind: 'cmt' renders a
|
|
308
|
-
// comment-only row (no prompt glyph); any other kind (or omitted) renders a
|
|
309
|
-
// command row prefixed '$'. `heading` titles the wrapping Panel ('quick start'
|
|
310
|
-
// default, matching every hand-rolled instance); pass `heading: null` to
|
|
311
|
-
// render the bare `.ds-cli-block` block with no Panel chrome.
|
|
312
|
-
// Note: this is a different component than the bare `.cli` single prompt+cmd
|
|
313
|
-
// row primitive (app-shell.css; see Install() above and the per-line usage
|
|
314
|
-
// in terminal/site quickstart renderers) — the two used to collide on the
|
|
315
|
-
// same `.cli` class name with incompatible display models.
|
|
316
|
-
export function CliBlock({ lines = [], heading = 'quick start', className = '' } = {}) {
|
|
317
|
-
if (!lines || !lines.length) return null;
|
|
318
|
-
const rows = lines.map((l, i) => {
|
|
319
|
-
const isComment = l && l.kind === 'cmt';
|
|
320
|
-
const text = l && l.text != null ? l.text : '';
|
|
321
|
-
return isComment
|
|
322
|
-
? h('div', { key: 'q' + i, class: 'ds-cli-comment' }, text)
|
|
323
|
-
: h('div', { key: 'q' + i, class: 'ds-cli-row' },
|
|
324
|
-
h('span', { class: 'prompt' }, '$'),
|
|
325
|
-
h('span', { class: 'cmd' }, text));
|
|
326
|
-
});
|
|
327
|
-
const body = h('div', { class: 'ds-cli-block' + (className ? ' ' + className : '') }, ...rows);
|
|
328
|
-
return heading == null ? body : Panel({ title: heading, children: body });
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function Receipt({ rows = [], emptyText = 'nothing here yet' }) {
|
|
332
|
-
if (!rows.length) return h('div', { class: 'empty' }, emptyText);
|
|
333
|
-
return h('table', { class: 'kv' },
|
|
334
|
-
h('tbody', {}, ...rows.map(([k, v], i) =>
|
|
335
|
-
h('tr', { key: i }, h('td', {}, k), h('td', {}, v))
|
|
336
|
-
))
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function Changelog({ entries = [], emptyText = 'no changelog entries yet' }) {
|
|
341
|
-
if (!entries.length) return h('div', { class: 'empty' }, emptyText);
|
|
342
|
-
return Panel({
|
|
343
|
-
kind: 'wide',
|
|
344
|
-
children: entries.map((e, i) =>
|
|
345
|
-
h('div', { key: i, class: 'row ds-changelog-row' },
|
|
346
|
-
h('span', { class: 'code' }, e.date),
|
|
347
|
-
h('span', { class: 'ds-changelog-ver' }, e.ver),
|
|
348
|
-
h('span', { class: 'title' }, e.msg)
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export function WorksList({ works = [], openedIndex = -1, onToggle }) {
|
|
355
|
-
return Panel({
|
|
356
|
-
children: works.map((w, i) => {
|
|
357
|
-
const isOpen = openedIndex === i;
|
|
358
|
-
return h('div', { key: i },
|
|
359
|
-
Row({
|
|
360
|
-
code: w.code,
|
|
361
|
-
title: w.title, sub: w.sub,
|
|
362
|
-
// Expand affordance: a chevron icon (down when open, right when
|
|
363
|
-
// collapsed) separated from the meta text by a CSS gap, not a
|
|
364
|
-
// literal +/- with a double-space.
|
|
365
|
-
meta: h('span', { class: 'ds-works-meta' },
|
|
366
|
-
w.meta != null ? h('span', {}, w.meta) : null,
|
|
367
|
-
Icon(isOpen ? 'chevron-down' : 'chevron-right')),
|
|
368
|
-
active: isOpen,
|
|
369
|
-
expanded: isOpen,
|
|
370
|
-
onClick: () => onToggle && onToggle(isOpen ? -1 : i)
|
|
371
|
-
}),
|
|
372
|
-
isOpen ? h('div', { class: 'work-detail', 'data-work-index': String(i) },
|
|
373
|
-
h('div', { class: 'ds-prose' },
|
|
374
|
-
h('p', { class: 'ds-work-body' }, w.body)
|
|
375
|
-
),
|
|
376
|
-
h('div', { class: 'ds-work-actions' },
|
|
377
|
-
Btn({ variant: 'primary', href: w.href || '#', children: 'open ->' }),
|
|
378
|
-
Btn({ href: w.source || '#', children: 'source' })
|
|
379
|
-
)
|
|
380
|
-
) : null
|
|
381
|
-
);
|
|
382
|
-
})
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function WritingList({ posts = [] }) {
|
|
387
|
-
return Panel({
|
|
388
|
-
children: posts.map((p, i) =>
|
|
389
|
-
RowLink({ key: i, code: p.date, title: p.title, meta: p.tag, href: p.href || '#' })
|
|
390
|
-
)
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export function Manifesto({ paragraphs = [], maxWidth }) {
|
|
395
|
-
return h('div', {
|
|
396
|
-
class: 'ds-prose ds-manifesto',
|
|
397
|
-
'data-max-width': maxWidth ? String(maxWidth) : null
|
|
398
|
-
},
|
|
399
|
-
...paragraphs.map((p, i) => h('p', {
|
|
400
|
-
key: i,
|
|
401
|
-
class: 'ds-manifesto-para' + (p.dim ? ' dim' : '')
|
|
402
|
-
}, p.text || p))
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// items: [n, label] or [n, label, {delta, tone: 'up'|'down', spark: number[]}]
|
|
407
|
-
// meta is optional and additive — every existing 2-tuple call site is untouched.
|
|
408
|
-
export function Kpi({ items = [], emptyText = 'no metrics yet' }) {
|
|
409
|
-
if (!items.length) return h('div', { class: 'empty' }, emptyText);
|
|
410
|
-
return h('div', { class: 'kpi' }, ...items.map(([n, l, meta], i) =>
|
|
411
|
-
h('div', { key: i, class: 'kpi-card' },
|
|
412
|
-
h('div', { class: 'num' }, String(n)),
|
|
413
|
-
h('div', { class: 'lbl' }, l),
|
|
414
|
-
meta && (meta.delta != null || meta.spark)
|
|
415
|
-
? h('div', { class: 'kpi-foot' },
|
|
416
|
-
meta.delta != null
|
|
417
|
-
? h('span', { class: 'kpi-delta kpi-delta-' + (meta.tone === 'down' ? 'down' : 'up') },
|
|
418
|
-
Icon(meta.tone === 'down' ? 'arrow-down' : 'arrow-up', { size: 12 }),
|
|
419
|
-
String(meta.delta))
|
|
420
|
-
: null,
|
|
421
|
-
meta.spark ? Sparkline({ values: meta.spark, tone: meta.tone }) : null)
|
|
422
|
-
: null)));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Minimal inline SVG trend line — token-stroke only, no raw color literals.
|
|
426
|
-
export function Sparkline({ values = [], width = 72, height = 24, tone }) {
|
|
427
|
-
if (!values.length) return null;
|
|
428
|
-
const max = Math.max(...values), min = Math.min(...values);
|
|
429
|
-
const span = (max - min) || 1;
|
|
430
|
-
const step = width / (values.length - 1 || 1);
|
|
431
|
-
const points = values.map((v, i) => [i * step, height - ((v - min) / span) * height]);
|
|
432
|
-
const d = points.map(([x, y], i) => (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1)).join(' ');
|
|
433
|
-
return h('svg', { class: 'ds-sparkline ds-sparkline-' + (tone === 'down' ? 'down' : 'up'), viewBox: '0 0 ' + width + ' ' + height, width, height, 'aria-hidden': 'true' },
|
|
434
|
-
h('path', { d, fill: 'none', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Horizontal token-only bar breakdown — e.g. revenue by channel, traffic by source.
|
|
438
|
-
export function BarChart({ items = [], emptyText = 'no data yet' }) {
|
|
439
|
-
if (!items.length) return h('div', { class: 'empty' }, emptyText);
|
|
440
|
-
const max = Math.max(...items.map(it => it.value || 0)) || 1;
|
|
441
|
-
return h('div', { class: 'ds-barchart' }, ...items.map((it, i) =>
|
|
442
|
-
h('div', { key: i, class: 'ds-barchart-row' },
|
|
443
|
-
h('div', { class: 'ds-barchart-label' }, it.label),
|
|
444
|
-
h('div', { class: 'ds-barchart-track' },
|
|
445
|
-
h('div', { class: 'ds-barchart-fill', style: '--bar-pct:' + Math.round((it.value / max) * 100) + '%' })),
|
|
446
|
-
h('div', { class: 'ds-barchart-value' }, it.display != null ? it.display : String(it.value)))));
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export function Table({ headers = [], rows = [], onRowClick, emptyText = 'nothing here yet', rowLabels, striped = false, compact = false, sortable = false, sortKey, sortDir = 'asc', onSort }) {
|
|
450
|
-
if (!rows || rows.length === 0) return h('div', { class: 'empty' }, emptyText);
|
|
451
|
-
// rowLabels lets callers supply a plain-text label per row when the first
|
|
452
|
-
// cell is a vnode (so the aria-label is meaningful, not the literal 'row').
|
|
453
|
-
const labelFor = (row, i) => {
|
|
454
|
-
if (Array.isArray(rowLabels) && rowLabels[i] != null) return String(rowLabels[i]);
|
|
455
|
-
const c = row[0];
|
|
456
|
-
return c == null ? 'row' : (typeof c === 'object' ? 'row' : String(c));
|
|
457
|
-
};
|
|
458
|
-
// Native <table>/<tr>/<th>/<td> already carry the correct implicit ARIA
|
|
459
|
-
// roles — explicit role="table"/row/columnheader/cell is redundant and only
|
|
460
|
-
// risks overriding native semantics, so it is omitted.
|
|
461
|
-
// Scroll containment lives on the component itself: a wide table used
|
|
462
|
-
// outside a Panel must never force page-level horizontal scroll.
|
|
463
|
-
// striped/compact are opt-in density modifiers (webgeist g-table parity) —
|
|
464
|
-
// default false so the existing contract/visual is byte-unchanged.
|
|
465
|
-
const wrapClass = 'ds-table-wrap' + (striped ? ' is-striped' : '') + (compact ? ' is-compact' : '');
|
|
466
|
-
// sortable is opt-in (default false, byte-unchanged for existing callers):
|
|
467
|
-
// a header becomes a real <button> announcing aria-sort, dispatching
|
|
468
|
-
// onSort(headerIndex) so the HOST owns the actual row-ordering logic (this
|
|
469
|
-
// component has no opinion on comparator/locale/type - it only renders the
|
|
470
|
-
// control and current state). A docstudio-style dense admin table needs
|
|
471
|
-
// sortable columns; Table previously had no way to express that at all.
|
|
472
|
-
const thFor = (hd, i) => {
|
|
473
|
-
if (!sortable || !onSort) return h('th', { key: i, scope: 'col' }, hd);
|
|
474
|
-
const isActive = sortKey === i;
|
|
475
|
-
const ariaSort = isActive ? (sortDir === 'desc' ? 'descending' : 'ascending') : 'none';
|
|
476
|
-
return h('th', { key: i, scope: 'col', 'aria-sort': ariaSort },
|
|
477
|
-
h('button', { type: 'button', class: 'ds-table-sort-btn' + (isActive ? ' is-active' : ''), onclick: () => onSort(i) },
|
|
478
|
-
h('span', { class: 'ds-table-sort-label' }, hd),
|
|
479
|
-
isActive ? Icon(sortDir === 'desc' ? 'chevron-down' : 'chevron-up', { size: 12 }) : null));
|
|
480
|
-
};
|
|
481
|
-
return h('div', { class: wrapClass }, h('table', {},
|
|
482
|
-
h('thead', {}, h('tr', {}, ...headers.map((hd, i) => thFor(hd, i)))),
|
|
483
|
-
h('tbody', {}, ...rows.map((row, i) => h('tr', {
|
|
484
|
-
key: i,
|
|
485
|
-
class: onRowClick ? 'clickable' : '',
|
|
486
|
-
onclick: onRowClick ? () => onRowClick(i) : null,
|
|
487
|
-
// Space scrolls by default — preventDefault on Space (and Enter) so
|
|
488
|
-
// keyboard activation matches click without page jump.
|
|
489
|
-
...(onRowClick ? { tabindex: '0', role: 'button', 'aria-label': 'open ' + labelFor(row, i), onkeydown: (e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onRowClick(i); } } } : {})
|
|
490
|
-
}, ...row.map((c, j) => h('td', { key: j }, c == null ? '' : (typeof c === 'object' ? c : String(c)))))))));
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// HealthTable — generic health-check table: given an arbitrary
|
|
494
|
-
// `{checkName: value}` object, infers a Chip tone per row from the value's
|
|
495
|
-
// own type (boolean true/false -> ok/miss Chip, object -> truncated JSON
|
|
496
|
-
// string, else the raw value stringified) so a new backend health check
|
|
497
|
-
// appears with zero per-check hardcoding at the call site. Ported from
|
|
498
|
-
// freddie's health page (src/components/freddie.js's `health` page), which
|
|
499
|
-
// hand-rolled this exact inference inline; promoted here as a reusable
|
|
500
|
-
// primitive for any dashboard/monitoring surface with a health-check map
|
|
501
|
-
// (agentgui's Live tab included). `okLabel`/`missLabel` let a caller
|
|
502
|
-
// localize the two Chip strings; `jsonTruncate` caps the JSON.stringify
|
|
503
|
-
// length for object-shaped values (matches freddie's own truncation width).
|
|
504
|
-
export function HealthTable({ checks = {}, emptyText = 'no health data', okLabel = 'ok', missLabel = 'no', jsonTruncate = 60 } = {}) {
|
|
505
|
-
const entries = Object.entries(checks);
|
|
506
|
-
if (!entries.length) return h('div', { class: 'empty' }, emptyText);
|
|
507
|
-
const rows = entries.map(([name, v]) => {
|
|
508
|
-
let cell;
|
|
509
|
-
if (typeof v === 'object' && v !== null) {
|
|
510
|
-
const s = JSON.stringify(v);
|
|
511
|
-
cell = h('span', { title: s.length > jsonTruncate ? s : null }, s.length > jsonTruncate ? s.slice(0, jsonTruncate) + '…' : s);
|
|
512
|
-
} else if (v === true) cell = Chip({ tone: 'ok', children: okLabel });
|
|
513
|
-
else if (v === false) cell = Chip({ tone: 'miss', children: missLabel });
|
|
514
|
-
else cell = String(v);
|
|
515
|
-
return [name, cell];
|
|
516
|
-
});
|
|
517
|
-
return Table({ headers: ['check', 'status'], rows });
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// ProcessRegistryTable — generic long-lived-process registry table: given a
|
|
521
|
-
// list of `{kind, key, state}`-shaped rows (any in-flight process the host
|
|
522
|
-
// tracks — an xstate machine, an ACP/direct-runner session, a background
|
|
523
|
-
// job), renders a uniform table. Ported from freddie's `machines` page
|
|
524
|
-
// (src/components/freddie.js), which used this exact shape for its xstate
|
|
525
|
-
// machine census; generalized here since the shape (kind/key/state) applies
|
|
526
|
-
// equally to agentgui's Live tab listing in-flight agent runner processes.
|
|
527
|
-
// `extraColumns` lets a caller append columns beyond the base three (e.g. a
|
|
528
|
-
// stop-action button) without forking the whole table.
|
|
529
|
-
export function ProcessRegistryTable({ processes = [], emptyText = 'no live processes', extraColumns = [] } = {}) {
|
|
530
|
-
if (!processes.length) return h('div', { class: 'empty' }, emptyText);
|
|
531
|
-
const headers = ['kind', 'key', 'state', ...extraColumns.map(c => c.header)];
|
|
532
|
-
const rows = processes.map(p => [
|
|
533
|
-
p.kind || '—', p.key || '—', p.state || '—',
|
|
534
|
-
...extraColumns.map(c => c.render(p))
|
|
535
|
-
]);
|
|
536
|
-
return Table({ headers, rows });
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
export function HomeView({ state = {}, onNav, onToggleWork, works = [], posts = [], manifesto = [], currentlyShipping } = {}) {
|
|
540
|
-
return [
|
|
541
|
-
// The page's one deliberate kicker. 'an entrypoint' is the collective's
|
|
542
|
-
// name-as-tagline — it says something the <h1> does not, and it is the
|
|
543
|
-
// masthead position where a print eyebrow actually belongs. The sections
|
|
544
|
-
// below intentionally carry NO eyebrow: each has a heading that already
|
|
545
|
-
// names it, so a kicker there would only restate the <h3> one word
|
|
546
|
-
// shorter. Do not add eyebrows to the sibling sections to "match" this.
|
|
547
|
-
Hero({
|
|
548
|
-
eyebrow: 'an entrypoint',
|
|
549
|
-
title: 'Small, weird, useful tools — built in public.',
|
|
550
|
-
body: '247420 is a creative collective of eight, scattered across three timezones. We have been shipping open-source tools for the web since 2018.',
|
|
551
|
-
accent: 'Some become the future. Most don\'t. That\'s the deal.'
|
|
552
|
-
}),
|
|
553
|
-
// Titleless section: promoted its former eyebrow to the actual heading
|
|
554
|
-
// rather than leaving a kicker hovering over an unnamed panel. The label
|
|
555
|
-
// was carrying the section's only name, so it became the <h3>.
|
|
556
|
-
currentlyShipping ? Section({
|
|
557
|
-
title: 'currently shipping',
|
|
558
|
-
children: Panel({
|
|
559
|
-
kind: 'wide',
|
|
560
|
-
children: currentlyShipping.map((row, i) => {
|
|
561
|
-
const dotNode = Dot({ tone: row.live ? 'live' : 'idle' });
|
|
562
|
-
dotNode.props = { ...dotNode.props, 'aria-label': row.live ? 'live status' : 'idle status' };
|
|
563
|
-
return Row({
|
|
564
|
-
key: i,
|
|
565
|
-
code: dotNode,
|
|
566
|
-
title: row.title, sub: row.sub, meta: row.meta
|
|
567
|
-
});
|
|
568
|
-
})
|
|
569
|
-
})
|
|
570
|
-
}) : null,
|
|
571
|
-
works.length ? Section({
|
|
572
|
-
title: 'Everything else.',
|
|
573
|
-
children: WorksList({ works, openedIndex: state.opened ?? -1, onToggle: onToggleWork })
|
|
574
|
-
}) : null,
|
|
575
|
-
posts.length ? Section({
|
|
576
|
-
title: 'When we have something to say.',
|
|
577
|
-
children: WritingList({ posts })
|
|
578
|
-
}) : null,
|
|
579
|
-
manifesto.length ? Section({
|
|
580
|
-
title: 'Eight people, three timezones, one ongoing conversation.',
|
|
581
|
-
children: Manifesto({ paragraphs: manifesto })
|
|
582
|
-
}) : null
|
|
583
|
-
].filter(Boolean);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
export function ProjectView({ project = {}, copied, onCopy } = {}) {
|
|
587
|
-
return [
|
|
588
|
-
h('div', { class: 'ds-prose' },
|
|
589
|
-
Heading({ level: 1, children: project.name }),
|
|
590
|
-
Lede({ children: project.tagline })
|
|
591
|
-
),
|
|
592
|
-
project.install ? [
|
|
593
|
-
Heading({ level: 3, children: 'install' }),
|
|
594
|
-
Install({ cmd: project.install, copied, onCopy }),
|
|
595
|
-
] : null,
|
|
596
|
-
project.receipt ? [
|
|
597
|
-
Heading({ level: 3, children: 'by the numbers' }),
|
|
598
|
-
Receipt({ rows: project.receipt }),
|
|
599
|
-
] : null,
|
|
600
|
-
project.changelog ? [
|
|
601
|
-
Heading({ level: 3, children: 'recent releases' }),
|
|
602
|
-
Changelog({ entries: project.changelog })
|
|
603
|
-
] : null
|
|
604
|
-
].filter(Boolean).flat();
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
export function PageHeader({ title, lede, eyebrow, right, compact, dense, id }) {
|
|
608
|
-
// `compact` drops the large leading/trailing section margins so a PageHeader
|
|
609
|
-
// used as a page's first element top-aligns cleanly without the consumer
|
|
610
|
-
// having to !important-override the .ds-section margin. `id` lands on the
|
|
611
|
-
// outermost section so the header can serve as a deep-link anchor.
|
|
612
|
-
// `dense` is the content-first working-surface form: one row - a small
|
|
613
|
-
// heading with the lede beside it, clamped to a single muted line - instead
|
|
614
|
-
// of a display H1 over a paragraph. App surfaces (files, dashboards,
|
|
615
|
-
// settings) should not spend 150px of fold on an intro.
|
|
616
|
-
if (dense) {
|
|
617
|
-
return h('section', { class: 'ds-section ds-section-compact ds-page-header-dense', id: id || null },
|
|
618
|
-
h('div', { class: 'ds-page-header-dense-row' },
|
|
619
|
-
...[
|
|
620
|
-
title != null ? h('h1', { key: 'dh' }, title) : null,
|
|
621
|
-
lede != null ? h('span', { key: 'dl', class: 'ds-page-header-dense-lede', title: typeof lede === 'string' ? lede : null }, lede) : null,
|
|
622
|
-
right != null ? h('div', { key: 'dr', class: 'ds-page-header-right' }, ...(Array.isArray(right) ? right : [right])) : null,
|
|
623
|
-
].filter(Boolean)));
|
|
624
|
-
}
|
|
625
|
-
return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : ''), id: id || null },
|
|
626
|
-
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
627
|
-
title != null ? h('h1', {}, title) : null,
|
|
628
|
-
lede != null ? h('p', { class: 'lede' }, lede) : null,
|
|
629
|
-
right != null ? h('div', { class: 'ds-page-header-right' }, ...(Array.isArray(right) ? right : [right])) : null
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export function SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key, label, resultCount }) {
|
|
634
|
-
// Shared clear path — both the Escape key and the visible clear button
|
|
635
|
-
// call this, so there is exactly one place that clears the field.
|
|
636
|
-
const doClear = (e) => { if (onInput) onInput('', e); };
|
|
637
|
-
const input = h('input', {
|
|
638
|
-
key: 'i',
|
|
639
|
-
type: 'search',
|
|
640
|
-
name,
|
|
641
|
-
class: 'ds-search-input',
|
|
642
|
-
placeholder,
|
|
643
|
-
'aria-label': label || placeholder,
|
|
644
|
-
value,
|
|
645
|
-
oninput: onInput ? (e) => onInput(e.target.value, e) : null,
|
|
646
|
-
onkeydown: (e) => {
|
|
647
|
-
// Escape clears the field in place (stays focused) rather than
|
|
648
|
-
// falling through to whatever ancestor Escape handler exists.
|
|
649
|
-
if (e.key === 'Escape' && value) { e.preventDefault(); e.stopPropagation(); doClear(e); return; }
|
|
650
|
-
// IME guard: the Enter that commits a CJK composition must not submit.
|
|
651
|
-
if (onSubmit && e.key === 'Enter' && !e.isComposing && e.keyCode !== 229) onSubmit(e.target.value, e);
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
// Visible clear (X) button — mouse/touch users have no way to discover the
|
|
655
|
-
// Escape-to-clear shortcut, so this surfaces the same clear path visibly.
|
|
656
|
-
// Only rendered when there's something to clear.
|
|
657
|
-
const clearBtn = value
|
|
658
|
-
? h('button', {
|
|
659
|
-
key: 'clr', type: 'button', class: 'ds-search-clear',
|
|
660
|
-
'aria-label': 'clear search',
|
|
661
|
-
onclick: doClear,
|
|
662
|
-
}, Icon('x'))
|
|
663
|
-
: null;
|
|
664
|
-
// Always return the same wrapping shape regardless of whether resultCount/
|
|
665
|
-
// clearBtn are present this render - a conditional bare-input-vs-wrapped-
|
|
666
|
-
// span return here previously changed SearchInput's VElement type at the
|
|
667
|
-
// SAME keyed slot from render to render (e.g. typing into an empty filter
|
|
668
|
-
// makes resultCount go from undefined to a string), and webjsx's applyDiff
|
|
669
|
-
// has no way to morph one element type into another in place - it produced
|
|
670
|
-
// a corrupted merged DOM node carrying attributes from both shapes.
|
|
671
|
-
return h('span', { key, class: 'ds-search-input-wrap' },
|
|
672
|
-
input,
|
|
673
|
-
clearBtn,
|
|
674
|
-
resultCount != null ? h('span', { key: 'cnt', class: 'sr-only', role: 'status', 'aria-live': 'polite' }, resultCount) : null);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
export function TextField({ label, value = '', type = 'text', placeholder = '', onInput, onChange, name, key, hint, multiline, rows = 4, maxLength, min, max, error, title, size = 'md', 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy }) {
|
|
678
|
-
// size: 'sm' | 'md' | 'lg' — md is the base .ds-field control; sm/lg add a
|
|
679
|
-
// wrapper modifier that snaps the control height/padding/font to --ctl-*.
|
|
680
|
-
const sizeCls = size === 'sm' ? ' ds-field--sm' : (size === 'lg' ? ' ds-field--lg' : '');
|
|
681
|
-
const errorId = error != null ? ((key ? key : 'tf') + '-err') : null;
|
|
682
|
-
const describedBy = ariaDescribedBy || errorId || null;
|
|
683
|
-
const input = multiline
|
|
684
|
-
? h('textarea', {
|
|
685
|
-
key: 'i', name, rows, placeholder, value,
|
|
686
|
-
maxlength: maxLength != null ? maxLength : null,
|
|
687
|
-
'aria-label': ariaLabel || null,
|
|
688
|
-
'aria-invalid': error != null ? 'true' : (ariaInvalid || null),
|
|
689
|
-
'aria-describedby': describedBy,
|
|
690
|
-
title: title || null,
|
|
691
|
-
oninput: onInput ? (e) => onInput(e.target.value, e) : null,
|
|
692
|
-
onchange: onChange ? (e) => onChange(e.target.value, e) : null
|
|
693
|
-
})
|
|
694
|
-
: h('input', {
|
|
695
|
-
key: 'i', type, name, placeholder, value,
|
|
696
|
-
maxlength: maxLength != null ? maxLength : null,
|
|
697
|
-
min: min != null ? String(min) : null,
|
|
698
|
-
max: max != null ? String(max) : null,
|
|
699
|
-
'aria-label': ariaLabel || null,
|
|
700
|
-
'aria-invalid': error != null ? 'true' : (ariaInvalid || null),
|
|
701
|
-
'aria-describedby': describedBy,
|
|
702
|
-
title: title || null,
|
|
703
|
-
oninput: onInput ? (e) => onInput(e.target.value, e) : null,
|
|
704
|
-
onchange: onChange ? (e) => onChange(e.target.value, e) : null
|
|
705
|
-
});
|
|
706
|
-
return h('label', { key, class: 'ds-field' + sizeCls },
|
|
707
|
-
...[
|
|
708
|
-
label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
|
|
709
|
-
input,
|
|
710
|
-
error != null ? h('span', { key: 'e', id: errorId, class: 'ds-field-error', role: 'alert', 'aria-live': 'polite', 'aria-atomic': 'true' }, error) : null,
|
|
711
|
-
maxLength != null ? h('span', { key: 'c', class: 'ds-field-count' }, String(value.length) + '/' + maxLength) : null,
|
|
712
|
-
hint != null ? h('span', { key: 'h', class: 'ds-field-hint' }, hint) : null
|
|
713
|
-
].filter(Boolean)
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export function Select({ label, value = '', options = [], onChange, name, key, placeholder, hint, title, size = 'md', 'aria-label': ariaLabel }) {
|
|
718
|
-
const sizeCls = size === 'sm' ? ' ds-field--sm' : (size === 'lg' ? ' ds-field--lg' : '');
|
|
719
|
-
const opts = [];
|
|
720
|
-
if (placeholder != null) opts.push(h('option', { key: '_ph', value: '', disabled: true, selected: value === '' || value == null }, placeholder));
|
|
721
|
-
for (const o of options) {
|
|
722
|
-
const id = typeof o === 'string' ? o : (o.value != null ? o.value : o.id);
|
|
723
|
-
const lab = typeof o === 'string' ? o : (o.label != null ? o.label : (o.id || o.value));
|
|
724
|
-
opts.push(h('option', { key: 'o-' + id, value: id, selected: id === value }, lab));
|
|
725
|
-
}
|
|
726
|
-
const select = h('select', {
|
|
727
|
-
key: 'i', name, class: 'ds-select',
|
|
728
|
-
// Guarantee an accessible name even when rendered without a visible label.
|
|
729
|
-
'aria-label': ariaLabel || (label == null ? (title || placeholder || name) : null),
|
|
730
|
-
title,
|
|
731
|
-
onchange: onChange ? (e) => onChange(e.target.value, e) : null
|
|
732
|
-
}, ...opts);
|
|
733
|
-
if (label == null && hint == null && size === 'md') return select;
|
|
734
|
-
if (label == null && hint == null) return h('label', { key, class: 'ds-field' + sizeCls }, select);
|
|
735
|
-
return h('label', { key, class: 'ds-field' + sizeCls },
|
|
736
|
-
label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
|
|
737
|
-
select,
|
|
738
|
-
hint != null ? h('span', { key: 'h', class: 'ds-field-hint' }, hint) : null
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
export function EventList({ items, events, emptyText = 'no events', rankPad = 3, loading = false, loadingText = 'loading events…' }) {
|
|
743
|
-
const list = items || events || [];
|
|
744
|
-
// Shape-matched skeleton rows for the slow first events fetch (the ccsniff
|
|
745
|
-
// cold walk can take 30-90s) - a lone spinner collapses the whole pane.
|
|
746
|
-
// Keying discipline mirrors ConversationList: a single keyed wrapper with
|
|
747
|
-
// all-keyed siblings (webjsx applyDiff crashes on mixed keyed/unkeyed).
|
|
748
|
-
if (loading && !list.length) {
|
|
749
|
-
return h('section', { class: 'ds-section ds-event-list' },
|
|
750
|
-
h('div', { key: 'st', role: 'status', 'aria-live': 'polite', class: 'ds-event-state lede' }, loadingText),
|
|
751
|
-
...Array.from({ length: 7 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-event-row-skeleton', 'aria-hidden': 'true' },
|
|
752
|
-
h('span', { key: 'r', class: 'ds-skel ds-skel-rank' }),
|
|
753
|
-
h('span', { key: 't', class: 'ds-skel ds-skel-title' }),
|
|
754
|
-
h('span', { key: 'm', class: 'ds-skel ds-skel-meta' }))));
|
|
755
|
-
}
|
|
756
|
-
if (!list.length) return h('p', { class: 'lede' }, emptyText);
|
|
757
|
-
return h('section', { class: 'ds-section ds-event-list' },
|
|
758
|
-
...list.map((it, i) => Row({
|
|
759
|
-
key: it.key || ('ev' + i),
|
|
760
|
-
code: it.code != null ? it.code : (it.rank != null ? it.rank : String(i + 1).padStart(rankPad, '0')),
|
|
761
|
-
title: it.title || '(empty)',
|
|
762
|
-
sub: it.sub || '',
|
|
763
|
-
active: it.active,
|
|
764
|
-
onClick: it.onClick,
|
|
765
|
-
kind: it.kind,
|
|
766
|
-
rail: it.rail,
|
|
767
|
-
// Forward a disclosure state when the host marks the row as a toggle,
|
|
768
|
-
// so a clickable event row announces aria-expanded.
|
|
769
|
-
expanded: it.expanded,
|
|
770
|
-
detail: it.detail,
|
|
771
|
-
actions: it.actions,
|
|
772
|
-
highlight: it.highlight,
|
|
773
|
-
meta: it.meta
|
|
774
|
-
}))
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
export function Form({ fields = [], submit = 'submit', onSubmit, columns = 1 }) {
|
|
779
|
-
const cols = columns > 1 ? String(columns) : null;
|
|
780
|
-
return h('form', { class: 'row-form', 'data-columns': cols, onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
|
|
781
|
-
...fields.map((f, i) => {
|
|
782
|
-
// Each control gets a stable id and an associated <label> so the
|
|
783
|
-
// placeholder is no longer the only (inaccessible) name. The label
|
|
784
|
-
// text falls back to label -> placeholder -> name.
|
|
785
|
-
const fieldId = 'ds-form-' + (f.name || 'field') + '-' + i;
|
|
786
|
-
const labelText = f.label != null ? f.label : (f.placeholder || f.name || '');
|
|
787
|
-
const control = f.kind === 'textarea'
|
|
788
|
-
? h('textarea', { key: 'i', id: fieldId, name: f.name, placeholder: f.placeholder || '', rows: f.rows || 4, required: f.required ? true : null })
|
|
789
|
-
: h('input', { key: 'i', id: fieldId, name: f.name, type: f.type || 'text', placeholder: f.placeholder || '', value: f.value || '', required: f.required ? true : null });
|
|
790
|
-
return h('label', { key: i, class: 'ds-field', for: fieldId },
|
|
791
|
-
labelText !== '' ? h('span', { key: 'l', class: 'ds-field-label' }, labelText) : null,
|
|
792
|
-
control);
|
|
793
|
-
}),
|
|
794
|
-
h('button', { type: 'submit', class: 'btn-primary' }, submit));
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export function Spinner({ size = 'base', tone = 'accent', label = 'loading', key } = {}) {
|
|
798
|
-
const SIZE_CLASS = { xs: 'ds-spinner-xs', sm: 'ds-spinner-sm', base: '', lg: 'ds-spinner-lg', xl: 'ds-spinner-xl' };
|
|
799
|
-
const sizeClass = SIZE_CLASS[size] != null ? SIZE_CLASS[size] : '';
|
|
800
|
-
return h('div', {
|
|
801
|
-
key, class: 'ds-spinner ' + sizeClass + ' tone-' + tone,
|
|
802
|
-
role: 'status', 'aria-live': 'polite', 'aria-label': label
|
|
803
|
-
},
|
|
804
|
-
h('span', { key: '1', 'aria-hidden': 'true' }),
|
|
805
|
-
h('span', { key: '2', 'aria-hidden': 'true' }),
|
|
806
|
-
h('span', { key: '3', 'aria-hidden': 'true' })
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Clamp a caller-supplied CSS length to a sane range so a raw prop like
|
|
811
|
-
// height="9999px" can't blow out the layout. Accepts a CSS length string
|
|
812
|
-
// (px/em/rem/%/vh/vw) or a bare number (treated as px); rejects anything else
|
|
813
|
-
// back to the default. Numeric values are clamped to [2, 600] (px-equivalent).
|
|
814
|
-
function clampLen(v, fallback) {
|
|
815
|
-
if (v == null) return fallback;
|
|
816
|
-
const s = String(v).trim();
|
|
817
|
-
const m = /^(\d+(?:\.\d+)?)(px|em|rem|%|vh|vw)?$/.exec(s);
|
|
818
|
-
if (!m) return fallback;
|
|
819
|
-
const unit = m[2] || 'px';
|
|
820
|
-
let n = parseFloat(m[1]);
|
|
821
|
-
if (unit === '%' || unit === 'vh' || unit === 'vw') n = Math.min(100, Math.max(0, n));
|
|
822
|
-
else n = Math.min(600, Math.max(2, n));
|
|
823
|
-
return n + unit;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
export function Skeleton({ height = '1em', width = '100%', count = 1, label = 'loading content', key } = {}) {
|
|
827
|
-
const h_ = clampLen(height, '1em');
|
|
828
|
-
const w_ = clampLen(width, '100%');
|
|
829
|
-
return h('div', {
|
|
830
|
-
key, class: 'ds-skeleton-group',
|
|
831
|
-
role: 'status', 'aria-busy': 'true', 'aria-label': label
|
|
832
|
-
},
|
|
833
|
-
...Array(count).fill(0).map((_, i) =>
|
|
834
|
-
h('div', { key: String(i), class: 'ds-skeleton', style: `height:${h_};width:${w_};`, 'aria-hidden': 'true' })
|
|
835
|
-
)
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// FilterPills — a role=group of pill toggle buttons for quick category filters.
|
|
840
|
-
// `options` is [{ id, label }]; `selected` the active id; clicking a pill calls
|
|
841
|
-
// onSelect(id). Pressed state is announced via aria-pressed.
|
|
842
|
-
export function FilterPills({ options = [], selected, onSelect, label = 'filters' } = {}) {
|
|
843
|
-
if (!options.length) return null;
|
|
844
|
-
return h('div', { class: 'ds-filter-pills', role: 'group', 'aria-label': label },
|
|
845
|
-
...options.map((o) => h('button', {
|
|
846
|
-
key: 'fp-' + o.id,
|
|
847
|
-
type: 'button',
|
|
848
|
-
class: 'ds-filter-pill' + (o.id === selected ? ' active' : ''),
|
|
849
|
-
'aria-pressed': o.id === selected ? 'true' : 'false',
|
|
850
|
-
onclick: () => onSelect && onSelect(o.id),
|
|
851
|
-
}, o.label != null ? o.label : o.id)));
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
export function Alert({ kind = 'info', children, onDismiss, title, key } = {}) {
|
|
855
|
-
const icons = { info: 'info', success: 'check', warn: 'warn', error: 'x' };
|
|
856
|
-
const cls = 'ds-alert ds-alert-' + kind;
|
|
857
|
-
return h('div', { key, class: cls, role: 'alert' },
|
|
858
|
-
h('span', { key: 'icon', class: 'ds-alert-icon' }, Icon(icons[kind] || 'info')),
|
|
859
|
-
h('div', { key: 'content', class: 'ds-alert-content' },
|
|
860
|
-
title ? h('div', { key: 'title', class: 'ds-alert-title' }, title) : null,
|
|
861
|
-
h('div', { key: 'msg', class: 'ds-alert-message' }, ...(Array.isArray(children) ? children : [children]))
|
|
862
|
-
),
|
|
863
|
-
onDismiss ? h('button', { key: 'dismiss', class: 'ds-alert-dismiss', 'aria-label': 'dismiss', onclick: onDismiss }, Icon('x')) : null
|
|
864
|
-
);
|
|
865
|
-
}
|
|
5
|
+
// This module is a barrel: every component lives in a single-responsibility
|
|
6
|
+
// submodule under ./content/, and the public export surface here is unchanged
|
|
7
|
+
// — no consumer import needs to move.
|
|
8
|
+
|
|
9
|
+
import { avatarInitial, Avatar } from './content/avatar.js';
|
|
10
|
+
import { Row, RowLink } from './content/row.js';
|
|
11
|
+
import { Panel, Card, PanelFromItems, Section, Receipt, Changelog } from './content/panel.js';
|
|
12
|
+
import { Hero, HeroFromPageData, Marquee, Manifesto, PageHeader } from './content/hero.js';
|
|
13
|
+
import { Install, CliBlock } from './content/cli.js';
|
|
14
|
+
import { WorksList, WritingList, EventList } from './content/lists.js';
|
|
15
|
+
import { Kpi, Sparkline, BarChart } from './content/charts.js';
|
|
16
|
+
import { Table, HealthTable, ProcessRegistryTable } from './content/table.js';
|
|
17
|
+
import { SearchInput, TextField, Select, Form } from './content/fields.js';
|
|
18
|
+
import { Spinner, Skeleton, Alert, FilterPills } from './content/feedback.js';
|
|
19
|
+
import { HomeView, ProjectView } from './content/views.js';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
avatarInitial, Avatar,
|
|
23
|
+
Row, RowLink,
|
|
24
|
+
Panel, Card, PanelFromItems, Section, Receipt, Changelog,
|
|
25
|
+
Hero, HeroFromPageData, Marquee, Manifesto, PageHeader,
|
|
26
|
+
Install, CliBlock,
|
|
27
|
+
WorksList, WritingList, EventList,
|
|
28
|
+
Kpi, Sparkline, BarChart,
|
|
29
|
+
Table, HealthTable, ProcessRegistryTable,
|
|
30
|
+
SearchInput, TextField, Select, Form,
|
|
31
|
+
Spinner, Skeleton, Alert, FilterPills,
|
|
32
|
+
HomeView, ProjectView,
|
|
33
|
+
};
|