privateboard 0.1.36 → 0.1.37

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/public/index.html CHANGED
@@ -69,6 +69,9 @@
69
69
  system-ui, sans-serif;
70
70
 
71
71
  --sidebar-w: 280px;
72
+ /* Collapsed icon-rail width · matches the ChatGPT folded sidebar.
73
+ Wide enough for a centered 32px hit target with breathing room. */
74
+ --mini-sidebar-w: 56px;
72
75
  }
73
76
 
74
77
  /* Logo stays on Inter so the brandmark reads consistently
@@ -670,14 +673,6 @@
670
673
  html.is-electron-mac .room-head .head-cast img {
671
674
  -webkit-app-region: no-drag;
672
675
  }
673
- /* The expand-btn's ::after halo paints OUTSIDE the button rect
674
- (inset: -16px), so it doesn't inherit the button's no-drag and
675
- gets eaten by `.room-head { drag }` above. Stamp no-drag on the
676
- pseudo directly so the 48×48 hit zone keeps registering clicks.
677
- (Same shape as the sidebar-collapse-btn::after override.) */
678
- html.is-electron-mac .room-head .room-head-expand::after {
679
- -webkit-app-region: no-drag;
680
- }
681
676
  /* ─── macOS Electron · top-strip drag coverage for non-room views.
682
677
  When the room view is hidden (All Reports / All Notes / Search /
683
678
  Agent Profile) or the room view is shown with no current room (the
@@ -839,22 +834,28 @@
839
834
  room. Single column = main fills the whole body-grid cleanly.
840
835
  The user's preferred --sidebar-w stays in localStorage so
841
836
  expanding restores the same width. */
837
+ /* Collapsed · the full sidebar + resizer drop out and a fixed-width
838
+ mini icon rail takes column 1. Source order [sidebar, resizer,
839
+ mini, main] + both hidden siblings → grid auto-places mini in
840
+ col 1, main in col 2. */
842
841
  body.sidebar-collapsed .body-grid {
843
- grid-template-columns: 1fr !important;
842
+ grid-template-columns: var(--mini-sidebar-w) 1fr !important;
844
843
  }
845
844
  body.sidebar-collapsed .sidebar,
846
845
  body.sidebar-collapsed .col-resizer {
847
846
  display: none;
848
847
  }
848
+ body.sidebar-collapsed .mini-sidebar {
849
+ display: flex;
850
+ }
849
851
  /* ─── Hover peek · floating sidebar while collapsed ───
850
- When the user hovers the collapsed-state trigger (the floating
851
- `.sidebar-expand-btn` or the in-header `.room-head-expand`), JS
852
- adds `.sidebar-peek` to body. The sidebar reappears as a floating
853
- overlay anchored to the body-grid's top-left edge so it sits ABOVE
854
- the chat content without re-flowing the grid. Mouseleave (with
855
- short grace) drops the class and the sidebar disappears again.
856
- This is purely an addition to the collapsed state — the column
857
- never reclaims its grid track. */
852
+ When the cursor reaches the viewport's left edge (over the mini
853
+ rail), JS adds `.sidebar-peek` to body. The full sidebar reappears
854
+ as a floating overlay anchored to the body-grid's top-left edge so
855
+ it sits ABOVE the chat content (and the mini rail) without
856
+ re-flowing the grid. Mouseleave drops the class and the float
857
+ disappears again, leaving the mini rail. This is purely an addition
858
+ to the collapsed state — the column never reclaims its grid track. */
858
859
  body.sidebar-collapsed.sidebar-peek .sidebar {
859
860
  display: flex;
860
861
  position: absolute;
@@ -913,12 +914,14 @@
913
914
  appearance: none;
914
915
  padding: 0;
915
916
  }
916
- /* Show floating button only when sidebar is collapsed AND we're
917
- in the empty / no-room state. When a room is loaded, the
918
- in-header `.room-head-expand` button takes over instead, so
919
- the icon doesn't sit on top of the room title. */
917
+ /* Retired · the persistent mini icon rail now owns the collapsed
918
+ state, and its logo button expands. The old floating round chip
919
+ would sit on top of the rail's top icon (both pin to the body-grid
920
+ top-left), so it stays hidden in every state. Kept in the DOM (and
921
+ this rule kept as `display: none`) so the edge-peek mousemove guard
922
+ that references `[data-sidebar-expand]` still resolves cleanly. */
920
923
  html.no-room body.sidebar-collapsed .sidebar-expand-btn {
921
- display: inline-flex;
924
+ display: none;
922
925
  }
