privateboard 0.1.9 → 0.1.11

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,607 @@
1
+ /* ═══════════════════════════════════════════
2
+ Voice Replay overlay
3
+ ═══════════════════════════════════════════
4
+ Floating playback card pinned to the bottom-right while replay
5
+ is in progress. Shares the dark / lime / mono register of the
6
+ rest of the app — reads as the same family as the dream-cycle
7
+ modal and the room-settings overlay.
8
+
9
+ States rendered into [data-vr-body]:
10
+ · Loading (vr-spinner-row) · while the key check + first
11
+ audio fetch are in flight
12
+ · Key prompt (vr-key-prompt) · no TTS provider configured
13
+ · Empty (vr-key-prompt variant) · room has no playable msgs
14
+ · Player (vr-speaker / vr-progress / vr-controls) · normal play
15
+ · Done (vr-key-prompt with ✓) · all messages played
16
+ ═══════════════════════════════════════════ */
17
+ .voice-replay-overlay {
18
+ position: fixed;
19
+ right: 24px;
20
+ bottom: 24px;
21
+ z-index: 9300;
22
+ width: 380px;
23
+ max-width: calc(100vw - 32px);
24
+ background: var(--panel, #131312);
25
+ border: 0.5px solid var(--lime-dim, #2D5532);
26
+ color: var(--text);
27
+ font-family: var(--mono);
28
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5);
29
+ animation: vr-rise 0.24s ease-out;
30
+ }
31
+ @keyframes vr-rise {
32
+ from { opacity: 0; transform: translateY(12px); }
33
+ to { opacity: 1; transform: translateY(0); }
34
+ }
35
+
36
+ .vr-head {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ padding: 8px 12px;
41
+ background: var(--panel-2, #1A1A18);
42
+ border-bottom: 0.5px solid var(--line);
43
+ }
44
+ .vr-kicker {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ gap: 6px;
48
+ font-size: 9.5px;
49
+ font-weight: 700;
50
+ letter-spacing: 0.18em;
51
+ text-transform: uppercase;
52
+ color: var(--lime, #6FB572);
53
+ }
54
+ .vr-kicker-glyph {
55
+ font-family: var(--font-human);
56
+ font-size: 13px;
57
+ letter-spacing: 0;
58
+ }
59
+ .vr-close {
60
+ width: 22px;
61
+ height: 22px;
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ background: transparent;
66
+ border: 0.5px solid var(--line-bright);
67
+ color: var(--text-dim);
68
+ font-size: 11px;
69
+ cursor: pointer;
70
+ transition: border-color 0.12s, color 0.12s;
71
+ }
72
+ .vr-close:hover { border-color: var(--lime, #6FB572); color: var(--lime, #6FB572); }
73
+
74
+ .vr-body {
75
+ padding: 14px 14px 14px;
76
+ }
77
+
78
+ /* ─── Loading row ───
79
+ Centered both axes so the loading state reads as a focused
80
+ placeholder, not a left-pinned debug log. The dots row sits
81
+ above the label with a small gap so the label has its own
82
+ visual weight, and the whole block is padded vertically to
83
+ approximate the height of the player view it'll be replaced
84
+ by — minimises the visual jump on transition. */
85
+ .vr-spinner-row {
86
+ display: flex;
87
+ flex-direction: column;
88
+ align-items: center;
89
+ justify-content: center;
90
+ gap: 12px;
91
+ padding: 28px 0 32px;
92
+ text-align: center;
93
+ }
94
+ .vr-spinner-dots {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ }
99
+ .vr-spinner-dot {
100
+ width: 7px; height: 7px;
101
+ background: var(--lime, #6FB572);
102
+ border-radius: 50%;
103
+ opacity: 0.35;
104
+ animation: vr-spinner 1.2s ease-in-out infinite;
105
+ box-shadow: 0 0 0 0 rgba(111, 181, 114, 0);
106
+ }
107
+ .vr-spinner-dots .vr-spinner-dot:nth-child(2) { animation-delay: 0.2s; }
108
+ .vr-spinner-dots .vr-spinner-dot:nth-child(3) { animation-delay: 0.4s; }
109
+ .vr-spinner-text {
110
+ font-family: var(--mono);
111
+ font-size: 10.5px;
112
+ font-weight: 700;
113
+ letter-spacing: 0.18em;
114
+ color: var(--text-soft);
115
+ text-transform: uppercase;
116
+ }
117
+ @keyframes vr-spinner {
118
+ 0%, 80%, 100% {
119
+ opacity: 0.25;
120
+ transform: scale(0.85);
121
+ box-shadow: 0 0 0 0 rgba(111, 181, 114, 0);
122
+ }
123
+ 40% {
124
+ opacity: 1;
125
+ transform: scale(1.1);
126
+ box-shadow: 0 0 6px 1px rgba(111, 181, 114, 0.5);
127
+ }
128
+ }
129
+
130
+ /* ─── Speaker card ─── */
131
+ .vr-speaker {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 10px;
135
+ margin-bottom: 10px;
136
+ }
137
+ .vr-avatar {
138
+ width: 32px; height: 32px;
139
+ background: var(--bg);
140
+ border: 0.5px solid var(--line-bright);
141
+ image-rendering: pixelated;
142
+ image-rendering: crisp-edges;
143
+ flex-shrink: 0;
144
+ display: inline-flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ }
148
+ .vr-avatar-placeholder {
149
+ font-family: var(--mono);
150
+ font-size: 13px;
151
+ font-weight: 700;
152
+ color: var(--text-soft);
153
+ }
154
+ .vr-speaker-text {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 2px;
158
+ min-width: 0;
159
+ }
160
+ .vr-speaker-name {
161
+ font-family: var(--font-human, var(--mono));
162
+ font-size: 12.5px;
163
+ font-weight: 700;
164
+ letter-spacing: -0.005em;
165
+ color: var(--text);
166
+ white-space: nowrap;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ }
170
+ .vr-author-role {
171
+ color: var(--text-soft);
172
+ font-weight: 500;
173
+ }
174
+ .vr-speaker-kind {
175
+ font-size: 8.5px;
176
+ letter-spacing: 0.18em;
177
+ text-transform: uppercase;
178
+ color: var(--text-faint);
179
+ }
180
+
181
+ /* ─── Message preview · 2-line clamp ─── */
182
+ .vr-preview {
183
+ font-family: var(--font-human, var(--mono));
184
+ font-size: 11.5px;
185
+ line-height: 1.4;
186
+ color: var(--text-soft);
187
+ letter-spacing: -0.003em;
188
+ margin-bottom: 12px;
189
+ display: -webkit-box;
190
+ -webkit-line-clamp: 2;
191
+ -webkit-box-orient: vertical;
192
+ overflow: hidden;
193
+ /* Subtle quote feel · italic, faint left border accent. */
194
+ font-style: italic;
195
+ border-left: 0.5px solid var(--lime-dim, #2D5532);
196
+ padding-left: 8px;
197
+ }
198
+
199
+ /* ─── Progress strip ─── */
200
+ .vr-progress-row {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 8px;
204
+ margin-bottom: 10px;
205
+ }
206
+ .vr-progress-counter {
207
+ font-size: 9px;
208
+ letter-spacing: 0.14em;
209
+ color: var(--text-dim);
210
+ white-space: nowrap;
211
+ }
212
+ .vr-progress-bar {
213
+ flex: 1;
214
+ height: 3px;
215
+ background: var(--line);
216
+ position: relative;
217
+ overflow: hidden;
218
+ }
219
+ .vr-progress-fill {
220
+ height: 100%;
221
+ background: var(--lime, #6FB572);
222
+ transition: width 0.24s ease-out;
223
+ }
224
+ .vr-progress-pct {
225
+ font-size: 9px;
226
+ letter-spacing: 0.04em;
227
+ color: var(--text-soft);
228
+ min-width: 28px;
229
+ text-align: right;
230
+ }
231
+
232
+ /* ─── Controls row ─── */
233
+ .vr-controls {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 6px;
237
+ flex-wrap: wrap;
238
+ }
239
+ .vr-btn {
240
+ appearance: none;
241
+ background: transparent;
242
+ border: 0.5px solid var(--line-bright);
243
+ color: var(--text);
244
+ font-family: var(--mono);
245
+ font-size: 9.5px;
246
+ font-weight: 700;
247
+ letter-spacing: 0.12em;
248
+ text-transform: uppercase;
249
+ padding: 4px 8px;
250
+ cursor: pointer;
251
+ transition: border-color 0.12s, color 0.12s, background 0.12s;
252
+ }
253
+ .vr-btn:hover {
254
+ border-color: var(--lime, #6FB572);
255
+ color: var(--lime, #6FB572);
256
+ }
257
+ .vr-btn-speed {
258
+ min-width: 38px;
259
+ text-align: center;
260
+ }
261
+ .vr-toggle {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ gap: 6px;
265
+ margin-left: auto;
266
+ font-size: 9px;
267
+ letter-spacing: 0.12em;
268
+ text-transform: uppercase;
269
+ color: var(--text-soft);
270
+ cursor: pointer;
271
+ }
272
+ .vr-toggle input[type="checkbox"] {
273
+ accent-color: var(--lime, #6FB572);
274
+ }
275
+
276
+ /* ─── Key prompt / empty / done ─── */
277
+ .vr-key-prompt {
278
+ display: flex;
279
+ flex-direction: column;
280
+ align-items: stretch;
281
+ gap: 12px;
282
+ padding: 4px 0;
283
+ }
284
+ .vr-key-icon {
285
+ width: 32px; height: 32px;
286
+ display: inline-flex;
287
+ align-items: center;
288
+ justify-content: center;
289
+ border: 0.5px solid var(--lime, #6FB572);
290
+ color: var(--lime, #6FB572);
291
+ font-size: 16px;
292
+ line-height: 1;
293
+ align-self: flex-start;
294
+ }
295
+ .vr-key-icon-done {
296
+ background: var(--lime, #6FB572);
297
+ color: var(--bg, #0A0A0A);
298
+ }
299
+ .vr-key-text {
300
+ display: flex;
301
+ flex-direction: column;
302
+ gap: 4px;
303
+ }
304
+ .vr-key-title {
305
+ font-family: var(--font-human, var(--mono));
306
+ font-size: 13px;
307
+ font-weight: 700;
308
+ color: var(--text);
309
+ letter-spacing: -0.005em;
310
+ }
311
+ .vr-key-deck {
312
+ font-family: var(--font-human, var(--mono));
313
+ font-size: 11.5px;
314
+ line-height: 1.45;
315
+ color: var(--text-soft);
316
+ letter-spacing: -0.003em;
317
+ }
318
+ .vr-key-actions {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 8px;
322
+ flex-wrap: wrap;
323
+ }
324
+ .vr-cta {
325
+ appearance: none;
326
+ background: var(--lime, #6FB572);
327
+ color: var(--bg, #0A0A0A);
328
+ border: 0.5px solid var(--lime, #6FB572);
329
+ padding: 6px 12px;
330
+ font-family: var(--mono);
331
+ font-size: 10px;
332
+ font-weight: 700;
333
+ letter-spacing: 0.14em;
334
+ text-transform: uppercase;
335
+ cursor: pointer;
336
+ transition: background 0.12s, color 0.12s;
337
+ }
338
+ .vr-cta:hover {
339
+ background: var(--bg, #0A0A0A);
340
+ color: var(--lime, #6FB572);
341
+ }
342
+ .vr-ghost {
343
+ appearance: none;
344
+ background: transparent;
345
+ border: 0.5px solid var(--line-bright);
346
+ color: var(--text-soft);
347
+ padding: 6px 12px;
348
+ font-family: var(--mono);
349
+ font-size: 10px;
350
+ font-weight: 700;
351
+ letter-spacing: 0.14em;
352
+ text-transform: uppercase;
353
+ cursor: pointer;
354
+ }
355
+ .vr-ghost:hover { border-color: var(--text); color: var(--text); }
356
+
357
+ /* ─── Inline error inside player body ─── */
358
+ .vr-error {
359
+ margin-top: 10px;
360
+ padding: 8px 10px;
361
+ font-family: var(--font-human, var(--mono));
362
+ font-size: 11px;
363
+ line-height: 1.45;
364
+ color: var(--red, #B5706A);
365
+ border: 0.5px solid var(--red, #B5706A);
366
+ background: var(--panel-2);
367
+ }
368
+
369
+ /* ═══════════════════════════════════════════
370
+ Currently-playing message highlight (in chat)
371
+ ═══════════════════════════════════════════
372
+ Applied to any message DOM node by
373
+ voice-replay.js while it's the active speaker.
374
+
375
+ Game-y · RTS-unit-selection inspired:
376
+ · 4 L-shaped corner brackets at the 4 corners,
377
+ drawn via 2 pseudo-elements with multi-
378
+ gradient backgrounds (each pseudo paints 2
379
+ opposing corners). Brackets pulse with a
380
+ 2s ease-in-out cycle that mirrors the
381
+ audio's breathing rhythm.
382
+ · Subtle lime tint + breathing inner glow on
383
+ the bubble itself so the message feels "lit
384
+ up" instead of just outlined.
385
+ · Floating `▶ SPEAKING` chip at top-right,
386
+ absolute-positioned with a pulsing dot.
387
+
388
+ Everything is non-blocking · pointer-events:
389
+ none on the decorations so the underlying
390
+ message stays clickable / hoverable. */
391
+ /* Same frame treatment is shared by THREE states · the only
392
+ difference is which class triggers it:
393
+ · `.is-replay-active` · adjourned-room voice replay (the
394
+ user clicked Voice Replay; the
395
+ JS handler adds + removes this).
396
+ · `.streaming` · live director / chair message
397
+ that's currently streaming text
398
+ (added by updateMessageBodyDom).
399
+ · `.is-streaming` · same as above but on chair
400
+ custom cards (chair-direct,
401
+ chair-intervention) which use a
402
+ different class machinery.
403
+ Bundling them under one rule means the live experience
404
+ inherits the replay decoration automatically — when a
405
+ director starts speaking in a live voice room (or just
406
+ streaming text in a normal text room), the same corner
407
+ brackets + breathing tint frame their bubble, then drop
408
+ the moment they go to "final." */
409
+ .is-replay-active,
410
+ [data-message-id].streaming,
411
+ [data-message-id].is-streaming {
412
+ position: relative;
413
+ /* Background uses the warm text token at very low alpha so
414
+ the bubble feels "lit" without leaning chartreuse —
415
+ earlier rev's lime tint + lime ring stacked into a
416
+ yellow-green halo on bright displays. The ring itself is
417
+ gone now; framing comes from the corner brackets, which
418
+ sit OUTSIDE the bubble so there's real breathing room
419
+ between the decoration and content. */
420
+ background: rgba(200, 197, 190, 0.05);
421
+ box-shadow: 0 0 36px rgba(200, 197, 190, 0.08);
422
+ animation: replay-active-breathe 2.4s ease-in-out infinite;
423
+ transition: background 0.24s, box-shadow 0.24s;
424
+ }
425
+
426
+ @keyframes replay-active-breathe {
427
+ 0%, 100% {
428
+ background: rgba(200, 197, 190, 0.04);
429
+ box-shadow: 0 0 28px rgba(200, 197, 190, 0.06);
430
+ }
431
+ 50% {
432
+ background: rgba(200, 197, 190, 0.08);
433
+ box-shadow: 0 0 44px rgba(200, 197, 190, 0.14);
434
+ }
435
+ }
436
+
437
+ /* Corner brackets · 4 corners painted by 2 pseudo-elements.
438
+ Each pseudo uses 4 linear-gradient backgrounds (one per
439
+ bracket arm) anchored to the corner.
440
+ `inset: -12px` pulls the brackets well outside the bubble
441
+ so there's clear visual breathing room between the bubble's
442
+ text padding and the bracket arms — a tight `-6px` made
443
+ the brackets feel glued to the bubble edge.
444
+ Color is the warm text token, not lime · the brackets are
445
+ FRAME chrome (passive), the SPEAKING chip is the ACTION
446
+ accent (lime). Splitting the duties keeps the visual
447
+ register cohesive with the rest of the theme.
448
+ Pulse uses transform + opacity so layout doesn't reflow. */
449
+ .is-replay-active::before,
450
+ .is-replay-active::after,
451
+ [data-message-id].streaming::before,
452
+ [data-message-id].streaming::after,
453
+ [data-message-id].is-streaming::before,
454
+ [data-message-id].is-streaming::after {
455
+ content: "";
456
+ position: absolute;
457
+ inset: -12px;
458
+ pointer-events: none;
459
+ animation: replay-bracket-pulse 1.8s ease-in-out infinite;
460
+ }
461
+ .is-replay-active::before,
462
+ [data-message-id].streaming::before,
463
+ [data-message-id].is-streaming::before {
464
+ /* Top-left + top-right corners. */
465
+ background-image:
466
+ linear-gradient(to right, var(--text-soft, #8E8B83) 16px, transparent 16px),
467
+ linear-gradient(to bottom, var(--text-soft, #8E8B83) 16px, transparent 16px),
468
+ linear-gradient(to left, var(--text-soft, #8E8B83) 16px, transparent 16px),
469
+ linear-gradient(to bottom, var(--text-soft, #8E8B83) 16px, transparent 16px);
470
+ background-position:
471
+ top left, /* TL horizontal */
472
+ top left, /* TL vertical */
473
+ top right, /* TR horizontal */
474
+ top right; /* TR vertical */
475
+ background-size:
476
+ 16px 1.5px,
477
+ 1.5px 16px,
478
+ 16px 1.5px,
479
+ 1.5px 16px;
480
+ background-repeat: no-repeat;
481
+ }
482
+ .is-replay-active::after,
483
+ [data-message-id].streaming::after,
484
+ [data-message-id].is-streaming::after {
485
+ /* Bottom-left + bottom-right corners. */
486
+ background-image:
487
+ linear-gradient(to right, var(--text-soft, #8E8B83) 16px, transparent 16px),
488
+ linear-gradient(to top, var(--text-soft, #8E8B83) 16px, transparent 16px),
489
+ linear-gradient(to left, var(--text-soft, #8E8B83) 16px, transparent 16px),
490
+ linear-gradient(to top, var(--text-soft, #8E8B83) 16px, transparent 16px);
491
+ background-position:
492
+ bottom left,
493
+ bottom left,
494
+ bottom right,
495
+ bottom right;
496
+ background-size:
497
+ 16px 1.5px,
498
+ 1.5px 16px,
499
+ 16px 1.5px,
500
+ 1.5px 16px;
501
+ background-repeat: no-repeat;
502
+ /* Stagger the pulse so the four corners breathe in pairs
503
+ (TL+TR together, BL+BR together) for a more alive feel
504
+ than a single synced pulse across all four. */
505
+ animation-delay: 0.9s;
506
+ }
507
+
508
+ @keyframes replay-bracket-pulse {
509
+ 0%, 100% {
510
+ opacity: 0.55;
511
+ transform: scale(1);
512
+ }
513
+ 50% {
514
+ opacity: 0.95;
515
+ transform: scale(1.012);
516
+ }
517
+ }
518
+
519
+ /* "▶ SPEAKING" chip · floating tag pinned just-above-right of
520
+ the active bubble. Injected by voice-replay.js when a
521
+ message becomes active, removed on advance. Pulsing dot
522
+ beside the label signals that audio is actively flowing;
523
+ the chip itself reads "speaker" before "animation". */
524
+ .vr-now-playing {
525
+ position: absolute;
526
+ top: -10px;
527
+ right: 12px;
528
+ z-index: 5;
529
+ display: inline-flex;
530
+ align-items: center;
531
+ gap: 6px;
532
+ padding: 3px 8px 3px 7px;
533
+ background: var(--bg, #0A0A0A);
534
+ border: 0.5px solid var(--lime, #6FB572);
535
+ font-family: var(--mono);
536
+ font-size: 8.5px;
537
+ font-weight: 700;
538
+ letter-spacing: 0.18em;
539
+ text-transform: uppercase;
540
+ color: var(--lime, #6FB572);
541
+ pointer-events: none;
542
+ box-shadow: 0 2px 12px rgba(111, 181, 114, 0.25);
543
+ animation: vr-np-rise 0.18s ease-out;
544
+ }
545
+ .vr-np-mark {
546
+ font-family: var(--mono);
547
+ font-size: 9px;
548
+ letter-spacing: 0;
549
+ }
550
+ .vr-np-dot {
551
+ width: 5px;
552
+ height: 5px;
553
+ background: var(--lime, #6FB572);
554
+ border-radius: 50%;
555
+ animation: vr-np-dot-pulse 1.1s ease-in-out infinite;
556
+ }
557
+ .vr-np-text {
558
+ line-height: 1;
559
+ }
560
+ @keyframes vr-np-rise {
561
+ from { opacity: 0; transform: translateY(4px); }
562
+ to { opacity: 1; transform: translateY(0); }
563
+ }
564
+ @keyframes vr-np-dot-pulse {
565
+ 0%, 100% { opacity: 0.35; transform: scale(0.85); box-shadow: 0 0 0 0 rgba(111, 181, 114, 0); }
566
+ 50% { opacity: 1; transform: scale(1.1); box-shadow: 0 0 6px 1px rgba(111, 181, 114, 0.7); }
567
+ }
568
+
569
+ /* Reduced-motion fallback · users with prefers-reduced-motion
570
+ get a solid lime ring + static brackets + static chip dot,
571
+ no pulsing / breathing. */
572
+ @media (prefers-reduced-motion: reduce) {
573
+ .is-replay-active,
574
+ .is-replay-active::before,
575
+ .is-replay-active::after,
576
+ [data-message-id].streaming,
577
+ [data-message-id].streaming::before,
578
+ [data-message-id].streaming::after,
579
+ [data-message-id].is-streaming,
580
+ [data-message-id].is-streaming::before,
581
+ [data-message-id].is-streaming::after,
582
+ .vr-now-playing,
583
+ .vr-np-dot {
584
+ animation: none !important;
585
+ }
586
+ .is-replay-active,
587
+ [data-message-id].streaming,
588
+ [data-message-id].is-streaming {
589
+ background: rgba(200, 197, 190, 0.06);
590
+ box-shadow: 0 0 22px rgba(200, 197, 190, 0.10);
591
+ }
592
+ .is-replay-active::before,
593
+ .is-replay-active::after,
594
+ [data-message-id].streaming::before,
595
+ [data-message-id].streaming::after,
596
+ [data-message-id].is-streaming::before,
597
+ [data-message-id].is-streaming::after { opacity: 0.7; }
598
+ .vr-np-dot { opacity: 1; transform: none; }
599
+ }
600
+
601
+ /* Sidebar collapsed · the overlay should never overlap with the
602
+ floating sidebar-expand button · push it left a touch when the
603
+ page has the no-room flag (which gates the expand button's
604
+ visibility too). */
605
+ html.no-room .voice-replay-overlay {
606
+ bottom: 56px;
607
+ }