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