@gemboss/ui 0.42.0 → 0.44.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,106 @@
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.44.0 (21/10/2026)
6
+
7
+ **The assistant frame for a page with no React: `gembossUI.assistantFrame`** (Chris 21/10/2026: GemFactory's assistant was a
8
+ hand-copied popup with none of the other services' shapes; `AssistantHost` is React-only and GemFactory's pages are not).
9
+
10
+ Added
11
+ - `packages/ui/src/assistant-plain.js` (`@gemboss/ui/assistant-plain.js`, its own entry, ES5 classic script after
12
+ `kit-plain.js`): `assistantFrame(opts)` draws `AssistantProvider` + `AssistantHost`'s frame around the page's chat
13
+ (`body`): hidden / status bar / popup / side panel / full screen, with `dock.ts`'s rules (`gembossUI.assistantDock`:
14
+ the reducer, its clamps and constants), the provider's storage keys under `storagePrefix`, `<prefix>-busy` / `-unread` /
15
+ `-side` and `--gb-assistant-side-w` on `<body>`, the rail link's reopen, the popup's grip and slide and the panel's seam
16
+ with their keys, the ⋯ menu (`menuItems`, Clear history, pin / float, minimise), and the same markup and classes, so
17
+ `assistant.css` draws it. For a page that reloads on every click it keeps the shape on screen for the tab
18
+ (`keepAcrossPages`), enters full screen on its own page (`fullHref`), and hides the page's `launchers` while it is up.
19
+ API: `open`, `toggle`, `openDock`, `openSide`, `minimize`, `dismiss`, `enterFull`, `leaveFull`, `setBusy`, `state`,
20
+ `onChange`, `update`, `destroy`, `el`, `pane`. Not yet: `secondTab`, page facts.
21
+ 6.9 KB gzip minified (ceiling 7,562 in `weight-budget.json`; a page without an assistant never loads it).
22
+ - `assistant-plain-browser.test.ts` (`test:ui-browser`): the reducer against `dock.ts` over 400 random sequences of 40
23
+ events; in Chromium, 19 steps (open, busy, ×, status bar, popup, resize, side, width, full and back) on the component and
24
+ on the plain frame at 1440 and 900px, the `.gb-dock` element, `<body>`'s classes and the stored keys compared after each;
25
+ the page-load, launcher, rail-link, ⋯ menu, grip-key and below-1100px behaviour; negative controls for both halves.
26
+
27
+ ## 0.43.0 (21/10/2026)
28
+
29
+ **The full-screen assistant stops paying for a blur nobody sees** (21/10/2026, measured in the GemBoss desktop app).
30
+
31
+ Changed
32
+ - `.gb-dock-full` sets `backdrop-filter: none` (and `-webkit-`), like `.gb-dock-side`. Its background is opaque, so the frost
33
+ never showed, but the GPU painted the page underneath and blurred the whole rectangle every frame. GPU process on
34
+ /app/assistant (1920×1055, 2×): 516 → 430 MB at the scroll peak, 270 → 241 MB at rest; drawn layers 7 → 5.
35
+ - Side effect, a fix: the frame is no longer the containing block of `position: fixed` children in full mode, so the thread
36
+ row's ⋯ menu opens under its button instead of 240px right and 56px down of it (and clipped). Popup and mini still frost.
37
+ - `opaque-dock-modes-skip-the-blur.test.ts` reads the modes from `MODE_CLASS`: any mode painted with an opaque token must turn
38
+ the blur off, and those tokens must be opaque in both themes.
39
+
40
+ **Editor menus at the admin's density** (Chris 21/10/2026, of the canvas's right-click menu: "kích thước của button trong context
41
+ menu cũng nên theo chuẩn chung với các button size khác … nó đang to hơn bình thường").
42
+
43
+ Changed
44
+ - `.gemboss-editor-menu-item` (the ⋯ menu, the canvas's right-click menu) under a mouse: a Polaris ActionList item, 32px tall
45
+ with 4px 6px padding (was 44px with 8px 12px, for every pointer); the group heading lines up at 6px. Under a finger or on a
46
+ screen 767px or narrower it keeps the 44px row. Measured on the admin's editor ⋯ menu: eight rows 366px → 270px.
47
+ `editor-plain.test.ts` holds both in Chromium; the browser gates that asked 44px of a menu row under a mouse
48
+ (check-generate-images-touchable, page-design-review-fixture) ask the kit's row now (44 under a finger or below 768, 32 above).
49
+
50
+ **An emoji button in the team chat composer** (Chris 21/10/2026: "Nút emoji — Dùng lại bảng chọn cảm xúc của gói (đã có,
51
+ chèn vào chỗ con trỏ), cho cả GemCommunity và GemCare vì chung gói").
52
+
53
+ Added
54
+ - `Composer`: an emoji button between the field and Send, on by default; `emoji={false}` turns it off. It opens the reactions'
55
+ own picker as a layer over the box (`.gb-tc-composer__emoji-pop`; the box's whole width on a phone); a pick goes in at the
56
+ caret, over a selection, as typing would (`execCommand("insertText")`, else `setRangeText` + `input`): focus stays in the
57
+ field after the emoji, undo takes it back, `onValueChange` and the app's own `input` listeners hear it, nothing past
58
+ `maxLength`. It joins the same "Frequently used" as a reaction. Escape closes it and puts focus back in the field; a press
59
+ or focus elsewhere closes it; under a finger it opens on the first emoji (no keyboard over it).
60
+ - `TEAM_CHAT_LABELS.emoji` ("Emoji", the button) and `pickEmoji` ("Pick an emoji", the picker's name).
61
+ - `ReactionPicker`: `items` is optional (absent: a picker that writes, plain buttons, none pressed), `label`, `className`,
62
+ `initialFocus` (`"search"` | `"emoji"`); type `ReactionPickerProps`. With `items` it draws what it drew.
63
+ - Icon `emoji` (a face; `reaction` is the face with a plus).
64
+ - Plain script: `gembossUI.teamChat.emojiButton({ textarea, button, host, labels })` wires any field (and its button, or one
65
+ it builds and returns) to the same button and picker, for a composer the page writes itself (GemCare); `reactionPicker`
66
+ takes no `items`, `label`, `className`. Held to the component by `team-chat-plain-browser.test.ts`.
67
+
68
+ Changed
69
+ - `team-chat.css`: `.gb-tc-composer__box` is `position: relative` (the picker's containing block); under 360px of the screen's
70
+ width the box's buttons have no gap, so the field keeps 150px beside three 44px buttons at 320. README "The team chat
71
+ layer": the composer and thread blocks carry the button.
72
+
73
+ **A direct message shows the person's picture** (GemCommunity #322 draws DMs with `ChannelItem kind="dm"` and `ChatHeader`,
74
+ and had only the generic person glyph to show).
75
+
76
+ Added
77
+ - `ChannelItem`: `avatar: { src, name }` on a DM, the kit's `Avatar` at the glyph's 20px (`gb-tc-channel__av`; initials and
78
+ tone when there is no `src`, `name` defaulting to the row's) with the presence dot on it.
79
+ - `ChatHeader`: `avatar: { src, name }` with `kind="dm"`, the `Avatar` at 24px left of the name in place of the glyph
80
+ (`gb-tc-head__av`). README block `header-dm`, held by `team-chat-contract.test.tsx`; type `DmAvatar`.
81
+ - Plain script: `channelItem` takes the same `avatar`, held to the component by `team-chat-plain-browser.test.ts`.
82
+ - Without `avatar`, and on a channel, the glyph as before.
83
+
84
+ **The assistant asks one question and a person only confirms: five pieces and three lighter steps of iris** (Chris 21/10/2026,
85
+ reviewing the Market redesign: "trợ lý trang cũng phải đặt câu hỏi rõ ràng và fill sẵn câu trả lời… chỉ confirm thôi", "đưa cho họ 1
86
+ đến 3 lựa chọn tối đa", "thoáng, ít chữ, có hình"; design in the artifact "Market, thiết kế lại + trợ lý trong trang").
87
+
88
+ Added
89
+ - `QuestionCard`: one question, one to three choices (the type refuses a fourth; a longer list is cut to three), the first the
90
+ assistant's best guess, one primary confirm, quiet "type my own" and "skip". Controlled, real radio inputs. `MAX_QUESTION_CHOICES`.
91
+ - `BriefingCard`: one proposal at the top of a screen, with its evidence and one to three buttons. An iris wash.
92
+ - `RangeBar` (+ `rangeBarGeometry`): a value against a market's spread, `sm` for a tile and `lg` with labels. A value off the axis is
93
+ pinned to its edge, a band given high-to-low is turned round, a non-number is left out.
94
+ - `ProgressRing` (+ `progressRingGeometry`, `RING_BOX`): "3 of 5" as a ring. A `max` that is not a positive number is an empty ring.
95
+ - `SpotArt` (+ `SPOT_ART_NAMES`): six brand pictures (`gem-radar`, `orbit`, `people`, `page`, `plug`, `price`), painted by tokens.
96
+ - Tokens `--gemboss-accent-wash`, `--gemboss-accent-tint`, `--gemboss-accent-mid`, light and dark (`theme-dark.css`). Fills, never text.
97
+ - Five entries, none in the frozen bundle: `question-card.css`, `briefing-card.css`, `range-bar.css`, `progress-ring.css`,
98
+ `spot-art.css` (`parts.mjs`, `package.json` exports, `weight-budget.json`).
99
+
100
+ Weight: the three tokens are the only thing that reaches the frozen bundle, +60 B gzip (`gemboss-ui.css` 22,655 → 22,715 B), so its ceiling
101
+ goes 22,683 → 22,743 B (the same 28 B of room it had). The five entries are 0.2 to 0.9 KB gzip each, with their own ceilings.
102
+
103
+ Nothing existing changes: no class, no token and no component that was there is touched.
104
+
5
105
  ## 0.42.0 (21/10/2026)
6
106
 
7
107
  **The editor's colour field offers the page's theme colours and this store's recent colours** (gemboss #4888, Chris "bấm
package/README.md CHANGED
@@ -74,6 +74,11 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
74
74
  | reader-dialog | `@gemboss/ui/reader-dialog.css` | `packages/ui/src/reader-dialog.css` | `ReaderDialog`: the popup sized for reading a document (0.41.0) | 0.4 KB | `ReaderDialog` imports it |
75
75
  | service-switcher | `@gemboss/ui/service-switcher.css` | `packages/ui/src/service-switcher.css` | `ServiceSwitcher` and `Switcher`: the button after the brand and its panel (0.36.0; any list, illustrated tiles 0.40.0) | 2.2 KB | `ServiceSwitcher` imports it, and `BrandLockup` through it (README "ServiceSwitcher") |
76
76
  | team-chat | `@gemboss/ui/team-chat.css` | `packages/ui/src/team-chat.css` | the team chat layer (20/10/2026): rail, messages, composer, thread and people panels, the screen's layout | 3.6 KB | `TeamChat` and its pieces import it (with `Avatar`'s portal.css and `Reactions`' reactions.css); a plain-HTML page links it (README "The team chat layer") |
77
+ | question-card | `@gemboss/ui/question-card.css` | `packages/ui/src/question-card.css` | `QuestionCard`: the assistant's one question with one to three answers, the pick chosen (0.43.0) | 0.9 KB | `QuestionCard` imports it |
78
+ | briefing-card | `@gemboss/ui/briefing-card.css` | `packages/ui/src/briefing-card.css` | `BriefingCard`: one proposal at the top of a screen, on an iris wash (0.43.0) | 0.5 KB | `BriefingCard` imports it |
79
+ | range-bar | `@gemboss/ui/range-bar.css` | `packages/ui/src/range-bar.css` | `RangeBar`: a value against a market's spread on one axis (0.43.0) | 0.6 KB | `RangeBar` imports it |
80
+ | progress-ring | `@gemboss/ui/progress-ring.css` | `packages/ui/src/progress-ring.css` | `ProgressRing`: "3 of 5" as a ring (0.43.0) | 0.2 KB | `ProgressRing` imports it |
81
+ | spot-art | `@gemboss/ui/spot-art.css` | `packages/ui/src/spot-art.css` | `SpotArt`: six brand pictures for pauses and empty states (0.43.0) | 0.4 KB | `SpotArt` imports it |
77
82
  | 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) |
78
83
 
79
84
  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).
@@ -307,6 +312,28 @@ minimise. Full screen's chrome shows ⋯ too when you pass either. Do not draw a
307
312
  menuItems={<AssistantMenuItem onClick={exportChat}>Export chat</AssistantMenuItem>}>
308
313
  ```
309
314
 
315
+ **A page with no React (0.44.0): `assistant-plain.js`.** The same frame for a server-rendered page (GemFactory's board,
316
+ gemcare): the five shapes, `dock.ts`'s rules copied in ES5, the provider's storage keys, `<prefix>-side` and
317
+ `--gb-assistant-side-w` on `<body>`, the rail link's reopen, the ⋯ menu, the grips and their keys, and the same markup,
318
+ so `assistant.css` draws it. `assistant-plain-browser.test.ts` holds it to `AssistantProvider` + `AssistantHost`: the same
319
+ steps on both, the `.gb-dock` element compared after each one. A classic script after `kit-plain.js`:
320
+
321
+ ```html
322
+ <script src="kit-plain.js"></script><script src="chat-plain.js"></script><script src="assistant-plain.js"></script>
323
+ <script>
324
+ const frame = gembossUI.assistantFrame({ title: "Factory Assistant", body: chatElement,
325
+ storagePrefix: "gemfactory.assistant", launchers: ".gb-assistant-launcher", fullHref: "/assistant",
326
+ onNewChat, onClearHistory, menuItems: [{ label: "Export chat", onClick: exportChat }] });
327
+ chat.onBusyChange = (busy) => frame.setBusy(busy);
328
+ </script>
329
+ ```
330
+
331
+ The page reloads on every click, so the frame keeps the shape on screen for the tab (sessionStorage
332
+ `<prefix>.presentation`, `keepAcrossPages: false` to turn it off): open on one page, open on the next. A run does not
333
+ survive the reload, and leaving the full-screen page leaves the status bar, as `leave-full` does. The full-screen page
334
+ is yours (`fullHref`); the frame enters full there by itself. `launchers` are hidden while the frame is up and open it.
335
+ Not in the plain frame yet: `secondTab` and page facts.
336
+
310
337
  ## The engine picker (0.25.0): `EnginePicker`, `BridgeNotice`
311
338
 
312
339
  Which engine answers the assistant, and the bridge on the user's machine, without spending the chat's space on it
@@ -1677,7 +1704,7 @@ and GemCare's chat, draw and accept the same thing.
1677
1704
  |---|---|
1678
1705
  | `@gemboss/ui/reactions-data` (plain TS, also re-exported from the root) | `REACTION_GROUPS` (GemCare's 137 emoji in 4 groups, each with search words), `REACTION_EMOJI`, `isReactionEmoji` (the server's check: refuse anything else), `searchReactions`, `reactionLabel` (names, two then `+n`), `toggleReaction` (the optimistic guess drawn before the server answers), `QUICK_REACTIONS` (👍 ✅), type `Reaction` = `{ emoji, names, mine }` |
1679
1706
  | `Reactions` (React, client) | the chips (emoji + NAMES of who put it; yours outlined + filled, `aria-pressed`) and "Add reaction", which opens `ReactionPicker` in flow under the row. `canReact={false}`: chips only; no chips and no right to react: nothing |
1680
- | `ReactionPicker` | search, "Frequently used" (this browser's last 9, `localStorage`, else the quick pair), the groups; arrows move in the grid, Enter in the search picks the first, Escape closes and returns focus |
1707
+ | `ReactionPicker` | search, "Frequently used" (this browser's last picks, `localStorage`, filled up to nine with `SUGGESTED_REACTIONS`), the groups; arrows move in the grid, Enter in the search picks the first, Escape closes and returns focus. `items` absent: a picker that WRITES an emoji (the team chat composer's): plain buttons, none pressed. `label` (its name, default "Pick a reaction"), `className`, `initialFocus` (`"search"`, default, or `"emoji"`: under a finger a focused search brings the keyboard up over it) |
1681
1708
  | `reactions.css` (its own entry, NOT inside `gemboss-ui.css`: link it where reactions are drawn, as `collections.css`) | `ui-reactions`, `ui-reactions__row`, `ui-reaction` (+ `is-mine`), `__emoji`, `__names`, `ui-reaction-add`, `ui-reaction-picker` (`__search`, `__body`, `__head`, `__grid`, `__btn`, `__empty`) |
1682
1709
 
1683
1710
  Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, transparent padding carries the rest, so a
@@ -1702,11 +1729,11 @@ token, so `data-theme="dark"` turns it dark unchanged. It holds no rule on `:roo
1702
1729
  | Component | Draws | Takes |
1703
1730
  |---|---|---|
1704
1731
  | `TeamChat` | the screen: `div.gb-tc[data-view][data-panel]` = rail · `section.gb-tc-main` · scrim · panel | `rail`, `children`, `panel`, `panelKind` (`"thread"` / `"people"`), `panelOpen`, `onClosePanel`, `view` (`"list"` / `"conversation"`) |
1705
- | `ChannelRail`, `ChannelSection`, `ChannelItem` | `nav.gb-tc-rail`: head (title + actions), groups ("Channels", "Direct messages"), rows with a kind glyph (# · lock · person), `@n` and unread badges | `title`, `actions`; `label`; `name`, `kind`, `unread`, `mentions`, `active` (`aria-current`), `online` (a DM), `href` + `linkAs` or `onOpen` |
1706
- | `ChatHeader` | `header.gb-tc-head`: back (phone only), glyph + name, topic, actions, the members toggle | `name`, `kind`, `topic`, `onBack`, `members: { count, open, onToggle, controls }`, `actions` |
1732
+ | `ChannelRail`, `ChannelSection`, `ChannelItem` | `nav.gb-tc-rail`: head (title + actions), groups ("Channels", "Direct messages"), rows with a kind glyph (# · lock · person) or a DM's picture, `@n` and unread badges | `title`, `actions`; `label`; `name`, `kind`, `unread`, `mentions`, `active` (`aria-current`), `online` (a DM), `avatar: { src, name }` (a DM), `href` + `linkAs` or `onOpen` |
1733
+ | `ChatHeader` | `header.gb-tc-head`: back (phone only), glyph (a DM: the person's picture) + name, topic, actions, the members toggle | `name`, `kind`, `topic`, `avatar: { src, name }` (a DM), `onBack`, `members: { count, open, onToggle, controls }`, `actions` |
1707
1734
  | `MessageList`, `DaySeparator`, `NewMessagesLine`, `JumpToLatest` | `div.gb-tc-body` > `div.gb-tc-log[role=log][aria-live=polite]` > `ol.gb-tc-list`; the sticky day pill; the "New messages" line; the round button with a count | `label`, `before` ("Load earlier"), `after` ("Linh is typing…"), `empty`, `jump`, `logRef`, `onScroll`; `label`, `dateTime`; `count`, `visible`, `onJump` |
1708
1735
  | `Message`, `ReadReceipt` | `li.gb-tc-msg`: avatar or (in a turn) the time on hover, quote, name + time, body, "(edited)", receipt, files, link cards, `Reactions`, thread preview, the action bar | `author`, `avatarSrc` / `avatar`, `time`, `dateTime`, `body`, `grouped`, `edited`, `mentionsMe`, `highlight`, `sending`, `failed`, `status`, `badges`, `replyTo`, `attachments`, `extra`, `reactions`, `onReact`, `thread`, `receipt`, `onReply`, `onOpenThread`, `moreActions`, `editor`, `messageId`; `read`, `of`, `label` |
1709
- | `Composer` | `form.gb-tc-composer`: "Replying to…", files, mention picker, one box (paperclip · field · send), "Also send to…", error | `onSend`, `value` / `defaultValue` + `onValueChange`, `placeholder`, `label`, `replyingTo` + `onCancelReply`, `files`, `picker`, `onAttach`, `also`, `error`, `busy`, `disabled`, `canSendEmpty`, `inputRef`, `onKeyDown`, `onPaste` |
1736
+ | `Composer` | `form.gb-tc-composer`: "Replying to…", files, mention picker, one box (paperclip · field · emoji · send), "Also send to…", error | `onSend`, `value` / `defaultValue` + `onValueChange`, `placeholder`, `label`, `replyingTo` + `onCancelReply`, `files`, `picker`, `onAttach`, `emoji` (default `true`), `also`, `error`, `busy`, `disabled`, `canSendEmpty`, `inputRef`, `onKeyDown`, `onPaste` |
1710
1737
  | `ThreadPanel` | `aside.gb-tc-thread`: head + close, notice (`gb-tc-thread__note`, `--error` in the danger colour), the root message, "N replies", the replies flat, a composer | `onClose`, `root`, `replyCount`, `children`, `composer`, `notice`, `noticeTone` (`"error"`), `title`, `subtitle` |
1711
1738
  | `PeoplePanel`, `PeopleSection`, `PersonRow` | `aside.gb-tc-people[data-collapsed]`: head + collapse button, sections "Online — 3", rows with avatar + presence dot, name, a line under it, a role chip | `collapsed`, `onToggleCollapse`, `actions`, `title`; `label`, `count`; `name`, `src` / `avatar`, `subtitle`, `role`, `presence` (`online` / `away` / `offline`), `href` + `linkAs` or `onSelect` |
1712
1739
 
@@ -1720,6 +1747,21 @@ local days unless `timeZone` (an IANA name) is given to `teamChatRows`, `chatDay
1720
1747
  as "2026-10-21", for a `DaySeparator`'s `dateTime`): give it on a screen the server renders first, or the server's zone and
1721
1748
  the browser's draw different day separators (React #418). Format the times with the same zone.
1722
1749
 
1750
+ **The emoji button** (Chris 21/10/2026: "Dùng lại bảng chọn cảm xúc của gói (đã có, chèn vào chỗ con trỏ), cho cả
1751
+ GemCommunity và GemCare vì chung gói"). The composer's box has it between the field and Send, on by default (`emoji={false}`
1752
+ for an app that does not want it), named by `labels.emoji` ("Emoji"), 44px under a finger like the box's other buttons. It
1753
+ opens the reactions' own picker (`ReactionPicker` with no `items`, named `labels.pickEmoji`): search, "Frequently used", the
1754
+ groups. The picker is a layer over the box at its right edge, above the button, `.gb-tc-composer__emoji-pop`; where the box
1755
+ is narrower than the picker's 420px (a phone) it takes the box's whole width, and its list is shorter on a short screen
1756
+ (`min(300px, 40vh)`). A pick goes in at the field's caret, over a selection when there is one (at the end of a field never
1757
+ focused), as typing would: `execCommand("insertText")`, so focus stays in the field with the caret after the emoji, the
1758
+ browser's undo takes it back, and the field fires `input`, so `onValueChange` and any listener of the app's own hear it
1759
+ (where a browser refuses it: `setRangeText` and an `input` event). Nothing is written past `maxLength`. The pick joins the
1760
+ same "Frequently used" a reaction does (`localStorage`, one list per browser). Escape closes it and puts focus back in the
1761
+ field where the caret was (a person who opened it was writing); a press or focus anywhere else closes it; under a finger it
1762
+ opens on the first emoji, not the search, so the keyboard does not come up over it. Under 360px of the screen's width the
1763
+ box's three 44px buttons touch (no gap), so the field keeps 150px at 320.
1764
+
1723
1765
  **The layout.** `.gb-tc` is a CSS grid and a size container: it decides by its OWN width (in an app shell the chat is the
1724
1766
  window minus the shell's rail), with container queries. `TEAM_CHAT_DRAWER_MAX` (1100) and `TEAM_CHAT_PHONE_MAX` (760) are the
1725
1767
  same numbers for JS.
@@ -1825,6 +1867,16 @@ The rail. The active row has `aria-current` (`"page"` on a link, `"true"` on a b
1825
1867
  <span class="gb-tc-badge" aria-hidden="true">1</span>
1826
1868
  </a>
1827
1869
  </li>
1870
+ <li>
1871
+ <a class="gb-tc-channel" href="/chat/dm/mai" aria-label="Mai Pham, direct message">
1872
+ <span class="gb-tc-channel__glyph" aria-hidden="true">
1873
+ <span class="gb-avatar gb-avatar--24 gb-tc-channel__av" data-tone="7" aria-hidden="true">MP</span>
1874
+ <span class="gb-tc-presence gb-tc-presence--offline">
1875
+ </span>
1876
+ </span>
1877
+ <span class="gb-tc-channel__name">Mai Pham</span>
1878
+ </a>
1879
+ </li>
1828
1880
  </ul>
1829
1881
  </div>
1830
1882
  </div>
@@ -1858,6 +1910,29 @@ the people panel.
1858
1910
  </header>
1859
1911
  ```
1860
1912
 
1913
+ A direct message's head: with `avatar` (`kind="dm"`), the person's picture (`Avatar`, 24px; their initials and tone when
1914
+ there is no `src`) stands left of the name in place of the glyph. A DM row in the rail takes the same `avatar`: the picture
1915
+ at the glyph's 20px (`gb-tc-channel__av`), the presence dot on it (Mai Pham above). Without `avatar`, the glyph, as before;
1916
+ on a channel, `avatar` is ignored.
1917
+
1918
+ <!-- team-chat: header-dm -->
1919
+ ```html
1920
+ <header class="gb-tc-head">
1921
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-head__back" aria-label="Back to channels">
1922
+ <svg>…</svg>
1923
+ </button>
1924
+ <div class="gb-tc-head__text">
1925
+ <h2 class="gb-tc-head__title">
1926
+ <span class="gb-avatar gb-avatar--24 gb-tc-head__av" data-tone="4" aria-hidden="true">
1927
+ <img class="gb-avatar-img" src="/avatars/linh.png" alt="" loading="lazy" width="24" height="24"/>
1928
+ </span>
1929
+ <span class="gb-tc-head__name">Linh Tran</span>
1930
+ </h2>
1931
+ <p class="gb-tc-head__sub">Just the two of you</p>
1932
+ </div>
1933
+ </header>
1934
+ ```
1935
+
1861
1936
  The list: a day, a message that starts a turn, the "New messages" line, and a turn of two (the second `gb-tc-msg--grouped`:
1862
1937
  the time in the avatar's column, shown on hover, and the author for screen readers). The jump button is `hidden` while the
1863
1938
  reader is at the bottom.
@@ -1993,6 +2068,9 @@ the app's files and @mention list; Send is `disabled` while there is nothing to
1993
2068
  <label class="gb-tc-sr" for="tc-input">Message</label>
1994
2069
  <textarea id="tc-input" class="gb-tc-composer__input" rows="1" maxlength="4000" placeholder="Message #general">
1995
2070
  </textarea>
2071
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-composer__emoji" aria-label="Emoji" aria-expanded="false">
2072
+ <svg>…</svg>
2073
+ </button>
1996
2074
  <button type="submit" class="ui-btn ui-btn--primary ui-btn--icon gb-tc-composer__send" aria-label="Send" disabled="">
1997
2075
  <svg>…</svg>
1998
2076
  </button>
@@ -2048,6 +2126,9 @@ The thread panel: the root, the "N replies" rule, the replies flat, a composer w
2048
2126
  <label class="gb-tc-sr" for="tc-thread-input">Reply to thread</label>
2049
2127
  <textarea id="tc-thread-input" class="gb-tc-composer__input" rows="1" maxlength="4000" placeholder="Reply…">
2050
2128
  </textarea>
2129
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-composer__emoji" aria-label="Emoji" aria-expanded="false">
2130
+ <svg>…</svg>
2131
+ </button>
2051
2132
  <button type="submit" class="ui-btn ui-btn--primary ui-btn--icon gb-tc-composer__send" aria-label="Send" disabled="">
2052
2133
  <svg>…</svg>
2053
2134
  </button>
@@ -2138,7 +2219,8 @@ during an IME composition (`isComposing`, keyCode 229); the field grows with its
2138
2219
  browser lacks it, set `height` from `scrollHeight` through CSSOM, which a `style-src 'self'` CSP allows); Escape or the scrim
2139
2220
  closes a drawer, focus moves into it and back, the rail and the conversation are `inert` meanwhile; the reaction picker's
2140
2221
  open state (`gb-tc-msg--active`, `aria-expanded` on its button); arrow keys along `div.gb-tc-acts[role=toolbar]`; when to
2141
- show `.gb-tc-jump` and its count.
2222
+ show `.gb-tc-jump` and its count. The composer's emoji button is the exception: `gembossUI.teamChat.emojiButton` (below)
2223
+ builds or wires it and does its behaviour.
2142
2224
 
2143
2225
  ### Plain HTML: `gembossUI.teamChat` (20/10/2026)
2144
2226
 
@@ -2155,9 +2237,10 @@ after `kit-plain.js` when the page has it (it adds `gembossUI.teamChat`; loaded
2155
2237
  | `daySeparator(label, dateTime)`, `newMessagesLine(labels)` | `DaySeparator`, `NewMessagesLine` |
2156
2238
  | `jumpToLatest({ count, visible, onJump, labels })`, `updateJump(button, { count, visible, labels })` | `JumpToLatest`; `updateJump` redraws the same button (a scroll handler keeps its focus) |
2157
2239
  | `readReceipt({ read, of, label })` | `ReadReceipt` (`null` when `of` ≤ 0) |
2158
- | `channelSection({ label, id, items })`, `channelItem(props)` | `ChannelSection`, `ChannelItem` (`href`: a link, else a button); `items` are rows or their props |
2240
+ | `channelSection({ label, id, items })`, `channelItem(props)` | `ChannelSection`, `ChannelItem` (`href`: a link, else a button; a DM's `avatar`); `items` are rows or their props |
2159
2241
  | `peopleSection({ label, count, id, items })`, `personRow(props)` | `PeopleSection`, `PersonRow` (a link, a button with `onSelect`, else a `div`) |
2160
- | `reactions(props)`, `reactionPicker(props)` | `Reactions` (plus `open` / `onOpenChange`), `ReactionPicker` |
2242
+ | `reactions(props)`, `reactionPicker(props)` | `Reactions` (plus `open` / `onOpenChange`), `ReactionPicker` (`items` absent: the composer's, which writes an emoji; `label`, `className`) |
2243
+ | `emojiButton({ textarea, button, host, labels })` | the `Composer`'s emoji button and picker, for a composer the page writes itself (below) |
2161
2244
  | `avatar({ name, src, size, className })`, `icon(name, size)` | `Avatar` (portal.tsx's initials and tone), `Icon` (the icons the builders draw, and `more`) |
2162
2245
  | `rows(messages, opts)`, `dayKey(at, timeZone)`, `dayISO(at, timeZone)`, `dayWord(at, now, timeZone)` | `teamChatRows`, `chatDayKey`, `chatDayISO`, `chatDayWord` |
2163
2246
  | `LABELS` | `TEAM_CHAT_LABELS`; `labels` (any subset) translates, functions included |
@@ -2191,6 +2274,28 @@ with `picking` takes no focus: after a redraw the page puts focus back, as it do
2191
2274
  Not built here: the screen's shell (`TeamChat`, `ChannelRail`, `ChatHeader`, `MessageList`, `Composer`, `ThreadPanel`,
2192
2275
  `PeoplePanel`) is markup a page writes once (the blocks above), with the behaviour listed just above.
2193
2276
 
2277
+ **The composer's emoji button, `emojiButton` (21/10/2026).** The one piece of the composer with behaviour of its own, so it
2278
+ is a helper rather than markup: it wires a field to the `Composer`'s emoji button and picker and does what the component
2279
+ does (above: the reactions' picker over the box, a pick at the caret as typing would, the field's `input` event, "Frequently
2280
+ used", Escape back to the field, closed by a press or focus elsewhere, the first emoji under a finger). Without `button` it
2281
+ builds the component's button and returns it; with one, it wires that button (giving it `aria-expanded` / `aria-controls`,
2282
+ and `labels.emoji` when it has no name). The picker goes into `host`: by default the field's `.gb-tc-composer__box`
2283
+ (`team-chat.css` makes the box its containing block), else the field's parent; another host needs `position: relative`.
2284
+ `team-chat-plain-browser.test.ts` holds the button and the picker to the component's, step by step.
2285
+
2286
+ ```js
2287
+ // GemCare's channel composer (inbox.html #chat-form) and the thread's (#ctp-form): the button before Send.
2288
+ for (const [input, send] of [["chat-input", "chat-send"], ["ctp-input", "ctp-send"]]) {
2289
+ document.getElementById(send).before(gembossUI.teamChat.emojiButton({ textarea: document.getElementById(input) }));
2290
+ }
2291
+ // A composer of other markup (GemCare's dock window): its own button, and its form as the host (`position: relative`).
2292
+ gembossUI.teamChat.emojiButton({ textarea: input, button: myEmojiButton, host: form });
2293
+ ```
2294
+
2295
+ GemCare draws its composers itself (inbox.html, the contract's composer markup by hand, wired by inbox.js; the dock window's
2296
+ built in inbox.js with its own classes), so it adopts this with the lines above after `npm run ui:sync`: its `input`
2297
+ listeners (Send turned on, the @mention picker, the draft) hear a pick as they hear typing.
2298
+
2194
2299
  The script carries two kinds of copy, both held by `team-chat-plain-data.test.ts`: the icons' drawing (icons.tsx) and the
2195
2300
  emoji set (reactions-data.ts) as a generated block (`UPDATE_PLAIN_TEAM_CHAT=1` on that test writes it), and the labels, the
2196
2301
  avatar's initials and tone, the reaction helpers and `teamChatRows` in ES5, called with the TypeScript's inputs and held to
@@ -2217,6 +2322,29 @@ applies; afterwards the outcome replaces the body, with Undo when the surface ca
2217
2322
  Presentational: no fetching and no state of its own. The surface loads the preview, owns the handlers, and decides
2218
2323
  what Undo puts back (GemFeedback: the merge record, or the values an action replaced, guarded by `if=` so Undo never
2219
2324
  overwrites a later edit). Catalog section "Thẻ hành động".
2325
+
2326
+ ## The assistant asks, a person only confirms (0.43.0)
2327
+
2328
+ Chris 21/10/2026, reviewing the Market redesign: a screen with a form to fill in is the wrong screen; the assistant should ask one
2329
+ clear question, have the answer filled in already, offer at most three choices, and let the person confirm. Five pieces, each its own
2330
+ entry, each presentational and server-renderable (no state, no effect):
2331
+
2332
+ | Piece | What it draws |
2333
+ | --- | --- |
2334
+ | `QuestionCard` | one question (`question`, `eyebrow`, `evidence` chips, `art`), **one to three** `choices` (`{ id, label, note, pick }`: the first is the assistant's best guess, `note` says where it came from), one primary `onConfirm`, and quiet `onOther` / `onSkip` when the surface handles them. Controlled: `value` + `onChange` move the selection, only the confirm commits. The type refuses a fourth choice (`QuestionChoices`), and a longer list that gets past it is cut to three at render. Real radio inputs, so a keyboard gets a radio group. |
2335
+ | `BriefingCard` | one proposal at the top of a screen: a `eyebrow` tag, the sentence (`title`), `lead` with the numbers, `evidence` chips, `actions` (one to three buttons) and `art`. An iris wash, no border: the one loud thing on the screen. Not `ActionCard`: that lists the changes of one apply; a briefing is a proposal that has not become a change yet. |
2336
+ | `RangeBar` | a value against a market's spread on one axis: `band` (where half of it sits), `mid`, `you`, `ticks`. `size="sm"` is the shape for a metric tile; `size="lg"` adds the labels. The surface passes the words and the numbers (`labels`, `format`): the bar does no money maths. `rangeBarGeometry` is the pure part, for a plain-HTML page. |
2337
+ | `ProgressRing` | "3 of 5" as a ring with the count inside. `progressRingGeometry` is the pure arc. A `max` that is not a positive number is an empty ring, never NaN. |
2338
+ | `SpotArt` | six pictures in the brand's gem drawing style (`gem-radar`, `orbit`, `people`, `page`, `plug`, `price`) for the pauses and empty states a text-only screen lacks. Painted by tokens, decorative by default, an image only when given a `title`. |
2339
+ | `question-card.css`, `briefing-card.css`, `range-bar.css`, `progress-ring.css`, `spot-art.css` | one entry each, loaded by its component; a plain-HTML surface links `@gemboss/ui/<name>.css` and writes the classes listed at the top of each file |
2340
+
2341
+ Three lighter steps of iris came with them, because a wash, a band and an art stroke need more than `--gemboss-accent-soft`:
2342
+ `--gemboss-accent-wash` (`#f7f8ff`), `--gemboss-accent-tint` (`#dfe2ff`), `--gemboss-accent-mid` (`#c9cffd`), each with a dark value
2343
+ in `theme-dark.css`. They are fills, never text.
2344
+
2345
+ The contract every surface keeps: nothing is saved by choosing, only by the confirm, and what the confirm did is shown with Undo
2346
+ after (`ActionCard`'s outcome, or the surface's own). The first choice is the assistant's best guess and says where it came from;
2347
+ a question with no guess to offer is asked in the assistant's chat, not here.
2220
2348
  ## Report pages (0.4.0)
2221
2349
 
2222
2350
  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
@@ -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.42.0 · Editor</h1>
11
+ <h1>@gemboss/ui 0.44.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;;