anentrypoint-design 0.0.385 → 0.0.387
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 +33 -0
- package/dist/247420.js +53 -51
- package/package.json +1 -1
- package/src/components/agent-chat/controls.js +143 -0
- package/src/components/agent-chat/empty-state.js +63 -0
- package/src/components/agent-chat/message-rows.js +141 -0
- package/src/components/agent-chat/surface.js +206 -0
- package/src/components/agent-chat/thread-behaviour.js +50 -0
- package/src/components/agent-chat.js +6 -546
- package/src/components/chat/composer-affordances.js +109 -0
- package/src/components/chat/composer.js +256 -0
- package/src/components/chat/threads.js +105 -0
- package/src/components/chat-message-parts/agent-nodes.js +103 -0
- package/src/components/chat-message-parts/inline.js +106 -0
- package/src/components/chat-message-parts/prose-nodes.js +133 -0
- package/src/components/chat-message-parts/renderers.js +89 -0
- package/src/components/chat-message-parts.js +12 -408
- package/src/components/chat-minimap/minimap.js +167 -0
- package/src/components/chat-minimap/paint.js +73 -0
- package/src/components/chat-minimap/preview.js +41 -0
- package/src/components/chat-minimap.js +7 -263
- package/src/components/chat.js +20 -628
- package/src/components/community/chrome.js +63 -0
- package/src/components/community/navigation.js +167 -0
- package/src/components/community/presence.js +106 -0
- package/src/components/community/shell.js +17 -0
- package/src/components/community/views.js +131 -0
- package/src/components/community.js +18 -447
- package/src/components/files/chrome.js +140 -0
- package/src/components/files/entries.js +156 -0
- package/src/components/files/grid-controls.js +63 -0
- package/src/components/files/grid.js +177 -0
- package/src/components/files/types.js +69 -0
- package/src/components/files-modals/dialogs.js +118 -0
- package/src/components/files-modals/modal-shell.js +153 -0
- package/src/components/files-modals/preview-bodies.js +129 -0
- package/src/components/files-modals/preview-containers.js +96 -0
- package/src/components/files-modals.js +14 -475
- package/src/components/files.js +15 -564
- package/src/components/freddie/pages-config.js +3 -94
- package/src/components/freddie/pages-models.js +97 -0
- package/src/components/freddie.js +2 -1
- package/src/components/interaction-primitives/mobile.js +25 -0
- package/src/components/interaction-primitives/pointer.js +214 -0
- package/src/components/interaction-primitives/reorderable.js +47 -0
- package/src/components/interaction-primitives/shortcuts.js +119 -0
- package/src/components/interaction-primitives.js +16 -388
- package/src/components/sessions/conversation-list.js +230 -0
- package/src/components/sessions/dashboard.js +202 -0
- package/src/components/sessions/detail-bits.js +49 -0
- package/src/components/sessions/format.js +42 -0
- package/src/components/sessions/session-card.js +92 -0
- package/src/components/sessions.js +16 -579
- package/src/components/voice/audio-cue.js +39 -0
- package/src/components/voice/capture.js +90 -0
- package/src/components/voice/playback.js +75 -0
- package/src/components/voice/settings-modal.js +104 -0
- package/src/components/voice.js +16 -293
- package/src/css/app-shell/base.css +23 -0
- package/src/css/app-shell/states-interactions.css +10 -0
- package/src/kits/os/freddie/chat-protocol.js +32 -0
- package/src/kits/os/freddie/chat-transport.js +178 -0
- package/src/kits/os/freddie/pages-chat.js +10 -180
- package/src/kits/os/shell-chrome.js +163 -0
- package/src/kits/os/shell-geometry.js +59 -0
- package/src/kits/os/shell.js +178 -335
- package/src/page-html/client-script.js +151 -0
- package/src/page-html/head-tags.js +59 -0
- package/src/page-html/markdown.js +68 -0
- package/src/page-html/page-styles.js +44 -0
- package/src/page-html.js +14 -291
|
@@ -1,448 +1,19 @@
|
|
|
1
1
|
// Community surface — matches upstream signatures.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
tabindex: '0',
|
|
21
|
-
'data-id': id,
|
|
22
|
-
onkeydown: (e) => {
|
|
23
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
24
|
-
e.preventDefault();
|
|
25
|
-
onClick && onClick(e);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
h('span', { class: 'cm-server-pill' }),
|
|
30
|
-
icon ? h('img', { src: icon, alt: name }) : Avatar({ name, shape: 'square', initialsCount: 2 }),
|
|
31
|
-
badge ? h('span', { class: 'cm-server-badge' }, badge > 99 ? '99+' : String(badge)) : null
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function ServerRail({ servers = [], activeId, onSelect, onAdd } = {}) {
|
|
36
|
-
return h('div', { class: 'cm-server-rail', role: 'navigation', 'aria-label': 'servers' },
|
|
37
|
-
h('a', { class: 'cm-server-back', href: '../', title: 'Back', 'aria-label': 'back' }, Icon('chevron-left')),
|
|
38
|
-
h('div', { class: 'cm-server-sep', 'aria-hidden': 'true' }),
|
|
39
|
-
...servers.map(s => ServerIcon({ ...s, active: s.id === activeId, onClick: () => onSelect && onSelect(s.id) })),
|
|
40
|
-
onAdd ? h('button', { class: 'cm-server-add', type: 'button', onclick: onAdd, title: 'Add server', 'aria-label': 'add server' }, '+') : null
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function ChannelItem({ id, name, type = 'text', active, voiceActive, voiceConnecting, badge, draggable, actions = [], participants = [], onClick, onContext } = {}) {
|
|
45
|
-
const ICON_FOR = { voice: 'speaker', forum: 'forum', threaded: 'thread', announcement: 'megaphone', page: 'page', thread: 'thread', text: 'hash' };
|
|
46
|
-
const icon = Icon(ICON_FOR[type] || 'hash', { size: 15 });
|
|
47
|
-
const handleActionClick = (a, e) => { e.stopPropagation(); a.onClick && a.onClick(id, e); };
|
|
48
|
-
return h('div', { class: 'cm-channel-item-wrap', 'data-channel-wrap': id },
|
|
49
|
-
h('div', {
|
|
50
|
-
class: 'cm-channel-item' + (active ? ' active' : '') + (voiceActive ? ' voice-active' : '') + (voiceConnecting ? ' voice-connecting' : ''),
|
|
51
|
-
'data-id': id,
|
|
52
|
-
'data-type': type,
|
|
53
|
-
draggable: draggable ? 'true' : null,
|
|
54
|
-
onclick: onClick,
|
|
55
|
-
oncontextmenu: (e) => { e.preventDefault(); onContext && onContext(id, e.clientX, e.clientY); },
|
|
56
|
-
onkeydown: (e) => {
|
|
57
|
-
if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {
|
|
58
|
-
e.preventDefault();
|
|
59
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
60
|
-
onContext && onContext(id, rect.left, rect.top + rect.height);
|
|
61
|
-
}
|
|
62
|
-
if (draggable) {
|
|
63
|
-
if (e.ctrlKey && e.key === 'ArrowUp') {
|
|
64
|
-
e.preventDefault();
|
|
65
|
-
window.dispatchEvent(new CustomEvent('reorder', { detail: { id, direction: 'up' } }));
|
|
66
|
-
}
|
|
67
|
-
if (e.ctrlKey && e.key === 'ArrowDown') {
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
window.dispatchEvent(new CustomEvent('reorder', { detail: { id, direction: 'down' } }));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
tabindex: '0',
|
|
74
|
-
role: 'option'
|
|
75
|
-
},
|
|
76
|
-
h('span', { class: 'cm-ch-icon' + (voiceActive ? ' voice-active-badge' : ''), 'data-voice-active': voiceActive ? 'true' : null }, icon),
|
|
77
|
-
voiceConnecting ? h('span', { class: 'cm-ch-spinner', title: 'Connecting…', 'aria-label': 'Connecting to voice channel…' }) : voiceActive ? h('span', { class: 'cm-ch-voice-badge', title: 'Voice active', 'aria-label': 'Voice channel active' }) : null,
|
|
78
|
-
h('span', { class: 'cm-ch-name' }, name),
|
|
79
|
-
badge ? h('span', { class: 'cm-ch-badge' }, badge > 99 ? '99+' : String(badge)) : null,
|
|
80
|
-
actions.length ? h('div', { class: 'cm-ch-actions' },
|
|
81
|
-
...actions.map(a => h('button', {
|
|
82
|
-
class: 'cm-ch-action-btn',
|
|
83
|
-
title: a.title || '',
|
|
84
|
-
'data-action': a.id || '',
|
|
85
|
-
onclick: (e) => handleActionClick(a, e)
|
|
86
|
-
}, a.icon || a.label || 'more'))
|
|
87
|
-
) : null
|
|
88
|
-
),
|
|
89
|
-
voiceActive && participants.length ? h('div', { class: 'cm-ch-voice-users' },
|
|
90
|
-
...participants.map(p => h('div', { class: 'cm-ch-voice-user' + (p.speaking ? ' speaking' : '') },
|
|
91
|
-
h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? `--avatar-bg:${p.color}` : null }, avatarInitial(p.identity)),
|
|
92
|
-
h('span', { class: 'cm-ch-voice-user-name' }, p.identity)
|
|
93
|
-
))
|
|
94
|
-
) : null
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function ChannelCategory({ id, name, channels = [], collapsed, activeId, onToggle, onAddChannel, onChannelClick, onChannelContext, onContextMenu, extraButton, channelDraggable } = {}) {
|
|
99
|
-
return h('div', { class: 'cm-channel-category', 'data-category': id },
|
|
100
|
-
h('div', {
|
|
101
|
-
class: 'cm-category-header' + (collapsed ? ' collapsed' : ''),
|
|
102
|
-
onclick: () => onToggle && onToggle(id),
|
|
103
|
-
oncontextmenu: onContextMenu ? (e) => { e.preventDefault(); onContextMenu(id, e.clientX, e.clientY); } : null
|
|
104
|
-
},
|
|
105
|
-
h('span', { class: 'cm-cat-arrow' }, Icon('chevron-down')),
|
|
106
|
-
h('span', { class: 'cm-cat-name' }, name),
|
|
107
|
-
extraButton ? h('button', { class: 'cm-cat-extra', onclick: (e) => { e.stopPropagation(); extraButton.onClick && extraButton.onClick(id, e); }, 'aria-label': extraButton.title || 'Category action' }, extraButton.icon || extraButton.label || '+') : null,
|
|
108
|
-
onAddChannel ? h('button', { class: 'cm-cat-add', onclick: (e) => { e.stopPropagation(); onAddChannel(id); }, 'aria-label': 'Add channel to ' + name }, '+') : null
|
|
109
|
-
),
|
|
110
|
-
collapsed ? null : h('div', { class: 'cm-cat-channels' },
|
|
111
|
-
...channels.map(c => ChannelItem({
|
|
112
|
-
...c,
|
|
113
|
-
draggable: channelDraggable,
|
|
114
|
-
active: c.id === activeId,
|
|
115
|
-
onClick: () => onChannelClick && onChannelClick(c),
|
|
116
|
-
onContext: onChannelContext
|
|
117
|
-
}))
|
|
118
|
-
)
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function VoiceUser({ identity, speaking, color } = {}) {
|
|
123
|
-
const initial = avatarInitial(identity);
|
|
124
|
-
return h('div', { class: 'cm-voice-user' + (speaking ? ' speaking' : '') },
|
|
125
|
-
h('div', { class: 'cm-voice-user-avatar', style: color ? `--avatar-bg:${color}` : null }, initial),
|
|
126
|
-
h('span', { class: 'cm-voice-user-name' }, identity)
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function UserPanel({ name, tag, color, muted, deafened, onMute, onDeafen, onSettings } = {}) {
|
|
131
|
-
const initial = avatarInitial(name);
|
|
132
|
-
const handleSettings = (e) => {
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
if (onSettings) {
|
|
135
|
-
// onSettings callback should open a drawer/modal with quick toggles
|
|
136
|
-
onSettings({
|
|
137
|
-
audioDevice: null, // controlled by consumer
|
|
138
|
-
micOn: !muted,
|
|
139
|
-
speakerOn: !deafened,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
return h('div', { class: 'cm-user-panel' },
|
|
144
|
-
h('div', { class: 'cm-user-avatar', style: color ? `--avatar-bg:${color}` : null },
|
|
145
|
-
h('span', { class: 'cm-user-status-dot' }),
|
|
146
|
-
initial
|
|
147
|
-
),
|
|
148
|
-
h('div', { class: 'cm-user-info' },
|
|
149
|
-
h('div', { class: 'cm-user-name' }, name || 'You'),
|
|
150
|
-
tag ? h('div', { class: 'cm-user-tag' }, tag) : null
|
|
151
|
-
),
|
|
152
|
-
h('div', { class: 'cm-user-controls' },
|
|
153
|
-
h('button', { class: 'cm-user-btn' + (muted ? ' muted' : ''), onclick: onMute, 'aria-label': muted ? 'Unmute microphone' : 'Mute microphone', 'aria-pressed': muted ? 'true' : 'false' }, Icon(muted ? 'mic-off' : 'mic')),
|
|
154
|
-
h('button', { class: 'cm-user-btn' + (deafened ? ' deafened' : ''), onclick: onDeafen, 'aria-label': deafened ? 'Undeafen' : 'Deafen', 'aria-pressed': deafened ? 'true' : 'false' }, Icon(deafened ? 'speaker-off' : 'speaker')),
|
|
155
|
-
h('button', { class: 'cm-user-btn', onclick: handleSettings, 'aria-label': 'Audio settings', title: 'Open audio settings' }, Icon('settings'))
|
|
156
|
-
)
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Skeleton rows for a cold channel-list load — reuses the kit-wide .ds-skel
|
|
161
|
-
// shimmer primitive (sessions.js / files.js) rather than a bare spinner.
|
|
162
|
-
function ChannelListSkeleton({ rows = 6 } = {}) {
|
|
163
|
-
return h('div', { class: 'cm-channel-list cm-channel-skeleton', 'aria-hidden': 'true' },
|
|
164
|
-
...Array.from({ length: rows }, (_, i) => h('div', { key: 'csk' + i, class: 'cm-channel-item-skeleton' },
|
|
165
|
-
h('span', { class: 'ds-skel ds-skel-icon' }), h('span', { class: 'ds-skel ds-skel-title' }))));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function ChannelSidebar({ serverName, channels = [], categories = [], activeId, collapsedCats, onChannelClick, onCategoryToggle, onAddChannel, onChannelContext, userPanelProps, loading = false } = {}) {
|
|
169
|
-
const collapsed = collapsedCats || new Set();
|
|
170
|
-
const uncategorized = channels.filter(c => !c.categoryId || !categories.find(cat => cat.id === c.categoryId));
|
|
171
|
-
const sorted = [...categories].sort((a, b) => (a.position || 0) - (b.position || 0));
|
|
172
|
-
return h('div', { class: 'cm-channel-sidebar' },
|
|
173
|
-
h('div', { class: 'cm-server-header' },
|
|
174
|
-
h('span', { class: 'cm-server-header-name' }, serverName || 'Server'),
|
|
175
|
-
),
|
|
176
|
-
loading ? ChannelListSkeleton() : h('div', { class: 'cm-channel-list' },
|
|
177
|
-
(sorted.length === 0 && uncategorized.length === 0)
|
|
178
|
-
? h('div', { class: 'cm-channel-empty', role: 'status' },
|
|
179
|
-
Icon('hash', { size: 20 }),
|
|
180
|
-
h('span', { class: 'cm-channel-empty-text' }, 'no channels yet — add one to get this server started'))
|
|
181
|
-
: null,
|
|
182
|
-
...sorted.map(cat => ChannelCategory({
|
|
183
|
-
id: cat.id,
|
|
184
|
-
name: cat.name,
|
|
185
|
-
channels: channels.filter(c => c.categoryId === cat.id).sort((a, b) => (a.position || 0) - (b.position || 0)),
|
|
186
|
-
collapsed: collapsed.has && collapsed.has(cat.id),
|
|
187
|
-
activeId,
|
|
188
|
-
onToggle: onCategoryToggle,
|
|
189
|
-
onAddChannel,
|
|
190
|
-
onChannelClick,
|
|
191
|
-
onChannelContext
|
|
192
|
-
})),
|
|
193
|
-
uncategorized.length ? ChannelCategory({
|
|
194
|
-
id: 'uncategorized',
|
|
195
|
-
name: 'CHANNELS',
|
|
196
|
-
channels: uncategorized,
|
|
197
|
-
activeId,
|
|
198
|
-
onChannelClick,
|
|
199
|
-
onChannelContext
|
|
200
|
-
}) : null
|
|
201
|
-
),
|
|
202
|
-
userPanelProps ? UserPanel(userPanelProps) : null
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function MemberItem({ identity, name, color, status = 'online' } = {}) {
|
|
207
|
-
const initial = avatarInitial(name || identity);
|
|
208
|
-
return h('div', { class: 'cm-member-item' },
|
|
209
|
-
h('div', { class: 'cm-member-avatar', style: color ? `--avatar-bg:${color}` : null },
|
|
210
|
-
h('span', { class: 'cm-member-status' + (status === 'online' ? ' online' : '') }),
|
|
211
|
-
initial
|
|
212
|
-
),
|
|
213
|
-
h('span', { class: 'cm-member-name' }, name || identity)
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Skeleton rows for a cold member-list load, matching FileSkeleton/session
|
|
218
|
-
// skeleton shape (icon + title placeholder), never a bare spinner.
|
|
219
|
-
function MemberListSkeleton({ rows = 6 } = {}) {
|
|
220
|
-
return h('div', { class: 'cm-member-list cm-member-skeleton open', 'aria-hidden': 'true' },
|
|
221
|
-
...Array.from({ length: rows }, (_, i) => h('div', { key: 'msk' + i, class: 'cm-member-item-skeleton' },
|
|
222
|
-
h('span', { class: 'ds-skel ds-skel-icon' }), h('span', { class: 'ds-skel ds-skel-title' }))));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function MemberList({ categories = [], open, loading = false } = {}) {
|
|
226
|
-
if (loading) return MemberListSkeleton();
|
|
227
|
-
const total = categories.reduce((n, cat) => n + (cat.members ? cat.members.length : 0), 0);
|
|
228
|
-
return h('div', { class: 'cm-member-list' + (open ? ' open' : '') },
|
|
229
|
-
total === 0
|
|
230
|
-
? h('div', { key: '_empty', class: 'cm-member-empty', role: 'status' },
|
|
231
|
-
Icon('members', { size: 20 }),
|
|
232
|
-
h('span', { class: 'cm-member-empty-text' }, 'no members in this channel yet'))
|
|
233
|
-
: null,
|
|
234
|
-
...categories.flatMap(cat => [
|
|
235
|
-
h('div', { class: 'cm-member-category', key: cat.label }, `${cat.label} — ${cat.members.length}`),
|
|
236
|
-
...cat.members.map((m, i) => MemberItem({ ...m, key: m.identity || i }))
|
|
237
|
-
])
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function ChatHeader({ icon = '#', name, topic, toolbar = [] } = {}) {
|
|
242
|
-
return h('div', { class: 'cm-chat-header' },
|
|
243
|
-
h('span', { class: 'cm-chat-header-icon' }, icon),
|
|
244
|
-
h('span', { class: 'cm-chat-header-name' }, name),
|
|
245
|
-
topic ? h('span', { class: 'cm-chat-header-topic' }, topic) : null,
|
|
246
|
-
h('div', { class: 'cm-chat-header-toolbar' }, ...toolbar)
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function VoiceStrip({ channelName, status, muted, deafened, onMute, onDeafen, onLeave, open } = {}) {
|
|
251
|
-
return h('div', { class: 'cm-voice-strip' + (open ? ' open' : ''), role: 'region', 'aria-label': 'voice controls' },
|
|
252
|
-
h('div', { class: 'cm-vs-label' },
|
|
253
|
-
h('span', { class: 'cm-vs-channel' }, Icon('speaker'), ' ' + (channelName || 'voice')),
|
|
254
|
-
h('span', { class: 'cm-vs-status' }, status || 'connected')
|
|
255
|
-
),
|
|
256
|
-
h('button', {
|
|
257
|
-
class: 'cm-vs-btn', type: 'button', onclick: onMute,
|
|
258
|
-
title: muted ? 'Unmute' : 'Mute',
|
|
259
|
-
'aria-label': muted ? 'unmute microphone' : 'mute microphone',
|
|
260
|
-
'aria-pressed': muted ? 'true' : 'false'
|
|
261
|
-
}, Icon(muted ? 'mic-off' : 'mic')),
|
|
262
|
-
h('button', {
|
|
263
|
-
class: 'cm-vs-btn', type: 'button', onclick: onDeafen,
|
|
264
|
-
title: deafened ? 'Undeafen' : 'Deafen',
|
|
265
|
-
'aria-label': deafened ? 'undeafen' : 'deafen',
|
|
266
|
-
'aria-pressed': deafened ? 'true' : 'false'
|
|
267
|
-
}, Icon(deafened ? 'speaker-off' : 'speaker')),
|
|
268
|
-
h('button', {
|
|
269
|
-
class: 'cm-vs-btn danger', type: 'button', onclick: onLeave,
|
|
270
|
-
title: 'Leave voice', 'aria-label': 'leave voice channel'
|
|
271
|
-
}, Icon('x'))
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function MobileHeader({ title, channelType, channelName, onMenu, onMembers } = {}) {
|
|
276
|
-
const ICON_FOR = { voice: 'speaker', forum: 'forum', threaded: 'thread', announcement: 'megaphone', page: 'page', thread: 'thread', text: 'hash' };
|
|
277
|
-
const titleNode = channelType
|
|
278
|
-
? [Icon(ICON_FOR[channelType] || 'hash', { size: 16 }), ' ' + (channelName || '')]
|
|
279
|
-
: [title || ''];
|
|
280
|
-
return h('div', { class: 'cm-mobile-header', role: 'banner' },
|
|
281
|
-
h('button', {
|
|
282
|
-
class: 'cm-mh-btn', type: 'button', onclick: onMenu,
|
|
283
|
-
title: 'Menu', 'aria-label': 'open navigation menu'
|
|
284
|
-
}, Icon('menu')),
|
|
285
|
-
h('span', { class: 'cm-mh-title' }, ...titleNode),
|
|
286
|
-
h('button', {
|
|
287
|
-
class: 'cm-mh-btn', type: 'button', onclick: onMembers,
|
|
288
|
-
title: 'Members', 'aria-label': 'show members'
|
|
289
|
-
}, Icon('members'))
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function ReplyBar({ quotedMessage, quotedAuthor, onCancel } = {}) {
|
|
294
|
-
return h('div', { class: 'cm-reply-bar', role: 'status' },
|
|
295
|
-
h('span', { class: 'cm-rb-label' }, 'Replying to ',
|
|
296
|
-
h('strong', { class: 'cm-rb-author' }, quotedAuthor || 'unknown')
|
|
297
|
-
),
|
|
298
|
-
h('span', { class: 'cm-rb-preview', title: quotedMessage || '' }, quotedMessage || ''),
|
|
299
|
-
h('button', {
|
|
300
|
-
class: 'cm-rb-cancel', type: 'button', onclick: onCancel,
|
|
301
|
-
title: 'Cancel reply', 'aria-label': 'cancel reply'
|
|
302
|
-
}, Icon('x'))
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function Banner({ tone = 'info', message, visible, actionLabel, onAction, onClick } = {}) {
|
|
307
|
-
if (!visible || !message) return null;
|
|
308
|
-
return h('div', {
|
|
309
|
-
class: 'cm-banner tone-' + tone + (onClick ? ' clickable' : ''),
|
|
310
|
-
role: tone === 'error' || tone === 'warning' ? 'alert' : 'status',
|
|
311
|
-
onclick: onClick || null
|
|
312
|
-
},
|
|
313
|
-
h('span', { class: 'cm-banner-msg' }, message),
|
|
314
|
-
actionLabel ? h('button', {
|
|
315
|
-
class: 'cm-banner-action', type: 'button',
|
|
316
|
-
onclick: (e) => { e.stopPropagation(); onAction && onAction(e); }
|
|
317
|
-
}, actionLabel) : null
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function fmtRelTime(ts) {
|
|
322
|
-
const t = Number(ts) || 0;
|
|
323
|
-
if (!t) return '';
|
|
324
|
-
const ms = t > 1e12 ? t : t * 1000;
|
|
325
|
-
const d = Math.max(0, Date.now() - ms);
|
|
326
|
-
const m = Math.floor(d / 60000);
|
|
327
|
-
if (m < 1) return 'now';
|
|
328
|
-
if (m < 60) return m + 'm';
|
|
329
|
-
const hr = Math.floor(m / 60);
|
|
330
|
-
if (hr < 24) return hr + 'h';
|
|
331
|
-
return Math.floor(hr / 24) + 'd';
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Skeleton rows for a cold thread/post-list load. Two lines per row (title +
|
|
335
|
-
// meta) mirrors cm-tp-item/cm-forum-item's actual shape so the shimmer
|
|
336
|
-
// doesn't jump on load. Reuses the kit-wide .ds-skel shimmer primitive.
|
|
337
|
-
function ListSkeleton({ cls, rows = 5 } = {}) {
|
|
338
|
-
return h('div', { class: cls + ' cm-list-skeleton', 'aria-hidden': 'true' },
|
|
339
|
-
...Array.from({ length: rows }, (_, i) => h('div', { key: 'lsk' + i, class: 'cm-list-item-skeleton' },
|
|
340
|
-
h('span', { class: 'ds-skel ds-skel-title' }), h('span', { class: 'ds-skel ds-skel-meta' }))));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export function ThreadPanel({ threads = [], activeId = null, title = 'Threads', onSelect, onCreate, onClose, loading = false } = {}) {
|
|
344
|
-
const list = Array.isArray(threads) ? threads : [];
|
|
345
|
-
return h('div', { class: 'cm-thread-panel', role: 'complementary', 'aria-label': title },
|
|
346
|
-
h('div', { class: 'cm-tp-head' },
|
|
347
|
-
h('span', { class: 'cm-tp-title' }, title),
|
|
348
|
-
h('div', { class: 'cm-tp-head-actions' },
|
|
349
|
-
onCreate ? h('button', { type: 'button', class: 'cm-tp-new', 'aria-label': 'new thread', title: 'New thread', onclick: onCreate }, '+') : null,
|
|
350
|
-
onClose ? h('button', { type: 'button', class: 'cm-tp-close', 'aria-label': 'close', title: 'Close', onclick: onClose }, Icon('x')) : null
|
|
351
|
-
)
|
|
352
|
-
),
|
|
353
|
-
loading ? ListSkeleton({ cls: 'cm-tp-list' }) : h('div', { class: 'cm-tp-list' },
|
|
354
|
-
list.length
|
|
355
|
-
? list.map(t => h('button', {
|
|
356
|
-
type: 'button', key: 'tp-' + t.id,
|
|
357
|
-
class: 'cm-tp-item' + (t.id === activeId ? ' is-active' : '') + (t.unread ? ' is-unread' : ''),
|
|
358
|
-
onclick: () => onSelect && onSelect(t.id)
|
|
359
|
-
},
|
|
360
|
-
t.unread ? h('span', { class: 'cm-tp-dot', 'aria-hidden': 'true' }) : null,
|
|
361
|
-
h('span', { class: 'cm-tp-item-title' }, t.title || '(untitled)'),
|
|
362
|
-
t.lastMessage ? h('span', { class: 'cm-tp-item-snippet' }, t.lastMessage) : null,
|
|
363
|
-
h('span', { class: 'cm-tp-item-meta' },
|
|
364
|
-
t.author ? h('span', { class: 'cm-tp-item-author' }, t.author) : null,
|
|
365
|
-
t.time ? h('span', { class: 'cm-tp-item-time' }, fmtRelTime(t.time)) : null
|
|
366
|
-
)
|
|
367
|
-
))
|
|
368
|
-
: h('div', { class: 'cm-tp-empty', role: 'status' },
|
|
369
|
-
Icon('thread', { size: 20 }),
|
|
370
|
-
h('span', { class: 'cm-tp-empty-text' }, onCreate ? 'no threads yet — start one' : 'no threads yet'))
|
|
371
|
-
)
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function ForumView({ posts = [], onSearch, onSort, onSelect, onNewPost, loading = false } = {}) {
|
|
376
|
-
const list = Array.isArray(posts) ? posts : [];
|
|
377
|
-
return h('div', { class: 'cm-forum', role: 'region', 'aria-label': 'forum' },
|
|
378
|
-
h('div', { class: 'cm-forum-toolbar' },
|
|
379
|
-
h('input', {
|
|
380
|
-
type: 'search', class: 'cm-forum-search', placeholder: 'Search posts…',
|
|
381
|
-
'aria-label': 'search posts',
|
|
382
|
-
oninput: onSearch ? (e) => onSearch(e.target.value) : null
|
|
383
|
-
}),
|
|
384
|
-
h('select', {
|
|
385
|
-
class: 'cm-forum-sort', 'aria-label': 'sort posts',
|
|
386
|
-
onchange: onSort ? (e) => onSort(e.target.value) : null
|
|
387
|
-
},
|
|
388
|
-
h('option', { value: 'recent' }, 'Recent'),
|
|
389
|
-
h('option', { value: 'replies' }, 'Most replies'),
|
|
390
|
-
h('option', { value: 'oldest' }, 'Oldest')
|
|
391
|
-
),
|
|
392
|
-
onNewPost ? h('button', { type: 'button', class: 'cm-forum-new', onclick: onNewPost }, 'New post') : null
|
|
393
|
-
),
|
|
394
|
-
loading ? ListSkeleton({ cls: 'cm-forum-list' }) : h('div', { class: 'cm-forum-list' },
|
|
395
|
-
list.length
|
|
396
|
-
? list.map(p => h('button', {
|
|
397
|
-
type: 'button', key: 'fp-' + p.id, class: 'cm-forum-item',
|
|
398
|
-
onclick: () => onSelect && onSelect(p.id)
|
|
399
|
-
},
|
|
400
|
-
h('div', { class: 'cm-forum-item-head' },
|
|
401
|
-
h('span', { class: 'cm-forum-item-title' }, p.title || '(untitled)'),
|
|
402
|
-
h('span', { class: 'cm-forum-item-replies' }, clampCount(p.replyCount), Icon('chevron-right', { size: 13 }))
|
|
403
|
-
),
|
|
404
|
-
p.snippet ? h('div', { class: 'cm-forum-item-snippet' }, p.snippet) : null,
|
|
405
|
-
h('div', { class: 'cm-forum-item-meta' },
|
|
406
|
-
p.author ? h('span', { class: 'cm-forum-item-author' }, p.author) : null,
|
|
407
|
-
p.time ? h('span', { class: 'cm-forum-item-time' }, fmtRelTime(p.time)) : null,
|
|
408
|
-
Array.isArray(p.tags) && p.tags.length
|
|
409
|
-
? h('span', { class: 'cm-forum-item-tags' }, ...p.tags.map((tag, i) =>
|
|
410
|
-
h('span', { class: 'cm-forum-tag', key: 'tg-' + i }, tag)))
|
|
411
|
-
: null
|
|
412
|
-
)
|
|
413
|
-
))
|
|
414
|
-
: h('div', { class: 'cm-forum-empty', role: 'status' },
|
|
415
|
-
Icon('forum', { size: 20 }),
|
|
416
|
-
h('span', { class: 'cm-forum-empty-text' }, onNewPost ? 'no posts yet — start the discussion' : 'no posts yet'))
|
|
417
|
-
)
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export function PageView({ title = '', html = '', isAdmin = false, onEdit } = {}) {
|
|
422
|
-
return h('div', { class: 'cm-page', role: 'document' },
|
|
423
|
-
h('div', { class: 'cm-page-head' },
|
|
424
|
-
h('h1', { class: 'cm-page-title' }, title || ''),
|
|
425
|
-
isAdmin && onEdit ? h('button', { type: 'button', class: 'cm-page-edit', onclick: onEdit }, 'Edit') : null
|
|
426
|
-
),
|
|
427
|
-
h('div', {
|
|
428
|
-
class: 'cm-page-body',
|
|
429
|
-
// Page bodies are host/user-authored HTML, so they pass through the
|
|
430
|
-
// DOMPurify gate before innerHTML — never injected raw (stored-XSS gate).
|
|
431
|
-
ref: (el) => {
|
|
432
|
-
if (!el) return;
|
|
433
|
-
if (!html) { el.innerHTML = '<p class="cm-page-empty">This page is empty.</p>'; return; }
|
|
434
|
-
sanitizeHtml(html).then((clean) => { el.innerHTML = clean; });
|
|
435
|
-
}
|
|
436
|
-
})
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export function CommunityShell({ serverRailProps, sidebarProps, children, memberListProps, voiceStripProps } = {}) {
|
|
441
|
-
return h('div', { class: 'cm-shell' },
|
|
442
|
-
serverRailProps ? ServerRail(serverRailProps) : null,
|
|
443
|
-
sidebarProps ? ChannelSidebar(sidebarProps) : null,
|
|
444
|
-
h('div', { class: 'cm-main' }, ...(Array.isArray(children) ? children : [children])),
|
|
445
|
-
memberListProps ? MemberList(memberListProps) : null,
|
|
446
|
-
voiceStripProps ? VoiceStrip(voiceStripProps) : null
|
|
447
|
-
);
|
|
448
|
-
}
|
|
2
|
+
//
|
|
3
|
+
// This module is a barrel: every component lives in a single-responsibility
|
|
4
|
+
// submodule under ./community/, and the public export surface here is unchanged
|
|
5
|
+
// — no consumer import needs to move.
|
|
6
|
+
|
|
7
|
+
import { ServerIcon, ServerRail, ChannelItem, ChannelCategory, ChannelSidebar } from './community/navigation.js';
|
|
8
|
+
import { VoiceUser, UserPanel, MemberItem, MemberList, VoiceStrip } from './community/presence.js';
|
|
9
|
+
import { ChatHeader, MobileHeader, ReplyBar, Banner } from './community/chrome.js';
|
|
10
|
+
import { ThreadPanel, ForumView, PageView } from './community/views.js';
|
|
11
|
+
import { CommunityShell } from './community/shell.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
ServerIcon, ServerRail, ChannelItem, ChannelCategory, ChannelSidebar,
|
|
15
|
+
VoiceUser, UserPanel, MemberItem, MemberList, VoiceStrip,
|
|
16
|
+
ChatHeader, MobileHeader, ReplyBar, Banner,
|
|
17
|
+
ThreadPanel, ForumView, PageView,
|
|
18
|
+
CommunityShell,
|
|
19
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Surrounding file-browser chrome: the bulk-action strip, the drop zone, the
|
|
2
|
+
// per-file upload progress list, the empty state, the breadcrumb path, the
|
|
3
|
+
// toolbar band, and the multi-root segmented picker.
|
|
4
|
+
|
|
5
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
6
|
+
import { Btn, Icon } from '../shell.js';
|
|
7
|
+
const h = webjsx.createElement;
|
|
8
|
+
|
|
9
|
+
// BulkBar — the act-on-selection strip shown while a multi-select is active.
|
|
10
|
+
// Host renders it above the grid; `actions` are [{ label, onClick, danger,
|
|
11
|
+
// disabled }]; `busy` disables everything while a bulk operation is in flight.
|
|
12
|
+
export function BulkBar({ count = 0, noun = 'file', nounPlural, actions = [], onClear, busy = false } = {}) {
|
|
13
|
+
if (!count) return null;
|
|
14
|
+
// 'entry' pluralizes to 'entries', not 'entrys' - handle the -y noun class
|
|
15
|
+
// unless the host passes an explicit plural.
|
|
16
|
+
const plural = nounPlural || (/[^aeiou]y$/.test(noun) ? noun.slice(0, -1) + 'ies' : noun + 's');
|
|
17
|
+
const kids = [
|
|
18
|
+
h('span', { key: 'count', class: 'ds-bulkbar-count', role: 'status', 'aria-live': 'polite' },
|
|
19
|
+
count + ' ' + (count === 1 ? noun : plural) + ' selected'),
|
|
20
|
+
...actions.map((a, i) => Btn({
|
|
21
|
+
key: 'bba' + i, danger: !!a.danger, disabled: busy || a.disabled,
|
|
22
|
+
onClick: a.onClick, children: a.label,
|
|
23
|
+
})),
|
|
24
|
+
onClear ? Btn({ key: 'bbclear', disabled: busy, onClick: onClear, children: 'clear selection' }) : null,
|
|
25
|
+
].filter(Boolean);
|
|
26
|
+
return h('div', { class: 'ds-bulkbar', role: 'toolbar', 'aria-label': 'bulk file actions', 'aria-busy': busy ? 'true' : null }, ...kids);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function FileToolbar({ left = [], right = [] } = {}) {
|
|
30
|
+
return h('div', { class: 'ds-file-toolbar' },
|
|
31
|
+
h('div', { class: 'ds-file-toolbar-left' }, ...left),
|
|
32
|
+
h('div', { class: 'ds-file-toolbar-right' }, ...right)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// RootsPicker — a segmented control for choosing among multiple allowed FS roots
|
|
37
|
+
// (so the app stops borrowing the history-tab .pill markup). Each root is
|
|
38
|
+
// { id, label }; `selected` is the active id. role=tablist for AT navigation.
|
|
39
|
+
export function RootsPicker({ roots = [], selected, onSelect, label = 'roots' } = {}) {
|
|
40
|
+
if (!roots.length) return null;
|
|
41
|
+
return h('div', { class: 'ds-roots-picker', role: 'tablist', 'aria-label': label },
|
|
42
|
+
...roots.map((r) => h('button', {
|
|
43
|
+
key: 'root-' + (r.id != null ? r.id : r.label),
|
|
44
|
+
type: 'button', role: 'tab',
|
|
45
|
+
class: 'ds-roots-tab' + ((r.id != null ? r.id : r.label) === selected ? ' active' : ''),
|
|
46
|
+
'aria-selected': (r.id != null ? r.id : r.label) === selected ? 'true' : 'false',
|
|
47
|
+
onclick: () => onSelect && onSelect(r.id != null ? r.id : r.label),
|
|
48
|
+
}, r.label || r.id)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function DropZone({ children, dragover, onDrop, onDragOver, onDragLeave, label = 'drop files here', onPick } = {}) {
|
|
52
|
+
// With children the zone is a passive WRAPPER: content renders normally and
|
|
53
|
+
// the dashed affordance appears only while a drag is over it (real file
|
|
54
|
+
// managers never burn a permanent band on a maybe-drop). Without children
|
|
55
|
+
// it keeps the explicit picker-block look.
|
|
56
|
+
const kids = Array.isArray(children) ? children : children ? [children] : [];
|
|
57
|
+
return h('div', {
|
|
58
|
+
class: 'ds-dropzone' + (kids.length ? ' ds-dropzone--wrap' : '') + (dragover ? ' dragover' : ''),
|
|
59
|
+
ondragover: (e) => { e.preventDefault(); onDragOver && onDragOver(e); },
|
|
60
|
+
ondragleave: (e) => { if (!e.currentTarget.contains(e.relatedTarget)) { onDragLeave && onDragLeave(e); } },
|
|
61
|
+
ondrop: (e) => { e.preventDefault(); onDrop && onDrop(e.dataTransfer.files); }
|
|
62
|
+
},
|
|
63
|
+
h('div', { class: 'ds-dropzone-inner' },
|
|
64
|
+
h('span', { class: 'ds-dropzone-glyph', role: 'img', 'aria-label': 'upload' }, Icon('arrow-up')),
|
|
65
|
+
h('span', { class: 'ds-dropzone-label' }, label),
|
|
66
|
+
onPick ? Btn({ onClick: onPick, children: 'pick files' }) : null
|
|
67
|
+
),
|
|
68
|
+
...kids
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// UploadProgress — per-file upload rows. Error rows are recoverable, not dead
|
|
73
|
+
// ends: each item may carry `actions` ([{ label, onClick }], e.g. 'replace' on
|
|
74
|
+
// a 409 collision) and the host may wire `onDismiss(item, index)` so error rows
|
|
75
|
+
// can be cleared without waiting for the next successful batch.
|
|
76
|
+
export function UploadProgress({ items = [], onDismiss } = {}) {
|
|
77
|
+
if (!items.length) return null;
|
|
78
|
+
return h('div', { class: 'ds-upload-progress' },
|
|
79
|
+
...items.map((it, i) => {
|
|
80
|
+
const indeterminate = !it.error && !it.done && !it.pct && it.indeterminate;
|
|
81
|
+
const status = it.error ? 'error' : (it.done ? 'complete' : (indeterminate ? 'uploading' : `uploading ${it.pct || 0}%`));
|
|
82
|
+
const rowActions = [
|
|
83
|
+
...((it.actions || []).map((a, ai) => h('button', {
|
|
84
|
+
key: 'ua' + ai, type: 'button', class: 'ds-upload-act',
|
|
85
|
+
'aria-label': `${a.label} ${it.name}`,
|
|
86
|
+
onclick: () => a.onClick && a.onClick(it, i),
|
|
87
|
+
}, a.label))),
|
|
88
|
+
(it.error && onDismiss) ? h('button', {
|
|
89
|
+
key: 'ud', type: 'button', class: 'ds-upload-act',
|
|
90
|
+
'aria-label': `dismiss ${it.name}`,
|
|
91
|
+
onclick: () => onDismiss(it, i),
|
|
92
|
+
}, 'dismiss') : null,
|
|
93
|
+
].filter(Boolean);
|
|
94
|
+
return h('div', {
|
|
95
|
+
key: it.name + i,
|
|
96
|
+
class: 'ds-upload-item' + (it.done ? ' done' : '') + (it.error ? ' error' : ''),
|
|
97
|
+
role: 'status',
|
|
98
|
+
'aria-label': `${it.name}: ${status}`,
|
|
99
|
+
'aria-live': 'polite'
|
|
100
|
+
},
|
|
101
|
+
h('span', { class: 'ds-upload-name' }, it.name),
|
|
102
|
+
h('span', { class: 'ds-upload-bar' + (indeterminate ? ' indeterminate' : '') },
|
|
103
|
+
h('span', { class: 'ds-upload-fill', 'data-pct': String(Math.max(0, Math.min(100, it.pct || 0))), 'aria-hidden': 'true' })
|
|
104
|
+
),
|
|
105
|
+
h('span', { class: 'ds-upload-pct', 'aria-hidden': 'true' }, (it.error ? 'err' : (it.done ? 'ok' : (indeterminate ? '...' : (it.pct || 0) + '%')))),
|
|
106
|
+
rowActions.length ? h('span', { class: 'ds-upload-actions', role: 'group', 'aria-label': `actions for ${it.name}` }, ...rowActions) : null
|
|
107
|
+
);
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function EmptyState({ text = 'nothing here', glyph = Icon('circle'), action } = {}) {
|
|
113
|
+
// action: { onClick, label } - an optional CTA (e.g. 'go up' / 'upload a
|
|
114
|
+
// file'), mirroring the SessionDashboard emptyAction contract so an empty
|
|
115
|
+
// directory is not a dead end. Children are built as an array + filtered so
|
|
116
|
+
// the keyed Btn never sits beside an unkeyed span (webjsx applyDiff 'key'
|
|
117
|
+
// crash on mixed keyed/unkeyed siblings).
|
|
118
|
+
return h('div', { class: 'ds-file-empty', role: 'status' },
|
|
119
|
+
...[
|
|
120
|
+
h('span', { key: 'glyph', class: 'ds-file-empty-glyph', 'aria-hidden': 'true' }, glyph),
|
|
121
|
+
h('span', { key: 'text', class: 'ds-file-empty-text' }, text),
|
|
122
|
+
(action && action.onClick)
|
|
123
|
+
? Btn({ key: 'ea', onClick: action.onClick, children: action.label || 'go up' })
|
|
124
|
+
: null,
|
|
125
|
+
].filter(Boolean)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function BreadcrumbPath({ segments = [], onNav, root = 'root' } = {}) {
|
|
130
|
+
const parts = [h('button', { key: 'root', class: 'ds-crumb-seg', onclick: () => onNav && onNav(0) }, root)];
|
|
131
|
+
segments.forEach((seg, i) => {
|
|
132
|
+
parts.push(h('span', { key: 'sep' + i, class: 'ds-crumb-sep', 'aria-hidden': 'true' }, Icon('chevron-right', { size: 13 })));
|
|
133
|
+
parts.push(h('button', {
|
|
134
|
+
key: 'seg' + i,
|
|
135
|
+
class: 'ds-crumb-seg' + (i === segments.length - 1 ? ' leaf' : ''),
|
|
136
|
+
onclick: () => onNav && onNav(i + 1)
|
|
137
|
+
}, seg));
|
|
138
|
+
});
|
|
139
|
+
return h('div', { class: 'ds-crumb-path' }, ...parts);
|
|
140
|
+
}
|