anentrypoint-design 0.0.28 → 0.0.30

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.28",
3
+ "version": "0.0.30",
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,105 @@
1
+ import * as webjsx from '../../vendor/webjsx/index.js';
2
+ import { Btn } from './shell.js';
3
+ import { fileGlyph, fmtFileSize } from './files.js';
4
+ const h = webjsx.createElement;
5
+
6
+ function Backdrop({ onClose, children, kind = '' } = {}) {
7
+ return h('div', {
8
+ class: 'ds-modal-backdrop',
9
+ onclick: (e) => { if (e.target === e.currentTarget && onClose) onClose(); }
10
+ },
11
+ h('div', { class: 'ds-modal' + (kind ? ' ds-modal-' + kind : '') }, ...(Array.isArray(children) ? children : [children]))
12
+ );
13
+ }
14
+
15
+ export function ConfirmDialog({ title = 'confirm', message, confirmLabel = 'confirm', cancelLabel = 'cancel', destructive, onConfirm, onCancel } = {}) {
16
+ return Backdrop({
17
+ onClose: onCancel,
18
+ kind: 'small',
19
+ children: [
20
+ h('div', { class: 'ds-modal-head' }, title),
21
+ h('div', { class: 'ds-modal-body' }, message || ''),
22
+ h('div', { class: 'ds-modal-actions' },
23
+ Btn({ onClick: onCancel, children: cancelLabel }),
24
+ h('button', {
25
+ class: destructive ? 'btn-stamp flame' : 'btn-stamp green',
26
+ onclick: onConfirm
27
+ }, confirmLabel)
28
+ )
29
+ ]
30
+ });
31
+ }
32
+
33
+ export function PromptDialog({ title = 'name', value = '', placeholder = '', confirmLabel = 'ok', cancelLabel = 'cancel', onConfirm, onCancel, onInput } = {}) {
34
+ return Backdrop({
35
+ onClose: onCancel,
36
+ kind: 'small',
37
+ children: [
38
+ h('div', { class: 'ds-modal-head' }, title),
39
+ h('div', { class: 'ds-modal-body' },
40
+ h('input', {
41
+ class: 'input ds-modal-input',
42
+ type: 'text',
43
+ value,
44
+ placeholder,
45
+ autofocus: true,
46
+ oninput: (e) => onInput && onInput(e.target.value),
47
+ onkeydown: (e) => {
48
+ if (e.key === 'Enter') { e.preventDefault(); onConfirm && onConfirm(e.target.value); }
49
+ if (e.key === 'Escape') { e.preventDefault(); onCancel && onCancel(); }
50
+ }
51
+ })
52
+ ),
53
+ h('div', { class: 'ds-modal-actions' },
54
+ Btn({ onClick: onCancel, children: cancelLabel }),
55
+ h('button', { class: 'btn-stamp green', onclick: () => onConfirm && onConfirm(value) }, confirmLabel)
56
+ )
57
+ ]
58
+ });
59
+ }
60
+
61
+ export function FilePreviewMedia({ src, type = 'other', name } = {}) {
62
+ if (type === 'image') return h('img', { class: 'ds-preview-media', src, alt: name || '' });
63
+ if (type === 'video') return h('video', { class: 'ds-preview-media', src, controls: true });
64
+ if (type === 'audio') return h('audio', { class: 'ds-preview-audio', src, controls: true });
65
+ return h('div', { class: 'ds-preview-fallback' },
66
+ h('span', { class: 'ds-preview-glyph' }, fileGlyph(type)),
67
+ h('span', {}, 'no inline preview for ' + (type || 'this file'))
68
+ );
69
+ }
70
+
71
+ export function FilePreviewCode({ content = '', lang } = {}) {
72
+ return h('pre', { class: 'ds-preview-code' + (lang ? ' lang-' + lang : '') },
73
+ h('code', { class: lang ? 'language-' + lang : '' }, content)
74
+ );
75
+ }
76
+
77
+ export function FilePreviewText({ content = '', truncated } = {}) {
78
+ return h('pre', { class: 'ds-preview-text' },
79
+ h('code', {}, content),
80
+ truncated ? h('div', { class: 'ds-preview-truncated' }, '… (truncated)') : null
81
+ );
82
+ }
83
+
84
+ export function FileViewer({ file, body, onClose, onAction } = {}) {
85
+ if (!file) return null;
86
+ const meta = [file.type, file.size != null ? fmtFileSize(file.size) : null, file.modified || null]
87
+ .filter(Boolean).join(' · ');
88
+ return Backdrop({
89
+ onClose,
90
+ kind: 'preview',
91
+ children: [
92
+ h('div', { class: 'ds-modal-head ds-preview-head', 'data-file-type': file.type || 'other' },
93
+ h('span', { class: 'ds-preview-name' }, file.name || ''),
94
+ h('span', { class: 'ds-preview-meta' }, meta),
95
+ h('span', { class: 'ds-preview-actions' },
96
+ onAction ? h('button', { class: 'ds-file-act', title: 'download', onclick: () => onAction('download') }, '↓') : null,
97
+ h('button', { class: 'ds-file-act', title: 'close', onclick: onClose }, '✕')
98
+ )
99
+ ),
100
+ h('div', { class: 'ds-preview-body', 'data-file-type': file.type || 'other' },
101
+ ...(Array.isArray(body) ? body : [body])
102
+ )
103
+ ]
104
+ });
105
+ }
@@ -0,0 +1,126 @@
1
+ import * as webjsx from '../../vendor/webjsx/index.js';
2
+ import { Btn, Glyph } from './shell.js';
3
+ const h = webjsx.createElement;
4
+
5
+ const FILE_TYPES = ['dir', 'image', 'video', 'audio', 'code', 'text', 'archive', 'document', 'symlink', 'other'];
6
+ const TYPE_GLYPH = {
7
+ dir: '▣', image: '◰', video: '▰', audio: '◎', code: '⌘',
8
+ text: '§', archive: '◐', document: '▢', symlink: '↗', other: '◌'
9
+ };
10
+
11
+ export function fileGlyph(type) {
12
+ return TYPE_GLYPH[type] || TYPE_GLYPH.other;
13
+ }
14
+
15
+ export function fmtFileSize(bytes) {
16
+ if (bytes == null || bytes === 0) return '—';
17
+ const u = ['B', 'KB', 'MB', 'GB', 'TB'];
18
+ let i = 0, n = bytes;
19
+ while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
20
+ return n.toFixed(i === 0 ? 0 : 1) + ' ' + u[i];
21
+ }
22
+
23
+ export function FileIcon({ type = 'other' } = {}) {
24
+ return h('span', { class: 'ds-file-icon', 'data-file-type': type }, fileGlyph(type));
25
+ }
26
+
27
+ export function FileRow({ name, type = 'other', size, modified, code, onOpen, onAction, active, key } = {}) {
28
+ const meta = [type === 'dir' ? null : fmtFileSize(size), modified || null].filter(Boolean).join(' · ');
29
+ return h('div', {
30
+ key,
31
+ class: 'ds-file-row row' + (active ? ' active' : ''),
32
+ 'data-file-type': type,
33
+ onclick: onOpen
34
+ },
35
+ code != null ? h('span', { class: 'code' }, code) : null,
36
+ FileIcon({ type }),
37
+ h('span', { class: 'title' }, name),
38
+ h('span', { class: 'ds-file-meta meta' }, meta || '—'),
39
+ onAction ? h('span', { class: 'ds-file-actions', onclick: (e) => e.stopPropagation() },
40
+ h('button', { class: 'ds-file-act', title: 'download', onclick: () => onAction('download') }, '↓'),
41
+ h('button', { class: 'ds-file-act', title: 'rename', onclick: () => onAction('rename') }, '✎'),
42
+ h('button', { class: 'ds-file-act ds-file-act-warn', title: 'delete', onclick: () => onAction('delete') }, '✕')
43
+ ) : null
44
+ );
45
+ }
46
+
47
+ export function FileGrid({ files = [], onOpen, onAction, emptyText = 'no files here yet' } = {}) {
48
+ if (!files.length) return EmptyState({ text: emptyText });
49
+ return h('div', { class: 'ds-file-grid' },
50
+ ...files.map((f, i) => FileRow({
51
+ key: f.path || f.name + i,
52
+ name: f.name,
53
+ type: f.type,
54
+ size: f.size,
55
+ modified: f.modified,
56
+ code: f.code,
57
+ active: f.active,
58
+ onOpen: onOpen ? () => onOpen(f) : null,
59
+ onAction: onAction ? (act) => onAction(act, f) : null
60
+ }))
61
+ );
62
+ }
63
+
64
+ export function FileToolbar({ left = [], right = [] } = {}) {
65
+ return h('div', { class: 'ds-file-toolbar' },
66
+ h('div', { class: 'ds-file-toolbar-left' }, ...left),
67
+ h('div', { class: 'ds-file-toolbar-right' }, ...right)
68
+ );
69
+ }
70
+
71
+ export function DropZone({ children, dragover, onDrop, onDragOver, onDragLeave, label = 'drop files here', onPick } = {}) {
72
+ return h('div', {
73
+ class: 'ds-dropzone' + (dragover ? ' dragover' : ''),
74
+ ondragover: (e) => { e.preventDefault(); onDragOver && onDragOver(e); },
75
+ ondragleave: (e) => { onDragLeave && onDragLeave(e); },
76
+ ondrop: (e) => { e.preventDefault(); onDrop && onDrop(e.dataTransfer.files); }
77
+ },
78
+ h('div', { class: 'ds-dropzone-inner' },
79
+ h('span', { class: 'ds-dropzone-glyph' }, '⇪'),
80
+ h('span', { class: 'ds-dropzone-label' }, label),
81
+ onPick ? Btn({ onClick: onPick, children: 'pick files' }) : null
82
+ ),
83
+ ...(Array.isArray(children) ? children : children ? [children] : [])
84
+ );
85
+ }
86
+
87
+ export function UploadProgress({ items = [] } = {}) {
88
+ if (!items.length) return null;
89
+ return h('div', { class: 'ds-upload-progress' },
90
+ ...items.map((it, i) => h('div', {
91
+ key: it.name + i,
92
+ class: 'ds-upload-item' + (it.done ? ' done' : '') + (it.error ? ' error' : '')
93
+ },
94
+ h('span', { class: 'ds-upload-name' }, it.name),
95
+ h('span', { class: 'ds-upload-bar' },
96
+ h('span', {
97
+ class: 'ds-upload-fill',
98
+ 'data-pct': String(Math.max(0, Math.min(100, it.pct || 0)))
99
+ })
100
+ ),
101
+ h('span', { class: 'ds-upload-pct' }, (it.error ? 'err' : (it.done ? 'ok' : (it.pct || 0) + '%')))
102
+ ))
103
+ );
104
+ }
105
+
106
+ export function EmptyState({ text = 'nothing here', glyph = '◌' } = {}) {
107
+ return h('div', { class: 'ds-file-empty' },
108
+ h('span', { class: 'ds-file-empty-glyph' }, glyph),
109
+ h('span', { class: 'ds-file-empty-text' }, text)
110
+ );
111
+ }
112
+
113
+ export function BreadcrumbPath({ segments = [], onNav, root = 'root' } = {}) {
114
+ const parts = [
115
+ h('button', { key: 'root', class: 'ds-crumb-seg', onclick: () => onNav && onNav(0) }, root)
116
+ ];
117
+ segments.forEach((seg, i) => {
118
+ parts.push(h('span', { key: 'sep' + i, class: 'ds-crumb-sep' }, '›'));
119
+ parts.push(h('button', {
120
+ key: 'seg' + i,
121
+ class: 'ds-crumb-seg' + (i === segments.length - 1 ? ' leaf' : ''),
122
+ onclick: () => onNav && onNav(i + 1)
123
+ }, seg));
124
+ });
125
+ return h('div', { class: 'ds-crumb-path' }, ...parts);
126
+ }