@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/CHANGELOG.md +234 -0
- package/README.md +546 -3
- package/catalog/editor.html +15 -13
- package/catalog/index.html +5047 -1109
- package/gemboss-ui.css +3 -3
- package/package.json +3 -1
- package/parts/components.css +2 -2
- package/parts/core.css +1 -1
- package/parts/shell.css +2 -2
- package/src/assistant/chat-markdown.ts +7 -4
- package/src/assistant.css +1 -1
- package/src/brand-gem.tsx +19 -0
- package/src/brand.tsx +4 -7
- package/src/chat-plain.js +2 -2
- package/src/editor/controls.tsx +50 -14
- package/src/editor/index.ts +3 -3
- package/src/editor/panel.tsx +10 -4
- package/src/editor/repeater.tsx +86 -8
- package/src/editor/swatch.ts +20 -0
- package/src/editor-plain.js +1 -1
- package/src/editor-repeater.css +1 -1
- package/src/editor.css +1 -1
- package/src/gemboss-field.css +1 -1
- package/src/icons.tsx +11 -0
- package/src/index.ts +11 -1
- package/src/service-switcher.css +1 -1
- package/src/service-switcher.tsx +108 -31
- package/src/shell.css +1 -1
- package/src/site.css +1 -0
- package/src/site.tsx +50 -0
- package/src/team-chat-data.ts +79 -0
- package/src/team-chat.css +1 -0
- package/src/team-chat.tsx +825 -0
- package/src/ui.css +1 -1
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
|
|
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-
|
|
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` (
|
|
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
|