@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.
@@ -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">&lt;button class="btn-primary" type="button"&gt;Send&lt;/button&gt;</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">&lt;button class="btn-ghost" type="button"&gt;Cancel&lt;/button&gt;
29
+ &lt;button class="btn-ghost btn-sm" type="button"&gt;Admin&lt;/button&gt;</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">&#x21A9;</button>
34
+ <button class="btn-icon" type="button" title="React">🙂</button>
35
+ <button class="btn-icon" type="button" title="More">&#8943;</button>
36
+ <button class="btn-icon" type="button" title="Add">+</button>
37
+ <button class="btn-icon" type="button" title="Close">&#x2715;</button>
38
+ <button class="btn-icon" type="button" title="Settings">&#9881;</button>
39
+ </div>
40
+ <div class="ds-code">&lt;button class="btn-icon" type="button" title="Reply"&gt;&#x21A9;&lt;/button&gt;</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">&lt;div class="field"&gt;
73
+ &lt;label for="ch-name"&gt;Channel name&lt;/label&gt;
74
+ &lt;input id="ch-name" type="text" placeholder="e.g. general" autocomplete="off"&gt;
75
+ &lt;/div&gt;</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">&lt;span class="tag"&gt;User&lt;/span&gt;
102
+ &lt;span class="tag tag--admin"&gt;Admin&lt;/span&gt;
103
+ &lt;span class="tag tag--bot"&gt;Bot&lt;/span&gt;
104
+ &lt;span class="tag tag--warn"&gt;Guest&lt;/span&gt;</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">&lt;article class="message" data-seq="…" data-msg-id="…" data-user-id="…"&gt;
154
+ &lt;span class="message-handle" data-user-id="…"&gt;Alice&lt;/span&gt;
155
+ &lt;time class="message-time" datetime="…"&gt;10:00 AM&lt;/time&gt;
156
+ &lt;div class="message-text"&gt;…&lt;/div&gt;
157
+ &lt;div class="reaction-bar"&gt;&lt;!-- populated by renderReactionBar() --&gt;&lt;/div&gt;
158
+ &lt;/article&gt;</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">&lt;!-- reacted = current user has reacted --&gt;
173
+ &lt;button class="reaction-pill reacted" …&gt;👍 &lt;span class="reaction-count"&gt;3&lt;/span&gt;&lt;/button&gt;
174
+ &lt;button class="reaction-pill" …&gt;🔥 &lt;span class="reaction-count"&gt;1&lt;/span&gt;&lt;/button&gt;</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>&lt;details&gt;</code>/<code>&lt;summary&gt;</code> for native collapse.
184
+ Channel items are <code>&lt;li&gt;</code> inside <code>&lt;ul class="channel-list"&gt;</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">&lt;div class="msg-context-menu" role="menu"&gt;
235
+ &lt;button class="msg-context-menu-item" role="menuitem"&gt;React&lt;/button&gt;
236
+ &lt;button class="msg-context-menu-item msg-context-menu-item--danger" role="menuitem"&gt;Delete&lt;/button&gt;
237
+ &lt;/div&gt;</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">&#x2715;</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 =&gt; {
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,3 @@
1
+ export function GET() {
2
+ return { activeOverview: true }
3
+ }
@@ -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,3 @@
1
+ export function GET() {
2
+ return { activePrinciples: true }
3
+ }
@@ -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>
@@ -0,0 +1,3 @@
1
+ export function GET() {
2
+ return { activeTokens: true }
3
+ }