@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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cx } from "../cx.js";
|
|
4
4
|
import { formatMoney } from "../format.js";
|
|
5
|
+
import { useFormat, useStrings } from "../context.js";
|
|
6
|
+
import { COMMERCE_STRINGS } from "../strings.js";
|
|
5
7
|
import { Icon } from "../Icon.js";
|
|
6
8
|
import { useControlled } from "../hooks.js";
|
|
7
9
|
import { Badge, Button, IconButton } from "./foundation.js";
|
|
@@ -65,12 +67,19 @@ export function ProductArt(p: ProductArtProps) {
|
|
|
65
67
|
|
|
66
68
|
ProductArt.kinds = Object.keys(ART) as ArtKind[];
|
|
67
69
|
|
|
68
|
-
/** Formats an amount
|
|
69
|
-
export function money(v: number | string, cur?: string) {
|
|
70
|
-
return formatMoney(Number(v), cur);
|
|
70
|
+
/** Formats an amount in a currency (default euros) and locale (default en-US). */
|
|
71
|
+
export function money(v: number | string, cur?: string, locale?: string) {
|
|
72
|
+
return formatMoney(Number(v), cur, undefined, locale);
|
|
73
|
+
}
|
|
74
|
+
/** `money` in the locale of the nearest `UIProvider`. */
|
|
75
|
+
function useMoney() {
|
|
76
|
+
const { locale } = useFormat();
|
|
77
|
+
return (v: number | string, cur?: string) => money(v, cur, locale);
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export function Price(p: PriceProps) {
|
|
81
|
+
const money = useMoney();
|
|
82
|
+
const { formatPercent } = useFormat();
|
|
74
83
|
const sale = p.compareAt && p.compareAt > p.value;
|
|
75
84
|
return (
|
|
76
85
|
<span className={cx("tf-price", p.size && "tf-price-" + p.size)}>
|
|
@@ -78,7 +87,7 @@ export function Price(p: PriceProps) {
|
|
|
78
87
|
{sale && <s>{money(p.compareAt ?? 0, p.currency)}</s>}
|
|
79
88
|
{sale && p.showSave !== false && (
|
|
80
89
|
<span className="tf-price-save">
|
|
81
|
-
{"−" + Math.round((1 - p.value / (p.compareAt || p.value)) * 100)
|
|
90
|
+
{"−" + formatPercent(Math.round((1 - p.value / (p.compareAt || p.value)) * 100))}
|
|
82
91
|
</span>
|
|
83
92
|
)}
|
|
84
93
|
</span>
|
|
@@ -86,13 +95,12 @@ export function Price(p: PriceProps) {
|
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
export function Rating(p: RatingProps) {
|
|
89
|
-
const
|
|
98
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
99
|
+
const { formatNumber } = useFormat();
|
|
100
|
+
const v = p.value || 0,
|
|
101
|
+
shown = formatNumber(v, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
|
90
102
|
return (
|
|
91
|
-
<span
|
|
92
|
-
className="tf-rating"
|
|
93
|
-
role="img"
|
|
94
|
-
aria-label={`${v.toFixed(1)} out of 5` + (p.count != null ? `, ${p.count} reviews` : "")}
|
|
95
|
-
>
|
|
103
|
+
<span className="tf-rating" role="img" aria-label={t.ratingLabel(shown, p.count)}>
|
|
96
104
|
<span className="tf-stars" aria-hidden>
|
|
97
105
|
{[0, 1, 2, 3, 4].map((i) => {
|
|
98
106
|
const f = Math.max(0, Math.min(1, v - i));
|
|
@@ -111,25 +119,27 @@ export function Rating(p: RatingProps) {
|
|
|
111
119
|
);
|
|
112
120
|
})}
|
|
113
121
|
</span>
|
|
114
|
-
<b>{
|
|
115
|
-
{p.count != null && <span>{"(" + p.count + ")"}</span>}
|
|
122
|
+
<b>{shown}</b>
|
|
123
|
+
{p.count != null && <span>{"(" + formatNumber(p.count) + ")"}</span>}
|
|
116
124
|
</span>
|
|
117
125
|
);
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
export function StockBadge(p: StockBadgeProps) {
|
|
129
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
121
130
|
const s = p.stock;
|
|
122
131
|
if (s == null) return null;
|
|
123
132
|
return s <= 0 ? (
|
|
124
|
-
<Badge tone="danger">{
|
|
133
|
+
<Badge tone="danger">{t.outOfStock}</Badge>
|
|
125
134
|
) : s <= (p.low || 10) ? (
|
|
126
|
-
<Badge tone="warning">{
|
|
135
|
+
<Badge tone="warning">{t.lowStock(s)}</Badge>
|
|
127
136
|
) : (
|
|
128
|
-
<Badge tone="success">{p.compact ?
|
|
137
|
+
<Badge tone="success">{p.compact ? t.inStock : t.inStockCount(s)}</Badge>
|
|
129
138
|
);
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
export function ProductCard(p: ProductCardProps) {
|
|
142
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
133
143
|
return (
|
|
134
144
|
<div className={cx("tf-product", p.className)} onClick={p.onClick}>
|
|
135
145
|
<div className="tf-product-media">
|
|
@@ -166,11 +176,11 @@ export function ProductCard(p: ProductCardProps) {
|
|
|
166
176
|
<Price value={p.price} compareAt={p.compareAt} />
|
|
167
177
|
{p.onAdd &&
|
|
168
178
|
(p.stock === 0 ? (
|
|
169
|
-
<span className="tf-card-desc">{
|
|
179
|
+
<span className="tf-card-desc">{t.soldOut}</span>
|
|
170
180
|
) : (
|
|
171
181
|
<IconButton
|
|
172
182
|
icon={p.added ? "check" : "plus"}
|
|
173
|
-
label={
|
|
183
|
+
label={t.addToCart(p.name)}
|
|
174
184
|
variant={p.added ? "solid" : "soft"}
|
|
175
185
|
size="sm"
|
|
176
186
|
onClick={(e) => {
|
|
@@ -186,6 +196,7 @@ export function ProductCard(p: ProductCardProps) {
|
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
export function VariantPicker(p: VariantPickerProps) {
|
|
199
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
189
200
|
const opts = (p.options || []).map((o) => (typeof o === "string" ? { value: o, label: o } : o));
|
|
190
201
|
const v = useControlled(p.value, p.defaultValue ?? "", p.onChange),
|
|
191
202
|
color = p.type === "color";
|
|
@@ -209,7 +220,7 @@ export function VariantPicker(p: VariantPickerProps) {
|
|
|
209
220
|
className={cx("tf-variant", on && "on", off && "off")}
|
|
210
221
|
aria-pressed={on}
|
|
211
222
|
disabled={off}
|
|
212
|
-
aria-label={color ? (off ?
|
|
223
|
+
aria-label={color ? (off ? t.variantSoldOut(lab) : lab) : undefined}
|
|
213
224
|
title={color ? lab : undefined}
|
|
214
225
|
onClick={() => {
|
|
215
226
|
v[1](val);
|
|
@@ -234,6 +245,7 @@ export function VariantPicker(p: VariantPickerProps) {
|
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
export function CartLine(p: CartLineProps) {
|
|
248
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
237
249
|
return (
|
|
238
250
|
<div className="tf-cartline">
|
|
239
251
|
<ProductArt kind={p.art} tint={p.tint} size={64} />
|
|
@@ -246,7 +258,7 @@ export function CartLine(p: CartLineProps) {
|
|
|
246
258
|
<div className="tf-number tf-number-sm">
|
|
247
259
|
<IconButton
|
|
248
260
|
icon={p.qty <= 1 ? "trash" : "minus"}
|
|
249
|
-
label={p.qty <= 1 ?
|
|
261
|
+
label={p.qty <= 1 ? t.removeItem(p.name) : t.oneLess}
|
|
250
262
|
size="sm"
|
|
251
263
|
onClick={() => {
|
|
252
264
|
p.qty <= 1 ? p.onRemove && p.onRemove() : p.onQty?.(p.qty - 1);
|
|
@@ -257,7 +269,7 @@ export function CartLine(p: CartLineProps) {
|
|
|
257
269
|
</span>
|
|
258
270
|
<IconButton
|
|
259
271
|
icon="plus"
|
|
260
|
-
label=
|
|
272
|
+
label={t.oneMore}
|
|
261
273
|
size="sm"
|
|
262
274
|
onClick={() => {
|
|
263
275
|
p.onQty?.(p.qty + 1);
|
|
@@ -273,6 +285,9 @@ export function CartLine(p: CartLineProps) {
|
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
export function OrderSummary(p: OrderSummaryProps) {
|
|
288
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
289
|
+
const money = useMoney();
|
|
290
|
+
const { formatPercent } = useFormat();
|
|
276
291
|
const lines = p.lines || [],
|
|
277
292
|
sub = lines.reduce((a, l) => a + l.price * l.qty, 0),
|
|
278
293
|
disc = p.discount
|
|
@@ -288,7 +303,7 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
288
303
|
toFree = (p.freeShippingFrom || 75) - (sub - disc);
|
|
289
304
|
return (
|
|
290
305
|
<div className="tf-summary">
|
|
291
|
-
{p.title !== false && <b className="tf-summary-title">{p.title ||
|
|
306
|
+
{p.title !== false && <b className="tf-summary-title">{p.title || t.orderSummary}</b>}
|
|
292
307
|
{p.showLines && (
|
|
293
308
|
<div
|
|
294
309
|
className="tf-col"
|
|
@@ -313,9 +328,7 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
313
328
|
)}
|
|
314
329
|
{toFree > 0 && p.freeShippingFrom !== 0 && (
|
|
315
330
|
<div className="tf-freeship">
|
|
316
|
-
<span>
|
|
317
|
-
Add <b>{money(Math.ceil(toFree * 100) / 100)}</b> for free shipping
|
|
318
|
-
</span>
|
|
331
|
+
<span>{t.freeShippingIn(<b>{money(Math.ceil(toFree * 100) / 100)}</b>)}</span>
|
|
319
332
|
<span className="tf-usage-track">
|
|
320
333
|
<span
|
|
321
334
|
className="tf-usage-fill tf-sf-mint"
|
|
@@ -339,7 +352,7 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
339
352
|
p.onCoupon?.(null);
|
|
340
353
|
}}
|
|
341
354
|
>
|
|
342
|
-
{
|
|
355
|
+
{t.remove}
|
|
343
356
|
</button>
|
|
344
357
|
</div>
|
|
345
358
|
) : (
|
|
@@ -348,14 +361,14 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
348
361
|
onSubmit={(e) => {
|
|
349
362
|
e.preventDefault();
|
|
350
363
|
const r = p.onCoupon?.(code[0].trim().toUpperCase());
|
|
351
|
-
err[1](r === false ?
|
|
364
|
+
err[1](r === false ? t.invalidCode : null);
|
|
352
365
|
if (r !== false) code[1]("");
|
|
353
366
|
}}
|
|
354
367
|
>
|
|
355
368
|
<input
|
|
356
369
|
className="tf-input"
|
|
357
|
-
placeholder=
|
|
358
|
-
aria-label=
|
|
370
|
+
placeholder={t.discountCode}
|
|
371
|
+
aria-label={t.discountCode}
|
|
359
372
|
value={code[0]}
|
|
360
373
|
onChange={(e) => {
|
|
361
374
|
code[1](e.target.value);
|
|
@@ -364,7 +377,7 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
364
377
|
aria-invalid={err[0] ? "true" : undefined}
|
|
365
378
|
/>
|
|
366
379
|
<Button type="submit" variant="soft" disabled={!code[0].trim()}>
|
|
367
|
-
{
|
|
380
|
+
{t.apply}
|
|
368
381
|
</Button>
|
|
369
382
|
{err[0] && (
|
|
370
383
|
<span
|
|
@@ -380,25 +393,25 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
380
393
|
))}
|
|
381
394
|
<dl className="tf-summary-rows">
|
|
382
395
|
<div>
|
|
383
|
-
<dt>{
|
|
396
|
+
<dt>{t.subtotal}</dt>
|
|
384
397
|
<dd>{money(sub)}</dd>
|
|
385
398
|
</div>
|
|
386
399
|
{disc > 0 && (
|
|
387
400
|
<div className="save">
|
|
388
|
-
<dt>{
|
|
401
|
+
<dt>{t.discount}</dt>
|
|
389
402
|
<dd>{"−" + money(disc)}</dd>
|
|
390
403
|
</div>
|
|
391
404
|
)}
|
|
392
405
|
<div>
|
|
393
|
-
<dt>{
|
|
394
|
-
<dd>{ship === 0 ?
|
|
406
|
+
<dt>{t.shipping}</dt>
|
|
407
|
+
<dd>{ship === 0 ? t.free : money(ship)}</dd>
|
|
395
408
|
</div>
|
|
396
409
|
<div className="total">
|
|
397
|
-
<dt>{
|
|
410
|
+
<dt>{t.total}</dt>
|
|
398
411
|
<dd>{money(total)}</dd>
|
|
399
412
|
</div>
|
|
400
413
|
<div className="vat">
|
|
401
|
-
<dt>{
|
|
414
|
+
<dt>{t.inclVat(formatPercent(p.vat || 19))}</dt>
|
|
402
415
|
<dd>{money(Math.round(vat * 100) / 100)}</dd>
|
|
403
416
|
</div>
|
|
404
417
|
</dl>
|
|
@@ -408,10 +421,11 @@ export function OrderSummary(p: OrderSummaryProps) {
|
|
|
408
421
|
}
|
|
409
422
|
|
|
410
423
|
export function CheckoutSteps(p: CheckoutStepsProps) {
|
|
411
|
-
const
|
|
424
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
425
|
+
const steps = p.steps || t.checkoutSteps,
|
|
412
426
|
cur = p.current || 0;
|
|
413
427
|
return (
|
|
414
|
-
<ol className="tf-csteps" aria-label=
|
|
428
|
+
<ol className="tf-csteps" aria-label={t.checkoutProgress}>
|
|
415
429
|
{steps.map((s, i) => (
|
|
416
430
|
<li
|
|
417
431
|
key={s}
|
|
@@ -436,18 +450,10 @@ export function CheckoutSteps(p: CheckoutStepsProps) {
|
|
|
436
450
|
|
|
437
451
|
/* Status keys and their visible text. */
|
|
438
452
|
const ORDER_STEPS = ["Placed", "Paid", "Packed", "Shipped", "Delivered"] as const;
|
|
439
|
-
const orderStepLabel = (s: string) =>
|
|
440
|
-
({
|
|
441
|
-
Placed: "Placed",
|
|
442
|
-
Paid: "Paid",
|
|
443
|
-
Packed: "Packed",
|
|
444
|
-
Shipped: "Shipped",
|
|
445
|
-
Delivered: "Delivered",
|
|
446
|
-
Refunded: "Refunded",
|
|
447
|
-
Cancelled: "Cancelled",
|
|
448
|
-
})[s] ?? s;
|
|
449
453
|
|
|
450
454
|
export function OrderTimeline(p: OrderTimelineProps) {
|
|
455
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
456
|
+
const orderStepLabel = (s: string) => t.orderStatus[s] ?? s;
|
|
451
457
|
const cur = (ORDER_STEPS as readonly string[]).indexOf(p.status),
|
|
452
458
|
times = p.times || {},
|
|
453
459
|
cancelled = p.status === "Cancelled" || p.status === "Refunded";
|
|
@@ -482,33 +488,35 @@ export function OrderTimeline(p: OrderTimelineProps) {
|
|
|
482
488
|
/* ── System screens ── */
|
|
483
489
|
|
|
484
490
|
export function PlanCard(p: PlanCardProps) {
|
|
491
|
+
const t = useStrings(COMMERCE_STRINGS);
|
|
492
|
+
const money = useMoney();
|
|
485
493
|
return (
|
|
486
494
|
<div className={cx("tf-plan", p.featured && "featured", p.current && "current")}>
|
|
487
|
-
{p.featured && <span className="tf-plan-flag">{p.flag ||
|
|
495
|
+
{p.featured && <span className="tf-plan-flag">{p.flag || t.mostPopular}</span>}
|
|
488
496
|
<div className="tf-plan-head">
|
|
489
497
|
<b>{p.name}</b>
|
|
490
498
|
{p.text && <span>{p.text}</span>}
|
|
491
499
|
</div>
|
|
492
500
|
<div className="tf-plan-price">
|
|
493
501
|
{p.price === 0 ? (
|
|
494
|
-
<b>{
|
|
502
|
+
<b>{t.free}</b>
|
|
495
503
|
) : p.price == null ? (
|
|
496
|
-
<b>{
|
|
504
|
+
<b>{t.letsTalk}</b>
|
|
497
505
|
) : (
|
|
498
506
|
<>
|
|
499
507
|
<b>{money(p.price)}</b>
|
|
500
|
-
<span>{p.per ||
|
|
508
|
+
<span>{p.per || t.perSeatMonth}</span>
|
|
501
509
|
</>
|
|
502
510
|
)}
|
|
503
511
|
{p.was && <s>{money(p.was)}</s>}
|
|
504
512
|
</div>
|
|
505
513
|
<Button
|
|
506
514
|
variant={p.current ? "soft" : p.featured ? "primary" : "secondary"}
|
|
507
|
-
disabled={p.current}
|
|
515
|
+
disabled={p.current || p.disabled}
|
|
508
516
|
onClick={p.onPick}
|
|
509
517
|
className="tf-plan-cta"
|
|
510
518
|
>
|
|
511
|
-
{p.current ?
|
|
519
|
+
{p.current ? t.currentPlan : p.cta || t.choosePlan(p.name)}
|
|
512
520
|
</Button>
|
|
513
521
|
<ul className="tf-plan-feats">
|
|
514
522
|
{(p.features || []).map((f, i) => (
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cx, durationToken, enterDelay, reduced } from "../cx.js";
|
|
5
|
+
import { formatLike, parseFormatted, type FormattedNumber } from "../format.js";
|
|
6
|
+
import { useFormat, useStrings } from "../context.js";
|
|
7
|
+
import { DATA_DISPLAY_STRINGS } from "../strings.js";
|
|
5
8
|
import { Icon } from "../Icon.js";
|
|
6
9
|
import { useMounted, useScrollFocus } from "../hooks.js";
|
|
7
10
|
import { Avatar, IconButton, TintIcon } from "./foundation.js";
|
|
@@ -26,44 +29,24 @@ import type {
|
|
|
26
29
|
} from "./types.js";
|
|
27
30
|
|
|
28
31
|
/* ── Data ── */
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
post: string;
|
|
35
|
-
}
|
|
36
|
-
function parseNum(v: string | number): ParsedNumber | null {
|
|
37
|
-
const m = /^([^0-9-]*)(-?[0-9][0-9,]*\.?[0-9]*)(.*)$/.exec(String(v));
|
|
38
|
-
if (!m) return null;
|
|
39
|
-
const raw = (m[2] as string).replace(/,/g, ""),
|
|
40
|
-
dec = (raw.split(".")[1] || "").length;
|
|
41
|
-
return {
|
|
42
|
-
pre: m[1] ?? "",
|
|
43
|
-
num: parseFloat(raw),
|
|
44
|
-
dec: dec,
|
|
45
|
-
comma: (m[2] as string).indexOf(",") > -1,
|
|
46
|
-
post: m[3] ?? "",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
export function fmt(n: number, o: ParsedNumber) {
|
|
50
|
-
let s = n.toFixed(o.dec);
|
|
51
|
-
if (o.comma) {
|
|
52
|
-
const parts = s.split(".");
|
|
53
|
-
parts[0] = (parts[0] as string).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
54
|
-
s = parts.join(".");
|
|
55
|
-
}
|
|
56
|
-
return o.pre + s + o.post;
|
|
57
|
-
}
|
|
32
|
+
/**
|
|
33
|
+
* Counts up to `value`. Strings are read in the locale of the nearest `UIProvider` ("13,05 €/m²"
|
|
34
|
+
* in de-DE, "$1,305.50" in en-US): the number animates, the text around it stays, and decimals and
|
|
35
|
+
* grouping are kept. The final text is always `value` itself.
|
|
36
|
+
*/
|
|
58
37
|
export function CountUp(p: CountUpProps) {
|
|
59
|
-
const
|
|
60
|
-
|
|
38
|
+
const { locale } = useFormat();
|
|
39
|
+
const o = parseFormatted(p.value, locale),
|
|
40
|
+
fmt = (n: number, shape: FormattedNumber) => formatLike(n, shape, locale),
|
|
41
|
+
// numbers are shown in the locale too; strings end exactly as given
|
|
42
|
+
final = o && typeof p.value === "number" ? fmt(p.value, o) : String(p.value),
|
|
43
|
+
st = React.useState(o ? fmt(0, o) : final),
|
|
61
44
|
last = React.useRef(0),
|
|
62
45
|
first = React.useRef(true),
|
|
63
46
|
el = React.useRef<HTMLSpanElement>(null);
|
|
64
47
|
React.useEffect(() => {
|
|
65
48
|
if (!o || reduced(el.current)) {
|
|
66
|
-
st[1](
|
|
49
|
+
st[1](final);
|
|
67
50
|
return;
|
|
68
51
|
}
|
|
69
52
|
const slow = durationToken("--dur-slow", 900, el.current);
|
|
@@ -79,17 +62,20 @@ export function CountUp(p: CountUpProps) {
|
|
|
79
62
|
if (start === null) start = t;
|
|
80
63
|
const k = Math.min(1, Math.max(0, (t - start - delay) / dur));
|
|
81
64
|
const e = 1 - Math.pow(1 - k, 3),
|
|
82
|
-
v = from + (target.
|
|
65
|
+
v = from + (target.value - from) * e;
|
|
83
66
|
last.current = v;
|
|
84
67
|
st[1](fmt(v, target));
|
|
85
68
|
if (k < 1) raf = requestAnimationFrame(tick);
|
|
86
|
-
else
|
|
69
|
+
else {
|
|
70
|
+
last.current = target.value;
|
|
71
|
+
st[1](final);
|
|
72
|
+
}
|
|
87
73
|
}
|
|
88
74
|
raf = requestAnimationFrame(tick);
|
|
89
75
|
return () => {
|
|
90
76
|
cancelAnimationFrame(raf);
|
|
91
77
|
};
|
|
92
|
-
}, [p.value]);
|
|
78
|
+
}, [p.value, locale]);
|
|
93
79
|
return (
|
|
94
80
|
<span ref={el} className={p.className}>
|
|
95
81
|
{st[0]}
|
|
@@ -160,6 +146,7 @@ const KIND: Record<string, [IconName, Tint]> = {
|
|
|
160
146
|
incident: ["bell", "blush"],
|
|
161
147
|
};
|
|
162
148
|
export function EventCard(p: EventCardProps) {
|
|
149
|
+
const t = useStrings(DATA_DISPLAY_STRINGS);
|
|
163
150
|
const k = KIND[p.kind || "meeting"] || (KIND.meeting as [IconName, Tint]),
|
|
164
151
|
tint = p.tint || k[1];
|
|
165
152
|
return (
|
|
@@ -178,9 +165,9 @@ export function EventCard(p: EventCardProps) {
|
|
|
178
165
|
</span>
|
|
179
166
|
{p.actions !== false && (
|
|
180
167
|
<span className="tf-row">
|
|
181
|
-
<IconButton icon="eye" label=
|
|
182
|
-
<IconButton icon="edit" label=
|
|
183
|
-
<IconButton icon="trash" label=
|
|
168
|
+
<IconButton icon="eye" label={t.open} size="sm" variant="outline" />
|
|
169
|
+
<IconButton icon="edit" label={t.edit} size="sm" variant="outline" />
|
|
170
|
+
<IconButton icon="trash" label={t.remove} size="sm" variant="outline" />
|
|
184
171
|
</span>
|
|
185
172
|
)}
|
|
186
173
|
</div>
|
|
@@ -312,6 +299,7 @@ export function Table(p: TableProps) {
|
|
|
312
299
|
);
|
|
313
300
|
}
|
|
314
301
|
export function ListRow(p: ListRowProps) {
|
|
302
|
+
const t = useStrings(DATA_DISPLAY_STRINGS);
|
|
315
303
|
return (
|
|
316
304
|
<button
|
|
317
305
|
type="button"
|
|
@@ -331,7 +319,7 @@ export function ListRow(p: ListRowProps) {
|
|
|
331
319
|
}}
|
|
332
320
|
>
|
|
333
321
|
<span className="tf-row-title">
|
|
334
|
-
{p.unread && <span className="tf-unread" role="img" aria-label=
|
|
322
|
+
{p.unread && <span className="tf-unread" role="img" aria-label={t.unread} />}
|
|
335
323
|
{p.title}
|
|
336
324
|
</span>
|
|
337
325
|
{p.subtitle && (
|
|
@@ -386,6 +374,7 @@ const FT: Record<string, [IconName, Tint]> = {
|
|
|
386
374
|
zip: ["folder", "butter"],
|
|
387
375
|
};
|
|
388
376
|
export function FileRow(p: FileRowProps) {
|
|
377
|
+
const strings = useStrings(DATA_DISPLAY_STRINGS);
|
|
389
378
|
const t = FT[p.kind || "doc"] || (FT.doc as [IconName, Tint]);
|
|
390
379
|
return (
|
|
391
380
|
<div className="tf-file">
|
|
@@ -399,11 +388,12 @@ export function FileRow(p: FileRowProps) {
|
|
|
399
388
|
<div className="tf-file-name">{p.name}</div>
|
|
400
389
|
<div className="tf-file-meta">{p.meta}</div>
|
|
401
390
|
</div>
|
|
402
|
-
<IconButton icon="download" label={
|
|
391
|
+
<IconButton icon="download" label={strings.downloadItem(p.name)} size="sm" />
|
|
403
392
|
</div>
|
|
404
393
|
);
|
|
405
394
|
}
|
|
406
395
|
export function KanbanColumn(p: KanbanColumnProps) {
|
|
396
|
+
const t = useStrings(DATA_DISPLAY_STRINGS);
|
|
407
397
|
return (
|
|
408
398
|
<section className="tf-kanban" aria-label={p.title}>
|
|
409
399
|
<div className="tf-kanban-head">
|
|
@@ -429,7 +419,7 @@ export function KanbanColumn(p: KanbanColumnProps) {
|
|
|
429
419
|
{p.title}
|
|
430
420
|
<span className="tf-kanban-count">{React.Children.count(p.children)}</span>
|
|
431
421
|
</span>
|
|
432
|
-
<IconButton icon="more" label=
|
|
422
|
+
<IconButton icon="more" label={t.columnOptions} size="sm" />
|
|
433
423
|
</div>
|
|
434
424
|
<Stagger
|
|
435
425
|
className="tf-col"
|
|
@@ -442,12 +432,13 @@ export function KanbanColumn(p: KanbanColumnProps) {
|
|
|
442
432
|
</Stagger>
|
|
443
433
|
<button type="button" className="tf-kanban-add" onClick={p.onAdd}>
|
|
444
434
|
<Icon name="plus" size={14} />
|
|
445
|
-
{
|
|
435
|
+
{t.addCard}
|
|
446
436
|
</button>
|
|
447
437
|
</section>
|
|
448
438
|
);
|
|
449
439
|
}
|
|
450
440
|
export function Progress(p: ProgressProps) {
|
|
441
|
+
const t = useStrings(DATA_DISPLAY_STRINGS);
|
|
451
442
|
const v = Math.max(0, Math.min(100, p.value || 0));
|
|
452
443
|
return (
|
|
453
444
|
<div
|
|
@@ -456,7 +447,7 @@ export function Progress(p: ProgressProps) {
|
|
|
456
447
|
aria-valuenow={v}
|
|
457
448
|
aria-valuemin={0}
|
|
458
449
|
aria-valuemax={100}
|
|
459
|
-
aria-label={p.label ||
|
|
450
|
+
aria-label={p.label || t.progress}
|
|
460
451
|
>
|
|
461
452
|
<span
|
|
462
453
|
style={{
|
|
@@ -467,6 +458,8 @@ export function Progress(p: ProgressProps) {
|
|
|
467
458
|
);
|
|
468
459
|
}
|
|
469
460
|
export function ProgressRing(p: ProgressRingProps) {
|
|
461
|
+
const t = useStrings(DATA_DISPLAY_STRINGS);
|
|
462
|
+
const { formatPercent } = useFormat();
|
|
470
463
|
const size = p.size || 88,
|
|
471
464
|
sw = p.stroke || 9,
|
|
472
465
|
r = (size - sw) / 2,
|
|
@@ -481,7 +474,7 @@ export function ProgressRing(p: ProgressRingProps) {
|
|
|
481
474
|
height: size,
|
|
482
475
|
}}
|
|
483
476
|
role="img"
|
|
484
|
-
aria-label={(p.label ||
|
|
477
|
+
aria-label={(p.label || t.progress) + " " + formatPercent(v, { maximumFractionDigits: 2 })}
|
|
485
478
|
>
|
|
486
479
|
<svg width={size} height={size} viewBox={"0 0 " + size + " " + size}>
|
|
487
480
|
<circle className="tf-arc-track" cx={size / 2} cy={size / 2} r={r} strokeWidth={sw} />
|
|
@@ -497,7 +490,7 @@ export function ProgressRing(p: ProgressRingProps) {
|
|
|
497
490
|
</svg>
|
|
498
491
|
<span className="tf-ring-label">
|
|
499
492
|
<span>
|
|
500
|
-
<CountUp value={v
|
|
493
|
+
<CountUp value={formatPercent(v, { maximumFractionDigits: 2 })} delay={150} />
|
|
501
494
|
{p.caption && <small>{p.caption}</small>}
|
|
502
495
|
</span>
|
|
503
496
|
</span>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { useStrings } from "../context.js";
|
|
4
|
+
import { DATA_TABLE_STRINGS } from "../strings.js";
|
|
3
5
|
import { Icon } from "../Icon.js";
|
|
4
6
|
import { cx } from "../cx.js";
|
|
5
7
|
import { SearchField } from "./inputs.js";
|
|
@@ -9,6 +11,7 @@ import type { DataTableProps, FilterBarProps, FilterDef } from "./types.js";
|
|
|
9
11
|
|
|
10
12
|
/* ── Data table: sort, select + bulk bar, column toggles, sticky head, expandable rows ── */
|
|
11
13
|
export function FilterBar(p: FilterBarProps) {
|
|
14
|
+
const t = useStrings(DATA_TABLE_STRINGS);
|
|
12
15
|
const active = p.value || {},
|
|
13
16
|
keys = Object.keys(active).filter((k) => active[k] != null && active[k] !== "");
|
|
14
17
|
function set(k: string, val: string | null) {
|
|
@@ -54,7 +57,7 @@ export function FilterBar(p: FilterBarProps) {
|
|
|
54
57
|
<button
|
|
55
58
|
type="button"
|
|
56
59
|
className="x"
|
|
57
|
-
aria-label={
|
|
60
|
+
aria-label={t.removeFilter(f.label)}
|
|
58
61
|
onClick={() => {
|
|
59
62
|
set(k, null);
|
|
60
63
|
}}
|
|
@@ -86,7 +89,7 @@ export function FilterBar(p: FilterBarProps) {
|
|
|
86
89
|
trigger={(s) => (
|
|
87
90
|
<button type="button" className="tf-chip tf-chip-dashed" onClick={s.toggle}>
|
|
88
91
|
<Icon name="plus" size={14} />
|
|
89
|
-
{
|
|
92
|
+
{t.addFilter}
|
|
90
93
|
</button>
|
|
91
94
|
)}
|
|
92
95
|
>
|
|
@@ -109,7 +112,7 @@ export function FilterBar(p: FilterBarProps) {
|
|
|
109
112
|
p.onChange?.({});
|
|
110
113
|
}}
|
|
111
114
|
>
|
|
112
|
-
{
|
|
115
|
+
{t.clearAll}
|
|
113
116
|
</button>
|
|
114
117
|
)}
|
|
115
118
|
{p.right && <div className="tf-filterbar-right">{p.right}</div>}
|
|
@@ -121,6 +124,7 @@ export function AddFilterMenu(p: {
|
|
|
121
124
|
avail: FilterDef[];
|
|
122
125
|
onPick?: (key: string, option: string) => void;
|
|
123
126
|
}) {
|
|
127
|
+
const t = useStrings(DATA_TABLE_STRINGS);
|
|
124
128
|
const step = React.useState<FilterDef | null>(null);
|
|
125
129
|
const cur = step[0];
|
|
126
130
|
if (cur)
|
|
@@ -128,7 +132,7 @@ export function AddFilterMenu(p: {
|
|
|
128
132
|
<Menu
|
|
129
133
|
items={[
|
|
130
134
|
{
|
|
131
|
-
label:
|
|
135
|
+
label: t.back,
|
|
132
136
|
icon: "chevronLeft",
|
|
133
137
|
onClick: () => {
|
|
134
138
|
step[1](null);
|
|
@@ -161,6 +165,7 @@ export function AddFilterMenu(p: {
|
|
|
161
165
|
export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
162
166
|
p: DataTableProps<R>,
|
|
163
167
|
) {
|
|
168
|
+
const t = useStrings(DATA_TABLE_STRINGS);
|
|
164
169
|
const cols = p.columns || [],
|
|
165
170
|
rk = p.rowKey || "id";
|
|
166
171
|
const sort = React.useState<{ key: string; dir: "asc" | "desc" } | null>(p.defaultSort || null),
|
|
@@ -229,11 +234,11 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
229
234
|
width={220}
|
|
230
235
|
flush
|
|
231
236
|
trigger={(s) => (
|
|
232
|
-
<Tooltip label=
|
|
237
|
+
<Tooltip label={t.columns}>
|
|
233
238
|
<button
|
|
234
239
|
type="button"
|
|
235
240
|
className="tf-iconbtn tf-iconbtn-soft"
|
|
236
|
-
aria-label=
|
|
241
|
+
aria-label={t.chooseColumns}
|
|
237
242
|
onClick={s.toggle}
|
|
238
243
|
>
|
|
239
244
|
<Icon name="grid" />
|
|
@@ -242,7 +247,7 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
242
247
|
)}
|
|
243
248
|
>
|
|
244
249
|
<div className="tf-menu tf-menu-static">
|
|
245
|
-
<div className="tf-menu-label">{
|
|
250
|
+
<div className="tf-menu-label">{t.columns}</div>
|
|
246
251
|
{cols
|
|
247
252
|
.filter((c) => c.hideable !== false && c.label)
|
|
248
253
|
.map((c) => {
|
|
@@ -287,12 +292,12 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
287
292
|
() => {
|
|
288
293
|
sel[1](all ? [] : ids);
|
|
289
294
|
},
|
|
290
|
-
|
|
295
|
+
t.selectAllRows,
|
|
291
296
|
some,
|
|
292
297
|
)}
|
|
293
298
|
</th>
|
|
294
299
|
)}
|
|
295
|
-
{p.renderExpanded && <th className="tf-dt-exp" aria-label=
|
|
300
|
+
{p.renderExpanded && <th className="tf-dt-exp" aria-label={t.expand} />}
|
|
296
301
|
{vis.map((c) => {
|
|
297
302
|
const s = sort[0] && sort[0].key === c.key ? sort[0].dir : null;
|
|
298
303
|
return (
|
|
@@ -347,7 +352,7 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
347
352
|
{rows.length === 0 ? (
|
|
348
353
|
<tr>
|
|
349
354
|
<td colSpan={span} className="tf-dt-empty">
|
|
350
|
-
{p.empty ||
|
|
355
|
+
{p.empty || t.nothingMatchesFilters}
|
|
351
356
|
</td>
|
|
352
357
|
</tr>
|
|
353
358
|
) : (
|
|
@@ -365,7 +370,7 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
365
370
|
() => {
|
|
366
371
|
toggle(id);
|
|
367
372
|
},
|
|
368
|
-
|
|
373
|
+
t.selectRow,
|
|
369
374
|
)}
|
|
370
375
|
</td>
|
|
371
376
|
)}
|
|
@@ -375,7 +380,7 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
375
380
|
type="button"
|
|
376
381
|
className={cx("tf-dt-toggle", ex && "open")}
|
|
377
382
|
aria-expanded={ex}
|
|
378
|
-
aria-label={ex ?
|
|
383
|
+
aria-label={ex ? t.collapseRow : t.expandRow}
|
|
379
384
|
onClick={() => {
|
|
380
385
|
const n = Object.assign({}, open[0]);
|
|
381
386
|
n[String(id)] = !ex;
|
|
@@ -407,8 +412,8 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
407
412
|
</table>
|
|
408
413
|
</div>
|
|
409
414
|
{p.selectable && selected.length > 0 && (
|
|
410
|
-
<div className="tf-bulkbar" role="toolbar" aria-label=
|
|
411
|
-
<b>{
|
|
415
|
+
<div className="tf-bulkbar" role="toolbar" aria-label={t.bulkActions}>
|
|
416
|
+
<b>{t.selectedCount(selected.length)}</b>
|
|
412
417
|
<button
|
|
413
418
|
type="button"
|
|
414
419
|
className="tf-linkbtn on-frame"
|
|
@@ -416,7 +421,7 @@ export function DataTable<R extends Record<string, any> = Record<string, any>>(
|
|
|
416
421
|
sel[1]([]);
|
|
417
422
|
}}
|
|
418
423
|
>
|
|
419
|
-
{
|
|
424
|
+
{t.clear}
|
|
420
425
|
</button>
|
|
421
426
|
<div className="tf-bulkbar-actions">
|
|
422
427
|
{(p.bulkActions || []).map((a) => (
|