@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.
- package/dist/components/admin.d.ts.map +1 -1
- package/dist/components/admin.js +95 -71
- package/dist/components/admin.js.map +1 -1
- package/dist/components/agents.d.ts.map +1 -1
- package/dist/components/agents.js +49 -35
- package/dist/components/agents.js.map +1 -1
- package/dist/components/app-chrome.d.ts.map +1 -1
- package/dist/components/app-chrome.js +6 -2
- package/dist/components/app-chrome.js.map +1 -1
- package/dist/components/approvals.d.ts.map +1 -1
- package/dist/components/approvals.js +22 -18
- package/dist/components/approvals.js.map +1 -1
- package/dist/components/building-blocks.d.ts.map +1 -1
- package/dist/components/building-blocks.js +6 -2
- package/dist/components/building-blocks.js.map +1 -1
- package/dist/components/charts.d.ts.map +1 -1
- package/dist/components/charts.js +20 -10
- package/dist/components/charts.js.map +1 -1
- package/dist/components/chat.d.ts.map +1 -1
- package/dist/components/chat.js +31 -19
- package/dist/components/chat.js.map +1 -1
- package/dist/components/commerce.d.ts +2 -2
- package/dist/components/commerce.d.ts.map +1 -1
- package/dist/components/commerce.js +45 -31
- package/dist/components/commerce.js.map +1 -1
- package/dist/components/data-display.d.ts +5 -9
- package/dist/components/data-display.d.ts.map +1 -1
- package/dist/components/data-display.js +34 -35
- package/dist/components/data-display.js.map +1 -1
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +15 -10
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/developer.d.ts.map +1 -1
- package/dist/components/developer.js +20 -11
- package/dist/components/developer.js.map +1 -1
- package/dist/components/feedback.d.ts.map +1 -1
- package/dist/components/feedback.js +12 -6
- package/dist/components/feedback.js.map +1 -1
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/form.js +12 -9
- package/dist/components/form.js.map +1 -1
- package/dist/components/forms.d.ts +2 -1
- package/dist/components/forms.d.ts.map +1 -1
- package/dist/components/forms.js +92 -42
- package/dist/components/forms.js.map +1 -1
- package/dist/components/foundation.d.ts +5 -2
- package/dist/components/foundation.d.ts.map +1 -1
- package/dist/components/foundation.js +15 -12
- package/dist/components/foundation.js.map +1 -1
- package/dist/components/inputs.d.ts.map +1 -1
- package/dist/components/inputs.js +3 -1
- package/dist/components/inputs.js.map +1 -1
- package/dist/components/mail.d.ts.map +1 -1
- package/dist/components/mail.js +15 -10
- package/dist/components/mail.js.map +1 -1
- package/dist/components/media.d.ts.map +1 -1
- package/dist/components/media.js +39 -18
- package/dist/components/media.js.map +1 -1
- package/dist/components/navigation.d.ts.map +1 -1
- package/dist/components/navigation.js +21 -9
- package/dist/components/navigation.js.map +1 -1
- package/dist/components/operations.d.ts.map +1 -1
- package/dist/components/operations.js +37 -27
- package/dist/components/operations.js.map +1 -1
- package/dist/components/previews.d.ts.map +1 -1
- package/dist/components/previews.js +14 -10
- package/dist/components/previews.js.map +1 -1
- package/dist/components/primitives.d.ts.map +1 -1
- package/dist/components/primitives.js +4 -1
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/real-estate.d.ts.map +1 -1
- package/dist/components/real-estate.js +43 -28
- 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 +18 -15
- package/dist/components/rich-text-editor.js.map +1 -1
- package/dist/components/select-menu.d.ts.map +1 -1
- package/dist/components/select-menu.js +4 -1
- package/dist/components/select-menu.js.map +1 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +8 -3
- package/dist/components/shell.js.map +1 -1
- package/dist/components/site.d.ts.map +1 -1
- package/dist/components/site.js +17 -13
- package/dist/components/site.js.map +1 -1
- package/dist/components/types.d.ts +102 -3
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +50 -34
- package/dist/components/workspace.js.map +1 -1
- package/dist/context.d.ts +39 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +76 -0
- package/dist/context.js.map +1 -0
- package/dist/format.d.ts +33 -6
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +75 -10
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +18 -11
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +11 -18
- package/dist/provider.js.map +1 -1
- package/dist/strings.d.ts +503 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +537 -0
- package/dist/strings.js.map +1 -0
- package/dist/themes.css +4 -0
- package/package.json +1 -1
- package/src/components/admin.tsx +117 -93
- package/src/components/agents.tsx +55 -41
- package/src/components/app-chrome.tsx +6 -2
- package/src/components/approvals.tsx +26 -22
- package/src/components/building-blocks.tsx +6 -2
- package/src/components/charts.tsx +21 -11
- package/src/components/chat.tsx +45 -35
- package/src/components/commerce.tsx +61 -53
- package/src/components/data-display.tsx +38 -45
- package/src/components/data-table.tsx +20 -15
- package/src/components/developer.tsx +26 -15
- package/src/components/feedback.tsx +19 -7
- package/src/components/form.tsx +17 -9
- package/src/components/forms.tsx +131 -53
- package/src/components/foundation.tsx +23 -36
- package/src/components/inputs.tsx +4 -2
- package/src/components/mail.tsx +16 -11
- package/src/components/media.tsx +64 -21
- package/src/components/navigation.tsx +25 -13
- package/src/components/operations.tsx +40 -32
- package/src/components/previews.tsx +17 -15
- package/src/components/primitives.tsx +8 -5
- package/src/components/real-estate.tsx +55 -35
- package/src/components/rich-text-editor.tsx +20 -17
- package/src/components/select-menu.tsx +5 -2
- package/src/components/shell.tsx +8 -3
- package/src/components/site.tsx +18 -14
- package/src/components/types.ts +111 -3
- package/src/components/workspace.tsx +79 -60
- package/src/context.ts +112 -0
- package/src/format.ts +108 -15
- package/src/index.ts +43 -0
- package/src/provider.tsx +36 -40
- package/src/strings.ts +1500 -0
package/src/components/admin.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useControlled, useId } from "../hooks.js";
|
|
4
|
-
import {
|
|
4
|
+
import { useFormat, useStrings } from "../context.js";
|
|
5
|
+
import { ADMIN_STRINGS } from "../strings.js";
|
|
5
6
|
import { cx } from "../cx.js";
|
|
6
7
|
import { Icon } from "../Icon.js";
|
|
7
8
|
import { toast } from "../toast-store.js";
|
|
@@ -46,6 +47,8 @@ import type {
|
|
|
46
47
|
|
|
47
48
|
/* ── Shared: range slider, bar list, status select, tile picker ── */
|
|
48
49
|
export function RangeSlider(p: RangeSliderProps) {
|
|
50
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
51
|
+
const { formatNumber } = useFormat();
|
|
49
52
|
const id = useId("rs"),
|
|
50
53
|
min = p.min || 0,
|
|
51
54
|
max = p.max != null ? p.max : 100,
|
|
@@ -80,7 +83,7 @@ export function RangeSlider(p: RangeSliderProps) {
|
|
|
80
83
|
max={max}
|
|
81
84
|
step={step}
|
|
82
85
|
value={lo}
|
|
83
|
-
aria-label=
|
|
86
|
+
aria-label={t.minimum}
|
|
84
87
|
onChange={(e) => {
|
|
85
88
|
set(0, e.target.value);
|
|
86
89
|
}}
|
|
@@ -91,7 +94,7 @@ export function RangeSlider(p: RangeSliderProps) {
|
|
|
91
94
|
max={max}
|
|
92
95
|
step={step}
|
|
93
96
|
value={hi}
|
|
94
|
-
aria-label=
|
|
97
|
+
aria-label={t.maximum}
|
|
95
98
|
onChange={(e) => {
|
|
96
99
|
set(1, e.target.value);
|
|
97
100
|
}}
|
|
@@ -100,7 +103,7 @@ export function RangeSlider(p: RangeSliderProps) {
|
|
|
100
103
|
{p.inputs !== false ? (
|
|
101
104
|
<div className="tf-range-inputs">
|
|
102
105
|
<label className="tf-range-num">
|
|
103
|
-
<small>{
|
|
106
|
+
<small>{t.min}</small>
|
|
104
107
|
<input
|
|
105
108
|
id={id}
|
|
106
109
|
className="tf-input"
|
|
@@ -116,7 +119,7 @@ export function RangeSlider(p: RangeSliderProps) {
|
|
|
116
119
|
–
|
|
117
120
|
</span>
|
|
118
121
|
<label className="tf-range-num">
|
|
119
|
-
<small>{
|
|
122
|
+
<small>{t.max}</small>
|
|
120
123
|
<input
|
|
121
124
|
className="tf-input"
|
|
122
125
|
inputMode="numeric"
|
|
@@ -140,6 +143,7 @@ export function RangeSlider(p: RangeSliderProps) {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
export function BarList(p: BarListProps) {
|
|
146
|
+
const { formatNumber } = useFormat();
|
|
143
147
|
const items = p.items || [],
|
|
144
148
|
max = Math.max.apply(null, items.map((i) => i.value).concat([1])),
|
|
145
149
|
f = p.format || formatNumber;
|
|
@@ -319,6 +323,8 @@ export function TilePicker(p: TilePickerProps) {
|
|
|
319
323
|
|
|
320
324
|
/* ── Live feed ── */
|
|
321
325
|
export function LiveFeed(p: LiveFeedProps) {
|
|
326
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
327
|
+
const { formatMoney } = useFormat();
|
|
322
328
|
const paused = useControlled(p.paused, false, p.onPause),
|
|
323
329
|
held = React.useRef<LiveFeedItem[] | null>(null),
|
|
324
330
|
filter = React.useState<string | null>(null);
|
|
@@ -334,7 +340,7 @@ export function LiveFeed(p: LiveFeedProps) {
|
|
|
334
340
|
<div className="tf-livefeed-head">
|
|
335
341
|
<span className={cx("tf-live", paused[0] && "off")}>
|
|
336
342
|
<span className="tf-live-dot" aria-hidden />
|
|
337
|
-
{paused[0] ?
|
|
343
|
+
{paused[0] ? t.paused : t.live}
|
|
338
344
|
</span>
|
|
339
345
|
{types.length > 0 && (
|
|
340
346
|
<div
|
|
@@ -349,7 +355,7 @@ export function LiveFeed(p: LiveFeedProps) {
|
|
|
349
355
|
filter[1](null);
|
|
350
356
|
}}
|
|
351
357
|
>
|
|
352
|
-
{
|
|
358
|
+
{t.all}
|
|
353
359
|
</Chip>
|
|
354
360
|
{types.map((t) => (
|
|
355
361
|
<Chip
|
|
@@ -377,13 +383,13 @@ export function LiveFeed(p: LiveFeedProps) {
|
|
|
377
383
|
paused[1](false);
|
|
378
384
|
}}
|
|
379
385
|
>
|
|
380
|
-
{
|
|
386
|
+
{t.newCount(waiting)}
|
|
381
387
|
</button>
|
|
382
388
|
)}
|
|
383
389
|
<IconButton
|
|
384
390
|
icon={paused[0] ? "play" : "pause"}
|
|
385
391
|
size="sm"
|
|
386
|
-
label={paused[0] ?
|
|
392
|
+
label={paused[0] ? t.resume : t.pause}
|
|
387
393
|
onClick={() => {
|
|
388
394
|
paused[1](!paused[0]);
|
|
389
395
|
}}
|
|
@@ -408,7 +414,7 @@ export function LiveFeed(p: LiveFeedProps) {
|
|
|
408
414
|
padding: 16,
|
|
409
415
|
}}
|
|
410
416
|
>
|
|
411
|
-
{p.emptyText ||
|
|
417
|
+
{p.emptyText || t.waitingForEvents}
|
|
412
418
|
</p>
|
|
413
419
|
) : (
|
|
414
420
|
shown.map((it) => (
|
|
@@ -463,6 +469,8 @@ export function WorldDots() {
|
|
|
463
469
|
|
|
464
470
|
/* ── Map with pins (background is yours: tiles, an SVG, an image) ── */
|
|
465
471
|
export function MapView(p: MapViewProps) {
|
|
472
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
473
|
+
const { formatNumber } = useFormat();
|
|
466
474
|
const b = p.bounds || {
|
|
467
475
|
n: 84,
|
|
468
476
|
s: -60,
|
|
@@ -487,7 +495,7 @@ export function MapView(p: MapViewProps) {
|
|
|
487
495
|
height: Hh,
|
|
488
496
|
}}
|
|
489
497
|
role="region"
|
|
490
|
-
aria-label={p.label ||
|
|
498
|
+
aria-label={p.label || t.viewMap}
|
|
491
499
|
>
|
|
492
500
|
{p.background || (!p.bounds ? <WorldDots /> : null) || (
|
|
493
501
|
<svg className="tf-map-grid" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden>
|
|
@@ -569,6 +577,8 @@ export function MapView(p: MapViewProps) {
|
|
|
569
577
|
|
|
570
578
|
/* ── Money and operations for admins ── */
|
|
571
579
|
export function BalanceCard(p: BalanceCardProps) {
|
|
580
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
581
|
+
const { formatMoney } = useFormat();
|
|
572
582
|
const days = p.perDay && p.perDay > 0 ? Math.floor(p.balance / p.perDay) : null,
|
|
573
583
|
low = p.low != null && p.balance <= p.low,
|
|
574
584
|
st = p.status || (p.balance <= 0 ? "danger" : low ? "warn" : "ok");
|
|
@@ -582,7 +592,9 @@ export function BalanceCard(p: BalanceCardProps) {
|
|
|
582
592
|
</div>
|
|
583
593
|
<StatusIndicator
|
|
584
594
|
status={st}
|
|
585
|
-
label={
|
|
595
|
+
label={
|
|
596
|
+
p.statusLabel || (st === "danger" ? t.balanceEmpty : low ? t.balanceLow : t.balanceOk)
|
|
597
|
+
}
|
|
586
598
|
size="sm"
|
|
587
599
|
/>
|
|
588
600
|
</div>
|
|
@@ -600,16 +612,14 @@ export function BalanceCard(p: BalanceCardProps) {
|
|
|
600
612
|
</span>
|
|
601
613
|
<span className="tf-balance-meta">
|
|
602
614
|
{[
|
|
603
|
-
p.perDay != null &&
|
|
604
|
-
days != null && p.balance > 0 &&
|
|
615
|
+
p.perDay != null && t.perDay(formatMoney(p.perDay, p.currency)),
|
|
616
|
+
days != null && p.balance > 0 && t.lastsAbout(days),
|
|
605
617
|
]
|
|
606
618
|
.filter(Boolean)
|
|
607
619
|
.join(" · ")}
|
|
608
620
|
</span>
|
|
609
621
|
{p.low != null && (
|
|
610
|
-
<span className="tf-balance-meta">
|
|
611
|
-
{`Warns below ${formatMoney(p.low, p.currency)}`}
|
|
612
|
-
</span>
|
|
622
|
+
<span className="tf-balance-meta">{t.warnsBelow(formatMoney(p.low, p.currency))}</span>
|
|
613
623
|
)}
|
|
614
624
|
</div>
|
|
615
625
|
{p.spark && (
|
|
@@ -631,7 +641,7 @@ export function BalanceCard(p: BalanceCardProps) {
|
|
|
631
641
|
{p.action}
|
|
632
642
|
{p.onTopUp && (
|
|
633
643
|
<Button size="sm" variant={low ? "primary" : "soft"} icon="plus" onClick={p.onTopUp}>
|
|
634
|
-
{p.topUpLabel ||
|
|
644
|
+
{p.topUpLabel || t.topUp}
|
|
635
645
|
</Button>
|
|
636
646
|
)}
|
|
637
647
|
</div>
|
|
@@ -641,6 +651,7 @@ export function BalanceCard(p: BalanceCardProps) {
|
|
|
641
651
|
}
|
|
642
652
|
|
|
643
653
|
export function ServiceMonitor(p: ServiceMonitorProps) {
|
|
654
|
+
const { formatNumber } = useFormat();
|
|
644
655
|
return (
|
|
645
656
|
<div className="tf-svcmon" role="list" aria-label={p.label}>
|
|
646
657
|
{(p.items || []).map((s) => (
|
|
@@ -676,6 +687,7 @@ export function ServiceMonitor(p: ServiceMonitorProps) {
|
|
|
676
687
|
}
|
|
677
688
|
|
|
678
689
|
export function RunHistory(p: RunHistoryProps) {
|
|
690
|
+
const { formatNumber } = useFormat();
|
|
679
691
|
const open = React.useState<string | number | null>(null);
|
|
680
692
|
return (
|
|
681
693
|
<div className="tf-runs" role="list" aria-label={p.label}>
|
|
@@ -718,6 +730,8 @@ export function RunHistory(p: RunHistoryProps) {
|
|
|
718
730
|
}
|
|
719
731
|
|
|
720
732
|
export function WebhookEndpoint(p: WebhookEndpointProps) {
|
|
733
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
734
|
+
const { formatNumber } = useFormat();
|
|
721
735
|
const open = React.useState(!!p.defaultOpen);
|
|
722
736
|
return (
|
|
723
737
|
<div className="tf-hook">
|
|
@@ -743,12 +757,12 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
|
|
|
743
757
|
</span>
|
|
744
758
|
)}
|
|
745
759
|
{p.onToggle && (
|
|
746
|
-
<Switch checked={p.enabled !== false} onChange={p.onToggle} label=
|
|
760
|
+
<Switch checked={p.enabled !== false} onChange={p.onToggle} label={t.enabled} />
|
|
747
761
|
)}
|
|
748
762
|
{p.menu && (
|
|
749
763
|
<Popover
|
|
750
764
|
width={200}
|
|
751
|
-
trigger={(s) => <IconButton icon="more" size="sm" label=
|
|
765
|
+
trigger={(s) => <IconButton icon="more" size="sm" label={t.more} onClick={s.toggle} />}
|
|
752
766
|
>
|
|
753
767
|
{() => <Menu items={p.menu || []} />}
|
|
754
768
|
</Popover>
|
|
@@ -757,7 +771,7 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
|
|
|
757
771
|
{p.secret && p.secret}
|
|
758
772
|
{(p.deliveries || []).length > 0 && (
|
|
759
773
|
<Collapsible
|
|
760
|
-
title=
|
|
774
|
+
title={t.recentDeliveries}
|
|
761
775
|
meta={formatNumber((p.deliveries || []).length)}
|
|
762
776
|
open={open[0]}
|
|
763
777
|
onOpenChange={open[1]}
|
|
@@ -782,7 +796,7 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
|
|
|
782
796
|
p.onRetry?.(d);
|
|
783
797
|
}}
|
|
784
798
|
>
|
|
785
|
-
{
|
|
799
|
+
{t.retry}
|
|
786
800
|
</Button>
|
|
787
801
|
)}
|
|
788
802
|
</div>
|
|
@@ -796,6 +810,7 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
|
|
|
796
810
|
}
|
|
797
811
|
|
|
798
812
|
export function BackupList(p: BackupListProps) {
|
|
813
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
799
814
|
return (
|
|
800
815
|
<div className="tf-backups" role="list" aria-label={p.label}>
|
|
801
816
|
{(p.items || []).map((b) => (
|
|
@@ -816,7 +831,7 @@ export function BackupList(p: BackupListProps) {
|
|
|
816
831
|
<IconButton
|
|
817
832
|
icon="download"
|
|
818
833
|
size="sm"
|
|
819
|
-
label=
|
|
834
|
+
label={t.download}
|
|
820
835
|
onClick={() => {
|
|
821
836
|
p.onDownload?.(b);
|
|
822
837
|
}}
|
|
@@ -830,7 +845,7 @@ export function BackupList(p: BackupListProps) {
|
|
|
830
845
|
p.onRestore?.(b);
|
|
831
846
|
}}
|
|
832
847
|
>
|
|
833
|
-
{
|
|
848
|
+
{t.restore}
|
|
834
849
|
</Button>
|
|
835
850
|
)}
|
|
836
851
|
</span>
|
|
@@ -842,6 +857,7 @@ export function BackupList(p: BackupListProps) {
|
|
|
842
857
|
}
|
|
843
858
|
|
|
844
859
|
export function BlockList(p: BlockListProps) {
|
|
860
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
845
861
|
const q = React.useState("");
|
|
846
862
|
const list = (p.items || []).filter(
|
|
847
863
|
(b) =>
|
|
@@ -856,7 +872,7 @@ export function BlockList(p: BlockListProps) {
|
|
|
856
872
|
>
|
|
857
873
|
<div className="tf-filterbar">
|
|
858
874
|
<SearchField
|
|
859
|
-
placeholder={p.searchPlaceholder ||
|
|
875
|
+
placeholder={p.searchPlaceholder || t.searchBlocked}
|
|
860
876
|
value={q[0]}
|
|
861
877
|
onChange={(e) => {
|
|
862
878
|
q[1](e.target.value);
|
|
@@ -868,12 +884,12 @@ export function BlockList(p: BlockListProps) {
|
|
|
868
884
|
/>
|
|
869
885
|
{p.onAdd && (
|
|
870
886
|
<Button size="sm" icon="plus" onClick={p.onAdd}>
|
|
871
|
-
{p.addLabel ||
|
|
887
|
+
{p.addLabel || t.block}
|
|
872
888
|
</Button>
|
|
873
889
|
)}
|
|
874
890
|
</div>
|
|
875
891
|
{list.length === 0 ? (
|
|
876
|
-
<p className="tf-card-desc">{p.emptyText ||
|
|
892
|
+
<p className="tf-card-desc">{p.emptyText || t.nothingBlocked}</p>
|
|
877
893
|
) : (
|
|
878
894
|
<div className="tf-blocks" role="list">
|
|
879
895
|
{list.map((b) => (
|
|
@@ -889,7 +905,7 @@ export function BlockList(p: BlockListProps) {
|
|
|
889
905
|
<small>{[b.kind, b.reason].filter(Boolean).join(" · ")}</small>
|
|
890
906
|
</span>
|
|
891
907
|
<span className="tf-blocks-meta">
|
|
892
|
-
{[b.by, b.until ?
|
|
908
|
+
{[b.by, b.until ? t.until(b.until) : t.permanent].filter(Boolean).join(" · ")}
|
|
893
909
|
</span>
|
|
894
910
|
{p.onRemove && (
|
|
895
911
|
<Button
|
|
@@ -899,7 +915,7 @@ export function BlockList(p: BlockListProps) {
|
|
|
899
915
|
p.onRemove?.(b);
|
|
900
916
|
}}
|
|
901
917
|
>
|
|
902
|
-
{p.removeLabel ||
|
|
918
|
+
{p.removeLabel || t.unblock}
|
|
903
919
|
</Button>
|
|
904
920
|
)}
|
|
905
921
|
</div>
|
|
@@ -914,14 +930,15 @@ export function BlockList(p: BlockListProps) {
|
|
|
914
930
|
|
|
915
931
|
/* ── Reviews and coupons ── */
|
|
916
932
|
export function ReviewCard(p: ReviewCardProps) {
|
|
933
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
917
934
|
const reply = React.useState(false),
|
|
918
935
|
txt = React.useState(p.reply || "");
|
|
919
936
|
const st = (
|
|
920
937
|
{
|
|
921
|
-
pending: ["signal",
|
|
922
|
-
published: ["ok",
|
|
923
|
-
hidden: ["idle",
|
|
924
|
-
flagged: ["warn",
|
|
938
|
+
pending: ["signal", t.reviewState.pending],
|
|
939
|
+
published: ["ok", t.reviewState.published],
|
|
940
|
+
hidden: ["idle", t.reviewState.hidden],
|
|
941
|
+
flagged: ["warn", t.reviewState.flagged],
|
|
925
942
|
} as Record<string, [Status, string]>
|
|
926
943
|
)[p.state || "pending"] as [Status, string];
|
|
927
944
|
return (
|
|
@@ -939,7 +956,7 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
939
956
|
{p.verified && (
|
|
940
957
|
<span className="tf-review-ver">
|
|
941
958
|
{" · "}
|
|
942
|
-
{
|
|
959
|
+
{t.verifiedPurchase}
|
|
943
960
|
</span>
|
|
944
961
|
)}
|
|
945
962
|
</b>
|
|
@@ -952,7 +969,7 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
952
969
|
<p className="tf-review-text">{p.highlight ? markWords(p.text, p.highlight) : p.text}</p>
|
|
953
970
|
{p.reply && !reply[0] && (
|
|
954
971
|
<div className="tf-review-reply">
|
|
955
|
-
<small>{p.replyLabel ||
|
|
972
|
+
<small>{p.replyLabel || t.yourReply}</small>
|
|
956
973
|
{p.reply}
|
|
957
974
|
</div>
|
|
958
975
|
)}
|
|
@@ -970,8 +987,8 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
970
987
|
onChange={(e) => {
|
|
971
988
|
txt[1](e.target.value);
|
|
972
989
|
}}
|
|
973
|
-
placeholder=
|
|
974
|
-
aria-label=
|
|
990
|
+
placeholder={t.writeReply}
|
|
991
|
+
aria-label={t.reply}
|
|
975
992
|
/>
|
|
976
993
|
<div
|
|
977
994
|
className="tf-row"
|
|
@@ -986,7 +1003,7 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
986
1003
|
reply[1](false);
|
|
987
1004
|
}}
|
|
988
1005
|
>
|
|
989
|
-
{
|
|
1006
|
+
{t.cancel}
|
|
990
1007
|
</Button>
|
|
991
1008
|
<Button
|
|
992
1009
|
size="sm"
|
|
@@ -996,7 +1013,7 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
996
1013
|
reply[1](false);
|
|
997
1014
|
}}
|
|
998
1015
|
>
|
|
999
|
-
{
|
|
1016
|
+
{t.postReply}
|
|
1000
1017
|
</Button>
|
|
1001
1018
|
</div>
|
|
1002
1019
|
</div>
|
|
@@ -1005,12 +1022,12 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
1005
1022
|
<div className="tf-review-actions">
|
|
1006
1023
|
{p.state !== "published" && p.onPublish && (
|
|
1007
1024
|
<Button size="sm" icon="check" onClick={p.onPublish}>
|
|
1008
|
-
{
|
|
1025
|
+
{t.publish}
|
|
1009
1026
|
</Button>
|
|
1010
1027
|
)}
|
|
1011
1028
|
{p.state !== "hidden" && p.onHide && (
|
|
1012
1029
|
<Button size="sm" variant="secondary" icon="eye" onClick={p.onHide}>
|
|
1013
|
-
{
|
|
1030
|
+
{t.hide}
|
|
1014
1031
|
</Button>
|
|
1015
1032
|
)}
|
|
1016
1033
|
{p.onReply && (
|
|
@@ -1022,7 +1039,7 @@ export function ReviewCard(p: ReviewCardProps) {
|
|
|
1022
1039
|
reply[1](true);
|
|
1023
1040
|
}}
|
|
1024
1041
|
>
|
|
1025
|
-
{p.reply ?
|
|
1042
|
+
{p.reply ? t.editReply : t.reply}
|
|
1026
1043
|
</Button>
|
|
1027
1044
|
)}
|
|
1028
1045
|
</div>
|
|
@@ -1042,6 +1059,8 @@ function markWords(text: string, words: string[]) {
|
|
|
1042
1059
|
}
|
|
1043
1060
|
|
|
1044
1061
|
export function CouponCard(p: CouponCardProps) {
|
|
1062
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
1063
|
+
const { formatMoney, formatNumber } = useFormat();
|
|
1045
1064
|
const used = p.used || 0,
|
|
1046
1065
|
st = p.status || (p.limit && used >= p.limit ? "idle" : "ok");
|
|
1047
1066
|
return (
|
|
@@ -1066,7 +1085,7 @@ export function CouponCard(p: CouponCardProps) {
|
|
|
1066
1085
|
<button
|
|
1067
1086
|
type="button"
|
|
1068
1087
|
className="tf-coupon-code"
|
|
1069
|
-
title=
|
|
1088
|
+
title={t.copy}
|
|
1070
1089
|
onClick={() => {
|
|
1071
1090
|
try {
|
|
1072
1091
|
navigator.clipboard.writeText(p.code);
|
|
@@ -1075,7 +1094,7 @@ export function CouponCard(p: CouponCardProps) {
|
|
|
1075
1094
|
}
|
|
1076
1095
|
toast &&
|
|
1077
1096
|
toast({
|
|
1078
|
-
text:
|
|
1097
|
+
text: t.copied,
|
|
1079
1098
|
});
|
|
1080
1099
|
p.onCopy && p.onCopy();
|
|
1081
1100
|
}}
|
|
@@ -1091,8 +1110,8 @@ export function CouponCard(p: CouponCardProps) {
|
|
|
1091
1110
|
max={p.limit || Math.max(used, 1)}
|
|
1092
1111
|
text={
|
|
1093
1112
|
p.limit
|
|
1094
|
-
?
|
|
1095
|
-
:
|
|
1113
|
+
? t.usedOf(formatNumber(used), formatNumber(p.limit))
|
|
1114
|
+
: t.used(formatNumber(used))
|
|
1096
1115
|
}
|
|
1097
1116
|
tone="sky"
|
|
1098
1117
|
warn={101}
|
|
@@ -1100,9 +1119,9 @@ export function CouponCard(p: CouponCardProps) {
|
|
|
1100
1119
|
/>
|
|
1101
1120
|
<span className="tf-coupon-foot">
|
|
1102
1121
|
{[
|
|
1103
|
-
p.until &&
|
|
1104
|
-
p.min != null &&
|
|
1105
|
-
p.revenue != null &&
|
|
1122
|
+
p.until && t.validUntil(p.until),
|
|
1123
|
+
p.min != null && t.fromAmount(formatMoney(p.min, p.currency)),
|
|
1124
|
+
p.revenue != null && t.revenue(formatMoney(p.revenue, p.currency)),
|
|
1106
1125
|
]
|
|
1107
1126
|
.filter(Boolean)
|
|
1108
1127
|
.join(" · ")}
|
|
@@ -1111,7 +1130,7 @@ export function CouponCard(p: CouponCardProps) {
|
|
|
1111
1130
|
{p.menu && (
|
|
1112
1131
|
<Popover
|
|
1113
1132
|
width={200}
|
|
1114
|
-
trigger={(s) => <IconButton icon="more" size="sm" label=
|
|
1133
|
+
trigger={(s) => <IconButton icon="more" size="sm" label={t.more} onClick={s.toggle} />}
|
|
1115
1134
|
>
|
|
1116
1135
|
{() => <Menu items={p.menu || []} />}
|
|
1117
1136
|
</Popover>
|
|
@@ -1122,6 +1141,7 @@ export function CouponCard(p: CouponCardProps) {
|
|
|
1122
1141
|
|
|
1123
1142
|
/* ── Locale, workspace and translations ── */
|
|
1124
1143
|
export function LocaleField(p: LocaleFieldProps) {
|
|
1144
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
1125
1145
|
const locs = p.locales || [],
|
|
1126
1146
|
v = useControlled(p.value, p.defaultValue || {}, p.onChange),
|
|
1127
1147
|
cur = React.useState(p.active || (locs[0] && locs[0].code)),
|
|
@@ -1137,7 +1157,7 @@ export function LocaleField(p: LocaleFieldProps) {
|
|
|
1137
1157
|
return (
|
|
1138
1158
|
<FormField id={id} label={p.label} hint={p.hint} error={p.error}>
|
|
1139
1159
|
<div className="tf-locfield">
|
|
1140
|
-
<div className="tf-locfield-tabs" role="tablist" aria-label=
|
|
1160
|
+
<div className="tf-locfield-tabs" role="tablist" aria-label={t.languages}>
|
|
1141
1161
|
{locs.map((l) => {
|
|
1142
1162
|
const has = (v[0][l.code] || "").trim();
|
|
1143
1163
|
return (
|
|
@@ -1153,11 +1173,11 @@ export function LocaleField(p: LocaleFieldProps) {
|
|
|
1153
1173
|
>
|
|
1154
1174
|
{l.flag && <span aria-hidden>{l.flag}</span>}
|
|
1155
1175
|
{l.label || l.code.toUpperCase()}
|
|
1156
|
-
{!has && <span className="tf-locfield-miss" title=
|
|
1176
|
+
{!has && <span className="tf-locfield-miss" title={t.missing} />}
|
|
1157
1177
|
</button>
|
|
1158
1178
|
);
|
|
1159
1179
|
})}
|
|
1160
|
-
<span className="tf-locfield-done">{
|
|
1180
|
+
<span className="tf-locfield-done">{t.doneOf(done, locs.length)}</span>
|
|
1161
1181
|
</div>
|
|
1162
1182
|
{p.multiline ? (
|
|
1163
1183
|
<textarea
|
|
@@ -1196,7 +1216,7 @@ export function LocaleField(p: LocaleFieldProps) {
|
|
|
1196
1216
|
set(v[0][base] || "");
|
|
1197
1217
|
}}
|
|
1198
1218
|
>
|
|
1199
|
-
{
|
|
1219
|
+
{t.copyFrom((locs.find((l) => l.code === base) || {}).label || base)}
|
|
1200
1220
|
</Button>
|
|
1201
1221
|
{p.onTranslate && (
|
|
1202
1222
|
<Button
|
|
@@ -1209,7 +1229,7 @@ export function LocaleField(p: LocaleFieldProps) {
|
|
|
1209
1229
|
});
|
|
1210
1230
|
}}
|
|
1211
1231
|
>
|
|
1212
|
-
{
|
|
1232
|
+
{t.translate}
|
|
1213
1233
|
</Button>
|
|
1214
1234
|
)}
|
|
1215
1235
|
</div>
|
|
@@ -1220,6 +1240,7 @@ export function LocaleField(p: LocaleFieldProps) {
|
|
|
1220
1240
|
}
|
|
1221
1241
|
|
|
1222
1242
|
export function LocaleSwitcher(p: LocaleSwitcherProps) {
|
|
1243
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
1223
1244
|
const lang = useControlled(
|
|
1224
1245
|
p.language,
|
|
1225
1246
|
p.languages && p.languages[0] && p.languages[0].code,
|
|
@@ -1238,7 +1259,7 @@ export function LocaleSwitcher(p: LocaleSwitcherProps) {
|
|
|
1238
1259
|
aria-haspopup="dialog"
|
|
1239
1260
|
aria-expanded={s.open}
|
|
1240
1261
|
onClick={s.toggle}
|
|
1241
|
-
title=
|
|
1262
|
+
title={t.languageAndCurrency}
|
|
1242
1263
|
>
|
|
1243
1264
|
{L0.flag ? <span>{L0.flag}</span> : <Icon name="globe" size={15} />}
|
|
1244
1265
|
<span>{(L0.short || lang[0] || "").toUpperCase()}</span>
|
|
@@ -1267,7 +1288,7 @@ export function LocaleSwitcher(p: LocaleSwitcherProps) {
|
|
|
1267
1288
|
padding: "0 8px",
|
|
1268
1289
|
}}
|
|
1269
1290
|
>
|
|
1270
|
-
{
|
|
1291
|
+
{t.language}
|
|
1271
1292
|
</span>
|
|
1272
1293
|
<div
|
|
1273
1294
|
className="tf-menu"
|
|
@@ -1312,7 +1333,7 @@ export function LocaleSwitcher(p: LocaleSwitcherProps) {
|
|
|
1312
1333
|
padding: "0 8px",
|
|
1313
1334
|
}}
|
|
1314
1335
|
>
|
|
1315
|
-
{
|
|
1336
|
+
{t.currency}
|
|
1316
1337
|
</span>
|
|
1317
1338
|
<Segmented size="sm" block value={cur[0]} onChange={cur[1]} options={p.currencies} />
|
|
1318
1339
|
</div>
|
|
@@ -1417,6 +1438,8 @@ export function WorkspaceSwitcher(p: WorkspaceSwitcherProps) {
|
|
|
1417
1438
|
|
|
1418
1439
|
/* ── Export and 2FA ── */
|
|
1419
1440
|
export function ExportDialog(p: ExportDialogProps) {
|
|
1441
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
1442
|
+
const { formatNumber, formatPercent } = useFormat();
|
|
1420
1443
|
const fmt = React.useState(p.format || (p.formats || ["csv"])[0]),
|
|
1421
1444
|
cols = React.useState((p.columns || []).filter((c) => c.default !== false).map((c) => c.key)),
|
|
1422
1445
|
range = React.useState<DateRange | null>(p.range || null),
|
|
@@ -1446,25 +1469,25 @@ export function ExportDialog(p: ExportDialogProps) {
|
|
|
1446
1469
|
className="tf-modal tf-export"
|
|
1447
1470
|
role="dialog"
|
|
1448
1471
|
aria-modal={p.inline ? undefined : "true"}
|
|
1449
|
-
aria-label={p.title ||
|
|
1472
|
+
aria-label={p.title || t.export}
|
|
1450
1473
|
>
|
|
1451
1474
|
<span className="tf-modal-close">
|
|
1452
|
-
<IconButton icon="x" label=
|
|
1475
|
+
<IconButton icon="x" label={t.close} size="sm" onClick={p.onClose} />
|
|
1453
1476
|
</span>
|
|
1454
1477
|
<div>
|
|
1455
|
-
<h2 className="tf-modal-title">{p.title ||
|
|
1478
|
+
<h2 className="tf-modal-title">{p.title || t.export}</h2>
|
|
1456
1479
|
<p
|
|
1457
1480
|
className="tf-modal-text"
|
|
1458
1481
|
style={{
|
|
1459
1482
|
marginTop: 6,
|
|
1460
1483
|
}}
|
|
1461
1484
|
>
|
|
1462
|
-
{p.text ||
|
|
1485
|
+
{p.text || t.rowsMatch(formatNumber(p.count || 0), p.count || 0)}
|
|
1463
1486
|
</p>
|
|
1464
1487
|
</div>
|
|
1465
1488
|
{st[0] === "form" ? (
|
|
1466
1489
|
<React.Fragment>
|
|
1467
|
-
<FormField label=
|
|
1490
|
+
<FormField label={t.format}>
|
|
1468
1491
|
<Segmented
|
|
1469
1492
|
block
|
|
1470
1493
|
value={fmt[0]}
|
|
@@ -1477,15 +1500,15 @@ export function ExportDialog(p: ExportDialogProps) {
|
|
|
1477
1500
|
</FormField>
|
|
1478
1501
|
{p.ranges !== false && (
|
|
1479
1502
|
<DatePicker
|
|
1480
|
-
label=
|
|
1503
|
+
label={t.period}
|
|
1481
1504
|
range
|
|
1482
1505
|
value={range[0]}
|
|
1483
1506
|
onChange={(r) => range[1](r as DateRange | null)}
|
|
1484
|
-
placeholder=
|
|
1507
|
+
placeholder={t.allTime}
|
|
1485
1508
|
/>
|
|
1486
1509
|
)}
|
|
1487
1510
|
{(p.columns || []).length > 0 && (
|
|
1488
|
-
<FormField label=
|
|
1511
|
+
<FormField label={t.columns} hint={t.selectedCount(cols[0].length)}>
|
|
1489
1512
|
<div className="tf-export-cols">
|
|
1490
1513
|
{(p.columns || []).map((c) => {
|
|
1491
1514
|
const on = cols[0].indexOf(c.key) > -1;
|
|
@@ -1518,14 +1541,14 @@ export function ExportDialog(p: ExportDialogProps) {
|
|
|
1518
1541
|
)}
|
|
1519
1542
|
<div className="tf-modal-foot">
|
|
1520
1543
|
<Button variant="ghost" onClick={p.onClose}>
|
|
1521
|
-
{
|
|
1544
|
+
{t.cancel}
|
|
1522
1545
|
</Button>
|
|
1523
1546
|
<Button
|
|
1524
1547
|
icon="download"
|
|
1525
1548
|
disabled={!cols[0].length && (p.columns || []).length > 0}
|
|
1526
1549
|
onClick={start}
|
|
1527
1550
|
>
|
|
1528
|
-
{
|
|
1551
|
+
{t.export}
|
|
1529
1552
|
</Button>
|
|
1530
1553
|
</div>
|
|
1531
1554
|
</React.Fragment>
|
|
@@ -1536,14 +1559,14 @@ export function ExportDialog(p: ExportDialogProps) {
|
|
|
1536
1559
|
gap: 8,
|
|
1537
1560
|
}}
|
|
1538
1561
|
>
|
|
1539
|
-
<Progress value={prog[0]} label=
|
|
1562
|
+
<Progress value={prog[0]} label={t.exportProgress} />
|
|
1540
1563
|
<span className="tf-card-desc">
|
|
1541
|
-
{
|
|
1564
|
+
{t.preparingFile + " " + formatPercent(Math.round(prog[0]))}
|
|
1542
1565
|
</span>
|
|
1543
1566
|
</div>
|
|
1544
1567
|
) : (
|
|
1545
1568
|
<React.Fragment>
|
|
1546
|
-
<StatusIndicator status="ok" label={p.doneText ||
|
|
1569
|
+
<StatusIndicator status="ok" label={p.doneText || t.fileReady} />
|
|
1547
1570
|
<div className="tf-modal-foot">
|
|
1548
1571
|
<Button
|
|
1549
1572
|
variant="ghost"
|
|
@@ -1551,11 +1574,11 @@ export function ExportDialog(p: ExportDialogProps) {
|
|
|
1551
1574
|
st[1]("form");
|
|
1552
1575
|
}}
|
|
1553
1576
|
>
|
|
1554
|
-
{
|
|
1577
|
+
{t.newExport}
|
|
1555
1578
|
</Button>
|
|
1556
1579
|
{p.download || (
|
|
1557
1580
|
<Button icon="download" onClick={p.onDownload}>
|
|
1558
|
-
{
|
|
1581
|
+
{t.download}
|
|
1559
1582
|
</Button>
|
|
1560
1583
|
)}
|
|
1561
1584
|
</div>
|
|
@@ -1576,10 +1599,11 @@ export function ExportDialog(p: ExportDialogProps) {
|
|
|
1576
1599
|
}
|
|
1577
1600
|
|
|
1578
1601
|
export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
1602
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
1579
1603
|
const step = React.useState(p.step || 0),
|
|
1580
1604
|
err = React.useState<string | null>(null),
|
|
1581
1605
|
saved = React.useState(false);
|
|
1582
|
-
const steps =
|
|
1606
|
+
const steps = t.twoFactorSteps;
|
|
1583
1607
|
return (
|
|
1584
1608
|
<div className="tf-2fa">
|
|
1585
1609
|
<ol className="tf-2fa-steps">
|
|
@@ -1592,7 +1616,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1592
1616
|
</ol>
|
|
1593
1617
|
{step[0] === 0 && (
|
|
1594
1618
|
<div className="tf-2fa-scan">
|
|
1595
|
-
<div className="tf-2fa-qr" role="img" aria-label=
|
|
1619
|
+
<div className="tf-2fa-qr" role="img" aria-label={t.qrCode}>
|
|
1596
1620
|
{p.qr || <Icon name="grid" size={48} />}
|
|
1597
1621
|
</div>
|
|
1598
1622
|
<div
|
|
@@ -1608,8 +1632,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1608
1632
|
margin: 0,
|
|
1609
1633
|
}}
|
|
1610
1634
|
>
|
|
1611
|
-
{p.scanText ||
|
|
1612
|
-
"Scan the code with your authenticator app. If you can't scan it, enter the key by hand."}
|
|
1635
|
+
{p.scanText || t.scanText}
|
|
1613
1636
|
</p>
|
|
1614
1637
|
<button
|
|
1615
1638
|
type="button"
|
|
@@ -1620,7 +1643,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1620
1643
|
} catch {
|
|
1621
1644
|
/* clipboard unavailable */
|
|
1622
1645
|
}
|
|
1623
|
-
toast && toast({ text:
|
|
1646
|
+
toast && toast({ text: t.copied });
|
|
1624
1647
|
}}
|
|
1625
1648
|
>
|
|
1626
1649
|
<span className="tf-mono">{(p.secret || "").replace(/(.{4})/g, "$1 ").trim()}</span>
|
|
@@ -1632,7 +1655,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1632
1655
|
step[1](1);
|
|
1633
1656
|
}}
|
|
1634
1657
|
>
|
|
1635
|
-
{
|
|
1658
|
+
{t.next}
|
|
1636
1659
|
</Button>
|
|
1637
1660
|
</div>
|
|
1638
1661
|
</div>
|
|
@@ -1651,7 +1674,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1651
1674
|
margin: 0,
|
|
1652
1675
|
}}
|
|
1653
1676
|
>
|
|
1654
|
-
{
|
|
1677
|
+
{t.enterCode}
|
|
1655
1678
|
</p>
|
|
1656
1679
|
<OtpInput
|
|
1657
1680
|
error={!!err[0]}
|
|
@@ -1660,7 +1683,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1660
1683
|
if (ok) {
|
|
1661
1684
|
err[1](null);
|
|
1662
1685
|
step[1](2);
|
|
1663
|
-
} else err[1](
|
|
1686
|
+
} else err[1](t.codeFailed);
|
|
1664
1687
|
});
|
|
1665
1688
|
}}
|
|
1666
1689
|
/>
|
|
@@ -1672,7 +1695,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1672
1695
|
step[1](0);
|
|
1673
1696
|
}}
|
|
1674
1697
|
>
|
|
1675
|
-
{
|
|
1698
|
+
{t.back}
|
|
1676
1699
|
</Button>
|
|
1677
1700
|
</div>
|
|
1678
1701
|
</div>
|
|
@@ -1690,8 +1713,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1690
1713
|
margin: 0,
|
|
1691
1714
|
}}
|
|
1692
1715
|
>
|
|
1693
|
-
{p.codesText ||
|
|
1694
|
-
"Each code works once if you lose your phone. Keep them somewhere safe."}
|
|
1716
|
+
{p.codesText || t.codesText}
|
|
1695
1717
|
</p>
|
|
1696
1718
|
<div className="tf-2fa-codes">
|
|
1697
1719
|
{(p.codes || []).map((c) => (
|
|
@@ -1708,14 +1730,14 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1708
1730
|
} catch {
|
|
1709
1731
|
/* clipboard unavailable */
|
|
1710
1732
|
}
|
|
1711
|
-
toast && toast({ text:
|
|
1733
|
+
toast && toast({ text: t.copied });
|
|
1712
1734
|
}}
|
|
1713
1735
|
>
|
|
1714
|
-
{
|
|
1736
|
+
{t.copy}
|
|
1715
1737
|
</Button>
|
|
1716
1738
|
{p.onDownload && (
|
|
1717
1739
|
<Button variant="secondary" icon="download" onClick={p.onDownload}>
|
|
1718
|
-
{
|
|
1740
|
+
{t.download}
|
|
1719
1741
|
</Button>
|
|
1720
1742
|
)}
|
|
1721
1743
|
</div>
|
|
@@ -1725,11 +1747,11 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1725
1747
|
saved[1](!saved[0]);
|
|
1726
1748
|
}}
|
|
1727
1749
|
>
|
|
1728
|
-
{
|
|
1750
|
+
{t.savedCodes}
|
|
1729
1751
|
</Checkbox>
|
|
1730
1752
|
<div>
|
|
1731
1753
|
<Button icon="check" disabled={!saved[0]} onClick={p.onDone}>
|
|
1732
|
-
{
|
|
1754
|
+
{t.turnOnTwoFactor}
|
|
1733
1755
|
</Button>
|
|
1734
1756
|
</div>
|
|
1735
1757
|
</div>
|
|
@@ -1741,6 +1763,8 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
|
|
|
1741
1763
|
/* ── Media ── */
|
|
1742
1764
|
|
|
1743
1765
|
export function ColorField(p: ColorFieldProps) {
|
|
1766
|
+
const t = useStrings(ADMIN_STRINGS);
|
|
1767
|
+
const { formatNumber } = useFormat();
|
|
1744
1768
|
const id = useId("cf"),
|
|
1745
1769
|
v = useControlled(p.value, p.defaultValue || "#000000", p.onChange);
|
|
1746
1770
|
function lum(hx: string) {
|
|
@@ -1771,12 +1795,12 @@ export function ColorField(p: ColorFieldProps) {
|
|
|
1771
1795
|
onChange={(e) => {
|
|
1772
1796
|
v[1](e.target.value);
|
|
1773
1797
|
}}
|
|
1774
|
-
aria-label=
|
|
1798
|
+
aria-label={t.pickColour}
|
|
1775
1799
|
/>
|
|
1776
1800
|
</label>
|
|
1777
1801
|
<input
|
|
1778
1802
|
id={id}
|
|
1779
|
-
aria-label={p.label ? undefined :
|
|
1803
|
+
aria-label={p.label ? undefined : t.pickColour}
|
|
1780
1804
|
className="tf-input tf-mono"
|
|
1781
1805
|
value={v[0]}
|
|
1782
1806
|
onChange={(e) => {
|
|
@@ -1787,13 +1811,13 @@ export function ColorField(p: ColorFieldProps) {
|
|
|
1787
1811
|
{ratio != null && p.against !== false && (
|
|
1788
1812
|
<span
|
|
1789
1813
|
className={cx("tf-colorf-ratio", ratio >= 4.5 ? "ok" : ratio >= 3 ? "warn" : "bad")}
|
|
1790
|
-
title={
|
|
1814
|
+
title={t.contrastAgainst(p.against || "#ffffff")}
|
|
1791
1815
|
>
|
|
1792
1816
|
{formatNumber(ratio, {
|
|
1793
1817
|
maximumFractionDigits: 1,
|
|
1794
1818
|
}) +
|
|
1795
1819
|
":1 " +
|
|
1796
|
-
(ratio >= 4.5 ?
|
|
1820
|
+
(ratio >= 4.5 ? t.contrastAA : ratio >= 3 ? t.contrastAALarge : t.contrastFails)}
|
|
1797
1821
|
</span>
|
|
1798
1822
|
)}
|
|
1799
1823
|
</div>
|