@godxjp/ui 20.0.0 → 20.2.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 (273) hide show
  1. package/README.md +8 -2
  2. package/dist/app/app-provider.js +37 -21
  3. package/dist/app/index.d.ts +2 -0
  4. package/dist/app/index.js +2 -0
  5. package/dist/app/storage.d.ts +17 -0
  6. package/dist/app/storage.js +28 -0
  7. package/dist/app/theme-axes.d.ts +2 -0
  8. package/dist/app/theme-axes.js +45 -0
  9. package/dist/components/charts/chart-cartesian.d.ts +2 -1
  10. package/dist/components/charts/chart-cartesian.js +52 -3
  11. package/dist/components/charts/chart-category-axis.d.ts +55 -0
  12. package/dist/components/charts/chart-category-axis.js +93 -0
  13. package/dist/components/charts/chart-frame.d.ts +10 -1
  14. package/dist/components/charts/chart-frame.js +19 -3
  15. package/dist/components/charts/compact-bar-trend.d.ts +1 -1
  16. package/dist/components/charts/compact-bar-trend.js +2 -0
  17. package/dist/components/charts/pie-chart.d.ts +1 -1
  18. package/dist/components/charts/pie-chart.js +12 -1
  19. package/dist/components/charts/recharts-peer.d.ts +49 -0
  20. package/dist/components/charts/recharts-peer.js +45 -0
  21. package/dist/components/data-display/card.d.ts +12 -1
  22. package/dist/components/data-display/card.js +22 -4
  23. package/dist/components/data-display/code-block.js +6 -2
  24. package/dist/components/data-display/data-table.d.ts +39 -4
  25. package/dist/components/data-display/data-table.js +802 -270
  26. package/dist/components/data-display/descriptions.d.ts +27 -6
  27. package/dist/components/data-display/descriptions.js +58 -17
  28. package/dist/components/data-display/index.d.ts +2 -0
  29. package/dist/components/data-display/index.js +2 -0
  30. package/dist/components/data-display/list-row.d.ts +10 -1
  31. package/dist/components/data-display/list-row.js +7 -1
  32. package/dist/components/data-display/popover.d.ts +9 -3
  33. package/dist/components/data-display/popover.js +65 -11
  34. package/dist/components/data-display/range-timeline.d.ts +38 -0
  35. package/dist/components/data-display/range-timeline.js +161 -0
  36. package/dist/components/data-display/scroll-area.js +13 -2
  37. package/dist/components/data-display/service-launcher-card.js +14 -12
  38. package/dist/components/data-display/table.d.ts +71 -3
  39. package/dist/components/data-display/table.js +56 -18
  40. package/dist/components/data-display/tree-list.js +4 -9
  41. package/dist/components/data-entry/calendar.d.ts +2 -2
  42. package/dist/components/data-entry/calendar.js +23 -3
  43. package/dist/components/data-entry/cascader.d.ts +1 -1
  44. package/dist/components/data-entry/cascader.js +188 -24
  45. package/dist/components/data-entry/color-picker.d.ts +1 -1
  46. package/dist/components/data-entry/color-picker.js +17 -5
  47. package/dist/components/data-entry/control-appearance.d.ts +64 -0
  48. package/dist/components/data-entry/control-appearance.js +39 -0
  49. package/dist/components/data-entry/control-surface.d.ts +61 -0
  50. package/dist/components/data-entry/control-surface.js +39 -0
  51. package/dist/components/data-entry/date-picker.d.ts +1 -1
  52. package/dist/components/data-entry/date-picker.js +344 -113
  53. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  54. package/dist/components/data-entry/date-range-picker.js +278 -140
  55. package/dist/components/data-entry/form-field.d.ts +1 -1
  56. package/dist/components/data-entry/form-field.js +39 -4
  57. package/dist/components/data-entry/form.d.ts +4 -0
  58. package/dist/components/data-entry/form.js +4 -2
  59. package/dist/components/data-entry/index.d.ts +7 -3
  60. package/dist/components/data-entry/index.js +10 -1
  61. package/dist/components/data-entry/input-otp.d.ts +1 -1
  62. package/dist/components/data-entry/input.d.ts +12 -34
  63. package/dist/components/data-entry/input.js +92 -24
  64. package/dist/components/data-entry/month-picker.d.ts +1 -1
  65. package/dist/components/data-entry/month-picker.js +47 -10
  66. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  67. package/dist/components/data-entry/month-range-picker.js +51 -11
  68. package/dist/components/data-entry/number-input.d.ts +7 -0
  69. package/dist/components/data-entry/number-input.js +147 -98
  70. package/dist/components/data-entry/password-input.d.ts +1 -1
  71. package/dist/components/data-entry/radio.d.ts +1 -1
  72. package/dist/components/data-entry/radio.js +61 -13
  73. package/dist/components/data-entry/search-input.d.ts +1 -1
  74. package/dist/components/data-entry/search-input.js +16 -2
  75. package/dist/components/data-entry/search-select.d.ts +2 -2
  76. package/dist/components/data-entry/search-select.js +209 -78
  77. package/dist/components/data-entry/select.d.ts +22 -4
  78. package/dist/components/data-entry/select.js +215 -163
  79. package/dist/components/data-entry/slider.d.ts +9 -1
  80. package/dist/components/data-entry/slider.js +87 -9
  81. package/dist/components/data-entry/switch.d.ts +3 -0
  82. package/dist/components/data-entry/switch.js +29 -3
  83. package/dist/components/data-entry/textarea.d.ts +14 -56
  84. package/dist/components/data-entry/textarea.js +80 -33
  85. package/dist/components/data-entry/time-picker.d.ts +2 -2
  86. package/dist/components/data-entry/time-picker.js +333 -109
  87. package/dist/components/data-entry/time-range-picker.d.ts +5 -0
  88. package/dist/components/data-entry/time-range-picker.js +89 -0
  89. package/dist/components/data-entry/transfer.d.ts +1 -1
  90. package/dist/components/data-entry/transfer.js +86 -28
  91. package/dist/components/data-entry/tree-select.d.ts +1 -1
  92. package/dist/components/data-entry/tree-select.js +201 -113
  93. package/dist/components/data-entry/tree-utils.js +7 -14
  94. package/dist/components/data-entry/upload-files.d.ts +2 -0
  95. package/dist/components/data-entry/upload-files.js +31 -0
  96. package/dist/components/data-entry/upload-request.d.ts +4 -0
  97. package/dist/components/data-entry/upload-request.js +53 -0
  98. package/dist/components/data-entry/upload-types.d.ts +28 -0
  99. package/dist/components/data-entry/upload-types.js +2 -0
  100. package/dist/components/data-entry/upload.d.ts +2 -2
  101. package/dist/components/data-entry/upload.js +475 -115
  102. package/dist/components/feedback/dialog.js +19 -19
  103. package/dist/components/feedback/sheet.js +3 -0
  104. package/dist/components/feedback/tooltip.js +3 -0
  105. package/dist/components/general/button.d.ts +2 -1
  106. package/dist/components/general/button.js +7 -1
  107. package/dist/components/general/index.d.ts +1 -0
  108. package/dist/components/general/index.js +2 -0
  109. package/dist/components/general/typography.d.ts +3 -0
  110. package/dist/components/general/typography.js +15 -1
  111. package/dist/components/general/visually-hidden.d.ts +3 -0
  112. package/dist/components/general/visually-hidden.js +9 -0
  113. package/dist/components/layout/app-launcher.d.ts +34 -0
  114. package/dist/components/layout/app-launcher.js +261 -0
  115. package/dist/components/layout/app-shell.d.ts +3 -1
  116. package/dist/components/layout/app-shell.js +34 -14
  117. package/dist/components/layout/auth-shell.d.ts +1 -1
  118. package/dist/components/layout/auth-shell.js +2 -0
  119. package/dist/components/layout/breadcrumb.d.ts +14 -2
  120. package/dist/components/layout/breadcrumb.js +46 -4
  121. package/dist/components/layout/flex.d.ts +1 -1
  122. package/dist/components/layout/flex.js +23 -2
  123. package/dist/components/layout/index.d.ts +3 -0
  124. package/dist/components/layout/index.js +5 -1
  125. package/dist/components/layout/org-switcher.js +20 -1
  126. package/dist/components/layout/page-container.d.ts +1 -1
  127. package/dist/components/layout/page-container.js +5 -3
  128. package/dist/components/layout/responsive-grid.d.ts +16 -2
  129. package/dist/components/layout/responsive-grid.js +29 -2
  130. package/dist/components/layout/sidebar.js +2 -1
  131. package/dist/components/layout/topbar-item.d.ts +3 -0
  132. package/dist/components/layout/topbar-item.js +18 -3
  133. package/dist/components/layout/topbar.d.ts +1 -1
  134. package/dist/components/layout/topbar.js +27 -6
  135. package/dist/components/navigation/dropdown-menu.d.ts +30 -2
  136. package/dist/components/navigation/dropdown-menu.js +73 -11
  137. package/dist/components/navigation/pagination.d.ts +2 -2
  138. package/dist/components/navigation/pagination.js +155 -83
  139. package/dist/components/navigation/steps.d.ts +2 -2
  140. package/dist/components/navigation/steps.js +29 -4
  141. package/dist/components/navigation/tabs.d.ts +5 -33
  142. package/dist/components/navigation/tabs.js +232 -64
  143. package/dist/components/ui/hover-card.js +3 -0
  144. package/dist/components/ui/input-otp.d.ts +26 -28
  145. package/dist/components/ui/input-otp.js +50 -7
  146. package/dist/components/ui/password-input.d.ts +36 -2
  147. package/dist/components/ui/password-input.js +27 -7
  148. package/dist/components/ui/rating.d.ts +25 -0
  149. package/dist/components/ui/rating.js +67 -27
  150. package/dist/components/ui/segmented.d.ts +9 -0
  151. package/dist/components/ui/segmented.js +17 -2
  152. package/dist/components/ui/separator.d.ts +4 -0
  153. package/dist/components/ui/separator.js +13 -0
  154. package/dist/components/ui/tag-input.d.ts +45 -0
  155. package/dist/components/ui/tag-input.js +109 -27
  156. package/dist/form/form-context.d.ts +29 -0
  157. package/dist/form/form-context.js +44 -4
  158. package/dist/form/form-field-array.d.ts +22 -0
  159. package/dist/form/form-field-array.js +50 -0
  160. package/dist/form/form-field-control.d.ts +2 -1
  161. package/dist/form/form-field-control.js +116 -40
  162. package/dist/form/form-root.d.ts +2 -1
  163. package/dist/form/form-root.js +142 -23
  164. package/dist/form/index.d.ts +3 -1
  165. package/dist/form/index.js +12 -1
  166. package/dist/i18n/index.d.ts +1 -1
  167. package/dist/i18n/index.js +3 -1
  168. package/dist/i18n/messages/en.json +59 -6
  169. package/dist/i18n/messages/ja.json +59 -6
  170. package/dist/i18n/messages/vi.json +59 -6
  171. package/dist/i18n/translate.d.ts +19 -0
  172. package/dist/i18n/translate.js +24 -0
  173. package/dist/inertia/index.d.ts +20 -0
  174. package/dist/inertia/index.js +60 -1
  175. package/dist/lib/control-styles.d.ts +25 -3
  176. package/dist/lib/control-styles.js +9 -3
  177. package/dist/lib/datetime/picker-format.d.ts +8 -0
  178. package/dist/lib/datetime/picker-format.js +54 -0
  179. package/dist/lib/overlay-portal.d.ts +18 -0
  180. package/dist/lib/overlay-portal.js +17 -0
  181. package/dist/props/components/app.prop.d.ts +13 -2
  182. package/dist/props/components/charts.prop.d.ts +21 -0
  183. package/dist/props/components/data-display.prop.d.ts +55 -4
  184. package/dist/props/components/data-entry.prop.d.ts +687 -43
  185. package/dist/props/components/form.prop.d.ts +142 -4
  186. package/dist/props/components/general.prop.d.ts +30 -1
  187. package/dist/props/components/index.d.ts +2 -2
  188. package/dist/props/components/layout.prop.d.ts +255 -8
  189. package/dist/props/components/navigation.prop.d.ts +113 -3
  190. package/dist/props/registry.d.ts +376 -10
  191. package/dist/props/registry.js +493 -9
  192. package/dist/props/vocabulary/data.prop.d.ts +179 -1
  193. package/dist/props/vocabulary/index.d.ts +4 -4
  194. package/dist/props/vocabulary/interaction.prop.d.ts +51 -2
  195. package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
  196. package/dist/props/vocabulary/shared.prop.d.ts +33 -0
  197. package/dist/styles/badge-layout.css +2 -1
  198. package/dist/styles/card-layout.css +36 -5
  199. package/dist/styles/chart-layout.css +17 -1
  200. package/dist/styles/control.css +520 -14
  201. package/dist/styles/data-display-layout.css +248 -7
  202. package/dist/styles/data-entry-layout.css +13 -0
  203. package/dist/styles/layout.css +221 -4
  204. package/dist/styles/navigation-layout.css +230 -1
  205. package/dist/styles/shell-layout.css +690 -21
  206. package/dist/styles/table-layout.css +95 -6
  207. package/dist/styles/text-layout.css +5 -0
  208. package/dist/tokens/base.css +1 -0
  209. package/dist/tokens/components/badge.css +1 -0
  210. package/dist/tokens/components/chart.css +6 -0
  211. package/dist/tokens/components/control.css +81 -3
  212. package/dist/tokens/components/data-display.css +9 -0
  213. package/dist/tokens/components/descriptions.css +11 -0
  214. package/dist/tokens/components/flex.css +8 -0
  215. package/dist/tokens/components/navigation.css +55 -0
  216. package/dist/tokens/components/shell.css +100 -5
  217. package/dist/tokens/components/table.css +14 -0
  218. package/dist/tokens/foundation.css +2 -0
  219. package/dist/tokens/semantic/layout.css +9 -0
  220. package/docs/CONSUMER-RULES.md +13 -0
  221. package/docs/DESIGN-AUTHORITY.md +43 -6
  222. package/docs/DEVELOPMENT.md +4 -3
  223. package/docs/FORMS.md +151 -6
  224. package/docs/FRAME-COVERAGE-REPORT.md +29 -16
  225. package/docs/README.md +1 -1
  226. package/docs/STANDARDS-vocabulary-tokens.md +1 -1
  227. package/docs/TESTING.md +15 -6
  228. package/docs/charts/cjk-category-axis.tsx +81 -0
  229. package/docs/data-display/card/index.tsx +22 -0
  230. package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
  231. package/docs/data-display/data-table/index.tsx +23 -0
  232. package/docs/data-display/descriptions.tsx +41 -0
  233. package/docs/data-display/list-row.tsx +6 -6
  234. package/docs/data-display/scroll-area.tsx +31 -25
  235. package/docs/data-display/table.tsx +104 -45
  236. package/docs/data-display/timeline.tsx +41 -0
  237. package/docs/data-entry/date-picker.tsx +85 -0
  238. package/docs/data-entry/date-range-picker.tsx +26 -0
  239. package/docs/data-entry/form-dynamic-fields.tsx +199 -0
  240. package/docs/data-entry/form.tsx +74 -4
  241. package/docs/data-entry/input-otp.tsx +24 -0
  242. package/docs/data-entry/input.tsx +46 -1
  243. package/docs/data-entry/month-range-picker.tsx +1 -1
  244. package/docs/data-entry/select.tsx +27 -0
  245. package/docs/data-entry/switch.tsx +41 -0
  246. package/docs/data-entry/textarea.tsx +38 -0
  247. package/docs/data-entry/time-picker.tsx +85 -1
  248. package/docs/data-entry/time-range-picker.tsx +55 -0
  249. package/docs/data-entry/transfer.tsx +17 -0
  250. package/docs/data-entry/upload.tsx +56 -12
  251. package/docs/feedback/tooltip.tsx +1 -1
  252. package/docs/general/activity.tsx +2 -2
  253. package/docs/general/button/index.tsx +14 -1
  254. package/docs/general/typography.tsx +14 -1
  255. package/docs/layout/app-launcher.tsx +195 -0
  256. package/docs/layout/app-shell-arrangements.tsx +5 -5
  257. package/docs/layout/app-shell.tsx +11 -0
  258. package/docs/layout/flex.tsx +50 -0
  259. package/docs/layout/responsive-grid.tsx +21 -1
  260. package/docs/layout/topbar.tsx +5 -9
  261. package/docs/navigation/app-setting-picker.tsx +11 -0
  262. package/docs/navigation/breadcrumb.tsx +48 -0
  263. package/docs/navigation/dropdown-menu.tsx +21 -0
  264. package/docs/navigation/pagination.tsx +52 -0
  265. package/docs/navigation/steps.tsx +37 -0
  266. package/docs/navigation/tabs.tsx +97 -1
  267. package/docs/query/button-refetch.tsx +1 -0
  268. package/docs/showcase/settings-security-mfa.tsx +3 -3
  269. package/package.json +24 -4
  270. package/scripts/_agent-setup.mjs +76 -9
  271. package/scripts/init-guinea-pig.mjs +26 -4
  272. package/scripts/ui-audit.mjs +243 -30
  273. package/scripts/visual-audit.mjs +45 -7
