@squadbase/vantage 0.6.2-dev1.d594c56 → 0.6.2-dev1.e333781

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 (202) hide show
  1. package/dist/{build-X5NVCQKT.js → build-YOPOGPQB.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-YOPOGPQB.js.map} +1 -1
  3. package/dist/{check-ZBIH422T.js → check-FBHT6W6J.js} +5 -5
  4. package/dist/{check-ZBIH422T.js.map → check-FBHT6W6J.js.map} +1 -1
  5. package/dist/chunk-2IAPIOPS.js +152 -0
  6. package/dist/chunk-2IAPIOPS.js.map +1 -0
  7. package/dist/chunk-57GHR5LN.js +163 -0
  8. package/dist/chunk-57GHR5LN.js.map +1 -0
  9. package/dist/{chunk-7DZGJQ3P.js → chunk-5C2SEDPH.js} +21 -4
  10. package/dist/chunk-5C2SEDPH.js.map +1 -0
  11. package/dist/chunk-6OTTGAPO.js +44 -0
  12. package/dist/chunk-6OTTGAPO.js.map +1 -0
  13. package/dist/{chunk-2ICNHUG6.js → chunk-7652CGJJ.js} +10 -3
  14. package/dist/{chunk-2ICNHUG6.js.map → chunk-7652CGJJ.js.map} +1 -1
  15. package/dist/{chunk-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
  16. package/dist/chunk-7S52M2SL.js.map +1 -0
  17. package/dist/chunk-JXFCSF3K.js +673 -0
  18. package/dist/chunk-JXFCSF3K.js.map +1 -0
  19. package/dist/{chunk-HKSVYMYJ.js → chunk-QOXXOFGL.js} +61 -929
  20. package/dist/chunk-QOXXOFGL.js.map +1 -0
  21. package/dist/{chunk-WAMZWZJB.js → chunk-R6MTJAHX.js} +6 -6
  22. package/dist/chunk-R6MTJAHX.js.map +1 -0
  23. package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
  24. package/dist/chunk-S5MDTHH7.js.map +1 -0
  25. package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
  26. package/dist/chunk-UWSQUHX6.js.map +1 -0
  27. package/dist/cli.js +6 -6
  28. package/dist/components/index.d.ts +11 -247
  29. package/dist/components/index.js +5 -2
  30. package/dist/delta-text-CvwBGJ95.d.ts +15 -0
  31. package/dist/{dev-4XEDTELV.js → dev-GUKQT6U6.js} +7 -7
  32. package/dist/{dev-4XEDTELV.js.map → dev-GUKQT6U6.js.map} +1 -1
  33. package/dist/doc/index.d.ts +215 -2
  34. package/dist/doc/index.js +307 -3
  35. package/dist/doc/index.js.map +1 -1
  36. package/dist/{doctor-5E2VEZPK.js → doctor-B2O3GJDU.js} +5 -5
  37. package/dist/{doctor-5E2VEZPK.js.map → doctor-B2O3GJDU.js.map} +1 -1
  38. package/dist/format-U0cQl069.d.ts +243 -0
  39. package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
  40. package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
  41. package/dist/slide/index.d.ts +272 -1
  42. package/dist/slide/index.js +972 -2
  43. package/dist/slide/index.js.map +1 -1
  44. package/dist/table/index.js +5 -2
  45. package/dist/table/index.js.map +1 -1
  46. package/dist/ui/index.d.ts +4 -1
  47. package/dist/ui/index.js +3 -3
  48. package/dist/ui/index.js.map +1 -1
  49. package/dist/{upgrade-JHXZBGQS.js → upgrade-7ZXMCTTR.js} +4 -4
  50. package/dist/{upgrade-JHXZBGQS.js.map → upgrade-7ZXMCTTR.js.map} +1 -1
  51. package/dist/vite/index.js +3 -3
  52. package/docs/en/changelog.md +2 -0
  53. package/docs/en/components.md +13 -2
  54. package/docs/en/doc/delta-text.md +40 -0
  55. package/docs/en/doc/doc-action-items.md +49 -0
  56. package/docs/en/doc/doc-callout.md +38 -0
  57. package/docs/en/doc/doc-data-table.md +60 -0
  58. package/docs/en/doc/doc-executive-summary.md +64 -0
  59. package/docs/en/doc/doc-figure.md +56 -0
  60. package/docs/en/doc/doc-key-findings.md +61 -0
  61. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  62. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  63. package/docs/en/doc/doc-report-header.md +46 -0
  64. package/docs/en/pages-and-metadata.md +15 -0
  65. package/docs/en/parts/area-chart.md +99 -0
  66. package/docs/en/parts/bar-chart.md +91 -0
  67. package/docs/en/parts/bar-list.md +50 -0
  68. package/docs/en/parts/category-bar.md +37 -0
  69. package/docs/en/parts/chart-frame.md +39 -0
  70. package/docs/en/parts/delta.md +4 -3
  71. package/docs/en/parts/donut-chart.md +72 -0
  72. package/docs/en/parts/funnel-steps.md +2 -1
  73. package/docs/en/parts/heatmap.md +53 -0
  74. package/docs/en/parts/kpi-card.md +2 -1
  75. package/docs/en/parts/legend.md +35 -0
  76. package/docs/en/parts/line-chart.md +94 -0
  77. package/docs/en/parts/progress-circle.md +33 -0
  78. package/docs/en/parts/sample-data.md +39 -0
  79. package/docs/en/parts/scatter-chart.md +56 -0
  80. package/docs/en/parts/tracker.md +36 -0
  81. package/docs/en/parts/updated-at.md +30 -0
  82. package/docs/en/parts/waterfall-chart.md +56 -0
  83. package/docs/en/slide/slide-chart-full.md +48 -0
  84. package/docs/en/slide/slide-chart-insight.md +52 -0
  85. package/docs/en/slide/slide-columns.md +46 -0
  86. package/docs/en/slide/slide-comparison.md +68 -0
  87. package/docs/en/slide/slide-cover.md +39 -0
  88. package/docs/en/slide/slide-exec-summary.md +47 -0
  89. package/docs/en/slide/slide-heading.md +38 -0
  90. package/docs/en/slide/slide-kpi-grid.md +55 -0
  91. package/docs/en/slide/slide-kpi-hero.md +58 -0
  92. package/docs/en/slide/slide-next-actions.md +49 -0
  93. package/docs/en/slide/slide-scorecard-table.md +54 -0
  94. package/docs/en/slide/slide-section.md +44 -0
  95. package/docs/en/slide/slide-source-line.md +34 -0
  96. package/docs/en/ui/alert-dialog.md +42 -0
  97. package/docs/en/ui/avatar.md +31 -0
  98. package/docs/en/ui/combobox.md +74 -0
  99. package/docs/en/ui/drawer.md +41 -0
  100. package/docs/en/ui/empty.md +1 -1
  101. package/docs/en/ui/error-state.md +4 -3
  102. package/docs/en/ui/field.md +42 -0
  103. package/docs/en/ui/hover-card.md +21 -0
  104. package/docs/en/ui/kbd.md +17 -0
  105. package/docs/en/ui/loading.md +1 -1
  106. package/docs/en/ui/pagination.md +36 -0
  107. package/docs/en/ui/radio-group.md +21 -0
  108. package/docs/en/ui/toast.md +44 -0
  109. package/docs/en/ui/toggle-group.md +4 -3
  110. package/docs/index.json +857 -77
  111. package/docs/ja/changelog.md +2 -0
  112. package/docs/ja/components.md +12 -1
  113. package/docs/ja/doc/delta-text.md +40 -0
  114. package/docs/ja/doc/doc-action-items.md +49 -0
  115. package/docs/ja/doc/doc-callout.md +38 -0
  116. package/docs/ja/doc/doc-data-table.md +60 -0
  117. package/docs/ja/doc/doc-executive-summary.md +64 -0
  118. package/docs/ja/doc/doc-figure.md +56 -0
  119. package/docs/ja/doc/doc-key-findings.md +61 -0
  120. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  121. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  122. package/docs/ja/doc/doc-report-header.md +46 -0
  123. package/docs/ja/page-types/slide.md +1 -0
  124. package/docs/ja/pages-and-metadata.md +15 -0
  125. package/docs/ja/parts/area-chart.md +49 -25
  126. package/docs/ja/parts/bar-chart.md +50 -24
  127. package/docs/ja/parts/bar-list.md +31 -8
  128. package/docs/ja/parts/chart-frame.md +36 -0
  129. package/docs/ja/parts/delta.md +5 -4
  130. package/docs/ja/parts/donut-chart.md +45 -26
  131. package/docs/ja/parts/funnel-steps.md +27 -8
  132. package/docs/ja/parts/heatmap.md +45 -0
  133. package/docs/ja/parts/kpi-card.md +2 -1
  134. package/docs/ja/parts/legend.md +36 -0
  135. package/docs/ja/parts/line-chart.md +48 -22
  136. package/docs/ja/parts/sample-data.md +61 -0
  137. package/docs/ja/parts/scatter-chart.md +69 -0
  138. package/docs/ja/parts/updated-at.md +27 -0
  139. package/docs/ja/parts/waterfall-chart.md +61 -0
  140. package/docs/ja/slide/slide-chart-full.md +48 -0
  141. package/docs/ja/slide/slide-chart-insight.md +52 -0
  142. package/docs/ja/slide/slide-columns.md +46 -0
  143. package/docs/ja/slide/slide-comparison.md +68 -0
  144. package/docs/ja/slide/slide-cover.md +39 -0
  145. package/docs/ja/slide/slide-exec-summary.md +47 -0
  146. package/docs/ja/slide/slide-heading.md +38 -0
  147. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  148. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  149. package/docs/ja/slide/slide-next-actions.md +49 -0
  150. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  151. package/docs/ja/slide/slide-section.md +44 -0
  152. package/docs/ja/slide/slide-source-line.md +34 -0
  153. package/docs/ja/ui/empty.md +1 -1
  154. package/docs/ja/ui/error-state.md +4 -3
  155. package/docs/ja/ui/loading.md +1 -1
  156. package/docs/ja/ui/toggle-group.md +4 -3
  157. package/package.json +2 -2
  158. package/registry/ui/area-chart.tsx +88 -26
  159. package/registry/ui/attention-list.tsx +303 -0
  160. package/registry/ui/bar-chart.tsx +88 -26
  161. package/registry/ui/bar-list.tsx +87 -24
  162. package/registry/ui/box-plot-chart.tsx +3906 -0
  163. package/registry/ui/category-bar.tsx +54 -19
  164. package/registry/ui/chart-frame.tsx +56 -21
  165. package/registry/ui/dashboard-card.tsx +2 -2
  166. package/registry/ui/delta.tsx +11 -8
  167. package/registry/ui/donut-chart.tsx +89 -26
  168. package/registry/ui/echart.tsx +194 -44
  169. package/registry/ui/funnel-steps.tsx +33 -5
  170. package/registry/ui/heatmap.tsx +55 -21
  171. package/registry/ui/histogram-chart.tsx +3986 -0
  172. package/registry/ui/kpi-card.tsx +41 -14
  173. package/registry/ui/legend.tsx +87 -24
  174. package/registry/ui/line-chart.tsx +88 -26
  175. package/registry/ui/page-shell.tsx +9 -9
  176. package/registry/ui/paired-chart.tsx +3827 -0
  177. package/registry/ui/sample-data.tsx +1 -1
  178. package/registry/ui/scatter-chart.tsx +88 -26
  179. package/registry/ui/section-header.tsx +2 -2
  180. package/registry/ui/slider.tsx +1 -1
  181. package/registry/ui/status-badge.tsx +8 -8
  182. package/registry/ui/tooltip.tsx +6 -2
  183. package/registry/ui/tracker.tsx +9 -3
  184. package/registry/ui/trend-indicator.tsx +2 -2
  185. package/registry/ui/waterfall-chart.tsx +88 -26
  186. package/skills/vantage-design/SKILL.md +27 -25
  187. package/skills/vantage-design/references/extract.md +16 -14
  188. package/skills/vantage-design/references/format.md +6 -5
  189. package/skills/vantage-design/references/presets.md +60 -110
  190. package/skills/vantage-pitfalls/SKILL.md +5 -0
  191. package/skills/vantage-slide/SKILL.md +54 -36
  192. package/templates/AGENTS.md +4 -2
  193. package/templates/agents/doc.md +48 -0
  194. package/templates/agents/page.md +22 -1
  195. package/templates/agents/slide.md +70 -2
  196. package/theme.css +16 -15
  197. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  198. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  199. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  200. package/dist/chunk-JKLLFVEU.js.map +0 -1
  201. package/dist/chunk-WAMZWZJB.js.map +0 -1
  202. package/dist/chunk-XPQSTZUU.js.map +0 -1
@@ -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
- return {
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
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
173
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
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: { color: tokens.popoverForeground ?? undefined, fontSize: 12 },
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 off the document root (`--positive`, …); `var(--token)` is read off the document root; any
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
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
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: ChartTokens | null,
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
- const budget = Math.max(80, (plotWidth ? Math.min(LEGEND_LINE_BUDGET, plotWidth) : LEGEND_LINE_BUDGET) - reserve)
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 ? Math.max(2, Math.min(TIME_AXIS_MAX_LABELS, Math.floor(plotWidth / TIME_LABEL_WIDTH))) : undefined,
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
- picked.length === 2 && pointGap !== null && Math.abs(picked[1]!.index - picked[0]!.index) * pointGap < HIGHLIGHT_LABEL_WIDTH
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
- right: reserveActions ? ACTIONS_RESERVE : 0,
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
- for (const p of list) {
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, tokens)
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"