@squadbase/vantage 0.6.2-dev1.48b34e1 → 0.6.2-dev1.56da87f
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/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
- 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-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
- package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/components/index.d.ts +49 -29
- package/dist/components/index.js +5 -5
- package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
- package/dist/doc/index.d.ts +14 -14
- package/dist/doc/index.js +198 -84
- package/dist/doc/index.js.map +1 -1
- package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
- package/dist/slide/index.d.ts +2 -2
- package/dist/slide/index.js +95 -44
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +6 -6
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +5 -5
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +344 -85
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +341 -85
- package/registry/ui/bar-list.tsx +332 -77
- package/registry/ui/box-plot-chart.tsx +405 -97
- package/registry/ui/category-bar.tsx +248 -40
- package/registry/ui/chart-frame.tsx +257 -46
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +171 -12
- package/registry/ui/donut-chart.tsx +339 -84
- package/registry/ui/echart.tsx +142 -4
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +293 -52
- package/registry/ui/histogram-chart.tsx +395 -97
- package/registry/ui/kpi-card.tsx +245 -44
- package/registry/ui/legend.tsx +330 -76
- package/registry/ui/line-chart.tsx +344 -85
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +141 -3
- package/registry/ui/paired-chart.tsx +349 -90
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +153 -9
- package/registry/ui/scatter-chart.tsx +391 -100
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +193 -28
- package/registry/ui/tracker.tsx +205 -39
- package/registry/ui/trend-indicator.tsx +149 -6
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +415 -102
- package/skills/vantage-add-feature/SKILL.md +5 -0
- package/templates/AGENTS.md +3 -2
- package/templates/agents/api.md +7 -0
- package/templates/agents/page.md +36 -0
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-57GHR5LN.js.map +0 -1
- package/dist/chunk-6OTTGAPO.js.map +0 -1
- package/dist/chunk-JXFCSF3K.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-QOXXOFGL.js.map +0 -1
package/registry/ui/kpi-card.tsx
CHANGED
|
@@ -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).
|
|
@@ -245,7 +379,72 @@ const KPI_FOOTER_CLASS = "flex-col items-start gap-2 rounded-none border-t-0 bg-
|
|
|
245
379
|
|
|
246
380
|
const PLACEHOLDER = /\{(\w+)\}/g
|
|
247
381
|
|
|
248
|
-
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
|
+
}
|
|
249
448
|
|
|
250
449
|
const isMissing = (v: string | null | undefined) => v === null || v === undefined || v === ""
|
|
251
450
|
|
|
@@ -306,11 +505,11 @@ function isProduction(): boolean {
|
|
|
306
505
|
}
|
|
307
506
|
}
|
|
308
507
|
|
|
309
|
-
function warnShallowSummary(template: string) {
|
|
508
|
+
function warnShallowSummary(template: string, t: KpiText) {
|
|
310
509
|
if (shallowSummaryWarned || isProduction() || !isShallowSummary(template)) return
|
|
311
510
|
shallowSummaryWarned = true
|
|
312
511
|
console.warn(
|
|
313
|
-
`[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}").`,
|
|
314
513
|
)
|
|
315
514
|
}
|
|
316
515
|
|
|
@@ -323,7 +522,7 @@ export function KpiCard({
|
|
|
323
522
|
label,
|
|
324
523
|
value,
|
|
325
524
|
format,
|
|
326
|
-
locale
|
|
525
|
+
locale: localeProp,
|
|
327
526
|
comparison: comparisonProp,
|
|
328
527
|
comparisons,
|
|
329
528
|
provisional,
|
|
@@ -334,7 +533,7 @@ export function KpiCard({
|
|
|
334
533
|
showPrevious = true,
|
|
335
534
|
summary,
|
|
336
535
|
target,
|
|
337
|
-
targetLabel
|
|
536
|
+
targetLabel,
|
|
338
537
|
targetDisplay,
|
|
339
538
|
period,
|
|
340
539
|
history,
|
|
@@ -350,10 +549,12 @@ export function KpiCard({
|
|
|
350
549
|
className,
|
|
351
550
|
...props
|
|
352
551
|
}: KpiCardProps) {
|
|
552
|
+
const locale = useLocale(localeProp)
|
|
553
|
+
const t = TEXT[uiLanguage(locale)]
|
|
353
554
|
const allComparisons = [...(comparisonProp ? [comparisonProp] : []), ...(comparisons ?? [])]
|
|
354
555
|
const comparison = allComparisons[0]
|
|
355
556
|
const isProvisional = provisional !== undefined && provisional !== false
|
|
356
|
-
const provisionalLabel = typeof provisional === "object" && provisional.label ? provisional.label :
|
|
557
|
+
const provisionalLabel = typeof provisional === "object" && provisional.label ? provisional.label : t.provisional
|
|
357
558
|
const sample = useSampleMark()
|
|
358
559
|
// `betterWhen` is the documented name and wins over the older `polarity`.
|
|
359
560
|
const polarity: Polarity | undefined = betterWhen ? (betterWhen === "down" ? "lower-is-better" : "higher-is-better") : polarityProp
|
|
@@ -363,15 +564,13 @@ export function KpiCard({
|
|
|
363
564
|
const stacked = allComparisons.length > 1
|
|
364
565
|
const hasTarget = target !== undefined
|
|
365
566
|
const grain = comparison?.grain ?? historyGrain ?? inferPeriodGrain(period)
|
|
366
|
-
const record = isProvisional || neutral ? null : detectRecord(history, historyGrain ?? grain, polarity)
|
|
567
|
+
const record = isProvisional || neutral ? null : detectRecord(history, historyGrain ?? grain, polarity, locale)
|
|
367
568
|
const sourceParts = source
|
|
368
569
|
? [
|
|
369
570
|
source.period !== undefined ? formatDatesInText(source.period, locale) : undefined,
|
|
370
|
-
source.count !== undefined ?
|
|
571
|
+
source.count !== undefined ? t.countFrom(formatValue(source.count, "number", locale)) : undefined,
|
|
371
572
|
source.excluded !== undefined && Number.isFinite(source.excluded) && source.excluded > 0
|
|
372
|
-
?
|
|
373
|
-
? `${formatValue(source.excluded, "number", locale)}件を除外`
|
|
374
|
-
: `${formatValue(source.excluded, "number", locale)} excluded`
|
|
573
|
+
? t.excluded(formatValue(source.excluded, "number", locale))
|
|
375
574
|
: undefined,
|
|
376
575
|
source.name,
|
|
377
576
|
source.note,
|
|
@@ -386,7 +585,7 @@ export function KpiCard({
|
|
|
386
585
|
const lower = polarity === "lower-is-better"
|
|
387
586
|
// The bar reads "how far toward the target"; a rate (points) or a lower-is-better target has no such fill.
|
|
388
587
|
const targetBar = hasTarget && !neutral && !byPoints && !lower
|
|
389
|
-
if (summary !== undefined) warnShallowSummary(summary)
|
|
588
|
+
if (summary !== undefined) warnShallowSummary(summary, t)
|
|
390
589
|
// Two rows of the parent grid (body, footer), shared by subgrid; a plain two-row grid when standalone.
|
|
391
590
|
const cardClass = cn("row-span-2 grid content-start gap-y-0 grid-rows-subgrid", className)
|
|
392
591
|
const valueSize = small ? "text-xl @[218px]/card:text-2xl" : "text-2xl @[218px]/card:text-4xl"
|
|
@@ -449,14 +648,14 @@ export function KpiCard({
|
|
|
449
648
|
/** An unsigned distance in the metric's unit — points for a rate. */
|
|
450
649
|
const fmtDistance = (v: number) => (isRate ? formatPoints(v, locale, { signDisplay: "never" }) : fmt(v))
|
|
451
650
|
const pct = (v: number) => formatValue(v, { type: "percent", digits: 0 }, locale)
|
|
452
|
-
const tl = targetLabel
|
|
651
|
+
const tl = targetLabel ?? t.target
|
|
453
652
|
const display = change ?? defaultChangeDisplay(format)
|
|
454
653
|
// Value digits only carry over to a change in points; a relative change keeps its own fixed digits.
|
|
455
654
|
const digits =
|
|
456
655
|
display === "points" && typeof format === "object" && format !== null && "digits" in format ? format.digits : undefined
|
|
457
656
|
const graded = comparison ? { ...comparison, grain } : undefined
|
|
458
|
-
const result = graded ? computeComparison(value, graded, { polarity, neutralBand }) : null
|
|
459
|
-
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
|
|
460
659
|
const progress = hasTarget && !neutral ? computeTargetProgress(value, target, { period, now, polarity }) : null
|
|
461
660
|
// A linear pace only makes sense for an accumulating total, not for a rate.
|
|
462
661
|
const projectedValue =
|
|
@@ -484,7 +683,7 @@ export function KpiCard({
|
|
|
484
683
|
: null,
|
|
485
684
|
remaining: progress ? fmtDistance(progress.remaining) : null,
|
|
486
685
|
},
|
|
487
|
-
result && result.direction === "none" && baseline ?
|
|
686
|
+
result && result.direction === "none" && baseline ? t.noBaseline(baseline) : null,
|
|
488
687
|
)
|
|
489
688
|
: null
|
|
490
689
|
|
|
@@ -496,9 +695,9 @@ export function KpiCard({
|
|
|
496
695
|
const projected = projectedValue !== null && projectedValue > actual ? projectedValue : actual
|
|
497
696
|
const scale = Math.max(target as number, actual, projected)
|
|
498
697
|
const at = (v: number) => (scale > 0 ? (v / scale) * 100 : 0)
|
|
499
|
-
const parts = [
|
|
698
|
+
const parts = [t.barLead(tl, pct(progress!.achievement))]
|
|
500
699
|
if (projectedValue !== null && progress!.projected != null)
|
|
501
|
-
parts.push(
|
|
700
|
+
parts.push(t.pace(formatValue(progress!.projected, { type: "percent", digits: 0 }, locale)))
|
|
502
701
|
return {
|
|
503
702
|
actualAt: at(actual),
|
|
504
703
|
projectedAt: at(projected),
|
|
@@ -514,23 +713,23 @@ export function KpiCard({
|
|
|
514
713
|
else if (isProvisional)
|
|
515
714
|
targetText = progress.achieved
|
|
516
715
|
? byPoints
|
|
517
|
-
?
|
|
518
|
-
:
|
|
716
|
+
? t.provisionalReached(tl, fmt(target))
|
|
717
|
+
: t.provisionalSoFar(tl, fmt(target), pct(progress.achievement))
|
|
519
718
|
: byPoints
|
|
520
|
-
?
|
|
521
|
-
:
|
|
719
|
+
? t.provisionalToGo(tl, fmt(target), fmtDistance(progress.remaining))
|
|
720
|
+
: t.provisionalToGoRate(tl, fmtDistance(progress.remaining), pct(progress.achievement))
|
|
522
721
|
else if (lower)
|
|
523
722
|
targetText = progress.achieved
|
|
524
|
-
?
|
|
525
|
-
:
|
|
723
|
+
? t.met(tl, fmt(target))
|
|
724
|
+
: t.toGo(tl, fmt(target), fmtDistance(progress.remaining))
|
|
526
725
|
else if (byPoints)
|
|
527
726
|
targetText = progress.achieved
|
|
528
|
-
?
|
|
529
|
-
:
|
|
727
|
+
? t.reached(tl, fmt(target))
|
|
728
|
+
: t.toGo(tl, fmt(target), fmtDistance(progress.remaining))
|
|
530
729
|
else
|
|
531
730
|
targetText = progress.achieved
|
|
532
|
-
?
|
|
533
|
-
:
|
|
731
|
+
? t.reachedRate(tl, fmt(target), pct(progress.achievement))
|
|
732
|
+
: t.toGoRate(tl, fmtDistance(progress.remaining), pct(progress.achievement))
|
|
534
733
|
}
|
|
535
734
|
|
|
536
735
|
const hasFooter =
|
|
@@ -595,7 +794,7 @@ export function KpiCard({
|
|
|
595
794
|
))}
|
|
596
795
|
{hasPrevious && comparison && (
|
|
597
796
|
<div data-slot="kpi-card-previous" className="col-span-full text-sm text-muted-foreground tabular-nums">
|
|
598
|
-
{result?.previous != null ?
|
|
797
|
+
{result?.previous != null ? t.previous(baseline ?? "", fmt(result.previous)) : t.noPrevious(baseline ?? "")}
|
|
599
798
|
</div>
|
|
600
799
|
)}
|
|
601
800
|
{bar && progress && (
|
|
@@ -603,11 +802,11 @@ export function KpiCard({
|
|
|
603
802
|
<div
|
|
604
803
|
data-slot="kpi-card-target-bar"
|
|
605
804
|
role="progressbar"
|
|
606
|
-
aria-label={
|
|
805
|
+
aria-label={t.progressLabel(tl)}
|
|
607
806
|
aria-valuemin={0}
|
|
608
807
|
aria-valuemax={target}
|
|
609
808
|
aria-valuenow={Math.min(Math.max(value as number, 0), target as number)}
|
|
610
|
-
aria-valuetext={
|
|
809
|
+
aria-valuetext={t.progressValue(tl, fmt(target), fmt(value), pct(progress.achievement))}
|
|
611
810
|
className="relative h-2 w-full overflow-hidden rounded-full bg-muted"
|
|
612
811
|
>
|
|
613
812
|
<div className="absolute inset-y-0 left-0 bg-chart-1" style={{ width: `${bar.actualAt}%` }} />
|
|
@@ -626,16 +825,17 @@ export function KpiCard({
|
|
|
626
825
|
</div>
|
|
627
826
|
<p
|
|
628
827
|
data-slot="kpi-card-target"
|
|
629
|
-
title={[bar.lead, bar.pace].filter(Boolean).join(
|
|
828
|
+
title={[bar.lead, bar.pace].filter(Boolean).join(t.joiner)}
|
|
630
829
|
className="truncate text-sm text-muted-foreground tabular-nums"
|
|
631
830
|
>
|
|
632
831
|
{bar.lead}
|
|
633
832
|
{bar.pace && (
|
|
634
833
|
<>
|
|
635
|
-
|
|
834
|
+
{t.joiner}
|
|
835
|
+
<span data-slot="kpi-card-projection" data-estimate="">{bar.pace}</span>
|
|
636
836
|
</>
|
|
637
837
|
)}
|
|
638
|
-
{isProvisional &&
|
|
838
|
+
{isProvisional && t.provisionalSuffix}
|
|
639
839
|
</p>
|
|
640
840
|
</div>
|
|
641
841
|
)}
|
|
@@ -660,15 +860,15 @@ export function KpiCard({
|
|
|
660
860
|
{sourceParts.length > 0 && (
|
|
661
861
|
<p
|
|
662
862
|
data-slot="kpi-card-source"
|
|
663
|
-
title={sourceParts.join(
|
|
863
|
+
title={sourceParts.join(t.joiner)}
|
|
664
864
|
className="line-clamp-2 w-full text-xs leading-normal text-muted-foreground tabular-nums"
|
|
665
865
|
>
|
|
666
|
-
{sourceParts.join(
|
|
866
|
+
{sourceParts.join(t.joiner)}
|
|
667
867
|
</p>
|
|
668
868
|
)}
|
|
669
869
|
{hasUpdated && (
|
|
670
870
|
<p data-slot="kpi-card-updated" className="text-xs leading-normal text-muted-foreground">
|
|
671
|
-
{updatedAtText(updatedAt, updatedAtDisplay, locale, now, timeZone)}
|
|
871
|
+
{updatedAtText(updatedAt, updatedAtDisplay, locale, t, now, timeZone)}
|
|
672
872
|
</p>
|
|
673
873
|
)}
|
|
674
874
|
{showSparkline && (
|
|
@@ -679,7 +879,7 @@ export function KpiCard({
|
|
|
679
879
|
height={SPARKLINE_HEIGHT}
|
|
680
880
|
color="text-chart-1"
|
|
681
881
|
highlight="latest"
|
|
682
|
-
aria-label={
|
|
882
|
+
aria-label={t.history(label, historyPoints.length)}
|
|
683
883
|
/>
|
|
684
884
|
</div>
|
|
685
885
|
)}
|
|
@@ -689,16 +889,17 @@ export function KpiCard({
|
|
|
689
889
|
)
|
|
690
890
|
}
|
|
691
891
|
|
|
692
|
-
/** 「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" など)。 */
|
|
693
893
|
function updatedAtText(
|
|
694
894
|
updatedAt: Date | string | number,
|
|
695
895
|
display: "relative" | "absolute" | "both",
|
|
696
896
|
locale: string,
|
|
897
|
+
t: KpiText,
|
|
697
898
|
now: Date | number | undefined,
|
|
698
899
|
timeZone?: string,
|
|
699
900
|
): string {
|
|
700
901
|
const relative = formatRelativeTime(updatedAt, { now, locale })
|
|
701
|
-
if (display === "relative") return
|
|
902
|
+
if (display === "relative") return t.updatedRelative(relative)
|
|
702
903
|
const absolute = formatDate(updatedAt, "datetime", locale, { timeZone })
|
|
703
|
-
return display === "absolute" ?
|
|
904
|
+
return display === "absolute" ? t.updatedAbsolute(absolute) : t.updatedBoth(absolute, relative)
|
|
704
905
|
}
|