@twentyfirstmedia/ui-core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/admin.d.ts.map +1 -1
- package/dist/components/admin.js +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/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/media.tsx
CHANGED
|
@@ -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=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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 ?
|
|
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=
|
|
198
|
+
<div className="tf-lightbox" role="dialog" aria-modal="true" aria-label={t.images}>
|
|
188
199
|
<IconButton
|
|
189
200
|
icon="x"
|
|
190
|
-
label=
|
|
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=
|
|
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=
|
|
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
|
|
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">{
|
|
274
|
-
<span className="tf-ba-tag right">{
|
|
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=
|
|
308
|
+
aria-label={p.label || t.compareBeforeAfter}
|
|
286
309
|
aria-valuemin={0}
|
|
287
310
|
aria-valuemax={100}
|
|
288
|
-
aria-valuenow={
|
|
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
|
-
|
|
291
|
-
|
|
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=
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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=
|
|
143
|
+
<nav className="tf-pager" aria-label={t.pagination}>
|
|
134
144
|
<IconButton
|
|
135
145
|
icon="chevronLeft"
|
|
136
|
-
label=
|
|
146
|
+
label={t.previousPage}
|
|
137
147
|
size="sm"
|
|
138
148
|
onClick={() => {
|
|
139
|
-
|
|
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
|
-
|
|
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=
|
|
173
|
+
label={t.nextPage}
|
|
164
174
|
size="sm"
|
|
165
175
|
onClick={() => {
|
|
166
|
-
|
|
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 ||
|
|
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 ||
|
|
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] ?
|
|
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 {
|
|
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
|
-
{
|
|
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 ||
|
|
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=
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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=
|
|
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>{
|
|
245
|
-
<b className="tf-mono">{d.days == null ? "—" :
|
|
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>{
|
|
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:
|
|
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:
|
|
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:
|
|
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" /> {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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">{
|
|
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 ||
|
|
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 ||
|
|
520
|
+
{p.confirmLabel || t.bookIt}
|
|
513
521
|
</Button>
|
|
514
522
|
)}
|
|
515
523
|
</div>
|