@squadbase/vantage 0.6.2-dev1.54bde93 → 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.
Files changed (87) hide show
  1. package/dist/{build-A23M4GCH.js → build-U7GO26HR.js} +3 -3
  2. package/dist/{build-A23M4GCH.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-FBHT6W6J.js → check-MRVAIHG5.js} +3 -3
  4. package/dist/{check-FBHT6W6J.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
  6. package/dist/chunk-3RJEOP3J.js.map +1 -0
  7. package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
  8. package/dist/chunk-77CISBMG.js.map +1 -0
  9. package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
  10. package/dist/chunk-EG4PX726.js.map +1 -0
  11. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  12. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  13. package/dist/{chunk-5C2SEDPH.js → chunk-PJ4SPXHS.js} +4 -4
  14. package/dist/{chunk-5C2SEDPH.js.map → chunk-PJ4SPXHS.js.map} +1 -1
  15. package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
  16. package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
  17. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  18. package/dist/chunk-ROHNXP7J.js.map +1 -0
  19. package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
  20. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  21. package/dist/cli.js +4 -4
  22. package/dist/components/index.d.ts +49 -29
  23. package/dist/components/index.js +5 -5
  24. package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
  25. package/dist/{dev-H4I2JL63.js → dev-O42JFQ64.js} +3 -3
  26. package/dist/{dev-H4I2JL63.js.map → dev-O42JFQ64.js.map} +1 -1
  27. package/dist/doc/index.d.ts +14 -14
  28. package/dist/doc/index.js +198 -84
  29. package/dist/doc/index.js.map +1 -1
  30. package/dist/{doctor-B2O3GJDU.js → doctor-YD3XAAOT.js} +3 -3
  31. package/dist/{doctor-B2O3GJDU.js.map → doctor-YD3XAAOT.js.map} +1 -1
  32. package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
  33. package/dist/slide/index.d.ts +2 -2
  34. package/dist/slide/index.js +95 -44
  35. package/dist/slide/index.js.map +1 -1
  36. package/dist/table/index.js +6 -6
  37. package/dist/ui/index.d.ts +1 -1
  38. package/dist/ui/index.js +5 -5
  39. package/docs/en/agent-skills.md +1 -1
  40. package/docs/ja/agent-skills.md +1 -1
  41. package/package.json +2 -2
  42. package/registry/ui/app-shell.tsx +138 -1
  43. package/registry/ui/area-chart.tsx +344 -85
  44. package/registry/ui/attention-list.tsx +177 -15
  45. package/registry/ui/bar-chart.tsx +341 -85
  46. package/registry/ui/bar-list.tsx +332 -77
  47. package/registry/ui/box-plot-chart.tsx +405 -97
  48. package/registry/ui/category-bar.tsx +248 -40
  49. package/registry/ui/chart-frame.tsx +257 -46
  50. package/registry/ui/data-table.tsx +229 -37
  51. package/registry/ui/date-range-picker.tsx +190 -36
  52. package/registry/ui/delta.tsx +171 -12
  53. package/registry/ui/donut-chart.tsx +339 -84
  54. package/registry/ui/echart.tsx +142 -4
  55. package/registry/ui/filter-bar.tsx +179 -20
  56. package/registry/ui/funnel-steps.tsx +354 -80
  57. package/registry/ui/heatmap.tsx +293 -52
  58. package/registry/ui/histogram-chart.tsx +395 -97
  59. package/registry/ui/kpi-card.tsx +245 -44
  60. package/registry/ui/legend.tsx +330 -76
  61. package/registry/ui/line-chart.tsx +344 -85
  62. package/registry/ui/multi-select.tsx +159 -9
  63. package/registry/ui/page-shell.tsx +141 -3
  64. package/registry/ui/paired-chart.tsx +349 -90
  65. package/registry/ui/progress-circle.tsx +205 -39
  66. package/registry/ui/sample-data.tsx +153 -9
  67. package/registry/ui/scatter-chart.tsx +391 -100
  68. package/registry/ui/searchable-select.tsx +145 -7
  69. package/registry/ui/states.tsx +170 -13
  70. package/registry/ui/status-badge.tsx +193 -28
  71. package/registry/ui/tracker.tsx +205 -39
  72. package/registry/ui/trend-indicator.tsx +149 -6
  73. package/registry/ui/updated-at.tsx +170 -16
  74. package/registry/ui/waterfall-chart.tsx +415 -102
  75. package/skills/vantage-design/SKILL.md +25 -27
  76. package/skills/vantage-design/references/extract.md +10 -13
  77. package/skills/vantage-design/references/format.md +12 -173
  78. package/skills/vantage-design/references/presets.md +0 -10
  79. package/templates/AGENTS.md +29 -15
  80. package/templates/agents/page.md +1 -1
  81. package/templates/agents/slide.md +7 -1
  82. package/dist/chunk-2N72FKLB.js.map +0 -1
  83. package/dist/chunk-57GHR5LN.js.map +0 -1
  84. package/dist/chunk-6OTTGAPO.js.map +0 -1
  85. package/dist/chunk-JXFCSF3K.js.map +0 -1
  86. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  87. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -14,11 +14,143 @@ import { cn } from "@squadbase/vantage/ui"
14
14
  import { Button } from "@squadbase/vantage/ui"
15
15
  import { Popover, PopoverContent, PopoverTrigger } from "@squadbase/vantage/ui"
16
16
  import { flattenOptions, type SelectOptions } from "@squadbase/vantage/components"
17
+ import { useSyncExternalStore } from "react"
17
18
  import type { KeyboardEvent, MouseEvent } from "react"
18
19
  import { CheckIcon, LoaderIcon, XIcon } from "lucide-react"
19
20
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@squadbase/vantage/ui"
20
21
  import { isGroupedOptions, type SelectOption } from "@squadbase/vantage/components"
21
22
 
23
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
24
+
25
+ /**
26
+ * The display language of the parts and the defaults that follow it.
27
+ *
28
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
29
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
30
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
31
+ *
32
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
33
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
34
+ */
35
+
36
+ /** The language of the built-in wording: Japanese or English. */
37
+ type UiLanguage = "ja" | "en"
38
+
39
+ /** The locale used when `<html lang>` gives none (and on the server). */
40
+ const FALLBACK_LOCALE = "en-US"
41
+
42
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
43
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
44
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
45
+ }
46
+
47
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
48
+ function normalizeLocale(tag: string | null | undefined): string {
49
+ const value = tag?.trim()
50
+ if (!value) return FALLBACK_LOCALE
51
+ try {
52
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
53
+ } catch {
54
+ return FALLBACK_LOCALE
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
60
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
61
+ */
62
+ function documentLocale(): string {
63
+ if (typeof document === "undefined") return FALLBACK_LOCALE
64
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
65
+ }
66
+
67
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
68
+ function resolveLocale(locale?: string | null): string {
69
+ return locale || documentLocale()
70
+ }
71
+
72
+ /** Picks the wording table of the locale's language. */
73
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
74
+ return table[uiLanguage(resolveLocale(locale))]
75
+ }
76
+
77
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
78
+ function defaultCurrency(locale?: string | null): string {
79
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
80
+ }
81
+
82
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
83
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
84
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
85
+ }
86
+
87
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
88
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
89
+
90
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
91
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
92
+
93
+ /**
94
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
95
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
96
+ * Monday for the rest.
97
+ */
98
+ function weekStartsOn(locale?: string | null): WeekDay {
99
+ const tag = normalizeLocale(resolveLocale(locale))
100
+ if (uiLanguage(tag) === "ja") return 1
101
+ try {
102
+ const info = new Intl.Locale(tag) as Intl.Locale & {
103
+ getWeekInfo?: () => { firstDay: number }
104
+ weekInfo?: { firstDay: number }
105
+ }
106
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
107
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
108
+ } catch {
109
+ // Fall through to the table below.
110
+ }
111
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
112
+ }
113
+
114
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
115
+
116
+ const listeners = new Set<() => void>()
117
+ let observer: MutationObserver | null = null
118
+
119
+ /** One observer on `<html lang>` for every subscribed part. */
120
+ function subscribe(onChange: () => void): () => void {
121
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
122
+ listeners.add(onChange)
123
+ if (!observer) {
124
+ observer = new MutationObserver(() => {
125
+ for (const listener of [...listeners]) listener()
126
+ })
127
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
128
+ }
129
+ return () => {
130
+ listeners.delete(onChange)
131
+ if (listeners.size === 0 && observer) {
132
+ observer.disconnect()
133
+ observer = null
134
+ }
135
+ }
136
+ }
137
+
138
+ const serverLocale = () => FALLBACK_LOCALE
139
+
140
+ /**
141
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
142
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
143
+ */
144
+ function useDocumentLocale(): string {
145
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
146
+ }
147
+
148
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
149
+ function useLocale(locale?: string | null): string {
150
+ const page = useDocumentLocale()
151
+ return locale || page
152
+ }
153
+
22
154
  /* ---------- inlined from @squadbase/squadkit registry/components/option-command.tsx (internal to @squadbase/vantage) ---------- */
