@twentyfirstmedia/ui-core 0.2.1 → 0.3.1

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 (147) hide show
  1. package/dist/components/admin.d.ts.map +1 -1
  2. package/dist/components/admin.js +95 -71
  3. package/dist/components/admin.js.map +1 -1
  4. package/dist/components/agents.d.ts.map +1 -1
  5. package/dist/components/agents.js +49 -35
  6. package/dist/components/agents.js.map +1 -1
  7. package/dist/components/app-chrome.d.ts.map +1 -1
  8. package/dist/components/app-chrome.js +6 -2
  9. package/dist/components/app-chrome.js.map +1 -1
  10. package/dist/components/approvals.d.ts.map +1 -1
  11. package/dist/components/approvals.js +22 -18
  12. package/dist/components/approvals.js.map +1 -1
  13. package/dist/components/building-blocks.d.ts.map +1 -1
  14. package/dist/components/building-blocks.js +6 -2
  15. package/dist/components/building-blocks.js.map +1 -1
  16. package/dist/components/charts.d.ts.map +1 -1
  17. package/dist/components/charts.js +20 -10
  18. package/dist/components/charts.js.map +1 -1
  19. package/dist/components/chat.d.ts.map +1 -1
  20. package/dist/components/chat.js +31 -19
  21. package/dist/components/chat.js.map +1 -1
  22. package/dist/components/commerce.d.ts +2 -2
  23. package/dist/components/commerce.d.ts.map +1 -1
  24. package/dist/components/commerce.js +45 -31
  25. package/dist/components/commerce.js.map +1 -1
  26. package/dist/components/data-display.d.ts +5 -9
  27. package/dist/components/data-display.d.ts.map +1 -1
  28. package/dist/components/data-display.js +34 -35
  29. package/dist/components/data-display.js.map +1 -1
  30. package/dist/components/data-table.d.ts.map +1 -1
  31. package/dist/components/data-table.js +15 -10
  32. package/dist/components/data-table.js.map +1 -1
  33. package/dist/components/developer.d.ts.map +1 -1
  34. package/dist/components/developer.js +20 -11
  35. package/dist/components/developer.js.map +1 -1
  36. package/dist/components/feedback.d.ts.map +1 -1
  37. package/dist/components/feedback.js +12 -6
  38. package/dist/components/feedback.js.map +1 -1
  39. package/dist/components/form.d.ts +2 -1
  40. package/dist/components/form.d.ts.map +1 -1
  41. package/dist/components/form.js +12 -9
  42. package/dist/components/form.js.map +1 -1
  43. package/dist/components/forms.d.ts +2 -1
  44. package/dist/components/forms.d.ts.map +1 -1
  45. package/dist/components/forms.js +92 -42
  46. package/dist/components/forms.js.map +1 -1
  47. package/dist/components/foundation.d.ts +5 -2
  48. package/dist/components/foundation.d.ts.map +1 -1
  49. package/dist/components/foundation.js +15 -12
  50. package/dist/components/foundation.js.map +1 -1
  51. package/dist/components/inputs.d.ts.map +1 -1
  52. package/dist/components/inputs.js +3 -1
  53. package/dist/components/inputs.js.map +1 -1
  54. package/dist/components/mail.d.ts.map +1 -1
  55. package/dist/components/mail.js +15 -10
  56. package/dist/components/mail.js.map +1 -1
  57. package/dist/components/media.d.ts.map +1 -1
  58. package/dist/components/media.js +39 -18
  59. package/dist/components/media.js.map +1 -1
  60. package/dist/components/navigation.d.ts.map +1 -1
  61. package/dist/components/navigation.js +21 -9
  62. package/dist/components/navigation.js.map +1 -1
  63. package/dist/components/operations.d.ts.map +1 -1
  64. package/dist/components/operations.js +37 -27
  65. package/dist/components/operations.js.map +1 -1
  66. package/dist/components/previews.d.ts.map +1 -1
  67. package/dist/components/previews.js +14 -10
  68. package/dist/components/previews.js.map +1 -1
  69. package/dist/components/primitives.d.ts.map +1 -1
  70. package/dist/components/primitives.js +4 -1
  71. package/dist/components/primitives.js.map +1 -1
  72. package/dist/components/real-estate.d.ts.map +1 -1
  73. package/dist/components/real-estate.js +43 -28
  74. package/dist/components/real-estate.js.map +1 -1
  75. package/dist/components/rich-text-editor.d.ts.map +1 -1
  76. package/dist/components/rich-text-editor.js +18 -15
  77. package/dist/components/rich-text-editor.js.map +1 -1
  78. package/dist/components/select-menu.d.ts.map +1 -1
  79. package/dist/components/select-menu.js +4 -1
  80. package/dist/components/select-menu.js.map +1 -1
  81. package/dist/components/shell.d.ts.map +1 -1
  82. package/dist/components/shell.js +8 -3
  83. package/dist/components/shell.js.map +1 -1
  84. package/dist/components/site.d.ts.map +1 -1
  85. package/dist/components/site.js +17 -13
  86. package/dist/components/site.js.map +1 -1
  87. package/dist/components/types.d.ts +102 -3
  88. package/dist/components/types.d.ts.map +1 -1
  89. package/dist/components/workspace.d.ts.map +1 -1
  90. package/dist/components/workspace.js +50 -34
  91. package/dist/components/workspace.js.map +1 -1
  92. package/dist/context.d.ts +39 -0
  93. package/dist/context.d.ts.map +1 -0
  94. package/dist/context.js +76 -0
  95. package/dist/context.js.map +1 -0
  96. package/dist/format.d.ts +33 -6
  97. package/dist/format.d.ts.map +1 -1
  98. package/dist/format.js +75 -10
  99. package/dist/format.js.map +1 -1
  100. package/dist/index.d.ts +3 -2
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +3 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/provider.d.ts +18 -11
  105. package/dist/provider.d.ts.map +1 -1
  106. package/dist/provider.js +11 -18
  107. package/dist/provider.js.map +1 -1
  108. package/dist/strings.d.ts +503 -0
  109. package/dist/strings.d.ts.map +1 -0
  110. package/dist/strings.js +537 -0
  111. package/dist/strings.js.map +1 -0
  112. package/dist/themes.css +4 -0
  113. package/package.json +1 -1
  114. package/src/components/admin.tsx +117 -93
  115. package/src/components/agents.tsx +55 -41
  116. package/src/components/app-chrome.tsx +6 -2
  117. package/src/components/approvals.tsx +26 -22
  118. package/src/components/building-blocks.tsx +6 -2
  119. package/src/components/charts.tsx +21 -11
  120. package/src/components/chat.tsx +45 -35
  121. package/src/components/commerce.tsx +61 -53
  122. package/src/components/data-display.tsx +38 -45
  123. package/src/components/data-table.tsx +20 -15
  124. package/src/components/developer.tsx +26 -15
  125. package/src/components/feedback.tsx +19 -7
  126. package/src/components/form.tsx +17 -9
  127. package/src/components/forms.tsx +131 -53
  128. package/src/components/foundation.tsx +23 -36
  129. package/src/components/inputs.tsx +4 -2
  130. package/src/components/mail.tsx +16 -11
  131. package/src/components/media.tsx +64 -21
  132. package/src/components/navigation.tsx +25 -13
  133. package/src/components/operations.tsx +40 -32
  134. package/src/components/previews.tsx +17 -15
  135. package/src/components/primitives.tsx +8 -5
  136. package/src/components/real-estate.tsx +55 -35
  137. package/src/components/rich-text-editor.tsx +20 -17
  138. package/src/components/select-menu.tsx +5 -2
  139. package/src/components/shell.tsx +8 -3
  140. package/src/components/site.tsx +18 -14
  141. package/src/components/types.ts +111 -3
  142. package/src/components/workspace.tsx +79 -60
  143. package/src/context.ts +112 -0
  144. package/src/format.ts +108 -15
  145. package/src/index.ts +43 -0
  146. package/src/provider.tsx +36 -40
  147. package/src/strings.ts +1500 -0
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { useFormat, useStrings } from "../context.js";
4
+ import { MEDIA_STRINGS } from "../strings.js";
3
5
  import { useControlled } from "../hooks.js";
