@godxjp/ui 32.0.4 → 32.1.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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/agent/START-HERE.md +7 -7
  3. package/agent/components/Checkbox.json +1 -1
  4. package/agent/components/CheckboxGroup.json +6 -0
  5. package/agent/components/CommandPalette.json +6 -0
  6. package/agent/components/DatePicker.json +6 -0
  7. package/agent/components/DropdownMenu.json +5 -0
  8. package/agent/components/PasswordStrength.json +3 -3
  9. package/agent/components/SearchInput.json +6 -0
  10. package/agent/components/Select.json +6 -0
  11. package/agent/components/Toaster.json +15 -4
  12. package/agent/components-index.json +1 -1
  13. package/agent/components.json +54 -8
  14. package/agent/index.json +6 -6
  15. package/agent/llms.txt +6 -6
  16. package/agent/patterns/card-grid-selection.json +21 -0
  17. package/agent/patterns/dependent-selects.json +20 -0
  18. package/agent/patterns-index.json +39 -0
  19. package/agent/patterns.json +41 -0
  20. package/agent/tokens.json +18 -0
  21. package/dist/components/data-entry/checkbox-group.d.ts +1 -1
  22. package/dist/components/data-entry/checkbox-group.js +58 -19
  23. package/dist/components/data-entry/command-palette.d.ts +21 -1
  24. package/dist/components/data-entry/command-palette.js +34 -34
  25. package/dist/components/data-entry/date-picker.js +21 -1
  26. package/dist/components/data-entry/index.d.ts +1 -1
  27. package/dist/components/data-entry/password-strength.d.ts +2 -0
  28. package/dist/components/data-entry/password-strength.js +8 -7
  29. package/dist/components/data-entry/search-input.d.ts +1 -1
  30. package/dist/components/data-entry/search-input.js +22 -2
  31. package/dist/components/data-entry/search-select.js +63 -11
  32. package/dist/components/feedback/sonner.d.ts +7 -1
  33. package/dist/components/feedback/sonner.js +22 -2
  34. package/dist/components/feedback/use-toast.d.ts +3 -1
  35. package/dist/components/feedback/use-toast.js +10 -1
  36. package/dist/components/navigation/dropdown-menu.d.ts +7 -1
  37. package/dist/components/navigation/dropdown-menu.js +18 -2
  38. package/dist/contracts/measurement.json +1 -1
  39. package/dist/i18n/messages/en.json +19 -1
  40. package/dist/i18n/messages/ja.json +18 -1
  41. package/dist/i18n/messages/vi.json +18 -1
  42. package/dist/lib/shortcut.d.ts +17 -0
  43. package/dist/lib/shortcut.js +37 -0
  44. package/dist/props/components/data-entry.prop.d.ts +36 -0
  45. package/dist/styles/control.css +23 -0
  46. package/dist/styles/dialog-layout.css +4 -1
  47. package/dist/styles/layers.json +6 -1
  48. package/dist/styles/navigation-layout.css +26 -0
  49. package/dist/styles/table-layout.css +14 -0
  50. package/dist/styles.css +1 -1
  51. package/dist/tokens/components/navigation.css +6 -0
  52. package/dist/tokens/foundation.css +2 -0
  53. package/package.json +2 -2
@@ -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 ? values.includes(optionValue) : value === optionValue;
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 values) {
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) => !values.includes(option.value) && (fold(option.value) === typed || fold(option.label) === typed)
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
- values,
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 && values.length >= maxCount;
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 already = values.includes(option.value);
312
- if (!already && atMaxCount) return;
313
- const next = already ? values.filter((entry) => entry !== option.value) : [...values, option.value];
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 = [...values];
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) {
@@ -713,6 +743,28 @@ function SearchSelect(props) {
713
743
  }
714
744
  })
715
745
  }
746
+ ) : null,
747
+ needConfirm ? /* @__PURE__ */ jsx(
748
+ "div",
749
+ {
750
+ className: "ui-search-select-footer",
751
+ "data-slot": "search-select-confirm",
752
+ onKeyDown: (event) => event.stopPropagation(),
753
+ children: /* @__PURE__ */ jsxs(Flex, { justify: "end", gap: "sm", children: [
754
+ /* @__PURE__ */ jsx(
755
+ Button,
756
+ {
757
+ type: "button",
758
+ variant: "ghost",
759
+ size: "sm",
760
+ disabled: working.length === 0,
761
+ onClick: () => setPending([]),
762
+ children: t("common.clear")
763
+ }
764
+ ),
765
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: confirm, children: t("dataEntry.timePicker.confirm") })
766
+ ] })
767
+ }
716
768
  ) : null
