anentrypoint-design 0.0.381 → 0.0.383

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.
Files changed (54) hide show
  1. package/dist/247420.css +406 -180
  2. package/dist/247420.js +26 -26
  3. package/package.json +1 -1
  4. package/src/components/content/avatar.js +31 -0
  5. package/src/components/content/charts.js +50 -0
  6. package/src/components/content/cli.js +46 -0
  7. package/src/components/content/feedback.js +77 -0
  8. package/src/components/content/fields.js +136 -0
  9. package/src/components/content/hero.js +146 -0
  10. package/src/components/content/lists.js +85 -0
  11. package/src/components/content/panel.js +79 -0
  12. package/src/components/content/row.js +115 -0
  13. package/src/components/content/table.js +97 -0
  14. package/src/components/content/views.js +81 -0
  15. package/src/components/content.js +29 -861
  16. package/src/components/editor-primitives/batch.js +60 -0
  17. package/src/components/editor-primitives/chrome.js +146 -0
  18. package/src/components/editor-primitives/collapse.js +46 -0
  19. package/src/components/editor-primitives/context-menu.js +127 -0
  20. package/src/components/editor-primitives/diagnostics.js +44 -0
  21. package/src/components/editor-primitives/focus-trap.js +26 -0
  22. package/src/components/editor-primitives/json-viewer.js +123 -0
  23. package/src/components/editor-primitives/layout.js +89 -0
  24. package/src/components/editor-primitives/modals.js +89 -0
  25. package/src/components/editor-primitives/pager.js +74 -0
  26. package/src/components/editor-primitives/property-grid.js +57 -0
  27. package/src/components/editor-primitives/shared.js +18 -0
  28. package/src/components/editor-primitives/split-panel.js +101 -0
  29. package/src/components/editor-primitives/toast.js +59 -0
  30. package/src/components/editor-primitives/tree.js +75 -0
  31. package/src/components/editor-primitives.js +35 -1048
  32. package/src/components/files-modals.js +1 -1
  33. package/src/components/overlay-primitives/approval-prompt.js +38 -0
  34. package/src/components/overlay-primitives/auth-modal.js +89 -0
  35. package/src/components/overlay-primitives/command-palette.js +101 -0
  36. package/src/components/overlay-primitives/emoji-picker.js +103 -0
  37. package/src/components/overlay-primitives/floating.js +146 -0
  38. package/src/components/overlay-primitives/full-screen.js +45 -0
  39. package/src/components/overlay-primitives/menus.js +135 -0
  40. package/src/components/overlay-primitives/popover.js +51 -0
  41. package/src/components/overlay-primitives/roving-menu.js +73 -0
  42. package/src/components/overlay-primitives/settings-popover.js +85 -0
  43. package/src/components/overlay-primitives/tooltip.js +55 -0
  44. package/src/components/overlay-primitives.js +33 -855
  45. package/src/css/app-shell/base.css +54 -1
  46. package/src/css/app-shell/chat-polish.css +39 -32
  47. package/src/css/app-shell/data-density.css +25 -21
  48. package/src/css/app-shell/files.css +37 -29
  49. package/src/css/app-shell/kits-appended.css +92 -50
  50. package/src/css/app-shell/responsive.css +73 -35
  51. package/src/css/app-shell/responsive2-workspace.css +16 -4
  52. package/src/css/app-shell/states-interactions.css +21 -4
  53. package/src/css/app-shell/topbar.css +5 -1
  54. package/src/kits/os/freddie-dashboard.css +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.381",
