@squadbase/vantage 0.6.2-dev1.54bde93 → 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/{build-A23M4GCH.js → build-U7GO26HR.js} +3 -3
- package/dist/{build-A23M4GCH.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-FBHT6W6J.js → check-MRVAIHG5.js} +3 -3
- package/dist/{check-FBHT6W6J.js.map → check-MRVAIHG5.js.map} +1 -1
- 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-5C2SEDPH.js → chunk-PJ4SPXHS.js} +4 -4
- package/dist/{chunk-5C2SEDPH.js.map → chunk-PJ4SPXHS.js.map} +1 -1
- 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/cli.js +4 -4
- 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/{dev-H4I2JL63.js → dev-O42JFQ64.js} +3 -3
- package/dist/{dev-H4I2JL63.js.map → dev-O42JFQ64.js.map} +1 -1
- 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/{doctor-B2O3GJDU.js → doctor-YD3XAAOT.js} +3 -3
- package/dist/{doctor-B2O3GJDU.js.map → doctor-YD3XAAOT.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/docs/en/agent-skills.md +1 -1
- package/docs/ja/agent-skills.md +1 -1
- 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/skills/vantage-design/SKILL.md +25 -27
- package/skills/vantage-design/references/extract.md +10 -13
- package/skills/vantage-design/references/format.md +12 -173
- package/skills/vantage-design/references/presets.md +0 -10
- package/templates/AGENTS.md +29 -15
- package/templates/agents/api.md +7 -0
- package/templates/agents/page.md +37 -1
- package/templates/agents/slide.md +7 -1
- 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/echart.tsx
CHANGED
|
@@ -58,9 +58,141 @@ import { Check, Copy, Download } from "lucide-react"
|
|
|
58
58
|
import { cn } from "@squadbase/vantage/ui"
|
|
59
59
|
import { Button } from "@squadbase/vantage/ui"
|
|
60
60
|
import type { ChartActionsOptions } from "@squadbase/vantage/components"
|
|
61
|
+
import { useSyncExternalStore } from "react"
|
|
61
62
|
import { createContext, type ForwardedRef, type RefObject } from "react"
|
|
62
63
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
63
64
|
|
|
65
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The display language of the parts and the defaults that follow it.
|
|
69
|
+
*
|
|
70
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
71
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
72
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
73
|
+
*
|
|
74
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
75
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
79
|
+
type UiLanguage = "ja" | "en"
|
|
80
|
+
|
|
81
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
82
|
+
const FALLBACK_LOCALE = "en-US"
|
|
83
|
+
|
|
84
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
85
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
86
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
90
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
91
|
+
const value = tag?.trim()
|
|
92
|
+
if (!value) return FALLBACK_LOCALE
|
|
93
|
+
try {
|
|
94
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
95
|
+
} catch {
|
|
96
|
+
return FALLBACK_LOCALE
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
102
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
103
|
+
*/
|
|
104
|
+
function documentLocale(): string {
|
|
105
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
106
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
110
|
+
function resolveLocale(locale?: string | null): string {
|
|
111
|
+
return locale || documentLocale()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Picks the wording table of the locale's language. */
|
|
115
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
116
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
120
|
+
function defaultCurrency(locale?: string | null): string {
|
|
121
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
125
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
126
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
130
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
131
|
+
|
|
132
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
133
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
137
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
138
|
+
* Monday for the rest.
|
|
139
|
+
*/
|
|
140
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
141
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
142
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
143
|
+
try {
|
|
144
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
145
|
+
getWeekInfo?: () => { firstDay: number }
|
|
146
|
+
weekInfo?: { firstDay: number }
|
|
147
|
+
}
|
|
148
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
149
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
150
|
+
} catch {
|
|
151
|
+
// Fall through to the table below.
|
|
152
|
+
}
|
|
153
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
157
|
+
|
|
158
|
+
const listeners = new Set<() => void>()
|
|
159
|
+
let observer: MutationObserver | null = null
|
|
160
|
+
|
|
161
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
162
|
+
function subscribe(onChange: () => void): () => void {
|
|
163
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
164
|
+
listeners.add(onChange)
|
|
165
|
+
if (!observer) {
|
|
166
|
+
observer = new MutationObserver(() => {
|
|
167
|
+
for (const listener of [...listeners]) listener()
|
|
168
|
+
})
|
|
169
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
170
|
+
}
|
|
171
|
+
return () => {
|
|
172
|
+
listeners.delete(onChange)
|
|
173
|
+
if (listeners.size === 0 && observer) {
|
|
174
|
+
observer.disconnect()
|
|
175
|
+
observer = null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
184
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
185
|
+
*/
|
|
186
|
+
function useDocumentLocale(): string {
|
|
187
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
191
|
+
function useLocale(locale?: string | null): string {
|
|
192
|
+
const page = useDocumentLocale()
|
|
193
|
+
return locale || page
|
|
194
|
+
}
|
|
195
|
+
|
|
64
196
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
65
197
|
|
|
66
198
|
/**
|
|
@@ -594,11 +726,16 @@ export function chartExportHeaderLayout(
|
|
|
594
726
|
return { lines, height: HEADER_PAD_TOP + lines.length * HEADER_LINE_HEIGHT + gap + HEADER_PAD_BOTTOM }
|
|
595
727
|
}
|
|
596
728
|
|
|
729
|
+
const TEXT = {
|
|
730
|
+
ja: { chart: "チャート", copied: "コピーしました", copy: "PNG をクリップボードにコピー", download: "PNG をダウンロード" },
|
|
731
|
+
en: { chart: "Chart", copied: "Copied", copy: "Copy PNG to clipboard", download: "Download PNG" },
|
|
732
|
+
}
|
|
733
|
+
|
|
597
734
|
function loadImage(src: string): Promise<HTMLImageElement> {
|
|
598
735
|
return new Promise((resolve, reject) => {
|
|
599
736
|
const image = new Image()
|
|
600
737
|
image.onload = () => resolve(image)
|
|
601
|
-
image.onerror = () => reject(new Error("PNG
|
|
738
|
+
image.onerror = () => reject(new Error("Couldn't load the PNG"))
|
|
602
739
|
image.src = src
|
|
603
740
|
})
|
|
604
741
|
}
|
|
@@ -854,6 +991,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
854
991
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
855
992
|
const [instance, setInstance] = useState<ECharts | null>(null)
|
|
856
993
|
const [copied, setCopied] = useState(false)
|
|
994
|
+
const t = TEXT[uiLanguage(useLocale())]
|
|
857
995
|
const tokens = useChartTokens(containerRef)
|
|
858
996
|
const reducedMotion = usePrefersReducedMotion()
|
|
859
997
|
// What the current instance last received, to pick the merge mode of the next update.
|
|
@@ -1005,7 +1143,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
1005
1143
|
data-slot="echart-canvas"
|
|
1006
1144
|
className="h-full w-full"
|
|
1007
1145
|
role={inFrame ? undefined : "img"}
|
|
1008
|
-
aria-label={inFrame ? undefined : (ariaLabel ??
|
|
1146
|
+
aria-label={inFrame ? undefined : (ariaLabel ?? t.chart)}
|
|
1009
1147
|
/>
|
|
1010
1148
|
{actions && (
|
|
1011
1149
|
<div
|
|
@@ -1017,7 +1155,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
1017
1155
|
variant="ghost"
|
|
1018
1156
|
size="icon-sm"
|
|
1019
1157
|
onClick={handleCopy}
|
|
1020
|
-
aria-label={copied ?
|
|
1158
|
+
aria-label={copied ? t.copied : t.copy}
|
|
1021
1159
|
>
|
|
1022
1160
|
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
1023
1161
|
</Button>
|
|
@@ -1026,7 +1164,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
1026
1164
|
variant="ghost"
|
|
1027
1165
|
size="icon-sm"
|
|
1028
1166
|
onClick={handleDownload}
|
|
1029
|
-
aria-label=
|
|
1167
|
+
aria-label={t.download}
|
|
1030
1168
|
>
|
|
1031
1169
|
<Download className="size-4" />
|
|
1032
1170
|
</Button>
|
|
@@ -24,6 +24,140 @@ import { Checkbox } from "@squadbase/vantage/ui"
|
|
|
24
24
|
import { Popover, PopoverContent, PopoverTrigger } from "@squadbase/vantage/ui"
|
|
25
25
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@squadbase/vantage/ui"
|
|
26
26
|
import type { FilterConfig, FilterOption, FilterValues } from "@squadbase/vantage/components"
|
|
27
|
+
import { useSyncExternalStore } from "react"
|
|
28
|
+
|
|
29
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The display language of the parts and the defaults that follow it.
|
|
33
|
+
*
|
|
34
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
35
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
36
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
37
|
+
*
|
|
38
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
39
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
43
|
+
type UiLanguage = "ja" | "en"
|
|
44
|
+
|
|
45
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
46
|
+
const FALLBACK_LOCALE = "en-US"
|
|
47
|
+
|
|
48
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
49
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
50
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
54
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
55
|
+
const value = tag?.trim()
|
|
56
|
+
if (!value) return FALLBACK_LOCALE
|
|
57
|
+
try {
|
|
58
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
59
|
+
} catch {
|
|
60
|
+
return FALLBACK_LOCALE
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
66
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
67
|
+
*/
|
|
68
|
+
function documentLocale(): string {
|
|
69
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
70
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
74
|
+
function resolveLocale(locale?: string | null): string {
|
|
75
|
+
return locale || documentLocale()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Picks the wording table of the locale's language. */
|
|
79
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
80
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
84
|
+
function defaultCurrency(locale?: string | null): string {
|
|
85
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
89
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
90
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
94
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
95
|
+
|
|
96
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
97
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
101
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
102
|
+
* Monday for the rest.
|
|
103
|
+
*/
|
|
104
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
105
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
106
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
107
|
+
try {
|
|
108
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
109
|
+
getWeekInfo?: () => { firstDay: number }
|
|
110
|
+
weekInfo?: { firstDay: number }
|
|
111
|
+
}
|
|
112
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
113
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
114
|
+
} catch {
|
|
115
|
+
// Fall through to the table below.
|
|
116
|
+
}
|
|
117
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
121
|
+
|
|
122
|
+
const listeners = new Set<() => void>()
|
|
123
|
+
let observer: MutationObserver | null = null
|
|
124
|
+
|
|
125
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
126
|
+
function subscribe(onChange: () => void): () => void {
|
|
127
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
128
|
+
listeners.add(onChange)
|
|
129
|
+
if (!observer) {
|
|
130
|
+
observer = new MutationObserver(() => {
|
|
131
|
+
for (const listener of [...listeners]) listener()
|
|
132
|
+
})
|
|
133
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
134
|
+
}
|
|
135
|
+
return () => {
|
|
136
|
+
listeners.delete(onChange)
|
|
137
|
+
if (listeners.size === 0 && observer) {
|
|
138
|
+
observer.disconnect()
|
|
139
|
+
observer = null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
148
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
149
|
+
*/
|
|
150
|
+
function useDocumentLocale(): string {
|
|
151
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
155
|
+
function useLocale(locale?: string | null): string {
|
|
156
|
+
const page = useDocumentLocale()
|
|
157
|
+
return locale || page
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ---------- end of inlined code ---------- */
|
|
27
161
|
|
|
28
162
|
/* --------------------------------------------------------------- context */
|
|
29
163
|
|
|
@@ -38,6 +172,25 @@ interface FilterBarContextValue {
|
|
|
38
172
|
|
|
39
173
|
const FilterBarContext = createContext<FilterBarContextValue | null>(null)
|
|
40
174
|
|
|
175
|
+
const TEXT = {
|
|
176
|
+
ja: {
|
|
177
|
+
filters: "フィルター",
|
|
178
|
+
selected: (count: number) => ` (${count}件選択中)`,
|
|
179
|
+
remove: (filter: string, option: string) => `${filter}: ${option} を削除`,
|
|
180
|
+
clear: "クリア",
|
|
181
|
+
clearAll: "すべてのフィルターをクリア",
|
|
182
|
+
},
|
|
183
|
+
en: {
|
|
184
|
+
filters: "Filters",
|
|
185
|
+
selected: (count: number) => ` (${count} selected)`,
|
|
186
|
+
remove: (filter: string, option: string) => `Remove ${filter}: ${option}`,
|
|
187
|
+
clear: "Clear",
|
|
188
|
+
clearAll: "Clear all filters",
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const useText = () => TEXT[uiLanguage(useLocale())]
|
|
193
|
+
|
|
41
194
|
function useFilterBar(): FilterBarContextValue {
|
|
42
195
|
const ctx = useContext(FilterBarContext)
|
|
43
196
|
if (!ctx) throw new Error("useFilterBar must be used within <FilterBar>")
|
|
@@ -55,20 +208,23 @@ export interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "on
|
|
|
55
208
|
}
|
|
56
209
|
|
|
57
210
|
export const FilterBar = forwardRef<HTMLDivElement, FilterBarProps>(
|
|
58
|
-
({ value, onChange, onClear, className, children, ...props }, ref) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
211
|
+
({ value, onChange, onClear, className, children, ...props }, ref) => {
|
|
212
|
+
const t = useText()
|
|
213
|
+
return (
|
|
214
|
+
<FilterBarContext.Provider value={{ value, onChange, onClear }}>
|
|
215
|
+
<div
|
|
216
|
+
ref={ref}
|
|
217
|
+
role="search"
|
|
218
|
+
aria-label={t.filters}
|
|
219
|
+
data-slot="filter-bar"
|
|
220
|
+
className={cn("flex flex-wrap items-center gap-2", className)}
|
|
221
|
+
{...props}
|
|
222
|
+
>
|
|
223
|
+
{children}
|
|
224
|
+
</div>
|
|
225
|
+
</FilterBarContext.Provider>
|
|
226
|
+
)
|
|
227
|
+
},
|
|
72
228
|
)
|
|
73
229
|
FilterBar.displayName = "FilterBar"
|
|
74
230
|
|
|
@@ -151,6 +307,7 @@ export function FilterBarMultiSelect({
|
|
|
151
307
|
className,
|
|
152
308
|
}: FilterBarMultiSelectProps) {
|
|
153
309
|
const { value, onChange } = useFilterBar()
|
|
310
|
+
const t = useText()
|
|
154
311
|
const selected = (value[filterKey] as string[] | undefined) ?? []
|
|
155
312
|
|
|
156
313
|
const handleToggle = (option: string) => {
|
|
@@ -165,7 +322,7 @@ export function FilterBarMultiSelect({
|
|
|
165
322
|
<Popover>
|
|
166
323
|
<PopoverTrigger
|
|
167
324
|
render={<Button variant="outline" size="sm" className={cn("h-8 gap-1", className)} />}
|
|
168
|
-
aria-label={`${placeholder ?? label}${selected.length > 0 ?
|
|
325
|
+
aria-label={`${placeholder ?? label}${selected.length > 0 ? t.selected(selected.length) : ""}`}
|
|
169
326
|
data-slot="filter-bar-multi-select"
|
|
170
327
|
>
|
|
171
328
|
{placeholder ?? label}
|
|
@@ -216,6 +373,7 @@ interface ActiveChip {
|
|
|
216
373
|
|
|
217
374
|
export function FilterBarActiveChips({ filters, className }: FilterBarActiveChipsProps) {
|
|
218
375
|
const { value, onChange } = useFilterBar()
|
|
376
|
+
const t = useText()
|
|
219
377
|
|
|
220
378
|
const getActiveChips = (): ActiveChip[] => {
|
|
221
379
|
const chips: ActiveChip[] = []
|
|
@@ -293,7 +451,7 @@ export function FilterBarActiveChips({ filters, className }: FilterBarActiveChip
|
|
|
293
451
|
type="button"
|
|
294
452
|
className="ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20"
|
|
295
453
|
onClick={() => handleRemoveFilter(chip.key, chip.optionValue)}
|
|
296
|
-
aria-label={
|
|
454
|
+
aria-label={t.remove(chip.filterLabel, chip.optionLabel)}
|
|
297
455
|
>
|
|
298
456
|
<X className="size-3" aria-hidden="true" />
|
|
299
457
|
</button>
|
|
@@ -307,13 +465,14 @@ FilterBarActiveChips.displayName = "FilterBarActiveChips"
|
|
|
307
465
|
/* ----------------------------------------------------------- clear button */
|
|
308
466
|
|
|
309
467
|
export interface FilterBarClearButtonProps {
|
|
310
|
-
/** Override the button label. Defaults to "クリア". */
|
|
468
|
+
/** Override the button label. Defaults to "Clear" (ja: "クリア"). */
|
|
311
469
|
label?: string
|
|
312
470
|
className?: string
|
|
313
471
|
}
|
|
314
472
|
|
|
315
|
-
export function FilterBarClearButton({ label
|
|
473
|
+
export function FilterBarClearButton({ label, className }: FilterBarClearButtonProps) {
|
|
316
474
|
const { value, onClear } = useFilterBar()
|
|
475
|
+
const t = useText()
|
|
317
476
|
const hasActiveFilters = Object.values(value).some((v) =>
|
|
318
477
|
Array.isArray(v) ? v.length > 0 : v !== undefined && v !== "",
|
|
319
478
|
)
|
|
@@ -325,10 +484,10 @@ export function FilterBarClearButton({ label = "クリア", className }: FilterB
|
|
|
325
484
|
size="sm"
|
|
326
485
|
className={cn("h-7 px-2 text-xs", className)}
|
|
327
486
|
onClick={onClear}
|
|
328
|
-
aria-label=
|
|
487
|
+
aria-label={t.clearAll}
|
|
329
488
|
data-slot="filter-bar-clear-button"
|
|
330
489
|
>
|
|
331
|
-
{label}
|
|
490
|
+
{label ?? t.clear}
|
|
332
491
|
</Button>
|
|
333
492
|
)
|
|
334
493
|
}
|