@squadbase/vantage 0.6.2-dev1.9365dcd → 0.6.2-dev1.9d7130b

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 (230) hide show
  1. package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
  4. package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/chunk-2IAPIOPS.js +152 -0
  6. package/dist/chunk-2IAPIOPS.js.map +1 -0
  7. package/dist/chunk-3RJEOP3J.js +174 -0
  8. package/dist/chunk-3RJEOP3J.js.map +1 -0
  9. package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
  10. package/dist/chunk-77CISBMG.js.map +1 -0
  11. package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
  12. package/dist/chunk-ANZLVSEP.js.map +1 -0
  13. package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
  14. package/dist/chunk-CKWPK4RO.js.map +1 -0
  15. package/dist/chunk-EG4PX726.js +678 -0
  16. package/dist/chunk-EG4PX726.js.map +1 -0
  17. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  18. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  19. package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
  20. package/dist/chunk-PJ4SPXHS.js.map +1 -0
  21. package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
  22. package/dist/chunk-QNOX7WLZ.js.map +1 -0
  23. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  24. package/dist/chunk-ROHNXP7J.js.map +1 -0
  25. package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
  26. package/dist/chunk-S5MDTHH7.js.map +1 -0
  27. package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
  28. package/dist/chunk-SDJTTMLK.js.map +1 -0
  29. package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
  30. package/dist/chunk-SHZM3EXB.js.map +1 -0
  31. package/dist/chunk-TRMHNMMZ.js +61 -0
  32. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  33. package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
  34. package/dist/chunk-UWSQUHX6.js.map +1 -0
  35. package/dist/cli.js +6 -6
  36. package/dist/client/index.d.ts +2 -0
  37. package/dist/client/index.js +8 -2
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/components/index.d.ts +57 -273
  40. package/dist/components/index.js +6 -3
  41. package/dist/delta-text-CbrOaW83.d.ts +17 -0
  42. package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
  43. package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
  44. package/dist/doc/index.d.ts +215 -2
  45. package/dist/doc/index.js +422 -4
  46. package/dist/doc/index.js.map +1 -1
  47. package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
  48. package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
  49. package/dist/format-DihQkL1N.d.ts +249 -0
  50. package/dist/query/index.js +2 -2
  51. package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
  52. package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
  53. package/dist/slide/index.d.ts +272 -1
  54. package/dist/slide/index.js +1023 -2
  55. package/dist/slide/index.js.map +1 -1
  56. package/dist/table/index.js +9 -6
  57. package/dist/table/index.js.map +1 -1
  58. package/dist/ui/index.d.ts +5 -2
  59. package/dist/ui/index.js +6 -6
  60. package/dist/ui/index.js.map +1 -1
  61. package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
  62. package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
  63. package/dist/vite/index.js +3 -3
  64. package/docs/en/agent-skills.md +1 -1
  65. package/docs/en/changelog.md +2 -0
  66. package/docs/en/components.md +13 -2
  67. package/docs/en/doc/delta-text.md +40 -0
  68. package/docs/en/doc/doc-action-items.md +49 -0
  69. package/docs/en/doc/doc-callout.md +38 -0
  70. package/docs/en/doc/doc-data-table.md +60 -0
  71. package/docs/en/doc/doc-executive-summary.md +64 -0
  72. package/docs/en/doc/doc-figure.md +56 -0
  73. package/docs/en/doc/doc-key-findings.md +61 -0
  74. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  75. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  76. package/docs/en/doc/doc-report-header.md +46 -0
  77. package/docs/en/pages-and-metadata.md +15 -0
  78. package/docs/en/parts/area-chart.md +99 -0
  79. package/docs/en/parts/bar-chart.md +91 -0
  80. package/docs/en/parts/bar-list.md +50 -0
  81. package/docs/en/parts/category-bar.md +37 -0
  82. package/docs/en/parts/chart-frame.md +39 -0
  83. package/docs/en/parts/delta.md +4 -3
  84. package/docs/en/parts/donut-chart.md +72 -0
  85. package/docs/en/parts/funnel-steps.md +2 -1
  86. package/docs/en/parts/heatmap.md +53 -0
  87. package/docs/en/parts/kpi-card.md +2 -1
  88. package/docs/en/parts/legend.md +35 -0
  89. package/docs/en/parts/line-chart.md +94 -0
  90. package/docs/en/parts/progress-circle.md +33 -0
  91. package/docs/en/parts/sample-data.md +39 -0
  92. package/docs/en/parts/scatter-chart.md +56 -0
  93. package/docs/en/parts/tracker.md +36 -0
  94. package/docs/en/parts/updated-at.md +30 -0
  95. package/docs/en/parts/waterfall-chart.md +56 -0
  96. package/docs/en/slide/slide-chart-full.md +48 -0
  97. package/docs/en/slide/slide-chart-insight.md +52 -0
  98. package/docs/en/slide/slide-columns.md +46 -0
  99. package/docs/en/slide/slide-comparison.md +68 -0
  100. package/docs/en/slide/slide-cover.md +39 -0
  101. package/docs/en/slide/slide-exec-summary.md +47 -0
  102. package/docs/en/slide/slide-heading.md +38 -0
  103. package/docs/en/slide/slide-kpi-grid.md +55 -0
  104. package/docs/en/slide/slide-kpi-hero.md +58 -0
  105. package/docs/en/slide/slide-next-actions.md +49 -0
  106. package/docs/en/slide/slide-scorecard-table.md +54 -0
  107. package/docs/en/slide/slide-section.md +44 -0
  108. package/docs/en/slide/slide-source-line.md +34 -0
  109. package/docs/en/ui/alert-dialog.md +42 -0
  110. package/docs/en/ui/avatar.md +31 -0
  111. package/docs/en/ui/combobox.md +74 -0
  112. package/docs/en/ui/drawer.md +41 -0
  113. package/docs/en/ui/empty.md +1 -1
  114. package/docs/en/ui/error-state.md +4 -3
  115. package/docs/en/ui/field.md +42 -0
  116. package/docs/en/ui/hover-card.md +21 -0
  117. package/docs/en/ui/kbd.md +17 -0
  118. package/docs/en/ui/loading.md +1 -1
  119. package/docs/en/ui/pagination.md +36 -0
  120. package/docs/en/ui/radio-group.md +21 -0
  121. package/docs/en/ui/toast.md +44 -0
  122. package/docs/en/ui/toggle-group.md +4 -3
  123. package/docs/index.json +857 -77
  124. package/docs/ja/agent-skills.md +1 -1
  125. package/docs/ja/changelog.md +2 -0
  126. package/docs/ja/components.md +12 -1
  127. package/docs/ja/doc/delta-text.md +40 -0
  128. package/docs/ja/doc/doc-action-items.md +49 -0
  129. package/docs/ja/doc/doc-callout.md +38 -0
  130. package/docs/ja/doc/doc-data-table.md +60 -0
  131. package/docs/ja/doc/doc-executive-summary.md +64 -0
  132. package/docs/ja/doc/doc-figure.md +56 -0
  133. package/docs/ja/doc/doc-key-findings.md +61 -0
  134. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  135. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  136. package/docs/ja/doc/doc-report-header.md +46 -0
  137. package/docs/ja/page-types/slide.md +1 -0
  138. package/docs/ja/pages-and-metadata.md +15 -0
  139. package/docs/ja/parts/area-chart.md +49 -25
  140. package/docs/ja/parts/bar-chart.md +50 -24
  141. package/docs/ja/parts/bar-list.md +31 -8
  142. package/docs/ja/parts/chart-frame.md +36 -0
  143. package/docs/ja/parts/delta.md +5 -4
  144. package/docs/ja/parts/donut-chart.md +45 -26
  145. package/docs/ja/parts/funnel-steps.md +27 -8
  146. package/docs/ja/parts/heatmap.md +45 -0
  147. package/docs/ja/parts/kpi-card.md +2 -1
  148. package/docs/ja/parts/legend.md +36 -0
  149. package/docs/ja/parts/line-chart.md +48 -22
  150. package/docs/ja/parts/sample-data.md +61 -0
  151. package/docs/ja/parts/scatter-chart.md +69 -0
  152. package/docs/ja/parts/updated-at.md +27 -0
  153. package/docs/ja/parts/waterfall-chart.md +61 -0
  154. package/docs/ja/slide/slide-chart-full.md +48 -0
  155. package/docs/ja/slide/slide-chart-insight.md +52 -0
  156. package/docs/ja/slide/slide-columns.md +46 -0
  157. package/docs/ja/slide/slide-comparison.md +68 -0
  158. package/docs/ja/slide/slide-cover.md +39 -0
  159. package/docs/ja/slide/slide-exec-summary.md +47 -0
  160. package/docs/ja/slide/slide-heading.md +38 -0
  161. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  162. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  163. package/docs/ja/slide/slide-next-actions.md +49 -0
  164. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  165. package/docs/ja/slide/slide-section.md +44 -0
  166. package/docs/ja/slide/slide-source-line.md +34 -0
  167. package/docs/ja/ui/empty.md +1 -1
  168. package/docs/ja/ui/error-state.md +4 -3
  169. package/docs/ja/ui/loading.md +1 -1
  170. package/docs/ja/ui/toggle-group.md +4 -3
  171. package/package.json +2 -2
  172. package/registry/ui/app-shell.tsx +138 -1
  173. package/registry/ui/area-chart.tsx +389 -104
  174. package/registry/ui/attention-list.tsx +177 -15
  175. package/registry/ui/bar-chart.tsx +386 -104
  176. package/registry/ui/bar-list.tsx +376 -94
  177. package/registry/ui/box-plot-chart.tsx +450 -116
  178. package/registry/ui/category-bar.tsx +292 -57
  179. package/registry/ui/chart-frame.tsx +301 -63
  180. package/registry/ui/data-table.tsx +229 -37
  181. package/registry/ui/date-range-picker.tsx +190 -36
  182. package/registry/ui/delta.tsx +181 -19
  183. package/registry/ui/donut-chart.tsx +385 -103
  184. package/registry/ui/echart.tsx +313 -58
  185. package/registry/ui/filter-bar.tsx +179 -20
  186. package/registry/ui/funnel-steps.tsx +354 -80
  187. package/registry/ui/heatmap.tsx +338 -71
  188. package/registry/ui/histogram-chart.tsx +440 -116
  189. package/registry/ui/kpi-card.tsx +254 -48
  190. package/registry/ui/legend.tsx +374 -93
  191. package/registry/ui/line-chart.tsx +389 -104
  192. package/registry/ui/multi-select.tsx +159 -9
  193. package/registry/ui/page-shell.tsx +148 -10
  194. package/registry/ui/paired-chart.tsx +394 -109
  195. package/registry/ui/progress-circle.tsx +205 -39
  196. package/registry/ui/sample-data.tsx +154 -10
  197. package/registry/ui/scatter-chart.tsx +436 -119
  198. package/registry/ui/searchable-select.tsx +145 -7
  199. package/registry/ui/slider.tsx +1 -1
  200. package/registry/ui/states.tsx +170 -13
  201. package/registry/ui/status-badge.tsx +201 -36
  202. package/registry/ui/tooltip.tsx +6 -2
  203. package/registry/ui/tracker.tsx +214 -42
  204. package/registry/ui/trend-indicator.tsx +151 -8
  205. package/registry/ui/updated-at.tsx +170 -16
  206. package/registry/ui/waterfall-chart.tsx +460 -121
  207. package/skills/vantage-add-feature/SKILL.md +5 -0
  208. package/skills/vantage-design/SKILL.md +54 -54
  209. package/skills/vantage-design/references/extract.md +26 -27
  210. package/skills/vantage-design/references/format.md +12 -172
  211. package/skills/vantage-design/references/presets.md +50 -110
  212. package/skills/vantage-pitfalls/SKILL.md +5 -0
  213. package/skills/vantage-slide/SKILL.md +54 -36
  214. package/templates/AGENTS.md +35 -18
  215. package/templates/agents/api.md +7 -0
  216. package/templates/agents/doc.md +48 -0
  217. package/templates/agents/page.md +59 -2
  218. package/templates/agents/slide.md +77 -3
  219. package/theme.css +23 -19
  220. package/dist/chunk-2ICNHUG6.js.map +0 -1
  221. package/dist/chunk-2N72FKLB.js.map +0 -1
  222. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  223. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  224. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  225. package/dist/chunk-JKLLFVEU.js.map +0 -1
  226. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  227. package/dist/chunk-V3LF7BE7.js.map +0 -1
  228. package/dist/chunk-WAMZWZJB.js.map +0 -1
  229. package/dist/chunk-XPQSTZUU.js.map +0 -1
  230. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -43,7 +43,7 @@
