@godxjp/ui 32.3.3 → 32.4.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.
@@ -3,7 +3,7 @@
3
3
  You are about to write code against a design system you did not author. This file is the whole
4
4
  contract. Read it before you write JSX.
5
5
 
6
- **This catalog describes `@godxjp/ui` 32.3.3.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 32.4.0.** If the project you are editing has a different
7
7
  version in its `package.json`, read the pinned catalog for THAT version instead
8
8
  (`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
9
9
  not exist yet; older, and it hides props that do. Neither failure announces itself.
@@ -42,7 +42,7 @@
42
42
  "type": "number"
43
43
  },
44
44
  {
45
- "description": "Files larger than this limit are rejected with localized feedback and onReject.",
45
+ "description": "Files larger than this limit are rejected with ONE localized message that states the limit (「big.pdf: ファイルが大きすぎます(上限 10.0 MB)。」, gh#1280) and onReject.",
46
46
  "name": "maxSizeBytes",
47
47
  "type": "number"
48
48
  },
@@ -68,7 +68,7 @@
68
68
  "type": "string"
69
69
  },
70
70
  {
71
- "description": "Lands on the native `<input type=\"file\">`, NOT on the wrapper — the hidden input is the semantic focus target, so this is what makes a `<label htmlFor>` (or FormField, which injects it) actually focus the picker. Putting it on the visible dropzone instead is the usual reason a label click does nothing.",
71
+ "description": "Lands on the native `<input type=\"file\">` (so `#id` file attachment in e2e works), NOT on the wrapper. Since gh#1280 that input is plumbing — `aria-hidden`, `tabIndex={-1}`, and it forwards focus to the visible trigger, so a FormField label click lands on the dropzone/button. The FormField contract (name = label + the trigger's own action text, helper, aria-invalid/errormessage) is on that trigger, and the \"nothing picked\" state is read as 「未選択」/\"Chưa chọn tệp\" in the APP's locale — the browser's own 「選択されていません」 is no longer exposed.",
72
72
  "name": "id",
73
73
  "type": "string"
74
74
  },
@@ -9719,7 +9719,7 @@
9719
9719
  "type": "number"
9720
9720
  },
9721
9721
  {
9722
- "description": "Files larger than this limit are rejected with localized feedback and onReject.",
9722
+ "description": "Files larger than this limit are rejected with ONE localized message that states the limit (「big.pdf: ファイルが大きすぎます(上限 10.0 MB)。」, gh#1280) and onReject.",
9723
9723
  "name": "maxSizeBytes",
9724
9724
  "type": "number"
9725
9725
  },
@@ -9745,7 +9745,7 @@
9745
9745
  "type": "string"
9746
9746
  },
9747
9747
  {
9748
- "description": "Lands on the native `<input type=\"file\">`, NOT on the wrapper — the hidden input is the semantic focus target, so this is what makes a `<label htmlFor>` (or FormField, which injects it) actually focus the picker. Putting it on the visible dropzone instead is the usual reason a label click does nothing.",
9748
+ "description": "Lands on the native `<input type=\"file\">` (so `#id` file attachment in e2e works), NOT on the wrapper. Since gh#1280 that input is plumbing — `aria-hidden`, `tabIndex={-1}`, and it forwards focus to the visible trigger, so a FormField label click lands on the dropzone/button. The FormField contract (name = label + the trigger's own action text, helper, aria-invalid/errormessage) is on that trigger, and the \"nothing picked\" state is read as 「未選択」/\"Chưa chọn tệp\" in the APP's locale — the browser's own 「選択されていません」 is no longer exposed.",
9749
9749
  "name": "id",
9750
9750
  "type": "string"
9751
9751
  },
package/agent/index.json CHANGED
@@ -48,7 +48,7 @@
48
48
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
49
49
  "read": {
50
50
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.3.3/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.4.0/agent/index.json"
52
52
  },
53
53
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
54
54
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
@@ -62,5 +62,5 @@
62
62
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
63
63
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
64
64
  },
