@squadbase/vantage 0.7.1-dev1.9d223a2 → 0.7.1-dev1.9d23147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-U7GO26HR.js → build-AON4MAQQ.js} +3 -3
- package/dist/{build-U7GO26HR.js.map → build-AON4MAQQ.js.map} +1 -1
- 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-SDJTTMLK.js → chunk-SR43D6EF.js} +13 -8
- package/dist/chunk-SR43D6EF.js.map +1 -0
- package/dist/{chunk-P2KIHF65.js → chunk-U6MWBCM6.js} +28 -7
- package/dist/chunk-U6MWBCM6.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/components/index.d.ts +223 -6
- package/dist/components/index.js +4 -4
- package/dist/{dev-O42JFQ64.js → dev-BZ7FXDB3.js} +3 -3
- package/dist/{dev-O42JFQ64.js.map → dev-BZ7FXDB3.js.map} +1 -1
- 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/dist/vite/index.js +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-SDJTTMLK.js.map +0 -1
- package/dist/chunk-YJQCB3UP.js.map +0 -1
|
@@ -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"`
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
181
|
+
*/
|
|
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,
|
|
@@ -2273,6 +2290,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2273
2290
|
|
|
2274
2291
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2275
2292
|
const ACTIONS_RESERVE = 80
|
|
2293
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2294
|
+
const BAR_MAX_WIDTH = 40
|
|
2295
|
+
const BAR_RADIUS = 3
|
|
2296
|
+
const STACK_GAP = 1.5
|
|
2276
2297
|
|
|
2277
2298
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2278
2299
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2486,6 +2507,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2486
2507
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2487
2508
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2488
2509
|
})()
|
|
2510
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2511
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2489
2512
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2490
2513
|
|
|
2491
2514
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2695,15 +2718,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2695
2718
|
const seriesData = pad(values[c]!)
|
|
2696
2719
|
if (kind === "bar") {
|
|
2697
2720
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2698
|
-
// Stacked:
|
|
2699
|
-
const radius = stacked
|
|
2721
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2722
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2723
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2700
2724
|
return {
|
|
2701
2725
|
id: SERIES_ID_PREFIX + category,
|
|
2702
2726
|
type: "bar" as const,
|
|
2703
2727
|
name: category,
|
|
2704
2728
|
stack: stacked ? "total" : undefined,
|
|
2705
2729
|
barCategoryGap,
|
|
2706
|
-
|
|
2730
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2731
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2707
2732
|
markPoint: markPointFor(c, solid),
|
|
2708
2733
|
markLine: undefined,
|
|
2709
2734
|
data: seriesData.map((value, i) => {
|
|
@@ -2999,6 +3024,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2999
3024
|
},
|
|
3000
3025
|
})
|
|
3001
3026
|
}
|
|
3027
|
+
if (diverging) {
|
|
3028
|
+
markLines.push({
|
|
3029
|
+
name: "zero",
|
|
3030
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3031
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3032
|
+
label: { show: false },
|
|
3033
|
+
})
|
|
3034
|
+
}
|
|
3002
3035
|
if (forecast) {
|
|
3003
3036
|
const forecastSpan =
|
|
3004
3037
|
plotWidth && labels.length > 1
|
|
@@ -3144,6 +3177,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3144
3177
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3145
3178
|
boundaryGap: kind === "bar",
|
|
3146
3179
|
inverse: horizontalBars,
|
|
3180
|
+
// 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.
|
|
3181
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3147
3182
|
axisTick: { show: false },
|
|
3148
3183
|
axisLabel: {
|
|
3149
3184
|
hideOverlap: true,
|
|
@@ -3246,7 +3281,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3246
3281
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3247
3282
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3248
3283
|
? { left: 0, right: 0 }
|
|
3249
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3284
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3250
3285
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3251
3286
|
type: "plain",
|
|
3252
3287
|
icon: "circle",
|
|
@@ -197,6 +197,8 @@ export interface PageShellHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
+
* PageShellHeader — the page header band (title and update time, source / period, overall status); inside `PageShell` without `title`, with the title in `PageShellHeading`.
|
|
201
|
+
*
|
|
200
202
|
* A wrapping flex row, not a two-column grid: when the heading cannot keep its
|
|
201
203
|
* 20rem basis next to `PageShellHeaderEnd`, the end block drops below it. A
|
|
202
204
|
* `1fr auto` grid would instead hand the heading whatever the end block leaves,
|
|
@@ -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,
|
|
@@ -2273,6 +2290,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2273
2290
|
|
|
2274
2291
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2275
2292
|
const ACTIONS_RESERVE = 80
|
|
2293
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2294
|
+
const BAR_MAX_WIDTH = 40
|
|
2295
|
+
const BAR_RADIUS = 3
|
|
2296
|
+
const STACK_GAP = 1.5
|
|
2276
2297
|
|
|
2277
2298
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2278
2299
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2486,6 +2507,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2486
2507
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2487
2508
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2488
2509
|
})()
|
|
2510
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2511
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2489
2512
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2490
2513
|
|
|
2491
2514
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2695,15 +2718,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2695
2718
|
const seriesData = pad(values[c]!)
|
|
2696
2719
|
if (kind === "bar") {
|
|
2697
2720
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2698
|
-
// Stacked:
|
|
2699
|
-
const radius = stacked
|
|
2721
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2722
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2723
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2700
2724
|
return {
|
|
2701
2725
|
id: SERIES_ID_PREFIX + category,
|
|
2702
2726
|
type: "bar" as const,
|
|
2703
2727
|
name: category,
|
|
2704
2728
|
stack: stacked ? "total" : undefined,
|
|
2705
2729
|
barCategoryGap,
|
|
2706
|
-
|
|
2730
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2731
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2707
2732
|
markPoint: markPointFor(c, solid),
|
|
2708
2733
|
markLine: undefined,
|
|
2709
2734
|
data: seriesData.map((value, i) => {
|
|
@@ -2999,6 +3024,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2999
3024
|
},
|
|
3000
3025
|
})
|
|
3001
3026
|
}
|
|
3027
|
+
if (diverging) {
|
|
3028
|
+
markLines.push({
|
|
3029
|
+
name: "zero",
|
|
3030
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3031
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3032
|
+
label: { show: false },
|
|
3033
|
+
})
|
|
3034
|
+
}
|
|
3002
3035
|
if (forecast) {
|
|
3003
3036
|
const forecastSpan =
|
|
3004
3037
|
plotWidth && labels.length > 1
|
|
@@ -3144,6 +3177,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3144
3177
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3145
3178
|
boundaryGap: kind === "bar",
|
|
3146
3179
|
inverse: horizontalBars,
|
|
3180
|
+
// 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.
|
|
3181
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3147
3182
|
axisTick: { show: false },
|
|
3148
3183
|
axisLabel: {
|
|
3149
3184
|
hideOverlap: true,
|
|
@@ -3246,7 +3281,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3246
3281
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3247
3282
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3248
3283
|
? { left: 0, right: 0 }
|
|
3249
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3284
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3250
3285
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3251
3286
|
type: "plain",
|
|
3252
3287
|
icon: "circle",
|
|
@@ -3939,7 +3974,7 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
|
|
|
3939
3974
|
|
|
3940
3975
|
const option: AnyRecord = {
|
|
3941
3976
|
...barOpt,
|
|
3942
|
-
legend: { ...barOpt.legend, show: showLegend, data: legendNames, right: cfg.reserveActions ? 80 : 0 },
|
|
3977
|
+
legend: { ...barOpt.legend, show: showLegend, data: legendNames, left: 0, right: cfg.reserveActions ? 80 : 0 },
|
|
3943
3978
|
grid: [
|
|
3944
3979
|
{ top, height: upper, left: gridLeft, right, containLabel: true },
|
|
3945
3980
|
{ top: top + upper + PANEL_GAP, bottom: 4, left: gridLeft, right, containLabel: true },
|
package/registry/ui/progress.tsx
CHANGED
|
@@ -31,7 +31,7 @@ function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
|
|
|
31
31
|
return (
|
|
32
32
|
<ProgressPrimitive.Track
|
|
33
33
|
className={cn(
|
|
34
|
-
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-
|
|
34
|
+
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-border",
|
|
35
35
|
className
|
|
36
36
|
)}
|
|
37
37
|
data-slot="progress-track"
|
|
@@ -183,10 +183,25 @@ function useLocale(locale?: string | null): string {
|
|
|
183
183
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
187
|
-
* `role="img"`
|
|
186
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
187
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
188
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
189
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
190
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
191
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
192
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
188
193
|
*/
|
|
189
|
-
|
|
194
|
+
interface ChartFramePlot {
|
|
195
|
+
image: boolean
|
|
196
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
197
|
+
claimed: boolean
|
|
198
|
+
/** Take over the image role; returns the release. */
|
|
199
|
+
claim: () => () => void
|
|
200
|
+
ariaLabel?: string
|
|
201
|
+
ariaLabelledBy?: string
|
|
202
|
+
ariaDescribedBy?: string
|
|
203
|
+
}
|
|
204
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
190
205
|
|
|
191
206
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
192
207
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -410,6 +425,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
410
425
|
tooltip: {
|
|
411
426
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
412
427
|
transitionDuration: 0,
|
|
428
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
429
|
+
confine: true,
|
|
413
430
|
backgroundColor: tokens.popover ?? undefined,
|
|
414
431
|
borderColor: line,
|
|
415
432
|
borderWidth: 1,
|
|
@@ -1446,6 +1463,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
1446
1463
|
|
|
1447
1464
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
1448
1465
|
const ACTIONS_RESERVE = 80
|
|
1466
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
1467
|
+
const BAR_MAX_WIDTH = 40
|
|
1468
|
+
const BAR_RADIUS = 3
|
|
1469
|
+
const STACK_GAP = 1.5
|
|
1449
1470
|
|
|
1450
1471
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
1451
1472
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -1659,6 +1680,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1659
1680
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
1660
1681
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
1661
1682
|
})()
|
|
1683
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
1684
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
1662
1685
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
1663
1686
|
|
|
1664
1687
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -1868,15 +1891,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1868
1891
|
const seriesData = pad(values[c]!)
|
|
1869
1892
|
if (kind === "bar") {
|
|
1870
1893
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
1871
|
-
// Stacked:
|
|
1872
|
-
const radius = stacked
|
|
1894
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
1895
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
1896
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
1873
1897
|
return {
|
|
1874
1898
|
id: SERIES_ID_PREFIX + category,
|
|
1875
1899
|
type: "bar" as const,
|
|
1876
1900
|
name: category,
|
|
1877
1901
|
stack: stacked ? "total" : undefined,
|
|
1878
1902
|
barCategoryGap,
|
|
1879
|
-
|
|
1903
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
1904
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
1880
1905
|
markPoint: markPointFor(c, solid),
|
|
1881
1906
|
markLine: undefined,
|
|
1882
1907
|
data: seriesData.map((value, i) => {
|
|
@@ -2172,6 +2197,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2172
2197
|
},
|
|
2173
2198
|
})
|
|
2174
2199
|
}
|
|
2200
|
+
if (diverging) {
|
|
2201
|
+
markLines.push({
|
|
2202
|
+
name: "zero",
|
|
2203
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
2204
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
2205
|
+
label: { show: false },
|
|
2206
|
+
})
|
|
2207
|
+
}
|
|
2175
2208
|
if (forecast) {
|
|
2176
2209
|
const forecastSpan =
|
|
2177
2210
|
plotWidth && labels.length > 1
|
|
@@ -2317,6 +2350,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2317
2350
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
2318
2351
|
boundaryGap: kind === "bar",
|
|
2319
2352
|
inverse: horizontalBars,
|
|
2353
|
+
// 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.
|
|
2354
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
2320
2355
|
axisTick: { show: false },
|
|
2321
2356
|
axisLabel: {
|
|
2322
2357
|
hideOverlap: true,
|
|
@@ -2419,7 +2454,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2419
2454
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
2420
2455
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2421
2456
|
? { left: 0, right: 0 }
|
|
2422
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2457
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2423
2458
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2424
2459
|
type: "plain",
|
|
2425
2460
|
icon: "circle",
|
|
@@ -4226,8 +4261,8 @@ export function buildScatterOption(config: ScatterOptionConfig): EChartsOption {
|
|
|
4226
4261
|
legend: {
|
|
4227
4262
|
show: showLegend,
|
|
4228
4263
|
...(legendBelow ? { bottom: 0 } : { top: 0 }),
|
|
4229
|
-
// With a heading the legend reads from the left, after it; otherwise it sits at the
|
|
4230
|
-
...(headingWidth > 0 ? { left: headingWidth } :
|
|
4264
|
+
// With a heading the legend reads from the left, after it; otherwise it sits at the left edge (as in every chart).
|
|
4265
|
+
...(headingWidth > 0 ? { left: headingWidth } : { left: 0, ...(legendBelow ? {} : { right: config.reserveActions ? 72 : 0 }) }),
|
|
4231
4266
|
type: "plain",
|
|
4232
4267
|
icon: "circle",
|
|
4233
4268
|
itemWidth: 8,
|
|
@@ -4242,7 +4277,9 @@ export function buildScatterOption(config: ScatterOptionConfig): EChartsOption {
|
|
|
4242
4277
|
(guides.some((line) => line.axis === "x" && line.label) && (!showLegend || legendBelow) ? 14 : 0),
|
|
4243
4278
|
right: 24,
|
|
4244
4279
|
bottom: legendBelow ? 24 + 24 * legendLineCount : 24,
|
|
4245
|
-
left:
|
|
4280
|
+
// The rotated y axis name lies outside `containLabel`'s reach (its right edge is `nameGap` left of the axis line): the
|
|
4281
|
+
// margin holds its height, so the name is never cut at the chart's left edge.
|
|
4282
|
+
left: 36,
|
|
4246
4283
|
containLabel: true,
|
|
4247
4284
|
},
|
|
4248
4285
|
...(headingWidth > 0
|
|
@@ -4300,7 +4337,7 @@ export function buildScatterOption(config: ScatterOptionConfig): EChartsOption {
|
|
|
4300
4337
|
label: {
|
|
4301
4338
|
show: keptLabels === null,
|
|
4302
4339
|
position: "right" as const,
|
|
4303
|
-
distance:
|
|
4340
|
+
distance: 8,
|
|
4304
4341
|
fontSize: 11,
|
|
4305
4342
|
color: config.textColors?.label,
|
|
4306
4343
|
// Plain text: an outline / shadow reads as a halo on a dark surface.
|
|
@@ -4308,7 +4345,8 @@ export function buildScatterOption(config: ScatterOptionConfig): EChartsOption {
|
|
|
4308
4345
|
textShadowBlur: 0,
|
|
4309
4346
|
formatter: (p: { data?: { point?: ScatterPoint } }) => p.data?.point?.label ?? "",
|
|
4310
4347
|
},
|
|
4311
|
-
|
|
4348
|
+
// A name stays beside its own dot: one that would collide with another is hidden, not pushed onto a neighbouring dot.
|
|
4349
|
+
labelLayout: { hideOverlap: true },
|
|
4312
4350
|
}
|
|
4313
4351
|
: {}),
|
|
4314
4352
|
// The guide lines hang off the first series (a markLine belongs to one).
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { forwardRef, useEffect, useState, type SVGAttributes } from "react"
|
|
12
12
|
|
|
13
13
|
import { cn } from "@squadbase/vantage/ui"
|
|
14
|
-
import type { ChartHighlight } from "@squadbase/vantage/components"
|
|
14
|
+
import type { ChartColor, ChartHighlight } from "@squadbase/vantage/components"
|
|
15
15
|
|
|
16
16
|
export type SparklineVariant = "line" | "bar"
|
|
17
17
|
|
|
@@ -24,8 +24,14 @@ export interface SparklineProps extends SVGAttributes<SVGSVGElement> {
|
|
|
24
24
|
data: SparklineDataPoint[]
|
|
25
25
|
variant?: SparklineVariant
|
|
26
26
|
height?: number
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Line / bar colour: a theme series token (`"chart-1"`〜`"chart-5"`), a meaning colour
|
|
29
|
+
* (`"positive"` / `"negative"` / `"warning"` / `"neutral"` / `"info"`), or any CSS colour
|
|
30
|
+
* (`"#16a34a"`, `"var(--destructive)"`) — the same {@link ChartColor} the other charts take.
|
|
31
|
+
* Drives both stroke and fill via `currentColor`. Defaults to `"chart-1"`.
|
|
32
|
+
* Tailwind text-colour classes (`"text-primary"`) still work for compatibility; prefer the tokens above.
|
|
33
|
+
*/
|
|
34
|
+
color?: ChartColor
|
|
29
35
|
area?: boolean
|
|
30
36
|
/** マウント時に線を引く / 棒を伸ばす。既定 `false`(`prefers-reduced-motion` では常に無効)。 */
|
|
31
37
|
animate?: boolean
|
|
@@ -41,6 +47,32 @@ export interface SparklineProps extends SVGAttributes<SVGSVGElement> {
|
|
|
41
47
|
|
|
42
48
|
const VB_WIDTH = 100
|
|
43
49
|
|
|
50
|
+
/** Literal class names (not built from a template) so Tailwind's scanner keeps them. */
|
|
51
|
+
const TOKEN_CLASS: Record<string, string> = {
|
|
52
|
+
"chart-1": "text-chart-1",
|
|
53
|
+
"chart-2": "text-chart-2",
|
|
54
|
+
"chart-3": "text-chart-3",
|
|
55
|
+
"chart-4": "text-chart-4",
|
|
56
|
+
"chart-5": "text-chart-5",
|
|
57
|
+
positive: "text-positive",
|
|
58
|
+
warning: "text-warning",
|
|
59
|
+
negative: "text-negative",
|
|
60
|
+
neutral: "text-neutral",
|
|
61
|
+
info: "text-info",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `color` を class か inline の CSS colour に振り分ける。トークン・意味色は `text-*` に、
|
|
66
|
+
* `text-` で始まる旧来の class 指定はそのまま class に、それ以外(`#hex` / `var(--x)` / `rgb()` …)は CSS colour として扱う。
|
|
67
|
+
*/
|
|
68
|
+
function resolveSparklineColor(color: string): { className?: string; css?: string } {
|
|
69
|
+
const value = color.trim()
|
|
70
|
+
const tokenClass = TOKEN_CLASS[value]
|
|
71
|
+
if (tokenClass) return { className: tokenClass }
|
|
72
|
+
if (/^(?:[\w-]+:)*!?text-/.test(value) || (/\s/.test(value) && !value.includes("("))) return { className: color }
|
|
73
|
+
return { css: value }
|
|
74
|
+
}
|
|
75
|
+
|
|
44
76
|
/** Map raw values onto SVG y-coordinates (inverted, padded). */
|
|
45
77
|
function normalizeData(values: number[], height: number, vPad = 2): number[] {
|
|
46
78
|
const min = Math.min(...values)
|
|
@@ -117,7 +149,7 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
117
149
|
data = NO_ROWS,
|
|
118
150
|
variant = "line",
|
|
119
151
|
height = 40,
|
|
120
|
-
color = "
|
|
152
|
+
color = "chart-1",
|
|
121
153
|
area = false,
|
|
122
154
|
animate = false,
|
|
123
155
|
highlight = "auto",
|
|
@@ -135,6 +167,8 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
135
167
|
|
|
136
168
|
if (data.length === 0) return null
|
|
137
169
|
|
|
170
|
+
const { className: colorClass, css: colorCss } = resolveSparklineColor(color)
|
|
171
|
+
|
|
138
172
|
const values = data.map((d) => d.value)
|
|
139
173
|
const ys = normalizeData(values, height)
|
|
140
174
|
const step = ys.length > 1 ? VB_WIDTH / (ys.length - 1) : 0
|
|
@@ -160,6 +194,7 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
160
194
|
aria-hidden={ariaLabel ? undefined : true}
|
|
161
195
|
className={cn("overflow-visible", className)}
|
|
162
196
|
{...props}
|
|
197
|
+
style={colorCss ? { color: colorCss, ...props.style } : props.style}
|
|
163
198
|
>
|
|
164
199
|
{variant === "line" && (
|
|
165
200
|
<>
|
|
@@ -167,7 +202,7 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
167
202
|
<path
|
|
168
203
|
d={buildAreaPath(ys, height)}
|
|
169
204
|
strokeWidth={0}
|
|
170
|
-
className={cn("fill-current opacity-15",
|
|
205
|
+
className={cn("fill-current opacity-15", colorClass)}
|
|
171
206
|
/>
|
|
172
207
|
)}
|
|
173
208
|
<path
|
|
@@ -180,7 +215,7 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
180
215
|
pathLength={animate ? 1 : undefined}
|
|
181
216
|
strokeDasharray={animate ? 1 : undefined}
|
|
182
217
|
strokeDashoffset={animate ? (mounted ? 0 : 1) : undefined}
|
|
183
|
-
className={cn("stroke-current", animate && "motion-reduce:transition-none!",
|
|
218
|
+
className={cn("stroke-current", animate && "motion-reduce:transition-none!", colorClass)}
|
|
184
219
|
style={animate ? { transition: "stroke-dashoffset 0.6s ease-in-out" } : undefined}
|
|
185
220
|
/>
|
|
186
221
|
{/* A zero-length round-capped stroke with a non-scaling width stays a
|
|
@@ -198,7 +233,7 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
198
233
|
strokeWidth={p.kind === "latest" ? 5 : 4}
|
|
199
234
|
strokeLinecap="round"
|
|
200
235
|
vectorEffect="non-scaling-stroke"
|
|
201
|
-
className={cn("stroke-current", p.kind === "latest" ?
|
|
236
|
+
className={cn("stroke-current", p.kind === "latest" ? colorClass : "text-muted-foreground")}
|
|
202
237
|
/>
|
|
203
238
|
)
|
|
204
239
|
})}
|
|
@@ -214,7 +249,7 @@ export const Sparkline = forwardRef<SVGSVGElement, SparklineProps>(function Spar
|
|
|
214
249
|
width={rect.w}
|
|
215
250
|
height={animate ? (mounted ? rect.h : 0) : rect.h}
|
|
216
251
|
rx={0.5}
|
|
217
|
-
className={cn("fill-current", animate && "motion-reduce:transition-none!",
|
|
252
|
+
className={cn("fill-current", animate && "motion-reduce:transition-none!", colorClass)}
|
|
218
253
|
style={
|
|
219
254
|
animate
|
|
220
255
|
? {
|
package/registry/ui/states.tsx
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* business users. Defaults follow the page language (`<html lang>`: Japanese for `ja`, else
|
|
8
8
|
* English) and say what is happening, not why the system failed: keep raw errors
|
|
9
9
|
* ("upstream timeout after 30s") in `details`, which stays folded behind "Show details" (ja: "詳細を表示").
|
|
10
|
+
* `variant="inline"` renders one line of muted text (no panel, no action, no `details`) for a card row or table cell.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* import { ErrorState } from "@/components/ui/states"
|
|
@@ -175,8 +176,26 @@ const TEXT = {
|
|
|
175
176
|
|
|
176
177
|
const useText = () => TEXT[uiLanguage(useLocale())]
|
|
177
178
|
|
|
178
|
-
export
|
|
179
|
+
export type StateVariant = "default" | "inline"
|
|
180
|
+
|
|
181
|
+
export function Loading({
|
|
182
|
+
label,
|
|
183
|
+
variant = "default",
|
|
184
|
+
className,
|
|
185
|
+
}: {
|
|
186
|
+
label?: string
|
|
187
|
+
variant?: StateVariant
|
|
188
|
+
className?: string
|
|
189
|
+
}) {
|
|
179
190
|
const t = useText()
|
|
191
|
+
if (variant === "inline") {
|
|
192
|
+
return (
|
|
193
|
+
<span className={cn("inline-flex items-center gap-2 text-sm text-muted-foreground", className)} role="status" aria-live="polite">
|
|
194
|
+
<span className="size-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
195
|
+
{label ?? t.loading}
|
|
196
|
+
</span>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
180
199
|
return (
|
|
181
200
|
<div className={cn("flex min-h-[30vh] items-center justify-center", className)} role="status" aria-live="polite">
|
|
182
201
|
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
@@ -192,15 +211,20 @@ export function Empty({
|
|
|
192
211
|
description,
|
|
193
212
|
icon,
|
|
194
213
|
action,
|
|
214
|
+
variant = "default",
|
|
195
215
|
className,
|
|
196
216
|
}: {
|
|
197
217
|
title?: string
|
|
198
218
|
description?: string
|
|
199
219
|
icon?: ReactNode
|
|
200
220
|
action?: ReactNode
|
|
221
|
+
variant?: StateVariant
|
|
201
222
|
className?: string
|
|
202
223
|
}) {
|
|
203
224
|
const t = useText()
|
|
225
|
+
if (variant === "inline") {
|
|
226
|
+
return <span className={cn("text-sm text-muted-foreground", className)}>{title ?? t.empty}</span>
|
|
227
|
+
}
|
|
204
228
|
return (
|
|
205
229
|
<div className={cn("flex flex-col items-center justify-center gap-2 p-10 text-center", className)}>
|
|
206
230
|
{icon ? <div className="text-3xl text-muted-foreground">{icon}</div> : null}
|
|
@@ -216,6 +240,7 @@ export function ErrorState({
|
|
|
216
240
|
message,
|
|
217
241
|
details,
|
|
218
242
|
action,
|
|
243
|
+
variant = "default",
|
|
219
244
|
className,
|
|
220
245
|
}: {
|
|
221
246
|
title?: string
|
|
@@ -224,9 +249,17 @@ export function ErrorState({
|
|
|
224
249
|
/** Technical detail (error message, status code), folded behind "Show details" (ja: "詳細を表示"). */
|
|
225
250
|
details?: ReactNode
|
|
226
251
|
action?: ReactNode
|
|
252
|
+
variant?: StateVariant
|
|
227
253
|
className?: string
|
|
228
254
|
}) {
|
|
229
255
|
const t = useText()
|
|
256
|
+
if (variant === "inline") {
|
|
257
|
+
return (
|
|
258
|
+
<span className={cn("text-sm text-destructive", className)} role="alert">
|
|
259
|
+
{title ?? t.errorTitle}
|
|
260
|
+
</span>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
230
263
|
const shownMessage = message === undefined ? t.errorMessage : message
|
|
231
264
|
return (
|
|
232
265
|
<div
|
|
@@ -239,12 +239,12 @@ const legacyColorTones: Record<string, StatusTone> = {
|
|
|
239
239
|
* One literal class set per tone — Tailwind only emits classes it can see
|
|
240
240
|
* literally in the source.
|
|
241
241
|
*/
|
|
242
|
-
const toneStyles: Record<StatusTone, { dot: string; bg: string; text: string
|
|
243
|
-
positive: { dot: "bg-positive", bg: "bg-positive-subtle", text: "text-positive"
|
|
244
|
-
negative: { dot: "bg-negative", bg: "bg-negative-subtle", text: "text-negative"
|
|
245
|
-
warning: { dot: "bg-warning", bg: "bg-warning-subtle", text: "text-warning"
|
|
246
|
-
info: { dot: "bg-info", bg: "bg-info-subtle", text: "text-info"
|
|
247
|
-
neutral: { dot: "bg-neutral", bg: "bg-neutral-subtle", text: "text-neutral"
|
|
242
|
+
const toneStyles: Record<StatusTone, { dot: string; bg: string; text: string }> = {
|
|
243
|
+
positive: { dot: "bg-positive", bg: "bg-positive-subtle", text: "text-positive" },
|
|
244
|
+
negative: { dot: "bg-negative", bg: "bg-negative-subtle", text: "text-negative" },
|
|
245
|
+
warning: { dot: "bg-warning", bg: "bg-warning-subtle", text: "text-warning" },
|
|
246
|
+
info: { dot: "bg-info", bg: "bg-info-subtle", text: "text-info" },
|
|
247
|
+
neutral: { dot: "bg-neutral", bg: "bg-neutral-subtle", text: "text-neutral" },
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const resolveTone = (color: string | undefined): StatusTone => {
|
|
@@ -275,7 +275,7 @@ export const StatusBadge = forwardRef<HTMLSpanElement, StatusBadgeProps>(functio
|
|
|
275
275
|
ref={ref}
|
|
276
276
|
data-slot="status-badge"
|
|
277
277
|
variant="outline"
|
|
278
|
-
className={cn("gap-1.5 rounded-md
|
|
278
|
+
className={cn("gap-1.5 rounded-md border-transparent", style.bg, style.text, className)}
|
|
279
279
|
aria-label={displayLabel}
|
|
280
280
|
>
|
|
281
281
|
<span className={cn("inline-block size-1.5 rounded-full", style.dot)} aria-hidden="true" />
|
package/registry/ui/tabs.tsx
CHANGED
|
@@ -63,6 +63,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
|
63
63
|
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-[color,background-color,border-color,box-shadow] group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
64
64
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
65
65
|
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
|
66
|
+
"[&_[data-slot=badge]]:h-4 [&_[data-slot=badge]]:min-w-4 [&_[data-slot=badge]]:px-1.5 [&_[data-slot=badge]]:tabular-nums [&_[data-slot=badge][data-variant=secondary]]:bg-foreground/10 [&_[data-slot=badge][data-variant=secondary]]:text-current",
|
|
66
67
|
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
67
68
|
className
|
|
68
69
|
)}
|