basalt-ui 1.8.0 → 1.10.0
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/AGENTS.md +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +64 -34
- package/agent/rules/basalt-agent.md +49 -6
- package/agent/rules/basalt-charts.md +74 -5
- package/configs/oxlint-plugin.js +279 -0
- package/configs/oxlint-plugin.test.ts +216 -1
- package/configs/oxlint.json +5 -2
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +1 -0
- package/dist/agent-chat/index.d.ts +4 -3
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/charts/hooks/useHoverSync.d.ts +18 -2
- package/dist/charts/hooks/useHoverSync.d.ts.map +1 -1
- package/dist/charts/hooks/useHoverSync.js +12 -7
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +6 -0
- package/dist/charts/kinds/Bars.d.ts +2 -0
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +3 -1
- package/dist/charts/kinds/Donut.d.ts +2 -0
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +8 -4
- package/dist/charts/kinds/DualPanel.d.ts +2 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +2 -1
- package/dist/charts/kinds/Heatmap.d.ts +8 -0
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +7 -3
- package/dist/charts/kinds/MultiLine.d.ts +2 -0
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +2 -1
- package/dist/charts/kinds/StackedArea.d.ts +2 -0
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +2 -1
- package/dist/charts/kinds/ZonedLine.d.ts +2 -0
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +2 -1
- package/dist/charts/primitives/Axes.d.ts +6 -1
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +3 -2
- package/dist/charts/primitives/ChartFrame.d.ts +8 -1
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +8 -4
- package/dist/charts/primitives/ChartPending.d.ts +34 -0
- package/dist/charts/primitives/ChartPending.d.ts.map +1 -0
- package/dist/charts/primitives/ChartPending.js +23 -0
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -2
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +6 -1
- package/dist/charts/primitives/HoverOverlay.d.ts +5 -4
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +4 -2
- package/dist/cli/index.d.ts +55 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +217 -5
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +13 -5
- package/dist/surfaces.d.ts +10 -0
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +21 -0
- package/llms.txt +9 -2
- package/package.json +8 -5
- package/src/agent/ai-sdk-transport.test.ts +269 -0
- package/src/agent/parts.test.ts +261 -0
- package/src/agent/thread.test.ts +147 -0
- package/src/agent/use-agent-stream.test.tsx +231 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +261 -0
- package/src/agent/use-agent-thread-runs.test.tsx +398 -0
- package/src/agent/use-agent-thread-runs.ts +7 -1
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +299 -0
- package/src/agent-chat/index.ts +4 -3
- package/src/charts/hooks/useHoverSync.ts +43 -7
- package/src/charts/index.ts +6 -0
- package/src/charts/kinds/Bars.tsx +4 -0
- package/src/charts/kinds/Donut.tsx +10 -4
- package/src/charts/kinds/DualPanel.tsx +5 -1
- package/src/charts/kinds/Heatmap.tsx +19 -2
- package/src/charts/kinds/MultiLine.tsx +4 -1
- package/src/charts/kinds/StackedArea.tsx +4 -1
- package/src/charts/kinds/ZonedLine.tsx +4 -1
- package/src/charts/primitives/Axes.tsx +7 -1
- package/src/charts/primitives/ChartFrame.test.tsx +67 -0
- package/src/charts/primitives/ChartFrame.tsx +17 -4
- package/src/charts/primitives/ChartPending.tsx +59 -0
- package/src/charts/primitives/ChartTooltip.tsx +15 -3
- package/src/charts/primitives/HoverOverlay.tsx +12 -6
- package/src/cli/index.ts +439 -5
- package/src/cli/placement-engine.test.ts +104 -0
- package/src/dashboard/stat-card.test.tsx +5 -3
- package/src/dashboard/stat-card.tsx +12 -0
- package/src/guard/check-source.test.ts +20 -0
- package/src/guard/index.ts +20 -9
- package/src/provider/index.test.tsx +6 -5
- package/src/surfaces.ts +22 -0
- package/src/theme/use-basalt-spacing.test.tsx +7 -4
- package/src/tokens/build-fonts-css.test.ts +3 -2
|
@@ -73,17 +73,23 @@ export function AxisBottomDate({
|
|
|
73
73
|
scale,
|
|
74
74
|
top,
|
|
75
75
|
tickValues,
|
|
76
|
+
tickFormat = fmtAxisDate,
|
|
76
77
|
}: {
|
|
77
78
|
scale: AxisScale
|
|
78
79
|
top: number
|
|
79
80
|
tickValues: string[]
|
|
81
|
+
/**
|
|
82
|
+
* Defaults to `fmtAxisDate` (DD.MM). Override for a sub-day window, where DD.MM collapses every
|
|
83
|
+
* tick to the same label.
|
|
84
|
+
*/
|
|
85
|
+
tickFormat?: TickFormatter<string>
|
|
80
86
|
}) {
|
|
81
87
|
return (
|
|
82
88
|
<AxisBottom
|
|
83
89
|
top={top}
|
|
84
90
|
scale={scale}
|
|
85
91
|
tickValues={tickValues}
|
|
86
|
-
tickFormat={
|
|
92
|
+
tickFormat={tickFormat}
|
|
87
93
|
tickLabelProps={{
|
|
88
94
|
fill: VX.faint,
|
|
89
95
|
fontFamily: TICK_FONT_FAMILY,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ChartFrame`'s `isPending` invariant — the one behavior here that regresses silently. A pending
|
|
3
|
+
* chart that quietly fell back to rendering its legend + body would put a densified "not measured"
|
|
4
|
+
* shape back on screen exactly where `ChartPending` is supposed to reserve a static, contentless
|
|
5
|
+
* placeholder (see `ChartPending`'s JSDoc for the three-state "nothing to draw" rationale).
|
|
6
|
+
*
|
|
7
|
+
* A DOM harness now exists (`tests/setup/dom.ts`, preloaded via the root `bunfig.toml`; see
|
|
8
|
+
* `theme/use-basalt-spacing.test.tsx`'s doc) — `renderToStaticMarkup` is used deliberately here
|
|
9
|
+
* instead: `ChartFrame`/`ChartPending` live in `src/charts/**`, which is Mantine-free, so no
|
|
10
|
+
* `MantineProvider` wrapper is needed either. `useChartSize` never measures under SSR (no
|
|
11
|
+
* `ResizeObserver`), so the plot rect falls back to `minWidth` × the resolved fixed height —
|
|
12
|
+
* non-zero, which is what lets `children`/`ChartPending` render at all in this harness. Converting
|
|
13
|
+
* to the DOM harness would only be worth it if a future assertion here needed a real measured size
|
|
14
|
+
* (a live `ResizeObserver` reading) rather than this SSR fallback rect.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, test } from 'bun:test'
|
|
17
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
18
|
+
import { ChartFrame } from './ChartFrame'
|
|
19
|
+
import type { SeriesStyle } from '../series'
|
|
20
|
+
|
|
21
|
+
const series: SeriesStyle[] = [{ key: 'a', label: 'Series A', color: '#000', mark: 'line' }]
|
|
22
|
+
|
|
23
|
+
const CHART_BODY_MARKER = 'CHART_BODY_MARKER'
|
|
24
|
+
|
|
25
|
+
function renderFrame(isPending: boolean): string {
|
|
26
|
+
return renderToStaticMarkup(
|
|
27
|
+
<ChartFrame series={series} legend={{}} isPending={isPending}>
|
|
28
|
+
{() => <svg>{CHART_BODY_MARKER}</svg>}
|
|
29
|
+
</ChartFrame>,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('a pending ChartFrame renders neither the legend nor the chart body', () => {
|
|
34
|
+
const markup = renderFrame(true)
|
|
35
|
+
|
|
36
|
+
test('the legend is absent — no series label anywhere in the markup', () => {
|
|
37
|
+
expect(markup).not.toContain('Series A')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('the chart body (children) never runs', () => {
|
|
41
|
+
expect(markup).not.toContain(CHART_BODY_MARKER)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('the outer container is marked aria-busy', () => {
|
|
45
|
+
expect(markup).toContain('aria-busy="true"')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('the placeholder label renders in its place', () => {
|
|
49
|
+
expect(markup).toContain('Loading…')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('a non-pending ChartFrame is unaffected — the legend and body both render', () => {
|
|
54
|
+
const markup = renderFrame(false)
|
|
55
|
+
|
|
56
|
+
test('the legend renders the series label', () => {
|
|
57
|
+
expect(markup).toContain('Series A')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('the chart body renders', () => {
|
|
61
|
+
expect(markup).toContain(CHART_BODY_MARKER)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('no aria-busy attribute is present', () => {
|
|
65
|
+
expect(markup).not.toContain('aria-busy')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -3,6 +3,7 @@ import { useChartSize } from '../hooks/useChartSize'
|
|
|
3
3
|
import { deriveLegend } from '../series'
|
|
4
4
|
import type { ChartLegendConfig, LegendPlacement, SeriesStyle } from '../series'
|
|
5
5
|
import { ChartLegend } from './ChartLegend'
|
|
6
|
+
import { ChartPending } from './ChartPending'
|
|
6
7
|
|
|
7
8
|
const DEFAULT_HEIGHT = 240
|
|
8
9
|
const DEFAULT_MIN_WIDTH = 200
|
|
@@ -36,6 +37,13 @@ export type ChartFrameProps = {
|
|
|
36
37
|
chartId?: string
|
|
37
38
|
/** `false` only for the sparkline exemption — every other chart gets a legend by default. */
|
|
38
39
|
legend?: ChartFrameLegend | false
|
|
40
|
+
/**
|
|
41
|
+
* The query behind this chart hasn't resolved yet. Renders `ChartPending` (see its JSDoc for the
|
|
42
|
+
* three-state "nothing to draw" rationale) over the full plot rect in place of `children`,
|
|
43
|
+
* suppresses the legend entirely (a legend naming a series with nothing yet to point at is its
|
|
44
|
+
* own small lie), and marks the outer container `aria-busy="true"`.
|
|
45
|
+
*/
|
|
46
|
+
isPending?: boolean
|
|
39
47
|
/**
|
|
40
48
|
* Accessible text alternative for the chart, applied as `aria-label` (+ `role="img"`) on the
|
|
41
49
|
* outer container so screen readers announce something other than an unlabeled graphic. Every
|
|
@@ -106,6 +114,7 @@ export function ChartFrame({
|
|
|
106
114
|
chartId,
|
|
107
115
|
legend = {},
|
|
108
116
|
ariaLabel,
|
|
117
|
+
isPending = false,
|
|
109
118
|
children,
|
|
110
119
|
}: ChartFrameProps): ReactNode {
|
|
111
120
|
const { ref: containerRef, width: containerW, height: containerH } = useChartSize()
|
|
@@ -113,6 +122,7 @@ export function ChartFrame({
|
|
|
113
122
|
|
|
114
123
|
const placement = legend === false ? 'bottom' : (legend.placement ?? 'bottom')
|
|
115
124
|
const vertical = placement === 'left' || placement === 'right'
|
|
125
|
+
const legendVisible = legend !== false && !isPending
|
|
116
126
|
|
|
117
127
|
const resolvedHeight = fill
|
|
118
128
|
? containerH
|
|
@@ -120,8 +130,8 @@ export function ChartFrame({
|
|
|
120
130
|
? Math.round(containerW / aspectRatio)
|
|
121
131
|
: (height ?? DEFAULT_HEIGHT)
|
|
122
132
|
|
|
123
|
-
const sideLegendWidth =
|
|
124
|
-
const topBottomLegendHeight =
|
|
133
|
+
const sideLegendWidth = legendVisible && vertical ? legendW : 0
|
|
134
|
+
const topBottomLegendHeight = legendVisible && !vertical ? legendH : 0
|
|
125
135
|
|
|
126
136
|
const plot = {
|
|
127
137
|
width: Math.max(containerW - sideLegendWidth, minWidth),
|
|
@@ -129,7 +139,7 @@ export function ChartFrame({
|
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
const legendNode =
|
|
132
|
-
legend === false ? null : (
|
|
142
|
+
legend === false || isPending ? null : (
|
|
133
143
|
<div ref={legendRef} style={legendWrapperStyle(vertical)}>
|
|
134
144
|
<ChartLegend
|
|
135
145
|
items={deriveLegend(series)}
|
|
@@ -148,9 +158,12 @@ export function ChartFrame({
|
|
|
148
158
|
ref={containerRef}
|
|
149
159
|
style={outerStyle(fill, vertical)}
|
|
150
160
|
{...(ariaLabel !== undefined && { role: 'img', 'aria-label': ariaLabel })}
|
|
161
|
+
{...(isPending && { 'aria-busy': 'true' })}
|
|
151
162
|
>
|
|
152
163
|
{legendNode !== null && (placement === 'top' || placement === 'left') && legendNode}
|
|
153
|
-
{plot.width > 0 &&
|
|
164
|
+
{plot.width > 0 &&
|
|
165
|
+
plot.height > 0 &&
|
|
166
|
+
(isPending ? <ChartPending width={plot.width} height={plot.height} /> : children(plot))}
|
|
154
167
|
{legendNode !== null && (placement === 'bottom' || placement === 'right') && legendNode}
|
|
155
168
|
</div>
|
|
156
169
|
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import { VX } from '../../tokens'
|
|
3
|
+
|
|
4
|
+
export type ChartCenterProps = {
|
|
5
|
+
width: number
|
|
6
|
+
height: number
|
|
7
|
+
children: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const centerStyle = (width: number, height: number): CSSProperties => ({
|
|
11
|
+
width,
|
|
12
|
+
height,
|
|
13
|
+
display: 'flex',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
justifyContent: 'center',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal layout primitive: centers `children` inside a `width` × `height` box. Exists because
|
|
20
|
+
* `src/charts/**` cannot import Mantine's `Flex`/`Group`/`Center` (the Mantine-free boundary), so
|
|
21
|
+
* there was previously nothing to reach for when a chart file needed to center something. Not a
|
|
22
|
+
* general layout system — width, height, children, nothing else.
|
|
23
|
+
*/
|
|
24
|
+
export function ChartCenter({ width, height, children }: ChartCenterProps): ReactNode {
|
|
25
|
+
return <div style={centerStyle(width, height)}>{children}</div>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ChartPendingProps = {
|
|
29
|
+
width: number
|
|
30
|
+
height: number
|
|
31
|
+
/** Default `'Loading…'`. */
|
|
32
|
+
label?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const labelStyle: CSSProperties = {
|
|
36
|
+
color: VX.faint,
|
|
37
|
+
fontSize: VX.text.sm,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The placeholder for a chart whose data hasn't arrived yet — the third "nothing to draw" state
|
|
42
|
+
* alongside measured-and-empty and measured-and-absent (a real gap in coverage). Collapsing an
|
|
43
|
+
* in-flight query into either of those (the `data ?? []` idiom) densifies it into a fully-hatched
|
|
44
|
+
* "not measured" window: a positive claim that the series WAS watched and carried nothing, when in
|
|
45
|
+
* fact it was never asked. `ChartPending` makes "not asked yet" its own rendered state instead of
|
|
46
|
+
* borrowing one of the other two.
|
|
47
|
+
*
|
|
48
|
+
* Reserves exactly the plot's footprint and draws NOTHING that could be mistaken for a
|
|
49
|
+
* measurement — no axes, no gridlines, no hatching, no data marks — just a faint, static, centered
|
|
50
|
+
* label. No animation: the package's motion doctrine bans looping/pulsing idle motion, so a static
|
|
51
|
+
* reserved box is the correct answer here, not a compromise.
|
|
52
|
+
*/
|
|
53
|
+
export function ChartPending({ width, height, label = 'Loading…' }: ChartPendingProps): ReactNode {
|
|
54
|
+
return (
|
|
55
|
+
<ChartCenter width={width} height={height}>
|
|
56
|
+
<span style={labelStyle}>{label}</span>
|
|
57
|
+
</ChartCenter>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode, RefObject } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { VX } from '../../tokens'
|
|
3
4
|
import { fmtTooltipDate } from '../utils/format'
|
|
4
5
|
|
|
@@ -24,7 +25,10 @@ export function useTooltipStyles(): CSSProperties {
|
|
|
24
25
|
return TOOLTIP_STYLES
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Outer tooltip shell. Renders nothing when tip is null. Renders to `document.body` via a
|
|
30
|
+
* portal, so it may be authored anywhere in the tree, including inside `<svg>`.
|
|
31
|
+
*/
|
|
28
32
|
export function ChartTooltip({
|
|
29
33
|
tip,
|
|
30
34
|
tooltipRef,
|
|
@@ -37,10 +41,18 @@ export function ChartTooltip({
|
|
|
37
41
|
children: ReactNode
|
|
38
42
|
}) {
|
|
39
43
|
if (!tip) return null
|
|
40
|
-
|
|
44
|
+
// SSR guard: react-dom/server (renderToStaticMarkup, used by this repo's tests — see
|
|
45
|
+
// dashboard/stat-card.test.tsx) has no jsdom and does not support portals.
|
|
46
|
+
if (typeof document === 'undefined') return null
|
|
47
|
+
// Portal to document.body for two reasons: (1) authoring this inside an <svg> tree would
|
|
48
|
+
// otherwise create the div in the SVG namespace, where it mounts, accepts every prop, and
|
|
49
|
+
// is never painted; (2) TOOLTIP_STYLES' position: 'fixed' breaks inside any transformed
|
|
50
|
+
// ancestor unless the node is portalled out to the body.
|
|
51
|
+
return createPortal(
|
|
41
52
|
<div ref={tooltipRef} style={{ ...styles, left: tip.x, top: tip.y }}>
|
|
42
53
|
{children}
|
|
43
|
-
</div
|
|
54
|
+
</div>,
|
|
55
|
+
document.body,
|
|
44
56
|
)
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PointerEventHandler } from 'react'
|
|
2
2
|
|
|
3
|
-
/** Transparent <rect> that captures
|
|
3
|
+
/** Transparent <rect> that captures pointer events (mouse + touch + pen) for tooltip + crosshair
|
|
4
|
+
* sync. */
|
|
4
5
|
export function HoverOverlay({
|
|
5
6
|
width,
|
|
6
7
|
height,
|
|
@@ -9,16 +10,21 @@ export function HoverOverlay({
|
|
|
9
10
|
}: {
|
|
10
11
|
width: number
|
|
11
12
|
height: number
|
|
12
|
-
onMove:
|
|
13
|
-
onLeave:
|
|
13
|
+
onMove: PointerEventHandler<SVGRectElement>
|
|
14
|
+
onLeave: PointerEventHandler<SVGRectElement>
|
|
14
15
|
}) {
|
|
15
16
|
return (
|
|
16
17
|
<rect
|
|
17
18
|
width={width}
|
|
18
19
|
height={height}
|
|
19
20
|
fill="transparent"
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// `pan-y`, not `none`: `none` would turn a full-width chart into a scroll dead zone on a
|
|
22
|
+
// phone. `pan-y` lets vertical page scroll pass through while a horizontal drag still
|
|
23
|
+
// reaches this overlay to scrub the chart.
|
|
24
|
+
style={{ touchAction: 'pan-y' }}
|
|
25
|
+
onPointerMove={onMove}
|
|
26
|
+
onPointerLeave={onLeave}
|
|
27
|
+
onPointerCancel={onLeave}
|
|
22
28
|
/>
|
|
23
29
|
)
|
|
24
30
|
}
|