@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.
@@ -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 = new Intl.DateTimeFormat(locale, {
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(() => new Intl.NumberFormat(locale, options), [locale, options]);
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
- periodPage(viewYear - pageStep)[periodCells - 1],
301
- minimum,
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 periodPanel = /* @__PURE__ */ jsxs("div", { className: "ui-month-picker-panel", children: [
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: new Intl.NumberFormat(locale, { useGrouping: false }).format(viewYear) }),
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 dayPanel = range ? /* @__PURE__ */ jsx(
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 ? periodPanel : dayPanel,
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
- () => new Intl.NumberFormat(locale, {
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 ? `${new Intl.NumberFormat(locale).format(overflowCount)}+` : count != null ? new Intl.NumberFormat(locale).format(count) : "";
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 = new Intl.DateTimeFormat(locale, options);
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: new Intl.NumberFormat(locale, {
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 = new Intl.NumberFormat(locale);
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) {
@@ -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 = new Intl.PluralRules(locale).select(count);
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" ? new Intl.NumberFormat(locale).format(value) : String(value)
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 new Intl.DateTimeFormat(locale, format).format(date);
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
  }
@@ -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) => new Intl.NumberFormat(locale, {
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 = new Intl.NumberFormat(locale, { style: "currency", currency });
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
@@ -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";
@@ -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
 
@@ -66,3 +66,8 @@
66
66
  --focus-field-shadow: 0 0 0 var(--focus-ring-glow-width)
67
67
  hsl(var(--focus-ring-glow-color) / var(--focus-ring-glow-alpha));
68
68
  }
69
+
70
+ :root[data-focus-outline="off"] {
71
+ --focus-outline: 0;
72
+ --focus-field-shadow: none;
73
+ }
@@ -108,9 +108,10 @@
108
108
  transparent 70%
109
109
  );
110
110
 
111
- --focus-outline: 0;
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: none;
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,