@gemboss/ui 0.30.2 → 0.33.1
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/CHANGELOG.md +150 -0
- package/README.md +115 -4
- package/catalog/editor.html +1 -1
- package/catalog/index.html +321 -160
- package/gemboss-ui.css +4 -4
- package/package.json +1 -1
- package/parts/components.css +2 -2
- package/parts/core.css +1 -1
- package/parts/shell.css +2 -2
- package/src/assistant/bridge.tsx +636 -0
- package/src/assistant/chat-markdown.ts +3 -2
- package/src/assistant/chat.tsx +45 -7
- package/src/assistant/engine.tsx +21 -4
- package/src/assistant/host.tsx +113 -31
- package/src/assistant/index.ts +3 -1
- package/src/assistant.css +1 -1
- package/src/chat-plain.js +1 -1
- package/src/collections.css +1 -1
- package/src/editor.css +1 -1
- package/src/index-table.tsx +4 -2
- package/src/portal.css +1 -1
- package/src/resource-list-select.tsx +4 -2
- package/src/search-dialog.tsx +67 -13
- package/src/selection.tsx +71 -8
- package/src/shell.css +1 -1
- package/src/shell.tsx +88 -0
- package/src/ui.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,156 @@
|
|
|
2
2
|
|
|
3
3
|
Versions up to 0.9.0 are described in the README, each in the section that introduced it.
|
|
4
4
|
|
|
5
|
+
## 0.33.1 (10/10/2026)
|
|
6
|
+
|
|
7
|
+
**PanelTabs' row (`.gemboss-ec-tabs`) carries what GemFactory patched in its own CSS** (server/ui.mjs "VÁ GÓI: PanelTabs
|
|
8
|
+
cột", 06/10/2026, with "move it into the kit's editor.css" written on it). editor.css.
|
|
9
|
+
|
|
10
|
+
Changed
|
|
11
|
+
- A tab label never wraps: a tab is never narrower than its text (`min-width: max-content`, `nowrap`), the tabs still
|
|
12
|
+
share the row while they fit, and the ROW scrolls sideways when they do not (thin bar, `overscroll-behavior-x:
|
|
13
|
+
contain`). Four tabs with two-word labels in a 240px column broke onto two lines.
|
|
14
|
+
- Hover on an unselected tab (`--gemboss-primary-soft`, text to `--gemboss-text`); keyboard focus draws the `--ui-focus`
|
|
15
|
+
ring (the browser's own before).
|
|
16
|
+
- 44px tall at 767px and below under a mouse too (it was only under `pointer: coarse`).
|
|
17
|
+
|
|
18
|
+
## 0.33.0 (10/10/2026)
|
|
19
|
+
|
|
20
|
+
**The bulk bar stays one row.** talent-radar /pool moved to `SelectableResourceList` (collections PR 6) with five bulk
|
|
21
|
+
actions, and the bar wrapped to three rows at 390 (~150px above the first candidate); its own bar had been one
|
|
22
|
+
scrolling row since 03/10. README "Collections".
|
|
23
|
+
|
|
24
|
+
Changed
|
|
25
|
+
- `BulkBar` (IndexTable, SelectableResourceList): actions that do not fit in the row after the box and the count go into
|
|
26
|
+
a ⋯ `Menu` at its end (`.ui-bulkbar__more`), in order; each action is either a button or a menu row, never both. The
|
|
27
|
+
widths come from a hidden copy of the buttons (`.ui-bulkbar__measure`) in the surface's font, re-measured on resize
|
|
28
|
+
and when the count changes, so the fold follows the real labels (two short actions stay buttons at 320; five long
|
|
29
|
+
ones fold at 768), not a breakpoint. Menu rows are off with nothing selected, like the buttons. Before the first
|
|
30
|
+
measure (`data-fit="pending"`, a server render before hydration) the row does not wrap; the overflow is clipped.
|
|
31
|
+
Plain HTML bars (kit-plain.js) are unchanged.
|
|
32
|
+
|
|
33
|
+
- A linked activity label (`CompactList variant="activity"`, `CompactItem.href`) is text-coloured (underline on hover)
|
|
34
|
+
instead of the browser's blue, and its box is 24px (WCAG 2.5.8) with the row's height unchanged: talent-radar
|
|
35
|
+
/activity had 71 links under 24px at 320 and 390.
|
|
36
|
+
|
|
37
|
+
Added
|
|
38
|
+
- `fitActions(avail, widths, gap, menuW)`: the fold rule, exported for tests.
|
|
39
|
+
- `moreActionsLabel` on `IndexTable` and `SelectableResourceList` (default "More actions").
|
|
40
|
+
|
|
41
|
+
## 0.32.1 (10/10/2026)
|
|
42
|
+
|
|
43
|
+
**Anchors land below the topbar, and at all.** The shell's document scrolls under the fixed 56px `.gemboss-platform-topbar`, so a jump
|
|
44
|
+
to `#section` (or `scrollIntoView()`) parked the section's heading behind the bar. search-radar worked around it with
|
|
45
|
+
`scrollMarginTop: 72` on four sections (`/aeo#changes`, `#gaps`, `#trend`, `/aeo/brand/<x>#evidence`, 08/10/2026).
|
|
46
|
+
|
|
47
|
+
Fixed
|
|
48
|
+
- `:root:has(.gemboss-platform-topbar) { scroll-padding-top: 64px }` in shell.css (56 + 8 of air), only while the bar is on
|
|
49
|
+
the page; a surface without the shell keeps the browser default. A surface's own `scroll-margin-top` on a section can
|
|
50
|
+
go. anchor-scroll.browser.test.ts (real Chromium, 390 and 1280; red without the rule: the heading at 0, under the bar).
|
|
51
|
+
- A hard load of `/page#section` on a streamed page (Next App Router + loading.tsx) now reaches the section. The browser
|
|
52
|
+
looked for the fragment while it sat in React's hidden Suspense block and scrolled nowhere: on search-radar `/aeo#gaps`
|
|
53
|
+
stayed at the top on 5 of 6 loads. `AppShell` keeps the target where a fragment jump puts it while the page is still
|
|
54
|
+
arriving (until 600 ms of a quiet DOM, 5 s at most), and stops the moment the reader scrolls, presses a key, touches or
|
|
55
|
+
clicks. Once per load; client navigations are Next's. Measured on search-radar with this build: 36 of 36 loads land.
|
|
56
|
+
|
|
57
|
+
## 0.32.0 (09/10/2026)
|
|
58
|
+
|
|
59
|
+
**The bridge, once** (assistant sync, Chris 08/10/2026: "mọi service có panel này đồng bộ style + code dùng gemboss ui").
|
|
60
|
+
content-radar, partner-radar and search-radar each carried ~300 lines for the bridge on the user's machine, copied one
|
|
61
|
+
from the other, and reviews fixed one copy at a time. README "The bridge".
|
|
62
|
+
|
|
63
|
+
Added
|
|
64
|
+
- `createBridgeClient({ port, codeKey, app, tokenPath?, tokenSince? })`: `probe`, `detect` (Chrome's Local Network Access
|
|
65
|
+
permission read first; asked from a click only), `ensureCode`, `installCommand`, `dataToken` (reused until under an hour
|
|
66
|
+
is left; its error is the turn's), `turn(body, { signal, version })` (code + token; errors marked `atBridge` with the
|
|
67
|
+
status, so `bridgeTurnError` reads 401 as "another code" and a dead connection as "gone"), `control("stop" | "remove")`.
|
|
68
|
+
- `readTurnEvents(res, onEvent, { atBridge })` (the turn's `data:` events, whole across chunks, heartbeats skipped; a
|
|
69
|
+
read that dies is the bridge's only for the bridge's stream; a stream that ends without `done` throws
|
|
70
|
+
`BRIDGE_CLOSED_EARLY` / `SERVER_CLOSED_EARLY` instead of showing half an answer as done; an early exit cancels the
|
|
71
|
+
reader) and `postTurn(url, body)` for the surface's own route.
|
|
72
|
+
- `useBridge(client, { open, wantsLocal, pickerOpen, statusUrl, onDetected })`: probes while open, the server's engines
|
|
73
|
+
and the bridge together, merged (`engines`); a 3 s poll while a teammate installs; again on focus / tab / permission
|
|
74
|
+
change, also once paired, one at a time, and dropped when the user acted while it was out (an epoch); `lost()`,
|
|
75
|
+
`pair(code)`, `allowAccess()`, `control(kind)` (401 → Not connected, no answer → lost), `recheck()`. `local` is decided
|
|
76
|
+
after mount (no hydration mismatch on the dev machine); the client is pinned to the first render; a pairing code that
|
|
77
|
+
storage refuses lives in memory, so the command and the turns carry the same one.
|
|
78
|
+
- `bridgePickerProps(b, { latest, busy, controls, onControl, outdatedDetail, installNote, labels })` → `EnginePicker`'s
|
|
79
|
+
`bridge`, `setup` (per case: Allow / unblock / `BridgePairForm` + command / install / update) and `statusActions`
|
|
80
|
+
(Check again, Stop, Remove). `BRIDGE_SETUP_EN`, `bridgeControlNote`, `pickerBridgeState`, `mergeBridgeEngines`.
|
|
81
|
+
- `useEnginePicker().close()`: closes and puts focus on the chip at once (Stop / Remove before a confirm opens); also
|
|
82
|
+
`focusChip()`, `chipRef`, and `EnginePicker chipRef`. Every surface did it with `querySelector(".gb-engine-chip")`.
|
|
83
|
+
- `AssistantChat` / `ChatMarkdown` `linkPolicy(href, label, target)`: one prop for every link in an answer (written, bare,
|
|
84
|
+
a picture from another host): `true` = the kit's link, `false` = its words, a node = drawn as given; `target` is what
|
|
85
|
+
the kit would link (normalised) or null, for a policy that draws its own link. Replaces the
|
|
86
|
+
`ownLink` + `renderLink` pair used to turn links into text (kept; `linkPolicy` wins when both are given). A claimed
|
|
87
|
+
token carries `own: true`. React only (`chat-plain.js` is at its weight ceiling; gemcare keeps the pair).
|
|
88
|
+
|
|
89
|
+
Changed
|
|
90
|
+
- `AssistantChat` goes back to the end when a user message is ADDED, wherever it was sent from (a page's button, the
|
|
91
|
+
📎 upload): before, only a send from its own composer did, and partner-radar scrolled by hand (#968). A note or an
|
|
92
|
+
answer arriving while the reader is up leaves them there, and so does an edit that drops a user message or swaps its id
|
|
93
|
+
(negative controls in chat-browser.test.tsx; the 0.31.0 pinKey fails the new test: 1717px from the end).
|
|
94
|
+
- `BridgePairForm` says "Pairing…" and takes one press at a time while the probe runs (up to 8 s).
|
|
95
|
+
- `chat-plain.js`: a token its `ownLink` claims carries `own: true`, like the React parser's.
|
|
96
|
+
|
|
97
|
+
No stylesheet changed; chat-plain.js stays under its weight ceiling. New tests: bridge.test.tsx (14),
|
|
98
|
+
bridge-browser.test.tsx (8, Chromium, fake bridge on test port 8797 answered by the router: open, poll, lost, pair,
|
|
99
|
+
permission, Stop, a stale probe after Stop, Stop answered 401), chat.test.tsx (+2), chat-browser.test.tsx (+1),
|
|
100
|
+
engine-browser.test.tsx (+1). Review #3952 (in this version): probes ordered by an epoch and one at a time, a stream
|
|
101
|
+
without `done` is an error, the reader is cancelled on an early exit, `linkPolicy` gets the checked target, the
|
|
102
|
+
README's `onDetected` keeps a "your machine only" engine, a stored-code fallback in memory, `local` after mount, the
|
|
103
|
+
client pinned, Stop's 401 is Not connected, the pinKey counts additions only.
|
|
104
|
+
|
|
105
|
+
## 0.31.0 (09/10/2026)
|
|
106
|
+
|
|
107
|
+
The assistant header groups its controls (Chris 08/10/2026: "cái button clear history cần đưa lên topbar để tiết kiệm
|
|
108
|
+
không gian nhưng trên đó nhiều icon rồi nên cần gom nhóm"). One change in the package, every surface follows.
|
|
109
|
+
|
|
110
|
+
- `AssistantHost` header: four controls, + New chat, ⤢ Open full screen, ⋯ More, × Close. "Pin to the side" /
|
|
111
|
+
"Float as a popup" and "Shrink to a bar" (minimise) are rows of ⋯ now, not icons. At 1440 the header held five.
|
|
112
|
+
- `onClearHistory` (+ `canClearHistory`): a "Clear history" row (danger) in ⋯, in the panel header and in full screen's
|
|
113
|
+
chrome. The surface confirms (it knows what is cleared). Before this, content-radar spent a 44px row of its body on it.
|
|
114
|
+
- `menuItems` + `AssistantMenuItem`: a surface's own rows at the top of ⋯.
|
|
115
|
+
- ⋯ wears the fleet's row (`ui-menu__item`) and surface (`gemboss-popover-surface`) from shell.css, which the admin
|
|
116
|
+
loads; its placement is in assistant.css (`.gb-dock-menu*`), because the kit's `.ui-menu` rules are in kit-plain.css,
|
|
117
|
+
which the admin does not load. Escape closes ⋯ only (focus back on ⋯; the surface's "Escape hides the assistant" does
|
|
118
|
+
not fire on the same key; an Escape meant for a modal elsewhere is left to it); a press outside or the window losing
|
|
119
|
+
focus closes it; pressing ⋯ does not drag the popup. A row closes ⋯ and focuses it BEFORE its handler, so a confirm
|
|
120
|
+
the handler opens returns focus to ⋯, not to <body>. No `menu` / `menuitem` roles (no arrow keys, like the kit's Menu);
|
|
121
|
+
the rows do not inherit the popup header's "Drag this bar…" tooltip.
|
|
122
|
+
- Labels `more`, `clearHistory`. Tests: `assistant/header-menu.browser.test.ts` (Chromium, the admin's stylesheets only).
|
|
123
|
+
|
|
124
|
+
`SearchDialog` for a surface whose topbar centre IS the search (content-radar, 08/10/2026: "click opens a popup like
|
|
125
|
+
other surfaces"):
|
|
126
|
+
|
|
127
|
+
- `trigger="field"`: the door is the topbar's search box (TopbarSearch's `gemboss-topbar-search`: 36px, 44px under a
|
|
128
|
+
finger or at phone width, the hint at the right, `hint` prop, ⌘K by default), a link that opens the popup. `icon`
|
|
129
|
+
(default) is the 36px magnifier as before.
|
|
130
|
+
- `SearchItem.external`: a result outside the surface opens in a new tab (link or Enter) and the popup closes behind it,
|
|
131
|
+
marked ↗. Before, every result navigated the tab, so a collected post took the person off the surface.
|
|
132
|
+
- A search that throws says "Search failed: <message>" instead of "Nothing matches.".
|
|
133
|
+
- `onNavigate(href)`: a result inside the surface (Enter, plain click) goes through it instead of a full reload, so a
|
|
134
|
+
Next surface keeps a running assistant answer (content-radar passes `router.push`). ⌘ / Ctrl / Shift / Alt-click is
|
|
135
|
+
left to the browser and keeps the popup open.
|
|
136
|
+
- `minChars`: fewer characters say "Type at least N characters." instead of "Nothing matches.".
|
|
137
|
+
- Escape closes the popup in one press and is marked handled: a surface's own window listener ("Escape hides the
|
|
138
|
+
assistant") no longer fired on the same key, and the search box's clear-the-words Escape no longer took the first press.
|
|
139
|
+
- The `field` door has no aria-label: its name is the words on it (WCAG 2.5.3).
|
|
140
|
+
- Closing forgets the answer, and an answer still on its way when it closed: reopened, the popup drew the previous results
|
|
141
|
+
and highlight for a frame, and Enter in that frame opened the previous highlight. Tests: `search-dialog.browser.test.ts` (with a negative control: the reopen case
|
|
142
|
+
goes red without the reset).
|
|
143
|
+
|
|
144
|
+
`.ui-btn--on` (ui.css): the ON look for a plain button (accent-soft fill, accent border, accent-strong text, the kit's
|
|
145
|
+
selected look), for a CTA whose thing is already in effect ("Scheduled · Fri 09:00").
|
|
146
|
+
|
|
147
|
+
## 0.30.3 (09/10/2026)
|
|
148
|
+
|
|
149
|
+
- The inspector under a finger (`pointer: coarse`): its tabs (`.gemboss-ei-tab`, were 27px) and group heads
|
|
150
|
+
(`.gemboss-ei-gh`, were 13px) are at least 44px tall, and its colour swatches (`.gemboss-ei-sw`) are 44x44 (were 30x30)
|
|
151
|
+
and wrap in a narrow column. Measured on the funnel canvas's theme Settings tab at 320 / 390 / 768 with touch; the theme
|
|
152
|
+
editor's inspector is the same component. Mouse sizes are unchanged. (The lockfile's entry for the package was still
|
|
153
|
+
0.30.1; it now says 0.30.3.)
|
|
154
|
+
|
|
5
155
|
## 0.30.2 (09/10/2026)
|
|
6
156
|
|
|
7
157
|
The admin's chat moves onto the package's composer and engine picker (gemboss admin, assistant sync).
|
package/README.md
CHANGED
|
@@ -290,6 +290,17 @@ Its styles are the `gemboss-assistant-*` rules in `packages/ui/src/shell.css` (t
|
|
|
290
290
|
@imports too), and they reach you through `gemboss-ui.css` together with `gemboss-field.css`. The admin passes a second tab ("Talk to a person", gemcare) through `secondTab` and its button
|
|
291
291
|
through `fullActions`; a surface without those leaves them out.
|
|
292
292
|
|
|
293
|
+
**The header's ⋯ menu (0.31.0).** The header shows four controls: + (`onNewChat`), full screen, ⋯ and ×. ⋯ holds
|
|
294
|
+
what is used once in a while: your rows (`menuItems`, as `AssistantMenuItem`s), "Clear history" when you pass
|
|
295
|
+
`onClearHistory` (greyed out with `canClearHistory={false}`; confirm inside your handler), then pin / float and
|
|
296
|
+
minimise. Full screen's chrome shows ⋯ too when you pass either. Do not draw a Clear button of your own in the body.
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
<AssistantHost title="Radar Assistant" onNewChat={reset} onExpand={expand} onShrink={shrink}
|
|
300
|
+
onClearHistory={clearHistory} canClearHistory={messages.length > 0}
|
|
301
|
+
menuItems={<AssistantMenuItem onClick={exportChat}>Export chat</AssistantMenuItem>}>
|
|
302
|
+
```
|
|
303
|
+
|
|
293
304
|
## The engine picker (0.25.0): `EnginePicker`, `BridgeNotice`
|
|
294
305
|
|
|
295
306
|
Which engine answers the assistant, and the bridge on the user's machine, without spending the chat's space on it
|
|
@@ -327,8 +338,14 @@ const picker = useEnginePicker(); // open + tab, shared with the notice
|
|
|
327
338
|
lists each engine with its own models at once, and one click picks both and closes (the admin's Polaris popup);
|
|
328
339
|
`problem` is a red line on the Engine tab ("Wait for the current answer to finish"); `disabled` disables the chip while
|
|
329
340
|
a switch is in flight.
|
|
330
|
-
- The
|
|
331
|
-
|
|
341
|
+
- The picker itself knows no bridge: the surface hands in its state, the command and the callbacks. Since 0.32.0 the
|
|
342
|
+
surface usually gets all three from `useBridge` + `bridgePickerProps` (next section). `EnginePanel` is the same tabs
|
|
343
|
+
without the chip (catalog, a settings page).
|
|
344
|
+
- **Closing from inside (0.32.0)**: a button in the panel that acts and then shuts it (Stop bridge, Remove, before a
|
|
345
|
+
confirm opens) calls `picker.close()`: it closes and puts focus on the chip at once, so the confirm records the chip as
|
|
346
|
+
where to return. `picker.props.onOpenChange(false)` (an outside click) closes without moving focus; the button that had
|
|
347
|
+
it is gone with the panel, so focus would fall to `<body>`. `picker.focusChip()` / `picker.chipRef` for the rest;
|
|
348
|
+
`EnginePicker chipRef` takes your own ref.
|
|
332
349
|
|
|
333
350
|
Tests: `packages/ui/src/assistant/engine.test.tsx` (markup, styles, its own entry), `packages/ui/src/assistant/engine-browser.test.tsx`
|
|
334
351
|
(Chromium: top layer through a clipping frame, keys, focus, copy, Open setup, 44px; with negative controls).
|
|
@@ -377,6 +394,83 @@ which would otherwise sit above it in the top layer (the component does the same
|
|
|
377
394
|
clipping frame with its negative control, no popover API, keys, Escape, Copy, redraws that keep focus / scroll / a
|
|
378
395
|
caller's field without a blur or change, merge rules, 44px).
|
|
379
396
|
|
|
397
|
+
## The bridge (0.32.0): `useBridge`, `createBridgeClient`, `bridgePickerProps`
|
|
398
|
+
|
|
399
|
+
The bridge is the small server a teammate installs on their machine so the assistant can run Claude Code / Codex on their
|
|
400
|
+
own subscription; the page talks to it at `http://127.0.0.1:<port>`, and the surface's server never sees that call.
|
|
401
|
+
content-radar, partner-radar and search-radar carried the same ~300 lines for it each (find it behind Chrome's Local Network
|
|
402
|
+
Access permission, pair, a data token, a turn and its stream, polls, "it went away", the Setup / Status tabs), and a
|
|
403
|
+
review fixed one copy at a time. Once, here:
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
import { bridgePickerProps, bridgeControlNote, createBridgeClient, postTurn, readTurnEvents, useBridge } from "@gemboss/ui/assistant";
|
|
407
|
+
|
|
408
|
+
// Module level (one per page): its data token is cached on it.
|
|
409
|
+
const bridgeClient = createBridgeClient({ port: 8789, codeKey: "pr-bridge-code", app: "partner-radar" });
|
|
410
|
+
|
|
411
|
+
const picker = useEnginePicker();
|
|
412
|
+
const b = useBridge(bridgeClient, { open, wantsLocal: engine !== "api", pickerOpen: picker.open,
|
|
413
|
+
// An engine that cannot run here (not on the server, and not "your machine only": the bridge may come) → the API one.
|
|
414
|
+
onDetected: (eng) => setEngine((cur) => (eng[cur] && !eng[cur].installed && !eng[cur].localOnly && eng.api?.installed ? "api" : cur)) });
|
|
415
|
+
// b.bridge (absent | unpaired | paired | needs-permission | asking | blocked, + lost / off / wrong), b.status (the
|
|
416
|
+
// server's engines), b.engines (merged: a paired bridge's engines replace "runs on your machine only"), b.pairCode.
|
|
417
|
+
|
|
418
|
+
async function send(text) {
|
|
419
|
+
const viaBridge = engine !== "api" && b.paired;
|
|
420
|
+
const body = { message: text, engine, sessionId, page };
|
|
421
|
+
const id = turns.start();
|
|
422
|
+
try {
|
|
423
|
+
const res = viaBridge ? await bridgeClient.turn(body, { signal, version: b.version }) : await postTurn("/api/assistant", body, { signal });
|
|
424
|
+
await readTurnEvents(res, (ev) => { if (ev.type === "text") turns.write(id, ev.delta); /* … */ }, { atBridge: viaBridge });
|
|
425
|
+
turns.finish(id);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (turns.fail(id, e).bridge) b.lost(); // gone / 401 → BridgeNotice + probe again; anything else is the turn's
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function onControl(kind) { // Stop / Remove on Status
|
|
432
|
+
picker.close(); // closes, focus on the chip (the confirm returns there)
|
|
433
|
+
if (kind === "remove" && !(await confirm(…))) return;
|
|
434
|
+
try { turns.note(bridgeControlNote(kind, await b.control(kind)), { bridge: true }); }
|
|
435
|
+
catch (e) { turns.note(`Couldn't ${kind} the bridge: ${e.message}`); }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
<EnginePicker {...picker.props} engines={…} engine={engine} onEngineChange={setEngine}
|
|
439
|
+
{...bridgePickerProps(b, { latest: BRIDGE_LATEST, busy, controls: engine !== "api", onControl,
|
|
440
|
+
installNote: "… A bridge installed for content-radar does not count: this one uses another port." })}
|
|
441
|
+
showBridgeDot={engine !== "api"} />
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- **`createBridgeClient`** (no React): `probe(code)` (401 = another code → `unpaired`; any failure → `absent`; never
|
|
445
|
+
throws), `detect({ fromClick })` (reads the permission first: `denied` → `blocked`; not asked → `needs-permission`
|
|
446
|
+
and no call at all; from a click it waits 60 s, the dialog's time), `ensureCode()` (8 characters, no 0/O/1/I, kept in
|
|
447
|
+
`codeKey`), `installCommand(code)`, `dataToken()` (POST `tokenPath`, reused until under an hour is left; its error is the
|
|
448
|
+
turn's, never the bridge's), `turn(body, { signal, version })` (adds the code and the token; `tokenSince` keeps the
|
|
449
|
+
header off an older bridge whose CORS refuses it; throws marked `atBridge`, with the status), `control("stop" | "remove")`.
|
|
450
|
+
- **`readTurnEvents(res, onEvent, { atBridge })`**: the turn's `data: {…}` events, whole across chunks, `: ping` skipped. A
|
|
451
|
+
read that throws is the bridge's only for the bridge's stream; the user's Stop stays a Stop. A stream that ends WITHOUT
|
|
452
|
+
`done` throws (`BRIDGE_CLOSED_EARLY`, the bridge's; `SERVER_CLOSED_EARLY`, the turn's): half an answer is not a finished
|
|
453
|
+
one. Any early exit cancels the reader, so the bridge sees the client go. `postTurn(url, body)` is the same POST to the
|
|
454
|
+
surface's own route (errors unmarked, with `status`).
|
|
455
|
+
- **`useBridge`**: probes only while `open`, the server's engines and the bridge together (`statusUrl`, default
|
|
456
|
+
`/api/assistant`); every 3 s while `wantsLocal` and nothing is found (a teammate is installing); again on focus, on
|
|
457
|
+
return to the tab and on a permission change, also once paired (a bridge that stops is noticed); never over a state the
|
|
458
|
+
user is in the middle of (`asking`, a wrong code), and never over a user action that happened while it was out (an
|
|
459
|
+
epoch: a `/status` that left before Stop does not bring "running" back); one background probe at a time. `lost()` after
|
|
460
|
+
a failed turn, `pair(code)`, `allowAccess()` from the Allow click, `control(kind)` (no answer → lost; 401 → Not connected
|
|
461
|
+
with the Pair form; another HTTP error leaves the state), `recheck()`. On the dev machine (`local`, default: the page is
|
|
462
|
+
on localhost) it probes nothing; `b.local` switches after mount, so the server's markup and the first render agree. The
|
|
463
|
+
client is the first render's for the hook's life (make it at module level: its data token is cached on it).
|
|
464
|
+
- **`bridgePickerProps(b, { latest, … })`** → `bridge`, `setup`, `statusActions` for `EnginePicker`: the dot and Status
|
|
465
|
+
rows, Setup per case (Allow / unblock / the Pair form + command / install / update), Check again, Stop and Remove
|
|
466
|
+
(`controls`). The words are `BRIDGE_SETUP_EN` (`labels` overrides); `outdatedDetail` and `installNote` are the surface's.
|
|
467
|
+
- What stays the surface's: which engines exist and what they cost, the request body, what each event does to its
|
|
468
|
+
messages, its confirm dialog.
|
|
469
|
+
|
|
470
|
+
Tests: `bridge.test.tsx` (the client, the stream and the picker's props, with fetch and the permission faked),
|
|
471
|
+
`bridge-browser.test.tsx` (Chromium, a fake bridge on a test port answered by the router: open, poll, lost, pair,
|
|
472
|
+
permission, Stop; each with its negative control).
|
|
473
|
+
|
|
380
474
|
## The chat body (components since 0.29.0)
|
|
381
475
|
|
|
382
476
|
Chris 08/10/2026: "đồng bộ hết giao diện của AI assistant… mọi service có panel này đồng bộ style + code dùng gemboss ui".
|
|
@@ -440,7 +534,16 @@ async function send(text: string) {
|
|
|
440
534
|
bridge's); an `AbortController` you abort yourself throws an `AbortError`, which reads as the user's Stop.
|
|
441
535
|
- **Following the end**: the log follows while the reader is at the end, and stops at their first move up (wheel, finger,
|
|
442
536
|
ArrowUp / PageUp / Home), not 24px later. A send, or another conversation shown, brings it back (`ChatThread pinKey`;
|
|
443
|
-
`AssistantChat` does both).
|
|
537
|
+
`AssistantChat` does both). Since 0.32.0 so does a NEW user message from anywhere: a page's "Ask about this partner", the
|
|
538
|
+
📎 upload, a surface's own starter (no `requestAnimationFrame(() => thread.scrollTop = …)` in the surface any more). A
|
|
539
|
+
note or an answer arriving while the reader is up leaves them where they are.
|
|
540
|
+
- **Links** (0.32.0): `linkPolicy={(href, label, target) => …}` decides for EVERY link in an answer (written, bare, a
|
|
541
|
+
picture from another host), asked with the href as written and `target`, what the kit would link (`linkTarget`,
|
|
542
|
+
normalised) or null when it refuses it: `true` / undefined = the kit's link (in-app through `onNavigate`, https in a new
|
|
543
|
+
tab, words when `target` is null); `false` / null = its words; any other node is drawn as given (a router link, a
|
|
544
|
+
`customer:12` button). Drawing your own link for an ordinary URL: build it from `target.href`, never the raw `href`. The radars' "this path is text" is `linkPolicy={(h) => !keepAsText(h)}`.
|
|
545
|
+
`ownLink` + `renderLink` still work; `linkPolicy` wins when both are given. React only: `chat-plain.js` keeps the pair
|
|
546
|
+
(it sits at its weight ceiling, and gemcare, its one user, draws only its own scheme).
|
|
444
547
|
- **Full screen**: `AssistantChat` measures how far the frame's floated buttons (`fullActions`, Back) reach into it and
|
|
445
548
|
gives its heading (or, without one, its log) that much room (`useFullChromeRoom`, for your own column); a `fullActions`
|
|
446
549
|
button with no icon keeps its words on a phone. Measured in the child because only the child knows where its column is
|
|
@@ -1169,7 +1272,12 @@ import { Avatar, Dialog, Leaderboard, Podium, SearchDialog, ThemeToggle, themeSc
|
|
|
1169
1272
|
- `SearchDialog`: the topbar search button opening a 640px popup (Featurebase's, the admin's ⌘K).
|
|
1170
1273
|
`initial` before typing, `search(q)` while typing (150 ms debounce, latest answer wins); ↑ ↓ Enter
|
|
1171
1274
|
Esc; ⌘K / Ctrl+K and "/" open it. Results are links; without JavaScript the button goes to
|
|
1172
|
-
`fallbackHref`.
|
|
1275
|
+
`fallbackHref`. 0.31.0: `trigger="field"` draws the door as the topbar's search box (TopbarSearch's look, `hint`
|
|
1276
|
+
at its right edge) for a surface whose topbar centre is the search; `external: true` on a result opens it in a new
|
|
1277
|
+
tab (↗) and closes the popup; a `search` that throws shows "Search failed: …"; `onNavigate(href)` takes results inside
|
|
1278
|
+
the surface without a full reload (pass `router.push`); `minChars` asks for more characters before searching. Escape
|
|
1279
|
+
closes it in one press and is marked handled (`defaultPrevented`); a surface listening for Escape in the CAPTURE phase
|
|
1280
|
+
runs before it, so check `document.querySelector("dialog[open]")` there.
|
|
1173
1281
|
- `Dialog`: the kit's native `<dialog class="ui-modal">` (see kit-plain.css) driven by `open`; a
|
|
1174
1282
|
backdrop click closes it, focus returns to the opener.
|
|
1175
1283
|
- `ThemeToggle` + `themeScript`: `data-theme="dark|light"` on <html>, remembered in localStorage
|
|
@@ -1403,6 +1511,9 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
|
|
|
1403
1511
|
its own rule sets its button on `gb:selection`, which fires after). collections-gate reports a list with selection
|
|
1404
1512
|
boxes and no bar (`select-without-bulk`). An open ⋯ menu or dialog takes Escape first; a closed one kept in a row
|
|
1405
1513
|
does not.
|
|
1514
|
+
- **The bulk bar is one row (0.33.0).** Bulk actions that do not fit after the box and the count go into a ⋯ menu at
|
|
1515
|
+
the bar's end, in order (Polaris BulkActions). React measures the real button widths, so the fold follows the labels,
|
|
1516
|
+
not a breakpoint; pass every action and let the bar decide. Order them by use: the first ones stay buttons.
|
|
1406
1517
|
- **A sort control is 44 × 44 under a finger (0.18.0)**: the header button grows to 44 tall under `pointer: coarse`
|
|
1407
1518
|
and to 44 wide under `pointer: coarse` or at 768 and below (a short label like "ID" was 44 × 30 on an iPad); the
|
|
1408
1519
|
chips below 768 have the same floor. Under a mouse the header keeps its Polaris size.
|
package/catalog/editor.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
.cat-code summary{padding:10px 16px;font-size:13px;cursor:pointer;color:var(--gemboss-text-muted);border-top:1px solid var(--gemboss-divider);min-height:24px}
|
|
9
9
|
.cat-code pre,.cat-api{margin:0;padding:12px 16px;overflow:auto;background:#0f1012;color:#e8e8e6;font-size:12px;line-height:1.5;border-radius:0}
|
|
10
10
|
.cat-api{border-radius:12px;margin:0 0 16px}</style></head><body><div class="cat">
|
|
11
|
-
<h1>@gemboss/ui 0.
|
|
11
|
+
<h1>@gemboss/ui 0.33.1 · Editor</h1>
|
|
12
12
|
<p>Khung của mọi editor: <code>EditorFrame</code> (cột trái · khe kéo · preview · cột phải dock trên mép phải preview) và <code>EditorTopbar</code> (← · tiêu đề · cụm giữa · cụm phải · dòng trạng thái). Là đúng khung của theme / page / section editor trong admin. Gói chỉ vẽ KHUNG; chat, Configure, canvas, page picker, menu, Publish là của editor, đi vào bằng slot. Trang HTML thuần nạp <code>editor.css</code> + <code>editor-plain.js</code> rồi gọi <code>gembossUI.initEditorFrame()</code>: kéo khe (chuột, chạm, phím mũi tên / Home / End), đổi thiết bị, mở ⋯ và Publish ▾, ẩn cột trái, Chat | Preview trên điện thoại. <a href="index.html">← Catalog</a></p>
|
|
13
13
|
<h2>React</h2><pre class="cat-api"><code>import "@gemboss/ui/editor.css";
|
|
14
14
|
import { EditorFrame, EditorTopbar, EditorBack, EditorChatToggle, EditorPaneTabs, EditorSaveState, DeviceToggle } from "@gemboss/ui/editor";
|