@godxjp/ui 23.2.0 → 23.3.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/dist/app/timezones.js +2 -1
- package/dist/components/charts/chart-cartesian.js +2 -4
- package/dist/components/charts/chart-frame.js +2 -1
- package/dist/components/data-entry/date-picker.js +12 -14
- package/dist/components/data-entry/number-input.js +2 -1
- package/dist/components/feedback/index.d.ts +2 -2
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/feedback/skeleton.d.ts +11 -2
- package/dist/components/feedback/skeleton.js +8 -0
- package/dist/components/general/button.js +2 -1
- package/dist/components/layout/error-surface.js +3 -2
- package/dist/components/ui/toggle.js +2 -1
- package/dist/i18n/translate.js +3 -2
- package/dist/lib/datetime/picker-format.js +2 -1
- package/dist/lib/format.js +3 -2
- package/dist/lib/intl-cache.d.ts +32 -0
- package/dist/lib/intl-cache.js +29 -0
- package/dist/props/components/data-entry.prop.d.ts +11 -0
- package/dist/props/components/feedback.prop.d.ts +19 -0
- package/dist/props/registry.d.ts +5 -0
- package/dist/props/registry.js +5 -0
- package/dist/styles/alert-layout.css +22 -0
- package/dist/styles/card-layout.css +4 -0
- package/dist/styles/form-layout.css +11 -1
- package/dist/tokens/axes.css +5 -0
- package/dist/tokens/foundation.css +4 -4
- package/docs/DESIGN-AUTHORITY.md +44 -20
- package/package.json +21 -5
- package/scripts/_agent-setup.mjs +196 -29
- package/scripts/guinea-pig-skill.md +14 -0
- package/scripts/postinstall.mjs +9 -0
- package/scripts/ui-audit.mjs +135 -32
package/dist/app/timezones.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { translate } from "../i18n/translate.js";
|
|
2
|
+
import { dateTimeFormat } from "../lib/intl-cache.js";
|
|
2
3
|
const APP_TIMEZONE_PRESET = [
|
|
3
4
|
"UTC",
|
|
4
5
|
"Asia/Ho_Chi_Minh",
|
|
@@ -113,7 +114,7 @@ function getTimezoneCityName(timezone) {
|
|
|
113
114
|
function getTimezoneOffsetLabel(timezone, locale = "en") {
|
|
114
115
|
if (timezone === "UTC") return "UTC";
|
|
115
116
|
try {
|
|
116
|
-
const parts =
|
|
117
|
+
const parts = dateTimeFormat(locale, {
|
|
117
118
|
timeZone: resolveTimezoneForIntl(timezone),
|
|
118
119
|
timeZoneName: "shortOffset"
|
|
119
120
|
}).formatToParts(/* @__PURE__ */ new Date());
|
|
@@ -22,6 +22,7 @@ import { useTranslation } from "../../i18n/use-translation.js";
|
|
|
22
22
|
import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
|
|
23
23
|
import { buildCartesianSummary } from "./chart-summary.js";
|
|
24
24
|
import { useCategoryAxisMetrics } from "./chart-category-axis.js";
|
|
25
|
+
import { listFormat } from "../../lib/intl-cache.js";
|
|
25
26
|
function CartesianChart({
|
|
26
27
|
kind,
|
|
27
28
|
data,
|
|
@@ -45,10 +46,7 @@ function CartesianChart({
|
|
|
45
46
|
assertRechartsPeer();
|
|
46
47
|
const { t, locale } = useTranslation();
|
|
47
48
|
const fmt = useChartNumberFormat(numberFormat);
|
|
48
|
-
const list = React.useMemo(
|
|
49
|
-
() => new Intl.ListFormat(locale, { style: "narrow", type: "unit" }),
|
|
50
|
-
[locale]
|
|
51
|
-
);
|
|
49
|
+
const list = React.useMemo(() => listFormat(locale, { style: "narrow", type: "unit" }), [locale]);
|
|
52
50
|
const hasData = data.length > 0 && series.length > 0;
|
|
53
51
|
const summary = buildCartesianSummary(
|
|
54
52
|
data,
|
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { numberFormat } from "../../lib/intl-cache.js";
|
|
6
7
|
const CHART_COLORS = [
|
|
7
8
|
"var(--chart-1)",
|
|
8
9
|
"var(--chart-2)",
|
|
@@ -25,7 +26,7 @@ function chartHeight(size = "md", height) {
|
|
|
25
26
|
}
|
|
26
27
|
function useChartNumberFormat(options) {
|
|
27
28
|
const { locale } = useTranslation();
|
|
28
|
-
return React.useMemo(() =>
|
|
29
|
+
return React.useMemo(() => numberFormat(locale, options), [locale, options]);
|
|
29
30
|
}
|
|
30
31
|
function ChartFrame({
|
|
31
32
|
label,
|
|
@@ -22,6 +22,7 @@ import { CONTROL_STATUS_CHROME_CLASS, CONTROL_VARIANT_CHROME_CLASS } from "./con
|
|
|
22
22
|
import { Input } from "./input.js";
|
|
23
23
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
24
24
|
import { Calendar } from "./calendar.js";
|
|
25
|
+
import { dateTimeFormat, numberFormat } from "../../lib/intl-cache.js";
|
|
25
26
|
const ISO_HINT = "yyyy-mm-dd";
|
|
26
27
|
const PERIOD_PICKERS = /* @__PURE__ */ new Set(["month", "quarter", "year"]);
|
|
27
28
|
const PERIOD_COLUMNS = 3;
|
|
@@ -49,6 +50,7 @@ function DatePicker(props) {
|
|
|
49
50
|
disabledDate,
|
|
50
51
|
cellRender,
|
|
51
52
|
allowClear,
|
|
53
|
+
triggerLabel,
|
|
52
54
|
format: formatProp,
|
|
53
55
|
parseFormat,
|
|
54
56
|
minDate,
|
|
@@ -296,13 +298,9 @@ function DatePicker(props) {
|
|
|
296
298
|
(index) => index < periodCells
|
|
297
299
|
);
|
|
298
300
|
const pageStep = picker === "year" ? periodCells : 1;
|
|
299
|
-
const prevDisabled = !pickerDateAllowed(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
void 0
|
|
303
|
-
);
|
|
304
|
-
const nextDisabled = !pickerDateAllowed(periodPage(viewYear + pageStep)[0], void 0, maximum);
|
|
305
|
-
const periodLabel = (date, index) => picker === "year" ? new Intl.DateTimeFormat(locale, { year: "numeric" }).format(date) : picker === "quarter" ? t("dataEntry.datePicker.quarter", { quarter: index + 1 }) ?? `Q${index + 1}` : new Intl.DateTimeFormat(locale, { month: "short" }).format(date);
|
|
301
|
+
const prevDisabled = () => !pickerDateAllowed(periodPage(viewYear - pageStep)[periodCells - 1], minimum, void 0);
|
|
302
|
+
const nextDisabled = () => !pickerDateAllowed(periodPage(viewYear + pageStep)[0], void 0, maximum);
|
|
303
|
+
const periodLabel = (date, index) => picker === "year" ? dateTimeFormat(locale, { year: "numeric" }).format(date) : picker === "quarter" ? t("dataEntry.datePicker.quarter", { quarter: index + 1 }) ?? `Q${index + 1}` : dateTimeFormat(locale, { month: "short" }).format(date);
|
|
306
304
|
const periodPick = (date) => {
|
|
307
305
|
if (!range) {
|
|
308
306
|
choose(date);
|
|
@@ -318,7 +316,7 @@ function DatePicker(props) {
|
|
|
318
316
|
choose({ from: pendingFrom, to: date });
|
|
319
317
|
if (!needConfirm) setOpen(false);
|
|
320
318
|
};
|
|
321
|
-
const
|
|
319
|
+
const renderPeriodPanel = () => /* @__PURE__ */ jsxs("div", { className: "ui-month-picker-panel", children: [
|
|
322
320
|
/* @__PURE__ */ jsxs("div", { className: "ui-month-picker-nav", children: [
|
|
323
321
|
/* @__PURE__ */ jsx(
|
|
324
322
|
Button,
|
|
@@ -326,21 +324,21 @@ function DatePicker(props) {
|
|
|
326
324
|
type: "button",
|
|
327
325
|
variant: "outline",
|
|
328
326
|
size: "icon-sm",
|
|
329
|
-
disabled: prevDisabled,
|
|
327
|
+
disabled: prevDisabled(),
|
|
330
328
|
"aria-label": t("dataEntry.monthPicker.previousYear") ?? "Previous year",
|
|
331
329
|
className: "ui-month-picker-nav-button",
|
|
332
330
|
onClick: () => setViewYear(viewYear - pageStep),
|
|
333
331
|
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "ui-month-picker-icon", "aria-hidden": "true" })
|
|
334
332
|
}
|
|
335
333
|
),
|
|
336
|
-
/* @__PURE__ */ jsx("span", { className: "ui-month-picker-nav-label", "aria-live": "polite", children:
|
|
334
|
+
/* @__PURE__ */ jsx("span", { className: "ui-month-picker-nav-label", "aria-live": "polite", children: numberFormat(locale, { useGrouping: false }).format(viewYear) }),
|
|
337
335
|
/* @__PURE__ */ jsx(
|
|
338
336
|
Button,
|
|
339
337
|
{
|
|
340
338
|
type: "button",
|
|
341
339
|
variant: "outline",
|
|
342
340
|
size: "icon-sm",
|
|
343
|
-
disabled: nextDisabled,
|
|
341
|
+
disabled: nextDisabled(),
|
|
344
342
|
"aria-label": t("dataEntry.monthPicker.nextYear") ?? "Next year",
|
|
345
343
|
className: "ui-month-picker-nav-button",
|
|
346
344
|
onClick: () => setViewYear(viewYear + pageStep),
|
|
@@ -390,7 +388,7 @@ function DatePicker(props) {
|
|
|
390
388
|
showClose,
|
|
391
389
|
onClose: () => setOpen(false)
|
|
392
390
|
};
|
|
393
|
-
const
|
|
391
|
+
const renderDayPanel = () => range ? /* @__PURE__ */ jsx(
|
|
394
392
|
Calendar,
|
|
395
393
|
{
|
|
396
394
|
mode: "range",
|
|
@@ -458,7 +456,7 @@ function DatePicker(props) {
|
|
|
458
456
|
type: "button",
|
|
459
457
|
disabled: allDisabled,
|
|
460
458
|
tabIndex: -1,
|
|
461
|
-
"aria-label": (isPeriod ? t("dataEntry.monthPicker.openGrid") : range ? t("dataEntry.dateRangePicker.openCalendar") : t("dataEntry.datePicker.openCalendar")) ?? "Open calendar",
|
|
459
|
+
"aria-label": triggerLabel ?? (isPeriod ? t("dataEntry.monthPicker.openGrid") : range ? t("dataEntry.dateRangePicker.openCalendar") : t("dataEntry.datePicker.openCalendar")) ?? "Open calendar",
|
|
462
460
|
className: range ? "text-muted-foreground hover:text-foreground shrink-0" : "ui-control-inline-affix-action",
|
|
463
461
|
children: /* @__PURE__ */ jsx(
|
|
464
462
|
CalendarIcon,
|
|
@@ -502,7 +500,7 @@ function DatePicker(props) {
|
|
|
502
500
|
},
|
|
503
501
|
index
|
|
504
502
|
)) }) : null,
|
|
505
|
-
isPeriod ?
|
|
503
|
+
isPeriod ? renderPeriodPanel() : renderDayPanel(),
|
|
506
504
|
showTime ? /* @__PURE__ */ jsx(Flex, { pad: "sm", children: /* @__PURE__ */ jsx(
|
|
507
505
|
TimePicker,
|
|
508
506
|
{
|
|
@@ -7,6 +7,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
7
7
|
import { pickFieldA11y } from "../../lib/field-a11y.js";
|
|
8
8
|
import { Button } from "../general/button.js";
|
|
9
9
|
import { Input } from "./input.js";
|
|
10
|
+
import { numberFormat } from "../../lib/intl-cache.js";
|
|
10
11
|
function decimalsOf(n) {
|
|
11
12
|
if (!Number.isFinite(n)) return 0;
|
|
12
13
|
const s = String(n);
|
|
@@ -77,7 +78,7 @@ const NumberInput = React.forwardRef(
|
|
|
77
78
|
const numericValue = isControlled ? controlledValue ?? null : internal;
|
|
78
79
|
const effectivePrecision = precision ?? decimalsOf(step);
|
|
79
80
|
const intlFormatter = React.useMemo(
|
|
80
|
-
() =>
|
|
81
|
+
() => numberFormat(locale, {
|
|
81
82
|
minimumFractionDigits: 0,
|
|
82
83
|
maximumFractionDigits: Math.max(effectivePrecision, 0),
|
|
83
84
|
useGrouping: false
|
|
@@ -6,8 +6,8 @@ export { TwoFactorSetup } from "./two-factor-setup.js";
|
|
|
6
6
|
export type { TwoFactorSetupLabels, TwoFactorSetupProps } from "./two-factor-setup.js";
|
|
7
7
|
export { Toaster } from "./sonner.js";
|
|
8
8
|
export { toast } from "./use-toast.js";
|
|
9
|
-
export { Skeleton, SkeletonRows, SkeletonTable, SkeletonDetail, SkeletonStat, SkeletonArticle, SkeletonAvatar, SkeletonButton, SkeletonInput, SkeletonNode, SkeletonImage, } from "./skeleton.js";
|
|
10
|
-
export type { SkeletonProp, SkeletonProps, SkeletonWidth, SkeletonArticleProp, SkeletonArticleProps, SkeletonAvatarProp, SkeletonAvatarProps, SkeletonButtonProp, SkeletonButtonProps, SkeletonInputProp, SkeletonInputProps, SkeletonNodeProp, SkeletonNodeProps, SkeletonImageProp, SkeletonImageProps, } from "./skeleton.js";
|
|
9
|
+
export { Skeleton, SkeletonForm, SkeletonRows, SkeletonTable, SkeletonDetail, SkeletonStat, SkeletonArticle, SkeletonAvatar, SkeletonButton, SkeletonInput, SkeletonNode, SkeletonImage, } from "./skeleton.js";
|
|
10
|
+
export type { SkeletonProp, SkeletonProps, SkeletonWidth, SkeletonArticleProp, SkeletonArticleProps, SkeletonAvatarProp, SkeletonAvatarProps, SkeletonButtonProp, SkeletonButtonProps, SkeletonFormProp, SkeletonFormProps, SkeletonInputProp, SkeletonInputProps, SkeletonNodeProp, SkeletonNodeProps, SkeletonImageProp, SkeletonImageProps, } from "./skeleton.js";
|
|
11
11
|
export { Banner } from "./banner.js";
|
|
12
12
|
export type { BannerProp, BannerProps } from "./banner.js";
|
|
13
13
|
export { Alert, AlertTitle, AlertContent, AlertDescription, AlertActions, AlertQueryError, } from "./alert.js";
|
|
@@ -44,6 +44,7 @@ import { Toaster } from "./sonner.js";
|
|
|
44
44
|
import { toast } from "./use-toast.js";
|
|
45
45
|
import {
|
|
46
46
|
Skeleton,
|
|
47
|
+
SkeletonForm,
|
|
47
48
|
SkeletonRows,
|
|
48
49
|
SkeletonTable,
|
|
49
50
|
SkeletonDetail,
|
|
@@ -114,6 +115,7 @@ export {
|
|
|
114
115
|
SkeletonAvatar,
|
|
115
116
|
SkeletonButton,
|
|
116
117
|
SkeletonDetail,
|
|
118
|
+
SkeletonForm,
|
|
117
119
|
SkeletonImage,
|
|
118
120
|
SkeletonInput,
|
|
119
121
|
SkeletonNode,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type { SkeletonArticleProp, SkeletonAvatarProp, SkeletonButtonProp, SkeletonImageProp, SkeletonInputProp, SkeletonNodeProp, SkeletonProp } from "../../props/components/feedback.prop.js";
|
|
3
|
-
export type { SkeletonProp, SkeletonProp as SkeletonProps, SkeletonWidth, SkeletonArticleProp, SkeletonArticleProp as SkeletonArticleProps, SkeletonAvatarProp, SkeletonAvatarProp as SkeletonAvatarProps, SkeletonButtonProp, SkeletonButtonProp as SkeletonButtonProps, SkeletonInputProp, SkeletonInputProp as SkeletonInputProps, SkeletonNodeProp, SkeletonNodeProp as SkeletonNodeProps, SkeletonImageProp, SkeletonImageProp as SkeletonImageProps, } from "../../props/components/feedback.prop.js";
|
|
2
|
+
import type { SkeletonArticleProp, SkeletonAvatarProp, SkeletonButtonProp, SkeletonFormProp, SkeletonImageProp, SkeletonInputProp, SkeletonNodeProp, SkeletonProp } from "../../props/components/feedback.prop.js";
|
|
3
|
+
export type { SkeletonProp, SkeletonProp as SkeletonProps, SkeletonFormProp, SkeletonFormProp as SkeletonFormProps, SkeletonWidth, SkeletonArticleProp, SkeletonArticleProp as SkeletonArticleProps, SkeletonAvatarProp, SkeletonAvatarProp as SkeletonAvatarProps, SkeletonButtonProp, SkeletonButtonProp as SkeletonButtonProps, SkeletonInputProp, SkeletonInputProp as SkeletonInputProps, SkeletonNodeProp, SkeletonNodeProp as SkeletonNodeProps, SkeletonImageProp, SkeletonImageProp as SkeletonImageProps, } from "../../props/components/feedback.prop.js";
|
|
4
4
|
declare function SkeletonBlock({ active, loading, className, children, ...props }: SkeletonProp): React.JSX.Element;
|
|
5
5
|
/** Stands in for an `Avatar` — circle for a person, square for an entity mark. */
|
|
6
6
|
export declare function SkeletonAvatar({ size, shape, active, className }: SkeletonAvatarProp): React.JSX.Element;
|
|
@@ -39,6 +39,15 @@ interface SkeletonRowsProps {
|
|
|
39
39
|
}
|
|
40
40
|
/** Skeleton for a flat list of rows (use inside a Card or section). */
|
|
41
41
|
export declare function SkeletonRows({ rows, columns, className }: SkeletonRowsProps): React.JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* Skeleton of a `Form columns={N}` — label + control PAIRS on the form's own grid (gh#552).
|
|
44
|
+
*
|
|
45
|
+
* It renders through `ResponsiveGrid` with the SAME `columns` value the form takes, so the two
|
|
46
|
+
* share one breakpoint ladder rather than two that agree today. `SkeletonRows` could get the column
|
|
47
|
+
* count right and never the inside of a cell: a form field is a short label stacked on a full-width
|
|
48
|
+
* control, and a flat line list is neither.
|
|
49
|
+
*/
|
|
50
|
+
export declare function SkeletonForm({ columns, fields, active, className }: SkeletonFormProp): React.JSX.Element;
|
|
42
51
|
/** Skeleton matching the DataTable layout — header row + N body rows. */
|
|
43
52
|
export declare function SkeletonTable({ rows, columns }: SkeletonRowsProps): React.JSX.Element;
|
|
44
53
|
/** Skeleton matching a Card detail layout — title + 6 metadata rows. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ImageIcon } from "lucide-react";
|
|
3
|
+
import { ResponsiveGrid } from "../layout/responsive-grid.js";
|
|
3
4
|
import { cn } from "../../lib/utils.js";
|
|
4
5
|
import { tableCellPaddingClass, tableRowHeightClass } from "../../lib/control-styles.js";
|
|
5
6
|
function measureStyle(width) {
|
|
@@ -169,6 +170,12 @@ function SkeletonRows({ rows = 6, columns = 4, className }) {
|
|
|
169
170
|
j
|
|
170
171
|
)) }, i)) });
|
|
171
172
|
}
|
|
173
|
+
function SkeletonForm({ columns = 1, fields = 6, active, className }) {
|
|
174
|
+
return /* @__PURE__ */ jsx("div", { className: cn("ui-skeleton-form", className), "aria-busy": "true", children: /* @__PURE__ */ jsx(ResponsiveGrid, { columns, children: Array.from({ length: fields }).map((_, i) => /* @__PURE__ */ jsxs("div", { className: "ui-skeleton-form-field", children: [
|
|
175
|
+
/* @__PURE__ */ jsx(Skeleton, { active, className: "ui-skeleton-form-label" }),
|
|
176
|
+
/* @__PURE__ */ jsx(Skeleton, { active, className: "ui-skeleton-form-control" })
|
|
177
|
+
] }, i)) }) });
|
|
178
|
+
}
|
|
172
179
|
function SkeletonTable({ rows = 8, columns = 5 }) {
|
|
173
180
|
return /* @__PURE__ */ jsxs("div", { className: "ui-skeleton-table", "aria-busy": "true", children: [
|
|
174
181
|
/* @__PURE__ */ jsx("div", { className: cn("ui-skeleton-table-head", tableCellPaddingClass, tableRowHeightClass), children: Array.from({ length: columns }).map((_, j) => /* @__PURE__ */ jsx(Skeleton, { className: cn("ui-skeleton-caption", j === 0 ? "w-1/5" : "flex-1") }, j)) }),
|
|
@@ -205,6 +212,7 @@ export {
|
|
|
205
212
|
SkeletonAvatar,
|
|
206
213
|
SkeletonButton,
|
|
207
214
|
SkeletonDetail,
|
|
215
|
+
SkeletonForm,
|
|
208
216
|
SkeletonImage,
|
|
209
217
|
SkeletonInput,
|
|
210
218
|
SkeletonNode,
|
|
@@ -6,6 +6,7 @@ import { cva } from "class-variance-authority";
|
|
|
6
6
|
import { Loader2 } from "lucide-react";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
8
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
9
|
+
import { numberFormat } from "../../lib/intl-cache.js";
|
|
9
10
|
const buttonVariants = cva("ui-button", {
|
|
10
11
|
variants: {
|
|
11
12
|
variant: {
|
|
@@ -78,7 +79,7 @@ const Button = React.forwardRef(
|
|
|
78
79
|
loadingText ?? children
|
|
79
80
|
] }) : children;
|
|
80
81
|
const showCount = !asChild && count != null && (count !== 0 || showZero);
|
|
81
|
-
const countLabel = showCount && count != null && count > overflowCount ? `${
|
|
82
|
+
const countLabel = showCount && count != null && count > overflowCount ? `${numberFormat(locale).format(overflowCount)}+` : count != null ? numberFormat(locale).format(count) : "";
|
|
82
83
|
const countNode = showCount ? /* @__PURE__ */ jsx("span", { "data-slot": "button-count", className: "ui-button-count", children: countLabel }) : null;
|
|
83
84
|
return /* @__PURE__ */ jsx(
|
|
84
85
|
Comp,
|
|
@@ -8,6 +8,7 @@ import { EmptyState } from "../data-display/empty-state.js";
|
|
|
8
8
|
import { Progress } from "../data-display/progress.js";
|
|
9
9
|
import { Text } from "../general/typography.js";
|
|
10
10
|
import { CenteredShell } from "./centered-shell.js";
|
|
11
|
+
import { dateTimeFormat, numberFormat } from "../../lib/intl-cache.js";
|
|
11
12
|
const STATUS_META = {
|
|
12
13
|
400: { icon: TriangleAlert, tone: "warning" },
|
|
13
14
|
403: { icon: ShieldAlert, tone: "warning" },
|
|
@@ -37,7 +38,7 @@ function formatMaintenanceWindow(maintenance, locale) {
|
|
|
37
38
|
};
|
|
38
39
|
const start = new Date(maintenance.start);
|
|
39
40
|
if (Number.isNaN(start.getTime())) return maintenance.start;
|
|
40
|
-
const formatter =
|
|
41
|
+
const formatter = dateTimeFormat(locale, options);
|
|
41
42
|
if (maintenance.end === void 0) return formatter.format(start);
|
|
42
43
|
const end = new Date(maintenance.end);
|
|
43
44
|
if (Number.isNaN(end.getTime())) return formatter.format(start);
|
|
@@ -72,7 +73,7 @@ const ErrorSurface = React.forwardRef(
|
|
|
72
73
|
const maintenanceProgress = maintenance?.progress;
|
|
73
74
|
const hasProgress = typeof maintenanceProgress === "number" && Number.isFinite(maintenanceProgress);
|
|
74
75
|
const progressLabel = hasProgress ? t("layout.errorSurface.maintenanceProgress", {
|
|
75
|
-
percent:
|
|
76
|
+
percent: numberFormat(locale, {
|
|
76
77
|
style: "percent",
|
|
77
78
|
maximumFractionDigits: 0
|
|
78
79
|
}).format(Math.max(0, Math.min(100, maintenanceProgress)) / 100)
|
|
@@ -5,6 +5,7 @@ import { ToggleButton } from "react-aria-components";
|
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
8
|
+
import { numberFormat } from "../../lib/intl-cache.js";
|
|
8
9
|
const toggleVariants = cva("ui-toggle", {
|
|
9
10
|
variants: {
|
|
10
11
|
variant: {
|
|
@@ -33,7 +34,7 @@ function useCounterPill({
|
|
|
33
34
|
const visible = count != null && (count !== 0 || showZero);
|
|
34
35
|
const formatted = React.useMemo(() => {
|
|
35
36
|
if (count == null) return "";
|
|
36
|
-
const format =
|
|
37
|
+
const format = numberFormat(locale);
|
|
37
38
|
return count > overflowCount ? `${format.format(overflowCount)}+` : format.format(count);
|
|
38
39
|
}, [count, locale, overflowCount]);
|
|
39
40
|
if (!visible) {
|
package/dist/i18n/translate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import en from "./messages/en.json" with { type: "json" };
|
|
2
2
|
import ja from "./messages/ja.json" with { type: "json" };
|
|
3
3
|
import vi from "./messages/vi.json" with { type: "json" };
|
|
4
|
+
import { numberFormat, pluralRules } from "../lib/intl-cache.js";
|
|
4
5
|
const MESSAGE_CATALOG = {
|
|
5
6
|
vi,
|
|
6
7
|
en,
|
|
@@ -56,7 +57,7 @@ function selectPlural(value, locale, params) {
|
|
|
56
57
|
if (count === void 0) {
|
|
57
58
|
return value.other ?? Object.values(value)[0] ?? "";
|
|
58
59
|
}
|
|
59
|
-
const category =
|
|
60
|
+
const category = pluralRules(locale).select(count);
|
|
60
61
|
return value[category] ?? value.other ?? Object.values(value)[0] ?? "";
|
|
61
62
|
}
|
|
62
63
|
function interpolate(template, locale, params) {
|
|
@@ -64,7 +65,7 @@ function interpolate(template, locale, params) {
|
|
|
64
65
|
return Object.entries(params).reduce(
|
|
65
66
|
(text, [key, value]) => text.replaceAll(
|
|
66
67
|
`{${key}}`,
|
|
67
|
-
typeof value === "number" ?
|
|
68
|
+
typeof value === "number" ? numberFormat(locale).format(value) : String(value)
|
|
68
69
|
),
|
|
69
70
|
template
|
|
70
71
|
);
|
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
startOfYear
|
|
10
10
|
} from "date-fns";
|
|
11
11
|
import { parseDateInput, toIsoDate } from "./parse.js";
|
|
12
|
+
import { dateTimeFormat } from "../intl-cache.js";
|
|
12
13
|
function formatPickerDate(date, format, locale, withTime = false) {
|
|
13
14
|
if (!date || !isValid(date)) return "";
|
|
14
15
|
if (typeof format === "function") return format(date);
|
|
15
|
-
if (typeof format === "object") return
|
|
16
|
+
if (typeof format === "object") return dateTimeFormat(locale, format).format(date);
|
|
16
17
|
if (typeof format === "string") return formatDate(date, format);
|
|
17
18
|
return withTime ? formatDate(date, "yyyy-MM-dd HH:mm") : toIsoDate(date);
|
|
18
19
|
}
|
package/dist/lib/format.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getSyncedLocale, translateCurrent } from "../i18n/translate.js";
|
|
2
|
+
import { numberFormat } from "./intl-cache.js";
|
|
2
3
|
function formatBytes(n, locale = getSyncedLocale()) {
|
|
3
4
|
if (n == null) return "\u2014";
|
|
4
|
-
const num = (digits, scaled) =>
|
|
5
|
+
const num = (digits, scaled) => numberFormat(locale, {
|
|
5
6
|
minimumFractionDigits: digits,
|
|
6
7
|
maximumFractionDigits: digits
|
|
7
8
|
}).format(scaled);
|
|
@@ -12,7 +13,7 @@ function formatBytes(n, locale = getSyncedLocale()) {
|
|
|
12
13
|
}
|
|
13
14
|
function formatCurrency(amountMinor, currency, locale = getSyncedLocale()) {
|
|
14
15
|
if (amountMinor == null || !currency) return "\u2014";
|
|
15
|
-
const formatter =
|
|
16
|
+
const formatter = numberFormat(locale, { style: "currency", currency });
|
|
16
17
|
const minorUnitDigits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
|
|
17
18
|
const major = amountMinor / Math.pow(10, minorUnitDigits);
|
|
18
19
|
return formatter.format(major);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memoised `Intl` formatters, keyed by locale + options.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS, AS A MEASUREMENT (gh#557). A consumer ported a legacy CakePHP grid that keeps
|
|
5
|
+
* every row in the DOM — 8,262 rows, no virtualisation available — and measured 27-30s against
|
|
6
|
+
* 2.3s for the same markup built from native `<input>` / `<select>`. They had already ruled out
|
|
7
|
+
* the obvious suspect: deleting `Popover` / `PopoverContent` from `DatePicker` entirely changed
|
|
8
|
+
* nothing.
|
|
9
|
+
*
|
|
10
|
+
* Rendered here through `react-dom/server`, 2,000 rows at a time, one CLOSED `DatePicker` costs
|
|
11
|
+
* **412.6us** against **4.2us** for `<input type="date">` — 98x. Counting constructor calls during
|
|
12
|
+
* that render, each closed instance built:
|
|
13
|
+
*
|
|
14
|
+
* 12 x new Intl.DateTimeFormat
|
|
15
|
+
* 1 x new Intl.NumberFormat
|
|
16
|
+
*
|
|
17
|
+
* Constructing an `Intl` formatter is one of the most expensive things a JS engine does: it
|
|
18
|
+
* resolves locale data and compiles a pattern. Routing those same constructions through this
|
|
19
|
+
* cache, changing nothing else, took the instance from **412.6us to 178us — a 57% cut**. For the
|
|
20
|
+
* reporter's grid that is roughly 99,000 formatter constructions that no longer happen.
|
|
21
|
+
*
|
|
22
|
+
* WHY A SHARED MODULE AND NOT A `useMemo` AT EACH CALL SITE. `useMemo` memoises per INSTANCE, and
|
|
23
|
+
* the instance is exactly what there are 8,262 of. Two rows formatting dates in the same locale
|
|
24
|
+
* want the same formatter object, and nothing below module scope can give them one.
|
|
25
|
+
*
|
|
26
|
+
* SAFETY. `Intl` formatters are immutable and stateless — `format()` mutates nothing — so a single
|
|
27
|
+
* instance is safely shared across every caller and every React tree, concurrent SSR included.
|
|
28
|
+
*/
|
|
29
|
+
export declare function dateTimeFormat(locale: string, options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
30
|
+
export declare function numberFormat(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
31
|
+
export declare function pluralRules(locale: string, options?: Intl.PluralRulesOptions): Intl.PluralRules;
|
|
32
|
+
export declare function listFormat(locale: string, options?: Intl.ListFormatOptions): Intl.ListFormat;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const MAX_ENTRIES = 256;
|
|
2
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3
|
+
function memoise(kind, locale, options, make) {
|
|
4
|
+
const key = `${kind} ${locale} ${options === void 0 ? "" : JSON.stringify(options)}`;
|
|
5
|
+
const hit = cache.get(key);
|
|
6
|
+
if (hit !== void 0) return hit;
|
|
7
|
+
const made = make();
|
|
8
|
+
if (cache.size >= MAX_ENTRIES) cache.delete(cache.keys().next().value);
|
|
9
|
+
cache.set(key, made);
|
|
10
|
+
return made;
|
|
11
|
+
}
|
|
12
|
+
function dateTimeFormat(locale, options) {
|
|
13
|
+
return memoise("dtf", locale, options, () => new Intl.DateTimeFormat(locale, options));
|
|
14
|
+
}
|
|
15
|
+
function numberFormat(locale, options) {
|
|
16
|
+
return memoise("nf", locale, options, () => new Intl.NumberFormat(locale, options));
|
|
17
|
+
}
|
|
18
|
+
function pluralRules(locale, options) {
|
|
19
|
+
return memoise("pr", locale, options, () => new Intl.PluralRules(locale, options));
|
|
20
|
+
}
|
|
21
|
+
function listFormat(locale, options) {
|
|
22
|
+
return memoise("lf", locale, options, () => new Intl.ListFormat(locale, options));
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
dateTimeFormat,
|
|
26
|
+
listFormat,
|
|
27
|
+
numberFormat,
|
|
28
|
+
pluralRules
|
|
29
|
+
};
|
|
@@ -778,6 +778,17 @@ export type PickerDateFormatProp = string | Intl.DateTimeFormatOptions | ((date:
|
|
|
778
778
|
* `value`/`defaultValue`/`onValueChange` rather than antd's `onChange`.
|
|
779
779
|
*/
|
|
780
780
|
export type DatePickerBaseProp = FieldA11yProps & PickerChromeProp & {
|
|
781
|
+
/**
|
|
782
|
+
* The accessible NAME of the button that opens the calendar, for a screen where more than one
|
|
783
|
+
* date field exists. Default 「カレンダーを開く」 / "Open calendar" — correct for one picker on a
|
|
784
|
+
* page, and useless for three: a reader hears the same sentence three times with nothing to
|
|
785
|
+
* say which field each button belongs to (WCAG 2.2 SC 2.4.6, gh#551).
|
|
786
|
+
*
|
|
787
|
+
* Same axis, same shape, same reason as Select's `clearLabel`: name the control after the
|
|
788
|
+
* FIELD it serves — `開始日のカレンダーを開く`. The default is unchanged, so nothing moves until
|
|
789
|
+
* a caller says something.
|
|
790
|
+
*/
|
|
791
|
+
triggerLabel?: string;
|
|
781
792
|
/** Display format; native submission remains ISO. */
|
|
782
793
|
format?: PickerDateFormatProp;
|
|
783
794
|
/** Parser for a custom display function or Intl era display; ISO always remains accepted. */
|
|
@@ -83,6 +83,25 @@ export type SkeletonRowsProp = {
|
|
|
83
83
|
rows?: number;
|
|
84
84
|
columns?: number;
|
|
85
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* The skeleton of a `Form columns={N}` — label + control PAIRS on the same grid the form uses,
|
|
88
|
+
* not the flat line list `SkeletonRows` draws (gh#552).
|
|
89
|
+
*
|
|
90
|
+
* Why it is its own component rather than `SkeletonRows` with better defaults: `SkeletonRows` has
|
|
91
|
+
* no idea a form field is two stacked things, so a consumer approximating one got the column count
|
|
92
|
+
* right and the inside of every cell wrong. Worse, "close enough" is a thing that DRIFTS — change
|
|
93
|
+
* the form's `columns` and forget the skeleton and the layout jumps again on load, with nothing
|
|
94
|
+
* red to say so. Sharing `ResponsiveGrid` is what stops that: one `columns` value, one ladder.
|
|
95
|
+
*/
|
|
96
|
+
export type SkeletonFormProp = {
|
|
97
|
+
/** Columns of the form this stands in for — passed straight to `ResponsiveGrid`. */
|
|
98
|
+
columns?: number;
|
|
99
|
+
/** How many label+control pairs to draw. */
|
|
100
|
+
fields?: number;
|
|
101
|
+
/** Shimmer, matching the rest of the family. */
|
|
102
|
+
active?: boolean;
|
|
103
|
+
className?: string;
|
|
104
|
+
};
|
|
86
105
|
/**
|
|
87
106
|
* A skeleton line's MEASURE. `number` is read as pixels, matching antd's
|
|
88
107
|
* `SkeletonParagraphProps["width"]` (components/skeleton/Paragraph.tsx); a string is any CSS length
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -1955,6 +1955,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
1955
1955
|
readonly file: "components/feedback.prop.ts";
|
|
1956
1956
|
readonly vocabulary: readonly ["SizeProp", "ShapeProp", "ClassNameProp"];
|
|
1957
1957
|
};
|
|
1958
|
+
readonly SkeletonFormProp: {
|
|
1959
|
+
readonly group: "feedback";
|
|
1960
|
+
readonly file: "components/feedback.prop.ts";
|
|
1961
|
+
readonly vocabulary: readonly ["ClassNameProp"];
|
|
1962
|
+
};
|
|
1958
1963
|
readonly SkeletonInputProp: {
|
|
1959
1964
|
readonly group: "feedback";
|
|
1960
1965
|
readonly file: "components/feedback.prop.ts";
|
package/dist/props/registry.js
CHANGED
|
@@ -2254,6 +2254,11 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2254
2254
|
file: "components/feedback.prop.ts",
|
|
2255
2255
|
vocabulary: ["SizeProp", "ShapeProp", "ClassNameProp"]
|
|
2256
2256
|
},
|
|
2257
|
+
SkeletonFormProp: {
|
|
2258
|
+
group: "feedback",
|
|
2259
|
+
file: "components/feedback.prop.ts",
|
|
2260
|
+
vocabulary: ["ClassNameProp"]
|
|
2261
|
+
},
|
|
2257
2262
|
SkeletonInputProp: {
|
|
2258
2263
|
group: "feedback",
|
|
2259
2264
|
file: "components/feedback.prop.ts",
|
|
@@ -216,6 +216,28 @@
|
|
|
216
216
|
gap: var(--skeleton-cell-gap);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
.ui-skeleton-form-field {
|
|
220
|
+
display: grid;
|
|
221
|
+
grid-template-columns: minmax(0, 1fr);
|
|
222
|
+
gap: var(--space-stack-xs);
|
|
223
|
+
min-inline-size: 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.ui-skeleton-form-label {
|
|
227
|
+
block-size: var(--skeleton-caption-height);
|
|
228
|
+
inline-size: var(--skeleton-label-width);
|
|
229
|
+
max-inline-size: 100%;
|
|
230
|
+
border-radius: var(--skeleton-radius);
|
|
231
|
+
background: var(--skeleton-background, hsl(var(--muted)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.ui-skeleton-form-control {
|
|
235
|
+
block-size: var(--control-height);
|
|
236
|
+
inline-size: 100%;
|
|
237
|
+
border-radius: var(--skeleton-element-radius);
|
|
238
|
+
background: var(--skeleton-background, hsl(var(--muted)));
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
.ui-skeleton-table {
|
|
220
242
|
overflow: hidden;
|
|
221
243
|
border: 1px solid hsl(var(--border));
|
|
@@ -329,6 +329,10 @@
|
|
|
329
329
|
padding: var(--card-space-body-y) var(--card-space-inset);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
[data-slot="card-content"][data-flush] [data-slot="tabs-panel"] {
|
|
333
|
+
padding-inline: 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
332
336
|
[data-slot="card-footer"] {
|
|
333
337
|
display: flex;
|
|
334
338
|
flex-wrap: wrap;
|
|
@@ -39,7 +39,17 @@
|
|
|
39
39
|
|
|
40
40
|
@container responsive-grid (min-width: 40rem) {
|
|
41
41
|
.ui-responsive-grid > .ui-form-field {
|
|
42
|
-
grid-column: span var(--form-field-col-span, 1);
|
|
42
|
+
grid-column: span min(var(--responsive-grid-sm, 1), var(--form-field-col-span, 1));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
@container responsive-grid (min-width: 48rem) {
|
|
46
|
+
.ui-responsive-grid > .ui-form-field {
|
|
47
|
+
grid-column: span min(var(--responsive-grid-md, 1), var(--form-field-col-span, 1));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
@container responsive-grid (min-width: 64rem) {
|
|
51
|
+
.ui-responsive-grid > .ui-form-field {
|
|
52
|
+
grid-column: span min(var(--responsive-grid-lg, 1), var(--form-field-col-span, 1));
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
package/dist/tokens/axes.css
CHANGED
|
@@ -108,9 +108,10 @@
|
|
|
108
108
|
transparent 70%
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
-
--focus-outline:
|
|
111
|
+
--focus-outline: 1;
|
|
112
112
|
--focus-outline-weight: var(--stroke-hairline);
|
|
113
113
|
--focus-outline-color: var(--focus-ring-color, var(--ring));
|
|
114
|
+
|
|
114
115
|
--control-outline-width: calc(var(--stroke-md) * var(--focus-outline));
|
|
115
116
|
--focus-outline-offset: 0px;
|
|
116
117
|
|
|
@@ -125,7 +126,8 @@
|
|
|
125
126
|
--focus-ring-glow-color: var(--control-outline);
|
|
126
127
|
--focus-ring-glow-alpha: var(--control-outline-alpha);
|
|
127
128
|
|
|
128
|
-
--focus-field-shadow:
|
|
129
|
+
--focus-field-shadow: 0 0 0 var(--focus-ring-glow-width)
|
|
130
|
+
hsl(var(--focus-ring-glow-color) / var(--focus-ring-glow-alpha));
|
|
129
131
|
|
|
130
132
|
--focus-ring-offset: var(--focus-outline-offset);
|
|
131
133
|
|
|
@@ -255,7 +257,6 @@
|
|
|
255
257
|
--activity-interval: var(--duration-loop);
|
|
256
258
|
|
|
257
259
|
--activity-stagger-step: 160ms;
|
|
258
|
-
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
.ui-scale-fixed {
|
|
@@ -306,7 +307,6 @@
|
|
|
306
307
|
--space-chrome-y: var(--space-4);
|
|
307
308
|
--space-chrome-gap: var(--space-2);
|
|
308
309
|
--field-label-gap: var(--space-2);
|
|
309
|
-
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
.dark,
|