@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,236 @@
|
|
|
1
|
+
<h1 class="ds-page-title">Tokens</h1>
|
|
2
|
+
<p class="ds-page-subtitle">
|
|
3
|
+
CSS custom properties that define the visual language. All tokens are defined on
|
|
4
|
+
<code>:root</code> and override per theme. Switch the theme picker to see them update live.
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<!-- ── Colors ────────────────────────────────────────────────────────────── -->
|
|
8
|
+
<div class="ds-section">
|
|
9
|
+
<h2 class="ds-section-title">Backgrounds</h2>
|
|
10
|
+
<p class="ds-section-desc">Layered surfaces create the sense of depth (HIG: Depth).</p>
|
|
11
|
+
<div class="token-grid">
|
|
12
|
+
<div class="token-swatch">
|
|
13
|
+
<div class="token-swatch-preview" style="background: var(--bg-base)"></div>
|
|
14
|
+
<div class="token-swatch-label">
|
|
15
|
+
<span class="token-swatch-name">--bg-base</span>
|
|
16
|
+
<span class="token-swatch-value" data-css-var="--bg-base">—</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="token-swatch">
|
|
20
|
+
<div class="token-swatch-preview" style="background: var(--bg-sidebar)"></div>
|
|
21
|
+
<div class="token-swatch-label">
|
|
22
|
+
<span class="token-swatch-name">--bg-sidebar</span>
|
|
23
|
+
<span class="token-swatch-value" data-css-var="--bg-sidebar">—</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="token-swatch">
|
|
27
|
+
<div class="token-swatch-preview" style="background: var(--bg-elevated)"></div>
|
|
28
|
+
<div class="token-swatch-label">
|
|
29
|
+
<span class="token-swatch-name">--bg-elevated</span>
|
|
30
|
+
<span class="token-swatch-value" data-css-var="--bg-elevated">—</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="token-swatch">
|
|
34
|
+
<div class="token-swatch-preview" style="background: var(--bg-input)"></div>
|
|
35
|
+
<div class="token-swatch-label">
|
|
36
|
+
<span class="token-swatch-name">--bg-input</span>
|
|
37
|
+
<span class="token-swatch-value" data-css-var="--bg-input">—</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="token-swatch">
|
|
41
|
+
<div class="token-swatch-preview" style="background: var(--bg-hover)"></div>
|
|
42
|
+
<div class="token-swatch-label">
|
|
43
|
+
<span class="token-swatch-name">--bg-hover</span>
|
|
44
|
+
<span class="token-swatch-value" data-css-var="--bg-hover">—</span>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="token-swatch">
|
|
48
|
+
<div class="token-swatch-preview" style="background: var(--bg-active)"></div>
|
|
49
|
+
<div class="token-swatch-label">
|
|
50
|
+
<span class="token-swatch-name">--bg-active</span>
|
|
51
|
+
<span class="token-swatch-value" data-css-var="--bg-active">—</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="ds-section">
|
|
58
|
+
<h2 class="ds-section-title">Text</h2>
|
|
59
|
+
<p class="ds-section-desc">Three levels of hierarchy. Use muted only for metadata, never for actionable content.</p>
|
|
60
|
+
<div class="ds-canvas">
|
|
61
|
+
<p style="font-size: 15px; font-weight: 600; color: var(--text-primary); margin: 0 0 8px;">
|
|
62
|
+
--text-primary — Channel name, message handle, body text
|
|
63
|
+
</p>
|
|
64
|
+
<p style="font-size: 14px; color: var(--text-secondary); margin: 0 0 8px;">
|
|
65
|
+
--text-secondary — Descriptions, labels, nav items at rest
|
|
66
|
+
</p>
|
|
67
|
+
<p style="font-size: 13px; color: var(--text-muted); margin: 0;">
|
|
68
|
+
--text-muted — Timestamps, section headers, placeholder text
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="ds-section">
|
|
74
|
+
<h2 class="ds-section-title">Accent & Status</h2>
|
|
75
|
+
<div class="token-grid">
|
|
76
|
+
<div class="token-swatch">
|
|
77
|
+
<div class="token-swatch-preview" style="background: var(--accent)"></div>
|
|
78
|
+
<div class="token-swatch-label">
|
|
79
|
+
<span class="token-swatch-name">--accent</span>
|
|
80
|
+
<span class="token-swatch-value" data-css-var="--accent">—</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="token-swatch">
|
|
84
|
+
<div class="token-swatch-preview" style="background: var(--color-success)"></div>
|
|
85
|
+
<div class="token-swatch-label">
|
|
86
|
+
<span class="token-swatch-name">--color-success</span>
|
|
87
|
+
<span class="token-swatch-value" data-css-var="--color-success">—</span>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="token-swatch">
|
|
91
|
+
<div class="token-swatch-preview" style="background: var(--color-danger)"></div>
|
|
92
|
+
<div class="token-swatch-label">
|
|
93
|
+
<span class="token-swatch-name">--color-danger</span>
|
|
94
|
+
<span class="token-swatch-value" data-css-var="--color-danger">—</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="token-swatch">
|
|
98
|
+
<div class="token-swatch-preview" style="background: var(--color-warning)"></div>
|
|
99
|
+
<div class="token-swatch-label">
|
|
100
|
+
<span class="token-swatch-name">--color-warning</span>
|
|
101
|
+
<span class="token-swatch-value" data-css-var="--color-warning">—</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="token-swatch">
|
|
105
|
+
<div class="token-swatch-preview" style="background: var(--border)"></div>
|
|
106
|
+
<div class="token-swatch-label">
|
|
107
|
+
<span class="token-swatch-name">--border</span>
|
|
108
|
+
<span class="token-swatch-value" data-css-var="--border">—</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<hr class="ds-divider">
|
|
115
|
+
|
|
116
|
+
<!-- ── Typography ─────────────────────────────────────────────────────────── -->
|
|
117
|
+
<div class="ds-section">
|
|
118
|
+
<h2 class="ds-section-title">Typography</h2>
|
|
119
|
+
<p class="ds-section-desc">System fonts. SF Pro on Apple, Segoe UI on Windows — no web font download required.</p>
|
|
120
|
+
|
|
121
|
+
<div class="ds-canvas">
|
|
122
|
+
<div class="token-row">
|
|
123
|
+
<div class="token-row-preview" style="background: var(--bg-hover); display: flex; align-items: center; justify-content: center;">
|
|
124
|
+
<span style="font-family: var(--font-sans); font-size: 13px; font-weight: 600;">Aa</span>
|
|
125
|
+
</div>
|
|
126
|
+
<span class="token-row-name">--font-sans</span>
|
|
127
|
+
<span class="token-row-value">-apple-system, SF Pro Text, system-ui, Segoe UI, sans-serif</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="token-row">
|
|
130
|
+
<div class="token-row-preview" style="background: var(--bg-hover); display: flex; align-items: center; justify-content: center;">
|
|
131
|
+
<span style="font-family: var(--font-mono); font-size: 13px; font-weight: 600;">01</span>
|
|
132
|
+
</div>
|
|
133
|
+
<span class="token-row-name">--font-mono</span>
|
|
134
|
+
<span class="token-row-value">SF Mono, ui-monospace, Cascadia Code, Fira Code, JetBrains Mono</span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="ds-canvas" style="margin-top: 12px;">
|
|
139
|
+
<p style="font-size: 20px; font-weight: 700; margin: 0 0 6px; line-height: 1.2;">Page title — 20px/700</p>
|
|
140
|
+
<p style="font-size: 17px; font-weight: 600; margin: 0 0 6px; line-height: 1.3;">Section title — 17px/600</p>
|
|
141
|
+
<p style="font-size: 15px; font-weight: 500; margin: 0 0 6px;">Message handle — 15px/500</p>
|
|
142
|
+
<p style="font-size: 14px; color: var(--text-secondary); margin: 0 0 6px; line-height: 1.5;">Body text — 14px/400, --text-secondary for descriptions</p>
|
|
143
|
+
<p style="font-size: 13px; color: var(--text-secondary); margin: 0 0 6px;">UI label — 13px/400, most controls</p>
|
|
144
|
+
<p style="font-size: 12px; color: var(--text-muted); margin: 0 0 6px;">Timestamp / metadata — 12px/400, --text-muted</p>
|
|
145
|
+
<p style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin: 0;">Section label — 11px/600 uppercase</p>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<hr class="ds-divider">
|
|
150
|
+
|
|
151
|
+
<!-- ── Radii ──────────────────────────────────────────────────────────────── -->
|
|
152
|
+
<div class="ds-section">
|
|
153
|
+
<h2 class="ds-section-title">Border Radius</h2>
|
|
154
|
+
<p class="ds-section-desc">Consistent rounding communicates surface type and elevation.</p>
|
|
155
|
+
<div class="ds-canvas ds-canvas--grid">
|
|
156
|
+
<div style="text-align: center;">
|
|
157
|
+
<div style="width: 60px; height: 60px; background: var(--accent); border-radius: var(--radius-sm); margin: 0 auto 8px;"></div>
|
|
158
|
+
<code style="font-size: 11px;">--radius-sm<br>6px</code>
|
|
159
|
+
</div>
|
|
160
|
+
<div style="text-align: center;">
|
|
161
|
+
<div style="width: 60px; height: 60px; background: var(--accent); border-radius: var(--radius-md); margin: 0 auto 8px;"></div>
|
|
162
|
+
<code style="font-size: 11px;">--radius-md<br>10px</code>
|
|
163
|
+
</div>
|
|
164
|
+
<div style="text-align: center;">
|
|
165
|
+
<div style="width: 60px; height: 60px; background: var(--accent); border-radius: var(--radius-lg); margin: 0 auto 8px;"></div>
|
|
166
|
+
<code style="font-size: 11px;">--radius-lg<br>14px</code>
|
|
167
|
+
</div>
|
|
168
|
+
<div style="text-align: center;">
|
|
169
|
+
<div style="width: 60px; height: 60px; background: var(--accent); border-radius: var(--radius-xl); margin: 0 auto 8px;"></div>
|
|
170
|
+
<code style="font-size: 11px;">--radius-xl<br>20px</code>
|
|
171
|
+
</div>
|
|
172
|
+
<div style="text-align: center;">
|
|
173
|
+
<div style="width: 60px; height: 60px; background: var(--accent); border-radius: var(--radius-pill); margin: 0 auto 8px;"></div>
|
|
174
|
+
<code style="font-size: 11px;">--radius-pill<br>999px</code>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="ds-annotation">
|
|
178
|
+
<strong>Usage guide:</strong> sm → nav items, code blocks, small chips. md → inputs, popovers.
|
|
179
|
+
lg → panels, modals, cards. xl → large overlays, sheets. pill → tags, reaction pills, primary buttons.
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<hr class="ds-divider">
|
|
184
|
+
|
|
185
|
+
<!-- ── Shadows ─────────────────────────────────────────────────────────────── -->
|
|
186
|
+
<div class="ds-section">
|
|
187
|
+
<h2 class="ds-section-title">Shadows</h2>
|
|
188
|
+
<p class="ds-section-desc">Used for elevation — overlays, floating elements, modals.</p>
|
|
189
|
+
<div class="ds-canvas ds-canvas--grid" style="gap: 32px;">
|
|
190
|
+
<div style="text-align: center;">
|
|
191
|
+
<div style="width: 80px; height: 80px; background: var(--bg-elevated); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); margin: 0 auto 12px;"></div>
|
|
192
|
+
<code style="font-size: 11px; color: var(--text-muted);">--shadow-sm<br>nav items, small chips</code>
|
|
193
|
+
</div>
|
|
194
|
+
<div style="text-align: center;">
|
|
195
|
+
<div style="width: 80px; height: 80px; background: var(--bg-elevated); border-radius: var(--radius-md); box-shadow: var(--shadow-md); margin: 0 auto 12px;"></div>
|
|
196
|
+
<code style="font-size: 11px; color: var(--text-muted);">--shadow-md<br>popovers, context menus</code>
|
|
197
|
+
</div>
|
|
198
|
+
<div style="text-align: center;">
|
|
199
|
+
<div style="width: 80px; height: 80px; background: var(--bg-elevated); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); margin: 0 auto 12px;"></div>
|
|
200
|
+
<code style="font-size: 11px; color: var(--text-muted);">--shadow-lg<br>modals, sheets</code>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<hr class="ds-divider">
|
|
206
|
+
|
|
207
|
+
<!-- ── Motion ─────────────────────────────────────────────────────────────── -->
|
|
208
|
+
<div class="ds-section">
|
|
209
|
+
<h2 class="ds-section-title">Motion</h2>
|
|
210
|
+
<p class="ds-section-desc">One transition token. Use it for all interactive state changes.</p>
|
|
211
|
+
<div class="ds-canvas">
|
|
212
|
+
<div class="token-row" style="border: none; padding: 0;">
|
|
213
|
+
<span class="token-row-name">--transition</span>
|
|
214
|
+
<span class="token-row-value">200ms cubic-bezier(0.25, 0.1, 0.25, 1)</span>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="ds-annotation">
|
|
218
|
+
<strong>Refactoring opportunity:</strong> Several components hardcode their own transition values
|
|
219
|
+
instead of using <code>var(--transition)</code>. When adding new interactive elements, always
|
|
220
|
+
use the token. Consider a pass through <code>base.css</code> to replace hardcoded transitions.
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<script>
|
|
225
|
+
// Read computed CSS variable values and show them under swatches
|
|
226
|
+
document.querySelectorAll('[data-css-var]').forEach(el => {
|
|
227
|
+
const update = () => {
|
|
228
|
+
const val = getComputedStyle(document.documentElement)
|
|
229
|
+
.getPropertyValue(el.dataset.cssVar).trim()
|
|
230
|
+
el.textContent = val || '—'
|
|
231
|
+
}
|
|
232
|
+
update()
|
|
233
|
+
// Re-read on theme change (theme.js swaps the stylesheet link)
|
|
234
|
+
document.getElementById('theme-stylesheet')?.addEventListener('load', update)
|
|
235
|
+
})
|
|
236
|
+
</script>
|
|
@@ -19,11 +19,13 @@ import { ThreadPanelView } from './views/ThreadPanelView.js'
|
|
|
19
19
|
import { ComposerView } from './views/ComposerView.js'
|
|
20
20
|
import { SidebarView } from './views/SidebarView.js'
|
|
21
21
|
import { CallView } from './views/CallView.js'
|
|
22
|
+
import { ChatHeaderView } from './views/ChatHeaderView.js'
|
|
22
23
|
import { cancelActiveEdit } from './views/shared/MessageInteractions.js'
|
|
23
24
|
import { closeEmojiPicker } from './views/shared/EmojiPickerSingleton.js'
|
|
24
25
|
import { getSettings, syncFromServer, patchSettings } from './settings-sync.js'
|
|
25
26
|
import { initSwipeNav } from './swipe-nav.js'
|
|
26
27
|
import { initRouter } from './router.js'
|
|
28
|
+
import { attachResizeHandle } from './resizable.js'
|
|
27
29
|
|
|
28
30
|
const BASE_PATH = window.__BASE_PATH__ ?? ''
|
|
29
31
|
|
|
@@ -68,7 +70,7 @@ if (chatPanelEl) {
|
|
|
68
70
|
|
|
69
71
|
const wsController = new WebSocketController(model, `${BASE_PATH}/ws`)
|
|
70
72
|
const ws = wsController.ws
|
|
71
|
-
|
|
73
|
+
new ChatController(model, ws)
|
|
72
74
|
|
|
73
75
|
// ── 4. Views ──────────────────────────────────────────────────────────────────
|
|
74
76
|
|
|
@@ -85,12 +87,24 @@ if (chatPanelEl) {
|
|
|
85
87
|
const composerEl = chatPanelEl.querySelector('.composer')
|
|
86
88
|
const threadPanelEl = document.getElementById('thread-panel')
|
|
87
89
|
|
|
90
|
+
const headerEl = chatPanelEl.querySelector('.chat-header')
|
|
91
|
+
if (headerEl) {
|
|
92
|
+
new ChatHeaderView(model, headerEl)
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
if (messagesEl) {
|
|
89
96
|
new MessageListView(model, messagesEl, sentinelEl)
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
if (threadPanelEl) {
|
|
93
100
|
new ThreadPanelView(model, threadPanelEl)
|
|
101
|
+
attachResizeHandle(threadPanelEl, {
|
|
102
|
+
edge: 'left',
|
|
103
|
+
cssVar: '--thread-panel-width',
|
|
104
|
+
min: 260,
|
|
105
|
+
max: 640,
|
|
106
|
+
prefKey: 'thread_panel_width',
|
|
107
|
+
})
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
if (composerEl) {
|
|
@@ -98,11 +112,32 @@ if (chatPanelEl) {
|
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
// CallView (WebRTC) — only if call UI elements are present
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
const tilePanelEl = document.getElementById('tile-panel')
|
|
116
|
+
if (tilePanelEl || document.getElementById('btn-start-call')) {
|
|
117
|
+
new CallView(model, ws)
|
|
118
|
+
}
|
|
119
|
+
if (tilePanelEl) {
|
|
120
|
+
attachResizeHandle(tilePanelEl, {
|
|
121
|
+
edge: 'left',
|
|
122
|
+
cssVar: '--tile-panel-width',
|
|
123
|
+
min: 200,
|
|
124
|
+
max: 640,
|
|
125
|
+
prefKey: 'tile_panel_width',
|
|
126
|
+
})
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
129
|
|
|
130
|
+
// Sidebar resize — desktop only (mobile sidebar is full-screen overlay)
|
|
131
|
+
if (sidebarEl && window.matchMedia('(min-width: 769px)').matches) {
|
|
132
|
+
attachResizeHandle(sidebarEl, {
|
|
133
|
+
edge: 'right',
|
|
134
|
+
cssVar: '--sidebar-width',
|
|
135
|
+
min: 180,
|
|
136
|
+
max: 480,
|
|
137
|
+
prefKey: 'sidebar_width',
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
106
141
|
// ── 5. SPA navigation ─────────────────────────────────────────────────────────
|
|
107
142
|
|
|
108
143
|
// router.js morphs the DOM and dispatches 'chatpanel:navigated'.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resizable.js — attach a drag-to-resize handle to a panel edge.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* attachResizeHandle(el, {
|
|
6
|
+
* edge: 'right' | 'left', // which edge gets the handle
|
|
7
|
+
* cssVar: '--sidebar-width', // CSS custom property to update on :root
|
|
8
|
+
* min: 180, // minimum width in px
|
|
9
|
+
* max: 600, // maximum width in px
|
|
10
|
+
* prefKey: 'sidebar_width', // settings-sync pref key (omit to skip persistence)
|
|
11
|
+
* })
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getPref, setPref } from './settings-sync.js'
|
|
15
|
+
|
|
16
|
+
export function attachResizeHandle(el, { edge, cssVar, min = 160, max = 700, prefKey } = {}) {
|
|
17
|
+
// Restore persisted width
|
|
18
|
+
if (prefKey) {
|
|
19
|
+
const saved = getPref(prefKey)
|
|
20
|
+
if (saved != null) {
|
|
21
|
+
const px = parseInt(saved, 10)
|
|
22
|
+
if (px >= min && px <= max) {
|
|
23
|
+
document.documentElement.style.setProperty(cssVar, `${px}px`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const handle = document.createElement('div')
|
|
29
|
+
handle.className = `resize-handle resize-handle--${edge}`
|
|
30
|
+
handle.setAttribute('aria-hidden', 'true')
|
|
31
|
+
el.appendChild(handle)
|
|
32
|
+
|
|
33
|
+
let startX = 0
|
|
34
|
+
let startWidth = 0
|
|
35
|
+
|
|
36
|
+
const onMove = e => {
|
|
37
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
38
|
+
const dx = clientX - startX
|
|
39
|
+
const newWidth = edge === 'right'
|
|
40
|
+
? Math.max(min, Math.min(max, startWidth + dx))
|
|
41
|
+
: Math.max(min, Math.min(max, startWidth - dx))
|
|
42
|
+
document.documentElement.style.setProperty(cssVar, `${newWidth}px`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onEnd = () => {
|
|
46
|
+
document.removeEventListener('mousemove', onMove)
|
|
47
|
+
document.removeEventListener('mouseup', onEnd)
|
|
48
|
+
document.removeEventListener('touchmove', onMove)
|
|
49
|
+
document.removeEventListener('touchend', onEnd)
|
|
50
|
+
document.body.style.userSelect = ''
|
|
51
|
+
document.body.style.cursor = ''
|
|
52
|
+
|
|
53
|
+
if (prefKey) {
|
|
54
|
+
const current = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim()
|
|
55
|
+
setPref(prefKey, parseInt(current, 10))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const onStart = e => {
|
|
60
|
+
if (e.button != null && e.button !== 0) return
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
startX = e.touches ? e.touches[0].clientX : e.clientX
|
|
63
|
+
startWidth = el.getBoundingClientRect().width
|
|
64
|
+
document.body.style.userSelect = 'none'
|
|
65
|
+
document.body.style.cursor = 'col-resize'
|
|
66
|
+
document.addEventListener('mousemove', onMove)
|
|
67
|
+
document.addEventListener('mouseup', onEnd)
|
|
68
|
+
document.addEventListener('touchmove', onMove, { passive: false })
|
|
69
|
+
document.addEventListener('touchend', onEnd)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handle.addEventListener('mousedown', onStart)
|
|
73
|
+
handle.addEventListener('touchstart', onStart, { passive: false })
|
|
74
|
+
}
|
|
@@ -86,8 +86,11 @@ export class RtcPeerManager {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Video stream → render tile + ensure audio (video stream may carry audio track)
|
|
89
|
-
|
|
90
|
-
const
|
|
89
|
+
// Use semantic slot name (cam/screen) so WS signaling can address tiles by kind.
|
|
90
|
+
const slots = this.#getTransceiverSlots(pc)
|
|
91
|
+
const slotName = event.transceiver === slots.screen ? 'screen' : 'cam'
|
|
92
|
+
const tileId = `${peerId}-${slotName}`
|
|
93
|
+
const label = this.#displayNames.get(peerId) ?? peerId
|
|
91
94
|
this.#handlers.onTrack(peerId, tileId, stream, label)
|
|
92
95
|
this.#handlers.onAudio(peerId, stream)
|
|
93
96
|
}
|
|
@@ -1,25 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* settings-sync.js — client-side
|
|
2
|
+
* settings-sync.js — all client-side persistence in one place.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two namespaces:
|
|
5
|
+
* settings — synced to server (last_channel_id, mobile_chat_open, …)
|
|
6
|
+
* prefs — local-only UI preferences (theme, panel widths, devices, …)
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* Callers never touch localStorage directly — they use the exports below.
|
|
9
|
+
*
|
|
10
|
+
* Storage keys:
|
|
11
|
+
* devchitchat_settings { settings: {…}, updated_at: number }
|
|
12
|
+
* devchitchat_prefs { theme, sidebar_width, thread_panel_width,
|
|
13
|
+
* tile_panel_width, tile_layout, devices, … }
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
|
-
const
|
|
16
|
+
const SETTINGS_KEY = 'devchitchat_settings'
|
|
17
|
+
const PREFS_KEY = 'devchitchat_prefs'
|
|
11
18
|
const BASE_PATH = window.__BASE_PATH__ ?? ''
|
|
12
19
|
|
|
13
20
|
function readLocal() {
|
|
14
21
|
try {
|
|
15
|
-
return JSON.parse(localStorage.getItem(
|
|
22
|
+
return JSON.parse(localStorage.getItem(SETTINGS_KEY) ?? '{}')
|
|
16
23
|
} catch {
|
|
17
24
|
return {}
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
function writeLocal(data) {
|
|
22
|
-
localStorage.setItem(
|
|
29
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(data))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Local-only UI preferences ─────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function readPrefs() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(localStorage.getItem(PREFS_KEY) ?? '{}')
|
|
37
|
+
} catch {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read a UI preference. Returns `defaultValue` when the key has never been set.
|
|
44
|
+
* @param {string} key
|
|
45
|
+
* @param {*} [defaultValue]
|
|
46
|
+
*/
|
|
47
|
+
export function getPref(key, defaultValue = null) {
|
|
48
|
+
const prefs = readPrefs()
|
|
49
|
+
return key in prefs ? prefs[key] : defaultValue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write one or more UI preferences.
|
|
54
|
+
* @param {string|Record<string,*>} keyOrPatch — key string or { key: value } map
|
|
55
|
+
* @param {*} [value] — value when keyOrPatch is a string
|
|
56
|
+
*/
|
|
57
|
+
export function setPref(keyOrPatch, value) {
|
|
58
|
+
const prefs = readPrefs()
|
|
59
|
+
const patch = typeof keyOrPatch === 'string' ? { [keyOrPatch]: value } : keyOrPatch
|
|
60
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify({ ...prefs, ...patch }))
|
|
23
61
|
}
|
|
24
62
|
|
|
25
63
|
// Returns current settings object (instant, synchronous)
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each theme is a separate CSS file in /themes/<name>.css.
|
|
5
5
|
* The <html data-theme> attribute is set so themes can also use attribute selectors.
|
|
6
|
+
* Theme preference is stored via settings-sync getPref/setPref ('theme' key).
|
|
6
7
|
*/
|
|
8
|
+
import { getPref, setPref } from './settings-sync.js'
|
|
9
|
+
|
|
7
10
|
const THEMES = ['dark', 'light', 'ocean', 'forest', 'rose']
|
|
8
|
-
const STORAGE_KEY = 'devchitchat_theme'
|
|
9
11
|
const BASE_PATH = window.__BASE_PATH__ ?? ''
|
|
10
12
|
const stylesheet = document.getElementById('theme-stylesheet')
|
|
11
13
|
const picker = document.getElementById('theme-picker')
|
|
@@ -15,13 +17,13 @@ function applyTheme(name) {
|
|
|
15
17
|
document.documentElement.dataset.theme = theme
|
|
16
18
|
if (stylesheet) stylesheet.href = `${BASE_PATH}/themes/${theme}.css`
|
|
17
19
|
if (picker) picker.value = theme
|
|
18
|
-
|
|
20
|
+
setPref('theme', theme)
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// Restore saved theme immediately (before paint)
|
|
22
|
-
applyTheme(
|
|
24
|
+
applyTheme(getPref('theme', 'dark'))
|
|
23
25
|
|
|
24
26
|
// Wire picker
|
|
25
27
|
if (picker) {
|
|
26
|
-
picker.addEventListener('change',
|
|
28
|
+
picker.addEventListener('change', e => applyTheme(e.target.value))
|
|
27
29
|
}
|