@squadbase/vantage 0.6.2-dev1.5aae04e → 0.6.2-dev1.5c79ef5

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 (228) 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-design/SKILL.md +54 -54
  208. package/skills/vantage-design/references/extract.md +26 -27
  209. package/skills/vantage-design/references/format.md +12 -172
  210. package/skills/vantage-design/references/presets.md +50 -110
  211. package/skills/vantage-pitfalls/SKILL.md +5 -0
  212. package/skills/vantage-slide/SKILL.md +54 -36
  213. package/templates/AGENTS.md +32 -16
  214. package/templates/agents/doc.md +48 -0
  215. package/templates/agents/page.md +23 -2
  216. package/templates/agents/slide.md +77 -3
  217. package/theme.css +23 -19
  218. package/dist/chunk-2ICNHUG6.js.map +0 -1
  219. package/dist/chunk-2N72FKLB.js.map +0 -1
  220. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  221. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  222. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  223. package/dist/chunk-JKLLFVEU.js.map +0 -1
  224. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  225. package/dist/chunk-V3LF7BE7.js.map +0 -1
  226. package/dist/chunk-WAMZWZJB.js.map +0 -1
  227. package/dist/chunk-XPQSTZUU.js.map +0 -1
  228. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -11,14 +11,105 @@
11
11
  * never carries the meaning alone: put the date and the state in `tooltip`
12
12
  * (also the block's accessible name).
13
13
  */
14
- import { forwardRef, type ComponentProps } from "react"
14
+ import { forwardRef, useCallback, useRef, type ComponentProps } from "react"
15
15
 
16
16
  import { cn } from "@squadbase/vantage/ui"
17
17
  import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
18
18
  import type { ChartColor } from "@squadbase/vantage/components"
19
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
19
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
20
20
  import type { ChartColors, ChartSeriesColor, ChartTone, ValueFormatter } from "@squadbase/vantage/components"
21
21
 
22
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
23
+
24
+ /**
25
+ * The display language of the parts and the defaults that follow it.
26
+ *
27
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
28
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
29
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
30
+ *
31
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
32
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
33
+ */
34
+
35
+ /** The language of the built-in wording: Japanese or English. */
36
+ type UiLanguage = "ja" | "en"
37
+
38
+ /** The locale used when `<html lang>` gives none (and on the server). */
39
+ const FALLBACK_LOCALE = "en-US"
40
+
41
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
42
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
43
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
44
+ }
45
+
46
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
47
+ function normalizeLocale(tag: string | null | undefined): string {
48
+ const value = tag?.trim()
49
+ if (!value) return FALLBACK_LOCALE
50
+ try {
51
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
52
+ } catch {
53
+ return FALLBACK_LOCALE
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
59
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
60
+ */
61
+ function documentLocale(): string {
62
+ if (typeof document === "undefined") return FALLBACK_LOCALE
63
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
64
+ }
65
+
66
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
67
+ function resolveLocale(locale?: string | null): string {
68
+ return locale || documentLocale()
69
+ }
70
+
71
+ /** Picks the wording table of the locale's language. */
72
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
73
+ return table[uiLanguage(resolveLocale(locale))]
74
+ }
75
+
76
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
77
+ function defaultCurrency(locale?: string | null): string {
78
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
79
+ }
80
+
81
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
82
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
83
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
84
+ }
85
+
86
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
87
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
88
+
89
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
90
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
91
+
92
+ /**
93
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
94
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
95
+ * Monday for the rest.
96
+ */
97
+ function weekStartsOn(locale?: string | null): WeekDay {
98
+ const tag = normalizeLocale(resolveLocale(locale))
99
+ if (uiLanguage(tag) === "ja") return 1
100
+ try {
101
+ const info = new Intl.Locale(tag) as Intl.Locale & {
102
+ getWeekInfo?: () => { firstDay: number }
103
+ weekInfo?: { firstDay: number }
104
+ }
105
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
106
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
107
+ } catch {
108
+ // Fall through to the table below.
109
+ }
110
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
111
+ }
112
+
22
113
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
23
114
 
24
115
  const ROOT_PROP_KEYS = new Set(["id", "style", "role", "tabIndex", "lang", "hidden"])
