@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.
- package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-3RJEOP3J.js +174 -0
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
- package/dist/chunk-CKWPK4RO.js.map +1 -0
- package/dist/chunk-EG4PX726.js +678 -0
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
- package/dist/chunk-QNOX7WLZ.js.map +1 -0
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
- package/dist/chunk-SHZM3EXB.js.map +1 -0
- package/dist/chunk-TRMHNMMZ.js +61 -0
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +57 -273
- package/dist/components/index.js +6 -3
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +422 -4
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/query/index.js +2 -2
- package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
- package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1023 -2
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +9 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -2
- package/dist/ui/index.js +6 -6
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
- package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +2 -0
- package/docs/en/components.md +13 -2
- package/docs/en/doc/delta-text.md +40 -0
- package/docs/en/doc/doc-action-items.md +49 -0
- package/docs/en/doc/doc-callout.md +38 -0
- package/docs/en/doc/doc-data-table.md +60 -0
- package/docs/en/doc/doc-executive-summary.md +64 -0
- package/docs/en/doc/doc-figure.md +56 -0
- package/docs/en/doc/doc-key-findings.md +61 -0
- package/docs/en/doc/doc-kpi-scorecard.md +80 -0
- package/docs/en/doc/doc-notes-and-sources.md +43 -0
- package/docs/en/doc/doc-report-header.md +46 -0
- package/docs/en/pages-and-metadata.md +15 -0
- package/docs/en/parts/area-chart.md +99 -0
- package/docs/en/parts/bar-chart.md +91 -0
- package/docs/en/parts/bar-list.md +50 -0
- package/docs/en/parts/category-bar.md +37 -0
- package/docs/en/parts/chart-frame.md +39 -0
- package/docs/en/parts/delta.md +4 -3
- package/docs/en/parts/donut-chart.md +72 -0
- package/docs/en/parts/funnel-steps.md +2 -1
- package/docs/en/parts/heatmap.md +53 -0
- package/docs/en/parts/kpi-card.md +2 -1
- package/docs/en/parts/legend.md +35 -0
- package/docs/en/parts/line-chart.md +94 -0
- package/docs/en/parts/progress-circle.md +33 -0
- package/docs/en/parts/sample-data.md +39 -0
- package/docs/en/parts/scatter-chart.md +56 -0
- package/docs/en/parts/tracker.md +36 -0
- package/docs/en/parts/updated-at.md +30 -0
- package/docs/en/parts/waterfall-chart.md +56 -0
- package/docs/en/slide/slide-chart-full.md +48 -0
- package/docs/en/slide/slide-chart-insight.md +52 -0
- package/docs/en/slide/slide-columns.md +46 -0
- package/docs/en/slide/slide-comparison.md +68 -0
- package/docs/en/slide/slide-cover.md +39 -0
- package/docs/en/slide/slide-exec-summary.md +47 -0
- package/docs/en/slide/slide-heading.md +38 -0
- package/docs/en/slide/slide-kpi-grid.md +55 -0
- package/docs/en/slide/slide-kpi-hero.md +58 -0
- package/docs/en/slide/slide-next-actions.md +49 -0
- package/docs/en/slide/slide-scorecard-table.md +54 -0
- package/docs/en/slide/slide-section.md +44 -0
- package/docs/en/slide/slide-source-line.md +34 -0
- package/docs/en/ui/alert-dialog.md +42 -0
- package/docs/en/ui/avatar.md +31 -0
- package/docs/en/ui/combobox.md +74 -0
- package/docs/en/ui/drawer.md +41 -0
- package/docs/en/ui/empty.md +1 -1
- package/docs/en/ui/error-state.md +4 -3
- package/docs/en/ui/field.md +42 -0
- package/docs/en/ui/hover-card.md +21 -0
- package/docs/en/ui/kbd.md +17 -0
- package/docs/en/ui/loading.md +1 -1
- package/docs/en/ui/pagination.md +36 -0
- package/docs/en/ui/radio-group.md +21 -0
- package/docs/en/ui/toast.md +44 -0
- package/docs/en/ui/toggle-group.md +4 -3
- package/docs/index.json +857 -77
- package/docs/ja/agent-skills.md +1 -1
- package/docs/ja/changelog.md +2 -0
- package/docs/ja/components.md +12 -1
- package/docs/ja/doc/delta-text.md +40 -0
- package/docs/ja/doc/doc-action-items.md +49 -0
- package/docs/ja/doc/doc-callout.md +38 -0
- package/docs/ja/doc/doc-data-table.md +60 -0
- package/docs/ja/doc/doc-executive-summary.md +64 -0
- package/docs/ja/doc/doc-figure.md +56 -0
- package/docs/ja/doc/doc-key-findings.md +61 -0
- package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
- package/docs/ja/doc/doc-notes-and-sources.md +43 -0
- package/docs/ja/doc/doc-report-header.md +46 -0
- package/docs/ja/page-types/slide.md +1 -0
- package/docs/ja/pages-and-metadata.md +15 -0
- package/docs/ja/parts/area-chart.md +49 -25
- package/docs/ja/parts/bar-chart.md +50 -24
- package/docs/ja/parts/bar-list.md +31 -8
- package/docs/ja/parts/chart-frame.md +36 -0
- package/docs/ja/parts/delta.md +5 -4
- package/docs/ja/parts/donut-chart.md +45 -26
- package/docs/ja/parts/funnel-steps.md +27 -8
- package/docs/ja/parts/heatmap.md +45 -0
- package/docs/ja/parts/kpi-card.md +2 -1
- package/docs/ja/parts/legend.md +36 -0
- package/docs/ja/parts/line-chart.md +48 -22
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -0
- package/docs/ja/parts/updated-at.md +27 -0
- package/docs/ja/parts/waterfall-chart.md +61 -0
- package/docs/ja/slide/slide-chart-full.md +48 -0
- package/docs/ja/slide/slide-chart-insight.md +52 -0
- package/docs/ja/slide/slide-columns.md +46 -0
- package/docs/ja/slide/slide-comparison.md +68 -0
- package/docs/ja/slide/slide-cover.md +39 -0
- package/docs/ja/slide/slide-exec-summary.md +47 -0
- package/docs/ja/slide/slide-heading.md +38 -0
- package/docs/ja/slide/slide-kpi-grid.md +55 -0
- package/docs/ja/slide/slide-kpi-hero.md +58 -0
- package/docs/ja/slide/slide-next-actions.md +49 -0
- package/docs/ja/slide/slide-scorecard-table.md +54 -0
- package/docs/ja/slide/slide-section.md +44 -0
- package/docs/ja/slide/slide-source-line.md +34 -0
- package/docs/ja/ui/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- package/docs/ja/ui/loading.md +1 -1
- package/docs/ja/ui/toggle-group.md +4 -3
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +389 -104
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +386 -104
- package/registry/ui/bar-list.tsx +376 -94
- package/registry/ui/box-plot-chart.tsx +450 -116
- package/registry/ui/category-bar.tsx +292 -57
- package/registry/ui/chart-frame.tsx +301 -63
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +181 -19
- package/registry/ui/donut-chart.tsx +385 -103
- package/registry/ui/echart.tsx +313 -58
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +338 -71
- package/registry/ui/histogram-chart.tsx +440 -116
- package/registry/ui/kpi-card.tsx +254 -48
- package/registry/ui/legend.tsx +374 -93
- package/registry/ui/line-chart.tsx +389 -104
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +148 -10
- package/registry/ui/paired-chart.tsx +394 -109
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +154 -10
- package/registry/ui/scatter-chart.tsx +436 -119
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +201 -36
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +214 -42
- package/registry/ui/trend-indicator.tsx +151 -8
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +460 -121
- package/skills/vantage-add-feature/SKILL.md +5 -0
- package/skills/vantage-design/SKILL.md +54 -54
- package/skills/vantage-design/references/extract.md +26 -27
- package/skills/vantage-design/references/format.md +12 -172
- package/skills/vantage-design/references/presets.md +50 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +35 -18
- package/templates/agents/api.md +7 -0
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +59 -2
- package/templates/agents/slide.md +77 -3
- package/theme.css +23 -19
- package/dist/chunk-2ICNHUG6.js.map +0 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-WAMZWZJB.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
package/registry/ui/tracker.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{ char: "万", size: 1e4 },
|
|
145
|
-
] as const
|
|
232
|
+
interface CompactUnit {
|
|
233
|
+
token: string
|
|
234
|
+
size: number
|
|
235
|
+
}
|
|
146
236
|
|
|
147
|
-
const
|
|
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
|
|
152
|
-
* Only the
|
|
153
|
-
* any other format —
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
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 =
|
|
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.
|
|
173
|
-
return text.replace(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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> は
|
|
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
|
|
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 ?
|
|
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}
|
|
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}: ` : ""}
|
|
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={
|
|
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-
|
|
23
|
-
negative: "text-
|
|
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
|
|
55
|
-
|
|
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
|