package/README.md CHANGED
@@ -133,8 +133,15 @@ One token `--phi-unit` drives page/section/card spacing; micro control gaps use
133
133
 
134
134
  ```bash
135
135
  pnpm preview # preview app → http://localhost:6008 (fixed port, kills stale)
136
+ pnpm typecheck && pnpm lint && pnpm run audit # the cheap gates — run these freely
137
+ pnpm vitest run src/components/<group>/__tests__ --maxWorkers=2 # ONLY what you touched
138
+
139
+ # Heavy — CI's job. Run locally AT MOST ONCE, right before opening a PR, and never
140
+ # with other agents working on the same machine: `verify` builds the library AND the
141
+ # preview site AND runs all 506 test files. Measured with agents in parallel: 70 vitest
142
+ # workers, load average 90, an overheating laptop.
136
143
  pnpm preview:build # static build — also what deploys to GitHub Pages
137
- pnpm verify # typecheck · lint · format · the 5 guards · test
144
+ pnpm verify # typecheck · lint · format · the 5 guards · FULL suite
138
145
  pnpm release --ui <patch|minor|major> --mcp <…|skip> # publish lib + MCP in lockstep
139
146
  ```
140
147
 
@@ -174,5 +181,4 @@ This repo ships two packages — `@godxjp/ui` (this dir) and `@godxjp/ui-mcp` (`
174
181
  - [docs/COMPONENTS.md](./docs/COMPONENTS.md) · [docs/TOKENS.md](./docs/TOKENS.md) · [docs/SPACING.md](./docs/SPACING.md)
175
182
  - [docs/PROPS-VOCABULARY.md](./docs/PROPS-VOCABULARY.md) · [docs/PROPS-REGISTRY.md](./docs/PROPS-REGISTRY.md)
176
183
  - [docs/DATETIME.md](./docs/DATETIME.md) · [docs/FORMS.md](./docs/FORMS.md) · [docs/TESTING.md](./docs/TESTING.md)
177
- - **Architecture decisions** under `debate/*/04-Decision.md` (ADRs from the design debates)
178
184
  - MCP: **godxjp-ui-mcp** (`.mcp.json`) — live catalog for agents
@@ -14,7 +14,13 @@ import {
14
14
  enableLiveRelativeFormatting,
15
15
  syncDatetimeContext
16
16
  } from "../lib/datetime/index.js";
17
- import { DEFAULT_STORAGE_KEY, readStoredPreferences, writeStoredPreferences } from "./storage.js";
17
+ import {
18
+ DEFAULT_STORAGE_KEY,
19
+ pickPersistedAxes,
20
+ readStoredPreferences,
21
+ resolvePersistedAxes,
22
+ writeStoredPreferences
23
+ } from "./storage.js";
18
24
  import {
19
25
  applyThemeAxes,
20
26
  PREFERS_DARK_SCHEME_QUERY
@@ -105,8 +111,18 @@ function AppProvider({
105
111
  fontSize,
106
112
  scaling
107
113
  });
114
+ const persistKey = Array.isArray(persist) ? [...persist].sort().join(",") : String(persist);
115
+ const persistedAxes = React.useMemo(
116
+ () => resolvePersistedAxes(persist),
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ [persistKey]
119
+ );
120
+ const commitPreferences = React.useCallback(() => {
121
+ if (persistedAxes.size === 0) return;
122
+ writeStoredPreferences(storageKey, pickPersistedAxes(prefsRef.current, persistedAxes));
123
+ }, [persistedAxes, storageKey]);
108
124
  React.useEffect(() => {
109
- const stored = persist ? readStoredPreferences(storageKey) : {};
125
+ const stored = pickPersistedAxes(readStoredPreferences(storageKey), persistedAxes);
110
126
  const nextLocale = stored.locale ?? defaultLocale;
111
127
  const nextTimezone = stored.timezone ?? resolveDefaultTimezone(defaultTimezone, systemTimezone);
112
128
  const nextTimeFormat = resolveInitialTimeFormat(
@@ -154,7 +170,7 @@ function AppProvider({
154
170
  initialDensity,
155
171
  initialFontSize,
156
172
  initialScaling,
157
- persist,
173
+ persistedAxes,
158
174
  storageKey,
159
175
  systemTimezone
160
176
  ]);
@@ -188,81 +204,81 @@ function AppProvider({
188
204
  prefsRef.current = { ...prefsRef.current, locale: next };
189
205
  setLocaleState(next);
190
206
  onLocaleChange?.(next);
191
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
207
+ commitPreferences();
192
208
  },
193
- [onLocaleChange, persist, storageKey]
209
+ [onLocaleChange, commitPreferences]
194
210
  );
195
211
  const setTimezone = React.useCallback(
196
212
  (next) => {
197
213
  prefsRef.current = { ...prefsRef.current, timezone: next };
198
214
  setTimezoneState(next);
199
215
  onTimezoneChange?.(next);
200
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
216
+ commitPreferences();
201
217
  },
202
- [onTimezoneChange, persist, storageKey]
218
+ [onTimezoneChange, commitPreferences]
203
219
  );
204
220
  const setTimeFormat = React.useCallback(
205
221
  (next) => {
206
222
  prefsRef.current = { ...prefsRef.current, timeFormat: next };
207
223
  setTimeFormatState(next);
208
224
  onTimeFormatChange?.(next);
209
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
225
+ commitPreferences();
210
226
  },
211
- [onTimeFormatChange, persist, storageKey]
227
+ [onTimeFormatChange, commitPreferences]
212
228
  );
213
229
  const setDateFormat = React.useCallback(
214
230
  (next) => {
215
231
  prefsRef.current = { ...prefsRef.current, dateFormat: next };
216
232
  setDateFormatState(next);
217
233
  onDateFormatChange?.(next);
218
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
234
+ commitPreferences();
219
235
  },
220
- [onDateFormatChange, persist, storageKey]
236
+ [onDateFormatChange, commitPreferences]
221
237
  );
222
238
  const setTheme = React.useCallback(
223
239
  (next) => {
224
240
  prefsRef.current = { ...prefsRef.current, theme: next };
225
241
  setThemeState(next);
226
242
  onThemeChange?.(next);
227
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
243
+ commitPreferences();
228
244
  },
229
- [onThemeChange, persist, storageKey]
245
+ [onThemeChange, commitPreferences]
230
246
  );
231
247
  const setBrand = React.useCallback(
232
248
  (next) => {
233
249
  prefsRef.current = { ...prefsRef.current, brand: next };
234
250
  setBrandState(next);
235
251
  onBrandChange?.(next);
236
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
252
+ commitPreferences();
237
253
  },
238
- [onBrandChange, persist, storageKey]
254
+ [onBrandChange, commitPreferences]
239
255
  );
240
256
  const setDensity = React.useCallback(
241
257
  (next) => {
242
258
  prefsRef.current = { ...prefsRef.current, density: next };
243
259
  setDensityState(next);
244
260
  onDensityChange?.(next);
245
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
261
+ commitPreferences();
246
262
  },
247
- [onDensityChange, persist, storageKey]
263
+ [onDensityChange, commitPreferences]
248
264
  );
249
265
  const setFontSize = React.useCallback(
250
266
  (next) => {
251
267
  prefsRef.current = { ...prefsRef.current, fontSize: next };
252
268
  setFontSizeState(next);
253
269
  onFontSizeChange?.(next);
254
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
270
+ commitPreferences();
255
271
  },
256
- [onFontSizeChange, persist, storageKey]
272
+ [onFontSizeChange, commitPreferences]
257
273
  );
258
274
  const setScaling = React.useCallback(
259
275
  (next) => {
260
276
  prefsRef.current = { ...prefsRef.current, scaling: next };
261
277
  setScalingState(next);
262
278
  onScalingChange?.(next);
263
- if (persist) writeStoredPreferences(storageKey, prefsRef.current);
279
+ commitPreferences();
264
280
  },
265
- [onScalingChange, persist, storageKey]
281
+ [onScalingChange, commitPreferences]
266
282
  );
267
283
  const requestHeaders = React.useMemo(
268
284
  () => buildRequestHeaders(locale, timezone, timeFormat, dateFormat),
@@ -12,3 +12,5 @@ export { AppProvider, useAppContext, useOptionalAppContext, useAppLocale, useApp
12
12
  export { useFormatting, useDateTime } from "./use-formatting.js";
13
13
  export { syncDatetimeContext, getDatetimeContext, formatDate, isFormatDateValue, detectFormatDateKind, formatAppDate, formatAppDateTime, formatAppTime, formatAppDateLong, formatAppRelative, formatCalendarDate, formatTimeOfDay, parseDateInput, normalizeHhmm, isValidHhmm, type FormatDateOptions, type FormatDateKind, type FormatDatetimeOptions, } from "../lib/datetime/index.js";
14
14
  export { useTranslation, usePickerLocales } from "../i18n/use-translation.js";
15
+ export { OverlayPortalProvider } from "../lib/overlay-portal.js";
16
+ export type { OverlayPortalProviderProps } from "../lib/overlay-portal.js";
package/dist/app/index.js CHANGED
@@ -36,8 +36,10 @@ import {
36
36
  isValidHhmm
37
37
  } from "../lib/datetime/index.js";
38
38
  import { useTranslation, usePickerLocales } from "../i18n/use-translation.js";
39
+ import { OverlayPortalProvider } from "../lib/overlay-portal.js";
39
40
  export {
40
41
  AppProvider,
42
+ OverlayPortalProvider,
41
43
  detectFormatDateKind,
42
44
  formatAppDate,
43
45
  formatAppDateLong,
@@ -14,6 +14,23 @@ export type StoredAppPreferences = {
14
14
  /** Continuous global size multiplier; `null` defers to the density preset. */
15
15
  scaling?: number | null;
16
16
  };
