@gemboss/ui 0.44.0 → 0.46.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,117 @@
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.46.0 (21/10/2026)
6
+
7
+ **`.gemboss-page-section-content` is gone** (gemboss one panel P2b, 21/10/2026).
8
+
9
+ Removed
10
+ - The rule that made every button, input and textarea 44px inside `.gemboss-page-section-content`. That class was the admin
11
+ page editor's "Edit section content" drawer, which P2b replaced with the section's column on the Settings tab (the Canvas's
12
+ own). No element carries it any more, so the rule matched nothing (gemboss `a-tap-target-rule-outlived-its-markup`). The
13
+ page editor's dock rule (`.gemboss-pageedit .gemboss-ec-dock-body`) still sizes its controls.
14
+
15
+ **One language button for the fleet: `LanguageSwitcher`, and the button says the language** (Chris 21/10/2026: "bộ switch
16
+ ngôn ngữ ở gemcommunity, gemnews … chuẩn hoá ở gemboss ui để khi chọn một ngôn ngữ nó hiển thị lên trên cái icon luôn ví dụ
17
+ là VI, EN hay chữ Zhong của trung quốc"). Each surface drew its own; GemCommunity's topbar was a globe alone.
18
+
19
+ Added
20
+ - `LanguageSwitcher` (language-switcher.tsx, exported from the root): the kit's `details.ui-menu`, its button the `globe`
21
+ and the current language's short label ("EN", "VI", "中"), named "Language: Tiếng Việt"; the menu lists each language
22
+ by its native name in its own `lang`, the current one checked (`menuitemradio`), an optional `suggested` row first.
23
+ Props `value`, `languages`, `onChange(code)` (the app stores it), `suggested`, `labels` (`trigger`, `suggested`),
24
+ `variant` (`topbar`: ThemeToggle's 36 / 44px ghost; `inline`: a pill with a caret), `end`, `className`. A language with
25
+ an `href` is a link (`hreflang`). Keys: an opening focuses the checked row, ↑ ↓ wrap, Home / End, Escape back to the
26
+ button, Tab leaves; a click outside closes it. The list floats in the top layer through `./menu-layer`, as `Menu`'s.
27
+ - `FLEET_LANGUAGES` (en English EN, vi Tiếng Việt VI, zh 中文 中), `LANGUAGE_SWITCHER_LABELS`, `matchLanguage(languages,
28
+ navigator.languages | Accept-Language)`, `languageShort(language)`; types `Language`, `LanguageSwitcherLabels`,
29
+ `LanguageSwitcherProps`.
30
+ - `language-switcher.css` (`@gemboss/ui/language-switcher.css`), its own entry, 1.1 KB gzip (ceiling 1,593): the button,
31
+ both variants, the check column; the core's menu rules repeated at zero specificity, so a page with only the tokens and
32
+ this file (gemboss.ai) draws the same menu, and where the core is loaded nothing changes.
33
+ - `language-switcher-plain.js` (`@gemboss/ui/language-switcher-plain.js`), its own file, 2.3 KB gzip (ceiling 2,902):
34
+ `gembossUI.languageSwitcher(opts)` draws the component's DOM for a page with no React (`mount` replaces a placeholder)
35
+ and returns `{ el, value(), set(code), open(), close(), destroy() }`; with `FLEET_LANGUAGES`, `LANGUAGE_SWITCHER_LABELS`,
36
+ `matchLanguage`, `languageShort`.
37
+ - Icon `chevronDown` (the inline variant's caret).
38
+
39
+ Tests
40
+ - `language-switcher.test.tsx`: the markup and ARIA, the three short labels ("中", not "ZH"), translated labels, the
41
+ suggested row and separator, links, an unknown value, strings as text, the helpers, the plain file's defaults against
42
+ the TypeScript, no closing script tag in it, the CSS's classes, 44px rule and tokens only.
43
+ - `language-switcher-browser.test.ts` (`test:ui-browser`): in Chromium, the component and the plain switcher on the same
44
+ topbar and site header, DOM, focus, open state and `onChange` compared after each of 14 steps (keys, Escape, Enter, the
45
+ current row, a click outside, Tab), with defaults and with labels / suggested / inline / left / links; negative control
46
+ one `lang` changed. The label per language and the list's place; 36 / 44px, 44px rows; the phone bar at 390 / 360 / 320;
47
+ contrast in both themes; the standalone menu equal to the core's (negative control without the file).
48
+
49
+ **One onboarding checklist for GemBoss and GemCommunity** (21/10/2026, Chris: "cái onboarding checklist của gemcommunity
50
+ với của gemboss nút tick màu xanh progress bar nên đồng bộ nhé để dùng cùng một gemboss ui kit"). The admin's "Set up your
51
+ store" drew grey ✓ glyphs and a near-black bar in Polaris; GemCommunity's "Start here" drew filled green ticks, an iris bar
52
+ and a "✓ 2 completed" fold in its own markup. Both now render `SetupGuide` (README "Setup guide").
53
+
54
+ Changed
55
+ - The tick is an 18px ring; a done step fills it with `--gemboss-success` and a white check drawn by two borders (no
56
+ glyph, so it is the same tick in every app); `unknown` is a dashed ring with its `?`. The glyphs stay in the markup for a
57
+ page without the sheet.
58
+ - The bar fills in the same green from the first step (it was the accent, green only at the end); the
59
+ `ui-setup__bar--done` class is no longer rendered.
60
+ - The steps done when the guide first renders fold behind one "✓ N completed" row (`<details class="ui-setup__done">`, not
61
+ in the steps' `name` group, so opening it never shuts the open step). Decided once, like the open step: a step finished
62
+ on screen stays in the list. `foldDone={false}` lists them as before.
63
+ - The count's amber/green tones ship in `setup-guide.css` (scoped to `.ui-setup`): the admin loads `ui.css` without
64
+ `kit-plain.css`, where the badge tones live, and drew a grey count.
65
+ - The open step's body is indented 30px (18px mark + 12px gap).
66
+
67
+ Added
68
+ - `optional` (steps under a hairline and an "Optional" heading, never counted), `onDismiss` (a × at the end of the head,
69
+ 44px under a thumb; a dismissible guide does not also fold), `SetupStep.action` (the step's control at the end of a
70
+ plain row, or under its body), `SetupStep.className`, `labels.completed` / `labels.optional` / `labels.dismiss`, and the
71
+ `SetupGuideLabels` type. Under a thumb a step's buttons are 44px too (the admin's rule, now the kit's).
72
+
73
+ Tests
74
+ - `setup-guide.test.tsx`: the fold (order, count, outside the group, `foldDone={false}`, nothing done → no fold), the
75
+ optional group never counted, `action` never inside a `<summary>`, `onDismiss` markup, one green token for tick and bar,
76
+ the × in the coarse block (with its negative control). `setup-guide-browser.test.tsx`: computed colours of the done tick,
77
+ its check and the bar, the todo ring; a step finished on screen stays out of the fold; opening the fold keeps the open
78
+ step open; the × dismisses; the ×, the rows and the fold are 44px at 390 (negative control: without the rule the × is 28).
79
+ - Catalog "Hướng dẫn thiết lập": the admin's and GemCommunity's guides as demos.
80
+
81
+ ## 0.45.0 (21/10/2026)
82
+
83
+ **The reaction picker speaks the app's language** (21/10/2026, seen on GemCommunity's Vietnamese chat with 0.43.0: the
84
+ picker opened by the composer's emoji button and by "More reactions" said "Search emoji", "No emoji matches.", "Frequently
85
+ used", "Smileys"…).
86
+
87
+ Added
88
+ - `REACTION_LABELS` and type `ReactionLabels` (reactions.tsx, exported from the root): every word `Reactions` and
89
+ `ReactionPicker` write, English by default: `pickReaction`, `searchEmoji`, `noEmojiMatches`, `frequentlyUsed`,
90
+ `groupName(id)` (a group's heading by its English id in `REACTION_GROUPS`, which stays the key), `pickReactionEmoji(emoji,
91
+ words, mine)`, `addReaction`, `addReactionTo(target)`, `reactionChip(emoji, names, mine)`. A `labels` prop (any subset) on
92
+ `Reactions` (passed on to its picker) and on `ReactionPicker`; its `label` prop still wins over `pickReaction`.
93
+ - `TEAM_CHAT_LABELS` carries those keys (same defaults) and `messageFrom(author)` ("the message from Linh", the target a
94
+ message gives its reaction row). `Message` passes its `labels` to its `Reactions` row and to the "More reactions" picker,
95
+ and uses `addReaction` for that button's tooltip (was a hard-coded "Add reaction"); `Composer` passes its `labels` to the
96
+ emoji button's picker.
97
+ - `team-chat-plain.js`: `LABELS` has the same keys; `reactions(props)` and `reactionPicker(props)` take `labels`, `message`
98
+ passes its own to both, `emojiButton({ labels })` to its picker.
99
+ - `searchReactions(query, groupName?)`: with the picker's `groupName`, a query found in a TRANSLATED group name (case and
100
+ Vietnamese marks ignored: "mat cuoi" finds "Mặt cười") adds that whole group. A name left as its English id is not
101
+ searched, so an English picker finds what it did. The emoji's own search words stay English for now.
102
+
103
+ Unchanged
104
+ - An app that passes no labels draws the same markup, word for word (held by `reactions.test.tsx`).
105
+
106
+ Tests
107
+ - `reactions.test.tsx`: the defaults; markup with Vietnamese labels (picker both kinds, row); every label replaced by a
108
+ marker leaves no English but the data (search words, names); the translated-group search; a guard that parses
109
+ `reactions.tsx` (TypeScript's parser) and fails on any string or JSX text outside `// labels:begin` … `// labels:end`
110
+ that is not a class name, key or attribute value, with negative controls for five words put back.
111
+ - `team-chat-plain-data.test.ts`: `searchReactions` with group names against the TypeScript; no picker word written in the
112
+ plain code outside `LABELS`. `team-chat-plain-browser.test.ts`: the translated picker, row and message drawn both ways;
113
+ live, a Vietnamese message's two pickers and a Vietnamese composer's, searched by a group's Vietnamese name and to
114
+ nothing, the same DOM both ways. `team-chat.test.tsx`: a message's reaction words follow its labels.
115
+
5
116
  ## 0.44.0 (21/10/2026)
6
117
 
7
118
  **The assistant frame for a page with no React: `gembossUI.assistantFrame`** (Chris 21/10/2026: GemFactory's assistant was a
package/README.md CHANGED
@@ -73,6 +73,7 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
73
73
  | setup-guide | `@gemboss/ui/setup-guide.css` | `packages/ui/src/setup-guide.css` | `SetupGuide` (0.30.0) | 0.9 KB | `SetupGuide` imports it |
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
+ | language-switcher | `@gemboss/ui/language-switcher.css` | `packages/ui/src/language-switcher.css` | `LanguageSwitcher`: the globe + "EN" / "VI" / "中" and the menu of languages (21/10/2026; works without the core) | 1.1 KB | `LanguageSwitcher` imports it; a plain-HTML page links it with `language-switcher-plain.js` (README "LanguageSwitcher") |
76
77
  | 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
78
  | 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
79
  | 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 |
@@ -1412,6 +1413,82 @@ its negative control changes one attribute and the comparison sees it. Then the
1412
1413
  beside it, idempotence, `<head>`, alone, the wrong order). Catalog section "Chuyển dịch vụ", last frame: the plain page,
1413
1414
  live.
1414
1415
 
1416
+ ## LanguageSwitcher (21/10/2026): the button says which language the page is in
1417
+
1418
+ Chris 21/10/2026: "bộ switch ngôn ngữ ở gemcommunity, gemnews và xem những chỗ nào khác nên chuẩn hoá ở gemboss ui để khi
1419
+ chọn một ngôn ngữ nó hiển thị lên trên cái icon luôn ví dụ là VI, EN hay chữ Zhong của trung quốc ntn đó để dễ hiểu hơn là
1420
+ icon quả địa cầu như hiện tại". Each surface drew its own (GemCommunity's topbar a globe alone, its sign-in pages
1421
+ "🌐 EN ⌄", GemNews its own): one component now, whose button shows the globe AND the current language's short label,
1422
+ "EN", "VI", "中" (the character, not "ZH": the button is read by people).
1423
+
1424
+ ```tsx
1425
+ import { FLEET_LANGUAGES, LanguageSwitcher, matchLanguage } from "@gemboss/ui";
1426
+
1427
+ // The topbar's end slot, beside ThemeToggle and the account menu. The app keeps the choice (cookie, storage, profile).
1428
+ <AppShell actions={<><ThemeToggle /><LanguageSwitcher value={lang} onChange={saveLang} labels={{ trigger: t("Language") }} /><AccountMenu … /></>} … />
1429
+
1430
+ // A sign-in page, a site header, a footer: the pill with a caret; the browser's language suggested first.
1431
+ <LanguageSwitcher variant="inline" value={lang} onChange={saveLang}
1432
+ suggested={matchLanguage(FLEET_LANGUAGES, navigator.languages)} labels={{ trigger: "Ngôn ngữ", suggested: "Gợi ý" }} />
1433
+
1434
+ // A site whose languages are paths: each row is a link (hreflang), so it switches with no script.
1435
+ <LanguageSwitcher value="vi" languages={FLEET_LANGUAGES.map((l) => ({ ...l, href: l.code === "en" ? "/" : `/${l.code}/` }))} />
1436
+ ```
1437
+
1438
+ | | |
1439
+ | --- | --- |
1440
+ | Props | `value` (the current code, required; one not in the list shows its code upper case), `languages` (default `FLEET_LANGUAGES`: `{ code, label, short }` for en "English" EN, vi "Tiếng Việt" VI, zh "中文" 中; `label` is the NATIVE name, `short` what the button shows, default the code's first part upper case; `href` makes the row a link), `onChange(code)` (a row other than the current one; the app stores it and passes the new `value`), `suggested` (a code: that row first, with the hint, then a separator; one the list does not have changes nothing), `labels` (`trigger` "Language", `suggested` "Suggested": `LANGUAGE_SWITCHER_LABELS`), `variant` (`topbar` default, `inline`), `end` (default true: the menu's right edge on the button's), `className` (on the `<details>`) |
1441
+ | The button | `summary.gb-lang__btn`: the kit's `globe` at 18px, then `span.gb-lang__short` ("VI", in its own `lang`, so "中" takes a Chinese glyph), in the text colour (the globe muted), at least 2ch wide so EN / VI / 中 do not move the bar. Its name and tooltip: `<trigger>: <the current language's label>` ("Language: Tiếng Việt", "Ngôn ngữ: 中文"); `aria-haspopup="menu"`. `topbar`: ThemeToggle's ghost, 36px; `inline`: a pill on the surface with the chips' line and the `chevronDown` caret (new in the set), turned while open. 44px under a finger or at phone width (≤767px), both |
1442
+ | The menu | the kit's, not a new one: `details.ui-menu` (it opens with no JavaScript), its list `ui-menu__list gemboss-popover-surface` floated in the top layer by `./menu-layer` as `Menu`'s is, its rows `ui-menu__item`, so it looks like the account menu. `role="menu"` named `trigger`; each row a `menuitemradio` with `aria-checked`, `data-lang`, a check column (`.gb-lang__check`, shown on the checked row only) and the name in its own `lang` (`span.gb-lang__name lang="zh"`), so a screen reader says "中文" in Chinese. The suggested row carries `ui-menu__hint`, then `hr.ui-menu__sep` |
1443
+ | Keys | an opening (a click, Enter / Space, ↓ / ↑ on the button) puts focus on the checked row; ↑ ↓ wrap, Home / End; Escape closes it, focus back on the button; Tab closes it and focus goes on from the button, forward or back; a click outside closes it; a row closes it, focus on the button, and calls `onChange` when it is not the current one. Rows are out of the Tab order (`tabindex="-1"`), as a menu's are |
1444
+ | Helpers | `matchLanguage(languages, wanted)`: the first of `navigator.languages`, an Accept-Language header or one tag that the list has, exactly or by its base language ("zh-CN" → "zh"), else undefined; pure, so a server reads the header with it. `languageShort(language)` |
1445
+ | CSS | `language-switcher.css`, its own entry, 1.1 KB gzip, imported by the component. The menu's base rules (the core's `ui-menu` ones) are repeated there at zero specificity (`:where`) with the core's values: where the core is loaded it wins and nothing changes; a page that links only the tokens and this file (gemboss.ai, whose pages the core would reset) draws the same menu. Tokens only, so `data-theme="dark"` follows |
1446
+ | The topbar | at 320 / 360 / 390 the kit's phone bar still fits with the switcher between ThemeToggle and the account chip (☰, brand, search, toggle, switcher, chip; no horizontal scroll) |
1447
+
1448
+ ### Plain HTML: `gembossUI.languageSwitcher()` (21/10/2026)
1449
+
1450
+ For a page with no React (gemboss.ai, GemNews's extension popup, GemFactory, GemCare, GemPress): `language-switcher-plain.js`
1451
+ draws the component's DOM (classes, ARIA, order) and runs its keys, so `language-switcher.css` draws it unchanged. Its own
1452
+ file, 2.3 KB gzip, loaded only where the switcher is. A classic script that ADDS to `gembossUI`, after `kit-plain.js` when
1453
+ the page has it; it holds no closing script tag, so a server can inline it.
1454
+
1455
+ ```html
1456
+ <link rel="stylesheet" href="/ui/language-switcher.css"> <!-- require.resolve("@gemboss/ui/language-switcher.css") -->
1457
+ …
1458
+ <span data-gb-language-switcher hidden></span> <!-- where the button goes -->
1459
+ …
1460
+ <script src="/ui/kit-plain.js"></script> <!-- if the page has it: FIRST (the menu then floats) -->
1461
+ <script src="/ui/language-switcher-plain.js"></script> <!-- @gemboss/ui/language-switcher-plain.js -->
1462
+ <script>
1463
+ var sw = gembossUI.languageSwitcher({
1464
+ value: document.documentElement.lang || "en",
1465
+ onChange: function (code) { document.cookie = "lang=" + code + "; path=/; max-age=31536000"; location.reload(); },
1466
+ suggested: gembossUI.matchLanguage(gembossUI.FLEET_LANGUAGES, navigator.languages),
1467
+ labels: { trigger: "Ngôn ngữ", suggested: "Gợi ý" },
1468
+ variant: "inline",
1469
+ mount: document.querySelector("[data-gb-language-switcher]"), // replaced by the switcher; none: append sw.el
1470
+ });
1471
+ </script>
1472
+ ```
1473
+
1474
+ | | |
1475
+ | --- | --- |
1476
+ | Options | the component's props, same names and defaults (`value`, `languages`, `onChange`, `suggested`, `labels`, `variant`, `end`, `className`), plus `mount`: an element the switcher REPLACES (a placeholder the server wrote) |
1477
+ | Returns | `{ el, value(), set(code), open(), close(), destroy() }`. A chosen row updates the switcher itself before `onChange` (the component waits for its `value` prop); `set` is for a change from elsewhere; `destroy` removes it and its two document listeners |
1478
+ | `gembossUI` | `languageSwitcher`, `FLEET_LANGUAGES`, `LANGUAGE_SWITCHER_LABELS`, `matchLanguage`, `languageShort`. With `kit-plain.js` the open list floats in the top layer (`gembossUI.floatMenu`) and `initMenus` closes it like any kit menu; without it the list hangs under the button (`position: absolute`), as every kit menu does where there is no popover API |
1479
+ | A Chrome extension | a popup or options page loads both files by `src` (no inline script: an extension's CSP forbids it) and calls it from its own script; `onChange` writes `chrome.storage` |
1480
+
1481
+ `packages/ui/src/language-switcher-browser.test.ts` (Chromium, `test:ui-browser`) renders the component and the plain
1482
+ switcher on the same shells (the kit's topbar with the `topbar` variant, the site header with `inline`) and compares the
1483
+ two DOMs node for node, attribute for attribute, with focus, open state and the `onChange` calls, after each step of one
1484
+ script: closed, opened by a click, ↓ ↓ Home End ↑, Escape, ↓ on the button, Home + Enter, the current row, a click outside,
1485
+ Tab; with the defaults and with Vietnamese labels, a suggested row, the inline variant, left-aligned, links. NEGATIVE
1486
+ CONTROL: one `lang` changed on the plain page and the comparison sees it. Then: the short label per language, the list
1487
+ under the button's right edge, one tick; 36 / 44px and 44px rows; the phone bar at 390 / 360 / 320; contrast in both
1488
+ themes (label ≥ 4.5:1, tick ≥ 3:1); the standalone page's menu equal to the core's (negative control: without the file,
1489
+ the browser's popover box). `LANGUAGE_SWITCHER_SHOTS=<dir>` writes the screenshots (closed / open, both variants, both
1490
+ themes, vi / en / zh). `language-switcher.test.tsx` holds the markup, the labels, the helpers and the plain file's defaults.
1491
+
1415
1492
  ## Repeater rows (16/10/2026): `RepeaterList`, `useKeyedRows`
1416
1493
 
1417
1494
  A setting that is a list (an accordion's items, a pricing table's plans, choice cards) is the element's content. The admin's
@@ -1703,8 +1780,9 @@ and GemCare's chat, draw and accept the same thing.
1703
1780
  | Piece | What |
1704
1781
  |---|---|
1705
1782
  | `@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 }` |
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 |
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) |
1783
+ | `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. `labels` (below) reaches its picker too |
1784
+ | `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 `labels.pickReaction`), `className`, `initialFocus` (`"search"`, default, or `"emoji"`: under a finger a focused search brings the keyboard up over it), `labels` |
1785
+ | `REACTION_LABELS`, type `ReactionLabels` (21/10/2026) | every word the two write, English by default; `labels` (any subset) translates. The keys and how a search treats a translated group: "The words" in the team chat layer below (`TEAM_CHAT_LABELS` carries the same keys) |
1708
1786
  | `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`) |
1709
1787
 
1710
1788
  Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, transparent padding carries the rest, so a
@@ -1739,7 +1817,8 @@ token, so `data-theme="dark"` turns it dark unchanged. It holds no rule on `:roo
1739
1817
 
1740
1818
  Props only: no fetching, no store, no router. The app owns the data and the actions; the components keep only the screen's
1741
1819
  own state (the reaction picker a message opened, the composer's text when the app does not hold it, focus). Every word they
1742
- write themselves is in `TEAM_CHAT_LABELS`; pass `labels` (any subset) to translate. `teamChatRows(messages, { meId,
1820
+ write themselves is in `TEAM_CHAT_LABELS`; pass `labels` (any subset) to translate (the reactions' and the picker's words
1821
+ too: "The words", below). `teamChatRows(messages, { meId,
1743
1822
  isUnread })` (plain TS, `team-chat-data.ts`) says where the day separators and the "New messages" line go and which
1744
1823
  messages are `grouped`: same author, same day, under 5 minutes apart, not a reply, and not the first new one (GemCare's
1745
1824
  rules). `chatDayWord` gives "Today" / "Yesterday"; older days the app formats in its own locale. Days are the viewer's
@@ -1762,6 +1841,47 @@ field where the caret was (a person who opened it was writing); a press or focus
1762
1841
  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
1842
  box's three 44px buttons touch (no gap), so the field keeps 150px at 320.
1764
1843
 
1844
+ **The words of the reactions and the picker** (21/10/2026: GemCommunity's Vietnamese chat showed "Search emoji",
1845
+ "Frequently used" and "Smileys"). `TEAM_CHAT_LABELS` carries `REACTION_LABELS`' keys (reactions.tsx) with the same defaults,
1846
+ and a `Message` passes its `labels` to the `Reactions` row and to the picker "More reactions" opens, the `Composer` to the
1847
+ picker its emoji button opens. So one `labels` object translates the chat, the reaction row and both pickers; outside the
1848
+ team chat, `Reactions` and `ReactionPicker` take the same `labels`. An app that passes nothing gets the English below,
1849
+ word for word.
1850
+
1851
+ | Key | Default | Where |
1852
+ |---|---|---|
1853
+ | `pickReaction` | "Pick a reaction" | the picker's accessible name (a `label` prop wins: the composer's picker is named `pickEmoji`, "Pick an emoji") |
1854
+ | `searchEmoji` | "Search emoji" | the search field's placeholder and accessible name |
1855
+ | `noEmojiMatches` | "No emoji matches." | the search found nothing |
1856
+ | `frequentlyUsed` | "Frequently used" | the first heading |
1857
+ | `groupName(id)` | the id: "Smileys", "Hands", "Marks", "Work" | each group's heading. `id` is the group's English id in `REACTION_GROUPS`: it stays the key, the name is display only |
1858
+ | `pickReactionEmoji(emoji, words, mine)` | "Add 🎉 party tada celebrate", "Remove 👍 like thumbs up yes ok" | a picker button's accessible name where the picker puts reactions (the composer's: the emoji and its words, no verb) |
1859
+ | `addReaction` | "Add reaction" | the tooltip of the button that opens the picker: the reaction row's, the action bar's |
1860
+ | `addReactionTo(target)` | "Add a reaction to the message from Linh" ("… to this" with no `target`) | the reaction row's button name |
1861
+ | `reactionChip(emoji, names, mine)` | "👍, Linh, Quan. Remove yours", "🎉, Bo. Add yours" | a chip's accessible name (`names` joined by ", ") |
1862
+ | `messageFrom(author)` (team chat only) | "the message from Linh" | the `target` a message gives its reaction row |
1863
+
1864
+ ```tsx
1865
+ const VI: Partial<TeamChatLabels> = {
1866
+ pickReaction: "Chọn cảm xúc", pickEmoji: "Chọn emoji", searchEmoji: "Tìm emoji", noEmojiMatches: "Không có emoji nào khớp.",
1867
+ frequentlyUsed: "Hay dùng",
1868
+ groupName: (id) => ({ Smileys: "Mặt cười", Hands: "Bàn tay", Marks: "Ký hiệu", Work: "Công việc" } as Record<string, string>)[id] ?? id,
1869
+ pickReactionEmoji: (e, words, mine) => `${mine ? "Bỏ" : "Thả"} ${e} ${words}`,
1870
+ addReaction: "Thêm cảm xúc", addReactionTo: (target) => `Thêm cảm xúc cho ${target ?? "mục này"}`,
1871
+ reactionChip: (e, names, mine) => `${e}, ${names}. ${mine ? "Bỏ của bạn" : "Thả cùng"}`, messageFrom: (a) => `tin nhắn của ${a}`,
1872
+ };
1873
+ <Message {...m} labels={VI} /> <Composer onSend={send} labels={VI} /> <Reactions items={r} labels={VI} />
1874
+ // A plain page: the same object.
1875
+ gembossUI.teamChat.message({ ...m, labels: VI }); gembossUI.teamChat.emojiButton({ textarea, labels: VI });
1876
+ ```
1877
+
1878
+ **Search** matches each emoji's search words, which are English (as before), and, when `groupName` gives a group a name
1879
+ other than its id, that name too: ignoring case and Vietnamese marks ("mặt cười", "mat cuoi" or "Mặt" for "Mặt cười"), it
1880
+ adds the whole group, in picker order, to what the words found. An English name is not searched, so an English picker
1881
+ finds exactly what it did. Still English for now: the emoji's own search words, a button's tooltip (the first of them) and
1882
+ the words read after the emoji in a button's name. `reactions.test.tsx` holds `reactions.tsx` to the table (no string or
1883
+ JSX text outside it but class names, keys and attribute values), and `team-chat-plain-data.test.ts` the plain script.
1884
+
1765
1885
  **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
1766
1886
  window minus the shell's rail), with container queries. `TEAM_CHAT_DRAWER_MAX` (1100) and `TEAM_CHAT_PHONE_MAX` (760) are the
1767
1887
  same numbers for JS.
@@ -2239,11 +2359,11 @@ after `kit-plain.js` when the page has it (it adds `gembossUI.teamChat`; loaded
2239
2359
  | `readReceipt({ read, of, label })` | `ReadReceipt` (`null` when `of` ≤ 0) |
2240
2360
  | `channelSection({ label, id, items })`, `channelItem(props)` | `ChannelSection`, `ChannelItem` (`href`: a link, else a button; a DM's `avatar`); `items` are rows or their props |
2241
2361
  | `peopleSection({ label, count, id, items })`, `personRow(props)` | `PeopleSection`, `PersonRow` (a link, a button with `onSelect`, else a `div`) |
2242
- | `reactions(props)`, `reactionPicker(props)` | `Reactions` (plus `open` / `onOpenChange`), `ReactionPicker` (`items` absent: the composer's, which writes an emoji; `label`, `className`) |
2362
+ | `reactions(props)`, `reactionPicker(props)` | `Reactions` (plus `open` / `onOpenChange`), `ReactionPicker` (`items` absent: the composer's, which writes an emoji; `label`, `className`); both take `labels` ("The words" above) |
2243
2363
  | `emojiButton({ textarea, button, host, labels })` | the `Composer`'s emoji button and picker, for a composer the page writes itself (below) |
2244
2364
  | `avatar({ name, src, size, className })`, `icon(name, size)` | `Avatar` (portal.tsx's initials and tone), `Icon` (the icons the builders draw, and `more`) |
2245
2365
  | `rows(messages, opts)`, `dayKey(at, timeZone)`, `dayISO(at, timeZone)`, `dayWord(at, now, timeZone)` | `teamChatRows`, `chatDayKey`, `chatDayISO`, `chatDayWord` |
2246
- | `LABELS` | `TEAM_CHAT_LABELS`; `labels` (any subset) translates, functions included |
2366
+ | `LABELS` | `TEAM_CHAT_LABELS` (the reactions' and the picker's keys included); `labels` (any subset) translates, functions included: `message` passes them to its reaction row and both pickers, `emojiButton` to its picker |
2247
2367
 
2248
2368
  ```html
2249
2369
  <link rel="stylesheet" href="/ui/portal.css"><link rel="stylesheet" href="/ui/reactions.css"><link rel="stylesheet" href="/ui/team-chat.css">
@@ -2404,6 +2524,33 @@ visits (localStorage); `group` names the steps' `name` when two guides render in
2404
2524
  ]} />
2405
2525
  ```
2406
2526
 
2527
+ **One checklist for GemBoss and GemCommunity (21/10/2026).** Chris: "cái onboarding checklist của gemcommunity với của
2528
+ gemboss nút tick màu xanh progress bar nên đồng bộ nhé để dùng cùng một gemboss ui kit". The admin's "Set up your store"
2529
+ (StoreChecklistCard) and GemCommunity's feed "Start here" both render this guide; the guide took what each did better:
2530
+
2531
+ | | Before (GemBoss admin / GemCommunity) | Now |
2532
+ |---|---|---|
2533
+ | Tick | grey ✓ glyph / 18px green circle, white ✓ | 18px ring; done fills `--gemboss-success` with a white check drawn in CSS (same in every app and font); unknown is a dashed ring with `?` |
2534
+ | Bar | 6px near-black / 6px iris gradient | 6px, the ticks' green from the first step (it counts them); `ui-setup__bar--done` is no longer drawn |
2535
+ | Count | Polaris badge / "2/5 steps done" text | the kit badge, amber then green; its tones ship in `setup-guide.css` (the admin loads `ui.css`, not `kit-plain.css`) |
2536
+ | Done steps | listed / folded behind "✓ 2 completed" | folded behind one "✓ N completed" row (`foldDone`, default on; `labels.completed`) |
2537
+ | Extra steps | "Optional" group / none | `optional`: under a hairline and an "Optional" heading (`labels.optional`), never counted |
2538
+ | Row control | buttons in the open step / an "Open" button at the row's end | `body` for a step that explains; `action` at the end of a plain row (under the body when there is one) |
2539
+ | Put away | fold, remembered / × | fold with `rememberKey`, or `onDismiss`: a × (`labels.dismiss`) and no fold |
2540
+
2541
+ The fold is decided once, like the open step: a step finished while the guide is on screen stays in the list and turns
2542
+ green; the next render that mounts the guide folds it. `className` on a step is for the surface's own rule (GemCommunity
2543
+ hides its Telegram step where its rail asks for Telegram). The ×, the rows, the head and a step's buttons are 44px under a
2544
+ thumb and at phone width.
2545
+
2546
+ ```tsx
2547
+ <SetupGuide title="Start here" description="A few steps to get into the loop." onDismiss={hide}
2548
+ labels={{ completed: (n) => `${n} completed`, dismiss: "Hide" }} steps={[
2549
+ { id: "profile", title: "Fill your profile", state: "done" },
2550
+ { id: "post", title: "Write your first post", action: <Link className="ui-btn" href="/">Open</Link> },
2551
+ ]} />
2552
+ ```
2553
+
2407
2554
  ## Collections (0.13.0): tables, lists, grids, boards
2408
2555
 
2409
2556
  Chris settled these on 05/10/2026 after an inventory of 136 hand-made collections on 11 surfaces (86 tables scrolling
@@ -2629,6 +2776,7 @@ something the base cannot express, add a token or a prop here, and every surface
2629
2776
  | `packages/ui/src/editor-layers-plain.js` | 0.22.0: `gembossUI.initEditorLayers()`, the layers', the outline's and the drawers' behaviour for plain HTML. Load after `kit-plain.js`. |
2630
2777
  | `packages/ui/src/service-switcher-plain.js` | 0.36.0: `gembossUI.initServiceSwitcher()`, the ServiceSwitcher drawn and driven for plain HTML (`@gemboss/ui/service-switcher-plain.js`). After `kit-plain.js` when the page has it; works alone. See "ServiceSwitcher". |
2631
2778
  | `packages/ui/src/team-chat-plain.js` | 20/10/2026: `gembossUI.teamChat`, builders that return the team chat's rows (messages, day separators, "New messages", jump, receipt, rail rows, people rows, reactions and the picker) with the components' markup, for plain HTML (`@gemboss/ui/team-chat-plain.js`). After `kit-plain.js` when the page has it; works alone. See "The team chat layer". |
2779
+ | `packages/ui/src/language-switcher-plain.js` | 21/10/2026: `gembossUI.languageSwitcher()`, the LanguageSwitcher drawn and driven for plain HTML (`@gemboss/ui/language-switcher-plain.js`), with `FLEET_LANGUAGES`, `matchLanguage`, `languageShort`. After `kit-plain.js` when the page has it; works alone. See "LanguageSwitcher". |
2632
2780
  | `catalog/editor.html` | 0.14.0: the frame rendered from the components and running (`packages/ui/scripts/build-catalog-editor.mjs`), linked from `catalog/index.html`. |
2633
2781
  | `packages/ui/src/editor.css` | 0.9.0: the editors (frame, top bar, preview, Configure dock, inspector and controls, Layers, settings drawer, page picker, menus). A separate entry, NOT inside `gemboss-ui.css`; the admin imports it from the editor components, never from a layout. See "Editor CSS" above. |
2634
2782
  | `packages/ui/src/chat.css`, `chat-composer.css`, `assistant.css`, `auth.css`, `app-shell.css` | the chat body, the composer, the assistant frame, the sign-in screen, the topbar slots + base text + the page width tiers (`ui-page`, 0.8.1). All inside `gemboss-ui.css`; the admin @imports `chat.css` and `chat-composer.css` itself. |
@@ -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.44.0 · Editor</h1>
11
+ <h1>@gemboss/ui 0.46.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;;