@gemboss/ui 0.25.0 → 0.32.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.
- package/CHANGELOG.md +276 -0
- package/README.md +370 -24
- package/catalog/editor.html +1 -1
- package/catalog/index.html +912 -63
- package/gemboss-ui.css +3 -3
- package/package.json +14 -2
- package/parts/components.css +2 -2
- package/parts/core.css +1 -1
- package/parts/shell.css +1 -1
- package/src/action-card.css +1 -0
- package/src/action-card.tsx +149 -0
- package/src/assistant/bridge.tsx +636 -0
- package/src/assistant/chat-markdown.ts +280 -0
- package/src/assistant/chat-turns.ts +120 -0
- package/src/assistant/chat.tsx +909 -0
- package/src/assistant/engine-mark-art.ts +45 -0
- package/src/assistant/engine-mark.tsx +39 -0
- package/src/assistant/engine.tsx +119 -12
- package/src/assistant/host.tsx +115 -32
- package/src/assistant/index.ts +8 -1
- package/src/assistant-chat.css +1 -0
- package/src/assistant-engine.css +1 -1
- package/src/assistant.css +1 -1
- package/src/auth.css +1 -1
- package/src/auth.tsx +17 -2
- package/src/chat-composer.css +1 -1
- package/src/chat-plain.js +6 -0
- package/src/chat.css +1 -1
- package/src/editor/frame.tsx +11 -4
- package/src/editor/index.ts +1 -1
- package/src/editor.css +1 -1
- package/src/engine-plain.js +1 -0
- package/src/index.ts +4 -1
- package/src/kit-plain.js +1 -1
- package/src/minichart.css +1 -0
- package/src/minichart.tsx +165 -0
- package/src/portal.css +1 -1
- package/src/search-dialog.tsx +67 -13
- package/src/setup-guide.css +1 -0
- package/src/setup-guide.tsx +116 -0
- package/src/ui.css +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
|
|
|
56
56
|
| core | `@gemboss/ui/core.css` | `parts/core.css` | the box-sizing reset + the tokens | 1.0 KB | every surface, first |
|
|
57
57
|
| components | `@gemboss/ui/components.css` | `parts/components.css` | the kit (`ui.css`), the field look, the plain-HTML primitives (`kit-plain.css`: menu, chips, report pieces, popup body) | 8.2 KB | every surface, second |
|
|
58
58
|
| shell | `@gemboss/ui/shell.css` | `parts/shell.css` | rail, topbar, main, mobile drawer, the thread rail, topbar slots, base text, page widths (`app-shell.css`) | 5.5 KB | every surface, third |
|
|
59
|
-
| auth | `@gemboss/ui/auth.css` | `packages/ui/src/auth.css` | the sign-in screen | 2.
|
|
59
|
+
| auth | `@gemboss/ui/auth.css` | `packages/ui/src/auth.css` | the sign-in screen (+ the grid backdrop, 0.30.0) | 2.8 KB | `AuthLayout` & co. import it |
|
|
60
60
|
| assistant | `@gemboss/ui/assistant.css` | `packages/ui/src/assistant.css` | the dock and the launcher | 2.2 KB | `AssistantHost`, the launcher import it |
|
|
61
61
|
| portal | `@gemboss/ui/portal.css` | `packages/ui/src/portal.css` | `TopbarNav`, `SearchDialog`, `Avatar`, `Leaderboard`, `Podium`, the `ThemeToggle` button | 2.1 KB | those components import it |
|
|
62
62
|
| theme-dark | `@gemboss/ui/theme-dark.css` | `packages/ui/src/theme-dark.css` | the dark palette | 0.6 KB | `ThemeToggle` and `themeScript` (`theme-script.ts`, without portal.css) import it |
|
|
@@ -64,8 +64,11 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
|
|
|
64
64
|
| editor | `@gemboss/ui/editor.css` | `packages/ui/src/editor.css` | the editors | 7.9 KB | the `@gemboss/ui/editor` components import it |
|
|
65
65
|
| reactions | `@gemboss/ui/reactions.css` | `packages/ui/src/reactions.css` | the emoji reactions and their picker | 0.8 KB | `Reactions` / `ReactionPicker` import it |
|
|
66
66
|
| assistant-engine | `@gemboss/ui/assistant-engine.css` | `packages/ui/src/assistant-engine.css` | the engine picker (chip + Engine/Setup/Status popup) and the bridge notice | 1.9 KB | `EnginePicker` / `EnginePanel` / `BridgeNotice` import it |
|
|
67
|
-
| chat | `@gemboss/ui/chat.css` + `chat-composer.css` | `packages/ui/src/chat.css`, `packages/ui/src/chat-composer.css` | the chat body, the composer | 3.0 + 0.9 KB | the
|
|
68
|
-
|
|
|
67
|
+
| chat | `@gemboss/ui/chat.css` + `chat-composer.css` | `packages/ui/src/chat.css`, `packages/ui/src/chat-composer.css` | the chat body, the composer | 3.0 + 0.9 KB | the chat components (0.29.0); a page writing the markup links them |
|
|
68
|
+
| assistant-chat | `@gemboss/ui/assistant-chat.css` | `packages/ui/src/assistant-chat.css` | the chat pieces the package draws (0.29.0): column, starters that wrap, meta, errors, notes, paperclip | 0.9 KB | `AssistantChat` and its pieces import it |
|
|
69
|
+
| minichart | `@gemboss/ui/minichart.css` | `packages/ui/src/minichart.css` | `Sparkline`, `ShareBar` (0.30.0) | 0.5 KB | those components import it |
|
|
70
|
+
| setup-guide | `@gemboss/ui/setup-guide.css` | `packages/ui/src/setup-guide.css` | `SetupGuide` (0.30.0) | 0.9 KB | `SetupGuide` imports it |
|
|
71
|
+
| bundle | `@gemboss/ui/gemboss-ui.css` | `gemboss-ui.css` | core + components + shell + auth + assistant + chat + portal + theme-dark | 21.9 KB | compatibility only (see CHANGELOG 0.23.0) |
|
|
69
72
|
|
|
70
73
|
The core alone is 14.7 KB against 21.7 for the bundle (each part opens with a short header naming the gemboss commit, kept through minify, so a surface serving the parts can still say which commit its CSS is); a list page adds collections (4.6), a sign-in page auth (2.6).
|
|
71
74
|
|
|
@@ -196,6 +199,21 @@ then the mark, then the foot lines as the visible height shrinks, follows the OS
|
|
|
196
199
|
tracks the on-screen keyboard. Re-theme it with tokens on `.gb-auth`, e.g. a light iris panel:
|
|
197
200
|
`--gb-panel-bg`, `--gb-panel-ink`, `--gb-panel-muted`, `--gb-panel-eyebrow`, `--gb-panel-facet-o`.
|
|
198
201
|
|
|
202
|
+
**The panel's ground: `backdrop` (0.30.0).** `"facet"` (default, unchanged): the mark's facet lines, oversized, at 5%, over
|
|
203
|
+
iris washes on ink. `"grid"`: gemform's sign-in ground (Chris 08/10/2026: "cái background bên dưới logo cũng đẹp"), a 34px
|
|
204
|
+
grid of iris hairlines at 10% that fades out from the panel's top-left corner, on an ink gradient. `"none"`: the ground
|
|
205
|
+
alone. Re-tune the grid with `--gb-panel-grid-bg`, `--gb-panel-grid-line`, `--gb-panel-grid-size`, `--gb-panel-grid-fade`
|
|
206
|
+
on `.gb-auth`. Like the panel itself it is drawn only at 900 wide AND 520 tall, and it is dark in both themes. A page
|
|
207
|
+
that writes the markup puts `gb-auth-panel--grid` (or `--none`) beside `gb-auth-panel`. The grid brings its OWN ground
|
|
208
|
+
(`--gb-panel-grid-bg`, an ink gradient), not `--gb-panel-bg`: a surface that re-themed the panel light (with dark
|
|
209
|
+
`--gb-panel-ink`) sets `--gb-panel-grid-bg` too, or its words land dark on ink. The grid's rules live in auth.css
|
|
210
|
+
(+0.2 KB gzip on the sign-in entry and the bundle): an entry of its own would need a second component to import it.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<AuthLayout brand="gemform" tagline="GemBoss Ops · internal tool" backdrop="grid"
|
|
214
|
+
panel={{ eyebrow: "gemform · GemBoss Ops", heading: "Every Gem registration, in one place." }}>…</AuthLayout>
|
|
215
|
+
```
|
|
216
|
+
|
|
199
217
|
## The assistant frame
|
|
200
218
|
|
|
201
219
|
```tsx
|
|
@@ -272,6 +290,17 @@ Its styles are the `gemboss-assistant-*` rules in `packages/ui/src/shell.css` (t
|
|
|
272
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
|
|
273
291
|
through `fullActions`; a surface without those leaves them out.
|
|
274
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
|
+
|
|
275
304
|
## The engine picker (0.25.0): `EnginePicker`, `BridgeNotice`
|
|
276
305
|
|
|
277
306
|
Which engine answers the assistant, and the bridge on the user's machine, without spending the chat's space on it
|
|
@@ -304,35 +333,285 @@ const picker = useEnginePicker(); // open + tab, shared with the notice
|
|
|
304
333
|
select (more than six models, or grouped ones) keep it open. Closing goes back to the Engine tab. 44px targets under a coarse pointer and at phone width.
|
|
305
334
|
- **Not in the chat**: no standing warning, no command in the conversation. The amber dot on the chip and on the Setup
|
|
306
335
|
tab is the warning. `BridgeNotice` is for a turn that actually failed, once.
|
|
307
|
-
-
|
|
308
|
-
|
|
336
|
+
- **For the admin (0.29.0)**: `EngineChoice.mark` ("claude", "codex") is shown on the chip in place of the name when the
|
|
337
|
+
composer is too narrow (`ChatComposer` density icon); `EngineChoice.models` on every engine + `onPick(engine, model)`
|
|
338
|
+
lists each engine with its own models at once, and one click picks both and closes (the admin's Polaris popup);
|
|
339
|
+
`problem` is a red line on the Engine tab ("Wait for the current answer to finish"); `disabled` disables the chip while
|
|
340
|
+
a switch is in flight.
|
|
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.
|
|
309
349
|
|
|
310
350
|
Tests: `packages/ui/src/assistant/engine.test.tsx` (markup, styles, its own entry), `packages/ui/src/assistant/engine-browser.test.tsx`
|
|
311
351
|
(Chromium: top layer through a clipping frame, keys, focus, copy, Open setup, 44px; with negative controls).
|
|
312
352
|
|
|
313
|
-
|
|
353
|
+
**Escape (0.27.0).** An Escape that closes the panel is marked handled (`preventDefault`) also when focus is outside it
|
|
354
|
+
(the composer): a surface whose own Escape hides the assistant and checks `defaultPrevented` leaves that key alone, and the
|
|
355
|
+
next Escape is its own again. Before, the key went on unmarked and React re-renders between the picker's listener and the
|
|
356
|
+
surface's, so each surface needed a window-capture marker to tell the two apart (gemlabs #922, gemfeedback #66). With
|
|
357
|
+
0.27.0 the surface's `!e.defaultPrevented` is enough.
|
|
358
|
+
|
|
359
|
+
### Plain HTML: `gembossUI.enginePicker()` (0.27.0)
|
|
360
|
+
|
|
361
|
+
`engine-plain.js`, after `kit-plain.js`, for a page with no React (gemcare). The same markup and classes as the component
|
|
362
|
+
(asserted: same class set for the same options), so `assistant-engine.css` draws it, and the same behaviour (top layer,
|
|
363
|
+
tabs and keys, focus back to the chip, closing goes back to Engine, Copy, 44px under a coarse pointer, Escape marked):
|
|
364
|
+
|
|
365
|
+
```html
|
|
366
|
+
<link rel="stylesheet" href="/ui/assistant-engine.css"> <!-- require.resolve("@gemboss/ui/assistant-engine.css") -->
|
|
367
|
+
<script src="/ui/kit-plain.js"></script><script src="/ui/engine-plain.js"></script>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
const picker = gembossUI.enginePicker(document.querySelector(".gemboss-ec-toolbar-lead"), {
|
|
372
|
+
engines: [{ id: "server", label: "GemCare AI, paid" },
|
|
373
|
+
{ id: "claude", label: "Claude Code", badge: "your plan", badgeTone: "success", note: "Your own subscription." }],
|
|
374
|
+
engine: "claude", onEngineChange: (id) => …,
|
|
375
|
+
models, model, onModelChange, efforts, effort, onEffortChange, effortDisabled, notes, modelControl,
|
|
376
|
+
bridge: { state: "outdated", version: "8", latest: "10", autostart: true, detail: "…" }, showBridgeDot: true,
|
|
377
|
+
setup: { command, pairingCode, steps, note }, statusActions: [checkAgainButton],
|
|
378
|
+
onOpenChange, onTabChange, locked, end, labels,
|
|
379
|
+
});
|
|
380
|
+
picker.update({ bridge: { state: "running" } }); // a poll: bridge / setup / labels merge one level; patched in place
|
|
381
|
+
picker.openTab("setup"); picker.close(); picker.isOpen(); picker.destroy();
|
|
382
|
+
log.append(gembossUI.bridgeNotice({ text: "The bridge on your machine is not responding.", onOpenSetup: () => picker.openTab("setup") }));
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Strings are text; `steps`, `notes`, `detail`, `statusActions` and `modelControl` may also be a Node, an array of Nodes or a
|
|
386
|
+
DocumentFragment: placed once and left where they are, so a field in them keeps focus, caret and events across redraws.
|
|
387
|
+
A redraw PATCHES the panel in place (the same elements, only what changed, as React does): focus on a tab, an engine row
|
|
388
|
+
or the model select stays, and a clamped panel keeps its scroll; behaviour is delegated from the panel (`data-*`).
|
|
389
|
+
A pick from the engine list, the model or the effort updates the picker at once and calls back (no callback: that row is
|
|
390
|
+
disabled, as in the component); `update()` is how the page says anything else changed. An Escape it does not own is left
|
|
391
|
+
alone: an IME composition, or one in a modal opened from the panel; and focus entering such a modal closes the panel,
|
|
392
|
+
which would otherwise sit above it in the top layer (the component does the same since 0.27.0). Tests:
|
|
393
|
+
`packages/ui/src/engine-plain.test.ts` (Chromium: the component's class set on all three tabs, top layer through a
|
|
394
|
+
clipping frame with its negative control, no popover API, keys, Escape, Copy, redraws that keep focus / scroll / a
|
|
395
|
+
caller's field without a blur or change, merge rules, 44px).
|
|
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
|
+
|
|
474
|
+
## The chat body (components since 0.29.0)
|
|
475
|
+
|
|
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".
|
|
477
|
+
Since 0.29.0 the package DRAWS the chat, not only its CSS: the surface hands in messages and actions and keeps its own
|
|
478
|
+
streaming, bridge and sessions. Each piece is the better of the two surfaces that had it (inventory 08/10/2026,
|
|
479
|
+
`Gems/_local/fleet/assistant-sync-2026-10/`):
|
|
480
|
+
|
|
481
|
+
| Piece | Component | Taken from | Why that one |
|
|
482
|
+
|---|---|---|---|
|
|
483
|
+
| Composer | `ChatComposer` | admin (shape, density, one action slot) + partner-radar (a DIV, IME guard) | one bordered shell, send/stop the same 44px circle, grows to 84px, says less as it narrows (measured); a form broke the engine popup's own form; an IME's Enter must not send |
|
|
484
|
+
| Markdown | `ChatMarkdown` | admin | tables, folded code, safe links, trusted pictures, no raw HTML; the radars fed an HTML string to `dangerouslySetInnerHTML` |
|
|
485
|
+
| Steps | `ChatSteps` | admin, with partner-radar's words | rows print and stop (one heartbeat moves, not a spinner per row); a step says what it did in words + its target, a trace keeps the raw call folded |
|
|
486
|
+
| Working | `ChatPending` | admin | ✳, a verb that changes, the seconds: the wait reads alive |
|
|
487
|
+
| Starters | `ChatStarters` | partner-radar | they wrap (radius 12): a long question as a nowrap pill is cut in a 400px panel; a fill-in example |
|
|
488
|
+
| Under an answer | `ChatMeta` | partner-radar | Copy · engine · seconds · cost |
|
|
489
|
+
| Errors | `ChatError`, `BridgeNotice` | partner-radar | a fix the user can take (sentence, command, Copy, the original folded); a bridge failure is one line |
|
|
490
|
+
| Engine | `EnginePicker` | partner-radar (Setup / Status tabs) + admin (mark, models per engine) | see "The engine picker" |
|
|
491
|
+
| Frame, sessions | `AssistantHost`, `SessionRail` | (already the package's) | |
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
import { AssistantChat, ChatAttach, EnginePicker, useChatTurns, useEnginePicker } from "@gemboss/ui/assistant"; // brings its CSS
|
|
495
|
+
|
|
496
|
+
const turns = useChatTurns(); // messages with ids; a turn is written BY ID
|
|
497
|
+
const picker = useEnginePicker();
|
|
498
|
+
async function send(text: string) {
|
|
499
|
+
turns.say(text);
|
|
500
|
+
const id = turns.start();
|
|
501
|
+
try {
|
|
502
|
+
for await (const ev of stream(text)) {
|
|
503
|
+
if (ev.type === "text") turns.write(id, ev.text);
|
|
504
|
+
if (ev.type === "tool") turns.step(id, { id: ev.id, label: TOOL_LABEL[ev.name], target: ev.target, state: ev.state });
|
|
505
|
+
}
|
|
506
|
+
turns.finish(id, { meta: ["Claude Code", "4.2s"] });
|
|
507
|
+
} catch (e) {
|
|
508
|
+
if (turns.fail(id, e).bridge) probeTheBridgeAgain(); // mark the bridge's own call with atBridge(e, status)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
<AssistantHost title="Partner Assistant" …>
|
|
513
|
+
<AssistantWorkspaceLayout dock={presentation !== "full"}>
|
|
514
|
+
<AssistantChat messages={turns.messages} busy={busy} onSend={send} onStop={stop}
|
|
515
|
+
title="Partner Assistant" // shown at full screen only (the frame's header says it elsewhere)
|
|
516
|
+
intro={<p className="gemboss-md-p">Describe a meeting…</p>}
|
|
517
|
+
starters={[{ label: "Had dinner with Amit…", hint: "Example, click to fill in", fill: true }]}
|
|
518
|
+
moreStarters={SUGGESTIONS} moreStartersTitle="Or ask about partners in the radar:"
|
|
519
|
+
engine={<EnginePicker {...picker.props} … />} tools={<ChatAttach accept=".pdf,.docx" onFiles={upload} />}
|
|
520
|
+
onOpenSetup={() => picker.openTab("setup")} renderExtra={(m) => m.proposals?.map(…)} onNavigate={router.push} />
|
|
521
|
+
</AssistantWorkspaceLayout>
|
|
522
|
+
</AssistantHost>
|
|
523
|
+
```
|
|
314
524
|
|
|
315
|
-
|
|
316
|
-
the
|
|
525
|
+
- **`messages`** (`ChatMessageData`): `role` user / assistant / note; `text` (markdown for an answer, `markdown: false` for
|
|
526
|
+
plain); `pending` (the heartbeat until text, then the cursor); `steps`; `error` + `fix: { text, command }`, or `bridge:
|
|
527
|
+
true` (one BridgeNotice line whose action calls `onOpenSetup`); `warn`; `stopped`; `meta` (after Copy); a note's `tone`
|
|
528
|
+
warn / down + `action` makes it a BridgeNotice ("This bridge is outdated · Open setup").
|
|
529
|
+
- **Failed turns** (`bridgeTurnError`, used by `turns.fail`): only a failure AT the bridge is the bridge's: its call threw
|
|
530
|
+
(it is gone, the machine slept) or it answered 401 (this browser's pairing code is not its code). Its 400 / 503 and a
|
|
531
|
+
failure on the way to the server (a data token, an expired session) are the turn's. The surface marks the bridge's own
|
|
532
|
+
call (`throw atBridge(err, res.status)`); it never guesses from "this turn used the bridge" (content-radar #937).
|
|
533
|
+
- **Failed turns, timeouts**: give the bridge's call its deadline with `AbortSignal.timeout(ms)` (a `TimeoutError`, the
|
|
534
|
+
bridge's); an `AbortController` you abort yourself throws an `AbortError`, which reads as the user's Stop.
|
|
535
|
+
- **Following the end**: the log follows while the reader is at the end, and stops at their first move up (wheel, finger,
|
|
536
|
+
ArrowUp / PageUp / Home), not 24px later. A send, or another conversation shown, brings it back (`ChatThread pinKey`;
|
|
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).
|
|
547
|
+
- **Full screen**: `AssistantChat` measures how far the frame's floated buttons (`fullActions`, Back) reach into it and
|
|
548
|
+
gives its heading (or, without one, its log) that much room (`useFullChromeRoom`, for your own column); a `fullActions`
|
|
549
|
+
button with no icon keeps its words on a phone. Measured in the child because only the child knows where its column is
|
|
550
|
+
(at 1920 the 820px column does not reach the buttons at all).
|
|
551
|
+
- **Loading**: the chat components import chat.css and chat-composer.css, which the old `gemboss-ui.css` bundle also
|
|
552
|
+
carries. A surface still linking the bundle gets them twice, the second copy after its own CSS: move to the parts
|
|
553
|
+
first (see "Load only what you use").
|
|
554
|
+
- **Composer density**: `full` "Claude Code · Opus" → `compact` "Claude Code" → `icon` the engine's MARK (`EngineChoice.mark`)
|
|
555
|
+
→ `tight`. An engine with no mark keeps its (cut) name.
|
|
556
|
+
- **Markdown** (0.29.2): a bare `https://…` in the prose is a link (GitHub's reading: the sentence's trailing `. , : ; ! ?
|
|
557
|
+
_ ~ ' "` and an unpartnered `)` / `]` stay text; `<https://…>` too), through the same `linkTarget` as `[x](…)`, so only
|
|
558
|
+
http(s) with a host. It ends at a space, `*`, CJK text or full-width punctuation ("访问https://x.test。然后" links only
|
|
559
|
+
the URL), and one glued to a word (`foohttps://…`) is not a link. Inside `**bold**` a URL stays plain text. `####` to
|
|
560
|
+
`######` are headings at the lowest step (`h5.gemboss-md-h3`); a heading line with no words is dropped.
|
|
561
|
+
- **Pictures on the page's own origin** (0.29.2): `imageHosts={[{ path: "/api/img/" }]}` (no `host`) draws
|
|
562
|
+
`` as written, a root-relative path checked after the browser's normalisation: `../`,
|
|
563
|
+
`%2e%2e`, `\` that leave `path`, an encoded `/` or `\`, a double encoding (`%25`), a `;` (`..;/`), `//host` and `/\host`
|
|
564
|
+
are refused (the alt stays as text). `path` must be a directory below the root (`/x/`, not `/` nor `/x`), or the entry
|
|
565
|
+
is ignored. No need to rewrite the answer into absolute URLs first.
|
|
566
|
+
- **A form in the engine popup** (0.29.2): the send circle is `.gemboss-ec-toolbar > button[type=submit]`, the toolbar's
|
|
567
|
+
own child. Before, `.gemboss-ec-input button[type=submit]` also painted a submit button in the popup (it lives in the
|
|
568
|
+
leading slot in the DOM): a surface's Pair form can keep `type="submit"`.
|
|
569
|
+
- A surface that needs its own arrangement uses the pieces directly (`ChatThread`, `ChatMessage`, `ChatBubble`,
|
|
570
|
+
`ChatMarkdown`, `ChatSteps`, `ChatPending`, `ChatStarters`, `ChatMeta`, `ChatError`, `ChatNote`, `ChatComposer`,
|
|
571
|
+
`ChatAttach`) inside `ChatLabelsProvider` for its words. Every word is in `CHAT_EN` (English chrome, Chris 03/10/2026).
|
|
572
|
+
|
|
573
|
+
CSS: `chat.css` (the body; also inside `gemboss-ui.css`, and the admin `@import`s it), `chat-composer.css` (toolbar, density,
|
|
574
|
+
Stop) and `assistant-chat.css` (0.29.0: the column, wrapping starters, the meta line, errors, notes, the paperclip, the
|
|
575
|
+
room for full screen's buttons). The chat components import all three; a page that writes the markup itself links them.
|
|
317
576
|
|
|
318
577
|
| Piece | Classes |
|
|
319
578
|
|---|---|
|
|
320
|
-
|
|
|
321
|
-
| Message list | `.gemboss-ec-scroll` (thin visible scrollbar) |
|
|
579
|
+
| Column | `.gb-chat` › (`.gemboss-ec-head.gb-chat-head`) + `.gemboss-ec-scroll[role=log]` + `.gemboss-ec-input` |
|
|
322
580
|
| Messages | `.gemboss-ec-msg.user` / `.gemboss-ec-msg.assistant` › `.gemboss-ec-bubble` |
|
|
323
581
|
| Markdown inside a bubble | `.gemboss-md-p / -h -h1..3 / -list / -quote / -rule / -code / -codeblock / -table(wrap) / -img(link)`, links `.gemboss-ec-md-link` |
|
|
324
|
-
| Steps
|
|
325
|
-
| Working / streaming | `.gemboss-ec-pending` (`-star`, `-verb`, `-meta`), `.gemboss-ec-cursor` after the streamed text |
|
|
326
|
-
| Suggestions
|
|
327
|
-
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
`chat
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
582
|
+
| Steps | `ul.gemboss-narr` › `li.gemboss-narr-row.ok|.running|.failed|.thinking|.trace|.steer` › `.gemboss-narr-dot` + `.gemboss-narr-text` (+ `.gb-chat-target`) |
|
|
583
|
+
| Working / streaming | `.gemboss-ec-pending` (`-star`, `-verb`, `-meta`, `.gb-chat-sr`), `.gemboss-ec-cursor` after the streamed text |
|
|
584
|
+
| Suggestions | `.gemboss-ec-starters` › `button.gemboss-ec-starter.gb-chat-starter` (› `.gb-chat-starter-hint`); a group title `.gb-chat-label` |
|
|
585
|
+
| Under an answer, errors, notes | `.gb-chat-meta`; `.ui-notice.ui-notice--danger.gb-chat-error` › `.gb-chat-cmd` + `details.gb-chat-original`; `.gb-chat-note`; `.gb-engine-notice` |
|
|
586
|
+
| Composer | `div.gemboss-ec-input[data-density]` › `textarea` + `div.gemboss-ec-toolbar` › `div.gemboss-ec-toolbar-lead` (chips) + `div.gemboss-ec-toolbar-tools` (`button.gemboss-ec-attach-btn`) + `button[type=submit]` (a DIRECT child: the send circle) or `button.gemboss-ec-stop` |
|
|
587
|
+
|
|
588
|
+
Tests: `packages/ui/src/assistant/chat.test.tsx` (markup, parser, every class styled), `chat-browser.test.tsx` (Chromium:
|
|
589
|
+
Enter / IME, Stop in send's place, growing box, density with the mark, following the end, the turn store by id, failed
|
|
590
|
+
turns, full screen's buttons, 44px; each with its negative control).
|
|
591
|
+
|
|
592
|
+
### Plain HTML: `gembossUI.chat` (0.29.0)
|
|
593
|
+
|
|
594
|
+
`chat-plain.js`, after `kit-plain.js` (and `engine-plain.js` when the page has the picker), for a page with no React
|
|
595
|
+
(gemcare's inbox and Reports assistants). The same markup as the components, element by element, and the same markdown
|
|
596
|
+
(`chat-plain.test.ts` renders both from the same data and compares the trees). `render()` redraws from the data each
|
|
597
|
+
time; a message whose look did not change keeps its element, and a changed one keeps the folds the reader opened and its
|
|
598
|
+
heartbeat's clock:
|
|
599
|
+
|
|
600
|
+
```html
|
|
601
|
+
<link rel="stylesheet" href="/ui/chat.css"><link rel="stylesheet" href="/ui/chat-composer.css">
|
|
602
|
+
<link rel="stylesheet" href="/ui/assistant-chat.css"><link rel="stylesheet" href="/ui/assistant-engine.css">
|
|
603
|
+
<script src="/ui/kit-plain.js"></script><script src="/ui/engine-plain.js"></script><script src="/ui/chat-plain.js"></script>
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
```js
|
|
607
|
+
const log = gembossUI.chat.thread(document.getElementById("ai-thread")); // role=log, follows the end
|
|
608
|
+
gembossUI.chat.render(log.el, messages, { intro, starters, onStarter, onOpenSetup: () => picker.openTab("setup"),
|
|
609
|
+
ownLink: (h) => /^(customer|conversation):\d+$/.test(h), renderLink: (href, label) => aiLinkButton(href, label) });
|
|
610
|
+
const box = gembossUI.chat.composer(host, { placeholder: "Ask about this customer", maxLength: 1000, leading: pickerHost,
|
|
611
|
+
thread: log, onSend: (text) => ask(text), onStop: () => controller.abort() }); // thread: back to the end on a send
|
|
612
|
+
box.setBusy(true); log.setBusy(true);
|
|
613
|
+
const { error, bridge } = gembossUI.chat.turnError(err); // the same rule as bridgeTurnError
|
|
614
|
+
```
|
|
336
615
|
|
|
337
616
|
## Editor CSS (0.9.x)
|
|
338
617
|
|
|
@@ -418,7 +697,7 @@ import { DeviceToggle, EditorBack, EditorChatToggle, EditorFrame, EditorPaneTabs
|
|
|
418
697
|
| `EditorBack` | `.gemboss-editor-back` ← | A `<button type="button">`, or `as={Link}` / `as="a"` with `href`. Named "Back" for a screen reader (`label`). |
|
|
419
698
|
| `EditorChatToggle` | `.gemboss-editor-chat-toggle` | Shows / hides the left column; filled while open, pressed when collapsed. |
|
|
420
699
|
| `EditorSaveState` | `.gemboss-editor-savestate` (`.is-saving` with the spinner, `.is-saved`) | `idle` draws nothing, or `idleText` dimmed (the section editor's "Autosaves"). Texts and titles are props. |
|
|
421
|
-
| `DeviceToggle`, `MonitorGlyph`, `PhoneGlyph` | `.gemboss-pv-devices` › `.gemboss-pv-device` | Desktop / tablet / mobile. Moved from the admin unchanged. |
|
|
700
|
+
| `DeviceToggle`, `MonitorGlyph`, `TabletGlyph`, `PhoneGlyph` | `.gemboss-pv-devices` › `.gemboss-pv-device` | Desktop / tablet / mobile. Moved from the admin unchanged; `TabletGlyph` exported 0.26.0 (it was inline in the toggle). |
|
|
422
701
|
|
|
423
702
|
One module per piece behind one entry, so a bundler gives each editor only what it draws (measured: one module for all
|
|
424
703
|
of it cost the page and section editors about 1 KB of code they never ran). Not in `@gemboss/ui` itself: like
|
|
@@ -993,7 +1272,12 @@ import { Avatar, Dialog, Leaderboard, Podium, SearchDialog, ThemeToggle, themeSc
|
|
|
993
1272
|
- `SearchDialog`: the topbar search button opening a 640px popup (Featurebase's, the admin's ⌘K).
|
|
994
1273
|
`initial` before typing, `search(q)` while typing (150 ms debounce, latest answer wins); ↑ ↓ Enter
|
|
995
1274
|
Esc; ⌘K / Ctrl+K and "/" open it. Results are links; without JavaScript the button goes to
|
|
996
|
-
`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.
|
|
997
1281
|
- `Dialog`: the kit's native `<dialog class="ui-modal">` (see kit-plain.css) driven by `open`; a
|
|
998
1282
|
backdrop click closes it, focus returns to the opener.
|
|
999
1283
|
- `ThemeToggle` + `themeScript`: `data-theme="dark|light"` on <html>, remembered in localStorage
|
|
@@ -1033,6 +1317,28 @@ Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, tra
|
|
|
1033
1317
|
message with reactions does not grow by a full line. A plain-HTML surface writes the same classes (markup at the top of
|
|
1034
1318
|
`reactions.css`, and the catalog's "Reactions" section).
|
|
1035
1319
|
|
|
1320
|
+
|
|
1321
|
+
## Action card (0.28.0): an assistant proposes, a person applies
|
|
1322
|
+
|
|
1323
|
+
Chris 08/10/2026: the Feedback Assistant lists likely duplicates and merges them, and carries out status, owner, label
|
|
1324
|
+
and date changes on request, "follow theo gemboss ui". GemFeedback built it first (`components/MergeCard.tsx`,
|
|
1325
|
+
`components/ActionCard.tsx` there); this is its look, moved here so every assistant in the fleet asks the same way.
|
|
1326
|
+
|
|
1327
|
+
The contract every surface keeps: **the model never changes anything**. It proposes (a link it writes, a tool result);
|
|
1328
|
+
the card shows what would change item by item, what to know first, and how far it reaches; only the primary button
|
|
1329
|
+
applies; afterwards the outcome replaces the body, with Undo when the surface can put things back.
|
|
1330
|
+
|
|
1331
|
+
| Piece | What it draws |
|
|
1332
|
+
| --- | --- |
|
|
1333
|
+
| `ActionCard` | `title`, `aside` (a link beside it), `loading` (spinner line), `warnings` (warning banner, "Before you apply"), `summary` + `actions` (the foot), `outcome` (`{tone: "success" \| "info", children, undo?: {onUndo, label, busy}}`, replaces body, warnings and actions), `error` |
|
|
1334
|
+
| `ActionCardSection` | a labelled list ("Keeping", "Merging into it"), or unlabelled |
|
|
1335
|
+
| `ActionCardItem` | `name` (+ `href`, drawn with `renderLink` for a router link: a list link, 24px), `control` (a checkbox before the name, in a 44px label on touch), `meta`, `excerpt` (3 lines), `footer` (a secondary action), `kept` (accent edge), `muted` |
|
|
1336
|
+
| `ActionChange` | "Idea → Planned"; "Already Planned" when equal; `problem` ("Merged into #7", "Changed since") instead |
|
|
1337
|
+
| `action-card.css` | its own entry, loaded by the component; a plain-HTML surface links `@gemboss/ui/action-card.css` and writes `ui-action-card`, `__head`, `__title`, `__aside`, `__section`, `__label`, `__list`, `__item` (`--kept`, `--muted`), `__name`, `__pick`, `__meta`, `__excerpt`, `__item-foot`, `__change`, `__same`, `__warn`, `__foot`, `__summary`, `__actions` |
|
|
1338
|
+
|
|
1339
|
+
Presentational: no fetching and no state of its own. The surface loads the preview, owns the handlers, and decides
|
|
1340
|
+
what Undo puts back (GemFeedback: the merge record, or the values an action replaced, guarded by `if=` so Undo never
|
|
1341
|
+
overwrites a later edit). Catalog section "Thẻ hành động".
|
|
1036
1342
|
## Report pages (0.4.0)
|
|
1037
1343
|
|
|
1038
1344
|
Plain CSS in `kit-plain.css` for a numbers page written by hand (first user: gemnews `/admin`, 03/10/2026; that page moved to GemCommunity the same day, so gemnews no longer takes the package). The
|
|
@@ -1052,6 +1358,46 @@ surface computes every number and ratio; the kit only draws. Markup for each is
|
|
|
1052
1358
|
- `ui-grid--2` / `--3`: card grid that drops to one column; `ui-section-head`: an `h2` + note between page head and
|
|
1053
1359
|
card titles.
|
|
1054
1360
|
|
|
1361
|
+
## Mini charts (0.30.0): `Sparkline`, `ShareBar`
|
|
1362
|
+
|
|
1363
|
+
Two charts small enough for a card or a table cell, each its own entry (`minichart.css`, imported by the component).
|
|
1364
|
+
Before this, six surfaces drew a sparkline by hand (admin intelligence, analytics and operator; talent-radar; search-radar;
|
|
1365
|
+
gemwatcher; gemform) with three different baselines.
|
|
1366
|
+
|
|
1367
|
+
```tsx
|
|
1368
|
+
import { Sparkline, ShareBar } from "@gemboss/ui";
|
|
1369
|
+
<Sparkline points={[3, 5, null, 4, 8]} label="Registrations, last 5 days" />
|
|
1370
|
+
<ShareBar label="Status" parts={[{ label: "New", value: 25, tone: "info" }, { label: "Spam", value: 2, tone: "critical" }]} />
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
- `Sparkline`: oldest point first; `null` is a gap (the line breaks, a lone point is a dot); counts start at 0 unless
|
|
1374
|
+
`min` / `max` fix the scale; `invert` for a rank. Muted line, the newest point one iris dot; `tone` `series` /
|
|
1375
|
+
`success` / `critical`. With `label` it is an image with that name, without it decoration (`aria-hidden`).
|
|
1376
|
+
`sparklinePaths(points, opts)` is the geometry alone, for a page that writes the SVG itself.
|
|
1377
|
+
- `ShareBar`: one 6px bar split by share; parts at 0 take no room; tones are the badge tones (`info`, `success`,
|
|
1378
|
+
`warning`, `critical`, `neutral`) or `series-1..5` (the default, in order). The bar is one image whose name lists every
|
|
1379
|
+
part, so a screen reader hears the numbers the colours stand for. Each segment grows by its SHARE (value / total), so
|
|
1380
|
+
fractions fill the track. The share is an inline `flex-grow`: a plain-HTML page under a CSP that forbids inline style
|
|
1381
|
+
(gemcare) sizes the segments from its own stylesheet or script.
|
|
1382
|
+
|
|
1383
|
+
## Setup guide (0.30.0): `SetupGuide`
|
|
1384
|
+
|
|
1385
|
+
The admin's "Set up your store" card (Shopify's Setup guide) as a component, its own entry (`setup-guide.css`): a title
|
|
1386
|
+
with the count, a bar, collapsed steps of which the first not done is open. Native `<details>`: the head folds the guide,
|
|
1387
|
+
the steps share one `name` so the browser keeps one open, and a page without React writing the same markup (catalog
|
|
1388
|
+
"Hướng dẫn thiết lập") behaves the same with no script. The row reveals; what changes something is a button in the
|
|
1389
|
+
step's `body`; a step with no body is a plain row (no toggle that opens nothing). The open step is decided ONCE, as in
|
|
1390
|
+
the admin: a state change while someone reads a step never shuts it. `state: "unknown"` (could not be read) is counted
|
|
1391
|
+
apart, never as progress. The bar is a `<progress>` (no inline style). `rememberKey` keeps a folded guide folded across
|
|
1392
|
+
visits (localStorage); `group` names the steps' `name` when two guides render in separate React roots. No steps: no guide.
|
|
1393
|
+
|
|
1394
|
+
```tsx
|
|
1395
|
+
<SetupGuide title="Telegram alerts" steps={[
|
|
1396
|
+
{ id: "bot", title: "Connect a bot", state: bot ? "done" : "todo", body: <ConnectBot /> },
|
|
1397
|
+
{ id: "chat", title: "Choose where alerts go", state: chats.length ? "done" : "todo", body: <Chats /> },
|
|
1398
|
+
]} />
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1055
1401
|
## Collections (0.13.0): tables, lists, grids, boards
|
|
1056
1402
|
|
|
1057
1403
|
Chris settled these on 05/10/2026 after an inventory of 136 hand-made collections on 11 surfaces (86 tables scrolling
|
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.32.0 · 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";
|