@gemboss/ui 0.30.2 → 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 CHANGED
@@ -2,6 +2,104 @@
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.32.0 (09/10/2026)
6
+
7
+ **The bridge, once** (assistant sync, Chris 08/10/2026: "mọi service có panel này đồng bộ style + code dùng gemboss ui").
8
+ content-radar, partner-radar and search-radar each carried ~300 lines for the bridge on the user's machine, copied one
9
+ from the other, and reviews fixed one copy at a time. README "The bridge".
10
+
11
+ Added
12
+ - `createBridgeClient({ port, codeKey, app, tokenPath?, tokenSince? })`: `probe`, `detect` (Chrome's Local Network Access
13
+ permission read first; asked from a click only), `ensureCode`, `installCommand`, `dataToken` (reused until under an hour
14
+ is left; its error is the turn's), `turn(body, { signal, version })` (code + token; errors marked `atBridge` with the
15
+ status, so `bridgeTurnError` reads 401 as "another code" and a dead connection as "gone"), `control("stop" | "remove")`.
16
+ - `readTurnEvents(res, onEvent, { atBridge })` (the turn's `data:` events, whole across chunks, heartbeats skipped; a
17
+ read that dies is the bridge's only for the bridge's stream; a stream that ends without `done` throws
18
+ `BRIDGE_CLOSED_EARLY` / `SERVER_CLOSED_EARLY` instead of showing half an answer as done; an early exit cancels the
19
+ reader) and `postTurn(url, body)` for the surface's own route.
20
+ - `useBridge(client, { open, wantsLocal, pickerOpen, statusUrl, onDetected })`: probes while open, the server's engines
21
+ and the bridge together, merged (`engines`); a 3 s poll while a teammate installs; again on focus / tab / permission
22
+ change, also once paired, one at a time, and dropped when the user acted while it was out (an epoch); `lost()`,
23
+ `pair(code)`, `allowAccess()`, `control(kind)` (401 → Not connected, no answer → lost), `recheck()`. `local` is decided
24
+ after mount (no hydration mismatch on the dev machine); the client is pinned to the first render; a pairing code that
25
+ storage refuses lives in memory, so the command and the turns carry the same one.
26
+ - `bridgePickerProps(b, { latest, busy, controls, onControl, outdatedDetail, installNote, labels })` → `EnginePicker`'s
27
+ `bridge`, `setup` (per case: Allow / unblock / `BridgePairForm` + command / install / update) and `statusActions`
28
+ (Check again, Stop, Remove). `BRIDGE_SETUP_EN`, `bridgeControlNote`, `pickerBridgeState`, `mergeBridgeEngines`.
29
+ - `useEnginePicker().close()`: closes and puts focus on the chip at once (Stop / Remove before a confirm opens); also
30
+ `focusChip()`, `chipRef`, and `EnginePicker chipRef`. Every surface did it with `querySelector(".gb-engine-chip")`.
31
+ - `AssistantChat` / `ChatMarkdown` `linkPolicy(href, label, target)`: one prop for every link in an answer (written, bare,
32
+ a picture from another host): `true` = the kit's link, `false` = its words, a node = drawn as given; `target` is what
33
+ the kit would link (normalised) or null, for a policy that draws its own link. Replaces the
34
+ `ownLink` + `renderLink` pair used to turn links into text (kept; `linkPolicy` wins when both are given). A claimed
35
+ token carries `own: true`. React only (`chat-plain.js` is at its weight ceiling; gemcare keeps the pair).
36
+
37
+ Changed
38
+ - `AssistantChat` goes back to the end when a user message is ADDED, wherever it was sent from (a page's button, the
39
+ 📎 upload): before, only a send from its own composer did, and partner-radar scrolled by hand (#968). A note or an
40
+ answer arriving while the reader is up leaves them there, and so does an edit that drops a user message or swaps its id
41
+ (negative controls in chat-browser.test.tsx; the 0.31.0 pinKey fails the new test: 1717px from the end).
42
+ - `BridgePairForm` says "Pairing…" and takes one press at a time while the probe runs (up to 8 s).
43
+ - `chat-plain.js`: a token its `ownLink` claims carries `own: true`, like the React parser's.
44
+
45
+ No stylesheet changed; chat-plain.js stays under its weight ceiling. New tests: bridge.test.tsx (14),
46
+ bridge-browser.test.tsx (8, Chromium, fake bridge on test port 8797 answered by the router: open, poll, lost, pair,
47
+ permission, Stop, a stale probe after Stop, Stop answered 401), chat.test.tsx (+2), chat-browser.test.tsx (+1),
48
+ engine-browser.test.tsx (+1). Review #3952 (in this version): probes ordered by an epoch and one at a time, a stream
49
+ without `done` is an error, the reader is cancelled on an early exit, `linkPolicy` gets the checked target, the
50
+ README's `onDetected` keeps a "your machine only" engine, a stored-code fallback in memory, `local` after mount, the
51
+ client pinned, Stop's 401 is Not connected, the pinKey counts additions only.
52
+
53
+ ## 0.31.0 (09/10/2026)
54
+
55
+ 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
56
+ 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.
57
+
58
+ - `AssistantHost` header: four controls, + New chat, ⤢ Open full screen, ⋯ More, × Close. "Pin to the side" /
59
+ "Float as a popup" and "Shrink to a bar" (minimise) are rows of ⋯ now, not icons. At 1440 the header held five.
60
+ - `onClearHistory` (+ `canClearHistory`): a "Clear history" row (danger) in ⋯, in the panel header and in full screen's
61
+ chrome. The surface confirms (it knows what is cleared). Before this, content-radar spent a 44px row of its body on it.
62
+ - `menuItems` + `AssistantMenuItem`: a surface's own rows at the top of ⋯.
63
+ - ⋯ wears the fleet's row (`ui-menu__item`) and surface (`gemboss-popover-surface`) from shell.css, which the admin
64
+ loads; its placement is in assistant.css (`.gb-dock-menu*`), because the kit's `.ui-menu` rules are in kit-plain.css,
65
+ which the admin does not load. Escape closes ⋯ only (focus back on ⋯; the surface's "Escape hides the assistant" does
66
+ not fire on the same key; an Escape meant for a modal elsewhere is left to it); a press outside or the window losing
67
+ focus closes it; pressing ⋯ does not drag the popup. A row closes ⋯ and focuses it BEFORE its handler, so a confirm
68
+ the handler opens returns focus to ⋯, not to <body>. No `menu` / `menuitem` roles (no arrow keys, like the kit's Menu);
69
+ the rows do not inherit the popup header's "Drag this bar…" tooltip.
70
+ - Labels `more`, `clearHistory`. Tests: `assistant/header-menu.browser.test.ts` (Chromium, the admin's stylesheets only).
71
+
72
+ `SearchDialog` for a surface whose topbar centre IS the search (content-radar, 08/10/2026: "click opens a popup like
73
+ other surfaces"):
74
+
75
+ - `trigger="field"`: the door is the topbar's search box (TopbarSearch's `gemboss-topbar-search`: 36px, 44px under a
76
+ finger or at phone width, the hint at the right, `hint` prop, ⌘K by default), a link that opens the popup. `icon`
77
+ (default) is the 36px magnifier as before.
78
+ - `SearchItem.external`: a result outside the surface opens in a new tab (link or Enter) and the popup closes behind it,
79
+ marked ↗. Before, every result navigated the tab, so a collected post took the person off the surface.
80
+ - A search that throws says "Search failed: <message>" instead of "Nothing matches.".
81
+ - `onNavigate(href)`: a result inside the surface (Enter, plain click) goes through it instead of a full reload, so a
82
+ Next surface keeps a running assistant answer (content-radar passes `router.push`). ⌘ / Ctrl / Shift / Alt-click is
83
+ left to the browser and keeps the popup open.
84
+ - `minChars`: fewer characters say "Type at least N characters." instead of "Nothing matches.".
85
+ - Escape closes the popup in one press and is marked handled: a surface's own window listener ("Escape hides the
86
+ assistant") no longer fired on the same key, and the search box's clear-the-words Escape no longer took the first press.
87
+ - The `field` door has no aria-label: its name is the words on it (WCAG 2.5.3).
88
+ - Closing forgets the answer, and an answer still on its way when it closed: reopened, the popup drew the previous results
89
+ 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
90
+ goes red without the reset).
91
+
92
+ `.ui-btn--on` (ui.css): the ON look for a plain button (accent-soft fill, accent border, accent-strong text, the kit's
93
+ selected look), for a CTA whose thing is already in effect ("Scheduled · Fri 09:00").
94
+
95
+ ## 0.30.3 (09/10/2026)
96
+
97
+ - The inspector under a finger (`pointer: coarse`): its tabs (`.gemboss-ei-tab`, were 27px) and group heads
98
+ (`.gemboss-ei-gh`, were 13px) are at least 44px tall, and its colour swatches (`.gemboss-ei-sw`) are 44x44 (were 30x30)
99
+ and wrap in a narrow column. Measured on the funnel canvas's theme Settings tab at 320 / 390 / 768 with touch; the theme
100
+ editor's inspector is the same component. Mouse sizes are unchanged. (The lockfile's entry for the package was still
101
+ 0.30.1; it now says 0.30.3.)
102
+
5
103
  ## 0.30.2 (09/10/2026)
6
104
 
7
105
  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 package knows no bridge: the surface polls its own, computes `outdated`, builds the command, and owns the
331
- behaviour of every callback. `EnginePanel` is the same tabs without the chip (catalog, a settings page).
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
@@ -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.30.2 · Editor</h1>
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 &quot;@gemboss/ui/editor.css&quot;;
14
14
  import { EditorFrame, EditorTopbar, EditorBack, EditorChatToggle, EditorPaneTabs, EditorSaveState, DeviceToggle } from &quot;@gemboss/ui/editor&quot;;