@squadbase/vantage 0.6.2-dev1.48b34e1 → 0.6.2-dev1.56da87f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
  2. package/dist/chunk-3RJEOP3J.js.map +1 -0
  3. package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
  4. package/dist/chunk-77CISBMG.js.map +1 -0
  5. package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
  6. package/dist/chunk-EG4PX726.js.map +1 -0
  7. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  8. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  9. package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
  10. package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
  11. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  12. package/dist/chunk-ROHNXP7J.js.map +1 -0
  13. package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
  14. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  15. package/dist/components/index.d.ts +49 -29
  16. package/dist/components/index.js +5 -5
  17. package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
  18. package/dist/doc/index.d.ts +14 -14
  19. package/dist/doc/index.js +198 -84
  20. package/dist/doc/index.js.map +1 -1
  21. package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
  22. package/dist/slide/index.d.ts +2 -2
  23. package/dist/slide/index.js +95 -44
  24. package/dist/slide/index.js.map +1 -1
  25. package/dist/table/index.js +6 -6
  26. package/dist/ui/index.d.ts +1 -1
  27. package/dist/ui/index.js +5 -5
  28. package/package.json +2 -2
  29. package/registry/ui/app-shell.tsx +138 -1
  30. package/registry/ui/area-chart.tsx +344 -85
  31. package/registry/ui/attention-list.tsx +177 -15
  32. package/registry/ui/bar-chart.tsx +341 -85
  33. package/registry/ui/bar-list.tsx +332 -77
  34. package/registry/ui/box-plot-chart.tsx +405 -97
  35. package/registry/ui/category-bar.tsx +248 -40
  36. package/registry/ui/chart-frame.tsx +257 -46
  37. package/registry/ui/data-table.tsx +229 -37
  38. package/registry/ui/date-range-picker.tsx +190 -36
  39. package/registry/ui/delta.tsx +171 -12
  40. package/registry/ui/donut-chart.tsx +339 -84
  41. package/registry/ui/echart.tsx +142 -4
  42. package/registry/ui/filter-bar.tsx +179 -20
  43. package/registry/ui/funnel-steps.tsx +354 -80
  44. package/registry/ui/heatmap.tsx +293 -52
  45. package/registry/ui/histogram-chart.tsx +395 -97
  46. package/registry/ui/kpi-card.tsx +245 -44
  47. package/registry/ui/legend.tsx +330 -76
  48. package/registry/ui/line-chart.tsx +344 -85
  49. package/registry/ui/multi-select.tsx +159 -9
  50. package/registry/ui/page-shell.tsx +141 -3
  51. package/registry/ui/paired-chart.tsx +349 -90
  52. package/registry/ui/progress-circle.tsx +205 -39
  53. package/registry/ui/sample-data.tsx +153 -9
  54. package/registry/ui/scatter-chart.tsx +391 -100
  55. package/registry/ui/searchable-select.tsx +145 -7
  56. package/registry/ui/states.tsx +170 -13
  57. package/registry/ui/status-badge.tsx +193 -28
  58. package/registry/ui/tracker.tsx +205 -39
  59. package/registry/ui/trend-indicator.tsx +149 -6
  60. package/registry/ui/updated-at.tsx +170 -16
  61. package/registry/ui/waterfall-chart.tsx +415 -102
  62. package/skills/vantage-add-feature/SKILL.md +5 -0
  63. package/templates/AGENTS.md +3 -2
  64. package/templates/agents/api.md +7 -0
  65. package/templates/agents/page.md +36 -0
  66. package/dist/chunk-2N72FKLB.js.map +0 -1
  67. package/dist/chunk-57GHR5LN.js.map +0 -1
  68. package/dist/chunk-6OTTGAPO.js.map +0 -1
  69. package/dist/chunk-JXFCSF3K.js.map +0 -1
  70. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  71. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -1,11 +1,12 @@
1
+ import { FALLBACK_LOCALE, resolveLocale, uiLanguage, defaultCurrency } from './chunk-ROHNXP7J.js';
2
+
1
3
  // @squadbase/vantage — generated build. Do not edit.
2
4
 
3
5
  // ../squadkit/registry/lib/format.ts
4
- var DEFAULT_LOCALE = "ja-JP";
6
+ var DEFAULT_LOCALE = FALLBACK_LOCALE;
5
7
  var EMPTY_VALUE = "\u2014";
6
8
  var MINUS_SIGN = "\u2212";
7
9
  var PERCENT_DIGITS = 1;
8
- var DEFAULT_CURRENCY = "JPY";
9
10
  var MULTIPLE_SUFFIX = "\xD7";
10
11
  var POINTS_SUFFIX = "pt";