@@ -126,9 +217,10 @@ function resolveValueFormatter(
126
217
  function resolveAxisFormatter(
127
218
  valueFormatter: ValueFormatter | undefined,
128
219
  format: ValueFormat | undefined,
129
- locale: string = DEFAULT_LOCALE,
220
+ localeArg?: string,
130
221
  ): ValueFormatter {
131
222
  if (valueFormatter) return valueFormatter
223
+ const locale = resolveLocale(localeArg)
132
224
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
133
225
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
134
226
  const currency = "currency" in spec ? spec.currency : undefined
@@ -137,40 +229,75 @@ function resolveAxisFormatter(
137
229
  return (value) => formatValue(value, short, locale)
138
230
  }
139
231
 
140
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
141
- const JA_UNITS = [
142
- { char: "兆", size: 1e12 },
143
- { char: "億", size: 1e8 },
144
- { char: "万", size: 1e4 },
145
- ] as const
232
+ interface CompactUnit {
233
+ token: string
234
+ size: number
235
+ }
146
236
 
147
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
237
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
238
+
239
+ /**
240
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
241
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
242
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
243
+ */
244
+ function compactUnits(locale: string): CompactUnit[] | null {
245
+ const cached = compactUnitCache.get(locale)
246
+ if (cached !== undefined) return cached
247
+ let units: CompactUnit[] | null = []
248
+ try {
249
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
250
+ const seen = new Set<string>()
251
+ for (let exp = 3; exp <= 15; exp++) {
252
+ const parts = compact.formatToParts(10 ** exp)
253
+ const at = parts.findIndex((p) => p.type === "compact")
254
+ if (at === -1) continue
255
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
256
+ units = null
257
+ break
258
+ }
259
+ const token = parts[at]!.value
260
+ if (seen.has(token)) continue
261
+ seen.add(token)
262
+ units.push({ token, size: 10 ** exp })
263
+ }
264
+ } catch {
265
+ units = null
266
+ }
267
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
268
+ compactUnitCache.set(locale, sorted)
269
+ return sorted
270
+ }
271
+
272
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
148
273
 
149
274
  /**
150
275
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
151
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
152
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
153
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
276
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
277
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
278
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
279
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
154
280
  */
155
281
  function resolveAxisTickFormatter(
156
282
  valueFormatter: ValueFormatter | undefined,
157
283
  format: ValueFormat | undefined,
158
- locale: string = DEFAULT_LOCALE,
284
+ localeArg?: string,
159
285
  ): (top: number) => ValueFormatter {
286
+ const locale = resolveLocale(localeArg)
160
287
  const base = resolveAxisFormatter(valueFormatter, format, locale)
161
288
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
162
- const aligns =
163
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
164
- if (!aligns) return () => base
289
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
290
+ if (!units) return () => base
165
291
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
292
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
166
293
  return (top) => {
167
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
294
+ const unit = units.find((u) => Math.abs(top) >= u.size)
168
295
  if (!unit) return base
169
296
  return (value) => {
170
297
  const text = base(value)
171
298
  if (value === 0 || !Number.isFinite(value)) return text
172
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
173
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
299
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
300
+ return text.replace(numberToken, () => scaled)
174
301
  }
175
302
  }
176
303
  }
@@ -199,8 +326,20 @@ function clamp(value: number, min: number, max: number): number {
199
326
  /** How many categories a chart colours individually; the rest fold into「その他」. */
200
327
  const MAX_CHART_SERIES = 5
201
328
 
202
- /** The series name the folded categories get. */
203
- const OTHER_SERIES_LABEL = "その他"
329
+ /** The series name the folded categories get, by language. */
330
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
331
+
332
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
333
+ function otherSeriesLabel(locale?: string): string {
334
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
335
+ }
336
+
337
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
338
+
339
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
340
+ function isOtherName(name: string | null | undefined): boolean {
341
+ return typeof name === "string" && OTHER_NAMES.has(name)
342
+ }
204
343
 
205
344
  interface AggregatedItems<Item extends { name: string; value: number }> {
206
345
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -220,7 +359,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
220
359
  function aggregateTopN<Item extends { name: string; value: number }>(
221
360
  items: readonly Item[],
222
361
  n: number = MAX_CHART_SERIES,
223
- otherLabel: string = OTHER_SERIES_LABEL,
362
+ otherLabel: string = otherSeriesLabel(),
224
363
  ): AggregatedItems<Item> {
225
364
  if (items.length <= n) return { top: [...items], rest: [], other: null }
226
365
  const ranked = items
@@ -249,7 +388,7 @@ function aggregateSeries(
249
388
  data: readonly Record<string, unknown>[],
250
389
  categories: readonly string[],
251
390
  n: number = MAX_CHART_SERIES,
252
- otherLabel: string = OTHER_SERIES_LABEL,
391
+ otherLabel: string = otherSeriesLabel(),
253
392
  ): AggregatedSeries {
254
393
  const totals = categories.map((name) => ({
255
394
  name,
@@ -281,12 +420,14 @@ function aggregateSeriesKeeping(
281
420
  categories: readonly string[],
282
421
  keep: string | undefined,
283
422
  n: number = MAX_CHART_SERIES,
423
+ otherLabel: string = otherSeriesLabel(),
284
424
  ): AggregatedSeries {
285
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
425
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
286
426
  const aggregated = aggregateSeries(
287
427
  data,
288
428
  categories.filter((c) => c !== keep),
289
429
  n,
430
+ otherLabel,
290
431
  )
291
432
  return { ...aggregated, categories: [...aggregated.categories, keep] }
292
433
  }
@@ -322,7 +463,7 @@ function seriesColorsWithComparison(
322
463
  * Colour per drawn series: the record's token for that category, else the
323
464
  * array's token at the category's position in `allCategories` (cycled), else
324
465
  * the token palette by drawn position. `otherCategory` — and a category the
325
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
466
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
326
467
  */
327
468
  function seriesColorsFor(
328
469
  categories: readonly string[],
@@ -334,7 +475,7 @@ function seriesColorsFor(
334
475
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
335
476
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
336
477
  return categories.map((category, i) => {
337
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
478
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
338
479
  const fromRecord = record?.[category]
339
480
  if (fromRecord) return fromRecord
340
481
  if (list && list.length > 0) {
@@ -381,7 +522,7 @@ interface DonutSlicesConfig {
381
522
  */
382
523
  function donutSlices(
383
524
  data: readonly Record<string, unknown>[],
384
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
525
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
385
526
  ): DonutSlice[] {
386
527
  const items = data
387
528
  .map((row, i) => {
@@ -394,7 +535,8 @@ function donutSlices(
394
535
  const slices: DonutSlice[] = top.map((item, rank) => ({
395
536
  name: item.name,
396
537
  value: item.value,
397
- color: palette[rank % palette.length]!,
538
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
539
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
398
540
  isOther: false,
399
541
  row: item.row,
400
542
  }))
@@ -467,7 +609,7 @@ interface BarListRowsConfig extends BarListOptions {
467
609
  */
468
610
  function barListRows<Row extends object>(
469
611
  data: readonly Row[],
470
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
612
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
471
613
  ): BarListRow<Row>[] {
472
614
  const items = data.map((row): BarListRow<Row> => {
473
615
  const cells = row as Record<string, unknown>
@@ -513,19 +655,21 @@ const BAR_LIST_OTHER_TINT = 30
513
655
 
514
656
  /**
515
657
  * The fill of each BarList row: all real rows in the same tint of `color`
516
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
658
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
517
659
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
518
660
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
519
661
  * for that row. A tint, not the solid token, so the label printed over the bar
520
662
  * keeps its contrast.
521
663
  */
522
664
  function barListFills(
523
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
665
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
524
666
  color: ChartColor,
525
667
  otherColor: string,
526
668
  ): string[] {
527
669
  return rows.map((row) =>
528
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
670
+ row.isOther || isOtherName(row.name)
671
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
672
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
529
673
  )
530
674
  }
531
675
 
@@ -741,11 +885,30 @@ interface ChartSummaryInput {
741
885
  format?: ValueFormat
742
886
  /** Wins over `format`. */
743
887
  valueFormatter?: ValueFormatter
888
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
889
+ locale?: string
890
+ }
891
+
892
+ const SUMMARY_TEXT = {
893
+ ja: {
894
+ up: "、前の点より増加",
895
+ down: "、前の点より減少",
896
+ flat: "、前の点と横ばい",
897
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
898
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
899
+ },
900
+ en: {
901
+ up: ", up from the previous point",
902
+ down: ", down from the previous point",
903
+ flat: ", flat from the previous point",
904
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
905
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
906
+ },
744
907
  }
745
908
 
746
909
  /**
747
910
  * One-sentence summary built from the rows, for `summary="auto"`:
748
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
911
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
749
912
  * computed from the last two non-empty points of the first series — never
750
913
  * written by hand, so it cannot contradict the data. `undefined` when there is
751
914
  * no usable point.
@@ -761,26 +924,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
761
924
  }
762
925
  const latest = points[points.length - 1]
763
926
  if (!latest) return undefined
764
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
927
+ const locale = resolveLocale(input.locale)
928
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
929
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
765
930
  const prev = points[points.length - 2]
766
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
931
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
767
932
  const head = input.title ? `${input.title}: ` : ""
768
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
933
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
769
934
  }
770
935
 
771
936
  /**
772
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
773
- * (largest first). `undefined` when there is nothing to describe.
937
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
938
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
774
939
  */
775
940
  function buildShareSummary(
776
941
  slices: readonly { name: string; value: number }[],
777
942
  title?: string,
943
+ locale?: string,
778
944
  ): string | undefined {
779
945
  const total = slices.reduce((sum, s) => sum + s.value, 0)
780
946
  const top = slices[0]
781
947
  if (!top || total <= 0) return undefined
782
948
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
783
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
949
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
784
950
  }
785
951
 
786
952
  /* ---------- end of inlined code ---------- */
@@ -816,9 +982,15 @@ export const Tracker = forwardRef<HTMLDivElement, TrackerProps>(function Tracker
816
982
  { data = NO_ROWS, defaultColor, hoverEffect = true, className, ...props },
817
983
  ref,
818
984
  ) {
985
+ const containerRef = useRef<HTMLDivElement>(null)
986
+ const setRef = useCallback((node: HTMLDivElement | null) => {
987
+ containerRef.current = node
988
+ if (typeof ref === "function") return ref(node)
989
+ if (ref) ref.current = node
990
+ }, [ref])
819
991
  const fallback = defaultColor ? blockColorToCss(defaultColor) : DEFAULT_BLOCK_COLOR
820
992
  return (
821
- <div ref={ref} data-slot="tracker" className={cn("flex h-6 w-full gap-0.5", className)} {...props}>
993
+ <div ref={setRef} data-slot="tracker" className={cn("flex h-6 w-full gap-0.5", className)} {...props}>
822
994
  {data.map((block, i) => {
823
995
  const blockClass = cn(
824
996
  "h-full min-w-0 flex-1 rounded-[2px]",
@@ -831,7 +1003,7 @@ export const Tracker = forwardRef<HTMLDivElement, TrackerProps>(function Tracker
831
1003
  delay={0}
832
1004
  render={<div data-slot="tracker-block" role="img" tabIndex={0} aria-label={block.tooltip} className={blockClass} style={style} />}
833
1005
  />
834
- <TooltipContent>{block.tooltip}</TooltipContent>
1006
+ <TooltipContent container={containerRef}>{block.tooltip}</TooltipContent>
835
1007
  </Tooltip>
836
1008
  ) : (
837
1009
  <div key={block.key ?? i} data-slot="tracker-block" className={blockClass} style={style} />
@@ -15,12 +15,159 @@ import { cva } from "class-variance-authority"
15
15
 
16
16
  import { cn } from "@squadbase/vantage/ui"
17
17
  import type { TrendDirection } from "@squadbase/vantage/components"
18
+ import { useSyncExternalStore } from "react"
19
+
20
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
21
+
22
+ /**
23
+ * The display language of the parts and the defaults that follow it.
24
+ *
25
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
26
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
27
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
28
+ *
29
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
30
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
31
+ */
32
+
33
+ /** The language of the built-in wording: Japanese or English. */
34
+ type UiLanguage = "ja" | "en"
35
+
36
+ /** The locale used when `<html lang>` gives none (and on the server). */
37
+ const FALLBACK_LOCALE = "en-US"
38
+
39
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
40
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
41
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
42
+ }
43
+
44
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
45
+ function normalizeLocale(tag: string | null | undefined): string {
46
+ const value = tag?.trim()
47
+ if (!value) return FALLBACK_LOCALE
48
+ try {
49
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
50
+ } catch {
51
+ return FALLBACK_LOCALE
52
+ }
53
+ }
54
+
55
+ /**
56
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
57
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
58
+ */
59
+ function documentLocale(): string {
60
+ if (typeof document === "undefined") return FALLBACK_LOCALE
61
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
62
+ }
63
+
64
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
65
+ function resolveLocale(locale?: string | null): string {
66
+ return locale || documentLocale()
67
+ }
68
+
69
+ /** Picks the wording table of the locale's language. */
70
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
71
+ return table[uiLanguage(resolveLocale(locale))]
72
+ }
73
+
74
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
75
+ function defaultCurrency(locale?: string | null): string {
76
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
77
+ }
78
+
79
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
80
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
81
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
82
+ }
83
+
84
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
85
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
86
+
87
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
88
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
89
+
90
+ /**
91
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
92
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
93
+ * Monday for the rest.
94
+ */
95
+ function weekStartsOn(locale?: string | null): WeekDay {
96
+ const tag = normalizeLocale(resolveLocale(locale))
97
+ if (uiLanguage(tag) === "ja") return 1
98
+ try {
99
+ const info = new Intl.Locale(tag) as Intl.Locale & {
100
+ getWeekInfo?: () => { firstDay: number }
101
+ weekInfo?: { firstDay: number }
102
+ }
103
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
104
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
105
+ } catch {
106
+ // Fall through to the table below.
107
+ }
108
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
109
+ }
110
+
111
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
112
+
113
+ const listeners = new Set<() => void>()
114
+ let observer: MutationObserver | null = null
115
+
116
+ /** One observer on `<html lang>` for every subscribed part. */
117
+ function subscribe(onChange: () => void): () => void {
118
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
119
+ listeners.add(onChange)
120
+ if (!observer) {
121
+ observer = new MutationObserver(() => {
122
+ for (const listener of [...listeners]) listener()
123
+ })
124
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
125
+ }
126
+ return () => {
127
+ listeners.delete(onChange)
128
+ if (listeners.size === 0 && observer) {
129
+ observer.disconnect()
130
+ observer = null
131
+ }
132
+ }
133
+ }
134
+
135
+ const serverLocale = () => FALLBACK_LOCALE
136
+
137
+ /**
138
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
139
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
140
+ */
141
+ function useDocumentLocale(): string {
142
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
143
+ }
144
+
145
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
146
+ function useLocale(locale?: string | null): string {
147
+ const page = useDocumentLocale()
148
+ return locale || page
149
+ }
150
+
151
+ /* ---------- end of inlined code ---------- */
152
+
153
+ const TEXT = {
154
+ ja: {
155
+ flat: (value: number) => `変化なし ${value}%`,
156
+ up: (value: number) => `${value}% 増加`,
157
+ down: (value: number) => `${value}% 減少`,
158
+ },
159
+ en: {
160
+ flat: (value: number) => `No change ${value}%`,
161
+ up: (value: number) => `Up ${value}%`,
162
+ down: (value: number) => `Down ${value}%`,
163
+ },
164
+ }
18
165
 
19
166
  const trendIndicatorVariants = cva("inline-flex items-center gap-0.5 text-sm font-medium", {
20
167
  variants: {
21
168
  sentiment: {
22
- positive: "text-chart-1",
23
- negative: "text-primary",
169
+ positive: "text-positive",
170
+ negative: "text-negative",
24
171
  neutral: "text-muted-foreground",
25
172
  },
26
173
  },
@@ -51,12 +198,8 @@ export const TrendIndicator = forwardRef<HTMLSpanElement, TrendIndicatorProps>(
51
198
 
52
199
  const Icon = direction === "up" ? ArrowUp : direction === "down" ? ArrowDown : Minus
53
200
 
54
- const ariaLabel =
55
- direction === "neutral"
56
- ? `変化なし ${value}%`
57
- : direction === "up"
58
- ? `${value}% 増加`
59
- : `${value}% 減少`
201
+ const t = TEXT[uiLanguage(useLocale())]
202
+ const ariaLabel = direction === "neutral" ? t.flat(value) : direction === "up" ? t.up(value) : t.down(value)
60
203
 
61
204
  return (
62
205
  <span