@squadbase/vantage 0.6.2-dev1.d7c0911 → 0.6.2-dev1.e30f2cc
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-A23M4GCH.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-A23M4GCH.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-FBHT6W6J.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-FBHT6W6J.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-57GHR5LN.js +163 -0
- package/dist/chunk-57GHR5LN.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-5C2SEDPH.js} +21 -4
- package/dist/chunk-5C2SEDPH.js.map +1 -0
- package/dist/chunk-6OTTGAPO.js +44 -0
- package/dist/chunk-6OTTGAPO.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
- package/dist/chunk-7S52M2SL.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-JXFCSF3K.js +673 -0
- package/dist/chunk-JXFCSF3K.js.map +1 -0
- package/dist/{chunk-WRSX2MDK.js → chunk-QOXXOFGL.js} +115 -930
- package/dist/chunk-QOXXOFGL.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-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 +13 -249
- package/dist/components/index.js +5 -2
- package/dist/delta-text-CvwBGJ95.d.ts +15 -0
- package/dist/{dev-4XEDTELV.js → dev-H4I2JL63.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-H4I2JL63.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +307 -3
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-B2O3GJDU.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-B2O3GJDU.js.map} +1 -1
- package/dist/format-U0cQl069.d.ts +243 -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 +974 -4
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +8 -5
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +4 -1
- package/dist/ui/index.js +3 -3
- 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/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/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 +26 -25
- package/registry/ui/accordion.tsx +1 -1
- package/registry/ui/alert-dialog.tsx +1 -1
- package/registry/ui/alert.tsx +1 -1
- package/registry/ui/app-shell.tsx +1 -1
- package/registry/ui/area-chart.tsx +89 -27
- package/registry/ui/attention-list.tsx +303 -0
- package/registry/ui/avatar.tsx +1 -1
- package/registry/ui/badge.tsx +1 -1
- package/registry/ui/bar-chart.tsx +89 -27
- package/registry/ui/bar-list.tsx +88 -25
- package/registry/ui/box-plot-chart.tsx +3906 -0
- package/registry/ui/breadcrumb.tsx +1 -1
- package/registry/ui/button.tsx +1 -1
- package/registry/ui/calendar.tsx +1 -1
- package/registry/ui/card.tsx +1 -1
- package/registry/ui/category-bar.tsx +55 -20
- package/registry/ui/chart-frame.tsx +57 -22
- package/registry/ui/checkbox.tsx +1 -1
- package/registry/ui/collapsible.tsx +1 -1
- package/registry/ui/combobox.tsx +1 -1
- package/registry/ui/command.tsx +1 -1
- package/registry/ui/dashboard-card.tsx +5 -5
- package/registry/ui/data-table.tsx +1 -1
- package/registry/ui/date-range-picker.tsx +1 -1
- package/registry/ui/delta.tsx +12 -9
- package/registry/ui/dialog.tsx +1 -1
- package/registry/ui/donut-chart.tsx +90 -27
- package/registry/ui/drawer.tsx +1 -1
- package/registry/ui/dropdown-menu.tsx +1 -1
- package/registry/ui/echart.tsx +195 -45
- package/registry/ui/field.tsx +1 -1
- package/registry/ui/filter-bar.tsx +1 -1
- package/registry/ui/funnel-steps.tsx +34 -6
- package/registry/ui/heatmap.tsx +56 -22
- package/registry/ui/histogram-chart.tsx +3986 -0
- package/registry/ui/hover-card.tsx +1 -1
- package/registry/ui/input-group.tsx +1 -1
- package/registry/ui/input.tsx +1 -1
- package/registry/ui/kbd.tsx +1 -1
- package/registry/ui/kpi-card.tsx +42 -15
- package/registry/ui/label.tsx +1 -1
- package/registry/ui/legend.tsx +88 -25
- package/registry/ui/line-chart.tsx +89 -27
- package/registry/ui/metric-value.tsx +1 -1
- package/registry/ui/multi-select.tsx +1 -1
- package/registry/ui/page-shell.tsx +11 -11
- package/registry/ui/pagination.tsx +1 -1
- package/registry/ui/paired-chart.tsx +3827 -0
- package/registry/ui/placeholder.tsx +1 -1
- package/registry/ui/popover.tsx +1 -1
- package/registry/ui/progress-circle.tsx +1 -1
- package/registry/ui/progress.tsx +1 -1
- package/registry/ui/radio-group.tsx +1 -1
- package/registry/ui/sample-data.tsx +2 -2
- package/registry/ui/scatter-chart.tsx +89 -27
- package/registry/ui/scroll-area.tsx +1 -1
- package/registry/ui/searchable-select.tsx +1 -1
- package/registry/ui/section-header.tsx +3 -3
- package/registry/ui/segmented-control.tsx +1 -1
- package/registry/ui/select.tsx +1 -1
- package/registry/ui/separator.tsx +1 -1
- package/registry/ui/sheet.tsx +1 -1
- package/registry/ui/sidebar.tsx +1 -1
- package/registry/ui/skeleton.tsx +1 -1
- package/registry/ui/slider.tsx +2 -2
- package/registry/ui/sparkline.tsx +1 -1
- package/registry/ui/spinner.tsx +1 -1
- package/registry/ui/states.tsx +1 -1
- package/registry/ui/status-badge.tsx +9 -9
- package/registry/ui/switch.tsx +1 -1
- package/registry/ui/table.tsx +1 -1
- package/registry/ui/tabs.tsx +1 -1
- package/registry/ui/textarea.tsx +1 -1
- package/registry/ui/toast.tsx +1 -1
- package/registry/ui/toggle-group.tsx +1 -1
- package/registry/ui/toggle.tsx +1 -1
- package/registry/ui/tooltip.tsx +7 -3
- package/registry/ui/tracker.tsx +10 -4
- package/registry/ui/trend-indicator.tsx +3 -3
- package/registry/ui/updated-at.tsx +1 -1
- package/registry/ui/waterfall-chart.tsx +89 -27
- package/skills/vantage-design/SKILL.md +34 -24
- package/skills/vantage-design/references/extract.md +16 -14
- package/skills/vantage-design/references/format.md +6 -5
- package/skills/vantage-design/references/presets.md +60 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +4 -2
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +32 -1
- package/templates/agents/slide.md +70 -2
- package/theme.css +32 -24
- package/dist/chunk-2ICNHUG6.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-WAMZWZJB.js.map +0 -1
- package/dist/chunk-WRSX2MDK.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Copied by `vantage add ui area-chart`. This file is yours to edit.
|
|
2
|
-
// Generated from @squadbase/squadkit registry/charts/area-chart.tsx by `
|
|
2
|
+
// Generated from @squadbase/squadkit registry/charts/area-chart.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -15,7 +15,7 @@ import { forwardRef, useMemo, type ForwardedRef, type ReactElement, type Ref } f
|
|
|
15
15
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
16
16
|
import { EChart } from "@squadbase/vantage/components"
|
|
17
17
|
import type { ChartComparison, ChartForecast, NumericKeys } from "@squadbase/vantage/components"
|
|
18
|
-
import { createContext, useCallback, useEffect, useRef, useState } from "react"
|
|
18
|
+
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
19
19
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
20
20
|
import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
21
21
|
import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
@@ -48,6 +48,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
|
|
|
48
48
|
interface ChartTokens {
|
|
49
49
|
/** Resolved `--chart-1..5`, in order. */
|
|
50
50
|
series: string[]
|
|
51
|
+
/** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
|
|
52
|
+
fontFamily?: string
|
|
51
53
|
/** Body text: titles and the default `textStyle`. */
|
|
52
54
|
foreground: string | null
|
|
53
55
|
/** Secondary text: axis labels, legend, subtitles. */
|
|
@@ -56,10 +58,23 @@ interface ChartTokens {
|
|
|
56
58
|
border: string | null
|
|
57
59
|
/** Tooltip surface. */
|
|
58
60
|
popover: string | null
|
|
61
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
62
|
+
colors?: Record<string, string>
|
|
63
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
64
|
+
surface?: string | null
|
|
59
65
|
/** Tooltip text. */
|
|
60
66
|
popoverForeground: string | null
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
70
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
71
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
72
|
+
const OPTION_COLOR_TOKENS = [
|
|
73
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
74
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
75
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
76
|
+
]
|
|
77
|
+
|
|
63
78
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
64
79
|
|
|
65
80
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -115,14 +130,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
115
130
|
const color = read(token)
|
|
116
131
|
if (color) series.push(color)
|
|
117
132
|
}
|
|
118
|
-
|
|
133
|
+
const tokens: ChartTokens = {
|
|
119
134
|
series,
|
|
135
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
136
|
+
surface: readChartSurface(target),
|
|
137
|
+
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
120
138
|
foreground: read("--foreground"),
|
|
121
139
|
mutedForeground: read("--muted-foreground"),
|
|
122
140
|
border: read("--border"),
|
|
123
141
|
popover: read("--popover"),
|
|
124
142
|
popoverForeground: read("--popover-foreground"),
|
|
125
143
|
}
|
|
144
|
+
tokenScopes.set(tokens, target)
|
|
145
|
+
return tokens
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -130,8 +150,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
130
150
|
if (a === b) return true
|
|
131
151
|
if (!a || !b) return false
|
|
132
152
|
return (
|
|
153
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
154
|
+
a.surface === b.surface &&
|
|
155
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
156
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
133
157
|
a.series.length === b.series.length &&
|
|
134
158
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
159
|
+
a.fontFamily === b.fontFamily &&
|
|
135
160
|
a.foreground === b.foreground &&
|
|
136
161
|
a.mutedForeground === b.mutedForeground &&
|
|
137
162
|
a.border === b.border &&
|
|
@@ -154,7 +179,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
154
179
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
155
180
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
156
181
|
*/
|
|
157
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
182
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
158
183
|
if (typeof document === "undefined") return () => {}
|
|
159
184
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
160
185
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -169,8 +194,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
169
194
|
|
|
170
195
|
const themeObserver = new MutationObserver(schedule)
|
|
171
196
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
198
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
199
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
200
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
201
|
+
}
|
|
174
202
|
|
|
175
203
|
const styleObserver = new MutationObserver(schedule)
|
|
176
204
|
if (document.head) {
|
|
@@ -226,7 +254,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
226
254
|
// Let the page background show through: charts sit on cards, not on a canvas.
|
|
227
255
|
backgroundColor: "transparent",
|
|
228
256
|
...(tokens.series.length > 0 ? { color: tokens.series } : {}),
|
|
229
|
-
textStyle: { color: text },
|
|
257
|
+
textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
|
|
230
258
|
title: { textStyle: { color: text }, subtextStyle: { color: muted } },
|
|
231
259
|
legend: { textStyle: { color: muted } },
|
|
232
260
|
categoryAxis: axisTheme(tokens, false),
|
|
@@ -242,7 +270,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
242
270
|
padding: [8, 12],
|
|
243
271
|
// Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
|
|
244
272
|
extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
|
|
245
|
-
textStyle: {
|
|
273
|
+
textStyle: {
|
|
274
|
+
color: tokens.popoverForeground ?? undefined,
|
|
275
|
+
fontSize: 12,
|
|
276
|
+
...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
|
|
277
|
+
},
|
|
246
278
|
axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
|
|
247
279
|
},
|
|
248
280
|
}
|
|
@@ -254,22 +286,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
254
286
|
* components that build an ECharts option from token colours themselves (the
|
|
255
287
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
256
288
|
*/
|
|
257
|
-
function useChartTokens(): ChartTokens | null {
|
|
289
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
258
290
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
259
291
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
260
292
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
261
293
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
262
294
|
const tokensRef = useRef(tokens)
|
|
263
295
|
useEffect(() => {
|
|
296
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
264
297
|
const sync = () => {
|
|
265
|
-
const next = readChartTokens()
|
|
298
|
+
const next = readChartTokens(target)
|
|
266
299
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
267
300
|
tokensRef.current = next
|
|
268
301
|
setTokens(next)
|
|
269
302
|
}
|
|
270
303
|
sync()
|
|
271
|
-
return watchChartTokens(sync)
|
|
272
|
-
}, [])
|
|
304
|
+
return watchChartTokens(sync, target)
|
|
305
|
+
}, [scope])
|
|
273
306
|
return tokens
|
|
274
307
|
}
|
|
275
308
|
|
|
@@ -281,7 +314,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
281
314
|
/**
|
|
282
315
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
283
316
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
284
|
-
* variants `"positive-fill"`, …) are read
|
|
317
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
285
318
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
286
319
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
287
320
|
*/
|
|
@@ -292,12 +325,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
292
325
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
293
326
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
294
327
|
if (typeof document === "undefined") return ""
|
|
295
|
-
|
|
328
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
329
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
296
330
|
}
|
|
297
331
|
if (typeof document === "undefined") return color
|
|
298
332
|
let value = color.trim()
|
|
299
333
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
300
|
-
const styles = getComputedStyle(document.documentElement)
|
|
334
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
301
335
|
if (cssVar) {
|
|
302
336
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
303
337
|
} else if (value.includes("var(")) {
|
|
@@ -343,7 +377,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
343
377
|
if (color) return color
|
|
344
378
|
}
|
|
345
379
|
}
|
|
346
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
380
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
347
381
|
}
|
|
348
382
|
|
|
349
383
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -357,9 +391,10 @@ const WIDTH_BUCKET = 40
|
|
|
357
391
|
*/
|
|
358
392
|
function useChartSurface<E extends Element>(
|
|
359
393
|
forwarded: ForwardedRef<E>,
|
|
360
|
-
tokens
|
|
361
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
394
|
+
tokens?: ChartTokens | null,
|
|
395
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
362
396
|
const [node, setNode] = useState<E | null>(null)
|
|
397
|
+
const scopedTokens = useChartTokens(node)
|
|
363
398
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
364
399
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
365
400
|
const ref = useCallback(
|
|
@@ -373,7 +408,7 @@ function useChartSurface<E extends Element>(
|
|
|
373
408
|
useEffect(() => {
|
|
374
409
|
if (!node) return
|
|
375
410
|
setSurface(readChartSurface(node) ?? undefined)
|
|
376
|
-
}, [node, tokens])
|
|
411
|
+
}, [node, tokens, scopedTokens])
|
|
377
412
|
useEffect(() => {
|
|
378
413
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
379
414
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -382,7 +417,7 @@ function useChartSurface<E extends Element>(
|
|
|
382
417
|
observer.observe(node)
|
|
383
418
|
return () => observer.disconnect()
|
|
384
419
|
}, [node])
|
|
385
|
-
return { ref, surface, width }
|
|
420
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
386
421
|
}
|
|
387
422
|
|
|
388
423
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -1536,6 +1571,10 @@ const toDate = (t: ParsedTime) => new Date(t.year, t.month - 1, t.day)
|
|
|
1536
1571
|
|
|
1537
1572
|
/** How many x labels a time axis shows at most (the step is picked so the labels never crowd). */
|
|
1538
1573
|
const TIME_AXIS_MAX_LABELS = 6
|
|
1574
|
+
/** Under this plot width (px) the axis, legend and labels switch to their compact form. */
|
|
1575
|
+
const NARROW_PLOT_WIDTH = 400
|
|
1576
|
+
/** The most x labels a narrow plot shows (a 262px plot reads 4 at most). */
|
|
1577
|
+
const NARROW_TIME_AXIS_MAX_LABELS = 4
|
|
1539
1578
|
|
|
1540
1579
|
/** Calendar-friendly label steps per granularity (every 3 months, every week …). */
|
|
1541
1580
|
const TIME_AXIS_STEPS: Record<TimeGranularity, readonly number[]> = {
|
|
@@ -1981,7 +2020,9 @@ const LEGEND_LINE_BUDGET = 320
|
|
|
1981
2020
|
/** How many lines the plain legend takes, estimated from its labels (12px text + 8px dot + gaps). */
|
|
1982
2021
|
function legendLines(items: readonly string[], plotWidth?: number, reserve = 0): number {
|
|
1983
2022
|
const width = items.reduce((sum, name) => sum + [...name].length * 12 + 8 + 5 + 16, 0)
|
|
1984
|
-
|
|
2023
|
+
// A narrow plot has no room for the PNG buttons beside the legend: they move off its line, so no reserve.
|
|
2024
|
+
const narrow = plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2025
|
+
const budget = Math.max(80, (plotWidth ? (narrow ? plotWidth : Math.min(LEGEND_LINE_BUDGET, plotWidth)) : LEGEND_LINE_BUDGET) - (narrow ? 0 : reserve))
|
|
1985
2026
|
return Math.min(3, Math.max(1, Math.ceil(width / budget)))
|
|
1986
2027
|
}
|
|
1987
2028
|
|
|
@@ -2236,7 +2277,15 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2236
2277
|
? null
|
|
2237
2278
|
: timeAxisLabelStep(
|
|
2238
2279
|
rawIndex,
|
|
2239
|
-
plotWidth
|
|
2280
|
+
plotWidth
|
|
2281
|
+
? Math.max(
|
|
2282
|
+
2,
|
|
2283
|
+
Math.min(
|
|
2284
|
+
plotWidth < NARROW_PLOT_WIDTH ? NARROW_TIME_AXIS_MAX_LABELS : TIME_AXIS_MAX_LABELS,
|
|
2285
|
+
Math.floor(plotWidth / TIME_LABEL_WIDTH),
|
|
2286
|
+
),
|
|
2287
|
+
)
|
|
2288
|
+
: undefined,
|
|
2240
2289
|
)
|
|
2241
2290
|
// Plain (non-date) x labels of a line chart: thinned to every n-th so they never touch, never rotated.
|
|
2242
2291
|
const plainStep = (() => {
|
|
@@ -2282,8 +2331,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2282
2331
|
: []
|
|
2283
2332
|
// On a narrow plot the max and the latest label would print over each other: keep the latest.
|
|
2284
2333
|
const pointGap = plotWidth && labels.length > 1 ? plotWidth / (kind === "bar" ? labels.length : labels.length - 1) : null
|
|
2334
|
+
// By default only the latest value is labelled; an explicit `highlight` keeps its max + latest.
|
|
2335
|
+
const latestOnly = cfg.highlight === undefined && !shareHighlight && picked.length === 2
|
|
2285
2336
|
const highlightPoints =
|
|
2286
|
-
|
|
2337
|
+
latestOnly
|
|
2338
|
+
? [picked[1]!]
|
|
2339
|
+
: picked.length === 2 && pointGap !== null && Math.abs(picked[1]!.index - picked[0]!.index) * pointGap < HIGHLIGHT_LABEL_WIDTH
|
|
2287
2340
|
? [picked[1]!]
|
|
2288
2341
|
: picked
|
|
2289
2342
|
|
|
@@ -2494,6 +2547,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2494
2547
|
? { width: 0, color, opacity: 0 }
|
|
2495
2548
|
: { width: 2, color, opacity: dimSeries ? DIMMED : 1 },
|
|
2496
2549
|
itemStyle: { color, opacity: dimSeries ? DIMMED : 1 },
|
|
2550
|
+
// Hovering a series fades the others (not for a stack, where the bands read together).
|
|
2551
|
+
emphasis: stacked ? undefined : { focus: "series" as const },
|
|
2497
2552
|
areaStyle: areaStyle && dimSeries ? { ...areaStyle, opacity: DIMMED * 0.3 } : areaStyle,
|
|
2498
2553
|
markPoint: markPointFor(c, color),
|
|
2499
2554
|
markLine: undefined,
|
|
@@ -2940,7 +2995,10 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2940
2995
|
legend: {
|
|
2941
2996
|
show: showLegend,
|
|
2942
2997
|
top: 0,
|
|
2943
|
-
|
|
2998
|
+
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
2999
|
+
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3000
|
+
? { left: 0, right: 0 }
|
|
3001
|
+
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2944
3002
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2945
3003
|
type: "plain",
|
|
2946
3004
|
icon: "circle",
|
|
@@ -3001,7 +3059,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3001
3059
|
}
|
|
3002
3060
|
}
|
|
3003
3061
|
const shares = percent ? cartesianSeriesValues(i <= lastActual ? [row] : [], categories, "percent") : null
|
|
3004
|
-
|
|
3062
|
+
// Rows follow the legend order (categories first), not ECharts' own series order.
|
|
3063
|
+
const legendRank = (p: AxisTooltipParam) => {
|
|
3064
|
+
const at = categories.indexOf(p.seriesName ?? "")
|
|
3065
|
+
return at === -1 ? categories.length : at
|
|
3066
|
+
}
|
|
3067
|
+
for (const p of [...list].sort((a, b) => legendRank(a) - legendRank(b))) {
|
|
3005
3068
|
const color = typeof p.color === "string" ? p.color : "currentColor"
|
|
3006
3069
|
if (p.seriesId === FORECAST_SERIES_ID) {
|
|
3007
3070
|
// At and before the boundary the forecast repeats the actual value.
|
|
@@ -3521,9 +3584,8 @@ function AreaChartImpl<T extends Record<string, unknown>>(
|
|
|
3521
3584
|
}: AreaChartProps<T>,
|
|
3522
3585
|
ref: ForwardedRef<HTMLDivElement>,
|
|
3523
3586
|
) {
|
|
3524
|
-
const tokens = useChartTokens()
|
|
3525
3587
|
// Highlight labels are backed by the surface the chart sits on (a card, the page).
|
|
3526
|
-
const { ref: frameRef, surface, width: plotWidth } = useChartSurface(ref
|
|
3588
|
+
const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
|
|
3527
3589
|
// Ticks and on-plot labels read short (¥1億); the tooltip reads the value as `format` says.
|
|
3528
3590
|
const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3529
3591
|
const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format), [valueFormatter, format])
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Copied by `vantage add ui attention-list`. This file is yours to edit.
|
|
2
|
+
// Generated from @squadbase/squadkit registry/components/attention-list.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
|
|
3
|
+
"use client"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AttentionList — 期日超過・期限間近・リスク・滞留に当たる行を上に寄せ、理由と次の一手を添えて並べる要対応一覧。
|
|
7
|
+
* 判定は `judgeAttention`(純関数)が行い、部品は並べ替えと描画だけをする。区分の文言としきい値
|
|
8
|
+
* (`dueSoonDays` / `staleDays`)は props で渡す。業務の意味(請求・商談・契約など)は呼び元が決める。
|
|
9
|
+
*
|
|
10
|
+
* - 区分は 1 行に 1 つ。複数に当たるときは 期日超過 > リスク > 期限間近 > 滞留 の順で強いほうを採る。
|
|
11
|
+
* - 並びは区分の強さ、同じ区分では日数の大きい(遅れている・止まっている)行が先。日数が同じなら元の順。
|
|
12
|
+
* - 区分の色は StatusBadge の意味色(negative / warning / info)。色だけでなく区分名と日数の文で伝える。
|
|
13
|
+
* - 判定に当たらない行は既定で出さない(`includeUnflagged` で末尾に出す)。当たる行がなければ Empty を出す。
|
|
14
|
+
* - 行の項目は関数(`getDueDate` など)か列名(`dueDateKey` など)で渡す。列名は JSON で書けるので、下書きはこちらを使う。
|
|
15
|
+
* 両方あれば関数が先。どちらも無ければ、期日・動き・リスクは「無い」として判定せず、行の ID は行の順番、見出しは空になる。
|
|
16
|
+
* - 行を押すと `onRowClick(row)` を呼ぶ。渡さなければ押せない行(button ではなく li)になる。
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* import { AttentionList } from "@/components/attention-list"
|
|
20
|
+
* // Vantage: import { AttentionList } from "@squadbase/vantage/components"
|
|
21
|
+
*
|
|
22
|
+
* <AttentionList
|
|
23
|
+
* rows={receivables}
|
|
24
|
+
* asOf="2026-10-23"
|
|
25
|
+
* dueSoonDays={7}
|
|
26
|
+
* getRowId={(r) => r.id}
|
|
27
|
+
* getTitle={(r) => r.customer}
|
|
28
|
+
* getDueDate={(r) => r.dueDate}
|
|
29
|
+
* getNextAction={(r) => r.nextAction}
|
|
30
|
+
* onRowClick={(r) => openDetail(r.id)}
|
|
31
|
+
* />
|
|
32
|
+
*/
|
|
33
|
+
import { differenceInCalendarDays, parseISO } from "date-fns"
|
|
34
|
+
import { useMemo, type HTMLAttributes, type ReactNode } from "react"
|
|
35
|
+
|
|
36
|
+
import { formatDate } from "@squadbase/vantage/components"
|
|
37
|
+
import { cn } from "@squadbase/vantage/ui"
|
|
38
|
+
import { Empty } from "@squadbase/vantage/ui"
|
|
39
|
+
import { StatusBadge } from "@squadbase/vantage/components"
|
|
40
|
+
|
|
41
|
+
export type AttentionKind = "overdue" | "risk" | "dueSoon" | "stale"
|
|
42
|
+
|
|
43
|
+
/** 強い順。並びと、複数に当たったときの採用に使う。 */
|
|
44
|
+
const KIND_ORDER: readonly AttentionKind[] = ["overdue", "risk", "dueSoon", "stale"]
|
|
45
|
+
|
|
46
|
+
const KIND_TONES: Record<AttentionKind, string> = {
|
|
47
|
+
overdue: "negative",
|
|
48
|
+
risk: "negative",
|
|
49
|
+
dueSoon: "warning",
|
|
50
|
+
stale: "info",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_ATTENTION_LABELS: Record<AttentionKind, string> = {
|
|
54
|
+
overdue: "期日超過",
|
|
55
|
+
risk: "リスク",
|
|
56
|
+
dueSoon: "期限間近",
|
|
57
|
+
stale: "滞留",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type DateInput = Date | string | null | undefined
|
|
61
|
+
|
|
62
|
+
export interface AttentionJudgement {
|
|
63
|
+
kind: AttentionKind
|
|
64
|
+
/**
|
|
65
|
+
* overdue: 期日を過ぎた日数(1 以上)/ dueSoon: 期日までの日数(0 = 当日)/
|
|
66
|
+
* stale: 最終の動きからの日数/ risk: 0。
|
|
67
|
+
*/
|
|
68
|
+
days: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface JudgeAttentionInput {
|
|
72
|
+
/** 期日。無ければ期日の判定(超過・間近)はしない。 */
|
|
73
|
+
dueDate?: DateInput
|
|
74
|
+
/** 最後に動きがあった日。無ければ滞留の判定はしない。 */
|
|
75
|
+
lastActivityAt?: DateInput
|
|
76
|
+
/** 呼び元がリスクと判定した行。 */
|
|
77
|
+
risk?: boolean
|
|
78
|
+
/** 基準日。必ず渡す(部品の内で「今日」を決めない)。 */
|
|
79
|
+
asOf: Date | string
|
|
80
|
+
/** 期日の何日前から「期限間近」にするか。当日を含み、この日数ちょうども含む。既定 7。 */
|
|
81
|
+
dueSoonDays?: number
|
|
82
|
+
/** 何日動きがなければ「滞留」にするか。この日数ちょうどから滞留。未指定なら滞留は判定しない。 */
|
|
83
|
+
staleDays?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toDate(value: DateInput): Date | null {
|
|
87
|
+
if (value === null || value === undefined || value === "") return null
|
|
88
|
+
const date = typeof value === "string" ? parseISO(value) : value
|
|
89
|
+
return Number.isFinite(date.getTime()) ? date : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 1 行が要対応かを判定する。当たらなければ `null`。日数は暦日(時刻は無視)で数える。
|
|
94
|
+
* 期日の当日は「超過」ではなく「期限間近(0 日)」。
|
|
95
|
+
*/
|
|
96
|
+
export function judgeAttention(input: JudgeAttentionInput): AttentionJudgement | null {
|
|
97
|
+
const asOf = toDate(input.asOf)
|
|
98
|
+
if (!asOf) return null
|
|
99
|
+
const dueSoonDays = input.dueSoonDays ?? 7
|
|
100
|
+
|
|
101
|
+
const due = toDate(input.dueDate)
|
|
102
|
+
if (due) {
|
|
103
|
+
const untilDue = differenceInCalendarDays(due, asOf)
|
|
104
|
+
if (untilDue < 0) return { kind: "overdue", days: -untilDue }
|
|
105
|
+
}
|
|
106
|
+
if (input.risk) return { kind: "risk", days: 0 }
|
|
107
|
+
if (due) {
|
|
108
|
+
const untilDue = differenceInCalendarDays(due, asOf)
|
|
109
|
+
if (untilDue <= dueSoonDays) return { kind: "dueSoon", days: untilDue }
|
|
110
|
+
}
|
|
111
|
+
const last = toDate(input.lastActivityAt)
|
|
112
|
+
if (last && input.staleDays !== undefined) {
|
|
113
|
+
const idle = differenceInCalendarDays(asOf, last)
|
|
114
|
+
if (idle >= input.staleDays) return { kind: "stale", days: idle }
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 区分の強さ → 日数の大きい順(dueSoon は期日が近い順)。同順位は 0 を返し、元の順を保つ。 */
|
|
120
|
+
export function compareAttention(a: AttentionJudgement | null, b: AttentionJudgement | null): number {
|
|
121
|
+
if (!a || !b) return a ? -1 : b ? 1 : 0
|
|
122
|
+
const byKind = KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind)
|
|
123
|
+
if (byKind !== 0) return byKind
|
|
124
|
+
return a.kind === "dueSoon" ? a.days - b.days : b.days - a.days
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function defaultReason(j: AttentionJudgement, labels: Record<AttentionKind, string>): string {
|
|
128
|
+
switch (j.kind) {
|
|
129
|
+
case "overdue":
|
|
130
|
+
return `期日を ${j.days} 日過ぎています`
|
|
131
|
+
case "dueSoon":
|
|
132
|
+
return j.days === 0 ? "期日は今日です" : `期日まであと ${j.days} 日です`
|
|
133
|
+
case "stale":
|
|
134
|
+
return `${j.days} 日間動きがありません`
|
|
135
|
+
case "risk":
|
|
136
|
+
return labels.risk
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface AttentionListProps<T> {
|
|
141
|
+
rows: T[]
|
|
142
|
+
/** 基準日(`"2026-10-23"` か Date)。 */
|
|
143
|
+
asOf: Date | string
|
|
144
|
+
/** 期日の何日前から「期限間近」にするか。既定 7。 */
|
|
145
|
+
dueSoonDays?: number
|
|
146
|
+
/** この日数動きがなければ「滞留」。未指定なら滞留は判定しない。 */
|
|
147
|
+
staleDays?: number
|
|
148
|
+
/** 区分名の差し替え(例: `{ overdue: "入金遅れ" }`)。 */
|
|
149
|
+
labels?: Partial<Record<AttentionKind, string>>
|
|
150
|
+
/** 行の ID。省略時は `idKey` の列、それも無ければ行の順番。 */
|
|
151
|
+
getRowId?: (row: T) => string
|
|
152
|
+
/** 行の見出し(顧客名・案件名など)。省略時は `titleKey` の列、それも無ければ空。 */
|
|
153
|
+
getTitle?: (row: T) => ReactNode
|
|
154
|
+
getDueDate?: (row: T) => DateInput
|
|
155
|
+
getLastActivityAt?: (row: T) => DateInput
|
|
156
|
+
/** リスクと判定する行。期日・滞留より先に出す。 */
|
|
157
|
+
isRisk?: (row: T) => boolean
|
|
158
|
+
/** 行の ID の列名(JSON で書ける `getRowId`)。 */
|
|
159
|
+
idKey?: string
|
|
160
|
+
/** 見出しの列名(JSON で書ける `getTitle`)。 */
|
|
161
|
+
titleKey?: string
|
|
162
|
+
/** 期日の列名(JSON で書ける `getDueDate`)。値は `"2026-10-23"` 形式の日付か Date。 */
|
|
163
|
+
dueDateKey?: string
|
|
164
|
+
/** 最後に動きがあった日の列名(JSON で書ける `getLastActivityAt`)。 */
|
|
165
|
+
lastActivityAtKey?: string
|
|
166
|
+
/** リスクの列名(JSON で書ける `isRisk`)。値が真の行をリストにする。 */
|
|
167
|
+
riskKey?: string
|
|
168
|
+
/** 次の一手の列名(JSON で書ける `getNextAction`)。値が空の行には添えない。 */
|
|
169
|
+
nextActionKey?: string
|
|
170
|
+
/** 理由の文。省略すると区分と日数から作る。 */
|
|
171
|
+
getReason?: (row: T, judgement: AttentionJudgement) => ReactNode
|
|
172
|
+
/** 次の一手。判定に当たる行に添える。 */
|
|
173
|
+
getNextAction?: (row: T, judgement: AttentionJudgement) => ReactNode
|
|
174
|
+
/** 見出しの右に出す補足(金額・担当者など)。 */
|
|
175
|
+
getMeta?: (row: T) => ReactNode
|
|
176
|
+
/** 行を押したとき。渡すと行が button になる。 */
|
|
177
|
+
onRowClick?: (row: T) => void
|
|
178
|
+
/** 判定に当たらない行も末尾に出す。既定 false。 */
|
|
179
|
+
includeUnflagged?: boolean
|
|
180
|
+
/** 該当なしのときの見出し・説明。 */
|
|
181
|
+
emptyTitle?: string
|
|
182
|
+
emptyDescription?: string
|
|
183
|
+
className?: string
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cell(row: unknown, key: string | undefined): unknown {
|
|
187
|
+
if (!key || row === null || typeof row !== "object") return undefined
|
|
188
|
+
return (row as Record<string, unknown>)[key]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function asDateInput(value: unknown): DateInput {
|
|
192
|
+
return typeof value === "string" || value instanceof Date ? value : undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function asNode(value: unknown): ReactNode {
|
|
196
|
+
return typeof value === "string" || typeof value === "number" ? value : null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function AttentionList<T>({
|
|
200
|
+
rows,
|
|
201
|
+
asOf,
|
|
202
|
+
dueSoonDays,
|
|
203
|
+
staleDays,
|
|
204
|
+
labels,
|
|
205
|
+
getRowId,
|
|
206
|
+
getTitle,
|
|
207
|
+
getDueDate,
|
|
208
|
+
getLastActivityAt,
|
|
209
|
+
isRisk,
|
|
210
|
+
idKey,
|
|
211
|
+
titleKey,
|
|
212
|
+
dueDateKey,
|
|
213
|
+
lastActivityAtKey,
|
|
214
|
+
riskKey,
|
|
215
|
+
nextActionKey,
|
|
216
|
+
getReason,
|
|
217
|
+
getNextAction,
|
|
218
|
+
getMeta,
|
|
219
|
+
onRowClick,
|
|
220
|
+
includeUnflagged = false,
|
|
221
|
+
emptyTitle = "対応が必要な項目はありません",
|
|
222
|
+
emptyDescription,
|
|
223
|
+
className,
|
|
224
|
+
...rest
|
|
225
|
+
}: AttentionListProps<T> & Omit<HTMLAttributes<HTMLUListElement>, keyof AttentionListProps<T>>) {
|
|
226
|
+
const mergedLabels = useMemo(() => ({ ...DEFAULT_ATTENTION_LABELS, ...labels }), [labels])
|
|
227
|
+
const colorMap = KIND_TONES
|
|
228
|
+
const dueOf = (row: T) => (getDueDate ? getDueDate(row) : asDateInput(cell(row, dueDateKey)))
|
|
229
|
+
const nextActionOf = (row: T, j: AttentionJudgement): ReactNode =>
|
|
230
|
+
getNextAction ? getNextAction(row, j) : asNode(cell(row, nextActionKey))
|
|
231
|
+
|
|
232
|
+
// アクセサは呼び元の判定そのものなので、rows が同じ参照でも毎回かけ直す(メモ化すると古い判定が残る)
|
|
233
|
+
const items = (rows ?? [])
|
|
234
|
+
.map((row, index) => ({
|
|
235
|
+
row,
|
|
236
|
+
index,
|
|
237
|
+
judgement: judgeAttention({
|
|
238
|
+
dueDate: dueOf(row),
|
|
239
|
+
lastActivityAt: getLastActivityAt ? getLastActivityAt(row) : asDateInput(cell(row, lastActivityAtKey)),
|
|
240
|
+
risk: isRisk ? isRisk(row) : Boolean(cell(row, riskKey)),
|
|
241
|
+
asOf,
|
|
242
|
+
dueSoonDays,
|
|
243
|
+
staleDays,
|
|
244
|
+
}),
|
|
245
|
+
}))
|
|
246
|
+
.filter((item) => includeUnflagged || item.judgement)
|
|
247
|
+
.sort((a, b) => compareAttention(a.judgement, b.judgement) || a.index - b.index)
|
|
248
|
+
|
|
249
|
+
if (items.length === 0) {
|
|
250
|
+
return <Empty title={emptyTitle} description={emptyDescription} className={className} />
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<ul data-slot="attention-list" className={cn("divide-y divide-border", className)} {...rest}>
|
|
255
|
+
{items.map(({ row, index, judgement }) => {
|
|
256
|
+
const due = dueOf(row)
|
|
257
|
+
const body = (
|
|
258
|
+
<>
|
|
259
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
|
|
260
|
+
{judgement && (
|
|
261
|
+
<StatusBadge status={judgement.kind} label={mergedLabels[judgement.kind]} colorMap={colorMap} />
|
|
262
|
+
)}
|
|
263
|
+
<span className="min-w-0 font-medium">{getTitle ? getTitle(row) : asNode(cell(row, titleKey))}</span>
|
|
264
|
+
{getMeta && <span className="text-sm tabular-nums text-muted-foreground">{getMeta(row)}</span>}
|
|
265
|
+
{due ? (
|
|
266
|
+
<span className="ml-auto text-sm tabular-nums text-muted-foreground">
|
|
267
|
+
期日 {formatDate(due)}
|
|
268
|
+
</span>
|
|
269
|
+
) : null}
|
|
270
|
+
</div>
|
|
271
|
+
{judgement && (
|
|
272
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
273
|
+
{getReason ? getReason(row, judgement) : defaultReason(judgement, mergedLabels)}
|
|
274
|
+
</p>
|
|
275
|
+
)}
|
|
276
|
+
{judgement && (getNextAction || nextActionKey) && nextActionOf(row, judgement) ? (
|
|
277
|
+
<p className="mt-1 text-sm">
|
|
278
|
+
<span className="text-muted-foreground">次の一手: </span>
|
|
279
|
+
{nextActionOf(row, judgement)}
|
|
280
|
+
</p>
|
|
281
|
+
) : null}
|
|
282
|
+
</>
|
|
283
|
+
)
|
|
284
|
+
return (
|
|
285
|
+
<li key={getRowId ? getRowId(row) : String(cell(row, idKey) ?? index)} data-slot="attention-list-row">
|
|
286
|
+
{onRowClick ? (
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={() => onRowClick(row)}
|
|
290
|
+
className="block w-full px-3 py-3 text-left transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
291
|
+
>
|
|
292
|
+
{body}
|
|
293
|
+
</button>
|
|
294
|
+
) : (
|
|
295
|
+
<div className="px-3 py-3">{body}</div>
|
|
296
|
+
)}
|
|
297
|
+
</li>
|
|
298
|
+
)
|
|
299
|
+
})}
|
|
300
|
+
</ul>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
AttentionList.displayName = "AttentionList"
|
package/registry/ui/avatar.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Copied by `vantage add ui avatar`. This file is yours to edit.
|
|
2
|
-
// Generated from @squadbase/squadkit registry/ui/avatar.tsx by `
|
|
2
|
+
// Generated from @squadbase/squadkit registry/ui/avatar.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
import * as React from "react"
|
package/registry/ui/badge.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Copied by `vantage add ui badge`. This file is yours to edit.
|
|
2
|
-
// Generated from @squadbase/squadkit registry/ui/badge.tsx by `
|
|
2
|
+
// Generated from @squadbase/squadkit registry/ui/badge.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
import { mergeProps } from "@base-ui/react/merge-props"
|