@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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { leaving, usePresence } from "../presence.js";
|
|
5
|
-
import {
|
|
5
|
+
import { useFormat, useStrings } from "../context.js";
|
|
6
|
+
import { WORKSPACE_STRINGS } from "../strings.js";
|
|
6
7
|
import { Icon } from "../Icon.js";
|
|
7
8
|
import { useScrollFocus, useTrap } from "../hooks.js";
|
|
8
9
|
import { cx } from "../cx.js";
|
|
@@ -28,8 +29,9 @@ import type {
|
|
|
28
29
|
WizardProps,
|
|
29
30
|
} from "./types.js";
|
|
30
31
|
export function SubNav(p: SubNavProps) {
|
|
32
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
31
33
|
return (
|
|
32
|
-
<nav className="tf-subnav" aria-label={
|
|
34
|
+
<nav className="tf-subnav" aria-label={t.sectionPages(p.eyebrow)}>
|
|
33
35
|
<div className="tf-subnav-head">
|
|
34
36
|
{p.eyebrow && <span className="tf-subnav-eyebrow">{p.eyebrow}</span>}
|
|
35
37
|
{p.action && (
|
|
@@ -65,6 +67,7 @@ export function SubNav(p: SubNavProps) {
|
|
|
65
67
|
);
|
|
66
68
|
}
|
|
67
69
|
export function Drawer(p: DrawerProps) {
|
|
70
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
68
71
|
const ref = React.useRef<HTMLElement>(null);
|
|
69
72
|
const pr = usePresence(p.open ?? true);
|
|
70
73
|
useTrap(ref, p.onClose);
|
|
@@ -114,7 +117,7 @@ export function Drawer(p: DrawerProps) {
|
|
|
114
117
|
{p.subtitle && <div className="tf-panel-sub">{p.subtitle}</div>}
|
|
115
118
|
</div>
|
|
116
119
|
</div>
|
|
117
|
-
<IconButton icon="x" label=
|
|
120
|
+
<IconButton icon="x" label={t.close} onClick={p.onClose} />
|
|
118
121
|
</div>
|
|
119
122
|
{p.children}
|
|
120
123
|
</aside>
|
|
@@ -122,6 +125,7 @@ export function Drawer(p: DrawerProps) {
|
|
|
122
125
|
);
|
|
123
126
|
}
|
|
124
127
|
export function CommandPalette(p: CommandPaletteProps) {
|
|
128
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
125
129
|
const q = React.useState(""),
|
|
126
130
|
idx = React.useState(0),
|
|
127
131
|
ref = React.useRef<HTMLDivElement>(null),
|
|
@@ -165,7 +169,7 @@ export function CommandPalette(p: CommandPaletteProps) {
|
|
|
165
169
|
items: [PaletteItem, number][];
|
|
166
170
|
}[] = [];
|
|
167
171
|
res.forEach((it, i) => {
|
|
168
|
-
const g = it.group ||
|
|
172
|
+
const g = it.group || t.results;
|
|
169
173
|
if (!groups.length || groups[groups.length - 1].g !== g)
|
|
170
174
|
groups.push({
|
|
171
175
|
g: g,
|
|
@@ -187,13 +191,13 @@ export function CommandPalette(p: CommandPaletteProps) {
|
|
|
187
191
|
className="tf-palette"
|
|
188
192
|
role="dialog"
|
|
189
193
|
aria-modal="true"
|
|
190
|
-
aria-label=
|
|
194
|
+
aria-label={t.commandPalette}
|
|
191
195
|
>
|
|
192
196
|
<div className="tf-palette-input">
|
|
193
197
|
<Icon name="search" size={18} />
|
|
194
198
|
<input
|
|
195
|
-
placeholder={p.placeholder ||
|
|
196
|
-
aria-label=
|
|
199
|
+
placeholder={p.placeholder || t.paletteSearchPlaceholder}
|
|
200
|
+
aria-label={t.search}
|
|
197
201
|
value={q[0]}
|
|
198
202
|
onChange={(e) => {
|
|
199
203
|
q[1](e.target.value);
|
|
@@ -202,12 +206,12 @@ export function CommandPalette(p: CommandPaletteProps) {
|
|
|
202
206
|
}}
|
|
203
207
|
onKeyDown={key}
|
|
204
208
|
/>
|
|
205
|
-
<kbd>{
|
|
209
|
+
<kbd>{t.escKey}</kbd>
|
|
206
210
|
</div>
|
|
207
|
-
<div className="tf-palette-list" role="listbox" aria-label=
|
|
211
|
+
<div className="tf-palette-list" role="listbox" aria-label={t.results}>
|
|
208
212
|
{p.loading && (
|
|
209
213
|
<div className="tf-palette-empty" role="status">
|
|
210
|
-
<Spinner />
|
|
214
|
+
<Spinner /> {t.searching}
|
|
211
215
|
</div>
|
|
212
216
|
)}
|
|
213
217
|
{res.length ? (
|
|
@@ -243,21 +247,21 @@ export function CommandPalette(p: CommandPaletteProps) {
|
|
|
243
247
|
</div>
|
|
244
248
|
))
|
|
245
249
|
) : p.loading ? null : (
|
|
246
|
-
<div className="tf-palette-empty">{p.empty ??
|
|
250
|
+
<div className="tf-palette-empty">{p.empty ?? t.nothingMatches(q[0])}</div>
|
|
247
251
|
)}
|
|
248
252
|
</div>
|
|
249
253
|
<div className="tf-palette-foot">
|
|
250
254
|
<span>
|
|
251
255
|
<kbd>↑</kbd>
|
|
252
|
-
<kbd>↓</kbd> {
|
|
256
|
+
<kbd>↓</kbd> {t.paletteMove}
|
|
253
257
|
</span>
|
|
254
258
|
<span>
|
|
255
|
-
<kbd>↵</kbd> {
|
|
259
|
+
<kbd>↵</kbd> {t.paletteOpen}
|
|
256
260
|
</span>
|
|
257
261
|
<span>
|
|
258
262
|
<kbd>⌘</kbd>
|
|
259
263
|
<kbd>K</kbd>
|
|
260
|
-
{"
|
|
264
|
+
{" " + t.paletteAnywhere}
|
|
261
265
|
</span>
|
|
262
266
|
</div>
|
|
263
267
|
</div>
|
|
@@ -265,10 +269,11 @@ export function CommandPalette(p: CommandPaletteProps) {
|
|
|
265
269
|
);
|
|
266
270
|
}
|
|
267
271
|
export function NotificationList(p: NotificationListProps) {
|
|
268
|
-
const
|
|
269
|
-
|
|
272
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
273
|
+
const f = React.useState<"all" | "unread">("all"),
|
|
274
|
+
items = (p.items || []).filter((n) => f[0] === "all" || n.unread);
|
|
270
275
|
return (
|
|
271
|
-
<div className="tf-notes" role="region" aria-label=
|
|
276
|
+
<div className="tf-notes" role="region" aria-label={t.notifications}>
|
|
272
277
|
<div
|
|
273
278
|
className="tf-header"
|
|
274
279
|
style={{
|
|
@@ -281,7 +286,7 @@ export function NotificationList(p: NotificationListProps) {
|
|
|
281
286
|
fontSize: 16,
|
|
282
287
|
}}
|
|
283
288
|
>
|
|
284
|
-
{
|
|
289
|
+
{t.notifications}
|
|
285
290
|
</b>
|
|
286
291
|
<Button
|
|
287
292
|
variant="ghost"
|
|
@@ -290,7 +295,7 @@ export function NotificationList(p: NotificationListProps) {
|
|
|
290
295
|
disabled={!(p.items || []).some((n) => n.unread)}
|
|
291
296
|
onClick={p.onReadAll}
|
|
292
297
|
>
|
|
293
|
-
{
|
|
298
|
+
{t.markAllRead}
|
|
294
299
|
</Button>
|
|
295
300
|
</div>
|
|
296
301
|
<div
|
|
@@ -298,7 +303,12 @@ export function NotificationList(p: NotificationListProps) {
|
|
|
298
303
|
padding: "0 8px",
|
|
299
304
|
}}
|
|
300
305
|
>
|
|
301
|
-
<Tabs
|
|
306
|
+
<Tabs
|
|
307
|
+
block
|
|
308
|
+
items={[t.all, t.unread]}
|
|
309
|
+
value={f[0] === "all" ? t.all : t.unread}
|
|
310
|
+
onChange={(v) => f[1](v === t.unread ? "unread" : "all")}
|
|
311
|
+
/>
|
|
302
312
|
</div>
|
|
303
313
|
<div className="tf-notes-list">
|
|
304
314
|
{items.length ? (
|
|
@@ -325,21 +335,22 @@ export function NotificationList(p: NotificationListProps) {
|
|
|
325
335
|
) : (
|
|
326
336
|
<div className="tf-notes-empty">
|
|
327
337
|
<TintIcon icon="check" tint="mint" size={40} />
|
|
328
|
-
<b>{
|
|
338
|
+
<b>{t.allCaughtUp}</b>
|
|
329
339
|
</div>
|
|
330
340
|
)}
|
|
331
341
|
</div>
|
|
332
342
|
</div>
|
|
333
343
|
);
|
|
334
344
|
}
|
|
335
|
-
const STEP = (): Record<FlowStep["kind"], [IconName, Tint
|
|
336
|
-
trigger: ["zap", "sky"
|
|
337
|
-
condition: ["filter", "blush"
|
|
338
|
-
agent: ["bot", "lilac"
|
|
339
|
-
approval: ["user", "butter"
|
|
340
|
-
action: ["send", "mint"
|
|
345
|
+
const STEP = (): Record<FlowStep["kind"], [IconName, Tint]> => ({
|
|
346
|
+
trigger: ["zap", "sky"],
|
|
347
|
+
condition: ["filter", "blush"],
|
|
348
|
+
agent: ["bot", "lilac"],
|
|
349
|
+
approval: ["user", "butter"],
|
|
350
|
+
action: ["send", "mint"],
|
|
341
351
|
});
|
|
342
352
|
export function Flow(p: FlowProps) {
|
|
353
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
343
354
|
return (
|
|
344
355
|
<ol
|
|
345
356
|
className="tf-flow tf-stagger"
|
|
@@ -351,7 +362,8 @@ export function Flow(p: FlowProps) {
|
|
|
351
362
|
>
|
|
352
363
|
{(p.steps || []).map((s, i) => {
|
|
353
364
|
const steps = STEP(),
|
|
354
|
-
|
|
365
|
+
kind = steps[s.kind] ? s.kind : "action",
|
|
366
|
+
k = steps[kind],
|
|
355
367
|
st =
|
|
356
368
|
i < (p.active == null ? -1 : p.active)
|
|
357
369
|
? "done"
|
|
@@ -367,9 +379,9 @@ export function Flow(p: FlowProps) {
|
|
|
367
379
|
</span>
|
|
368
380
|
<div className="tf-flow-card">
|
|
369
381
|
<div className="tf-flow-kind">
|
|
370
|
-
{
|
|
371
|
-
{st === "active" && <span className="tf-flow-live">
|
|
372
|
-
{st === "wait" && <span className="tf-flow-live wait">{
|
|
382
|
+
{t.flowStep[kind]}
|
|
383
|
+
{st === "active" && <span className="tf-flow-live">{t.flowRunning}</span>}
|
|
384
|
+
{st === "wait" && <span className="tf-flow-live wait">{t.waitingForYou}</span>}
|
|
373
385
|
</div>
|
|
374
386
|
<div className="tf-flow-text">{s.text}</div>
|
|
375
387
|
{st === "wait" && p.onApprove && (
|
|
@@ -380,11 +392,11 @@ export function Flow(p: FlowProps) {
|
|
|
380
392
|
}}
|
|
381
393
|
>
|
|
382
394
|
<Button size="sm" icon="check" onClick={p.onApprove}>
|
|
383
|
-
{
|
|
395
|
+
{t.approve}
|
|
384
396
|
</Button>
|
|
385
397
|
{p.onReject && (
|
|
386
398
|
<Button size="sm" variant="ghost" onClick={p.onReject}>
|
|
387
|
-
{
|
|
399
|
+
{t.reject}
|
|
388
400
|
</Button>
|
|
389
401
|
)}
|
|
390
402
|
</div>
|
|
@@ -474,6 +486,7 @@ export function FileThumb(p: FileThumbProps) {
|
|
|
474
486
|
);
|
|
475
487
|
}
|
|
476
488
|
export function FileCard(p: FileCardProps) {
|
|
489
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
477
490
|
const k = FILE_KIND[p.kind] || FILE_KIND.doc;
|
|
478
491
|
return (
|
|
479
492
|
<button type="button" className="tf-filecard" onClick={p.onClick}>
|
|
@@ -483,17 +496,18 @@ export function FileCard(p: FileCardProps) {
|
|
|
483
496
|
<span className="tf-filecard-name">{p.name}</span>
|
|
484
497
|
<span className="tf-filecard-meta">
|
|
485
498
|
{[p.size, p.meta].filter(Boolean).join(" · ")}
|
|
486
|
-
{p.shared && <Icon name="users" size={12} label=
|
|
499
|
+
{p.shared && <Icon name="users" size={12} label={t.shared} />}
|
|
487
500
|
</span>
|
|
488
501
|
</button>
|
|
489
502
|
);
|
|
490
503
|
}
|
|
491
504
|
export function Wizard(p: WizardProps) {
|
|
505
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
492
506
|
const steps = p.steps || [],
|
|
493
507
|
cur = p.current || 0,
|
|
494
508
|
done = p.done || {};
|
|
495
509
|
return (
|
|
496
|
-
<div className="tf-wizard" role="group" aria-label={p.label ||
|
|
510
|
+
<div className="tf-wizard" role="group" aria-label={p.label || t.setup}>
|
|
497
511
|
<aside className="tf-wizard-rail">
|
|
498
512
|
{p.brand && (
|
|
499
513
|
<span
|
|
@@ -528,7 +542,7 @@ export function Wizard(p: WizardProps) {
|
|
|
528
542
|
</ol>
|
|
529
543
|
</aside>
|
|
530
544
|
<div className="tf-wizard-main">
|
|
531
|
-
<div className="tf-card-desc">{
|
|
545
|
+
<div className="tf-card-desc">{t.stepOf(cur + 1, steps.length)}</div>
|
|
532
546
|
<div key={cur} className="tf-wizard-body">
|
|
533
547
|
{steps[cur] && steps[cur].icon && (
|
|
534
548
|
<TintIcon icon={steps[cur].icon} tint="sky" size={44} />
|
|
@@ -550,8 +564,9 @@ export function Wizard(p: WizardProps) {
|
|
|
550
564
|
</div>
|
|
551
565
|
);
|
|
552
566
|
}
|
|
553
|
-
const LEVELS = () => ["None", "View", "Edit", "Full"];
|
|
554
567
|
export function PermissionMatrix(p: PermissionMatrixProps) {
|
|
568
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
569
|
+
const LEVELS = () => t.permissionLevels;
|
|
555
570
|
const box = React.useRef<HTMLDivElement>(null);
|
|
556
571
|
useScrollFocus(box);
|
|
557
572
|
return (
|
|
@@ -559,7 +574,7 @@ export function PermissionMatrix(p: PermissionMatrixProps) {
|
|
|
559
574
|
<table className="tf-table tf-perm">
|
|
560
575
|
<thead>
|
|
561
576
|
<tr>
|
|
562
|
-
<th>{
|
|
577
|
+
<th>{t.role}</th>
|
|
563
578
|
{(p.areas || []).map((a) => (
|
|
564
579
|
<th key={a}>{a}</th>
|
|
565
580
|
))}
|
|
@@ -583,11 +598,7 @@ export function PermissionMatrix(p: PermissionMatrixProps) {
|
|
|
583
598
|
>
|
|
584
599
|
{r.name}
|
|
585
600
|
</div>
|
|
586
|
-
{r.people != null && (
|
|
587
|
-
<div className="tf-card-desc">
|
|
588
|
-
{r.people === 1 ? "1 member" : `${r.people} members`}
|
|
589
|
-
</div>
|
|
590
|
-
)}
|
|
601
|
+
{r.people != null && <div className="tf-card-desc">{t.memberCount(r.people)}</div>}
|
|
591
602
|
</td>
|
|
592
603
|
{r.levels.map((lv, ai) => (
|
|
593
604
|
<td key={ai}>
|
|
@@ -611,12 +622,18 @@ export function PermissionMatrix(p: PermissionMatrixProps) {
|
|
|
611
622
|
</div>
|
|
612
623
|
);
|
|
613
624
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
625
|
+
/** Money with cents, in the locale of the nearest UIProvider. */
|
|
626
|
+
function useMoney2() {
|
|
627
|
+
const { formatMoney } = useFormat();
|
|
628
|
+
return (n: number) =>
|
|
629
|
+
formatMoney(n, null, {
|
|
630
|
+
minimumFractionDigits: 2,
|
|
631
|
+
});
|
|
618
632
|
}
|
|
619
633
|
export function InvoicePreview(p: InvoicePreviewProps) {
|
|
634
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
635
|
+
const money2 = useMoney2();
|
|
636
|
+
const { formatPercent } = useFormat();
|
|
620
637
|
const lines = p.lines || [];
|
|
621
638
|
let net = 0;
|
|
622
639
|
const vat: Record<number, number> = {};
|
|
@@ -649,47 +666,47 @@ export function InvoicePreview(p: InvoicePreviewProps) {
|
|
|
649
666
|
{p.from && <div className="tf-paper-small">{p.from}</div>}
|
|
650
667
|
</div>
|
|
651
668
|
<div className="tf-paper-id">
|
|
652
|
-
<div className="tf-paper-kind">{p.kind ||
|
|
669
|
+
<div className="tf-paper-kind">{p.kind || t.invoice}</div>
|
|
653
670
|
<div className="tf-mono">{"#" + p.number}</div>
|
|
654
671
|
</div>
|
|
655
672
|
</div>
|
|
656
673
|
<div className="tf-paper-meta">
|
|
657
674
|
<div>
|
|
658
|
-
<span>{
|
|
675
|
+
<span>{t.billTo}</span>
|
|
659
676
|
<b>{p.to}</b>
|
|
660
677
|
</div>
|
|
661
678
|
<div>
|
|
662
|
-
<span>{
|
|
679
|
+
<span>{t.issued}</span>
|
|
663
680
|
<b>{p.issued}</b>
|
|
664
681
|
</div>
|
|
665
682
|
<div>
|
|
666
|
-
<span>{
|
|
683
|
+
<span>{t.due}</span>
|
|
667
684
|
<b>{p.due}</b>
|
|
668
685
|
</div>
|
|
669
686
|
</div>
|
|
670
687
|
<div className="tf-paper-lines">
|
|
671
688
|
{lines.map((l, i) => (
|
|
672
689
|
<div key={i}>
|
|
673
|
-
<span>{(l.d ||
|
|
690
|
+
<span>{(l.d || t.untitledLine) + (l.q !== 1 ? " × " + l.q : "")}</span>
|
|
674
691
|
<b>{fmt(l.q * l.u)}</b>
|
|
675
692
|
</div>
|
|
676
693
|
))}
|
|
677
694
|
</div>
|
|
678
695
|
<div className="tf-paper-totals">
|
|
679
696
|
<div>
|
|
680
|
-
<span>{
|
|
697
|
+
<span>{t.net}</span>
|
|
681
698
|
<b>{fmt(net)}</b>
|
|
682
699
|
</div>
|
|
683
700
|
{Object.keys(vat)
|
|
684
701
|
.filter((k) => +k > 0)
|
|
685
702
|
.map((k) => (
|
|
686
703
|
<div key={k}>
|
|
687
|
-
<span>{
|
|
704
|
+
<span>{t.vatAt(formatPercent(Number(k), { maximumFractionDigits: 2 }))}</span>
|
|
688
705
|
<b>{fmt(vat[Number(k)] ?? 0)}</b>
|
|
689
706
|
</div>
|
|
690
707
|
))}
|
|
691
708
|
<div className="grand">
|
|
692
|
-
<span>{
|
|
709
|
+
<span>{t.totalDue}</span>
|
|
693
710
|
<b>{fmt(net + tax)}</b>
|
|
694
711
|
</div>
|
|
695
712
|
</div>
|
|
@@ -698,6 +715,8 @@ export function InvoicePreview(p: InvoicePreviewProps) {
|
|
|
698
715
|
);
|
|
699
716
|
}
|
|
700
717
|
export function TransactionRow(p: TransactionRowProps) {
|
|
718
|
+
const t = useStrings(WORKSPACE_STRINGS);
|
|
719
|
+
const money2 = useMoney2();
|
|
701
720
|
const open = React.useState(false),
|
|
702
721
|
inc = p.amount > 0;
|
|
703
722
|
return (
|
|
@@ -706,13 +725,13 @@ export function TransactionRow(p: TransactionRowProps) {
|
|
|
706
725
|
<TintIcon icon={inc ? "download" : "send"} tint={inc ? "mint" : "plain"} />
|
|
707
726
|
<div className="tf-tx-text">
|
|
708
727
|
<b className="tf-mono">{p.text}</b>
|
|
709
|
-
<span>{p.date + (p.matched && p.matchRef ?
|
|
728
|
+
<span>{p.date + (p.matched && p.matchRef ? " · " + t.matchedTo(p.matchRef) : "")}</span>
|
|
710
729
|
</div>
|
|
711
730
|
<b className={cx("tf-tx-amt", inc ? "in" : "out")}>
|
|
712
731
|
{(inc ? "+" : "−") + money2(Math.abs(p.amount))}
|
|
713
732
|
</b>
|
|
714
733
|
{p.matched ? (
|
|
715
|
-
<Badge tone="success">{
|
|
734
|
+
<Badge tone="success">{t.matched}</Badge>
|
|
716
735
|
) : (
|
|
717
736
|
<Button
|
|
718
737
|
size="sm"
|
|
@@ -721,7 +740,7 @@ export function TransactionRow(p: TransactionRowProps) {
|
|
|
721
740
|
open[1](!open[0]);
|
|
722
741
|
}}
|
|
723
742
|
>
|
|
724
|
-
{open[0] ?
|
|
743
|
+
{open[0] ? t.close : t.match}
|
|
725
744
|
</Button>
|
|
726
745
|
)}
|
|
727
746
|
</div>
|
|
@@ -734,7 +753,7 @@ export function TransactionRow(p: TransactionRowProps) {
|
|
|
734
753
|
minWidth: 0,
|
|
735
754
|
}}
|
|
736
755
|
>
|
|
737
|
-
{p.hint ||
|
|
756
|
+
{p.hint || t.noSuggestion}
|
|
738
757
|
</span>
|
|
739
758
|
<Button
|
|
740
759
|
size="sm"
|
|
@@ -744,7 +763,7 @@ export function TransactionRow(p: TransactionRowProps) {
|
|
|
744
763
|
p.onMatch && p.onMatch();
|
|
745
764
|
}}
|
|
746
765
|
>
|
|
747
|
-
{
|
|
766
|
+
{t.accept}
|
|
748
767
|
</Button>
|
|
749
768
|
</div>
|
|
750
769
|
)}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* The context `UIProvider` fills, and the hooks components read it with. Kept apart from the
|
|
4
|
+
* provider so a component that only needs its strings or formatters does not pull in theming.
|
|
5
|
+
*/
|
|
6
|
+
import { createContext, useContext } from "react";
|
|
7
|
+
import { LOCALE, formatDate, formatMoney, formatNumber, formatPercent } from "./format.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_STRINGS,
|
|
10
|
+
SHARED_STRINGS,
|
|
11
|
+
mergeStrings,
|
|
12
|
+
type UIMessages,
|
|
13
|
+
type UIStrings,
|
|
14
|
+
} from "./strings.js";
|
|
15
|
+
import type { Density, Kit, Motion, Surface, Theme } from "./provider.js";
|
|
16
|
+
|
|
17
|
+
export interface UIContextValue {
|
|
18
|
+
kit: Kit | undefined;
|
|
19
|
+
theme: Theme | undefined;
|
|
20
|
+
density: Density | undefined;
|
|
21
|
+
surface: Surface | undefined;
|
|
22
|
+
motion: Motion | undefined;
|
|
23
|
+
/** Locale for formatted numbers and dates (`"en-US"` unless a provider sets one). */
|
|
24
|
+
locale: string;
|
|
25
|
+
/** Every provider's `messages`, merged (outer first). Components add their defaults. */
|
|
26
|
+
messages: UIMessages;
|
|
27
|
+
/** Element portals render into (null: document.body). */
|
|
28
|
+
portalRoot: HTMLElement | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const UIContext = createContext<UIContextValue>({
|
|
32
|
+
kit: undefined,
|
|
33
|
+
theme: undefined,
|
|
34
|
+
density: undefined,
|
|
35
|
+
surface: undefined,
|
|
36
|
+
motion: undefined,
|
|
37
|
+
locale: LOCALE,
|
|
38
|
+
messages: {},
|
|
39
|
+
portalRoot: null,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Current kit, theme, density, locale, strings and portal target. */
|
|
43
|
+
export function useUI(): UIContextValue {
|
|
44
|
+
return useContext(UIContext);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Resolved strings per (messages, defaults): components get stable objects, and an app without
|
|
48
|
+
// messages shares one object per group.
|
|
49
|
+
const resolved = new WeakMap<object, WeakMap<object, unknown>>();
|
|
50
|
+
function resolve<T extends object>(messages: UIMessages, defaults: T): T {
|
|
51
|
+
let byGroup = resolved.get(messages);
|
|
52
|
+
if (!byGroup) resolved.set(messages, (byGroup = new WeakMap()));
|
|
53
|
+
let out = byGroup.get(defaults) as T | undefined;
|
|
54
|
+
if (!out) {
|
|
55
|
+
out = mergeStrings(defaults as unknown as UIStrings, messages) as unknown as T;
|
|
56
|
+
byGroup.set(defaults, out);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
const NO_GROUP = {};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Strings for a component: the shared strings plus `group` (the component file's defaults), with
|
|
64
|
+
* the `messages` of the surrounding providers applied. Each component file passes only its own
|
|
65
|
+
* group, so a bundle carries just the defaults of the components it uses.
|
|
66
|
+
*/
|
|
67
|
+
export function useStrings<G extends object = Record<never, never>>(
|
|
68
|
+
group?: G,
|
|
69
|
+
): typeof SHARED_STRINGS & G {
|
|
70
|
+
const messages = useContext(UIContext).messages;
|
|
71
|
+
const g = group ?? (NO_GROUP as G);
|
|
72
|
+
let base = withShared.get(g) as (typeof SHARED_STRINGS & G) | undefined;
|
|
73
|
+
if (!base) withShared.set(g, (base = { ...SHARED_STRINGS, ...g }));
|
|
74
|
+
return resolve(messages, base);
|
|
75
|
+
}
|
|
76
|
+
const withShared = new WeakMap<object, object>();
|
|
77
|
+
|
|
78
|
+
/** Every built-in string for this part of the tree (English unless a `UIProvider` sets `messages`). */
|
|
79
|
+
export function useUIStrings(): UIStrings {
|
|
80
|
+
return resolve(useContext(UIContext).messages, DEFAULT_STRINGS);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `formatNumber`, `formatMoney`, `formatPercent` and `formatDate`, bound to a locale. */
|
|
84
|
+
export interface Formatters {
|
|
85
|
+
locale: string;
|
|
86
|
+
formatNumber: (v: number, o?: Intl.NumberFormatOptions) => string;
|
|
87
|
+
formatMoney: (v: number, currency?: string | null, o?: Intl.NumberFormatOptions) => string;
|
|
88
|
+
formatPercent: (v: number, o?: Intl.NumberFormatOptions) => string;
|
|
89
|
+
formatDate: (input: Date | string | number, o?: Intl.DateTimeFormatOptions) => string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const formatters = new Map<string, Formatters>();
|
|
93
|
+
/** The formatters for a locale (cached, so the functions keep their identity). */
|
|
94
|
+
export function formattersFor(locale: string): Formatters {
|
|
95
|
+
let f = formatters.get(locale);
|
|
96
|
+
if (!f) {
|
|
97
|
+
f = {
|
|
98
|
+
locale,
|
|
99
|
+
formatNumber: (v, o) => formatNumber(v, o, locale),
|
|
100
|
+
formatMoney: (v, currency, o) => formatMoney(v, currency, o, locale),
|
|
101
|
+
formatPercent: (v, o) => formatPercent(v, o, locale),
|
|
102
|
+
formatDate: (input, o) => formatDate(input, o, locale),
|
|
103
|
+
};
|
|
104
|
+
formatters.set(locale, f);
|
|
105
|
+
}
|
|
106
|
+
return f;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The format helpers in the locale of the nearest `UIProvider` (`"en-US"` without one). */
|
|
110
|
+
export function useFormat(): Formatters {
|
|
111
|
+
return formattersFor(useContext(UIContext).locale);
|
|
112
|
+
}
|