@devchitchat/chat 5.0.0 → 5.0.1
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 +1 -1
- package/pages/channels/[channelId].phtml +0 -2
- package/pages/design/_layout.html +349 -0
- package/pages/design/_layout.js +13 -0
- package/pages/design/components/index.js +3 -0
- package/pages/design/components/index.phtml +380 -0
- package/pages/design/index.js +3 -0
- package/pages/design/index.phtml +78 -0
- package/pages/design/principles/index.js +3 -0
- package/pages/design/principles/index.phtml +147 -0
- package/pages/design/tokens/index.js +3 -0
- package/pages/design/tokens/index.phtml +236 -0
- package/pages/public/client/app.js +38 -3
- package/pages/public/client/resizable.js +74 -0
- package/pages/public/client/rtc-peer-manager.js +5 -2
- package/pages/public/client/settings-sync.js +45 -7
- package/pages/public/client/theme.js +6 -4
- package/pages/public/client/views/CallView.js +24 -31
- package/pages/public/client/views/ChatHeaderView.js +67 -0
- package/pages/public/client/views/ComposerView.js +5 -2
- package/pages/public/client/views/MessageListView.js +2 -2
- package/pages/public/client/views/SidebarView.js +122 -47
- package/pages/public/client/views/shared/EmojiPickerSingleton.js +3 -3
- package/pages/public/themes/base.css +25 -2
- package/src/ws/ChatServer.js +2 -1
- package/src/ws/handlers/rtcHandlers.js +7 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
<h1 class="ds-page-title">Components</h1>
|
|
2
|
+
<p class="ds-page-subtitle">
|
|
3
|
+
Live examples using the real CSS classes. No stubs — every element here is styled by
|
|
4
|
+
<code>base.css</code> and the active theme.
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<!-- ── Buttons ────────────────────────────────────────────────────────────── -->
|
|
8
|
+
<div class="ds-section">
|
|
9
|
+
<h2 class="ds-section-title">Buttons</h2>
|
|
10
|
+
<p class="ds-section-desc">
|
|
11
|
+
Three roles: primary (one per view), ghost (secondary actions), icon (toolbars).
|
|
12
|
+
Danger is a modifier, not a fourth role.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<span class="ds-label">Primary actions</span>
|
|
16
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
17
|
+
<button class="btn-primary" type="button">Send</button>
|
|
18
|
+
<button class="btn-primary" type="button" disabled>Disabled</button>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="ds-code"><button class="btn-primary" type="button">Send</button></div>
|
|
21
|
+
|
|
22
|
+
<span class="ds-label">Ghost — secondary / low-emphasis</span>
|
|
23
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
24
|
+
<button class="btn-ghost" type="button">Cancel</button>
|
|
25
|
+
<button class="btn-ghost btn-sm" type="button">Admin</button>
|
|
26
|
+
<button class="btn-ghost btn-sm" type="button">Sign out</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="ds-code"><button class="btn-ghost" type="button">Cancel</button>
|
|
29
|
+
<button class="btn-ghost btn-sm" type="button">Admin</button></div>
|
|
30
|
+
|
|
31
|
+
<span class="ds-label">Icon — toolbars and tight spaces</span>
|
|
32
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
33
|
+
<button class="btn-icon" type="button" title="Reply">↩</button>
|
|
34
|
+
<button class="btn-icon" type="button" title="React">🙂</button>
|
|
35
|
+
<button class="btn-icon" type="button" title="More">⋯</button>
|
|
36
|
+
<button class="btn-icon" type="button" title="Add">+</button>
|
|
37
|
+
<button class="btn-icon" type="button" title="Close">✕</button>
|
|
38
|
+
<button class="btn-icon" type="button" title="Settings">⚙</button>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="ds-code"><button class="btn-icon" type="button" title="Reply">↩</button></div>
|
|
41
|
+
|
|
42
|
+
<span class="ds-label">Danger — destructive actions</span>
|
|
43
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
44
|
+
<button class="btn-danger" type="button">Delete channel</button>
|
|
45
|
+
<button class="btn-ghost" style="color: var(--color-danger);" type="button">Remove</button>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="ds-annotation">
|
|
48
|
+
<strong>Naming inconsistency:</strong> Buttons like <code>.btn-start-call</code>,
|
|
49
|
+
<code>.btn-join-call</code>, and <code>.btn-leave</code> are hardcoded to specific use cases.
|
|
50
|
+
They should be replaced with semantic compositions: <code>.btn-primary</code> for Join,
|
|
51
|
+
<code>.btn-ghost</code> for Leave (with <code>--color-danger</code>). Reduces CSS surface area.
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<hr class="ds-divider">
|
|
56
|
+
|
|
57
|
+
<!-- ── Form controls ──────────────────────────────────────────────────────── -->
|
|
58
|
+
<div class="ds-section">
|
|
59
|
+
<h2 class="ds-section-title">Form Controls</h2>
|
|
60
|
+
|
|
61
|
+
<span class="ds-label">Text input</span>
|
|
62
|
+
<div class="ds-canvas">
|
|
63
|
+
<div class="field">
|
|
64
|
+
<label for="ex-name">Channel name</label>
|
|
65
|
+
<input id="ex-name" type="text" placeholder="e.g. general" maxlength="80" autocomplete="off">
|
|
66
|
+
</div>
|
|
67
|
+
<div class="field" style="margin-top: 12px;">
|
|
68
|
+
<label for="ex-topic">Topic <span style="font-weight:400;color:var(--text-muted)">(optional)</span></label>
|
|
69
|
+
<input id="ex-topic" type="text" placeholder="What this channel is for" maxlength="240" autocomplete="off">
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="ds-code"><div class="field">
|
|
73
|
+
<label for="ch-name">Channel name</label>
|
|
74
|
+
<input id="ch-name" type="text" placeholder="e.g. general" autocomplete="off">
|
|
75
|
+
</div></div>
|
|
76
|
+
|
|
77
|
+
<span class="ds-label">Select</span>
|
|
78
|
+
<div class="ds-canvas">
|
|
79
|
+
<div class="field">
|
|
80
|
+
<label for="ex-visibility">Visibility</label>
|
|
81
|
+
<select id="ex-visibility">
|
|
82
|
+
<option value="public">Public</option>
|
|
83
|
+
<option value="restricted">Restricted</option>
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<hr class="ds-divider">
|
|
90
|
+
|
|
91
|
+
<!-- ── Tags ───────────────────────────────────────────────────────────────── -->
|
|
92
|
+
<div class="ds-section">
|
|
93
|
+
<h2 class="ds-section-title">Tags</h2>
|
|
94
|
+
<p class="ds-section-desc">Used in admin views to label user roles and status.</p>
|
|
95
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
96
|
+
<span class="tag">User</span>
|
|
97
|
+
<span class="tag tag--admin">Admin</span>
|
|
98
|
+
<span class="tag tag--bot">Bot</span>
|
|
99
|
+
<span class="tag tag--warn">Guest</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="ds-code"><span class="tag">User</span>
|
|
102
|
+
<span class="tag tag--admin">Admin</span>
|
|
103
|
+
<span class="tag tag--bot">Bot</span>
|
|
104
|
+
<span class="tag tag--warn">Guest</span></div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<hr class="ds-divider">
|
|
108
|
+
|
|
109
|
+
<!-- ── Message ────────────────────────────────────────────────────────────── -->
|
|
110
|
+
<div class="ds-section">
|
|
111
|
+
<h2 class="ds-section-title">Message</h2>
|
|
112
|
+
<p class="ds-section-desc">
|
|
113
|
+
The core display unit. Uses CSS grid: handle + timestamp on one row, text spanning the full width.
|
|
114
|
+
Reaction bar and thread link appear below the text.
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<span class="ds-label">Default</span>
|
|
118
|
+
<div class="ds-canvas ds-canvas--dark">
|
|
119
|
+
<div id="messages" style="padding: 8px 0; pointer-events: none;">
|
|
120
|
+
<article class="message" data-msg-id="ex1" data-user-id="u_ex" data-seq="1">
|
|
121
|
+
<span class="message-handle" data-user-id="u_ex">Alice</span>
|
|
122
|
+
<time class="message-time" datetime="2026-09-12T10:00:00Z">10:00 AM</time>
|
|
123
|
+
<div class="message-text">
|
|
124
|
+
<p>This is a message. It can contain <strong>bold</strong>, <em>italic</em>,
|
|
125
|
+
<code>inline code</code>, and <a href="#">links</a>.</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="reaction-bar">
|
|
128
|
+
<button class="reaction-pill reacted" type="button" title="3 reactions" data-emoji="👍" data-msg-id="ex1">
|
|
129
|
+
👍 <span class="reaction-count">3</span>
|
|
130
|
+
</button>
|
|
131
|
+
<button class="reaction-pill" type="button" title="1 reaction" data-emoji="🔥" data-msg-id="ex1">
|
|
132
|
+
🔥 <span class="reaction-count">1</span>
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</article>
|
|
136
|
+
<article class="message" data-msg-id="ex2" data-user-id="u_ex2" data-seq="2">
|
|
137
|
+
<span class="message-handle" data-user-id="u_ex2">Bob</span>
|
|
138
|
+
<time class="message-time" datetime="2026-09-12T10:01:00Z">10:01 AM<span class="message-edited">(edited)</span></time>
|
|
139
|
+
<div class="message-text">
|
|
140
|
+
<p>A reply mentioning <span class="mention">@Alice</span> — mentions are highlighted inline.</p>
|
|
141
|
+
</div>
|
|
142
|
+
<a class="thread-replies-link" href="#">3 replies</a>
|
|
143
|
+
<div class="reaction-bar"></div>
|
|
144
|
+
</article>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="ds-annotation">
|
|
148
|
+
<strong>Ownership note:</strong> <code>.message-hover-actions</code> is created and injected
|
|
149
|
+
by <code>MessageInteractions.js</code>, not baked into the HTML. The message article is pure
|
|
150
|
+
content; interaction affordances are layered on top. This keeps the SSR template clean and
|
|
151
|
+
lets the same <code>makeMessageEl</code> function work in both the channel view and thread panel.
|
|
152
|
+
</div>
|
|
153
|
+
<div class="ds-code"><article class="message" data-seq="…" data-msg-id="…" data-user-id="…">
|
|
154
|
+
<span class="message-handle" data-user-id="…">Alice</span>
|
|
155
|
+
<time class="message-time" datetime="…">10:00 AM</time>
|
|
156
|
+
<div class="message-text">…</div>
|
|
157
|
+
<div class="reaction-bar"><!-- populated by renderReactionBar() --></div>
|
|
158
|
+
</article></div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<hr class="ds-divider">
|
|
162
|
+
|
|
163
|
+
<!-- ── Reaction bar ────────────────────────────────────────────────────────── -->
|
|
164
|
+
<div class="ds-section">
|
|
165
|
+
<h2 class="ds-section-title">Reaction Bar</h2>
|
|
166
|
+
<p class="ds-section-desc">Emoji reactions on a message. Reacted pills are tinted with the accent color.</p>
|
|
167
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
168
|
+
<button class="reaction-pill reacted" type="button" title="3 reactions">👍 <span class="reaction-count">3</span></button>
|
|
169
|
+
<button class="reaction-pill" type="button" title="1 reaction">🔥 <span class="reaction-count">1</span></button>
|
|
170
|
+
<button class="reaction-pill" type="button" title="2 reactions">❤️ <span class="reaction-count">2</span></button>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="ds-code"><!-- reacted = current user has reacted -->
|
|
173
|
+
<button class="reaction-pill reacted" …>👍 <span class="reaction-count">3</span></button>
|
|
174
|
+
<button class="reaction-pill" …>🔥 <span class="reaction-count">1</span></button></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<hr class="ds-divider">
|
|
178
|
+
|
|
179
|
+
<!-- ── Sidebar ─────────────────────────────────────────────────────────────── -->
|
|
180
|
+
<div class="ds-section">
|
|
181
|
+
<h2 class="ds-section-title">Sidebar Navigation</h2>
|
|
182
|
+
<p class="ds-section-desc">
|
|
183
|
+
Hubs use <code><details></code>/<code><summary></code> for native collapse.
|
|
184
|
+
Channel items are <code><li></code> inside <code><ul class="channel-list"></code>.
|
|
185
|
+
</p>
|
|
186
|
+
|
|
187
|
+
<div class="ds-canvas ds-canvas--flush" style="max-width: 260px;">
|
|
188
|
+
<div style="padding: 8px 0;">
|
|
189
|
+
<details class="hub-header" data-hub-id="ex-hub" open>
|
|
190
|
+
<summary class="hub-name" data-hub-id="ex-hub">
|
|
191
|
+
<span>Engineering</span>
|
|
192
|
+
</summary>
|
|
193
|
+
<ul class="channel-list">
|
|
194
|
+
<li class="channel-item active">
|
|
195
|
+
<a class="channel-link" href="#" data-channel-id="c_1">general</a>
|
|
196
|
+
<span class="call-badge" aria-label="Call active"></span>
|
|
197
|
+
</li>
|
|
198
|
+
<li class="channel-item">
|
|
199
|
+
<a class="channel-link" href="#" data-channel-id="c_2">design</a>
|
|
200
|
+
<span class="call-badge" aria-label="Call active"></span>
|
|
201
|
+
</li>
|
|
202
|
+
<li class="channel-item" data-mention="">
|
|
203
|
+
<a class="channel-link" href="#" data-channel-id="c_3">backend</a>
|
|
204
|
+
<span class="call-badge" aria-label="Call active"></span>
|
|
205
|
+
</li>
|
|
206
|
+
</ul>
|
|
207
|
+
</details>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="ds-annotation">
|
|
211
|
+
<strong>Progressive disclosure:</strong> Hub management (edit, delete) is available only via
|
|
212
|
+
right-click on desktop or long-press on mobile. No gear icon is shown at rest — this keeps
|
|
213
|
+
the sidebar focused on navigation, not administration.
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<hr class="ds-divider">
|
|
218
|
+
|
|
219
|
+
<!-- ── Context menu ───────────────────────────────────────────────────────── -->
|
|
220
|
+
<div class="ds-section">
|
|
221
|
+
<h2 class="ds-section-title">Context Menu</h2>
|
|
222
|
+
<p class="ds-section-desc">
|
|
223
|
+
Desktop right-click on messages and sidebar items. Positioned near the cursor, flips if near
|
|
224
|
+
viewport edge. Shared between <code>MessageInteractions</code> and <code>SidebarView</code>.
|
|
225
|
+
</p>
|
|
226
|
+
<div class="ds-canvas">
|
|
227
|
+
<div class="msg-context-menu" style="position: relative; display: inline-block; box-shadow: var(--shadow-md);">
|
|
228
|
+
<button class="msg-context-menu-item" type="button" role="menuitem">React</button>
|
|
229
|
+
<button class="msg-context-menu-item" type="button" role="menuitem">Reply in thread</button>
|
|
230
|
+
<button class="msg-context-menu-item" type="button" role="menuitem">Edit message</button>
|
|
231
|
+
<button class="msg-context-menu-item msg-context-menu-item--danger" type="button" role="menuitem">Delete message</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="ds-code"><div class="msg-context-menu" role="menu">
|
|
235
|
+
<button class="msg-context-menu-item" role="menuitem">React</button>
|
|
236
|
+
<button class="msg-context-menu-item msg-context-menu-item--danger" role="menuitem">Delete</button>
|
|
237
|
+
</div></div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<hr class="ds-divider">
|
|
241
|
+
|
|
242
|
+
<!-- ── Modal ──────────────────────────────────────────────────────────────── -->
|
|
243
|
+
<div class="ds-section">
|
|
244
|
+
<h2 class="ds-section-title">Modal</h2>
|
|
245
|
+
<p class="ds-section-desc">
|
|
246
|
+
Used for hub and channel settings on desktop. Managed by <code>modal.js</code>
|
|
247
|
+
— call <code>showModal({ title, build })</code> rather than constructing the HTML directly.
|
|
248
|
+
</p>
|
|
249
|
+
<div class="ds-canvas ds-canvas--flush" style="position: relative; min-height: 360px; overflow: hidden;">
|
|
250
|
+
<div style="position: absolute; inset: 0; background: rgba(0,0,0,0.4); backdrop-filter: blur(4px); border-radius: var(--radius-lg);"></div>
|
|
251
|
+
<div class="modal" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0;">
|
|
252
|
+
<div class="modal-header">
|
|
253
|
+
<span class="modal-title">Channel settings</span>
|
|
254
|
+
<button class="btn-icon modal-close" type="button" aria-label="Close">✕</button>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="modal-body">
|
|
257
|
+
<div class="field">
|
|
258
|
+
<label for="ex-modal-name">Channel name</label>
|
|
259
|
+
<input id="ex-modal-name" type="text" value="general" autocomplete="off">
|
|
260
|
+
</div>
|
|
261
|
+
<div class="field" style="margin-top: 12px;">
|
|
262
|
+
<label for="ex-modal-topic">Topic</label>
|
|
263
|
+
<input id="ex-modal-topic" type="text" placeholder="Optional" autocomplete="off">
|
|
264
|
+
</div>
|
|
265
|
+
<div class="modal-footer">
|
|
266
|
+
<button class="btn-ghost" type="button">Cancel</button>
|
|
267
|
+
<button class="btn-primary" type="button">Save</button>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="modal-danger-zone">
|
|
270
|
+
<p>Deleting this channel removes all its messages permanently.</p>
|
|
271
|
+
<button class="btn-danger" type="button">Delete channel</button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="ds-code">import { showModal, dismiss } from './modal.js'
|
|
277
|
+
|
|
278
|
+
showModal({
|
|
279
|
+
title: 'Channel settings',
|
|
280
|
+
build: body => {
|
|
281
|
+
// populate body element with form content
|
|
282
|
+
}
|
|
283
|
+
})</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<hr class="ds-divider">
|
|
287
|
+
|
|
288
|
+
<!-- ── Mention ────────────────────────────────────────────────────────────── -->
|
|
289
|
+
<div class="ds-section">
|
|
290
|
+
<h2 class="ds-section-title">Mentions</h2>
|
|
291
|
+
<p class="ds-section-desc">
|
|
292
|
+
Inline @mention spans. Only rendered when the handle matches a known member or bot — unknown
|
|
293
|
+
handles are left as plain text.
|
|
294
|
+
</p>
|
|
295
|
+
<div class="ds-canvas">
|
|
296
|
+
<p style="font-size: 14px; line-height: 1.6; margin: 0;">
|
|
297
|
+
Hey <span class="mention">@alice</span> can you review this before <span class="mention">@bob</span> ships it?
|
|
298
|
+
(Unknown handles like @nobody stay as plain text.)
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="ds-annotation">
|
|
302
|
+
<strong>Validation:</strong> <code>renderText()</code> in <code>shared/messages.js</code> accepts
|
|
303
|
+
a <code>knownHandles</code> Set. If provided, only handles in that set get the
|
|
304
|
+
<code>.mention</code> class. The set is derived from <code>AppModel.knownHandles</code>
|
|
305
|
+
(members + bots). This prevents styling random @-prefixed words as mentions.
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<hr class="ds-divider">
|
|
310
|
+
|
|
311
|
+
<!-- ── Admin components ───────────────────────────────────────────────────── -->
|
|
312
|
+
<div class="ds-section">
|
|
313
|
+
<h2 class="ds-section-title">Admin Components</h2>
|
|
314
|
+
<p class="ds-section-desc">Used only in <code>/admin/*</code> routes. Not part of the chat UI.</p>
|
|
315
|
+
|
|
316
|
+
<span class="ds-label">Data table</span>
|
|
317
|
+
<div class="ds-canvas">
|
|
318
|
+
<table class="data-table">
|
|
319
|
+
<thead>
|
|
320
|
+
<tr>
|
|
321
|
+
<th>Name</th>
|
|
322
|
+
<th>Handle</th>
|
|
323
|
+
<th>Role</th>
|
|
324
|
+
<th>Joined</th>
|
|
325
|
+
</tr>
|
|
326
|
+
</thead>
|
|
327
|
+
<tbody>
|
|
328
|
+
<tr>
|
|
329
|
+
<td>Alice</td>
|
|
330
|
+
<td>@alice</td>
|
|
331
|
+
<td><span class="tag tag--admin">Admin</span></td>
|
|
332
|
+
<td>Sep 1, 2026</td>
|
|
333
|
+
</tr>
|
|
334
|
+
<tr>
|
|
335
|
+
<td>Bob</td>
|
|
336
|
+
<td>@bob</td>
|
|
337
|
+
<td><span class="tag">User</span></td>
|
|
338
|
+
<td>Sep 5, 2026</td>
|
|
339
|
+
</tr>
|
|
340
|
+
<tr>
|
|
341
|
+
<td>Helpbot</td>
|
|
342
|
+
<td>@helpbot</td>
|
|
343
|
+
<td><span class="tag tag--bot">Bot</span></td>
|
|
344
|
+
<td>Sep 8, 2026</td>
|
|
345
|
+
</tr>
|
|
346
|
+
</tbody>
|
|
347
|
+
</table>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<span class="ds-label" style="margin-top: 24px;">Notice banners</span>
|
|
351
|
+
<div class="ds-canvas" style="display: flex; flex-direction: column; gap: 8px;">
|
|
352
|
+
<div class="admin-notice admin-notice--success">Invite link created and ready to share.</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<hr class="ds-divider">
|
|
357
|
+
|
|
358
|
+
<!-- ── Resize handles ────────────────────────────────────────────────────── -->
|
|
359
|
+
<div class="ds-section">
|
|
360
|
+
<h2 class="ds-section-title">Resizable Panels</h2>
|
|
361
|
+
<p class="ds-section-desc">
|
|
362
|
+
Drag handles injected by <code>resizable.js</code>. Width persisted via <code>getPref</code>
|
|
363
|
+
and applied as a CSS custom property on <code>:root</code>.
|
|
364
|
+
</p>
|
|
365
|
+
<div class="ds-canvas" style="display: flex; gap: 0; height: 100px; overflow: hidden; border-radius: var(--radius-lg); position: relative;">
|
|
366
|
+
<div style="flex: 1; background: var(--bg-sidebar); display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--text-muted);">Sidebar</div>
|
|
367
|
+
<div class="resize-handle resize-handle--right" style="position: relative; cursor: col-resize;"></div>
|
|
368
|
+
<div style="flex: 2; background: var(--bg-base); display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--text-muted);">Chat panel</div>
|
|
369
|
+
<div class="resize-handle resize-handle--left" style="position: relative; cursor: col-resize;"></div>
|
|
370
|
+
<div style="flex: 1; background: var(--bg-sidebar); display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--text-muted);">Thread panel</div>
|
|
371
|
+
</div>
|
|
372
|
+
<div class="ds-code">import { attachResizeHandle } from './resizable.js'
|
|
373
|
+
|
|
374
|
+
attachResizeHandle(sidebarEl, {
|
|
375
|
+
edge: 'right',
|
|
376
|
+
cssVar: '--sidebar-width',
|
|
377
|
+
min: 180, max: 480,
|
|
378
|
+
prefKey: 'sidebar_width',
|
|
379
|
+
})</div>
|
|
380
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<h1 class="ds-page-title">Design System</h1>
|
|
2
|
+
<p class="ds-page-subtitle">
|
|
3
|
+
A living reference for the UI language of devchitchat. Built with and for the codebase —
|
|
4
|
+
every example here uses the real CSS and renders in the actual running app.
|
|
5
|
+
Themed: change the picker in the nav to see all tokens update live.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<div class="ds-section">
|
|
9
|
+
<h2 class="ds-section-title">What's in here</h2>
|
|
10
|
+
<p class="ds-section-desc">Three sections, each with a specific job.</p>
|
|
11
|
+
|
|
12
|
+
<div class="ds-canvas">
|
|
13
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;">
|
|
14
|
+
<div>
|
|
15
|
+
<a href="{{base}}/design/principles" style="text-decoration: none;">
|
|
16
|
+
<p style="font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin: 0 0 4px;">Principles</p>
|
|
17
|
+
<p style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Why we build the way we build</p>
|
|
18
|
+
<p style="font-size: 13px; color: var(--text-secondary); line-height: 1.5; margin: 0;">Ownership, direction of communication, progressive disclosure, and the tensions between them.</p>
|
|
19
|
+
</a>
|
|
20
|
+
</div>
|
|
21
|
+
<div>
|
|
22
|
+
<a href="{{base}}/design/tokens" style="text-decoration: none;">
|
|
23
|
+
<p style="font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin: 0 0 4px;">Tokens</p>
|
|
24
|
+
<p style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Colors, typography, spacing, radii</p>
|
|
25
|
+
<p style="font-size: 13px; color: var(--text-secondary); line-height: 1.5; margin: 0;">Every CSS custom property used across all five themes, with live swatches that update when you switch.</p>
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<a href="{{base}}/design/components" style="text-decoration: none;">
|
|
30
|
+
<p style="font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin: 0 0 4px;">Components</p>
|
|
31
|
+
<p style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Every UI pattern in one place</p>
|
|
32
|
+
<p style="font-size: 13px; color: var(--text-secondary); line-height: 1.5; margin: 0;">Live examples of every button, input, message, overlay, and sidebar element with their class names.</p>
|
|
33
|
+
</a>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="ds-section">
|
|
40
|
+
<h2 class="ds-section-title">Design language</h2>
|
|
41
|
+
<p class="ds-section-desc">Informed by Apple's Human Interface Guidelines.</p>
|
|
42
|
+
|
|
43
|
+
<div class="ds-canvas">
|
|
44
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; font-size: 14px; color: var(--text-secondary); line-height: 1.6;">
|
|
45
|
+
<div>
|
|
46
|
+
<p style="font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Clarity</p>
|
|
47
|
+
<p style="margin: 0;">Text is legible. Icons are unambiguous. Whitespace is generous. The interface
|
|
48
|
+
surfaces what the user needs without decoration for its own sake.</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p style="font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Deference</p>
|
|
52
|
+
<p style="margin: 0;">The UI steps back so content — messages, names, channels — takes focus.
|
|
53
|
+
Backgrounds recede. Borders are subtle. Actions appear when needed, not always.</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<p style="font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Depth</p>
|
|
57
|
+
<p style="margin: 0;">Layering through blur, shadow, and elevation communicates hierarchy.
|
|
58
|
+
Modals float above content. The sidebar is behind the chat. The call panel is beside it.</p>
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<p style="font-weight: 600; color: var(--text-primary); margin: 0 0 6px;">Feedback</p>
|
|
62
|
+
<p style="margin: 0;">Every interaction has a response — hover states, transitions at
|
|
63
|
+
200ms cubic-bezier ease, and status indicators so the user always knows what the app is doing.</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="ds-section">
|
|
70
|
+
<h2 class="ds-section-title">Stack</h2>
|
|
71
|
+
<p class="ds-section-desc">No build step. No framework. Plain browser APIs throughout.</p>
|
|
72
|
+
<div class="ds-code">CSS custom properties → theming
|
|
73
|
+
ES modules (import/export) → component isolation
|
|
74
|
+
EventTarget → model observation
|
|
75
|
+
CustomEvent (document) → view-to-view + view-to-controller communication
|
|
76
|
+
Handlebars (.phtml) → server-side rendering
|
|
77
|
+
index97 → file-based routing</div>
|
|
78
|
+
</div>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<h1 class="ds-page-title">Principles</h1>
|
|
2
|
+
<p class="ds-page-subtitle">
|
|
3
|
+
The design decisions behind how this codebase is built and how future features should be added.
|
|
4
|
+
These are not rules enforced by a linter — they are judgment guides that help resolve the
|
|
5
|
+
inevitable tensions between competing concerns.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<!-- ── 1. Ownership ─────────────────────────────────────────────────────── -->
|
|
9
|
+
<div class="principle-card">
|
|
10
|
+
<p class="principle-num">01</p>
|
|
11
|
+
<h2 class="principle-title">Ownership — with scope as a judgment call</h2>
|
|
12
|
+
<p class="principle-body">
|
|
13
|
+
Every piece of DOM has one owner. That owner is a View. The View reaches only <em>downward</em>
|
|
14
|
+
into its own subtree — never <code>closest()</code> up to a parent or sideways to a sibling.
|
|
15
|
+
</p>
|
|
16
|
+
<p class="principle-body">
|
|
17
|
+
But "single" ownership is only half the idea. The scope of that ownership requires judgment.
|
|
18
|
+
A view that owns too little becomes boilerplate with no coherent responsibility.
|
|
19
|
+
A view that owns too much conflates unrelated concerns and becomes hard to reason about.
|
|
20
|
+
</p>
|
|
21
|
+
<div class="principle-tension">
|
|
22
|
+
<strong>The tension:</strong> What is the smallest unit that has a coherent <em>responsibility</em>
|
|
23
|
+
(not just a coherent size)? <code>ChatHeaderView</code> is small in lines, but its responsibility
|
|
24
|
+
is clear: channel identity display and the call entry point. That coherence justifies it as a
|
|
25
|
+
distinct view — not its line count.
|
|
26
|
+
</div>
|
|
27
|
+
<p class="principle-body" style="margin-top: 12px;">
|
|
28
|
+
<strong>How to apply:</strong> When deciding where code belongs, ask: "what is this element's
|
|
29
|
+
job?" If two views have valid reasons to touch the same element, that element needs its own view
|
|
30
|
+
with a clear responsibility, and the two callers communicate through events.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- ── 2. Unidirectional communication ───────────────────────────────────── -->
|
|
35
|
+
<div class="principle-card">
|
|
36
|
+
<p class="principle-num">02</p>
|
|
37
|
+
<h2 class="principle-title">Unidirectional communication</h2>
|
|
38
|
+
<p class="principle-body">
|
|
39
|
+
Communication flows in one direction: <strong>model → views</strong> and
|
|
40
|
+
<strong>user gestures → controllers → model</strong>. Views never push state back into the model
|
|
41
|
+
directly — they dispatch events that controllers handle.
|
|
42
|
+
</p>
|
|
43
|
+
<div class="principle-tension">
|
|
44
|
+
<strong>The tension:</strong> Event-driven systems can become chaotic and opaque.
|
|
45
|
+
The risk isn't events themselves — it's <em>bidirectional</em> or <em>cyclical</em> event flows
|
|
46
|
+
where A fires an event causing B to fire one that causes A to update. Naming the intended
|
|
47
|
+
direction makes violations obvious.
|
|
48
|
+
</div>
|
|
49
|
+
<p class="principle-body" style="margin-top: 12px;">
|
|
50
|
+
<strong>How to apply:</strong> Trace any event from where it is dispatched to where it is
|
|
51
|
+
handled. The flow should be a DAG, not a cycle. If you find yourself listening for an event
|
|
52
|
+
inside the same view that dispatches it, something is wrong.
|
|
53
|
+
</p>
|
|
54
|
+
<div class="ds-code" style="margin-top: 12px; margin-bottom: 0;">WebSocket → WebSocketController → AppModel (mutate)
|
|
55
|
+
↓
|
|
56
|
+
Views (listen, render DOM)
|
|
57
|
+
↓
|
|
58
|
+
User gesture → dispatch CustomEvent
|
|
59
|
+
↓
|
|
60
|
+
ChatController → ws.send()</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- ── 3. Fat model, thin views ──────────────────────────────────────────── -->
|
|
64
|
+
<div class="principle-card">
|
|
65
|
+
<p class="principle-num">03</p>
|
|
66
|
+
<h2 class="principle-title">Fat model, thin views — held open for critique</h2>
|
|
67
|
+
<p class="principle-body">
|
|
68
|
+
Application state lives in <code>AppModel</code>. Views render it and dispatch user gestures.
|
|
69
|
+
A view that accumulates its own state is a signal that something belongs in the model.
|
|
70
|
+
</p>
|
|
71
|
+
<div class="principle-tension">
|
|
72
|
+
<strong>The tension:</strong> This was an anchoring design choice — it should not be
|
|
73
|
+
treated as permanently correct. A model that grows without discipline becomes a god object.
|
|
74
|
+
The countervailing test: <em>does any other view care about this state?</em> If no, keep it
|
|
75
|
+
local to the view. If yes, it belongs in the model.
|
|
76
|
+
</div>
|
|
77
|
+
<p class="principle-body" style="margin-top: 12px;">
|
|
78
|
+
<strong>Stay open to:</strong> View-local state that doesn't need to be shared is legitimate.
|
|
79
|
+
Urgency mode in <code>ComposerView</code>. Whether the compose overlay is open. Whether a
|
|
80
|
+
device picker is showing in <code>CallView</code>. These don't belong in <code>AppModel</code>
|
|
81
|
+
because nothing else observes them.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- ── 4. Inside-out development ─────────────────────────────────────────── -->
|
|
86
|
+
<div class="principle-card">
|
|
87
|
+
<p class="principle-num">04</p>
|
|
88
|
+
<h2 class="principle-title">Inside-out development</h2>
|
|
89
|
+
<p class="principle-body">
|
|
90
|
+
Build from the center outward: schema → core → service → transport → client.
|
|
91
|
+
Never build UI against an API that doesn't exist yet. The model or schema is the anchoring
|
|
92
|
+
technique — starting there forces clarity about the data before the interface.
|
|
93
|
+
</p>
|
|
94
|
+
<div class="principle-tension">
|
|
95
|
+
<strong>The tension:</strong> Product iteration sometimes demands a UI prototype before
|
|
96
|
+
the backend is defined. In those cases, use a stub server response and stub data in the
|
|
97
|
+
model — but acknowledge explicitly that the real API must be designed before the UI ships.
|
|
98
|
+
</div>
|
|
99
|
+
<p class="principle-body" style="margin-top: 12px;">
|
|
100
|
+
<strong>How to apply:</strong> Before touching a template or a view, ask: does the WS message
|
|
101
|
+
type exist? Does the model event exist? If no, add those first — even as empty stubs — so the
|
|
102
|
+
client has something real to integrate against.
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- ── 5. Progressive disclosure ─────────────────────────────────────────── -->
|
|
107
|
+
<div class="principle-card">
|
|
108
|
+
<p class="principle-num">05</p>
|
|
109
|
+
<h2 class="principle-title">Progressive disclosure</h2>
|
|
110
|
+
<p class="principle-body">
|
|
111
|
+
Don't show management controls until the user signals intent to manage something.
|
|
112
|
+
Gear icons on every channel were noise — they answered a question nobody was asking.
|
|
113
|
+
Right-click / long-press surfaces them only at the moment of intent.
|
|
114
|
+
</p>
|
|
115
|
+
<p class="principle-body">
|
|
116
|
+
This is a UI practice rooted in deference (HIG): the interface steps back until the user steps
|
|
117
|
+
forward. It reduces visual weight and keeps the primary use case — reading and writing messages
|
|
118
|
+
— uncluttered.
|
|
119
|
+
</p>
|
|
120
|
+
<div class="principle-tension">
|
|
121
|
+
<strong>The tension:</strong> Discoverability. If an action is hidden until right-click,
|
|
122
|
+
new users may not find it. Mitigate with contextual hints (cursor changes, tooltips,
|
|
123
|
+
empty states that teach the gesture) rather than always-visible controls.
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- ── 6. Single source of truth ─────────────────────────────────────────── -->
|
|
128
|
+
<div class="principle-card">
|
|
129
|
+
<p class="principle-num">06</p>
|
|
130
|
+
<h2 class="principle-title">Single source of truth per concern</h2>
|
|
131
|
+
<p class="principle-body">
|
|
132
|
+
Every concern has one owner. <code>localStorage</code> has one owner: <code>settings-sync.js</code>.
|
|
133
|
+
Channel state has one owner: <code>AppModel</code>. Theme state has one owner: <code>theme.js</code>.
|
|
134
|
+
This is an anchoring technique — it prevents concerns from becoming unwieldy as the codebase grows.
|
|
135
|
+
</p>
|
|
136
|
+
<p class="principle-body">
|
|
137
|
+
The alternative is scattered ownership: five views each reading <code>localStorage.getItem('theme')</code>,
|
|
138
|
+
each with slightly different key names and serialization. Any refactor requires finding and
|
|
139
|
+
updating all five.
|
|
140
|
+
</p>
|
|
141
|
+
<div class="principle-tension">
|
|
142
|
+
<strong>The tension:</strong> A single owner can become a bottleneck or a dumping ground.
|
|
143
|
+
Keep concerns narrow. <code>settings-sync.js</code> owns persistence preferences.
|
|
144
|
+
It should not own user identity or call state — those are different concerns with different
|
|
145
|
+
owners (<code>AppModel</code>, <code>CallView</code>).
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|