privateboard 0.1.7 → 0.1.9

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.
@@ -69,33 +69,36 @@
69
69
  }
70
70
  .us-classification .right { color: var(--text-faint, #3A382F); letter-spacing: 0.12em; }
71
71
 
72
- .us-head {
73
- display: grid;
74
- grid-template-columns: 1fr auto;
75
- gap: 14px;
76
- align-items: center;
77
- padding: 14px 18px;
78
- border-bottom: 0.5px dashed var(--line-bright, #2A2A26);
79
- flex: 0 0 auto;
80
- }
81
- .us-head .us-title {
82
- font-size: 14px;
83
- font-weight: 700;
84
- color: var(--text, #C8C5BE);
85
- letter-spacing: -0.01em;
86
- }
87
- .us-head .us-title::before { content: "▸ "; color: var(--lime, #6FB572); }
88
- .us-head .us-close {
89
- width: 28px; height: 28px;
72
+ /* Close button · absolute-positioned in the modal's top-right
73
+ corner, pinned BELOW the classification strip so it doesn't
74
+ overlap "// LOCAL" at the right edge of that strip. The strip is
75
+ ~22-24px tall (6px + 9px font + 6px + 0.5px border). The button
76
+ sits at 48px from the modal top — roughly aligned with the
77
+ first pane's `padding-top: 18px` baseline, so it visually
78
+ anchors to the section content rather than crowding the strip
79
+ below. The previous header strip (`.us-head`) that hosted this
80
+ button has been retired — first the "Preference" title, then
81
+ the surrounding bar with its dashed divider. */
82
+ .us-close {
83
+ position: absolute;
84
+ top: 38px;
85
+ right: 14px;
86
+ z-index: 1;
87
+ width: 24px; height: 24px;
90
88
  background: transparent;
91
89
  border: 0.5px solid var(--line-bright, #2A2A26);
92
90
  color: var(--text-dim, #5C5A52);
93
- font-size: 14px;
91
+ font-size: 12px;
92
+ line-height: 1;
94
93
  cursor: pointer;
95
94
  font-family: var(--mono);
95
+ display: inline-flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ padding: 0;
96
99
  transition: all 0.12s;
97
100
  }
98
- .us-head .us-close:hover {
101
+ .us-close:hover {
99
102
  border-color: var(--lime, #6FB572);
100
103
  color: var(--lime, #6FB572);
101
104
  }
@@ -261,50 +264,76 @@
261
264
  margin-top: 5px;
262
265
  }
263
266
 
264
- /* ─── Toggle row · pill button + supporting deck text ───
265
- Matches the visual vocabulary of the day-picker pills (mono
266
- uppercase, hairline border, lime accent when on) so the User pane
267
- doesn't introduce a third button style for one toggle. */
267
+ /* ─── Toggle row · proper switch + supporting deck text ───
268
+ Switch shape (track + sliding thumb) instead of the prior pill
269
+ button: the affordance reads "on/off state" at a glance, not
270
+ "press me to do something." Track is a 32×18 rounded rect, thumb
271
+ is a 14×14 disc that slides 14px between rest positions. Lime
272
+ fill when on; panel-3 fill + hairline border when off. */
268
273
  .us-toggle-row {
269
274
  display: flex;
270
275
  align-items: center;
271
276
  gap: 12px;
272
277
  flex-wrap: wrap;
273
278
  }
274
- .us-toggle-pill {
279
+ .us-switch {
275
280
  appearance: none;
276
281
  background: transparent;
277
- border: 0.5px solid var(--line-bright, #2A2A26);
278
- color: var(--text-soft, #8E8B83);
279
- font-family: var(--mono);
280
- font-size: 10.5px;
281
- letter-spacing: 0.18em;
282
- text-transform: uppercase;
283
- font-weight: 700;
284
- padding: 6px 14px;
282
+ border: none;
283
+ padding: 0;
285
284
  display: inline-flex;
286
285
  align-items: center;
287
- gap: 8px;
286
+ gap: 10px;
288
287
  cursor: pointer;
289
- transition: color 0.12s, border-color 0.12s, background 0.12s;
290
- }
291
- .us-toggle-pill:hover {
292
- color: var(--text, #C8C5BE);
293
- border-color: var(--text-faint, #3A382F);
288
+ font-family: var(--mono);
289
+ color: inherit;
294
290
  }
295
- .us-toggle-pill.on {
296
- color: var(--lime, #6FB572);
297
- border-color: var(--lime, #6FB572);
298
- background: var(--panel-2, #1A1A18);
291
+ .us-switch-track {
292
+ position: relative;
293
+ display: inline-block;
294
+ width: 32px;
295
+ height: 18px;
296
+ background: var(--panel-3, #21211E);
297
+ border: 0.5px solid var(--line-bright, #2A2A26);
298
+ border-radius: 9px;
299
+ flex-shrink: 0;
300
+ transition: background 0.18s, border-color 0.18s;
299
301
  }
300
- .us-toggle-dot {
301
- width: 6px;
302
- height: 6px;
302
+ .us-switch-thumb {
303
+ position: absolute;
304
+ top: 1px;
305
+ left: 1px;
306
+ width: 14px;
307
+ height: 14px;
303
308
  border-radius: 50%;
304
309
  background: var(--text-faint, #3A382F);
305
- transition: background 0.12s;
310
+ transition: transform 0.18s ease, background 0.18s;
311
+ }
312
+ .us-switch.on .us-switch-track {
313
+ background: var(--lime, #6FB572);
314
+ border-color: var(--lime, #6FB572);
315
+ }
316
+ .us-switch.on .us-switch-thumb {
317
+ transform: translateX(14px);
318
+ background: var(--bg, #0A0A0A);
319
+ }
320
+ .us-switch:hover .us-switch-track { border-color: var(--text-faint, #3A382F); }
321
+ .us-switch.on:hover .us-switch-track { border-color: var(--lime, #6FB572); }
322
+ .us-switch:focus-visible { outline: none; }
323
+ .us-switch:focus-visible .us-switch-track {
324
+ outline: 1.5px solid var(--lime, #6FB572);
325
+ outline-offset: 2px;
326
+ }
327
+ .us-switch-label {
328
+ font-size: 10.5px;
329
+ letter-spacing: 0.18em;
330
+ text-transform: uppercase;
331
+ font-weight: 700;
332
+ color: var(--text-soft, #8E8B83);
333
+ transition: color 0.12s;
306
334
  }
307
- .us-toggle-pill.on .us-toggle-dot { background: var(--lime, #6FB572); }
335
+ .us-switch.on .us-switch-label { color: var(--lime, #6FB572); }
336
+
308
337
  .us-toggle-deck {
309
338
  font-size: 11px;
310
339
  color: var(--text-faint, #3A382F);
@@ -777,7 +806,6 @@
777
806
 
778
807
  @media (max-width: 600px) {
779
808
  .user-settings-overlay { padding: 12px; }
780
- .us-head { padding-left: 14px; padding-right: 14px; }
781
809
  .us-pane { padding: 14px; }
782
810
  .us-foot { padding: 10px 14px; }
783
811
  }
@@ -895,9 +923,11 @@
895
923
  display: flex;
896
924
  align-items: flex-end;
897
925
  gap: 4px;
898
- height: 96px;
926
+ height: 180px;
899
927
  /* The stack within each bar lives at the bottom (flex-end) so
900
- bar height grows upward like a real chart axis. */
928
+ bar height grows upward like a real chart axis. Bumped from
929
+ 96px → 180px so the chart reads as a real exhibit instead of
930
+ a squashed sparkline. */
901
931
  }
902
932
  .us-chart-bar {
903
933
  appearance: none;
@@ -915,27 +945,46 @@
915
945
  position: relative;
916
946
  }
917
947
  .us-chart-bar:hover .us-chart-stack { filter: brightness(1.15); }
918
- .us-chart-bar.empty .us-chart-stack {
919
- height: 1px !important;
920
- background: var(--line-bright, #2A2A26);
921
- }
922
- .us-chart-bar.today::before {
923
- /* Outline marker for today's bar a hairline ring around the
924
- full bar slot (including the empty space above the stack)
925
- so the present is locatable even on a zero-token day. */
926
- content: "";
948
+ /* Hover tooltip · two-line custom tip rendered via ::before from the
949
+ bar's data-tip-day / data-tip-num attributes. Floats above the
950
+ column so it doesn't overlap the stack or get clipped by the bar's
951
+ contents. Native browser title= is replaced by aria-label= on the
952
+ button to keep a11y without the 500ms native-tooltip delay.
953
+ Suppressed on `.empty` bars · zero-usage days show no tooltip
954
+ (the row is already empty; a tooltip saying "no usage" is noise). */
955
+ .us-chart-bar:not(.empty)::before {
956
+ content: attr(data-tip-day) "\A" attr(data-tip-num);
957
+ white-space: pre;
958
+ text-align: center;
927
959
  position: absolute;
928
- inset: 0 -1px 14px -1px;
929
- border: 0.5px solid var(--text-faint, #3A382F);
960
+ bottom: calc(100% + 6px);
961
+ left: 50%;
962
+ transform: translateX(-50%);
963
+ background: var(--bg, #0A0A0A);
964
+ border: 1px solid var(--line-bright, #2A2A26);
965
+ padding: 6px 10px;
966
+ font-family: var(--mono, "Inter", system-ui, sans-serif);
967
+ font-size: 10px;
968
+ line-height: 1.5;
969
+ letter-spacing: 0.04em;
970
+ color: var(--text, #C8C5BE);
971
+ opacity: 0;
930
972
  pointer-events: none;
973
+ transition: opacity 0.12s ease;
974
+ z-index: 10;
931
975
  }
932
- .us-chart-bar.active::before {
933
- content: "";
934
- position: absolute;
935
- inset: 0 -1px 14px -1px;
936
- border: 1px solid var(--lime, #6FB572);
937
- pointer-events: none;
976
+ .us-chart-bar:not(.empty):hover::before { opacity: 1; }
977
+ .us-chart-bar.empty .us-chart-stack {
978
+ height: 1px !important;
979
+ background: var(--line-bright, #2A2A26);
938
980
  }
981
+ /* Today / active markers carry only on the tick label below
982
+ (color shift) — no perimeter border around the bar slot.
983
+ Earlier the today/active states drew an outline ring via
984
+ `::before` which read as an ugly box around an otherwise
985
+ clean stacked-bar; the tick-colour change carries enough
986
+ signal. See `.us-chart-bar.today .us-chart-tick` and
987
+ `.us-chart-bar.active .us-chart-tick` below. */
939
988
  .us-chart-stack {
940
989
  display: flex;
941
990
  flex-direction: column-reverse; /* first segment at the bottom */
@@ -226,18 +226,6 @@
226
226
  </div>
227
227
  </div>
228
228
 
229
- <div class="us-row">
230
- <div class="us-row-label">Typing sound</div>
231
- <div class="us-row-field">
232
- <div class="us-toggle-row">
233
- <button type="button" class="us-toggle-pill" data-us-sfx-typing aria-pressed="false">
234
- <span class="us-toggle-dot"></span>
235
- <span class="us-toggle-label" data-us-sfx-typing-label>off</span>
236
- </button>
237
- <span class="us-toggle-deck">a soft keyboard click as directors stream their replies. Persists locally.</span>
238
- </div>
239
- </div>
240
- </div>
241
229
  </div>
242
230
  `;
243
231
  }
@@ -268,6 +256,66 @@
268
256
  `;
269
257
  }
270
258
 
259
+ /* ── Other settings · misc per-user toggles that don't fit a
260
+ dedicated section. Currently just the typing-sound effect; a
261
+ natural home for future small ambient / UX preferences (sound
262
+ cues, animations, etc.) without growing the nav for each one. */
263
+ function otherSettingsSectionHTML() {
264
+ return `
265
+ <div class="us-pane-head">
266
+ <div class="us-pane-tag">▸ Other settings</div>
267
+ <div class="us-pane-deck">small UX preferences that don't fit elsewhere. All persisted locally.</div>
268
+ </div>
269
+
270
+ <div class="us-pane-body">
271
+ <div class="us-row">
272
+ <div class="us-row-label">Typing sound</div>
273
+ <div class="us-row-field">
274
+ <div class="us-toggle-row">
275
+ <button type="button" class="us-switch" data-us-sfx-typing role="switch" aria-checked="false">
276
+ <span class="us-switch-track" aria-hidden="true">
277
+ <span class="us-switch-thumb"></span>
278
+ </span>
279
+ <span class="us-switch-label" data-us-sfx-typing-label>off</span>
280
+ </button>
281
+ <span class="us-toggle-deck">a soft keyboard click as directors stream their replies in chat. Brief generation stays silent regardless of this setting.</span>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ `;
287
+ }
288
+
289
+ function wireOtherSettingsSection() {
290
+ if (!paneEl) return;
291
+ // Typing-sound toggle · the persistence + audio context lives in
292
+ // window.boardroomTypingSfx (typing-sfx.js); this row only mirrors
293
+ // the current state and proxies clicks. Reading inside wire-up
294
+ // (not at HTML build time) means the pill always reflects the
295
+ // LATEST stored state when the section re-mounts.
296
+ const sfxBtn = paneEl.querySelector("[data-us-sfx-typing]");
297
+ const sfxLabel = paneEl.querySelector("[data-us-sfx-typing-label]");
298
+ if (sfxBtn && sfxLabel && window.boardroomTypingSfx) {
299
+ const paint = () => {
300
+ const on = window.boardroomTypingSfx.isEnabled();
301
+ sfxBtn.classList.toggle("on", on);
302
+ // role="switch" wants `aria-checked`, not `aria-pressed`.
303
+ sfxBtn.setAttribute("aria-checked", on ? "true" : "false");
304
+ sfxLabel.textContent = on ? "on" : "off";
305
+ };
306
+ paint();
307
+ sfxBtn.addEventListener("click", () => {
308
+ const next = !window.boardroomTypingSfx.isEnabled();
309
+ window.boardroomTypingSfx.setEnabled(next);
310
+ paint();
311
+ // Audible confirmation when turning ON · the click that just
312
+ // toggled also serves as the gesture the AudioContext needs,
313
+ // so this tick is actually heard.
314
+ if (next) window.boardroomTypingSfx.tick();
315
+ });
316
+ }
317
+ }
318
+
271
319
  /* ── Usage section ────────────────────────────────────────── */
272
320
  // Provider → CSS-variable color slot. Lets each model bar/swatch
273
321
  // pick up the right accent from the active theme without baking
@@ -404,11 +452,20 @@
404
452
  if (isToday) cls.push("today");
405
453
  if (isSelected) cls.push("active");
406
454
  if (total === 0) cls.push("empty");
407
- const tooltip = total > 0
408
- ? `${fmtDayLong(d.day)} · ${fmtTokens(total)} tokens`
409
- : `${fmtDayLong(d.day)} · no usage`;
455
+ // Custom hover tooltip · two lines (day + token count) rendered
456
+ // via CSS `::before` from `data-tip-day` / `data-tip-num`. We
457
+ // use `aria-label` (not `title`) so screen readers still get
458
+ // the info but the native browser tooltip with its ~500ms
459
+ // delay doesn't fight with the instant custom one.
460
+ const dayLabel = fmtDayLong(d.day);
461
+ const numLabel = total > 0 ? `${fmtTokens(total)} tokens` : "no usage";
462
+ const aria = `${dayLabel} · ${numLabel}`;
410
463
  return `
411
- <button type="button" class="${cls.join(' ')}" data-usage-day="${escape(d.day)}" title="${escape(tooltip)}">
464
+ <button type="button" class="${cls.join(' ')}"
465
+ data-usage-day="${escape(d.day)}"
466
+ data-tip-day="${escape(dayLabel)}"
467
+ data-tip-num="${escape(numLabel)}"
468
+ aria-label="${escape(aria)}">
412
469
  <span class="us-chart-stack" style="height:${heightPct.toFixed(2)}%">${segHtml}</span>
413
470
  <span class="us-chart-tick">${escape(fmtDayLabel(d.day))}</span>
414
471
  </button>
@@ -931,10 +988,7 @@
931
988
  <span class="right">// local</span>
932
989
  </div>
933
990
 
934
- <header class="us-head">
935
- <div class="us-title">Preference</div>
936
- <button type="button" class="us-close" aria-label="Close">✕</button>
937
- </header>
991
+ <button type="button" class="us-close" aria-label="Close">✕</button>
938
992
 
939
993
  <div class="us-frame">
940
994
  <nav class="us-nav" role="tablist">
@@ -943,6 +997,7 @@
943
997
  <a href="#" class="us-nav-item" data-section="usage" role="tab" aria-selected="false">Usage</a>
944
998
  <a href="#" class="us-nav-item" data-section="keys" role="tab" aria-selected="false">API Key</a>
945
999
  <a href="#" class="us-nav-item" data-section="default" role="tab" aria-selected="false">Default Model</a>
1000
+ <a href="#" class="us-nav-item" data-section="other" role="tab" aria-selected="false">Other settings</a>
946
1001
  <div class="us-nav-foot" data-us-version aria-label="App version">
947
1002
  <span class="us-nav-foot-label">version</span>
948
1003
  <span class="us-nav-foot-value" data-us-version-value>·</span>
@@ -975,12 +1030,14 @@
975
1030
  else if (id === "usage") paneEl.innerHTML = usageSectionHTML();
976
1031
  else if (id === "keys") paneEl.innerHTML = keysSectionHTML();
977
1032
  else if (id === "default") paneEl.innerHTML = defaultModelSectionHTML();
1033
+ else if (id === "other") paneEl.innerHTML = otherSettingsSectionHTML();
978
1034
 
979
1035
  // Section-specific wiring
980
1036
  if (id === "user") wireUserSection();
981
1037
  if (id === "keys") wireKeysSection();
982
1038
  if (id === "usage") wireUsageSection();
983
1039
  if (id === "default") wireDefaultModelSection();
1040
+ if (id === "other") wireOtherSettingsSection();
984
1041
 
985
1042
  // Active rail item
986
1043
  modal.querySelectorAll(".us-nav-item").forEach((el) => {
@@ -1035,32 +1092,6 @@
1035
1092
  });
1036
1093
  introCount.textContent = introInput.value.length;
1037
1094
 
1038
- // Typing-sound toggle · the persistence + audio context lives in
1039
- // window.boardroomTypingSfx (typing-sfx.js), so this row only
1040
- // mirrors the current state and proxies clicks. Reading inside the
1041
- // wire-up call (not at HTML build time) means the pill always
1042
- // reflects the LATEST stored state when the User pane re-mounts.
1043
- const sfxBtn = paneEl.querySelector("[data-us-sfx-typing]");
1044
- const sfxLabel = paneEl.querySelector("[data-us-sfx-typing-label]");
1045
- if (sfxBtn && sfxLabel && window.boardroomTypingSfx) {
1046
- const paint = () => {
1047
- const on = window.boardroomTypingSfx.isEnabled();
1048
- sfxBtn.classList.toggle("on", on);
1049
- sfxBtn.setAttribute("aria-pressed", on ? "true" : "false");
1050
- sfxLabel.textContent = on ? "on" : "off";
1051
- };
1052
- paint();
1053
- sfxBtn.addEventListener("click", () => {
1054
- const next = !window.boardroomTypingSfx.isEnabled();
1055
- window.boardroomTypingSfx.setEnabled(next);
1056
- paint();
1057
- // Audible confirmation when turning ON · the click that just
1058
- // toggled also serves as the gesture the AudioContext needs,
1059
- // so this tick will actually be heard.
1060
- if (next) window.boardroomTypingSfx.tick();
1061
- });
1062
- }
1063
-
1064
1095
  // Regenerate avatar · same pattern as agent-profile's
1065
1096
  // regenerateProfileAvatar: pull a fresh randomSeed, persist it to
1066
1097
  // the user prefs, repaint. No counter, no name/intro composition —