11
12
  function normalize(format) {
@@ -32,8 +33,9 @@ function formatParts(locale, options, value) {
32
33
  return joinParts(new Intl.NumberFormat(locale, options).formatToParts(value));
33
34
  }
34
35
  var joinParts = (parts) => parts.map((p) => p.type === "currency" ? p.value.normalize("NFKC") : p.type === "minusSign" ? MINUS_SIGN : p.value).join("");
35
- function formatValue(value, format, locale = DEFAULT_LOCALE, options) {
36
+ function formatValue(value, format, locale, options) {
36
37
  if (value === null || value === void 0 || Number.isNaN(value)) return EMPTY_VALUE;
38
+ locale = resolveLocale(locale);
37
39
  const { type, digits, currency, notation, unit, divisor, durationUnit } = normalize(format);
38
40
  if (divisor !== void 0 && Number.isFinite(divisor) && divisor !== 0) value = value / divisor;
39
41
  const signDisplay = options?.signDisplay ?? "auto";
@@ -50,9 +52,11 @@ function formatValue(value, format, locale = DEFAULT_LOCALE, options) {
50
52
  locale,
51
53
  {
52
54
  style: "currency",
53
- currency: currency ?? DEFAULT_CURRENCY,
55
+ currency: currency ?? defaultCurrency(locale),
54
56
  notation: notation ?? "compact",
55
57
  ...digitOptions(digits, notation === "standard" ? void 0 : 1, sig),
58
+ // Compact amounts drop the currency's own minimum fraction digits outside ja ("$50M", not "$50.0M").
59
+ ...notation !== "standard" && digits === void 0 && sig === void 0 && uiLanguage(locale) !== "ja" ? { minimumFractionDigits: 0 } : {},
56
60
  signDisplay
57
61
  },
58
62
  value
@@ -67,8 +71,12 @@ function formatValue(value, format, locale = DEFAULT_LOCALE, options) {
67
71
  }
68
72
  }
69
73
  var SECONDS_PER = { second: 1, minute: 60, hour: 3600, day: 86400 };
74
+ var DURATION_WORDS = {
75
+ ja: { day: "\u65E5", hour: "\u6642\u9593", minute: "\u5206", second: "\u79D2", sep: "" },
76
+ en: { day: "d", hour: "h", minute: "min", second: "s", sep: " " }
77
+ };
70
78
  function durationWords(locale) {
71
- return locale.toLowerCase().startsWith("ja") ? { day: "\u65E5", hour: "\u6642\u9593", minute: "\u5206", second: "\u79D2", sep: "" } : { day: "d", hour: "h", minute: "min", second: "s", sep: " " };
79
+ return DURATION_WORDS[uiLanguage(locale)];
72
80
  }
73
81
  function formatDuration(value, unit, locale, digits, signDisplay) {
74
82
  const w = durationWords(locale);
@@ -91,21 +99,22 @@ function formatDuration(value, unit, locale, digits, signDisplay) {
91
99
  const sign = zero ? "" : value < 0 ? MINUS_SIGN : signDisplay === "exceptZero" ? "+" : "";
92
100
  return `${sign}${body}`;
93
101
  }
94
- function formatDelta(value, format, locale = DEFAULT_LOCALE) {
102
+ function formatDelta(value, format, locale) {
95
103
  return formatValue(value, format, locale, { signDisplay: "exceptZero" });
96
104
  }
97
- function formatPoints(difference, locale = DEFAULT_LOCALE, options) {
105
+ function formatPoints(difference, locale, options) {
98
106
  if (difference === null || difference === void 0 || Number.isNaN(difference)) return EMPTY_VALUE;
99
107
  const digits = options?.digits ?? PERCENT_DIGITS;
100
108
  const number = formatParts(
101
- locale,
109
+ resolveLocale(locale),
102
110
  { minimumFractionDigits: digits, maximumFractionDigits: digits, signDisplay: options?.signDisplay ?? "exceptZero" },
103
111
  difference * 100
104
112
  );
105
113
  return `${number}${POINTS_SUFFIX}`;
106
114
  }
107
- function toValueFormatter(format, locale = DEFAULT_LOCALE) {
108
- return (value) => formatValue(value, format, locale);
115
+ function toValueFormatter(format, locale) {
116
+ const resolved = resolveLocale(locale);
117
+ return (value) => formatValue(value, format, resolved);
109
118
  }
110
119
  var DATE_STYLE_OPTIONS = {
111
120
  date: { year: "numeric", month: "2-digit", day: "2-digit" },
@@ -115,18 +124,19 @@ var DATE_STYLE_OPTIONS = {
115
124
  year: { year: "numeric" }
116
125
  };
117
126
  var CALENDAR_DATE = /^\d{4}-\d{2}(-\d{2})?$/;
118
- function formatDate(value, style = "date", locale = DEFAULT_LOCALE, options) {
127
+ function formatDate(value, style = "date", locale, options) {
119
128
  if (value === null || value === void 0 || value === "") return EMPTY_VALUE;
120
129
  const calendar = typeof value === "string" && CALENDAR_DATE.test(value);
121
130
  const date = value instanceof Date ? value : new Date(value);
122
131
  if (!Number.isFinite(date.getTime())) return EMPTY_VALUE;
123
- return new Intl.DateTimeFormat(locale, {
132
+ return new Intl.DateTimeFormat(resolveLocale(locale), {
124
133
  ...DATE_STYLE_OPTIONS[style],
125
134
  timeZone: calendar ? "UTC" : options?.timeZone
126
135
  }).format(date);
127
136
  }
137
+ var RANGE_SEPARATOR = { ja: "\u301C", en: " \u2013 " };
128
138
  function formatDateRange(start, end, options) {
129
- const locale = options?.locale ?? DEFAULT_LOCALE;
139
+ const locale = resolveLocale(options?.locale);
130
140
  const parse = (value) => {
131
141
  if (value === null || value === void 0 || value === "") return null;
132
142
  const calendar = typeof value === "string" && CALENDAR_DATE.test(value);
@@ -148,16 +158,17 @@ function formatDateRange(start, end, options) {
148
158
  const sameYear = year(a) === year(b);
149
159
  const endFull = full(b, true);
150
160
  if (startText === endFull) return startText;
151
- const separator = locale.toLowerCase().startsWith("ja") ? "\u301C" : " \u2013 ";
161
+ const separator = RANGE_SEPARATOR[uiLanguage(locale)];
152
162
  return `${startText}${separator}${sameYear ? full(b, false) : endFull}`;
153
163
  }
154
- function formatDatesInText(text, locale = DEFAULT_LOCALE) {
164
+ function formatDatesInText(text, locale) {
165
+ const resolved = resolveLocale(locale);
155
166
  return text.replace(
156
167
  /(?<![\d-])(\d{4})-(0[1-9]|1[0-2])(-(0[1-9]|[12]\d|3[01]))?(?![\d-])/g,
157
- (match, _y, _m, day) => formatDate(match, day ? "date" : "year-month", locale)
168
+ (match, _y, _m, day) => formatDate(match, day ? "date" : "year-month", resolved)
158
169
  );
159
170
  }
160
171
 
161
172
  export { DEFAULT_LOCALE, EMPTY_VALUE, MINUS_SIGN, formatDate, formatDateRange, formatDatesInText, formatDelta, formatKind, formatPoints, formatValue, toValueFormatter };
162
- //# sourceMappingURL=chunk-57GHR5LN.js.map
163
- //# sourceMappingURL=chunk-57GHR5LN.js.map
173
+ //# sourceMappingURL=chunk-3RJEOP3J.js.map
174
+ //# sourceMappingURL=chunk-3RJEOP3J.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../squadkit/registry/lib/format.ts"],"names":[],"mappings":"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","file":"chunk-3RJEOP3J.js","sourcesContent":["import { defaultCurrency, FALLBACK_LOCALE, resolveLocale, uiLanguage } from \"./locale\"\n\n/**\n * Locale-aware number formatting shared by KPI cards, tables and charts. All\n * shortening is delegated to `Intl.NumberFormat` (`notation: \"compact\"`); this\n * module never hard-codes unit thresholds or unit strings.\n *\n * Outputs below are for ja-JP (`<html lang=\"ja\">`); en-US reads `48.2M`, `$48.2M`, `3 h 20 min`, `10/20/2026`.\n *\n * | `format` | input | ja-JP output |\n * |--------------------------------------------|--------------|----------------|\n * | `\"compact\"` (default) | `48_200_000` | `4,820万` |\n * | `\"number\"` | `1234.567` | `1,234.57` |\n * | `\"currency\"` (JPY, compact) | `48_200_000` | `¥4,820万` |\n * | `{ type: \"currency\", notation: \"standard\" }` | `48_200_000` | `¥48,200,000` |\n * | `{ type: \"currency\", currency: \"USD\" }` | `1234.5` | `$1,234.5` |\n * | `\"percent\"` — pass a **ratio** | `0.124` | `12.4%` |\n * | `{ type: \"percent\", digits: 0 }` | `0.124` | `12%` |\n * | `\"multiple\"` | `1.5` | `1.5×` |\n * | `{ type: \"number\", unit: \"件\" }` | `48_210` | `48,210件` |\n * | `{ type: \"compact\", unit: \"人\" }` | `12_345` | `1.2万人` |\n * | `\"duration\"` — pass **seconds** | `12_000` | `3時間20分` |\n * | `{ type: \"duration\", durationUnit: \"minute\" }` | `2160` | `1.5日` |\n *\n * - `percent` takes a ratio (0.124 → 12.4%). Passing 12.4 renders 1,240%. The fraction\n * digits are **fixed**, never trimmed (`PERCENT_DIGITS` = 1: 0.14 → \"14.0%\", never \"14%\"\n * beside \"11.9%\"). Pass `digits` in the format to share another count across cards.\n * - A minus sign is always U+2212 (`MINUS_SIGN`, \"−1,234\" / \"−5.0%\"), never the hyphen-minus.\n * - `currency` defaults to JPY in ja (USD otherwise) and to compact notation (万・億 in ja, K・M・B in en); pass\n * `notation: \"standard\"` for every digit (tables, invoices). In ja-JP the yen\n * sign is the half-width `¥`.\n * - `unit` is a counter appended right after the number, with no space\n * (\"48,210件\", \"1.2万人\"), for `number` / `compact` / `currency`. It is\n * ignored for `percent` and `duration`, and for `multiple` it replaces the\n * default `×` (\"2.7倍\"). Do not write it into a `valueFormatter` by hand.\n * - Charts take the same `format` a KpiCard takes (`format=\"currency\"`).\n * `toValueFormatter(format)` turns it into a function for places that need\n * one (a `valueFormatter`, table cells).\n * - Missing values (null / undefined / NaN) render as `—` (`EMPTY_VALUE`).\n * - Without a `locale`, every function here uses the page's (`<html lang>`, else `\"en-US\"`).\n * - Dates go through `formatDate` (`Intl.DateTimeFormat`): `\"date\"` → `2026/10/20`,\n * `\"datetime\"` → `2026/10/20 14:30`, `\"year-month\"` → `2026年10月`, `\"month\"` → `10月`,\n * `\"year\"` → `2026年`.\n * - A period goes through `formatDateRange`: `2026/9/23〜10/20` (same year drops the end year),\n * `2025/12/20〜2026/1/18` across years; month and day are not zero-padded.\n */\n\n/**\n * The kind of number a value is. Pick by what the number means, not by how\n * long it is: `currency` for money, `percent` for a ratio, `multiple` for a\n * factor (\"1.5×\"), `duration` for a length of time (\"3時間20分\"), `number` for\n * exact counts in tables, `compact` (default) for large headline counts (万・億).\n */\nexport type FormatKind = \"number\" | \"compact\" | \"currency\" | \"percent\" | \"multiple\" | \"duration\"\n\n/** The unit a `duration` value is measured in. */\nexport type DurationUnit = \"second\" | \"minute\" | \"hour\" | \"day\"\n\n/**\n * How to display a number: a `FormatKind` alone, or an object for digits,\n * currency, notation and a counter. The same value is accepted by KpiCard,\n * Delta, the charts (`format`) and `formatValue`.\n *\n * @example\n * \"currency\" // ¥4,820万\n * { type: \"currency\", notation: \"standard\" } // ¥48,200,000\n * { type: \"percent\", digits: 1 } // 2.0%\n * { type: \"number\", unit: \"件\" } // 48,210件\n */\nexport type ValueFormat =\n | FormatKind\n | {\n type: FormatKind\n /** Fixed number of fraction digits. */\n digits?: number\n /** ISO 4217 code for `currency`. Default `\"JPY\"` when the locale is ja, else `\"USD\"`. */\n currency?: string\n /** `currency` only: `\"compact\"` (default, 万・億) or `\"standard\"` (every digit). */\n notation?: \"compact\" | \"standard\"\n /** Counter appended after the number, e.g. `\"件\"`, `\"人\"`. Ignored for `percent` and `duration`. For `multiple` it replaces the default `×` (`\"倍\"` → `2.7倍`). */\n unit?: string\n /** 値をこの数で割ってから整形する。例: `divisor: 100`(セント → ドル)。0 と非有限は無視。 */\n divisor?: number\n /** `duration` only: the unit the value is in. Default `\"second\"`. Reads as \"45秒\" / \"12分\" / \"3時間20分\" / \"1.5日\" (`digits` sets the fraction digits of days). */\n durationUnit?: DurationUnit\n }\n\n/**\n * The fallback locale (`\"en-US\"`). A function given no `locale` uses the page's (`<html lang>`) and falls back to\n * this one when there is none (SSR, no `lang`). Every function here takes a `locale` to override it.\n */\nexport const DEFAULT_LOCALE = FALLBACK_LOCALE\n\n/** Shown in place of a missing value (null / undefined / NaN). */\nexport const EMPTY_VALUE = \"—\"\n\n/** The minus sign every formatter writes (U+2212), so a negative never mixes with a hyphen-minus. */\nexport const MINUS_SIGN = \"\\u2212\"\n\n/** Fraction digits of a `percent` value (and of the points in `formatPoints`) when `digits` is not given. Fixed, never trimmed. */\nexport const PERCENT_DIGITS = 1\n\nconst MULTIPLE_SUFFIX = \"×\"\n/** Suffix for a difference between two rates, in percentage points. */\nconst POINTS_SUFFIX = \"pt\"\n\n/** Extra options for `formatValue`. */\nexport interface FormatValueOptions {\n /** `\"auto\"` (default) signs negatives only; `\"exceptZero\"` also writes `+` on positives (for differences). */\n signDisplay?: \"auto\" | \"exceptZero\"\n /** Round to this many significant digits (e.g. 3 for an estimate: 79,653,985 → 7,970万). */\n significantDigits?: number\n}\n\ntype NormalizedFormat = Exclude<ValueFormat, FormatKind>\n\nfunction normalize(format: ValueFormat | undefined): NormalizedFormat {\n if (format === undefined) return { type: \"compact\" }\n return typeof format === \"string\" ? { type: format } : format\n}\n\n/**\n * The kind of a format, whether it was given as a string or an object. Use it\n * to branch on the meaning of a metric (e.g. a `percent` metric compares in\n * points) without unpacking `ValueFormat` yourself.\n *\n * @param format - The format as a KpiCard / chart takes it. `undefined` means the default.\n * @returns The `FormatKind`; `\"compact\"` when `format` is omitted.\n *\n * @example\n * formatKind(\"percent\") // \"percent\"\n * formatKind({ type: \"currency\", notation: \"standard\" }) // \"currency\"\n * formatKind(undefined) // \"compact\"\n */\nexport function formatKind(format: ValueFormat | undefined): FormatKind {\n return normalize(format).type\n}\n\nfunction digitOptions(\n digits: number | undefined,\n defaultMax: number | undefined,\n significantDigits: number | undefined,\n): Intl.NumberFormatOptions {\n if (significantDigits !== undefined) return { maximumSignificantDigits: significantDigits }\n if (digits !== undefined) return { minimumFractionDigits: digits, maximumFractionDigits: digits }\n return defaultMax === undefined ? {} : { maximumFractionDigits: defaultMax }\n}\n\n/**\n * Format through Intl, letting Intl decide whether the number is large enough to\n * shorten. Shortened numbers keep digit grouping (\"7,965万\"), and the full-width\n * currency sign some locales emit (\"¥\") is folded to its half-width form (\"¥\").\n */\nfunction formatParts(locale: string, options: Intl.NumberFormatOptions, value: number): string {\n if (options.notation === \"compact\") {\n const compact = new Intl.NumberFormat(locale, {\n ...options,\n useGrouping: \"always\" as unknown as boolean,\n }).formatToParts(value)\n if (!compact.some((p) => p.type === \"compact\")) return formatParts(locale, { ...options, notation: \"standard\" }, value)\n return joinParts(compact)\n }\n return joinParts(new Intl.NumberFormat(locale, options).formatToParts(value))\n}\n\nconst joinParts = (parts: Intl.NumberFormatPart[]): string =>\n parts\n .map((p) => (p.type === \"currency\" ? p.value.normalize(\"NFKC\") : p.type === \"minusSign\" ? MINUS_SIGN : p.value))\n .join(\"\")\n\n/**\n * Format a number for display — KPI values, table cells, chart labels and\n * sentences built from data (titles, summaries). See the table at the top of\n * this module.\n *\n * @param value - The number. `null` / `undefined` / `NaN` render as `—`.\n * For `percent`, a ratio (0.124 means 12.4%).\n * @param format - How to display it. Default `\"compact\"`.\n * @param locale - BCP 47 locale. Default: the page's (`<html lang>`, else `\"en-US\"`).\n * @param options - `signDisplay` and `significantDigits` (rounding for estimates).\n * @returns The display string. In ja-JP the yen sign is the half-width `¥`.\n *\n * @example\n * formatValue(48_200_000, \"currency\") // \"¥4,820万\"\n * formatValue(48_200_000, { type: \"currency\", notation: \"standard\" }) // \"¥48,200,000\"\n * formatValue(0.021, \"percent\") // \"2.1%\"\n * formatValue(0.124, \"percent\", \"ja-JP\", { signDisplay: \"exceptZero\" }) // \"+12.4%\"\n * formatValue(79_653_985, \"currency\", \"ja-JP\", { significantDigits: 3 }) // \"¥7,970万\"\n * formatValue(48_210, { type: \"number\", unit: \"件\" }) // \"48,210件\"\n * formatValue(1.5, \"multiple\") // \"1.5×\"\n * formatValue(null) // \"—\"\n */\nexport function formatValue(\n value: number | null | undefined,\n format?: ValueFormat,\n locale?: string,\n options?: FormatValueOptions,\n): string {\n if (value === null || value === undefined || Number.isNaN(value)) return EMPTY_VALUE\n locale = resolveLocale(locale)\n const { type, digits, currency, notation, unit, divisor, durationUnit } = normalize(format)\n if (divisor !== undefined && Number.isFinite(divisor) && divisor !== 0) value = value / divisor\n const signDisplay = options?.signDisplay ?? \"auto\"\n const sig = options?.significantDigits\n const withUnit = (s: string) => (unit ? `${s}${unit}` : s)\n\n switch (type) {\n case \"number\":\n return withUnit(formatParts(locale, { ...digitOptions(digits, 2, sig), signDisplay }, value))\n case \"percent\":\n return formatParts(locale, { style: \"percent\", ...digitOptions(digits ?? PERCENT_DIGITS, undefined, sig), signDisplay }, value)\n case \"currency\":\n return withUnit(\n formatParts(\n locale,\n {\n style: \"currency\",\n currency: currency ?? defaultCurrency(locale),\n notation: notation ?? \"compact\",\n ...digitOptions(digits, notation === \"standard\" ? undefined : 1, sig),\n // Compact amounts drop the currency's own minimum fraction digits outside ja (\"$50M\", not \"$50.0M\").\n ...(notation !== \"standard\" && digits === undefined && sig === undefined && uiLanguage(locale) !== \"ja\"\n ? { minimumFractionDigits: 0 }\n : {}),\n signDisplay,\n },\n value,\n ),\n )\n case \"multiple\":\n return `${formatParts(locale, { ...digitOptions(digits, 2, sig), signDisplay }, value)}${unit || MULTIPLE_SUFFIX}`\n case \"compact\":\n return withUnit(formatParts(locale, { ...digitOptions(digits, 1, sig), signDisplay, notation: \"compact\" }, value))\n case \"duration\":\n return formatDuration(value, durationUnit ?? \"second\", locale, digits, signDisplay)\n }\n}\n\nconst SECONDS_PER: Record<DurationUnit, number> = { second: 1, minute: 60, hour: 3600, day: 86_400 }\n\n/**\n * Unit words and spacing: Japanese reads \"3時間20分\" (no space, like \"48,210件\"),\n * other locales \"3 h 20 min\".\n */\nconst DURATION_WORDS = {\n ja: { day: \"日\", hour: \"時間\", minute: \"分\", second: \"秒\", sep: \"\" },\n en: { day: \"d\", hour: \"h\", minute: \"min\", second: \"s\", sep: \" \" },\n}\n\nfunction durationWords(locale: string) {\n return DURATION_WORDS[uiLanguage(locale)]\n}\n\n/**\n * A length of time in the largest sensible unit: under a minute in seconds, under\n * an hour in minutes (+ seconds), under a day in hours + minutes, from a day on\n * in days with a decimal (\"1.5日\"). Rounded before splitting, so 59.6 minutes\n * never reads \"59分60秒\".\n */\nfunction formatDuration(\n value: number,\n unit: DurationUnit,\n locale: string,\n digits: number | undefined,\n signDisplay: \"auto\" | \"exceptZero\",\n): string {\n const w = durationWords(locale)\n const abs = Math.round(Math.abs(value) * SECONDS_PER[unit])\n const join = (...parts: Array<[number, string]>) =>\n parts\n .filter(([n]) => n !== 0)\n .map(([n, u]) => `${n}${w.sep}${u}`)\n .join(w.sep) || `0${w.sep}${parts.at(-1)?.[1] ?? w.second}`\n let body: string\n let zero = false\n if (abs >= 86_400) {\n const days = new Intl.NumberFormat(locale, { maximumFractionDigits: digits ?? 1, minimumFractionDigits: digits ?? 0 })\n body = `${days.format(abs / 86_400)}${w.sep}${w.day}`\n } else if (abs >= 3600) {\n const minutes = Math.round(abs / 60)\n body = join([Math.floor(minutes / 60), w.hour], [minutes % 60, w.minute])\n } else if (abs >= 60) {\n body = join([Math.floor(abs / 60), w.minute], [abs % 60, w.second])\n } else {\n zero = abs === 0\n body = `${abs}${w.sep}${w.second}`\n }\n const sign = zero ? \"\" : value < 0 ? MINUS_SIGN : signDisplay === \"exceptZero\" ? \"+\" : \"\"\n return `${sign}${body}`\n}\n\n/**\n * Like `formatValue`, but always signed (except zero) — for a difference\n * between two values (\"+¥530万\", \"−120件\"). Use `formatPoints` instead for a\n * difference between two rates.\n *\n * @param value - The difference (current − previous). Missing values render as `—`.\n * @param format - Format of the metric itself. Default `\"compact\"`.\n * @param locale - BCP 47 locale. Default: the page's (`<html lang>`, else `\"en-US\"`).\n * @returns The signed display string; zero has no sign.\n *\n * @example\n * formatDelta(5_300_000, \"currency\") // \"+¥530万\"\n * formatDelta(-120, { type: \"number\", unit: \"件\" }) // \"−120件\"\n * formatDelta(0.031, \"percent\") // \"+3.1%\"\n * formatDelta(0, \"currency\") // \"¥0\"\n */\nexport function formatDelta(value: number | null | undefined, format?: ValueFormat, locale?: string): string {\n return formatValue(value, format, locale, { signDisplay: \"exceptZero\" })\n}\n\n/**\n * A difference between two rates in percentage points, always signed:\n * `formatPoints(0.021 - 0.026)` → `\"−0.5pt\"`. Use it instead of a relative\n * change for rate metrics (churn, conversion), where \"−19.2%\" reads as a drop\n * of nineteen points. `signDisplay: \"never\"` drops the sign (\"あと 0.5pt\").\n *\n * @param difference - The difference of two **ratios** (0.021 − 0.026). Missing values render as `—`.\n * @param locale - BCP 47 locale. Default: the page's (`<html lang>`, else `\"en-US\"`).\n * @param options - `digits`: fixed fraction digits (default `PERCENT_DIGITS` = 1, so \"0.0pt\" / \"+0.5pt\"); `signDisplay`:\n * `\"exceptZero\"` (default) or `\"never\"` when the sentence already says the direction.\n * @returns The difference in points with the `pt` suffix.\n *\n * @example\n * formatPoints(0.021 - 0.026) // \"−0.5pt\"\n * formatPoints(0.031 - 0.026) // \"+0.5pt\"\n * formatPoints(0.02 - 0.015, \"ja-JP\", { signDisplay: \"never\" }) // \"0.5pt\"(「目標まであと 0.5pt」)\n */\nexport function formatPoints(\n difference: number | null | undefined,\n locale?: string,\n options?: { digits?: number; signDisplay?: \"exceptZero\" | \"never\" },\n): string {\n if (difference === null || difference === undefined || Number.isNaN(difference)) return EMPTY_VALUE\n const digits = options?.digits ?? PERCENT_DIGITS\n const number = formatParts(\n resolveLocale(locale),\n { minimumFractionDigits: digits, maximumFractionDigits: digits, signDisplay: options?.signDisplay ?? \"exceptZero\" },\n difference * 100,\n )\n return `${number}${POINTS_SUFFIX}`\n}\n\n/**\n * Binds a format and locale into a single-argument formatter, for a chart\n * `valueFormatter` or a table cell: `valueFormatter={toValueFormatter(\"currency\")}`.\n * Prefer passing `format` to the chart directly; this is for APIs that only\n * take a function.\n *\n * @param format - How to display each value. Default `\"compact\"`.\n * @param locale - BCP 47 locale. Default: the page's (`<html lang>`, else `\"en-US\"`), read when the formatter is made.\n * @returns `(value) => string`, rendering missing values as `—`.\n *\n * @example\n * const yen = toValueFormatter(\"currency\")\n * yen(48_200_000) // \"¥4,820万\"\n * yen(null) // \"—\"\n * const count = toValueFormatter({ type: \"number\", unit: \"件\" })\n * count(48_210) // \"48,210件\"(表のセル)\n */\nexport function toValueFormatter(\n format?: ValueFormat,\n locale?: string,\n): (value: number | null | undefined) => string {\n const resolved = resolveLocale(locale)\n return (value) => formatValue(value, format, resolved)\n}\n\n/**\n * 日付の書式。何を表す日付かで選ぶ: 日単位は `\"date\"`、時刻まで要るなら `\"datetime\"`、\n * 月次の集計は `\"year-month\"`(年をまたぐ軸)か `\"month\"`(同じ年の中の軸)、年次は `\"year\"`。\n */\nexport type DateStyle = \"date\" | \"datetime\" | \"month\" | \"year-month\" | \"year\"\n\n/** `formatDate` の追加オプション。 */\nexport interface FormatDateOptions {\n /**\n * IANA のタイムゾーン名(例: `\"Asia/Tokyo\"`)。既定は実行環境のタイムゾーン。\n * サーバーとブラウザで描画が食い違う(SSR)なら固定する。日付だけの文字列(`\"2026-10-20\"`)には効かず、常にその暦日のまま出す。\n */\n timeZone?: string\n}\n\nconst DATE_STYLE_OPTIONS: Record<DateStyle, Intl.DateTimeFormatOptions> = {\n date: { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" },\n datetime: { year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", hourCycle: \"h23\" },\n month: { month: \"short\" },\n \"year-month\": { year: \"numeric\", month: \"long\" },\n year: { year: \"numeric\" },\n}\n\n/** `\"2026-10-20\"` / `\"2026-10\"` はタイムゾーンを持たない暦日。UTC で解釈して UTC で出し、ずれを防ぐ。 */\nconst CALENDAR_DATE = /^\\d{4}-\\d{2}(-\\d{2})?$/\n\n/**\n * 日付を表示用の文字列にする。表のセル・タイトル・文に埋める日付に使い、`toLocaleString` や手組みの\n * `getMonth() + 1` は使わない。\n *\n * @param value - `Date`、エポックミリ秒、または日付文字列(`\"2026-10-20\"`、ISO 8601)。`null` / `undefined` / 解釈できない値は `—`。\n * @param style - 何を表す日付か。既定 `\"date\"`。\n * @param locale - BCP 47 ロケール。既定はページのロケール(`<html lang>`、無ければ `\"en-US\"`)。\n * @param options - `timeZone`。\n * @returns 表示文字列。\n *\n * @example\n * formatDate(\"2026-10-20\") // \"2026/10/20\"\n * formatDate(new Date(\"2026-10-20T05:30:00Z\"), \"datetime\", \"ja-JP\", { timeZone: \"Asia/Tokyo\" }) // \"2026/10/20 14:30\"\n * formatDate(\"2026-10-01\", \"year-month\") // \"2026年10月\"\n * formatDate(\"2026-10-01\", \"month\") // \"10月\"\n * formatDate(null) // \"—\"\n */\nexport function formatDate(\n value: Date | string | number | null | undefined,\n style: DateStyle = \"date\",\n locale?: string,\n options?: FormatDateOptions,\n): string {\n if (value === null || value === undefined || value === \"\") return EMPTY_VALUE\n const calendar = typeof value === \"string\" && CALENDAR_DATE.test(value)\n const date = value instanceof Date ? value : new Date(value)\n if (!Number.isFinite(date.getTime())) return EMPTY_VALUE\n return new Intl.DateTimeFormat(resolveLocale(locale), {\n ...DATE_STYLE_OPTIONS[style],\n timeZone: calendar ? \"UTC\" : options?.timeZone,\n }).format(date)\n}\n\nconst RANGE_SEPARATOR = { ja: \"〜\", en: \" – \" }\n\n/** `formatDateRange` のオプション。 */\nexport interface FormatDateRangeOptions extends FormatDateOptions {\n /** BCP 47 ロケール。既定はページのロケール(`<html lang>`、無ければ `\"en-US\"`)。 */\n locale?: string\n}\n\n/**\n * 期間を「2026/9/23〜10/20」の形にする。同じ年なら終了側の年を省き、年をまたぐときは両方に付ける。\n * 月日はゼロ詰めしない。区切りは日本語で「〜」、それ以外のロケールでは「 – 」。始まりと終わりが同じ日なら 1 日だけ出す。\n *\n * @param start - 期間の最初の日(`Date`、エポックミリ秒、日付文字列)。`\"2026-09-23\"` のような日付だけの文字列はその暦日のまま出す。\n * @param end - 期間の最後の日。\n * @param options - `locale` と `timeZone`。\n * @returns 表示文字列。どちらかが解釈できなければ `—`。\n *\n * @example\n * formatDateRange(\"2026-09-23\", \"2026-10-20\") // \"2026/9/23〜10/20\"\n * formatDateRange(\"2025-12-20\", \"2026-01-18\") // \"2025/12/20〜2026/1/18\"\n */\nexport function formatDateRange(\n start: Date | string | number | null | undefined,\n end: Date | string | number | null | undefined,\n options?: FormatDateRangeOptions,\n): string {\n const locale = resolveLocale(options?.locale)\n const parse = (value: Date | string | number | null | undefined) => {\n if (value === null || value === undefined || value === \"\") return null\n const calendar = typeof value === \"string\" && CALENDAR_DATE.test(value)\n const date = value instanceof Date ? value : new Date(value)\n if (!Number.isFinite(date.getTime())) return null\n return { date, timeZone: calendar ? \"UTC\" : options?.timeZone }\n }\n const a = parse(start)\n const b = parse(end)\n if (!a || !b) return EMPTY_VALUE\n const full = (x: NonNullable<typeof a>, withYear: boolean) =>\n new Intl.DateTimeFormat(locale, {\n ...(withYear ? { year: \"numeric\" } : {}),\n month: \"numeric\",\n day: \"numeric\",\n timeZone: x.timeZone,\n } as Intl.DateTimeFormatOptions).format(x.date)\n const year = (x: NonNullable<typeof a>) =>\n new Intl.DateTimeFormat(\"en-US\", { year: \"numeric\", timeZone: x.timeZone }).format(x.date)\n const startText = full(a, true)\n const sameYear = year(a) === year(b)\n const endFull = full(b, true)\n if (startText === endFull) return startText\n const separator = RANGE_SEPARATOR[uiLanguage(locale)]\n return `${startText}${separator}${sameYear ? full(b, false) : endFull}`\n}\n\n/**\n * 文中の ISO の年月・日付(`2023-04`、`2026-10-20`)を `formatDate` の書式に置き換える。期間や出典の注記のように、\n * 利用者が文字列で渡す文に日付が混ざるときの表示用(`\"2023-04 以降\"` → `\"2023年4月 以降\"`)。日付でない数字は触らない。\n *\n * @param text - 日付を含みうる文。\n * @param locale - BCP 47 ロケール。既定はページのロケール(`<html lang>`、無ければ `\"en-US\"`)。\n * @returns ISO の年月は `\"year-month\"`、年月日は `\"date\"` で整形した文。\n */\nexport function formatDatesInText(text: string, locale?: string): string {\n const resolved = resolveLocale(locale)\n return text.replace(/(?<![\\d-])(\\d{4})-(0[1-9]|1[0-2])(-(0[1-9]|[12]\\d|3[01]))?(?![\\d-])/g, (match, _y, _m, day) =>\n formatDate(match, day ? \"date\" : \"year-month\", resolved),\n )\n}\n"]}