@twentyfirstmedia/ui-core 0.2.0 → 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/forms.tsx
CHANGED
|
@@ -3,7 +3,9 @@ import * as React from "react";
|
|
|
3
3
|
import { leaving, usePresence } from "../presence.js";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useFieldError } from "./form.js";
|
|
6
|
-
import { formatDate,
|
|
6
|
+
import { formatDate, WEEK_START } from "../format.js";
|
|
7
|
+
import { useFormat, useStrings, type Formatters } from "../context.js";
|
|
8
|
+
import { FORMS_STRINGS } from "../strings.js";
|
|
7
9
|
import { Icon } from "../Icon.js";
|
|
8
10
|
import { useControlled, useId, useOutside } from "../hooks.js";
|
|
9
11
|
import { Avatar, IconButton, TintIcon } from "./foundation.js";
|
|
@@ -101,6 +103,15 @@ export function TextField(p: TextFieldProps) {
|
|
|
101
103
|
onBlur={p.onBlur}
|
|
102
104
|
required={p.required}
|
|
103
105
|
disabled={p.disabled}
|
|
106
|
+
readOnly={p.readOnly}
|
|
107
|
+
min={p.min}
|
|
108
|
+
max={p.max}
|
|
109
|
+
step={p.step}
|
|
110
|
+
minLength={p.minLength}
|
|
111
|
+
maxLength={p.maxLength}
|
|
112
|
+
pattern={p.pattern}
|
|
113
|
+
inputMode={p.inputMode}
|
|
114
|
+
autoFocus={p.autoFocus}
|
|
104
115
|
aria-invalid={error ? "true" : undefined}
|
|
105
116
|
aria-describedby={error || p.hint ? `${id}-msg` : undefined}
|
|
106
117
|
autoComplete={p.autoComplete}
|
|
@@ -223,6 +234,7 @@ export function norm(o: string | Option): Option {
|
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
export function Combobox(p: ComboboxProps) {
|
|
237
|
+
const t = useStrings(FORMS_STRINGS);
|
|
226
238
|
const multi = !!p.multiple,
|
|
227
239
|
opts = (p.options || []).map(norm),
|
|
228
240
|
id = useId("cb");
|
|
@@ -310,7 +322,7 @@ export function Combobox(p: ComboboxProps) {
|
|
|
310
322
|
{labelOf(val)}
|
|
311
323
|
<button
|
|
312
324
|
type="button"
|
|
313
|
-
aria-label={
|
|
325
|
+
aria-label={t.removeItem(labelOf(val))}
|
|
314
326
|
onClick={(e) => {
|
|
315
327
|
e.stopPropagation();
|
|
316
328
|
v[1](selList.filter((x) => x !== val));
|
|
@@ -336,7 +348,7 @@ export function Combobox(p: ComboboxProps) {
|
|
|
336
348
|
: p.placeholder
|
|
337
349
|
: selOne != null && !q[0]
|
|
338
350
|
? labelOf(selOne)
|
|
339
|
-
: p.placeholder ||
|
|
351
|
+
: p.placeholder || t.selectPlaceholder
|
|
340
352
|
}
|
|
341
353
|
value={q[0]}
|
|
342
354
|
onChange={(e) => {
|
|
@@ -368,9 +380,7 @@ export function Combobox(p: ComboboxProps) {
|
|
|
368
380
|
role="listbox"
|
|
369
381
|
aria-multiselectable={multi || undefined}
|
|
370
382
|
>
|
|
371
|
-
{list.length === 0 && !canCreate &&
|
|
372
|
-
<div className="tf-combo-empty">{"No matches"}</div>
|
|
373
|
-
)}
|
|
383
|
+
{list.length === 0 && !canCreate && <div className="tf-combo-empty">{t.noMatches}</div>}
|
|
374
384
|
{list.map((o, i) => (
|
|
375
385
|
<div
|
|
376
386
|
key={o.value}
|
|
@@ -411,7 +421,7 @@ export function Combobox(p: ComboboxProps) {
|
|
|
411
421
|
}}
|
|
412
422
|
>
|
|
413
423
|
<Icon name="plus" size={15} />
|
|
414
|
-
<span className="tf-option-text">{
|
|
424
|
+
<span className="tf-option-text">{t.createOption(q[0].trim())}</span>
|
|
415
425
|
</div>
|
|
416
426
|
)}
|
|
417
427
|
</div>
|
|
@@ -422,6 +432,7 @@ export function Combobox(p: ComboboxProps) {
|
|
|
422
432
|
}
|
|
423
433
|
|
|
424
434
|
export function TagInput(p: TagInputProps) {
|
|
435
|
+
const strings = useStrings(FORMS_STRINGS);
|
|
425
436
|
const v = useControlled(p.value, p.defaultValue || [], p.onChange),
|
|
426
437
|
q = React.useState(""),
|
|
427
438
|
id = useId("tag");
|
|
@@ -444,7 +455,7 @@ export function TagInput(p: TagInputProps) {
|
|
|
444
455
|
{t}
|
|
445
456
|
<button
|
|
446
457
|
type="button"
|
|
447
|
-
aria-label={
|
|
458
|
+
aria-label={strings.removeItem(t)}
|
|
448
459
|
onClick={() => {
|
|
449
460
|
v[1](v[0].filter((x) => x !== t));
|
|
450
461
|
}}
|
|
@@ -456,7 +467,7 @@ export function TagInput(p: TagInputProps) {
|
|
|
456
467
|
<input
|
|
457
468
|
id={id}
|
|
458
469
|
className="tf-combo-input"
|
|
459
|
-
placeholder={v[0].length ? "" : p.placeholder ||
|
|
470
|
+
placeholder={v[0].length ? "" : p.placeholder || strings.tagPlaceholder}
|
|
460
471
|
value={q[0]}
|
|
461
472
|
onChange={(e) => {
|
|
462
473
|
const s = e.target.value;
|
|
@@ -479,21 +490,30 @@ export function TagInput(p: TagInputProps) {
|
|
|
479
490
|
}
|
|
480
491
|
|
|
481
492
|
/* ── Date picker (single or range, with presets) ── */
|
|
482
|
-
function monthName(d: Date) {
|
|
483
|
-
return formatDate(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
493
|
+
function monthName(d: Date, locale?: string) {
|
|
494
|
+
return formatDate(
|
|
495
|
+
d,
|
|
496
|
+
{
|
|
497
|
+
month: "long",
|
|
498
|
+
year: "numeric",
|
|
499
|
+
},
|
|
500
|
+
locale,
|
|
501
|
+
);
|
|
487
502
|
}
|
|
488
503
|
|
|
489
|
-
|
|
504
|
+
/** Short weekday names from the first day of the week (`locale` default en-US). */
|
|
505
|
+
export function weekdays(locale?: string) {
|
|
490
506
|
const out: string[] = [];
|
|
491
507
|
for (let i = 0; i < 7; i++) {
|
|
492
508
|
const d = new Date(2024, 0, 1 + ((WEEK_START + i - 1) % 7));
|
|
493
509
|
out.push(
|
|
494
|
-
formatDate(
|
|
495
|
-
|
|
496
|
-
|
|
510
|
+
formatDate(
|
|
511
|
+
d,
|
|
512
|
+
{
|
|
513
|
+
weekday: "short",
|
|
514
|
+
},
|
|
515
|
+
locale,
|
|
516
|
+
),
|
|
497
517
|
);
|
|
498
518
|
}
|
|
499
519
|
return out;
|
|
@@ -513,8 +533,8 @@ function same(a: Date | null | undefined, b: Date | null | undefined): boolean {
|
|
|
513
533
|
);
|
|
514
534
|
}
|
|
515
535
|
|
|
516
|
-
function fmtD(d: Date | null | undefined) {
|
|
517
|
-
return d ? formatDate(d) : "";
|
|
536
|
+
function fmtD(d: Date | null | undefined, locale?: string) {
|
|
537
|
+
return d ? formatDate(d, undefined, locale) : "";
|
|
518
538
|
}
|
|
519
539
|
|
|
520
540
|
function addD(d: Date, n: number) {
|
|
@@ -524,6 +544,8 @@ function addD(d: Date, n: number) {
|
|
|
524
544
|
}
|
|
525
545
|
|
|
526
546
|
export function Calendar(p: CalendarProps) {
|
|
547
|
+
const t = useStrings(FORMS_STRINGS);
|
|
548
|
+
const { locale } = useFormat();
|
|
527
549
|
const range = p.range ? ((p.value as DateRange | null | undefined) ?? null) : null;
|
|
528
550
|
const today = d0(p.today || new Date()),
|
|
529
551
|
sv = p.range ? range && range.from : (p.value as Date | null | undefined),
|
|
@@ -544,17 +566,17 @@ export function Calendar(p: CalendarProps) {
|
|
|
544
566
|
<div className="tf-cal-head">
|
|
545
567
|
<IconButton
|
|
546
568
|
icon="chevronLeft"
|
|
547
|
-
label=
|
|
569
|
+
label={t.previousMonth}
|
|
548
570
|
size="sm"
|
|
549
571
|
variant="soft"
|
|
550
572
|
onClick={() => {
|
|
551
573
|
move(-1);
|
|
552
574
|
}}
|
|
553
575
|
/>
|
|
554
|
-
<b>{monthName(first)}</b>
|
|
576
|
+
<b>{monthName(first, locale)}</b>
|
|
555
577
|
<IconButton
|
|
556
578
|
icon="chevronRight"
|
|
557
|
-
label=
|
|
579
|
+
label={t.nextMonth}
|
|
558
580
|
size="sm"
|
|
559
581
|
variant="soft"
|
|
560
582
|
onClick={() => {
|
|
@@ -562,8 +584,8 @@ export function Calendar(p: CalendarProps) {
|
|
|
562
584
|
}}
|
|
563
585
|
/>
|
|
564
586
|
</div>
|
|
565
|
-
<div className="tf-cal-grid" role="group" aria-label={monthName(first)}>
|
|
566
|
-
{weekdays().map((w, i) => (
|
|
587
|
+
<div className="tf-cal-grid" role="group" aria-label={monthName(first, locale)}>
|
|
588
|
+
{weekdays(locale).map((w, i) => (
|
|
567
589
|
<span key={i} className="tf-cal-wd">
|
|
568
590
|
{w}
|
|
569
591
|
</span>
|
|
@@ -589,7 +611,7 @@ export function Calendar(p: CalendarProps) {
|
|
|
589
611
|
isTo && from && "end",
|
|
590
612
|
)}
|
|
591
613
|
aria-pressed={isFrom || isTo}
|
|
592
|
-
aria-label={formatDate(d, { day: "numeric", month: "long", year: "numeric" })}
|
|
614
|
+
aria-label={formatDate(d, { day: "numeric", month: "long", year: "numeric" }, locale)}
|
|
593
615
|
onPointerEnter={() => {
|
|
594
616
|
p.range && hov[1](d);
|
|
595
617
|
}}
|
|
@@ -617,6 +639,8 @@ export function Calendar(p: CalendarProps) {
|
|
|
617
639
|
}
|
|
618
640
|
|
|
619
641
|
export function DatePicker(p: DatePickerProps) {
|
|
642
|
+
const t = useStrings(FORMS_STRINGS);
|
|
643
|
+
const { locale } = useFormat();
|
|
620
644
|
const id = useId("dp"),
|
|
621
645
|
v = useControlled(
|
|
622
646
|
p.value,
|
|
@@ -633,18 +657,18 @@ export function DatePicker(p: DatePickerProps) {
|
|
|
633
657
|
const presets =
|
|
634
658
|
p.presets !== false && p.range
|
|
635
659
|
? [
|
|
636
|
-
[
|
|
637
|
-
[
|
|
638
|
-
[
|
|
639
|
-
[
|
|
660
|
+
[t.lastDays(7), -6],
|
|
661
|
+
[t.lastDays(30), -29],
|
|
662
|
+
[t.lastDays(90), -89],
|
|
663
|
+
[t.thisYear, "y"],
|
|
640
664
|
]
|
|
641
665
|
: null;
|
|
642
666
|
const rv = p.range ? ((v[0] as DateRange | null) ?? null) : null;
|
|
643
667
|
const txt = p.range
|
|
644
668
|
? rv && rv.from
|
|
645
|
-
? fmtD(rv.from) + " – " + (rv.to ? fmtD(rv.to) : "…")
|
|
669
|
+
? fmtD(rv.from, locale) + " – " + (rv.to ? fmtD(rv.to, locale) : "…")
|
|
646
670
|
: ""
|
|
647
|
-
: fmtD(v[0] as Date | null);
|
|
671
|
+
: fmtD(v[0] as Date | null, locale);
|
|
648
672
|
return (
|
|
649
673
|
<FormField
|
|
650
674
|
id={id}
|
|
@@ -667,7 +691,7 @@ export function DatePicker(p: DatePickerProps) {
|
|
|
667
691
|
onClick={s.toggle}
|
|
668
692
|
>
|
|
669
693
|
<Icon name="calendar" size={15} />
|
|
670
|
-
<span>{txt || p.placeholder || (p.range ?
|
|
694
|
+
<span>{txt || p.placeholder || (p.range ? t.pickRange : t.pickDate)}</span>
|
|
671
695
|
</button>
|
|
672
696
|
)}
|
|
673
697
|
>
|
|
@@ -722,6 +746,7 @@ export function DatePicker(p: DatePickerProps) {
|
|
|
722
746
|
|
|
723
747
|
/* ── Number, slider, OTP, upload ── */
|
|
724
748
|
export function NumberInput(p: NumberInputProps) {
|
|
749
|
+
const t = useStrings(FORMS_STRINGS);
|
|
725
750
|
const id = useId("num"),
|
|
726
751
|
v = useControlled(p.value, p.defaultValue != null ? p.defaultValue : 0, p.onChange),
|
|
727
752
|
step = p.step || 1;
|
|
@@ -735,7 +760,7 @@ export function NumberInput(p: NumberInputProps) {
|
|
|
735
760
|
<div className="tf-number">
|
|
736
761
|
<IconButton
|
|
737
762
|
icon="minus"
|
|
738
|
-
label=
|
|
763
|
+
label={t.decrease}
|
|
739
764
|
size="sm"
|
|
740
765
|
disabled={p.min != null && v[0] <= p.min}
|
|
741
766
|
onClick={() => {
|
|
@@ -766,7 +791,7 @@ export function NumberInput(p: NumberInputProps) {
|
|
|
766
791
|
{p.unit && <span className="tf-number-unit">{p.unit}</span>}
|
|
767
792
|
<IconButton
|
|
768
793
|
icon="plus"
|
|
769
|
-
label=
|
|
794
|
+
label={t.increase}
|
|
770
795
|
size="sm"
|
|
771
796
|
disabled={p.max != null && v[0] >= p.max}
|
|
772
797
|
onClick={() => {
|
|
@@ -827,6 +852,7 @@ export function Slider(p: SliderProps) {
|
|
|
827
852
|
}
|
|
828
853
|
|
|
829
854
|
export function OtpInput(p: OtpInputProps) {
|
|
855
|
+
const t = useStrings(FORMS_STRINGS);
|
|
830
856
|
const n = p.length || 6,
|
|
831
857
|
st = React.useState(Array(n).fill("")),
|
|
832
858
|
refs = React.useRef<(HTMLInputElement | null)[]>([]);
|
|
@@ -841,7 +867,7 @@ export function OtpInput(p: OtpInputProps) {
|
|
|
841
867
|
<div
|
|
842
868
|
className={cx("tf-otp", p.error && "invalid")}
|
|
843
869
|
role="group"
|
|
844
|
-
aria-label={p.label ||
|
|
870
|
+
aria-label={p.label || t.verificationCode}
|
|
845
871
|
>
|
|
846
872
|
{st[0].map((c, i) => (
|
|
847
873
|
<React.Fragment key={i}>
|
|
@@ -855,7 +881,7 @@ export function OtpInput(p: OtpInputProps) {
|
|
|
855
881
|
autoComplete={i === 0 ? "one-time-code" : "off"}
|
|
856
882
|
maxLength={1}
|
|
857
883
|
value={c}
|
|
858
|
-
aria-label={
|
|
884
|
+
aria-label={t.digitN(i + 1)}
|
|
859
885
|
onChange={(e) => {
|
|
860
886
|
set(i, e.target.value.replace(/\D/g, "").slice(-1));
|
|
861
887
|
}}
|
|
@@ -882,26 +908,65 @@ export function OtpInput(p: OtpInputProps) {
|
|
|
882
908
|
);
|
|
883
909
|
}
|
|
884
910
|
|
|
885
|
-
function fsize(b: number) {
|
|
911
|
+
function fsize(b: number, f: Pick<Formatters, "formatNumber">) {
|
|
886
912
|
return b > 1048576
|
|
887
|
-
? `${formatNumber(b / 1048576, { maximumFractionDigits: 1 })} MB`
|
|
888
|
-
: `${formatNumber(Math.max(1, Math.round(b / 1024)))} KB`;
|
|
913
|
+
? `${f.formatNumber(b / 1048576, { maximumFractionDigits: 1 })} MB`
|
|
914
|
+
: `${f.formatNumber(Math.max(1, Math.round(b / 1024)))} KB`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Whether a file matches an `accept` list ("image/*,.pdf,application/pdf"); empty accepts all. */
|
|
918
|
+
function acceptsFile(file: { name: string; type: string }, accept?: string): boolean {
|
|
919
|
+
const rules = (accept || "")
|
|
920
|
+
.split(",")
|
|
921
|
+
.map((r) => r.trim().toLowerCase())
|
|
922
|
+
.filter(Boolean);
|
|
923
|
+
if (!rules.length) return true;
|
|
924
|
+
const name = file.name.toLowerCase(),
|
|
925
|
+
type = (file.type || "").toLowerCase();
|
|
926
|
+
return rules.some((r) =>
|
|
927
|
+
r.startsWith(".")
|
|
928
|
+
? name.endsWith(r)
|
|
929
|
+
: r.endsWith("/*")
|
|
930
|
+
? type.startsWith(r.slice(0, -1))
|
|
931
|
+
: type === r,
|
|
932
|
+
);
|
|
889
933
|
}
|
|
890
934
|
|
|
891
935
|
export function FileDrop(p: FileDropProps) {
|
|
936
|
+
const t = useStrings(FORMS_STRINGS);
|
|
937
|
+
const fm = useFormat();
|
|
892
938
|
const over = React.useState(false),
|
|
893
939
|
files = useControlled(p.files, [], p.onChange),
|
|
894
|
-
inp = React.useRef<HTMLInputElement>(null)
|
|
940
|
+
inp = React.useRef<HTMLInputElement>(null),
|
|
941
|
+
timers = React.useRef<ReturnType<typeof setInterval>[]>([]);
|
|
942
|
+
// Simulated progress is a demo of the look; with `onFiles` the app reports real progress, so
|
|
943
|
+
// an uncontrolled list without simulation shows no progress at all.
|
|
944
|
+
const simulate = p.simulateProgress ?? !p.onFiles,
|
|
945
|
+
tracked = simulate || p.files !== undefined;
|
|
946
|
+
React.useEffect(
|
|
947
|
+
() => () => {
|
|
948
|
+
timers.current.forEach(clearInterval);
|
|
949
|
+
},
|
|
950
|
+
[],
|
|
951
|
+
);
|
|
895
952
|
function take(list: FileList | null) {
|
|
896
|
-
|
|
953
|
+
let picked = Array.from(list || []);
|
|
954
|
+
const rejected = picked.filter((f) => !acceptsFile(f, p.accept));
|
|
955
|
+
picked = picked.filter((f) => acceptsFile(f, p.accept));
|
|
956
|
+
if (p.multiple === false) picked = picked.slice(0, 1);
|
|
957
|
+
if (rejected.length) p.onReject?.(rejected);
|
|
958
|
+
if (!picked.length) return;
|
|
959
|
+
p.onFiles?.(picked);
|
|
960
|
+
const add: UploadFile[] = picked.map((f) => ({
|
|
897
961
|
name: f.name,
|
|
898
962
|
size: f.size,
|
|
899
963
|
progress: 0,
|
|
964
|
+
file: f,
|
|
900
965
|
id: Math.random().toString(36).slice(2),
|
|
901
966
|
}));
|
|
902
|
-
if (!add.length) return;
|
|
903
967
|
const all = (p.multiple === false ? [] : files[0]).concat(add);
|
|
904
968
|
files[1](all);
|
|
969
|
+
if (!simulate) return;
|
|
905
970
|
add.forEach((f, k) => {
|
|
906
971
|
let t = 0;
|
|
907
972
|
const iv = setInterval(
|
|
@@ -913,6 +978,7 @@ export function FileDrop(p: FileDropProps) {
|
|
|
913
978
|
},
|
|
914
979
|
160 + k * 40,
|
|
915
980
|
);
|
|
981
|
+
timers.current.push(iv);
|
|
916
982
|
});
|
|
917
983
|
}
|
|
918
984
|
return (
|
|
@@ -949,8 +1015,8 @@ export function FileDrop(p: FileDropProps) {
|
|
|
949
1015
|
}}
|
|
950
1016
|
>
|
|
951
1017
|
<TintIcon icon="upload" tint={over[0] ? "mint" : "sky"} size={40} />
|
|
952
|
-
<b>{over[0] ?
|
|
953
|
-
<span>{p.hint ||
|
|
1018
|
+
<b>{over[0] ? t.dropToUpload : p.title || t.dropFiles}</b>
|
|
1019
|
+
<span>{p.hint || t.dropHint}</span>
|
|
954
1020
|
<input
|
|
955
1021
|
ref={inp}
|
|
956
1022
|
type="file"
|
|
@@ -971,19 +1037,30 @@ export function FileDrop(p: FileDropProps) {
|
|
|
971
1037
|
}}
|
|
972
1038
|
>
|
|
973
1039
|
{files[0].map((f) => {
|
|
974
|
-
|
|
1040
|
+
// untracked: a file that is only listed (the app uploads it, or not yet)
|
|
1041
|
+
const pending = tracked && f.progress < 100,
|
|
1042
|
+
done = tracked && f.progress >= 100;
|
|
975
1043
|
return (
|
|
976
1044
|
<div key={f.id} className="tf-upload">
|
|
977
|
-
<TintIcon icon="file" tint={
|
|
1045
|
+
<TintIcon icon="file" tint={pending ? "sky" : "mint"} />
|
|
978
1046
|
<div className="tf-upload-text">
|
|
979
1047
|
<b>{f.name}</b>
|
|
980
1048
|
<span>
|
|
981
1049
|
{done
|
|
982
|
-
?
|
|
983
|
-
:
|
|
1050
|
+
? t.uploaded(fsize(f.size, fm))
|
|
1051
|
+
: pending
|
|
1052
|
+
? fm.formatPercent(Math.round(f.progress)) + " · " + fsize(f.size, fm)
|
|
1053
|
+
: fsize(f.size, fm)}
|
|
984
1054
|
</span>
|
|
985
|
-
{
|
|
986
|
-
<span
|
|
1055
|
+
{pending && (
|
|
1056
|
+
<span
|
|
1057
|
+
className="tf-progress"
|
|
1058
|
+
role="progressbar"
|
|
1059
|
+
aria-label={f.name}
|
|
1060
|
+
aria-valuemin={0}
|
|
1061
|
+
aria-valuemax={100}
|
|
1062
|
+
aria-valuenow={Math.round(f.progress)}
|
|
1063
|
+
>
|
|
987
1064
|
<span
|
|
988
1065
|
style={{
|
|
989
1066
|
width: f.progress + "%",
|
|
@@ -993,11 +1070,12 @@ export function FileDrop(p: FileDropProps) {
|
|
|
993
1070
|
)}
|
|
994
1071
|
</div>
|
|
995
1072
|
<IconButton
|
|
996
|
-
icon={
|
|
997
|
-
label={
|
|
1073
|
+
icon={pending ? "x" : "trash"}
|
|
1074
|
+
label={pending ? t.cancelUpload : t.removeItem(f.name)}
|
|
998
1075
|
size="sm"
|
|
999
1076
|
onClick={() => {
|
|
1000
1077
|
files[1](files[0].filter((x) => x.id !== f.id));
|
|
1078
|
+
p.onRemove?.(f);
|
|
1001
1079
|
}}
|
|
1002
1080
|
/>
|
|
1003
1081
|
</div>
|
|
@@ -28,60 +28,47 @@ function slot(
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
/** Every native `<button>` attribute, on top of the documented props. */
|
|
32
|
+
type NativeRest<P> = P & Omit<React.ComponentPropsWithRef<"button">, keyof P>;
|
|
33
|
+
|
|
34
|
+
export function Button(props: NativeRest<ButtonProps>) {
|
|
35
|
+
const { variant, size, icon, iconRight, asChild, className, children, type, ...native } = props;
|
|
36
|
+
const v = variant || "primary";
|
|
37
|
+
const cls = cx("tf-btn", "tf-btn-" + v, size === "sm" && "tf-btn-sm", className);
|
|
34
38
|
const content = (inner: React.ReactNode) => (
|
|
35
39
|
<>
|
|
36
|
-
{
|
|
40
|
+
{icon && <Icon name={icon} size={15} />}
|
|
37
41
|
{inner}
|
|
38
|
-
{
|
|
42
|
+
{iconRight && <Icon name={iconRight} size={15} />}
|
|
39
43
|
</>
|
|
40
44
|
);
|
|
41
|
-
if (
|
|
42
|
-
const el = slot(React.Children.only(
|
|
45
|
+
if (asChild) {
|
|
46
|
+
const el = slot(React.Children.only(children), cls, native, content);
|
|
43
47
|
if (el) return el;
|
|
44
48
|
}
|
|
45
49
|
return (
|
|
46
|
-
<button
|
|
47
|
-
|
|
48
|
-
className={cls}
|
|
49
|
-
style={p.style}
|
|
50
|
-
disabled={p.disabled}
|
|
51
|
-
onClick={p.onClick}
|
|
52
|
-
>
|
|
53
|
-
{content(p.children)}
|
|
50
|
+
<button {...native} type={type || "button"} className={cls}>
|
|
51
|
+
{content(children)}
|
|
54
52
|
</button>
|
|
55
53
|
);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
export function IconButton(
|
|
56
|
+
export function IconButton(props: NativeRest<IconButtonProps>) {
|
|
57
|
+
const { icon, label, variant, size, asChild, className, children, type, ...native } = props;
|
|
59
58
|
const cls = cx(
|
|
60
59
|
"tf-iconbtn",
|
|
61
|
-
"tf-iconbtn-" + (
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
"tf-iconbtn-" + (variant || "soft"),
|
|
61
|
+
size && "tf-iconbtn-" + size,
|
|
62
|
+
className,
|
|
64
63
|
);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
cls,
|
|
69
|
-
{ "aria-label": p.label, title: p.label, style: p.style },
|
|
70
|
-
() => <Icon name={p.icon} />,
|
|
71
|
-
);
|
|
64
|
+
const named = { "aria-label": label, title: label, ...native };
|
|
65
|
+
if (asChild && children) {
|
|
66
|
+
const el = slot(React.Children.only(children), cls, named, () => <Icon name={icon} />);
|
|
72
67
|
if (el) return el;
|
|
73
68
|
}
|
|
74
69
|
return (
|
|
75
|
-
<button
|
|
76
|
-
|
|
77
|
-
aria-label={p.label}
|
|
78
|
-
title={p.label}
|
|
79
|
-
className={cls}
|
|
80
|
-
style={p.style}
|
|
81
|
-
disabled={p.disabled}
|
|
82
|
-
onClick={p.onClick}
|
|
83
|
-
>
|
|
84
|
-
<Icon name={p.icon} />
|
|
70
|
+
<button {...named} type={type || "button"} className={cls}>
|
|
71
|
+
<Icon name={icon} />
|
|
85
72
|
</button>
|
|
86
73
|
);
|
|
87
74
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cx } from "../cx.js";
|
|
3
|
+
import { useStrings } from "../context.js";
|
|
3
4
|
import { Icon } from "../Icon.js";
|
|
4
5
|
import { useId } from "../hooks.js";
|
|
5
6
|
import { useFieldError } from "./form.js";
|
|
@@ -127,13 +128,14 @@ export function Textarea(p: TextareaProps) {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
export function SearchField(p: SearchFieldProps) {
|
|
131
|
+
const t = useStrings();
|
|
130
132
|
return (
|
|
131
133
|
<label className={cx("tf-search", p.className)} style={p.style}>
|
|
132
134
|
<Icon name="search" size={15} />
|
|
133
135
|
<input
|
|
134
136
|
type="search"
|
|
135
|
-
placeholder={p.placeholder ||
|
|
136
|
-
aria-label={p.placeholder ||
|
|
137
|
+
placeholder={p.placeholder || t.search}
|
|
138
|
+
aria-label={p.label || p.placeholder || t.search}
|
|
137
139
|
value={p.value}
|
|
138
140
|
onChange={p.onChange}
|
|
139
141
|
/>
|
package/src/components/mail.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { useStrings } from "../context.js";
|
|
4
|
+
import { MAIL_STRINGS } from "../strings.js";
|
|
3
5
|
import { Icon } from "../Icon.js";
|
|
4
6
|
import { cx } from "../cx.js";
|
|
5
7
|
import { useControlled } from "../hooks.js";
|
|
@@ -20,6 +22,7 @@ import type {
|
|
|
20
22
|
|
|
21
23
|
/* ── Mail ── */
|
|
22
24
|
export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
25
|
+
const t = useStrings(MAIL_STRINGS);
|
|
23
26
|
const q = React.useState(""),
|
|
24
27
|
list = (p.accounts || []).filter(
|
|
25
28
|
(a) => !q[0] || a.address.toLowerCase().indexOf(q[0].toLowerCase()) > -1,
|
|
@@ -38,9 +41,9 @@ export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
|
38
41
|
>
|
|
39
42
|
<span className="tf-option-text">
|
|
40
43
|
<b className={cur ? "tf-mono" : undefined}>
|
|
41
|
-
{cur ? cur.address : p.allLabel ||
|
|
44
|
+
{cur ? cur.address : p.allLabel || t.allInboxes}
|
|
42
45
|
</b>
|
|
43
|
-
<small>{cur ? cur.domain :
|
|
46
|
+
<small>{cur ? cur.domain : t.addressCount((p.accounts || []).length)}</small>
|
|
44
47
|
</span>
|
|
45
48
|
<Icon name="chevronDown" size={14} />
|
|
46
49
|
</button>
|
|
@@ -54,7 +57,7 @@ export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
|
54
57
|
}}
|
|
55
58
|
>
|
|
56
59
|
<SearchField
|
|
57
|
-
placeholder=
|
|
60
|
+
placeholder={t.findAddress}
|
|
58
61
|
value={q[0]}
|
|
59
62
|
onChange={(e) => {
|
|
60
63
|
q[1](e.target.value);
|
|
@@ -70,7 +73,7 @@ export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
|
70
73
|
}}
|
|
71
74
|
>
|
|
72
75
|
<Icon name="inbox" size={15} />
|
|
73
|
-
<span className="tf-option-text">{p.allLabel ||
|
|
76
|
+
<span className="tf-option-text">{p.allLabel || t.allInboxes}</span>
|
|
74
77
|
{p.unread != null && <span className="tf-nav-count">{p.unread}</span>}
|
|
75
78
|
</button>
|
|
76
79
|
{list.map((a) => (
|
|
@@ -98,6 +101,7 @@ export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
export function InboxList(p: InboxListProps) {
|
|
104
|
+
const t = useStrings(MAIL_STRINGS);
|
|
101
105
|
return (
|
|
102
106
|
<div className="tf-inboxlist" role="list">
|
|
103
107
|
{(p.items || []).map((m) => (
|
|
@@ -113,7 +117,7 @@ export function InboxList(p: InboxListProps) {
|
|
|
113
117
|
<span
|
|
114
118
|
className="tf-mailrow-dot"
|
|
115
119
|
role={m.unread ? "img" : undefined}
|
|
116
|
-
aria-label={m.unread ?
|
|
120
|
+
aria-label={m.unread ? t.unread : undefined}
|
|
117
121
|
/>
|
|
118
122
|
<span className="tf-mailrow-main">
|
|
119
123
|
<span className="tf-mailrow-top">
|
|
@@ -194,13 +198,14 @@ export function MailThread(p: MailThreadProps) {
|
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
export function SignatureEditor(p: SignatureEditorProps) {
|
|
201
|
+
const t = useStrings(MAIL_STRINGS);
|
|
197
202
|
const v = useControlled<Record<string, string>>(p.value, p.defaultValue || {}, p.onChange),
|
|
198
203
|
fields = p.fields || [
|
|
199
|
-
{ key: "name", label:
|
|
200
|
-
{ key: "role", label:
|
|
201
|
-
{ key: "company", label:
|
|
202
|
-
{ key: "phone", label:
|
|
203
|
-
{ key: "website", label:
|
|
204
|
+
{ key: "name", label: t.colName },
|
|
205
|
+
{ key: "role", label: t.role },
|
|
206
|
+
{ key: "company", label: t.company },
|
|
207
|
+
{ key: "phone", label: t.phone },
|
|
208
|
+
{ key: "website", label: t.website },
|
|
204
209
|
];
|
|
205
210
|
function set(k: string, x: string) {
|
|
206
211
|
const n: Record<string, string> = Object.assign({}, v[0]);
|
|
@@ -234,7 +239,7 @@ export function SignatureEditor(p: SignatureEditorProps) {
|
|
|
234
239
|
padding: 0,
|
|
235
240
|
}}
|
|
236
241
|
>
|
|
237
|
-
{p.previewLabel ||
|
|
242
|
+
{p.previewLabel || t.preview}
|
|
238
243
|
</span>
|
|
239
244
|
<div className="tf-paper tf-sig-paper">
|
|
240
245
|
<p>—</p>
|