23
155
 
24
156
  interface OptionCommandListProps {
@@ -136,6 +268,11 @@ export interface SearchableSelectProps {
136
268
  className?: string
137
269
  }
138
270
 
271
+ const TEXT = {
272
+ ja: { placeholder: "選択してください", search: "検索...", empty: "見つかりませんでした", clear: "クリア" },
273
+ en: { placeholder: "Select an option", search: "Search...", empty: "No results found.", clear: "Clear" },
274
+ }
275
+
139
276
  /** Not given yet (a draft fills props in steps): render empty, keep the same value between renders. */
140
277
  const NO_OPTIONS: never[] = []
141
278
 
@@ -146,9 +283,9 @@ export const SearchableSelect = forwardRef<HTMLDivElement, SearchableSelectProps
146
283
  value,
147
284
  onChange,
148
285
  onSearch,
149
- placeholder = "選択してください",
150
- searchPlaceholder = "検索...",
151
- emptyText = "見つかりませんでした",
286
+ placeholder,
287
+ searchPlaceholder,
288
+ emptyText,
152
289
  isLoading = false,
153
290
  disabled = false,
154
291
  clearable = true,
@@ -157,6 +294,7 @@ export const SearchableSelect = forwardRef<HTMLDivElement, SearchableSelectProps
157
294
  ref,
158
295
  ) {
159
296
  const [open, setOpen] = useState(false)
297
+ const t = TEXT[uiLanguage(useLocale())]
160
298
 
161
299
  const allOptions = useMemo(() => flattenOptions(options), [options])
162
300
  const selectedOption = useMemo(
@@ -187,10 +325,10 @@ export const SearchableSelect = forwardRef<HTMLDivElement, SearchableSelectProps
187
325
  role="combobox"
188
326
  aria-expanded={open}
189
327
  >
190
- <span className="truncate">{selectedOption ? selectedOption.label : placeholder}</span>
328
+ <span className="truncate">{selectedOption ? selectedOption.label : (placeholder ?? t.placeholder)}</span>
191
329
  <span className="flex shrink-0 items-center gap-1">
192
330
  {clearable && value && (
193
- <RemoveButton label="クリア" onRemove={() => onChange?.(undefined)} />
331
+ <RemoveButton label={t.clear} onRemove={() => onChange?.(undefined)} />
194
332
  )}
195
333
  <ChevronDownIcon className="size-4 opacity-50" />
196
334
  </span>
@@ -202,8 +340,8 @@ export const SearchableSelect = forwardRef<HTMLDivElement, SearchableSelectProps
202
340
  onSelect={handleSelect}
203
341
  onSearch={onSearch}
204
342
  isLoading={isLoading}
205
- searchPlaceholder={searchPlaceholder}
206
- emptyText={emptyText}
343
+ searchPlaceholder={searchPlaceholder ?? t.search}
344
+ emptyText={emptyText ?? t.empty}
207
345
  />
208
346
  </PopoverContent>
209
347
  </Popover>
@@ -4,9 +4,9 @@
4
4
 
5
5
  /**
6
6
  * Loading / Empty / ErrorState — the three non-data states of a panel, worded for
7
- * business users. Defaults are Japanese and say what is happening, not why the
8
- * system failed: keep raw errors ("upstream timeout after 30s") in `details`,
9
- * which stays folded behind "詳細を表示".
7
+ * business users. Defaults follow the page language (`<html lang>`: Japanese for `ja`, else
8
+ * English) and say what is happening, not why the system failed: keep raw errors
9
+ * ("upstream timeout after 30s") in `details`, which stays folded behind "Show details" (ja: "詳細を表示").
10
10
  *
11
11
  * @example
12
12
  * import { ErrorState } from "@/components/ui/states"
@@ -21,20 +21,174 @@
21
21
  */
22
22
  import type { ReactNode } from "react"
23
23
  import { cn } from "@squadbase/vantage/ui"
24
+ import { useSyncExternalStore } from "react"
24
25
 
25
- export function Loading({ label = "読み込んでいます…", className }: { label?: string; className?: string }) {
26
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
27
+
28
+ /**
29
+ * The display language of the parts and the defaults that follow it.
30
+ *
31
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
32
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
33
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
34
+ *
35
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
36
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
37
+ */
38
+
39
+ /** The language of the built-in wording: Japanese or English. */
40
+ type UiLanguage = "ja" | "en"
41
+
42
+ /** The locale used when `<html lang>` gives none (and on the server). */
43
+ const FALLBACK_LOCALE = "en-US"
44
+
45
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
46
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
47
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
48
+ }
49
+
50
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
51
+ function normalizeLocale(tag: string | null | undefined): string {
52
+ const value = tag?.trim()
53
+ if (!value) return FALLBACK_LOCALE
54
+ try {
55
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
56
+ } catch {
57
+ return FALLBACK_LOCALE
58
+ }
59
+ }
60
+
61
+ /**
62
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
63
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
64
+ */
65
+ function documentLocale(): string {
66
+ if (typeof document === "undefined") return FALLBACK_LOCALE
67
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
68
+ }
69
+
70
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
71
+ function resolveLocale(locale?: string | null): string {
72
+ return locale || documentLocale()
73
+ }
74
+
75
+ /** Picks the wording table of the locale's language. */
76
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
77
+ return table[uiLanguage(resolveLocale(locale))]
78
+ }
79
+
80
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
81
+ function defaultCurrency(locale?: string | null): string {
82
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
83
+ }
84
+
85
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
86
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
87
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
88
+ }
89
+
90
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
91
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
92
+
93
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
94
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
95
+
96
+ /**
97
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
98
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
99
+ * Monday for the rest.
100
+ */
101
+ function weekStartsOn(locale?: string | null): WeekDay {
102
+ const tag = normalizeLocale(resolveLocale(locale))
103
+ if (uiLanguage(tag) === "ja") return 1
104
+ try {
105
+ const info = new Intl.Locale(tag) as Intl.Locale & {
106
+ getWeekInfo?: () => { firstDay: number }
107
+ weekInfo?: { firstDay: number }
108
+ }
109
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
110
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
111
+ } catch {
112
+ // Fall through to the table below.
113
+ }
114
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
115
+ }
116
+
117
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
118
+
119
+ const listeners = new Set<() => void>()
120
+ let observer: MutationObserver | null = null
121
+
122
+ /** One observer on `<html lang>` for every subscribed part. */
123
+ function subscribe(onChange: () => void): () => void {
124
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
125
+ listeners.add(onChange)
126
+ if (!observer) {
127
+ observer = new MutationObserver(() => {
128
+ for (const listener of [...listeners]) listener()
129
+ })
130
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
131
+ }
132
+ return () => {
133
+ listeners.delete(onChange)
134
+ if (listeners.size === 0 && observer) {
135
+ observer.disconnect()
136
+ observer = null
137
+ }
138
+ }
139
+ }
140
+
141
+ const serverLocale = () => FALLBACK_LOCALE
142
+
143
+ /**
144
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
145
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
146
+ */
147
+ function useDocumentLocale(): string {
148
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
149
+ }
150
+
151
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
152
+ function useLocale(locale?: string | null): string {
153
+ const page = useDocumentLocale()
154
+ return locale || page
155
+ }
156
+
157
+ /* ---------- end of inlined code ---------- */
158
+
159
+ const TEXT = {
160
+ ja: {
161
+ loading: "読み込んでいます…",
162
+ empty: "表示できるデータがありません",
163
+ errorTitle: "データを読み込めませんでした",
164
+ errorMessage: "時間をおいて再試行してください。解決しない場合は管理者にお問い合わせください。",
165
+ details: "詳細を表示",
166
+ },
167
+ en: {
168
+ loading: "Loading…",
169
+ empty: "No data to display",
170
+ errorTitle: "Couldn't load the data",
171
+ errorMessage: "Try again in a moment. If the problem continues, contact your administrator.",
172
+ details: "Show details",
173
+ },
174
+ }
175
+
176
+ const useText = () => TEXT[uiLanguage(useLocale())]
177
+
178
+ export function Loading({ label, className }: { label?: string; className?: string }) {
179
+ const t = useText()
26
180
  return (
27
181
  <div className={cn("flex min-h-[30vh] items-center justify-center", className)} role="status" aria-live="polite">
28
182
  <div className="flex items-center gap-3 text-sm text-muted-foreground">
29
183
  <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
30
- {label}
184
+ {label ?? t.loading}
31
185
  </div>
32
186
  </div>
33
187
  )
34
188
  }