923
926
  .sidebar-expand-btn:hover {
924
927
  color: var(--lime);
@@ -961,67 +964,230 @@
961
964
  -webkit-app-region: no-drag;
962
965
  }
963
966
 
964
- /* ─── In-header expand button ───
965
- Lives at the leading edge of `.room-head` (rendered by
966
- renderHeader() in app.js). Visual twin of `.sidebar-collapse-btn`
967
- so the collapse/expand pair reads as the SAME control across
968
- the two places it can appear (in-sidebar when open · in-header
969
- when sidebar collapsed AND a room is loaded). Icon-only · no
970
- chrome · same Lucide PanelLeft mask · same hover-lime cascade.
971
- Always rendered in the mirrored variant (panel folded to the
972
- right edge) since this button only ever appears in the collapsed
973
- state · clicking it expands the sidebar back. */
974
- .room-head-expand {
975
- appearance: none;
967
+ /* ─── Mini (collapsed) sidebar · ChatGPT-style icon rail ───
968
+ A persistent narrow rail shown while collapsed. `display: none`
969
+ by default; flipped to `flex` by the collapsed-state rule above.
970
+ Shares the sidebar chrome surface (`--sidebar-bg`) + a hairline
971
+ right edge so it reads as the same column the full sidebar used. */
972
+ .mini-sidebar {
973
+ display: none;
974
+ flex-direction: column;
975
+ align-items: center;
976
+ width: var(--mini-sidebar-w);
977
+ height: 100%;
978
+ min-height: 0;
979
+ background: var(--sidebar-bg);
980
+ border-right: 1px solid var(--line);
981
+ padding: 10px 0;
982
+ /* `visible` (not `hidden`) so the per-icon hover tooltips can
983
+ escape the 56px rail and pop out to the right. The icon set is
984
+ short enough that it never overflows the rail vertically, so
985
+ there's nothing to clip anyway. The enclosing `.body-grid`
986
+ still bounds everything. */
987
+ overflow: visible;
988
+ }
989
+ .mini-top {
990
+ display: flex;
991
+ flex-direction: column;
992
+ align-items: center;
993
+ gap: 4px;
994
+ flex: 1 1 auto;
995
+ min-height: 0;
996
+ width: 100%;
997
+ }
998
+ .mini-foot {
999
+ display: flex;
1000
+ flex-direction: column;
1001
+ align-items: center;
1002
+ flex: 0 0 auto;
1003
+ padding-top: 8px;
1004
+ }
1005
+ /* Icon button · 32×32 centered hit target. Stroke icons ride
1006
+ currentColor (text-soft idle → text on hover) via the shared
1007
+ mask-image vocabulary, matching the full sidebar's nav glyphs. */
1008
+ .mini-btn {
1009
+ position: relative;
1010
+ display: flex;
1011
+ align-items: center;
1012
+ justify-content: center;
1013
+ width: 32px;
1014
+ height: 32px;
1015
+ padding: 0;
1016
+ border: none;
976
1017
  background: transparent;
977
- border: 0;
978
1018
  color: var(--text-soft);
1019
+ border-radius: 8px;
979
1020
  cursor: pointer;
980
- width: 16px;
981
- height: 16px;
982
- padding: 0;
1021
+ text-decoration: none;
983
1022
  flex-shrink: 0;
984
- transition: color 0.12s;
985
- /* position: relative anchors the ::after hit-area expander so
986
- the cursor doesn't have to land precisely on the 16px icon. */
987
- position: relative;
988
- display: none;
1023
+ transition: background 0.12s, color 0.12s;
1024
+ appearance: none;
1025
+ }
1026
+ /* Hover tooltip · CSS-only via ::after + data-tip, mirroring the
1027
+ `.ib-action::after` pattern. Pops to the RIGHT of the icon (the
1028
+ rail hugs the window's left edge, so above/left would clip) after
1029
+ ~300ms dwell. data-tip is populated from data-i18n-tip by i18n.js,
1030
+ with a static English fallback for the pre-i18n frame. */
1031
+ .mini-btn[data-tip]::after {
1032
+ content: attr(data-tip);
1033
+ position: absolute;
1034
+ left: calc(100% + 10px);
1035
+ top: 50%;
1036
+ transform: translateY(-50%) translateX(-3px);
1037
+ background: var(--panel-2);
1038
+ border: 0.5px solid var(--line-strong);
1039
+ padding: 5px 9px;
1040
+ font-family: var(--mono);
1041
+ font-size: 10px;
1042
+ letter-spacing: 0.04em;
1043
+ color: var(--text);
1044
+ white-space: nowrap;
1045
+ pointer-events: none;
1046
+ opacity: 0;
1047
+ visibility: hidden;
1048
+ box-shadow: 0 4px 14px -6px rgba(0, 0, 0, 0.55);
1049
+ transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0.18s;
1050
+ z-index: 250;
1051
+ }
1052
+ .mini-btn[data-tip]:hover::after,
1053
+ .mini-btn[data-tip]:focus-visible::after {
1054
+ opacity: 1;
1055
+ visibility: visible;
1056
+ transform: translateY(-50%) translateX(0);
1057
+ transition: opacity 0.14s ease 0.3s, transform 0.14s ease 0.3s, visibility 0s linear 0.3s;
989
1058
  }
990
- .room-head-expand::before {
1059
+ .mini-btn:hover {
1060
+ background: var(--panel-2);
1061
+ color: var(--text);
1062
+ }
1063
+ /* Selected state · the icon for the current destination (reports /
1064
+ notes / new-room / new-agent via shared trigger attrs, rooms /
1065
+ agents via setMiniRailContext). Settled `--panel-3` chip + full
1066
+ --text glyph, matching the full sidebar's `.new-btn.active`
1067
+ language (background-only, no accent — per project CSS rules).
1068
+ Placed after :hover so an active icon keeps its chip on hover
1069
+ instead of dropping to the lighter hover tone. */
1070
+ .mini-btn.active,
1071
+ .mini-btn.active:hover {
1072
+ background: var(--panel-3);
1073
+ color: var(--text);
1074
+ }
1075
+ /* Glyph · same 18px Lucide mask the full sidebar nav buttons use.
1076
+ `--icon` is supplied per-button by the shared rules below (data-
1077
+ attribute scoped so both the .new-btn and .mini-btn carriers pick
1078
+ it up from one definition). */
1079
+ .mini-btn::before {
991
1080
  content: "";
992
- display: block;
993
- width: 16px;
994
- height: 16px;
1081
+ width: 18px;
1082
+ height: 18px;
1083
+ flex-shrink: 0;
995
1084
  background-color: currentColor;
996
- -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
997
- mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1085
+ -webkit-mask-image: var(--icon, none);
1086
+ mask-image: var(--icon, none);
998
1087
  -webkit-mask-repeat: no-repeat;
999
1088
  mask-repeat: no-repeat;
1000
1089
  -webkit-mask-position: center;
1001
1090
  mask-position: center;
1002
- -webkit-mask-size: 16px 16px;
1003
- mask-size: 16px 16px;
1004
- /* Same scaleX(-1) the sidebar-collapse-btn uses when
1005
- body.sidebar-collapsed · since this button is ONLY visible
1006
- in the collapsed state, the flip is unconditional here. */
1007
- transform: scaleX(-1);
1091
+ -webkit-mask-size: 18px 18px;
1092
+ mask-size: 18px 18px;
1093
+ transition: color 0.12s;
1008
1094
  }
1009
- /* Invisible hit-area expander · matches `.sidebar-collapse-btn::after`
1010
- so the click rectangle grows to 48×48 around the 16px icon. */
1011
- .room-head-expand::after {
1095
+ /* Tab + user buttons render real children (.ic / avatar), not a mask
1096
+ glyph. Suppress the shared ::before without an `--icon` its
1097
+ `currentColor` fill would paint a solid square block next to the
1098
+ real content. (The logo keeps its ::before for the hover fold-icon
1099
+ below.) */
1100
+ .mini-tab::before,
1101
+ .mini-user::before { content: none; }
1102
+ .mini-tab .ic { width: 18px; height: 18px; }
1103
+ /* Logo → fold glyph on hover · telegraphs "click to expand" using the
1104
+ same Lucide PanelLeft + 3-rows icon as the in-sidebar collapse
1105
+ button. At rest the brand mark shows; on hover it fades out and the
1106
+ fold glyph (overlaid, currentColor) fades in. The explicit fold
1107
+ mask here also overrides the generic .mini-btn::before, which —
1108
+ lacking an --icon — would otherwise paint a solid square. */
1109
+ .mini-logo { position: relative; }
1110
+ /* The chair avatar is a two-layer stack: open-eye frame always
1111
+ visible, closed-eye frame flashed in by the blink keyframe. The
1112
+ wrapper carries an occasional idle hop. */
1113
+ .mini-logo-av {
1114
+ position: relative;
1115
+ width: 26px;
1116
+ height: 26px;
1117
+ transition: opacity 0.12s;
1118
+ animation: chair-hop 7s ease-in-out infinite;
1119
+ }
1120
+ .mini-logo-av img {
1121
+ position: absolute;
1122
+ inset: 0;
1123
+ width: 100%;
1124
+ height: 100%;
1125
+ image-rendering: pixelated;
1126
+ image-rendering: crisp-edges;
1127
+ }
1128
+ /* Closed-eye frame · hidden at rest, briefly opaque on the blink
1129
+ keyframe. A different cycle length from the hop keeps the two from
1130
+ locking into a robotic sync. */
1131
+ .mini-logo-av .cl-blink {
1132
+ opacity: 0;
1133
+ animation: chair-blink 5.7s ease-in-out infinite;
1134
+ }
1135
+ .mini-logo:hover .mini-logo-av { opacity: 0; }
1136
+ /* Occasional double-bounce hop · still ~85% of the cycle, then a
1137
+ quick two-step hop so the chair reads as "alive" without being
1138
+ busy. */
1139
+ @keyframes chair-hop {
1140
+ 0%, 60%, 100% { transform: translateY(0); }
1141
+ 66% { transform: translateY(-4px); }
1142
+ 72% { transform: translateY(0); }
1143
+ 77% { transform: translateY(-2px); }
1144
+ 82% { transform: translateY(0); }
1145
+ }
1146
+ /* Single crisp blink near the end of the cycle (~170ms closed). */
1147
+ @keyframes chair-blink {
1148
+ 0%, 93.5%, 100% { opacity: 0; }
1149
+ 94%, 96.5% { opacity: 1; }
1150
+ 97% { opacity: 0; }
1151
+ }
1152
+ /* Respect reduced-motion · hold the open-eye frame, no hop / blink. */
1153
+ @media (prefers-reduced-motion: reduce) {
1154
+ .mini-logo-av,
1155
+ .mini-logo-av .cl-blink { animation: none; }
1156
+ }
1157
+ .mini-logo::before {
1012
1158
  content: "";
1013
1159
  position: absolute;
1014
- inset: -16px;
1160
+ top: 50%;
1161
+ left: 50%;
1162
+ /* scaleX(-1) mirrors the fold so the rows flip to the right edge —
1163
+ the same direction the in-sidebar collapse button uses in its
1164
+ collapsed state to telegraph "the panel expands out from here". */
1165
+ transform: translate(-50%, -50%) scaleX(-1);
1166
+ -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1167
+ mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1168
+ opacity: 0;
1169
+ transition: opacity 0.12s;
1015
1170
  }
1016
- body.sidebar-collapsed .room-head-expand {
1017
- display: block;
1018
- /* Extra 5px breathing room between the icon and the title cluster
1019
- on top of the room-head's own 12px column gap · the icon sat
1020
- too close to the kicker / subject otherwise. Scoped to the
1021
- collapsed state since the button itself only exists then. */
1022
- margin-right: 5px;
1171
+ .mini-logo:hover::before { opacity: 1; }
1172
+ /* Divider between the action group and the rooms/agents switchers. */
1173
+ .mini-sep {
1174
+ width: 20px;
1175
+ height: 1px;
1176
+ background: var(--line-bright);
1177
+ margin: 6px 0;
1178
+ flex-shrink: 0;
1179
+ }
1180
+ .mini-user { width: 32px; height: 32px; }
1181
+ .mini-user .mini-user-av { width: 26px; height: 26px; }
1182
+ .mini-user:hover { background: var(--panel-2); }
1183
+
1184
+ /* macOS Electron · clicks must reach the rail's buttons (the window
1185
+ drag region otherwise swallows them), and the traffic-light cluster
1186
+ is hidden while collapsed (see syncElectronTrafficLights), so the
1187
+ logo can sit near the top with normal padding. */
1188
+ html.is-electron-mac .mini-sidebar {
1189
+ -webkit-app-region: no-drag;
1023
1190
  }
1024
- .room-head-expand:hover { color: var(--lime); }
1025
1191
 
1026
1192
  /* ─── Sidebar tabs (Rooms / Agents) — bar-style nav ───
1027
1193
  Reference: `public/icons/bar.png`. Two layers:
@@ -1507,27 +1673,33 @@
1507
1673
  [data-convene-trigger]::before {
1508
1674
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/><path d='M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z'/></svg>");
1509
1675
  }
1510
- /* "New agent" · UserPlus (Lucide) — silhouette with a small plus
1511
- to its right, Claude's "add member" pattern. */
1676
+ /* "New agent" · UserRoundPlus (Lucide) — rounder, bigger-headed
1677
+ "add person" glyph. Replaces the thinner UserPlus, whose small
1678
+ low-left silhouette + floating plus read a size smaller than the
1679
+ box-filling neighbours (SquarePen / FileText) — most visible in
1680
+ the collapsed mini rail where icons stand alone without labels. */
1512
1681
  [data-agent-composer-trigger]::before {
1513
- --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'/><circle cx='9' cy='7' r='4'/><line x1='19' x2='19' y1='8' y2='14'/><line x1='22' x2='16' y1='11' y2='11'/></svg>");
1682
+ --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M2 21a8 8 0 0 1 13.292-6'/><circle cx='10' cy='8' r='5'/><path d='M19 16v6'/><path d='M22 19h-6'/></svg>");
1514
1683
  }
1515
1684
  /* "All Reports" · FileText (Lucide) — single document with three
1516
1685
  text lines, cleaner than the previous gradient-stack glyph. */
1517
- .new-btn.nav-reports::before {
1686
+ .new-btn.nav-reports::before,
1687
+ .mini-reports::before {
1518
1688
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/><path d='M14 2v4a2 2 0 0 0 2 2h4'/><path d='M10 9H8'/><path d='M16 13H8'/><path d='M16 17H8'/></svg>");
1519
1689
  }
1520
1690
  /* "All Notes" · Bookmark (Lucide) — pennant-shaped bookmark glyph
1521
1691
  mirroring the qcta save-button icon. Matches the chairman's-notes
1522
1692
  vocabulary across the app (sidebar entry, save action, in-room
1523
1693
  overlay all share the bookmark register). */
1524
- .new-btn.nav-notes::before {
1694
+ .new-btn.nav-notes::before,
1695
+ .mini-notes::before {
1525
1696
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'/></svg>");
1526
1697
  }
1527
1698
  /* "Search" · Search (Lucide) — circle + diagonal handle. Standard
1528
1699
  magnifying-glass glyph in the same Lucide line-icon vocabulary as
1529
1700
  the rest of the sidebar. */
1530
- .new-btn.nav-search::before {
1701
+ .new-btn.nav-search::before,
1702
+ .mini-search::before {
1531
1703
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='8'/><path d='m21 21-4.3-4.3'/></svg>");
1532
1704
  }
1533
1705
  /* All Reports / All Notes / Search nav-button shape · label-only. */
@@ -1888,11 +2060,12 @@
1888
2060
  overflow: hidden;
1889
2061
  }
1890
2062
  /* Pixel-art variant · when prefs.avatarSeed exists, app.renderUserBlock
1891
- swaps the initial-letter chip for an AvatarSkill SVG. Drop the
1892
- lime fill (the SVG is its own surface) and use the bg color
1893
- instead, keep the box border-less so the SVG bleeds edge-to-edge. */
2063
+ swaps the initial-letter chip for an AvatarSkill SVG. The pixel SVG
2064
+ is transparent, so we drop the fill entirely (was --lime, then
2065
+ --bg) and let it sit directly on the surface behind it — no boxed
2066
+ square around the character. */
1894
2067
  .user-av.has-pixel-av {
1895
- background: var(--bg);
2068
+ background: transparent;
1896
2069
  }
1897
2070
  .user-av.has-pixel-av svg {
1898
2071
  width: 100%;
@@ -3722,15 +3895,6 @@
3722
3895
  -webkit-backdrop-filter: none;
3723
3896
  }
3724
3897
  }
3725
- /* When the sidebar is collapsed the room-head gains a leading
3726
- auto-sized track for the in-header `.room-head-expand` button.
3727
- When expanded the button is display:none, so a 0-width track
3728
- would still leave a `gap` artifact — switching to a 3-track
3729
- template only in the collapsed state keeps the header visually
3730
- identical to before whenever the sidebar is open. */
3731
- body.sidebar-collapsed .room-head {
3732
- grid-template-columns: auto 1fr auto;
3733
- }
3734
3898
  /* `overflow: visible` so the tone-tag hover tooltip (positioned via
3735
3899
  ::after below the tag) can escape this container. The room-subject
3736
3900
  has its own overflow:hidden + text-overflow ellipsis rule, so the
@@ -8634,6 +8798,25 @@
8634
8798
  .rt-avatar[data-agent]:hover {
8635
8799
  filter: brightness(1.18);
8636
8800
  }
8801
+ /* Speaking squash-blink · the avatar's eyes are baked into the
8802
+ sprite (img / inline pixel-SVG), so a quick vertical scaleY
8803
+ squash is the cheapest "blink while talking" cue. Only the
8804
+ speaking seat animates; the dip is brief + shallow so it reads
8805
+ as a blink, not a bounce. transform keeps the static
8806
+ translateX(-50%) so the sprite stays centered on the chair. */
8807
+ .rt-seat-speaking .rt-avatar {
8808
+ transform-origin: center 45%;
8809
+ animation: rt-blink-squash 4.2s ease-in-out infinite;
8810
+ }
8811
+ @keyframes rt-blink-squash {
8812
+ 0%, 88%, 100% { transform: translateX(-50%) scaleY(1); }
8813
+ 92% { transform: translateX(-50%) scaleY(0.88); }
8814
+ 96% { transform: translateX(-50%) scaleY(1); }
8815
+ }
8816
+ /* Respect reduced-motion · hold the avatar at full height. */
8817
+ @media (prefers-reduced-motion: reduce) {
8818
+ .rt-seat-speaking .rt-avatar { animation: none; }
8819
+ }
8637
8820
 
8638
8821
  /* Name plate · small mono caption ABOVE the avatar (was below,
8639
8822
  but back-row director seats had their names land on the table
@@ -15888,6 +16071,45 @@
15888
16071
  <!-- Resizable handle: sidebar | main -->
15889
16072
  <div class="col-resizer" data-resize data-var="--sidebar-w" data-side="left" data-min="220" data-max="480" data-i18n-title="col_resize"></div>
15890
16073
 
16074
+ <!-- ─── Mini (collapsed) sidebar · ChatGPT-style icon rail ───
16075
+ Shown ONLY while body.sidebar-collapsed (the full .sidebar is
16076
+ display:none then). Source order matters: it sits between the
16077
+ (hidden) resizer and <main> so grid auto-placement drops it in
16078
+ column 1 and main in column 2 when collapsed.
16079
+ Icon order: logo (→ expand), new room, new agent, search,
16080
+ reports, notes, divider, rooms (→ expand+tab), agents
16081
+ (→ expand+tab). Foot: user avatar (→ settings). New room /
16082
+ agent / search / reports / notes keep the room collapsed (they
16083
+ reuse the same document-delegated data-* triggers the full
16084
+ sidebar uses); only the logo + rooms + agents expand. -->
16085
+ <aside class="mini-sidebar" aria-label="Collapsed sidebar navigation">
16086
+ <div class="mini-top">
16087
+ <button type="button" class="mini-btn mini-logo" data-sidebar-expand data-i18n-tip="sidebar_expand" data-tip="Expand sidebar" aria-label="Expand sidebar">
16088
+ <span class="mini-logo-av">
16089
+ <img class="cl-open" src="/avatars/chair.svg" alt="" aria-hidden="true">
16090
+ <img class="cl-blink" src="/avatars/chair-blink.svg" alt="" aria-hidden="true">
16091
+ </span>
16092
+ </button>
16093
+ <button type="button" class="mini-btn mini-new-room" data-convene-trigger data-i18n-tip="sidebar_new_room" data-tip="New room" aria-label="New room"></button>
16094
+ <button type="button" class="mini-btn mini-new-agent" data-agent-composer-trigger data-i18n-tip="sidebar_new_agent" data-tip="New agent" aria-label="New agent"></button>
16095
+ <button type="button" class="mini-btn mini-search" data-search-trigger data-tip="Search" aria-label="Search"></button>
16096
+ <button type="button" class="mini-btn mini-reports" data-reports-trigger data-i18n-tip="sidebar_all_reports" data-tip="Reports" aria-label="All Reports"></button>
16097
+ <button type="button" class="mini-btn mini-notes" data-notes-trigger data-i18n-tip="sidebar_all_notes" data-tip="Notes" aria-label="All Notes"></button>
16098
+ <span class="mini-sep" aria-hidden="true"></span>
16099
+ <button type="button" class="mini-btn mini-tab" data-mini-tab="rooms" data-i18n-tip="sidebar_tab_rooms" data-tip="Rooms" aria-label="Rooms">
16100
+ <i class="ic ic-rooms"></i>
16101
+ </button>
16102
+ <button type="button" class="mini-btn mini-tab" data-mini-tab="agents" data-i18n-tip="sidebar_tab_agents" data-tip="Agents" aria-label="Agents">
16103
+ <i class="ic ic-agents"></i>
16104
+ </button>
16105
+ </div>
16106
+ <div class="mini-foot">
16107
+ <a href="#" class="mini-btn mini-user" data-user-settings-trigger data-i18n-tip="settings_title" data-tip="Settings" aria-label="Settings">
16108
+ <div class="user-av mini-user-av" data-user-avatar>K</div>
16109
+ </a>
16110
+ </div>
16111
+ </aside>
16112
+
15891
16113
  <!-- ═══════════════ MAIN: room view + agent profile view ═══════════════ -->
15892
16114
  <main class="main">
15893
16115
 
@@ -16530,6 +16752,21 @@
16530
16752
  clearSidebarPeek();
16531
16753
  return;
16532
16754
  }
16755
+ // Mini-rail rooms / agents · expand the full sidebar. The tab
16756
+ // switch + first-room/agent default is owned by the sidebar-tab
16757
+ // IIFE's click handler (it has ROOMS_KEY / appFirstRoomId /
16758
+ // activate in scope — they're NOT visible here). Both handlers
16759
+ // fire on the same click: this one expands, that one navigates.
16760
+ // The other mini buttons (new room / agent / search / reports /
16761
+ // notes) deliberately do NOT expand — they fire their own
16762
+ // document-delegated triggers and the rail stays collapsed.
16763
+ if (e.target.closest("[data-mini-tab]")) {
16764
+ e.preventDefault();
16765
+ applySidebarCollapsed(false);
16766
+ writeSidebarCollapsed(false);
16767
+ clearSidebarPeek();
16768
+ return;
16769
+ }
16533
16770
  });
16534
16771
 
16535
16772
  // ─── Sidebar edge-peek · Arc-browser style ───
@@ -16579,6 +16816,10 @@
16579
16816
  // miss `.sidebar` during the open animation. Immediate close on
16580
16817
  // exit is handled by the in→out transition branch below, which
16581
16818
  // bypasses this lock.
16819
+ // The floated full sidebar is opaque (--sidebar-bg) at 280px and
16820
+ // pins to left:0, so it cleanly covers the 56px mini rail while
16821
+ // peeked — hovering the left edge reveals the full sidebar, and
16822
+ // moving off it drops back to the rail.
16582
16823
  setPeek(true);
16583
16824
  peekLockedUntil = Date.now() + PEEK_OPEN_LOCK_MS;
16584
16825
  }
@@ -16994,6 +17235,34 @@
16994
17235
  tracker. These don't conflict with stopPropagation because they
16995
17236
  match elements that no other capture handler swallows. */
16996
17237
  document.addEventListener("click", (e) => {
17238
+ // Mini-rail rooms / agents switcher · expand is handled by the
17239
+ // collapse IIFE; here we own the tab switch + default selection.
17240
+ // Seed the FIRST room / agent when coming from a non-room /
17241
+ // non-agent context (reports, notes, composer, the other tab) so
17242
+ // the icon actually lands you on content instead of restoring the
17243
+ // last sub-state (e.g. "new" / "reports"). If you're already
17244
+ // viewing a room / agent profile, leave it so expand keeps place.
17245
+ const miniTab = e.target.closest("[data-mini-tab]");
17246
+ if (miniTab) {
17247
+ e.preventDefault();
17248
+ const which = miniTab.getAttribute("data-mini-tab");
17249
+ if (which === "rooms") {
17250
+ if (!window.app || !window.app.currentRoomId) {
17251
+ const first = appFirstRoomId();
17252
+ if (first) lsSet(ROOMS_KEY, first);
17253
+ }
17254
+ } else if (which === "agents") {
17255
+ const agentView = document.querySelector('[data-main-view="agent"]');
17256
+ const inAgentProfile = agentView && !agentView.hasAttribute("hidden");
17257
+ if (!inAgentProfile) {
17258
+ const firstAgent = document.querySelector('[data-sidebar-panel="agents"] .agent-row[data-agent-profile]');
17259
+ const id = firstAgent && firstAgent.getAttribute("data-agent-profile");
17260
+ if (id) lsSet(AGENTS_KEY, id);
17261
+ }
17262
+ }
17263
+ activate(which, { fromTabClick: true });
17264
+ return;
17265
+ }
16997
17266
  // Tab click
16998
17267
  const tab = e.target.closest(".sidebar-tab[data-sidebar-tab]");
16999
17268
  if (tab) {
@@ -5041,19 +5041,20 @@
5041
5041
  const intro = typeof parsed.intro === "string" ? parsed.intro.trim() : "";
5042
5042
  const chairSynthesis = typeof parsed.chairSynthesis === "string" ? parsed.chairSynthesis.trim() : "";
5043
5043
 
5044
- // Resolve director id → display name from the rendered member
5045
- // list above (the brief renderer attaches `data-director-id` to
5046
- // each cover-byline author chip; fall back to the id when no
5047
- // mapping is found).
5044
+ // Resolve director id → display name from the global stash that
5045
+ // `load()` populates from state.members + historicalMembers +
5046
+ // chair. Falls back to scanning `[data-director-id]` chips for
5047
+ // legacy / forward-compat (none currently rendered, but cheap),
5048
+ // and finally to the id itself when nothing matches.
5048
5049
  const directorNameById = (() => {
5049
- const out = {};
5050
+ const out = Object.assign({}, window.__reportAgentNames || {});
5050
5051
  try {
5051
5052
  const chips = document.querySelectorAll("[data-director-id]");
5052
5053
  chips.forEach((el) => {
5053
5054
  const id = el.getAttribute("data-director-id");
5054
- if (id && el.textContent) out[id] = el.textContent.trim();
5055
+ if (id && el.textContent && !out[id]) out[id] = el.textContent.trim();
5055
5056
  });
5056
- } catch (_) { /* during pre-paint, no chips yet fall through */ }
5057
+ } catch (_) { /* defensive · DOM may not be ready */ }
5057
5058
  return out;
5058
5059
  })();
5059
5060
  const dn = (id) => directorNameById[id] || id || "—";
@@ -6420,6 +6421,25 @@
6420
6421
  const j = await stateRes.json();
6421
6422
  room = j.room;
6422
6423
  members = j.members || [];
6424
+ // Author-id → display-name map · drives `renderViewsCompared`'s
6425
+ // director chips ("Every director on the table", alignment
6426
+ // cards, divergence sides). Earlier the helper scraped
6427
+ // `[data-director-id]` chips off the rendered DOM, but no
6428
+ // chip ever carried that attribute — the lookup always fell
6429
+ // through and the chips printed raw agent ids like
6430
+ // `1khezh9da65g`. Stash a real lookup on window from BOTH
6431
+ // active members and historicalMembers (excused directors
6432
+ // still appear in views-compared if they participated) plus
6433
+ // the chair (occasionally referenced too).
6434
+ const nameMap = {};
6435
+ for (const m of (j.members || [])) {
6436
+ if (m && m.id && m.name) nameMap[m.id] = m.name;
6437
+ }
6438
+ for (const m of (j.historicalMembers || [])) {
6439
+ if (m && m.id && m.name && !nameMap[m.id]) nameMap[m.id] = m.name;
6440
+ }
6441
+ if (j.chair && j.chair.id && j.chair.name) nameMap[j.chair.id] = j.chair.name;
6442
+ window.__reportAgentNames = nameMap;
6423
6443
  } else {
6424
6444
  members = [];
6425
6445
  }