717
769
  ] }) })
718
770
  }
@@ -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,3 +1,5 @@
1
1
  import { toast as sonnerToast } from "sonner";
2
+ type SonnerToast = typeof sonnerToast;
3
+ declare const toast: SonnerToast;
2
4
  export type { ExternalToast } from "sonner";
3
- export { sonnerToast, sonnerToast as toast };
5
+ export { sonnerToast, toast };
@@ -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
- sonnerToast as toast
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: tag ? tag.props.children : 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.0.4",
3
+ "version": "32.1.0",
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": {
@@ -14,6 +14,7 @@
14
14
  "previous": "前へ",
15
15
  "next": "次へ",
16
16
  "selectedCount": "{count} 件選択",
17
+ "selectedOfTotal": "{total} 件中 {count} 件選択",
17
18
  "typeToConfirm": "確認のため {phrase} と入力"
18
19
  },
19
20
  "dataEntry": {
@@ -32,7 +33,12 @@
32
33
  "placeholder": "期間を選択",
33
34
  "from": "開始",
34
35
  "to": "終了",
35
- "openCalendar": "カレンダーを開く"
36
+ "openCalendar": "カレンダーを開く",
37
+ "selectEnd": "終了日を選択",
38
+ "days": {
39
+ "other": "{count}日間"
40
+ },
41
+ "summary": "{range}({days})"
36
42
  },
37
43
  "timePicker": {
38
44
  "placeholder": "時刻を選択",
@@ -50,6 +56,10 @@
50
56
  "searchInput": {
51
57
  "placeholder": "検索…"
52
58
  },
59
+ "commandPalette": {
60
+ "recent": "最近の検索",
61
+ "clearRecent": "検索履歴を消去"
62
+ },
53
63
  "autocomplete": {
54
64
  "placeholder": "検索または選択…",
55
65
  "empty": "結果が見つかりません"
@@ -237,10 +247,14 @@
237
247
  "dropped": "{label} を {count} 件中 {position} 番目に置きました。",
238
248
  "cancelled": "移動を取り消しました。{label} は {count} 件中 {position} 番目に戻りました。"
239
249
  },
250
+ "checkboxGroup": {
251
+ "selectAll": "すべて選択"
252
+ },
240
253
  "passwordStrength": {
241
254
  "weak": "弱い",
242
255
  "fair": "普通",
243
256
  "strong": "強い",
257
+ "empty": "パスワードを入力",
244
258
  "meter": "パスワードの強度 4 段階中 {score}",
245
259
  "passed": "達成: ",
246
260
  "failed": "未達成: ",
@@ -263,6 +277,9 @@
263
277
  "dismiss": "閉じる",
264
278
  "verifying": "確認中…",
265
279
  "stepUpFailed": "再認証に失敗しました。もう一度お試しください。"
280
+ },
281
+ "toast": {
282
+ "close": "通知を閉じる"
266
283
  }
267
284
  },
268
285
  "layout": {
@@ -14,6 +14,7 @@
14
14
  "previous": "Trước",
15
15
  "next": "Tiếp",
16
16
  "selectedCount": "{count} đã chọn",
17
+ "selectedOfTotal": "Đã chọn {count}/{total}",
17
18
  "typeToConfirm": "Nhập {phrase} để xác nhận"
18
19
  },
19
20
  "dataEntry": {
@@ -32,7 +33,12 @@
32
33
  "placeholder": "Chọn khoảng ngày",
33
34
  "from": "Từ",
34
35
  "to": "Đến",
35
- "openCalendar": "Mở lịch"
36
+ "openCalendar": "Mở lịch",
37
+ "selectEnd": "Chọn ngày kết thúc",
38
+ "days": {
39
+ "other": "{count} ngày"
40
+ },
41
+ "summary": "{range} · {days}"
36
42
  },
