@gemboss/ui 0.39.0 → 0.40.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/README.md CHANGED
@@ -59,6 +59,7 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
59
59
  | auth | `@gemboss/ui/auth.css` | `packages/ui/src/auth.css` | the sign-in screen (+ the grid backdrop, 0.30.0) | 2.8 KB | `AuthLayout` & co. import it |
60
60
  | assistant | `@gemboss/ui/assistant.css` | `packages/ui/src/assistant.css` | the dock and the launcher | 2.2 KB | `AssistantHost`, the launcher import it |
61
61
  | portal | `@gemboss/ui/portal.css` | `packages/ui/src/portal.css` | `TopbarNav`, `SearchDialog`, `Avatar`, `Leaderboard`, `Podium`, the `ThemeToggle` button | 2.1 KB | those components import it |
62
+ | site | `@gemboss/ui/site.css` | `packages/ui/src/site.css` | the public site layer (0.40.0): the bar, the lockup at the site size, links, buttons, the footer | 1.0 KB | `SiteHeader` / `SiteBrand` import it; a plain-HTML page links it |
62
63
  | theme-dark | `@gemboss/ui/theme-dark.css` | `packages/ui/src/theme-dark.css` | the dark palette | 0.6 KB | `ThemeToggle` and `themeScript` (`theme-script.ts`, without portal.css) import it |
63
64
  | collections | `@gemboss/ui/collections.css` | `packages/ui/src/collections.css` | tables, lists, grids, boards | 4.6 KB | `IndexTable`, `ResourceList`, `Board`… import it |
64
65
  | editor | `@gemboss/ui/editor.css` | `packages/ui/src/editor.css` | the editors | 7.9 KB | the `@gemboss/ui/editor` components import it |
@@ -70,7 +71,8 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
70
71
  | assistant-chat | `@gemboss/ui/assistant-chat.css` | `packages/ui/src/assistant-chat.css` | the chat pieces the package draws (0.29.0): column, starters that wrap, meta, errors, notes, paperclip | 0.9 KB | `AssistantChat` and its pieces import it |
71
72
  | minichart | `@gemboss/ui/minichart.css` | `packages/ui/src/minichart.css` | `Sparkline`, `ShareBar` (0.30.0) | 0.5 KB | those components import it |
72
73
  | setup-guide | `@gemboss/ui/setup-guide.css` | `packages/ui/src/setup-guide.css` | `SetupGuide` (0.30.0) | 0.9 KB | `SetupGuide` imports it |
73
- | service-switcher | `@gemboss/ui/service-switcher.css` | `packages/ui/src/service-switcher.css` | `ServiceSwitcher`: the button after the brand and its panel of Gem services (0.36.0) | 1.8 KB | `ServiceSwitcher` imports it, and `BrandLockup` through it (README "ServiceSwitcher") |
74
+ | 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") |
75
+ | 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") |
74
76
  | 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) |
75
77
 
76
78
  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).
@@ -639,7 +641,7 @@ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
639
641
  | Preview | `.gemboss-pv` › `.gemboss-pv-stage` › `.gemboss-pv-frame` (`.desktop` / `.tablet` / `.mobile`, `.gemboss-pv-fit` for scale-to-fit) › `iframe.gemboss-pv-iframe`; `.gemboss-pv-spin`; empty state `.gemboss-pv-empty` (`-art`, `-title`, `-sub`, `-cta`). Device toggle `.gemboss-pv-devices › button.gemboss-pv-device` |
640
642
  | Left column | tab row `div.gemboss-ec-tabs[role=tablist] › button[role=tab][aria-selected]` (add `.gemboss-ec-tab` for the 44px pointer floors); Configure pane `.gemboss-ec-dock-body` |
641
643
  | Inspector | `.gemboss-ei` (`--docked` inside the Configure pane) › `.gemboss-ei-head` (`-title`, `-x`, `-savedot`), `.gemboss-ei-tabs › .gemboss-ei-tab`, `.gemboss-ei-body`, `.gemboss-ei-foot`. Groups `.gemboss-ei-group › .gemboss-ei-grouphead` (collapsible) and `.gemboss-ei-gh` / `.gemboss-ei-grp`. Field `.gemboss-ei-fld › label + control + .gemboss-ei-hint` |
