@godxjp/ui 32.3.3 → 32.4.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/agent/START-HERE.md +1 -1
- package/agent/components/AlertDialog.json +1 -1
- package/agent/components/AppProvider.json +1 -1
- package/agent/components/Upload.json +2 -2
- package/agent/components.json +4 -4
- package/agent/index.json +2 -2
- package/agent/llms.txt +3 -3
- package/dist/app/app-provider.js +17 -1
- package/dist/app/storage.d.ts +6 -0
- package/dist/app/storage.js +2 -1
- package/dist/components/data-entry/select.js +7 -1
- package/dist/components/data-entry/upload.js +49 -14
- package/dist/components/feedback/dialog.js +12 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +7 -5
- package/dist/i18n/messages/ja.json +7 -5
- package/dist/i18n/messages/vi.json +9 -7
- package/dist/lib/overlay-dismiss-label.d.ts +2 -0
- package/dist/lib/overlay-dismiss-label.js +41 -0
- package/dist/styles/layers.json +1 -1
- package/dist/themes/godx.d.ts +4 -0
- package/dist/themes/godx.js +0 -1
- package/package.json +2 -2
package/agent/START-HERE.md
CHANGED
|
@@ -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.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 32.4.1.** 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.
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"type": "string"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
"description": "Dismiss action label
|
|
34
|
+
"description": "Dismiss action label. Defaults to `feedback.alert.cancel` — en 'Cancel', ja 'キャンセル', vi 'Quay lại' (not 'Hủy': in Vietnamese that is also the verb of the destructive actions a confirm guards, gh#1284). A development warning fires when `confirmLabel` and the cancel label are the same word once tone marks are ignored.",
|
|
35
35
|
"name": "cancelLabel",
|
|
36
36
|
"type": "string"
|
|
37
37
|
},
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "string (BCP-47)"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
|
-
"description": "Product defaults the host opts into — `import { godxPreset } from \"@godxjp/ui/themes/godx\"` (pair it with `@godxjp/ui/themes/godx.css` after the base stylesheet; a shadow root that embeds `@godxjp/ui/styles/core` takes the font-free `@godxjp/ui/themes/godx-tokens.css` instead, since `@font-face` must live in the document). Its `defaultLocale`
|
|
13
|
+
"description": "Product defaults the host opts into — `import { godxPreset } from \"@godxjp/ui/themes/godx\"` (pair it with `@godxjp/ui/themes/godx.css` after the base stylesheet; a shadow root that embeds `@godxjp/ui/styles/core` takes the font-free `@godxjp/ui/themes/godx-tokens.css` instead, since `@font-face` must live in the document). Its `defaultLocale` fills only what the props leave unset — it pins NO `timeZone` since 32.4.1 (gh#1282: it pinned `Asia/Ho_Chi_Minh` and every app showed Vietnam time to users in Japan; pass `defaultTimezone` for a fixed zone), its `brandMark` is what an `AuthIdentity` with no `brand` draws, and its `name` is written to `<html data-preset>`. Omit for the neutral defaults (ink primary, `en`, no product mark).",
|
|
14
14
|
"name": "preset",
|
|
15
15
|
"type": "AppPreset"
|
|
16
16
|
},
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"type": "number"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
|
-
"description": "Files larger than this limit are rejected with localized
|
|
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\"
|
|
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
|
},
|
package/agent/components.json
CHANGED
|
@@ -7664,7 +7664,7 @@
|
|
|
7664
7664
|
"type": "string"
|
|
7665
7665
|
},
|
|
7666
7666
|
{
|
|
7667
|
-
"description": "Dismiss action label
|
|
7667
|
+
"description": "Dismiss action label. Defaults to `feedback.alert.cancel` — en 'Cancel', ja 'キャンセル', vi 'Quay lại' (not 'Hủy': in Vietnamese that is also the verb of the destructive actions a confirm guards, gh#1284). A development warning fires when `confirmLabel` and the cancel label are the same word once tone marks are ignored.",
|
|
7668
7668
|
"name": "cancelLabel",
|
|
7669
7669
|
"type": "string"
|
|
7670
7670
|
},
|
|
@@ -8641,7 +8641,7 @@
|
|
|
8641
8641
|
"type": "string (BCP-47)"
|
|
8642
8642
|
},
|
|
8643
8643
|
{
|
|
8644
|
-
"description": "Product defaults the host opts into — `import { godxPreset } from \"@godxjp/ui/themes/godx\"` (pair it with `@godxjp/ui/themes/godx.css` after the base stylesheet; a shadow root that embeds `@godxjp/ui/styles/core` takes the font-free `@godxjp/ui/themes/godx-tokens.css` instead, since `@font-face` must live in the document). Its `defaultLocale`
|
|
8644
|
+
"description": "Product defaults the host opts into — `import { godxPreset } from \"@godxjp/ui/themes/godx\"` (pair it with `@godxjp/ui/themes/godx.css` after the base stylesheet; a shadow root that embeds `@godxjp/ui/styles/core` takes the font-free `@godxjp/ui/themes/godx-tokens.css` instead, since `@font-face` must live in the document). Its `defaultLocale` fills only what the props leave unset — it pins NO `timeZone` since 32.4.1 (gh#1282: it pinned `Asia/Ho_Chi_Minh` and every app showed Vietnam time to users in Japan; pass `defaultTimezone` for a fixed zone), its `brandMark` is what an `AuthIdentity` with no `brand` draws, and its `name` is written to `<html data-preset>`. Omit for the neutral defaults (ink primary, `en`, no product mark).",
|
|
8645
8645
|
"name": "preset",
|
|
8646
8646
|
"type": "AppPreset"
|
|
8647
8647
|
},
|
|
@@ -9719,7 +9719,7 @@
|
|
|
9719
9719
|
"type": "number"
|
|
9720
9720
|
},
|
|
9721
9721
|
{
|
|
9722
|
-
"description": "Files larger than this limit are rejected with localized
|
|
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\"
|
|
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.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.4.1/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.
|
|
65
|
+
"version": "32.4.1"
|
|
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.
|
|
4
|
+
> 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.4.1.
|
|
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.
|
|
7
|
+
(`npx @godxjp/ui-mcp@32.4.1`). 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.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v32.4.1/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.
|
package/dist/app/app-provider.js
CHANGED
|
@@ -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";
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
APP_REQUEST_HEADER_TIME_FORMAT,
|
|
43
44
|
APP_REQUEST_HEADER_TIMEZONE
|
|
44
45
|
} from "./types.js";
|
|
46
|
+
const LEGACY_GODX_PRESET_TIMEZONE = "Asia/Ho_Chi_Minh";
|
|
45
47
|
function resolveProviderLocale(defaultLocale, presetLocale) {
|
|
46
48
|
const fromProp = resolveRegisteredLocale(defaultLocale);
|
|
47
49
|
if (fromProp) return fromProp;
|
|
@@ -137,6 +139,7 @@ function AppProvider({
|
|
|
137
139
|
[persistKey]
|
|
138
140
|
);
|
|
139
141
|
const formatChosenRef = React.useRef({ dateFormat: false, timeFormat: false });
|
|
142
|
+
const timezoneChosenRef = React.useRef(false);
|
|
140
143
|
const commitPreferences = React.useCallback(() => {
|
|
141
144
|
if (persistedAxes.size === 0) return;
|
|
142
145
|
const out = pickPersistedAxes(prefsRef.current, persistedAxes);
|
|
@@ -148,6 +151,10 @@ function AppProvider({
|
|
|
148
151
|
if (formatChosenRef.current.timeFormat) out.timeFormatChosen = true;
|
|
149
152
|
else delete out.timeFormat;
|
|
150
153
|
}
|
|
154
|
+
if (out.timezone !== void 0) {
|
|
155
|
+
if (timezoneChosenRef.current) out.timezoneChosen = true;
|
|
156
|
+
else delete out.timezone;
|
|
157
|
+
}
|
|
151
158
|
writeStoredPreferences(storageKey, out);
|
|
152
159
|
}, [persistedAxes, storageKey]);
|
|
153
160
|
React.useEffect(() => {
|
|
@@ -165,6 +172,10 @@ function AppProvider({
|
|
|
165
172
|
);
|
|
166
173
|
if (derivable) delete stored.timeFormat;
|
|
167
174
|
}
|
|
175
|
+
if (!raw.timezoneChosen && stored.timezone === LEGACY_GODX_PRESET_TIMEZONE) {
|
|
176
|
+
delete stored.timezone;
|
|
177
|
+
}
|
|
178
|
+
timezoneChosenRef.current = stored.timezone !== void 0;
|
|
168
179
|
formatChosenRef.current = {
|
|
169
180
|
dateFormat: stored.dateFormat !== void 0,
|
|
170
181
|
timeFormat: stored.timeFormat !== void 0
|
|
@@ -272,6 +283,7 @@ function AppProvider({
|
|
|
272
283
|
const setTimezone = React.useCallback(
|
|
273
284
|
(next) => {
|
|
274
285
|
prefsRef.current = { ...prefsRef.current, timezone: next };
|
|
286
|
+
timezoneChosenRef.current = true;
|
|
275
287
|
setTimezoneState(next);
|
|
276
288
|
onTimezoneChange?.(next);
|
|
277
289
|
commitPreferences();
|
|
@@ -430,6 +442,10 @@ function AppProvider({
|
|
|
430
442
|
setScaling
|
|
431
443
|
]
|
|
432
444
|
);
|
|
445
|
+
const isOutermostProvider = React.useContext(AppContext) === null;
|
|
446
|
+
useOverlayDismissLabel(
|
|
447
|
+
isOutermostProvider ? translate(locale, fallbackLocale, "common.dismiss") : null
|
|
448
|
+
);
|
|
433
449
|
return /* @__PURE__ */ jsx(I18nProvider, { locale, children: /* @__PURE__ */ jsx(AppPresetContext.Provider, { value: preset, children: /* @__PURE__ */ jsx(AppContext.Provider, { value, children }) }) });
|
|
434
450
|
}
|
|
435
451
|
function useAppContext() {
|
package/dist/app/storage.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export type StoredAppPreferences = {
|
|
|
20
20
|
*/
|
|
21
21
|
dateFormatChosen?: boolean;
|
|
22
22
|
timeFormatChosen?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The viewer PICKED the stored timezone (setTimezone), as opposed to it being the default. Only a
|
|
25
|
+
* picked zone is stored (gh#1282): storing the default froze it, which is how the godx preset's
|
|
26
|
+
* pinned `Asia/Ho_Chi_Minh` outlived the preset change.
|
|
27
|
+
*/
|
|
28
|
+
timezoneChosen?: boolean;
|
|
23
29
|
};
|
|
24
30
|
/**
|
|
25
31
|
* The axes a viewer's preferences are made of — the keys of {@link StoredAppPreferences}.
|
package/dist/app/storage.js
CHANGED
|
@@ -50,7 +50,8 @@ function readStoredPreferences(storageKey) {
|
|
|
50
50
|
fontSize: isAppFontSize(parsed.fontSize) ? parsed.fontSize : void 0,
|
|
51
51
|
scaling: typeof parsed.scaling === "number" && Number.isFinite(parsed.scaling) ? parsed.scaling : void 0,
|
|
52
52
|
dateFormatChosen: parsed.dateFormatChosen === true ? true : void 0,
|
|
53
|
-
timeFormatChosen: parsed.timeFormatChosen === true ? true : void 0
|
|
53
|
+
timeFormatChosen: parsed.timeFormatChosen === true ? true : void 0,
|
|
54
|
+
timezoneChosen: parsed.timezoneChosen === true ? true : void 0
|
|
54
55
|
};
|
|
55
56
|
} catch {
|
|
56
57
|
return {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useLayoutEffect } from "@react-aria/utils";
|
|
4
5
|
import {
|
|
5
6
|
Button as AriaButton,
|
|
6
7
|
ButtonContext,
|
|
@@ -631,6 +632,11 @@ function DataSelect(props) {
|
|
|
631
632
|
"aria-label": props["aria-label"],
|
|
632
633
|
"aria-labelledby": props["aria-labelledby"]
|
|
633
634
|
});
|
|
635
|
+
const [hasNativeLabel, setHasNativeLabel] = React.useState(false);
|
|
636
|
+
useLayoutEffect(() => {
|
|
637
|
+
const labelled = typeof document !== "undefined" && props.id !== void 0 && document.querySelector(`label[for="${CSS.escape(props.id)}"]`) !== null;
|
|
638
|
+
setHasNativeLabel(labelled);
|
|
639
|
+
}, [props.id]);
|
|
634
640
|
if (props.mode !== void 0 || searchable) {
|
|
635
641
|
const antdFilter = showSearchConfig?.filterOption;
|
|
636
642
|
const filterOption = antdFilter === void 0 ? props.filterOption : antdFilter === false ? () => true : antdFilter === true ? void 0 : (option, query) => antdFilter(query, option);
|
|
@@ -696,7 +702,7 @@ function DataSelect(props) {
|
|
|
696
702
|
Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
|
|
697
703
|
);
|
|
698
704
|
const placeholderName = typeof placeholder === "string" ? placeholder : void 0;
|
|
699
|
-
const nameFromPlaceholder = ariaProps["aria-label"] === void 0 && ariaProps["aria-labelledby"] === void 0 && inheritedName["aria-label"] === void 0 && inheritedName["aria-labelledby"] === void 0 ? placeholderName : void 0;
|
|
705
|
+
const nameFromPlaceholder = ariaProps["aria-label"] === void 0 && ariaProps["aria-labelledby"] === void 0 && inheritedName["aria-label"] === void 0 && inheritedName["aria-labelledby"] === void 0 && !hasNativeLabel ? placeholderName : void 0;
|
|
700
706
|
const optionTestId = (optionValue) => dataTestId ? `${dataTestId}-option-${optionValue}` : void 0;
|
|
701
707
|
let flatIndex = 0;
|
|
702
708
|
const renderItem = (option) => {
|
|
@@ -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 {
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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") })
|
|
@@ -404,6 +404,11 @@ function DialogCancel({ asChild, className, onClick, ...props }) {
|
|
|
404
404
|
DialogCancel.displayName = "DialogCancel";
|
|
405
405
|
const AlertDialogAction = DialogAction;
|
|
406
406
|
const AlertDialogCancel = DialogCancel;
|
|
407
|
+
function sameWordIgnoringMarks(a, b) {
|
|
408
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
409
|
+
const base = (text) => text.normalize("NFD").replace(/\p{M}/gu, "").replace(/đ/g, "d").replace(/Đ/g, "D").trim().toLowerCase();
|
|
410
|
+
return base(a) === base(b);
|
|
411
|
+
}
|
|
407
412
|
function AlertDialog({
|
|
408
413
|
open,
|
|
409
414
|
onOpenChange,
|
|
@@ -431,7 +436,13 @@ function AlertDialog({
|
|
|
431
436
|
const effectiveVariant = needsPhrase ? "destructive" : variant;
|
|
432
437
|
const showDangerIcon = effectiveVariant === "destructive";
|
|
433
438
|
const resolvedConfirm = confirmLabel ?? (needsPhrase ? t("common.delete") : t("common.continue"));
|
|
434
|
-
const resolvedCancel = cancelLabel ?? t("
|
|
439
|
+
const resolvedCancel = cancelLabel ?? t("feedback.alert.cancel");
|
|
440
|
+
React.useEffect(() => {
|
|
441
|
+
if (!isDevelopment() || !sameWordIgnoringMarks(resolvedConfirm, resolvedCancel)) return;
|
|
442
|
+
console.warn(
|
|
443
|
+
`[@godxjp/ui] AlertDialog: confirmLabel "${String(resolvedConfirm)}" and the cancel label "${String(resolvedCancel)}" are the same word once tone marks are ignored \u2014 a reader tells them apart by colour alone and a screen reader not at all. Rename one (gh#1284).`
|
|
444
|
+
);
|
|
445
|
+
}, [resolvedConfirm, resolvedCancel]);
|
|
435
446
|
const busy = pending || verifying;
|
|
436
447
|
const confirmLabelText = verifying ? t("feedback.alert.verifying") : pending ? t("common.working") : resolvedConfirm;
|
|
437
448
|
const reset = () => {
|
|
@@ -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.4.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,
|
|
@@ -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…",
|
|
@@ -283,7 +284,8 @@
|
|
|
283
284
|
"alert": {
|
|
284
285
|
"dismiss": "Dismiss",
|
|
285
286
|
"verifying": "Verifying…",
|
|
286
|
-
"stepUpFailed": "Re-authentication failed. Please try again."
|
|
287
|
+
"stepUpFailed": "Re-authentication failed. Please try again.",
|
|
288
|
+
"cancel": "Cancel"
|
|
287
289
|
},
|
|
288
290
|
"toast": {
|
|
289
291
|
"close": "Close notification"
|
|
@@ -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": "選択…",
|
|
@@ -278,7 +279,8 @@
|
|
|
278
279
|
"alert": {
|
|
279
280
|
"dismiss": "閉じる",
|
|
280
281
|
"verifying": "確認中…",
|
|
281
|
-
"stepUpFailed": "再認証に失敗しました。もう一度お試しください。"
|
|
282
|
+
"stepUpFailed": "再認証に失敗しました。もう一度お試しください。",
|
|
283
|
+
"cancel": "キャンセル"
|
|
282
284
|
},
|
|
283
285
|
"toast": {
|
|
284
286
|
"close": "通知を閉じる"
|
|
@@ -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",
|
|
@@ -99,13 +99,14 @@
|
|
|
99
99
|
"download": "Tải xuống",
|
|
100
100
|
"retry": "Thử lại",
|
|
101
101
|
"start": "Tải lên",
|
|
102
|
-
"cancel": "
|
|
102
|
+
"cancel": "Hủy",
|
|
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…",
|
|
@@ -215,7 +216,7 @@
|
|
|
215
216
|
"empty": "Không có mục nào khớp",
|
|
216
217
|
"error": "Không tải được",
|
|
217
218
|
"confirm": "Xác nhận",
|
|
218
|
-
"cancel": "
|
|
219
|
+
"cancel": "Hủy",
|
|
219
220
|
"clear": "Bỏ chọn",
|
|
220
221
|
"more": "Tải thêm",
|
|
221
222
|
"openDialog": "Chọn từ danh sách",
|
|
@@ -279,7 +280,8 @@
|
|
|
279
280
|
"alert": {
|
|
280
281
|
"dismiss": "Đóng",
|
|
281
282
|
"verifying": "Đang xác minh…",
|
|
282
|
-
"stepUpFailed": "Xác thực lại thất bại. Vui lòng thử lại."
|
|
283
|
+
"stepUpFailed": "Xác thực lại thất bại. Vui lòng thử lại.",
|
|
284
|
+
"cancel": "Quay lại"
|
|
283
285
|
},
|
|
284
286
|
"toast": {
|
|
285
287
|
"close": "Đóng thông báo"
|
|
@@ -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
|
+
};
|
package/dist/styles/layers.json
CHANGED
package/dist/themes/godx.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ import type { AppPreset } from "../app/preset.js";
|
|
|
11
11
|
* A shadow root that embeds `@godxjp/ui/styles/core` adds `@godxjp/ui/themes/godx-tokens.css` (the
|
|
12
12
|
* same colours without the fonts) instead.
|
|
13
13
|
*
|
|
14
|
+
* NO `timeZone` (gh#1282): a preset must not pin a country's zone. 32.x shipped
|
|
15
|
+
* `"Asia/Ho_Chi_Minh"` here, so every godx app showed Vietnam time to its users in Japan; the zone
|
|
16
|
+
* now falls back to the viewer's browser, and an app that needs a fixed one passes `defaultTimezone`.
|
|
17
|
+
*
|
|
14
18
|
* Core never imports this module, which is what keeps the GoDX artwork out of a neutral bundle.
|
|
15
19
|
*/
|
|
16
20
|
export declare const godxPreset: AppPreset;
|
package/dist/themes/godx.js
CHANGED