@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
|
@@ -11,9 +11,100 @@ import { forwardRef, useEffect, useState, type ComponentProps } from "react"
|
|
|
11
11
|
|
|
12
12
|
import { cn } from "@squadbase/vantage/ui"
|
|
13
13
|
import type { ChartColor } from "@squadbase/vantage/components"
|
|
14
|
-
import {
|
|
14
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
15
15
|
import type { ChartColors, ChartSeriesColor, ChartTone, ValueFormatter } from "@squadbase/vantage/components"
|
|
16
16
|
|
|
17
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The display language of the parts and the defaults that follow it.
|
|
21
|
+
*
|
|
22
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
23
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
24
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
25
|
+
*
|
|
26
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
27
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
31
|
+
type UiLanguage = "ja" | "en"
|
|
32
|
+
|
|
33
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
34
|
+
const FALLBACK_LOCALE = "en-US"
|
|
35
|
+
|
|
36
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
37
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
38
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
42
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
43
|
+
const value = tag?.trim()
|
|
44
|
+
if (!value) return FALLBACK_LOCALE
|
|
45
|
+
try {
|
|
46
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
47
|
+
} catch {
|
|
48
|
+
return FALLBACK_LOCALE
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
54
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
55
|
+
*/
|
|
56
|
+
function documentLocale(): string {
|
|
57
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
58
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
62
|
+
function resolveLocale(locale?: string | null): string {
|
|
63
|
+
return locale || documentLocale()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Picks the wording table of the locale's language. */
|
|
67
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
68
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
72
|
+
function defaultCurrency(locale?: string | null): string {
|
|
73
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
77
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
78
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
82
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
83
|
+
|
|
84
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
85
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
89
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
90
|
+
* Monday for the rest.
|
|
91
|
+
*/
|
|
92
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
93
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
94
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
95
|
+
try {
|
|
96
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
97
|
+
getWeekInfo?: () => { firstDay: number }
|
|
98
|
+
weekInfo?: { firstDay: number }
|
|
99
|
+
}
|
|
100
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
101
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
102
|
+
} catch {
|
|
103
|
+
// Fall through to the table below.
|
|
104
|
+
}
|
|
105
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
106
|
+
}
|
|
107
|
+
|
|
17
108
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
18
109
|
|
|
19
110
|
const ROOT_PROP_KEYS = new Set(["id", "style", "role", "tabIndex", "lang", "hidden"])
|
|
@@ -121,9 +212,10 @@ function resolveValueFormatter(
|
|
|
121
212
|
function resolveAxisFormatter(
|
|
122
213
|
valueFormatter: ValueFormatter | undefined,
|
|
123
214
|
format: ValueFormat | undefined,
|
|
124
|
-
|
|
215
|
+
localeArg?: string,
|
|
125
216
|
): ValueFormatter {
|
|
126
217
|
if (valueFormatter) return valueFormatter
|
|
218
|
+
const locale = resolveLocale(localeArg)
|
|
127
219
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
128
220
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
129
221
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -132,40 +224,75 @@ function resolveAxisFormatter(
|
|
|
132
224
|
return (value) => formatValue(value, short, locale)
|
|
133
225
|
}
|
|
134
226
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{ char: "万", size: 1e4 },
|
|
140
|
-
] as const
|
|
227
|
+
interface CompactUnit {
|
|
228
|
+
token: string
|
|
229
|
+
size: number
|
|
230
|
+
}
|
|
141
231
|
|
|
142
|
-
const
|
|
232
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
236
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
237
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
238
|
+
*/
|
|
239
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
240
|
+
const cached = compactUnitCache.get(locale)
|
|
241
|
+
if (cached !== undefined) return cached
|
|
242
|
+
let units: CompactUnit[] | null = []
|
|
243
|
+
try {
|
|
244
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
245
|
+
const seen = new Set<string>()
|
|
246
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
247
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
248
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
249
|
+
if (at === -1) continue
|
|
250
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
251
|
+
units = null
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
const token = parts[at]!.value
|
|
255
|
+
if (seen.has(token)) continue
|
|
256
|
+
seen.add(token)
|
|
257
|
+
units.push({ token, size: 10 ** exp })
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
units = null
|
|
261
|
+
}
|
|
262
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
263
|
+
compactUnitCache.set(locale, sorted)
|
|
264
|
+
return sorted
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
143
268
|
|
|
144
269
|
/**
|
|
145
270
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
146
|
-
* that largest tick, so
|
|
147
|
-
* Only the
|
|
148
|
-
* any other format —
|
|
271
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
272
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
273
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
274
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
149
275
|
*/
|
|
150
276
|
function resolveAxisTickFormatter(
|
|
151
277
|
valueFormatter: ValueFormatter | undefined,
|
|
152
278
|
format: ValueFormat | undefined,
|
|
153
|
-
|
|
279
|
+
localeArg?: string,
|
|
154
280
|
): (top: number) => ValueFormatter {
|
|
281
|
+
const locale = resolveLocale(localeArg)
|
|
155
282
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
156
283
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (!aligns) return () => base
|
|
284
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
285
|
+
if (!units) return () => base
|
|
160
286
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
287
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
161
288
|
return (top) => {
|
|
162
|
-
const unit =
|
|
289
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
163
290
|
if (!unit) return base
|
|
164
291
|
return (value) => {
|
|
165
292
|
const text = base(value)
|
|
166
293
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
167
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
168
|
-
return text.replace(
|
|
294
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
295
|
+
return text.replace(numberToken, () => scaled)
|
|
169
296
|
}
|
|
170
297
|
}
|
|
171
298
|
}
|
|
@@ -194,8 +321,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
194
321
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
195
322
|
const MAX_CHART_SERIES = 5
|
|
196
323
|
|
|
197
|
-
/** The series name the folded categories get. */
|
|
198
|
-
const
|
|
324
|
+
/** The series name the folded categories get, by language. */
|
|
325
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
326
|
+
|
|
327
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
328
|
+
function otherSeriesLabel(locale?: string): string {
|
|
329
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
333
|
+
|
|
334
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
335
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
336
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
337
|
+
}
|
|
199
338
|
|
|
200
339
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
201
340
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -215,7 +354,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
215
354
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
216
355
|
items: readonly Item[],
|
|
217
356
|
n: number = MAX_CHART_SERIES,
|
|
218
|
-
otherLabel: string =
|
|
357
|
+
otherLabel: string = otherSeriesLabel(),
|
|
219
358
|
): AggregatedItems<Item> {
|
|
220
359
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
221
360
|
const ranked = items
|
|
@@ -244,7 +383,7 @@ function aggregateSeries(
|
|
|
244
383
|
data: readonly Record<string, unknown>[],
|
|
245
384
|
categories: readonly string[],
|
|
246
385
|
n: number = MAX_CHART_SERIES,
|
|
247
|
-
otherLabel: string =
|
|
386
|
+
otherLabel: string = otherSeriesLabel(),
|
|
248
387
|
): AggregatedSeries {
|
|
249
388
|
const totals = categories.map((name) => ({
|
|
250
389
|
name,
|
|
@@ -276,12 +415,14 @@ function aggregateSeriesKeeping(
|
|
|
276
415
|
categories: readonly string[],
|
|
277
416
|
keep: string | undefined,
|
|
278
417
|
n: number = MAX_CHART_SERIES,
|
|
418
|
+
otherLabel: string = otherSeriesLabel(),
|
|
279
419
|
): AggregatedSeries {
|
|
280
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
420
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
281
421
|
const aggregated = aggregateSeries(
|
|
282
422
|
data,
|
|
283
423
|
categories.filter((c) => c !== keep),
|
|
284
424
|
n,
|
|
425
|
+
otherLabel,
|
|
285
426
|
)
|
|
286
427
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
287
428
|
}
|
|
@@ -317,7 +458,7 @@ function seriesColorsWithComparison(
|
|
|
317
458
|
* Colour per drawn series: the record's token for that category, else the
|
|
318
459
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
319
460
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
320
|
-
* data itself names
|
|
461
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
321
462
|
*/
|
|
322
463
|
function seriesColorsFor(
|
|
323
464
|
categories: readonly string[],
|
|
@@ -329,7 +470,7 @@ function seriesColorsFor(
|
|
|
329
470
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
330
471
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
331
472
|
return categories.map((category, i) => {
|
|
332
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
473
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
333
474
|
const fromRecord = record?.[category]
|
|
334
475
|
if (fromRecord) return fromRecord
|
|
335
476
|
if (list && list.length > 0) {
|
|
@@ -376,7 +517,7 @@ interface DonutSlicesConfig {
|
|
|
376
517
|
*/
|
|
377
518
|
function donutSlices(
|
|
378
519
|
data: readonly Record<string, unknown>[],
|
|
379
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
520
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
380
521
|
): DonutSlice[] {
|
|
381
522
|
const items = data
|
|
382
523
|
.map((row, i) => {
|
|
@@ -389,7 +530,8 @@ function donutSlices(
|
|
|
389
530
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
390
531
|
name: item.name,
|
|
391
532
|
value: item.value,
|
|
392
|
-
|
|
533
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
534
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
393
535
|
isOther: false,
|
|
394
536
|
row: item.row,
|
|
395
537
|
}))
|
|
@@ -462,7 +604,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
462
604
|
*/
|
|
463
605
|
function barListRows<Row extends object>(
|
|
464
606
|
data: readonly Row[],
|
|
465
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
607
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
466
608
|
): BarListRow<Row>[] {
|
|
467
609
|
const items = data.map((row): BarListRow<Row> => {
|
|
468
610
|
const cells = row as Record<string, unknown>
|
|
@@ -508,19 +650,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
508
650
|
|
|
509
651
|
/**
|
|
510
652
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
511
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
653
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
512
654
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
513
655
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
514
656
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
515
657
|
* keeps its contrast.
|
|
516
658
|
*/
|
|
517
659
|
function barListFills(
|
|
518
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
660
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
519
661
|
color: ChartColor,
|
|
520
662
|
otherColor: string,
|
|
521
663
|
): string[] {
|
|
522
664
|
return rows.map((row) =>
|
|
523
|
-
row.isOther
|
|
665
|
+
row.isOther || isOtherName(row.name)
|
|
666
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
667
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
524
668
|
)
|
|
525
669
|
}
|
|
526
670
|
|
|
@@ -736,11 +880,30 @@ interface ChartSummaryInput {
|
|
|
736
880
|
format?: ValueFormat
|
|
737
881
|
/** Wins over `format`. */
|
|
738
882
|
valueFormatter?: ValueFormatter
|
|
883
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
884
|
+
locale?: string
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const SUMMARY_TEXT = {
|
|
888
|
+
ja: {
|
|
889
|
+
up: "、前の点より増加",
|
|
890
|
+
down: "、前の点より減少",
|
|
891
|
+
flat: "、前の点と横ばい",
|
|
892
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
893
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
894
|
+
},
|
|
895
|
+
en: {
|
|
896
|
+
up: ", up from the previous point",
|
|
897
|
+
down: ", down from the previous point",
|
|
898
|
+
flat: ", flat from the previous point",
|
|
899
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
900
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
901
|
+
},
|
|
739
902
|
}
|
|
740
903
|
|
|
741
904
|
/**
|
|
742
905
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
743
|
-
* 「<title>: 最新 <index> は
|
|
906
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
744
907
|
* computed from the last two non-empty points of the first series — never
|
|
745
908
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
746
909
|
* no usable point.
|
|
@@ -756,26 +919,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
756
919
|
}
|
|
757
920
|
const latest = points[points.length - 1]
|
|
758
921
|
if (!latest) return undefined
|
|
759
|
-
const
|
|
922
|
+
const locale = resolveLocale(input.locale)
|
|
923
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
924
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
760
925
|
const prev = points[points.length - 2]
|
|
761
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
926
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
762
927
|
const head = input.title ? `${input.title}: ` : ""
|
|
763
|
-
return `${head}
|
|
928
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
764
929
|
}
|
|
765
930
|
|
|
766
931
|
/**
|
|
767
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
768
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
932
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
933
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
769
934
|
*/
|
|
770
935
|
function buildShareSummary(
|
|
771
936
|
slices: readonly { name: string; value: number }[],
|
|
772
937
|
title?: string,
|
|
938
|
+
locale?: string,
|
|
773
939
|
): string | undefined {
|
|
774
940
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
775
941
|
const top = slices[0]
|
|
776
942
|
if (!top || total <= 0) return undefined
|
|
777
943
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
778
|
-
return `${title ? `${title}: ` : ""}
|
|
944
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
779
945
|
}
|
|
780
946
|
|
|
781
947
|
/* ---------- end of inlined code ---------- */
|
|
@@ -28,9 +28,151 @@ import { createContext, useContext, useMemo, type HTMLAttributes, type ReactNode
|
|
|
28
28
|
|
|
29
29
|
import { cn } from "@squadbase/vantage/ui"
|
|
30
30
|
import { Badge } from "@squadbase/vantage/ui"
|
|
31
|
+
import { useSyncExternalStore } from "react"
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The display language of the parts and the defaults that follow it.
|
|
37
|
+
*
|
|
38
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
39
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
40
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
41
|
+
*
|
|
42
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
43
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
47
|
+
type UiLanguage = "ja" | "en"
|
|
48
|
+
|
|
49
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
50
|
+
const FALLBACK_LOCALE = "en-US"
|
|
51
|
+
|
|
52
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
53
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
54
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
58
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
59
|
+
const value = tag?.trim()
|
|
60
|
+
if (!value) return FALLBACK_LOCALE
|
|
61
|
+
try {
|
|
62
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
63
|
+
} catch {
|
|
64
|
+
return FALLBACK_LOCALE
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
70
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
71
|
+
*/
|
|
72
|
+
function documentLocale(): string {
|
|
73
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
74
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
78
|
+
function resolveLocale(locale?: string | null): string {
|
|
79
|
+
return locale || documentLocale()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Picks the wording table of the locale's language. */
|
|
83
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
84
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
88
|
+
function defaultCurrency(locale?: string | null): string {
|
|
89
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
93
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
94
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
98
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
99
|
+
|
|
100
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
101
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
105
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
106
|
+
* Monday for the rest.
|
|
107
|
+
*/
|
|
108
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
109
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
110
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
111
|
+
try {
|
|
112
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
113
|
+
getWeekInfo?: () => { firstDay: number }
|
|
114
|
+
weekInfo?: { firstDay: number }
|
|
115
|
+
}
|
|
116
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
117
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
118
|
+
} catch {
|
|
119
|
+
// Fall through to the table below.
|
|
120
|
+
}
|
|
121
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
125
|
+
|
|
126
|
+
const listeners = new Set<() => void>()
|
|
127
|
+
let observer: MutationObserver | null = null
|
|
128
|
+
|
|
129
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
130
|
+
function subscribe(onChange: () => void): () => void {
|
|
131
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
132
|
+
listeners.add(onChange)
|
|
133
|
+
if (!observer) {
|
|
134
|
+
observer = new MutationObserver(() => {
|
|
135
|
+
for (const listener of [...listeners]) listener()
|
|
136
|
+
})
|
|
137
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
138
|
+
}
|
|
139
|
+
return () => {
|
|
140
|
+
listeners.delete(onChange)
|
|
141
|
+
if (listeners.size === 0 && observer) {
|
|
142
|
+
observer.disconnect()
|
|
143
|
+
observer = null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
152
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
153
|
+
*/
|
|
154
|
+
function useDocumentLocale(): string {
|
|
155
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
159
|
+
function useLocale(locale?: string | null): string {
|
|
160
|
+
const page = useDocumentLocale()
|
|
161
|
+
return locale || page
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ---------- end of inlined code ---------- */
|
|
165
|
+
|
|
166
|
+
const TEXT = {
|
|
167
|
+
ja: { label: "見本の値", title: "実データにまだつながっていない見本の値です" },
|
|
168
|
+
en: { label: "Sample data", title: "Sample values, not connected to real data yet" },
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The mark's wording in English (the fallback language). Without a `label`, the mark follows the page's
|
|
173
|
+
* language (`<html lang>`): ja reads「見本の値」.
|
|
174
|
+
*/
|
|
175
|
+
export const DEFAULT_SAMPLE_LABEL = TEXT.en.label
|
|
34
176
|
|
|
35
177
|
interface SampleState {
|
|
36
178
|
label: string
|
|
@@ -52,32 +194,34 @@ export function SampleClaim({ children }: { children?: ReactNode }) {
|
|
|
52
194
|
}
|
|
53
195
|
|
|
54
196
|
export interface SampleBadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
55
|
-
/**
|
|
197
|
+
/** 印の文言。既定はページの言語で「見本の値」(英語は "Sample data")。 */
|
|
56
198
|
label?: string
|
|
57
199
|
}
|
|
58
200
|
|
|
59
|
-
export function SampleBadge({ label
|
|
201
|
+
export function SampleBadge({ label, className, ...props }: SampleBadgeProps) {
|
|
202
|
+
const t = TEXT[uiLanguage(useLocale())]
|
|
60
203
|
return (
|
|
61
204
|
<Badge
|
|
62
205
|
data-slot="sample-badge"
|
|
63
|
-
title={
|
|
64
|
-
className={cn("border-warning/
|
|
206
|
+
title={t.title}
|
|
207
|
+
className={cn("border-warning/20 dark:border-warning/25 bg-warning-subtle text-warning align-middle", className)}
|
|
65
208
|
{...props}
|
|
66
209
|
>
|
|
67
|
-
{label}
|
|
210
|
+
{label ?? t.label}
|
|
68
211
|
</Badge>
|
|
69
212
|
)
|
|
70
213
|
}
|
|
71
214
|
|
|
72
215
|
export interface SampleDataProps {
|
|
73
|
-
/**
|
|
216
|
+
/** 印の文言。既定はページの言語で「見本の値」(英語は "Sample data")。 */
|
|
74
217
|
label?: string
|
|
75
218
|
children?: ReactNode
|
|
76
219
|
}
|
|
77
220
|
|
|
78
|
-
export function SampleData({ label
|
|
221
|
+
export function SampleData({ label, children }: SampleDataProps) {
|
|
222
|
+
const shown = label ?? TEXT[uiLanguage(useLocale())].label
|
|
79
223
|
// A stable value, so the cards inside do not re-render when only the parent does.
|
|
80
|
-
const state = useMemo(() => ({ label }), [
|
|
224
|
+
const state = useMemo(() => ({ label: shown }), [shown])
|
|
81
225
|
return (
|
|
82
226
|
<div data-slot="sample-data" style={{ display: "contents" }}>
|
|
83
227
|
<SampleContext.Provider value={state}>{children}</SampleContext.Provider>
|