@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.
- package/README.md +8 -2
- package/dist/app/app-provider.js +37 -21
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/storage.d.ts +17 -0
- package/dist/app/storage.js +28 -0
- package/dist/app/theme-axes.d.ts +2 -0
- package/dist/app/theme-axes.js +45 -0
- package/dist/components/charts/chart-cartesian.d.ts +2 -1
- package/dist/components/charts/chart-cartesian.js +52 -3
- package/dist/components/charts/chart-category-axis.d.ts +55 -0
- package/dist/components/charts/chart-category-axis.js +93 -0
- package/dist/components/charts/chart-frame.d.ts +10 -1
- package/dist/components/charts/chart-frame.js +19 -3
- package/dist/components/charts/compact-bar-trend.d.ts +1 -1
- package/dist/components/charts/compact-bar-trend.js +2 -0
- package/dist/components/charts/pie-chart.d.ts +1 -1
- package/dist/components/charts/pie-chart.js +12 -1
- package/dist/components/charts/recharts-peer.d.ts +49 -0
- package/dist/components/charts/recharts-peer.js +45 -0
- package/dist/components/data-display/card.d.ts +12 -1
- package/dist/components/data-display/card.js +22 -4
- package/dist/components/data-display/code-block.js +6 -2
- package/dist/components/data-display/data-table.d.ts +39 -4
- package/dist/components/data-display/data-table.js +802 -270
- package/dist/components/data-display/descriptions.d.ts +27 -6
- package/dist/components/data-display/descriptions.js +58 -17
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/list-row.d.ts +10 -1
- package/dist/components/data-display/list-row.js +7 -1
- package/dist/components/data-display/popover.d.ts +9 -3
- package/dist/components/data-display/popover.js +65 -11
- package/dist/components/data-display/range-timeline.d.ts +38 -0
- package/dist/components/data-display/range-timeline.js +161 -0
- package/dist/components/data-display/scroll-area.js +13 -2
- package/dist/components/data-display/service-launcher-card.js +14 -12
- package/dist/components/data-display/table.d.ts +71 -3
- package/dist/components/data-display/table.js +56 -18
- package/dist/components/data-display/tree-list.js +4 -9
- package/dist/components/data-entry/calendar.d.ts +2 -2
- package/dist/components/data-entry/calendar.js +23 -3
- package/dist/components/data-entry/cascader.d.ts +1 -1
- package/dist/components/data-entry/cascader.js +188 -24
- package/dist/components/data-entry/color-picker.d.ts +1 -1
- package/dist/components/data-entry/color-picker.js +17 -5
- package/dist/components/data-entry/control-appearance.d.ts +64 -0
- package/dist/components/data-entry/control-appearance.js +39 -0
- package/dist/components/data-entry/control-surface.d.ts +61 -0
- package/dist/components/data-entry/control-surface.js +39 -0
- package/dist/components/data-entry/date-picker.d.ts +1 -1
- package/dist/components/data-entry/date-picker.js +344 -113
- package/dist/components/data-entry/date-range-picker.d.ts +1 -1
- package/dist/components/data-entry/date-range-picker.js +278 -140
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +39 -4
- package/dist/components/data-entry/form.d.ts +4 -0
- package/dist/components/data-entry/form.js +4 -2
- package/dist/components/data-entry/index.d.ts +7 -3
- package/dist/components/data-entry/index.js +10 -1
- package/dist/components/data-entry/input-otp.d.ts +1 -1
- package/dist/components/data-entry/input.d.ts +12 -34
- package/dist/components/data-entry/input.js +92 -24
- package/dist/components/data-entry/month-picker.d.ts +1 -1
- package/dist/components/data-entry/month-picker.js +47 -10
- package/dist/components/data-entry/month-range-picker.d.ts +1 -1
- package/dist/components/data-entry/month-range-picker.js +51 -11
- package/dist/components/data-entry/number-input.d.ts +7 -0
- package/dist/components/data-entry/number-input.js +147 -98
- package/dist/components/data-entry/password-input.d.ts +1 -1
- package/dist/components/data-entry/radio.d.ts +1 -1
- package/dist/components/data-entry/radio.js +61 -13
- package/dist/components/data-entry/search-input.d.ts +1 -1
- package/dist/components/data-entry/search-input.js +16 -2
- package/dist/components/data-entry/search-select.d.ts +2 -2
- package/dist/components/data-entry/search-select.js +209 -78
- package/dist/components/data-entry/select.d.ts +22 -4
- package/dist/components/data-entry/select.js +215 -163
- package/dist/components/data-entry/slider.d.ts +9 -1
- package/dist/components/data-entry/slider.js +87 -9
- package/dist/components/data-entry/switch.d.ts +3 -0
- package/dist/components/data-entry/switch.js +29 -3
- package/dist/components/data-entry/textarea.d.ts +14 -56
- package/dist/components/data-entry/textarea.js +80 -33
- package/dist/components/data-entry/time-picker.d.ts +2 -2
- package/dist/components/data-entry/time-picker.js +333 -109
- package/dist/components/data-entry/time-range-picker.d.ts +5 -0
- package/dist/components/data-entry/time-range-picker.js +89 -0
- package/dist/components/data-entry/transfer.d.ts +1 -1
- package/dist/components/data-entry/transfer.js +86 -28
- package/dist/components/data-entry/tree-select.d.ts +1 -1
- package/dist/components/data-entry/tree-select.js +201 -113
- package/dist/components/data-entry/tree-utils.js +7 -14
- package/dist/components/data-entry/upload-files.d.ts +2 -0
- package/dist/components/data-entry/upload-files.js +31 -0
- package/dist/components/data-entry/upload-request.d.ts +4 -0
- package/dist/components/data-entry/upload-request.js +53 -0
- package/dist/components/data-entry/upload-types.d.ts +28 -0
- package/dist/components/data-entry/upload-types.js +2 -0
- package/dist/components/data-entry/upload.d.ts +2 -2
- package/dist/components/data-entry/upload.js +475 -115
- package/dist/components/feedback/dialog.js +19 -19
- package/dist/components/feedback/sheet.js +3 -0
- package/dist/components/feedback/tooltip.js +3 -0
- package/dist/components/general/button.d.ts +2 -1
- package/dist/components/general/button.js +7 -1
- package/dist/components/general/index.d.ts +1 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/general/typography.d.ts +3 -0
- package/dist/components/general/typography.js +15 -1
- package/dist/components/general/visually-hidden.d.ts +3 -0
- package/dist/components/general/visually-hidden.js +9 -0
- package/dist/components/layout/app-launcher.d.ts +34 -0
- package/dist/components/layout/app-launcher.js +261 -0
- package/dist/components/layout/app-shell.d.ts +3 -1
- package/dist/components/layout/app-shell.js +34 -14
- package/dist/components/layout/auth-shell.d.ts +1 -1
- package/dist/components/layout/auth-shell.js +2 -0
- package/dist/components/layout/breadcrumb.d.ts +14 -2
- package/dist/components/layout/breadcrumb.js +46 -4
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/flex.js +23 -2
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.js +5 -1
- package/dist/components/layout/org-switcher.js +20 -1
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +5 -3
- package/dist/components/layout/responsive-grid.d.ts +16 -2
- package/dist/components/layout/responsive-grid.js +29 -2
- package/dist/components/layout/sidebar.js +2 -1
- package/dist/components/layout/topbar-item.d.ts +3 -0
- package/dist/components/layout/topbar-item.js +18 -3
- package/dist/components/layout/topbar.d.ts +1 -1
- package/dist/components/layout/topbar.js +27 -6
- package/dist/components/navigation/dropdown-menu.d.ts +30 -2
- package/dist/components/navigation/dropdown-menu.js +73 -11
- package/dist/components/navigation/pagination.d.ts +2 -2
- package/dist/components/navigation/pagination.js +155 -83
- package/dist/components/navigation/steps.d.ts +2 -2
- package/dist/components/navigation/steps.js +29 -4
- package/dist/components/navigation/tabs.d.ts +5 -33
- package/dist/components/navigation/tabs.js +232 -64
- package/dist/components/ui/hover-card.js +3 -0
- package/dist/components/ui/input-otp.d.ts +26 -28
- package/dist/components/ui/input-otp.js +50 -7
- package/dist/components/ui/password-input.d.ts +36 -2
- package/dist/components/ui/password-input.js +27 -7
- package/dist/components/ui/rating.d.ts +25 -0
- package/dist/components/ui/rating.js +67 -27
- package/dist/components/ui/segmented.d.ts +9 -0
- package/dist/components/ui/segmented.js +17 -2
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +13 -0
- package/dist/components/ui/tag-input.d.ts +45 -0
- package/dist/components/ui/tag-input.js +109 -27
- package/dist/form/form-context.d.ts +29 -0
- package/dist/form/form-context.js +44 -4
- package/dist/form/form-field-array.d.ts +22 -0
- package/dist/form/form-field-array.js +50 -0
- package/dist/form/form-field-control.d.ts +2 -1
- package/dist/form/form-field-control.js +116 -40
- package/dist/form/form-root.d.ts +2 -1
- package/dist/form/form-root.js +142 -23
- package/dist/form/index.d.ts +3 -1
- package/dist/form/index.js +12 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +3 -1
- package/dist/i18n/messages/en.json +59 -6
- package/dist/i18n/messages/ja.json +59 -6
- package/dist/i18n/messages/vi.json +59 -6
- package/dist/i18n/translate.d.ts +19 -0
- package/dist/i18n/translate.js +24 -0
- package/dist/inertia/index.d.ts +20 -0
- package/dist/inertia/index.js +60 -1
- package/dist/lib/control-styles.d.ts +25 -3
- package/dist/lib/control-styles.js +9 -3
- package/dist/lib/datetime/picker-format.d.ts +8 -0
- package/dist/lib/datetime/picker-format.js +54 -0
- package/dist/lib/overlay-portal.d.ts +18 -0
- package/dist/lib/overlay-portal.js +17 -0
- package/dist/props/components/app.prop.d.ts +13 -2
- package/dist/props/components/charts.prop.d.ts +21 -0
- package/dist/props/components/data-display.prop.d.ts +55 -4
- package/dist/props/components/data-entry.prop.d.ts +687 -43
- package/dist/props/components/form.prop.d.ts +142 -4
- package/dist/props/components/general.prop.d.ts +30 -1
- package/dist/props/components/index.d.ts +2 -2
- package/dist/props/components/layout.prop.d.ts +255 -8
- package/dist/props/components/navigation.prop.d.ts +113 -3
- package/dist/props/registry.d.ts +376 -10
- package/dist/props/registry.js +493 -9
- package/dist/props/vocabulary/data.prop.d.ts +179 -1
- package/dist/props/vocabulary/index.d.ts +4 -4
- package/dist/props/vocabulary/interaction.prop.d.ts +51 -2
- package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
- package/dist/props/vocabulary/shared.prop.d.ts +33 -0
- package/dist/styles/badge-layout.css +2 -1
- package/dist/styles/card-layout.css +36 -5
- package/dist/styles/chart-layout.css +17 -1
- package/dist/styles/control.css +520 -14
- package/dist/styles/data-display-layout.css +248 -7
- package/dist/styles/data-entry-layout.css +13 -0
- package/dist/styles/layout.css +221 -4
- package/dist/styles/navigation-layout.css +230 -1
- package/dist/styles/shell-layout.css +690 -21
- package/dist/styles/table-layout.css +95 -6
- package/dist/styles/text-layout.css +5 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/badge.css +1 -0
- package/dist/tokens/components/chart.css +6 -0
- package/dist/tokens/components/control.css +81 -3
- package/dist/tokens/components/data-display.css +9 -0
- package/dist/tokens/components/descriptions.css +11 -0
- package/dist/tokens/components/flex.css +8 -0
- package/dist/tokens/components/navigation.css +55 -0
- package/dist/tokens/components/shell.css +100 -5
- package/dist/tokens/components/table.css +14 -0
- package/dist/tokens/foundation.css +2 -0
- package/dist/tokens/semantic/layout.css +9 -0
- package/docs/CONSUMER-RULES.md +13 -0
- package/docs/DESIGN-AUTHORITY.md +43 -6
- package/docs/DEVELOPMENT.md +4 -3
- package/docs/FORMS.md +151 -6
- package/docs/FRAME-COVERAGE-REPORT.md +29 -16
- package/docs/README.md +1 -1
- package/docs/STANDARDS-vocabulary-tokens.md +1 -1
- package/docs/TESTING.md +15 -6
- package/docs/charts/cjk-category-axis.tsx +81 -0
- package/docs/data-display/card/index.tsx +22 -0
- package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
- package/docs/data-display/data-table/index.tsx +23 -0
- package/docs/data-display/descriptions.tsx +41 -0
- package/docs/data-display/list-row.tsx +6 -6
- package/docs/data-display/scroll-area.tsx +31 -25
- package/docs/data-display/table.tsx +104 -45
- package/docs/data-display/timeline.tsx +41 -0
- package/docs/data-entry/date-picker.tsx +85 -0
- package/docs/data-entry/date-range-picker.tsx +26 -0
- package/docs/data-entry/form-dynamic-fields.tsx +199 -0
- package/docs/data-entry/form.tsx +74 -4
- package/docs/data-entry/input-otp.tsx +24 -0
- package/docs/data-entry/input.tsx +46 -1
- package/docs/data-entry/month-range-picker.tsx +1 -1
- package/docs/data-entry/select.tsx +27 -0
- package/docs/data-entry/switch.tsx +41 -0
- package/docs/data-entry/textarea.tsx +38 -0
- package/docs/data-entry/time-picker.tsx +85 -1
- package/docs/data-entry/time-range-picker.tsx +55 -0
- package/docs/data-entry/transfer.tsx +17 -0
- package/docs/data-entry/upload.tsx +56 -12
- package/docs/feedback/tooltip.tsx +1 -1
- package/docs/general/activity.tsx +2 -2
- package/docs/general/button/index.tsx +14 -1
- package/docs/general/typography.tsx +14 -1
- package/docs/layout/app-launcher.tsx +195 -0
- package/docs/layout/app-shell-arrangements.tsx +5 -5
- package/docs/layout/app-shell.tsx +11 -0
- package/docs/layout/flex.tsx +50 -0
- package/docs/layout/responsive-grid.tsx +21 -1
- package/docs/layout/topbar.tsx +5 -9
- package/docs/navigation/app-setting-picker.tsx +11 -0
- package/docs/navigation/breadcrumb.tsx +48 -0
- package/docs/navigation/dropdown-menu.tsx +21 -0
- package/docs/navigation/pagination.tsx +52 -0
- package/docs/navigation/steps.tsx +37 -0
- package/docs/navigation/tabs.tsx +97 -1
- package/docs/query/button-refetch.tsx +1 -0
- package/docs/showcase/settings-security-mfa.tsx +3 -3
- package/package.json +24 -4
- package/scripts/_agent-setup.mjs +76 -9
- package/scripts/init-guinea-pig.mjs +26 -4
- package/scripts/ui-audit.mjs +243 -30
- 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 ·
|
|
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
|
package/dist/app/app-provider.js
CHANGED
|
@@ -14,7 +14,13 @@ import {
|
|
|
14
14
|
enableLiveRelativeFormatting,
|
|
15
15
|
syncDatetimeContext
|
|
16
16
|
} from "../lib/datetime/index.js";
|
|
17
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
commitPreferences();
|
|
192
208
|
},
|
|
193
|
-
[onLocaleChange,
|
|
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
|
-
|
|
216
|
+
commitPreferences();
|
|
201
217
|
},
|
|
202
|
-
[onTimezoneChange,
|
|
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
|
-
|
|
225
|
+
commitPreferences();
|
|
210
226
|
},
|
|
211
|
-
[onTimeFormatChange,
|
|
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
|
-
|
|
234
|
+
commitPreferences();
|
|
219
235
|
},
|
|
220
|
-
[onDateFormatChange,
|
|
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
|
-
|
|
243
|
+
commitPreferences();
|
|
228
244
|
},
|
|
229
|
-
[onThemeChange,
|
|
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
|
-
|
|
252
|
+
commitPreferences();
|
|
237
253
|
},
|
|
238
|
-
[onBrandChange,
|
|
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
|
-
|
|
261
|
+
commitPreferences();
|
|
246
262
|
},
|
|
247
|
-
[onDensityChange,
|
|
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
|
-
|
|
270
|
+
commitPreferences();
|
|
255
271
|
},
|
|
256
|
-
[onFontSizeChange,
|
|
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
|
-
|
|
279
|
+
commitPreferences();
|
|
264
280
|
},
|
|
265
|
-
[onScalingChange,
|
|
281
|
+
[onScalingChange, commitPreferences]
|
|
266
282
|
);
|
|
267
283
|
const requestHeaders = React.useMemo(
|
|
268
284
|
() => buildRequestHeaders(locale, timezone, timeFormat, dateFormat),
|
package/dist/app/index.d.ts
CHANGED
|
@@ -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,
|
package/dist/app/storage.d.ts
CHANGED
|
@@ -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 };
|
package/dist/app/storage.js
CHANGED
|
@@ -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
|
};
|
package/dist/app/theme-axes.d.ts
CHANGED
|
@@ -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;
|
package/dist/app/theme-axes.js
CHANGED
|
@@ -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
|
-
|
|
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__ */
|
|
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;
|