@godxjp/ui 32.4.0 → 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.
@@ -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.4.0.** If the project you are editing has a different
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 (defaults to translated cancel).",
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` and `timeZone` fill only what the props leave unset, 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).",
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
  },
@@ -7664,7 +7664,7 @@
7664
7664
  "type": "string"
7665
7665
  },
7666
7666
  {
7667
- "description": "Dismiss action label (defaults to translated cancel).",
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` and `timeZone` fill only what the props leave unset, 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).",
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
  },
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.4.0/agent/index.json"
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.4.0"
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.0.
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.4.0`). It is searchable and version-locked. These files exist for agents
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.4.0/agent/...`. A catalog that does not match the installed
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.
@@ -43,6 +43,7 @@ import {
43
43
  APP_REQUEST_HEADER_TIME_FORMAT,
44
44
  APP_REQUEST_HEADER_TIMEZONE
45
45
  } from "./types.js";
46
+ const LEGACY_GODX_PRESET_TIMEZONE = "Asia/Ho_Chi_Minh";
46
47
  function resolveProviderLocale(defaultLocale, presetLocale) {
47
48
  const fromProp = resolveRegisteredLocale(defaultLocale);
48
49
  if (fromProp) return fromProp;
@@ -138,6 +139,7 @@ function AppProvider({
138
139
  [persistKey]
139
140
  );
140
141
  const formatChosenRef = React.useRef({ dateFormat: false, timeFormat: false });
142
+ const timezoneChosenRef = React.useRef(false);
141
143
  const commitPreferences = React.useCallback(() => {
142
144
  if (persistedAxes.size === 0) return;
143
145
  const out = pickPersistedAxes(prefsRef.current, persistedAxes);
@@ -149,6 +151,10 @@ function AppProvider({
149
151
  if (formatChosenRef.current.timeFormat) out.timeFormatChosen = true;
150
152
  else delete out.timeFormat;
151
153
  }
154
+ if (out.timezone !== void 0) {
155
+ if (timezoneChosenRef.current) out.timezoneChosen = true;
156
+ else delete out.timezone;
157
+ }
152
158
  writeStoredPreferences(storageKey, out);
153
159
  }, [persistedAxes, storageKey]);
154
160
  React.useEffect(() => {
@@ -166,6 +172,10 @@ function AppProvider({
166
172
  );
167
173
  if (derivable) delete stored.timeFormat;
168
174
  }
175
+ if (!raw.timezoneChosen && stored.timezone === LEGACY_GODX_PRESET_TIMEZONE) {
176
+ delete stored.timezone;
177
+ }
178
+ timezoneChosenRef.current = stored.timezone !== void 0;
169
179
  formatChosenRef.current = {
170
180
  dateFormat: stored.dateFormat !== void 0,
171
181
  timeFormat: stored.timeFormat !== void 0
@@ -273,6 +283,7 @@ function AppProvider({
273
283
  const setTimezone = React.useCallback(
274
284
  (next) => {
275
285
  prefsRef.current = { ...prefsRef.current, timezone: next };
286
+ timezoneChosenRef.current = true;
276
287
  setTimezoneState(next);
277
288
  onTimezoneChange?.(next);
278
289
  commitPreferences();
@@ -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}.
@@ -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) => {
@@ -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("common.cancel");
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.4.0",
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,
@@ -284,7 +284,8 @@
284
284
  "alert": {
285
285
  "dismiss": "Dismiss",
286
286
  "verifying": "Verifying…",
287
- "stepUpFailed": "Re-authentication failed. Please try again."
287
+ "stepUpFailed": "Re-authentication failed. Please try again.",
288
+ "cancel": "Cancel"
288
289
  },
289
290
  "toast": {
290
291
  "close": "Close notification"
@@ -279,7 +279,8 @@
279
279
  "alert": {
280
280
  "dismiss": "閉じる",
281
281
  "verifying": "確認中…",
282
- "stepUpFailed": "再認証に失敗しました。もう一度お試しください。"
282
+ "stepUpFailed": "再認証に失敗しました。もう一度お試しください。",
283
+ "cancel": "キャンセル"
283
284
  },
284
285
  "toast": {
285
286
  "close": "通知を閉じる"
@@ -99,7 +99,7 @@
99
99
  "download": "Tải xuống",
100
100
  "retry": "Thử lại",
101
101
  "start": "Tải lên",
102
- "cancel": "Huỷ",
102
+ "cancel": "Hủy",
103
103
  "rejectAccept": "{name}: loại tệp không được phép.",
104
104
  "rejectSize": "{name}: tệp quá lớn (tối đa {max}).",
105
105
  "rejectCount": "{name}: đã đủ số lượng tệp.",
@@ -216,7 +216,7 @@
216
216
  "empty": "Không có mục nào khớp",
217
217
  "error": "Không tải được",
218
218
  "confirm": "Xác nhận",
219
- "cancel": "Huỷ",
219
+ "cancel": "Hủy",
220
220
  "clear": "Bỏ chọn",
221
221
  "more": "Tải thêm",
222
222
  "openDialog": "Chọn từ danh sách",
@@ -280,7 +280,8 @@
280
280
  "alert": {
281
281
  "dismiss": "Đóng",
282
282
  "verifying": "Đang xác minh…",
283
- "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"
284
285
  },
285
286
  "toast": {
286
287
  "close": "Đóng thông báo"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "32.4.0",
2
+ "version": "32.4.1",
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",
@@ -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;
@@ -3,7 +3,6 @@ import { Logo } from "../components/general/logo.js";
3
3
  const godxPreset = Object.freeze({
4
4
  name: "godx",
5
5
  defaultLocale: "vi",
6
- timeZone: "Asia/Ho_Chi_Minh",
7
6
  brandMark: createElement(Logo, { mark: "godx", tone: "success" }),
8
7
  emailBrandLabel: "GoDX"
9
8
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "32.4.0",
4
- "godxUiMcp": "32.4.0",
3
+ "version": "32.4.1",
4
+ "godxUiMcp": "32.4.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",