37
43
  "timePicker": {
38
44
  "placeholder": "Chọn giờ",
@@ -50,6 +56,10 @@
50
56
  "searchInput": {
51
57
  "placeholder": "Tìm kiếm…"
52
58
  },
59
+ "commandPalette": {
60
+ "recent": "Tìm kiếm gần đây",
61
+ "clearRecent": "Xóa lịch sử tìm kiếm"
62
+ },
53
63
  "autocomplete": {
54
64
  "placeholder": "Tìm hoặc chọn…",
55
65
  "empty": "Không tìm thấy kết quả"
@@ -238,10 +248,14 @@
238
248
  "dropped": "Đã thả {label} ở vị trí {position}/{count}.",
239
249
  "cancelled": "Đã huỷ di chuyển. {label} trở lại vị trí {position}/{count}."
240
250
  },
251
+ "checkboxGroup": {
252
+ "selectAll": "Chọn tất cả"
253
+ },
241
254
  "passwordStrength": {
242
255
  "weak": "Yếu",
243
256
  "fair": "Trung bình",
244
257
  "strong": "Mạnh",
258
+ "empty": "Nhập mật khẩu",
245
259
  "meter": "Độ mạnh mật khẩu {score} trên 4",
246
260
  "passed": "Đạt: ",
247
261
  "failed": "Chưa đạt: ",
@@ -264,6 +278,9 @@
264
278
  "dismiss": "Đóng",
265
279
  "verifying": "Đang xác minh…",
266
280
  "stepUpFailed": "Xác thực lại thất bại. Vui lòng thử lại."
281
+ },
282
+ "toast": {
283
+ "close": "Đóng thông báo"
267
284
  }
268
285
  },
