@squadbase/vantage 0.7.1-dev1.01a6413 → 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/dist/{chunk-MQUIAP66.js → chunk-5HBUBVQ3.js} +28 -7
- 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-YJQCB3UP.js → chunk-I2GWIPTD.js} +219 -67
- package/dist/chunk-I2GWIPTD.js.map +1 -0
- package/dist/{chunk-P2KIHF65.js → chunk-U6MWBCM6.js} +28 -7
- package/dist/chunk-U6MWBCM6.js.map +1 -0
- package/dist/components/index.d.ts +223 -6
- package/dist/components/index.js +4 -4
- package/dist/doc/index.js +1 -1
- package/dist/slide/index.js +1 -1
- package/dist/table/index.js +4 -4
- package/dist/ui/index.d.ts +35 -4
- package/dist/ui/index.js +174 -10
- package/dist/ui/index.js.map +1 -1
- package/package.json +2 -2
- package/registry/parts.json +101 -3
- 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 +47 -14
- package/registry/ui/checkbox.tsx +4 -3
- package/registry/ui/code-block.tsx +44 -0
- package/registry/ui/combo-chart.tsx +20 -3
- package/registry/ui/combobox.tsx +7 -0
- package/registry/ui/context-menu.tsx +71 -0
- package/registry/ui/dashboard-card.tsx +20 -9
- package/registry/ui/data-table.tsx +7 -1
- package/registry/ui/date-range-picker.tsx +51 -4
- package/registry/ui/delta.tsx +36 -11
- 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 +74 -1
- package/registry/ui/forecast-chart.tsx +42 -7
- package/registry/ui/funnel-steps.tsx +22 -4
- package/registry/ui/heatmap.tsx +20 -3
- package/registry/ui/histogram-chart.tsx +42 -7
- package/registry/ui/kpi-card.tsx +12 -8
- package/registry/ui/legend.tsx +73 -15
- package/registry/ui/line-chart.tsx +43 -8
- package/registry/ui/page-shell.tsx +2 -0
- package/registry/ui/paired-chart.tsx +43 -8
- package/registry/ui/progress.tsx +1 -1
- package/registry/ui/scatter-chart.tsx +50 -12
- package/registry/ui/sparkline.tsx +43 -8
- 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 +20 -3
- package/registry/ui/waterfall-chart.tsx +64 -11
- package/dist/chunk-EG4PX726.js.map +0 -1
- package/dist/chunk-MQUIAP66.js.map +0 -1
- package/dist/chunk-P2KIHF65.js.map +0 -1
- package/dist/chunk-YJQCB3UP.js.map +0 -1
|
@@ -163,10 +163,25 @@ function useLocale(locale?: string | null): string {
|
|
|
163
163
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
167
|
-
* `role="img"`
|
|
166
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
167
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
168
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
169
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
170
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
171
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
172
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
168
173
|
*/
|
|
169
|
-
|
|
174
|
+
interface ChartFramePlot {
|
|
175
|
+
image: boolean
|
|
176
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
177
|
+
claimed: boolean
|
|
178
|
+
/** Take over the image role; returns the release. */
|
|
179
|
+
claim: () => () => void
|
|
180
|
+
ariaLabel?: string
|
|
181
|
+
ariaLabelledBy?: string
|
|
182
|
+
ariaDescribedBy?: string
|
|
183
|
+
}
|
|
184
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
170
185
|
|
|
171
186
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
172
187
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -390,6 +405,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
390
405
|
tooltip: {
|
|
391
406
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
392
407
|
transitionDuration: 0,
|
|
408
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
409
|
+
confine: true,
|
|
393
410
|
backgroundColor: tokens.popover ?? undefined,
|
|
394
411
|
borderColor: line,
|
|
395
412
|
borderWidth: 1,
|
|
@@ -1439,8 +1456,11 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
|
|
|
1439
1456
|
/>
|
|
1440
1457
|
)
|
|
1441
1458
|
|
|
1459
|
+
// aria-label is prohibited on a role-less div: a labelled bar is a group (the label names it, the marker stays reachable).
|
|
1460
|
+
const labelled = props["aria-label"] != null || props["aria-labelledby"] != null
|
|
1461
|
+
|
|
1442
1462
|
return (
|
|
1443
|
-
<div ref={ref} data-slot="category-bar" className={cn("w-full", className)} {...props}>
|
|
1463
|
+
<div ref={ref} data-slot="category-bar" role={labelled ? "group" : undefined} className={cn("w-full", className)} {...props}>
|
|
1444
1464
|
{showLabels && labels.length > 0 && (
|
|
1445
1465
|
<div data-slot="category-bar-labels" className="relative mb-2 h-4 text-xs text-muted-foreground tabular-nums">
|
|
1446
1466
|
{labels.map((label, i) => (
|
|
@@ -1481,7 +1501,7 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
|
|
|
1481
1501
|
>
|
|
1482
1502
|
{marker.tooltip ? (
|
|
1483
1503
|
<Tooltip>
|
|
1484
|
-
<TooltipTrigger render={<
|
|
1504
|
+
<TooltipTrigger render={<button type="button" aria-label={marker.tooltip} className="block cursor-default py-1" />}>
|
|
1485
1505
|
{markerDot}
|
|
1486
1506
|
</TooltipTrigger>
|
|
1487
1507
|
<TooltipContent>{marker.tooltip}</TooltipContent>
|
|
@@ -8,20 +8,23 @@
|
|
|
8
8
|
* by the title and described by the summary, so a screen reader hears what the
|
|
9
9
|
* chart says, not just that there is one.
|
|
10
10
|
*
|
|
11
|
-
* The plot
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The plot is the frame's one `role="img"`. When it is an `EChart`, that
|
|
12
|
+
* canvas takes the role (and the frame's name and description) and the plot
|
|
13
|
+
* area drops it, so the copy / download buttons are not inside the image;
|
|
14
|
+
* any other plot keeps the role on the plot area. `dataTable` adds the numbers
|
|
15
|
+
* as a visually-hidden table beside it, so a screen reader can read the values
|
|
16
|
+
* the canvas only draws.
|
|
14
17
|
*
|
|
15
18
|
* The plot area keeps its height in every state: while loading it shows a
|
|
16
19
|
* skeleton shaped like the final chart, and with no data it says so (and offers
|
|
17
20
|
* one action) instead of drawing empty axes.
|
|
18
21
|
*/
|
|
19
|
-
import { forwardRef, useId, type HTMLAttributes, type ReactNode } from "react"
|
|
22
|
+
import { forwardRef, useCallback, useId, useMemo, useState, type HTMLAttributes, type ReactNode } from "react"
|
|
20
23
|
|
|
21
24
|
import { cn } from "@squadbase/vantage/ui"
|
|
22
25
|
import type { ChartEmptyState } from "@squadbase/vantage/components"
|
|
23
26
|
import { useSyncExternalStore } from "react"
|
|
24
|
-
import { createContext,
|
|
27
|
+
import { createContext, useEffect, useRef, type ForwardedRef, type RefObject } from "react"
|
|
25
28
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
26
29
|
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
27
30
|
import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
@@ -170,10 +173,25 @@ function useLocale(locale?: string | null): string {
|
|
|
170
173
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
171
174
|
|
|
172
175
|
/**
|
|
173
|
-
*
|
|
174
|
-
* `role="img"`
|
|
176
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
177
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
178
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
179
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
180
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
181
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
182
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
175
183
|
*/
|
|
176
|
-
|
|
184
|
+
interface ChartFramePlot {
|
|
185
|
+
image: boolean
|
|
186
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
187
|
+
claimed: boolean
|
|
188
|
+
/** Take over the image role; returns the release. */
|
|
189
|
+
claim: () => () => void
|
|
190
|
+
ariaLabel?: string
|
|
191
|
+
ariaLabelledBy?: string
|
|
192
|
+
ariaDescribedBy?: string
|
|
193
|
+
}
|
|
194
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
177
195
|
|
|
178
196
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
179
197
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -397,6 +415,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
397
415
|
tooltip: {
|
|
398
416
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
399
417
|
transitionDuration: 0,
|
|
418
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
419
|
+
confine: true,
|
|
400
420
|
backgroundColor: tokens.popover ?? undefined,
|
|
401
421
|
borderColor: line,
|
|
402
422
|
borderWidth: 1,
|
|
@@ -1422,7 +1442,8 @@ export interface ChartFrameProps extends Omit<HTMLAttributes<HTMLElement>, "titl
|
|
|
1422
1442
|
/** 見出しが無いときのプロット領域のラベル。 */
|
|
1423
1443
|
ariaLabel?: string
|
|
1424
1444
|
/**
|
|
1425
|
-
*
|
|
1445
|
+
* プロットの役割。既定 `"img"`(canvas のグラフ。`role="img"` と名前・要約は
|
|
1446
|
+
* `EChart` が canvas にだけ付け、操作ボタンは画像の外に置く)。行がボタンやリンクになる
|
|
1426
1447
|
* HTML のグラフ(BarList・FunnelSteps)は `"group"` にして、中身を読ませる。
|
|
1427
1448
|
*/
|
|
1428
1449
|
plotRole?: "img" | "group"
|
|
@@ -1462,6 +1483,18 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1462
1483
|
const titleId = title ? `${id}-title` : undefined
|
|
1463
1484
|
const summaryId = summary ? `${id}-summary` : undefined
|
|
1464
1485
|
const state = isLoading ? "loading" : isEmpty ? "empty" : "ready"
|
|
1486
|
+
// An EChart inside takes the image role for its canvas (see ChartFramePlot).
|
|
1487
|
+
const [claims, setClaims] = useState(0)
|
|
1488
|
+
const claim = useCallback(() => {
|
|
1489
|
+
setClaims((n) => n + 1)
|
|
1490
|
+
return () => setClaims((n) => n - 1)
|
|
1491
|
+
}, [])
|
|
1492
|
+
const claimed = claims > 0 && plotRole === "img"
|
|
1493
|
+
const plotOwnsImage = state === "ready" && claimed
|
|
1494
|
+
const plot = useMemo(
|
|
1495
|
+
() => ({ image: plotRole === "img", claimed, claim, ariaLabel: titleId ? undefined : ariaLabel, ariaLabelledBy: titleId, ariaDescribedBy: summaryId }),
|
|
1496
|
+
[plotRole, claimed, claim, titleId, ariaLabel, summaryId],
|
|
1497
|
+
)
|
|
1465
1498
|
|
|
1466
1499
|
return (
|
|
1467
1500
|
<figure data-slot="chart-frame" className={cn("m-0 flex w-full min-w-0 flex-col gap-3", className)} {...props}>
|
|
@@ -1483,10 +1516,10 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1483
1516
|
ref={ref}
|
|
1484
1517
|
data-slot="chart-frame-body"
|
|
1485
1518
|
data-state={state}
|
|
1486
|
-
role={state === "ready" ? plotRole : "group"}
|
|
1487
|
-
aria-label={titleId ? undefined : ariaLabel}
|
|
1488
|
-
aria-labelledby={titleId}
|
|
1489
|
-
aria-describedby={summaryId}
|
|
1519
|
+
role={plotOwnsImage ? undefined : state === "ready" ? plotRole : "group"}
|
|
1520
|
+
aria-label={plotOwnsImage || titleId ? undefined : ariaLabel}
|
|
1521
|
+
aria-labelledby={plotOwnsImage ? undefined : titleId}
|
|
1522
|
+
aria-describedby={plotOwnsImage ? undefined : summaryId}
|
|
1490
1523
|
aria-busy={isLoading || undefined}
|
|
1491
1524
|
className="relative w-full min-w-0"
|
|
1492
1525
|
style={{ height: typeof height === "number" ? `${height}px` : height }}
|
|
@@ -1496,7 +1529,7 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1496
1529
|
) : state === "empty" ? (
|
|
1497
1530
|
<ChartEmpty {...emptyState} defaultTitle={t.emptyTitle} />
|
|
1498
1531
|
) : (
|
|
1499
|
-
<ChartFrameContext.Provider value>{children}</ChartFrameContext.Provider>
|
|
1532
|
+
<ChartFrameContext.Provider value={plot}>{children}</ChartFrameContext.Provider>
|
|
1500
1533
|
)}
|
|
1501
1534
|
</div>
|
|
1502
1535
|
{footnote && (
|
package/registry/ui/checkbox.tsx
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
|
6
6
|
|
|
7
7
|
import { cn } from "@squadbase/vantage/ui"
|
|
8
|
-
import { CheckIcon } from "lucide-react"
|
|
8
|
+
import { CheckIcon, MinusIcon } from "lucide-react"
|
|
9
9
|
|
|
10
10
|
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
11
11
|
return (
|
|
12
12
|
<CheckboxPrimitive.Root
|
|
13
13
|
data-slot="checkbox"
|
|
14
14
|
className={cn(
|
|
15
|
-
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
|
15
|
+
"group/checkbox peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground dark:data-indeterminate:bg-primary",
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -21,7 +21,8 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
|
21
21
|
data-slot="checkbox-indicator"
|
|
22
22
|
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
|
23
23
|
>
|
|
24
|
-
<CheckIcon />
|
|
24
|
+
<CheckIcon className="group-data-indeterminate/checkbox:hidden" />
|
|
25
|
+
<MinusIcon className="hidden group-data-indeterminate/checkbox:block" />
|
|
25
26
|
</CheckboxPrimitive.Indicator>
|
|
26
27
|
</CheckboxPrimitive.Root>
|
|
27
28
|
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copied by `vantage add ui code-block`. This file is yours to edit.
|
|
2
|
+
// Generated from @squadbase/squadkit registry/ui/code-block.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
|
|
3
|
+
"use client"
|
|
4
|
+
|
|
5
|
+
import * as React from "react"
|
|
6
|
+
import { CheckIcon, CopyIcon } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
import { cn } from "@squadbase/vantage/ui"
|
|
9
|
+
import { Button } from "@squadbase/vantage/ui"
|
|
10
|
+
|
|
11
|
+
type CodeBlockProps = Omit<React.ComponentProps<"div">, "children"> & {
|
|
12
|
+
/** The text shown and copied. No syntax highlighting. */
|
|
13
|
+
code: string
|
|
14
|
+
/** Plain-text label above the code (a file name or language). */
|
|
15
|
+
title?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Monospace text with a copy button. */
|
|
19
|
+
function CodeBlock({ code, title, className, ...props }: CodeBlockProps) {
|
|
20
|
+
const [copied, setCopied] = React.useState(false)
|
|
21
|
+
|
|
22
|
+
const copy = async () => {
|
|
23
|
+
await navigator.clipboard.writeText(code)
|
|
24
|
+
setCopied(true)
|
|
25
|
+
setTimeout(() => setCopied(false), 1500)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div data-slot="code-block" className={cn("rounded-lg bg-muted", className)} {...props}>
|
|
30
|
+
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-1.5">
|
|
31
|
+
<span className="truncate text-xs text-muted-foreground">{title}</span>
|
|
32
|
+
<Button type="button" variant="ghost" size="xs" onClick={copy} aria-label="Copy code">
|
|
33
|
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
34
|
+
{copied ? "Copied" : "Copy"}
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
<pre className="overflow-x-auto p-3 font-mono text-sm text-foreground">
|
|
38
|
+
<code>{code}</code>
|
|
39
|
+
</pre>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { CodeBlock, type CodeBlockProps }
|
|
@@ -171,10 +171,25 @@ function useLocale(locale?: string | null): string {
|
|
|
171
171
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
175
|
-
* `role="img"`
|
|
174
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
175
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
176
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
177
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
178
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
179
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
180
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
176
181
|
*/
|
|
177
|
-
|
|
182
|
+
interface ChartFramePlot {
|
|
183
|
+
image: boolean
|
|
184
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
185
|
+
claimed: boolean
|
|
186
|
+
/** Take over the image role; returns the release. */
|
|
187
|
+
claim: () => () => void
|
|
188
|
+
ariaLabel?: string
|
|
189
|
+
ariaLabelledBy?: string
|
|
190
|
+
ariaDescribedBy?: string
|
|
191
|
+
}
|
|
192
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
178
193
|
|
|
179
194
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
180
195
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -398,6 +413,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
398
413
|
tooltip: {
|
|
399
414
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
400
415
|
transitionDuration: 0,
|
|
416
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
417
|
+
confine: true,
|
|
401
418
|
backgroundColor: tokens.popover ?? undefined,
|
|
402
419
|
borderColor: line,
|
|
403
420
|
borderWidth: 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
|
+
}
|
|
@@ -129,7 +129,7 @@ export const DashboardCardFooter = forwardRef<HTMLDivElement, DashboardCardFoote
|
|
|
129
129
|
<div
|
|
130
130
|
ref={ref}
|
|
131
131
|
data-slot="dashboard-card-footer"
|
|
132
|
-
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)}
|
|
133
133
|
{...props}
|
|
134
134
|
/>
|
|
135
135
|
),
|
|
@@ -161,7 +161,10 @@ export interface DashboardCardPresetProps
|
|
|
161
161
|
Pick<DashboardCardProps, "subgrid" | "size"> {
|
|
162
162
|
title?: ReactNode
|
|
163
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
|
|
164
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. */
|
|
165
168
|
footer?: ReactNode
|
|
166
169
|
headerClassName?: string
|
|
167
170
|
contentClassName?: string
|
|
@@ -171,6 +174,7 @@ export interface DashboardCardPresetProps
|
|
|
171
174
|
export function DashboardCardPreset({
|
|
172
175
|
title,
|
|
173
176
|
description,
|
|
177
|
+
headline,
|
|
174
178
|
actions,
|
|
175
179
|
footer,
|
|
176
180
|
children,
|
|
@@ -183,21 +187,28 @@ export function DashboardCardPreset({
|
|
|
183
187
|
}: DashboardCardPresetProps) {
|
|
184
188
|
// Inside SampleData the card shows the mark in its header, and the parts inside it do not repeat it.
|
|
185
189
|
const sample = useSampleMark()
|
|
186
|
-
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)
|
|
187
194
|
return (
|
|
188
|
-
<DashboardCard className={className} subgrid={subgrid} {...props}>
|
|
195
|
+
<DashboardCard className={cn(floatingMark && "relative", className)} subgrid={subgrid} {...props}>
|
|
189
196
|
<SampleClaim>
|
|
197
|
+
{floatingMark && <SampleBadge label={sample.label} className="absolute top-2 right-2 z-10" />}
|
|
190
198
|
{hasHeader && (
|
|
191
199
|
<DashboardCardHeader className={cn(subgrid && "row-start-1", headerClassName)}>
|
|
192
|
-
{(
|
|
193
|
-
<div>
|
|
194
|
-
|
|
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>
|
|
195
206
|
{description && <DashboardCardDescription>{description}</DashboardCardDescription>}
|
|
196
207
|
</div>
|
|
197
208
|
)}
|
|
198
|
-
{(
|
|
199
|
-
<DashboardCardAction className=
|
|
200
|
-
{
|
|
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>}
|
|
201
212
|
{actions}
|
|
202
213
|
</DashboardCardAction>
|
|
203
214
|
)}
|
|
@@ -311,6 +311,8 @@ const TEXT = {
|
|
|
311
311
|
trend: (points: number, first: string, last: string, min: string, max: string) =>
|
|
312
312
|
`${points}点の推移、${first}から${last}、最小${min}、最大${max}`,
|
|
313
313
|
noData: "データがありません",
|
|
314
|
+
search: "検索",
|
|
315
|
+
pageSizeLabel: "1ページの表示件数",
|
|
314
316
|
pageSize: (size: number) => `${size}件`,
|
|
315
317
|
range: (total: number, start: number, end: number) => `${total}件中 ${start} - ${end} を表示`,
|
|
316
318
|
first: "最初",
|
|
@@ -328,6 +330,8 @@ const TEXT = {
|
|
|
328
330
|
trend: (points: number, first: string, last: string, min: string, max: string) =>
|
|
329
331
|
`Trend over ${points} points: ${first} to ${last}, min ${min}, max ${max}`,
|
|
330
332
|
noData: "No results.",
|
|
333
|
+
search: "Search",
|
|
334
|
+
pageSizeLabel: "Rows per page",
|
|
331
335
|
pageSize: (size: number) => `${size} / page`,
|
|
332
336
|
range: (total: number, start: number, end: number) => `Showing ${start}–${end} of ${total}`,
|
|
333
337
|
first: "First",
|
|
@@ -920,6 +924,7 @@ export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps
|
|
|
920
924
|
({ showSearch, toolbar, bulkActions, className, ...props }, ref) => {
|
|
921
925
|
const { globalFilter, setGlobalFilter, hasSelection, selectedRows, enableFiltering } =
|
|
922
926
|
useDataTable()
|
|
927
|
+
const t = useText()
|
|
923
928
|
|
|
924
929
|
const showSearchEffective = showSearch ?? enableFiltering
|
|
925
930
|
|
|
@@ -938,6 +943,7 @@ export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps
|
|
|
938
943
|
<div className="relative">
|
|
939
944
|
<Search className="absolute top-2.5 left-2.5 size-4 text-muted-foreground" />
|
|
940
945
|
<Input
|
|
946
|
+
aria-label={t.search}
|
|
941
947
|
value={globalFilter}
|
|
942
948
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
943
949
|
className="max-w-sm pl-8"
|
|
@@ -1207,7 +1213,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
1207
1213
|
value={String(currentPageSize)}
|
|
1208
1214
|
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
1209
1215
|
>
|
|
1210
|
-
<SelectTrigger size="sm" className="min-w-[80px]">
|
|
1216
|
+
<SelectTrigger size="sm" className="min-w-[80px]" aria-label={t.pageSizeLabel}>
|
|
1211
1217
|
<SelectValue />
|
|
1212
1218
|
</SelectTrigger>
|
|
1213
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}
|