17
+ /**
18
+ * The axes a viewer's preferences are made of — the keys of {@link StoredAppPreferences}.
19
+ *
20
+ * They do NOT share an owner. `theme`, `density`, `fontSize`, `scaling` and `brand` are the
21
+ * viewer's, and belong in this browser. `locale`, `timezone`, `timeFormat` and `dateFormat` are
22
+ * frequently the SERVER's — resolved per request from a cookie, an account row or an Accept-Language
23
+ * header — and a copy in local storage then wins over the value the server just sent, because
24
+ * storage is read after the props. That is why `persist` takes a LIST as well as a boolean: one
25
+ * flag for axes with two different owners forces an all-or-nothing choice, and the consumer that
26
+ * hits it turns persistence off entirely and loses the viewer's theme with it.
27
+ */
28
+ export declare const APP_PREFERENCE_AXES: readonly ["locale", "timezone", "timeFormat", "dateFormat", "theme", "brand", "density", "fontSize", "scaling"];
29
+ export type AppPreferenceAxis = (typeof APP_PREFERENCE_AXES)[number];
30
+ /** `true` → every axis, `false` → none, a list → exactly those. */
31
+ export declare function resolvePersistedAxes(persist: boolean | readonly AppPreferenceAxis[]): ReadonlySet<AppPreferenceAxis>;
32
+ /** The subset of `preferences` on the given axes; the rest are dropped, not set to undefined. */
33
+ export declare function pickPersistedAxes(preferences: StoredAppPreferences, axes: ReadonlySet<AppPreferenceAxis>): StoredAppPreferences;
17
34
  export declare function readStoredPreferences(storageKey: string): StoredAppPreferences;
