@squadbase/vantage 0.7.0 → 0.7.1-dev1.0d13f9a
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/README.md +1 -1
- package/dist/add-AU3EDPNW.js +6 -0
- package/dist/add-AU3EDPNW.js.map +1 -0
- package/dist/{chunk-AABMKHEU.js → chunk-35B3YIBF.js} +16 -7
- package/dist/chunk-35B3YIBF.js.map +1 -0
- package/dist/{chunk-QNOX7WLZ.js → chunk-5HBUBVQ3.js} +94 -8
- package/dist/chunk-5HBUBVQ3.js.map +1 -0
- package/dist/{chunk-EG4PX726.js → chunk-GWXKENNH.js} +122 -5
- package/dist/chunk-GWXKENNH.js.map +1 -0
- package/dist/{chunk-77CISBMG.js → chunk-I2GWIPTD.js} +3685 -350
- package/dist/chunk-I2GWIPTD.js.map +1 -0
- package/dist/{chunk-TRMHNMMZ.js → chunk-LYUGDHSF.js} +3 -3
- package/dist/{chunk-TRMHNMMZ.js.map → chunk-LYUGDHSF.js.map} +1 -1
- package/dist/{chunk-2IAPIOPS.js → chunk-U6MWBCM6.js} +28 -7
- package/dist/chunk-U6MWBCM6.js.map +1 -0
- package/dist/{chunk-3RJEOP3J.js → chunk-XRKSIC5Z.js} +5 -3
- package/dist/chunk-XRKSIC5Z.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/components/index.d.ts +849 -12
- package/dist/components/index.js +5 -5
- package/dist/{delta-text-CbrOaW83.d.ts → delta-text-tav2Lh5d.d.ts} +1 -1
- package/dist/doc/index.d.ts +2 -2
- package/dist/doc/index.js +4 -4
- package/dist/{format-DihQkL1N.d.ts → format-D34piCei.d.ts} +2 -1
- package/dist/geo-japan-7BMSOJRX.js +8 -0
- package/dist/geo-japan-7BMSOJRX.js.map +1 -0
- package/dist/geo-us-GVOG7VXH.js +8 -0
- package/dist/geo-us-GVOG7VXH.js.map +1 -0
- package/dist/geo-world-35HL56UU.js +8 -0
- package/dist/geo-world-35HL56UU.js.map +1 -0
- package/dist/slide/index.d.ts +2 -2
- package/dist/slide/index.js +4 -4
- package/dist/table/index.js +5 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +35 -4
- package/dist/ui/index.js +174 -11
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-6DT7ADRP.js → upgrade-MCHMELWC.js} +3 -3
- package/dist/{upgrade-6DT7ADRP.js.map → upgrade-MCHMELWC.js.map} +1 -1
- package/docs/en/agent-skills.md +13 -14
- package/docs/en/changelog.md +8 -0
- package/docs/en/parts/draft-chart.md +42 -0
- package/docs/en/parts/page-shell.md +1 -1
- package/docs/index.json +70 -50
- package/docs/ja/agent-skills.md +14 -14
- package/docs/ja/changelog.md +8 -0
- package/docs/ja/parts/draft-chart.md +40 -0
- package/docs/ja/parts/page-shell.md +1 -1
- package/package.json +2 -2
- package/registry/parts.json +5319 -0
- package/registry/ui/area-chart.tsx +43 -8
- package/registry/ui/bar-chart.tsx +43 -8
- package/registry/ui/bar-list.tsx +43 -8
- package/registry/ui/box-plot-chart.tsx +42 -7
- package/registry/ui/category-bar.tsx +25 -5
- package/registry/ui/chart-frame.tsx +55 -14
- package/registry/ui/checkbox.tsx +4 -3
- package/registry/ui/code-block.tsx +44 -0
- package/registry/ui/combo-chart.tsx +1869 -0
- package/registry/ui/combobox.tsx +7 -0
- package/registry/ui/context-menu.tsx +71 -0
- package/registry/ui/dashboard-card.tsx +23 -10
- package/registry/ui/data-table.tsx +10 -2
- package/registry/ui/date-range-picker.tsx +51 -4
- package/registry/ui/delta.tsx +39 -12
- package/registry/ui/donut-chart.tsx +43 -8
- package/registry/ui/echart.tsx +167 -7
- package/registry/ui/file-dropzone.tsx +143 -0
- package/registry/ui/filter-bar.tsx +77 -2
- package/registry/ui/forecast-chart.tsx +4769 -0
- package/registry/ui/funnel-steps.tsx +23 -5
- package/registry/ui/heatmap.tsx +20 -3
- package/registry/ui/histogram-chart.tsx +42 -7
- package/registry/ui/kpi-card.tsx +15 -9
- package/registry/ui/legend.tsx +73 -15
- package/registry/ui/line-chart.tsx +43 -8
- package/registry/ui/metric-value.tsx +2 -2
- package/registry/ui/page-shell.tsx +27 -4
- package/registry/ui/paired-chart.tsx +44 -9
- package/registry/ui/pivot-table.tsx +1260 -0
- package/registry/ui/progress-circle.tsx +2 -2
- package/registry/ui/progress.tsx +1 -1
- package/registry/ui/scatter-chart.tsx +50 -12
- package/registry/ui/sparkline.tsx +44 -9
- package/registry/ui/states.tsx +34 -1
- package/registry/ui/status-badge.tsx +7 -7
- package/registry/ui/tabs.tsx +1 -0
- package/registry/ui/treemap.tsx +1934 -0
- package/registry/ui/waterfall-chart.tsx +64 -11
- package/templates/AGENTS.md +3 -3
- package/templates/agents/page.md +5 -4
- package/dist/add-4GHF2TX2.js +0 -6
- package/dist/add-4GHF2TX2.js.map +0 -1
- package/dist/chunk-2IAPIOPS.js.map +0 -1
- package/dist/chunk-3RJEOP3J.js.map +0 -1
- package/dist/chunk-77CISBMG.js.map +0 -1
- package/dist/chunk-AABMKHEU.js.map +0 -1
- package/dist/chunk-EG4PX726.js.map +0 -1
- package/dist/chunk-NZ5QIIG4.js +0 -74
- package/dist/chunk-NZ5QIIG4.js.map +0 -1
- package/dist/chunk-QNOX7WLZ.js.map +0 -1
package/registry/ui/combobox.tsx
CHANGED
|
@@ -29,6 +29,7 @@ function ComboboxTrigger({
|
|
|
29
29
|
return (
|
|
30
30
|
<ComboboxPrimitive.Trigger
|
|
31
31
|
data-slot="combobox-trigger"
|
|
32
|
+
aria-label="Toggle options"
|
|
32
33
|
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
|
33
34
|
{...props}
|
|
34
35
|
>
|
|
@@ -44,6 +45,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|
|
44
45
|
return (
|
|
45
46
|
<ComboboxPrimitive.Clear
|
|
46
47
|
data-slot="combobox-clear"
|
|
48
|
+
aria-label="Clear"
|
|
47
49
|
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
|
48
50
|
className={cn(className)}
|
|
49
51
|
{...props}
|
|
@@ -263,6 +265,11 @@ function ComboboxChip({
|
|
|
263
265
|
render={<Button variant="ghost" size="icon-xs" />}
|
|
264
266
|
className="-ml-1 opacity-50 hover:opacity-100"
|
|
265
267
|
data-slot="combobox-chip-remove"
|
|
268
|
+
aria-label={
|
|
269
|
+
typeof children === "string" || typeof children === "number"
|
|
270
|
+
? `Remove ${children}`
|
|
271
|
+
: "Remove"
|
|
272
|
+
}
|
|
266
273
|
>
|
|
267
274
|
<XIcon
|
|
268
275
|
className="pointer-events-none"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copied by `vantage add ui context-menu`. This file is yours to edit.
|
|
2
|
+
// Generated from @squadbase/squadkit registry/ui/context-menu.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
|
|
3
|
+
"use client"
|
|
4
|
+
|
|
5
|
+
import * as React from "react"
|
|
6
|
+
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
|
|
7
|
+
|
|
8
|
+
import { cn } from "@squadbase/vantage/ui"
|
|
9
|
+
import {
|
|
10
|
+
DropdownMenuCheckboxItem,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuGroup,
|
|
13
|
+
DropdownMenuItem,
|
|
14
|
+
DropdownMenuLabel,
|
|
15
|
+
DropdownMenuRadioGroup,
|
|
16
|
+
DropdownMenuRadioItem,
|
|
17
|
+
DropdownMenuSeparator,
|
|
18
|
+
DropdownMenuShortcut,
|
|
19
|
+
DropdownMenuSub,
|
|
20
|
+
DropdownMenuSubContent,
|
|
21
|
+
DropdownMenuSubTrigger,
|
|
22
|
+
} from "@squadbase/vantage/ui"
|
|
23
|
+
|
|
24
|
+
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
|
|
25
|
+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The area that opens the menu on right-click (long-press on touch). */
|
|
29
|
+
function ContextMenuTrigger({ className, ...props }: ContextMenuPrimitive.Trigger.Props) {
|
|
30
|
+
return (
|
|
31
|
+
<ContextMenuPrimitive.Trigger
|
|
32
|
+
data-slot="context-menu-trigger"
|
|
33
|
+
className={cn("select-none", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The popup is the same as DropdownMenu's (same look); only the anchor differs: the pointer, not a button.
|
|
40
|
+
function ContextMenuContent({
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
|
44
|
+
return (
|
|
45
|
+
<DropdownMenuContent
|
|
46
|
+
data-slot="context-menu-content"
|
|
47
|
+
align="start"
|
|
48
|
+
side="right"
|
|
49
|
+
sideOffset={0}
|
|
50
|
+
className={cn("w-auto", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
ContextMenu,
|
|
58
|
+
ContextMenuTrigger,
|
|
59
|
+
ContextMenuContent,
|
|
60
|
+
DropdownMenuGroup as ContextMenuGroup,
|
|
61
|
+
DropdownMenuLabel as ContextMenuLabel,
|
|
62
|
+
DropdownMenuItem as ContextMenuItem,
|
|
63
|
+
DropdownMenuCheckboxItem as ContextMenuCheckboxItem,
|
|
64
|
+
DropdownMenuRadioGroup as ContextMenuRadioGroup,
|
|
65
|
+
DropdownMenuRadioItem as ContextMenuRadioItem,
|
|
66
|
+
DropdownMenuSeparator as ContextMenuSeparator,
|
|
67
|
+
DropdownMenuShortcut as ContextMenuShortcut,
|
|
68
|
+
DropdownMenuSub as ContextMenuSub,
|
|
69
|
+
DropdownMenuSubTrigger as ContextMenuSubTrigger,
|
|
70
|
+
DropdownMenuSubContent as ContextMenuSubContent,
|
|
71
|
+
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* DashboardCardPreset — a card (title, description, action, content) for one tile that holds a chart, ranking or table; not for a single KPI, since `KpiCard` is itself a card.
|
|
7
|
+
*
|
|
8
|
+
* `DashboardCard` is the tighter card used for dashboard tiles: smaller padding
|
|
7
9
|
* than the plain `Card`, a title/description/action header, and a matching
|
|
8
10
|
* skeleton for the loading state. `DashboardCardPreset` wires the common
|
|
9
11
|
* arrangement in one prop-driven component.
|
|
@@ -127,7 +129,7 @@ export const DashboardCardFooter = forwardRef<HTMLDivElement, DashboardCardFoote
|
|
|
127
129
|
<div
|
|
128
130
|
ref={ref}
|
|
129
131
|
data-slot="dashboard-card-footer"
|
|
130
|
-
className={cn("flex items-center
|
|
132
|
+
className={cn("-mx-1 -mb-1 flex min-w-0 items-center justify-between gap-3 border-t px-4 py-2.5 text-xs text-muted-foreground tabular-nums group-data-[size=sm]:px-3", className)}
|
|
131
133
|
{...props}
|
|
132
134
|
/>
|
|
133
135
|
),
|
|
@@ -159,7 +161,10 @@ export interface DashboardCardPresetProps
|
|
|
159
161
|
Pick<DashboardCardProps, "subgrid" | "size"> {
|
|
160
162
|
title?: ReactNode
|
|
161
163
|
description?: ReactNode
|
|
164
|
+
/** One headline number or status at the right end of the header ("212 tasks", a `StatusBadge`). Put the number in `<span className="font-medium text-foreground">`. */
|
|
165
|
+
headline?: ReactNode
|
|
162
166
|
actions?: ReactNode
|
|
167
|
+
/** One line at the bottom of the card: a summary fact on the left, an action or hint on the right. */
|
|
163
168
|
footer?: ReactNode
|
|
164
169
|
headerClassName?: string
|
|
165
170
|
contentClassName?: string
|
|
@@ -169,6 +174,7 @@ export interface DashboardCardPresetProps
|
|
|
169
174
|
export function DashboardCardPreset({
|
|
170
175
|
title,
|
|
171
176
|
description,
|
|
177
|
+
headline,
|
|
172
178
|
actions,
|
|
173
179
|
footer,
|
|
174
180
|
children,
|
|
@@ -181,21 +187,28 @@ export function DashboardCardPreset({
|
|
|
181
187
|
}: DashboardCardPresetProps) {
|
|
182
188
|
// Inside SampleData the card shows the mark in its header, and the parts inside it do not repeat it.
|
|
183
189
|
const sample = useSampleMark()
|
|
184
|
-
const
|
|
190
|
+
const hasTitle = Boolean(title || description)
|
|
191
|
+
// A card with no title of its own (the chart inside has it) floats the mark in the corner instead of adding a row of its own.
|
|
192
|
+
const floatingMark = sample && !hasTitle && !headline && !actions
|
|
193
|
+
const hasHeader = hasTitle || headline || actions || (sample && !floatingMark)
|
|
185
194
|
return (
|
|
186
|
-
<DashboardCard className={className} subgrid={subgrid} {...props}>
|
|
195
|
+
<DashboardCard className={cn(floatingMark && "relative", className)} subgrid={subgrid} {...props}>
|
|
187
196
|
<SampleClaim>
|
|
197
|
+
{floatingMark && <SampleBadge label={sample.label} className="absolute top-2 right-2 z-10" />}
|
|
188
198
|
{hasHeader && (
|
|
189
199
|
<DashboardCardHeader className={cn(subgrid && "row-start-1", headerClassName)}>
|
|
190
|
-
{(
|
|
191
|
-
<div>
|
|
192
|
-
|
|
200
|
+
{(hasTitle || sample) && (
|
|
201
|
+
<div className="min-w-0">
|
|
202
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
203
|
+
{title && <DashboardCardTitle>{title}</DashboardCardTitle>}
|
|
204
|
+
{sample && <SampleBadge label={sample.label} />}
|
|
205
|
+
</div>
|
|
193
206
|
{description && <DashboardCardDescription>{description}</DashboardCardDescription>}
|
|
194
207
|
</div>
|
|
195
208
|
)}
|
|
196
|
-
{(
|
|
197
|
-
<DashboardCardAction className=
|
|
198
|
-
{
|
|
209
|
+
{(headline || actions) && (
|
|
210
|
+
<DashboardCardAction className="flex items-center gap-2">
|
|
211
|
+
{headline && <div data-slot="dashboard-card-headline" className="text-sm tabular-nums text-muted-foreground">{headline}</div>}
|
|
199
212
|
{actions}
|
|
200
213
|
</DashboardCardAction>
|
|
201
214
|
)}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* DataTablePreset — a table (toolbar, body, pagination) over TanStack Table.
|
|
7
|
+
*
|
|
8
|
+
* `DataTable` is the compound form: `<DataTable>` owns the
|
|
7
9
|
* table instance and publishes it on context; the toolbar, body, pagination and
|
|
8
10
|
* column-visibility parts read from that context, so a page composes only the
|
|
9
11
|
* pieces it wants. `DataTablePreset` assembles the usual arrangement.
|
|
@@ -309,6 +311,8 @@ const TEXT = {
|
|
|
309
311
|
trend: (points: number, first: string, last: string, min: string, max: string) =>
|
|
310
312
|
`${points}点の推移、${first}から${last}、最小${min}、最大${max}`,
|
|
311
313
|
noData: "データがありません",
|
|
314
|
+
search: "検索",
|
|
315
|
+
pageSizeLabel: "1ページの表示件数",
|
|
312
316
|
pageSize: (size: number) => `${size}件`,
|
|
313
317
|
range: (total: number, start: number, end: number) => `${total}件中 ${start} - ${end} を表示`,
|
|
314
318
|
first: "最初",
|
|
@@ -326,6 +330,8 @@ const TEXT = {
|
|
|
326
330
|
trend: (points: number, first: string, last: string, min: string, max: string) =>
|
|
327
331
|
`Trend over ${points} points: ${first} to ${last}, min ${min}, max ${max}`,
|
|
328
332
|
noData: "No results.",
|
|
333
|
+
search: "Search",
|
|
334
|
+
pageSizeLabel: "Rows per page",
|
|
329
335
|
pageSize: (size: number) => `${size} / page`,
|
|
330
336
|
range: (total: number, start: number, end: number) => `Showing ${start}–${end} of ${total}`,
|
|
331
337
|
first: "First",
|
|
@@ -918,6 +924,7 @@ export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps
|
|
|
918
924
|
({ showSearch, toolbar, bulkActions, className, ...props }, ref) => {
|
|
919
925
|
const { globalFilter, setGlobalFilter, hasSelection, selectedRows, enableFiltering } =
|
|
920
926
|
useDataTable()
|
|
927
|
+
const t = useText()
|
|
921
928
|
|
|
922
929
|
const showSearchEffective = showSearch ?? enableFiltering
|
|
923
930
|
|
|
@@ -936,6 +943,7 @@ export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps
|
|
|
936
943
|
<div className="relative">
|
|
937
944
|
<Search className="absolute top-2.5 left-2.5 size-4 text-muted-foreground" />
|
|
938
945
|
<Input
|
|
946
|
+
aria-label={t.search}
|
|
939
947
|
value={globalFilter}
|
|
940
948
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
941
949
|
className="max-w-sm pl-8"
|
|
@@ -1205,7 +1213,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
1205
1213
|
value={String(currentPageSize)}
|
|
1206
1214
|
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
1207
1215
|
>
|
|
1208
|
-
<SelectTrigger size="sm" className="min-w-[80px]">
|
|
1216
|
+
<SelectTrigger size="sm" className="min-w-[80px]" aria-label={t.pageSizeLabel}>
|
|
1209
1217
|
<SelectValue />
|
|
1210
1218
|
</SelectTrigger>
|
|
1211
1219
|
<SelectContent>
|
|
@@ -12,6 +12,7 @@ import { forwardRef, useCallback, useMemo, useState } from "react"
|
|
|
12
12
|
import { CalendarIcon } from "lucide-react"
|
|
13
13
|
import {
|
|
14
14
|
addMonths,
|
|
15
|
+
differenceInCalendarDays,
|
|
15
16
|
endOfDay,
|
|
16
17
|
endOfMonth,
|
|
17
18
|
endOfWeek,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
startOfMonth,
|
|
21
22
|
startOfWeek,
|
|
22
23
|
subDays,
|
|
24
|
+
subYears,
|
|
23
25
|
subMonths,
|
|
24
26
|
subWeeks,
|
|
25
27
|
} from "date-fns"
|
|
@@ -28,7 +30,7 @@ import type { DateRange as RdpDateRange } from "react-day-picker"
|
|
|
28
30
|
|
|
29
31
|
import { cn } from "@squadbase/vantage/ui"
|
|
30
32
|
import { useIsMobile } from "@squadbase/vantage/ui"
|
|
31
|
-
import { formatDate } from "@squadbase/vantage/components"
|
|
33
|
+
import { formatDate, formatDateRange as formatRange } from "@squadbase/vantage/components"
|
|
32
34
|
import { Button } from "@squadbase/vantage/ui"
|
|
33
35
|
import { Calendar } from "@squadbase/vantage/ui"
|
|
34
36
|
import { Popover, PopoverContent, PopoverTrigger } from "@squadbase/vantage/ui"
|
|
@@ -213,6 +215,23 @@ export function fiscalPeriodRange(
|
|
|
213
215
|
return { from: periodStart, to: endOfMonth(addMonths(periodStart, length - 1)) }
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
/**
|
|
219
|
+
* 選択範囲の比較期間を計算する。`previous-period` は同じ日数だけ直前にずらした範囲、
|
|
220
|
+
* `previous-year` は両端を 1 年前にした範囲(うるう日は 2/28 に丸める)。
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* comparisonRange({ from: new Date(2026, 2, 1), to: new Date(2026, 2, 31) }, "previous-period")
|
|
224
|
+
* // 2026-01-29〜2026-02-28
|
|
225
|
+
*/
|
|
226
|
+
export function comparisonRange(
|
|
227
|
+
{ from, to }: { from: Date; to: Date },
|
|
228
|
+
kind: "previous-period" | "previous-year",
|
|
229
|
+
): { from: Date; to: Date } {
|
|
230
|
+
if (kind === "previous-year") return { from: subYears(from, 1), to: subYears(to, 1) }
|
|
231
|
+
const days = differenceInCalendarDays(to, from) + 1
|
|
232
|
+
return { from: subDays(from, days), to: subDays(from, 1) }
|
|
233
|
+
}
|
|
234
|
+
|
|
216
235
|
const PRESET_LABELS = {
|
|
217
236
|
ja: {
|
|
218
237
|
today: "今日",
|
|
@@ -251,8 +270,14 @@ const PRESET_LABELS = {
|
|
|
251
270
|
} as const
|
|
252
271
|
|
|
253
272
|
const TEXT = {
|
|
254
|
-
ja: {
|
|
255
|
-
|
|
273
|
+
ja: {
|
|
274
|
+
placeholder: "期間を選択",
|
|
275
|
+
comparison: { "previous-period": "前期間", "previous-year": "前年同期" },
|
|
276
|
+
},
|
|
277
|
+
en: {
|
|
278
|
+
placeholder: "Pick a date range",
|
|
279
|
+
comparison: { "previous-period": "vs. previous period", "previous-year": "vs. same period last year" },
|
|
280
|
+
},
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
/** カレンダーに渡す date-fns の locale。`ja*` なら日本語、それ以外は react-day-picker の既定(英語)。 */
|
|
@@ -398,6 +423,14 @@ export interface DateRangePickerProps {
|
|
|
398
423
|
* <DateRangePicker fiscalYearStartMonth={1} />
|
|
399
424
|
*/
|
|
400
425
|
fiscalYearStartMonth?: number
|
|
426
|
+
/**
|
|
427
|
+
* 比較期間。渡すと選択範囲から比較する範囲を計算し、トリガーに添える。
|
|
428
|
+
* `previous-period` は直前の同じ日数、`previous-year` は 1 年前の同じ日付。比較の日付は渡さない。
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* <DateRangePicker comparison="previous-year" />
|
|
432
|
+
*/
|
|
433
|
+
comparison?: "previous-period" | "previous-year"
|
|
401
434
|
maxDate?: Date
|
|
402
435
|
minDate?: Date
|
|
403
436
|
placeholder?: string
|
|
@@ -415,6 +448,7 @@ export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
|
415
448
|
showPresets = true,
|
|
416
449
|
locale: localeProp,
|
|
417
450
|
fiscalYearStartMonth,
|
|
451
|
+
comparison,
|
|
418
452
|
maxDate,
|
|
419
453
|
minDate,
|
|
420
454
|
placeholder,
|
|
@@ -451,11 +485,19 @@ export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
|
451
485
|
const formatDateRange = () => {
|
|
452
486
|
if (!value?.from) return placeholder ?? t.placeholder
|
|
453
487
|
if (!value.to) return formatDate(value.from, "date", locale)
|
|
454
|
-
|
|
488
|
+
// The same writer as the page header's period chip, so one range reads the same in both places.
|
|
489
|
+
return formatRange(value.from, value.to, { locale })
|
|
455
490
|
}
|
|
456
491
|
// 選んだ期間がプリセットの結果と一致するときは、日付の前にそのプリセット名を出す。
|
|
457
492
|
const selectedPreset = findMatchingPreset(presets, value)
|
|
458
493
|
const presetLabel = selectedPreset?.label
|
|
494
|
+
const comparisonText =
|
|
495
|
+
comparison && value?.from && value.to
|
|
496
|
+
? (() => {
|
|
497
|
+
const c = comparisonRange({ from: value.from, to: value.to }, comparison)
|
|
498
|
+
return `${t.comparison[comparison]} ${formatRange(c.from, c.to, { locale })}`
|
|
499
|
+
})()
|
|
500
|
+
: undefined
|
|
459
501
|
|
|
460
502
|
// 開いたとき、右の月が終了月・左がその前月になるよう、表示月(左の月)を決める。1 か月表示(モバイル)は終了月。
|
|
461
503
|
const endMonth = value?.to ?? value?.from
|
|
@@ -492,6 +534,11 @@ export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
|
492
534
|
) : (
|
|
493
535
|
<span>{formatDateRange()}</span>
|
|
494
536
|
)}
|
|
537
|
+
{comparisonText && (
|
|
538
|
+
<span data-slot="date-range-picker-comparison" className="text-muted-foreground">
|
|
539
|
+
{comparisonText}
|
|
540
|
+
</span>
|
|
541
|
+
)}
|
|
495
542
|
</PopoverTrigger>
|
|
496
543
|
<PopoverContent
|
|
497
544
|
align={align}
|
package/registry/ui/delta.tsx
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Delta — the change of a value against a comparison,
|
|
6
|
+
* Delta — the change of a value against a comparison, as arrow + signed change + comparison label on one line; it is not a card, so place it beside a value.
|
|
7
|
+
*
|
|
8
|
+
* It renders as arrow + signed
|
|
7
9
|
* change + comparison label ("↑ +12.4% vs. prior month"; ja: "↑ +12.4% 前月比") on one line, in the page's
|
|
8
10
|
* language (`<html lang>`) unless `locale` is given.
|
|
9
11
|
*
|
|
@@ -191,7 +193,7 @@ const deltaVariants = cva(
|
|
|
191
193
|
{
|
|
192
194
|
variants: {
|
|
193
195
|
variant: {
|
|
194
|
-
badge: "h-5 rounded-md
|
|
196
|
+
badge: "h-5 rounded-md px-2 py-0.5 text-xs font-medium [&>svg]:size-3",
|
|
195
197
|
inline: "text-sm [&>svg]:size-3.5",
|
|
196
198
|
},
|
|
197
199
|
sentiment: {
|
|
@@ -201,9 +203,9 @@ const deltaVariants = cva(
|
|
|
201
203
|
},
|
|
202
204
|
},
|
|
203
205
|
compoundVariants: [
|
|
204
|
-
{ variant: "badge", sentiment: "positive", className: "
|
|
205
|
-
{ variant: "badge", sentiment: "negative", className: "
|
|
206
|
-
{ variant: "badge", sentiment: "neutral", className: "
|
|
206
|
+
{ variant: "badge", sentiment: "positive", className: "bg-positive-subtle" },
|
|
207
|
+
{ variant: "badge", sentiment: "negative", className: "bg-negative-subtle" },
|
|
208
|
+
{ variant: "badge", sentiment: "neutral", className: "bg-neutral-subtle" },
|
|
207
209
|
],
|
|
208
210
|
defaultVariants: {
|
|
209
211
|
variant: "badge",
|
|
@@ -285,6 +287,8 @@ export function deltaAriaLabel(
|
|
|
285
287
|
return t.change(label, change === "points" ? amount.replace(/pt$/, t.points) : amount, direction === "up")
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
const SENTIMENT_TEXT = { positive: "text-positive", negative: "text-negative", neutral: "text-neutral" } as const
|
|
291
|
+
|
|
288
292
|
export const Delta = forwardRef<HTMLSpanElement, DeltaProps>(
|
|
289
293
|
(
|
|
290
294
|
{
|
|
@@ -328,18 +332,41 @@ export const Delta = forwardRef<HTMLSpanElement, DeltaProps>(
|
|
|
328
332
|
data-sentiment={result.sentiment}
|
|
329
333
|
aria-label={deltaAriaLabel(result, locale, display, format, changeDigits)}
|
|
330
334
|
className={cn(
|
|
331
|
-
|
|
335
|
+
variant === "badge"
|
|
336
|
+
? "inline-flex w-fit shrink-0 items-center gap-1.5 font-medium whitespace-nowrap tabular-nums"
|
|
337
|
+
: deltaVariants({ variant, sentiment: result.sentiment }),
|
|
338
|
+
// The chip carries the colour of the change; the root repeats it for the "—" case and for `className` consumers.
|
|
339
|
+
variant === "badge" && SENTIMENT_TEXT[result.sentiment],
|
|
332
340
|
!comparable && "text-muted-foreground",
|
|
333
341
|
className,
|
|
334
342
|
)}
|
|
335
343
|
{...props}
|
|
336
344
|
>
|
|
337
|
-
{
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
345
|
+
{variant === "badge" ? (
|
|
346
|
+
<>
|
|
347
|
+
<span
|
|
348
|
+
data-slot="delta-chip"
|
|
349
|
+
className={cn(deltaVariants({ variant, sentiment: result.sentiment }), !comparable && "text-muted-foreground")}
|
|
350
|
+
>
|
|
351
|
+
{comparable && <Icon aria-hidden="true" />}
|
|
352
|
+
<span aria-hidden="true">{text ?? DELTA_NOT_COMPARABLE}</span>
|
|
353
|
+
</span>
|
|
354
|
+
{showLabel && (
|
|
355
|
+
<span aria-hidden="true" className="text-xs font-normal text-muted-foreground">
|
|
356
|
+
{result.label}
|
|
357
|
+
</span>
|
|
358
|
+
)}
|
|
359
|
+
</>
|
|
360
|
+
) : (
|
|
361
|
+
<>
|
|
362
|
+
{comparable && <Icon aria-hidden="true" />}
|
|
363
|
+
<span aria-hidden="true">{text ?? DELTA_NOT_COMPARABLE}</span>
|
|
364
|
+
{showLabel && (
|
|
365
|
+
<span aria-hidden="true" className="font-normal text-muted-foreground">
|
|
366
|
+
{result.label}
|
|
367
|
+
</span>
|
|
368
|
+
)}
|
|
369
|
+
</>
|
|
343
370
|
)}
|
|
344
371
|
</span>
|
|
345
372
|
)
|
|
@@ -176,10 +176,25 @@ function useLocale(locale?: string | null): string {
|
|
|
176
176
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
|
-
*
|
|
180
|
-
* `role="img"`
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
180
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
181
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
182
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
183
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
184
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
185
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
186
|
+
*/
|
|
187
|
+
interface ChartFramePlot {
|
|
188
|
+
image: boolean
|
|
189
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
190
|
+
claimed: boolean
|
|
191
|
+
/** Take over the image role; returns the release. */
|
|
192
|
+
claim: () => () => void
|
|
193
|
+
ariaLabel?: string
|
|
194
|
+
ariaLabelledBy?: string
|
|
195
|
+
ariaDescribedBy?: string
|
|
196
|
+
}
|
|
197
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
183
198
|
|
|
184
199
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
185
200
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -403,6 +418,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
403
418
|
tooltip: {
|
|
404
419
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
405
420
|
transitionDuration: 0,
|
|
421
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
422
|
+
confine: true,
|
|
406
423
|
backgroundColor: tokens.popover ?? undefined,
|
|
407
424
|
borderColor: line,
|
|
408
425
|
borderWidth: 1,
|
|
@@ -2278,6 +2295,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2278
2295
|
|
|
2279
2296
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2280
2297
|
const ACTIONS_RESERVE = 80
|
|
2298
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2299
|
+
const BAR_MAX_WIDTH = 40
|
|
2300
|
+
const BAR_RADIUS = 3
|
|
2301
|
+
const STACK_GAP = 1.5
|
|
2281
2302
|
|
|
2282
2303
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2283
2304
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2491,6 +2512,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2491
2512
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2492
2513
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2493
2514
|
})()
|
|
2515
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2516
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2494
2517
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2495
2518
|
|
|
2496
2519
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2700,15 +2723,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2700
2723
|
const seriesData = pad(values[c]!)
|
|
2701
2724
|
if (kind === "bar") {
|
|
2702
2725
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2703
|
-
// Stacked:
|
|
2704
|
-
const radius = stacked
|
|
2726
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2727
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2728
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2705
2729
|
return {
|
|
2706
2730
|
id: SERIES_ID_PREFIX + category,
|
|
2707
2731
|
type: "bar" as const,
|
|
2708
2732
|
name: category,
|
|
2709
2733
|
stack: stacked ? "total" : undefined,
|
|
2710
2734
|
barCategoryGap,
|
|
2711
|
-
|
|
2735
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2736
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2712
2737
|
markPoint: markPointFor(c, solid),
|
|
2713
2738
|
markLine: undefined,
|
|
2714
2739
|
data: seriesData.map((value, i) => {
|
|
@@ -3004,6 +3029,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3004
3029
|
},
|
|
3005
3030
|
})
|
|
3006
3031
|
}
|
|
3032
|
+
if (diverging) {
|
|
3033
|
+
markLines.push({
|
|
3034
|
+
name: "zero",
|
|
3035
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3036
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3037
|
+
label: { show: false },
|
|
3038
|
+
})
|
|
3039
|
+
}
|
|
3007
3040
|
if (forecast) {
|
|
3008
3041
|
const forecastSpan =
|
|
3009
3042
|
plotWidth && labels.length > 1
|
|
@@ -3149,6 +3182,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3149
3182
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3150
3183
|
boundaryGap: kind === "bar",
|
|
3151
3184
|
inverse: horizontalBars,
|
|
3185
|
+
// Diverging: the axis stays at the plot's edge (not at 0, where labels would hit the negative bars); the zero line is a mark line.
|
|
3186
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3152
3187
|
axisTick: { show: false },
|
|
3153
3188
|
axisLabel: {
|
|
3154
3189
|
hideOverlap: true,
|
|
@@ -3251,7 +3286,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3251
3286
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3252
3287
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3253
3288
|
? { left: 0, right: 0 }
|
|
3254
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3289
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3255
3290
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3256
3291
|
type: "plain",
|
|
3257
3292
|
icon: "circle",
|