3
+ "version": "0.0.383",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -0,0 +1,31 @@
1
+ // Avatar — generic identity disc: an image when `src` resolves, else a
2
+ // letter fallback derived from `name`/`fallback`. Kit previously only had
3
+ // scoped one-offs (chat.js `.chat-avatar`, community.js `.cm-user-avatar`)
4
+ // duplicating this same letter-fallback logic; this is the reusable version.
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ const h = webjsx.createElement;
8
+
9
+ // avatarInitial — the single shared letter-fallback computation behind
10
+ // Avatar and every custom-colored avatar wrapper (community.js's voice/user/
11
+ // member rows use their own `--avatar-bg` CSS-variable styling and can't
12
+ // drop in the Avatar element directly, but still want the SAME fallback
13
+ // text, not their own independently-drifting .slice(0,n).toUpperCase()).
14
+ // Empty-guards to '?' identically everywhere it's used.
15
+ export function avatarInitial(name, count = 1) {
16
+ return name ? String(name).trim().slice(0, count).toUpperCase() || '?' : '?';
17
+ }
18
+
19
+ // Avatar — the single letter-fallback/image avatar primitive. `initialsCount`
20
+ // (default 1) controls how many leading characters of `name` become the
21
+ // fallback letters when no `src`/`fallback` is given (community.js's
22
+ // pill-shaped ServerIcon wants 2); `shape` ('circle' default, or 'square')
23
+ // covers non-circular consumers without each hand-rolling its own
24
+ // .slice(0,n).toUpperCase() (previously duplicated across 5+ call sites in
25
+ // community.js and chat.js with drifting char-counts/empty-guards).
26
+ export function Avatar({ name, src, fallback, size = 'md', shape = 'circle', initialsCount = 1, key } = {}) {
27
+ const letter = fallback != null ? fallback : avatarInitial(name, initialsCount);
28
+ const cls = 'ds-avatar ds-avatar-' + size + (shape === 'square' ? ' ds-avatar-square' : '');
29
+ if (src) return h('img', { key, class: cls, src, alt: name || '', loading: 'lazy' });
30
+ return h('span', { key, class: cls, 'aria-hidden': !!name, role: name ? 'img' : undefined, 'aria-label': name || undefined }, letter);
31
+ }
@@ -0,0 +1,50 @@
1
+ // Metric visuals — Kpi (stat cards with optional delta/sparkline footer),
2
+ // Sparkline (inline trend line) and BarChart (horizontal breakdown). All
3
+ // token-stroke/token-fill only; no raw color literals.
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Icon } from '../shell.js';
7
+ const h = webjsx.createElement;
8
+
9
+ // items: [n, label] or [n, label, {delta, tone: 'up'|'down', spark: number[]}]
10
+ // meta is optional and additive — every existing 2-tuple call site is untouched.
11
+ export function Kpi({ items = [], emptyText = 'no metrics yet' }) {
12
+ if (!items.length) return h('div', { class: 'empty' }, emptyText);
13
+ return h('div', { class: 'kpi' }, ...items.map(([n, l, meta], i) =>
14
+ h('div', { key: i, class: 'kpi-card' },
15
+ h('div', { class: 'num' }, String(n)),
16
+ h('div', { class: 'lbl' }, l),
17
+ meta && (meta.delta != null || meta.spark)
18
+ ? h('div', { class: 'kpi-foot' },
19
+ meta.delta != null
20
+ ? h('span', { class: 'kpi-delta kpi-delta-' + (meta.tone === 'down' ? 'down' : 'up') },
21
+ Icon(meta.tone === 'down' ? 'arrow-down' : 'arrow-up', { size: 12 }),
22
+ String(meta.delta))
23
+ : null,
24
+ meta.spark ? Sparkline({ values: meta.spark, tone: meta.tone }) : null)
25
+ : null)));
26
+ }
27
+
28
+ // Minimal inline SVG trend line — token-stroke only, no raw color literals.
29
+ export function Sparkline({ values = [], width = 72, height = 24, tone }) {
30
+ if (!values.length) return null;
31
+ const max = Math.max(...values), min = Math.min(...values);
32
+ const span = (max - min) || 1;
33
+ const step = width / (values.length - 1 || 1);
34
+ const points = values.map((v, i) => [i * step, height - ((v - min) / span) * height]);
35
+ const d = points.map(([x, y], i) => (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1)).join(' ');
36
+ return h('svg', { class: 'ds-sparkline ds-sparkline-' + (tone === 'down' ? 'down' : 'up'), viewBox: '0 0 ' + width + ' ' + height, width, height, 'aria-hidden': 'true' },
37
+ h('path', { d, fill: 'none', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
38
+ }
39
+
40
+ // Horizontal token-only bar breakdown — e.g. revenue by channel, traffic by source.
41
+ export function BarChart({ items = [], emptyText = 'no data yet' }) {
42
+ if (!items.length) return h('div', { class: 'empty' }, emptyText);
43
+ const max = Math.max(...items.map(it => it.value || 0)) || 1;
44
+ return h('div', { class: 'ds-barchart' }, ...items.map((it, i) =>
45
+ h('div', { key: i, class: 'ds-barchart-row' },
46
+ h('div', { class: 'ds-barchart-label' }, it.label),
47
+ h('div', { class: 'ds-barchart-track' },
48
+ h('div', { class: 'ds-barchart-fill', style: '--bar-pct:' + Math.round((it.value / max) * 100) + '%' })),
49
+ h('div', { class: 'ds-barchart-value' }, it.display != null ? it.display : String(it.value)))));
50
+ }
@@ -0,0 +1,46 @@
1
+ // Command-line blocks — Install (the single-line copyable `.cli` prompt+cmd
2
+ // row) and CliBlock (the multi-line `.ds-cli-block` quickstart list). These
3
+ // are two different contracts that once collided on the same `.cli` class
4
+ // name with incompatible display models; keep them distinct.
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ import { Panel } from './panel.js';
8
+ const h = webjsx.createElement;
9
+
10
+ export function Install({ cmd, copied, onCopy }) {
11
+ return h('div', { class: 'cli' },
12
+ h('span', { class: 'prompt' }, '$'),
13
+ h('span', { class: 'cmd' }, cmd),
14
+ h('span', { class: 'copy', onclick: () => onCopy && onCopy(cmd) }, copied ? 'copied' : 'copy')
15
+ );
16
+ }
17
+
18
+ // CliBlock — the shared 'quickstart.lines[] -> stacked CLI block' renderer
19
+ // every portfolio consumer theme.mjs (zellous/wireweave/247420) had hand-rolled
20
+ // identically: lines.map((l,i) => a div per line holding a prompt span ('$' or
21
+ // '#' for a comment line) and a cmd span, all wrapped in a Panel. This factory
22
+ // targets the multi-line `.ds-cli-block` contract defined in gm-prose.css
23
+ // (`.ds-cli-block` holding `.ds-cli-row` rows — each a prompt+cmd pair — and
24
+ // `.ds-cli-comment` comment rows). `lines` is [{kind, text}] where kind: 'cmt' renders a
25
+ // comment-only row (no prompt glyph); any other kind (or omitted) renders a
26
+ // command row prefixed '$'. `heading` titles the wrapping Panel ('quick start'
27
+ // default, matching every hand-rolled instance); pass `heading: null` to
28
+ // render the bare `.ds-cli-block` block with no Panel chrome.
29
+ // Note: this is a different component than the bare `.cli` single prompt+cmd
30
+ // row primitive (app-shell.css; see Install() above and the per-line usage
31
+ // in terminal/site quickstart renderers) — the two used to collide on the
32
+ // same `.cli` class name with incompatible display models.
33
+ export function CliBlock({ lines = [], heading = 'quick start', className = '' } = {}) {
34
+ if (!lines || !lines.length) return null;
35
+ const rows = lines.map((l, i) => {
36
+ const isComment = l && l.kind === 'cmt';
37
+ const text = l && l.text != null ? l.text : '';
38
+ return isComment
39
+ ? h('div', { key: 'q' + i, class: 'ds-cli-comment' }, text)
40
+ : h('div', { key: 'q' + i, class: 'ds-cli-row' },
41
+ h('span', { class: 'prompt' }, '$'),
42
+ h('span', { class: 'cmd' }, text));
43
+ });
44
+ const body = h('div', { class: 'ds-cli-block' + (className ? ' ' + className : '') }, ...rows);
45
+ return heading == null ? body : Panel({ title: heading, children: body });
46
+ }
@@ -0,0 +1,77 @@
1
+ // Status feedback — the transient/ambient state surfaces: Spinner and
2
+ // Skeleton (loading), Alert (result/error messaging) and FilterPills (an
3
+ // in-place category toggle strip).
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Icon } from '../shell.js';
7
+ const h = webjsx.createElement;
8
+
9
+ export function Spinner({ size = 'base', tone = 'accent', label = 'loading', key } = {}) {
10
+ const SIZE_CLASS = { xs: 'ds-spinner-xs', sm: 'ds-spinner-sm', base: '', lg: 'ds-spinner-lg', xl: 'ds-spinner-xl' };
11
+ const sizeClass = SIZE_CLASS[size] != null ? SIZE_CLASS[size] : '';
12
+ return h('div', {
13
+ key, class: 'ds-spinner ' + sizeClass + ' tone-' + tone,
14
+ role: 'status', 'aria-live': 'polite', 'aria-label': label
15
+ },
16
+ h('span', { key: '1', 'aria-hidden': 'true' }),
17
+ h('span', { key: '2', 'aria-hidden': 'true' }),
18
+ h('span', { key: '3', 'aria-hidden': 'true' })
19
+ );
20
+ }
21
+
22
+ // Clamp a caller-supplied CSS length to a sane range so a raw prop like
23
+ // height="9999px" can't blow out the layout. Accepts a CSS length string
24
+ // (px/em/rem/%/vh/vw) or a bare number (treated as px); rejects anything else
25
+ // back to the default. Numeric values are clamped to [2, 600] (px-equivalent).
26
+ function clampLen(v, fallback) {
27
+ if (v == null) return fallback;
28
+ const s = String(v).trim();
29
+ const m = /^(\d+(?:\.\d+)?)(px|em|rem|%|vh|vw)?$/.exec(s);
30
+ if (!m) return fallback;
31
+ const unit = m[2] || 'px';
32
+ let n = parseFloat(m[1]);
33
+ if (unit === '%' || unit === 'vh' || unit === 'vw') n = Math.min(100, Math.max(0, n));
34
+ else n = Math.min(600, Math.max(2, n));
35
+ return n + unit;
36
+ }
37
+
38
+ export function Skeleton({ height = '1em', width = '100%', count = 1, label = 'loading content', key } = {}) {
39
+ const h_ = clampLen(height, '1em');
40
+ const w_ = clampLen(width, '100%');
41
+ return h('div', {
42
+ key, class: 'ds-skeleton-group',
43
+ role: 'status', 'aria-busy': 'true', 'aria-label': label
44
+ },
45
+ ...Array(count).fill(0).map((_, i) =>
46
+ h('div', { key: String(i), class: 'ds-skeleton', style: `height:${h_};width:${w_};`, 'aria-hidden': 'true' })
47
+ )
48
+ );
49
+ }
50
+
51
+ // FilterPills — a role=group of pill toggle buttons for quick category filters.
52
+ // `options` is [{ id, label }]; `selected` the active id; clicking a pill calls
53
+ // onSelect(id). Pressed state is announced via aria-pressed.
54
+ export function FilterPills({ options = [], selected, onSelect, label = 'filters' } = {}) {
55
+ if (!options.length) return null;
56
+ return h('div', { class: 'ds-filter-pills', role: 'group', 'aria-label': label },
57
+ ...options.map((o) => h('button', {
58
+ key: 'fp-' + o.id,
59
+ type: 'button',
60
+ class: 'ds-filter-pill' + (o.id === selected ? ' active' : ''),
61
+ 'aria-pressed': o.id === selected ? 'true' : 'false',
62
+ onclick: () => onSelect && onSelect(o.id),
63
+ }, o.label != null ? o.label : o.id)));
64
+ }
65
+
66
+ export function Alert({ kind = 'info', children, onDismiss, title, key } = {}) {
67
+ const icons = { info: 'info', success: 'check', warn: 'warn', error: 'x' };
68
+ const cls = 'ds-alert ds-alert-' + kind;
69
+ return h('div', { key, class: cls, role: 'alert' },
70
+ h('span', { key: 'icon', class: 'ds-alert-icon' }, Icon(icons[kind] || 'info')),
71
+ h('div', { key: 'content', class: 'ds-alert-content' },
72
+ title ? h('div', { key: 'title', class: 'ds-alert-title' }, title) : null,
73
+ h('div', { key: 'msg', class: 'ds-alert-message' }, ...(Array.isArray(children) ? children : [children]))
74
+ ),
75
+ onDismiss ? h('button', { key: 'dismiss', class: 'ds-alert-dismiss', 'aria-label': 'dismiss', onclick: onDismiss }, Icon('x')) : null
76
+ );
77
+ }
@@ -0,0 +1,136 @@
1
+ // Form controls — the standalone field primitives (SearchInput, TextField,
2
+ // Select) and the declarative `Form` builder that lays out a fields[] spec.
3
+ // Every control carries a real accessible name; SearchInput additionally
4
+ // owns the single shared clear path (Escape key and visible X button).
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ import { Icon } from '../shell.js';
8
+ const h = webjsx.createElement;
9
+
10
+ export function SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key, label, resultCount }) {
11
+ // Shared clear path — both the Escape key and the visible clear button
12
+ // call this, so there is exactly one place that clears the field.
13
+ const doClear = (e) => { if (onInput) onInput('', e); };
14
+ const input = h('input', {
15
+ key: 'i',
16
+ type: 'search',
17
+ name,
18
+ class: 'ds-search-input',
19
+ placeholder,
20
+ 'aria-label': label || placeholder,
21
+ value,
22
+ oninput: onInput ? (e) => onInput(e.target.value, e) : null,
23
+ onkeydown: (e) => {
24
+ // Escape clears the field in place (stays focused) rather than
25
+ // falling through to whatever ancestor Escape handler exists.
26
+ if (e.key === 'Escape' && value) { e.preventDefault(); e.stopPropagation(); doClear(e); return; }
27
+ // IME guard: the Enter that commits a CJK composition must not submit.
28
+ if (onSubmit && e.key === 'Enter' && !e.isComposing && e.keyCode !== 229) onSubmit(e.target.value, e);
29
+ }
30
+ });
31
+ // Visible clear (X) button — mouse/touch users have no way to discover the
32
+ // Escape-to-clear shortcut, so this surfaces the same clear path visibly.
33
+ // Only rendered when there's something to clear.
34
+ const clearBtn = value
35
+ ? h('button', {
36
+ key: 'clr', type: 'button', class: 'ds-search-clear',
37
+ 'aria-label': 'clear search',
38
+ onclick: doClear,
39
+ }, Icon('x'))
40
+ : null;
41
+ // Always return the same wrapping shape regardless of whether resultCount/
42
+ // clearBtn are present this render - a conditional bare-input-vs-wrapped-
43
+ // span return here previously changed SearchInput's VElement type at the
44
+ // SAME keyed slot from render to render (e.g. typing into an empty filter
45
+ // makes resultCount go from undefined to a string), and webjsx's applyDiff
46
+ // has no way to morph one element type into another in place - it produced
47
+ // a corrupted merged DOM node carrying attributes from both shapes.
48
+ return h('span', { key, class: 'ds-search-input-wrap' },
49
+ input,
50
+ clearBtn,
51
+ resultCount != null ? h('span', { key: 'cnt', class: 'sr-only', role: 'status', 'aria-live': 'polite' }, resultCount) : null);
52
+ }
53
+
54
+ 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 }) {
55
+ // size: 'sm' | 'md' | 'lg' — md is the base .ds-field control; sm/lg add a
56
+ // wrapper modifier that snaps the control height/padding/font to --ctl-*.
57
+ const sizeCls = size === 'sm' ? ' ds-field--sm' : (size === 'lg' ? ' ds-field--lg' : '');
58
+ const errorId = error != null ? ((key ? key : 'tf') + '-err') : null;
59
+ const describedBy = ariaDescribedBy || errorId || null;
60
+ const input = multiline
61
+ ? h('textarea', {
62
+ key: 'i', name, rows, placeholder, value,
63
+ maxlength: maxLength != null ? maxLength : null,
64
+ 'aria-label': ariaLabel || null,
65
+ 'aria-invalid': error != null ? 'true' : (ariaInvalid || null),
66
+ 'aria-describedby': describedBy,
67
+ title: title || null,
68
+ oninput: onInput ? (e) => onInput(e.target.value, e) : null,
69
+ onchange: onChange ? (e) => onChange(e.target.value, e) : null
70
+ })
71
+ : h('input', {
72
+ key: 'i', type, name, placeholder, value,
73
+ maxlength: maxLength != null ? maxLength : null,
74
+ min: min != null ? String(min) : null,
75
+ max: max != null ? String(max) : null,
76
+ 'aria-label': ariaLabel || null,
77
+ 'aria-invalid': error != null ? 'true' : (ariaInvalid || null),
78
+ 'aria-describedby': describedBy,
79
+ title: title || null,
80
+ oninput: onInput ? (e) => onInput(e.target.value, e) : null,
81
+ onchange: onChange ? (e) => onChange(e.target.value, e) : null
82
+ });
83
+ return h('label', { key, class: 'ds-field' + sizeCls },
84
+ ...[
85
+ label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
86
+ input,
87
+ error != null ? h('span', { key: 'e', id: errorId, class: 'ds-field-error', role: 'alert', 'aria-live': 'polite', 'aria-atomic': 'true' }, error) : null,
88
+ maxLength != null ? h('span', { key: 'c', class: 'ds-field-count' }, String(value.length) + '/' + maxLength) : null,
89
+ hint != null ? h('span', { key: 'h', class: 'ds-field-hint' }, hint) : null
90
+ ].filter(Boolean)
91
+ );
92
+ }
93
+
94
+ export function Select({ label, value = '', options = [], onChange, name, key, placeholder, hint, title, size = 'md', 'aria-label': ariaLabel }) {
95
+ const sizeCls = size === 'sm' ? ' ds-field--sm' : (size === 'lg' ? ' ds-field--lg' : '');
96
+ const opts = [];
97
+ if (placeholder != null) opts.push(h('option', { key: '_ph', value: '', disabled: true, selected: value === '' || value == null }, placeholder));
98
+ for (const o of options) {
99
+ const id = typeof o === 'string' ? o : (o.value != null ? o.value : o.id);
100
+ const lab = typeof o === 'string' ? o : (o.label != null ? o.label : (o.id || o.value));
101
+ opts.push(h('option', { key: 'o-' + id, value: id, selected: id === value }, lab));
102
+ }
103
+ const select = h('select', {
104
+ key: 'i', name, class: 'ds-select',
105
+ // Guarantee an accessible name even when rendered without a visible label.
106
+ 'aria-label': ariaLabel || (label == null ? (title || placeholder || name) : null),
107
+ title,
108
+ onchange: onChange ? (e) => onChange(e.target.value, e) : null
109
+ }, ...opts);
110
+ if (label == null && hint == null && size === 'md') return select;
111
+ if (label == null && hint == null) return h('label', { key, class: 'ds-field' + sizeCls }, select);
112
+ return h('label', { key, class: 'ds-field' + sizeCls },
113
+ label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
114
+ select,
115
+ hint != null ? h('span', { key: 'h', class: 'ds-field-hint' }, hint) : null
116
+ );
117
+ }
118
+
119
+ export function Form({ fields = [], submit = 'submit', onSubmit, columns = 1 }) {
120
+ const cols = columns > 1 ? String(columns) : null;
121
+ return h('form', { class: 'row-form', 'data-columns': cols, onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
122
+ ...fields.map((f, i) => {
123
+ // Each control gets a stable id and an associated <label> so the
124
+ // placeholder is no longer the only (inaccessible) name. The label
125
+ // text falls back to label -> placeholder -> name.
126
+ const fieldId = 'ds-form-' + (f.name || 'field') + '-' + i;
127
+ const labelText = f.label != null ? f.label : (f.placeholder || f.name || '');
128
+ const control = f.kind === 'textarea'
129
+ ? h('textarea', { key: 'i', id: fieldId, name: f.name, placeholder: f.placeholder || '', rows: f.rows || 4, required: f.required ? true : null })
130
+ : h('input', { key: 'i', id: fieldId, name: f.name, type: f.type || 'text', placeholder: f.placeholder || '', value: f.value || '', required: f.required ? true : null });
131
+ return h('label', { key: i, class: 'ds-field', for: fieldId },
132
+ labelText !== '' ? h('span', { key: 'l', class: 'ds-field-label' }, labelText) : null,
133
+ control);
134
+ }),
135
+ h('button', { type: 'submit', class: 'btn-primary' }, submit));
136
+ }
@@ -0,0 +1,146 @@
1
+ // Masthead blocks — the page-opening surfaces: Hero (the asymmetric
2
+ // two-column grid), HeroFromPageData (the same shape driven by a parsed
3
+ // page-data object), PageHeader (display and dense forms), Marquee (the
4
+ // signature ticker) and Manifesto (long-form prose block).
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ const h = webjsx.createElement;
8
+
9
+ export function Hero({ eyebrow, title, body, accent, actions, badges }) {
10
+ // Eyebrow + title share the title grid-area so the named-area layout stays
11
+ // intact; body occupies the wide left column, badges + actions stack in
12
+ // the narrow right column so it carries real visual weight instead of
13
+ // sitting empty beside the body copy.
14
+ const badgeList = Array.isArray(badges) ? badges.filter(Boolean) : [];
15
+ const badgeRow = badgeList.length
16
+ ? h('div', { class: 'ds-hero-stats' }, ...badgeList.map((b, i) =>
17
+ h('span', { key: 'hb' + i, class: 'ds-hero-stat' }, String(b && b.label != null ? b.label : b))))
18
+ : null;
19
+ const actionRow = actions ? h('div', { class: 'ds-hero-actions' }, ...(Array.isArray(actions) ? actions : [actions])) : null;
20
+ const aside = (badgeRow || actionRow) ? h('div', { class: 'ds-hero-aside' }, badgeRow, actionRow) : null;
21
+ return h('div', { class: 'ds-hero' },
22
+ h('div', { class: 'ds-hero-head' },
23
+ eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
24
+ h('h1', { class: 'ds-hero-title' }, title)
25
+ ),
26
+ body ? h('p', { class: 'ds-hero-body' },
27
+ body,
28
+ accent ? h('span', { class: 'ds-hero-accent' }, ' ' + accent) : null
29
+ ) : null,
30
+ aside
31
+ );
32
+ }
33
+
34
+ // HeroFromPageData — a single factory for the "hero block driven by a page-data
35
+ // object" shape that recurs, independently hand-rolled, across every flatspace
36
+ // consumer theme.mjs (heading/subheading/body/badges/ctas/install all read off
37
+ // a `hero` object parsed from the `__site__` JSON script tag). Consumers differ
38
+ // only in which fields their content YAML populates; this factory renders every
39
+ // field it is given and omits what is absent, so it is a drop-in for the
40
+ // narrowest (heading+body only) or richest (badges+ctas+install) hero shape
41
+ // alike. Returns null on a falsy `hero` so callers can write
42
+ // `HeroFromPageData(page.hero)` unconditionally, matching the existing
43
+ // `!home.hero ? null : ...` guard every hand-rolled version repeats.
44
+ //
45
+ // Shape: { heading, title, subheading, body, accent, badges, ctas, install }
46
+ // heading/title — hero <h1> text (heading wins if both given)
47
+ // subheading — a Lede-style standalone line above `body`
48
+ // body — the hero paragraph
49
+ // accent — a muted trailing aside appended to `body`
50
+ // badges — [{label, desc}] or [string], rendered as a stat strip
51
+ // ctas — [{label, href, primary}], rendered as Btn-equivalent links
52
+ // install — a single install command string, rendered as a `.cli` block
53
+ export function HeroFromPageData(hero) {
54
+ if (!hero) return null;
55
+ const heading = hero.heading || hero.title || '';
56
+ const badges = Array.isArray(hero.badges) ? hero.badges.filter(Boolean) : [];
57
+ const ctas = Array.isArray(hero.ctas) ? hero.ctas.filter(Boolean) : [];
58
+ const badgeRow = badges.length
59
+ ? h('div', { class: 'ds-hero-stats' }, ...badges.map((b, i) =>
60
+ h('span', { key: 'hb' + i, class: 'ds-hero-stat' },
61
+ h('strong', { class: 'ds-hero-stat-n' }, String(b && b.label != null ? b.label : b)),
62
+ (b && b.desc) ? h('span', { class: 'ds-hero-stat-l' }, String(b.desc)) : null,
63
+ )))
64
+ : null;
65
+ const ctaRow = ctas.length
66
+ ? h('div', { class: 'ds-hero-actions' }, ...ctas.map((c, i) =>
67
+ h('a', {
68
+ key: 'hc' + i,
69
+ class: (c.primary || i === 0) ? 'btn btn-accent' : 'btn btn-ghost',
70
+ href: c.href || '#',
71
+ }, c.label || c.cta || 'go')))
72
+ : null;
73
+ const installRow = hero.install
74
+ ? h('div', { class: 'cli' },
75
+ h('span', { class: 'prompt' }, '$'),
76
+ h('span', { class: 'cmd' }, hero.install))
77
+ : null;
78
+ return h('div', { class: 'ds-hero' },
79
+ h('div', { class: 'ds-hero-head' },
80
+ hero.eyebrow ? h('span', { class: 'eyebrow' }, hero.eyebrow) : null,
81
+ h('h1', { class: 'ds-hero-title' }, heading)
82
+ ),
83
+ hero.subheading ? h('p', { class: 'ds-hero-body lede' }, hero.subheading) : null,
84
+ hero.body ? h('p', { class: 'ds-hero-body' },
85
+ hero.body,
86
+ hero.accent ? h('span', { class: 'ds-hero-accent' }, ' ' + hero.accent) : null,
87
+ ) : null,
88
+ (badgeRow || ctaRow || installRow)
89
+ ? h('div', { class: 'ds-hero-aside' }, badgeRow, installRow, ctaRow)
90
+ : null,
91
+ );
92
+ }
93
+
94
+ export function Marquee({ items = [], sep = '/' }) {
95
+ // No items -> no ticker: an empty marquee still paints its border-block
96
+ // rules as an unexplained full-width stripe.
97
+ if (!items.length) return null;
98
+ // Two identical runs make the -50% translate loop seamless. Each text and
99
+ // separator is a keyed span so webjsx applyDiff never sees a primitive
100
+ // sibling beside a keyed VElement.
101
+ const run = (runKey) => items.flatMap((it, i) => [
102
+ h('span', { class: 'ds-marquee-item', key: `${runKey}-i${i}` }, it),
103
+ h('span', { class: 'ds-marquee-sep', key: `${runKey}-s${i}`, 'aria-hidden': 'true' }, sep),
104
+ ]);
105
+ return h('div', { class: 'ds-marquee', role: 'marquee' },
106
+ h('div', { class: 'ds-marquee-track' }, ...run('a'), ...run('b'))
107
+ );
108
+ }
109
+
110
+ export function Manifesto({ paragraphs = [], maxWidth }) {
111
+ return h('div', {
112
+ class: 'ds-prose ds-manifesto',
113
+ 'data-max-width': maxWidth ? String(maxWidth) : null
114
+ },
115
+ ...paragraphs.map((p, i) => h('p', {
116
+ key: i,
117
+ class: 'ds-manifesto-para' + (p.dim ? ' dim' : '')
118
+ }, p.text || p))
119
+ );
120
+ }
121
+
122
+ export function PageHeader({ title, lede, eyebrow, right, compact, dense, id }) {
123
+ // `compact` drops the large leading/trailing section margins so a PageHeader
124
+ // used as a page's first element top-aligns cleanly without the consumer
125
+ // having to !important-override the .ds-section margin. `id` lands on the
126
+ // outermost section so the header can serve as a deep-link anchor.
127
+ // `dense` is the content-first working-surface form: one row - a small
128
+ // heading with the lede beside it, clamped to a single muted line - instead
129
+ // of a display H1 over a paragraph. App surfaces (files, dashboards,
130
+ // settings) should not spend 150px of fold on an intro.
131
+ if (dense) {
132
+ return h('section', { class: 'ds-section ds-section-compact ds-page-header-dense', id: id || null },
133
+ h('div', { class: 'ds-page-header-dense-row' },
134
+ ...[
135
+ title != null ? h('h1', { key: 'dh' }, title) : null,
136
+ lede != null ? h('span', { key: 'dl', class: 'ds-page-header-dense-lede', title: typeof lede === 'string' ? lede : null }, lede) : null,
137
+ right != null ? h('div', { key: 'dr', class: 'ds-page-header-right' }, ...(Array.isArray(right) ? right : [right])) : null,
138
+ ].filter(Boolean)));
139
+ }
140
+ return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : ''), id: id || null },
141
+ eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
142
+ title != null ? h('h1', {}, title) : null,
143
+ lede != null ? h('p', { class: 'lede' }, lede) : null,
144
+ right != null ? h('div', { class: 'ds-page-header-right' }, ...(Array.isArray(right) ? right : [right])) : null
145
+ );
146
+ }
@@ -0,0 +1,85 @@
1
+ // Row-backed lists — the three domain list renderers built on Row/RowLink:
2
+ // WorksList (expand-to-detail portfolio entries), WritingList (dated posts)
3
+ // and EventList (a dense event feed with a shape-matched loading skeleton).
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Btn, Icon } from '../shell.js';
7
+ import { Row, RowLink } from './row.js';
8
+ import { Panel } from './panel.js';
9
+ const h = webjsx.createElement;
10
+
11
+ export function WorksList({ works = [], openedIndex = -1, onToggle }) {
12
+ return Panel({
13
+ children: works.map((w, i) => {
14
+ const isOpen = openedIndex === i;
15
+ return h('div', { key: i },
16
+ Row({
17
+ code: w.code,
18
+ title: w.title, sub: w.sub,
19
+ // Expand affordance: a chevron icon (down when open, right when
20
+ // collapsed) separated from the meta text by a CSS gap, not a
21
+ // literal +/- with a double-space.
22
+ meta: h('span', { class: 'ds-works-meta' },
23
+ w.meta != null ? h('span', {}, w.meta) : null,
24
+ Icon(isOpen ? 'chevron-down' : 'chevron-right')),
25
+ active: isOpen,
26
+ expanded: isOpen,
27
+ onClick: () => onToggle && onToggle(isOpen ? -1 : i)
28
+ }),
29
+ isOpen ? h('div', { class: 'work-detail', 'data-work-index': String(i) },
30
+ h('div', { class: 'ds-prose' },
31
+ h('p', { class: 'ds-work-body' }, w.body)
32
+ ),
33
+ h('div', { class: 'ds-work-actions' },
34
+ Btn({ variant: 'primary', href: w.href || '#', children: 'open ->' }),
35
+ Btn({ href: w.source || '#', children: 'source' })
36
+ )
37
+ ) : null
38
+ );
39
+ })
40
+ });
41
+ }
42
+
43
+ export function WritingList({ posts = [] }) {
44
+ return Panel({
45
+ children: posts.map((p, i) =>
46
+ RowLink({ key: i, code: p.date, title: p.title, meta: p.tag, href: p.href || '#' })
47
+ )
48
+ });
49
+ }
50
+
51
+ export function EventList({ items, events, emptyText = 'no events', rankPad = 3, loading = false, loadingText = 'loading events…' }) {
52
+ const list = items || events || [];
53
+ // Shape-matched skeleton rows for the slow first events fetch (the ccsniff
54
+ // cold walk can take 30-90s) - a lone spinner collapses the whole pane.
55
+ // Keying discipline mirrors ConversationList: a single keyed wrapper with
56
+ // all-keyed siblings (webjsx applyDiff crashes on mixed keyed/unkeyed).
57
+ if (loading && !list.length) {
58
+ return h('section', { class: 'ds-section ds-event-list' },
59
+ h('div', { key: 'st', role: 'status', 'aria-live': 'polite', class: 'ds-event-state lede' }, loadingText),
60
+ ...Array.from({ length: 7 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-event-row-skeleton', 'aria-hidden': 'true' },
61
+ h('span', { key: 'r', class: 'ds-skel ds-skel-rank' }),
62
+ h('span', { key: 't', class: 'ds-skel ds-skel-title' }),
63
+ h('span', { key: 'm', class: 'ds-skel ds-skel-meta' }))));
64
+ }
65
+ if (!list.length) return h('p', { class: 'lede' }, emptyText);
66
+ return h('section', { class: 'ds-section ds-event-list' },
67
+ ...list.map((it, i) => Row({
68
+ key: it.key || ('ev' + i),
69
+ code: it.code != null ? it.code : (it.rank != null ? it.rank : String(i + 1).padStart(rankPad, '0')),
70
+ title: it.title || '(empty)',
71
+ sub: it.sub || '',
72
+ active: it.active,
73
+ onClick: it.onClick,
74
+ kind: it.kind,
75
+ rail: it.rail,
76
+ // Forward a disclosure state when the host marks the row as a toggle,
77
+ // so a clickable event row announces aria-expanded.
78
+ expanded: it.expanded,
79
+ detail: it.detail,
80
+ actions: it.actions,
81
+ highlight: it.highlight,
82
+ meta: it.meta
83
+ }))
84
+ );
85
+ }