18
35
  export declare function writeStoredPreferences(storageKey: string, preferences: StoredAppPreferences): void;
19
36
  export { DEFAULT_STORAGE_KEY };
@@ -8,6 +8,31 @@ import {
8
8
  isAppTheme
9
9
  } from "./theme-axes.js";
10
10
  const DEFAULT_STORAGE_KEY = "godxjp.app";
11
+ const APP_PREFERENCE_AXES = [
12
+ "locale",
13
+ "timezone",
14
+ "timeFormat",
15
+ "dateFormat",
16
+ "theme",
17
+ "brand",
18
+ "density",
19
+ "fontSize",
20
+ "scaling"
21
+ ];
22
+ function resolvePersistedAxes(persist) {
23
+ if (persist === true) return new Set(APP_PREFERENCE_AXES);
24
+ if (persist === false) return /* @__PURE__ */ new Set();
25
+ return new Set(persist);
26
+ }
27
+ function pickPersistedAxes(preferences, axes) {
28
+ const out = {};
29
+ for (const axis of APP_PREFERENCE_AXES) {
30
+ if (axes.has(axis) && preferences[axis] !== void 0) {
31
+ out[axis] = preferences[axis];
32
+ }
33
+ }
34
+ return out;
35
+ }
11
36
  function readStoredPreferences(storageKey) {
12
37
  if (typeof window === "undefined") return {};
13
38
  try {
@@ -37,7 +62,10 @@ function writeStoredPreferences(storageKey, preferences) {
37
62
  }
38
63
  }
39
64
  export {
65
+ APP_PREFERENCE_AXES,
40
66
  DEFAULT_STORAGE_KEY,
67
+ pickPersistedAxes,
41
68
  readStoredPreferences,
69
+ resolvePersistedAxes,
42
70
  writeStoredPreferences
43
71
  };
@@ -43,3 +43,5 @@ export declare const isAppFontSize: (v: unknown) => v is AppFontSize;
43
43
  * `data-brand` so the app's own `--primary` token applies.
44
44
  */
45
45
  export declare function applyThemeAxes(el: HTMLElement, axes: Partial<AppThemeAxes>): void;
46
+ /** Apply an entity's action palette, including portals. Call the returned cleanup on scope exit. */
47
+ export declare function applyPrimaryColor(root: HTMLElement, color: string, foreground?: string | null): () => void;
@@ -39,12 +39,57 @@ function applyThemeAxes(el, axes) {
39
39
  else el.style.setProperty("--scaling", String(axes.scaling));
40
40
  }
41
41
  }
