anentrypoint-design 0.0.218 → 0.0.219
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/app-shell.css +4 -0
- package/dist/.nojekyll +0 -0
- package/dist/247420.css +4 -0
- package/dist/247420.js +13 -13
- package/dist/app-shell.css +594 -0
- package/dist/colors_and_type.css +197 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +308 -0
- package/dist/preview/buttons.html +28 -0
- package/dist/preview/colors-core.html +45 -0
- package/dist/preview/colors-lore.html +28 -0
- package/dist/preview/colors-semantic.html +34 -0
- package/dist/preview/dateline.html +19 -0
- package/dist/preview/dropzone.html +30 -0
- package/dist/preview/file-grid.html +19 -0
- package/dist/preview/file-row.html +20 -0
- package/dist/preview/file-toolbar.html +40 -0
- package/dist/preview/file-viewer.html +31 -0
- package/dist/preview/header.html +24 -0
- package/dist/preview/icons-unicode.html +26 -0
- package/dist/preview/index-row.html +25 -0
- package/dist/preview/inputs.html +22 -0
- package/dist/preview/manifesto.html +52 -0
- package/dist/preview/motion-default.js +106 -0
- package/dist/preview/rules.html +16 -0
- package/dist/preview/spacing.html +18 -0
- package/dist/preview/stamps-lore.html +20 -0
- package/dist/preview/stamps.html +14 -0
- package/dist/preview/theme-ink.html +15 -0
- package/dist/preview/type-display.html +16 -0
- package/dist/preview/type-mono.html +15 -0
- package/dist/preview/type-prose.html +11 -0
- package/dist/preview/type-scale.html +20 -0
- package/dist/preview/wordmarks.html +28 -0
- package/dist/robots.txt +8 -0
- package/dist/site/content/globals/navigation.yaml +5 -0
- package/dist/site/content/globals/site.yaml +16 -0
- package/dist/site/content/pages/freddie.yaml +88 -0
- package/dist/site/content/pages/home.yaml +190 -0
- package/dist/site/theme.mjs +368 -0
- package/dist/sitemap.xml +31 -0
- package/dist/slides/deck-stage-overlay.js +63 -0
- package/dist/slides/deck-stage-state.js +81 -0
- package/dist/slides/deck-stage-style.js +117 -0
- package/dist/slides/deck-stage.js +159 -0
- package/dist/slides/index.html +276 -0
- package/dist/src/bootstrap.js +25 -0
- package/dist/src/components/chat.js +199 -0
- package/dist/src/components/community.js +167 -0
- package/dist/src/components/content.js +213 -0
- package/dist/src/components/files-modals.js +107 -0
- package/dist/src/components/files.js +118 -0
- package/dist/src/components/freddie/helpers.js +50 -0
- package/dist/src/components/freddie.js +33 -0
- package/dist/src/components/shell.js +117 -0
- package/dist/src/components/theme-toggle.js +70 -0
- package/dist/src/components.js +52 -0
- package/dist/src/debug.js +30 -0
- package/dist/src/deck-stage.js +21 -0
- package/dist/src/highlight.js +32 -0
- package/dist/src/index.js +86 -0
- package/dist/src/kits/os/about-app.js +52 -0
- package/dist/src/kits/os/app-panes.css +152 -0
- package/dist/src/kits/os/browser-app.js +58 -0
- package/dist/src/kits/os/files-app.js +44 -0
- package/dist/src/kits/os/freddie/helpers.js +59 -0
- package/dist/src/kits/os/freddie/pages-chat.js +143 -0
- package/dist/src/kits/os/freddie/pages-core.js +101 -0
- package/dist/src/kits/os/freddie/pages-os.js +51 -0
- package/dist/src/kits/os/freddie/pages-tools.js +183 -0
- package/dist/src/kits/os/freddie/routes.js +24 -0
- package/dist/src/kits/os/freddie-dashboard.css +51 -0
- package/dist/src/kits/os/freddie-dashboard.js +101 -0
- package/dist/src/kits/os/icons.js +17 -0
- package/dist/src/kits/os/index.js +5 -0
- package/dist/src/kits/os/launcher.css +61 -0
- package/dist/src/kits/os/launcher.js +79 -0
- package/dist/src/kits/os/monitor-app.js +34 -0
- package/dist/src/kits/os/shell.js +214 -0
- package/dist/src/kits/os/terminal-app.js +45 -0
- package/dist/src/kits/os/theme.css +412 -0
- package/dist/src/kits/os/validate.css +19 -0
- package/dist/src/kits/os/validator-app.js +55 -0
- package/dist/src/kits/os/wm.css +115 -0
- package/dist/src/kits/os/wm.js +111 -0
- package/dist/src/markdown.js +39 -0
- package/dist/src/motion.js +35 -0
- package/dist/src/page-html.js +196 -0
- package/dist/src/styles.js +25 -0
- package/dist/src/theme.js +99 -0
- package/dist/src/web-components/ds-chat.js +45 -0
- package/dist/ui_kits/aicat/README.md +7 -0
- package/dist/ui_kits/aicat/app.js +156 -0
- package/dist/ui_kits/aicat/index.html +26 -0
- package/dist/ui_kits/aicat/sample-square.png +0 -0
- package/dist/ui_kits/aicat/sample-svg.svg +1 -0
- package/dist/ui_kits/aicat/sample.pdf +32 -0
- package/dist/ui_kits/blog/README.md +3 -0
- package/dist/ui_kits/blog/index.html +90 -0
- package/dist/ui_kits/chat/README.md +5 -0
- package/dist/ui_kits/chat/app.js +110 -0
- package/dist/ui_kits/chat/index.html +26 -0
- package/dist/ui_kits/chat/sample-square.png +0 -0
- package/dist/ui_kits/chat/sample-svg.svg +1 -0
- package/dist/ui_kits/chat/sample.pdf +32 -0
- package/dist/ui_kits/community/app.js +134 -0
- package/dist/ui_kits/community/index.html +24 -0
- package/dist/ui_kits/dashboard/app.js +92 -0
- package/dist/ui_kits/dashboard/index.html +26 -0
- package/dist/ui_kits/docs/README.md +3 -0
- package/dist/ui_kits/docs/index.html +123 -0
- package/dist/ui_kits/error_404/app.js +56 -0
- package/dist/ui_kits/error_404/index.html +26 -0
- package/dist/ui_kits/file_browser/README.md +48 -0
- package/dist/ui_kits/file_browser/app.js +231 -0
- package/dist/ui_kits/file_browser/index.html +33 -0
- package/dist/ui_kits/gallery/app.js +121 -0
- package/dist/ui_kits/gallery/index.html +26 -0
- package/dist/ui_kits/homepage/README.md +7 -0
- package/dist/ui_kits/homepage/app.js +167 -0
- package/dist/ui_kits/homepage/index.html +46 -0
- package/dist/ui_kits/project_page/README.md +3 -0
- package/dist/ui_kits/project_page/app.js +154 -0
- package/dist/ui_kits/project_page/index.html +45 -0
- package/dist/ui_kits/search/app.js +107 -0
- package/dist/ui_kits/search/index.html +26 -0
- package/dist/ui_kits/settings/app.js +133 -0
- package/dist/ui_kits/settings/index.html +26 -0
- package/dist/ui_kits/signin/app.js +115 -0
- package/dist/ui_kits/signin/index.html +26 -0
- package/dist/ui_kits/slide_deck/app.js +174 -0
- package/dist/ui_kits/slide_deck/index.html +26 -0
- package/dist/ui_kits/system_primer/app.js +152 -0
- package/dist/ui_kits/system_primer/index.html +26 -0
- package/dist/ui_kits/terminal/app.js +150 -0
- package/dist/ui_kits/terminal/index.html +26 -0
- package/dist/vendor/webjsx/applyDiff.js +182 -0
- package/dist/vendor/webjsx/attributes.js +171 -0
- package/dist/vendor/webjsx/constants.js +4 -0
- package/dist/vendor/webjsx/createDOMElement.js +52 -0
- package/dist/vendor/webjsx/createElement.js +75 -0
- package/dist/vendor/webjsx/elementTags.js +115 -0
- package/dist/vendor/webjsx/factory.js +6 -0
- package/dist/vendor/webjsx/index.js +6 -0
- package/dist/vendor/webjsx/jsx-dev-runtime.js +2 -0
- package/dist/vendor/webjsx/jsx-runtime.js +30 -0
- package/dist/vendor/webjsx/jsx.js +2 -0
- package/dist/vendor/webjsx/package.json +39 -0
- package/dist/vendor/webjsx/renderSuspension.js +25 -0
- package/dist/vendor/webjsx/types.js +5 -0
- package/dist/vendor/webjsx/utils.js +84 -0
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Community surface — matches upstream signatures.
|
|
2
|
+
|
|
3
|
+
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
4
|
+
const h = webjsx.createElement;
|
|
5
|
+
|
|
6
|
+
export function ServerIcon({ id, name, icon, active, badge, onClick } = {}) {
|
|
7
|
+
const initials = (name || '?').slice(0, 2).toUpperCase();
|
|
8
|
+
return h('div', { class: 'cm-server-icon' + (active ? ' active' : ''), onclick: onClick, title: name, 'data-id': id },
|
|
9
|
+
h('span', { class: 'cm-server-pill' }),
|
|
10
|
+
icon ? h('img', { src: icon, alt: name }) : h('span', {}, initials),
|
|
11
|
+
badge ? h('span', { class: 'cm-server-badge' }, badge > 99 ? '99+' : String(badge)) : null
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ServerRail({ servers = [], activeId, onSelect, onAdd } = {}) {
|
|
16
|
+
return h('div', { class: 'cm-server-rail' },
|
|
17
|
+
h('a', { class: 'cm-server-back', href: '../', title: 'Back' }, '◰'),
|
|
18
|
+
h('div', { class: 'cm-server-sep' }),
|
|
19
|
+
...servers.map(s => ServerIcon({ ...s, active: s.id === activeId, onClick: () => onSelect && onSelect(s.id) })),
|
|
20
|
+
onAdd ? h('button', { class: 'cm-server-add', onclick: onAdd, title: 'Add server' }, '+') : null
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ChannelItem({ id, name, type = 'text', active, voiceActive, onClick, onContext } = {}) {
|
|
25
|
+
const icon = type === 'voice' ? '🔊' : type === 'forum' ? '◻' : '#';
|
|
26
|
+
return h('div', {
|
|
27
|
+
class: 'cm-channel-item' + (active ? ' active' : '') + (voiceActive ? ' voice-active' : ''),
|
|
28
|
+
'data-id': id,
|
|
29
|
+
onclick: onClick,
|
|
30
|
+
oncontextmenu: (e) => { e.preventDefault(); onContext && onContext(id, e.clientX, e.clientY); }
|
|
31
|
+
},
|
|
32
|
+
h('span', { class: 'cm-ch-icon' }, icon),
|
|
33
|
+
h('span', { class: 'cm-ch-name' }, name)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ChannelCategory({ id, name, channels = [], collapsed, activeId, onToggle, onAddChannel, onChannelClick, onChannelContext } = {}) {
|
|
38
|
+
return h('div', { class: 'cm-channel-category' },
|
|
39
|
+
h('div', {
|
|
40
|
+
class: 'cm-category-header' + (collapsed ? ' collapsed' : ''),
|
|
41
|
+
onclick: () => onToggle && onToggle(id)
|
|
42
|
+
},
|
|
43
|
+
h('svg', { class: 'cm-cat-arrow', viewBox: '0 0 24 24' }, h('path', { d: 'M7 10l5 5 5-5z' })),
|
|
44
|
+
h('span', { class: 'cm-cat-name' }, name),
|
|
45
|
+
onAddChannel ? h('button', { class: 'cm-cat-add', onclick: (e) => { e.stopPropagation(); onAddChannel(id); }, title: 'Add channel' }, '+') : null
|
|
46
|
+
),
|
|
47
|
+
collapsed ? null : h('div', { class: 'cm-cat-channels' },
|
|
48
|
+
...channels.map(c => ChannelItem({
|
|
49
|
+
...c,
|
|
50
|
+
active: c.id === activeId,
|
|
51
|
+
onClick: () => onChannelClick && onChannelClick(c),
|
|
52
|
+
onContext: onChannelContext
|
|
53
|
+
}))
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function VoiceUser({ identity, speaking, color } = {}) {
|
|
59
|
+
const initial = (identity || '?').slice(0, 1).toUpperCase();
|
|
60
|
+
return h('div', { class: 'cm-voice-user' + (speaking ? ' speaking' : '') },
|
|
61
|
+
h('div', { class: 'cm-voice-user-avatar', style: color ? `background:${color}` : '' }, initial),
|
|
62
|
+
h('span', { class: 'cm-voice-user-name' }, identity)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function UserPanel({ name, tag, color, muted, deafened, onMute, onDeafen, onSettings } = {}) {
|
|
67
|
+
const initial = (name || '?').slice(0, 1).toUpperCase();
|
|
68
|
+
return h('div', { class: 'cm-user-panel' },
|
|
69
|
+
h('div', { class: 'cm-user-avatar', style: color ? `background:${color}` : '' },
|
|
70
|
+
h('span', { class: 'cm-user-status-dot' }),
|
|
71
|
+
initial
|
|
72
|
+
),
|
|
73
|
+
h('div', { class: 'cm-user-info' },
|
|
74
|
+
h('div', { class: 'cm-user-name' }, name || 'You'),
|
|
75
|
+
tag ? h('div', { class: 'cm-user-tag' }, tag) : null
|
|
76
|
+
),
|
|
77
|
+
h('div', { class: 'cm-user-controls' },
|
|
78
|
+
h('button', { class: 'cm-user-btn' + (muted ? ' muted' : ''), onclick: onMute, title: muted ? 'Unmute' : 'Mute' }, muted ? '🔇' : '🎤'),
|
|
79
|
+
h('button', { class: 'cm-user-btn' + (deafened ? ' deafened' : ''), onclick: onDeafen, title: deafened ? 'Undeafen' : 'Deafen' }, deafened ? '🔕' : '🎧'),
|
|
80
|
+
h('button', { class: 'cm-user-btn', onclick: onSettings, title: 'Settings' }, '⚙')
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ChannelSidebar({ serverName, channels = [], categories = [], activeId, collapsedCats, onChannelClick, onCategoryToggle, onAddChannel, onChannelContext, userPanelProps } = {}) {
|
|
86
|
+
const collapsed = collapsedCats || new Set();
|
|
87
|
+
const uncategorized = channels.filter(c => !c.categoryId || !categories.find(cat => cat.id === c.categoryId));
|
|
88
|
+
const sorted = [...categories].sort((a, b) => (a.position || 0) - (b.position || 0));
|
|
89
|
+
return h('div', { class: 'cm-channel-sidebar' },
|
|
90
|
+
h('div', { class: 'cm-server-header' },
|
|
91
|
+
h('span', { class: 'cm-server-header-name' }, serverName || 'Server'),
|
|
92
|
+
),
|
|
93
|
+
h('div', { class: 'cm-channel-list' },
|
|
94
|
+
...sorted.map(cat => ChannelCategory({
|
|
95
|
+
id: cat.id,
|
|
96
|
+
name: cat.name,
|
|
97
|
+
channels: channels.filter(c => c.categoryId === cat.id).sort((a, b) => (a.position || 0) - (b.position || 0)),
|
|
98
|
+
collapsed: collapsed.has && collapsed.has(cat.id),
|
|
99
|
+
activeId,
|
|
100
|
+
onToggle: onCategoryToggle,
|
|
101
|
+
onAddChannel,
|
|
102
|
+
onChannelClick,
|
|
103
|
+
onChannelContext
|
|
104
|
+
})),
|
|
105
|
+
uncategorized.length ? ChannelCategory({
|
|
106
|
+
id: 'uncategorized',
|
|
107
|
+
name: 'CHANNELS',
|
|
108
|
+
channels: uncategorized,
|
|
109
|
+
activeId,
|
|
110
|
+
onChannelClick,
|
|
111
|
+
onChannelContext
|
|
112
|
+
}) : null
|
|
113
|
+
),
|
|
114
|
+
userPanelProps ? UserPanel(userPanelProps) : null
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function MemberItem({ identity, name, color, status = 'online' } = {}) {
|
|
119
|
+
const initial = (name || identity || '?').slice(0, 1).toUpperCase();
|
|
120
|
+
return h('div', { class: 'cm-member-item' },
|
|
121
|
+
h('div', { class: 'cm-member-avatar', style: color ? `background:${color}` : '' },
|
|
122
|
+
h('span', { class: 'cm-member-status' + (status === 'online' ? ' online' : '') }),
|
|
123
|
+
initial
|
|
124
|
+
),
|
|
125
|
+
h('span', { class: 'cm-member-name' }, name || identity)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function MemberList({ categories = [], open } = {}) {
|
|
130
|
+
return h('div', { class: 'cm-member-list' + (open ? ' open' : '') },
|
|
131
|
+
...categories.flatMap(cat => [
|
|
132
|
+
h('div', { class: 'cm-member-category', key: cat.label }, `${cat.label} — ${cat.members.length}`),
|
|
133
|
+
...cat.members.map((m, i) => MemberItem({ ...m, key: m.identity || i }))
|
|
134
|
+
])
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function ChatHeader({ icon = '#', name, topic, toolbar = [] } = {}) {
|
|
139
|
+
return h('div', { class: 'cm-chat-header' },
|
|
140
|
+
h('span', { class: 'cm-chat-header-icon' }, icon),
|
|
141
|
+
h('span', { class: 'cm-chat-header-name' }, name),
|
|
142
|
+
topic ? h('span', { class: 'cm-chat-header-topic' }, topic) : null,
|
|
143
|
+
h('div', { class: 'cm-chat-header-toolbar' }, ...toolbar)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function VoiceStrip({ channelName, status, muted, deafened, onMute, onDeafen, onLeave, open } = {}) {
|
|
148
|
+
return h('div', { class: 'cm-voice-strip' + (open ? ' open' : '') },
|
|
149
|
+
h('div', { class: 'cm-vs-label' },
|
|
150
|
+
h('span', { class: 'cm-vs-channel' }, '🔊 ' + (channelName || 'voice')),
|
|
151
|
+
h('span', { class: 'cm-vs-status' }, status || 'connected')
|
|
152
|
+
),
|
|
153
|
+
h('button', { class: 'cm-vs-btn', onclick: onMute, title: 'Mute' }, muted ? '🔇' : '🎤'),
|
|
154
|
+
h('button', { class: 'cm-vs-btn', onclick: onDeafen, title: 'Deafen' }, deafened ? '🔕' : '🎧'),
|
|
155
|
+
h('button', { class: 'cm-vs-btn danger', onclick: onLeave, title: 'Leave' }, '✕')
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function CommunityShell({ serverRailProps, sidebarProps, children, memberListProps, voiceStripProps } = {}) {
|
|
160
|
+
return h('div', { class: 'cm-shell' },
|
|
161
|
+
serverRailProps ? ServerRail(serverRailProps) : null,
|
|
162
|
+
sidebarProps ? ChannelSidebar(sidebarProps) : null,
|
|
163
|
+
h('div', { class: 'cm-main' }, ...(Array.isArray(children) ? children : [children])),
|
|
164
|
+
memberListProps ? MemberList(memberListProps) : null,
|
|
165
|
+
voiceStripProps ? VoiceStrip(voiceStripProps) : null
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Content blocks: Panel, Row, RowLink, Section, Hero, Install, Receipt,
|
|
2
|
+
// Changelog, WorksList, WritingList, Manifesto, Kpi, Table, HomeView,
|
|
3
|
+
// ProjectView, Form. Pure factories.
|
|
4
|
+
|
|
5
|
+
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
6
|
+
import { Btn, Heading, Lede, Dot } from './shell.js';
|
|
7
|
+
const h = webjsx.createElement;
|
|
8
|
+
|
|
9
|
+
export function Panel({ title, count, right, style = '', children, kind }) {
|
|
10
|
+
const cls = 'panel' + (kind ? ' panel-' + kind : '');
|
|
11
|
+
return h('div', { class: cls, style },
|
|
12
|
+
title != null ? h('div', { class: 'panel-head' },
|
|
13
|
+
h('span', {}, title),
|
|
14
|
+
right != null ? right : (count != null ? h('span', {}, String(count)) : null)
|
|
15
|
+
) : null,
|
|
16
|
+
h('div', { class: 'panel-body' }, ...(Array.isArray(children) ? children : [children]))
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Row({ code, title, sub, meta, active, onClick, key, style, href, kind, cols, leading, trailing, target }) {
|
|
21
|
+
const isLink = kind === 'link' || (href != null && !onClick);
|
|
22
|
+
const cls = 'row' + (active ? ' active' : '') + (cols ? ' row-grid' : '');
|
|
23
|
+
const props = { key, class: cls, style: cols ? `${style ? style + ';' : ''}grid-template-columns:${cols}` : style };
|
|
24
|
+
if (isLink) { props.href = href || '#'; if (target) props.target = target; }
|
|
25
|
+
else if (onClick) { props.onclick = onClick; }
|
|
26
|
+
return h(isLink ? 'a' : 'div', props,
|
|
27
|
+
leading != null ? leading : (code != null ? h('span', { class: 'code' }, code) : null),
|
|
28
|
+
h('span', { class: 'title' }, title, sub ? h('span', { class: 'sub' }, sub) : null),
|
|
29
|
+
trailing != null ? trailing : (meta != null ? h('span', { class: 'meta' }, meta) : null));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function RowLink({ code, title, sub, meta, href = '#', key, target }) {
|
|
33
|
+
return Row({ code, title, sub, meta, href, kind: 'link', key, target });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Section({ title, eyebrow, children }) {
|
|
37
|
+
return h('section', { class: 'ds-section' },
|
|
38
|
+
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
39
|
+
title ? h('h3', {}, title) : null,
|
|
40
|
+
...(Array.isArray(children) ? children : [children])
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Hero({ eyebrow, title, body, accent, badge, badgeCount, actions }) {
|
|
45
|
+
return h('div', { class: 'ds-hero' },
|
|
46
|
+
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
47
|
+
h('h1', { class: 'ds-hero-title' }, title),
|
|
48
|
+
body ? h('p', { class: 'ds-hero-body' },
|
|
49
|
+
body,
|
|
50
|
+
accent ? h('span', { class: 'ds-hero-accent' }, ' ' + accent) : null
|
|
51
|
+
) : null,
|
|
52
|
+
actions ? h('div', { class: 'ds-hero-actions', style: 'display:flex;gap:10px;flex-wrap:wrap;margin-top:8px' }, ...(Array.isArray(actions) ? actions : [actions])) : null,
|
|
53
|
+
badge ? Panel({ title: badge, count: badgeCount, kind: 'inline', children: [] }) : null
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function Install({ cmd, copied, onCopy }) {
|
|
58
|
+
return h('div', { class: 'cli' },
|
|
59
|
+
h('span', { class: 'prompt' }, '$'),
|
|
60
|
+
h('span', { class: 'cmd' }, cmd),
|
|
61
|
+
h('span', { class: 'copy', onclick: () => onCopy && onCopy(cmd) }, copied ? 'copied' : 'copy')
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Receipt({ rows = [] }) {
|
|
66
|
+
return h('table', { class: 'kv' },
|
|
67
|
+
h('tbody', {}, ...rows.map(([k, v], i) =>
|
|
68
|
+
h('tr', { key: i }, h('td', {}, k), h('td', {}, v))
|
|
69
|
+
))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function Changelog({ entries = [] }) {
|
|
74
|
+
return Panel({
|
|
75
|
+
kind: 'wide',
|
|
76
|
+
children: entries.map((e, i) =>
|
|
77
|
+
h('div', { key: i, class: 'row ds-changelog-row' },
|
|
78
|
+
h('span', { class: 'code' }, e.date),
|
|
79
|
+
h('span', { class: 'ds-changelog-ver' }, e.ver),
|
|
80
|
+
h('span', { class: 'title' }, e.msg)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function WorksList({ works = [], openedIndex = -1, onToggle }) {
|
|
87
|
+
return Panel({
|
|
88
|
+
children: works.map((w, i) => {
|
|
89
|
+
const isOpen = openedIndex === i;
|
|
90
|
+
return h('div', { key: i },
|
|
91
|
+
Row({
|
|
92
|
+
code: w.code,
|
|
93
|
+
title: w.title, sub: w.sub,
|
|
94
|
+
meta: w.meta + ' ' + (isOpen ? '−' : '+'),
|
|
95
|
+
active: isOpen,
|
|
96
|
+
onClick: () => onToggle && onToggle(isOpen ? -1 : i)
|
|
97
|
+
}),
|
|
98
|
+
isOpen ? h('div', { class: 'work-detail', 'data-work-index': String(i) },
|
|
99
|
+
h('div', { class: 'ds-prose' },
|
|
100
|
+
h('p', { class: 'ds-work-body' }, w.body)
|
|
101
|
+
),
|
|
102
|
+
h('div', { class: 'ds-work-actions' },
|
|
103
|
+
Btn({ primary: true, href: w.href || '#', children: 'open ↗' }),
|
|
104
|
+
Btn({ href: w.source || '#', children: 'source' })
|
|
105
|
+
)
|
|
106
|
+
) : null
|
|
107
|
+
);
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function WritingList({ posts = [] }) {
|
|
113
|
+
return Panel({
|
|
114
|
+
children: posts.map((p, i) =>
|
|
115
|
+
RowLink({ key: i, code: p.date, title: p.title, meta: p.tag, href: p.href || '#' })
|
|
116
|
+
)
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function Manifesto({ paragraphs = [], maxWidth }) {
|
|
121
|
+
return h('div', {
|
|
122
|
+
class: 'ds-prose ds-manifesto',
|
|
123
|
+
'data-max-width': maxWidth ? String(maxWidth) : null
|
|
124
|
+
},
|
|
125
|
+
...paragraphs.map((p, i) => h('p', {
|
|
126
|
+
key: i,
|
|
127
|
+
class: 'ds-manifesto-para' + (p.dim ? ' dim' : '')
|
|
128
|
+
}, p.text || p))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function Kpi({ items = [] }) {
|
|
133
|
+
return h('div', { class: 'kpi' }, ...items.map(([n, l], i) =>
|
|
134
|
+
h('div', { key: i, class: 'kpi-card' },
|
|
135
|
+
h('div', { class: 'num' }, String(n)),
|
|
136
|
+
h('div', { class: 'lbl' }, l))));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function Table({ headers = [], rows = [], onRowClick, emptyText = 'nothing here yet' }) {
|
|
140
|
+
if (!rows || rows.length === 0) return h('div', { class: 'empty' }, emptyText);
|
|
141
|
+
return h('table', {},
|
|
142
|
+
h('thead', {}, h('tr', {}, ...headers.map((hd, i) => h('th', { key: i }, hd)))),
|
|
143
|
+
h('tbody', {}, ...rows.map((row, i) => h('tr', {
|
|
144
|
+
key: i,
|
|
145
|
+
class: onRowClick ? 'clickable' : '',
|
|
146
|
+
onclick: onRowClick ? () => onRowClick(i) : null
|
|
147
|
+
}, ...row.map((c, j) => h('td', { key: j }, c == null ? '' : (typeof c === 'object' ? c : String(c))))))));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function HomeView({ state = {}, onNav, onToggleWork, works = [], posts = [], manifesto = [], currentlyShipping } = {}) {
|
|
151
|
+
return [
|
|
152
|
+
Hero({
|
|
153
|
+
eyebrow: 'an entrypoint',
|
|
154
|
+
title: 'Small, weird, useful tools — built in public.',
|
|
155
|
+
body: '247420 is a creative collective of eight, scattered across three timezones. We have been shipping open-source tools for the web since 2018.',
|
|
156
|
+
accent: 'Some become the future. Most don\'t. That\'s the deal.'
|
|
157
|
+
}),
|
|
158
|
+
currentlyShipping ? Section({
|
|
159
|
+
eyebrow: 'currently shipping',
|
|
160
|
+
children: Panel({
|
|
161
|
+
kind: 'wide',
|
|
162
|
+
children: currentlyShipping.map((row, i) =>
|
|
163
|
+
Row({
|
|
164
|
+
key: i,
|
|
165
|
+
code: Dot({ tone: row.live ? 'live' : 'idle' }),
|
|
166
|
+
title: row.title, sub: row.sub, meta: row.meta
|
|
167
|
+
})
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
}) : null,
|
|
171
|
+
works.length ? Section({
|
|
172
|
+
eyebrow: 'works', title: 'Everything else.',
|
|
173
|
+
children: WorksList({ works, openedIndex: state.opened ?? -1, onToggle: onToggleWork })
|
|
174
|
+
}) : null,
|
|
175
|
+
posts.length ? Section({
|
|
176
|
+
eyebrow: 'writing', title: 'When we have something to say.',
|
|
177
|
+
children: WritingList({ posts })
|
|
178
|
+
}) : null,
|
|
179
|
+
manifesto.length ? Section({
|
|
180
|
+
eyebrow: 'who\'s here', title: 'Eight people, three timezones, one ongoing conversation.',
|
|
181
|
+
children: Manifesto({ paragraphs: manifesto })
|
|
182
|
+
}) : null
|
|
183
|
+
].filter(Boolean);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function ProjectView({ project = {}, copied, onCopy } = {}) {
|
|
187
|
+
return [
|
|
188
|
+
h('div', { class: 'ds-prose' },
|
|
189
|
+
Heading({ level: 1, children: project.name }),
|
|
190
|
+
Lede({ children: project.tagline })
|
|
191
|
+
),
|
|
192
|
+
project.install ? [
|
|
193
|
+
Heading({ level: 3, children: 'install' }),
|
|
194
|
+
Install({ cmd: project.install, copied, onCopy }),
|
|
195
|
+
] : null,
|
|
196
|
+
project.receipt ? [
|
|
197
|
+
Heading({ level: 3, children: 'by the numbers' }),
|
|
198
|
+
Receipt({ rows: project.receipt }),
|
|
199
|
+
] : null,
|
|
200
|
+
project.changelog ? [
|
|
201
|
+
Heading({ level: 3, children: 'recent releases' }),
|
|
202
|
+
Changelog({ entries: project.changelog })
|
|
203
|
+
] : null
|
|
204
|
+
].filter(Boolean).flat();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function Form({ fields = [], submit = 'submit', onSubmit }) {
|
|
208
|
+
return h('form', { class: 'row-form', onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
|
|
209
|
+
...fields.map((f, i) => f.kind === 'textarea'
|
|
210
|
+
? h('textarea', { key: i, name: f.name, placeholder: f.placeholder || '', rows: f.rows || 4 })
|
|
211
|
+
: h('input', { key: i, name: f.name, type: f.type || 'text', placeholder: f.placeholder || '', value: f.value || '', required: f.required ? 'true' : null })),
|
|
212
|
+
h('button', { type: 'submit', class: 'btn-primary' }, submit));
|
|
213
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// File modals — matches upstream signatures + class names.
|
|
2
|
+
|
|
3
|
+
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
4
|
+
import { Btn } from './shell.js';
|
|
5
|
+
import { fileGlyph, fmtFileSize } from './files.js';
|
|
6
|
+
const h = webjsx.createElement;
|
|
7
|
+
|
|
8
|
+
function Backdrop({ onClose, children, kind = '' } = {}) {
|
|
9
|
+
return h('div', {
|
|
10
|
+
class: 'ds-modal-backdrop',
|
|
11
|
+
onclick: (e) => { if (e.target === e.currentTarget && onClose) onClose(); }
|
|
12
|
+
},
|
|
13
|
+
h('div', { class: 'ds-modal' + (kind ? ' ds-modal-' + kind : '') }, ...(Array.isArray(children) ? children : [children]))
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ConfirmDialog({ title = 'confirm', message, confirmLabel = 'confirm', cancelLabel = 'cancel', destructive, onConfirm, onCancel } = {}) {
|
|
18
|
+
return Backdrop({
|
|
19
|
+
onClose: onCancel,
|
|
20
|
+
kind: 'small',
|
|
21
|
+
children: [
|
|
22
|
+
h('div', { class: 'ds-modal-head' }, title),
|
|
23
|
+
h('div', { class: 'ds-modal-body' }, message || ''),
|
|
24
|
+
h('div', { class: 'ds-modal-actions' },
|
|
25
|
+
Btn({ onClick: onCancel, children: cancelLabel }),
|
|
26
|
+
h('button', {
|
|
27
|
+
class: destructive ? 'btn-primary danger' : 'btn-primary',
|
|
28
|
+
onclick: onConfirm
|
|
29
|
+
}, confirmLabel)
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PromptDialog({ title = 'name', value = '', placeholder = '', confirmLabel = 'ok', cancelLabel = 'cancel', onConfirm, onCancel, onInput } = {}) {
|
|
36
|
+
return Backdrop({
|
|
37
|
+
onClose: onCancel,
|
|
38
|
+
kind: 'small',
|
|
39
|
+
children: [
|
|
40
|
+
h('div', { class: 'ds-modal-head' }, title),
|
|
41
|
+
h('div', { class: 'ds-modal-body' },
|
|
42
|
+
h('input', {
|
|
43
|
+
class: 'input ds-modal-input',
|
|
44
|
+
type: 'text',
|
|
45
|
+
value,
|
|
46
|
+
placeholder,
|
|
47
|
+
autofocus: true,
|
|
48
|
+
oninput: (e) => onInput && onInput(e.target.value),
|
|
49
|
+
onkeydown: (e) => {
|
|
50
|
+
if (e.key === 'Enter') { e.preventDefault(); onConfirm && onConfirm(e.target.value); }
|
|
51
|
+
if (e.key === 'Escape') { e.preventDefault(); onCancel && onCancel(); }
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
),
|
|
55
|
+
h('div', { class: 'ds-modal-actions' },
|
|
56
|
+
Btn({ onClick: onCancel, children: cancelLabel }),
|
|
57
|
+
h('button', { class: 'btn-primary', onclick: () => onConfirm && onConfirm(value) }, confirmLabel)
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function FilePreviewMedia({ src, type = 'other', name } = {}) {
|
|
64
|
+
if (type === 'image') return h('img', { class: 'ds-preview-media', src, alt: name || '' });
|
|
65
|
+
if (type === 'video') return h('video', { class: 'ds-preview-media', src, controls: true });
|
|
66
|
+
if (type === 'audio') return h('audio', { class: 'ds-preview-audio', src, controls: true });
|
|
67
|
+
return h('div', { class: 'ds-preview-fallback' },
|
|
68
|
+
h('span', { class: 'ds-preview-glyph' }, fileGlyph(type)),
|
|
69
|
+
h('span', {}, 'no inline preview for ' + (type || 'this file'))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function FilePreviewCode({ content = '', lang } = {}) {
|
|
74
|
+
return h('pre', { class: 'ds-preview-code' + (lang ? ' lang-' + lang : '') },
|
|
75
|
+
h('code', { class: lang ? 'language-' + lang : '' }, content)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function FilePreviewText({ content = '', truncated } = {}) {
|
|
80
|
+
return h('pre', { class: 'ds-preview-text' },
|
|
81
|
+
h('code', {}, content),
|
|
82
|
+
truncated ? h('div', { class: 'ds-preview-truncated' }, '… (truncated)') : null
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function FileViewer({ file, body, onClose, onAction } = {}) {
|
|
87
|
+
if (!file) return null;
|
|
88
|
+
const meta = [file.type, file.size != null ? fmtFileSize(file.size) : null, file.modified || null]
|
|
89
|
+
.filter(Boolean).join(' · ');
|
|
90
|
+
return Backdrop({
|
|
91
|
+
onClose,
|
|
92
|
+
kind: 'preview',
|
|
93
|
+
children: [
|
|
94
|
+
h('div', { class: 'ds-modal-head ds-preview-head', 'data-file-type': file.type || 'other' },
|
|
95
|
+
h('span', { class: 'ds-preview-name' }, file.name || ''),
|
|
96
|
+
h('span', { class: 'ds-preview-meta' }, meta),
|
|
97
|
+
h('span', { class: 'ds-preview-actions' },
|
|
98
|
+
onAction ? h('button', { class: 'ds-file-act', title: 'download', onclick: () => onAction('download') }, '↓') : null,
|
|
99
|
+
h('button', { class: 'ds-file-act', title: 'close', onclick: onClose }, '✕')
|
|
100
|
+
)
|
|
101
|
+
),
|
|
102
|
+
h('div', { class: 'ds-preview-body', 'data-file-type': file.type || 'other' },
|
|
103
|
+
...(Array.isArray(body) ? body : [body])
|
|
104
|
+
)
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// File primitives — matches upstream signatures.
|
|
2
|
+
|
|
3
|
+
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
4
|
+
import { Btn } from './shell.js';
|
|
5
|
+
const h = webjsx.createElement;
|
|
6
|
+
|
|
7
|
+
const FILE_TYPES = ['dir', 'image', 'video', 'audio', 'code', 'text', 'archive', 'document', 'symlink', 'other'];
|
|
8
|
+
const TYPE_GLYPH = {
|
|
9
|
+
dir: '▣', image: '◰', video: '▰', audio: '◎', code: '⌘',
|
|
10
|
+
text: '§', archive: '◐', document: '▢', symlink: '↗', other: '◌'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function fileGlyph(type) {
|
|
14
|
+
return TYPE_GLYPH[type] || TYPE_GLYPH.other;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function fmtFileSize(bytes) {
|
|
18
|
+
if (bytes == null || bytes === 0) return '—';
|
|
19
|
+
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
20
|
+
let i = 0, n = bytes;
|
|
21
|
+
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
|
22
|
+
return n.toFixed(i === 0 ? 0 : 1) + ' ' + u[i];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function FileIcon({ type = 'other' } = {}) {
|
|
26
|
+
return h('span', { class: 'ds-file-icon', 'data-file-type': type }, fileGlyph(type));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function FileRow({ name, type = 'other', size, modified, code, onOpen, onAction, active, key } = {}) {
|
|
30
|
+
const meta = [type === 'dir' ? null : fmtFileSize(size), modified || null].filter(Boolean).join(' · ');
|
|
31
|
+
return h('div', {
|
|
32
|
+
key,
|
|
33
|
+
class: 'ds-file-row row' + (active ? ' active' : ''),
|
|
34
|
+
'data-file-type': type,
|
|
35
|
+
onclick: onOpen
|
|
36
|
+
},
|
|
37
|
+
code != null ? h('span', { class: 'code' }, code) : null,
|
|
38
|
+
FileIcon({ type }),
|
|
39
|
+
h('span', { class: 'title' }, name),
|
|
40
|
+
h('span', { class: 'ds-file-meta meta' }, meta || '—'),
|
|
41
|
+
onAction ? h('span', { class: 'ds-file-actions', onclick: (e) => e.stopPropagation() },
|
|
42
|
+
h('button', { class: 'ds-file-act', title: 'download', onclick: () => onAction('download') }, '↓'),
|
|
43
|
+
h('button', { class: 'ds-file-act', title: 'rename', onclick: () => onAction('rename') }, '✎'),
|
|
44
|
+
h('button', { class: 'ds-file-act ds-file-act-warn', title: 'delete', onclick: () => onAction('delete') }, '✕')
|
|
45
|
+
) : null
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function FileGrid({ files = [], onOpen, onAction, emptyText = 'no files here yet' } = {}) {
|
|
50
|
+
if (!files.length) return EmptyState({ text: emptyText });
|
|
51
|
+
return h('div', { class: 'ds-file-grid' },
|
|
52
|
+
...files.map((f, i) => FileRow({
|
|
53
|
+
key: f.path || f.name + i,
|
|
54
|
+
name: f.name, type: f.type, size: f.size, modified: f.modified, code: f.code, active: f.active,
|
|
55
|
+
onOpen: onOpen ? () => onOpen(f) : null,
|
|
56
|
+
onAction: onAction ? (act) => onAction(act, f) : null
|
|
57
|
+
}))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function FileToolbar({ left = [], right = [] } = {}) {
|
|
62
|
+
return h('div', { class: 'ds-file-toolbar' },
|
|
63
|
+
h('div', { class: 'ds-file-toolbar-left' }, ...left),
|
|
64
|
+
h('div', { class: 'ds-file-toolbar-right' }, ...right)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function DropZone({ children, dragover, onDrop, onDragOver, onDragLeave, label = 'drop files here', onPick } = {}) {
|
|
69
|
+
return h('div', {
|
|
70
|
+
class: 'ds-dropzone' + (dragover ? ' dragover' : ''),
|
|
71
|
+
ondragover: (e) => { e.preventDefault(); onDragOver && onDragOver(e); },
|
|
72
|
+
ondragleave: (e) => { onDragLeave && onDragLeave(e); },
|
|
73
|
+
ondrop: (e) => { e.preventDefault(); onDrop && onDrop(e.dataTransfer.files); }
|
|
74
|
+
},
|
|
75
|
+
h('div', { class: 'ds-dropzone-inner' },
|
|
76
|
+
h('span', { class: 'ds-dropzone-glyph' }, '⇪'),
|
|
77
|
+
h('span', { class: 'ds-dropzone-label' }, label),
|
|
78
|
+
onPick ? Btn({ onClick: onPick, children: 'pick files' }) : null
|
|
79
|
+
),
|
|
80
|
+
...(Array.isArray(children) ? children : children ? [children] : [])
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function UploadProgress({ items = [] } = {}) {
|
|
85
|
+
if (!items.length) return null;
|
|
86
|
+
return h('div', { class: 'ds-upload-progress' },
|
|
87
|
+
...items.map((it, i) => h('div', {
|
|
88
|
+
key: it.name + i,
|
|
89
|
+
class: 'ds-upload-item' + (it.done ? ' done' : '') + (it.error ? ' error' : '')
|
|
90
|
+
},
|
|
91
|
+
h('span', { class: 'ds-upload-name' }, it.name),
|
|
92
|
+
h('span', { class: 'ds-upload-bar' },
|
|
93
|
+
h('span', { class: 'ds-upload-fill', 'data-pct': String(Math.max(0, Math.min(100, it.pct || 0))) })
|
|
94
|
+
),
|
|
95
|
+
h('span', { class: 'ds-upload-pct' }, (it.error ? 'err' : (it.done ? 'ok' : (it.pct || 0) + '%')))
|
|
96
|
+
))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function EmptyState({ text = 'nothing here', glyph = '◌' } = {}) {
|
|
101
|
+
return h('div', { class: 'ds-file-empty' },
|
|
102
|
+
h('span', { class: 'ds-file-empty-glyph' }, glyph),
|
|
103
|
+
h('span', { class: 'ds-file-empty-text' }, text)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function BreadcrumbPath({ segments = [], onNav, root = 'root' } = {}) {
|
|
108
|
+
const parts = [h('button', { key: 'root', class: 'ds-crumb-seg', onclick: () => onNav && onNav(0) }, root)];
|
|
109
|
+
segments.forEach((seg, i) => {
|
|
110
|
+
parts.push(h('span', { key: 'sep' + i, class: 'ds-crumb-sep' }, '›'));
|
|
111
|
+
parts.push(h('button', {
|
|
112
|
+
key: 'seg' + i,
|
|
113
|
+
class: 'ds-crumb-seg' + (i === segments.length - 1 ? ' leaf' : ''),
|
|
114
|
+
onclick: () => onNav && onNav(i + 1)
|
|
115
|
+
}, seg));
|
|
116
|
+
});
|
|
117
|
+
return h('div', { class: 'ds-crumb-path' }, ...parts);
|
|
118
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Freddie helpers — minimal stubs. Real freddie surfaces ship downstream
|
|
2
|
+
// (gm-cc, foph, hermes-fork). The SDK exposes the registry shape and
|
|
3
|
+
// localStorage helpers so consumer pages can wire to existing patterns.
|
|
4
|
+
|
|
5
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
6
|
+
const h = webjsx.createElement;
|
|
7
|
+
|
|
8
|
+
export function renderPageStub({ id, title }) {
|
|
9
|
+
return h('div', { class: 'ds-freddie-stub' },
|
|
10
|
+
h('span', { class: 'eyebrow' }, 'freddie · ' + id),
|
|
11
|
+
h('h2', {}, title || id),
|
|
12
|
+
h('p', { class: 'dim' }, 'this page renderer is a stub. consumers override it on their own freddie router.')
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SKILL_LABELS = {
|
|
17
|
+
transcribe: 'transcribe',
|
|
18
|
+
summarize: 'summarize',
|
|
19
|
+
translate: 'translate',
|
|
20
|
+
extract: 'extract',
|
|
21
|
+
classify: 'classify',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function skillLabel(slug) {
|
|
25
|
+
return SKILL_LABELS[slug] || slug;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const RECENT_KEY = 'ds247420.recent.paths';
|
|
29
|
+
|
|
30
|
+
export function getRecentPaths() {
|
|
31
|
+
if (typeof localStorage === 'undefined') return [];
|
|
32
|
+
try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); }
|
|
33
|
+
catch { return []; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveRecentPath(path) {
|
|
37
|
+
if (typeof localStorage === 'undefined' || !path) return;
|
|
38
|
+
const list = getRecentPaths().filter(p => p !== path);
|
|
39
|
+
list.unshift(path);
|
|
40
|
+
try { localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, 10))); } catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper used by consumers that want to render chat-message arrays with
|
|
44
|
+
// our ChatMessage factory but pre-formatted for freddie's data shape.
|
|
45
|
+
export function renderChatMessages(messages = [], opts = {}) {
|
|
46
|
+
// Import lazily to keep this module light.
|
|
47
|
+
return import('../chat.js').then(({ ChatMessage }) =>
|
|
48
|
+
messages.map((m, i) => ChatMessage({ ...m, key: m.key != null ? m.key : i, ...opts }))
|
|
49
|
+
);
|
|
50
|
+
}
|