@gemboss/ui 0.47.0 → 0.50.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 +315 -0
- package/README.md +162 -18
- package/catalog/editor.html +1 -1
- package/catalog/index.html +53 -49
- package/gemboss-ui.css +4 -4
- package/package.json +23 -2
- package/parts/components.css +3 -3
- package/parts/core.css +1 -1
- package/parts/shell.css +2 -2
- package/src/app-shell.css +1 -1
- package/src/assistant/chat.tsx +50 -6
- package/src/assistant/host.tsx +19 -7
- package/src/assistant/index.ts +1 -1
- package/src/assistant/sessions.tsx +2 -1
- package/src/assistant-chat.css +1 -1
- package/src/assistant.css +1 -1
- package/src/auth-signin-plain.js +1 -0
- package/src/auth-sso-plain.js +1 -1
- package/src/auth.tsx +105 -8
- package/src/briefing-card.css +1 -1
- package/src/collections.css +1 -1
- package/src/create-shell.css +1 -0
- package/src/create-shell.tsx +221 -0
- package/src/editor-insert.css +1 -0
- package/src/editor.css +1 -1
- package/src/empty-stage.css +1 -0
- package/src/empty-stage.tsx +31 -0
- package/src/icons.tsx +2 -0
- package/src/index-table.tsx +7 -4
- package/src/index.ts +16 -2
- package/src/kit-plain.css +1 -1
- package/src/kit-plain.js +1 -1
- package/src/language-switcher-plain.js +1 -1
- package/src/language-switcher.css +1 -1
- package/src/language-switcher.tsx +10 -41
- package/src/list-page.css +1 -0
- package/src/list-page.tsx +239 -0
- package/src/menu-extras.css +1 -0
- package/src/menu-keys.ts +199 -0
- package/src/menu-layer.ts +2 -1
- package/src/menu-plain.js +1 -0
- package/src/menu.css +1 -0
- package/src/menu.tsx +83 -35
- package/src/pager-extras.css +1 -0
- package/src/pager.css +1 -0
- package/src/pager.tsx +88 -0
- package/src/portal.css +1 -1
- package/src/portal.tsx +19 -4
- package/src/segmented.tsx +12 -2
- package/src/selection.tsx +3 -1
- package/src/session-rail-plain.js +1 -1
- package/src/settings-dialog.tsx +29 -2
- package/src/shell.css +1 -1
- package/src/stage-path.css +1 -0
- package/src/stage-path.tsx +89 -0
- package/src/stage.css +1 -0
- package/src/stage.tsx +52 -0
- package/src/starting-point.css +1 -0
- package/src/starting-point.tsx +93 -0
- package/src/status-list.css +1 -0
- package/src/status-list.tsx +65 -0
- package/src/status-matrix.css +1 -0
- package/src/status-matrix.tsx +131 -0
- package/src/team-chat.css +1 -1
- package/src/topnav-fold.css +1 -0
- package/src/translation-row.css +1 -0
- package/src/translation-row.tsx +113 -0
- package/src/ui.css +1 -1
package/src/stage.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage (0.49, 22/10/2026): the ONE way the app draws a picture, taken from gemboss.ai's mega menu, where each panel has one
|
|
5
|
+
* visual: a night-blue stage lit with iris from one corner, a faint dot grid fading out, and on it either glass tiles (one
|
|
6
|
+
* picked, white and glowing) or white windows lifted off the stage showing a real page. Chris (22/10): pictures clean, colours
|
|
7
|
+
* in harmony, easy to read, the website's look; the design language lets an illustration light itself, and this is that
|
|
8
|
+
* light, named once here instead of a hex per picture.
|
|
9
|
+
*
|
|
10
|
+
* <Stage> the lit panel (aria-hidden: a picture says nothing a reader needs)
|
|
11
|
+
* <StageWindow title="…" image /> a white window with its three dots, a title and a real screenshot (or bare lines)
|
|
12
|
+
* <StageTile on>…</StageTile> a glass tile; `on` is the one picked
|
|
13
|
+
* <StageBubble me>…</StageBubble> a line of chat: the assistant's in glass, the person's in white
|
|
14
|
+
*/
|
|
15
|
+
import "./stage.css";
|
|
16
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
17
|
+
|
|
18
|
+
const cx = (...p: Array<string | false | null | undefined>) => p.filter(Boolean).join(" ");
|
|
19
|
+
|
|
20
|
+
/** `decorative={false}`: the stage holds words a reader needs (a panel beside a question), so it is not hidden from them. */
|
|
21
|
+
export function Stage({ children, className, style, decorative = true }: {
|
|
22
|
+
children?: ReactNode; className?: string; style?: CSSProperties; decorative?: boolean;
|
|
23
|
+
}) {
|
|
24
|
+
return <div className={cx("ui-stage", className)} style={style} aria-hidden={decorative ? "true" : undefined}>{children}</div>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A white window lifted off the stage: three dots, an optional title, then a real screenshot or two lines of text. */
|
|
28
|
+
export function StageWindow({ title, image, alt = "", children, className, style }: {
|
|
29
|
+
title?: string; image?: string; /** For a stage that is not decorative: what the screenshot shows. */ alt?: string;
|
|
30
|
+
children?: ReactNode; className?: string; style?: CSSProperties;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={cx("ui-stage__win", className)} style={style}>
|
|
34
|
+
<div className="ui-stage__bar"><i /><i /><i />{title ? <b>{title}</b> : null}</div>
|
|
35
|
+
{image ? <img className="ui-stage__shot" src={image} alt={alt} loading="lazy" /> : null}
|
|
36
|
+
{children ? <div className="ui-stage__body">{children}</div> : null}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function StageTile({ on, children }: { on?: boolean; children?: ReactNode }) {
|
|
42
|
+
return <div className={cx("ui-stage__tile", on && "ui-stage__tile--on")}>{children}</div>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function StageBubble({ me, children }: { me?: boolean; children?: ReactNode }) {
|
|
46
|
+
return <div className={cx("ui-stage__bubble", me && "ui-stage__bubble--me")}>{children}</div>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A line of placeholder text inside a window (`short` is half as long). */
|
|
50
|
+
export function StageLine({ short, strong }: { short?: boolean; strong?: boolean }) {
|
|
51
|
+
return <span className={cx("ui-stage__line", short && "ui-stage__line--short", strong && "ui-stage__line--strong")} />;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.ui-start{display:flex;flex-direction:column;gap:20px}.ui-start__head{display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center}.ui-start__title{margin:0;font-size:var(--gemboss-fs-h2, 20px);font-weight:600;color:var(--ui-text)}.ui-start__sub{margin:0;font-size:var(--gemboss-fs-body, 14px);color:var(--ui-text-muted)}.ui-start__doors{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:16px}.ui-start__door{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--ui-border);border-radius:var(--gemboss-radius-xl, 16px);background:var(--ui-surface)}.ui-start__name{margin:4px 0 0;font-size:var(--gemboss-fs-h3, 16px);font-weight:600;color:var(--ui-text)}.ui-start__line{margin:0;flex:1 1 auto;font-size:var(--gemboss-fs-body, 14px);line-height:1.5;color:var(--ui-text-muted)}.ui-start__go{align-self:stretch;margin-top:8px}.ui-start__stage{box-sizing:border-box;height:152px}.ui-start__stage--fan{justify-content:flex-end;align-items:center;padding-bottom:0}.ui-start__stage .ui-start__win{position:absolute;top:24px;bottom:-12px;width:58%}.ui-start__stage .ui-start__win--front{left:28%}.ui-start__stage .ui-start__win--back{left:12%;transform:rotate(-6deg);transform-origin:50% 100%;opacity:.85}.ui-start__stage .ui-start__win .ui-stage__shot{flex:1 1 auto;min-height:0;object-fit:cover;object-position:50% 0}.ui-start__slot{display:block;margin:8px;padding:8px;text-align:center;border:1px dashed var(--gemboss-divider, #e5e7ea);border-radius:var(--gemboss-radius-sm, 6px);font-size:var(--gemboss-fs-micro, 11px);color:var(--gemboss-text-muted, #686b71)}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StartingPoint (0.49, 22/10/2026): "Choose your starting point". The same three doors wherever something new is made: ask the
|
|
5
|
+
* assistant, start from a template, start blank. Learned from Replo, where Plans, Tasks, Skills and reports all open this way, each
|
|
6
|
+
* door with a small picture of what happens behind it (the chat door shows a two-line exchange). One shape everywhere teaches the
|
|
7
|
+
* person once.
|
|
8
|
+
*
|
|
9
|
+
* At most three doors (Chris's rule for any choice). Each picture is a Stage (stage.tsx): the website's mega-menu visual, a lit
|
|
10
|
+
* night-blue panel with glass and white windows. A door may pass real screenshots (`images`): the template door fans two of them,
|
|
11
|
+
* the chat door shows the page it is about to draw.
|
|
12
|
+
*/
|
|
13
|
+
import "./starting-point.css";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
import { Stage, StageBubble, StageLine, StageTile, StageWindow } from "./stage";
|
|
16
|
+
|
|
17
|
+
export type StartingArt = "chat" | "template" | "blank";
|
|
18
|
+
|
|
19
|
+
export interface StartingDoor {
|
|
20
|
+
key: string;
|
|
21
|
+
art: StartingArt;
|
|
22
|
+
title: string;
|
|
23
|
+
/** One line on what happens behind the door. */
|
|
24
|
+
line: string;
|
|
25
|
+
/** The button's words ("Ask Gem", "Browse templates", "Start blank"). */
|
|
26
|
+
action: string;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
/** The chat door's picture: `ask` is the first line, `answer` the second. By default Gem asks and the person answers (a door
|
|
29
|
+
* Gem builds behind); `from: "me"` when the person asks and Gem answers. `step` is the tile under both, what happens next
|
|
30
|
+
* (default "Drafting"). */
|
|
31
|
+
chat?: { ask: string; answer: string; from?: "gem" | "me"; step?: string };
|
|
32
|
+
/** Real screenshots for the picture (the template door shows two, the chat door one). Without them, windows draw lines. */
|
|
33
|
+
images?: readonly string[];
|
|
34
|
+
busy?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Art({ door }: { door: StartingDoor }) {
|
|
39
|
+
const [first, second] = door.images ?? [];
|
|
40
|
+
if (door.art === "chat") {
|
|
41
|
+
return (
|
|
42
|
+
<Stage className="ui-start__stage">
|
|
43
|
+
<StageBubble me={door.chat?.from === "me"}>{door.chat?.ask ?? "What are we making?"}</StageBubble>
|
|
44
|
+
<StageBubble me={door.chat?.from !== "me"}>{door.chat?.answer ?? "…"}</StageBubble>
|
|
45
|
+
<StageTile on><em>01</em>{door.chat?.step ?? (first ? "Drafting your page" : "Drafting")}</StageTile>
|
|
46
|
+
</Stage>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (door.art === "template") {
|
|
50
|
+
return (
|
|
51
|
+
<Stage className="ui-start__stage ui-start__stage--fan">
|
|
52
|
+
<StageWindow className="ui-start__win ui-start__win--back" image={second}>{second ? null : <><StageLine strong /><StageLine /><StageLine short /></>}</StageWindow>
|
|
53
|
+
<StageWindow className="ui-start__win ui-start__win--front" image={first}>{first ? null : <><StageLine strong /><StageLine /><StageLine short /></>}</StageWindow>
|
|
54
|
+
</Stage>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
<Stage className="ui-start__stage ui-start__stage--fan">
|
|
59
|
+
<StageWindow className="ui-start__win ui-start__win--front" title="Untitled page">
|
|
60
|
+
<span className="ui-start__slot">+ Add a section</span>
|
|
61
|
+
</StageWindow>
|
|
62
|
+
</Stage>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function StartingPoint({ title = "Choose your starting point", sub, doors }: {
|
|
67
|
+
title?: ReactNode;
|
|
68
|
+
sub?: ReactNode;
|
|
69
|
+
/** One to three doors, in the order a person should consider them. */
|
|
70
|
+
doors: readonly StartingDoor[];
|
|
71
|
+
}) {
|
|
72
|
+
return (
|
|
73
|
+
<section className="ui-start">
|
|
74
|
+
<div className="ui-start__head">
|
|
75
|
+
<h2 className="ui-start__title">{title}</h2>
|
|
76
|
+
{sub ? <p className="ui-start__sub">{sub}</p> : null}
|
|
77
|
+
</div>
|
|
78
|
+
<div className="ui-start__doors">
|
|
79
|
+
{doors.slice(0, 3).map((door) => (
|
|
80
|
+
<div key={door.key} className="ui-start__door">
|
|
81
|
+
<Art door={door} />
|
|
82
|
+
<h3 className="ui-start__name">{door.title}</h3>
|
|
83
|
+
<p className="ui-start__line">{door.line}</p>
|
|
84
|
+
<button type="button" className="ui-btn ui-btn--primary ui-start__go" onClick={door.onClick}
|
|
85
|
+
disabled={door.disabled || door.busy} aria-busy={door.busy || undefined}>
|
|
86
|
+
{door.action}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.ui-statuslist{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,260px),1fr));gap:var(--ui-space-2) var(--ui-space-4);margin:0;padding:0;list-style:none}.ui-statuslist__item{display:flex;align-items:baseline;gap:var(--ui-space-2);min-width:0;font-size:var(--ui-fs-caption);line-height:20px;color:var(--ui-text-muted)}.ui-statuslist__dot{flex:none;align-self:center;width:8px;height:8px;box-sizing:border-box;border:1.5px solid;border-radius:50%}:is(.ui-statuslist__item--ok,.ui-statuslist__item--warning,.ui-statuslist__item--critical)>.ui-statuslist__dot{background:currentColor}.ui-statuslist__item--ok>.ui-statuslist__dot{color:var(--gemboss-success)}.ui-statuslist__item--warning>.ui-statuslist__dot{color:var(--gemboss-warning)}.ui-statuslist__item--critical>.ui-statuslist__dot{color:var(--gemboss-danger)}.ui-statuslist__label{flex:none;font-weight:650;color:var(--ui-text-strong)}.ui-statuslist__item--muted>.ui-statuslist__label{color:var(--ui-text-muted)}.ui-statuslist__text{min-width:0;overflow-wrap:anywhere}.ui-statuslist__text>a{color:inherit;text-underline-offset:2px}.ui-statuslist__text>a:hover{color:var(--ui-text-strong)}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status list — @gemboss/ui, unreleased (after 0.49.0). A quiet strip of system health: GemPress's project home (redesign approved
|
|
3
|
+
* by Chris 23/10/2026) ends with "Backups: nightly · last 22 Oct", "Site rebuild: on publish", "Webhooks: delivering", one line
|
|
4
|
+
* each. GemPress drew it with its own CSS (`gp-sys`); every service has the same few things to say about itself (backups, hooks,
|
|
5
|
+
* keys, the assistant), so the strip lives here.
|
|
6
|
+
*
|
|
7
|
+
* The contract it draws, which every surface keeps: QUIET WHEN FINE. Each item is a dot, a name and one muted sentence; the dot is
|
|
8
|
+
* green, amber or red (`ok`, `warning`, `critical`) or an empty ring (`muted`: off, not applicable), and the sentence says what the
|
|
9
|
+
* colour means, so colour never stands alone. An item with `href` makes its sentence a link to where it is fixed. A banner is for
|
|
10
|
+
* when a person must act: something that needs a person today goes in a `ui-banner` above the page, not in this strip.
|
|
11
|
+
*
|
|
12
|
+
* Presentational and server-renderable: no state, no effect; the tones are the kit's success / warning / danger tokens, so the
|
|
13
|
+
* dark palette follows. `status-list.css` is its own entry; a plain-HTML surface links `@gemboss/ui/status-list.css` and writes
|
|
14
|
+
* the classes listed at the top of that file.
|
|
15
|
+
*/
|
|
16
|
+
import "./status-list.css";
|
|
17
|
+
import React, { type ReactNode } from "react";
|
|
18
|
+
|
|
19
|
+
const cx = (...p: Array<string | false | null | undefined>) => p.filter(Boolean).join(" ");
|
|
20
|
+
|
|
21
|
+
export type StatusListTone = "ok" | "warning" | "critical" | "muted";
|
|
22
|
+
/** Each tone's class, written whole (never built from the tone), so a search for a class finds where it is drawn. */
|
|
23
|
+
const ITEM_CLASS: Readonly<Record<StatusListTone, string>> = {
|
|
24
|
+
ok: "ui-statuslist__item ui-statuslist__item--ok",
|
|
25
|
+
warning: "ui-statuslist__item ui-statuslist__item--warning",
|
|
26
|
+
critical: "ui-statuslist__item ui-statuslist__item--critical",
|
|
27
|
+
muted: "ui-statuslist__item ui-statuslist__item--muted",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface StatusListItem {
|
|
31
|
+
key: string;
|
|
32
|
+
/** The thing: "Backups". */
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
/** One sentence about it: "nightly · last 22 Oct". */
|
|
35
|
+
text: ReactNode;
|
|
36
|
+
tone: StatusListTone;
|
|
37
|
+
/** Where it is fixed or read: the sentence becomes a link. */
|
|
38
|
+
href?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StatusListProps {
|
|
42
|
+
items: readonly StatusListItem[];
|
|
43
|
+
/** The list's name for a screen reader, when no heading names it ("System"). */
|
|
44
|
+
label?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function StatusList({ items, label, className }: StatusListProps) {
|
|
49
|
+
if (!items.length) return null;
|
|
50
|
+
return (
|
|
51
|
+
<ul className={cx("ui-statuslist", className)} aria-label={label}>
|
|
52
|
+
{items.map((it) => {
|
|
53
|
+
// A tone that is not one of the four (plain JS, a typo) is muted: never a colour the sentence does not explain.
|
|
54
|
+
const itemClass = Object.prototype.hasOwnProperty.call(ITEM_CLASS, it.tone) ? ITEM_CLASS[it.tone] : ITEM_CLASS.muted;
|
|
55
|
+
return (
|
|
56
|
+
<li key={it.key} className={itemClass}>
|
|
57
|
+
<span className="ui-statuslist__dot" aria-hidden="true" />
|
|
58
|
+
<b className="ui-statuslist__label">{it.label}</b>
|
|
59
|
+
<span className="ui-statuslist__text">{it.href ? <a href={it.href}>{it.text}</a> : it.text}</span>
|
|
60
|
+
</li>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</ul>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.ui-matrix__caption{caption-side:bottom;padding:var(--ui-space-2) var(--ui-space-3);border-top:1px solid var(--ui-border);text-align:left;font-size:var(--ui-fs-caption);line-height:16px;color:var(--ui-text-muted)}.ui-matrix th.ui-matrix__row{font-weight:550;color:var(--ui-text-strong);white-space:normal;overflow-wrap:anywhere}.ui-matrix__row>a{display:inline-flex;align-items:center;min-height:24px;color:inherit;text-underline-offset:2px}.ui-matrix__cell{color:var(--ui-text-muted)}.ui-matrix__live{font-weight:650;color:var(--ui-text-strong)}.ui-matrix__cell--live .ui-matrix__live{color:var(--gemboss-success-strong)}.ui-matrix__cell--draft .ui-matrix__live{color:var(--gemboss-warning)}.ui-matrix__sub{display:block;margin-top:2px;font-size:var(--ui-fs-caption);line-height:16px}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status matrix — @gemboss/ui, unreleased (after 0.49.0). Rows × columns of status cells: GemPress's language map (redesign approved
|
|
3
|
+
* by Chris 23/10/2026), one row per collection, one column per language, each cell "how many are live of how many there are, and
|
|
4
|
+
* what waits". GemPress drew it with its own classes (`gp-cov`); GemNews and GemCommunity translations will want the same map, and
|
|
5
|
+
* nothing in it is about languages: any "N things × M targets, each partly done" fits (stores × channels, pages × locales).
|
|
6
|
+
*
|
|
7
|
+
* The contract it draws, which every surface keeps:
|
|
8
|
+
* - a cell says "**live** / total", and under it what waits: "5 draft · 2 changed" (and "N missing" when the surface counts them);
|
|
9
|
+
* - its tone is ALL LIVE (green number) or HAS DRAFTS (amber number), nothing else: a cell part live with nothing waiting is plain.
|
|
10
|
+
* Colour never stands alone, there is always the number and the words (`statusMatrixTone` is the rule, pure, for a plain page);
|
|
11
|
+
* - a cell with nothing to count (`null`, absent, or a row whose `total` is 0) is "—", never "0 / 0";
|
|
12
|
+
* - each row's name is its header (`th scope="row"`), a link when it has `href`; the column heads are the surface's words.
|
|
13
|
+
*
|
|
14
|
+
* Built on the kit table (`ui-table-wrap` + `ui-table`, `is-num` for the numbers, all in the core): this entry only adds the cells,
|
|
15
|
+
* the row header and the caption. Presentational and server-renderable: no state, no effect; the colours are tokens, so the dark
|
|
16
|
+
* palette follows. `status-matrix.css` is its own entry; a plain-HTML surface links `@gemboss/ui/status-matrix.css` and writes the
|
|
17
|
+
* classes listed at the top of that file.
|
|
18
|
+
*/
|
|
19
|
+
import "./status-matrix.css";
|
|
20
|
+
import React, { type ReactNode } from "react";
|
|
21
|
+
|
|
22
|
+
const cx = (...p: Array<string | false | null | undefined>) => p.filter(Boolean).join(" ");
|
|
23
|
+
const whole = (n: unknown) => (typeof n === "number" && Number.isFinite(n) ? Math.max(Math.round(n), 0) : 0);
|
|
24
|
+
|
|
25
|
+
/** One cell: how many are live, and what waits. */
|
|
26
|
+
export interface StatusMatrixCell {
|
|
27
|
+
live: number;
|
|
28
|
+
draft: number;
|
|
29
|
+
/** Live, but its source changed since (it needs another look). */
|
|
30
|
+
changed?: number;
|
|
31
|
+
/** Not started. Printed only when the surface counts it. */
|
|
32
|
+
missing?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StatusMatrixColumn {
|
|
36
|
+
key: string;
|
|
37
|
+
label: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface StatusMatrixRow {
|
|
41
|
+
key: string;
|
|
42
|
+
label: ReactNode;
|
|
43
|
+
href?: string;
|
|
44
|
+
/** How many there are in the row; every cell counts against it. */
|
|
45
|
+
total: number;
|
|
46
|
+
/** By column key. `null` or absent: nothing to count in that column ("—"). */
|
|
47
|
+
cells: Readonly<Record<string, StatusMatrixCell | null | undefined>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StatusMatrixLabels {
|
|
51
|
+
draft?: (n: string) => string;
|
|
52
|
+
changed?: (n: string) => string;
|
|
53
|
+
missing?: (n: string) => string;
|
|
54
|
+
/** A cell with nothing to count (default "—"). */
|
|
55
|
+
none?: ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StatusMatrixProps {
|
|
59
|
+
columns: readonly StatusMatrixColumn[];
|
|
60
|
+
rows: readonly StatusMatrixRow[];
|
|
61
|
+
/** What the numbers mean, under the table: "Live / published. Nothing goes live until a person approves it." */
|
|
62
|
+
caption?: ReactNode;
|
|
63
|
+
/** The heading over the rows' names ("Collection"). */
|
|
64
|
+
rowHeading?: ReactNode;
|
|
65
|
+
/** When set, a column after the names with each row's total under this heading ("EN": the source has them all). */
|
|
66
|
+
totalHeading?: ReactNode;
|
|
67
|
+
/** A number as the surface writes it ("1,204"). Default `String`. */
|
|
68
|
+
format?: (n: number) => string;
|
|
69
|
+
labels?: StatusMatrixLabels;
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type StatusMatrixTone = "live" | "draft" | null;
|
|
74
|
+
|
|
75
|
+
/** A cell's tone: every one live, or something waiting (drafts or changed), or neither. Nothing to count has no tone. */
|
|
76
|
+
export function statusMatrixTone(cell: StatusMatrixCell | null | undefined, total: number): StatusMatrixTone {
|
|
77
|
+
const t = whole(total);
|
|
78
|
+
if (!cell || !t) return null;
|
|
79
|
+
if (whole(cell.live) >= t) return "live";
|
|
80
|
+
return whole(cell.draft) + whole(cell.changed) > 0 ? "draft" : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function StatusMatrix({ columns, rows, caption, rowHeading, totalHeading, format = String, labels, className }: StatusMatrixProps) {
|
|
84
|
+
if (!rows.length) return null; // the surface says why the map is empty; an empty grid of headers says nothing
|
|
85
|
+
const words = {
|
|
86
|
+
draft: labels?.draft ?? ((n: string) => `${n} draft`),
|
|
87
|
+
changed: labels?.changed ?? ((n: string) => `${n} changed`),
|
|
88
|
+
missing: labels?.missing ?? ((n: string) => `${n} missing`),
|
|
89
|
+
};
|
|
90
|
+
const none = labels?.none ?? "—";
|
|
91
|
+
const cell = (row: StatusMatrixRow, col: StatusMatrixColumn) => {
|
|
92
|
+
const c = row.cells[col.key];
|
|
93
|
+
const total = whole(row.total);
|
|
94
|
+
if (!c || !total) return <td key={col.key} className="is-num ui-matrix__cell">{none}</td>;
|
|
95
|
+
const tone = statusMatrixTone(c, total);
|
|
96
|
+
const waits = [
|
|
97
|
+
whole(c.draft) ? words.draft(format(whole(c.draft))) : "",
|
|
98
|
+
whole(c.changed) ? words.changed(format(whole(c.changed))) : "",
|
|
99
|
+
whole(c.missing) ? words.missing(format(whole(c.missing))) : "",
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
return (
|
|
102
|
+
<td key={col.key} className={cx("is-num ui-matrix__cell", tone === "live" && "ui-matrix__cell--live", tone === "draft" && "ui-matrix__cell--draft")}>
|
|
103
|
+
<b className="ui-matrix__live">{format(Math.min(whole(c.live), total))}</b>{` / ${format(total)}`}
|
|
104
|
+
{waits.length ? <span className="ui-matrix__sub">{waits.join(" · ")}</span> : null}
|
|
105
|
+
</td>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
return (
|
|
109
|
+
<div className="ui-table-wrap">
|
|
110
|
+
<table className={cx("ui-table ui-matrix", className)}>
|
|
111
|
+
{caption ? <caption className="ui-matrix__caption">{caption}</caption> : null}
|
|
112
|
+
<thead>
|
|
113
|
+
<tr>
|
|
114
|
+
<th scope="col">{rowHeading}</th>
|
|
115
|
+
{totalHeading ? <th scope="col" className="is-num">{totalHeading}</th> : null}
|
|
116
|
+
{columns.map((c) => <th scope="col" className="is-num" key={c.key}>{c.label}</th>)}
|
|
117
|
+
</tr>
|
|
118
|
+
</thead>
|
|
119
|
+
<tbody>
|
|
120
|
+
{rows.map((r) => (
|
|
121
|
+
<tr key={r.key}>
|
|
122
|
+
<th scope="row" className="ui-matrix__row">{r.href ? <a href={r.href}>{r.label}</a> : r.label}</th>
|
|
123
|
+
{totalHeading ? <td className="is-num">{format(whole(r.total))}</td> : null}
|
|
124
|
+
{columns.map((c) => cell(r, c))}
|
|
125
|
+
</tr>
|
|
126
|
+
))}
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
package/src/team-chat.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.gb-tc{container:gb-tc / inline-size;position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr);height:100%;min-height:0;overflow:hidden;box-sizing:border-box;background:var(--gemboss-surface);color:var(--gemboss-text);font-size:var(--gemboss-fs-body)}.gb-tc>.gb-tc-rail{grid-column:1;grid-row:1}.gb-tc>.gb-tc-main{grid-column:2;grid-row:1}.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people{grid-column:3;grid-row:1;height:auto}.gb-tc-main,.gb-tc-rail,.gb-tc-thread,.gb-tc-people{display:flex;flex-direction:column;min-width:0;min-height:0}.gb-tc-main{position:relative}.gb-tc-main>*,.gb-tc-rail>*,.gb-tc-thread>*,.gb-tc-people>*{flex:none}.gb-tc-main>.gb-tc-body,.gb-tc-thread>.gb-tc-body,.gb-tc-rail>.gb-tc-rail__scroll,.gb-tc-people>.gb-tc-people__body{flex:1 1 0;min-height:0}.gb-tc-body,.gb-tc-composer,.gb-tc-rail__list,.gb-tc-people__list{grid-template-columns:minmax(0,1fr)}.gb-tc-scrim{display:none}.gb-tc :is(.gb-tc-thread,.gb-tc-people,.gb-tc-scrim,.gb-tc-replying,.gb-tc-jump,.gb-tc-composer__files,.gb-tc-composer__picker)[hidden]{display:none}.gb-tc-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.gb-tc-rail{width:var(--gb-tc-rail-w, 240px);box-sizing:border-box;border-right:1px solid var(--gemboss-divider);background:var(--gemboss-rail-bg)}.gb-tc-rail__head,.gb-tc-head,.gb-tc-panel-head{display:flex;align-items:center;gap:8px;min-height:56px;min-width:0;box-sizing:border-box;border-bottom:1px solid var(--gemboss-divider)}.gb-tc-rail__head{justify-content:space-between;padding:8px 8px 8px 16px}.gb-tc-rail__title,.gb-tc-panel-head__title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--gemboss-fs-h3);font-weight:650;line-height:1.3;color:var(--gemboss-text-strong)}.gb-tc-rail__actions,.gb-tc-head__actions,.gb-tc-panel-head__actions{flex:none;display:flex;align-items:center;gap:4px}.gb-tc-rail__scroll{overflow-y:auto;overscroll-behavior:contain;padding:8px 8px 16px}.gb-tc-rail__section+.gb-tc-rail__section{margin-top:16px}.gb-tc-rail__heading,.gb-tc-people__heading{margin:0 8px 4px;font-size:var(--gemboss-fs-caption);font-weight:650;line-height:20px;color:var(--gemboss-text-muted)}.gb-tc-rail__list,.gb-tc-people__list{list-style:none;margin:0;padding:0;display:grid;gap:1px}.gb-tc-channel{display:flex;align-items:center;gap:8px;width:100%;min-width:0;min-height:32px;padding:0 8px;box-sizing:border-box;border:0;border-radius:var(--gemboss-radius-md);background:none;cursor:pointer;text-align:left;text-decoration:none;font:inherit;font-size:var(--gemboss-fs-body);color:var(--gemboss-text-muted)}.gb-tc-channel:hover{background:var(--gemboss-hover);color:var(--gemboss-text-strong)}.gb-tc-channel:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-channel--unread{color:var(--gemboss-text-strong);font-weight:650}.gb-tc-channel[aria-current]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong);font-weight:650}.gb-tc-channel__glyph{position:relative;flex:none;display:inline-grid;place-items:center;width:20px;height:20px}.gb-tc-channel__glyph>.gb-tc-presence{right:-3px;bottom:-2px;width:7px;height:7px;border-color:var(--gemboss-rail-bg)}.gb-avatar.gb-tc-channel__av{width:20px;height:20px;font-size:8px}.gb-tc-channel__name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-badge{flex:none;min-width:20px;height:18px;padding:0 6px;box-sizing:border-box;border-radius:var(--gemboss-radius-full);background:var(--gemboss-accent);color:var(--gemboss-on-primary);font-size:var(--gemboss-fs-micro);font-weight:700;line-height:18px;text-align:center;font-variant-numeric:tabular-nums}.gb-tc-badge--mention{background:var(--gemboss-warning-soft);color:var(--gemboss-warning);box-shadow:inset 0 0 0 1px var(--gemboss-warning-border)}.gb-tc-head{padding:8px 12px 8px 16px}.gb-tc .gb-tc-head__back{display:none}.gb-tc-head__text,.gb-tc-panel-head__text{flex:1 1 auto;min-width:0}.gb-tc-head__title{margin:0;display:flex;align-items:center;gap:6px;min-width:0;font-size:var(--gemboss-fs-h3);font-weight:650;line-height:1.3;color:var(--gemboss-text-strong)}.gb-tc-head__glyph{flex:none;display:inline-flex;color:var(--gemboss-text-muted)}.gb-avatar.gb-tc-head__av{flex:none;margin-right:2px}.gb-tc-head__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-head__sub,.gb-tc-panel-head__sub{margin:2px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--gemboss-fs-caption);line-height:16px;color:var(--gemboss-text-muted)}.gb-tc .gb-tc-head__members{gap:6px;padding:0 8px}.gb-tc .gb-tc-head__members[aria-expanded=true]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong)}.gb-tc-head__count{font-variant-numeric:tabular-nums}.gb-tc-body{position:relative;display:grid;grid-template-rows:minmax(0,1fr);min-width:0;min-height:0}.gb-tc-log{container:gb-tc-log / inline-size;min-height:0;overflow-y:auto;overscroll-behavior:contain}.gb-tc-log:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-list{list-style:none;margin:0;padding:8px 0 16px;display:flex;flex-direction:column}.gb-tc-list>*{flex-shrink:0}.gb-tc-older,.gb-tc-empty,.gb-tc-status{display:flex;justify-content:center;padding:8px 16px;color:var(--gemboss-text-muted);font-size:var(--gemboss-fs-caption)}.gb-tc-empty{padding-block:32px;font-size:var(--gemboss-fs-body)}.gb-tc-status{justify-content:flex-start;padding-left:60px}.gb-tc-day{position:sticky;top:0;z-index:1;display:flex;justify-content:center;margin:4px 0 0;padding:4px 0;background:var(--gemboss-surface);pointer-events:none}.gb-tc-day__label{padding:1px 12px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-full);background:var(--gemboss-surface);box-shadow:var(--gemboss-shadow-resting);font-size:var(--gemboss-fs-caption);font-weight:600;line-height:20px;color:var(--gemboss-text)}.gb-tc-new{display:flex;align-items:center;gap:8px;margin:6px 16px;color:var(--gemboss-accent)}.gb-tc-new:before,.gb-tc-new:after{content:"";flex:1;height:1px;background:currentColor}.gb-tc-new__label{font-size:var(--gemboss-fs-caption);font-weight:650;line-height:16px}.gb-tc-msg{position:relative;display:grid;grid-template-columns:36px minmax(0,1fr);column-gap:8px;align-items:start;padding:8px 16px 4px}.gb-tc-msg:focus{outline:none}.gb-tc-msg:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-msg--grouped{padding-block:2px}.gb-tc-msg>.gb-avatar,.gb-tc-msg>.gb-tc-msg__hover-time{grid-column:1;grid-row:1}.gb-tc-msg>.gb-tc-msg__main{grid-column:2;grid-row:1;min-width:0}.gb-avatar.gb-tc-msg__av{width:36px;height:36px;font-size:13px;margin-top:2px}.gb-tc-msg:hover,.gb-tc-msg--active,.gb-tc-msg--editing{background:var(--gemboss-divider-soft)}.gb-tc-msg:has(:focus-visible){background:var(--gemboss-divider-soft)}.gb-tc-msg__hover-time{justify-self:end;opacity:0;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:var(--gemboss-fs-micro);line-height:20px;color:var(--gemboss-text-muted)}.gb-tc-msg:hover>.gb-tc-msg__hover-time,.gb-tc-msg:focus-within>.gb-tc-msg__hover-time{opacity:1}.gb-tc-msg__who{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px;min-width:0;line-height:20px}.gb-tc-msg__name{font-weight:650;color:var(--gemboss-text-strong);overflow-wrap:anywhere}.gb-tc-msg__time{font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted);font-variant-numeric:tabular-nums}.gb-tc-msg__body{white-space:pre-wrap;overflow-wrap:anywhere;line-height:20px;color:var(--gemboss-text)}.gb-tc-msg__edited{margin-left:4px;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted);white-space:nowrap}.gb-tc-msg__status{margin:2px 0 0;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-msg--failed .gb-tc-msg__status{color:var(--gemboss-danger)}.gb-tc-msg--sending>.gb-tc-msg__main{opacity:.6}.gb-tc-msg__files,.gb-tc-msg__extra{margin-top:4px;min-width:0}.gb-tc-msg__editor{display:grid;gap:6px;margin-top:2px}.gb-tc-msg--mention{box-shadow:inset 2px 0 0 var(--gemboss-warning)}.gb-tc-msg--highlight{background:var(--gemboss-accent-soft)}.gb-tc-quote{display:flex;align-items:baseline;gap:6px;max-width:100%;min-height:28px;margin:0 0 4px;padding:4px 8px;box-sizing:border-box;border:0;border-left:2px solid var(--gemboss-divider);border-radius:0 var(--gemboss-radius-sm) var(--gemboss-radius-sm) 0;background:none;cursor:pointer;text-align:left;font:inherit;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-quote:hover{background:var(--gemboss-hover)}.gb-tc-quote:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:1px}.gb-tc-quote[aria-busy=true]{opacity:.55;cursor:progress}.gb-tc-quote__who{flex:none;font-weight:650;color:var(--gemboss-text)}.gb-tc-quote__text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-msg .ui-reactions{margin-top:2px}.gb-tc-msg .ui-reaction-picker{margin:4px 0;max-width:calc(100cqi - 64px)}.gb-tc-threadprev{display:flex;align-items:center;gap:8px;max-width:100%;min-height:32px;margin-top:4px;padding:4px 8px 4px 4px;box-sizing:border-box;border:1px solid transparent;border-radius:var(--gemboss-radius-md);background:none;cursor:pointer;text-align:left;font:inherit;font-size:var(--gemboss-fs-caption)}.gb-tc-threadprev:hover{border-color:var(--gemboss-divider);background:var(--gemboss-surface)}.gb-tc-threadprev:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:1px}.gb-tc-threadprev__faces{flex:none;display:flex;gap:2px}.gb-tc-threadprev__faces>.gb-avatar{width:22px;height:22px;font-size:9px;border-radius:var(--gemboss-radius-xs)}.gb-tc-threadprev__count{flex:none;font-weight:650;color:var(--gemboss-accent-strong)}.gb-tc-threadprev__last{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--gemboss-text-muted)}.gb-tc-receipt{display:inline-flex;margin-left:6px;vertical-align:-2px;color:var(--gemboss-text-muted)}.gb-tc-receipt--all{color:var(--gemboss-success)}.gb-tc-receipt__tick{stroke:var(--gemboss-surface)}.gb-tc-acts{position:absolute;top:-14px;right:16px;z-index:2;display:flex;align-items:center;gap:0;padding:2px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-md);background:var(--gemboss-surface);box-shadow:var(--gemboss-shadow-raised);white-space:nowrap;opacity:0;pointer-events:none}.gb-tc-list>.gb-tc-msg:first-child>.gb-tc-acts{top:2px}.gb-tc-msg:hover>.gb-tc-acts,.gb-tc-msg--active>.gb-tc-acts,.gb-tc-acts:focus-within{opacity:1;pointer-events:auto}.gb-tc-act{display:inline-grid;place-items:center;min-width:32px;height:32px;padding:0 6px;box-sizing:border-box;border:0;border-radius:var(--gemboss-radius-sm);background:transparent;cursor:pointer;font:inherit;font-size:16px;line-height:1;color:var(--gemboss-text-muted)}.gb-tc-act:hover{background:var(--gemboss-hover);color:var(--gemboss-text-strong)}.gb-tc-act:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-act[aria-pressed=true],.gb-tc-act[aria-expanded=true]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong)}@container gb-tc-log (max-width: 560px){.gb-tc-act--quick{display:none}.gb-tc-acts{right:8px}.gb-tc-msg{padding-inline:12px}.gb-tc-status{padding-left:56px}}@media (hover: none){.gb-tc-msg:focus-within{background:var(--gemboss-divider-soft)}.gb-tc-msg:focus-within>.gb-tc-acts{opacity:1;pointer-events:auto}.gb-tc-act--quick{display:none}.gb-tc-acts{top:-22px}}.gb-tc-jump{position:absolute;right:16px;bottom:16px;z-index:3;display:grid;place-items:center;width:40px;height:40px;padding:0;border:1px solid var(--gemboss-divider);border-radius:50%;background:var(--gemboss-surface);color:var(--gemboss-text-strong);box-shadow:var(--gemboss-shadow-raised);cursor:pointer}.gb-tc-jump[hidden]{display:none}.gb-tc-jump:hover{background:var(--gemboss-hover)}.gb-tc-jump:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:2px}.gb-tc-jump__n{position:absolute;top:-6px;right:-6px;min-width:20px;height:20px;padding:0 4px;box-sizing:border-box;border-radius:var(--gemboss-radius-full);background:var(--gemboss-accent);color:var(--gemboss-on-primary);font-size:var(--gemboss-fs-micro);font-weight:700;line-height:20px;text-align:center;font-variant-numeric:tabular-nums}.gb-tc-composer{display:grid;gap:6px;min-width:0;padding:8px 16px 16px}.gb-tc-composer__box{position:relative;display:flex;align-items:flex-end;gap:4px;min-width:0;padding:4px;box-sizing:border-box;border:1px solid var(--gemboss-field-line);border-radius:var(--gemboss-radius-lg);background:var(--gemboss-field-bg)}.gb-tc-composer__box:hover{border-color:var(--gemboss-field-line-hover)}.gb-tc-composer__box:focus-within{border-color:var(--gemboss-field-line-active);box-shadow:0 0 0 1px var(--gemboss-field-line-active)}.gb-tc-composer__input{flex:1 1 auto;min-width:0;min-height:32px;max-height:40vh;margin:0;padding:6px 4px;box-sizing:border-box;border:0;background:transparent;resize:none;outline:none;field-sizing:content;font:inherit;font-size:var(--gemboss-fs-body);line-height:20px;color:var(--gemboss-field-ink)}.gb-tc-composer__input::placeholder{color:var(--gemboss-field-placeholder)}.gb-tc-composer__input:disabled{cursor:not-allowed}.gb-tc-composer__box>.ui-btn--icon{width:32px;height:32px}.gb-tc .gb-tc-composer__attach,.gb-tc .gb-tc-composer__emoji,.gb-tc .gb-tc-composer__send,.gb-tc .gb-tc-panel-head__close,.gb-tc .gb-tc-people__toggle{flex:none}.gb-tc .gb-tc-composer__send:disabled{opacity:.35}.gb-tc .gb-tc-composer__emoji[aria-expanded=true]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong)}.gb-tc-composer__emoji-pop{position:absolute;right:0;bottom:calc(100% + 6px);z-index:4}.gb-tc-composer__emoji-pop .ui-reaction-picker__body{max-height:min(300px,40vh)}.gb-tc-replying{display:flex;align-items:center;gap:8px;min-width:0;padding:2px 2px 2px 8px;box-sizing:border-box;border-left:2px solid var(--gemboss-accent);border-radius:var(--gemboss-radius-sm);background:var(--gemboss-accent-soft)}.gb-tc-replying__text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text)}.gb-tc-composer__files,.gb-tc-composer__picker{min-width:0}.gb-tc-composer__picker{max-height:240px;overflow-y:auto;overscroll-behavior:contain}.gb-tc-composer__also{justify-self:start}.gb-tc-composer__error{margin:0;font-size:var(--gemboss-fs-caption);color:var(--gemboss-danger)}.gb-tc-thread,.gb-tc-people{height:100%;box-sizing:border-box;border-left:1px solid var(--gemboss-divider);background:var(--gemboss-surface);color:var(--gemboss-text)}.gb-tc-thread:focus,.gb-tc-people:focus{outline:none}.gb-tc-panel-head{padding:8px 8px 8px 16px}.gb-tc-thread{width:var(--gb-tc-thread-w, 380px)}.gb-tc-thread__note{margin:8px 16px 0;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-thread__note--error{color:var(--gemboss-danger-strong, var(--gemboss-danger))}.gb-tc-thread__count{display:flex;align-items:center;gap:8px;margin:8px 16px 4px;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-thread__count:after{content:"";flex:1;border-top:1px solid var(--gemboss-divider)}.gb-tc-people{width:var(--gb-tc-people-w, 260px)}.gb-tc-people__body{overflow-y:auto;overscroll-behavior:contain;padding:8px 8px 16px}.gb-tc-people__section+.gb-tc-people__section{margin-top:16px}.gb-tc-person{display:flex;align-items:center;gap:8px;width:100%;min-width:0;min-height:40px;padding:4px 8px;box-sizing:border-box;border:0;border-radius:var(--gemboss-radius-md);background:none;text-align:left;text-decoration:none;font:inherit;color:inherit}:is(a,button).gb-tc-person{cursor:pointer}:is(a,button).gb-tc-person:hover{background:var(--gemboss-hover)}.gb-tc-person:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-person__av{position:relative;flex:none;display:inline-flex}.gb-tc-person__text{flex:1 1 auto;min-width:0;display:grid}.gb-tc-person__name,.gb-tc-person__sub{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-person__name{font-weight:550;line-height:20px;color:var(--gemboss-text-strong)}.gb-tc-person__sub{font-size:var(--gemboss-fs-caption);line-height:16px;color:var(--gemboss-text-muted)}.gb-tc-person__role{flex:none;padding:0 6px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-full);font-size:var(--gemboss-fs-micro);font-weight:650;line-height:18px;color:var(--gemboss-text-muted)}.gb-tc-person--offline .gb-tc-person__name{color:var(--gemboss-text-muted)}.gb-tc-person--offline .gb-avatar{opacity:.55}.gb-tc-presence{position:absolute;right:-1px;bottom:-1px;width:8px;height:8px;border-radius:50%;border:2px solid var(--gemboss-surface);background:var(--gemboss-success)}.gb-tc-presence--away{background:var(--gemboss-warning)}.gb-tc-presence--offline{background:var(--gemboss-surface);box-shadow:inset 0 0 0 1.5px var(--gemboss-text-muted)}.gb-tc-presence--online{background:var(--gemboss-success)}.gb-tc-people[data-collapsed]{width:56px}.gb-tc-people[data-collapsed] .gb-tc-panel-head{justify-content:center;padding:8px 0}.gb-tc-people[data-collapsed] .gb-tc-panel-head__actions{display:none}.gb-tc-people[data-collapsed] :is(.gb-tc-panel-head__text,.gb-tc-people__heading,.gb-tc-person__text,.gb-tc-person__role){position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.gb-tc-people[data-collapsed] .gb-tc-people__body{padding:8px 4px}.gb-tc-people[data-collapsed] .gb-tc-people__section+.gb-tc-people__section{margin-top:8px;padding-top:8px;border-top:1px solid var(--gemboss-divider)}.gb-tc-people[data-collapsed] .gb-tc-person{justify-content:center;padding:4px 0}@media (pointer: coarse),(max-width: 47.9975em){.gb-tc-channel,.gb-tc-person,.gb-tc-quote,.gb-tc-threadprev{min-height:44px}.gb-tc-act{min-width:44px;height:44px}.gb-tc-jump{width:48px;height:48px}.gb-tc-composer__input{font-size:16px;min-height:44px;padding-block:12px}.gb-tc-composer__box>.ui-btn--icon{width:44px;height:44px}}@container gb-tc (max-width: 1100px){.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people,.gb-tc>.gb-tc-scrim{grid-area:1 / 1 / -1 / -1}.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people{position:absolute;top:0;right:0;bottom:0;z-index:5;width:min(var(--gb-tc-thread-w, 380px),100%);box-shadow:var(--gemboss-shadow-overlay)}.gb-tc>.gb-tc-people{width:min(var(--gb-tc-people-w, 260px) + 40px,100%)}.gb-tc>.gb-tc-people[data-collapsed]{display:none}.gb-tc>.gb-tc-scrim{display:block;position:absolute;inset:0;z-index:4;background:color-mix(in srgb,var(--gemboss-dark-bg) 32%,transparent)}}@container gb-tc (max-width: 760px){.gb-tc>.gb-tc-rail{grid-column:1 / -1;width:auto;border-right:0}.gb-tc>.gb-tc-main{grid-column:1 / -1}.gb-tc:not([data-view=list])>.gb-tc-rail,.gb-tc[data-view=list]>:is(.gb-tc-main,.gb-tc-scrim,.gb-tc-thread,.gb-tc-people){display:none}.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people{left:0;width:auto;border-left:0;box-shadow:none}.gb-tc>.gb-tc-scrim{display:none}.gb-tc .gb-tc-head__back{display:inline-flex;margin-left:-8px}.gb-tc-head{padding-left:12px}.gb-tc-composer{padding:8px 12px 12px}}@container gb-tc (max-width: 359px){.gb-tc-composer__box{gap:0}}@media (prefers-reduced-motion: no-preference){.gb-tc-msg{transition:background-color .15s linear}.gb-tc-acts{transition:opacity .1s linear}}
|
|
1
|
+
.gb-tc{container:gb-tc / inline-size;position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr);height:100%;min-height:0;overflow:hidden;box-sizing:border-box;background:var(--gemboss-surface);color:var(--gemboss-text);font-size:var(--gemboss-fs-body)}.gb-tc>.gb-tc-rail{grid-column:1;grid-row:1}.gb-tc>.gb-tc-main{grid-column:2;grid-row:1}.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people{grid-column:3;grid-row:1;height:auto}.gb-tc-main,.gb-tc-rail,.gb-tc-thread,.gb-tc-people{display:flex;flex-direction:column;min-width:0;min-height:0}.gb-tc-main{position:relative}.gb-tc-main>*,.gb-tc-rail>*,.gb-tc-thread>*,.gb-tc-people>*{flex:none}.gb-tc-main>.gb-tc-body,.gb-tc-thread>.gb-tc-body,.gb-tc-rail>.gb-tc-rail__scroll,.gb-tc-people>.gb-tc-people__body{flex:1 1 0;min-height:0}.gb-tc-body,.gb-tc-composer,.gb-tc-rail__list,.gb-tc-people__list{grid-template-columns:minmax(0,1fr)}.gb-tc-scrim{display:none}.gb-tc :is(.gb-tc-thread,.gb-tc-people,.gb-tc-scrim,.gb-tc-replying,.gb-tc-jump,.gb-tc-composer__files,.gb-tc-composer__picker)[hidden]{display:none}.gb-tc-sr{position:absolute;left:0;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.gb-tc-rail{width:var(--gb-tc-rail-w, 240px);box-sizing:border-box;border-right:1px solid var(--gemboss-divider);background:var(--gemboss-rail-bg)}.gb-tc-rail__head,.gb-tc-head,.gb-tc-panel-head{display:flex;align-items:center;gap:8px;min-height:56px;min-width:0;box-sizing:border-box;border-bottom:1px solid var(--gemboss-divider)}.gb-tc-rail__head{justify-content:space-between;padding:8px 8px 8px 16px}.gb-tc-rail__title,.gb-tc-panel-head__title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--gemboss-fs-h3);font-weight:650;line-height:1.3;color:var(--gemboss-text-strong)}.gb-tc-rail__actions,.gb-tc-head__actions,.gb-tc-panel-head__actions{flex:none;display:flex;align-items:center;gap:4px}.gb-tc-rail__scroll{overflow-y:auto;overscroll-behavior:contain;padding:8px 8px 16px}.gb-tc-rail__section+.gb-tc-rail__section{margin-top:16px}.gb-tc-rail__heading,.gb-tc-people__heading{margin:0 8px 4px;font-size:var(--gemboss-fs-caption);font-weight:650;line-height:20px;color:var(--gemboss-text-muted)}.gb-tc-rail__list,.gb-tc-people__list{list-style:none;margin:0;padding:0;display:grid;gap:1px}.gb-tc-channel{display:flex;align-items:center;gap:8px;width:100%;min-width:0;min-height:32px;padding:0 8px;box-sizing:border-box;border:0;border-radius:var(--gemboss-radius-md);background:none;cursor:pointer;text-align:left;text-decoration:none;font:inherit;font-size:var(--gemboss-fs-body);color:var(--gemboss-text-muted)}.gb-tc-channel:hover{background:var(--gemboss-hover);color:var(--gemboss-text-strong)}.gb-tc-channel:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-channel--unread{color:var(--gemboss-text-strong);font-weight:650}.gb-tc-channel[aria-current]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong);font-weight:650}.gb-tc-channel__glyph{position:relative;flex:none;display:inline-grid;place-items:center;width:20px;height:20px}.gb-tc-channel__glyph>.gb-tc-presence{right:-3px;bottom:-2px;width:7px;height:7px;border-color:var(--gemboss-rail-bg)}.gb-avatar.gb-tc-channel__av{width:20px;height:20px;font-size:8px}.gb-tc-channel__name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-badge{flex:none;min-width:20px;height:18px;padding:0 6px;box-sizing:border-box;border-radius:var(--gemboss-radius-full);background:var(--gemboss-accent);color:var(--gemboss-on-primary);font-size:var(--gemboss-fs-micro);font-weight:700;line-height:18px;text-align:center;font-variant-numeric:tabular-nums}.gb-tc-badge--mention{background:var(--gemboss-warning-soft);color:var(--gemboss-warning);box-shadow:inset 0 0 0 1px var(--gemboss-warning-border)}.gb-tc-head{padding:8px 12px 8px 16px}.gb-tc .gb-tc-head__back{display:none}.gb-tc-head__text,.gb-tc-panel-head__text{flex:1 1 auto;min-width:0}.gb-tc-head__title{margin:0;display:flex;align-items:center;gap:6px;min-width:0;font-size:var(--gemboss-fs-h3);font-weight:650;line-height:1.3;color:var(--gemboss-text-strong)}.gb-tc-head__glyph{flex:none;display:inline-flex;color:var(--gemboss-text-muted)}.gb-avatar.gb-tc-head__av{flex:none;margin-right:2px}.gb-tc-head__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-head__sub,.gb-tc-panel-head__sub{margin:2px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--gemboss-fs-caption);line-height:16px;color:var(--gemboss-text-muted)}.gb-tc .gb-tc-head__members{gap:6px;padding:0 8px}.gb-tc .gb-tc-head__members[aria-expanded=true]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong)}.gb-tc-head__count{font-variant-numeric:tabular-nums}.gb-tc-body{position:relative;display:grid;grid-template-rows:minmax(0,1fr);min-width:0;min-height:0}.gb-tc-log{container:gb-tc-log / inline-size;min-height:0;overflow-y:auto;overscroll-behavior:contain}.gb-tc-log:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-list{list-style:none;margin:0;padding:8px 0 16px;display:flex;flex-direction:column}.gb-tc-list>*{flex-shrink:0}.gb-tc-older,.gb-tc-empty,.gb-tc-status{display:flex;justify-content:center;padding:8px 16px;color:var(--gemboss-text-muted);font-size:var(--gemboss-fs-caption)}.gb-tc-empty{padding-block:32px;font-size:var(--gemboss-fs-body)}.gb-tc-status{justify-content:flex-start;padding-left:60px}.gb-tc-day{position:sticky;top:0;z-index:1;display:flex;justify-content:center;margin:4px 0 0;padding:4px 0;background:var(--gemboss-surface);pointer-events:none}.gb-tc-day__label{padding:1px 12px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-full);background:var(--gemboss-surface);box-shadow:var(--gemboss-shadow-resting);font-size:var(--gemboss-fs-caption);font-weight:600;line-height:20px;color:var(--gemboss-text)}.gb-tc-new{display:flex;align-items:center;gap:8px;margin:6px 16px;color:var(--gemboss-accent)}.gb-tc-new:before,.gb-tc-new:after{content:"";flex:1;height:1px;background:currentColor}.gb-tc-new__label{font-size:var(--gemboss-fs-caption);font-weight:650;line-height:16px}.gb-tc-msg{position:relative;display:grid;grid-template-columns:36px minmax(0,1fr);column-gap:8px;align-items:start;padding:8px 16px 4px}.gb-tc-msg:focus{outline:none}.gb-tc-msg:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-msg--grouped{padding-block:2px}.gb-tc-msg>.gb-avatar,.gb-tc-msg>.gb-tc-msg__hover-time{grid-column:1;grid-row:1}.gb-tc-msg>.gb-tc-msg__main{grid-column:2;grid-row:1;min-width:0}.gb-avatar.gb-tc-msg__av{width:36px;height:36px;font-size:13px;margin-top:2px}.gb-tc-msg:hover,.gb-tc-msg--active,.gb-tc-msg--editing{background:var(--gemboss-divider-soft)}.gb-tc-msg:has(:focus-visible){background:var(--gemboss-divider-soft)}.gb-tc-msg__hover-time{justify-self:end;opacity:0;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:var(--gemboss-fs-micro);line-height:20px;color:var(--gemboss-text-muted)}.gb-tc-msg:hover>.gb-tc-msg__hover-time,.gb-tc-msg:focus-within>.gb-tc-msg__hover-time{opacity:1}.gb-tc-msg__who{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px;min-width:0;line-height:20px}.gb-tc-msg__name{font-weight:650;color:var(--gemboss-text-strong);overflow-wrap:anywhere}.gb-tc-msg__time{font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted);font-variant-numeric:tabular-nums}.gb-tc-msg__body{white-space:pre-wrap;overflow-wrap:anywhere;line-height:20px;color:var(--gemboss-text)}.gb-tc-msg__edited{margin-left:4px;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted);white-space:nowrap}.gb-tc-msg__status{margin:2px 0 0;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-msg--failed .gb-tc-msg__status{color:var(--gemboss-danger)}.gb-tc-msg--sending>.gb-tc-msg__main{opacity:.6}.gb-tc-msg__files,.gb-tc-msg__extra{margin-top:4px;min-width:0}.gb-tc-msg__editor{display:grid;gap:6px;margin-top:2px}.gb-tc-msg--mention{box-shadow:inset 2px 0 0 var(--gemboss-warning)}.gb-tc-msg--highlight{background:var(--gemboss-accent-soft)}.gb-tc-quote{display:flex;align-items:baseline;gap:6px;max-width:100%;min-height:28px;margin:0 0 4px;padding:4px 8px;box-sizing:border-box;border:0;border-left:2px solid var(--gemboss-divider);border-radius:0 var(--gemboss-radius-sm) var(--gemboss-radius-sm) 0;background:none;cursor:pointer;text-align:left;font:inherit;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-quote:hover{background:var(--gemboss-hover)}.gb-tc-quote:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:1px}.gb-tc-quote[aria-busy=true]{opacity:.55;cursor:progress}.gb-tc-quote__who{flex:none;font-weight:650;color:var(--gemboss-text)}.gb-tc-quote__text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-msg .ui-reactions{margin-top:2px}.gb-tc-msg .ui-reaction-picker{margin:4px 0;max-width:calc(100cqi - 64px)}.gb-tc-threadprev{display:flex;align-items:center;gap:8px;max-width:100%;min-height:32px;margin-top:4px;padding:4px 8px 4px 4px;box-sizing:border-box;border:1px solid transparent;border-radius:var(--gemboss-radius-md);background:none;cursor:pointer;text-align:left;font:inherit;font-size:var(--gemboss-fs-caption)}.gb-tc-threadprev:hover{border-color:var(--gemboss-divider);background:var(--gemboss-surface)}.gb-tc-threadprev:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:1px}.gb-tc-threadprev__faces{flex:none;display:flex;gap:2px}.gb-tc-threadprev__faces>.gb-avatar{width:22px;height:22px;font-size:9px;border-radius:var(--gemboss-radius-xs)}.gb-tc-threadprev__count{flex:none;font-weight:650;color:var(--gemboss-accent-strong)}.gb-tc-threadprev__last{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--gemboss-text-muted)}.gb-tc-receipt{display:inline-flex;margin-left:6px;vertical-align:-2px;color:var(--gemboss-text-muted)}.gb-tc-receipt--all{color:var(--gemboss-success)}.gb-tc-receipt__tick{stroke:var(--gemboss-surface)}.gb-tc-acts{position:absolute;top:-14px;right:16px;z-index:2;display:flex;align-items:center;gap:0;padding:2px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-md);background:var(--gemboss-surface);box-shadow:var(--gemboss-shadow-raised);white-space:nowrap;opacity:0;pointer-events:none}.gb-tc-list>.gb-tc-msg:first-child>.gb-tc-acts{top:2px}.gb-tc-msg:hover>.gb-tc-acts,.gb-tc-msg--active>.gb-tc-acts,.gb-tc-acts:focus-within{opacity:1;pointer-events:auto}.gb-tc-act{display:inline-grid;place-items:center;min-width:32px;height:32px;padding:0 6px;box-sizing:border-box;border:0;border-radius:var(--gemboss-radius-sm);background:transparent;cursor:pointer;font:inherit;font-size:16px;line-height:1;color:var(--gemboss-text-muted)}.gb-tc-act:hover{background:var(--gemboss-hover);color:var(--gemboss-text-strong)}.gb-tc-act:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-act[aria-pressed=true],.gb-tc-act[aria-expanded=true]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong)}@container gb-tc-log (max-width: 560px){.gb-tc-act--quick{display:none}.gb-tc-acts{right:8px}.gb-tc-msg{padding-inline:12px}.gb-tc-status{padding-left:56px}}@media (hover: none){.gb-tc-msg:focus-within{background:var(--gemboss-divider-soft)}.gb-tc-msg:focus-within>.gb-tc-acts{opacity:1;pointer-events:auto}.gb-tc-act--quick{display:none}.gb-tc-acts{top:-22px}}.gb-tc-jump{position:absolute;right:16px;bottom:16px;z-index:3;display:grid;place-items:center;width:40px;height:40px;padding:0;border:1px solid var(--gemboss-divider);border-radius:50%;background:var(--gemboss-surface);color:var(--gemboss-text-strong);box-shadow:var(--gemboss-shadow-raised);cursor:pointer}.gb-tc-jump[hidden]{display:none}.gb-tc-jump:hover{background:var(--gemboss-hover)}.gb-tc-jump:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:2px}.gb-tc-jump__n{position:absolute;top:-6px;right:-6px;min-width:20px;height:20px;padding:0 4px;box-sizing:border-box;border-radius:var(--gemboss-radius-full);background:var(--gemboss-accent);color:var(--gemboss-on-primary);font-size:var(--gemboss-fs-micro);font-weight:700;line-height:20px;text-align:center;font-variant-numeric:tabular-nums}.gb-tc-composer{display:grid;gap:6px;min-width:0;padding:8px 16px 16px}.gb-tc-composer__box{position:relative;display:flex;align-items:flex-end;gap:4px;min-width:0;padding:4px;box-sizing:border-box;border:1px solid var(--gemboss-field-line);border-radius:var(--gemboss-radius-lg);background:var(--gemboss-field-bg)}.gb-tc-composer__box:hover{border-color:var(--gemboss-field-line-hover)}.gb-tc-composer__box:focus-within{border-color:var(--gemboss-field-line-active);box-shadow:0 0 0 1px var(--gemboss-field-line-active)}.gb-tc-composer__input{flex:1 1 auto;min-width:0;min-height:32px;max-height:40vh;margin:0;padding:6px 4px;box-sizing:border-box;border:0;background:transparent;resize:none;outline:none;field-sizing:content;font:inherit;font-size:var(--gemboss-fs-body);line-height:20px;color:var(--gemboss-field-ink)}.gb-tc-composer__input::placeholder{color:var(--gemboss-field-placeholder)}.gb-tc-composer__input:disabled{cursor:not-allowed}.gb-tc-composer__box>.ui-btn--icon{width:32px;height:32px}.gb-tc .gb-tc-composer__attach,.gb-tc .gb-tc-composer__emoji,.gb-tc .gb-tc-composer__send,.gb-tc .gb-tc-panel-head__close,.gb-tc .gb-tc-people__toggle{flex:none}.gb-tc .gb-tc-composer__send:disabled{opacity:.35}.gb-tc .gb-tc-composer__emoji[aria-expanded=true]{background:var(--gemboss-accent-soft);color:var(--gemboss-accent-strong)}.gb-tc-composer__emoji-pop{position:absolute;right:0;bottom:calc(100% + 6px);z-index:4}.gb-tc-composer__emoji-pop .ui-reaction-picker__body{max-height:min(300px,40vh)}.gb-tc-replying{display:flex;align-items:center;gap:8px;min-width:0;padding:2px 2px 2px 8px;box-sizing:border-box;border-left:2px solid var(--gemboss-accent);border-radius:var(--gemboss-radius-sm);background:var(--gemboss-accent-soft)}.gb-tc-replying__text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text)}.gb-tc-composer__files,.gb-tc-composer__picker{min-width:0}.gb-tc-composer__picker{max-height:240px;overflow-y:auto;overscroll-behavior:contain}.gb-tc-composer__also{justify-self:start}.gb-tc-composer__error{margin:0;font-size:var(--gemboss-fs-caption);color:var(--gemboss-danger)}.gb-tc-thread,.gb-tc-people{height:100%;box-sizing:border-box;border-left:1px solid var(--gemboss-divider);background:var(--gemboss-surface);color:var(--gemboss-text)}.gb-tc-thread:focus,.gb-tc-people:focus{outline:none}.gb-tc-panel-head{padding:8px 8px 8px 16px}.gb-tc-thread{width:var(--gb-tc-thread-w, 380px)}.gb-tc-thread__note{margin:8px 16px 0;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-thread__note--error{color:var(--gemboss-danger-strong, var(--gemboss-danger))}.gb-tc-thread__count{display:flex;align-items:center;gap:8px;margin:8px 16px 4px;font-size:var(--gemboss-fs-caption);color:var(--gemboss-text-muted)}.gb-tc-thread__count:after{content:"";flex:1;border-top:1px solid var(--gemboss-divider)}.gb-tc-people{width:var(--gb-tc-people-w, 260px)}.gb-tc-people__body{overflow-y:auto;overscroll-behavior:contain;padding:8px 8px 16px}.gb-tc-people__section+.gb-tc-people__section{margin-top:16px}.gb-tc-person{display:flex;align-items:center;gap:8px;width:100%;min-width:0;min-height:40px;padding:4px 8px;box-sizing:border-box;border:0;border-radius:var(--gemboss-radius-md);background:none;text-align:left;text-decoration:none;font:inherit;color:inherit}:is(a,button).gb-tc-person{cursor:pointer}:is(a,button).gb-tc-person:hover{background:var(--gemboss-hover)}.gb-tc-person:focus-visible{outline:2px solid var(--gemboss-accent);outline-offset:-2px}.gb-tc-person__av{position:relative;flex:none;display:inline-flex}.gb-tc-person__text{flex:1 1 auto;min-width:0;display:grid}.gb-tc-person__name,.gb-tc-person__sub{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gb-tc-person__name{font-weight:550;line-height:20px;color:var(--gemboss-text-strong)}.gb-tc-person__sub{font-size:var(--gemboss-fs-caption);line-height:16px;color:var(--gemboss-text-muted)}.gb-tc-person__role{flex:none;padding:0 6px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-full);font-size:var(--gemboss-fs-micro);font-weight:650;line-height:18px;color:var(--gemboss-text-muted)}.gb-tc-person--offline .gb-tc-person__name{color:var(--gemboss-text-muted)}.gb-tc-person--offline .gb-avatar{opacity:.55}.gb-tc-presence{position:absolute;right:-1px;bottom:-1px;width:8px;height:8px;border-radius:50%;border:2px solid var(--gemboss-surface);background:var(--gemboss-success)}.gb-tc-presence--away{background:var(--gemboss-warning)}.gb-tc-presence--offline{background:var(--gemboss-surface);box-shadow:inset 0 0 0 1.5px var(--gemboss-text-muted)}.gb-tc-presence--online{background:var(--gemboss-success)}.gb-tc-people[data-collapsed]{width:56px}.gb-tc-people[data-collapsed] .gb-tc-panel-head{justify-content:center;padding:8px 0}.gb-tc-people[data-collapsed] .gb-tc-panel-head__actions{display:none}.gb-tc-people[data-collapsed] :is(.gb-tc-panel-head__text,.gb-tc-people__heading,.gb-tc-person__text,.gb-tc-person__role){position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.gb-tc-people[data-collapsed] .gb-tc-people__body{padding:8px 4px}.gb-tc-people[data-collapsed] .gb-tc-people__section+.gb-tc-people__section{margin-top:8px;padding-top:8px;border-top:1px solid var(--gemboss-divider)}.gb-tc-people[data-collapsed] .gb-tc-person{justify-content:center;padding:4px 0}@media (pointer: coarse),(max-width: 47.9975em){.gb-tc-channel,.gb-tc-person,.gb-tc-quote,.gb-tc-threadprev{min-height:44px}.gb-tc-act{min-width:44px;height:44px}.gb-tc-jump{width:48px;height:48px}.gb-tc-composer__input{font-size:16px;min-height:44px;padding-block:12px}.gb-tc-composer__box>.ui-btn--icon{width:44px;height:44px}}@container gb-tc (max-width: 1100px){.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people,.gb-tc>.gb-tc-scrim{grid-area:1 / 1 / -1 / -1}.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people{position:absolute;top:0;right:0;bottom:0;z-index:5;width:min(var(--gb-tc-thread-w, 380px),100%);box-shadow:var(--gemboss-shadow-overlay)}.gb-tc>.gb-tc-people{width:min(var(--gb-tc-people-w, 260px) + 40px,100%)}.gb-tc>.gb-tc-people[data-collapsed]{display:none}.gb-tc>.gb-tc-scrim{display:block;position:absolute;inset:0;z-index:4;background:color-mix(in srgb,var(--gemboss-dark-bg) 32%,transparent)}}@container gb-tc (max-width: 760px){.gb-tc>.gb-tc-rail{grid-column:1 / -1;width:auto;border-right:0}.gb-tc>.gb-tc-main{grid-column:1 / -1}.gb-tc:not([data-view=list])>.gb-tc-rail,.gb-tc[data-view=list]>:is(.gb-tc-main,.gb-tc-scrim,.gb-tc-thread,.gb-tc-people){display:none}.gb-tc>.gb-tc-thread,.gb-tc>.gb-tc-people{left:0;width:auto;border-left:0;box-shadow:none}.gb-tc>.gb-tc-scrim{display:none}.gb-tc .gb-tc-head__back{display:inline-flex;margin-left:-8px}.gb-tc-head{padding-left:12px}.gb-tc-composer{padding:8px 12px 12px}}@container gb-tc (max-width: 359px){.gb-tc-composer__box{gap:0}}@media (prefers-reduced-motion: no-preference){.gb-tc-msg{transition:background-color .15s linear}.gb-tc-acts{transition:opacity .1s linear}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.gb-topnav-fold{min-width:0}.gb-topnav-fold>summary{flex-shrink:1;min-width:0}.gb-topnav-fold-label{min-width:0;overflow:hidden;text-overflow:ellipsis}.gb-topnav-foot{margin-top:4px;padding:8px 6px 2px;border-top:1px solid var(--gemboss-divider, #e5e7ea)}@media (max-width: 600px){.gb-topnav-unfolded{display:none}}@media (max-width: 760px){.gb-topnav.gb-topnav--fold-760{overflow:visible}.gb-topnav--fold-760>.gb-topnav-row,.gemboss-platform-topbar:has(.gb-topnav--fold-760) .gb-topnav-unfolded{display:none}.gb-topnav--fold-760>.gb-topnav-fold{display:flex}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.ui-translation{container-type:inline-size;display:grid;gap:var(--ui-space-2);padding:var(--ui-space-3);border:1px solid var(--ui-border);border-radius:var(--ui-radius-lg);background:var(--ui-surface)}.ui-translation--current{border-color:var(--gemboss-primary);box-shadow:inset 3px 0 0 var(--gemboss-primary)}.ui-translation:focus-visible{outline:2px solid var(--ui-focus);outline-offset:2px}.ui-translation--current:focus{outline:none}.ui-translation__head,.ui-translation__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ui-space-2);font-size:var(--ui-fs-caption);line-height:16px;color:var(--ui-text-muted)}.ui-translation__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:var(--ui-fs-caption);color:var(--ui-text-strong);overflow-wrap:anywhere}.ui-translation__state{flex:none}.ui-translation__by{min-width:0;overflow-wrap:anywhere}.ui-translation__body{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--ui-space-2) var(--ui-space-4)}.ui-translation__source{margin:0;padding-top:6px;font-size:var(--ui-fs-body);line-height:20px;color:var(--ui-text-muted);white-space:pre-wrap;overflow-wrap:anywhere}.ui-translation__target{display:grid;min-width:0}.ui-translation__checks{min-width:0;overflow-wrap:anywhere}.ui-translation__checks--ok{color:var(--gemboss-success-strong)}.ui-translation__checks--warn{font-weight:600;color:var(--gemboss-warning)}.ui-translation__action{display:inline-flex;gap:var(--ui-space-2);margin-left:auto}@media (max-width: 900px){.ui-translation__body{grid-template-columns:minmax(0,1fr)}}@container (max-width: 600px){.ui-translation__body{grid-template-columns:minmax(0,1fr)}}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation row — @gemboss/ui, unreleased (after 0.49.0). One translatable string under review: GemPress's review screen
|
|
3
|
+
* (redesign approved by Chris 23/10/2026) shows a page's strings one under the other, the source beside its translation, and a
|
|
4
|
+
* person walks them with J / K, fixes what the checks flag and approves. GemPress drew it with its own CSS (`gp-str`); GemNews and
|
|
5
|
+
* GemCommunity translations will review strings the same way, so the row lives here.
|
|
6
|
+
*
|
|
7
|
+
* The contract it draws, which every surface keeps:
|
|
8
|
+
* - a head: the string's key (code), its state as a badge (`draft` "To review", `approved`, `stale` "Source changed", `missing`
|
|
9
|
+
* "Not translated"; the words are the surface's with `stateLabel`), and who wrote it (`by`: the model, or "edited by a person");
|
|
10
|
+
* - the source and the target SIDE BY SIDE, read across like a translation tool; one column under 900px, or when the row itself
|
|
11
|
+
* is narrower than 600px (beside a pinned panel);
|
|
12
|
+
* - a foot: what the checks found on the left (green "✓ Numbers and tags match", or the problems in amber words; colour never
|
|
13
|
+
* alone), the surface's `action` on the right (Approve, Save and approve);
|
|
14
|
+
* - `current`: an ink border and a 3px ink bar on the left, the row the keys act on (`aria-current`). The row is focusable
|
|
15
|
+
* (`tabIndex={-1}`, never a Tab stop) so a keyboard-driven list can move focus to it; pass `id` to find it.
|
|
16
|
+
*
|
|
17
|
+
* Presentational and server-renderable: no state, no effect. The editable control is the SURFACE's (it owns the value, the save,
|
|
18
|
+
* the keys): pass it as `target`, a node, or a function that gets the ids of the source and the checks so the control can say
|
|
19
|
+
* `aria-describedby` (a screen reader then hears the source and the problems with the field). Other attributes (`onFocus`,
|
|
20
|
+
* `data-*`) go on the row. `translation-row.css` is its own entry; a plain-HTML surface links `@gemboss/ui/translation-row.css`
|
|
21
|
+
* and writes the classes listed at the top of that file.
|
|
22
|
+
*/
|
|
23
|
+
import "./translation-row.css";
|
|
24
|
+
import React, { type HTMLAttributes, type ReactNode } from "react";
|
|
25
|
+
|
|
26
|
+
const cx = (...p: Array<string | false | null | undefined>) => p.filter(Boolean).join(" ");
|
|
27
|
+
|
|
28
|
+
export type TranslationState = "draft" | "approved" | "stale" | "missing";
|
|
29
|
+
|
|
30
|
+
/** The badge's words by default (GemPress's). */
|
|
31
|
+
export const TRANSLATION_STATE_LABELS: Readonly<Record<TranslationState, string>> = {
|
|
32
|
+
draft: "To review",
|
|
33
|
+
approved: "Approved",
|
|
34
|
+
stale: "Source changed",
|
|
35
|
+
missing: "Not translated",
|
|
36
|
+
};
|
|
37
|
+
/** The kit badge tone for each state (kit-plain.css). Not translated is the plain badge: nothing to review yet. */
|
|
38
|
+
const TONE: Readonly<Record<TranslationState, string | null>> = {
|
|
39
|
+
draft: "ui-badge--info",
|
|
40
|
+
approved: "ui-badge--success",
|
|
41
|
+
stale: "ui-badge--warning",
|
|
42
|
+
missing: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The ids a `target` function gets: the key, the source and the checks line, for `aria-describedby`. */
|
|
46
|
+
export interface TranslationRowIds {
|
|
47
|
+
key: string;
|
|
48
|
+
source: string;
|
|
49
|
+
checks: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TranslationRowProps extends Omit<HTMLAttributes<HTMLElement>, "children" | "className" | "id" | "tabIndex"> {
|
|
53
|
+
/** The string's key: "hero.title". */
|
|
54
|
+
keyName: string;
|
|
55
|
+
state: TranslationState;
|
|
56
|
+
source: ReactNode;
|
|
57
|
+
/** The source's language (`lang` on it), so it is read in the right voice. */
|
|
58
|
+
sourceLang?: string;
|
|
59
|
+
/** The editable control, owned by the surface: a node, or a function given the ids it can point `aria-describedby` at. */
|
|
60
|
+
target: ReactNode | ((ids: TranslationRowIds) => ReactNode);
|
|
61
|
+
/** What the checks found: `ok` with what matched, or not with the problems. Absent: no line (an approved string). */
|
|
62
|
+
checks?: { ok: boolean; text: ReactNode } | null;
|
|
63
|
+
/** Who wrote it: the model's name, or "edited by a person". */
|
|
64
|
+
by?: ReactNode;
|
|
65
|
+
/** The surface's button(s), at the end of the foot. */
|
|
66
|
+
action?: ReactNode;
|
|
67
|
+
/** The row the keys act on. */
|
|
68
|
+
current?: boolean;
|
|
69
|
+
/** The badge's words, when the surface's are not the defaults (another language). */
|
|
70
|
+
stateLabel?: ReactNode;
|
|
71
|
+
/** The row's id, and the base of the ids inside it. Default from `keyName`; pass one when a key shows twice on a page. */
|
|
72
|
+
id?: string;
|
|
73
|
+
/** `li` inside the surface's `<ol>` (default `div`). */
|
|
74
|
+
as?: "div" | "li";
|
|
75
|
+
className?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The base of a row's ids when the surface gives none: "hero.title" → "ui-tr-hero-title". */
|
|
79
|
+
export function translationRowId(keyName: string): string {
|
|
80
|
+
return `ui-tr-${keyName.replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "") || "row"}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function TranslationRow({
|
|
84
|
+
keyName, state, source, sourceLang, target, checks, by, action, current, stateLabel, id, as = "div", className, ...rest
|
|
85
|
+
}: TranslationRowProps) {
|
|
86
|
+
const base = id ?? translationRowId(keyName);
|
|
87
|
+
const ids: TranslationRowIds = { key: `${base}-key`, source: `${base}-src`, checks: `${base}-checks` };
|
|
88
|
+
const Tag = as;
|
|
89
|
+
return (
|
|
90
|
+
<Tag {...rest} id={id} className={cx("ui-translation", current && "ui-translation--current", className)} data-state={state} tabIndex={-1}
|
|
91
|
+
aria-current={current ? "true" : undefined}>
|
|
92
|
+
<div className="ui-translation__head">
|
|
93
|
+
<code className="ui-translation__key" id={ids.key}>{keyName}</code>
|
|
94
|
+
<span className={cx("ui-badge", TONE[state], "ui-translation__state")}>{stateLabel ?? TRANSLATION_STATE_LABELS[state] ?? state}</span>
|
|
95
|
+
{by ? <span className="ui-translation__by">{by}</span> : null}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="ui-translation__body">
|
|
98
|
+
<p className="ui-translation__source" id={ids.source} lang={sourceLang}>{source}</p>
|
|
99
|
+
<div className="ui-translation__target">{typeof target === "function" ? target(ids) : target}</div>
|
|
100
|
+
</div>
|
|
101
|
+
{checks || action ? (
|
|
102
|
+
<div className="ui-translation__foot">
|
|
103
|
+
{checks ? (
|
|
104
|
+
<span className={cx("ui-translation__checks", checks.ok ? "ui-translation__checks--ok" : "ui-translation__checks--warn")} id={ids.checks}>
|
|
105
|
+
{checks.ok ? <span aria-hidden="true">✓ </span> : null}{checks.text}
|
|
106
|
+
</span>
|
|
107
|
+
) : null}
|
|
108
|
+
{action ? <span className="ui-translation__action">{action}</span> : null}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</Tag>
|
|
112
|
+
);
|
|
113
|
+
}
|