43
43
  * line takes its name from it ("昨日 617"), the same way a `label` on any `vs` does.
44
44
  * - **Two or more comparisons sit under the value**, not beside it, so the value
45
45
  * keeps its place and its width whatever the card holds.
46
- * - **Polarity drives everything.** With `polarity="lower-is-better"` the target
46
+ * - **`betterWhen` drives everything.** With `betterWhen="down"` the target
47
47
  * is met at or below it, and the record badge marks the lowest value.
48
48
  * - **Rates change in points.** With a `percent` format the delta and
49
49
  * `{difference}` are in percentage points ("-0.5pt"); `change="relative"` opts
@@ -91,7 +91,7 @@ import {
91
91
  type PeriodGrain,
92
92
  type Polarity,
93
93
  } from "@squadbase/vantage/components"
94
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatDate, formatDatesInText, formatDelta, formatKind, formatPoints, formatValue, type ValueFormat } from "@squadbase/vantage/components"
94
+ import { EMPTY_VALUE, formatDate, formatDatesInText, formatDelta, formatKind, formatPoints, formatValue, type ValueFormat } from "@squadbase/vantage/components"
95
95
  import { computeTargetProgress, detectRecord, inferPeriodGrain, type KpiPeriod } from "@squadbase/vantage/components"
96
96
  import { formatRelativeTime } from "@squadbase/vantage/components"