269
286
  "layout": {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A global keyboard shortcut, parsed from a combo string. Shared by `CommandPalette` (toggles the
3
+ * dialog) and `SearchInput` (focuses the field) so both read `"mod+k"` the same way (gh#1250).
4
+ */
5
+ export type ShortcutSpec = {
6
+ key: string;
7
+ mod: boolean;
8
+ shift: boolean;
9
+ alt: boolean;
10
+ meta: boolean;
11
+ ctrl: boolean;
12
+ };
13
+ /** `"mod+shift+p"` → its parts. `true` is `mod+k`. */
14
+ export declare function parseShortcut(shortcut: boolean | string): ShortcutSpec | null;
15
+ export declare function shortcutMatches(spec: ShortcutSpec, event: KeyboardEvent): boolean;
16
+ /** The hint a trigger shows: `mod+shift+p` → `⌘⇧P`. */
17
+ export declare function shortcutHint(spec: ShortcutSpec): string;
@@ -0,0 +1,37 @@
1
+ import { isApplePlatform } from "./platform.js";
2
+ function parseShortcut(shortcut) {
3
+ if (shortcut === false) return null;
4
+ const parts = (shortcut === true ? "mod+k" : shortcut).toLowerCase().split("+").map((p) => p.trim());
5
+ const key = parts.pop() ?? "";
6
+ if (!key) return null;
7
+ return {
8
+ key,
9
+ mod: parts.includes("mod"),
10
+ meta: parts.includes("meta") || parts.includes("cmd"),
11
+ ctrl: parts.includes("ctrl"),
12
+ shift: parts.includes("shift"),
13
+ alt: parts.includes("alt") || parts.includes("option")
14
+ };
15
+ }
16
+ function shortcutMatches(spec, event) {
17
+ const keyMatches = event.key.toLowerCase() === spec.key || /^[a-z]$/.test(spec.key) && event.code === `Key${spec.key.toUpperCase()}`;
18
+ if (!keyMatches) return false;
19
+ if (spec.mod && !(isApplePlatform() ? event.metaKey : event.ctrlKey)) return false;
20
+ if (spec.meta && !event.metaKey) return false;
21
+ if (spec.ctrl && !event.ctrlKey) return false;
22
+ return event.shiftKey === spec.shift && event.altKey === spec.alt;
23
+ }
24
+ function shortcutHint(spec) {
25
+ return [
26
+ spec.meta || spec.mod && isApplePlatform() ? "\u2318" : "",
27
+ spec.ctrl || spec.mod && !isApplePlatform() ? "Ctrl+" : "",
28
+ spec.alt ? "\u2325" : "",
29
+ spec.shift ? "\u21E7" : "",
30
+ spec.key.length === 1 ? spec.key.toUpperCase() : spec.key
31
+ ].join("");
32
+ }
33
+ export {
34
+ parseShortcut,
35
+ shortcutHint,
36
+ shortcutMatches
37
+ };
@@ -468,6 +468,13 @@ export type SearchInputProp = FieldA11yProps & {
468
468
  status?: ControlStatusProp;
469
469
  /** Chrome level — antd `variant`. Default `outlined`. */
470
470
  variant?: ControlVariantProp;
471
+ /**
472
+ * A global shortcut that FOCUSES the field (gh#1250). `false` (default) binds none; `true` is
473
+ * ⌘K on Apple platforms and Ctrl+K elsewhere; a combo string (`"mod+/"`, `"mod+shift+f"`) binds
474
+ * that, read exactly as `CommandPalette.shortcut`. Shows a trailing key-cap hint while the field
475
+ * is empty. Never fires during IME composition, nor while `disabled`.
476
+ */
477
+ shortcut?: boolean | string;
471
478
  };
472
479
  /**
473
480
  * @see Checkbox
@@ -510,6 +517,16 @@ export type CheckboxGroupProp = FieldA11yProps & {
510
517
  id?: IdProp;
511
518
  className?: ClassNameProp;
512
519
  children?: React.ReactNode;
520
+ /**
521
+ * A leading "select all" box (gh#1245), `options` form only. Its state is derived from the group
522
+ * value — checked when every enabled option is selected, mixed on a partial selection, clear
523
+ * otherwise — and it is followed by a muted "{n} of {m} selected" count and a separator. One
524
+ * click is one `onValueChange`. Disabled options are outside it in both directions: neither
525
+ * counted nor added nor removed. `{ label }` replaces the default "Select all" text.
526
+ */
527
+ selectAll?: boolean | {
528
+ label?: React.ReactNode;
529
+ };
513
530
  };
514
531
  /** @see Radio.Group */
515
532
  export type RadioGroupProp = FieldA11yProps & {
@@ -1014,6 +1031,15 @@ export type DatePickerProp = DatePickerBaseProp & ({
1014
1031
  showTime?: false;
1015
1032
  /** Which endpoints may stay empty (antd `allowEmpty`), as `[from, to]`. */
1016
1033
  allowEmpty?: [boolean, boolean];
1034
+ /**
1035
+ * A polite live summary under the day grid: the start date while only the start is
1036
+ * held, then `Intl.DateTimeFormat#formatRange` of both edges and the INCLUSIVE day count,
1037
+ * plural-aware (en "7 days", ja 「7日間」). Default `true`. Day grid only — the
1038
+ * month / quarter / year period grids have no day count to report and ignore it. The
1039
+ * `to` edge's "Select end date" prompt while only the start is held is independent of
1040
+ * this flag.
1041
+ */
1042
+ showSummary?: boolean;
1017
1043
  presets?: {
1018
1044
  label: React.ReactNode;
1019
1045
  value: DateRange | (() => DateRange);
@@ -1623,6 +1649,14 @@ export type SearchSelectMultipleProp = {
1623
1649
  * a token only counts when it matches a row; in `mode="tags"` it is accepted as it stands.
1624
1650
  */
1625
1651
  tokenSeparators?: string[];
1652
+ /**
1653
+ * Hold the panel's picks as a DRAFT until OK commits them (antd's `needConfirm`, the same word
1654
+ * `TimePicker` / `DatePicker` use). Row toggles move a pending set; the footer's OK fires ONE
1655
+ * `onValueChange` with the whole set and closes; its Clear empties the pending set; Escape or an
1656
+ * outside click discards it, so the trigger keeps the committed values. `onSelect` /
1657
+ * `onDeselect` do not fire for draft toggles. Default `false` (commit on every toggle).
1658
+ */
1659
+ needConfirm?: boolean;
1626
1660
  } & SelectTagsCreateProp;
1627
1661
  /**
1628
1662
  * antd `labelInValue`, single. The value carries its own label, so a screen that never sees the
@@ -1650,6 +1684,8 @@ export type SelectLabelInValueMultipleProp = {
1650
1684
  maxTagPlaceholder?: MaxTagPlaceholderProp;
1651
1685
  maxTagTextLength?: number;
1652
1686
  tokenSeparators?: string[];
1687
+ /** @see SearchSelectMultipleProp.needConfirm */
1688
+ needConfirm?: boolean;
1653
1689
  tagRender?: (props: {
1654
1690
  value: string;
1655
1691
  label: React.ReactNode;