4
6
  import { cx } from "../cx.js";
5
7
  import { Icon } from "../Icon.js";
@@ -17,6 +19,7 @@ import type {
17
19
 
18
20
  /* ── Media ── */
19
21
  export function ImageVariants(p: ImageVariantsProps) {
22
+ const t = useStrings(MEDIA_STRINGS);
20
23
  const sel = useControlled<string | number>(p.selected, p.variants?.[0]?.id ?? "", p.onSelect),
21
24
  cur: Partial<ImageVariant> = (p.variants || []).find((v) => v.id === sel[0]) || {};
22
25
  return (
@@ -40,7 +43,7 @@ export function ImageVariants(p: ImageVariantsProps) {
40
43
  )}
41
44
  {cur.label && <span className="tf-imgvar-tag">{cur.label}</span>}
42
45
  </div>
43
- <div className="tf-imgvar-list" role="listbox" aria-label="Variants">
46
+ <div className="tf-imgvar-list" role="listbox" aria-label={t.variants}>
44
47
  {(p.variants || []).map((v) => (
45
48
  <button
46
49
  key={v.id}
@@ -89,6 +92,7 @@ export function ImageVariants(p: ImageVariantsProps) {
89
92
  }
90
93
 
91
94
  export function Gallery(p: GalleryProps) {
95
+ const t = useStrings(MEDIA_STRINGS);
92
96
  const items = p.items || [],
93
97
  i = React.useState(p.start || 0),
94
98
  full = React.useState(false),
@@ -109,9 +113,16 @@ export function Gallery(p: GalleryProps) {
109
113
  document.removeEventListener("keydown", k);
110
114
  };
111
115
  });
112
- function pic(it: GalleryItem, cls: string) {
116
+ function pic(it: GalleryItem, cls: string, loading: "lazy" | "eager" = p.loading || "lazy") {
113
117
  return it.src ? (
114
- <img className={cls} src={it.src} alt={it.alt || ""} />
118
+ <img
119
+ className={cls}
120
+ src={it.src}
121
+ alt={it.alt || ""}
122
+ loading={loading}
123
+ decoding="async"
124
+ referrerPolicy={p.referrerPolicy}
125
+ />
115
126
  ) : (
116
127
  <div className={cx(cls, "tf-gallery-ph", "tf-t-" + (it.tint || "sky"))}>
117
128
  {it.art || <Icon name="image" size={28} />}
@@ -129,7 +140,7 @@ export function Gallery(p: GalleryProps) {
129
140
  {n > 1 && (
130
141
  <IconButton
131
142
  icon="chevronLeft"
132
- label="Previous image"
143
+ label={t.previousImage}
133
144
  className="tf-gallery-prev"
134
145
  onClick={() => {
135
146
  go(-1);
@@ -139,7 +150,7 @@ export function Gallery(p: GalleryProps) {
139
150
  {n > 1 && (
140
151
  <IconButton
141
152
  icon="chevronRight"
142
- label="Next image"
153
+ label={t.nextImage}
143
154
  className="tf-gallery-next"
144
155
  onClick={() => {
145
156
  go(1);
@@ -150,7 +161,7 @@ export function Gallery(p: GalleryProps) {
150
161
  {!full[0] && p.fullscreen !== false && (
151
162
  <IconButton
152
163
  icon="expand"
153
- label="Full screen"
164
+ label={t.fullScreen}
154
165
  className="tf-gallery-full"
155
166
  onClick={() => {
156
167
  full[1](true);
@@ -164,7 +175,7 @@ export function Gallery(p: GalleryProps) {
164
175
  <div className="tf-gallery">
165
176
  {stage}
166
177
  {p.thumbs !== false && n > 1 && (
167
- <div className="tf-gallery-thumbs" role="tablist" aria-label="Images">
178
+ <div className="tf-gallery-thumbs" role="tablist" aria-label={t.images}>
168
179
  {items.map((it, k) => (
169
180
  <button
170
181
  key={k}
@@ -175,30 +186,30 @@ export function Gallery(p: GalleryProps) {
175
186
  onClick={() => {
176
187
  i[1](k);
177
188
  }}
178
- aria-label={it.caption || it.alt || (it.src ? `Image ${k + 1}` : undefined)}
189
+ aria-label={it.caption || it.alt || (it.src ? t.imageN(k + 1) : undefined)}
179
190
  >
180
- {pic(it, "tf-gallery-thumb")}
191
+ {pic(it, "tf-gallery-thumb", "lazy")}
181
192
  </button>
182
193
  ))}
183
194
  </div>
184
195
  )}
185
196
  {full[0] && (
186
197
  <Portal>
187
- <div className="tf-lightbox" role="dialog" aria-modal="true" aria-label="Images">
198
+ <div className="tf-lightbox" role="dialog" aria-modal="true" aria-label={t.images}>
188
199
  <IconButton
189
200
  icon="x"
190
- label="Close"
201
+ label={t.close}
191
202
  className="tf-lightbox-close"
192
203
  onClick={() => {
193
204
  full[1](false);
194
205
  }}
195
206
  />
196
207
  <div className="tf-lightbox-stage">
197
- {pic(cur, "tf-gallery-img")}
208
+ {pic(cur, "tf-gallery-img", "eager")}
198
209
  {n > 1 && (
199
210
  <IconButton
200
211
  icon="chevronLeft"
201
- label="Previous image"
212
+ label={t.previousImage}
202
213
  className="tf-gallery-prev"
203
214
  onClick={() => {
204
215
  go(-1);
@@ -208,7 +219,7 @@ export function Gallery(p: GalleryProps) {
208
219
  {n > 1 && (
209
220
  <IconButton
210
221
  icon="chevronRight"
211
- label="Next image"
222
+ label={t.nextImage}
212
223
  className="tf-gallery-next"
213
224
  onClick={() => {
214
225
  go(1);
@@ -227,7 +238,12 @@ export function Gallery(p: GalleryProps) {
227
238
  }
228
239
 
229
240
  export function BeforeAfter(p: BeforeAfterProps) {
241
+ const t = useStrings(MEDIA_STRINGS);
242
+ const fm = useFormat();
230
243
  const pos = React.useState(p.start != null ? p.start : 50),
244
+ pct = Math.round(pos[0]),
245
+ before = p.beforeLabel || t.before,
246
+ after = p.afterLabel || t.after,
231
247
  ref = React.useRef<HTMLDivElement>(null),
232
248
  drag = React.useRef(false);
233
249
  function at(e: { clientX: number }) {
@@ -237,7 +253,14 @@ export function BeforeAfter(p: BeforeAfterProps) {
237
253
  }
238
254
  function pane(x: MediaPane | undefined, cls: string) {
239
255
  return x && x.src ? (
240
- <img className={cls} src={x.src} alt={x.alt || ""} />
256
+ <img
257
+ className={cls}
258
+ src={x.src}
259
+ alt={x.alt || ""}
260
+ loading={p.loading || "lazy"}
261
+ decoding="async"
262
+ referrerPolicy={p.referrerPolicy}
263
+ />
241
264
  ) : (
242
265
  <div className={cx(cls, "tf-ba-ph", x && x.tint && "tf-t-" + x.tint)}>{x && x.art}</div>
243
266
  );
@@ -270,8 +293,8 @@ export function BeforeAfter(p: BeforeAfterProps) {
270
293
  >
271
294
  {pane(p.before, "tf-ba-img")}
272
295
  </div>
273
- <span className="tf-ba-tag left">{p.beforeLabel || "Before"}</span>
274
- <span className="tf-ba-tag right">{p.afterLabel || "After"}</span>
296
+ <span className="tf-ba-tag left">{before}</span>
297
+ <span className="tf-ba-tag right">{after}</span>
275
298
  <div
276
299
  className="tf-ba-line"
277
300
  style={{
@@ -282,13 +305,33 @@ export function BeforeAfter(p: BeforeAfterProps) {
282
305
  className="tf-ba-knob"
283
306
  role="slider"
284
307
  tabIndex={0}
285
- aria-label="Compare before and after"
308
+ aria-label={p.label || t.compareBeforeAfter}
286
309
  aria-valuemin={0}
287
310
  aria-valuemax={100}
288
- aria-valuenow={Math.round(pos[0])}
311
+ aria-valuenow={pct}
312
+ aria-valuetext={
313
+ p.valueText
314
+ ? p.valueText(pct)
315
+ : `${before} ${fm.formatPercent(pct)}, ${after} ${fm.formatPercent(100 - pct)}`
316
+ }
289
317
  onKeyDown={(e) => {
290
- if (e.key === "ArrowLeft") pos[1](Math.max(0, pos[0] - 5));
291
- if (e.key === "ArrowRight") pos[1](Math.min(100, pos[0] + 5));
318
+ const to =
319
+ e.key === "ArrowLeft" || e.key === "ArrowDown"
320
+ ? pos[0] - 5
321
+ : e.key === "ArrowRight" || e.key === "ArrowUp"
322
+ ? pos[0] + 5
323
+ : e.key === "PageDown"
324
+ ? pos[0] - 25
325
+ : e.key === "PageUp"
326
+ ? pos[0] + 25
327
+ : e.key === "Home"
328
+ ? 0
329
+ : e.key === "End"
330
+ ? 100
331
+ : null;
332
+ if (to == null) return;
333
+ e.preventDefault();
334
+ pos[1](Math.max(0, Math.min(100, to)));
292
335
  }}
293
336
  >
294
337
  <Icon name="chevronLeft" size={12} />
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { useStrings } from "../context.js";
4
+ import { NAVIGATION_STRINGS } from "../strings.js";
3
5
  import { cx } from "../cx.js";
4
6
  import { Icon } from "../Icon.js";
5
7
  import { moveFocus, useControlled, useId, useOutside } from "../hooks.js";
@@ -104,9 +106,10 @@ export function Tabs(p: TabsProps) {
104
106
  }
105
107
 
106
108
  export function Breadcrumb(p: BreadcrumbProps) {
109
+ const t = useStrings(NAVIGATION_STRINGS);
107
110
  const items = p.items || [];
108
111
  return (
109
- <nav className="tf-crumbs" aria-label="Breadcrumb">
112
+ <nav className="tf-crumbs" aria-label={t.breadcrumb}>
110
113
  {items.map((it, i) => {
111
114
  const last = i === items.length - 1;
112
115
  return (
@@ -121,22 +124,29 @@ export function Breadcrumb(p: BreadcrumbProps) {
121
124
  }
122
125
 
123
126
  export function Pagination(p: PaginationProps) {
124
- const total = p.total || 1,
125
- st = React.useState(p.page || 1),
126
- cur = st[0],
127
+ const t = useStrings(NAVIGATION_STRINGS);
128
+ const total = Math.max(1, p.total || 1),
129
+ // `page` is controlled once someone listens; `page` alone stays the start page (as before)
130
+ controlled = p.page !== undefined && p.onChange !== undefined,
131
+ v = useControlled(controlled ? p.page : undefined, p.defaultPage ?? p.page ?? 1, p.onChange),
132
+ cur = Math.min(total, Math.max(1, v[0])),
133
+ go = (n: number) => {
134
+ const next = Math.min(total, Math.max(1, n));
135
+ if (next !== cur) v[1](next);
136
+ },
127
137
  pages: (number | "…")[] = [];
128
138
  for (let i = 1; i <= total; i++) {
129
139
  if (i === 1 || i === total || Math.abs(i - cur) <= 1) pages.push(i);
130
140
  else if (pages[pages.length - 1] !== "…") pages.push("…");
131
141
  }
132
142
  return (
133
- <nav className="tf-pager" aria-label="Pagination">
143
+ <nav className="tf-pager" aria-label={t.pagination}>
134
144
  <IconButton
135
145
  icon="chevronLeft"
136
- label="Previous page"
146
+ label={t.previousPage}
137
147
  size="sm"
138
148
  onClick={() => {
139
- st[1](Math.max(1, cur - 1));
149
+ go(cur - 1);
140
150
  }}
141
151
  />
142
152
  {pages.map((n, i) =>
@@ -151,7 +161,7 @@ export function Pagination(p: PaginationProps) {
151
161
  className="tf-page"
152
162
  aria-current={n === cur ? "page" : undefined}
153
163
  onClick={() => {
154
- st[1](n);
164
+ go(n);
155
165
  }}
156
166
  >
157
167
  {n}
@@ -160,10 +170,10 @@ export function Pagination(p: PaginationProps) {
160
170
  )}
161
171
  <IconButton
162
172
  icon="chevronRight"
163
- label="Next page"
173
+ label={t.nextPage}
164
174
  size="sm"
165
175
  onClick={() => {
166
- st[1](Math.min(total, cur + 1));
176
+ go(cur + 1);
167
177
  }}
168
178
  />
169
179
  </nav>
@@ -262,6 +272,7 @@ export function Menubar(p: MenubarProps) {
262
272
 
263
273
  /* ── NavigationMenu: site navigation with dropdown panels ── */
264
274
  export function NavigationMenu(p: NavigationMenuProps) {
275
+ const strings = useStrings(NAVIGATION_STRINGS);
265
276
  const uid = useId("nav");
266
277
  const root = React.useRef<HTMLElement>(null);
267
278
  const [open, setOpen] = React.useState<number | null>(null);
@@ -277,7 +288,7 @@ export function NavigationMenu(p: NavigationMenuProps) {
277
288
  ref={root}
278
289
  className={cx("tf-navmenu", p.className)}
279
290
  style={p.style}
280
- aria-label={p.label || "Main"}
291
+ aria-label={p.label || strings.mainNavigation}
281
292
  onKeyDown={(e) => {
282
293
  if (e.key === "Escape" && open != null) {
283
294
  const trigger = root.current?.querySelector<HTMLElement>(`#${uid}-t${open}`);
@@ -348,6 +359,7 @@ export function NavigationMenu(p: NavigationMenuProps) {
348
359
 
349
360
  /* ── Sidebar: app navigation with groups, nested pages and a collapsed (icons) mode ── */
350
361
  export function Sidebar(p: SidebarProps) {
362
+ const t = useStrings(NAVIGATION_STRINGS);
351
363
  const collapsed = useControlled(p.collapsed, !!p.defaultCollapsed, p.onCollapsedChange);
352
364
  const idOf = (it: { id?: string; label: string }) => it.id || it.label;
353
365
  const hasCurrent = (it: SidebarItem) =>
@@ -399,7 +411,7 @@ export function Sidebar(p: SidebarProps) {
399
411
  <nav
400
412
  className={cx("tf-sidebar", collapsed[0] && "collapsed", p.className)}
401
413
  style={p.style}
402
- aria-label={p.label || "Sidebar"}
414
+ aria-label={p.label || t.sidebar}
403
415
  >
404
416
  {p.header && <div className="tf-sidebar-head">{p.header}</div>}
405
417
  <div className="tf-sidebar-body">
@@ -420,7 +432,7 @@ export function Sidebar(p: SidebarProps) {
420
432
  {p.footer}
421
433
  <IconButton
422
434
  icon={collapsed[0] ? "chevronRight" : "chevronLeft"}
423
- label={collapsed[0] ? "Expand sidebar" : "Collapse sidebar"}
435
+ label={collapsed[0] ? t.expandSidebar : t.collapseSidebar}
424
436
  variant="ghost"
425
437
  size="sm"
426
438
  onClick={() => collapsed[1](!collapsed[0])}
@@ -3,7 +3,8 @@ import * as React from "react";
3
3
  import { Icon } from "../Icon.js";
4
4
  import { cx } from "../cx.js";
5
5
  import { useScrollFocus } from "../hooks.js";
6
- import { formatMoney, formatPercent } from "../format.js";
6
+ import { useFormat, useStrings } from "../context.js";
7
+ import { OPERATIONS_STRINGS } from "../strings.js";
7
8
  import { StatusIndicator, StatusShape } from "./status.js";
8
9
  import { Meter, UptimeBar } from "./developer.js";
9
10
  import { DataTable } from "./data-table.js";
@@ -50,19 +51,15 @@ export function ConnectionCard(p: ConnectionCardProps) {
50
51
  }
51
52
 
52
53
  export function ConnectionBanner(p: ConnectionBannerProps) {
54
+ const t = useStrings(OPERATIONS_STRINGS);
53
55
  if (!p.state || p.state === "online") return null;
54
56
  return (
55
57
  <div className={cx("tf-connbanner", p.state)} role="status">
56
58
  <StatusShape status={p.state === "offline" ? "danger" : "warn"} />
57
- <span>
58
- {p.text ||
59
- (p.state === "offline"
60
- ? "Offline. Changes are kept and sent when you’re back."
61
- : `Connection lost. Retrying in ${p.seconds != null ? p.seconds : "…"} s`)}
62
- </span>
59
+ <span>{p.text || (p.state === "offline" ? t.offline : t.connectionLost(p.seconds))}</span>
63
60
  {p.onRetry && (
64
61
  <button type="button" className="tf-linkbtn" onClick={p.onRetry}>
65
- {"Retry now"}
62
+ {t.retryNow}
66
63
  </button>
67
64
  )}
68
65
  </div>
@@ -70,6 +67,7 @@ export function ConnectionBanner(p: ConnectionBannerProps) {
70
67
  }
71
68
 
72
69
  export function ServerCard(p: ServerCardProps) {
70
+ const t = useStrings(OPERATIONS_STRINGS);
73
71
  return (
74
72
  <div className="tf-server">
75
73
  <div className="tf-server-top">
@@ -90,7 +88,7 @@ export function ServerCard(p: ServerCardProps) {
90
88
  m.value == null ? (
91
89
  <div key={m.label} className="tf-usage-top">
92
90
  <span>{m.label}</span>
93
- <b>{m.unknown || "Unknown"}</b>
91
+ <b>{m.unknown || t.unknown}</b>
94
92
  </div>
95
93
  ) : (
96
94
  <Meter key={m.label} label={m.label} value={m.value} max={m.max} text={m.text} />
@@ -104,8 +102,9 @@ export function ServerCard(p: ServerCardProps) {
104
102
  }
105
103
 
106
104
  export function DeployPipeline(p: DeployPipelineProps) {
105
+ const t = useStrings(OPERATIONS_STRINGS);
107
106
  return (
108
- <ol className="tf-pipeline" tabIndex={0} aria-label="Deploy pipeline">
107
+ <ol className="tf-pipeline" tabIndex={0} aria-label={t.deployPipeline}>
109
108
  {(p.stages || []).map((s, i) => (
110
109
  <li key={i} className={cx("tf-stage", "st-" + (s.status || "idle"))}>
111
110
  <StatusShape status={s.status === "done" ? "ok" : s.status || "idle"} />
@@ -119,6 +118,7 @@ export function DeployPipeline(p: DeployPipelineProps) {
119
118
  }
120
119
 
121
120
  export function DnsRecordTable(p: DnsRecordTableProps) {
121
+ const t = useStrings(OPERATIONS_STRINGS);
122
122
  return (
123
123
  <DataTable
124
124
  rows={p.records || []}
@@ -130,7 +130,7 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
130
130
  columns={[
131
131
  {
132
132
  key: "type",
133
- label: "Type",
133
+ label: t.colType,
134
134
  sortable: true,
135
135
  render: (r) => (
136
136
  <Badge tone="neutral" dot={false} className="tf-mono">
@@ -140,13 +140,13 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
140
140
  },
141
141
  {
142
142
  key: "name",
143
- label: "Name",
143
+ label: t.colName,
144
144
  sortable: true,
145
145
  render: (r) => <span className="tf-mono">{r.name}</span>,
146
146
  },
147
147
  {
148
148
  key: "content",
149
- label: "Content",
149
+ label: t.colContent,
150
150
  render: (r) => (
151
151
  <Tooltip label={r.content}>
152
152
  <span className="tf-mono tf-trunc" tabIndex={0}>
@@ -163,14 +163,14 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
163
163
  },
164
164
  {
165
165
  key: "proxy",
166
- label: "Proxy",
166
+ label: t.colProxy,
167
167
  render: (r) =>
168
168
  r.proxy == null ? (
169
169
  "—"
170
170
  ) : (
171
171
  <StatusIndicator
172
172
  status={r.proxy ? "ok" : "idle"}
173
- label={r.proxy ? "Proxied" : "DNS only"}
173
+ label={r.proxy ? t.proxied : t.dnsOnly}
174
174
  />
175
175
  ),
176
176
  },
@@ -189,7 +189,7 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
189
189
  <button
190
190
  type="button"
191
191
  className="tf-iconbtn tf-iconbtn-ghost tf-iconbtn-sm"
192
- aria-label="Actions"
192
+ aria-label={t.actions}
193
193
  onClick={s.toggle}
194
194
  >
195
195
  <Icon name="more" />
@@ -219,6 +219,7 @@ export function DnsRecordTable(p: DnsRecordTableProps) {
219
219
  }
220
220
 
221
221
  export function DomainList(p: DomainListProps) {
222
+ const t = useStrings(OPERATIONS_STRINGS);
222
223
  return (
223
224
  <div className="tf-domains">
224
225
  {(p.items || [])
@@ -241,8 +242,8 @@ export function DomainList(p: DomainListProps) {
241
242
  <small>{[d.registrar, d.dns].filter(Boolean).join(" · ")}</small>
242
243
  </span>
243
244
  <span className="tf-domainrow-cell">
244
- <small>{"Expires"}</small>
245
- <b className="tf-mono">{d.days == null ? "—" : `${d.days} days`}</b>
245
+ <small>{t.expires}</small>
246
+ <b className="tf-mono">{d.days == null ? "—" : t.daysCount(d.days)}</b>
246
247
  </span>
247
248
  <span className="tf-domainrow-cell">
248
249
  <small>{"SSL"}</small>
@@ -256,6 +257,7 @@ export function DomainList(p: DomainListProps) {
256
257
  }
257
258
 
258
259
  export function MailAuthStatus(p: MailAuthStatusProps) {
260
+ const t = useStrings(OPERATIONS_STRINGS);
259
261
  const cols = p.columns || ["MX", "SPF", "DKIM", "DMARC"];
260
262
  const box = React.useRef<HTMLDivElement>(null);
261
263
  useScrollFocus(box);
@@ -264,7 +266,7 @@ export function MailAuthStatus(p: MailAuthStatusProps) {
264
266
  <table className="tf-table tf-mailauth">
265
267
  <thead>
266
268
  <tr>
267
- <th>{"Domain"}</th>
269
+ <th>{t.domain}</th>
268
270
  {cols.map((c) => (
269
271
  <th key={c}>{c}</th>
270
272
  ))}
@@ -303,6 +305,8 @@ export function MailAuthStatus(p: MailAuthStatusProps) {
303
305
  }
304
306
 
305
307
  export function LedgerTable(p: LedgerTableProps) {
308
+ const t = useStrings(OPERATIONS_STRINGS);
309
+ const { formatMoney } = useFormat();
306
310
  const cur = p.currency;
307
311
  return (
308
312
  <DataTable
@@ -316,24 +320,24 @@ export function LedgerTable(p: LedgerTableProps) {
316
320
  columns={[
317
321
  {
318
322
  key: "date",
319
- label: "Date",
323
+ label: t.colDate,
320
324
  sortable: true,
321
325
  render: (r) => <span className="tf-mono">{r.date}</span>,
322
326
  },
323
327
  {
324
328
  key: "ref",
325
- label: "Ref.",
329
+ label: t.colRef,
326
330
  render: (r) => <span className="tf-mono">{r.ref}</span>,
327
331
  },
328
332
  {
329
333
  key: "text",
330
- label: "Description",
334
+ label: t.colDescription,
331
335
  render: (r) => (
332
336
  <span className="tf-option-text">
333
337
  <b>{r.text}</b>
334
338
  {r.suggested && (
335
339
  <small>
336
- <StatusShape status="signal" /> {`Suggested by ${r.suggested}`}
340
+ <StatusShape status="signal" /> {t.suggestedBy(r.suggested)}
337
341
  </small>
338
342
  )}
339
343
  </span>
@@ -341,7 +345,7 @@ export function LedgerTable(p: LedgerTableProps) {
341
345
  },
342
346
  {
343
347
  key: "category",
344
- label: "Category",
348
+ label: t.colCategory,
345
349
  render: (r) =>
346
350
  p.categories ? (
347
351
  <InlineEdit
@@ -359,7 +363,7 @@ export function LedgerTable(p: LedgerTableProps) {
359
363
  },
360
364
  {
361
365
  key: "project",
362
- label: "Project",
366
+ label: t.colProject,
363
367
  render: (r) =>
364
368
  r.project && (
365
369
  <span
@@ -376,7 +380,7 @@ export function LedgerTable(p: LedgerTableProps) {
376
380
  },
377
381
  {
378
382
  key: "net",
379
- label: "Net",
383
+ label: t.net,
380
384
  align: "right",
381
385
  sortable: true,
382
386
  render: (r) =>
@@ -386,7 +390,7 @@ export function LedgerTable(p: LedgerTableProps) {
386
390
  },
387
391
  {
388
392
  key: "vat",
389
- label: "VAT",
393
+ label: t.vat,
390
394
  align: "right",
391
395
  render: (r) =>
392
396
  formatMoney(r.vat || 0, cur, {
@@ -395,7 +399,7 @@ export function LedgerTable(p: LedgerTableProps) {
395
399
  },
396
400
  {
397
401
  key: "gross",
398
- label: "Gross",
402
+ label: t.grossLabel,
399
403
  align: "right",
400
404
  sortable: true,
401
405
  render: (r) => (
@@ -408,7 +412,7 @@ export function LedgerTable(p: LedgerTableProps) {
408
412
  },
409
413
  {
410
414
  key: "status",
411
- label: "Status",
415
+ label: t.status,
412
416
  render: (r) => <StatusIndicator status={r.status || "ok"} label={r.statusLabel} />,
413
417
  },
414
418
  ]}
@@ -417,6 +421,8 @@ export function LedgerTable(p: LedgerTableProps) {
417
421
  }
418
422
 
419
423
  export function PnLStatement(p: PnLStatementProps) {
424
+ const t = useStrings(OPERATIONS_STRINGS);
425
+ const { formatMoney } = useFormat();
420
426
  const cols = p.columns || [];
421
427
  const box = React.useRef<HTMLDivElement>(null);
422
428
  useScrollFocus(box);
@@ -431,7 +437,7 @@ export function PnLStatement(p: PnLStatementProps) {
431
437
  {c}
432
438
  </th>
433
439
  ))}
434
- {p.total !== false && <th className="tf-num">{"Total"}</th>}
440
+ {p.total !== false && <th className="tf-num">{t.total}</th>}
435
441
  </tr>
436
442
  </thead>
437
443
  <tbody>
@@ -467,6 +473,8 @@ export function PnLStatement(p: PnLStatementProps) {
467
473
  }
468
474
 
469
475
  export function ReceiptSuggestion(p: ReceiptSuggestionProps) {
476
+ const t = useStrings(OPERATIONS_STRINGS);
477
+ const { formatPercent } = useFormat();
470
478
  return (
471
479
  <div className="tf-receipt">
472
480
  <div className="tf-receipt-src">
@@ -504,12 +512,12 @@ export function ReceiptSuggestion(p: ReceiptSuggestionProps) {
504
512
  >
505
513
  {p.onDiscard && (
506
514
  <Button variant="ghost" size="sm" onClick={p.onDiscard}>
507
- {p.discardLabel || "Not a receipt"}
515
+ {p.discardLabel || t.notAReceipt}
508
516
  </Button>
509
517
  )}
510
518
  {p.onConfirm && (
511
519
  <Button size="sm" icon="check" onClick={p.onConfirm}>
512
- {p.confirmLabel || "Book it"}
520
+ {p.confirmLabel || t.bookIt}
513
521
  </Button>
514
522
  )}
515
523
  </div>