97
97
  import { Sparkline } from "@squadbase/vantage/components"
@@ -100,6 +100,140 @@ import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle }
100
100
  import { Skeleton } from "@squadbase/vantage/ui"
101
101
  import { Delta } from "@squadbase/vantage/components"
102
102
  import { SampleBadge, useSampleMark } from "@squadbase/vantage/components"
103
+ import { useSyncExternalStore } from "react"
104
+
105
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
106
+
107
+ /**
108
+ * The display language of the parts and the defaults that follow it.
109
+ *
110
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
111
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
112
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
113
+ *
114
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
115
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
116
+ */
117
+
118
+ /** The language of the built-in wording: Japanese or English. */
119
+ type UiLanguage = "ja" | "en"
120
+
121
+ /** The locale used when `<html lang>` gives none (and on the server). */
122
+ const FALLBACK_LOCALE = "en-US"
123
+
124
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
125
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
126
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
127
+ }
128
+
129
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
130
+ function normalizeLocale(tag: string | null | undefined): string {
131
+ const value = tag?.trim()
132
+ if (!value) return FALLBACK_LOCALE
133
+ try {
134
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
135
+ } catch {
136
+ return FALLBACK_LOCALE
137
+ }
138
+ }
139
+
140
+ /**
141
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
142
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
143
+ */
144
+ function documentLocale(): string {
145
+ if (typeof document === "undefined") return FALLBACK_LOCALE
146
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
147
+ }
148
+
149
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
150
+ function resolveLocale(locale?: string | null): string {
151
+ return locale || documentLocale()
152
+ }
153
+
154
+ /** Picks the wording table of the locale's language. */
155
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
156
+ return table[uiLanguage(resolveLocale(locale))]
157
+ }
158
+
159
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
160
+ function defaultCurrency(locale?: string | null): string {
161
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
162
+ }
163
+
164
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
165
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
166
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
167
+ }
168
+
169
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
170
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
171
+
172
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
173
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
174
+
175
+ /**
176
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
177
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
178
+ * Monday for the rest.
179
+ */
180
+ function weekStartsOn(locale?: string | null): WeekDay {
181
+ const tag = normalizeLocale(resolveLocale(locale))
182
+ if (uiLanguage(tag) === "ja") return 1
183
+ try {
184
+ const info = new Intl.Locale(tag) as Intl.Locale & {
185
+ getWeekInfo?: () => { firstDay: number }
186
+ weekInfo?: { firstDay: number }
187
+ }
188
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
189
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
190
+ } catch {
191
+ // Fall through to the table below.
192
+ }
193
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
194
+ }
195
+
196
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
197
+
198
+ const listeners = new Set<() => void>()
199
+ let observer: MutationObserver | null = null
200
+
201
+ /** One observer on `<html lang>` for every subscribed part. */
202
+ function subscribe(onChange: () => void): () => void {
203
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
204
+ listeners.add(onChange)
205
+ if (!observer) {
206
+ observer = new MutationObserver(() => {
207
+ for (const listener of [...listeners]) listener()
208
+ })
209
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
210
+ }
211
+ return () => {
212
+ listeners.delete(onChange)
213
+ if (listeners.size === 0 && observer) {
214
+ observer.disconnect()
215
+ observer = null
216
+ }
217
+ }
218
+ }
219
+
220
+ const serverLocale = () => FALLBACK_LOCALE
221
+
222
+ /**
223
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
224
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
225
+ */
226
+ function useDocumentLocale(): string {
227
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
228
+ }
229
+
230
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
231
+ function useLocale(locale?: string | null): string {
232
+ const page = useDocumentLocale()
233
+ return locale || page
234
+ }
235
+
236
+ /* ---------- end of inlined code ---------- */
103
237
 