42
+ function applyPrimaryColor(root, color, foreground) {
43
+ const parse = (value) => {
44
+ if (!/^#[\da-f]{6}$/i.test(value)) return null;
45
+ return [1, 3, 5].map((offset) => parseInt(value.slice(offset, offset + 2), 16) / 255);
46
+ };
47
+ const rgb = parse(color);
48
+ if (!rgb) return () => {
49
+ };
50
+ const luminance = (channels) => channels.reduce(
51
+ (sum, value, index) => sum + (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4) * [0.2126, 0.7152, 0.0722][index],
52
+ 0
53
+ );
54
+ const text = foreground && parse(foreground) || (luminance(rgb) > 0.179 ? [0, 0, 0] : [1, 1, 1]);
55
+ const hsl = (channels) => {
56
+ const [r, g, b] = channels;
57
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
58
+ const delta = max - min, lightness = (max + min) / 2;
59
+ const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
60
+ const hue = delta === 0 ? 0 : 60 * (max === r ? ((g - b) / delta + 6) % 6 : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4);
61
+ return `${hue} ${saturation * 100}% ${lightness * 100}%`;
62
+ };
63
+ const away = luminance(text) > 0.5 ? 0 : 1;
64
+ const shade = (amount) => hsl(rgb.map((channel) => channel * (1 - amount) + away * amount));
65
+ const palette = {
66
+ "--primary": hsl(rgb),
67
+ "--primary-foreground": hsl(text),
68
+ "--primary-hover": shade(0.12),
69
+ "--primary-active": shade(0.24),
70
+ "--primary-border": hsl(rgb),
71
+ "--ring": hsl(rgb)
72
+ };
73
+ const previous = Object.keys(palette).map((key) => [
74
+ key,
75
+ root.style.getPropertyValue(key),
76
+ root.style.getPropertyPriority(key)
77
+ ]);
78
+ for (const [key, value] of Object.entries(palette)) root.style.setProperty(key, value);
79
+ return () => {
80
+ for (const [key, value, priority] of previous) {
81
+ if (value) root.style.setProperty(key, value, priority);
82
+ else root.style.removeProperty(key);
83
+ }
84
+ };
85
+ }
42
86
  export {
43
87
  APP_BRANDS,
44
88
  APP_DENSITIES,
45
89
  APP_FONT_SIZES,
46
90
  APP_THEMES,
47
91
  PREFERS_DARK_SCHEME_QUERY,
92
+ applyPrimaryColor,
48
93
  applyThemeAxes,
49
94
  isAppBrand,
50
95
  isAppDensity,
@@ -11,6 +11,7 @@ type CartesianChartProps = {
11
11
  series: ChartSeriesProp[];
12
12
  categoryKey: string;
13
13
  label: React.ReactNode;
14
+ showCaption?: boolean;
14
15
  description?: React.ReactNode;
15
16
  size?: "xs" | "sm" | "md" | "lg";
16
17
  height?: number;
@@ -27,5 +28,5 @@ type CartesianChartProps = {
27
28
  /** bar */
28
29
  horizontal?: boolean;
29
30
  };
30
- export declare function CartesianChart({ kind, data, series, categoryKey, label, description, size, height, showLegend, showGrid, numberFormat, emptyMessage, className, id, curved, stacked, horizontal, }: CartesianChartProps): React.JSX.Element;
31
+ export declare function CartesianChart({ kind, data, series, categoryKey, label, showCaption, description, size, height, showLegend, showGrid, numberFormat, emptyMessage, className, id, curved, stacked, horizontal, }: CartesianChartProps): React.JSX.Element;
31
32
  export {};
@@ -13,18 +13,22 @@ import {
13
13
  ResponsiveContainer,
14
14
  Tooltip,
15
15
  XAxis,
16
- YAxis
17
- } from "recharts";
16
+ YAxis,
17
+ assertRechartsPeer
18
+ } from "./recharts-peer.js";
19
+ import { cn } from "../../lib/utils.js";
18
20
  import { EmptyState } from "../data-display/empty-state.js";
19
21
  import { useTranslation } from "../../i18n/use-translation.js";
20
22
  import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
21
23
  import { buildCartesianSummary } from "./chart-summary.js";
24
+ import { useCategoryAxisMetrics } from "./chart-category-axis.js";
22
25
  function CartesianChart({
23
26
  kind,
24
27
  data,
25
28
  series,
26
29
  categoryKey,
27
30
  label,
31
+ showCaption = true,
28
32
  description,
29
33
  size = "md",
30
34
  height,
@@ -38,6 +42,7 @@ function CartesianChart({
38
42
  stacked = false,
39
43
  horizontal = false
40
44
  }) {
45
+ assertRechartsPeer();
41
46
  const { t, locale } = useTranslation();
42
47
  const fmt = useChartNumberFormat(numberFormat);
43
48
  const list = React.useMemo(
@@ -56,6 +61,41 @@ function CartesianChart({
56
61
  const resolvedHeight = chartHeight(size, height);
57
62
  const tickFormatter = (value) => fmt.format(value);
58
63
  const curve = curved ? "monotone" : "linear";
64
+ const measuresCategoryAxis = kind === "bar" && horizontal;
65
+ const canvasRef = React.useRef(null);
66
+ const probeRef = React.useRef(null);
67
+ const categoryLabels = React.useMemo(
68
+ () => measuresCategoryAxis ? data.map((datum) => String(datum[categoryKey] ?? "")) : [],
69
+ [measuresCategoryAxis, data, categoryKey]
70
+ );
71
+ const categoryAxis = useCategoryAxisMetrics(
72
+ canvasRef,
73
+ probeRef,
74
+ categoryLabels,
75
+ measuresCategoryAxis && hasData
76
+ );
77
+ const categoryTick = (tickProps) => {
78
+ const full = String(tickProps.payload?.value ?? "");
79
+ const shown = categoryAxis.display(full);
80
+ return (
81
+ // `recharts-text` is what chart-layout.css styles the tick typography through; the rest of
82
+ // the class list is recharts' own (it hands `recharts-cartesian-axis-tick-value` in).
83
+ /* @__PURE__ */ jsxs(
84
+ "text",
85
+ {
86
+ className: cn("recharts-text", tickProps.className),
87
+ x: tickProps.x,
88
+ y: tickProps.y,
89
+ textAnchor: tickProps.textAnchor,
90
+ dominantBaseline: "central",
91
+ children: [
92
+ shown === full ? null : /* @__PURE__ */ jsx("title", { children: full }),
93
+ shown
94
+ ]
95
+ }
96
+ )
97
+ );
98
+ };
59
99
  const axes = (numberAxisVertical) => /* @__PURE__ */ jsxs(Fragment, { children: [
60
100
  showGrid ? /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }) : null,
61
101
  numberAxisVertical ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -95,7 +135,9 @@ function CartesianChart({
95
135
  dataKey: categoryKey,
96
136
  tickLine: false,
97
137
  stroke: "hsl(var(--muted-foreground))",
98
- fontSize: 12
138
+ fontSize: 12,
139
+ width: categoryAxis.width,
140
+ tick: categoryTick
99
141
  }
100
142
  )
101
143
  ] }),
@@ -158,6 +200,7 @@ function CartesianChart({
158
200
  ChartFrame,
159
201
  {
160
202
  label,
203
+ showCaption,
161
204
  description,
162
205
  summaryRows: summary.rows,
163
206
  imgSummary: summary.img,
@@ -165,6 +208,12 @@ function CartesianChart({
165
208
  height: resolvedHeight,
166
209
  className,
167
210
  id,
211
+ canvasRef,
212
+ canvasExtra: measuresCategoryAxis ? (
213
+ // Out-of-flow, hidden, and inside `.ui-chart` — so it inherits exactly the typography
214
+ // the axis ticks are painted in, including the `[lang="ja"]` face.
215
+ /* @__PURE__ */ jsx("span", { ref: probeRef, className: "ui-chart-axis-probe", "aria-hidden": "true" })
216
+ ) : null,
168
217
  children: hasData ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, children: chart() }) : /* @__PURE__ */ jsx(EmptyState, { title: emptyMessage ?? t("chart.empty") })
169
218
  }
170
219
  );
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Category-axis sizing for the HORIZONTAL bar chart (gh#409 · 1).
4
+ *
5
+ * recharts reserves a fixed 60px for the category axis whatever the ticks say, so a Japanese
6
+ * company name — 8–10 full-width glyphs, 100–125px — was painted OUTSIDE the chart box and the
7
+ * SVG clip cut it from the START, removing exactly the identifying `株式会社` prefix. Latin text
8
+ * degrades gracefully there; 全角 does not, and `docs/DESIGN-AUTHORITY.md` gives anything the user
9
+ * READS to the Japanese standard.
10
+ *
11
+ * The mechanism: measure every tick in the real rendered typography, size the axis from the
12
+ * WIDEST one, cap that at a token-owned fraction of the canvas so one long name can never eat
13
+ * the bars, and truncate anything past the cap at its END — the start, which identifies the row,
14
+ * always survives, and the full string stays reachable through the tick's `<title>` and the
15
+ * figure's screen-reader list.
16
+ *
17
+ * The two knobs are component tokens (`src/tokens/components/chart.css`). They are read back in
18
+ * px from real CSS properties on the probe element, because a custom property's computed value
19
+ * keeps whatever unit its author wrote.
20
+ */
21
+ /** Axis knobs resolved from the probe's computed style. `gap` is px, `maxFraction` unitless. */
22
+ export type CategoryAxisKnobs = {
23
+ /** Space between the tick text and the plot area. */
24
+ gap: number;
25
+ /** Share of the canvas the category axis may take, 0..1. */
26
+ maxFraction: number;
27
+ };
28
+ /** What the chart needs to draw the axis: its reserved width and the text of each tick. */
29
+ export type CategoryAxisMetrics = {
30
+ /** `width` for recharts' `YAxis`, or `undefined` while nothing has been measured yet. */
31
+ width: number | undefined;
32
+ /** Tick text to paint — the original label, or one truncated at its END. */
33
+ display: (label: string) => string;
34
+ };
35
+ /**
36
+ * Axis width from the measured tick widths: the widest tick, capped, plus the gap.
37
+ * Pure — the browser measurement is injected, so the rule is testable without a layout engine.
38
+ */
39
+ export declare function resolveCategoryAxisWidth(textWidths: readonly number[], available: number, { gap, maxFraction }: CategoryAxisKnobs): {
40
+ width: number;
41
+ textCap: number;
42
+ };
43
+ /**
44
+ * Cut `label` at its END until it fits `maxWidth`, appending an ellipsis. The first glyph is
45
+ * always kept: a tick that reads `…` identifies nothing, and a tick cut from the front is the
46
+ * bug this exists to fix.
47
+ */
48
+ export declare function truncateToWidth(label: string, maxWidth: number, measure: (text: string) => number): string;
49
+ /**
50
+ * Measure the category ticks in the live document and keep the axis sized to them.
51
+ *
52
+ * Re-runs when the canvas resizes and once `document.fonts` settles, because a CJK web face
53
+ * arriving after first paint changes every one of these widths.
54
+ */
55
+ export declare function useCategoryAxisMetrics(canvasRef: React.RefObject<HTMLDivElement | null>, probeRef: React.RefObject<HTMLSpanElement | null>, labels: readonly string[], enabled: boolean): CategoryAxisMetrics;
@@ -0,0 +1,93 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const ELLIPSIS = "\u2026";
4
+ function resolveCategoryAxisWidth(textWidths, available, { gap, maxFraction }) {
5
+ const textCap = Math.max(0, available * maxFraction - gap);
6
+ const widest = textWidths.length > 0 ? Math.max(...textWidths) : 0;
7
+ return { width: Math.ceil(Math.min(widest, textCap) + gap), textCap };
8
+ }
9
+ function truncateToWidth(label, maxWidth, measure) {
10
+ if (measure(label) <= maxWidth) return label;
11
+ const glyphs = [...label];
12
+ for (let keep = glyphs.length - 1; keep > 1; keep -= 1) {
13
+ const candidate = glyphs.slice(0, keep).join("") + ELLIPSIS;
14
+ if (measure(candidate) <= maxWidth) return candidate;
15
+ }
16
+ return `${glyphs[0] ?? ""}${ELLIPSIS}`;
17
+ }
18
+ function readKnobs(probe) {
19
+ const style = getComputedStyle(probe);
20
+ const gap = Number.parseFloat(style.marginInlineEnd);
21
+ const maxFraction = Number.parseFloat(
22
+ style.getPropertyValue("--chart-category-axis-max-fraction")
23
+ );
24
+ return {
25
+ gap: Number.isFinite(gap) ? gap : 0,
26
+ maxFraction: Number.isFinite(maxFraction) ? maxFraction : 1
27
+ };
28
+ }
29
+ function sameTruncations(a, b) {
30
+ if (a.size !== b.size) return false;
31
+ for (const [key, value] of a) {
32
+ if (b.get(key) !== value) return false;
33
+ }
34
+ return true;
35
+ }
36
+ function useCategoryAxisMetrics(canvasRef, probeRef, labels, enabled) {
37
+ const [measured, setMeasured] = React.useState(void 0);
38
+ const labelsRef = React.useRef(labels);
39
+ labelsRef.current = labels;
40
+ const key = labels.join("\0");
41
+ React.useEffect(() => {
42
+ const canvas = canvasRef.current;
43
+ const probe = probeRef.current;
44
+ if (!enabled || !canvas || !probe) {
45
+ setMeasured(void 0);
46
+ return;
47
+ }
48
+ const ticks = labelsRef.current;
49
+ const measure = (text) => {
50
+ probe.textContent = text;
51
+ return probe.getBoundingClientRect().width;
52
+ };
53
+ const run = () => {
54
+ const available = canvas.clientWidth;
55
+ if (available <= 0) return;
56
+ const knobs = readKnobs(probe);
57
+ const widths = ticks.map(measure);
58
+ const { width, textCap } = resolveCategoryAxisWidth(widths, available, knobs);
59
+ const truncations2 = /* @__PURE__ */ new Map();
60
+ ticks.forEach((label, index) => {
61
+ if ((widths[index] ?? 0) > textCap) {
62
+ truncations2.set(label, truncateToWidth(label, textCap, measure));
63
+ }
64
+ });
65
+ probe.textContent = "";
66
+ setMeasured(
67
+ (previous) => previous && previous.width === width && sameTruncations(previous.truncations, truncations2) ? previous : { width, truncations: truncations2 }
68
+ );
69
+ };
70
+ run();
71
+ const observer = new ResizeObserver(run);
72
+ observer.observe(canvas);
73
+ let live = true;
74
+ void document.fonts?.ready.then(() => {
75
+ if (live) run();
76
+ });
77
+ return () => {
78
+ live = false;
79
+ observer.disconnect();
80
+ };
81
+ }, [canvasRef, probeRef, enabled, key]);
82
+ const truncations = measured?.truncations;
83
+ const display = React.useCallback(
84
+ (label) => truncations?.get(label) ?? label,
85
+ [truncations]
86
+ );
87
+ return { width: measured?.width, display };
88
+ }
89
+ export {
90
+ resolveCategoryAxisWidth,
91
+ truncateToWidth,
92
+ useCategoryAxisMetrics
93
+ };
@@ -18,6 +18,11 @@ export declare function useChartNumberFormat(options?: Intl.NumberFormatOptions)
18
18
  type ChartFrameProps = {
19
19
  /** Visible caption; also the accessible name via `aria-labelledby` → `<figcaption>`. */
20
20
  label: React.ReactNode;
21
+ /**
22
+ * Paint the caption. `false` keeps the `<figcaption>` in the DOM as `sr-only`, so
23
+ * `aria-labelledby` still names the figure — only the duplicate visible title goes away.
24
+ */
25
+ showCaption?: boolean;
21
26
  /** Extra context appended to the screen-reader description. */
22
27
  description?: React.ReactNode;
23
28
  /** Pre-formatted "category: value" rows for the screen-reader text alternative. */
@@ -39,11 +44,15 @@ type ChartFrameProps = {
39
44
  id?: string;
40
45
  children: React.ReactNode;
41
46
  ref?: React.Ref<HTMLElement>;
47
+ /** The plot box itself — charts that measure their own axis need to read its width. */
48
+ canvasRef?: React.Ref<HTMLDivElement>;
49
+ /** Rendered inside the canvas, after `children` (the axis measuring probe). */
50
+ canvasExtra?: React.ReactNode;
42
51
  };
43
52
  /**
44
53
  * The accessible figure wrapper. The chart body passed as `children` is wrapped
45
54
  * in a `role="img"` element with a short `aria-label`; the full data is exposed as
46
55
  * a sibling visually-hidden list referenced by the figure's `aria-describedby`.
47
56
  */
48
- export declare function ChartFrame({ label, description, summaryRows, imgSummary, hasData, height, size, footer, className, id, children, ref, }: ChartFrameProps): React.JSX.Element;
57
+ export declare function ChartFrame({ label, showCaption, description, summaryRows, imgSummary, hasData, height, size, footer, className, id, children, ref, canvasRef, canvasExtra, }: ChartFrameProps): React.JSX.Element;
49
58
  export {};
@@ -29,6 +29,7 @@ function useChartNumberFormat(options) {
29
29
  }
30
30
  function ChartFrame({
31
31
  label,
32
+ showCaption = true,
32
33
  description,
33
34
  summaryRows,
34
35
  imgSummary,
@@ -39,7 +40,9 @@ function ChartFrame({
39
40
  className,
40
41
  id,
41
42
  children,
42
- ref
43
+ ref,
44
+ canvasRef,
45
+ canvasExtra
43
46
  }) {
44
47
  const reactId = React.useId();
45
48
  const titleId = `${id ?? reactId}-title`;
@@ -55,8 +58,21 @@ function ChartFrame({
55
58
  "aria-labelledby": titleId,
56
59
  "aria-describedby": descId,
57
60
  children: [
58
- /* @__PURE__ */ jsx("figcaption", { id: titleId, className: "ui-chart-title", children: label }),
59
- /* @__PURE__ */ jsx("div", { className: "ui-chart-canvas", style: { height }, role: "img", "aria-label": imgSummary, children }),
61
+ /* @__PURE__ */ jsx("figcaption", { id: titleId, className: showCaption ? "ui-chart-title" : "sr-only", children: label }),
62
+ /* @__PURE__ */ jsxs(
63
+ "div",
64
+ {
65
+ ref: canvasRef,
66
+ className: "ui-chart-canvas",
67
+ style: { height },
68
+ role: "img",
69
+ "aria-label": imgSummary,
70
+ children: [
71
+ children,
72
+ canvasExtra
73
+ ]
74
+ }
75
+ ),
60
76
  footer ? /* @__PURE__ */ jsx("div", { className: "ui-chart-footer", children: footer }) : null,
61
77
  /* @__PURE__ */ jsxs("p", { id: descId, className: "sr-only", children: [
62
78
  description ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -12,4 +12,4 @@ export type { CompactBarTrendProp, CompactBarTrendProp as CompactBarTrendProps,
12
12
  * localized one-line summary, and a visually-hidden per-category value list referenced through
13
13
  * `aria-describedby` (WCAG 1.1.1).
14
14
  */
15
- export declare function CompactBarTrend({ data, categoryKey, valueKey, label, description, size, emphasizedIndex, showCategoryLabels, numberFormat, footer, emptyMessage, className, id, ref, }: CompactBarTrendProp): React.JSX.Element;
15
+ export declare function CompactBarTrend({ data, categoryKey, valueKey, label, showCaption, description, size, emphasizedIndex, showCategoryLabels, numberFormat, footer, emptyMessage, className, id, ref, }: CompactBarTrendProp): React.JSX.Element;