642
- | Controls | text / select `.gemboss-field.gemboss-ei-inp` (+ `.gemboss-ei-suffix`, `.gemboss-ei-mono`), segmented `.gemboss-ei-seg`, colour `.gemboss-ei-color` / `.gemboss-ei-swatches › .gemboss-ei-sw` / `.gemboss-ei-swatch`, range `.gemboss-ei-range` + `.gemboss-ei-rangeval`, spacing box `.gemboss-ei-edgebox › .gemboss-ei-edge`, image `.gemboss-ei-img` (`-thumb`, `-ctl`, `-actions`, `-grid`, `-cell`), repeater `.gemboss-ei-rep-item` (`-head`, `-acts`, `-add`), blocks `.gemboss-ei-block` (`-head`, `-x`), add `.gemboss-ei-addrow` / `.gemboss-ei-addchip`, element tree `.gemboss-ei-tree › .gemboss-ei-tnode` (`-tname`, `-tacts`, `-tadd`), notes `.gemboss-ei-note`, `.gemboss-ei-oos` (`-h`), `.gemboss-ei-empty`, drag `.gemboss-ei-drag` / `.gemboss-ei-dragmask` |
644
+ | Controls | text / select `.gemboss-field.gemboss-ei-inp` (+ `.gemboss-ei-mono`), a number with its unit inside `UnitField` (`label.gemboss-field.gemboss-field--group` › `.gemboss-field__bare` + `.gemboss-field__suffix`, 0.40.0; `.gemboss-ei-suffix` is gone), segmented `.gemboss-ei-seg`, colour `.gemboss-ei-color` / `.gemboss-ei-swatches › .gemboss-ei-sw` / `.gemboss-ei-swatch`, range `.gemboss-ei-range` + `.gemboss-ei-rangeval`, spacing box `.gemboss-ei-edgebox › .gemboss-ei-edge`, image `.gemboss-ei-img` (`-thumb`, `-ctl`, `-actions`, `-grid`, `-cell`), repeater `.gemboss-ei-rep-item` (`-head`, `-acts`, `-add`), blocks `.gemboss-ei-block` (`-head`, `-x`), add `.gemboss-ei-addrow` (its buttons are the kit's `.ui-btn` via `ChipButton`, 0.40.0; `.gemboss-ei-addchip` is gone), element tree `.gemboss-ei-tree › .gemboss-ei-tnode` (`-tname`, `-tacts`, `-tadd`), notes `.gemboss-ei-note`, `.gemboss-ei-oos` (`-h`), `.gemboss-ei-empty`, drag `.gemboss-ei-drag` / `.gemboss-ei-dragmask` |
643
645
  | Layers | `.gemboss-layers` › `.gemboss-layer-head` + `.gemboss-layers-list` › `.gemboss-layer` (`.is-sel`) › `.gemboss-layer-main` (`.gemboss-layer-type`) + `.gemboss-layer-acts` (`.gemboss-layer-del`) |
644
646
  | Settings drawer | `.gemboss-settings` › `.gemboss-picker-head` + `.gemboss-settings-body` › `.gemboss-settings-field`, `-grouphead`, `-gen`, `-note`, `-error`, `-handle`; `.gemboss-settings-foot` (`-save`, `-cancel`); backdrop `.gemboss-picker-backdrop`, empty `.gemboss-picker-empty` |
645
647
  | Page / section editor bar | `.gemboss-pageedit` (`-top`, `-title`, `-kind`, `-status`, `-standing`, `-scope`, `-modes`, `-notice`, `-chip`, `-pv`; the left cluster's gap is `--gemboss-tb-gap`, 12px, for a child that hands it back; above 1000px the bar's tracks are `auto fit-content(100%) auto`, so its free space sits beside the centre), `.gemboss-sec-type`, section insert `.gemboss-sec-insert-*`, draft preview `.gemboss-page-draft-stage` / `-status`, `.gemboss-page-section-content`, global styles bundle `.gemboss-bundle-look` |
@@ -766,7 +768,7 @@ import { ColorControl, EdgeBox, Field, ImageControl, InspectorGroup, PanelAction
766
768
  |---|---|---|
767
769
  | `PanelTabs` | `variant="column"`: `.gemboss-ec-tabs` › `.gemboss-ec-tab` (Configure · Chat, `icon` before the label). `variant="inspector"`: `.gemboss-ei-tabs` › `.gemboss-ei-tab(.on)` | `role=tablist` / `tab`, `aria-selected`, `data-value` (the tab's id, for plain HTML), `aria-controls` from a tab's `controls`. `roving` (default on): only the selected tab is a Tab stop, ←/→ move the selection and the focus, wrapping. The admin's Configure · Chat row passes `roving={false}` (both tabs Tab stops, no arrows: how it always behaved). |
768
770
  | `PanelHeader`, `PanelAction` | `header.gemboss-ei-head` › `.gemboss-ei-title` (+ `small` subtitle) · `.gemboss-ei-act` buttons · `.gemboss-ei-x` ✕ | ✕ only with `onClose` (a docked panel has none); `onDragStart` adds `.gemboss-ei-drag` and takes the mouse-down of a floating panel. |
769
- | `InspectorGroup` | `.gemboss-ei-grp` › `button.gemboss-ei-gh` (▾ / ▸, 11px / 700 uppercase, `note` as `.gemboss-ei-gh-note`) + the body | `aria-expanded`, `data-open`; closed, CSS hides everything after the header (the body stays mounted). Controlled (`open` + `onOpenChange`, which receives the NEXT state) or not (`defaultOpen`). `collapsible={false}`: the schema form's static group, `section.gemboss-ei-group` › `h4.gemboss-ei-grouphead` + `hint`. |
771
+ | `InspectorGroup` | `.gemboss-ei-grp` › `button.gemboss-ei-gh` (13px / 650, the chevron at its end, `note` as `.gemboss-ei-gh-note`) + the body | `aria-expanded`, `data-open`; closed, CSS hides everything after the header (the body stays mounted; an inline `display` on the body's first element would win over that, so give it a bare wrapper). Controlled (`open` + `onOpenChange`, which receives the NEXT state) or not (`defaultOpen`). `attrs`: `data-*` on the group itself, so a page that finds groups by an attribute needs no wrapper (a wrapper drops the divider between groups). `collapsible={false}`: the schema form's static group, `section.gemboss-ei-group` › `h4.gemboss-ei-grouphead` + `hint`. |
770
772
  | `Field` | `.gemboss-ei-fld` › `label` (13px / 500; `htmlFor`; `labelValue` as `.gemboss-ei-rangeval` at its right edge) · control · `hint` | No label when `label` is undefined / null. `className` (the admin's `togrow`), `style`. |
771
773
  | `Seg` | `.gemboss-ei-seg[role=group]` › `button[aria-pressed][data-value]` (`.on`) | A click hands back the option's value; `label` names the group; `style` on the group. |
772
774
  | `ColorControl` | `.gemboss-ei-color` › `input[type=color].gemboss-ei-swatch` + text box | The swatch commits on every change; the text box keeps a draft and commits on blur (every blur) or Enter. Anything but a 6-digit hex disables the swatch, with a title saying why (`swatchState`). |
@@ -1228,6 +1230,32 @@ import { AppShell, BrandLockup, ServiceSwitcher } from "@gemboss/ui";
1228
1230
  **When.** On the topbar of an INTERNAL service, beside its logo. Not on a public surface (GemFeedback's board,
1229
1231
  GemCommunity, a storefront): the panel names internal tools and links to their admin pages.
1230
1232
 
1233
+ **The same panel for any list: `Switcher` (0.40.0).** `ServiceSwitcher` is `Switcher` with the Gem services. A public
1234
+ surface that has its own list to switch between (GemCommunity's groups: Chris 20/10/2026, "kèm hình ảnh minh hoạ, tên và
1235
+ description … như các dịch vụ gemboss") uses `Switcher` with its own items, never the services:
1236
+
1237
+ ```tsx
1238
+ import { Switcher } from "@gemboss/ui";
1239
+
1240
+ <Switcher
1241
+ items={[
1242
+ { id: "gemcommunity", name: "GemCommunity", description: "The GemBoss users' group.", href: "/c/gemcommunity", group: "mine", media: { glyph: "◈" }, tint: "iris" },
1243
+ { id: "sellers-cn", name: "China Brands Go Global", description: "Chinese brands going global.", href: "/c/sellers-cn/signup", group: "discover", media: { src: "/covers/cn.jpg" }, badge: "Apply" },
1244
+ ]}
1245
+ groups={[{ id: "mine", title: "Your groups" }, { id: "discover", title: "Discover" }]}
1246
+ current="gemcommunity"
1247
+ labels={{ trigger: "Switch group", title: "Communities", search: "Find a group" }}
1248
+ foot={<button type="button" className="ui-btn">Create a community</button>}
1249
+ />
1250
+ ```
1251
+
1252
+ | | |
1253
+ | --- | --- |
1254
+ | Items | `id`, `name`, `href` (same tab), `description` (two lines at most), `group` (a `groups` id; unknown or none: a last group without a heading), `tint` (`SERVICE_TINTS`), `badge` (a chip after the name: "Apply"). The picture: `media: { src }` (an image) or `media: { glyph }` (an emoji or two letters, drawn 44px on a panel of the tint) makes the tile a card; without `media`, the service kind: a 40px mark (`icon` or `glyph`). |
1255
+ | A card | `a.gb-svc__tile.gb-svc__tile--media` > `span.gb-svc__media` (16:9 across the tile, an `<img alt="" loading="lazy">` covering it, or `span.gb-svc__media-glyph`), the name (+ `span.gb-svc__badge`), the line. A group with a card lays its grid out as `gb-svc__grid--media`: 208px columns, two a row on a phone. |
1256
+ | Props | `items`, `groups`, `current`, `labels` (as ServiceSwitcher's), `search`, `allHref` (default none) or `foot` (the app's own foot, actions; wins), `triggerIcon` (default `apps`), `className`. Everything else (the dialog, where it opens, search, keys, the phone sheet) is ServiceSwitcher's, because it is the same code. |
1257
+
1258
+
1231
1259
  | | |
1232
1260
  | --- | --- |
1233
1261
  | Props | `current` (a `ServiceId`, required): the service this page belongs to. `services` (default `GEM_SERVICES`; another list keeps the groups' order). `labels`: `trigger` ("Switch service", the button's name and tooltip), `title` ("Gem services"), `close`, `search` ("Find a service"), `noMatch` ("No service matches “{query}”."), `all`. `allHref`: the foot link, default `SERVICES_HEALTH_URL` (GemFactory's `/services`, every service and its health); `null` draws no foot. `search` (default true). `className` (the button). `BrandLockup switcher={…}` takes the same, less `className`. Every prop is plain data, so a server component can pass it (BrandLockup is not a client module; ServiceSwitcher is) |
@@ -1540,6 +1568,69 @@ import { Avatar, Dialog, Leaderboard, Podium, SearchDialog, ThemeToggle, themeSc
1540
1568
  (after the name, e.g. a level) and `id` (a `#me` anchor). `title` may be left out on a page whose own
1541
1569
  `<h1>` names the board; pass `label` then. Screen-reader text uses `.gb-sr-only`, styled in portal.css.
1542
1570
 
1571
+ ## The public site layer (0.40.0): `SiteHeader`, `SiteBrand`, links, buttons, footer
1572
+
1573
+ **Which layer.** A PUBLIC page (gemboss.ai, the docs, news, a product's home page before sign-in) uses this layer. An APP
1574
+ surface (the admin, a portal behind sign-in, GemFeedback's board) keeps `Topbar` + `BrandLockup`. Chris 20/10/2026: "có gemboss
1575
+ ui thì có gemboss website ui"; measured the same day, the public pages had drifted on the same detail: the logo name 18px on
1576
+ gemboss.ai and the docs, 16px on news, 13.3px on the community home; the bar 65 / 57 / 69 / 56px.
1577
+
1578
+ **Two lockup sizes, one gem** (Chris 20/10/2026): site 18px (`SiteBrand`, `.gb-brand--site`), app 13.3px (`BrandLockup`). The gem
1579
+ is the same 26px-tall drawing (`brand-gem.tsx`). The site name stays on a phone; the app one hides at ≤460px, where an app's bar
1580
+ is full of controls. `SiteBrand` does not go through `BrandLockup`: brand.tsx brings the ServiceSwitcher, whose panel names
1581
+ internal tools and their admin URLs, and a public page must not carry it.
1582
+
1583
+ The values are gemboss.ai's own (`website/assets/styles.css`): the bar 64px with the content in a 1132px column, nav links
1584
+ 14px/500, the primary button ink (dark: it inverts with `--gemboss-primary`) 40px and 36px in the bar, footer headings 13px/600
1585
+ and links 14px. Every colour is a `--gemboss-*` token, so `theme-dark.css` applies unchanged.
1586
+
1587
+ ```tsx
1588
+ import { SiteBrand, SiteHeader } from "@gemboss/ui"; // brings @gemboss/ui/site.css
1589
+
1590
+ <SiteHeader
1591
+ brand={<SiteBrand name="GemBoss" href="/" />}
1592
+ nav={<><a className="gb-site-link" href="/product" aria-current="page">Product</a><a className="gb-site-link" href="/pricing">Pricing</a></>}
1593
+ actions={<><a className="gb-site-link" href="/login">Sign in</a><a className="gb-site-btn gb-site-btn--primary" href="/start">Start free</a></>} />
1594
+ ```
1595
+
1596
+ **Plain HTML** needs only the tokens and `site.css`, NOT the core (which resets the page): site.css repeats the lockup's base
1597
+ rules under `gb-brand--site`, so gemboss.ai and the docs can adopt it beside their own stylesheet (`site-browser.test.tsx`
1598
+ "standalone" holds it). Link `gemboss-tokens.css` (or map the `--gemboss-*` tokens the file reads onto your own palette, which
1599
+ also carries your dark mode), then `site.css`, and write the same markup (the catalog's "Trang công khai" section shows it
1600
+ rendered; copy the gem's `<svg>` from there):
1601
+
1602
+ ```html
1603
+ <header class="gb-site-header"><div class="gb-site-header__in">
1604
+ <a class="gb-brand gb-brand--site" href="/" aria-label="GemBoss home"><svg class="gb-brand-gem" …></svg><span class="gb-brand-name">GemBoss</span></a>
1605
+ <nav class="gb-site-nav" aria-label="Main"><a class="gb-site-link" href="/pricing">Pricing</a></nav>
1606
+ <div class="gb-site-actions"><a class="gb-site-link" href="/login">Sign in</a><a class="gb-site-btn gb-site-btn--primary" href="/start">Start free</a></div>
1607
+ </div></header>
1608
+
1609
+ <footer class="gb-site-footer"><div class="gb-site-wrap">
1610
+ <div class="gb-site-footer__grid">
1611
+ <div><!-- brand + a line --></div>
1612
+ <div class="gb-site-footer__col"><h4>Product</h4><a href="/funnels">Adaptive funnels</a></div>
1613
+ </div>
1614
+ <div class="gb-site-footer__bottom"><span>© 2026 GemBoss</span><span><a href="/terms">Terms</a></span></div>
1615
+ </div></footer>
1616
+ ```
1617
+
1618
+ | Class | What |
1619
+ |---|---|
1620
+ | `gb-site-header`, `gb-site-header__in` | the sticky bar (64px) and its 1132px row; ≤900px the nav folds away, ≤380px the bar's text actions too |
1621
+ | `gb-brand gb-brand--site` | the lockup at the site size (`SiteBrand` writes it) |
1622
+ | `gb-site-nav`, `gb-site-link` | the bar's links, 14px/500; `aria-current="page"` is 600 |
1623
+ | `gb-site-actions` | the right side of the bar |
1624
+ | `gb-site-btn` + `--primary` / `--secondary` | the site's buttons: 40px, 36px in the bar, 44px under a finger or at ≤768px |
1625
+ | `gb-site-footer`, `__grid`, `__col`, `__bottom`, `gb-site-wrap` | the footer: a brand column + link columns, then one row of small print |
1626
+
1627
+ What it holds, measured in Chromium (`site-browser.test.tsx`): the bar 64px; the site name 18px and the app name 13.3px side by
1628
+ side; at 390px the site name shown, every control 44px, the nav folded; no sideways scroll at 1280, 390 or 320 (at 320 it was 15px
1629
+ too wide with "Sign in" in the bar, hence the ≤380px rule).
1630
+
1631
+ Not in this layer yet: a menu button for the folded nav (each site keeps its own), and the page sections (hero, features, FAQ,
1632
+ pricing, CTA band), which are the next step.
1633
+
1543
1634
  ## Reactions (0.21.0): one emoji set and one chip for the fleet
1544
1635
 
1545
1636
  Chris 06/10/2026: "reaction các thứ comment nên chuẩn hoá một kiểu, bên gemcare cũng có, nên đưa vào gemboss ui". GemCare's
@@ -1558,6 +1649,458 @@ message with reactions does not grow by a full line. A plain-HTML surface writes
1558
1649
  `reactions.css`, and the catalog's "Reactions" section).
1559
1650
 
1560
1651
 
1652
+ ## The team chat layer (20/10/2026): `TeamChat`, the rail, messages, composer, thread and people
1653
+
1654
+ Chris 20/10/2026: bring GemCare's Slack-like channel chat to GemCommunity, and make the two inherit the same UI so the next
1655
+ upgrade happens in one place. GemCare built the chat (its `public/assets/inbox.{html,css,js}`, origin/main 20/10/2026); its
1656
+ look and behaviour moved here: the channel rail with unread and @mention badges, day separators, the "New messages" line,
1657
+ a person's consecutive messages grouped into one turn, the reply quote, "(edited)", the read receipt, reactions, the thread
1658
+ preview under a message, the hover action bar, the thread panel on the right, the round "jump to the latest". New for the
1659
+ community: the PEOPLE panel, and a layout that gives a small screen its space back.
1660
+
1661
+ **What it needs.** `team-chat.css`, its own entry (the React components import it); `Avatar` from `portal.css` and
1662
+ `Reactions` from `reactions.css` (reused, not redrawn: the components import them too); the kit's `ui-btn` and `ui-check`
1663
+ from the core's `components.css`; `theme-dark.css` for the dark palette. Every colour in `team-chat.css` is a `--gemboss-*`
1664
+ token, so `data-theme="dark"` turns it dark unchanged. It holds no rule on `:root`, `html` or `body`.
1665
+
1666
+ | Component | Draws | Takes |
1667
+ |---|---|---|
1668
+ | `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"`) |
1669
+ | `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` |
1670
+ | `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` |
1671
+ | `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` |
1672
+ | `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` |
1673
+ | `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` |
1674
+ | `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` |
1675
+ | `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` |
1676
+
1677
+ Props only: no fetching, no store, no router. The app owns the data and the actions; the components keep only the screen's
1678
+ own state (the reaction picker a message opened, the composer's text when the app does not hold it, focus). Every word they
1679
+ write themselves is in `TEAM_CHAT_LABELS`; pass `labels` (any subset) to translate. `teamChatRows(messages, { meId,
1680
+ isUnread })` (plain TS, `team-chat-data.ts`) says where the day separators and the "New messages" line go and which
1681
+ messages are `grouped`: same author, same day, under 5 minutes apart, not a reply, and not the first new one (GemCare's
1682
+ rules). `chatDayWord` gives "Today" / "Yesterday"; older days the app formats in its own locale.
1683
+
1684
+ **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
1685
+ window minus the shell's rail), with container queries. `TEAM_CHAT_DRAWER_MAX` (1100) and `TEAM_CHAT_PHONE_MAX` (760) are the
1686
+ same numbers for JS.
1687
+
1688
+ | Width of `.gb-tc` | Right panel | Rail / conversation |
1689
+ |---|---|---|
1690
+ | > 1100px | a third column: a thread 380px (`--gb-tc-thread-w`), the people panel 260px (`--gb-tc-people-w`), 56px collapsed | rail 240px (`--gb-tc-rail-w`) + conversation |
1691
+ | ≤ 1100px | a drawer over the conversation from the right (thread 380px, people 300px), with a scrim; a collapsed people panel is a CLOSED drawer | the conversation keeps its whole width |
1692
+ | ≤ 760px | a sheet over the whole chat | `data-view="list"`: the rail alone; `"conversation"`: the conversation alone, with a back button in its head |
1693
+
1694
+ While the panel is a drawer, `TeamChat` moves focus into it on open, makes the rail and the conversation `inert` (Tab stays
1695
+ in the drawer), closes it on Escape or a press on the scrim (`onClosePanel`), and gives focus back to what opened it.
1696
+ Escape closes a thread at any width. The message list is a container too: where IT is narrower than 560px (the thread
1697
+ panel, a docked window) the action bar drops its quick reactions and the rows their side padding.
1698
+
1699
+ **Under a finger** (`pointer: coarse`) and at phone width every control is 44px: rail rows, people rows, the quote, the
1700
+ thread preview, the action bar's buttons, the composer's buttons (the jump button 48px); under a mouse the rail rows and
1701
+ the action bar are 32px, as the admin's density. The composer's field is 16px there (iOS zooms the page under it). With
1702
+ no hover (`hover: none`) the action bar is ONE TAP away: a tap focuses the message row (`tabindex="-1"`) and the bar shows.
1703
+ GemCare's bar was always on under a finger, a row of 44px buttons under every message; this gives that height back to
1704
+ the conversation.
1705
+
1706
+ ### Plain HTML: the markup contract
1707
+
1708
+ A surface with no React (GemCare) writes exactly this, links `team-chat.css` beside the core, `portal.css` and
1709
+ `reactions.css`, and does the behaviour itself (below). `team-chat-contract.test.tsx` renders every component with the data
1710
+ of the block named after it and holds the two to the same tree (tags, classes, attributes, text; `<svg>…</svg>` is an
1711
+ icon from the set, `Icon` in React, drawn as in the catalog), so this markup and the components cannot drift. Ids here
1712
+ (`tc-…`) are the app's own; keep the pairs (`aria-labelledby` → `id`, `for` → `id`, `aria-controls` → `id`) pointing at
1713
+ each other.
1714
+
1715
+ The screen. `data-panel` names the panel in the right slot (absent: none); the scrim is drawn only while the panel is
1716
+ OPEN (a thread is open; the people panel is not collapsed).
1717
+
1718
+ <!-- team-chat: screen -->
1719
+ ```html
1720
+ <div class="gb-tc" data-view="conversation" data-panel="thread">
1721
+ <nav class="gb-tc-rail" aria-label="Channels">…</nav>
1722
+ <section class="gb-tc-main" aria-label="Conversation">…</section>
1723
+ <div class="gb-tc-scrim" aria-hidden="true">
1724
+ </div>
1725
+ <aside class="gb-tc-thread" aria-label="Thread">…</aside>
1726
+ </div>
1727
+ ```
1728
+
1729
+ The rail. The active row has `aria-current` (`"page"` on a link, `"true"` on a button); a row with unread messages adds
1730
+ `gb-tc-channel--unread`; its `aria-label` says the counts (a badge cannot be read out). A DM row may carry a presence dot.
1731
+
1732
+ <!-- team-chat: rail -->
1733
+ ```html
1734
+ <nav class="gb-tc-rail" aria-label="Channels">
1735
+ <div class="gb-tc-rail__head">
1736
+ <h2 class="gb-tc-rail__title">Acme team</h2>
1737
+ <div class="gb-tc-rail__actions">
1738
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon" aria-label="New message">
1739
+ <svg>…</svg>
1740
+ </button>
1741
+ </div>
1742
+ </div>
1743
+ <div class="gb-tc-rail__scroll">
1744
+ <div class="gb-tc-rail__section" role="group" aria-labelledby="tc-channels-h">
1745
+ <h3 class="gb-tc-rail__heading" id="tc-channels-h">Channels</h3>
1746
+ <ul class="gb-tc-rail__list">
1747
+ <li>
1748
+ <button type="button" class="gb-tc-channel" aria-current="true" aria-label="general">
1749
+ <span class="gb-tc-channel__glyph" aria-hidden="true">
1750
+ <svg>…</svg>
1751
+ </span>
1752
+ <span class="gb-tc-channel__name">general</span>
1753
+ </button>
1754
+ </li>
1755
+ <li>
1756
+ <button type="button" class="gb-tc-channel gb-tc-channel--unread" aria-label="launch, 2 mentioning you, 5 unread">
1757
+ <span class="gb-tc-channel__glyph" aria-hidden="true">
1758
+ <svg>…</svg>
1759
+ </span>
1760
+ <span class="gb-tc-channel__name">launch</span>
1761
+ <span class="gb-tc-badge gb-tc-badge--mention" aria-hidden="true">@2</span>
1762
+ <span class="gb-tc-badge" aria-hidden="true">5</span>
1763
+ </button>
1764
+ </li>
1765
+ <li>
1766
+ <button type="button" class="gb-tc-channel" aria-label="payroll, private channel">
1767
+ <span class="gb-tc-channel__glyph" aria-hidden="true">
1768
+ <svg>…</svg>
1769
+ </span>
1770
+ <span class="gb-tc-channel__name">payroll</span>
1771
+ </button>
1772
+ </li>
1773
+ </ul>
1774
+ </div>
1775
+ <div class="gb-tc-rail__section" role="group" aria-labelledby="tc-dms-h">
1776
+ <h3 class="gb-tc-rail__heading" id="tc-dms-h">Direct messages</h3>
1777
+ <ul class="gb-tc-rail__list">
1778
+ <li>
1779
+ <a class="gb-tc-channel gb-tc-channel--unread" href="/chat/dm/linh" aria-label="Linh Tran, direct message, 1 unread">
1780
+ <span class="gb-tc-channel__glyph" aria-hidden="true">
1781
+ <svg>…</svg>
1782
+ <span class="gb-tc-presence gb-tc-presence--online">
1783
+ </span>
1784
+ </span>
1785
+ <span class="gb-tc-channel__name">Linh Tran</span>
1786
+ <span class="gb-tc-badge" aria-hidden="true">1</span>
1787
+ </a>
1788
+ </li>
1789
+ </ul>
1790
+ </div>
1791
+ </div>
1792
+ </nav>
1793
+ ```
1794
+
1795
+ The conversation's head. The back button shows only at phone width; the members toggle is `aria-expanded` and points at
1796
+ the people panel.
1797
+
1798
+ <!-- team-chat: header -->
1799
+ ```html
1800
+ <header class="gb-tc-head">
1801
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-head__back" aria-label="Back to channels">
1802
+ <svg>…</svg>
1803
+ </button>
1804
+ <div class="gb-tc-head__text">
1805
+ <h2 class="gb-tc-head__title">
1806
+ <span class="gb-tc-head__glyph" aria-hidden="true">
1807
+ <svg>…</svg>
1808
+ </span>
1809
+ <span class="gb-tc-head__name">general</span>
1810
+ </h2>
1811
+ <p class="gb-tc-head__sub">Everyone on the team is here.</p>
1812
+ </div>
1813
+ <div class="gb-tc-head__actions">
1814
+ <button type="button" class="ui-btn ui-btn--ghost gb-tc-head__members" aria-expanded="false" aria-controls="tc-people" aria-label="Members, 12">
1815
+ <svg>…</svg>
1816
+ <span class="gb-tc-head__count">12</span>
1817
+ </button>
1818
+ </div>
1819
+ </header>
1820
+ ```
1821
+
1822
+ 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`:
1823
+ the time in the avatar's column, shown on hover, and the author for screen readers). The jump button is `hidden` while the
1824
+ reader is at the bottom.
1825
+
1826
+ <!-- team-chat: list -->
1827
+ ```html
1828
+ <div class="gb-tc-body">
1829
+ <div class="gb-tc-log" role="log" aria-live="polite" aria-label="Messages in #general" tabindex="0">
1830
+ <ol class="gb-tc-list">
1831
+ <li class="gb-tc-day">
1832
+ <time class="gb-tc-day__label" datetime="2026-10-20">Today</time>
1833
+ </li>
1834
+ <li class="gb-tc-msg" tabindex="-1">
1835
+ <span class="gb-avatar gb-avatar--32 gb-tc-msg__av" data-tone="4" aria-hidden="true">LT</span>
1836
+ <div class="gb-tc-msg__main">
1837
+ <div class="gb-tc-msg__who">
1838
+ <span class="gb-tc-msg__name">Linh Tran</span>
1839
+ <time class="gb-tc-msg__time" datetime="2026-10-20T09:41:00+07:00">09:41</time>
1840
+ </div>
1841
+ <div class="gb-tc-msg__body">Morning! The launch checklist is in the doc.</div>
1842
+ </div>
1843
+ </li>
1844
+ <li class="gb-tc-new">
1845
+ <span class="gb-tc-new__label" role="separator" aria-label="New messages">New messages</span>
1846
+ </li>
1847
+ <li class="gb-tc-msg" tabindex="-1">
1848
+ <span class="gb-avatar gb-avatar--32 gb-tc-msg__av" data-tone="0" aria-hidden="true">QL</span>
1849
+ <div class="gb-tc-msg__main">
1850
+ <div class="gb-tc-msg__who">
1851
+ <span class="gb-tc-msg__name">Quan Le</span>
1852
+ <time class="gb-tc-msg__time" datetime="2026-10-20T09:44:00+07:00">09:44</time>
1853
+ </div>
1854
+ <div class="gb-tc-msg__body">On it.</div>
1855
+ </div>
1856
+ </li>
1857
+ <li class="gb-tc-msg gb-tc-msg--grouped" tabindex="-1">
1858
+ <time class="gb-tc-msg__hover-time" datetime="2026-10-20T09:45:00+07:00">09:45</time>
1859
+ <div class="gb-tc-msg__main">
1860
+ <span class="gb-tc-sr">Quan Le, 09:45: </span>
1861
+ <div class="gb-tc-msg__body">Pushed the fix for the cart drawer.</div>
1862
+ </div>
1863
+ </li>
1864
+ </ol>
1865
+ </div>
1866
+ <button type="button" class="gb-tc-jump" aria-label="Jump to the latest message, 3 new">
1867
+ <svg>…</svg>
1868
+ <span class="gb-tc-jump__n" aria-hidden="true">3</span>
1869
+ </button>
1870
+ </div>
1871
+ ```
1872
+
1873
+ A message with everything: the quote it answers, "(edited)", a read receipt (the viewer's own messages; a ring, a slice
1874
+ per share, filled and ticked when everyone has read it), reactions (the package's `Reactions`; none, no row), the thread
1875
+ preview, the action bar. Modifiers on the `li`: `gb-tc-msg--mention` (calls the viewer: an amber bar on the left),
1876
+ `--highlight` (jumped to), `--sending` (faded, no bar, `p.gb-tc-msg__status` "Sending…"), `--failed` (the status in the danger
1877
+ colour), `--active` (the reaction picker is open: the bar stays), `--editing` (the app's edit box in `div.gb-tc-msg__editor`
1878
+ in place of the body, no bar). Slots: `div.gb-tc-msg__files` (pictures, files), `div.gb-tc-msg__extra` (link cards), the
1879
+ app's own buttons at the end of `div.gb-tc-acts`. "Add reaction" opens the package's picker (`ui-reaction-picker`) in the
1880
+ flow under the body; the quick pair (👍 ✅) is `gb-tc-act--quick`.
1881
+
1882
+ <!-- team-chat: message -->
1883
+ ```html
1884
+ <li class="gb-tc-msg" data-msg-id="42" tabindex="-1">
1885
+ <span class="gb-avatar gb-avatar--32 gb-tc-msg__av" data-tone="7" aria-hidden="true">MP</span>
1886
+ <div class="gb-tc-msg__main">
1887
+ <button type="button" class="gb-tc-quote">
1888
+ <span class="gb-tc-quote__who">Linh Tran</span>
1889
+ <span class="gb-tc-quote__text">Can someone own the hero copy?</span>
1890
+ </button>
1891
+ <div class="gb-tc-msg__who">
1892
+ <span class="gb-tc-msg__name">Mai Pham</span>
1893
+ <time class="gb-tc-msg__time" datetime="2026-10-20T10:02:00+07:00">10:02</time>
1894
+ </div>
1895
+ <div class="gb-tc-msg__body">Mine, shipping it at noon.<span class="gb-tc-msg__edited">(edited)</span>
1896
+ <span class="gb-tc-receipt gb-tc-receipt--all" role="img" aria-label="Read by everyone (3)" title="Read by everyone (3)">
1897
+ <svg>…</svg>
1898
+ </span>
1899
+ </div>
1900
+ <div class="ui-reactions">
1901
+ <div class="ui-reactions__row">
1902
+ <button type="button" class="ui-reaction" aria-pressed="false" title="Linh, Quan" aria-label="👍, Linh, Quan. Add yours">
1903
+ <span class="ui-reaction__emoji">👍</span>
1904
+ <span class="ui-reaction__names">Linh, Quan</span>
1905
+ </button>
1906
+ <button type="button" class="ui-reaction-add" title="Add reaction" aria-label="Add a reaction to the message from Mai Pham" aria-expanded="false">
1907
+ <svg>…</svg>
1908
+ </button>
1909
+ </div>
1910
+ </div>
1911
+ <button type="button" class="gb-tc-threadprev" aria-label="Open the thread, 3 replies, Last reply 2 hours ago">
1912
+ <span class="gb-tc-threadprev__faces" aria-hidden="true">
1913
+ <span class="gb-avatar gb-avatar--24" data-tone="4" aria-hidden="true">LT</span>
1914
+ <span class="gb-avatar gb-avatar--24" data-tone="0" aria-hidden="true">QL</span>
1915
+ </span>
1916
+ <span class="gb-tc-threadprev__count">3 replies</span>
1917
+ <span class="gb-tc-threadprev__last">Last reply 2 hours ago</span>
1918
+ </button>
1919
+ </div>
1920
+ <div class="gb-tc-acts" role="toolbar" aria-label="Actions for the message from Mai Pham">
1921
+ <button type="button" class="gb-tc-act gb-tc-act--quick" aria-pressed="false" aria-label="Add 👍 on the message from Mai Pham">👍</button>
1922
+ <button type="button" class="gb-tc-act gb-tc-act--quick" aria-pressed="false" aria-label="Add ✅ on the message from Mai Pham">✅</button>
1923
+ <button type="button" class="gb-tc-act" aria-expanded="false" aria-label="More reactions for the message from Mai Pham" title="Add reaction">
1924
+ <svg>…</svg>
1925
+ </button>
1926
+ <button type="button" class="gb-tc-act" aria-label="Reply to the message from Mai Pham" title="Reply">
1927
+ <svg>…</svg>
1928
+ </button>
1929
+ <button type="button" class="gb-tc-act" aria-label="Reply in thread to the message from Mai Pham" title="Reply in thread">
1930
+ <svg>…</svg>
1931
+ </button>
1932
+ </div>
1933
+ </li>
1934
+ ```
1935
+
1936
+ The composer. "Replying to…" sits right above the box; `div.gb-tc-composer__files` and `div.gb-tc-composer__picker` hold
1937
+ the app's files and @mention list; Send is `disabled` while there is nothing to send.
1938
+
1939
+ <!-- team-chat: composer -->
1940
+ ```html
1941
+ <form class="gb-tc-composer">
1942
+ <div class="gb-tc-replying">
1943
+ <span class="gb-tc-replying__text">Replying to Linh Tran: Can someone own the hero copy?</span>
1944
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon" aria-label="Stop replying">
1945
+ <svg>…</svg>
1946
+ </button>
1947
+ </div>
1948
+ <div class="gb-tc-composer__files">…</div>
1949
+ <div class="gb-tc-composer__picker">…</div>
1950
+ <div class="gb-tc-composer__box">
1951
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-composer__attach" aria-label="Attach a file">
1952
+ <svg>…</svg>
1953
+ </button>
1954
+ <label class="gb-tc-sr" for="tc-input">Message</label>
1955
+ <textarea id="tc-input" class="gb-tc-composer__input" rows="1" maxlength="4000" placeholder="Message #general">
1956
+ </textarea>
1957
+ <button type="submit" class="ui-btn ui-btn--primary ui-btn--icon gb-tc-composer__send" aria-label="Send" disabled="">
1958
+ <svg>…</svg>
1959
+ </button>
1960
+ </div>
1961
+ </form>
1962
+ ```
1963
+
1964
+ The thread panel: the root, the "N replies" rule, the replies flat, a composer with "Also send to the channel".
1965
+
1966
+ <!-- team-chat: thread -->
1967
+ ```html
1968
+ <aside id="tc-thread" class="gb-tc-thread" aria-labelledby="tc-thread-title" tabindex="-1">
1969
+ <header class="gb-tc-panel-head">
1970
+ <div class="gb-tc-panel-head__text">
1971
+ <h2 class="gb-tc-panel-head__title" id="tc-thread-title">Thread</h2>
1972
+ <p class="gb-tc-panel-head__sub">#general</p>
1973
+ </div>
1974
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-panel-head__close" aria-label="Close thread">
1975
+ <svg>…</svg>
1976
+ </button>
1977
+ </header>
1978
+ <div class="gb-tc-body">
1979
+ <div class="gb-tc-log" role="log" aria-live="polite" aria-labelledby="tc-thread-title" tabindex="0">
1980
+ <ol class="gb-tc-list">
1981
+ <li class="gb-tc-msg" tabindex="-1">
1982
+ <span class="gb-avatar gb-avatar--32 gb-tc-msg__av" data-tone="4" aria-hidden="true">LT</span>
1983
+ <div class="gb-tc-msg__main">
1984
+ <div class="gb-tc-msg__who">
1985
+ <span class="gb-tc-msg__name">Linh Tran</span>
1986
+ <time class="gb-tc-msg__time" datetime="2026-10-20T09:41:00+07:00">09:41</time>
1987
+ </div>
1988
+ <div class="gb-tc-msg__body">Can someone own the hero copy?</div>
1989
+ </div>
1990
+ </li>
1991
+ <li class="gb-tc-thread__count">
1992
+ <span>1 reply</span>
1993
+ </li>
1994
+ <li class="gb-tc-msg" tabindex="-1">
1995
+ <span class="gb-avatar gb-avatar--32 gb-tc-msg__av" data-tone="7" aria-hidden="true">MP</span>
1996
+ <div class="gb-tc-msg__main">
1997
+ <div class="gb-tc-msg__who">
1998
+ <span class="gb-tc-msg__name">Mai Pham</span>
1999
+ <time class="gb-tc-msg__time" datetime="2026-10-20T10:02:00+07:00">10:02</time>
2000
+ </div>
2001
+ <div class="gb-tc-msg__body">Mine, shipping it at noon.</div>
2002
+ </div>
2003
+ </li>
2004
+ </ol>
2005
+ </div>
2006
+ </div>
2007
+ <form class="gb-tc-composer">
2008
+ <div class="gb-tc-composer__box">
2009
+ <label class="gb-tc-sr" for="tc-thread-input">Reply to thread</label>
2010
+ <textarea id="tc-thread-input" class="gb-tc-composer__input" rows="1" maxlength="4000" placeholder="Reply…">
2011
+ </textarea>
2012
+ <button type="submit" class="ui-btn ui-btn--primary ui-btn--icon gb-tc-composer__send" aria-label="Send" disabled="">
2013
+ <svg>…</svg>
2014
+ </button>
2015
+ </div>
2016
+ <label class="ui-check gb-tc-composer__also">
2017
+ <input type="checkbox"/>
2018
+ <span>Also send to #general</span>
2019
+ </label>
2020
+ </form>
2021
+ </aside>
2022
+ ```
2023
+
2024
+ The people panel. Collapsed: `data-collapsed` on the `aside`, the toggle `aria-expanded="false"` and labelled "Show
2025
+ members", no `gb-tc-panel-head__actions`; the names stay in the accessibility tree (clipped, not removed). A row is a link,
2026
+ a button, or a `div` when it does nothing; `gb-tc-person--offline` fades it. On its own (outside `.gb-tc`, e.g. an app
2027
+ shell's right column) it fills its column's height.
2028
+
2029
+ <!-- team-chat: people -->
2030
+ ```html
2031
+ <aside id="tc-people" class="gb-tc-people" aria-labelledby="tc-people-title" tabindex="-1">
2032
+ <header class="gb-tc-panel-head">
2033
+ <div class="gb-tc-panel-head__text">
2034
+ <h2 class="gb-tc-panel-head__title" id="tc-people-title">Members</h2>
2035
+ </div>
2036
+ <button type="button" class="ui-btn ui-btn--ghost ui-btn--icon gb-tc-people__toggle" aria-expanded="true" aria-controls="tc-people-body" aria-label="Hide members" title="Hide members">
2037
+ <svg>…</svg>
2038
+ </button>
2039
+ </header>
2040
+ <div class="gb-tc-people__body" id="tc-people-body">
2041
+ <div class="gb-tc-people__section" role="group" aria-labelledby="tc-online-h">
2042
+ <h3 class="gb-tc-people__heading" id="tc-online-h">Online — 2</h3>
2043
+ <ul class="gb-tc-people__list">
2044
+ <li>
2045
+ <button type="button" class="gb-tc-person" title="Linh Tran">
2046
+ <span class="gb-tc-person__av">
2047
+ <span class="gb-avatar gb-avatar--32" data-tone="4" aria-hidden="true">LT</span>
2048
+ <span class="gb-tc-presence gb-tc-presence--online">
2049
+ <span class="gb-tc-sr">Online</span>
2050
+ </span>
2051
+ </span>
2052
+ <span class="gb-tc-person__text">
2053
+ <span class="gb-tc-person__name">Linh Tran</span>
2054
+ <span class="gb-tc-person__sub">Support lead</span>
2055
+ </span>
2056
+ <span class="gb-tc-person__role">Admin</span>
2057
+ </button>
2058
+ </li>
2059
+ <li>
2060
+ <a class="gb-tc-person" href="/u/quan" title="Quan Le">
2061
+ <span class="gb-tc-person__av">
2062
+ <span class="gb-avatar gb-avatar--32" data-tone="0" aria-hidden="true">QL</span>
2063
+ <span class="gb-tc-presence gb-tc-presence--away">
2064
+ <span class="gb-tc-sr">Away</span>
2065
+ </span>
2066
+ </span>
2067
+ <span class="gb-tc-person__text">
2068
+ <span class="gb-tc-person__name">Quan Le</span>
2069
+ <span class="gb-tc-person__sub">In a meeting</span>
2070
+ </span>
2071
+ </a>
2072
+ </li>
2073
+ </ul>
2074
+ </div>
2075
+ <div class="gb-tc-people__section" role="group" aria-labelledby="tc-members-h">
2076
+ <h3 class="gb-tc-people__heading" id="tc-members-h">Members — 1</h3>
2077
+ <ul class="gb-tc-people__list">
2078
+ <li>
2079
+ <div class="gb-tc-person gb-tc-person--offline" title="Bao Ngo">
2080
+ <span class="gb-tc-person__av">
2081
+ <span class="gb-avatar gb-avatar--32" data-tone="6" aria-hidden="true">BN</span>
2082
+ <span class="gb-tc-presence gb-tc-presence--offline">
2083
+ <span class="gb-tc-sr">Offline</span>
2084
+ </span>
2085
+ </span>
2086
+ <span class="gb-tc-person__text">
2087
+ <span class="gb-tc-person__name">Bao Ngo</span>
2088
+ </span>
2089
+ </div>
2090
+ </li>
2091
+ </ul>
2092
+ </div>
2093
+ </div>
2094
+ </aside>
2095
+ ```
2096
+
2097
+ **What a plain-HTML surface does itself** (the React components do it): Enter sends and Shift+Enter is a new line, but not
2098
+ during an IME composition (`isComposing`, keyCode 229); the field grows with its text (`field-sizing: content`; where a
2099
+ browser lacks it, set `height` from `scrollHeight` through CSSOM, which a `style-src 'self'` CSP allows); Escape or the scrim
2100
+ closes a drawer, focus moves into it and back, the rail and the conversation are `inert` meanwhile; the reaction picker's
2101
+ open state (`gb-tc-msg--active`, `aria-expanded` on its button); arrow keys along `div.gb-tc-acts[role=toolbar]`; when to
2102
+ show `.gb-tc-jump` and its count.
2103
+
1561
2104
  ## Action card (0.28.0): an assistant proposes, a person applies
1562
2105
 
1563
2106
  Chris 08/10/2026: the Feedback Assistant lists likely duplicates and merges them, and carries out status, owner, label