35
189
 
36
190
  export function Empty({
37
- title = "表示できるデータがありません",
191
+ title,
38
192
  description,
39
193
  icon,
40
194
  action,
@@ -46,10 +200,11 @@ export function Empty({
46
200
  action?: ReactNode
47
201
  className?: string
48
202
  }) {
203
+ const t = useText()
49
204
  return (
50
205
  <div className={cn("flex flex-col items-center justify-center gap-2 p-10 text-center", className)}>
51
206
  {icon ? <div className="text-3xl text-muted-foreground">{icon}</div> : null}
52
- <p className="font-medium">{title}</p>
207
+ <p className="font-medium">{title ?? t.empty}</p>
53
208
  {description ? <p className="max-w-sm text-sm text-muted-foreground">{description}</p> : null}
54
209
  {action ? <div className="mt-2">{action}</div> : null}
55
210
  </div>
@@ -57,8 +212,8 @@ export function Empty({
57
212
  }
58
213
 
59
214
  export function ErrorState({
60
- title = "データを読み込めませんでした",
61
- message = "時間をおいて再試行してください。解決しない場合は管理者にお問い合わせください。",
215
+ title,
216
+ message,
62
217
  details,
63
218
  action,
64
219
  className,
@@ -66,21 +221,23 @@ export function ErrorState({
66
221
  title?: string
67
222
  /** What happened and what to do next, in plain words. */
68
223
  message?: ReactNode
69
- /** Technical detail (error message, status code), folded behind "詳細を表示". */
224
+ /** Technical detail (error message, status code), folded behind "Show details" (ja: "詳細を表示"). */
70
225
  details?: ReactNode
71
226
  action?: ReactNode
72
227
  className?: string
73
228
  }) {
229
+ const t = useText()
230
+ const shownMessage = message === undefined ? t.errorMessage : message
74
231
  return (
75
232
  <div
76
233
  className={cn("rounded-xl border border-destructive/30 bg-destructive/5 p-6", className)}
77
234
  role="alert"
78
235
  >
79
- <p className="font-semibold text-destructive">{title}</p>
80
- {message ? <p className="mt-1 text-sm text-muted-foreground">{message}</p> : null}
236
+ <p className="font-semibold text-destructive">{title ?? t.errorTitle}</p>
237
+ {shownMessage ? <p className="mt-1 text-sm text-muted-foreground">{shownMessage}</p> : null}
81
238
  {details ? (
82
239
  <details className="mt-2 text-xs text-muted-foreground">
83
- <summary className="cursor-pointer select-none">詳細を表示</summary>
240
+ <summary className="cursor-pointer select-none">{t.details}</summary>
84
241
  <div className="mt-1 font-mono break-all">{details}</div>
85
242
  </details>
86
243
  ) : null}