@gemboss/ui 0.41.0 → 0.43.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,116 @@
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.43.0 (21/10/2026)
6
+
7
+ **The full-screen assistant stops paying for a blur nobody sees** (21/10/2026, measured in the GemBoss desktop app).
8
+
9
+ Changed
10
+ - `.gb-dock-full` sets `backdrop-filter: none` (and `-webkit-`), like `.gb-dock-side`. Its background is opaque, so the frost
11
+ never showed, but the GPU painted the page underneath and blurred the whole rectangle every frame. GPU process on
12
+ /app/assistant (1920×1055, 2×): 516 → 430 MB at the scroll peak, 270 → 241 MB at rest; drawn layers 7 → 5.
13
+ - Side effect, a fix: the frame is no longer the containing block of `position: fixed` children in full mode, so the thread
14
+ row's ⋯ menu opens under its button instead of 240px right and 56px down of it (and clipped). Popup and mini still frost.
15
+ - `opaque-dock-modes-skip-the-blur.test.ts` reads the modes from `MODE_CLASS`: any mode painted with an opaque token must turn
16
+ the blur off, and those tokens must be opaque in both themes.
17
+
18
+ **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
19
+ 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").
20
+
21
+ Changed
22
+ - `.gemboss-editor-menu-item` (the ⋯ menu, the canvas's right-click menu) under a mouse: a Polaris ActionList item, 32px tall
23
+ 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
24
+ screen 767px or narrower it keeps the 44px row. Measured on the admin's editor ⋯ menu: eight rows 366px → 270px.
25
+ `editor-plain.test.ts` holds both in Chromium; the browser gates that asked 44px of a menu row under a mouse
26
+ (check-generate-images-touchable, page-design-review-fixture) ask the kit's row now (44 under a finger or below 768, 32 above).
27
+
28
+ **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ó,
29
+ chèn vào chỗ con trỏ), cho cả GemCommunity và GemCare vì chung gói").
30
+
31
+ Added
32
+ - `Composer`: an emoji button between the field and Send, on by default; `emoji={false}` turns it off. It opens the reactions'
33
+ 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
34
+ caret, over a selection, as typing would (`execCommand("insertText")`, else `setRangeText` + `input`): focus stays in the
35
+ field after the emoji, undo takes it back, `onValueChange` and the app's own `input` listeners hear it, nothing past
36
+ `maxLength`. It joins the same "Frequently used" as a reaction. Escape closes it and puts focus back in the field; a press
37
+ or focus elsewhere closes it; under a finger it opens on the first emoji (no keyboard over it).
38
+ - `TEAM_CHAT_LABELS.emoji` ("Emoji", the button) and `pickEmoji` ("Pick an emoji", the picker's name).
39
+ - `ReactionPicker`: `items` is optional (absent: a picker that writes, plain buttons, none pressed), `label`, `className`,
40
+ `initialFocus` (`"search"` | `"emoji"`); type `ReactionPickerProps`. With `items` it draws what it drew.
41
+ - Icon `emoji` (a face; `reaction` is the face with a plus).
42
+ - Plain script: `gembossUI.teamChat.emojiButton({ textarea, button, host, labels })` wires any field (and its button, or one
43
+ it builds and returns) to the same button and picker, for a composer the page writes itself (GemCare); `reactionPicker`
44
+ takes no `items`, `label`, `className`. Held to the component by `team-chat-plain-browser.test.ts`.
45
+
46
+ Changed
47
+ - `team-chat.css`: `.gb-tc-composer__box` is `position: relative` (the picker's containing block); under 360px of the screen's
48
+ width the box's buttons have no gap, so the field keeps 150px beside three 44px buttons at 320. README "The team chat
49
+ layer": the composer and thread blocks carry the button.
50
+
51
+ **A direct message shows the person's picture** (GemCommunity #322 draws DMs with `ChannelItem kind="dm"` and `ChatHeader`,
52
+ and had only the generic person glyph to show).
53
+
54
+ Added
55
+ - `ChannelItem`: `avatar: { src, name }` on a DM, the kit's `Avatar` at the glyph's 20px (`gb-tc-channel__av`; initials and
56
+ tone when there is no `src`, `name` defaulting to the row's) with the presence dot on it.
57
+ - `ChatHeader`: `avatar: { src, name }` with `kind="dm"`, the `Avatar` at 24px left of the name in place of the glyph
58
+ (`gb-tc-head__av`). README block `header-dm`, held by `team-chat-contract.test.tsx`; type `DmAvatar`.
59
+ - Plain script: `channelItem` takes the same `avatar`, held to the component by `team-chat-plain-browser.test.ts`.
60
+ - Without `avatar`, and on a channel, the glyph as before.
61
+
62
+ **The assistant asks one question and a person only confirms: five pieces and three lighter steps of iris** (Chris 21/10/2026,
63
+ 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
64
+ đế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").
65
+
66
+ Added
67
+ - `QuestionCard`: one question, one to three choices (the type refuses a fourth; a longer list is cut to three), the first the
68
+ assistant's best guess, one primary confirm, quiet "type my own" and "skip". Controlled, real radio inputs. `MAX_QUESTION_CHOICES`.
69
+ - `BriefingCard`: one proposal at the top of a screen, with its evidence and one to three buttons. An iris wash.
70
+ - `RangeBar` (+ `rangeBarGeometry`): a value against a market's spread, `sm` for a tile and `lg` with labels. A value off the axis is
71
+ pinned to its edge, a band given high-to-low is turned round, a non-number is left out.
72
+ - `ProgressRing` (+ `progressRingGeometry`, `RING_BOX`): "3 of 5" as a ring. A `max` that is not a positive number is an empty ring.
73
+ - `SpotArt` (+ `SPOT_ART_NAMES`): six brand pictures (`gem-radar`, `orbit`, `people`, `page`, `plug`, `price`), painted by tokens.
74
+ - Tokens `--gemboss-accent-wash`, `--gemboss-accent-tint`, `--gemboss-accent-mid`, light and dark (`theme-dark.css`). Fills, never text.
75
+ - Five entries, none in the frozen bundle: `question-card.css`, `briefing-card.css`, `range-bar.css`, `progress-ring.css`,
76
+ `spot-art.css` (`parts.mjs`, `package.json` exports, `weight-budget.json`).
77
+
78
+ 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
79
+ 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.
80
+
81
+ Nothing existing changes: no class, no token and no component that was there is touched.
82
+
83
+ ## 0.42.0 (21/10/2026)
84
+
85
+ **The editor's colour field offers the page's theme colours and this store's recent colours** (gemboss #4888, Chris "bấm
86
+ trước gõ sau").
87
+
88
+ Added
89
+ - `ColorControl` takes `choices` (`ColorChoice { value, label, swatch }`): chips under the field while it is in use; a chip
90
+ writes its `value` (a theme chip writes `var(--gem-color-<role>)`). `themeColorChoices(palette)` lists the theme's roles in
91
+ `TOKEN_NAMES` order. No new CSS: `.gemboss-ei-swatches` / `.gemboss-ei-sw`.
92
+
93
+ **Team chat days in a given time zone; leaderboard words in the page's language** (follow-ups of GemCommunity's UX audit,
94
+ 20–21/10/2026: /chat threw React #418 because the server (UTC) and a browser in Hanoi drew different day separators, and the
95
+ app had to feed the rules shifted dates; the podium's screen-reader words were English on vi and zh pages).
96
+
97
+ Added
98
+ - `timeZone` (IANA name) on `teamChatRows` options and as a last argument of `chatDayKey` and `chatDayWord`: days are that
99
+ zone's calendar days. An unknown name or an invalid date falls back to the local day, as without it.
100
+ - `chatDayISO(at, timeZone?)`: the day as "2026-10-21", for a `DaySeparator`'s `dateTime`.
101
+ - Plain script: `gembossUI.teamChat.rows` reads `opts.timeZone`; `dayKey`, `dayWord` take it last; `dayISO` is new. Held to
102
+ the TypeScript by `team-chat-plain-data.test.ts` under four zones (one unknown).
103
+ - `Podium`: `label` (the list's name, default "Top three") and `rankLabel` (default `(n) => "Rank " + n`); `Leaderboard`:
104
+ `rankLabel`. Defaults keep the markup as it was.
105
+
106
+ **"Frequently used" in the reaction picker always holds nine** (Chris 20/10/2026: GemCare's row held nine; the package's showed
107
+ only what this browser had used, one or two for a new person, which read as empty).
108
+
109
+ Changed
110
+ - `ReactionPicker` (React) and `gembossUI.teamChat.reactionPicker` (plain): "Frequently used" is what this browser used, most
111
+ recent first, then the `SUGGESTED_REACTIONS` it has not used (👍 ✅ 🙏 🎉 😂 ❤️ 👀 🙌 🔥), nine in all.
112
+ - `reactions-data.ts` exports `SUGGESTED_REACTIONS`, `FREQUENT_MAX` and `frequentReactions(recent)`. The plain script carries
113
+ the list in its data block and the same function in ES5, held to the TypeScript by `team-chat-plain-data.test.ts`.
114
+
5
115
  ## 0.41.0 (20/10/2026)
6
116
 
7
117
  **A document read over its list** (Chris 20/10/2026, after GemMedia's and partner-radar's Docs drew the same popup with
@@ -17,6 +127,33 @@ Added
17
127
  - `reader-dialog.test.tsx` (markup, every class styled, selectors), `reader-dialog-browser.test.tsx` (Chromium: size,
18
128
  fill, scroll, phone head, page lock, Escape; negative control without the entry finds the 460px popup).
19
129
 
130
+ **A colour field reads every colour, not only a 6-digit hex** (A4, 21/10/2026). Measured on 169 element pages: of 3,283 colour
131
+ values, 66% are `rgb()`, 17% `rgba()`, 9% a 6-digit hex, ~3% `oklch()`. Only the hex 9% had a swatch: nine colours in ten showed a
132
+ locked black square.
133
+
134
+ Changed
135
+ - `swatchState` shows any one colour (`parseColor`: hex of 3, 4, 6 or 8 digits, `rgb`/`rgba`, `hsl`/`hsla`, `oklch`). A token, a
136
+ gradient, a scheme id or a padded value is still not a colour the swatch can stand for: off, with the reason.
137
+ - `ColorControl`: a pick on the swatch keeps the value's transparency and way of writing (`pickSwatch`: a hex stays a hex, else
138
+ `rgb()`, `rgba()` with an alpha), so turning the swatch on can never drop an alpha. A colour's opacity sits beside it in a `%`
139
+ box (`input[data-alpha]`, `withOpacity`); a value that is not one colour has none. No CSS change (the box's width is inline).
140
+ - `swatchWhy`: "Type a color to use your own"; the kept-as-written reason names what the swatch reads.
141
+ - `editor-plain.js`: the same swatch and `%` box on a page without React, for a hex and `rgb()`/`rgba()` (92% of the colours
142
+ measured); `hsl()` and `oklch()` stay a locked swatch there (named in editor-panel.test.tsx). Exposes `gembossUI.colorOf`,
143
+ `pickSwatch`, `withOpacity`.
144
+
145
+ Added
146
+ - `parseColor`, `pickSwatch`, `opacityOf`, `withOpacity`, type `Rgba` (from `@gemboss/ui/editor`).
147
+
148
+ **A colour field offers colours to click before one to type** (A4b, 21/10/2026; Chris: "bấm trước gõ sau").
149
+
150
+ Added
151
+ - `ColorControl` `choices`: chips under the field while it is in use (focus inside it), each a `ColorChoice` `{ value, label,
152
+ swatch }`: the value written on a click, its name, the colour it paints. A token value painted by a chip shows that colour on the
153
+ swatch, and a pick on the swatch then starts from it. None drawn in SSR or when `choices` is empty.
154
+ - `themeColorChoices(palette)`: the theme's colour roles as choices (`var(--gem-color-<role>)`, in `TOKEN_NAMES` order), painted
155
+ with the colour the page draws for each; a role the palette does not paint is left out. Type `ColorChoice`.
156
+
20
157
  **The team chat's rows for a plain page, from the package: `gembossUI.teamChat`** (Chris 20/10/2026: the markup in ONE place,
21
158
  so GemCare stops writing it by hand and a change to it is made once).
22
159
 
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).
@@ -1589,7 +1594,8 @@ import { Avatar, Dialog, Leaderboard, Podium, SearchDialog, ThemeToggle, themeSc
1589
1594
  Opt-in: a surface without it (the admin) stays light.
1590
1595
  - `Avatar` (24 · 32 · 40 · 56): picture or initials on one of eight tones picked from the name.
1591
1596
  - `Leaderboard`: 🥇 🥈 🥉 then the rank, avatar, name, count; the leader's count in gold; nothing at
1592
- all when empty. `Podium`: the first three, second · first · third.
1597
+ all when empty. `Podium`: the first three, second · first · third. Screen readers hear "Rank 1: " before each
1598
+ name and "Top three" for the podium; pass `rankLabel={(n) => …}` (both) and `label` (Podium) in the page's language.
1593
1599
  - `initialsOf` moved to `initials.ts` (plain, not "use client") so server components can call it; it is
1594
1600
  still exported from the package and from account.tsx.
1595
1601
  - **Without the whole kit (0.7.2):** a surface with its own skin (GemCommunity) takes only these pieces'
@@ -1676,7 +1682,7 @@ and GemCare's chat, draw and accept the same thing.
1676
1682
  |---|---|
1677
1683
  | `@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 }` |
1678
1684
  | `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 |
1679
- | `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 |
1685
+ | `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) |
1680
1686
  | `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`) |
1681
1687
 
1682
1688
  Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, transparent padding carries the rest, so a
@@ -1701,11 +1707,11 @@ token, so `data-theme="dark"` turns it dark unchanged. It holds no rule on `:roo
1701
1707
  | Component | Draws | Takes |
1702
1708
  |---|---|---|
1703
1709
  | `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"`) |
1704
- | `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` |
1705
- | `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` |
1710
+ | `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` |
1711
+ | `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` |
1706
1712
  | `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` |
1707
1713
  | `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` |
1708
- | `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` |
1714
+ | `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` |
1709
1715
  | `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` |
1710
1716
  | `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` |
1711
1717
 
@@ -1714,7 +1720,25 @@ own state (the reaction picker a message opened, the composer's text when the ap
1714
1720
  write themselves is in `TEAM_CHAT_LABELS`; pass `labels` (any subset) to translate. `teamChatRows(messages, { meId,
1715
1721
  isUnread })` (plain TS, `team-chat-data.ts`) says where the day separators and the "New messages" line go and which
1716
1722
  messages are `grouped`: same author, same day, under 5 minutes apart, not a reply, and not the first new one (GemCare's
1717
- rules). `chatDayWord` gives "Today" / "Yesterday"; older days the app formats in its own locale.
1723
+ rules). `chatDayWord` gives "Today" / "Yesterday"; older days the app formats in its own locale. Days are the viewer's
1724
+ local days unless `timeZone` (an IANA name) is given to `teamChatRows`, `chatDayKey`, `chatDayWord` and `chatDayISO` (the day
1725
+ as "2026-10-21", for a `DaySeparator`'s `dateTime`): give it on a screen the server renders first, or the server's zone and
1726
+ the browser's draw different day separators (React #418). Format the times with the same zone.
1727
+
1728
+ **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ả
1729
+ GemCommunity và GemCare vì chung gói"). The composer's box has it between the field and Send, on by default (`emoji={false}`
1730
+ for an app that does not want it), named by `labels.emoji` ("Emoji"), 44px under a finger like the box's other buttons. It
1731
+ opens the reactions' own picker (`ReactionPicker` with no `items`, named `labels.pickEmoji`): search, "Frequently used", the
1732
+ groups. The picker is a layer over the box at its right edge, above the button, `.gb-tc-composer__emoji-pop`; where the box
1733
+ 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
1734
+ (`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
1735
+ focused), as typing would: `execCommand("insertText")`, so focus stays in the field with the caret after the emoji, the
1736
+ browser's undo takes it back, and the field fires `input`, so `onValueChange` and any listener of the app's own hear it
1737
+ (where a browser refuses it: `setRangeText` and an `input` event). Nothing is written past `maxLength`. The pick joins the
1738
+ same "Frequently used" a reaction does (`localStorage`, one list per browser). Escape closes it and puts focus back in the
1739
+ field where the caret was (a person who opened it was writing); a press or focus anywhere else closes it; under a finger it
1740
+ 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
1741
+ box's three 44px buttons touch (no gap), so the field keeps 150px at 320.
1718
1742
 
1719
1743
  **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
1720
1744
  window minus the shell's rail), with container queries. `TEAM_CHAT_DRAWER_MAX` (1100) and `TEAM_CHAT_PHONE_MAX` (760) are the
@@ -1821,6 +1845,16 @@ The rail. The active row has `aria-current` (`"page"` on a link, `"true"` on a b
1821
1845
  <span class="gb-tc-badge" aria-hidden="true">1</span>
1822
1846
  </a>
1823
1847
  </li>
1848
+ <li>
1849
+ <a class="gb-tc-channel" href="/chat/dm/mai" aria-label="Mai Pham, direct message">
1850
+ <span class="gb-tc-channel__glyph" aria-hidden="true">
1851
+ <span class="gb-avatar gb-avatar--24 gb-tc-channel__av" data-tone="7" aria-hidden="true">MP</span>
1852
+ <span class="gb-tc-presence gb-tc-presence--offline">
1853
+ </span>
1854
+ </span>
1855
+ <span class="gb-tc-channel__name">Mai Pham</span>
1856
+ </a>
1857
+ </li>
1824
1858
  </ul>
1825
1859
  </div>
1826
1860
  </div>
@@ -1854,6 +1888,29 @@ the people panel.
1854
1888
  </header>
1855
1889
  ```
1856
1890
 
1891
+ A direct message's head: with `avatar` (`kind="dm"`), the person's picture (`Avatar`, 24px; their initials and tone when
1892
+ 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
1893
+ at the glyph's 20px (`gb-tc-channel__av`), the presence dot on it (Mai Pham above). Without `avatar`, the glyph, as before;
1894
+ on a channel, `avatar` is ignored.
1895
+
1896
+ <!-- team-chat: header-dm -->
1897
+ ```html
1898
+ <header class="gb-tc-head">
1899
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-head__back" aria-label="Back to channels">
1900
+ <svg>…</svg>
1901
+ </button>
1902
+ <div class="gb-tc-head__text">
1903
+ <h2 class="gb-tc-head__title">
1904
+ <span class="gb-avatar gb-avatar--24 gb-tc-head__av" data-tone="4" aria-hidden="true">
1905
+ <img class="gb-avatar-img" src="/avatars/linh.png" alt="" loading="lazy" width="24" height="24"/>
1906
+ </span>
1907
+ <span class="gb-tc-head__name">Linh Tran</span>
1908
+ </h2>
1909
+ <p class="gb-tc-head__sub">Just the two of you</p>
1910
+ </div>
1911
+ </header>
1912
+ ```
1913
+
1857
1914
  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`:
1858
1915
  the time in the avatar's column, shown on hover, and the author for screen readers). The jump button is `hidden` while the
1859
1916
  reader is at the bottom.
@@ -1989,6 +2046,9 @@ the app's files and @mention list; Send is `disabled` while there is nothing to
1989
2046
  <label class="gb-tc-sr" for="tc-input">Message</label>
1990
2047
  <textarea id="tc-input" class="gb-tc-composer__input" rows="1" maxlength="4000" placeholder="Message #general">
1991
2048
  </textarea>
2049
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-composer__emoji" aria-label="Emoji" aria-expanded="false">
2050
+ <svg>…</svg>
2051
+ </button>
1992
2052
  <button type="submit" class="ui-btn ui-btn--primary ui-btn--icon gb-tc-composer__send" aria-label="Send" disabled="">
1993
2053
  <svg>…</svg>
1994
2054
  </button>
@@ -2044,6 +2104,9 @@ The thread panel: the root, the "N replies" rule, the replies flat, a composer w
2044
2104
  <label class="gb-tc-sr" for="tc-thread-input">Reply to thread</label>
2045
2105
  <textarea id="tc-thread-input" class="gb-tc-composer__input" rows="1" maxlength="4000" placeholder="Reply…">
2046
2106
  </textarea>
2107
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-composer__emoji" aria-label="Emoji" aria-expanded="false">
2108
+ <svg>…</svg>
2109
+ </button>
2047
2110
  <button type="submit" class="ui-btn ui-btn--primary ui-btn--icon gb-tc-composer__send" aria-label="Send" disabled="">
2048
2111
  <svg>…</svg>
2049
2112
  </button>
@@ -2134,7 +2197,8 @@ during an IME composition (`isComposing`, keyCode 229); the field grows with its
2134
2197
  browser lacks it, set `height` from `scrollHeight` through CSSOM, which a `style-src 'self'` CSP allows); Escape or the scrim
2135
2198
  closes a drawer, focus moves into it and back, the rail and the conversation are `inert` meanwhile; the reaction picker's
2136
2199
  open state (`gb-tc-msg--active`, `aria-expanded` on its button); arrow keys along `div.gb-tc-acts[role=toolbar]`; when to
2137
- show `.gb-tc-jump` and its count.
2200
+ show `.gb-tc-jump` and its count. The composer's emoji button is the exception: `gembossUI.teamChat.emojiButton` (below)
2201
+ builds or wires it and does its behaviour.
2138
2202
 
2139
2203
  ### Plain HTML: `gembossUI.teamChat` (20/10/2026)
2140
2204
 
@@ -2151,11 +2215,12 @@ after `kit-plain.js` when the page has it (it adds `gembossUI.teamChat`; loaded
2151
2215
  | `daySeparator(label, dateTime)`, `newMessagesLine(labels)` | `DaySeparator`, `NewMessagesLine` |
2152
2216
  | `jumpToLatest({ count, visible, onJump, labels })`, `updateJump(button, { count, visible, labels })` | `JumpToLatest`; `updateJump` redraws the same button (a scroll handler keeps its focus) |
2153
2217
  | `readReceipt({ read, of, label })` | `ReadReceipt` (`null` when `of` ≤ 0) |
2154
- | `channelSection({ label, id, items })`, `channelItem(props)` | `ChannelSection`, `ChannelItem` (`href`: a link, else a button); `items` are rows or their props |
2218
+ | `channelSection({ label, id, items })`, `channelItem(props)` | `ChannelSection`, `ChannelItem` (`href`: a link, else a button; a DM's `avatar`); `items` are rows or their props |
2155
2219
  | `peopleSection({ label, count, id, items })`, `personRow(props)` | `PeopleSection`, `PersonRow` (a link, a button with `onSelect`, else a `div`) |
2156
- | `reactions(props)`, `reactionPicker(props)` | `Reactions` (plus `open` / `onOpenChange`), `ReactionPicker` |
2220
+ | `reactions(props)`, `reactionPicker(props)` | `Reactions` (plus `open` / `onOpenChange`), `ReactionPicker` (`items` absent: the composer's, which writes an emoji; `label`, `className`) |
2221
+ | `emojiButton({ textarea, button, host, labels })` | the `Composer`'s emoji button and picker, for a composer the page writes itself (below) |
2157
2222
  | `avatar({ name, src, size, className })`, `icon(name, size)` | `Avatar` (portal.tsx's initials and tone), `Icon` (the icons the builders draw, and `more`) |
2158
- | `rows(messages, opts)`, `dayKey(at)`, `dayWord(at, now)` | `teamChatRows`, `chatDayKey`, `chatDayWord` |
2223
+ | `rows(messages, opts)`, `dayKey(at, timeZone)`, `dayISO(at, timeZone)`, `dayWord(at, now, timeZone)` | `teamChatRows`, `chatDayKey`, `chatDayISO`, `chatDayWord` |
2159
2224
  | `LABELS` | `TEAM_CHAT_LABELS`; `labels` (any subset) translates, functions included |
2160
2225
 
2161
2226
  ```html
@@ -2187,6 +2252,28 @@ with `picking` takes no focus: after a redraw the page puts focus back, as it do
2187
2252
  Not built here: the screen's shell (`TeamChat`, `ChannelRail`, `ChatHeader`, `MessageList`, `Composer`, `ThreadPanel`,
2188
2253
  `PeoplePanel`) is markup a page writes once (the blocks above), with the behaviour listed just above.
2189
2254
 
2255
+ **The composer's emoji button, `emojiButton` (21/10/2026).** The one piece of the composer with behaviour of its own, so it
2256
+ is a helper rather than markup: it wires a field to the `Composer`'s emoji button and picker and does what the component
2257
+ does (above: the reactions' picker over the box, a pick at the caret as typing would, the field's `input` event, "Frequently
2258
+ used", Escape back to the field, closed by a press or focus elsewhere, the first emoji under a finger). Without `button` it
2259
+ builds the component's button and returns it; with one, it wires that button (giving it `aria-expanded` / `aria-controls`,
2260
+ and `labels.emoji` when it has no name). The picker goes into `host`: by default the field's `.gb-tc-composer__box`
2261
+ (`team-chat.css` makes the box its containing block), else the field's parent; another host needs `position: relative`.
2262
+ `team-chat-plain-browser.test.ts` holds the button and the picker to the component's, step by step.
2263
+
2264
+ ```js
2265
+ // GemCare's channel composer (inbox.html #chat-form) and the thread's (#ctp-form): the button before Send.
2266
+ for (const [input, send] of [["chat-input", "chat-send"], ["ctp-input", "ctp-send"]]) {
2267
+ document.getElementById(send).before(gembossUI.teamChat.emojiButton({ textarea: document.getElementById(input) }));
2268
+ }
2269
+ // A composer of other markup (GemCare's dock window): its own button, and its form as the host (`position: relative`).
2270
+ gembossUI.teamChat.emojiButton({ textarea: input, button: myEmojiButton, host: form });
2271
+ ```
2272
+
2273
+ GemCare draws its composers itself (inbox.html, the contract's composer markup by hand, wired by inbox.js; the dock window's
2274
+ built in inbox.js with its own classes), so it adopts this with the lines above after `npm run ui:sync`: its `input`
2275
+ listeners (Send turned on, the @mention picker, the draft) hear a pick as they hear typing.
2276
+
2190
2277
  The script carries two kinds of copy, both held by `team-chat-plain-data.test.ts`: the icons' drawing (icons.tsx) and the
2191
2278
  emoji set (reactions-data.ts) as a generated block (`UPDATE_PLAIN_TEAM_CHAT=1` on that test writes it), and the labels, the
2192
2279
  avatar's initials and tone, the reaction helpers and `teamChatRows` in ES5, called with the TypeScript's inputs and held to
@@ -2213,6 +2300,29 @@ applies; afterwards the outcome replaces the body, with Undo when the surface ca
2213
2300
  Presentational: no fetching and no state of its own. The surface loads the preview, owns the handlers, and decides
2214
2301
  what Undo puts back (GemFeedback: the merge record, or the values an action replaced, guarded by `if=` so Undo never
2215
2302
  overwrites a later edit). Catalog section "Thẻ hành động".
2303
+
2304
+ ## The assistant asks, a person only confirms (0.43.0)
2305
+
2306
+ 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
2307
+ clear question, have the answer filled in already, offer at most three choices, and let the person confirm. Five pieces, each its own
2308
+ entry, each presentational and server-renderable (no state, no effect):
2309
+
2310
+ | Piece | What it draws |
2311
+ | --- | --- |
2312
+ | `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. |
2313
+ | `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. |
2314
+ | `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. |
2315
+ | `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. |
2316
+ | `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`. |
2317
+ | `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 |
2318
+
2319
+ Three lighter steps of iris came with them, because a wash, a band and an art stroke need more than `--gemboss-accent-soft`:
2320
+ `--gemboss-accent-wash` (`#f7f8ff`), `--gemboss-accent-tint` (`#dfe2ff`), `--gemboss-accent-mid` (`#c9cffd`), each with a dark value
2321
+ in `theme-dark.css`. They are fills, never text.
2322
+
2323
+ The contract every surface keeps: nothing is saved by choosing, only by the confirm, and what the confirm did is shown with Undo
2324
+ after (`ActionCard`'s outcome, or the surface's own). The first choice is the assistant's best guess and says where it came from;
2325
+ a question with no guess to offer is asked in the assistant's chat, not here.
2216
2326
  ## Report pages (0.4.0)
2217
2327
 
2218
2328
  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