@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
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { useMounted, useWidth } from "../hooks.js";
5
- import { formatNumber } from "../format.js";
5
+ import { useFormat, useStrings } from "../context.js";
6
+ import { CHARTS_STRINGS } from "../strings.js";
6
7
  import { cx } from "../cx.js";
7
8
  import { Icon } from "../Icon.js";
8
9
  import { CountUp } from "./data-display.js";
@@ -24,6 +25,7 @@ function niceMax(m: number) {
24
25
  return Math.ceil(m / p) * p;
25
26
  }
26
27
  export function BarChart(p: BarChartProps) {
28
+ const t = useStrings(CHARTS_STRINGS);
27
29
  const mw = useWidth(p.width, 560),
28
30
  data = p.data || [],
29
31
  W = mw[1],
@@ -43,7 +45,7 @@ export function BarChart(p: BarChartProps) {
43
45
  viewBox={"0 0 " + W + " " + (Hh + 22)}
44
46
  preserveAspectRatio="xMinYMin meet"
45
47
  role="img"
46
- aria-label={p.label || "Bar chart"}
48
+ aria-label={p.label || t.barChart}
47
49
  >
48
50
  {ticks.map((t) => {
49
51
  const y = Hh - t * (Hh - 10);
@@ -92,6 +94,7 @@ export function BarChart(p: BarChartProps) {
92
94
  );
93
95
  }
94
96
  export function LineChart(p: LineChartProps) {
97
+ const t = useStrings(CHARTS_STRINGS);
95
98
  const mw = useWidth(p.width, 560),
96
99
  series = p.series || [],
97
100
  labels = p.labels || [],
@@ -122,7 +125,7 @@ export function LineChart(p: LineChartProps) {
122
125
  viewBox={"0 0 " + W + " " + (Hh + 22)}
123
126
  preserveAspectRatio="xMinYMin meet"
124
127
  role="img"
125
- aria-label={p.label || "Line chart"}
128
+ aria-label={p.label || t.lineChart}
126
129
  >
127
130
  {[0, 0.5, 1].map((t) => {
128
131
  const y = Hh - t * (Hh - 12);
@@ -181,6 +184,7 @@ export function LineChart(p: LineChartProps) {
181
184
  );
182
185
  }
183
186
  export function Sparkline(p: SparklineProps) {
187
+ const t = useStrings(CHARTS_STRINGS);
184
188
  const v = p.values || [],
185
189
  W = p.width || 120,
186
190
  Hh = p.height || 36,
@@ -203,7 +207,7 @@ export function Sparkline(p: SparklineProps) {
203
207
  width={W}
204
208
  height={Hh}
205
209
  role="img"
206
- aria-label={p.label || "Trend"}
210
+ aria-label={p.label || t.trend}
207
211
  >
208
212
  <path
209
213
  className={cx("tf-line", "tf-line-" + (p.tint || "ink"))}
@@ -217,6 +221,7 @@ export function Sparkline(p: SparklineProps) {
217
221
  );
218
222
  }
219
223
  export function Donut(p: DonutProps) {
224
+ const t = useStrings(CHARTS_STRINGS);
220
225
  const data = p.data || [],
221
226
  size = p.size || 168,
222
227
  sw = p.stroke || 22,
@@ -245,7 +250,7 @@ export function Donut(p: DonutProps) {
245
250
  height={size}
246
251
  viewBox={"0 0 " + size + " " + size}
247
252
  role="img"
248
- aria-label={p.label || "Breakdown"}
253
+ aria-label={p.label || t.breakdown}
249
254
  >
250
255
  <circle className="tf-arc-track" cx={size / 2} cy={size / 2} r={r} strokeWidth={sw} />
251
256
  {data.map((d, i) => {
@@ -361,6 +366,7 @@ export function Legend(p: LegendProps) {
361
366
  );
362
367
  }
363
368
  export function StackedBarChart(p: StackedBarChartProps) {
369
+ const t = useStrings(CHARTS_STRINGS);
364
370
  const mw = useWidth(p.width, 560),
365
371
  W = mw[1],
366
372
  Hh = p.height || 200,
@@ -382,7 +388,7 @@ export function StackedBarChart(p: StackedBarChartProps) {
382
388
  height={Hh + 22}
383
389
  viewBox={"0 0 " + W + " " + (Hh + 22)}
384
390
  role="img"
385
- aria-label={p.label || "Stacked bar chart"}
391
+ aria-label={p.label || t.stackedBarChart}
386
392
  onPointerLeave={() => {
387
393
  hov[1](null);
388
394
  }}
@@ -533,6 +539,7 @@ function smooth(pts: number[][]) {
533
539
  return d;
534
540
  }
535
541
  export function AreaChart(p: AreaChartProps) {
542
+ const t = useStrings(CHARTS_STRINGS);
536
543
  const mw = useWidth(p.width, 560),
537
544
  W = mw[1],
538
545
  Hh = p.height || 200,
@@ -573,7 +580,7 @@ export function AreaChart(p: AreaChartProps) {
573
580
  height={Hh + 22}
574
581
  viewBox={"0 0 " + W + " " + (Hh + 22)}
575
582
  role="img"
576
- aria-label={p.label || "Area chart"}
583
+ aria-label={p.label || t.areaChart}
577
584
  onPointerMove={(e) => {
578
585
  const r = e.currentTarget.getBoundingClientRect(),
579
586
  x = ((e.clientX - r.left) * W) / r.width;
@@ -672,6 +679,7 @@ export function AreaChart(p: AreaChartProps) {
672
679
  );
673
680
  }
674
681
  export function Heatmap(p: HeatmapProps) {
682
+ const t = useStrings(CHARTS_STRINGS);
675
683
  const rows = p.rows || [],
676
684
  cols = p.cols || [],
677
685
  vals = p.values || [],
@@ -690,7 +698,7 @@ export function Heatmap(p: HeatmapProps) {
690
698
  gridTemplateColumns: "auto repeat(" + cols.length + ", minmax(0, 1fr))",
691
699
  }}
692
700
  role="img"
693
- aria-label={p.label || "Heatmap"}
701
+ aria-label={p.label || t.heatmap}
694
702
  onPointerLeave={() => {
695
703
  hov[1](null);
696
704
  }}
@@ -738,17 +746,18 @@ export function Heatmap(p: HeatmapProps) {
738
746
  : p.caption || ""}
739
747
  </span>
740
748
  <span className="tf-heat-scale">
741
- {"Less"}
749
+ {t.less}
742
750
  {[0, 1, 2, 3, 4].map((l) => (
743
751
  <i key={l} className={"tf-heat-cell l" + l} />
744
752
  ))}
745
- {"More"}
753
+ {t.more}
746
754
  </span>
747
755
  </div>
748
756
  </div>
749
757
  );
750
758
  }
751
759
  export function Funnel(p: FunnelProps) {
760
+ const { formatNumber } = useFormat();
752
761
  const steps = p.steps || [],
753
762
  top = (steps[0] && steps[0].value) || 1,
754
763
  f = p.format || ((v) => formatNumber(v)),
@@ -791,6 +800,7 @@ export function Funnel(p: FunnelProps) {
791
800
  );
792
801
  }
793
802
  export function Gauge(p: GaugeProps) {
803
+ const t = useStrings(CHARTS_STRINGS);
794
804
  const size = p.size || 200,
795
805
  sw = p.stroke || 18,
796
806
  r = (size - sw) / 2,
@@ -819,7 +829,7 @@ export function Gauge(p: GaugeProps) {
819
829
  height={size / 2 + sw / 2}
820
830
  viewBox={"0 0 " + size + " " + (size / 2 + sw / 2)}
821
831
  role="img"
822
- aria-label={`${p.label || "Gauge"}: ${v} of ${max}`}
832
+ aria-label={t.gaugeValue(p.label || t.gauge, v, max)}
823
833
  >
824
834
  <path
825
835
  className="tf-arc-track"
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { useFormat, useStrings } from "../context.js";
4
+ import { CHAT_STRINGS } from "../strings.js";
3
5
  import { cx } from "../cx.js";
4
- import { formatNumber } from "../format.js";
5
6
  import { Icon } from "../Icon.js";
6
7
  import { useControlled } from "../hooks.js";
7
8
  import { Avatar, Badge, TintIcon } from "./foundation.js";
@@ -152,6 +153,7 @@ export function hl(line: string, k: string | number): React.ReactNode[] {
152
153
  }
153
154
 
154
155
  export function CodeBlock(p: CodeBlockProps) {
156
+ const t = useStrings(CHAT_STRINGS);
155
157
  const copied = React.useState(false);
156
158
  function copy() {
157
159
  try {
@@ -168,9 +170,9 @@ export function CodeBlock(p: CodeBlockProps) {
168
170
  <div className="tf-code">
169
171
  <div className="tf-code-head">
170
172
  <span>{p.lang || "text"}</span>
171
- <button type="button" className="tf-code-copy" onClick={copy} aria-label="Copy code">
173
+ <button type="button" className="tf-code-copy" onClick={copy} aria-label={t.copyCode}>
172
174
  <Icon name={copied[0] ? "check" : "file"} size={13} />
173
- {copied[0] ? "Copied" : "Copy"}
175
+ {copied[0] ? t.copied : t.copy}
174
176
  </button>
175
177
  </div>
176
178
  <pre tabIndex={0}>
@@ -191,8 +193,9 @@ export function CodeBlock(p: CodeBlockProps) {
191
193
 
192
194
  /* ── Chat building blocks ── */
193
195
  export function TypingDots(p: { label?: string }) {
196
+ const t = useStrings(CHAT_STRINGS);
194
197
  return (
195
- <span className="tf-typing" role="status" aria-label={p.label || "Working"}>
198
+ <span className="tf-typing" role="status" aria-label={p.label || t.working}>
196
199
  <i />
197
200
  <i />
198
201
  <i />
@@ -217,6 +220,7 @@ export function AgentMark(p: AgentMarkProps) {
217
220
  }
218
221
 
219
222
  export function ChatMessage(p: ChatMessageProps) {
223
+ const t = useStrings(CHAT_STRINGS);
220
224
  const me = p.role === "user",
221
225
  copied = React.useState(false),
222
226
  fb = React.useState(p.feedback || null);
@@ -240,7 +244,7 @@ export function ChatMessage(p: ChatMessageProps) {
240
244
  return (
241
245
  <div className={cx("tf-msg", me ? "me" : "ai", p.className)}>
242
246
  {me ? (
243
- <Avatar name={p.name || "You"} size={32} />
247
+ <Avatar name={p.name || t.you} size={32} />
244
248
  ) : (
245
249
  <AgentMark tint={p.tint} live={p.streaming} />
246
250
  )}
@@ -301,29 +305,29 @@ export function ChatMessage(p: ChatMessageProps) {
301
305
  )}
302
306
  {!me && !p.streaming && p.actions !== false && (
303
307
  <div className="tf-msg-actions">
304
- <Tooltip label={copied[0] ? "Copied" : "Copy"}>
305
- <button type="button" className="tf-msg-act" aria-label="Copy reply" onClick={copy}>
308
+ <Tooltip label={copied[0] ? t.copied : t.copy}>
309
+ <button type="button" className="tf-msg-act" aria-label={t.copyReply} onClick={copy}>
306
310
  <Icon name={copied[0] ? "check" : "file"} size={14} />
307
311
  </button>
308
312
  </Tooltip>
309
313
  {p.onRetry && (
310
- <Tooltip label="Regenerate">
314
+ <Tooltip label={t.regenerate}>
311
315
  <button
312
316
  type="button"
313
317
  className="tf-msg-act"
314
- aria-label="Regenerate reply"
318
+ aria-label={t.regenerateReply}
315
319
  onClick={p.onRetry}
316
320
  >
317
321
  <Icon name="refresh" size={14} />
318
322
  </button>
319
323
  </Tooltip>
320
324
  )}
321
- <Tooltip label="Good reply">
325
+ <Tooltip label={t.goodReply}>
322
326
  <button
323
327
  type="button"
324
328
  className={cx("tf-msg-act", fb[0] === "up" && "on")}
325
329
  aria-pressed={fb[0] === "up"}
326
- aria-label="Good reply"
330
+ aria-label={t.goodReply}
327
331
  onClick={() => {
328
332
  fb[1](fb[0] === "up" ? null : "up");
329
333
  p.onFeedback && p.onFeedback("up");
@@ -332,12 +336,12 @@ export function ChatMessage(p: ChatMessageProps) {
332
336
  <Icon name="thumbUp" size={14} />
333
337
  </button>
334
338
  </Tooltip>
335
- <Tooltip label="Bad reply">
339
+ <Tooltip label={t.badReply}>
336
340
  <button
337
341
  type="button"
338
342
  className={cx("tf-msg-act", fb[0] === "down" && "on")}
339
343
  aria-pressed={fb[0] === "down"}
340
- aria-label="Bad reply"
344
+ aria-label={t.badReply}
341
345
  onClick={() => {
342
346
  fb[1](fb[0] === "down" ? null : "down");
343
347
  p.onFeedback && p.onFeedback("down");
@@ -363,6 +367,7 @@ const TOOL_ICON: Record<string, IconName | null> = {
363
367
  };
364
368
 
365
369
  export function ToolCall(p: ToolCallProps) {
370
+ const t = useStrings(CHAT_STRINGS);
366
371
  const open = React.useState(!!p.defaultOpen),
367
372
  st = p.status || "done";
368
373
  return (
@@ -392,13 +397,13 @@ export function ToolCall(p: ToolCallProps) {
392
397
  <div className="tf-tool-body">
393
398
  {p.input != null && (
394
399
  <div>
395
- <span className="tf-tool-label">{"Input"}</span>
400
+ <span className="tf-tool-label">{t.toolInput}</span>
396
401
  <pre>{typeof p.input === "string" ? p.input : JSON.stringify(p.input, null, 2)}</pre>
397
402
  </div>
398
403
  )}
399
404
  {p.output != null && (
400
405
  <div>
401
- <span className="tf-tool-label">{st === "error" ? "Error" : "Result"}</span>
406
+ <span className="tf-tool-label">{st === "error" ? t.toolError : t.toolResult}</span>
402
407
  <pre className={st === "error" ? "err" : undefined}>
403
408
  {typeof p.output === "string" ? p.output : JSON.stringify(p.output, null, 2)}
404
409
  </pre>
@@ -411,6 +416,7 @@ export function ToolCall(p: ToolCallProps) {
411
416
  }
412
417
 
413
418
  export function Reasoning(p: ReasoningProps) {
419
+ const t = useStrings(CHAT_STRINGS);
414
420
  const open = React.useState(!!p.defaultOpen);
415
421
  return (
416
422
  <div className={cx("tf-reason", open[0] && "open", p.streaming && "live")}>
@@ -423,7 +429,7 @@ export function Reasoning(p: ReasoningProps) {
423
429
  }}
424
430
  >
425
431
  <Icon name="sparkle" size={13} />
426
- {p.streaming ? "Thinking…" : `Thought for ${p.duration || "a few seconds"}`}
432
+ {p.streaming ? t.thinking : t.thoughtFor(p.duration)}
427
433
  <Icon name="chevronDown" size={13} className="tf-tool-chev" />
428
434
  </button>
429
435
  {open[0] && (
@@ -470,6 +476,7 @@ export function PromptStarters(p: PromptStartersProps) {
470
476
  }
471
477
 
472
478
  export function ModelPicker(p: ModelPickerProps) {
479
+ const t = useStrings(CHAT_STRINGS);
473
480
  const models = p.models || [],
474
481
  cur = models.find((m) => m.id === p.value) || models[0] || {};
475
482
  return (
@@ -493,7 +500,7 @@ export function ModelPicker(p: ModelPickerProps) {
493
500
  >
494
501
  {(s) => (
495
502
  <div className="tf-menu tf-menu-static" role="menu">
496
- <div className="tf-menu-label">{"Model"}</div>
503
+ <div className="tf-menu-label">{t.model}</div>
497
504
  {models.map((m) => (
498
505
  <button
499
506
  key={m.id}
@@ -520,6 +527,7 @@ export function ModelPicker(p: ModelPickerProps) {
520
527
  }
521
528
 
522
529
  export function Composer(p: ComposerProps) {
530
+ const t = useStrings(CHAT_STRINGS);
523
531
  const fileRef = React.useRef<HTMLInputElement>(null);
524
532
  const ta = React.useRef<HTMLTextAreaElement>(null),
525
533
  v = useControlled(p.value, "", p.onChange),
@@ -559,7 +567,7 @@ export function Composer(p: ComposerProps) {
559
567
  >
560
568
  {cmds.length > 0 && (
561
569
  <div className="tf-popover tf-composer-cmds top" role="listbox">
562
- <div className="tf-menu-label">{"Commands"}</div>
570
+ <div className="tf-menu-label">{t.commands}</div>
563
571
  {cmds.map((c, i) => (
564
572
  <button
565
573
  key={c.name}
@@ -592,7 +600,7 @@ export function Composer(p: ComposerProps) {
592
600
  {p.onRemoveAttachment && (
593
601
  <button
594
602
  type="button"
595
- aria-label={`Remove ${f.name}`}
603
+ aria-label={t.removeItem(f.name)}
596
604
  onClick={() => {
597
605
  p.onRemoveAttachment?.(i);
598
606
  }}
@@ -608,8 +616,8 @@ export function Composer(p: ComposerProps) {
608
616
  ref={ta}
609
617
  rows={1}
610
618
  className="tf-composer-input"
611
- placeholder={p.placeholder || "Message…"}
612
- aria-label={p.label || p.placeholder || "Message"}
619
+ placeholder={p.placeholder || t.messagePlaceholder}
620
+ aria-label={p.label || p.placeholder || t.message}
613
621
  value={v[0]}
614
622
  disabled={p.disabled}
615
623
  onChange={(e) => {
@@ -642,11 +650,11 @@ export function Composer(p: ComposerProps) {
642
650
  />
643
651
  <div className="tf-composer-bar">
644
652
  {p.onAttach && (
645
- <Tooltip label="Attach files">
653
+ <Tooltip label={t.attachFiles}>
646
654
  <button
647
655
  type="button"
648
656
  className="tf-composer-tool"
649
- aria-label="Attach files"
657
+ aria-label={t.attachFiles}
650
658
  onClick={() => fileRef.current?.click()}
651
659
  >
652
660
  <Icon name="paperclip" size={16} />
@@ -665,11 +673,11 @@ export function Composer(p: ComposerProps) {
665
673
  </Tooltip>
666
674
  )}
667
675
  {p.commands && (
668
- <Tooltip label="Commands" shortcut="/">
676
+ <Tooltip label={t.commands} shortcut="/">
669
677
  <button
670
678
  type="button"
671
679
  className="tf-composer-tool"
672
- aria-label="Commands"
680
+ aria-label={t.commands}
673
681
  onClick={() => {
674
682
  v[1]("/");
675
683
  ta.current && ta.current.focus();
@@ -681,18 +689,16 @@ export function Composer(p: ComposerProps) {
681
689
  )}
682
690
  {p.models && <ModelPicker models={p.models} value={p.model} onChange={p.onModel} />}
683
691
  {p.left}
684
- <span className="tf-composer-hint">
685
- {p.hint != null ? p.hint : "Enter to send · Shift+Enter for a new line"}
686
- </span>
692
+ <span className="tf-composer-hint">{p.hint != null ? p.hint : t.composerHint}</span>
687
693
  {p.busy ? (
688
- <button type="button" className="tf-send stop" aria-label="Stop" onClick={p.onStop}>
694
+ <button type="button" className="tf-send stop" aria-label={t.stop} onClick={p.onStop}>
689
695
  <span className="tf-stop-sq" />
690
696
  </button>
691
697
  ) : (
692
698
  <button
693
699
  type="button"
694
700
  className="tf-send"
695
- aria-label="Send"
701
+ aria-label={t.send}
696
702
  disabled={!v[0].trim() && !files.length}
697
703
  onClick={send}
698
704
  >
@@ -714,11 +720,12 @@ const AGENT_TONE: Record<string, Tone> = {
714
720
  };
715
721
 
716
722
  export function AgentStatus(p: AgentStatusProps) {
723
+ const t = useStrings(CHAT_STRINGS);
717
724
  const s = p.status || "Idle";
718
725
  return (
719
726
  <span className="tf-agentstatus">
720
727
  <Badge tone={AGENT_TONE[s] || "neutral"} live={s === "Running" || s === "Thinking"}>
721
- {s}
728
+ {t.agentStatus[s] ?? s}
722
729
  </Badge>
723
730
  {p.step && <span className="tf-agentstatus-step">{p.step}</span>}
724
731
  </span>
@@ -736,6 +743,8 @@ const TRACE_ICON: Record<string, IconName> = {
736
743
  };
737
744
 
738
745
  export function RunTrace(p: RunTraceProps) {
746
+ const t = useStrings(CHAT_STRINGS);
747
+ const { formatNumber } = useFormat();
739
748
  const steps = p.steps || [],
740
749
  total = steps.reduce((a, s) => Math.max(a, (s.start || 0) + (s.ms || 0)), 1),
741
750
  sel = React.useState(p.defaultOpen != null ? p.defaultOpen : null);
@@ -747,8 +756,8 @@ export function RunTrace(p: RunTraceProps) {
747
756
  return (
748
757
  <div className="tf-trace">
749
758
  <div className="tf-trace-head">
750
- <span>{"Step"}</span>
751
- <span>{`Timeline · ${fms(total)}`}</span>
759
+ <span>{t.step}</span>
760
+ <span>{t.timeline(fms(total))}</span>
752
761
  </div>
753
762
  {steps.map((s, i) => {
754
763
  const open = sel[0] === i,
@@ -777,7 +786,7 @@ export function RunTrace(p: RunTraceProps) {
777
786
  width: Math.max(1.5, ((s.ms || 0) / total) * 100) + "%",
778
787
  }}
779
788
  />
780
- <em>{st === "running" ? "running…" : fms(s.ms || 0)}</em>
789
+ <em>{st === "running" ? t.stepRunning : fms(s.ms || 0)}</em>
781
790
  </span>
782
791
  </button>
783
792
  {open && s.detail && (
@@ -794,12 +803,13 @@ export function RunTrace(p: RunTraceProps) {
794
803
  }
795
804
 
796
805
  export function UsageMeter(p: UsageMeterProps) {
806
+ const t = useStrings(CHAT_STRINGS);
797
807
  const pct = Math.min(100, ((p.used || 0) / (p.limit || 1)) * 100),
798
808
  tone = pct >= 90 ? "blush" : pct >= 70 ? "butter" : "mint";
799
809
  return (
800
810
  <div className="tf-usage">
801
811
  <div className="tf-usage-top">
802
- <span>{p.label || "Context"}</span>
812
+ <span>{p.label || t.context}</span>
803
813
  <b>{(p.format || String)(p.used) + " / " + (p.format || String)(p.limit)}</b>
804
814
  </div>
805
815
  <span className="tf-usage-track">