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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.382",
|
|
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
|
+
}
|