65
- "version": "32.3.3"
65
+ "version": "32.4.0"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
3
  > A Japanese-enterprise React design system: 161 components, 2195 design tokens,
4
- > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.3.3.
4
+ > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.4.0.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@32.3.3`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@32.4.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -26,7 +26,7 @@ that can only fetch URLs.
26
26
  ## Pinning
27
27
 
28
28
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
- the tag: `.../godx-jp/godxjp-ui/v32.3.3/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v32.4.0/agent/...`. A catalog that does not match the installed
30
30
  package describes props that are absent, or hides props that are present, and says nothing either way.
31
31
 
32
32
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
@@ -13,7 +13,8 @@ import {
13
13
  import { AppPresetContext } from "./preset.js";
14
14
  import { warnUnknownLocale } from "../i18n/locale-tags.js";
15
15
  import { syncAppRequestHeaders } from "./request-headers.js";
16
- import { syncI18nLocale } from "../i18n/translate.js";
16
+ import { syncI18nLocale, translate } from "../i18n/translate.js";
17
+ import { useOverlayDismissLabel } from "../lib/overlay-dismiss-label.js";
17
18
  const RTL_LANGUAGE_SUBTAGS = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "ps", "sd", "yi", "dv", "ckb"]);
18
19
  function localeDirection(locale) {
19
20
  return RTL_LANGUAGE_SUBTAGS.has(locale.split("-")[0]?.toLowerCase() ?? "") ? "rtl" : "ltr";
@@ -430,6 +431,10 @@ function AppProvider({
430
431
  setScaling
431
432
  ]
432
433
  );
434
+ const isOutermostProvider = React.useContext(AppContext) === null;
435
+ useOverlayDismissLabel(
436
+ isOutermostProvider ? translate(locale, fallbackLocale, "common.dismiss") : null
437
+ );
433
438
  return /* @__PURE__ */ jsx(I18nProvider, { locale, children: /* @__PURE__ */ jsx(AppPresetContext.Provider, { value: preset, children: /* @__PURE__ */ jsx(AppContext.Provider, { value, children }) }) });
434
439
  }
435
440
  function useAppContext() {
@@ -14,7 +14,7 @@ import {
14
14
  import { useTranslation } from "../../i18n/use-translation.js";
15
15
  import { formatBytes } from "../../lib/format.js";
16
16
  import { cn } from "../../lib/utils.js";
17
- import { resolveFieldA11y } from "../../lib/field-a11y.js";
17
+ import { mergeAriaIds, pickFieldA11y } from "../../lib/field-a11y.js";
18
18
  import { Button } from "../general/button.js";
19
19
  import { Icon } from "../general/icon.js";
20
20
  import { UploadCropDialog } from "./upload-crop-dialog.js";
@@ -105,13 +105,17 @@ function Upload({
105
105
  children,
106
106
  ...ariaProps
107
107
  }) {
108
- const { t } = useTranslation();
108
+ const { t, locale } = useTranslation();
109
109
  const disabled = disabledProp || readOnly;
110
- const triggerOwnsAriaLabel = variant === "button";
111
- const inputA11y = resolveFieldA11y(
112
- triggerOwnsAriaLabel ? { ...ariaProps, "aria-label": void 0 } : ariaProps,
113
- t("dataEntry.upload.inputLabel")
114
- );
110
+ const fieldA11y = pickFieldA11y(ariaProps);
111
+ const fieldLabelledBy = fieldA11y["aria-labelledby"];
112
+ const triggerId = React.useId();
113
+ const selectionId = React.useId();
114
+ const triggerRef = React.useRef(null);
115
+ const callerAriaLabel = fieldLabelledBy ? void 0 : fieldA11y["aria-label"];
116
+ const setTriggerRef = (element) => {
117
+ triggerRef.current = element;
118
+ };
115
119
  const resolvedListType = listType ?? (variant === "picture" ? "picture" : "text");
116
120
  const accept = acceptProp ?? defaultAcceptForVariant(variant);
117
121
  const maxCount = maxCountProp ?? defaultMaxCount(variant);
@@ -121,6 +125,18 @@ function Upload({
121
125
  const [cropFile, setCropFile] = React.useState(null);
122
126
  const [previewItem, setPreviewItem] = React.useState(null);
123
127
  const [items, setItems] = useUploadList(value, defaultValue, onValueChange);
128
+ const triggerIsGroup = variant === "dropzone" && !openFileDialogOnClick;
129
+ const triggerA11y = {
130
+ id: triggerId,
131
+ "aria-labelledby": fieldLabelledBy ? `${fieldLabelledBy} ${triggerId}` : void 0,
132
+ "aria-describedby": mergeAriaIds(
133
+ fieldA11y["aria-describedby"],
134
+ triggerIsGroup && fieldA11y["aria-invalid"] ? fieldA11y["aria-errormessage"] : void 0,
135
+ items.length === 0 ? selectionId : void 0
136
+ ),
137
+ "aria-errormessage": triggerIsGroup ? void 0 : fieldA11y["aria-errormessage"],
138
+ "aria-invalid": triggerIsGroup ? void 0 : fieldA11y["aria-invalid"]
139
+ };
124
140
  const isSingleAvatar = variant === "avatar" || variant === "avatar-crop" || variant === "picture" && maxCount === 1;
125
141
  const singleItem = isSingleAvatar ? items[0] ?? null : null;
126
142
  const draft = useUploadDraft({
@@ -235,7 +251,8 @@ function Upload({
235
251
  setRejection(
236
252
  t(
237
253
  `dataEntry.upload.reject${reason[0].toUpperCase()}${reason.slice(1)}`,
238
- { name: file.name }
254
+ // `max` is the limit the size message states (gh#1280); the other reasons ignore it.
255
+ { name: file.name, max: maxSizeBytes != null ? formatBytes(maxSizeBytes, locale) : "" }
239
256
  )
240
257
  );
241
258
  onReject?.({ file, reason, error });
@@ -402,10 +419,13 @@ function Upload({
402
419
  type: "file",
403
420
  ...directory ? { webkitdirectory: "", directory: "" } : {},
404
421
  className: "sr-only",
422
+ "aria-hidden": "true",
423
+ tabIndex: -1,
424
+ "data-field": fieldA11y["data-field"],
405
425
  accept,
406
426
  multiple: multiple && !isSingleAvatar,
407
427
  disabled,
408
- ...inputA11y,
428
+ onFocus: () => triggerRef.current?.focus(),
409
429
  onChange: (e) => {
410
430
  pickFiles(e.target.files);
411
431
  e.target.value = "";
@@ -436,7 +456,8 @@ function Upload({
436
456
  ] })
437
457
  }
438
458
  ),
439
- /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "sr-only", "data-slot": "upload-status", children: rejection ?? (errorCount > 0 ? t("dataEntry.upload.statusFailed", { count: errorCount }) : uploadingCount > 0 ? t("dataEntry.upload.statusUploading", { count: uploadingCount }) : items.length > 0 ? t("dataEntry.upload.statusReady", { count: items.length, done: doneCount }) : "") })
459
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "sr-only", "data-slot": "upload-status", children: errorCount > 0 ? t("dataEntry.upload.statusFailed", { count: errorCount }) : uploadingCount > 0 ? t("dataEntry.upload.statusUploading", { count: uploadingCount }) : items.length > 0 ? t("dataEntry.upload.statusReady", { count: items.length, done: doneCount }) : "" }),
460
+ items.length === 0 ? /* @__PURE__ */ jsx("span", { id: selectionId, className: "sr-only", "data-slot": "upload-selection", children: t("dataEntry.upload.noFileSelected") }) : null
440
461
  ] });
441
462
  const openPicker = () => {
442
463
  if (!disabled && openFileDialogOnClick) inputRef.current?.click();
@@ -449,10 +470,12 @@ function Upload({
449
470
  /* @__PURE__ */ jsxs(
450
471
  "div",
451
472
  {
473
+ ref: setTriggerRef,
452
474
  role: openFileDialogOnClick ? "button" : "group",
453
475
  tabIndex: disabled ? -1 : 0,
454
476
  "aria-disabled": disabled,
455
- "aria-label": t("dataEntry.upload.dropzoneLabel"),
477
+ ...triggerA11y,
478
+ "aria-label": callerAriaLabel ?? t("dataEntry.upload.dropzoneLabel"),
456
479
  onClick: openPicker,
457
480
  onKeyDown: (e) => {
458
481
  if (e.key === "Enter" || e.key === " ") {
@@ -490,7 +513,7 @@ function Upload({
490
513
  if (variant === "button") {
491
514
  const iconOnly = typeof triggerSize === "string" && triggerSize.startsWith("icon");
492
515
  const label = children ?? t("dataEntry.upload.buttonLabel");
493
- const triggerAriaLabel = ariaProps["aria-label"] ?? (iconOnly ? typeof label === "string" ? label : t("dataEntry.upload.buttonLabel") : void 0);
516
+ const triggerAriaLabel = callerAriaLabel ?? (iconOnly ? typeof label === "string" ? label : t("dataEntry.upload.buttonLabel") : void 0);
494
517
  return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
495
518
  hiddenInput,
496
519
  liveRegion,
@@ -503,6 +526,8 @@ function Upload({
503
526
  size: triggerSize,
504
527
  disabled,
505
528
  onClick: openPicker,
529
+ ref: setTriggerRef,
530
+ ...triggerA11y,
506
531
  "aria-label": triggerAriaLabel,
507
532
  className: "ui-upload-trigger",
508
533
  children: [
@@ -547,7 +572,9 @@ function Upload({
547
572
  onClick: openPicker,
548
573
  "data-disabled": disabled ? "" : void 0,
549
574
  className: "ui-upload-tile-add",
550
- "aria-label": t("dataEntry.upload.addImage"),
575
+ ref: setTriggerRef,
576
+ ...triggerA11y,
577
+ "aria-label": callerAriaLabel ?? t("dataEntry.upload.addImage"),
551
578
  children: [
552
579
  /* @__PURE__ */ jsx(ImagePlus, { className: "ui-upload-tile-add-icon", "aria-hidden": "true" }),
553
580
  /* @__PURE__ */ jsx("span", { className: "ui-upload-tile-add-label", children: t("dataEntry.upload.addImage") })
@@ -568,6 +595,9 @@ function Upload({
568
595
  disabled,
569
596
  variant: triggerVariant,
570
597
  onClick: openPicker,
598
+ ref: setTriggerRef,
599
+ ...triggerA11y,
600
+ "aria-label": callerAriaLabel,
571
601
  className: "ui-upload-trigger",
572
602
  children: children ?? t("dataEntry.upload.addImage")
573
603
  }
@@ -591,6 +621,9 @@ function Upload({
591
621
  type: "button",
592
622
  disabled,
593
623
  onClick: openPicker,
624
+ ref: setTriggerRef,
625
+ ...triggerA11y,
626
+ "aria-label": callerAriaLabel,
594
627
  className: "ui-upload-picture-empty",
595
628
  children: [
596
629
  /* @__PURE__ */ jsx(ImagePlus, { className: "ui-upload-picture-empty-icon", "aria-hidden": "true" }),
@@ -641,7 +674,9 @@ function Upload({
641
674
  "data-pending-delete": item?.pendingDelete ? "" : void 0,
642
675
  "data-disabled": disabled ? "" : void 0,
643
676
  className: "ui-upload-avatar ui-focus-ring",
644
- "aria-label": t("dataEntry.upload.avatarLabel"),
677
+ ref: setTriggerRef,
678
+ ...triggerA11y,
679
+ "aria-label": callerAriaLabel ?? t("dataEntry.upload.avatarLabel"),
645
680
  children: [
646
681
  !showPlaceholder && item?.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-avatar-image" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-placeholder", children: /* @__PURE__ */ jsx(Camera, { className: "ui-upload-avatar-icon", "aria-hidden": "true" }) }),
647
682
  !disabled && /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-change", children: t("dataEntry.upload.change") })
@@ -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.3",
3
+ "version": "32.4.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,
@@ -15,7 +15,8 @@
15
15
  "next": "Next",
16
16
  "selectedCount": "{count} selected",
17
17
  "selectedOfTotal": "{count} of {total} selected",
18
- "typeToConfirm": "Type {phrase} to confirm"
18
+ "typeToConfirm": "Type {phrase} to confirm",
19
+ "dismiss": "Dismiss"
19
20
  },
20
21
  "dataEntry": {
21
22
  "calendar": {
@@ -89,7 +90,6 @@
89
90
  "cropTitle": "Crop photo",
90
91
  "cropZoom": "Zoom",
91
92
  "cropConfirm": "Use photo",
92
- "inputLabel": "Choose files",
93
93
  "statusUploading": "Uploading {count} file(s)…",
94
94
  "statusReady": "{count} file(s), {done} uploaded",
95
95
  "statusFailed": "{count} upload(s) failed",
@@ -102,11 +102,12 @@
102
102
  "start": "Upload",
103
103
  "cancel": "Cancel",
104
104
  "rejectAccept": "{name}: file type is not allowed.",
105
- "rejectSize": "{name}: file is too large.",
105
+ "rejectSize": "{name}: file is too large (limit {max}).",
106
106
  "rejectCount": "{name}: file limit reached.",
107
107
  "rejectBeforeUpload": "{name}: file validation failed.",
108
108
  "readFailed": "Could not read the selected folder.",
109
- "callbackFailed": "The selected files could not be added."
109
+ "callbackFailed": "The selected files could not be added.",
110
+ "noFileSelected": "No file selected"
110
111
  },
111
112
  "cascader": {
112
113
  "placeholder": "Select…",
@@ -15,7 +15,8 @@
15
15
  "next": "次へ",
16
16
  "selectedCount": "{count} 件選択",
17
17
  "selectedOfTotal": "{total} 件中 {count} 件選択",
18
- "typeToConfirm": "確認のため {phrase} と入力"
18
+ "typeToConfirm": "確認のため {phrase} と入力",
19
+ "dismiss": "閉じる"
19
20
  },
20
21
  "dataEntry": {
21
22
  "calendar": {
@@ -88,7 +89,6 @@
88
89
  "cropTitle": "写真を切り抜き",
89
90
  "cropZoom": "ズーム",
90
91
  "cropConfirm": "この写真を使用",
91
- "inputLabel": "ファイルを選択",
92
92
  "statusUploading": "{count} 件をアップロード中…",
93
93
  "statusReady": "{count} 件中 {done} 件アップロード済み",
94
94
  "statusFailed": "{count} 件のアップロードに失敗",
@@ -101,11 +101,12 @@
101
101
  "start": "アップロード",
102
102
  "cancel": "キャンセル",
103
103
  "rejectAccept": "{name}: 許可されていない形式です。",
104
- "rejectSize": "{name}: ファイルが大きすぎます。",
104
+ "rejectSize": "{name}: ファイルが大きすぎます(上限 {max})。",
105
105
  "rejectCount": "{name}: ファイル数の上限です。",
106
106
  "rejectBeforeUpload": "{name}: ファイルの検証に失敗しました。",
107
107
  "readFailed": "選択したフォルダーを読み取れませんでした。",
108
- "callbackFailed": "選択したファイルを追加できませんでした。"
108
+ "callbackFailed": "選択したファイルを追加できませんでした。",
109
+ "noFileSelected": "未選択"
109
110
  },
110
111
  "cascader": {
111
112
  "placeholder": "選択…",
@@ -15,7 +15,8 @@
15
15
  "next": "Tiếp",
16
16
  "selectedCount": "{count} đã chọn",
17
17
  "selectedOfTotal": "Đã chọn {count}/{total}",
18
- "typeToConfirm": "Nhập {phrase} để xác nhận"
18
+ "typeToConfirm": "Nhập {phrase} để xác nhận",
19
+ "dismiss": "Bỏ qua"
19
20
  },
20
21
  "dataEntry": {
21
22
  "calendar": {
@@ -88,7 +89,6 @@
88
89
  "cropTitle": "Cắt ảnh",
89
90
  "cropZoom": "Thu phóng",
90
91
  "cropConfirm": "Dùng ảnh này",
91
- "inputLabel": "Chọn tệp",
92
92
  "statusUploading": "Đang tải lên {count} tệp…",
93
93
  "statusReady": "{count} tệp, đã tải lên {done}",
94
94
  "statusFailed": "{count} tệp tải lên thất bại",
@@ -101,11 +101,12 @@
101
101
  "start": "Tải lên",
102
102
  "cancel": "Huỷ",
103
103
  "rejectAccept": "{name}: loại tệp không được phép.",
104
- "rejectSize": "{name}: tệp quá lớn.",
104
+ "rejectSize": "{name}: tệp quá lớn (tối đa {max}).",
105
105
  "rejectCount": "{name}: đã đủ số lượng tệp.",
106
106
  "rejectBeforeUpload": "{name}: kiểm tra tệp thất bại.",
107
107
  "readFailed": "Không thể đọc thư mục đã chọn.",
108
- "callbackFailed": "Không thể thêm các tệp đã chọn."
108
+ "callbackFailed": "Không thể thêm các tệp đã chọn.",
109
+ "noFileSelected": "Chưa chọn tệp"
109
110
  },
110
111
  "cascader": {
111
112
  "placeholder": "Chọn…",
@@ -0,0 +1,2 @@
1
+ /** Keep every React Aria dismiss button in the document named `label`; `null` does nothing. */
2
+ export declare function useOverlayDismissLabel(label: string | null): void;
@@ -0,0 +1,41 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const CANDIDATE = 'button[tabindex="-1"]';
4
+ function isDismissButton(element) {
5
+ return element instanceof HTMLButtonElement && element.matches(CANDIDATE) && element.style.width === "1px" && element.style.height === "1px";
6
+ }
7
+ function relabel(root, label) {
8
+ const candidates = root.matches(CANDIDATE) ? [root] : Array.from(root.querySelectorAll(CANDIDATE));
9
+ for (const element of candidates) {
10
+ if (isDismissButton(element) && element.getAttribute("aria-label") !== label) {
11
+ element.setAttribute("aria-label", label);
12
+ }
13
+ }
14
+ }
15
+ function useOverlayDismissLabel(label) {
16
+ React.useEffect(() => {
17
+ if (label === null || typeof MutationObserver === "undefined" || !document.body) return;
18
+ relabel(document.body, label);
19
+ const observer = new MutationObserver((records) => {
20
+ for (const record of records) {
21
+ if (record.type === "attributes") {
22
+ if (record.target instanceof Element) relabel(record.target, label);
23
+ continue;
24
+ }
25
+ for (const node of record.addedNodes) {
26
+ if (node instanceof Element) relabel(node, label);
27
+ }
28
+ }
29
+ });
30
+ observer.observe(document.body, {
31
+ childList: true,
32
+ subtree: true,
33
+ attributes: true,
34
+ attributeFilter: ["aria-label"]
35
+ });
36
+ return () => observer.disconnect();
37
+ }, [label]);
38
+ }
39
+ export {
40
+ useOverlayDismissLabel
41
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "32.3.3",
2
+ "version": "32.4.0",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "32.3.3",
4
- "godxUiMcp": "32.3.3",
3
+ "version": "32.4.0",
4
+ "godxUiMcp": "32.4.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",