@twentyfirstmedia/ui-core 0.5.0 → 0.6.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 +31 -21
- package/dist/components/admin.js.map +1 -1
- package/dist/components/agents.d.ts.map +1 -1
- package/dist/components/agents.js +21 -18
- package/dist/components/agents.js.map +1 -1
- package/dist/components/app-chrome.d.ts.map +1 -1
- package/dist/components/app-chrome.js +5 -5
- package/dist/components/app-chrome.js.map +1 -1
- package/dist/components/approvals.d.ts.map +1 -1
- package/dist/components/approvals.js +2 -2
- package/dist/components/approvals.js.map +1 -1
- package/dist/components/building-blocks.d.ts.map +1 -1
- package/dist/components/building-blocks.js +13 -10
- package/dist/components/building-blocks.js.map +1 -1
- package/dist/components/charts.d.ts +2 -2
- package/dist/components/charts.d.ts.map +1 -1
- package/dist/components/charts.js +14 -11
- package/dist/components/charts.js.map +1 -1
- package/dist/components/chat.d.ts +2 -2
- package/dist/components/chat.d.ts.map +1 -1
- package/dist/components/chat.js +22 -18
- package/dist/components/chat.js.map +1 -1
- package/dist/components/commerce.d.ts.map +1 -1
- package/dist/components/commerce.js +20 -16
- package/dist/components/commerce.js.map +1 -1
- package/dist/components/data-display.d.ts +0 -5
- package/dist/components/data-display.d.ts.map +1 -1
- package/dist/components/data-display.js +41 -22
- package/dist/components/data-display.js.map +1 -1
- package/dist/components/data-table.d.ts +2 -2
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +3 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/developer.d.ts.map +1 -1
- package/dist/components/developer.js +7 -6
- package/dist/components/developer.js.map +1 -1
- package/dist/components/feedback.d.ts.map +1 -1
- package/dist/components/feedback.js +5 -4
- package/dist/components/feedback.js.map +1 -1
- package/dist/components/forms.d.ts.map +1 -1
- package/dist/components/forms.js +63 -21
- package/dist/components/forms.js.map +1 -1
- package/dist/components/foundation.d.ts.map +1 -1
- package/dist/components/foundation.js +13 -9
- package/dist/components/foundation.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/inputs.d.ts.map +1 -1
- package/dist/components/inputs.js +33 -8
- package/dist/components/inputs.js.map +1 -1
- package/dist/components/mail.d.ts.map +1 -1
- package/dist/components/mail.js +5 -5
- package/dist/components/mail.js.map +1 -1
- package/dist/components/media.d.ts.map +1 -1
- package/dist/components/media.js +62 -11
- package/dist/components/media.js.map +1 -1
- package/dist/components/navigation.d.ts +3 -1
- package/dist/components/navigation.d.ts.map +1 -1
- package/dist/components/navigation.js +287 -20
- package/dist/components/navigation.js.map +1 -1
- package/dist/components/operations.d.ts.map +1 -1
- package/dist/components/operations.js +10 -10
- package/dist/components/operations.js.map +1 -1
- package/dist/components/overlays.d.ts.map +1 -1
- package/dist/components/overlays.js +2 -2
- package/dist/components/overlays.js.map +1 -1
- package/dist/components/previews.js +2 -2
- package/dist/components/previews.js.map +1 -1
- package/dist/components/primitives.js +2 -2
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/qr-code.d.ts +8 -0
- package/dist/components/qr-code.d.ts.map +1 -0
- package/dist/components/qr-code.js +18 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/real-estate.d.ts.map +1 -1
- package/dist/components/real-estate.js +5 -5
- 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 +1 -1
- package/dist/components/rich-text-editor.js.map +1 -1
- package/dist/components/select-menu.js +1 -1
- package/dist/components/select-menu.js.map +1 -1
- package/dist/components/shell.d.ts +0 -4
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +100 -7
- package/dist/components/shell.js.map +1 -1
- package/dist/components/site.d.ts.map +1 -1
- package/dist/components/site.js +8 -6
- package/dist/components/site.js.map +1 -1
- package/dist/components/status.d.ts.map +1 -1
- package/dist/components/status.js +2 -2
- package/dist/components/status.js.map +1 -1
- package/dist/components/system.d.ts.map +1 -1
- package/dist/components/system.js +3 -2
- package/dist/components/system.js.map +1 -1
- package/dist/components/types.d.ts +257 -18
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +15 -12
- package/dist/components/workspace.js.map +1 -1
- package/dist/qr.d.ts +14 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +298 -0
- package/dist/qr.js.map +1 -0
- package/dist/strings.d.ts +30 -4
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +15 -0
- package/dist/strings.js.map +1 -1
- package/dist/themes.css +2345 -176
- package/package.json +2 -1
- package/src/components/admin.tsx +66 -23
- package/src/components/agents.tsx +22 -20
- package/src/components/app-chrome.tsx +9 -5
- package/src/components/approvals.tsx +3 -1
- package/src/components/building-blocks.tsx +24 -13
- package/src/components/charts.tsx +17 -12
- package/src/components/chat.tsx +54 -31
- package/src/components/commerce.tsx +58 -24
- package/src/components/data-display.tsx +106 -38
- package/src/components/data-table.tsx +11 -7
- package/src/components/developer.tsx +15 -6
- package/src/components/feedback.tsx +5 -3
- package/src/components/forms.tsx +101 -23
- package/src/components/foundation.tsx +9 -5
- package/src/components/index.ts +10 -1
- package/src/components/inputs.tsx +44 -7
- package/src/components/mail.tsx +7 -3
- package/src/components/media.tsx +88 -16
- package/src/components/navigation.tsx +651 -71
- package/src/components/operations.tsx +17 -8
- package/src/components/overlays.tsx +7 -2
- package/src/components/previews.tsx +2 -2
- package/src/components/primitives.tsx +2 -2
- package/src/components/qr-code.tsx +30 -0
- package/src/components/real-estate.tsx +7 -4
- package/src/components/rich-text-editor.tsx +7 -1
- package/src/components/select-menu.tsx +1 -1
- package/src/components/shell.tsx +237 -11
- package/src/components/site.tsx +18 -4
- package/src/components/status.tsx +2 -0
- package/src/components/system.tsx +3 -2
- package/src/components/types.ts +261 -18
- package/src/components/workspace.tsx +37 -15
- package/src/qr.ts +300 -0
- package/src/strings.ts +56 -1
package/src/components/forms.tsx
CHANGED
|
@@ -10,6 +10,11 @@ import { Icon } from "../Icon.js";
|
|
|
10
10
|
import { useControlled, useId, useOutside } from "../hooks.js";
|
|
11
11
|
import { Avatar, IconButton, TintIcon } from "./foundation.js";
|
|
12
12
|
import { Popover } from "./overlays.js";
|
|
13
|
+
import { registerIcons } from "../icons.js";
|
|
14
|
+
import { eyeOff } from "../icon-library.js";
|
|
15
|
+
|
|
16
|
+
// the password field's "hide" state needs this one library icon
|
|
17
|
+
registerIcons({ eyeOff });
|
|
13
18
|
import type {
|
|
14
19
|
CalendarProps,
|
|
15
20
|
ComboboxProps,
|
|
@@ -69,12 +74,30 @@ export function FormField(p: FormFieldProps) {
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export function TextField(p: TextFieldProps) {
|
|
77
|
+
const t = useStrings(FORMS_STRINGS);
|
|
78
|
+
const [shown, setShown] = React.useState(false);
|
|
79
|
+
const password = p.type === "password";
|
|
80
|
+
const action =
|
|
81
|
+
p.action ??
|
|
82
|
+
(password && p.reveal !== false ? (
|
|
83
|
+
<IconButton
|
|
84
|
+
icon={shown ? "eyeOff" : "eye"}
|
|
85
|
+
label={shown ? t.hidePassword : t.showPassword}
|
|
86
|
+
size="sm"
|
|
87
|
+
variant="ghost"
|
|
88
|
+
aria-pressed={shown}
|
|
89
|
+
disabled={p.disabled}
|
|
90
|
+
onClick={() => setShown(!shown)}
|
|
91
|
+
/>
|
|
92
|
+
) : null);
|
|
72
93
|
const auto = useId("tf");
|
|
73
94
|
const id = p.id || auto;
|
|
74
95
|
const fromForm = useFieldError(p.name);
|
|
75
96
|
const error = p.error || fromForm;
|
|
76
97
|
return (
|
|
77
98
|
<FormField
|
|
99
|
+
style={p.style}
|
|
100
|
+
className={p.className}
|
|
78
101
|
id={id}
|
|
79
102
|
label={p.label}
|
|
80
103
|
required={p.required}
|
|
@@ -88,6 +111,7 @@ export function TextField(p: TextFieldProps) {
|
|
|
88
111
|
"tf-inputwrap",
|
|
89
112
|
p.icon && "has-icon",
|
|
90
113
|
(error || p.success || p.suffix) && "has-suffix",
|
|
114
|
+
action && "has-action",
|
|
91
115
|
)}
|
|
92
116
|
>
|
|
93
117
|
{p.icon && <Icon name={p.icon} size={15} className="tf-input-icon" />}
|
|
@@ -95,12 +119,13 @@ export function TextField(p: TextFieldProps) {
|
|
|
95
119
|
id={id}
|
|
96
120
|
name={p.name}
|
|
97
121
|
className={cx("tf-input", p.success && "is-valid")}
|
|
98
|
-
type={p.type || "text"}
|
|
122
|
+
type={password && shown ? "text" : p.type || "text"}
|
|
99
123
|
placeholder={p.placeholder}
|
|
100
124
|
value={p.value}
|
|
101
125
|
defaultValue={p.defaultValue}
|
|
102
126
|
onChange={p.onChange}
|
|
103
127
|
onBlur={p.onBlur}
|
|
128
|
+
onKeyDown={p.onKeyDown}
|
|
104
129
|
required={p.required}
|
|
105
130
|
disabled={p.disabled}
|
|
106
131
|
readOnly={p.readOnly}
|
|
@@ -127,6 +152,7 @@ export function TextField(p: TextFieldProps) {
|
|
|
127
152
|
) : (
|
|
128
153
|
p.suffix && <span className="tf-input-suffix">{p.suffix}</span>
|
|
129
154
|
)}
|
|
155
|
+
{action && <span className="tf-input-action">{action}</span>}
|
|
130
156
|
</span>
|
|
131
157
|
</FormField>
|
|
132
158
|
);
|
|
@@ -139,7 +165,7 @@ export function RadioGroup(p: RadioGroupProps) {
|
|
|
139
165
|
const name = useId("rg"),
|
|
140
166
|
v = useControlled(p.value, p.defaultValue, p.onChange);
|
|
141
167
|
return (
|
|
142
|
-
<fieldset className={cx("tf-radiogroup", p.cards && "cards", p.className)}>
|
|
168
|
+
<fieldset style={p.style} className={cx("tf-radiogroup", p.cards && "cards", p.className)}>
|
|
143
169
|
{p.label && <legend className="tf-field-label">{p.label}</legend>}
|
|
144
170
|
<div className="tf-radiogroup-items">
|
|
145
171
|
{(p.options || []).map((o) => {
|
|
@@ -189,6 +215,7 @@ export function Segmented(p: SegmentedProps) {
|
|
|
189
215
|
name = useId("seg");
|
|
190
216
|
return (
|
|
191
217
|
<div
|
|
218
|
+
style={p.style}
|
|
192
219
|
className={cx("tf-seg", p.size === "sm" && "sm", p.block && "block", p.className)}
|
|
193
220
|
role="radiogroup"
|
|
194
221
|
aria-label={p.label}
|
|
@@ -204,16 +231,20 @@ export function Segmented(p: SegmentedProps) {
|
|
|
204
231
|
on = v[0] === opt.value;
|
|
205
232
|
return (
|
|
206
233
|
<label key={opt.value} className={cx("tf-seg-item", on && "on")}>
|
|
207
|
-
<input
|
|
208
|
-
type="radio"
|
|
209
|
-
name={name}
|
|
210
|
-
checked={on}
|
|
211
|
-
onChange={() => {
|
|
212
|
-
v[1](opt.value);
|
|
213
|
-
}}
|
|
214
|
-
/>
|
|
215
234
|
{opt.icon && <Icon name={opt.icon} size={14} />}
|
|
216
|
-
{
|
|
235
|
+
{/* the radio covers the whole segment (positioned on the label); inside the text's
|
|
236
|
+
span, a click on the text and a click on the radio hit the same target */}
|
|
237
|
+
<span className="tf-seg-label">
|
|
238
|
+
<input
|
|
239
|
+
type="radio"
|
|
240
|
+
name={name}
|
|
241
|
+
checked={on}
|
|
242
|
+
onChange={() => {
|
|
243
|
+
v[1](opt.value);
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
{opt.label}
|
|
247
|
+
</span>
|
|
217
248
|
</label>
|
|
218
249
|
);
|
|
219
250
|
})}
|
|
@@ -301,6 +332,7 @@ export function Combobox(p: ComboboxProps) {
|
|
|
301
332
|
};
|
|
302
333
|
return (
|
|
303
334
|
<FormField
|
|
335
|
+
style={p.style}
|
|
304
336
|
id={id}
|
|
305
337
|
label={p.label}
|
|
306
338
|
required={p.required}
|
|
@@ -442,7 +474,14 @@ export function TagInput(p: TagInputProps) {
|
|
|
442
474
|
q[1]("");
|
|
443
475
|
}
|
|
444
476
|
return (
|
|
445
|
-
<FormField
|
|
477
|
+
<FormField
|
|
478
|
+
style={p.style}
|
|
479
|
+
className={p.className}
|
|
480
|
+
id={id}
|
|
481
|
+
label={p.label}
|
|
482
|
+
hint={p.hint}
|
|
483
|
+
error={p.error}
|
|
484
|
+
>
|
|
446
485
|
<div
|
|
447
486
|
className="tf-combo-box tf-tags"
|
|
448
487
|
onClick={(e) => {
|
|
@@ -562,7 +601,7 @@ export function Calendar(p: CalendarProps) {
|
|
|
562
601
|
m[1](new Date(first.getFullYear(), first.getMonth() + n, 1));
|
|
563
602
|
}
|
|
564
603
|
return (
|
|
565
|
-
<div className="tf-cal">
|
|
604
|
+
<div style={p.style} className={cx("tf-cal", p.className)}>
|
|
566
605
|
<div className="tf-cal-head">
|
|
567
606
|
<IconButton
|
|
568
607
|
icon="chevronLeft"
|
|
@@ -671,6 +710,7 @@ export function DatePicker(p: DatePickerProps) {
|
|
|
671
710
|
: fmtD(v[0] as Date | null, locale);
|
|
672
711
|
return (
|
|
673
712
|
<FormField
|
|
713
|
+
style={p.style}
|
|
674
714
|
id={id}
|
|
675
715
|
label={p.label}
|
|
676
716
|
required={p.required}
|
|
@@ -756,7 +796,14 @@ export function NumberInput(p: NumberInputProps) {
|
|
|
756
796
|
v[1](Math.round(n * 1000) / 1000);
|
|
757
797
|
}
|
|
758
798
|
return (
|
|
759
|
-
<FormField
|
|
799
|
+
<FormField
|
|
800
|
+
style={p.style}
|
|
801
|
+
className={p.className}
|
|
802
|
+
id={id}
|
|
803
|
+
label={p.label}
|
|
804
|
+
hint={p.hint}
|
|
805
|
+
error={p.error}
|
|
806
|
+
>
|
|
760
807
|
<div className="tf-number">
|
|
761
808
|
<IconButton
|
|
762
809
|
icon="minus"
|
|
@@ -811,7 +858,7 @@ export function Slider(p: SliderProps) {
|
|
|
811
858
|
const pct = ((v[0] - min) / (max - min)) * 100,
|
|
812
859
|
f = p.format || String;
|
|
813
860
|
return (
|
|
814
|
-
<FormField id={id} label={p.label} hint={p.hint}>
|
|
861
|
+
<FormField style={p.style} className={p.className} id={id} label={p.label} hint={p.hint}>
|
|
815
862
|
<div className="tf-slider">
|
|
816
863
|
<div className="tf-slider-track">
|
|
817
864
|
<input
|
|
@@ -856,6 +903,16 @@ export function OtpInput(p: OtpInputProps) {
|
|
|
856
903
|
const n = p.length || 6,
|
|
857
904
|
st = React.useState(Array(n).fill("")),
|
|
858
905
|
refs = React.useRef<(HTMLInputElement | null)[]>([]);
|
|
906
|
+
const reset = React.useCallback(() => {
|
|
907
|
+
st[1](Array(n).fill(""));
|
|
908
|
+
refs.current[0]?.focus();
|
|
909
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- st[1] is stable
|
|
910
|
+
}, [n]);
|
|
911
|
+
React.useImperativeHandle(p.ref, () => ({ reset }), [reset]);
|
|
912
|
+
// a wrong code: empty the cells for the next try
|
|
913
|
+
React.useEffect(() => {
|
|
914
|
+
if (p.error && p.clearOnError !== false) reset();
|
|
915
|
+
}, [p.error, p.clearOnError, reset]);
|
|
859
916
|
function set(i: number, ch: string) {
|
|
860
917
|
const nx = st[0].slice();
|
|
861
918
|
nx[i] = ch;
|
|
@@ -865,7 +922,8 @@ export function OtpInput(p: OtpInputProps) {
|
|
|
865
922
|
}
|
|
866
923
|
return (
|
|
867
924
|
<div
|
|
868
|
-
|
|
925
|
+
style={p.style}
|
|
926
|
+
className={cx("tf-otp", p.error && "invalid", p.className)}
|
|
869
927
|
role="group"
|
|
870
928
|
aria-label={p.label || t.verificationCode}
|
|
871
929
|
>
|
|
@@ -932,6 +990,19 @@ function acceptsFile(file: { name: string; type: string }, accept?: string): boo
|
|
|
932
990
|
);
|
|
933
991
|
}
|
|
934
992
|
|
|
993
|
+
/** A hidden file input that carries the listed files into a surrounding form. */
|
|
994
|
+
function FormFiles(p: { name: string; files: UploadFile[] }) {
|
|
995
|
+
const ref = React.useRef<HTMLInputElement>(null);
|
|
996
|
+
React.useEffect(() => {
|
|
997
|
+
const el = ref.current;
|
|
998
|
+
if (!el || typeof DataTransfer === "undefined") return;
|
|
999
|
+
const dt = new DataTransfer();
|
|
1000
|
+
p.files.forEach((f) => f.file && dt.items.add(f.file));
|
|
1001
|
+
el.files = dt.files;
|
|
1002
|
+
}, [p.files]);
|
|
1003
|
+
return <input ref={ref} type="file" name={p.name} multiple hidden tabIndex={-1} />;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
935
1006
|
export function FileDrop(p: FileDropProps) {
|
|
936
1007
|
const t = useStrings(FORMS_STRINGS);
|
|
937
1008
|
const fm = useFormat();
|
|
@@ -955,8 +1026,11 @@ export function FileDrop(p: FileDropProps) {
|
|
|
955
1026
|
picked = picked.filter((f) => acceptsFile(f, p.accept));
|
|
956
1027
|
if (p.multiple === false) picked = picked.slice(0, 1);
|
|
957
1028
|
if (rejected.length) p.onReject?.(rejected);
|
|
1029
|
+
if (!picked.length || p.disabled) return;
|
|
1030
|
+
const kept = p.onFiles?.(picked);
|
|
1031
|
+
if (kept === false) return;
|
|
1032
|
+
if (Array.isArray(kept)) picked = kept;
|
|
958
1033
|
if (!picked.length) return;
|
|
959
|
-
p.onFiles?.(picked);
|
|
960
1034
|
const add: UploadFile[] = picked.map((f) => ({
|
|
961
1035
|
name: f.name,
|
|
962
1036
|
size: f.size,
|
|
@@ -983,27 +1057,29 @@ export function FileDrop(p: FileDropProps) {
|
|
|
983
1057
|
}
|
|
984
1058
|
return (
|
|
985
1059
|
<div
|
|
986
|
-
className="tf-col"
|
|
1060
|
+
className={cx("tf-col", p.className)}
|
|
987
1061
|
style={{
|
|
988
1062
|
gap: 12,
|
|
1063
|
+
...p.style,
|
|
989
1064
|
}}
|
|
990
1065
|
>
|
|
991
1066
|
<div
|
|
992
|
-
className={cx("tf-drop", over[0] && "over")}
|
|
1067
|
+
className={cx("tf-drop", over[0] && "over", p.disabled && "disabled")}
|
|
993
1068
|
role="button"
|
|
994
|
-
tabIndex={0}
|
|
1069
|
+
tabIndex={p.disabled ? -1 : 0}
|
|
1070
|
+
aria-disabled={p.disabled || undefined}
|
|
995
1071
|
onClick={() => {
|
|
996
|
-
inp.current?.click();
|
|
1072
|
+
if (!p.disabled) inp.current?.click();
|
|
997
1073
|
}}
|
|
998
1074
|
onKeyDown={(e) => {
|
|
999
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
1075
|
+
if (!p.disabled && (e.key === "Enter" || e.key === " ")) {
|
|
1000
1076
|
e.preventDefault();
|
|
1001
1077
|
inp.current?.click();
|
|
1002
1078
|
}
|
|
1003
1079
|
}}
|
|
1004
1080
|
onDragOver={(e) => {
|
|
1005
1081
|
e.preventDefault();
|
|
1006
|
-
over[1](true);
|
|
1082
|
+
if (!p.disabled) over[1](true);
|
|
1007
1083
|
}}
|
|
1008
1084
|
onDragLeave={() => {
|
|
1009
1085
|
over[1](false);
|
|
@@ -1023,11 +1099,13 @@ export function FileDrop(p: FileDropProps) {
|
|
|
1023
1099
|
hidden
|
|
1024
1100
|
multiple={p.multiple !== false}
|
|
1025
1101
|
accept={p.accept}
|
|
1102
|
+
disabled={p.disabled}
|
|
1026
1103
|
onChange={(e) => {
|
|
1027
1104
|
take(e.target.files);
|
|
1028
1105
|
e.target.value = "";
|
|
1029
1106
|
}}
|
|
1030
1107
|
/>
|
|
1108
|
+
{p.name && <FormFiles name={p.name} files={files[0]} />}
|
|
1031
1109
|
</div>
|
|
1032
1110
|
{files[0].length > 0 && (
|
|
1033
1111
|
<div
|
|
@@ -76,6 +76,7 @@ export function IconButton(props: NativeRest<IconButtonProps>) {
|
|
|
76
76
|
export function Chip(p: ChipProps) {
|
|
77
77
|
return (
|
|
78
78
|
<button
|
|
79
|
+
style={p.style}
|
|
79
80
|
type="button"
|
|
80
81
|
className={cx("tf-chip", p.className)}
|
|
81
82
|
aria-pressed={p.selected ? "true" : "false"}
|
|
@@ -101,6 +102,7 @@ export function Badge(p: BadgeProps) {
|
|
|
101
102
|
const tone = (p.tone && TONE_ALIAS[p.tone]) || p.tone || "neutral";
|
|
102
103
|
return (
|
|
103
104
|
<span
|
|
105
|
+
style={p.style}
|
|
104
106
|
className={cx(
|
|
105
107
|
"tf-badge",
|
|
106
108
|
"tf-badge-" + tone,
|
|
@@ -120,14 +122,15 @@ export function TintIcon(p: TintIconProps) {
|
|
|
120
122
|
className={cx("tf-tinticon", "tf-t-" + (p.tint || "plain"), p.className)}
|
|
121
123
|
aria-hidden={p.label ? undefined : true}
|
|
122
124
|
title={p.label}
|
|
123
|
-
style={
|
|
124
|
-
p.size
|
|
125
|
+
style={{
|
|
126
|
+
...(p.size
|
|
125
127
|
? {
|
|
126
128
|
width: p.size,
|
|
127
129
|
height: p.size,
|
|
128
130
|
}
|
|
129
|
-
: undefined
|
|
130
|
-
|
|
131
|
+
: undefined),
|
|
132
|
+
...p.style,
|
|
133
|
+
}}
|
|
131
134
|
>
|
|
132
135
|
<Icon name={p.icon} />
|
|
133
136
|
</span>
|
|
@@ -148,6 +151,7 @@ export function Avatar(p: AvatarProps) {
|
|
|
148
151
|
// a custom property, so a kit can recolour avatars (Pro keeps them monochrome)
|
|
149
152
|
["--av" as string]: "var(--" + t + ")",
|
|
150
153
|
fontSize: Math.round(size * 0.36),
|
|
154
|
+
...p.style,
|
|
151
155
|
}}
|
|
152
156
|
title={p.name}
|
|
153
157
|
>
|
|
@@ -161,7 +165,7 @@ export function AvatarStack(p: AvatarStackProps) {
|
|
|
161
165
|
max = p.max || 4,
|
|
162
166
|
extra = names.length - max;
|
|
163
167
|
return (
|
|
164
|
-
<span className="tf-avatar-stack">
|
|
168
|
+
<span style={p.style} className={cx("tf-avatar-stack", p.className)}>
|
|
165
169
|
{names.slice(0, max).map((n) => (
|
|
166
170
|
<Avatar key={n} name={n} size={p.size || 28} />
|
|
167
171
|
))}
|
package/src/components/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
TwoFactorSetup,
|
|
22
22
|
ColorField,
|
|
23
23
|
} from "./admin.js";
|
|
24
|
+
export { QrCode } from "./qr-code.js";
|
|
24
25
|
export {
|
|
25
26
|
AgentCard,
|
|
26
27
|
AgentDetails,
|
|
@@ -157,7 +158,15 @@ export {
|
|
|
157
158
|
AttachmentPreview,
|
|
158
159
|
} from "./mail.js";
|
|
159
160
|
export { ImageVariants, Gallery, BeforeAfter } from "./media.js";
|
|
160
|
-
export {
|
|
161
|
+
export {
|
|
162
|
+
Tabs,
|
|
163
|
+
Breadcrumb,
|
|
164
|
+
Pagination,
|
|
165
|
+
Menubar,
|
|
166
|
+
NavigationMenu,
|
|
167
|
+
Sidebar,
|
|
168
|
+
UserMenu,
|
|
169
|
+
} from "./navigation.js";
|
|
161
170
|
export {
|
|
162
171
|
ConnectionCard,
|
|
163
172
|
ConnectionBanner,
|
|
@@ -129,24 +129,60 @@ export function Textarea(p: TextareaProps) {
|
|
|
129
129
|
|
|
130
130
|
export function SearchField(p: SearchFieldProps) {
|
|
131
131
|
const t = useStrings();
|
|
132
|
+
const input = React.useRef<HTMLInputElement>(null);
|
|
133
|
+
// ⌘K / Ctrl+K: the last character of the hint is the key
|
|
134
|
+
const key = p.bindShortcut && p.shortcut ? p.shortcut.trim().slice(-1).toLowerCase() : "";
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (!key) return;
|
|
137
|
+
const k = (e: KeyboardEvent) => {
|
|
138
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey || e.key.toLowerCase() !== key) return;
|
|
139
|
+
const el = e.target as HTMLElement | null;
|
|
140
|
+
const typing =
|
|
141
|
+
!!el &&
|
|
142
|
+
el !== input.current &&
|
|
143
|
+
(el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName));
|
|
144
|
+
if (typing) return;
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
input.current?.focus();
|
|
147
|
+
input.current?.select();
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("keydown", k);
|
|
150
|
+
return () => document.removeEventListener("keydown", k);
|
|
151
|
+
}, [key]);
|
|
132
152
|
return (
|
|
133
153
|
<label className={cx("tf-search", p.className)} style={p.style}>
|
|
134
154
|
<Icon name="search" size={15} />
|
|
135
155
|
<input
|
|
156
|
+
ref={input}
|
|
136
157
|
type="search"
|
|
158
|
+
id={p.id}
|
|
159
|
+
name={p.name}
|
|
160
|
+
autoFocus={p.autoFocus}
|
|
137
161
|
placeholder={p.placeholder || t.search}
|
|
138
162
|
aria-label={p.label || p.placeholder || t.search}
|
|
163
|
+
aria-keyshortcuts={
|
|
164
|
+
key ? `Meta+${key.toUpperCase()} Control+${key.toUpperCase()}` : undefined
|
|
165
|
+
}
|
|
139
166
|
value={p.value}
|
|
167
|
+
defaultValue={p.defaultValue}
|
|
140
168
|
onChange={p.onChange}
|
|
169
|
+
onKeyDown={p.onKeyDown}
|
|
170
|
+
onFocus={p.onFocus}
|
|
171
|
+
onBlur={p.onBlur}
|
|
172
|
+
{...p.inputProps}
|
|
141
173
|
/>
|
|
142
|
-
{p.shortcut &&
|
|
174
|
+
{p.shortcut && (
|
|
175
|
+
<span className="tf-kbd" aria-hidden>
|
|
176
|
+
{p.shortcut}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
143
179
|
</label>
|
|
144
180
|
);
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
export function Checkbox(p: CheckboxProps) {
|
|
148
184
|
return (
|
|
149
|
-
<label className="tf-check">
|
|
185
|
+
<label style={p.style} className={cx("tf-check", p.className)}>
|
|
150
186
|
<span className="tf-checkbox">
|
|
151
187
|
<input
|
|
152
188
|
type="checkbox"
|
|
@@ -182,15 +218,16 @@ export function Checkbox(p: CheckboxProps) {
|
|
|
182
218
|
export function Switch(p: SwitchProps) {
|
|
183
219
|
return (
|
|
184
220
|
<label
|
|
185
|
-
className="tf-check"
|
|
186
|
-
style={
|
|
187
|
-
p.spread
|
|
221
|
+
className={cx("tf-check", p.className)}
|
|
222
|
+
style={{
|
|
223
|
+
...(p.spread
|
|
188
224
|
? {
|
|
189
225
|
justifyContent: "space-between",
|
|
190
226
|
width: "100%",
|
|
191
227
|
}
|
|
192
|
-
: undefined
|
|
193
|
-
|
|
228
|
+
: undefined),
|
|
229
|
+
...p.style,
|
|
230
|
+
}}
|
|
194
231
|
>
|
|
195
232
|
{p.spread && p.children}
|
|
196
233
|
<input
|
package/src/components/mail.tsx
CHANGED
|
@@ -30,6 +30,8 @@ export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
|
30
30
|
const cur = (p.accounts || []).find((a) => a.address === p.value);
|
|
31
31
|
return (
|
|
32
32
|
<Popover
|
|
33
|
+
style={p.style}
|
|
34
|
+
className={p.className}
|
|
33
35
|
width={320}
|
|
34
36
|
trigger={(s) => (
|
|
35
37
|
<button
|
|
@@ -103,7 +105,7 @@ export function AccountSwitcher(p: AccountSwitcherProps) {
|
|
|
103
105
|
export function InboxList(p: InboxListProps) {
|
|
104
106
|
const t = useStrings(MAIL_STRINGS);
|
|
105
107
|
return (
|
|
106
|
-
<div className="tf-inboxlist" role="list">
|
|
108
|
+
<div style={p.style} className={cx("tf-inboxlist", p.className)} role="list">
|
|
107
109
|
{(p.items || []).map((m) => (
|
|
108
110
|
<button
|
|
109
111
|
key={m.id}
|
|
@@ -149,7 +151,7 @@ export function MailThread(p: MailThreadProps) {
|
|
|
149
151
|
(p.messages || []).length ? [p.messages[p.messages.length - 1].id] : [],
|
|
150
152
|
);
|
|
151
153
|
return (
|
|
152
|
-
<div className="tf-thread">
|
|
154
|
+
<div style={p.style} className={cx("tf-thread", p.className)}>
|
|
153
155
|
{p.subject && <h3 className="tf-thread-subject">{p.subject}</h3>}
|
|
154
156
|
{(p.messages || []).map((m) => {
|
|
155
157
|
const o = open[0].indexOf(m.id) > -1;
|
|
@@ -213,7 +215,7 @@ export function SignatureEditor(p: SignatureEditorProps) {
|
|
|
213
215
|
v[1](n);
|
|
214
216
|
}
|
|
215
217
|
return (
|
|
216
|
-
<div className="tf-sig">
|
|
218
|
+
<div style={p.style} className={cx("tf-sig", p.className)}>
|
|
217
219
|
<div
|
|
218
220
|
className="tf-col"
|
|
219
221
|
style={{
|
|
@@ -257,6 +259,8 @@ export function SignatureEditor(p: SignatureEditorProps) {
|
|
|
257
259
|
export function AttachmentPreview(p: AttachmentPreviewProps) {
|
|
258
260
|
return (
|
|
259
261
|
<Drawer
|
|
262
|
+
style={p.style}
|
|
263
|
+
className={p.className}
|
|
260
264
|
title={p.name}
|
|
261
265
|
subtitle={p.meta}
|
|
262
266
|
onClose={p.onClose}
|
package/src/components/media.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useFormat, useStrings } from "../context.js";
|
|
4
4
|
import { MEDIA_STRINGS } from "../strings.js";
|
|
5
|
-
import { useControlled } from "../hooks.js";
|
|
5
|
+
import { moveFocus, useControlled } from "../hooks.js";
|
|
6
6
|
import { cx } from "../cx.js";
|
|
7
7
|
import { Icon } from "../Icon.js";
|
|
8
8
|
import { Portal } from "../portal.js";
|
|
@@ -23,7 +23,7 @@ export function ImageVariants(p: ImageVariantsProps) {
|
|
|
23
23
|
const sel = useControlled<string | number>(p.selected, p.variants?.[0]?.id ?? "", p.onSelect),
|
|
24
24
|
cur: Partial<ImageVariant> = (p.variants || []).find((v) => v.id === sel[0]) || {};
|
|
25
25
|
return (
|
|
26
|
-
<div className="tf-imgvar">
|
|
26
|
+
<div style={p.style} className={cx("tf-imgvar", p.className)}>
|
|
27
27
|
<div
|
|
28
28
|
className="tf-imgvar-stage"
|
|
29
29
|
style={
|
|
@@ -94,19 +94,52 @@ export function ImageVariants(p: ImageVariantsProps) {
|
|
|
94
94
|
export function Gallery(p: GalleryProps) {
|
|
95
95
|
const t = useStrings(MEDIA_STRINGS);
|
|
96
96
|
const items = p.items || [],
|
|
97
|
-
i =
|
|
97
|
+
i = useControlled(p.index, p.start || 0, p.onIndexChange),
|
|
98
98
|
full = React.useState(false),
|
|
99
99
|
n = items.length,
|
|
100
100
|
cur: GalleryItem = items[i[0]] || {};
|
|
101
|
+
const box = React.useRef<HTMLDivElement>(null),
|
|
102
|
+
thumbs = React.useRef<HTMLDivElement>(null),
|
|
103
|
+
opener = React.useRef<HTMLButtonElement>(null);
|
|
101
104
|
function go(d: number) {
|
|
102
105
|
i[1]((i[0] + d + n) % n);
|
|
103
106
|
}
|
|
107
|
+
const restore = React.useRef(false);
|
|
108
|
+
function close() {
|
|
109
|
+
full[1](false);
|
|
110
|
+
restore.current = true;
|
|
111
|
+
}
|
|
112
|
+
// the full-screen button comes back after the lightbox closes: focus returns to it
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
if (!full[0] && restore.current) {
|
|
115
|
+
restore.current = false;
|
|
116
|
+
opener.current?.focus();
|
|
117
|
+
}
|
|
118
|
+
}, [full[0]]);
|
|
119
|
+
// focus moves into the lightbox when it appears (the portal mounts a render later)
|
|
120
|
+
const attachBox = React.useCallback((n: HTMLDivElement | null) => {
|
|
121
|
+
box.current = n;
|
|
122
|
+
n?.querySelector<HTMLElement>("button")?.focus();
|
|
123
|
+
}, []);
|
|
104
124
|
React.useEffect(() => {
|
|
105
125
|
if (!full[0]) return;
|
|
126
|
+
const lb = box.current;
|
|
106
127
|
function k(e: KeyboardEvent) {
|
|
107
|
-
if (e.key === "Escape")
|
|
128
|
+
if (e.key === "Escape") close();
|
|
108
129
|
if (e.key === "ArrowRight") go(1);
|
|
109
130
|
if (e.key === "ArrowLeft") go(-1);
|
|
131
|
+
if (e.key === "Tab" && lb) {
|
|
132
|
+
const els = Array.from(lb.querySelectorAll<HTMLElement>("button"));
|
|
133
|
+
const a = els[0],
|
|
134
|
+
z = els[els.length - 1];
|
|
135
|
+
if (e.shiftKey && document.activeElement === a) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
z?.focus();
|
|
138
|
+
} else if (!e.shiftKey && document.activeElement === z) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
a?.focus();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
110
143
|
}
|
|
111
144
|
document.addEventListener("keydown", k);
|
|
112
145
|
return () => {
|
|
@@ -132,6 +165,19 @@ export function Gallery(p: GalleryProps) {
|
|
|
132
165
|
const stage = (
|
|
133
166
|
<div
|
|
134
167
|
className="tf-gallery-stage"
|
|
168
|
+
role="group"
|
|
169
|
+
aria-label={t.imageN(i[0] + 1)}
|
|
170
|
+
tabIndex={n > 1 ? 0 : undefined}
|
|
171
|
+
onKeyDown={(e) => {
|
|
172
|
+
if (e.target !== e.currentTarget) return;
|
|
173
|
+
if (e.key === "ArrowRight") {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
go(1);
|
|
176
|
+
} else if (e.key === "ArrowLeft") {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
go(-1);
|
|
179
|
+
}
|
|
180
|
+
}}
|
|
135
181
|
style={{
|
|
136
182
|
aspectRatio: p.ratio || "3 / 2",
|
|
137
183
|
}}
|
|
@@ -160,6 +206,7 @@ export function Gallery(p: GalleryProps) {
|
|
|
160
206
|
<span className="tf-gallery-count">{i[0] + 1 + " / " + n}</span>
|
|
161
207
|
{!full[0] && p.fullscreen !== false && (
|
|
162
208
|
<IconButton
|
|
209
|
+
ref={opener}
|
|
163
210
|
icon="expand"
|
|
164
211
|
label={t.fullScreen}
|
|
165
212
|
className="tf-gallery-full"
|
|
@@ -172,16 +219,35 @@ export function Gallery(p: GalleryProps) {
|
|
|
172
219
|
</div>
|
|
173
220
|
);
|
|
174
221
|
return (
|
|
175
|
-
<div
|
|
222
|
+
<div
|
|
223
|
+
style={p.style}
|
|
224
|
+
className={cx("tf-gallery", p.className)}
|
|
225
|
+
role="group"
|
|
226
|
+
aria-label={p.label || t.images}
|
|
227
|
+
>
|
|
176
228
|
{stage}
|
|
177
229
|
{p.thumbs !== false && n > 1 && (
|
|
178
|
-
<div
|
|
230
|
+
<div
|
|
231
|
+
ref={thumbs}
|
|
232
|
+
className="tf-gallery-thumbs"
|
|
233
|
+
role="tablist"
|
|
234
|
+
aria-label={p.label || t.images}
|
|
235
|
+
onKeyDown={(e) => {
|
|
236
|
+
if (moveFocus(e, thumbs.current, '[role="tab"]', "horizontal")) {
|
|
237
|
+
const k = Array.from(thumbs.current?.querySelectorAll('[role="tab"]') || []).indexOf(
|
|
238
|
+
document.activeElement as Element,
|
|
239
|
+
);
|
|
240
|
+
if (k > -1) i[1](k);
|
|
241
|
+
}
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
179
244
|
{items.map((it, k) => (
|
|
180
245
|
<button
|
|
181
246
|
key={k}
|
|
182
247
|
type="button"
|
|
183
248
|
role="tab"
|
|
184
249
|
aria-selected={k === i[0]}
|
|
250
|
+
tabIndex={k === i[0] ? 0 : -1}
|
|
185
251
|
className={cx(k === i[0] && "on")}
|
|
186
252
|
onClick={() => {
|
|
187
253
|
i[1](k);
|
|
@@ -195,15 +261,14 @@ export function Gallery(p: GalleryProps) {
|
|
|
195
261
|
)}
|
|
196
262
|
{full[0] && (
|
|
197
263
|
<Portal>
|
|
198
|
-
<div
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
/>
|
|
264
|
+
<div
|
|
265
|
+
ref={attachBox}
|
|
266
|
+
className="tf-lightbox"
|
|
267
|
+
role="dialog"
|
|
268
|
+
aria-modal="true"
|
|
269
|
+
aria-label={p.label || t.images}
|
|
270
|
+
>
|
|
271
|
+
<IconButton icon="x" label={t.close} className="tf-lightbox-close" onClick={close} />
|
|
207
272
|
<div className="tf-lightbox-stage">
|
|
208
273
|
{pic(cur, "tf-gallery-img", "eager")}
|
|
209
274
|
{n > 1 && (
|
|
@@ -268,9 +333,10 @@ export function BeforeAfter(p: BeforeAfterProps) {
|
|
|
268
333
|
return (
|
|
269
334
|
<div
|
|
270
335
|
ref={ref}
|
|
271
|
-
className="tf-ba"
|
|
336
|
+
className={cx("tf-ba", p.className)}
|
|
272
337
|
style={{
|
|
273
338
|
aspectRatio: p.ratio || "3 / 2",
|
|
339
|
+
...p.style,
|
|
274
340
|
}}
|
|
275
341
|
onPointerDown={(e) => {
|
|
276
342
|
drag.current = true;
|
|
@@ -283,6 +349,12 @@ export function BeforeAfter(p: BeforeAfterProps) {
|
|
|
283
349
|
onPointerUp={() => {
|
|
284
350
|
drag.current = false;
|
|
285
351
|
}}
|
|
352
|
+
onPointerCancel={() => {
|
|
353
|
+
drag.current = false;
|
|
354
|
+
}}
|
|
355
|
+
onLostPointerCapture={() => {
|
|
356
|
+
drag.current = false;
|
|
357
|
+
}}
|
|
286
358
|
>
|
|
287
359
|
{pane(p.after, "tf-ba-img")}
|
|
288
360
|
<div
|