@gemboss/ui 0.25.0 → 0.30.2
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 +178 -0
- package/README.md +259 -21
- package/catalog/editor.html +1 -1
- package/catalog/index.html +785 -63
- package/gemboss-ui.css +3 -3
- package/package.json +14 -2
- package/parts/components.css +1 -1
- 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/chat-markdown.ts +279 -0
- package/src/assistant/chat-turns.ts +120 -0
- package/src/assistant/chat.tsx +871 -0
- package/src/assistant/engine-mark-art.ts +45 -0
- package/src/assistant/engine-mark.tsx +39 -0
- package/src/assistant/engine.tsx +98 -8
- package/src/assistant/host.tsx +2 -1
- package/src/assistant/index.ts +5 -0
- package/src/assistant-chat.css +1 -0
- package/src/assistant-engine.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/setup-guide.css +1 -0
- package/src/setup-guide.tsx +116 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,184 @@
|
|
|
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.30.2 (09/10/2026)
|
|
6
|
+
|
|
7
|
+
The admin's chat moves onto the package's composer and engine picker (gemboss admin, assistant sync).
|
|
8
|
+
|
|
9
|
+
- `ChatComposer` `canSend`: overrides the empty-box rule for the action. The admin sends a reference picture with no words,
|
|
10
|
+
and holds send while an upload runs, without disabling the text box. React only: `chat-plain.js` sits at its weight
|
|
11
|
+
ceiling (8698 B gzip) and no plain-HTML surface needs it yet.
|
|
12
|
+
- `ChatComposer` `stopDisabled`: Stop stays in the slot, disabled, while a stop is on its way (the admin's turn can take
|
|
13
|
+
seconds to abort; a second press was a silent no-op).
|
|
14
|
+
- chat-composer.css / chat.css: the admin's own engine chip rules are deleted (`.gemboss-ec-enginepick`, `-enginepick-btn`,
|
|
15
|
+
`-chiplabel`, `-chipsub`, `-chipmark` and their density and 44px-target rules). The admin now draws `EnginePicker` like
|
|
16
|
+
every surface (`gb-engine-*`, assistant-engine.css); nothing in the fleet wears those classes any more.
|
|
17
|
+
|
|
18
|
+
## 0.30.1 (09/10/2026)
|
|
19
|
+
|
|
20
|
+
- `LayerList` / `SortableLayerList` under a finger (`pointer: coarse`): the row's action buttons are 44x44 (were 26x26,
|
|
21
|
+
the mouse size) and the row's pick button is at least 44px tall. Where the name and the buttons do not fit side by side
|
|
22
|
+
(the 300px Layers drawer), the row wraps and the buttons take a line of their own, so a name is never drawn under them.
|
|
23
|
+
Measured on the funnel canvas's section list at 768 with touch. Mouse sizes are unchanged.
|
|
24
|
+
|
|
25
|
+
## 0.30.0 (09/10/2026)
|
|
26
|
+
|
|
27
|
+
**What gemform had and the package did not** (Chris 08/10/2026: "cái page login của gemform cái background bên dưới logo
|
|
28
|
+
cũng đẹp… trong gemform có cái gì mà gemboss ui không có thì chuẩn hoá đăng ký vào gemboss ui"). gemform
|
|
29
|
+
(gemlabs `toolkits/gemform`) was inventoried screen by screen; three things in it were shared patterns the package lacked.
|
|
30
|
+
Everything else it draws (shell, KPIs, cards, tables with bulk actions, filters, pager, switches, banners, key-value
|
|
31
|
+
lists, skeletons) the package already has, and gemform moves onto those. README "The sign-in screen", "Mini charts",
|
|
32
|
+
"Setup guide".
|
|
33
|
+
|
|
34
|
+
Added
|
|
35
|
+
- `AuthLayout backdrop`: `"facet"` (default, the markup and pixels of 0.29.2), `"grid"` (gemform's ground: a 34px iris
|
|
36
|
+
grid fading out from the panel's top-left corner, on an ink gradient), `"none"`. Tokens `--gb-panel-grid-bg`,
|
|
37
|
+
`--gb-panel-grid-line`, `--gb-panel-grid-size`, `--gb-panel-grid-fade` on `.gb-auth`; classes `gb-auth-panel--grid` /
|
|
38
|
+
`--none` for a page that writes the markup. Drawn only where the panel is (900 wide AND 520 tall).
|
|
39
|
+
- `Sparkline` + `sparklinePaths` and `ShareBar`, a new entry `minichart.css` (0.5 KB gzip). Six hand-drawn sparklines in
|
|
40
|
+
the fleet (admin x3, talent-radar, search-radar, gemwatcher, gemform); the baseline is 0 for counts unless fixed.
|
|
41
|
+
- `SetupGuide`, a new entry `setup-guide.css` (0.9 KB gzip): the admin's Setup guide (StoreChecklistCard,
|
|
42
|
+
`gemboss-setup-*`) with the kit's tokens, all native `<details>` (no script for plain HTML). The admin keeps its own
|
|
43
|
+
until it moves onto this one in a PR of its own.
|
|
44
|
+
- The catalog draws the sign-in screens at 1280 (at the catalog's column width the panel never showed), the grid beside
|
|
45
|
+
the facet, the mini charts in "Báo cáo", and a "Hướng dẫn thiết lập" section.
|
|
46
|
+
|
|
47
|
+
Neither new entry is in `gemboss-ui.css` or the core (parts gate); the admin imports none of them. The grid's rules are
|
|
48
|
+
in auth.css (+0.2 KB gzip there and in the bundle). `apps/admin/scripts/check-token-fallbacks-agree.mjs` now reads
|
|
49
|
+
minichart.css and setup-guide.css too.
|
|
50
|
+
|
|
51
|
+
Review #3925 (in this version): class names are written out (no `ui-sharebar__seg--${tone}` stem: the admin's orphan-rule
|
|
52
|
+
gate went red on it); ShareBar grows by share, so fractions fill the track; a sparse array's holes are gaps; SetupGuide
|
|
53
|
+
decides the open step once, draws a step without a body as a plain row, fills a `<progress>` instead of a styled width,
|
|
54
|
+
renders nothing for no steps, remembers a fold (`rememberKey`), takes a `group` name; the reduced-motion block names the
|
|
55
|
+
head's chevron with its own selector (`setup-guide-browser.test.tsx` reads the computed transition, with the weak
|
|
56
|
+
selector as negative control); the specificity test computes specificity instead of matching strings.
|
|
57
|
+
|
|
58
|
+
## 0.29.2 (08/10/2026)
|
|
59
|
+
|
|
60
|
+
Fixes the rollout to search-radar (gemlabs #943) and content-radar (#944) found in the chat body. `chat-plain.js` does the
|
|
61
|
+
same as the components, held to them by chat-plain.test.ts.
|
|
62
|
+
|
|
63
|
+
- `ChatMarkdown` / `parseInline`: a bare `https://…` (and `<https://…>`) is a link again, as the surfaces' own renderers
|
|
64
|
+
drew it. GitHub's reading of where it ends: trailing `. , : ; ! ? _ ~ ' "` and a `)` or `]` with no partner inside the
|
|
65
|
+
URL go back to the sentence (linear time: a URL followed by 50 000 `)` parses in milliseconds). It also ends at `*`,
|
|
66
|
+
CJK text and full-width / typographic punctuation, and one glued to a word (`foohttps://…`) is not a link. Same
|
|
67
|
+
allowlist as a written link (`linkTarget`, then `new URL`: http(s) with a host), so `https://.`, `ftp:`, `javascript:`
|
|
68
|
+
stay text. New export `bareUrl`.
|
|
69
|
+
- `ChatMarkdown` / `parseBlocks`: `####` to `######` are headings at the bubble's lowest step (`level: 3`, an `h5` with
|
|
70
|
+
`gemboss-md-h3`) instead of four hashes in front of a sentence. `#######` and `#tag` stay text; a heading line with no
|
|
71
|
+
words (`#### `) is dropped instead of drawing an empty heading.
|
|
72
|
+
- `ImageHost.host` is optional: an entry with no host is the page's OWN origin (`imageHosts={[{ path: "/api/img/" }]}`),
|
|
73
|
+
for a root-relative picture (content-radar's `/api/img/…`). Checked after the browser's normalisation: the resolved
|
|
74
|
+
path must start with `path`; `../`, `%2e%2e`, `\`, an encoded slash or backslash, a double encoding (`%25`), a `;` path
|
|
75
|
+
parameter (`..;/`), `//host` and `/\host` are refused, and the src drawn is the resolved path. `path` must be a
|
|
76
|
+
directory below the root (`/x/`; `/` and `/x` are ignored). An entry WITH a host never opens relative paths.
|
|
77
|
+
- chat.css: the send circle is `.gemboss-ec-toolbar > button[type="submit"]` (the toolbar's own child, same specificity).
|
|
78
|
+
`.gemboss-ec-input button[type="submit"]` also reached the engine popup, a DOM descendant of the composer even in the
|
|
79
|
+
top layer, and drew a surface's Pair button there as the 44px filled circle (five surfaces wrote `type="button"` to
|
|
80
|
+
dodge it). Test of the forbidden combination in chat.test.tsx, and the popup measured in Chromium against the same
|
|
81
|
+
button outside the composer (chat-browser.test.tsx, chat-plain.test.ts), each with the old selector as its negative.
|
|
82
|
+
|
|
83
|
+
## 0.29.1 (08/10/2026)
|
|
84
|
+
|
|
85
|
+
- `ChatThread` and plain `chat.thread`: keep following when a native scroll event from their own write-to-end arrives
|
|
86
|
+
after new content. The increased content gap no longer masquerades as the reader scrolling up. Wheel, key, touch and
|
|
87
|
+
direct programmatic moves up still unpin the thread.
|
|
88
|
+
|
|
89
|
+
## 0.29.0 (08/10/2026)
|
|
90
|
+
|
|
91
|
+
**The assistant's chat body as components** (Chris 08/10/2026: "đồng bộ hết giao diện của AI assistant… mọi service có
|
|
92
|
+
panel này đồng bộ style + code dùng gemboss ui"). Until now the package drew the frame, the conversation list and the
|
|
93
|
+
engine picker; every surface wrote the bubbles, steps, starters and composer by hand on chat.css's classes, with its own
|
|
94
|
+
markdown renderer (five of them). The surface now hands in data and actions. README "The chat body".
|
|
95
|
+
|
|
96
|
+
Added
|
|
97
|
+
- `@gemboss/ui/assistant`: `AssistantChat` (thread + empty state + composer, from `messages`), and its pieces
|
|
98
|
+
`ChatThread` (role=log, follows the end only while the reader is there, aria-busy), `ChatMessage`, `ChatBubble`,
|
|
99
|
+
`ChatMarkdown` (the admin's parser and renderer: tables, headings demoted, fenced code that folds, safe links, pictures
|
|
100
|
+
only from fleet hosts, no raw HTML; `ownLink` / `renderLink` for a surface's own scheme), `ChatSteps` (rows print and
|
|
101
|
+
stop; kinds step / thinking / trace / steer), `ChatPending` (the one heartbeat; says its label once to a screen
|
|
102
|
+
reader), `ChatStarters` (they wrap), `ChatMeta` (Copy · engine · seconds · cost), `ChatError` (a fix: sentence,
|
|
103
|
+
command, Copy, the original folded), `ChatNote`, `ChatComposer` (a DIV, not a form; Enter sends, Shift+Enter and an IME's
|
|
104
|
+
Enter do not; one 44px slot that is send or stop; grows to 84px; `data-density` full → compact → icon → tight, measured),
|
|
105
|
+
`ChatAttach`, `PaperclipIcon`, `useComposerDensity`, `ChatLabelsProvider`, `CHAT_EN`.
|
|
106
|
+
- `useChatTurns()`: the conversation's state with an id per message; a turn is written BY ITS ID (`start`, `write`,
|
|
107
|
+
`step`, `finish`, `fail`, `note`), so a note landing mid-turn or the next turn opening cannot take its text (the
|
|
108
|
+
"patch the last element" bug of partner-radar #929 / content-radar #937). `bridgeTurnError(e)` + `atBridge(e, status)`:
|
|
109
|
+
only a failure AT the bridge (its call threw, or it answered 401: pairing code rejected) is the bridge's; its 400 / 503
|
|
110
|
+
and failures on the way to the server are the turn's.
|
|
111
|
+
- `parseBlocks`, `parseInline`, `linkTarget`, `imageSource`, `FLEET_IMAGE_HOSTS`: the markdown, testable without React.
|
|
112
|
+
- `EngineMark`, `ENGINE_MARK_ART`, `isEngineMarkId`: each engine's mark (from the admin).
|
|
113
|
+
- `EnginePicker`: `EngineChoice.mark` (the chip shows the engine's mark instead of its name when the composer is too
|
|
114
|
+
narrow), `EngineChoice.models` + `onPick(engine, model)` (every engine with its own models, one click picks both: the
|
|
115
|
+
admin's popup), `problem` (a red line on the Engine tab), `disabled` (the chip itself, while a switch is in flight).
|
|
116
|
+
- `assistant-chat.css`, a new entry (0.9 KB gzip), imported by the chat components; not in `gemboss-ui.css`.
|
|
117
|
+
chat.tsx imports chat.css and chat-composer.css too (they were "the surface links it").
|
|
118
|
+
- `chat-plain.js` (after `kit-plain.js`, and `engine-plain.js` when the page has the picker): `gembossUI.chat` with
|
|
119
|
+
`markdown`, `message`, `render`, `thread`, `steps`, `pending`, `starters`, `meta`, `error`, `note`, `composer`, `attach`,
|
|
120
|
+
`turnError`, `atBridge`, and `gembossUI.engineMark`. Same markup as the components, element by element (chat-plain.test.ts).
|
|
121
|
+
- `AssistantChat` `title` / `subtitle`: the chat's own heading where the frame has no title bar (full screen).
|
|
122
|
+
- `useFullChromeRoom(ref)`: how far full screen's floated buttons reach into an element (`--gb-chat-chrome-w` / `-h`,
|
|
123
|
+
`data-chrome`), for a surface arranging the pieces itself; `AssistantChat` uses it.
|
|
124
|
+
- Exports `./assistant/markdown` (the parser) and `./assistant/engine-mark-art` (the artwork): no JSX, no CSS, so a node
|
|
125
|
+
test or a server module imports them. The admin's `components/chat/markdown-blocks.ts` and `lib/engine-mark-art.ts`
|
|
126
|
+
now re-export these (one owner; the admin's tests read the package's code). A fence with a tag like `c#`, `c++`,
|
|
127
|
+
`objective-c` is a fence (it opened nothing, and its closing fence swallowed the rest of the answer as code).
|
|
128
|
+
|
|
129
|
+
Fixed
|
|
130
|
+
- Full screen: the frame's floated buttons (Back to the popup and a surface's `fullActions`) covered the first message of
|
|
131
|
+
a chat with no heading (content-radar /v2/assistant at 1280). `AssistantChat` measures how far they reach into it and
|
|
132
|
+
makes room (`data-gb-dock-chrome` on the host's button row is what it measures).
|
|
133
|
+
- On a phone, a `fullActions` button with no icon lost its words and showed an empty circle (assistant-chat.css).
|
|
134
|
+
- `BridgeNotice`'s "Open setup" was a 16px box with a 24px overlay, which a measuring tool reads as 16: its own box is now
|
|
135
|
+
24px with a mouse and 44px under touch / at phone width (negative margins keep the line 16px). A long sentence no longer
|
|
136
|
+
wraps away from its dot (dot | sentence, the action under the sentence).
|
|
137
|
+
- `engine-plain.js`: the chip carries the engine's mark (`mark`) when chat-plain.js is on the page.
|
|
138
|
+
|
|
139
|
+
Review #3910 (all in this version): the log unpins at the reader's first move up (wheel, finger, keys), not 24px later, and
|
|
140
|
+
goes back to the end on a send or another conversation (`ChatThread pinKey`; plain: `log.toEnd()` / composer `thread`);
|
|
141
|
+
`turns.step` keeps a field an update leaves out; a bridge `TimeoutError` is the bridge's; clicking the pick already in
|
|
142
|
+
force changes nothing; a step for an engine with no mark keeps its ✓ / ✕; message keys never mix ids and positions;
|
|
143
|
+
plain `render()` keeps a message's element when its look did not change, the heartbeat's clock, and the folds the reader
|
|
144
|
+
opened; the full-screen room is measured again when the buttons themselves change size.
|
|
145
|
+
|
|
146
|
+
## 0.28.0 (08/10/2026)
|
|
147
|
+
|
|
148
|
+
**Action card: an assistant proposes, a person applies.** The look of GemFeedback's merge and board-action cards (Chris
|
|
149
|
+
08/10/2026, "follow theo gemboss ui"), so every assistant in the fleet asks before a change the same way. README "Action card".
|
|
150
|
+
|
|
151
|
+
Added
|
|
152
|
+
- `ActionCard`, `ActionCardSection`, `ActionCardItem`, `ActionChange` (React), `action-card.css` (its own entry
|
|
153
|
+
`./action-card.css`, loaded by the component; `gemboss-ui.css` unchanged).
|
|
154
|
+
- Catalog section "Thẻ hành động".
|
|
155
|
+
- `packages/ui/weight-budget.json`: a ceiling for `packages/ui/src/action-card.css`.
|
|
156
|
+
## 0.27.0 (08/10/2026)
|
|
157
|
+
|
|
158
|
+
**The engine picker for plain HTML**, for gemcare's two assistants (inbox, Reports), which have no React.
|
|
159
|
+
|
|
160
|
+
Added
|
|
161
|
+
- `engine-plain.js` (new entry, after `kit-plain.js`): `gembossUI.enginePicker(host, options)` → `{ update, openTab, open,
|
|
162
|
+
close, isOpen, destroy, element, panel }` and `gembossUI.bridgeNotice(options)`. Same markup as `EnginePicker` (one
|
|
163
|
+
stylesheet, `assistant-engine.css`), same behaviour; a redraw patches in place. README "Plain HTML:
|
|
164
|
+
`gembossUI.enginePicker()`". 5.3 KB gzip minified, its own ceiling.
|
|
165
|
+
- `gembossUI.placeFloating(layer, anchor, end)` in kit-plain.js: the menus' placement, one plain copy (floatMenu and
|
|
166
|
+
engine-plain.js use it).
|
|
167
|
+
|
|
168
|
+
Changed
|
|
169
|
+
- `EnginePicker`: an Escape that closes the panel is marked handled (`preventDefault`) also when focus is outside the
|
|
170
|
+
panel. A surface whose Escape hides the assistant and checks `defaultPrevented` waits for the next one; the
|
|
171
|
+
window-capture markers the surfaces added (gemlabs #922, gemfeedback #66) are no longer needed. Not an Escape it does
|
|
172
|
+
not own: an IME composition (keyCode 229), or one in a modal opened from the panel; and focus entering such a modal
|
|
173
|
+
closes the panel, which would otherwise sit above it in the top layer.
|
|
174
|
+
|
|
175
|
+
## 0.26.0 (08/10/2026)
|
|
176
|
+
|
|
177
|
+
**A tablet glyph to draw with** (the funnel canvas's "All sizes": each page at desktop, tablet and phone widths).
|
|
178
|
+
|
|
179
|
+
Added
|
|
180
|
+
- `TabletGlyph` in `@gemboss/ui/editor`, beside `MonitorGlyph` and `PhoneGlyph`. `DeviceToggle` draws it for its Tablet
|
|
181
|
+
button (it carried the same SVG inline), so there is one tablet.
|
|
182
|
+
|
|
5
183
|
## 0.25.0 (07/10/2026)
|
|
6
184
|
|
|
7
185
|
**The assistant's engine picker** (Chris 07/10/2026): a chip in the composer row, as in the admin, that opens a popup
|
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
|
|
@@ -304,35 +322,193 @@ const picker = useEnginePicker(); // open + tab, shared with the notice
|
|
|
304
322
|
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
323
|
- **Not in the chat**: no standing warning, no command in the conversation. The amber dot on the chip and on the Setup
|
|
306
324
|
tab is the warning. `BridgeNotice` is for a turn that actually failed, once.
|
|
325
|
+
- **For the admin (0.29.0)**: `EngineChoice.mark` ("claude", "codex") is shown on the chip in place of the name when the
|
|
326
|
+
composer is too narrow (`ChatComposer` density icon); `EngineChoice.models` on every engine + `onPick(engine, model)`
|
|
327
|
+
lists each engine with its own models at once, and one click picks both and closes (the admin's Polaris popup);
|
|
328
|
+
`problem` is a red line on the Engine tab ("Wait for the current answer to finish"); `disabled` disables the chip while
|
|
329
|
+
a switch is in flight.
|
|
307
330
|
- The package knows no bridge: the surface polls its own, computes `outdated`, builds the command, and owns the
|
|
308
331
|
behaviour of every callback. `EnginePanel` is the same tabs without the chip (catalog, a settings page).
|
|
309
332
|
|
|
310
333
|
Tests: `packages/ui/src/assistant/engine.test.tsx` (markup, styles, its own entry), `packages/ui/src/assistant/engine-browser.test.tsx`
|
|
311
334
|
(Chromium: top layer through a clipping frame, keys, focus, copy, Open setup, 44px; with negative controls).
|
|
312
335
|
|
|
313
|
-
|
|
336
|
+
**Escape (0.27.0).** An Escape that closes the panel is marked handled (`preventDefault`) also when focus is outside it
|
|
337
|
+
(the composer): a surface whose own Escape hides the assistant and checks `defaultPrevented` leaves that key alone, and the
|
|
338
|
+
next Escape is its own again. Before, the key went on unmarked and React re-renders between the picker's listener and the
|
|
339
|
+
surface's, so each surface needed a window-capture marker to tell the two apart (gemlabs #922, gemfeedback #66). With
|
|
340
|
+
0.27.0 the surface's `!e.defaultPrevented` is enough.
|
|
341
|
+
|
|
342
|
+
### Plain HTML: `gembossUI.enginePicker()` (0.27.0)
|
|
343
|
+
|
|
344
|
+
`engine-plain.js`, after `kit-plain.js`, for a page with no React (gemcare). The same markup and classes as the component
|
|
345
|
+
(asserted: same class set for the same options), so `assistant-engine.css` draws it, and the same behaviour (top layer,
|
|
346
|
+
tabs and keys, focus back to the chip, closing goes back to Engine, Copy, 44px under a coarse pointer, Escape marked):
|
|
314
347
|
|
|
315
|
-
|
|
316
|
-
|
|
348
|
+
```html
|
|
349
|
+
<link rel="stylesheet" href="/ui/assistant-engine.css"> <!-- require.resolve("@gemboss/ui/assistant-engine.css") -->
|
|
350
|
+
<script src="/ui/kit-plain.js"></script><script src="/ui/engine-plain.js"></script>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
const picker = gembossUI.enginePicker(document.querySelector(".gemboss-ec-toolbar-lead"), {
|
|
355
|
+
engines: [{ id: "server", label: "GemCare AI, paid" },
|
|
356
|
+
{ id: "claude", label: "Claude Code", badge: "your plan", badgeTone: "success", note: "Your own subscription." }],
|
|
357
|
+
engine: "claude", onEngineChange: (id) => …,
|
|
358
|
+
models, model, onModelChange, efforts, effort, onEffortChange, effortDisabled, notes, modelControl,
|
|
359
|
+
bridge: { state: "outdated", version: "8", latest: "10", autostart: true, detail: "…" }, showBridgeDot: true,
|
|
360
|
+
setup: { command, pairingCode, steps, note }, statusActions: [checkAgainButton],
|
|
361
|
+
onOpenChange, onTabChange, locked, end, labels,
|
|
362
|
+
});
|
|
363
|
+
picker.update({ bridge: { state: "running" } }); // a poll: bridge / setup / labels merge one level; patched in place
|
|
364
|
+
picker.openTab("setup"); picker.close(); picker.isOpen(); picker.destroy();
|
|
365
|
+
log.append(gembossUI.bridgeNotice({ text: "The bridge on your machine is not responding.", onOpenSetup: () => picker.openTab("setup") }));
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Strings are text; `steps`, `notes`, `detail`, `statusActions` and `modelControl` may also be a Node, an array of Nodes or a
|
|
369
|
+
DocumentFragment: placed once and left where they are, so a field in them keeps focus, caret and events across redraws.
|
|
370
|
+
A redraw PATCHES the panel in place (the same elements, only what changed, as React does): focus on a tab, an engine row
|
|
371
|
+
or the model select stays, and a clamped panel keeps its scroll; behaviour is delegated from the panel (`data-*`).
|
|
372
|
+
A pick from the engine list, the model or the effort updates the picker at once and calls back (no callback: that row is
|
|
373
|
+
disabled, as in the component); `update()` is how the page says anything else changed. An Escape it does not own is left
|
|
374
|
+
alone: an IME composition, or one in a modal opened from the panel; and focus entering such a modal closes the panel,
|
|
375
|
+
which would otherwise sit above it in the top layer (the component does the same since 0.27.0). Tests:
|
|
376
|
+
`packages/ui/src/engine-plain.test.ts` (Chromium: the component's class set on all three tabs, top layer through a
|
|
377
|
+
clipping frame with its negative control, no popover API, keys, Escape, Copy, redraws that keep focus / scroll / a
|
|
378
|
+
caller's field without a blur or change, merge rules, 44px).
|
|
379
|
+
|
|
380
|
+
## The chat body (components since 0.29.0)
|
|
381
|
+
|
|
382
|
+
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".
|
|
383
|
+
Since 0.29.0 the package DRAWS the chat, not only its CSS: the surface hands in messages and actions and keeps its own
|
|
384
|
+
streaming, bridge and sessions. Each piece is the better of the two surfaces that had it (inventory 08/10/2026,
|
|
385
|
+
`Gems/_local/fleet/assistant-sync-2026-10/`):
|
|
386
|
+
|
|
387
|
+
| Piece | Component | Taken from | Why that one |
|
|
388
|
+
|---|---|---|---|
|
|
389
|
+
| 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 |
|
|
390
|
+
| Markdown | `ChatMarkdown` | admin | tables, folded code, safe links, trusted pictures, no raw HTML; the radars fed an HTML string to `dangerouslySetInnerHTML` |
|
|
391
|
+
| 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 |
|
|
392
|
+
| Working | `ChatPending` | admin | ✳, a verb that changes, the seconds: the wait reads alive |
|
|
393
|
+
| 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 |
|
|
394
|
+
| Under an answer | `ChatMeta` | partner-radar | Copy · engine · seconds · cost |
|
|
395
|
+
| Errors | `ChatError`, `BridgeNotice` | partner-radar | a fix the user can take (sentence, command, Copy, the original folded); a bridge failure is one line |
|
|
396
|
+
| Engine | `EnginePicker` | partner-radar (Setup / Status tabs) + admin (mark, models per engine) | see "The engine picker" |
|
|
397
|
+
| Frame, sessions | `AssistantHost`, `SessionRail` | (already the package's) | |
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import { AssistantChat, ChatAttach, EnginePicker, useChatTurns, useEnginePicker } from "@gemboss/ui/assistant"; // brings its CSS
|
|
401
|
+
|
|
402
|
+
const turns = useChatTurns(); // messages with ids; a turn is written BY ID
|
|
403
|
+
const picker = useEnginePicker();
|
|
404
|
+
async function send(text: string) {
|
|
405
|
+
turns.say(text);
|
|
406
|
+
const id = turns.start();
|
|
407
|
+
try {
|
|
408
|
+
for await (const ev of stream(text)) {
|
|
409
|
+
if (ev.type === "text") turns.write(id, ev.text);
|
|
410
|
+
if (ev.type === "tool") turns.step(id, { id: ev.id, label: TOOL_LABEL[ev.name], target: ev.target, state: ev.state });
|
|
411
|
+
}
|
|
412
|
+
turns.finish(id, { meta: ["Claude Code", "4.2s"] });
|
|
413
|
+
} catch (e) {
|
|
414
|
+
if (turns.fail(id, e).bridge) probeTheBridgeAgain(); // mark the bridge's own call with atBridge(e, status)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
<AssistantHost title="Partner Assistant" …>
|
|
419
|
+
<AssistantWorkspaceLayout dock={presentation !== "full"}>
|
|
420
|
+
<AssistantChat messages={turns.messages} busy={busy} onSend={send} onStop={stop}
|
|
421
|
+
title="Partner Assistant" // shown at full screen only (the frame's header says it elsewhere)
|
|
422
|
+
intro={<p className="gemboss-md-p">Describe a meeting…</p>}
|
|
423
|
+
starters={[{ label: "Had dinner with Amit…", hint: "Example, click to fill in", fill: true }]}
|
|
424
|
+
moreStarters={SUGGESTIONS} moreStartersTitle="Or ask about partners in the radar:"
|
|
425
|
+
engine={<EnginePicker {...picker.props} … />} tools={<ChatAttach accept=".pdf,.docx" onFiles={upload} />}
|
|
426
|
+
onOpenSetup={() => picker.openTab("setup")} renderExtra={(m) => m.proposals?.map(…)} onNavigate={router.push} />
|
|
427
|
+
</AssistantWorkspaceLayout>
|
|
428
|
+
</AssistantHost>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
- **`messages`** (`ChatMessageData`): `role` user / assistant / note; `text` (markdown for an answer, `markdown: false` for
|
|
432
|
+
plain); `pending` (the heartbeat until text, then the cursor); `steps`; `error` + `fix: { text, command }`, or `bridge:
|
|
433
|
+
true` (one BridgeNotice line whose action calls `onOpenSetup`); `warn`; `stopped`; `meta` (after Copy); a note's `tone`
|
|
434
|
+
warn / down + `action` makes it a BridgeNotice ("This bridge is outdated · Open setup").
|
|
435
|
+
- **Failed turns** (`bridgeTurnError`, used by `turns.fail`): only a failure AT the bridge is the bridge's: its call threw
|
|
436
|
+
(it is gone, the machine slept) or it answered 401 (this browser's pairing code is not its code). Its 400 / 503 and a
|
|
437
|
+
failure on the way to the server (a data token, an expired session) are the turn's. The surface marks the bridge's own
|
|
438
|
+
call (`throw atBridge(err, res.status)`); it never guesses from "this turn used the bridge" (content-radar #937).
|
|
439
|
+
- **Failed turns, timeouts**: give the bridge's call its deadline with `AbortSignal.timeout(ms)` (a `TimeoutError`, the
|
|
440
|
+
bridge's); an `AbortController` you abort yourself throws an `AbortError`, which reads as the user's Stop.
|
|
441
|
+
- **Following the end**: the log follows while the reader is at the end, and stops at their first move up (wheel, finger,
|
|
442
|
+
ArrowUp / PageUp / Home), not 24px later. A send, or another conversation shown, brings it back (`ChatThread pinKey`;
|
|
443
|
+
`AssistantChat` does both).
|
|
444
|
+
- **Full screen**: `AssistantChat` measures how far the frame's floated buttons (`fullActions`, Back) reach into it and
|
|
445
|
+
gives its heading (or, without one, its log) that much room (`useFullChromeRoom`, for your own column); a `fullActions`
|
|
446
|
+
button with no icon keeps its words on a phone. Measured in the child because only the child knows where its column is
|
|
447
|
+
(at 1920 the 820px column does not reach the buttons at all).
|
|
448
|
+
- **Loading**: the chat components import chat.css and chat-composer.css, which the old `gemboss-ui.css` bundle also
|
|
449
|
+
carries. A surface still linking the bundle gets them twice, the second copy after its own CSS: move to the parts
|
|
450
|
+
first (see "Load only what you use").
|
|
451
|
+
- **Composer density**: `full` "Claude Code · Opus" → `compact` "Claude Code" → `icon` the engine's MARK (`EngineChoice.mark`)
|
|
452
|
+
→ `tight`. An engine with no mark keeps its (cut) name.
|
|
453
|
+
- **Markdown** (0.29.2): a bare `https://…` in the prose is a link (GitHub's reading: the sentence's trailing `. , : ; ! ?
|
|
454
|
+
_ ~ ' "` and an unpartnered `)` / `]` stay text; `<https://…>` too), through the same `linkTarget` as `[x](…)`, so only
|
|
455
|
+
http(s) with a host. It ends at a space, `*`, CJK text or full-width punctuation ("访问https://x.test。然后" links only
|
|
456
|
+
the URL), and one glued to a word (`foohttps://…`) is not a link. Inside `**bold**` a URL stays plain text. `####` to
|
|
457
|
+
`######` are headings at the lowest step (`h5.gemboss-md-h3`); a heading line with no words is dropped.
|
|
458
|
+
- **Pictures on the page's own origin** (0.29.2): `imageHosts={[{ path: "/api/img/" }]}` (no `host`) draws
|
|
459
|
+
`` as written, a root-relative path checked after the browser's normalisation: `../`,
|
|
460
|
+
`%2e%2e`, `\` that leave `path`, an encoded `/` or `\`, a double encoding (`%25`), a `;` (`..;/`), `//host` and `/\host`
|
|
461
|
+
are refused (the alt stays as text). `path` must be a directory below the root (`/x/`, not `/` nor `/x`), or the entry
|
|
462
|
+
is ignored. No need to rewrite the answer into absolute URLs first.
|
|
463
|
+
- **A form in the engine popup** (0.29.2): the send circle is `.gemboss-ec-toolbar > button[type=submit]`, the toolbar's
|
|
464
|
+
own child. Before, `.gemboss-ec-input button[type=submit]` also painted a submit button in the popup (it lives in the
|
|
465
|
+
leading slot in the DOM): a surface's Pair form can keep `type="submit"`.
|
|
466
|
+
- A surface that needs its own arrangement uses the pieces directly (`ChatThread`, `ChatMessage`, `ChatBubble`,
|
|
467
|
+
`ChatMarkdown`, `ChatSteps`, `ChatPending`, `ChatStarters`, `ChatMeta`, `ChatError`, `ChatNote`, `ChatComposer`,
|
|
468
|
+
`ChatAttach`) inside `ChatLabelsProvider` for its words. Every word is in `CHAT_EN` (English chrome, Chris 03/10/2026).
|
|
469
|
+
|
|
470
|
+
CSS: `chat.css` (the body; also inside `gemboss-ui.css`, and the admin `@import`s it), `chat-composer.css` (toolbar, density,
|
|
471
|
+
Stop) and `assistant-chat.css` (0.29.0: the column, wrapping starters, the meta line, errors, notes, the paperclip, the
|
|
472
|
+
room for full screen's buttons). The chat components import all three; a page that writes the markup itself links them.
|
|
317
473
|
|
|
318
474
|
| Piece | Classes |
|
|
319
475
|
|---|---|
|
|
320
|
-
|
|
|
321
|
-
| Message list | `.gemboss-ec-scroll` (thin visible scrollbar) |
|
|
476
|
+
| Column | `.gb-chat` › (`.gemboss-ec-head.gb-chat-head`) + `.gemboss-ec-scroll[role=log]` + `.gemboss-ec-input` |
|
|
322
477
|
| Messages | `.gemboss-ec-msg.user` / `.gemboss-ec-msg.assistant` › `.gemboss-ec-bubble` |
|
|
323
478
|
| 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
|
-
|
|
479
|
+
| Steps | `ul.gemboss-narr` › `li.gemboss-narr-row.ok|.running|.failed|.thinking|.trace|.steer` › `.gemboss-narr-dot` + `.gemboss-narr-text` (+ `.gb-chat-target`) |
|
|
480
|
+
| Working / streaming | `.gemboss-ec-pending` (`-star`, `-verb`, `-meta`, `.gb-chat-sr`), `.gemboss-ec-cursor` after the streamed text |
|
|
481
|
+
| Suggestions | `.gemboss-ec-starters` › `button.gemboss-ec-starter.gb-chat-starter` (› `.gb-chat-starter-hint`); a group title `.gb-chat-label` |
|
|
482
|
+
| 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` |
|
|
483
|
+
| 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` |
|
|
484
|
+
|
|
485
|
+
Tests: `packages/ui/src/assistant/chat.test.tsx` (markup, parser, every class styled), `chat-browser.test.tsx` (Chromium:
|
|
486
|
+
Enter / IME, Stop in send's place, growing box, density with the mark, following the end, the turn store by id, failed
|
|
487
|
+
turns, full screen's buttons, 44px; each with its negative control).
|
|
488
|
+
|
|
489
|
+
### Plain HTML: `gembossUI.chat` (0.29.0)
|
|
490
|
+
|
|
491
|
+
`chat-plain.js`, after `kit-plain.js` (and `engine-plain.js` when the page has the picker), for a page with no React
|
|
492
|
+
(gemcare's inbox and Reports assistants). The same markup as the components, element by element, and the same markdown
|
|
493
|
+
(`chat-plain.test.ts` renders both from the same data and compares the trees). `render()` redraws from the data each
|
|
494
|
+
time; a message whose look did not change keeps its element, and a changed one keeps the folds the reader opened and its
|
|
495
|
+
heartbeat's clock:
|
|
496
|
+
|
|
497
|
+
```html
|
|
498
|
+
<link rel="stylesheet" href="/ui/chat.css"><link rel="stylesheet" href="/ui/chat-composer.css">
|
|
499
|
+
<link rel="stylesheet" href="/ui/assistant-chat.css"><link rel="stylesheet" href="/ui/assistant-engine.css">
|
|
500
|
+
<script src="/ui/kit-plain.js"></script><script src="/ui/engine-plain.js"></script><script src="/ui/chat-plain.js"></script>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
const log = gembossUI.chat.thread(document.getElementById("ai-thread")); // role=log, follows the end
|
|
505
|
+
gembossUI.chat.render(log.el, messages, { intro, starters, onStarter, onOpenSetup: () => picker.openTab("setup"),
|
|
506
|
+
ownLink: (h) => /^(customer|conversation):\d+$/.test(h), renderLink: (href, label) => aiLinkButton(href, label) });
|
|
507
|
+
const box = gembossUI.chat.composer(host, { placeholder: "Ask about this customer", maxLength: 1000, leading: pickerHost,
|
|
508
|
+
thread: log, onSend: (text) => ask(text), onStop: () => controller.abort() }); // thread: back to the end on a send
|
|
509
|
+
box.setBusy(true); log.setBusy(true);
|
|
510
|
+
const { error, bridge } = gembossUI.chat.turnError(err); // the same rule as bridgeTurnError
|
|
511
|
+
```
|
|
336
512
|
|
|
337
513
|
## Editor CSS (0.9.x)
|
|
338
514
|
|
|
@@ -418,7 +594,7 @@ import { DeviceToggle, EditorBack, EditorChatToggle, EditorFrame, EditorPaneTabs
|
|
|
418
594
|
| `EditorBack` | `.gemboss-editor-back` ← | A `<button type="button">`, or `as={Link}` / `as="a"` with `href`. Named "Back" for a screen reader (`label`). |
|
|
419
595
|
| `EditorChatToggle` | `.gemboss-editor-chat-toggle` | Shows / hides the left column; filled while open, pressed when collapsed. |
|
|
420
596
|
| `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. |
|
|
597
|
+
| `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
598
|
|
|
423
599
|
One module per piece behind one entry, so a bundler gives each editor only what it draws (measured: one module for all
|
|
424
600
|
of it cost the page and section editors about 1 KB of code they never ran). Not in `@gemboss/ui` itself: like
|
|
@@ -1033,6 +1209,28 @@ Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, tra
|
|
|
1033
1209
|
message with reactions does not grow by a full line. A plain-HTML surface writes the same classes (markup at the top of
|
|
1034
1210
|
`reactions.css`, and the catalog's "Reactions" section).
|
|
1035
1211
|
|
|
1212
|
+
|
|
1213
|
+
## Action card (0.28.0): an assistant proposes, a person applies
|
|
1214
|
+
|
|
1215
|
+
Chris 08/10/2026: the Feedback Assistant lists likely duplicates and merges them, and carries out status, owner, label
|
|
1216
|
+
and date changes on request, "follow theo gemboss ui". GemFeedback built it first (`components/MergeCard.tsx`,
|
|
1217
|
+
`components/ActionCard.tsx` there); this is its look, moved here so every assistant in the fleet asks the same way.
|
|
1218
|
+
|
|
1219
|
+
The contract every surface keeps: **the model never changes anything**. It proposes (a link it writes, a tool result);
|
|
1220
|
+
the card shows what would change item by item, what to know first, and how far it reaches; only the primary button
|
|
1221
|
+
applies; afterwards the outcome replaces the body, with Undo when the surface can put things back.
|
|
1222
|
+
|
|
1223
|
+
| Piece | What it draws |
|
|
1224
|
+
| --- | --- |
|
|
1225
|
+
| `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` |
|
|
1226
|
+
| `ActionCardSection` | a labelled list ("Keeping", "Merging into it"), or unlabelled |
|
|
1227
|
+
| `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` |
|
|
1228
|
+
| `ActionChange` | "Idea → Planned"; "Already Planned" when equal; `problem` ("Merged into #7", "Changed since") instead |
|
|
1229
|
+
| `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` |
|
|
1230
|
+
|
|
1231
|
+
Presentational: no fetching and no state of its own. The surface loads the preview, owns the handlers, and decides
|
|
1232
|
+
what Undo puts back (GemFeedback: the merge record, or the values an action replaced, guarded by `if=` so Undo never
|
|
1233
|
+
overwrites a later edit). Catalog section "Thẻ hành động".
|
|
1036
1234
|
## Report pages (0.4.0)
|
|
1037
1235
|
|
|
1038
1236
|
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 +1250,46 @@ surface computes every number and ratio; the kit only draws. Markup for each is
|
|
|
1052
1250
|
- `ui-grid--2` / `--3`: card grid that drops to one column; `ui-section-head`: an `h2` + note between page head and
|
|
1053
1251
|
card titles.
|
|
1054
1252
|
|
|
1253
|
+
## Mini charts (0.30.0): `Sparkline`, `ShareBar`
|
|
1254
|
+
|
|
1255
|
+
Two charts small enough for a card or a table cell, each its own entry (`minichart.css`, imported by the component).
|
|
1256
|
+
Before this, six surfaces drew a sparkline by hand (admin intelligence, analytics and operator; talent-radar; search-radar;
|
|
1257
|
+
gemwatcher; gemform) with three different baselines.
|
|
1258
|
+
|
|
1259
|
+
```tsx
|
|
1260
|
+
import { Sparkline, ShareBar } from "@gemboss/ui";
|
|
1261
|
+
<Sparkline points={[3, 5, null, 4, 8]} label="Registrations, last 5 days" />
|
|
1262
|
+
<ShareBar label="Status" parts={[{ label: "New", value: 25, tone: "info" }, { label: "Spam", value: 2, tone: "critical" }]} />
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
- `Sparkline`: oldest point first; `null` is a gap (the line breaks, a lone point is a dot); counts start at 0 unless
|
|
1266
|
+
`min` / `max` fix the scale; `invert` for a rank. Muted line, the newest point one iris dot; `tone` `series` /
|
|
1267
|
+
`success` / `critical`. With `label` it is an image with that name, without it decoration (`aria-hidden`).
|
|
1268
|
+
`sparklinePaths(points, opts)` is the geometry alone, for a page that writes the SVG itself.
|
|
1269
|
+
- `ShareBar`: one 6px bar split by share; parts at 0 take no room; tones are the badge tones (`info`, `success`,
|
|
1270
|
+
`warning`, `critical`, `neutral`) or `series-1..5` (the default, in order). The bar is one image whose name lists every
|
|
1271
|
+
part, so a screen reader hears the numbers the colours stand for. Each segment grows by its SHARE (value / total), so
|
|
1272
|
+
fractions fill the track. The share is an inline `flex-grow`: a plain-HTML page under a CSP that forbids inline style
|
|
1273
|
+
(gemcare) sizes the segments from its own stylesheet or script.
|
|
1274
|
+
|
|
1275
|
+
## Setup guide (0.30.0): `SetupGuide`
|
|
1276
|
+
|
|
1277
|
+
The admin's "Set up your store" card (Shopify's Setup guide) as a component, its own entry (`setup-guide.css`): a title
|
|
1278
|
+
with the count, a bar, collapsed steps of which the first not done is open. Native `<details>`: the head folds the guide,
|
|
1279
|
+
the steps share one `name` so the browser keeps one open, and a page without React writing the same markup (catalog
|
|
1280
|
+
"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
|
|
1281
|
+
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
|
|
1282
|
+
the admin: a state change while someone reads a step never shuts it. `state: "unknown"` (could not be read) is counted
|
|
1283
|
+
apart, never as progress. The bar is a `<progress>` (no inline style). `rememberKey` keeps a folded guide folded across
|
|
1284
|
+
visits (localStorage); `group` names the steps' `name` when two guides render in separate React roots. No steps: no guide.
|
|
1285
|
+
|
|
1286
|
+
```tsx
|
|
1287
|
+
<SetupGuide title="Telegram alerts" steps={[
|
|
1288
|
+
{ id: "bot", title: "Connect a bot", state: bot ? "done" : "todo", body: <ConnectBot /> },
|
|
1289
|
+
{ id: "chat", title: "Choose where alerts go", state: chats.length ? "done" : "todo", body: <Chats /> },
|
|
1290
|
+
]} />
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1055
1293
|
## Collections (0.13.0): tables, lists, grids, boards
|
|
1056
1294
|
|
|
1057
1295
|
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.30.2 · 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";
|