@godxjp/ui 32.0.5 → 32.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agent/START-HERE.md +7 -7
- package/agent/components/Calendar.json +1 -1
- package/agent/components/Checkbox.json +1 -1
- package/agent/components/CheckboxGroup.json +6 -0
- package/agent/components/CommandPalette.json +6 -0
- package/agent/components/DatePicker.json +6 -0
- package/agent/components/DropdownMenu.json +5 -0
- package/agent/components/ListRow.json +2 -1
- package/agent/components/PasswordStrength.json +3 -3
- package/agent/components/SearchInput.json +6 -0
- package/agent/components/Select.json +8 -2
- package/agent/components/Toaster.json +15 -4
- package/agent/components/Toggle.json +3 -2
- package/agent/components/ToggleGroup.json +1 -1
- package/agent/components-index.json +1 -1
- package/agent/components.json +63 -15
- package/agent/index.json +6 -6
- package/agent/llms.txt +6 -6
- package/agent/patterns/card-grid-selection.json +21 -0
- package/agent/patterns/dependent-selects.json +20 -0
- package/agent/patterns-index.json +39 -0
- package/agent/patterns.json +41 -0
- package/agent/tokens.json +60 -0
- package/dist/components/data-entry/checkbox-group.d.ts +1 -1
- package/dist/components/data-entry/checkbox-group.js +58 -19
- package/dist/components/data-entry/command-palette.d.ts +21 -1
- package/dist/components/data-entry/command-palette.js +34 -34
- package/dist/components/data-entry/date-picker.js +21 -1
- package/dist/components/data-entry/index.d.ts +1 -1
- package/dist/components/data-entry/password-strength.d.ts +2 -0
- package/dist/components/data-entry/password-strength.js +8 -7
- package/dist/components/data-entry/search-input.d.ts +1 -1
- package/dist/components/data-entry/search-input.js +22 -2
- package/dist/components/data-entry/search-select.js +68 -13
- package/dist/components/data-entry/toggle.d.ts +2 -2
- package/dist/components/data-entry/toggle.js +5 -1
- package/dist/components/feedback/sonner.d.ts +7 -1
- package/dist/components/feedback/sonner.js +22 -2
- package/dist/components/feedback/use-toast.d.ts +3 -1
- package/dist/components/feedback/use-toast.js +10 -1
- package/dist/components/navigation/dropdown-menu.d.ts +7 -1
- package/dist/components/navigation/dropdown-menu.js +18 -2
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +19 -1
- package/dist/i18n/messages/ja.json +18 -1
- package/dist/i18n/messages/vi.json +18 -1
- package/dist/lib/shortcut.d.ts +17 -0
- package/dist/lib/shortcut.js +37 -0
- package/dist/props/components/data-entry.prop.d.ts +36 -0
- package/dist/styles/control.css +35 -3
- package/dist/styles/data-display-layout.css +7 -0
- package/dist/styles/layers.json +6 -1
- package/dist/styles/navigation-layout.css +26 -0
- package/dist/styles/table-layout.css +14 -0
- package/dist/styles/toggle.css +14 -0
- package/dist/styles.css +1 -1
- package/dist/tokens/components/control.css +10 -0
- package/dist/tokens/components/navigation.css +6 -0
- package/dist/tokens/foundation.css +2 -0
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +4 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../../lib/datetime/picker-format.js";
|
|
14
14
|
import { Button } from "../general/button.js";
|
|
15
15
|
import { Flex } from "../layout/flex.js";
|
|
16
|
+
import { Text } from "../general/typography.js";
|
|
16
17
|
import { TimePicker } from "./time-picker.js";
|
|
17
18
|
import { useControlledLatch } from "../../lib/hooks.js";
|
|
18
19
|
import { pickFieldA11y, pickGroupFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
|
|
@@ -75,6 +76,7 @@ function DatePicker(props) {
|
|
|
75
76
|
preserveInvalidOnBlur,
|
|
76
77
|
placement = "bottom-start",
|
|
77
78
|
renderExtraFooter,
|
|
79
|
+
showSummary = true,
|
|
78
80
|
size,
|
|
79
81
|
status,
|
|
80
82
|
variant,
|
|
@@ -166,6 +168,7 @@ function DatePicker(props) {
|
|
|
166
168
|
React.useEffect(() => {
|
|
167
169
|
if (open) setInternalView(defaultPickerValue ?? valueAnchor);
|
|
168
170
|
}, [open]);
|
|
171
|
+
const halfRange = Boolean(rangeValue?.from && !rangeValue.to);
|
|
169
172
|
const resolvedPlaceholder = placeholder ?? (isPeriod ? t("dataEntry.monthPicker.placeholder") : range ? t("dataEntry.dateRangePicker.placeholder") : t("dataEntry.datePicker.placeholder")) ?? PERIOD_HINT[picker];
|
|
170
173
|
const clearControl = resolveAllowClear(allowClear, true, t("common.clear") ?? "Clear");
|
|
171
174
|
const showClear = clearControl.enabled && !anyDisabled && (range ? allowEmpty.every(Boolean) && Boolean(committedRange?.from || committedRange?.to) : text !== "");
|
|
@@ -389,6 +392,22 @@ function DatePicker(props) {
|
|
|
389
392
|
) }, index);
|
|
390
393
|
}) }, row)) })
|
|
391
394
|
] });
|
|
395
|
+
const renderSummary = () => {
|
|
396
|
+
const from = rangeValue?.from;
|
|
397
|
+
const to = rangeValue?.to;
|
|
398
|
+
const dates = dateTimeFormat(locale, { year: "numeric", month: "short", day: "numeric" });
|
|
399
|
+
let content;
|
|
400
|
+
if (from && to) {
|
|
401
|
+
const utc = (date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
|
|
402
|
+
const count = Math.round(Math.abs(utc(to) - utc(from)) / 864e5) + 1;
|
|
403
|
+
const [start, end] = +from <= +to ? [from, to] : [to, from];
|
|
404
|
+
content = t("dataEntry.dateRangePicker.summary", {
|
|
405
|
+
range: dates.formatRange(start, end),
|
|
406
|
+
days: t("dataEntry.dateRangePicker.days", { count })
|
|
407
|
+
});
|
|
408
|
+
} else if (from) content = dates.format(from);
|
|
409
|
+
return /* @__PURE__ */ jsx("div", { "data-slot": "date-picker-summary", "aria-live": "polite", "aria-atomic": "true", children: content ? /* @__PURE__ */ jsx(Flex, { pad: "sm", children: /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", tabular: true, children: content }) }) : null });
|
|
410
|
+
};
|
|
392
411
|
const calendarBounds = [
|
|
393
412
|
...minimum ? [{ before: minimum }] : [],
|
|
394
413
|
...maximum ? [{ after: maximum }] : [],
|
|
@@ -510,6 +529,7 @@ function DatePicker(props) {
|
|
|
510
529
|
index
|
|
511
530
|
)) }) : null,
|
|
512
531
|
isPeriod ? renderPeriodPanel() : renderDayPanel(),
|
|
532
|
+
range && showSummary && !isPeriod ? renderSummary() : null,
|
|
513
533
|
showTime ? /* @__PURE__ */ jsx(
|
|
514
534
|
Flex,
|
|
515
535
|
{
|
|
@@ -579,7 +599,7 @@ function DatePicker(props) {
|
|
|
579
599
|
"aria-readonly": inputReadOnly || void 0,
|
|
580
600
|
value: edge === "from" ? fromText : toText,
|
|
581
601
|
disabled: edgeDisabled(edge),
|
|
582
|
-
placeholder: resolvedPlaceholder,
|
|
602
|
+
placeholder: edge === "to" && halfRange && !isPeriod ? t("dataEntry.dateRangePicker.selectEnd") ?? resolvedPlaceholder : resolvedPlaceholder,
|
|
583
603
|
inputMode: "numeric",
|
|
584
604
|
autoComplete: "off",
|
|
585
605
|
"aria-label": (edge === "from" ? t("dataEntry.dateRangePicker.from") : t("dataEntry.dateRangePicker.to")) ?? edge,
|
|
@@ -50,7 +50,7 @@ export type { TransferProps, TransferItemProp } from "./transfer.js";
|
|
|
50
50
|
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
|
|
51
51
|
export type { CommandProps } from "./command.js";
|
|
52
52
|
export { CommandPalette } from "./command-palette.js";
|
|
53
|
-
export type { CommandPaletteGroup, CommandPaletteItem, CommandPaletteLabels, CommandPaletteProps, CommandPaletteSelectModifiers, } from "./command-palette.js";
|
|
53
|
+
export type { CommandPaletteGroup, CommandPaletteItem, CommandPaletteLabels, CommandPaletteProps, CommandPaletteRecent, CommandPaletteSelectModifiers, } from "./command-palette.js";
|
|
54
54
|
export { PasswordInput } from "./password-input.js";
|
|
55
55
|
export type { PasswordInputProps, PasswordVisibilityToggleProp } from "./password-input.js";
|
|
56
56
|
export { PasswordStrength, usePasswordStrength } from "./password-strength.js";
|
|
@@ -8,6 +8,8 @@ export type PasswordStrengthLabels = {
|
|
|
8
8
|
weak?: string;
|
|
9
9
|
fair?: string;
|
|
10
10
|
strong?: string;
|
|
11
|
+
/** The neutral line shown while nothing has been typed (gh#1247). */
|
|
12
|
+
empty?: string;
|
|
11
13
|
/** Checklist line per rule. */
|
|
12
14
|
rules?: Partial<Record<PasswordRule, string>>;
|
|
13
15
|
/** Screen-reader prefix before a met rule (e.g. "Passed: "). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Check, X } from "lucide-react";
|
|
3
|
+
import { Check, Minus, X } from "lucide-react";
|
|
4
4
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
5
|
const DEFAULT_PASSWORD_RULES = ["length", "upper", "lower", "number", "symbol"];
|
|
6
6
|
function usePasswordStrength(value, rules = [...DEFAULT_PASSWORD_RULES], minLength = 8) {
|
|
@@ -36,12 +36,13 @@ function PasswordStrength({
|
|
|
36
36
|
const { t } = useTranslation();
|
|
37
37
|
const normalizedRules = [...new Set(rules)];
|
|
38
38
|
const { score, checks } = usePasswordStrength(value, normalizedRules, minLength);
|
|
39
|
-
const
|
|
39
|
+
const empty = value.trim() === "";
|
|
40
|
+
const tone = empty ? void 0 : scoreTone(score);
|
|
40
41
|
const segments = Array.from({ length: 5 });
|
|
41
42
|
const key = scoreKey(score);
|
|
42
|
-
const strength = labels[key] ?? t(`dataEntry.passwordStrength.${key}`);
|
|
43
|
+
const strength = empty ? labels.empty ?? t("dataEntry.passwordStrength.empty") : labels[key] ?? t(`dataEntry.passwordStrength.${key}`);
|
|
43
44
|
const ruleLabel = (rule) => labels.rules?.[rule] ?? t(`dataEntry.passwordStrength.rules.${rule}`, { n: String(minLength) });
|
|
44
|
-
const announcement = labels.announcement ? labels.announcement(strength) : t("dataEntry.passwordStrength.announcement", { strength });
|
|
45
|
+
const announcement = empty ? "" : labels.announcement ? labels.announcement(strength) : t("dataEntry.passwordStrength.announcement", { strength });
|
|
45
46
|
return /* @__PURE__ */ jsxs("div", { className: "ui-password-strength", children: [
|
|
46
47
|
/* @__PURE__ */ jsx(
|
|
47
48
|
"div",
|
|
@@ -50,7 +51,7 @@ function PasswordStrength({
|
|
|
50
51
|
role: "img",
|
|
51
52
|
"aria-label": t("dataEntry.passwordStrength.meter", { score: String(score) }),
|
|
52
53
|
children: segments.map((_, index) => {
|
|
53
|
-
const filled = index < score + 1;
|
|
54
|
+
const filled = !empty && index < score + 1;
|
|
54
55
|
return /* @__PURE__ */ jsx(
|
|
55
56
|
"span",
|
|
56
57
|
{
|
|
@@ -75,9 +76,9 @@ function PasswordStrength({
|
|
|
75
76
|
"li",
|
|
76
77
|
{
|
|
77
78
|
className: "ui-password-strength-checklist-item",
|
|
78
|
-
"data-state": checks[rule] ? "passed" : "failed",
|
|
79
|
+
"data-state": empty ? "pending" : checks[rule] ? "passed" : "failed",
|
|
79
80
|
children: [
|
|
80
|
-
checks[rule] ? /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
|
|
81
|
+
empty ? /* @__PURE__ */ jsx(Minus, { "aria-hidden": "true" }) : checks[rule] ? /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
|
|
81
82
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: checks[rule] ? labels.passed ?? t("dataEntry.passwordStrength.passed") : labels.failed ?? t("dataEntry.passwordStrength.failed") }),
|
|
82
83
|
/* @__PURE__ */ jsx("span", { children: ruleLabel(rule) })
|
|
83
84
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { SearchInputProp } from "../../props/components/data-entry.prop.js";
|
|
3
3
|
export type SearchInputProps = SearchInputProp;
|
|
4
|
-
export declare function SearchInput({ value: controlledValue, defaultValue, placeholder, debounce, onValueChange, onSearch, label, ariaLabel, className, inputClassName, id, disabled, status, variant, ...ariaProps }: SearchInputProps): React.JSX.Element;
|
|
4
|
+
export declare function SearchInput({ value: controlledValue, defaultValue, placeholder, debounce, onValueChange, onSearch, label, ariaLabel, className, inputClassName, id, disabled, status, variant, shortcut, ...ariaProps }: SearchInputProps): React.JSX.Element;
|
|
@@ -8,6 +8,8 @@ import { Label } from "../data-entry/label.js";
|
|
|
8
8
|
import { cn } from "../../lib/utils.js";
|
|
9
9
|
import { resolveFieldA11y } from "../../lib/field-a11y.js";
|
|
10
10
|
import { useDebouncedValue } from "../../lib/hooks.js";
|
|
11
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
12
|
+
import { parseShortcut, shortcutHint, shortcutMatches } from "../../lib/shortcut.js";
|
|
11
13
|
function SearchInput({
|
|
12
14
|
value: controlledValue,
|
|
13
15
|
defaultValue = "",
|
|
@@ -23,9 +25,12 @@ function SearchInput({
|
|
|
23
25
|
disabled = false,
|
|
24
26
|
status,
|
|
25
27
|
variant,
|
|
28
|
+
shortcut = false,
|
|
26
29
|
...ariaProps
|
|
27
30
|
}) {
|
|
28
31
|
const { t } = useTranslation();
|
|
32
|
+
const inputRef = React.useRef(null);
|
|
33
|
+
const shortcutSpec = React.useMemo(() => parseShortcut(shortcut), [shortcut]);
|
|
29
34
|
const isControlled = controlledValue !== void 0;
|
|
30
35
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
31
36
|
const value = isControlled ? controlledValue : internal;
|
|
@@ -48,6 +53,17 @@ function SearchInput({
|
|
|
48
53
|
lastSearch.current = debounced;
|
|
49
54
|
onSearchRef.current?.(debounced);
|
|
50
55
|
}, [debounced]);
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
if (!shortcutSpec || disabled) return;
|
|
58
|
+
const handleKeyDown = (event) => {
|
|
59
|
+
if (isImeComposing(event) || !shortcutMatches(shortcutSpec, event)) return;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
inputRef.current?.focus();
|
|
62
|
+
};
|
|
63
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
64
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
65
|
+
}, [shortcutSpec, disabled]);
|
|
66
|
+
const showShortcut = Boolean(shortcutSpec && !value && !disabled);
|
|
51
67
|
const setValue = (v) => {
|
|
52
68
|
if (!isControlled) setInternal(v);
|
|
53
69
|
onValueChange?.(v);
|
|
@@ -59,6 +75,7 @@ function SearchInput({
|
|
|
59
75
|
/* @__PURE__ */ jsx(
|
|
60
76
|
Input,
|
|
61
77
|
{
|
|
78
|
+
ref: inputRef,
|
|
62
79
|
id: inputId,
|
|
63
80
|
type: "text",
|
|
64
81
|
role: "searchbox",
|
|
@@ -78,7 +95,9 @@ function SearchInput({
|
|
|
78
95
|
variant,
|
|
79
96
|
...fieldA11y,
|
|
80
97
|
className: cn(
|
|
81
|
-
"ui-search-input-control !ps-[var(--search-input-start-padding)]
|
|
98
|
+
"ui-search-input-control !ps-[var(--search-input-start-padding)]",
|
|
99
|
+
// gh#1250 — while the key cap shows, the text stops before it instead of running under it.
|
|
100
|
+
showShortcut ? "!pe-[calc(var(--search-input-edge-inset)+6ch)]" : "!pe-[var(--search-input-end-padding)]",
|
|
82
101
|
inputClassName
|
|
83
102
|
),
|
|
84
103
|
disabled
|
|
@@ -95,7 +114,8 @@ function SearchInput({
|
|
|
95
114
|
className: "ui-search-input-clear",
|
|
96
115
|
children: /* @__PURE__ */ jsx(X, {})
|
|
97
116
|
}
|
|
98
|
-
)
|
|
117
|
+
),
|
|
118
|
+
showShortcut && shortcutSpec ? /* @__PURE__ */ jsx("kbd", { className: "kbd ui-search-input-shortcut", "aria-hidden": "true", children: shortcutHint(shortcutSpec) }) : null
|
|
99
119
|
] })
|
|
100
120
|
] });
|
|
101
121
|
}
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
} from "./control-surface.js";
|
|
17
17
|
import { normalizeSelectOptions } from "../../lib/select-options.js";
|
|
18
18
|
import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
19
|
+
import { Button } from "../general/button.js";
|
|
20
|
+
import { Flex } from "../layout/flex.js";
|
|
19
21
|
import { Command, CommandGroup } from "./command.js";
|
|
20
22
|
import { Input } from "./input.js";
|
|
21
23
|
const DEBOUNCE_MS = 250;
|
|
@@ -97,6 +99,8 @@ function SearchSelect(props) {
|
|
|
97
99
|
const allowCreate = props.mode !== void 0 ? props.allowCreate : void 0;
|
|
98
100
|
const createLabel = props.mode !== void 0 ? props.createLabel : void 0;
|
|
99
101
|
const onCreate = props.mode !== void 0 ? props.onCreate : void 0;
|
|
102
|
+
const needConfirm = props.mode !== void 0 ? Boolean(props.needConfirm) : false;
|
|
103
|
+
const [pending, setPending] = React.useState(null);
|
|
100
104
|
const fold = React.useCallback(
|
|
101
105
|
(text) => caseSensitive ? text : text.toLowerCase(),
|
|
102
106
|
[caseSensitive]
|
|
@@ -125,6 +129,7 @@ function SearchSelect(props) {
|
|
|
125
129
|
const setOpen = React.useCallback(
|
|
126
130
|
(next) => {
|
|
127
131
|
if (!isOpenControlled) setInternalOpen(next);
|
|
132
|
+
if (!next) setPending(null);
|
|
128
133
|
onOpenChange?.(next);
|
|
129
134
|
},
|
|
130
135
|
[isOpenControlled, onOpenChange]
|
|
@@ -158,8 +163,10 @@ function SearchSelect(props) {
|
|
|
158
163
|
if (!multiple) return [];
|
|
159
164
|
return Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : [];
|
|
160
165
|
}, [multiple, rawValue]);
|
|
166
|
+
const drafting = needConfirm && open;
|
|
167
|
+
const working = drafting ? pending ?? values : values;
|
|
161
168
|
const value = multiple ? "" : Array.isArray(rawValue) ? rawValue[0] ?? "" : rawValue;
|
|
162
|
-
const isSelected = (optionValue) => multiple ?
|
|
169
|
+
const isSelected = (optionValue) => multiple ? working.includes(optionValue) : value === optionValue;
|
|
163
170
|
const hasSelection = multiple ? values.length > 0 : Boolean(value);
|
|
164
171
|
const reqId = React.useRef(0);
|
|
165
172
|
const [freshPage, setFreshPage] = React.useState(0);
|
|
@@ -220,7 +227,7 @@ function SearchSelect(props) {
|
|
|
220
227
|
if (!tagsMode) return { displayOptions: loaded, createOption: void 0 };
|
|
221
228
|
const known = new Set(loaded.map((option) => option.value));
|
|
222
229
|
const extras = [];
|
|
223
|
-
for (const entry of
|
|
230
|
+
for (const entry of working) {
|
|
224
231
|
if (known.has(entry)) continue;
|
|
225
232
|
known.add(entry);
|
|
226
233
|
const held = pickedOptions[entry] ?? { value: entry, label: entry };
|
|
@@ -232,7 +239,7 @@ function SearchSelect(props) {
|
|
|
232
239
|
);
|
|
233
240
|
const create = typedText && !exists && canCreate(typedText) ? { value: typedText, label: typedText } : void 0;
|
|
234
241
|
const exact = typedText ? loaded.find(
|
|
235
|
-
(option) => !
|
|
242
|
+
(option) => !working.includes(option.value) && (fold(option.value) === typed || fold(option.label) === typed)
|
|
236
243
|
) : void 0;
|
|
237
244
|
const rest = exact ? loaded.filter((option) => option !== exact) : loaded;
|
|
238
245
|
return {
|
|
@@ -242,7 +249,7 @@ function SearchSelect(props) {
|
|
|
242
249
|
}, [
|
|
243
250
|
tagsMode,
|
|
244
251
|
loaded,
|
|
245
|
-
|
|
252
|
+
working,
|
|
246
253
|
typedText,
|
|
247
254
|
pickedOptions,
|
|
248
255
|
staticOptions,
|
|
@@ -302,16 +309,23 @@ function SearchSelect(props) {
|
|
|
302
309
|
props.mode !== void 0 ? props.maxTagPlaceholder : void 0
|
|
303
310
|
);
|
|
304
311
|
const maxCount = props.mode !== void 0 ? props.maxCount : void 0;
|
|
305
|
-
const atMaxCount = maxCount !== void 0 &&
|
|
312
|
+
const atMaxCount = maxCount !== void 0 && working.length >= maxCount;
|
|
306
313
|
const remember = (option) => setPickedOptions((prev) => ({ ...prev, [option.value]: option }));
|
|
307
314
|
const isInvented = (entry) => tagsMode && !(staticOptions ?? []).some((option) => option.value === entry) && !loaded.some((option) => option.value === entry);
|
|
308
|
-
const select = (option) => {
|
|
315
|
+
const select = (option, { commit = false } = {}) => {
|
|
309
316
|
if (option.disabled) return;
|
|
310
317
|
if (props.mode !== void 0) {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
const
|
|
318
|
+
const toDraft = drafting && !commit;
|
|
319
|
+
const base = toDraft ? working : values;
|
|
320
|
+
const already = base.includes(option.value);
|
|
321
|
+
if (!already && maxCount !== void 0 && base.length >= maxCount) return;
|
|
322
|
+
const next = already ? base.filter((entry) => entry !== option.value) : [...base, option.value];
|
|
314
323
|
remember(option);
|
|
324
|
+
if (toDraft) {
|
|
325
|
+
setPending(next);
|
|
326
|
+
if (autoClearSearchValue) setQuery("");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
315
329
|
if (!isControlled) setInternalValue(next);
|
|
316
330
|
props.onValueChange?.(
|
|
317
331
|
next,
|
|
@@ -334,7 +348,7 @@ function SearchSelect(props) {
|
|
|
334
348
|
};
|
|
335
349
|
const selectTokens = (text) => {
|
|
336
350
|
if (props.mode === void 0) return;
|
|
337
|
-
const next = [...
|
|
351
|
+
const next = [...working];
|
|
338
352
|
const picked = [];
|
|
339
353
|
for (const raw of splitByTokenSeparators(text, tokenSeparators ?? [])) {
|
|
340
354
|
const token = raw.trim();
|
|
@@ -351,6 +365,10 @@ function SearchSelect(props) {
|
|
|
351
365
|
}
|
|
352
366
|
if (picked.length === 0) return;
|
|
353
367
|
for (const option of picked) remember(option);
|
|
368
|
+
if (drafting) {
|
|
369
|
+
setPending(next);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
354
372
|
if (!isControlled) setInternalValue(next);
|
|
355
373
|
props.onValueChange?.(
|
|
356
374
|
next,
|
|
@@ -362,7 +380,19 @@ function SearchSelect(props) {
|
|
|
362
380
|
}
|
|
363
381
|
};
|
|
364
382
|
const deselect = (chipValue) => {
|
|
365
|
-
select(optionFor(chipValue) ?? { value: chipValue, label: chipValue });
|
|
383
|
+
select(optionFor(chipValue) ?? { value: chipValue, label: chipValue }, { commit: true });
|
|
384
|
+
};
|
|
385
|
+
const confirm = () => {
|
|
386
|
+
if (props.mode !== void 0) {
|
|
387
|
+
const next = working;
|
|
388
|
+
if (!isControlled) setInternalValue(next);
|
|
389
|
+
props.onValueChange?.(
|
|
390
|
+
next,
|
|
391
|
+
next.map(optionFor).filter((entry) => entry !== null)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (autoClearSearchValue) setQuery("");
|
|
395
|
+
setOpen(false);
|
|
366
396
|
};
|
|
367
397
|
const clear = () => {
|
|
368
398
|
if (props.mode !== void 0) {
|
|
@@ -458,8 +488,11 @@ function SearchSelect(props) {
|
|
|
458
488
|
// order and make that token dead. Same reasoning as SelectTrigger.
|
|
459
489
|
width === "auto" && "w-auto",
|
|
460
490
|
width === "full" && "w-full",
|
|
461
|
-
// Reserve trailing room for the single clear-or-chevron overlay rendered below
|
|
462
|
-
|
|
491
|
+
// Reserve trailing room for the single clear-or-chevron overlay rendered below — only when
|
|
492
|
+
// something can sit there. `suffixIcon={null}` with no clear and no loading renders an empty
|
|
493
|
+
// overlay, and reserving the chevron's room anyway left a dead 2.25rem gap at the end of a
|
|
494
|
+
// `width="auto"` trigger (gh#1257).
|
|
495
|
+
(loadingProp || clearControl.enabled || suffixIcon !== null) && "ui-control-trigger-affixed"
|
|
463
496
|
)
|
|
464
497
|
};
|
|
465
498
|
const chipsRemovable = !disabled && !readOnly;
|
|
@@ -713,6 +746,28 @@ function SearchSelect(props) {
|
|
|
713
746
|
}
|
|
714
747
|
})
|
|
715
748
|
}
|
|
749
|
+
) : null,
|
|
750
|
+
needConfirm ? /* @__PURE__ */ jsx(
|
|
751
|
+
"div",
|
|
752
|
+
{
|
|
753
|
+
className: "ui-search-select-footer",
|
|
754
|
+
"data-slot": "search-select-confirm",
|
|
755
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
756
|
+
children: /* @__PURE__ */ jsxs(Flex, { justify: "end", gap: "sm", children: [
|
|
757
|
+
/* @__PURE__ */ jsx(
|
|
758
|
+
Button,
|
|
759
|
+
{
|
|
760
|
+
type: "button",
|
|
761
|
+
variant: "ghost",
|
|
762
|
+
size: "sm",
|
|
763
|
+
disabled: working.length === 0,
|
|
764
|
+
onClick: () => setPending([]),
|
|
765
|
+
children: t("common.clear")
|
|
766
|
+
}
|
|
767
|
+
),
|
|
768
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: confirm, children: t("dataEntry.timePicker.confirm") })
|
|
769
|
+
] })
|
|
770
|
+
}
|
|
716
771
|
) : null
|
|
717
772
|
] }) })
|
|
718
773
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const toggleVariants: (props?: ({
|
|
4
|
-
variant?: "default" | "outline" | "soft" | null | undefined;
|
|
4
|
+
variant?: "default" | "outline" | "quiet" | "soft" | null | undefined;
|
|
5
5
|
size?: "lg" | "md" | "sm" | "xs" | null | undefined;
|
|
6
6
|
shape?: "default" | "pill" | "sharp" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
@@ -78,7 +78,7 @@ declare function restoreDomProps<P extends {
|
|
|
78
78
|
export type ToggleProp = React.ComponentPropsWithoutRef<"button"> & TogglePressedFields & VariantProps<typeof toggleVariants> & ToggleCountFields;
|
|
79
79
|
export type ToggleProps = ToggleProp;
|
|
80
80
|
export declare const Toggle: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & TogglePressedFields & VariantProps<(props?: ({
|
|
81
|
-
variant?: "default" | "outline" | "soft" | null | undefined;
|
|
81
|
+
variant?: "default" | "outline" | "quiet" | "soft" | null | undefined;
|
|
82
82
|
size?: "lg" | "md" | "sm" | "xs" | null | undefined;
|
|
83
83
|
shape?: "default" | "pill" | "sharp" | null | undefined;
|
|
84
84
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string> & ToggleCountFields & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -18,7 +18,11 @@ const toggleVariants = cva("ui-toggle", {
|
|
|
18
18
|
// no pressed state at all. `soft` is antd `Tag`'s resting surface, on the SAME
|
|
19
19
|
// `hsl(var(--secondary))` fill `Badge variant="secondary"` and `Button variant="secondary"`
|
|
20
20
|
// already paint, so a chip, a badge and a button on one row are one family.
|
|
21
|
-
soft: "ui-toggle-soft"
|
|
21
|
+
soft: "ui-toggle-soft",
|
|
22
|
+
// A LOW-KEY pressed state (gh#1257): the `default` rest, and pressed as a --secondary fill with
|
|
23
|
+
// --foreground text and the primary hairline instead of a solid primary block — for a comment
|
|
24
|
+
// star or a reaction, where a pressed toggle is a mark beside content, not a selected filter.
|
|
25
|
+
quiet: "ui-toggle-quiet"
|
|
22
26
|
},
|
|
23
27
|
size: {
|
|
24
28
|
// The fourth step of the shared control ladder (gh#716): 24px, `--control-height-xs`, the
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type ToasterProps } from "sonner";
|
|
3
|
+
/**
|
|
4
|
+
* `--toast-duration` → milliseconds for Sonner's `duration` (gh#1253). Accepts the two CSS time
|
|
5
|
+
* units; anything else (unset, `initial`, a typo) is `undefined`, which leaves Sonner's own
|
|
6
|
+
* 4000ms default in charge rather than inventing a second one here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseToastDuration(raw: string): number | undefined;
|
|
3
9
|
/**
|
|
4
10
|
* `richColors` defaults ON: it is the switch that makes sonner apply the per-type properties at
|
|
5
11
|
* all (`[data-rich-colors='true'][data-type='success'] { … }`), so without it the tokens above
|
|
6
12
|
* are declared and never read. A consumer can still pass `richColors={false}` for a deliberately
|
|
7
13
|
* monochrome stack.
|
|
8
14
|
*/
|
|
9
|
-
declare function Toaster({ style, richColors, ...props }: ToasterProps): React.JSX.Element;
|
|
15
|
+
declare function Toaster({ style, richColors, toastOptions, ...props }: ToasterProps): React.JSX.Element;
|
|
10
16
|
export { Toaster };
|
|
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
|
|
|
5
5
|
import { CheckCircle2, Info, Loader2, OctagonX, TriangleAlert } from "lucide-react";
|
|
6
6
|
import { Toaster as Sonner } from "sonner";
|
|
7
7
|
import { useOverlayPortalContainer } from "../../lib/overlay-portal.js";
|
|
8
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
8
9
|
function useDocumentTheme() {
|
|
9
10
|
return React.useSyncExternalStore(
|
|
10
11
|
(onStoreChange) => {
|
|
@@ -25,6 +26,20 @@ function useDocumentTheme() {
|
|
|
25
26
|
() => "light"
|
|
26
27
|
);
|
|
27
28
|
}
|
|
29
|
+
function parseToastDuration(raw) {
|
|
30
|
+
const match = /^\s*(\d+(?:\.\d+)?)(ms|s)\s*$/.exec(raw);
|
|
31
|
+
if (!match) return void 0;
|
|
32
|
+
const ms = Number(match[1]) * (match[2] === "s" ? 1e3 : 1);
|
|
33
|
+
return ms > 0 ? ms : void 0;
|
|
34
|
+
}
|
|
35
|
+
function useToastDurationToken(host) {
|
|
36
|
+
const [duration, setDuration] = React.useState(void 0);
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
const element = host ?? document.documentElement;
|
|
39
|
+
setDuration(parseToastDuration(getComputedStyle(element).getPropertyValue("--toast-duration")));
|
|
40
|
+
}, [host]);
|
|
41
|
+
return duration;
|
|
42
|
+
}
|
|
28
43
|
const TOAST_TONES = [
|
|
29
44
|
{ type: "success", hue: "success", ink: "text-success" },
|
|
30
45
|
{ type: "error", hue: "destructive", ink: "text-error" },
|
|
@@ -45,9 +60,11 @@ const toneVars = Object.fromEntries(
|
|
|
45
60
|
[`--${type}-text`, `hsl(var(--${ink}))`]
|
|
46
61
|
])
|
|
47
62
|
);
|
|
48
|
-
function Toaster({ style, richColors = true, ...props }) {
|
|
63
|
+
function Toaster({ style, richColors = true, toastOptions, ...props }) {
|
|
49
64
|
const theme = useDocumentTheme();
|
|
65
|
+
const { t } = useTranslation();
|
|
50
66
|
const overlayPortalContainer = useOverlayPortalContainer();
|
|
67
|
+
const tokenDuration = useToastDurationToken(overlayPortalContainer);
|
|
51
68
|
const toaster = /* @__PURE__ */ jsx(
|
|
52
69
|
Sonner,
|
|
53
70
|
{
|
|
@@ -86,11 +103,14 @@ function Toaster({ style, richColors = true, ...props }) {
|
|
|
86
103
|
bottom: "var(--toast-mobile-offset)",
|
|
87
104
|
right: "var(--toast-mobile-offset)"
|
|
88
105
|
},
|
|
106
|
+
duration: tokenDuration,
|
|
107
|
+
toastOptions: { closeButtonAriaLabel: t("feedback.toast.close"), ...toastOptions },
|
|
89
108
|
...props
|
|
90
109
|
}
|
|
91
110
|
);
|
|
92
111
|
return overlayPortalContainer ? createPortal(toaster, overlayPortalContainer) : toaster;
|
|
93
112
|
}
|
|
94
113
|
export {
|
|
95
|
-
Toaster
|
|
114
|
+
Toaster,
|
|
115
|
+
parseToastDuration
|
|
96
116
|
};
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { toast as sonnerToast } from "sonner";
|
|
3
|
+
const withCloseButton = (show) => (message, data) => show(message, { closeButton: true, ...data });
|
|
4
|
+
const toast = Object.assign(
|
|
5
|
+
(...args) => sonnerToast(...args),
|
|
6
|
+
sonnerToast,
|
|
7
|
+
{
|
|
8
|
+
error: withCloseButton(sonnerToast.error),
|
|
9
|
+
warning: withCloseButton(sonnerToast.warning)
|
|
10
|
+
}
|
|
11
|
+
);
|
|
3
12
|
export {
|
|
4
13
|
sonnerToast,
|
|
5
|
-
|
|
14
|
+
toast
|
|
6
15
|
};
|
|
@@ -154,6 +154,12 @@ interface DropdownMenuItemPropsOwn {
|
|
|
154
154
|
disabled?: boolean;
|
|
155
155
|
onSelect?: (event: Event) => void;
|
|
156
156
|
textValue?: string;
|
|
157
|
+
/**
|
|
158
|
+
* A second, muted line under the label — the two-line row (gh#1246, parity with
|
|
159
|
+
* `MegaMenuLinkProp.description`). Rendered as RAC `<Text slot="description">`, so the item's
|
|
160
|
+
* accessible NAME stays the label and the description is wired as its `aria-describedby`.
|
|
161
|
+
*/
|
|
162
|
+
description?: React.ReactNode;
|
|
157
163
|
/** Mượn thẻ của con thay vì dựng `<div>` riêng — chỗ để nhét một `<Link>` vào một mục menu. */
|
|
158
164
|
asChild?: boolean;
|
|
159
165
|
/**
|
|
@@ -170,7 +176,7 @@ interface DropdownMenuItemPropsOwn {
|
|
|
170
176
|
id?: string;
|
|
171
177
|
}
|
|
172
178
|
type DropdownMenuItemProps = React.PropsWithChildren<DropdownMenuItemPropsOwn> & Record<`data-${string}`, unknown>;
|
|
173
|
-
export declare function DropdownMenuItem({ children, className, inset, variant, disabled, onSelect, textValue, asChild, ...forwarded }: DropdownMenuItemProps): React.JSX.Element;
|
|
179
|
+
export declare function DropdownMenuItem({ children, className, inset, variant, disabled, onSelect, textValue, description, asChild, ...forwarded }: DropdownMenuItemProps): React.JSX.Element;
|
|
174
180
|
interface DropdownMenuLabelPropsOwn {
|
|
175
181
|
className?: string;
|
|
176
182
|
inset?: boolean;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
Separator,
|
|
17
17
|
OverlayArrow,
|
|
18
18
|
SubmenuTrigger,
|
|
19
|
+
Text,
|
|
19
20
|
useLocale
|
|
20
21
|
} from "react-aria-components";
|
|
21
22
|
import { Check, ChevronRight } from "lucide-react";
|
|
@@ -397,15 +398,18 @@ function DropdownMenuItem({
|
|
|
397
398
|
disabled,
|
|
398
399
|
onSelect,
|
|
399
400
|
textValue,
|
|
401
|
+
description,
|
|
400
402
|
asChild,
|
|
401
403
|
...forwarded
|
|
402
404
|
}) {
|
|
403
405
|
const tag = borrowedTag(children, asChild);
|
|
406
|
+
const label = tag ? tag.props.children : children;
|
|
407
|
+
const hasDescription = description != null && description !== false;
|
|
404
408
|
return /* @__PURE__ */ jsx(
|
|
405
409
|
MenuItem,
|
|
406
410
|
{
|
|
407
411
|
isDisabled: disabled,
|
|
408
|
-
textValue,
|
|
412
|
+
textValue: textValue ?? (typeof label === "string" ? label : void 0),
|
|
409
413
|
onAction: onSelect ? () => onSelect(selectEvent("dropdownmenu.itemSelect")) : void 0,
|
|
410
414
|
className: cn(
|
|
411
415
|
"ui-dropdown-menu-item [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
@@ -420,6 +424,7 @@ function DropdownMenuItem({
|
|
|
420
424
|
"data-slot": "dropdown-menu-item",
|
|
421
425
|
"data-inset": inset,
|
|
422
426
|
"data-variant": variant,
|
|
427
|
+
"data-description": hasDescription ? "" : void 0,
|
|
423
428
|
...radixItemState(state),
|
|
424
429
|
// LAST, so a consumer's own `id`/`data-*` wins over ours rather than being
|
|
425
430
|
// overwritten by the slot bookkeeping above (gh#631).
|
|
@@ -428,7 +433,18 @@ function DropdownMenuItem({
|
|
|
428
433
|
content
|
|
429
434
|
);
|
|
430
435
|
},
|
|
431
|
-
children:
|
|
436
|
+
children: hasDescription ? /* @__PURE__ */ jsxs("span", { className: "ui-dropdown-menu-item-text", children: [
|
|
437
|
+
/* @__PURE__ */ jsx(Text, { slot: "label", className: "ui-dropdown-menu-item-label", children: label }),
|
|
438
|
+
/* @__PURE__ */ jsx(
|
|
439
|
+
Text,
|
|
440
|
+
{
|
|
441
|
+
slot: "description",
|
|
442
|
+
"data-slot": "dropdown-menu-item-description",
|
|
443
|
+
className: "ui-dropdown-menu-item-description",
|
|
444
|
+
children: description
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
] }) : label
|
|
432
448
|
}
|
|
433
449
|
);
|
|
434
450
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.1.1",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"previous": "Previous",
|
|
15
15
|
"next": "Next",
|
|
16
16
|
"selectedCount": "{count} selected",
|
|
17
|
+
"selectedOfTotal": "{count} of {total} selected",
|
|
17
18
|
"typeToConfirm": "Type {phrase} to confirm"
|
|
18
19
|
},
|
|
19
20
|
"dataEntry": {
|
|
@@ -32,7 +33,13 @@
|
|
|
32
33
|
"placeholder": "Select date range",
|
|
33
34
|
"from": "From",
|
|
34
35
|
"to": "To",
|
|
35
|
-
"openCalendar": "Open calendar"
|
|
36
|
+
"openCalendar": "Open calendar",
|
|
37
|
+
"selectEnd": "Select end date",
|
|
38
|
+
"days": {
|
|
39
|
+
"one": "{count} day",
|
|
40
|
+
"other": "{count} days"
|
|
41
|
+
},
|
|
42
|
+
"summary": "{range} · {days}"
|
|
36
43
|
},
|
|
37
44
|
"timePicker": {
|
|
38
45
|
"placeholder": "Select time",
|
|
@@ -50,6 +57,10 @@
|
|
|
50
57
|
"searchInput": {
|
|
51
58
|
"placeholder": "Search…"
|
|
52
59
|
},
|
|
60
|
+
"commandPalette": {
|
|
61
|
+
"recent": "Recent searches",
|
|
62
|
+
"clearRecent": "Clear recent searches"
|
|
63
|
+
},
|
|
53
64
|
"autocomplete": {
|
|
54
65
|
"placeholder": "Search or select…",
|
|
55
66
|
"empty": "No results found"
|
|
@@ -241,10 +252,14 @@
|
|
|
241
252
|
"dropped": "Dropped {label} at position {position} of {count}.",
|
|
242
253
|
"cancelled": "Move cancelled. {label} is back at position {position} of {count}."
|
|
243
254
|
},
|
|
255
|
+
"checkboxGroup": {
|
|
256
|
+
"selectAll": "Select all"
|
|
257
|
+
},
|
|
244
258
|
"passwordStrength": {
|
|
245
259
|
"weak": "Weak",
|
|
246
260
|
"fair": "Fair",
|
|
247
261
|
"strong": "Strong",
|
|
262
|
+
"empty": "Enter a password",
|
|
248
263
|
"meter": "Password strength {score} of 4",
|
|
249
264
|
"passed": "Passed: ",
|
|
250
265
|
"failed": "Not yet: ",
|
|
@@ -267,6 +282,9 @@
|
|
|
267
282
|
"dismiss": "Dismiss",
|
|
268
283
|
"verifying": "Verifying…",
|
|
269
284
|
"stepUpFailed": "Re-authentication failed. Please try again."
|
|
285
|
+
},
|
|
286
|
+
"toast": {
|
|
287
|
+
"close": "Close notification"
|
|
270
288
|
}
|
|
271
289
|
},
|
|
272
290
|
"layout": {
|