104
238
  export interface KpiCardSource {
105
239
  /** Period the data covers (e.g. "2023-04 以降"). ISO year-months / dates in it are written by the card ("2023年4月 以降"). */
@@ -125,11 +259,11 @@ export interface KpiCardProps extends Omit<ComponentProps<"div">, "children"> {
125
259
  value: number | null | undefined
126
260
  /**
127
261
  * Value format, also used for previous / target / remaining. Default `"compact"`.
128
- * `"currency"` is JPY in 万・億; `"percent"` takes a ratio (0.021 → 2.1%) and
262
+ * `"currency"` is JPY in 万・億 on a ja page (USD in K・M・B otherwise); `"percent"` takes a ratio (0.021 → 2.1%) and
129
263
  * switches the delta to points. See `formatValue` for the full table.
130
264
  */
131
265
  format?: ValueFormat
132
- /** Default `"ja-JP"`. */
266
+ /** Locale of the numbers and the built-in wording. Default: the page's (`<html lang>`, else `"en-US"`). */
133
267
  locale?: string
134
268
  /**
135
269
  * What the value is compared against; drives the delta (tier 3) and previous line (tier 4).
@@ -147,7 +281,9 @@ export interface KpiCardProps extends Omit<ComponentProps<"div">, "children"> {
147
281
  * @example provisional / provisional={{ label: "暫定" }}
148
282
  */
149
283
  provisional?: boolean | { label?: string }
150
- /** Whether a rise is good. Default `"higher-is-better"`. `"neutral"` never colours the change and skips the target and record logic. */
284
+ /** Which direction is good: `"up"` (default) or `"down"` (churn, error rate). Wins over `polarity`. */
285
+ betterWhen?: "up" | "down"
286
+ /** @deprecated Older name of `betterWhen` (`"higher-is-better"` / `"lower-is-better"`). `"neutral"` (never colours the change, skips the target and record logic) is only reachable here. `betterWhen` wins if both are given. */
151
287
  polarity?: Polarity
152
288
  /** `"relative"` ("+12.4%"), `"points"` ("-0.5pt") or `"difference"` ("+¥530万", in `format`). Default: points for a `percent` format, else relative. */
153
289
  change?: ChangeDisplay
@@ -160,7 +296,7 @@ export interface KpiCardProps extends Omit<ComponentProps<"div">, "children"> {
160
296
  /**
161
297
  * Target for the current period. Under the value it adds a 6px bar (actual, the pace
162
298
  * projection, a tick at the target) and one line "目標の 49%・このペースなら 98%"
163
- * (the projection only while `period` is in progress). With `polarity="lower-is-better"`
299
+ * (the projection only while `period` is in progress). With `betterWhen="down"`
164
300
  * or a rate target (points) there is no bar: "目標 … まであと …" in the footer.
165
301
  */
166
302
  target?: number
@@ -243,7 +379,72 @@ const KPI_FOOTER_CLASS = "flex-col items-start gap-2 rounded-none border-t-0 bg-
243
379
 
244
380
  const PLACEHOLDER = /\{(\w+)\}/g
245
381
 
246
- const isJapanese = (locale: string) => locale.toLowerCase().startsWith("ja")
382
+ const TEXT = {
383
+ ja: {
384
+ target: "目標",
385
+ provisional: "速報",
386
+ joiner: "・",
387
+ countFrom: (count: string) => `${count}件から集計`,
388
+ excluded: (count: string) => `${count}件を除外`,
389
+ noBaseline: (baseline: string) => `${baseline}の実績がないため、比較できません。`,
390
+ previous: (baseline: string, value: string) => `${baseline} ${value}`,
391
+ noPrevious: (baseline: string) => `${baseline}の実績なし`,
392
+ barLead: (tl: string, pct: string) => `${tl}の ${pct}`,
393
+ pace: (pct: string) => `このペースなら ${pct}`,
394
+ provisionalSuffix: "(確定前)",
395
+ provisionalReached: (tl: string, target: string) => `${tl} ${target} に到達(確定前)`,
396
+ provisionalSoFar: (tl: string, target: string, pct: string) => `${tl} ${target} に対して現時点 ${pct}(確定前)`,
397
+ provisionalToGo: (tl: string, target: string, distance: string) => `${tl} ${target} まであと ${distance}(確定前)`,
398
+ provisionalToGoRate: (tl: string, distance: string, pct: string) => `${tl}まであと ${distance}(現時点 ${pct}・確定前)`,
399
+ met: (tl: string, target: string) => `${tl} ${target} を達成`,
400
+ toGo: (tl: string, target: string, distance: string) => `${tl} ${target} まであと ${distance}`,
401
+ reached: (tl: string, target: string) => `${tl} ${target} に到達`,
402
+ reachedRate: (tl: string, target: string, pct: string) => `${tl} ${target} に到達(${pct} 達成)`,
403
+ toGoRate: (tl: string, distance: string, pct: string) => `${tl}まであと ${distance}(${pct} 達成)`,
404
+ progressLabel: (tl: string) => `${tl}に対する進捗`,
405
+ progressValue: (tl: string, target: string, value: string, pct: string) => `${tl} ${target} に対して ${value}(${pct})`,
406
+ history: (label: string, points: number) => `${label}の推移(直近 ${points} 点)`,
407
+ updatedRelative: (relative: string) => `${relative}に更新`,
408
+ updatedAbsolute: (absolute: string) => `${absolute} に更新`,
409
+ updatedBoth: (absolute: string, relative: string) => `${absolute} に更新(${relative})`,
410
+ summaryExample: "{label}は目標の {achievement} まで来ています。",
411
+ },
412
+ en: {
413
+ target: "Target",
414
+ provisional: "Preliminary",
415
+ joiner: " · ",
416
+ countFrom: (count: string) => `Based on ${count} records`,
417
+ excluded: (count: string) => `${count} excluded`,
418
+ noBaseline: (baseline: string) => `No ${baseline} data to compare against.`,
419
+ previous: (baseline: string, value: string) => `${capitalize(baseline)}: ${value}`,
420
+ noPrevious: (baseline: string) => `No ${baseline} data`,
421
+ barLead: (tl: string, pct: string) => `${tl}: ${pct}`,
422
+ pace: (pct: string) => `On pace for ${pct}`,
423
+ provisionalSuffix: " (preliminary)",
424
+ provisionalReached: (tl: string, target: string) => `${tl} ${target}: reached (preliminary)`,
425
+ provisionalSoFar: (tl: string, target: string, pct: string) => `${tl} ${target}: ${pct} so far (preliminary)`,
426
+ provisionalToGo: (tl: string, target: string, distance: string) => `${tl} ${target}: ${distance} to go (preliminary)`,
427
+ provisionalToGoRate: (tl: string, distance: string, pct: string) => `${tl}: ${distance} to go (${pct} so far, preliminary)`,
428
+ met: (tl: string, target: string) => `${tl} ${target}: met`,
429
+ toGo: (tl: string, target: string, distance: string) => `${tl} ${target}: ${distance} to go`,
430
+ reached: (tl: string, target: string) => `${tl} ${target}: reached`,
431
+ reachedRate: (tl: string, target: string, pct: string) => `${tl} ${target}: reached (${pct} achieved)`,
432
+ toGoRate: (tl: string, distance: string, pct: string) => `${tl}: ${distance} to go (${pct} achieved)`,
433
+ progressLabel: (tl: string) => `Progress toward ${tl}`,
434
+ progressValue: (tl: string, target: string, value: string, pct: string) => `${value} of ${tl} ${target} (${pct})`,
435
+ history: (label: string, points: number) => `${label} trend (last ${points} points)`,
436
+ updatedRelative: (relative: string) => `Updated ${relative}`,
437
+ updatedAbsolute: (absolute: string) => `Updated ${absolute}`,
438
+ updatedBoth: (absolute: string, relative: string) => `Updated ${absolute} (${relative})`,
439
+ summaryExample: "{label} is at {achievement} of target.",
440
+ },
441
+ } satisfies Record<UiLanguage, unknown>
442
+
443
+ type KpiText = (typeof TEXT)[UiLanguage]
444
+
445
+ function capitalize(text: string): string {
446
+ return text.charAt(0).toUpperCase() + text.slice(1)
447
+ }
247
448
 
248
449
  const isMissing = (v: string | null | undefined) => v === null || v === undefined || v === ""
249
450
 
@@ -304,11 +505,11 @@ function isProduction(): boolean {
304
505
  }
305
506
  }
306
507
 
307
- function warnShallowSummary(template: string) {
508
+ function warnShallowSummary(template: string, t: KpiText) {
308
509
  if (shallowSummaryWarned || isProduction() || !isShallowSummary(template)) return
309
510
  shallowSummaryWarned = true
310
511
  console.warn(
311
- `[KpiCard] summary="${template}" only repeats the delta badge and the previous actual. Say one fact beyond the comparison — composition, target, record or rate (e.g. "{label}は目標の {achievement} まで来ています。").`,
512
+ `[KpiCard] summary="${template}" only repeats the delta badge and the previous actual. Say one fact beyond the comparison — composition, target, record or rate (e.g. "${t.summaryExample}").`,
312
513
  )
313
514
  }
314
515
 
@@ -321,17 +522,18 @@ export function KpiCard({
321
522
  label,
322
523
  value,
323
524
  format,
324
- locale = DEFAULT_LOCALE,
525
+ locale: localeProp,
325
526
  comparison: comparisonProp,
326
527
  comparisons,
327
528
  provisional,
328
- polarity,
529
+ betterWhen,
530
+ polarity: polarityProp,
329
531
  change,
330
532
  neutralBand,
331
533
  showPrevious = true,
332
534
  summary,
333
535
  target,
334
- targetLabel = "目標",
536
+ targetLabel,
335
537
  targetDisplay,
336
538
  period,
337
539
  history,
@@ -347,26 +549,28 @@ export function KpiCard({
347
549
  className,
348
550
  ...props
349
551
  }: KpiCardProps) {
552
+ const locale = useLocale(localeProp)
553
+ const t = TEXT[uiLanguage(locale)]
350
554
  const allComparisons = [...(comparisonProp ? [comparisonProp] : []), ...(comparisons ?? [])]
351
555
  const comparison = allComparisons[0]
352
556
  const isProvisional = provisional !== undefined && provisional !== false
353
- const provisionalLabel = typeof provisional === "object" && provisional.label ? provisional.label : "速報"
557
+ const provisionalLabel = typeof provisional === "object" && provisional.label ? provisional.label : t.provisional
354
558
  const sample = useSampleMark()
559
+ // `betterWhen` is the documented name and wins over the older `polarity`.
560
+ const polarity: Polarity | undefined = betterWhen ? (betterWhen === "down" ? "lower-is-better" : "higher-is-better") : polarityProp
355
561
  const neutral = polarity === "neutral"
356
562
  const hasPrevious = comparison !== undefined && showPrevious
357
563
  // Two or more badges would squeeze the value from the right; they get their own row under it instead.
358
564
  const stacked = allComparisons.length > 1
359
565
  const hasTarget = target !== undefined
360
566
  const grain = comparison?.grain ?? historyGrain ?? inferPeriodGrain(period)
361
- const record = isProvisional || neutral ? null : detectRecord(history, historyGrain ?? grain, polarity)
567
+ const record = isProvisional || neutral ? null : detectRecord(history, historyGrain ?? grain, polarity, locale)
362
568
  const sourceParts = source
363
569
  ? [
364
570
  source.period !== undefined ? formatDatesInText(source.period, locale) : undefined,
365
- source.count !== undefined ? `${formatValue(source.count, "number", locale)}件から集計` : undefined,
571
+ source.count !== undefined ? t.countFrom(formatValue(source.count, "number", locale)) : undefined,
366
572
  source.excluded !== undefined && Number.isFinite(source.excluded) && source.excluded > 0
367
- ? isJapanese(locale)
368
- ? `${formatValue(source.excluded, "number", locale)}件を除外`
369
- : `${formatValue(source.excluded, "number", locale)} excluded`
573
+ ? t.excluded(formatValue(source.excluded, "number", locale))
370
574
  : undefined,
371
575
  source.name,
372
576
  source.note,
@@ -381,7 +585,7 @@ export function KpiCard({
381
585
  const lower = polarity === "lower-is-better"
382
586
  // The bar reads "how far toward the target"; a rate (points) or a lower-is-better target has no such fill.
383
587
  const targetBar = hasTarget && !neutral && !byPoints && !lower
384
- if (summary !== undefined) warnShallowSummary(summary)
588
+ if (summary !== undefined) warnShallowSummary(summary, t)
385
589
  // Two rows of the parent grid (body, footer), shared by subgrid; a plain two-row grid when standalone.
386
590
  const cardClass = cn("row-span-2 grid content-start gap-y-0 grid-rows-subgrid", className)
387
591
  const valueSize = small ? "text-xl @[218px]/card:text-2xl" : "text-2xl @[218px]/card:text-4xl"
@@ -444,14 +648,14 @@ export function KpiCard({
444
648
  /** An unsigned distance in the metric's unit — points for a rate. */
445
649
  const fmtDistance = (v: number) => (isRate ? formatPoints(v, locale, { signDisplay: "never" }) : fmt(v))
446
650
  const pct = (v: number) => formatValue(v, { type: "percent", digits: 0 }, locale)
447
- const tl = targetLabel
651
+ const tl = targetLabel ?? t.target
448
652
  const display = change ?? defaultChangeDisplay(format)
449
653
  // Value digits only carry over to a change in points; a relative change keeps its own fixed digits.
450
654
  const digits =
451
655
  display === "points" && typeof format === "object" && format !== null && "digits" in format ? format.digits : undefined
452
656
  const graded = comparison ? { ...comparison, grain } : undefined
453
- const result = graded ? computeComparison(value, graded, { polarity, neutralBand }) : null
454
- const baseline = graded ? comparisonBaseline(graded.vs, grain, graded.label, graded.toDate) : null
657
+ const result = graded ? computeComparison(value, graded, { polarity, neutralBand, locale }) : null
658
+ const baseline = graded ? comparisonBaseline(graded.vs, grain, graded.label, graded.toDate, locale) : null
455
659
  const progress = hasTarget && !neutral ? computeTargetProgress(value, target, { period, now, polarity }) : null
456
660
  // A linear pace only makes sense for an accumulating total, not for a rate.
457
661
  const projectedValue =
@@ -479,7 +683,7 @@ export function KpiCard({
479
683
  : null,
480
684
  remaining: progress ? fmtDistance(progress.remaining) : null,
481
685
  },
482
- result && result.direction === "none" && baseline ? `${baseline}の実績がないため、比較できません。` : null,
686
+ result && result.direction === "none" && baseline ? t.noBaseline(baseline) : null,
483
687
  )
484
688
  : null
485
689
 
@@ -491,9 +695,9 @@ export function KpiCard({
491
695
  const projected = projectedValue !== null && projectedValue > actual ? projectedValue : actual
492
696
  const scale = Math.max(target as number, actual, projected)
493
697
  const at = (v: number) => (scale > 0 ? (v / scale) * 100 : 0)
494
- const parts = [`${tl}の ${pct(progress!.achievement)}`]
698
+ const parts = [t.barLead(tl, pct(progress!.achievement))]
495
699
  if (projectedValue !== null && progress!.projected != null)
496
- parts.push(`このペースなら ${formatValue(progress!.projected, { type: "percent", digits: 0 }, locale)}`)
700
+ parts.push(t.pace(formatValue(progress!.projected, { type: "percent", digits: 0 }, locale)))
497
701
  return {
498
702
  actualAt: at(actual),
499
703
  projectedAt: at(projected),
@@ -509,23 +713,23 @@ export function KpiCard({
509
713
  else if (isProvisional)
510
714
  targetText = progress.achieved
511
715
  ? byPoints
512
- ? `${tl} ${fmt(target)} に到達(確定前)`
513
- : `${tl} ${fmt(target)} に対して現時点 ${pct(progress.achievement)}(確定前)`
716
+ ? t.provisionalReached(tl, fmt(target))
717
+ : t.provisionalSoFar(tl, fmt(target), pct(progress.achievement))
514
718
  : byPoints
515
- ? `${tl} ${fmt(target)} まであと ${fmtDistance(progress.remaining)}(確定前)`
516
- : `${tl}まであと ${fmtDistance(progress.remaining)}(現時点 ${pct(progress.achievement)}・確定前)`
719
+ ? t.provisionalToGo(tl, fmt(target), fmtDistance(progress.remaining))
720
+ : t.provisionalToGoRate(tl, fmtDistance(progress.remaining), pct(progress.achievement))
517
721
  else if (lower)
518
722
  targetText = progress.achieved
519
- ? `${tl} ${fmt(target)} を達成`
520
- : `${tl} ${fmt(target)} まであと ${fmtDistance(progress.remaining)}`
723
+ ? t.met(tl, fmt(target))
724
+ : t.toGo(tl, fmt(target), fmtDistance(progress.remaining))
521
725
  else if (byPoints)
522
726
  targetText = progress.achieved
523
- ? `${tl} ${fmt(target)} に到達`
524
- : `${tl} ${fmt(target)} まであと ${fmtDistance(progress.remaining)}`
727
+ ? t.reached(tl, fmt(target))
728
+ : t.toGo(tl, fmt(target), fmtDistance(progress.remaining))
525
729
  else
526
730
  targetText = progress.achieved
527
- ? `${tl} ${fmt(target)} に到達(${pct(progress.achievement)} 達成)`
528
- : `${tl}まであと ${fmtDistance(progress.remaining)}(${pct(progress.achievement)} 達成)`
731
+ ? t.reachedRate(tl, fmt(target), pct(progress.achievement))
732
+ : t.toGoRate(tl, fmtDistance(progress.remaining), pct(progress.achievement))
529
733
  }
530
734
 
531
735
  const hasFooter =
@@ -590,7 +794,7 @@ export function KpiCard({
590
794
  ))}
591
795
  {hasPrevious && comparison && (
592
796
  <div data-slot="kpi-card-previous" className="col-span-full text-sm text-muted-foreground tabular-nums">
593
- {result?.previous != null ? `${baseline} ${fmt(result.previous)}` : `${baseline}の実績なし`}
797
+ {result?.previous != null ? t.previous(baseline ?? "", fmt(result.previous)) : t.noPrevious(baseline ?? "")}
594
798
  </div>
595
799
  )}
596
800
  {bar && progress && (
@@ -598,11 +802,11 @@ export function KpiCard({
598
802
  <div
599
803
  data-slot="kpi-card-target-bar"
600
804
  role="progressbar"
601
- aria-label={`${tl}に対する進捗`}
805
+ aria-label={t.progressLabel(tl)}
602
806
  aria-valuemin={0}
603
807
  aria-valuemax={target}
604
808
  aria-valuenow={Math.min(Math.max(value as number, 0), target as number)}
605
- aria-valuetext={`${tl} ${fmt(target)} に対して ${fmt(value)}(${pct(progress.achievement)})`}
809
+ aria-valuetext={t.progressValue(tl, fmt(target), fmt(value), pct(progress.achievement))}
606
810
  className="relative h-2 w-full overflow-hidden rounded-full bg-muted"
607
811
  >
608
812
  <div className="absolute inset-y-0 left-0 bg-chart-1" style={{ width: `${bar.actualAt}%` }} />
@@ -621,16 +825,17 @@ export function KpiCard({
621
825
  </div>
622
826
  <p
623
827
  data-slot="kpi-card-target"
624
- title={[bar.lead, bar.pace].filter(Boolean).join("・")}
828
+ title={[bar.lead, bar.pace].filter(Boolean).join(t.joiner)}
625
829
  className="truncate text-sm text-muted-foreground tabular-nums"
626
830
  >
627
831
  {bar.lead}
628
832
  {bar.pace && (
629
833
  <>
630
- ・<span data-slot="kpi-card-projection" data-estimate="">{bar.pace}</span>
834
+ {t.joiner}
835
+ <span data-slot="kpi-card-projection" data-estimate="">{bar.pace}</span>
631
836
  </>
632
837
  )}
633
- {isProvisional && "(確定前)"}
838
+ {isProvisional && t.provisionalSuffix}
634
839
  </p>
635
840
  </div>
636
841
  )}
@@ -655,15 +860,15 @@ export function KpiCard({
655
860
  {sourceParts.length > 0 && (
656
861
  <p
657
862
  data-slot="kpi-card-source"
658
- title={sourceParts.join("・")}
863
+ title={sourceParts.join(t.joiner)}
659
864
  className="line-clamp-2 w-full text-xs leading-normal text-muted-foreground tabular-nums"
660
865
  >
661
- {sourceParts.join("・")}
866
+ {sourceParts.join(t.joiner)}
662
867
  </p>
663
868
  )}
664
869
  {hasUpdated && (
665
870
  <p data-slot="kpi-card-updated" className="text-xs leading-normal text-muted-foreground">
666
- {updatedAtText(updatedAt, updatedAtDisplay, locale, now, timeZone)}
871
+ {updatedAtText(updatedAt, updatedAtDisplay, locale, t, now, timeZone)}
667
872
  </p>
668
873
  )}
669
874
  {showSparkline && (
@@ -674,7 +879,7 @@ export function KpiCard({
674
879
  height={SPARKLINE_HEIGHT}
675
880
  color="text-chart-1"
676
881
  highlight="latest"
677
- aria-label={`${label}の推移(直近 ${historyPoints.length} 点)`}
882
+ aria-label={t.history(label, historyPoints.length)}
678
883
  />
679
884
  </div>
680
885
  )}
@@ -684,16 +889,17 @@ export function KpiCard({
684
889
  )
685
890
  }
686
891
 
687
- /** 「3 分前に更新」「2026/10/20 14:30 に更新」「2026/10/20 14:30 に更新(3 分前)」。 */
892
+ /** 「3 分前に更新」「2026/10/20 14:30 に更新」「2026/10/20 14:30 に更新(3 分前)」(英語は "Updated 3 minutes ago" など)。 */
688
893
  function updatedAtText(
689
894
  updatedAt: Date | string | number,
690
895
  display: "relative" | "absolute" | "both",
691
896
  locale: string,
897
+ t: KpiText,
692
898
  now: Date | number | undefined,
693
899
  timeZone?: string,
694
900
  ): string {
695
901
  const relative = formatRelativeTime(updatedAt, { now, locale })
696
- if (display === "relative") return `${relative}に更新`
902
+ if (display === "relative") return t.updatedRelative(relative)
697
903
  const absolute = formatDate(updatedAt, "datetime", locale, { timeZone })
698
- return display === "absolute" ? `${absolute} に更新` : `${absolute} に更新(${relative})`
904
+ return display === "absolute" ? t.updatedAbsolute(absolute) : t.updatedBoth(absolute, relative)
699
905
  }