@openpalm/channel-voice 0.9.0

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/web/styles.css ADDED
@@ -0,0 +1,552 @@
1
+ /* ================================================================
2
+ OpenPalm Voice — Styles
3
+ Matches setup wizard design system (light theme, orange primary)
4
+ ================================================================ */
5
+
6
+ /* --- Custom Properties --- */
7
+ :root {
8
+ --color-primary: #ff9d00;
9
+ --color-primary-hover: #e68a00;
10
+ --color-bg: #ffffff;
11
+ --color-bg-secondary: #f8f9fb;
12
+ --color-text: #1a1a1a;
13
+ --color-text-secondary: #6b7280;
14
+ --color-border: #e5e7eb;
15
+ --color-red: #dc2626;
16
+ --color-red-bg: #fef2f2;
17
+ --color-blue: #2563eb;
18
+ --color-blue-bg: #eff6ff;
19
+ --color-green: #16a34a;
20
+ --color-green-bg: #f0fdf4;
21
+ --color-gray: #9ca3af;
22
+ --color-gray-bg: #f9fafb;
23
+
24
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
25
+ --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
26
+
27
+ --radius-sm: 6px;
28
+ --radius-md: 8px;
29
+ --radius-lg: 12px;
30
+ --radius-pill: 9999px;
31
+
32
+ --space-1: 4px;
33
+ --space-2: 8px;
34
+ --space-3: 12px;
35
+ --space-4: 16px;
36
+ --space-5: 20px;
37
+ --space-6: 24px;
38
+ --space-7: 28px;
39
+ --space-8: 32px;
40
+ }
41
+
42
+ /* --- Reset --- */
43
+ *,
44
+ *::before,
45
+ *::after {
46
+ box-sizing: border-box;
47
+ margin: 0;
48
+ padding: 0;
49
+ }
50
+
51
+ html, body {
52
+ height: 100%;
53
+ }
54
+
55
+ body {
56
+ font-family: var(--font-sans);
57
+ font-size: 15px;
58
+ line-height: 1.5;
59
+ color: var(--color-text);
60
+ background: var(--color-bg-secondary);
61
+ -webkit-font-smoothing: antialiased;
62
+ -moz-osx-font-smoothing: grayscale;
63
+ }
64
+
65
+ /* --- Screen Reader Only --- */
66
+ .sr-only {
67
+ position: absolute;
68
+ width: 1px;
69
+ height: 1px;
70
+ padding: 0;
71
+ margin: -1px;
72
+ overflow: hidden;
73
+ clip: rect(0, 0, 0, 0);
74
+ white-space: nowrap;
75
+ border: 0;
76
+ }
77
+
78
+ /* --- App Layout --- */
79
+ .app {
80
+ display: flex;
81
+ flex-direction: column;
82
+ height: 100%;
83
+ max-width: 480px;
84
+ margin: 0 auto;
85
+ background: var(--color-bg);
86
+ border-left: 1px solid var(--color-border);
87
+ border-right: 1px solid var(--color-border);
88
+ }
89
+
90
+ /* --- Header --- */
91
+ .header {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ padding: var(--space-4) var(--space-5);
96
+ border-bottom: 1px solid var(--color-border);
97
+ flex-shrink: 0;
98
+ }
99
+
100
+ .header-brand {
101
+ font-size: 18px;
102
+ font-weight: 600;
103
+ letter-spacing: -0.01em;
104
+ }
105
+
106
+ .brand-slash {
107
+ color: var(--color-primary);
108
+ }
109
+
110
+ .brand-name {
111
+ color: var(--color-text);
112
+ }
113
+
114
+ .header-right {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--space-3);
118
+ }
119
+
120
+ .status-indicator {
121
+ font-size: 12px;
122
+ font-weight: 500;
123
+ color: var(--color-text-secondary);
124
+ padding: var(--space-1) var(--space-3);
125
+ background: var(--color-bg-secondary);
126
+ border-radius: var(--radius-pill);
127
+ border: 1px solid var(--color-border);
128
+ }
129
+
130
+ .icon-btn {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ width: 36px;
135
+ height: 36px;
136
+ border: 1px solid var(--color-border);
137
+ border-radius: var(--radius-md);
138
+ background: var(--color-bg);
139
+ color: var(--color-text-secondary);
140
+ cursor: pointer;
141
+ transition: background 0.15s, color 0.15s;
142
+ }
143
+
144
+ .icon-btn:hover {
145
+ background: var(--color-bg-secondary);
146
+ color: var(--color-text);
147
+ }
148
+
149
+ .icon-btn:focus-visible {
150
+ outline: 2px solid var(--color-primary);
151
+ outline-offset: 2px;
152
+ }
153
+
154
+ /* --- Conversation Log --- */
155
+ .log {
156
+ flex: 1;
157
+ overflow-y: auto;
158
+ padding: var(--space-4) var(--space-5);
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: var(--space-2);
162
+ }
163
+
164
+ .log:empty::before {
165
+ content: 'Tap the microphone to start';
166
+ display: block;
167
+ text-align: center;
168
+ color: var(--color-text-secondary);
169
+ font-size: 14px;
170
+ margin-top: var(--space-8);
171
+ }
172
+
173
+ .log-entry {
174
+ padding: var(--space-2) var(--space-3);
175
+ border-radius: var(--radius-md);
176
+ font-size: 14px;
177
+ line-height: 1.6;
178
+ word-break: break-word;
179
+ }
180
+
181
+ .log-entry[data-level="YOU"] {
182
+ background: #fff7ed;
183
+ border-left: 3px solid var(--color-primary);
184
+ color: var(--color-text);
185
+ }
186
+
187
+ .log-entry[data-level="AI"] {
188
+ background: var(--color-bg-secondary);
189
+ border-left: 3px solid var(--color-text);
190
+ color: var(--color-text);
191
+ }
192
+
193
+ .log-entry[data-level="ERR"] {
194
+ background: var(--color-red-bg);
195
+ border-left: 3px solid var(--color-red);
196
+ color: var(--color-red);
197
+ }
198
+
199
+ .log-entry[data-level="SYS"] {
200
+ background: var(--color-gray-bg);
201
+ border-left: 3px solid var(--color-gray);
202
+ color: var(--color-text-secondary);
203
+ font-size: 13px;
204
+ }
205
+
206
+ .log-entry[data-level="TX"] {
207
+ background: var(--color-blue-bg);
208
+ border-left: 3px solid var(--color-blue);
209
+ color: var(--color-blue);
210
+ font-family: var(--font-mono);
211
+ font-size: 12px;
212
+ }
213
+
214
+ /* Rendered markdown in AI responses */
215
+ .log-entry code {
216
+ font-family: var(--font-mono);
217
+ font-size: 0.9em;
218
+ background: rgba(0, 0, 0, 0.06);
219
+ padding: 1px 5px;
220
+ border-radius: 3px;
221
+ }
222
+
223
+ .log-entry pre {
224
+ margin: var(--space-2) 0;
225
+ padding: var(--space-3);
226
+ background: var(--color-bg-secondary);
227
+ border: 1px solid var(--color-border);
228
+ border-radius: var(--radius-sm);
229
+ overflow-x: auto;
230
+ }
231
+
232
+ .log-entry pre code {
233
+ background: none;
234
+ padding: 0;
235
+ font-size: 13px;
236
+ }
237
+
238
+ .log-entry strong {
239
+ font-weight: 600;
240
+ }
241
+
242
+ .log-label {
243
+ font-weight: 600;
244
+ font-size: 11px;
245
+ text-transform: uppercase;
246
+ letter-spacing: 0.05em;
247
+ margin-right: var(--space-2);
248
+ opacity: 0.7;
249
+ }
250
+
251
+ /* --- Controls --- */
252
+ .controls {
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ gap: var(--space-4);
257
+ padding: var(--space-6) var(--space-5);
258
+ flex-shrink: 0;
259
+ }
260
+
261
+ .continuous-btn {
262
+ width: 40px;
263
+ height: 40px;
264
+ border-radius: 50%;
265
+ border: 2px solid var(--color-border);
266
+ background: var(--color-bg);
267
+ color: var(--color-text-secondary);
268
+ cursor: pointer;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ transition: background 0.2s, border-color 0.2s, color 0.2s;
273
+ }
274
+
275
+ .continuous-btn:hover {
276
+ border-color: var(--color-primary);
277
+ color: var(--color-primary);
278
+ }
279
+
280
+ .continuous-btn:focus-visible {
281
+ outline: 2px solid var(--color-primary);
282
+ outline-offset: 2px;
283
+ }
284
+
285
+ .continuous-btn[aria-pressed="true"] {
286
+ border-color: var(--color-primary);
287
+ background: var(--color-primary);
288
+ color: var(--color-bg);
289
+ }
290
+
291
+ .record-btn {
292
+ position: relative;
293
+ width: 72px;
294
+ height: 72px;
295
+ border-radius: 50%;
296
+ border: 3px solid var(--color-primary);
297
+ background: var(--color-bg);
298
+ color: var(--color-primary);
299
+ cursor: pointer;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.1s;
304
+ }
305
+
306
+ .record-btn:hover {
307
+ background: #fff7ed;
308
+ transform: scale(1.04);
309
+ }
310
+
311
+ .record-btn:active {
312
+ transform: scale(0.96);
313
+ }
314
+
315
+ .record-btn:focus-visible {
316
+ outline: 2px solid var(--color-primary);
317
+ outline-offset: 4px;
318
+ }
319
+
320
+ .record-btn .spinner {
321
+ display: none;
322
+ }
323
+
324
+ /* Recording state */
325
+ .record-btn[data-state="recording"] {
326
+ border-color: var(--color-red);
327
+ background: var(--color-red);
328
+ color: var(--color-bg);
329
+ animation: pulse 1.5s ease-in-out infinite;
330
+ }
331
+
332
+ .record-btn[data-state="recording"]:hover {
333
+ background: #b91c1c;
334
+ border-color: #b91c1c;
335
+ }
336
+
337
+ /* Processing state */
338
+ .record-btn[data-state="processing"] {
339
+ border-color: var(--color-primary);
340
+ background: var(--color-primary);
341
+ color: var(--color-bg);
342
+ cursor: wait;
343
+ pointer-events: none;
344
+ }
345
+
346
+ .record-btn[data-state="processing"] .mic-icon {
347
+ display: none;
348
+ }
349
+
350
+ .record-btn[data-state="processing"] .spinner {
351
+ display: block;
352
+ width: 24px;
353
+ height: 24px;
354
+ border: 3px solid rgba(255, 255, 255, 0.3);
355
+ border-top-color: var(--color-bg);
356
+ border-radius: 50%;
357
+ animation: spin 0.8s linear infinite;
358
+ }
359
+
360
+ /* --- Animations --- */
361
+ @keyframes pulse {
362
+ 0%, 100% {
363
+ box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.4);
364
+ }
365
+ 50% {
366
+ box-shadow: 0 0 0 12px rgba(220, 38, 38, 0);
367
+ }
368
+ }
369
+
370
+ @keyframes spin {
371
+ to {
372
+ transform: rotate(360deg);
373
+ }
374
+ }
375
+
376
+ @media (prefers-reduced-motion: reduce) {
377
+ .record-btn[data-state="recording"] {
378
+ animation: none;
379
+ box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.25);
380
+ }
381
+
382
+ .record-btn[data-state="processing"] .spinner {
383
+ animation-duration: 2s;
384
+ }
385
+
386
+ .record-btn:hover {
387
+ transform: none;
388
+ }
389
+
390
+ .record-btn:active {
391
+ transform: none;
392
+ }
393
+ }
394
+
395
+ /* --- Footer --- */
396
+ .footer {
397
+ padding: var(--space-3) var(--space-5);
398
+ border-top: 1px solid var(--color-border);
399
+ text-align: center;
400
+ flex-shrink: 0;
401
+ }
402
+
403
+ .footer p {
404
+ font-family: var(--font-mono);
405
+ font-size: 11px;
406
+ color: var(--color-text-secondary);
407
+ letter-spacing: 0.02em;
408
+ }
409
+
410
+ /* --- Settings Dialog --- */
411
+ .settings-dialog {
412
+ border: 1px solid var(--color-border);
413
+ border-radius: var(--radius-lg);
414
+ padding: var(--space-6);
415
+ max-width: 360px;
416
+ width: calc(100% - var(--space-8));
417
+ background: var(--color-bg);
418
+ color: var(--color-text);
419
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
420
+ position-area: center;
421
+ }
422
+
423
+ .settings-dialog::backdrop {
424
+ background: rgba(0, 0, 0, 0.3);
425
+ }
426
+
427
+ .settings-dialog h2 {
428
+ font-size: 18px;
429
+ font-weight: 600;
430
+ margin-bottom: var(--space-5);
431
+ }
432
+
433
+ .field {
434
+ display: flex;
435
+ flex-direction: column;
436
+ gap: var(--space-1);
437
+ margin-bottom: var(--space-4);
438
+ }
439
+
440
+ .field-label {
441
+ font-size: 13px;
442
+ font-weight: 500;
443
+ color: var(--color-text-secondary);
444
+ }
445
+
446
+ .field-input {
447
+ font-family: var(--font-sans);
448
+ font-size: 14px;
449
+ padding: var(--space-2) var(--space-3);
450
+ border: 1px solid var(--color-border);
451
+ border-radius: var(--radius-sm);
452
+ background: var(--color-bg);
453
+ color: var(--color-text);
454
+ outline: none;
455
+ transition: border-color 0.15s;
456
+ }
457
+
458
+ .field-input::placeholder {
459
+ color: var(--color-gray);
460
+ }
461
+
462
+ .field-input:focus {
463
+ border-color: var(--color-primary);
464
+ }
465
+
466
+ .field-input:focus-visible {
467
+ outline: 2px solid var(--color-primary);
468
+ outline-offset: 1px;
469
+ }
470
+
471
+ .checkbox-field {
472
+ flex-direction: row;
473
+ align-items: center;
474
+ gap: var(--space-2);
475
+ }
476
+
477
+ .checkbox-field input[type="checkbox"] {
478
+ width: 16px;
479
+ height: 16px;
480
+ accent-color: var(--color-primary);
481
+ cursor: pointer;
482
+ }
483
+
484
+ .checkbox-field .field-label {
485
+ cursor: pointer;
486
+ }
487
+
488
+ .dialog-actions {
489
+ display: flex;
490
+ justify-content: flex-end;
491
+ margin-top: var(--space-5);
492
+ }
493
+
494
+ /* --- Buttons --- */
495
+ .btn {
496
+ font-family: var(--font-sans);
497
+ font-size: 14px;
498
+ font-weight: 600;
499
+ padding: var(--space-2) var(--space-5);
500
+ border: none;
501
+ border-radius: var(--radius-pill);
502
+ cursor: pointer;
503
+ transition: background 0.15s;
504
+ }
505
+
506
+ .btn:focus-visible {
507
+ outline: 2px solid var(--color-primary);
508
+ outline-offset: 2px;
509
+ }
510
+
511
+ .btn-primary {
512
+ background: var(--color-primary);
513
+ color: var(--color-text);
514
+ }
515
+
516
+ .btn-primary:hover {
517
+ background: var(--color-primary-hover);
518
+ }
519
+
520
+ /* --- Mobile Breakpoint --- */
521
+ @media (max-width: 640px) {
522
+ .app {
523
+ max-width: 100%;
524
+ border-left: none;
525
+ border-right: none;
526
+ }
527
+
528
+ .header {
529
+ padding: var(--space-3) var(--space-4);
530
+ }
531
+
532
+ .log {
533
+ padding: var(--space-3) var(--space-4);
534
+ }
535
+
536
+ .controls {
537
+ padding: var(--space-5) var(--space-4);
538
+ }
539
+
540
+ .footer {
541
+ padding: var(--space-2) var(--space-4);
542
+ }
543
+
544
+ .record-btn {
545
+ width: 64px;
546
+ height: 64px;
547
+ }
548
+
549
+ .settings-dialog {
550
+ padding: var(--space-5);
551
+ }
552
+ }
package/web/sw.js ADDED
@@ -0,0 +1,26 @@
1
+ const CACHE = 'voice-v2'
2
+ const SHELL = ['/', '/index.html', '/styles.css', '/app.js', '/manifest.webmanifest']
3
+
4
+ self.addEventListener('install', (e) => {
5
+ e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting()))
6
+ })
7
+
8
+ self.addEventListener('activate', (e) => {
9
+ e.waitUntil(
10
+ caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
11
+ .then(() => self.clients.claim())
12
+ )
13
+ })
14
+
15
+ self.addEventListener('fetch', (e) => {
16
+ const url = new URL(e.request.url)
17
+ if (url.pathname.startsWith('/api/')) return
18
+ // Network-first for all assets (cache is offline fallback only)
19
+ e.respondWith(
20
+ fetch(e.request).then((res) => {
21
+ const clone = res.clone()
22
+ caches.open(CACHE).then((c) => c.put(e.request, clone))
23
+ return res
24
+ }).catch(() => caches.match(e.request))
25
+ )
26
+ })