@twentyfirstmedia/ui-core 0.5.0 → 0.6.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/dist/components/admin.d.ts.map +1 -1
- package/dist/components/admin.js +31 -21
- package/dist/components/admin.js.map +1 -1
- package/dist/components/agents.d.ts.map +1 -1
- package/dist/components/agents.js +21 -18
- package/dist/components/agents.js.map +1 -1
- package/dist/components/app-chrome.d.ts.map +1 -1
- package/dist/components/app-chrome.js +5 -5
- package/dist/components/app-chrome.js.map +1 -1
- package/dist/components/approvals.d.ts.map +1 -1
- package/dist/components/approvals.js +2 -2
- package/dist/components/approvals.js.map +1 -1
- package/dist/components/building-blocks.d.ts.map +1 -1
- package/dist/components/building-blocks.js +13 -10
- package/dist/components/building-blocks.js.map +1 -1
- package/dist/components/charts.d.ts +2 -2
- package/dist/components/charts.d.ts.map +1 -1
- package/dist/components/charts.js +14 -11
- package/dist/components/charts.js.map +1 -1
- package/dist/components/chat.d.ts +2 -2
- package/dist/components/chat.d.ts.map +1 -1
- package/dist/components/chat.js +22 -18
- package/dist/components/chat.js.map +1 -1
- package/dist/components/commerce.d.ts.map +1 -1
- package/dist/components/commerce.js +20 -16
- package/dist/components/commerce.js.map +1 -1
- package/dist/components/data-display.d.ts +0 -5
- package/dist/components/data-display.d.ts.map +1 -1
- package/dist/components/data-display.js +41 -22
- package/dist/components/data-display.js.map +1 -1
- package/dist/components/data-table.d.ts +2 -2
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +3 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/developer.d.ts.map +1 -1
- package/dist/components/developer.js +7 -6
- package/dist/components/developer.js.map +1 -1
- package/dist/components/feedback.d.ts.map +1 -1
- package/dist/components/feedback.js +5 -4
- package/dist/components/feedback.js.map +1 -1
- package/dist/components/forms.d.ts.map +1 -1
- package/dist/components/forms.js +63 -21
- package/dist/components/forms.js.map +1 -1
- package/dist/components/foundation.d.ts.map +1 -1
- package/dist/components/foundation.js +13 -9
- package/dist/components/foundation.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/inputs.d.ts.map +1 -1
- package/dist/components/inputs.js +33 -8
- package/dist/components/inputs.js.map +1 -1
- package/dist/components/mail.d.ts.map +1 -1
- package/dist/components/mail.js +5 -5
- package/dist/components/mail.js.map +1 -1
- package/dist/components/media.d.ts.map +1 -1
- package/dist/components/media.js +62 -11
- package/dist/components/media.js.map +1 -1
- package/dist/components/navigation.d.ts +3 -1
- package/dist/components/navigation.d.ts.map +1 -1
- package/dist/components/navigation.js +287 -20
- package/dist/components/navigation.js.map +1 -1
- package/dist/components/operations.d.ts.map +1 -1
- package/dist/components/operations.js +10 -10
- package/dist/components/operations.js.map +1 -1
- package/dist/components/overlays.d.ts.map +1 -1
- package/dist/components/overlays.js +2 -2
- package/dist/components/overlays.js.map +1 -1
- package/dist/components/previews.js +2 -2
- package/dist/components/previews.js.map +1 -1
- package/dist/components/primitives.js +2 -2
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/qr-code.d.ts +8 -0
- package/dist/components/qr-code.d.ts.map +1 -0
- package/dist/components/qr-code.js +18 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/real-estate.d.ts.map +1 -1
- package/dist/components/real-estate.js +5 -5
- package/dist/components/real-estate.js.map +1 -1
- package/dist/components/rich-text-editor.d.ts.map +1 -1
- package/dist/components/rich-text-editor.js +1 -1
- package/dist/components/rich-text-editor.js.map +1 -1
- package/dist/components/select-menu.js +1 -1
- package/dist/components/select-menu.js.map +1 -1
- package/dist/components/shell.d.ts +0 -4
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +100 -7
- package/dist/components/shell.js.map +1 -1
- package/dist/components/site.d.ts.map +1 -1
- package/dist/components/site.js +8 -6
- package/dist/components/site.js.map +1 -1
- package/dist/components/status.d.ts.map +1 -1
- package/dist/components/status.js +2 -2
- package/dist/components/status.js.map +1 -1
- package/dist/components/system.d.ts.map +1 -1
- package/dist/components/system.js +3 -2
- package/dist/components/system.js.map +1 -1
- package/dist/components/types.d.ts +257 -18
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +15 -12
- package/dist/components/workspace.js.map +1 -1
- package/dist/qr.d.ts +14 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +298 -0
- package/dist/qr.js.map +1 -0
- package/dist/strings.d.ts +30 -4
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +15 -0
- package/dist/strings.js.map +1 -1
- package/dist/themes.css +2345 -176
- package/package.json +2 -1
- package/src/components/admin.tsx +66 -23
- package/src/components/agents.tsx +22 -20
- package/src/components/app-chrome.tsx +9 -5
- package/src/components/approvals.tsx +3 -1
- package/src/components/building-blocks.tsx +24 -13
- package/src/components/charts.tsx +17 -12
- package/src/components/chat.tsx +54 -31
- package/src/components/commerce.tsx +58 -24
- package/src/components/data-display.tsx +106 -38
- package/src/components/data-table.tsx +11 -7
- package/src/components/developer.tsx +15 -6
- package/src/components/feedback.tsx +5 -3
- package/src/components/forms.tsx +101 -23
- package/src/components/foundation.tsx +9 -5
- package/src/components/index.ts +10 -1
- package/src/components/inputs.tsx +44 -7
- package/src/components/mail.tsx +7 -3
- package/src/components/media.tsx +88 -16
- package/src/components/navigation.tsx +651 -71
- package/src/components/operations.tsx +17 -8
- package/src/components/overlays.tsx +7 -2
- package/src/components/previews.tsx +2 -2
- package/src/components/primitives.tsx +2 -2
- package/src/components/qr-code.tsx +30 -0
- package/src/components/real-estate.tsx +7 -4
- package/src/components/rich-text-editor.tsx +7 -1
- package/src/components/select-menu.tsx +1 -1
- package/src/components/shell.tsx +237 -11
- package/src/components/site.tsx +18 -4
- package/src/components/status.tsx +2 -0
- package/src/components/system.tsx +3 -2
- package/src/components/types.ts +261 -18
- package/src/components/workspace.tsx +37 -15
- package/src/qr.ts +300 -0
- package/src/strings.ts +56 -1
|
@@ -38,7 +38,7 @@ export function ConnectionCard(p: ConnectionCardProps) {
|
|
|
38
38
|
} as Record<string, Status>
|
|
39
39
|
)[p.status] || "idle";
|
|
40
40
|
return (
|
|
41
|
-
<div className="tf-conn">
|
|
41
|
+
<div style={p.style} className={cx("tf-conn", p.className)}>
|
|
42
42
|
<span className="tf-conn-ico">{p.logo || <Icon name={p.icon || "link"} size={18} />}</span>
|
|
43
43
|
<span className="tf-option-text">
|
|
44
44
|
<b>{p.name}</b>
|
|
@@ -54,7 +54,7 @@ export function ConnectionBanner(p: ConnectionBannerProps) {
|
|
|
54
54
|
const t = useStrings(OPERATIONS_STRINGS);
|
|
55
55
|
if (!p.state || p.state === "online") return null;
|
|
56
56
|
return (
|
|
57
|
-
<div className={cx("tf-connbanner", p.state)} role="status">
|
|
57
|
+
<div style={p.style} className={cx("tf-connbanner", p.state, p.className)} role="status">
|
|
58
58
|
<StatusShape status={p.state === "offline" ? "danger" : "warn"} />
|
|
59
59
|
<span>{p.text || (p.state === "offline" ? t.offline : t.connectionLost(p.seconds))}</span>
|
|
60
60
|
{p.onRetry && (
|
|
@@ -69,7 +69,7 @@ export function ConnectionBanner(p: ConnectionBannerProps) {
|
|
|
69
69
|
export function ServerCard(p: ServerCardProps) {
|
|
70
70
|
const t = useStrings(OPERATIONS_STRINGS);
|
|
71
71
|
return (
|
|
72
|
-
<div className="tf-server">
|
|
72
|
+
<div style={p.style} className={cx("tf-server", p.className)}>
|
|
73
73
|
<div className="tf-server-top">
|
|
74
74
|
<span className="tf-conn-ico">{p.logo || <Icon name="server" size={18} />}</span>
|
|
75
75
|
<span className="tf-option-text">
|
|
@@ -104,7 +104,12 @@ export function ServerCard(p: ServerCardProps) {
|
|
|
104
104
|
export function DeployPipeline(p: DeployPipelineProps) {
|
|
105
105
|
const t = useStrings(OPERATIONS_STRINGS);
|
|
106
106
|
return (
|
|
107
|
-
<ol
|
|
107
|
+
<ol
|
|
108
|
+
style={p.style}
|
|
109
|
+
className={cx("tf-pipeline", p.className)}
|
|
110
|
+
tabIndex={0}
|
|
111
|
+
aria-label={t.deployPipeline}
|
|
112
|
+
>
|
|
108
113
|
{(p.stages || []).map((s, i) => (
|
|
109
114
|
<li key={i} className={cx("tf-stage", "st-" + (s.status || "idle"))}>
|
|
110
115
|
<StatusShape status={s.status === "done" ? "ok" : s.status || "idle"} />
|
|
@@ -121,6 +126,8 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
|
|
|
121
126
|
const t = useStrings(OPERATIONS_STRINGS);
|
|
122
127
|
return (
|
|
123
128
|
<DataTable
|
|
129
|
+
style={p.style}
|
|
130
|
+
className={p.className}
|
|
124
131
|
rows={p.records || []}
|
|
125
132
|
rowKey="id"
|
|
126
133
|
columnToggle={false}
|
|
@@ -221,7 +228,7 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
|
|
|
221
228
|
export function DomainList(p: DomainListProps) {
|
|
222
229
|
const t = useStrings(OPERATIONS_STRINGS);
|
|
223
230
|
return (
|
|
224
|
-
<div className="tf-domains">
|
|
231
|
+
<div style={p.style} className={cx("tf-domains", p.className)}>
|
|
225
232
|
{(p.items || [])
|
|
226
233
|
.slice()
|
|
227
234
|
.sort((a, b) => (a.days == null ? 9999 : a.days) - (b.days == null ? 9999 : b.days))
|
|
@@ -262,7 +269,7 @@ export function MailAuthStatus(p: MailAuthStatusProps) {
|
|
|
262
269
|
const box = React.useRef<HTMLDivElement>(null);
|
|
263
270
|
useScrollFocus(box);
|
|
264
271
|
return (
|
|
265
|
-
<div ref={box} className="tf-scroll-x">
|
|
272
|
+
<div style={p.style} ref={box} className={cx("tf-scroll-x", p.className)}>
|
|
266
273
|
<table className="tf-table tf-mailauth">
|
|
267
274
|
<thead>
|
|
268
275
|
<tr>
|
|
@@ -310,6 +317,8 @@ export function LedgerTable(p: LedgerTableProps) {
|
|
|
310
317
|
const cur = p.currency;
|
|
311
318
|
return (
|
|
312
319
|
<DataTable
|
|
320
|
+
style={p.style}
|
|
321
|
+
className={p.className}
|
|
313
322
|
rows={p.rows || []}
|
|
314
323
|
selectable={p.selectable}
|
|
315
324
|
bulkActions={p.bulkActions}
|
|
@@ -427,7 +436,7 @@ export function PnLStatement(p: PnLStatementProps) {
|
|
|
427
436
|
const box = React.useRef<HTMLDivElement>(null);
|
|
428
437
|
useScrollFocus(box);
|
|
429
438
|
return (
|
|
430
|
-
<div ref={box} className="tf-scroll-x">
|
|
439
|
+
<div style={p.style} ref={box} className={cx("tf-scroll-x", p.className)}>
|
|
431
440
|
<table className="tf-table tf-pnl">
|
|
432
441
|
<thead>
|
|
433
442
|
<tr>
|
|
@@ -476,7 +485,7 @@ export function ReceiptSuggestion(p: ReceiptSuggestionProps) {
|
|
|
476
485
|
const t = useStrings(OPERATIONS_STRINGS);
|
|
477
486
|
const { formatPercent } = useFormat();
|
|
478
487
|
return (
|
|
479
|
-
<div className="tf-receipt">
|
|
488
|
+
<div style={p.style} className={cx("tf-receipt", p.className)}>
|
|
480
489
|
<div className="tf-receipt-src">
|
|
481
490
|
<TintIcon icon="paperclip" tint="sky" />
|
|
482
491
|
<span className="tf-option-text">
|
|
@@ -52,7 +52,11 @@ export function Popover(p: PopoverProps) {
|
|
|
52
52
|
</span>
|
|
53
53
|
);
|
|
54
54
|
return (
|
|
55
|
-
<span
|
|
55
|
+
<span
|
|
56
|
+
style={p.style}
|
|
57
|
+
ref={ref}
|
|
58
|
+
className={cx("tf-pop-anchor", p.block && "block", p.className)}
|
|
59
|
+
>
|
|
56
60
|
{trigger}
|
|
57
61
|
{pr.mounted && (
|
|
58
62
|
<div
|
|
@@ -108,7 +112,8 @@ export function Tooltip(p: TooltipProps) {
|
|
|
108
112
|
const child = React.Children.only(p.children) as React.ReactElement<Record<string, unknown>>;
|
|
109
113
|
return (
|
|
110
114
|
<span
|
|
111
|
-
|
|
115
|
+
style={p.style}
|
|
116
|
+
className={cx("tf-tip-anchor", p.className)}
|
|
112
117
|
onPointerEnter={show}
|
|
113
118
|
onPointerLeave={hide}
|
|
114
119
|
onFocus={() => {
|
|
@@ -15,7 +15,7 @@ export function EmailPreview(p: EmailPreviewProps) {
|
|
|
15
15
|
const t = useStrings(PREVIEWS_STRINGS);
|
|
16
16
|
const dev = useControlled<string>(p.device, "desktop", p.onDevice);
|
|
17
17
|
return (
|
|
18
|
-
<div className="tf-mailprev">
|
|
18
|
+
<div style={p.style} className={cx("tf-mailprev", p.className)}>
|
|
19
19
|
<div className="tf-mailprev-head">
|
|
20
20
|
<div className="tf-mailprev-inbox">
|
|
21
21
|
<Avatar name={p.fromName || p.from || ""} size={36} />
|
|
@@ -90,7 +90,7 @@ export function SearchAdPreview(p: SearchAdPreviewProps) {
|
|
|
90
90
|
.map((x) => " › " + x)
|
|
91
91
|
.join("");
|
|
92
92
|
return (
|
|
93
|
-
<div className="tf-adprev">
|
|
93
|
+
<div style={p.style} className={cx("tf-adprev", p.className)}>
|
|
94
94
|
<div
|
|
95
95
|
className="tf-row"
|
|
96
96
|
style={{
|
|
@@ -161,7 +161,7 @@ export function ToggleGroup(p: ToggleGroupProps) {
|
|
|
161
161
|
return (
|
|
162
162
|
<div
|
|
163
163
|
ref={root}
|
|
164
|
-
className={cx("tf-togglegroup", p.size === "sm" && "sm", p.className)}
|
|
164
|
+
className={cx("tf-togglegroup", p.size === "sm" && "sm", p.block && "block", p.className)}
|
|
165
165
|
style={p.style}
|
|
166
166
|
role="group"
|
|
167
167
|
aria-label={p.label}
|
|
@@ -191,7 +191,7 @@ export function ToggleGroup(p: ToggleGroupProps) {
|
|
|
191
191
|
}
|
|
192
192
|
>
|
|
193
193
|
{o.icon && <Icon name={o.icon} />}
|
|
194
|
-
{o.label}
|
|
194
|
+
{o.label && <span className="tf-toggle-label">{o.label}</span>}
|
|
195
195
|
</button>
|
|
196
196
|
);
|
|
197
197
|
})}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { useStrings } from "../context.js";
|
|
3
|
+
import { qrMatrix, qrPath } from "../qr.js";
|
|
4
|
+
import { ADMIN_STRINGS } from "../strings.js";
|
|
5
|
+
import type { QrCodeProps } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A QR code drawn as SVG, encoded on the device (no service is called): for otpauth:// links in
|
|
9
|
+
* two-factor setup, sign-in on another device, sharing a link. Dark modules on the paper colour,
|
|
10
|
+
* so it scans in dark mode too.
|
|
11
|
+
*/
|
|
12
|
+
export function QrCode(p: QrCodeProps) {
|
|
13
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
14
|
+
const quiet = p.quiet ?? 4;
|
|
15
|
+
const m = qrMatrix(p.value, p.level || "M");
|
|
16
|
+
const n = m.length + quiet * 2;
|
|
17
|
+
return (
|
|
18
|
+
<svg
|
|
19
|
+
className={cx("tf-qr", p.className)}
|
|
20
|
+
style={{ ...(p.size ? { width: p.size, height: p.size } : null), ...p.style }}
|
|
21
|
+
viewBox={`0 0 ${n} ${n}`}
|
|
22
|
+
role="img"
|
|
23
|
+
aria-label={p.label || t.qrCode}
|
|
24
|
+
shapeRendering="crispEdges"
|
|
25
|
+
>
|
|
26
|
+
<rect className="tf-qr-bg" width={n} height={n} />
|
|
27
|
+
<path d={qrPath(m, quiet)} fill="currentColor" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -53,11 +53,13 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
53
53
|
const x = p.listing || p;
|
|
54
54
|
return (
|
|
55
55
|
<article
|
|
56
|
+
style={p.style}
|
|
56
57
|
className={cx(
|
|
57
58
|
"tf-listing",
|
|
58
59
|
p.layout === "row" && "row",
|
|
59
60
|
p.selected && "selected",
|
|
60
61
|
x.status === "unread" && "unread",
|
|
62
|
+
p.className,
|
|
61
63
|
)}
|
|
62
64
|
onClick={
|
|
63
65
|
p.onOpen &&
|
|
@@ -168,7 +170,7 @@ export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
|
168
170
|
const all = p.total > 0 && p.selected === p.total,
|
|
169
171
|
some = (p.selected ?? 0) > 0 && !all;
|
|
170
172
|
return (
|
|
171
|
-
<div className="tf-results">
|
|
173
|
+
<div style={p.style} className={cx("tf-results", p.className)}>
|
|
172
174
|
{p.onSelectAll && (
|
|
173
175
|
<label className="tf-check">
|
|
174
176
|
{cbox({
|
|
@@ -248,7 +250,7 @@ export function SavedSearch(p: SavedSearchProps) {
|
|
|
248
250
|
const t = useStrings(REAL_ESTATE_STRINGS);
|
|
249
251
|
const { formatNumber } = useFormat();
|
|
250
252
|
return (
|
|
251
|
-
<div className={cx("tf-saved", p.enabled === false && "off")}>
|
|
253
|
+
<div style={p.style} className={cx("tf-saved", p.enabled === false && "off", p.className)}>
|
|
252
254
|
<div className="tf-saved-head">
|
|
253
255
|
<TintIcon icon={p.icon || "search"} tint={p.tint || "sky"} size={36} />
|
|
254
256
|
<div
|
|
@@ -318,7 +320,7 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
318
320
|
? ["warn", t.mediumConfidence]
|
|
319
321
|
: ["danger", t.lowConfidence];
|
|
320
322
|
return (
|
|
321
|
-
<div className="tf-valuation">
|
|
323
|
+
<div style={p.style} className={cx("tf-valuation", p.className)}>
|
|
322
324
|
<div className="tf-valuation-head">
|
|
323
325
|
<span
|
|
324
326
|
className="tf-menu-label"
|
|
@@ -403,7 +405,8 @@ export function EnergyClass(p: EnergyClassProps) {
|
|
|
403
405
|
i = scale.indexOf(p.value);
|
|
404
406
|
return (
|
|
405
407
|
<div
|
|
406
|
-
|
|
408
|
+
style={p.style}
|
|
409
|
+
className={cx("tf-energy", p.className)}
|
|
407
410
|
role="img"
|
|
408
411
|
aria-label={
|
|
409
412
|
t.energyClass(p.value) + (p.kwh != null ? ", " + formatNumber(p.kwh) + " kWh/(m²·a)" : "")
|
|
@@ -487,7 +487,13 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
487
487
|
? "⌘"
|
|
488
488
|
: "Ctrl+";
|
|
489
489
|
return (
|
|
490
|
-
<FormField
|
|
490
|
+
<FormField
|
|
491
|
+
style={p.style}
|
|
492
|
+
className={p.className}
|
|
493
|
+
label={p.label}
|
|
494
|
+
hint={p.hint}
|
|
495
|
+
error={p.error}
|
|
496
|
+
>
|
|
491
497
|
<div className={cx("tf-rte", over && "invalid")}>
|
|
492
498
|
<div className="tf-rte-bar" role="toolbar" aria-label={strings.formatting}>
|
|
493
499
|
{tools.map((t, i) => {
|
|
@@ -103,7 +103,7 @@ export function SelectMenu(p: SelectMenuProps) {
|
|
|
103
103
|
ref={btn}
|
|
104
104
|
id={id}
|
|
105
105
|
type="button"
|
|
106
|
-
className={cx("tf-input", "tf-selectmenu", !cur && "empty")}
|
|
106
|
+
className={cx("tf-input", "tf-selectmenu", p.size === "sm" && "sm", !cur && "empty")}
|
|
107
107
|
role="combobox"
|
|
108
108
|
aria-haspopup="listbox"
|
|
109
109
|
aria-expanded={open}
|
package/src/components/shell.tsx
CHANGED
|
@@ -5,6 +5,10 @@ import { SHELL_STRINGS } from "../strings.js";
|
|
|
5
5
|
import { Icon } from "../Icon.js";
|
|
6
6
|
import { cx, reduced } from "../cx.js";
|
|
7
7
|
import { Avatar } from "./foundation.js";
|
|
8
|
+
import { Tooltip } from "./overlays.js";
|
|
9
|
+
import { useControlled } from "../hooks.js";
|
|
10
|
+
import { Sidebar, UserMenu } from "./navigation.js";
|
|
11
|
+
import { WorkspaceSwitcher } from "./admin.js";
|
|
8
12
|
import type {
|
|
9
13
|
AppShellProps,
|
|
10
14
|
DockItem,
|
|
@@ -17,10 +21,43 @@ import type {
|
|
|
17
21
|
TopBarProps,
|
|
18
22
|
} from "./types.js";
|
|
19
23
|
|
|
24
|
+
/** True while the nav's labelled width does not fit the space the bar leaves it. */
|
|
25
|
+
function useTight(nav: React.RefObject<HTMLDivElement | null>, key: string) {
|
|
26
|
+
const [tight, setTight] = React.useState(false);
|
|
27
|
+
const full = React.useRef(0);
|
|
28
|
+
React.useLayoutEffect(() => {
|
|
29
|
+
const el = nav.current,
|
|
30
|
+
bar = el?.parentElement;
|
|
31
|
+
if (!el || !bar || typeof ResizeObserver === "undefined") return;
|
|
32
|
+
const check = () => {
|
|
33
|
+
const cs = getComputedStyle(bar);
|
|
34
|
+
const kids = Array.from(bar.children) as HTMLElement[];
|
|
35
|
+
const others = kids.reduce((s, k) => (k === el ? s : s + k.offsetWidth), 0);
|
|
36
|
+
const gaps = (parseFloat(cs.columnGap) || 0) * (kids.length - 1);
|
|
37
|
+
const avail =
|
|
38
|
+
bar.clientWidth -
|
|
39
|
+
(parseFloat(cs.paddingLeft) || 0) -
|
|
40
|
+
(parseFloat(cs.paddingRight) || 0) -
|
|
41
|
+
others -
|
|
42
|
+
gaps;
|
|
43
|
+
if (!el.classList.contains("tight")) full.current = el.scrollWidth;
|
|
44
|
+
setTight(full.current > avail + 1);
|
|
45
|
+
};
|
|
46
|
+
const ro = new ResizeObserver(check);
|
|
47
|
+
ro.observe(bar);
|
|
48
|
+
check();
|
|
49
|
+
return () => ro.disconnect();
|
|
50
|
+
}, [nav, key]);
|
|
51
|
+
return tight;
|
|
52
|
+
}
|
|
53
|
+
|
|
20
54
|
export function TopBar(p: TopBarProps) {
|
|
21
55
|
const t = useStrings(SHELL_STRINGS);
|
|
56
|
+
const navRef = React.useRef<HTMLDivElement>(null);
|
|
57
|
+
// labels give way to icons when the bar runs out of room, before anything scrolls
|
|
58
|
+
const tight = useTight(navRef, (p.items || []).map((it) => it.label).join("|"));
|
|
22
59
|
return (
|
|
23
|
-
<nav className="tf-topbar" aria-label={t.mainNavigation}>
|
|
60
|
+
<nav style={p.style} className={cx("tf-topbar", p.className)} aria-label={t.mainNavigation}>
|
|
24
61
|
{p.brand && (
|
|
25
62
|
<span className="tf-brand">
|
|
26
63
|
{p.brand}
|
|
@@ -28,7 +65,7 @@ export function TopBar(p: TopBarProps) {
|
|
|
28
65
|
{p.tag && <span className="tf-brand-tag">{p.tag}</span>}
|
|
29
66
|
</span>
|
|
30
67
|
)}
|
|
31
|
-
<div className="tf-nav">
|
|
68
|
+
<div ref={navRef} className={cx("tf-nav", tight && "tight")}>
|
|
32
69
|
{(p.items || []).map((it) => {
|
|
33
70
|
const active = it.label === p.active;
|
|
34
71
|
return (
|
|
@@ -56,6 +93,7 @@ export function TopBar(p: TopBarProps) {
|
|
|
56
93
|
className="tf-user"
|
|
57
94
|
aria-haspopup="menu"
|
|
58
95
|
aria-expanded={p.userOpen ? "true" : "false"}
|
|
96
|
+
title={p.user.email ? `${p.user.name} · ${p.user.email}` : p.user.name}
|
|
59
97
|
onClick={p.onUser}
|
|
60
98
|
>
|
|
61
99
|
<Avatar name={p.user.name} size={28} />
|
|
@@ -70,8 +108,8 @@ export function TopBar(p: TopBarProps) {
|
|
|
70
108
|
);
|
|
71
109
|
}
|
|
72
110
|
|
|
73
|
-
function railBtn(it: RailItem, i: number) {
|
|
74
|
-
|
|
111
|
+
function railBtn(it: RailItem, i: number, expanded: boolean) {
|
|
112
|
+
const btn = (
|
|
75
113
|
<button
|
|
76
114
|
key={it.label}
|
|
77
115
|
type="button"
|
|
@@ -80,22 +118,69 @@ function railBtn(it: RailItem, i: number) {
|
|
|
80
118
|
animationDelay: `calc(${200 + i * 50}ms * var(--motion-time, 1))`,
|
|
81
119
|
}}
|
|
82
120
|
aria-label={it.count ? `${it.label}, ${it.count}` : it.label}
|
|
83
|
-
title={it.label}
|
|
84
121
|
aria-current={it.active ? "page" : undefined}
|
|
85
122
|
onClick={it.onClick}
|
|
86
123
|
>
|
|
87
124
|
<Icon name={it.icon} />
|
|
125
|
+
{expanded && (
|
|
126
|
+
<span className="tf-rail-label" aria-hidden>
|
|
127
|
+
{it.label}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
88
130
|
{it.count ? <span className="tf-dotcount">{it.count}</span> : null}
|
|
89
131
|
</button>
|
|
90
132
|
);
|
|
133
|
+
return expanded ? (
|
|
134
|
+
btn
|
|
135
|
+
) : (
|
|
136
|
+
<Tooltip key={it.label} label={it.label} side="right">
|
|
137
|
+
{btn}
|
|
138
|
+
</Tooltip>
|
|
139
|
+
);
|
|
91
140
|
}
|
|
92
141
|
|
|
93
142
|
export function SideRail(p: SideRailProps) {
|
|
94
143
|
const t = useStrings(SHELL_STRINGS);
|
|
144
|
+
const [expanded, setExpanded] = useControlled(
|
|
145
|
+
p.expanded,
|
|
146
|
+
!!p.defaultExpanded,
|
|
147
|
+
p.onExpandedChange,
|
|
148
|
+
);
|
|
149
|
+
const toggleLabel = expanded ? t.collapseRail : t.expandRail;
|
|
150
|
+
const toggle = (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
className="tf-rail-btn tf-rail-toggle"
|
|
154
|
+
aria-label={toggleLabel}
|
|
155
|
+
aria-expanded={expanded}
|
|
156
|
+
onClick={() => setExpanded(!expanded)}
|
|
157
|
+
>
|
|
158
|
+
<Icon name={expanded ? "chevronLeft" : "chevronRight"} />
|
|
159
|
+
{expanded && (
|
|
160
|
+
<span className="tf-rail-label" aria-hidden>
|
|
161
|
+
{toggleLabel}
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
</button>
|
|
165
|
+
);
|
|
95
166
|
return (
|
|
96
|
-
<aside
|
|
97
|
-
|
|
98
|
-
|
|
167
|
+
<aside
|
|
168
|
+
style={p.style}
|
|
169
|
+
className={cx("tf-rail", expanded && "expanded", p.className)}
|
|
170
|
+
aria-label={p.label || t.tools}
|
|
171
|
+
>
|
|
172
|
+
<div className="tf-rail-group">{(p.top || []).map((it, i) => railBtn(it, i, expanded))}</div>
|
|
173
|
+
<div className="tf-rail-group">
|
|
174
|
+
{(p.bottom || []).map((it, i) => railBtn(it, i + 3, expanded))}
|
|
175
|
+
{p.toggle &&
|
|
176
|
+
(expanded ? (
|
|
177
|
+
toggle
|
|
178
|
+
) : (
|
|
179
|
+
<Tooltip label={toggleLabel} side="right">
|
|
180
|
+
{toggle}
|
|
181
|
+
</Tooltip>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
99
184
|
</aside>
|
|
100
185
|
);
|
|
101
186
|
}
|
|
@@ -129,12 +214,72 @@ export function Dock(p: DockProps) {
|
|
|
129
214
|
* UI floating over a full-bleed background (a map, a 3D scene, a video): four slots along the
|
|
130
215
|
* edges, glass by default. The background stays interactive wherever no slot sits on it.
|
|
131
216
|
*/
|
|
217
|
+
/** Measures how far the slots reach into the layout and publishes it as CSS variables. */
|
|
218
|
+
function useSafeArea(root: React.RefObject<HTMLDivElement | null>) {
|
|
219
|
+
React.useLayoutEffect(() => {
|
|
220
|
+
const el = root.current;
|
|
221
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
222
|
+
const slot = (c: string) => el.querySelector<HTMLElement>(`:scope > .tf-floating-ui > .${c}`);
|
|
223
|
+
const measure = () => {
|
|
224
|
+
const r = el.getBoundingClientRect();
|
|
225
|
+
const top = slot("tf-floating-top"),
|
|
226
|
+
start = slot("tf-floating-start"),
|
|
227
|
+
end = slot("tf-floating-end"),
|
|
228
|
+
bottom = slot("tf-floating-bottom");
|
|
229
|
+
const px = (n: number) => `${Math.max(0, Math.round(n))}px`;
|
|
230
|
+
el.style.setProperty(
|
|
231
|
+
"--floating-safe-top",
|
|
232
|
+
px(top ? top.getBoundingClientRect().bottom - r.top : 0),
|
|
233
|
+
);
|
|
234
|
+
el.style.setProperty(
|
|
235
|
+
"--floating-safe-start",
|
|
236
|
+
px(start ? start.getBoundingClientRect().right - r.left : 0),
|
|
237
|
+
);
|
|
238
|
+
el.style.setProperty(
|
|
239
|
+
"--floating-safe-end",
|
|
240
|
+
px(end ? r.right - end.getBoundingClientRect().left : 0),
|
|
241
|
+
);
|
|
242
|
+
el.style.setProperty(
|
|
243
|
+
"--floating-safe-bottom",
|
|
244
|
+
// the bottom bar, or the end slot while it is a bottom sheet (phones)
|
|
245
|
+
px(
|
|
246
|
+
Math.max(
|
|
247
|
+
bottom ? r.bottom - bottom.getBoundingClientRect().top : 0,
|
|
248
|
+
end && getComputedStyle(end).position === "absolute"
|
|
249
|
+
? r.bottom - end.getBoundingClientRect().top
|
|
250
|
+
: 0,
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
const ro = new ResizeObserver(measure);
|
|
256
|
+
ro.observe(el);
|
|
257
|
+
el.querySelectorAll(":scope > .tf-floating-ui > *").forEach((c) => ro.observe(c));
|
|
258
|
+
measure();
|
|
259
|
+
return () => ro.disconnect();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
132
263
|
export function FloatingLayout(p: FloatingLayoutProps) {
|
|
264
|
+
const t = useStrings(SHELL_STRINGS);
|
|
133
265
|
const surface = p.surface ?? "glass";
|
|
266
|
+
const root = React.useRef<HTMLDivElement>(null);
|
|
267
|
+
const [sheetFull, setSheetFull] = React.useState(false);
|
|
268
|
+
const sheet = p.end != null && p.endSheet !== false;
|
|
269
|
+
useSafeArea(root);
|
|
270
|
+
const size = (v: number | string | undefined) => (typeof v === "number" ? `${v}px` : v);
|
|
134
271
|
return (
|
|
135
272
|
<div
|
|
136
|
-
|
|
137
|
-
|
|
273
|
+
ref={root}
|
|
274
|
+
className={cx("tf-floating", sheet && "has-sheet", sheetFull && "sheet-full", p.className)}
|
|
275
|
+
style={
|
|
276
|
+
{
|
|
277
|
+
minHeight: p.height ?? 640,
|
|
278
|
+
...(p.startWidth != null ? { "--floating-start": size(p.startWidth) } : null),
|
|
279
|
+
...(p.endWidth != null ? { "--floating-end": size(p.endWidth) } : null),
|
|
280
|
+
...p.style,
|
|
281
|
+
} as React.CSSProperties
|
|
282
|
+
}
|
|
138
283
|
role="region"
|
|
139
284
|
aria-label={p.label}
|
|
140
285
|
data-surface={surface}
|
|
@@ -143,7 +288,28 @@ export function FloatingLayout(p: FloatingLayoutProps) {
|
|
|
143
288
|
<div className="tf-floating-ui">
|
|
144
289
|
{p.top != null && <div className="tf-floating-top">{p.top}</div>}
|
|
145
290
|
{p.start != null && <div className="tf-floating-start">{p.start}</div>}
|
|
146
|
-
{p.
|
|
291
|
+
{(p.controls != null || p.attribution != null) && (
|
|
292
|
+
<div className="tf-floating-controls">
|
|
293
|
+
{p.controls}
|
|
294
|
+
{p.attribution != null && (
|
|
295
|
+
<div className="tf-floating-attribution">{p.attribution}</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
{p.end != null && (
|
|
300
|
+
<div className="tf-floating-end">
|
|
301
|
+
{sheet && (
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
className="tf-floating-handle"
|
|
305
|
+
aria-expanded={sheetFull}
|
|
306
|
+
aria-label={sheetFull ? t.collapsePanel : t.expandPanel}
|
|
307
|
+
onClick={() => setSheetFull(!sheetFull)}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
{p.end}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
147
313
|
{p.bottom != null && <div className="tf-floating-bottom">{p.bottom}</div>}
|
|
148
314
|
</div>
|
|
149
315
|
{p.children}
|
|
@@ -318,6 +484,66 @@ const DOCK = (): DockItem[] => [
|
|
|
318
484
|
];
|
|
319
485
|
|
|
320
486
|
export function AppShell(p: AppShellProps) {
|
|
487
|
+
if (p.navigation === "sidebar") {
|
|
488
|
+
const groups = p.sidebarGroups || [
|
|
489
|
+
{
|
|
490
|
+
items: (p.nav || NAV()).map((n) => ({
|
|
491
|
+
id: n.label,
|
|
492
|
+
label: n.label,
|
|
493
|
+
icon: n.icon,
|
|
494
|
+
count: n.count,
|
|
495
|
+
})),
|
|
496
|
+
},
|
|
497
|
+
];
|
|
498
|
+
const brand = (collapsed: boolean) =>
|
|
499
|
+
p.workspaces ? (
|
|
500
|
+
<WorkspaceSwitcher variant="frame" {...p.workspaces} compact={collapsed} />
|
|
501
|
+
) : p.brand ? (
|
|
502
|
+
<span className={cx("tf-brand", collapsed && "tf-brand-mini")}>
|
|
503
|
+
{collapsed && typeof p.brand === "string" ? p.brand.slice(0, 1) : p.brand}
|
|
504
|
+
<span className="tf-brand-dot" aria-hidden />
|
|
505
|
+
{p.tag && !collapsed && <span className="tf-brand-tag">{p.tag}</span>}
|
|
506
|
+
</span>
|
|
507
|
+
) : null;
|
|
508
|
+
return (
|
|
509
|
+
<div className={cx("tf-shell", "tf-shell-sidebar", p.className)} style={p.style}>
|
|
510
|
+
<Sidebar
|
|
511
|
+
variant="shell"
|
|
512
|
+
groups={groups}
|
|
513
|
+
current={p.active}
|
|
514
|
+
onPick={p.onNavigate}
|
|
515
|
+
header={({ collapsed }) => brand(collapsed)}
|
|
516
|
+
footer={({ collapsed }) =>
|
|
517
|
+
p.user ? (
|
|
518
|
+
<UserMenu
|
|
519
|
+
name={p.user.name}
|
|
520
|
+
email={p.user.email}
|
|
521
|
+
items={p.userMenu}
|
|
522
|
+
compact={collapsed}
|
|
523
|
+
/>
|
|
524
|
+
) : null
|
|
525
|
+
}
|
|
526
|
+
{...p.sidebar}
|
|
527
|
+
/>
|
|
528
|
+
<div
|
|
529
|
+
className={cx("tf-sheet", !p.railTop && "tf-sheet-norail", !p.side && "tf-sheet-2")}
|
|
530
|
+
style={p.sheetStyle}
|
|
531
|
+
>
|
|
532
|
+
{p.railTop && <SideRail top={p.railTop} bottom={p.railBottom} />}
|
|
533
|
+
<main key={p.contentKey} className={cx("tf-main", p.dock !== false && "tf-main-dock")}>
|
|
534
|
+
{p.topExtra && <div className="tf-shell-tools">{p.topExtra}</div>}
|
|
535
|
+
{p.children}
|
|
536
|
+
</main>
|
|
537
|
+
{p.side || null}
|
|
538
|
+
{p.dock !== false && (
|
|
539
|
+
<div className="tf-dock-float">
|
|
540
|
+
<Dock items={p.dockItems || DOCK()} />
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
321
547
|
return (
|
|
322
548
|
<div className={cx("tf-shell", p.className)} style={p.style}>
|
|
323
549
|
<TopBar
|
package/src/components/site.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useStrings } from "../context.js";
|
|
4
4
|
import { SITE_STRINGS } from "../strings.js";
|
|
5
|
-
import { useControlled } from "../hooks.js";
|
|
5
|
+
import { useControlled, useId } from "../hooks.js";
|
|
6
6
|
import { cx } from "../cx.js";
|
|
7
7
|
import { Icon } from "../Icon.js";
|
|
8
8
|
import { AgentMark } from "./chat.js";
|
|
@@ -15,7 +15,10 @@ export function ChatLauncher(p: ChatLauncherProps) {
|
|
|
15
15
|
const t = useStrings(SITE_STRINGS);
|
|
16
16
|
const open = useControlled(p.open, false, p.onOpenChange);
|
|
17
17
|
return (
|
|
18
|
-
<div
|
|
18
|
+
<div
|
|
19
|
+
style={p.style}
|
|
20
|
+
className={cx("tf-launch", open[0] && "open", p.inline && "inline", p.className)}
|
|
21
|
+
>
|
|
19
22
|
{open[0] && (
|
|
20
23
|
<div className="tf-launch-panel" role="dialog" aria-label={p.title || t.assistant}>
|
|
21
24
|
<div className="tf-launch-head">
|
|
@@ -61,6 +64,8 @@ export function ChatLauncher(p: ChatLauncherProps) {
|
|
|
61
64
|
|
|
62
65
|
export function CookieConsent(p: CookieConsentProps) {
|
|
63
66
|
const t = useStrings(SITE_STRINGS);
|
|
67
|
+
const auto = useId("cookie");
|
|
68
|
+
const id = p.id || auto;
|
|
64
69
|
const more = React.useState(false),
|
|
65
70
|
cats = React.useState(
|
|
66
71
|
(p.categories || []).reduce<Record<string, boolean>>((a, c) => {
|
|
@@ -79,9 +84,12 @@ export function CookieConsent(p: CookieConsentProps) {
|
|
|
79
84
|
}
|
|
80
85
|
return (
|
|
81
86
|
<div
|
|
82
|
-
|
|
87
|
+
style={p.style}
|
|
88
|
+
id={id}
|
|
89
|
+
className={cx("tf-cookie", p.inline && "inline", p.className)}
|
|
83
90
|
role="dialog"
|
|
84
91
|
aria-label={p.title || t.privacySettings}
|
|
92
|
+
aria-describedby={`${id}-text`}
|
|
85
93
|
>
|
|
86
94
|
<div
|
|
87
95
|
className="tf-row"
|
|
@@ -100,6 +108,7 @@ export function CookieConsent(p: CookieConsentProps) {
|
|
|
100
108
|
>
|
|
101
109
|
<b>{p.title || t.privacySettings}</b>
|
|
102
110
|
<p
|
|
111
|
+
id={`${id}-text`}
|
|
103
112
|
className="tf-card-desc"
|
|
104
113
|
style={{
|
|
105
114
|
margin: 0,
|
|
@@ -168,6 +177,7 @@ export function CookieConsent(p: CookieConsentProps) {
|
|
|
168
177
|
{t.onlyNecessary}
|
|
169
178
|
</Button>
|
|
170
179
|
<Button
|
|
180
|
+
variant={p.equalChoice ? "secondary" : undefined}
|
|
171
181
|
onClick={() => {
|
|
172
182
|
done(true);
|
|
173
183
|
}}
|
|
@@ -184,7 +194,11 @@ export function AnnouncementBar(p: AnnouncementBarProps) {
|
|
|
184
194
|
const shut = React.useState(false);
|
|
185
195
|
if (shut[0]) return null;
|
|
186
196
|
return (
|
|
187
|
-
<div
|
|
197
|
+
<div
|
|
198
|
+
style={p.style}
|
|
199
|
+
className={cx("tf-announce", "tf-t-" + (p.tint || "butter"), p.className)}
|
|
200
|
+
role="status"
|
|
201
|
+
>
|
|
188
202
|
{p.icon !== false && <Icon name={p.icon || "sparkle"} size={15} />}
|
|
189
203
|
<span>{p.children}</span>
|
|
190
204
|
{p.action}
|
|
@@ -15,6 +15,7 @@ const STATUS = {
|
|
|
15
15
|
export function StatusShape(p: StatusShapeProps) {
|
|
16
16
|
return (
|
|
17
17
|
<span
|
|
18
|
+
style={p.style}
|
|
18
19
|
className={cx(
|
|
19
20
|
"tf-shape",
|
|
20
21
|
"tf-shape-" + (STATUS[p.status] || "ring"),
|
|
@@ -29,6 +30,7 @@ export function StatusShape(p: StatusShapeProps) {
|
|
|
29
30
|
export function StatusIndicator(p: StatusIndicatorProps) {
|
|
30
31
|
return (
|
|
31
32
|
<span
|
|
33
|
+
style={p.style}
|
|
32
34
|
className={cx("tf-status", p.size === "sm" && "sm", p.className)}
|
|
33
35
|
role={p.label ? undefined : "img"}
|
|
34
36
|
aria-label={p.label ? undefined : p.status}
|