@gemboss/ui 0.1.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.
@@ -0,0 +1,407 @@
1
+ /* The assistant's popup / status bar / full-screen host — @gemboss/ui (28/09/2026).
2
+ *
3
+ * Moved from gemboss apps/admin/src/components/assistant/AssistantDockHost.module.css, where it was
4
+ * written for the admin's assistant; gemwatcher's and content-radar's assistants now wear the same
5
+ * host. Class names took a `gb-dock` prefix instead of CSS-module hashes (a consumer without CSS
6
+ * modules must be able to load it); every value is unchanged. The geometry assumes the GemBoss shell
7
+ * (@gemboss/ui AppShell): 56px topbar, 240px rail, drawer below 760px.
8
+ *
9
+ * Rendered by AssistantHost (assistant/host.tsx). State and the rules for when it shows are
10
+ * assistant/dock.ts + assistant/provider.tsx.
11
+ */
12
+
13
+
14
+ .gb-dock {
15
+ position: fixed;
16
+ z-index: 320; /* above the mobile tab bar (300), below the page editor (400) */
17
+ right: 24px;
18
+ bottom: 0;
19
+ /* MEASURED 2026-09-12, not assumed: the rail is 240px at every width above 760 (the shell's own
20
+ `margin-left: 240px`), and there is NO bottom tab bar — `.gemboss-mobile-nav` is not in the DOM
21
+ at 320 or 390. An earlier pass here wrote 72px and 56px from a stale comment in AppShell and
22
+ the popup covered a third of the rail, swallowing the menu clicks this feature exists to free.
23
+ 240 (rail) + 24 (right gutter) + 24 (left gap) = 288. */
24
+ /* 460 is the app's own modal width (`.ui-modal`), so the assistant reads as part of the same
25
+ family instead of a wider one-off. 600 was a guess; this one already exists. */
26
+ width: min(460px, calc(100vw - 288px));
27
+ display: flex;
28
+ flex-direction: column;
29
+ min-height: 0;
30
+ /* Frosted, the way the settings dialog reads — translucent surface over a small blur, so the page
31
+ underneath shows through as texture rather than detail.
32
+ NOT a full-screen scrim, though that is what settings uses. A scrim would block the page, and a
33
+ popup you cannot work behind is just the assistant page in a smaller box: the whole point of
34
+ this surface is that the screen under it stays usable. */
35
+ /* 93%, AND THE 5 POINTS THIS GAINED ON 25/09/2026 ARE A CONTRAST MEASUREMENT.
36
+ A translucent surface is `a·white + (1−a)·backdrop`, so the darkest it can ever go is `a·white`.
37
+ At 88% that floor was #E0E0E0, and `--gemboss-text-muted` (#686b71) measures 4.05:1 there —
38
+ UNDER AA. That is not a hypothetical corner of this component: `.header` and `.miniBar` both
39
+ declare `background: transparent` on purpose (see the note beside them), so `.headerStatus`
40
+ 12px, `.miniStatus` 12px and `.booting` 14px all sit DIRECTLY on this surface in that colour.
41
+ 93% puts the floor at #EDEDED and muted at 4.56:1. Chosen with, and kept equal to,
42
+ `.gemboss-popover-surface` in app/gemboss-shell.css — `one-popover-surface-four-wearers.test.ts`
43
+ fails if the two numbers ever stop agreeing. Change them THERE too.
44
+
45
+ TWO BACKGROUNDS, and the first is not decoration. `color-mix` and `backdrop-filter` ship on
46
+ DIFFERENT schedules — Firefox 103–112 and Safari 15.4–16.1 have the blur and not the mix — and
47
+ a declaration the parser rejects is DROPPED, not defaulted. Without the plain line, the dock had
48
+ NO background at all in those browsers: the page running straight through the chat. It shipped
49
+ that way on 12/09/2026 and nobody could see it from here. NOT a custom property: `background:
50
+ var(--some-mix-token)` would fail at computed-value time instead, which falls back to `unset`
51
+ rather than to the line above — worse than the bug it would be trying to share. */
52
+ background: var(--gemboss-surface);
53
+ background: color-mix(in srgb, var(--gemboss-surface) 93%, transparent);
54
+ -webkit-backdrop-filter: blur(14px) saturate(1.4);
55
+ backdrop-filter: blur(14px) saturate(1.4);
56
+ border: 1px solid var(--gemboss-divider);
57
+ border-bottom: 0;
58
+ border-radius: 12px 12px 0 0;
59
+ box-shadow: 0 -2px 6px rgba(15, 23, 42, 0.05), 0 24px 64px rgba(15, 23, 42, 0.18);
60
+ overflow: hidden;
61
+
62
+ /* ── Arriving ────────────────────────────────────────────────────────────────────────────────
63
+ Short and plain: 140ms, ease-out, a small rise and a fade. Long enough to read as "it came
64
+ from somewhere", short enough that nobody waits for it — a popup a seller opens twenty times
65
+ a day must not perform.
66
+
67
+ TRANSFORM AND OPACITY ONLY. Height and horizontal offset are set from a pointer, and a
68
+ transition on those means the box trails the finger instead of following it. Those two stay
69
+ instant by construction, not by luck. */
70
+ transition: transform 140ms cubic-bezier(0.2, 0.8, 0.3, 1), opacity 140ms ease-out;
71
+ }
72
+ /* The entry itself. `@starting-style` is what lets an element animate the frame it appears in
73
+ (Chrome 117+/Safari 17.5+; the desktop app is Chromium 132). Anywhere older simply appears —
74
+ degrading to instant is the right failure for chrome nobody asked to watch. */
75
+ @starting-style {
76
+ .gb-dock-dock, .gb-dock-mini { transform: translateY(10px); opacity: 0; }
77
+ }
78
+ /* Nothing animates while a pointer is setting the numbers — see `dragging` in the host. */
79
+ .gb-dock-dragging, .gb-dock-dragging * { transition: none !important; }
80
+ /* Where `backdrop-filter` is missing there is no blur to smear the page, and 93% opaque over sharp
81
+ body text is a chat you read through. Back to a solid surface — what this dock wore before it was
82
+ frosted. Same reasoning, same shape, as `.gemboss-popover-surface`. */
83
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
84
+ .gb-dock { background: var(--gemboss-surface); }
85
+ }
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .gb-dock { transition: none; }
88
+ }
89
+
90
+ /* The floating popup IS the base geometry above; this class exists so the DOM names the state —
91
+ `styles[mode]` was `undefined` for it, which is invisible in a screenshot and unnameable in a test. */
92
+ .gb-dock-dock { }
93
+
94
+ /* Still mounted — that is what keeps a run alive — just not on screen. */
95
+ .gb-dock-hidden { display: none; }
96
+
97
+ /* ── Full: exactly the area `main` occupies, so the dedicated page looks untouched ── */
98
+ .gb-dock-full {
99
+ /* BELOW `.gemboss-route-shell` (z-index 5), which covers this exact rectangle while the next route
100
+ loads. At the popup's 320 the assistant stayed painted over the holding screen, so leaving this
101
+ page looked like nothing happened — and the screen's click-catcher was defeated, so clicks
102
+ during the wait landed on chat controls. Fixed position still puts it over `main`'s content. */
103
+ z-index: 4;
104
+ top: 56px;
105
+ left: 240px; /* the rail, measured — see the note on .host */
106
+ right: 0;
107
+ bottom: 0;
108
+ width: auto;
109
+ border: 0;
110
+ border-radius: 0;
111
+ box-shadow: none;
112
+ background: var(--gemboss-app-bg);
113
+ }
114
+
115
+ /* ── Full: the one control full screen needs — the way back to the popup ──────────────────────
116
+ Floated over the chat's own header, whose right half is empty, rather than given a bar of its
117
+ own: a whole strip of chrome for one button would push the conversation down on every screen. */
118
+ .gb-dock-fullchrome {
119
+ position: absolute;
120
+ top: 10px;
121
+ right: 16px;
122
+ z-index: 1;
123
+ display: flex;
124
+ gap: 8px;
125
+ }
126
+ .gb-dock-shrink {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ gap: 6px;
130
+ min-height: 44px; /* §9.1 control tier */
131
+ padding: 0 12px;
132
+ border: 1px solid var(--gemboss-divider);
133
+ border-radius: var(--gemboss-radius-full);
134
+ background: var(--gemboss-surface);
135
+ color: var(--gemboss-text-muted);
136
+ font: inherit;
137
+ font-size: var(--gemboss-fs-caption);
138
+ cursor: pointer;
139
+ white-space: nowrap;
140
+ }
141
+ .gb-dock-shrink:hover { background: var(--gemboss-hover); color: var(--gemboss-text); }
142
+ .gb-dock-shrink:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
143
+ .gb-dock-shrink svg { width: 16px; height: 16px; flex: 0 0 auto; }
144
+ /* On a phone the label would crowd the chat header; the icon carries it there. */
145
+ @media (max-width: 560px) {
146
+ .gb-dock-shrink span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
147
+ .gb-dock-shrink { padding: 0; width: 44px; justify-content: center; }
148
+ /* Except the way to a person: a head-and-shoulders icon alone does not say "a human answers". */
149
+ .gb-dock-shrink-keep span { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
150
+ .gb-dock-shrink-keep { padding: 0 12px; width: auto; }
151
+ /* Two controls do not fit in the empty half of the chat header at phone width — floated, they sat
152
+ on the title ("Ask th…", measured at 320 and 390). In the flow they cost one short strip here,
153
+ and only here. */
154
+ .gb-dock-fullchrome { position: static; justify-content: flex-end; padding: 8px 12px 0; }
155
+ }
156
+
157
+ /* ── Mini: a status bar, not a window ── */
158
+ .gb-dock-mini {
159
+ height: auto;
160
+ width: min(320px, calc(100vw - 288px));
161
+ -webkit-backdrop-filter: blur(14px) saturate(1.4); /* Safari; the host above carries the rest */
162
+ backdrop-filter: blur(14px) saturate(1.4);
163
+ border-radius: 12px 12px 0 0;
164
+ }
165
+ .gb-dock-mini .gb-dock-body { display: none; }
166
+ .gb-dock-minirow { display: flex; align-items: center; padding-right: 4px; }
167
+ .gb-dock-minirow .gb-dock-minibar { flex: 1 1 auto; min-width: 0; }
168
+ /* `width: 44px` is a BASIS, not a floor: the default `flex-shrink: 1` let this button give ground to
169
+ the row beside it, so the × measured under 44 at all six widths while the rule above read as done.
170
+ MEASURED 2026-09-12 — the same trap as every other "the declaration is there" bug today. */
171
+ .gb-dock-minirow .gb-dock-icon { flex: 0 0 auto; }
172
+
173
+ .gb-dock-minibar {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 8px;
177
+ width: 100%;
178
+ min-height: 48px;
179
+ padding: 0 12px;
180
+ border: 0;
181
+ background: transparent;
182
+ font: inherit;
183
+ color: var(--gemboss-text-strong);
184
+ cursor: pointer;
185
+ text-align: left;
186
+ }
187
+ .gb-dock-minibar:hover { background: var(--gemboss-hover); }
188
+ .gb-dock-minibar strong { font-size: 13px; font-weight: 600; flex: 0 0 auto; }
189
+ .gb-dock-ministatus {
190
+ flex: 1 1 auto;
191
+ min-width: 0;
192
+ font-size: 12px;
193
+ color: var(--gemboss-text-muted);
194
+ overflow: hidden;
195
+ text-overflow: ellipsis;
196
+ white-space: nowrap;
197
+ }
198
+ .gb-dock-minichevron { font-size: 10px; color: var(--gemboss-text-muted); }
199
+
200
+ /* ── Resize grip (dock only) ── */
201
+ .gb-dock-grip {
202
+ /* OVERLAID on the header's top edge, not stacked above it. As its own 14px row it pushed the
203
+ title down and the header read as top-heavy — Chris: "header hơi lệch do padding top hơi nhiều".
204
+ The drag area is unchanged; it simply no longer costs vertical space. */
205
+ position: absolute;
206
+ top: 0;
207
+ left: 0;
208
+ right: 0;
209
+ height: 14px;
210
+ z-index: 2;
211
+ display: grid;
212
+ place-items: center;
213
+ cursor: ns-resize;
214
+ touch-action: none;
215
+ background: transparent;
216
+ }
217
+ .gb-dock-grip > span {
218
+ display: block;
219
+ width: 44px; /* a mouse target wide enough to find without aiming */
220
+ height: 4px;
221
+ border-radius: 2px;
222
+ background: var(--gemboss-divider);
223
+ }
224
+ .gb-dock-grip:hover > span, .gb-dock-grip:focus-visible > span { background: var(--gemboss-text-muted); }
225
+ .gb-dock-grip:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
226
+
227
+ /* ── Dock header ── */
228
+ .gb-dock-header {
229
+ flex: 0 0 auto;
230
+ cursor: grab; /* the strip itself drags the popup sideways */
231
+ touch-action: none;
232
+ user-select: none;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 8px;
236
+ padding: 0 4px 0 16px;
237
+ min-height: 48px;
238
+ border-bottom: 1px solid var(--gemboss-divider);
239
+ background: transparent; /* the frosted host shows through; a second fill would double it */
240
+ }
241
+ .gb-dock-title {
242
+ font-size: 13px;
243
+ font-weight: 600;
244
+ color: var(--gemboss-text-strong);
245
+ white-space: nowrap;
246
+ /* Let the NAME absorb a squeeze. Adding the + button pushed the header to 290px inside a 277px
247
+ popup at 320 — a nowrap title with no min-width cannot give ground, so the row overflowed
248
+ instead. Measured 2026-09-12. */
249
+ flex: 0 1 auto;
250
+ min-width: 0;
251
+ overflow: hidden;
252
+ text-overflow: ellipsis;
253
+ }
254
+ .gb-dock-status {
255
+ flex: 1 1 auto;
256
+ min-width: 0;
257
+ font-size: 12px;
258
+ color: var(--gemboss-text-muted);
259
+ overflow: hidden;
260
+ text-overflow: ellipsis;
261
+ white-space: nowrap;
262
+ }
263
+ .gb-dock-header:active { cursor: grabbing; }
264
+ .gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; }
265
+ /* One ACTION, then the three WINDOW controls — ordered by how much each changes the window: bigger,
266
+ smaller, gone. The gap is what says they are two different kinds of button; without it four icons
267
+ in a row read as one undifferentiated strip, which is what "các icon đang không đồng đều" was
268
+ describing. The 📌 that used to sit in the middle of them is gone entirely — see lib/assistant-dock. */
269
+ .gb-dock-action { margin-right: 8px; }
270
+
271
+ .gb-dock-icon {
272
+ /* 44px control target — Apple HIG / WCAG 2.5.5. The icon inside stays 18px. */
273
+ width: 44px;
274
+ height: 44px;
275
+ display: grid;
276
+ place-items: center;
277
+ border: 0;
278
+ border-radius: 8px;
279
+ background: transparent;
280
+ color: var(--gemboss-text-muted);
281
+ cursor: pointer;
282
+ padding: 0;
283
+ }
284
+ .gb-dock-icon:hover { background: var(--gemboss-hover); color: var(--gemboss-text-strong); }
285
+ .gb-dock-icon:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
286
+ .gb-dock-icon svg { width: 18px; height: 18px; }
287
+ .gb-dock-icon-on { color: var(--gemboss-active-fg); background: var(--gemboss-active-bg); }
288
+
289
+ .gb-dock-dot {
290
+ flex: 0 0 auto;
291
+ width: 8px;
292
+ height: 8px;
293
+ border-radius: 50%;
294
+ background: var(--gemboss-divider);
295
+ }
296
+ .gb-dock-dot-busy { background: var(--gemboss-primary); animation: gb-dock-pulse 1.4s ease-in-out infinite; }
297
+ .gb-dock-dot-unread { background: var(--gemboss-success-strong); }
298
+ @keyframes gb-dock-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
299
+ @media (prefers-reduced-motion: reduce) { .gb-dock-dot-busy { animation: none; } }
300
+
301
+ /* ── Tabs: the assistant, or a person on the Gem team (dock only) ──────────────────────────────
302
+ Their own row under the header rather than inside it: at 320px the header already spends 176px
303
+ on four 44px controls, and two text tabs do not fit in what is left. */
304
+ .gb-dock-tabs {
305
+ flex: 0 0 auto;
306
+ display: flex;
307
+ gap: 4px;
308
+ padding: 0 8px;
309
+ border-bottom: 1px solid var(--gemboss-divider);
310
+ }
311
+ .gb-dock-tab {
312
+ position: relative;
313
+ display: inline-flex;
314
+ align-items: center;
315
+ gap: 6px;
316
+ min-height: 44px; /* §9.1 control tier */
317
+ padding: 0 12px;
318
+ border: 0;
319
+ border-bottom: 2px solid transparent;
320
+ background: transparent;
321
+ color: var(--gemboss-text-muted);
322
+ font: inherit;
323
+ font-size: 13px;
324
+ cursor: pointer;
325
+ white-space: nowrap;
326
+ }
327
+ .gb-dock-tab:hover { color: var(--gemboss-text-strong); }
328
+ .gb-dock-tab[aria-selected="true"] { color: var(--gemboss-text-strong); font-weight: 600; border-bottom-color: var(--gemboss-primary); }
329
+ .gb-dock-tab:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
330
+ .gb-dock-badge {
331
+ min-width: 18px;
332
+ padding: 0 6px;
333
+ border-radius: var(--gemboss-radius-full);
334
+ background: var(--gemboss-primary);
335
+ color: #fff;
336
+ font-size: 11px;
337
+ font-weight: 700;
338
+ line-height: 18px;
339
+ text-align: center;
340
+ }
341
+
342
+ /* ── Body: the assistant itself ── */
343
+ .gb-dock-body {
344
+ flex: 1 1 auto;
345
+ min-height: 0;
346
+ display: flex;
347
+ min-width: 0;
348
+ }
349
+ .gb-dock-body > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
350
+ /* Each tab's pane. A hidden pane stays MOUNTED — `display:none`, never unmount (see the host). */
351
+ .gb-dock-pane { display: flex; min-width: 0; min-height: 0; }
352
+ .gb-dock-pane > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
353
+ /* The package's own workspace (AssistantWorkspaceLayout) sizes itself to the viewport on its own page;
354
+ inside the host it fills the host instead. Moved here from the admin's assistant-dock-admin.css
355
+ (28/09/2026): every surface that puts the layout in the host needs it, or the layout keeps
356
+ `height: calc(100dvh - 56px)` inside a popup a few hundred pixels tall. */
357
+ .gb-dock-body .gemboss-assistant-layout,
358
+ .gb-dock-body .gemboss-assistant-page { height: auto; min-height: 0; }
359
+ .gb-dock-pane[hidden] { display: none; }
360
+
361
+ .gb-dock-booting {
362
+ flex: 1 1 auto;
363
+ display: grid;
364
+ place-items: center;
365
+ padding: 24px;
366
+ color: var(--gemboss-text-muted);
367
+ font-size: 14px;
368
+ }
369
+
370
+ .gb-dock-sr {
371
+ position: absolute;
372
+ width: 1px; height: 1px;
373
+ padding: 0; margin: -1px;
374
+ overflow: hidden;
375
+ clip: rect(0 0 0 0);
376
+ white-space: nowrap;
377
+ border: 0;
378
+ }
379
+
380
+ /* ── Phone: the popup spans the screen above the bottom tab bar; full mode loses the rail gutter ── */
381
+ /* ── Narrow: five 44px controls + a title do not fit ────────────────────────────────────────
382
+ MEASURED 2026-09-12 at 320px: the header row wanted 342px inside a 291px popup. 5 × 44 = 220
383
+ leaves 71px for a title and a status line. So the two controls that are about BROWSING WHILE IT
384
+ RUNS — the pin, and the word for what the dot already says — stand down; new chat, full screen,
385
+ minimize and close stay. Dropping a control beats letting the row overflow off the edge. */
386
+ @media (max-width: 420px) {
387
+ .gb-dock-status { display: none; }
388
+ }
389
+
390
+ /* ── Phone: the rail is an off-canvas drawer, so both geometries reclaim the full width ── */
391
+ @media (max-width: 760px) {
392
+ .gb-dock {
393
+ right: 8px;
394
+ left: 8px;
395
+ width: auto;
396
+ padding-bottom: env(safe-area-inset-bottom);
397
+ border-radius: 12px 12px 0 0;
398
+ }
399
+ .gb-dock-mini { width: auto; }
400
+ .gb-dock-full {
401
+ left: 0;
402
+ right: 0;
403
+ top: 56px;
404
+ bottom: 0;
405
+ border-radius: 0;
406
+ }
407
+ }