@twentyfirstmedia/ui-core 0.4.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.
Files changed (154) hide show
  1. package/ICONS-LICENSE +175 -0
  2. package/dist/components/admin.d.ts.map +1 -1
  3. package/dist/components/admin.js +31 -21
  4. package/dist/components/admin.js.map +1 -1
  5. package/dist/components/agents.d.ts.map +1 -1
  6. package/dist/components/agents.js +21 -18
  7. package/dist/components/agents.js.map +1 -1
  8. package/dist/components/app-chrome.d.ts.map +1 -1
  9. package/dist/components/app-chrome.js +5 -5
  10. package/dist/components/app-chrome.js.map +1 -1
  11. package/dist/components/approvals.d.ts.map +1 -1
  12. package/dist/components/approvals.js +2 -2
  13. package/dist/components/approvals.js.map +1 -1
  14. package/dist/components/building-blocks.d.ts.map +1 -1
  15. package/dist/components/building-blocks.js +13 -10
  16. package/dist/components/building-blocks.js.map +1 -1
  17. package/dist/components/charts.d.ts +2 -2
  18. package/dist/components/charts.d.ts.map +1 -1
  19. package/dist/components/charts.js +14 -11
  20. package/dist/components/charts.js.map +1 -1
  21. package/dist/components/chat.d.ts +2 -2
  22. package/dist/components/chat.d.ts.map +1 -1
  23. package/dist/components/chat.js +22 -18
  24. package/dist/components/chat.js.map +1 -1
  25. package/dist/components/commerce.d.ts.map +1 -1
  26. package/dist/components/commerce.js +20 -16
  27. package/dist/components/commerce.js.map +1 -1
  28. package/dist/components/data-display.d.ts +0 -5
  29. package/dist/components/data-display.d.ts.map +1 -1
  30. package/dist/components/data-display.js +41 -22
  31. package/dist/components/data-display.js.map +1 -1
  32. package/dist/components/data-table.d.ts +2 -2
  33. package/dist/components/data-table.d.ts.map +1 -1
  34. package/dist/components/data-table.js +3 -3
  35. package/dist/components/data-table.js.map +1 -1
  36. package/dist/components/developer.d.ts.map +1 -1
  37. package/dist/components/developer.js +7 -6
  38. package/dist/components/developer.js.map +1 -1
  39. package/dist/components/feedback.d.ts.map +1 -1
  40. package/dist/components/feedback.js +5 -4
  41. package/dist/components/feedback.js.map +1 -1
  42. package/dist/components/forms.d.ts.map +1 -1
  43. package/dist/components/forms.js +63 -21
  44. package/dist/components/forms.js.map +1 -1
  45. package/dist/components/foundation.d.ts.map +1 -1
  46. package/dist/components/foundation.js +13 -9
  47. package/dist/components/foundation.js.map +1 -1
  48. package/dist/components/index.d.ts +2 -1
  49. package/dist/components/index.d.ts.map +1 -1
  50. package/dist/components/index.js +2 -1
  51. package/dist/components/index.js.map +1 -1
  52. package/dist/components/inputs.d.ts.map +1 -1
  53. package/dist/components/inputs.js +33 -8
  54. package/dist/components/inputs.js.map +1 -1
  55. package/dist/components/mail.d.ts.map +1 -1
  56. package/dist/components/mail.js +5 -5
  57. package/dist/components/mail.js.map +1 -1
  58. package/dist/components/media.d.ts.map +1 -1
  59. package/dist/components/media.js +62 -11
  60. package/dist/components/media.js.map +1 -1
  61. package/dist/components/navigation.d.ts +3 -1
  62. package/dist/components/navigation.d.ts.map +1 -1
  63. package/dist/components/navigation.js +287 -20
  64. package/dist/components/navigation.js.map +1 -1
  65. package/dist/components/operations.d.ts.map +1 -1
  66. package/dist/components/operations.js +10 -10
  67. package/dist/components/operations.js.map +1 -1
  68. package/dist/components/overlays.d.ts.map +1 -1
  69. package/dist/components/overlays.js +2 -2
  70. package/dist/components/overlays.js.map +1 -1
  71. package/dist/components/previews.js +2 -2
  72. package/dist/components/previews.js.map +1 -1
  73. package/dist/components/primitives.js +2 -2
  74. package/dist/components/primitives.js.map +1 -1
  75. package/dist/components/qr-code.d.ts +8 -0
  76. package/dist/components/qr-code.d.ts.map +1 -0
  77. package/dist/components/qr-code.js +18 -0
  78. package/dist/components/qr-code.js.map +1 -0
  79. package/dist/components/real-estate.d.ts.map +1 -1
  80. package/dist/components/real-estate.js +5 -5
  81. package/dist/components/real-estate.js.map +1 -1
  82. package/dist/components/rich-text-editor.d.ts.map +1 -1
  83. package/dist/components/rich-text-editor.js +1 -1
  84. package/dist/components/rich-text-editor.js.map +1 -1
  85. package/dist/components/select-menu.js +1 -1
  86. package/dist/components/select-menu.js.map +1 -1
  87. package/dist/components/shell.d.ts +0 -4
  88. package/dist/components/shell.d.ts.map +1 -1
  89. package/dist/components/shell.js +100 -7
  90. package/dist/components/shell.js.map +1 -1
  91. package/dist/components/site.d.ts.map +1 -1
  92. package/dist/components/site.js +8 -6
  93. package/dist/components/site.js.map +1 -1
  94. package/dist/components/status.d.ts.map +1 -1
  95. package/dist/components/status.js +2 -2
  96. package/dist/components/status.js.map +1 -1
  97. package/dist/components/system.d.ts.map +1 -1
  98. package/dist/components/system.js +3 -2
  99. package/dist/components/system.js.map +1 -1
  100. package/dist/components/types.d.ts +257 -18
  101. package/dist/components/types.d.ts.map +1 -1
  102. package/dist/components/workspace.d.ts.map +1 -1
  103. package/dist/components/workspace.js +15 -12
  104. package/dist/components/workspace.js.map +1 -1
  105. package/dist/icon-library.d.ts +254 -3
  106. package/dist/icon-library.d.ts.map +1 -1
  107. package/dist/icon-library.js +518 -14
  108. package/dist/icon-library.js.map +1 -1
  109. package/dist/qr.d.ts +14 -0
  110. package/dist/qr.d.ts.map +1 -0
  111. package/dist/qr.js +298 -0
  112. package/dist/qr.js.map +1 -0
  113. package/dist/strings.d.ts +30 -4
  114. package/dist/strings.d.ts.map +1 -1
  115. package/dist/strings.js +15 -0
  116. package/dist/strings.js.map +1 -1
  117. package/dist/themes.css +2345 -176
  118. package/package.json +2 -1
  119. package/src/components/admin.tsx +66 -23
  120. package/src/components/agents.tsx +22 -20
  121. package/src/components/app-chrome.tsx +9 -5
  122. package/src/components/approvals.tsx +3 -1
  123. package/src/components/building-blocks.tsx +24 -13
  124. package/src/components/charts.tsx +17 -12
  125. package/src/components/chat.tsx +54 -31
  126. package/src/components/commerce.tsx +58 -24
  127. package/src/components/data-display.tsx +106 -38
  128. package/src/components/data-table.tsx +11 -7
  129. package/src/components/developer.tsx +15 -6
  130. package/src/components/feedback.tsx +5 -3
  131. package/src/components/forms.tsx +101 -23
  132. package/src/components/foundation.tsx +9 -5
  133. package/src/components/index.ts +10 -1
  134. package/src/components/inputs.tsx +44 -7
  135. package/src/components/mail.tsx +7 -3
  136. package/src/components/media.tsx +88 -16
  137. package/src/components/navigation.tsx +651 -71
  138. package/src/components/operations.tsx +17 -8
  139. package/src/components/overlays.tsx +7 -2
  140. package/src/components/previews.tsx +2 -2
  141. package/src/components/primitives.tsx +2 -2
  142. package/src/components/qr-code.tsx +30 -0
  143. package/src/components/real-estate.tsx +7 -4
  144. package/src/components/rich-text-editor.tsx +7 -1
  145. package/src/components/select-menu.tsx +1 -1
  146. package/src/components/shell.tsx +237 -11
  147. package/src/components/site.tsx +18 -4
  148. package/src/components/status.tsx +2 -0
  149. package/src/components/system.tsx +3 -2
  150. package/src/components/types.ts +261 -18
  151. package/src/components/workspace.tsx +37 -15
  152. package/src/icon-library.ts +523 -18
  153. package/src/qr.ts +300 -0
  154. 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 className="tf-pipeline" tabIndex={0} aria-label={t.deployPipeline}>
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 ref={ref} className={cx("tf-pop-anchor", p.block && "block", p.className)}>
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
- className="tf-tip-anchor"
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
- className="tf-energy"
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 label={p.label} hint={p.hint} error={p.error}>
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}
@@ -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
- return (
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 className="tf-rail" aria-label={t.tools}>
97
- <div className="tf-rail-group">{(p.top || []).map(railBtn)}</div>
98
- <div className="tf-rail-group">{(p.bottom || []).map((it, i) => railBtn(it, i + 3))}</div>
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
- className={cx("tf-floating", p.className)}
137
- style={{ minHeight: p.height ?? 640, ...p.style }}
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.end != null && <div className="tf-floating-end">{p.end}</div>}
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
@@ -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 className={cx("tf-launch", open[0] && "open", p.inline && "inline")}>
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
- className={cx("tf-cookie", p.inline && "inline")}
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 className={cx("tf-announce", "tf-t-" + (p.tint || "butter"))} role="status">
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}