anentrypoint-design 0.0.27 → 0.0.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
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",
@@ -20,7 +20,8 @@
20
20
  "README.md"
21
21
  ],
22
22
  "scripts": {
23
- "build": "node scripts/build.mjs"
23
+ "build": "node scripts/build.mjs",
24
+ "test": "node test.js"
24
25
  },
25
26
  "repository": {
26
27
  "type": "git",
@@ -0,0 +1,38 @@
1
+ import * as webjsx from '../vendor/webjsx/index.js';
2
+ import * as motion from './motion.js';
3
+ import { register } from './debug.js';
4
+
5
+ const ANIMATE_HREF = 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css';
6
+ const PRISM_CSS = 'https://cdn.jsdelivr.net/npm/prismjs@1.30.0/themes/prism-tomorrow.min.css';
7
+
8
+ export function ensureCdnLink(id, href) {
9
+ if (document.getElementById(id)) return;
10
+ const link = document.createElement('link');
11
+ link.id = id;
12
+ link.rel = 'stylesheet';
13
+ link.href = href;
14
+ document.head.appendChild(link);
15
+ }
16
+
17
+ export function ensureMotionCss() { ensureCdnLink('animate-style-cdn', ANIMATE_HREF); }
18
+ export function ensurePrismCss() { ensureCdnLink('prism-style-cdn', PRISM_CSS); }
19
+
20
+ export function mountKit({ root, view, screen, animateOnMount = true } = {}) {
21
+ if (!root) throw new Error('mountKit: root required');
22
+ if (typeof view !== 'function') throw new Error('mountKit: view fn required');
23
+ if (screen) document.body.dataset.screenLabel = screen;
24
+ ensureMotionCss();
25
+ ensurePrismCss();
26
+ let scheduled = false;
27
+ const render = () => {
28
+ scheduled = false;
29
+ webjsx.applyDiff(root, view());
30
+ if (animateOnMount) requestAnimationFrame(() => motion.animateTree(root));
31
+ };
32
+ const schedule = () => { if (scheduled) return; scheduled = true; queueMicrotask(render); };
33
+ register('bootstrap', () => ({ screen: screen || null, mounted: !!root.firstChild, root: root.id || root.tagName }));
34
+ render();
35
+ return { render, schedule };
36
+ }
37
+
38
+ export { webjsx };
@@ -0,0 +1,199 @@
1
+ import * as webjsx from '../../vendor/webjsx/index.js';
2
+ import { renderMarkdown, ensureReady as ensureMarkdownReady } from '../markdown.js';
3
+ import { highlightAllUnder, ensurePrism } from '../highlight.js';
4
+ import { register } from '../debug.js';
5
+
6
+ const h = webjsx.createElement;
7
+
8
+ let _stats = { messages: 0, lastKindCounts: {} };
9
+
10
+ export function fmtBytes(n) {
11
+ if (n == null) return '';
12
+ if (n < 1024) return n + ' B';
13
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
14
+ if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB';
15
+ return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
16
+ }
17
+
18
+ export function renderInline(text) {
19
+ if (text == null) return [];
20
+ const out = [];
21
+ const re = /(\*\*([^*]+)\*\*|\*([^*]+)\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\))/g;
22
+ let last = 0; let m; let i = 0;
23
+ const push = (n) => out.push(n);
24
+ while ((m = re.exec(text)) !== null) {
25
+ if (m.index > last) push(h('span', { key: 's' + i + 'a' }, text.slice(last, m.index)));
26
+ if (m[2] != null) push(h('strong', { key: 's' + i }, m[2]));
27
+ else if (m[3] != null) push(h('em', { key: 's' + i }, m[3]));
28
+ else if (m[4] != null) push(h('code', { key: 's' + i, class: 'chat-tick' }, m[4]));
29
+ else if (m[5] != null) push(h('a', { key: 's' + i, href: m[6], target: '_blank', rel: 'noopener' }, m[5]));
30
+ last = m.index + m[0].length; i += 1;
31
+ }
32
+ if (last < text.length) push(h('span', { key: 's' + i + 'a' }, text.slice(last)));
33
+ return out;
34
+ }
35
+
36
+ const FILE_GLYPHS = { pdf: '▤', zip: '▦', tar: '▦', gz: '▦', mp4: '▶', mp3: '♪', wav: '♪', csv: '⊞', json: '{}', md: '§', txt: '§', default: '◫' };
37
+ function fileGlyph(name) {
38
+ const ext = String(name || '').split('.').pop().toLowerCase();
39
+ return FILE_GLYPHS[ext] || FILE_GLYPHS.default;
40
+ }
41
+
42
+ function MdNode(p) {
43
+ const refSink = (el) => {
44
+ if (!el) return;
45
+ if (el.dataset.mdSrc === p.text) return;
46
+ el.dataset.mdSrc = p.text || '';
47
+ ensureMarkdownReady().then(() => renderMarkdown(p.text || '')).then((html) => { el.innerHTML = html; });
48
+ };
49
+ return h('div', { class: 'chat-bubble chat-md', ref: refSink });
50
+ }
51
+
52
+ function CodeNode(p) {
53
+ const refSink = (el) => {
54
+ if (!el) return;
55
+ if (el.dataset.codeKey === (p.lang || '') + '|' + (p.code || '').length) return;
56
+ el.dataset.codeKey = (p.lang || '') + '|' + (p.code || '').length;
57
+ ensurePrism().then(() => highlightAllUnder(el));
58
+ };
59
+ return h('div', { class: 'chat-bubble chat-code', ref: refSink },
60
+ h('div', { class: 'chat-code-head' },
61
+ h('span', { class: 'lang' }, p.lang || 'code'),
62
+ p.filename ? h('span', { class: 'name' }, p.filename) : null
63
+ ),
64
+ h('pre', {}, h('code', { class: p.lang ? 'lang-' + p.lang + ' language-' + p.lang : '' }, p.code || ''))
65
+ );
66
+ }
67
+
68
+ const PART_RENDERERS = {
69
+ text: (p) => h('div', { class: 'chat-bubble' }, ...renderInline(p.text || '')),
70
+ md: (p) => MdNode(p),
71
+ code: (p) => CodeNode(p),
72
+ image: (p) => h('a', { class: 'chat-image', href: p.href || p.src, target: '_blank', rel: 'noopener' },
73
+ h('img', { src: p.src, alt: p.alt || '', loading: 'lazy' }),
74
+ p.caption ? h('span', { class: 'cap' }, p.caption) : null
75
+ ),
76
+ pdf: (p) => h('div', { class: 'chat-pdf' },
77
+ h('div', { class: 'chat-pdf-head' },
78
+ h('span', { class: 'glyph' }, '▤'),
79
+ h('span', { class: 'name' }, p.name || 'document.pdf'),
80
+ p.size != null ? h('span', { class: 'size' }, fmtBytes(p.size)) : null,
81
+ h('a', { class: 'open', href: p.src, target: '_blank', rel: 'noopener' }, 'open ↗')
82
+ ),
83
+ h('embed', { src: p.src, type: 'application/pdf' })
84
+ ),
85
+ file: (p) => h('a', { class: 'chat-file', href: p.src, target: '_blank', rel: 'noopener', download: p.name || true },
86
+ h('span', { class: 'glyph' }, fileGlyph(p.name)),
87
+ h('span', { class: 'meta' },
88
+ h('span', { class: 'name' }, p.name || 'attachment'),
89
+ h('span', { class: 'size' }, [p.kindLabel || (p.name || '').split('.').pop().toUpperCase(), p.size != null ? fmtBytes(p.size) : null].filter(Boolean).join(' · '))
90
+ ),
91
+ h('span', { class: 'go' }, '↓')
92
+ ),
93
+ link: (p) => h('a', { class: 'chat-link', href: p.href, target: '_blank', rel: 'noopener' },
94
+ p.thumb ? h('img', { class: 'thumb', src: p.thumb, alt: '' }) : null,
95
+ h('span', { class: 'meta' },
96
+ h('span', { class: 'host' }, p.host || (() => { try { return new URL(p.href).host; } catch { return ''; } })()),
97
+ h('span', { class: 'title' }, p.title || p.href),
98
+ p.desc ? h('span', { class: 'desc' }, p.desc) : null
99
+ )
100
+ )
101
+ };
102
+
103
+ function renderPart(p, key) {
104
+ const fn = PART_RENDERERS[p.kind] || PART_RENDERERS.text;
105
+ const node = fn(p);
106
+ if (node && typeof node === 'object') node.props = { ...(node.props || {}), key: 'p' + key };
107
+ _stats.lastKindCounts[p.kind] = (_stats.lastKindCounts[p.kind] || 0) + 1;
108
+ return node;
109
+ }
110
+
111
+ export function ChatMessage({ who = 'them', avatar, text, parts, time, typing, key, aicat, reactions, receipt, name }) {
112
+ _stats.messages += 1;
113
+ const cls = 'chat-msg ' + who + (aicat && who === 'them' ? ' aicat' : '');
114
+ const av = h('span', { class: 'chat-avatar' }, avatar || (who === 'you' ? 'u' : '?'));
115
+ let bodyNodes;
116
+ if (typing) bodyNodes = [h('span', { class: 'chat-typing', key: 'typ' }, h('span'), h('span'), h('span'))];
117
+ else if (parts && parts.length) bodyNodes = parts.map((p, i) => renderPart(p, i));
118
+ else bodyNodes = [h('div', { class: 'chat-bubble', key: 't' }, ...renderInline(text || ''))];
119
+ const reactionRow = reactions && reactions.length
120
+ ? h('div', { class: 'chat-reactions' },
121
+ ...reactions.map((r, i) => h('span', { class: 'rxn' + (r.you ? ' you' : ''), key: 'r' + i },
122
+ h('span', { class: 'e' }, r.emoji), h('span', { class: 'n' }, String(r.count)))))
123
+ : null;
124
+ const tickNode = who === 'you' && receipt
125
+ ? h('span', { class: 'tick' + (receipt === 'read' ? ' read' : '') }, receipt === 'read' ? '✓✓' : '✓')
126
+ : null;
127
+ const metaItems = [];
128
+ if (name && who === 'them') metaItems.push(h('span', { class: 'who', key: 'w' }, name));
129
+ if (time) metaItems.push(h('span', { class: 't', key: 'ti' }, time));
130
+ if (tickNode) metaItems.push(tickNode);
131
+ const meta = metaItems.length ? h('div', { class: 'chat-meta' }, ...metaItems) : null;
132
+ const stack = h('div', { class: 'chat-stack' }, ...bodyNodes, reactionRow, meta);
133
+ return h('div', { key, class: cls }, who === 'you' ? stack : av, who === 'you' ? av : stack);
134
+ }
135
+
136
+ export function ChatComposer({ value, onInput, onSend, placeholder = 'message…', disabled }) {
137
+ const send = () => {
138
+ const v = (value || '').trim();
139
+ if (!v || disabled) return;
140
+ if (onSend) onSend(v);
141
+ };
142
+ return h('div', { class: 'chat-composer' },
143
+ h('textarea', { value: value || '', placeholder, rows: 1,
144
+ oninput: (e) => onInput && onInput(e.target.value),
145
+ onkeydown: (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } } }),
146
+ h('button', { class: 'send', disabled: disabled || !(value && value.trim()), onclick: send }, '↑')
147
+ );
148
+ }
149
+
150
+ export function Chat({ title = 'chat', sub, messages = [], composer, header } = {}) {
151
+ return h('div', { class: 'chat' },
152
+ header || h('div', { class: 'chat-head' },
153
+ h('span', { class: 'dot' }),
154
+ h('span', {}, title),
155
+ sub ? h('span', { class: 'sub' }, ' · ' + sub) : null,
156
+ h('span', { class: 'spread' }),
157
+ h('span', { class: 'sub' }, String(messages.length).padStart(2, '0') + ' msgs')
158
+ ),
159
+ h('div', { class: 'chat-thread' },
160
+ ...messages.map((m, i) => ChatMessage({ ...m, key: m.key != null ? m.key : i }))
161
+ ),
162
+ composer || null
163
+ );
164
+ }
165
+
166
+ export const AICAT_FACE = ` /\\_/\\\n( o.o )\n > ^ <`;
167
+
168
+ export function AICatPortrait({ name = 'aicat', status = 'idle', face } = {}) {
169
+ return h('div', { class: 'aicat-portrait' },
170
+ h('pre', { class: 'aicat-face' }, face || AICAT_FACE),
171
+ h('div', { class: 'aicat-meta' },
172
+ h('span', { class: 'name' }, name),
173
+ h('span', { class: 'status' }, h('span', { class: 'dot' }, '● '), status)
174
+ )
175
+ );
176
+ }
177
+
178
+ export function AICat({ name = 'aicat', messages = [], thinking, composer, status = 'online · purring' } = {}) {
179
+ const annotated = messages.map((m) =>
180
+ m.who === 'them' ? { ...m, aicat: true, avatar: m.avatar || '=^.^=' } : m);
181
+ const all = thinking
182
+ ? [...annotated, { who: 'them', aicat: true, avatar: '=^.^=', typing: true, key: '_thinking' }]
183
+ : annotated;
184
+ return h('div', { class: 'chat' },
185
+ h('div', { class: 'chat-head' },
186
+ h('span', { class: 'dot' }),
187
+ h('span', {}, name),
188
+ h('span', { class: 'sub' }, ' · ' + status),
189
+ h('span', { class: 'spread' }),
190
+ h('span', { class: 'sub' }, String(messages.length).padStart(2, '0') + ' turns')
191
+ ),
192
+ h('div', { class: 'chat-thread' },
193
+ ...all.map((m, i) => ChatMessage({ ...m, key: m.key != null ? m.key : i }))
194
+ ),
195
+ composer || null
196
+ );
197
+ }
198
+
199
+ register('chat', () => ({ messages: _stats.messages, lastKindCounts: { ..._stats.lastKindCounts } }));
@@ -0,0 +1,171 @@
1
+ import * as webjsx from '../../vendor/webjsx/index.js';
2
+ import { Btn, Heading, Lede } from './shell.js';
3
+ const h = webjsx.createElement;
4
+
5
+ export function Panel({ title, count, right, style = '', children, kind }) {
6
+ const cls = 'panel' + (kind ? ' panel-' + kind : '');
7
+ return h('div', { class: cls, style },
8
+ title != null ? h('div', { class: 'panel-head' },
9
+ h('span', {}, title),
10
+ right != null ? right : (count != null ? h('span', {}, String(count)) : null)
11
+ ) : null,
12
+ h('div', { class: 'panel-body' }, ...(Array.isArray(children) ? children : [children]))
13
+ );
14
+ }
15
+
16
+ export function Row({ code, title, sub, meta, active, onClick, key, style }) {
17
+ return h('div', {
18
+ key,
19
+ class: 'row' + (active ? ' active' : ''),
20
+ onclick: onClick,
21
+ style
22
+ },
23
+ code != null ? h('span', { class: 'code' }, code) : null,
24
+ h('span', { class: 'title' }, title, sub ? h('span', { class: 'sub' }, sub) : null),
25
+ meta != null ? h('span', { class: 'meta' }, meta) : null
26
+ );
27
+ }
28
+
29
+ export function RowLink({ code, title, sub, meta, href = '#', key }) {
30
+ return h('a', { key, class: 'row', href },
31
+ code != null ? h('span', { class: 'code' }, code) : null,
32
+ h('span', { class: 'title' }, title, sub ? h('span', { class: 'sub' }, sub) : null),
33
+ meta != null ? h('span', { class: 'meta' }, meta) : null
34
+ );
35
+ }
36
+
37
+ export function Hero({ title, body, accent, badge, badgeCount }) {
38
+ return h('div', { class: 'ds-hero' },
39
+ h('h1', { class: 'ds-hero-title' }, title),
40
+ body ? h('p', { class: 'ds-hero-body' },
41
+ body,
42
+ accent ? h('span', { class: 'ds-hero-accent' }, ' ' + accent) : null
43
+ ) : null,
44
+ badge ? Panel({ title: badge, count: badgeCount, kind: 'inline', children: [] }) : null
45
+ );
46
+ }
47
+
48
+ export function Install({ cmd, copied, onCopy }) {
49
+ return h('div', { class: 'cli' },
50
+ h('span', { class: 'prompt' }, '$'),
51
+ h('span', { class: 'cmd' }, cmd),
52
+ h('span', { class: 'copy', onclick: () => onCopy && onCopy(cmd) }, copied ? 'copied' : 'copy')
53
+ );
54
+ }
55
+
56
+ export function Receipt({ rows = [] }) {
57
+ return h('table', { class: 'kv' },
58
+ h('tbody', {}, ...rows.map(([k, v], i) =>
59
+ h('tr', { key: i }, h('td', {}, k), h('td', {}, v))
60
+ ))
61
+ );
62
+ }
63
+
64
+ export function Changelog({ entries = [] }) {
65
+ return Panel({
66
+ kind: 'wide',
67
+ children: entries.map((e, i) =>
68
+ h('div', { key: i, class: 'row ds-changelog-row' },
69
+ h('span', { class: 'code' }, e.date),
70
+ h('span', { class: 'ds-changelog-ver' }, e.ver),
71
+ h('span', { class: 'title' }, e.msg)
72
+ )
73
+ )
74
+ });
75
+ }
76
+
77
+ export function WorksList({ works = [], openedIndex = -1, onToggle }) {
78
+ return Panel({
79
+ title: `works · ${String(works.length).padStart(2, '0')} of ~${works.length}`,
80
+ right: h('a', { class: 'ds-link-accent', href: 'https://github.com/AnEntrypoint' }, 'all repos ↗'),
81
+ children: works.map((w, i) => {
82
+ const isOpen = openedIndex === i;
83
+ return h('div', { key: i },
84
+ Row({
85
+ code: w.code, title: w.title, sub: w.sub,
86
+ meta: w.meta + ' ' + (isOpen ? '−' : '+'),
87
+ active: isOpen,
88
+ onClick: () => onToggle && onToggle(isOpen ? -1 : i)
89
+ }),
90
+ isOpen ? h('div', {
91
+ class: 'work-detail',
92
+ 'data-work-index': String(i)
93
+ },
94
+ h('p', { class: 'ds-work-body' }, w.body),
95
+ h('div', { class: 'ds-work-actions' },
96
+ Btn({ primary: true, href: w.href || '#', children: 'open ↗' }),
97
+ Btn({ href: w.source || '#', children: 'source' })
98
+ )
99
+ ) : null
100
+ );
101
+ })
102
+ });
103
+ }
104
+
105
+ export function WritingList({ posts = [] }) {
106
+ return Panel({
107
+ children: posts.map((p, i) =>
108
+ RowLink({ key: i, code: p.date, title: p.title, meta: '§ ' + p.tag, href: p.href || '#' })
109
+ )
110
+ });
111
+ }
112
+
113
+ export function Manifesto({ paragraphs = [], maxWidth = 820 }) {
114
+ return Panel({
115
+ kind: 'manifesto',
116
+ style: `max-width:${maxWidth}px`,
117
+ children: h('div', { class: 'ds-manifesto' },
118
+ ...paragraphs.map((p, i) => h('p', {
119
+ key: i,
120
+ class: 'ds-manifesto-para' + (p.dim ? ' dim' : '')
121
+ }, p.text || p))
122
+ )
123
+ });
124
+ }
125
+
126
+ export function Section({ title, children }) {
127
+ return h('div', { class: 'ds-section' },
128
+ title ? h('h3', {}, title) : null,
129
+ ...(Array.isArray(children) ? children : [children])
130
+ );
131
+ }
132
+
133
+ export function HomeView({ state, onNav, onToggleWork, works, posts, manifesto, currentlyShipping }) {
134
+ return [
135
+ Hero({
136
+ title: 'the creative department of the internet.',
137
+ body: '247420 is a collective of mercurials. we ship fast, break things on purpose, and document honestly.',
138
+ accent: 'humor is load-bearing.'
139
+ }),
140
+ currentlyShipping ? h('div', { class: 'ds-section' },
141
+ Panel({
142
+ title: 'currently shipping',
143
+ count: currentlyShipping.length,
144
+ kind: 'inline',
145
+ children: currentlyShipping.map((row, i) =>
146
+ Row({
147
+ key: i,
148
+ code: h('span', { class: row.live ? 'ds-dot-live' : 'ds-dot-idle' }, row.live ? '●' : '○'),
149
+ title: row.title, sub: row.sub, meta: row.meta
150
+ })
151
+ )
152
+ })
153
+ ) : null,
154
+ Section({ title: '// works', children: WorksList({ works, openedIndex: state.opened, onToggle: onToggleWork }) }),
155
+ Section({ title: '// recent writing', children: WritingList({ posts }) }),
156
+ Section({ title: '// manifesto · rough draft', children: Manifesto({ paragraphs: manifesto }) })
157
+ ];
158
+ }
159
+
160
+ export function ProjectView({ project, copied, onCopy }) {
161
+ return [
162
+ Heading({ level: 1, children: project.name }),
163
+ Lede({ children: project.tagline }),
164
+ Heading({ level: 3, children: 'install' }),
165
+ Install({ cmd: project.install, copied, onCopy }),
166
+ Heading({ level: 3, children: 'receipt' }),
167
+ Receipt({ rows: project.receipt }),
168
+ Heading({ level: 3, children: 'changelog' }),
169
+ Changelog({ entries: project.changelog })
170
+ ];
171
+ }
@@ -0,0 +1,113 @@
1
+ import * as webjsx from '../../vendor/webjsx/index.js';
2
+ const h = webjsx.createElement;
3
+
4
+ export function Brand({ name = '247420', leaf } = {}) {
5
+ return h('span', { class: 'brand' }, name,
6
+ leaf ? h('span', { class: 'slash' }, ' / ') : null,
7
+ leaf || null);
8
+ }
9
+
10
+ export function Chip({ tone = '', children }) {
11
+ return h('span', { class: 'chip' + (tone ? ' ' + tone : '') }, children);
12
+ }
13
+
14
+ export function Btn({ href = '#', primary, children, onClick }) {
15
+ return h('a', {
16
+ class: primary ? 'btn-primary' : 'btn',
17
+ href,
18
+ onclick: onClick
19
+ }, children);
20
+ }
21
+
22
+ export function Glyph({ children, color }) {
23
+ return h('span', { class: 'glyph', style: color ? `color:${color}` : '' }, children);
24
+ }
25
+
26
+ export function Topbar({ brand = '247420', leaf = '', items = [], active = '', onNav, search } = {}) {
27
+ return h('header', { class: 'app-topbar' },
28
+ Brand({ name: brand, leaf }),
29
+ search ? h('label', { class: 'app-search' },
30
+ h('span', { class: 'icon' }, '⌕'),
31
+ h('input', { type: 'search', placeholder: search, 'aria-label': 'search' })
32
+ ) : null,
33
+ h('nav', {}, ...items.map(([label, href]) =>
34
+ h('a', {
35
+ key: label,
36
+ href,
37
+ class: active === label.replace(' ↗', '') ? 'active' : '',
38
+ onclick: (e) => {
39
+ if (!String(href).startsWith('http') && onNav) {
40
+ e.preventDefault();
41
+ onNav(label.replace(' ↗', ''));
42
+ }
43
+ }
44
+ }, label)
45
+ ))
46
+ );
47
+ }
48
+
49
+ export function Crumb({ trail = [], leaf = '', right } = {}) {
50
+ const parts = [];
51
+ trail.forEach((t, i) => {
52
+ parts.push(h('span', { key: 't' + i }, t));
53
+ parts.push(h('span', { key: 's' + i, class: 'sep' }, '›'));
54
+ });
55
+ parts.push(h('span', { key: 'leaf', class: 'leaf' }, leaf));
56
+ if (right) parts.push(h('span', { key: 'r', class: 'crumb-right' }, ...(Array.isArray(right) ? right : [right])));
57
+ return h('div', { class: 'app-crumb' }, ...parts);
58
+ }
59
+
60
+ export function Side({ sections = [] } = {}) {
61
+ return h('aside', { class: 'app-side' }, ...sections.flatMap(sec => [
62
+ h('div', { class: 'group', key: sec.group }, sec.group),
63
+ ...sec.items.map((item, i) => {
64
+ const { glyph, label, href = '#', active, count, color, onClick } = item;
65
+ return h('a', {
66
+ key: sec.group + i,
67
+ href,
68
+ class: active ? 'active' : '',
69
+ onclick: onClick
70
+ },
71
+ glyph != null ? Glyph({ children: glyph, color }) : null,
72
+ h('span', {}, label),
73
+ count != null ? h('span', { class: 'count' }, String(count)) : null
74
+ );
75
+ })
76
+ ]));
77
+ }
78
+
79
+ export function Status({ left = [], right = [] } = {}) {
80
+ return h('footer', { class: 'app-status' },
81
+ ...left.map((t, i) => h('span', { key: 'l' + i, class: 'item' }, t)),
82
+ h('span', { class: 'spread' }),
83
+ ...right.map((t, i) => h('span', { key: 'r' + i, class: 'item' }, t))
84
+ );
85
+ }
86
+
87
+ export function AppShell({ topbar, crumb, side, main, status, narrow } = {}) {
88
+ const hasSide = Boolean(side);
89
+ const sideMotionClass = hasSide
90
+ ? ' animate__animated animate__fadeInLeft'
91
+ : ' animate__animated animate__fadeOutLeft';
92
+ const sideNode = hasSide
93
+ ? side
94
+ : h('aside', { class: 'app-side', 'aria-hidden': 'true' });
95
+
96
+ return h('div', { class: 'app' },
97
+ topbar || null,
98
+ crumb || null,
99
+ h('div', { class: 'app-body' + (hasSide ? '' : ' no-side') },
100
+ h('div', { class: 'app-side-shell' + sideMotionClass }, sideNode),
101
+ h('main', { class: 'app-main' + (narrow ? ' narrow' : '') }, ...(Array.isArray(main) ? main : [main]))
102
+ ),
103
+ status || null
104
+ );
105
+ }
106
+
107
+ export function Heading({ level = 1, children, style = '' }) {
108
+ return h('h' + level, { style }, children);
109
+ }
110
+
111
+ export function Lede({ children }) {
112
+ return h('p', { class: 'lede' }, children);
113
+ }