basalt-ui 2.3.0 → 2.4.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/CHANGELOG.md +18 -0
- package/MIGRATING.md +57 -12
- package/agent/rules/basalt-state.md +1 -1
- package/configs/lefthook.yml +4 -1
- package/dist/charts/CartesianChart.d.ts.map +1 -1
- package/dist/charts/CartesianChart.js +6 -2
- package/dist/charts/DualPanel.d.ts +5 -1
- package/dist/charts/DualPanel.d.ts.map +1 -1
- package/dist/charts/DualPanel.js +32 -14
- package/dist/charts/Heatmap.js +2 -2
- package/dist/charts/Lines.js +2 -2
- package/dist/charts/MirroredBars.d.ts.map +1 -1
- package/dist/charts/MirroredBars.js +8 -7
- package/dist/charts/compile.d.ts +15 -2
- package/dist/charts/compile.d.ts.map +1 -1
- package/dist/charts/compile.js +153 -32
- package/dist/charts/marks.d.ts +8 -0
- package/dist/charts/marks.d.ts.map +1 -1
- package/dist/charts/marks.js +22 -1
- package/dist/charts/panel.js +2 -2
- package/dist/charts/types.d.ts +39 -1
- package/dist/charts/types.d.ts.map +1 -1
- package/dist/state/fields.d.ts +19 -3
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +14 -8
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/CartesianChart.tsx +7 -2
- package/src/charts/DualPanel.tsx +37 -15
- package/src/charts/Heatmap.tsx +2 -2
- package/src/charts/Lines.tsx +2 -2
- package/src/charts/MirroredBars.tsx +8 -7
- package/src/charts/compile.ts +203 -35
- package/src/charts/marks.ts +35 -1
- package/src/charts/panel.tsx +2 -2
- package/src/charts/types.ts +37 -1
- package/src/state/fields.ts +55 -19
package/src/charts/panel.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import { useCallback } from 'react'
|
|
|
21
21
|
import type { ReactElement } from 'react'
|
|
22
22
|
import { ChartShell } from './CartesianChart'
|
|
23
23
|
import type { ChartShellProps, ShellContext } from './CartesianChart'
|
|
24
|
-
import { defaultFormatX, utcHeaderDate, visibleSeries, xGuide, yGuide } from './compile'
|
|
24
|
+
import { defaultFormatX, engineRow, utcHeaderDate, visibleSeries, xGuide, yGuide } from './compile'
|
|
25
25
|
import type { CartesianDefinition } from './compile'
|
|
26
26
|
import { cursorBinding, sourceOnlyFocus } from './cursor'
|
|
27
27
|
import type { XScaleModel } from './scales'
|
|
@@ -205,7 +205,7 @@ function panelDefinition<T, X extends ChartX>(
|
|
|
205
205
|
const { x, d } = sourceOf<T>(primary, owned)
|
|
206
206
|
const extra: readonly ChartTooltipRow[] =
|
|
207
207
|
tooltip?.rows?.(d, { visible, hidden: hiddenSeries }) ?? []
|
|
208
|
-
return { title: header(x, d), rows: [...plan.rows(d), ...extra] }
|
|
208
|
+
return { title: header(x, d), rows: [...plan.rows(d), ...extra.map(engineRow)] }
|
|
209
209
|
},
|
|
210
210
|
},
|
|
211
211
|
}
|
package/src/charts/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export type ChartSeries<T> = {
|
|
|
31
31
|
* tier, a config value) is a step: a smoothed curve draws values that never existed. */
|
|
32
32
|
curve?: CurveName | undefined
|
|
33
33
|
strokeWidth?: number | undefined
|
|
34
|
+
/**
|
|
35
|
+
* What a `null` value does to the line (and its `area`). `'break'` (default) leaves a gap;
|
|
36
|
+
* `'connect'` draws through the series' non-null points only, skipping the null rows, so a series
|
|
37
|
+
* that is null on most shared x values (one lift in a wide table) is one continuous line. Points,
|
|
38
|
+
* markers, the cursor and the tooltip still read real values: a null row has no tooltip row.
|
|
39
|
+
*/
|
|
40
|
+
gaps?: 'break' | 'connect' | undefined
|
|
34
41
|
/** Dims the plotted stroke (a faint companion line). Strokes only. */
|
|
35
42
|
strokeOpacity?: number | undefined
|
|
36
43
|
/** Bars and areas. */
|
|
@@ -53,6 +60,12 @@ export type ChartSeries<T> = {
|
|
|
53
60
|
note?: string | undefined
|
|
54
61
|
/** Per-series tooltip value format; receives the datum so a row can read more than the value. */
|
|
55
62
|
formatValue?: ((v: number, d: T) => string) | undefined
|
|
63
|
+
/**
|
|
64
|
+
* A cursor dot for a series whose mark the consumer draws itself (`marks`, so `getValue` stays
|
|
65
|
+
* `null`): the y of the dot at each datum, `null` for none. The dot shows while the cursor is on
|
|
66
|
+
* that datum's x and takes the series color. A series drawn from `plot.rows` already has one.
|
|
67
|
+
*/
|
|
68
|
+
cursorValue?: ((d: T) => number | null) | undefined
|
|
56
69
|
/** A marker at a point (a PR star, a status dot); `null` for none. */
|
|
57
70
|
getMarker?:
|
|
58
71
|
| ((d: T) => {
|
|
@@ -95,12 +108,24 @@ export type AxisConfig<T> = {
|
|
|
95
108
|
scale?: 'linear' | 'log' | undefined
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
/** A rule along a band's boundary, painted with the band. A bound at the plot edge draws none. */
|
|
112
|
+
export type ZoneEdge<S extends string> = {
|
|
113
|
+
side: S | 'both'
|
|
114
|
+
color: string
|
|
115
|
+
dashed?: boolean | undefined
|
|
116
|
+
/** Default `1`. */
|
|
117
|
+
width?: number | undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
/** A horizontal band on one y axis. `±Infinity` reaches the plot edge. */
|
|
99
121
|
export type ZoneSpec = {
|
|
100
122
|
from: number
|
|
101
123
|
to: number
|
|
102
124
|
fill: string
|
|
103
125
|
axisSide?: 'left' | 'right' | undefined
|
|
126
|
+
/** A rule on the band's visual `top` (the larger bound) and/or `bottom` (the smaller), whichever
|
|
127
|
+
* of `from`/`to` that is. */
|
|
128
|
+
edge?: ZoneEdge<'top' | 'bottom'> | undefined
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
/**
|
|
@@ -113,6 +138,8 @@ export type XZoneSpec<X extends ChartX = ChartX> = {
|
|
|
113
138
|
to?: X | undefined
|
|
114
139
|
fill: string
|
|
115
140
|
align?: 'center' | 'edge' | undefined
|
|
141
|
+
/** A rule on the band's `start` (`from`) and/or `end` (`to`) boundary; an omitted bound has none. */
|
|
142
|
+
edge?: ZoneEdge<'start' | 'end'> | undefined
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
/** A horizontal reference line — annotation only, no domain role beyond staying inside the plot. */
|
|
@@ -121,6 +148,13 @@ export type RefLine = {
|
|
|
121
148
|
color: string
|
|
122
149
|
dashed?: boolean | undefined
|
|
123
150
|
axis?: 'left' | 'right' | undefined
|
|
151
|
+
/**
|
|
152
|
+
* Names the line in the legend (its color, dashed as `dashed`; a toggle hides the line), so a
|
|
153
|
+
* threshold needs no always-`null` series. `mark` is the swatch shape. Default `'line'`. The
|
|
154
|
+
* toggle state is keyed by the line's index in `refLines`: reordering the array while a line is
|
|
155
|
+
* toggled off moves the "off" to whichever line now sits at that index.
|
|
156
|
+
*/
|
|
157
|
+
legend?: { label: string; mark?: 'line' | 'bar' | undefined } | undefined
|
|
124
158
|
}
|
|
125
159
|
|
|
126
160
|
/** A fill between the line and a value, on one side of it, tracking the line. */
|
|
@@ -138,7 +172,9 @@ export type ChartTooltipRow = {
|
|
|
138
172
|
label: string
|
|
139
173
|
value: string
|
|
140
174
|
/** The row swatch; omitted, the row has none. */
|
|
141
|
-
color?: string
|
|
175
|
+
color?: string | undefined
|
|
176
|
+
/** The swatch shape; `'dashed'` for a reference or dashed series. Default `'solid'`. */
|
|
177
|
+
swatch?: 'solid' | 'dashed' | undefined
|
|
142
178
|
}
|
|
143
179
|
|
|
144
180
|
export type ChartTooltipConfig<T, X extends ChartX = ChartX> = {
|
package/src/state/fields.ts
CHANGED
|
@@ -279,6 +279,24 @@ type NumberHandleExtras<F> = F extends { kind: 'number' }
|
|
|
279
279
|
readonly step?: undefined
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/**
|
|
283
|
+
* `createLocalStore`'s `use()` read options. `fallback` is a CALL-SITE default for a value that
|
|
284
|
+
* arrives as a prop (a leader, a latest date): it applies only while the field is unset (nothing
|
|
285
|
+
* stored), is applied in render AFTER the store read, and is never persisted — `clear()` unsets
|
|
286
|
+
* back to it. `handle.fallback` and `isDefault` still report the definition's.
|
|
287
|
+
*
|
|
288
|
+
* An object fallback (range, multi) must be memoised by the caller: the value returned while unset
|
|
289
|
+
* IS that object, so a fresh literal per render changes identity every render.
|
|
290
|
+
*/
|
|
291
|
+
export type FieldReadOptions<F extends AnyField> = { fallback?: FieldValue<F> }
|
|
292
|
+
|
|
293
|
+
/** A `createLocalStore` field: the shared handle, plus `use({ fallback })`. */
|
|
294
|
+
export type LocalFieldHandle<F extends AnyField> = FieldHandle<F> & {
|
|
295
|
+
use(
|
|
296
|
+
read?: FieldReadOptions<F>,
|
|
297
|
+
): readonly [FieldValue<F>, (next: FieldValue<F>, opts?: FieldSetOptions) => void]
|
|
298
|
+
}
|
|
299
|
+
|
|
282
300
|
/**
|
|
283
301
|
* The binding every basalt control takes instead of `value`/`onChange` (C2). One field, both lanes,
|
|
284
302
|
* one resolution law. Handed out by `createSearchStore().field.<name>` /
|
|
@@ -771,7 +789,7 @@ type StoreRecord = Record<string, unknown>
|
|
|
771
789
|
export type FieldWrite = (next: unknown, opts?: FieldSetOptions) => void
|
|
772
790
|
|
|
773
791
|
/** One field's `use()`, untyped. @internal — see `FieldCodec`. */
|
|
774
|
-
export type FieldUse = () => readonly [unknown, FieldWrite]
|
|
792
|
+
export type FieldUse = (read?: FieldReadOptions<AnyField>) => readonly [unknown, FieldWrite]
|
|
775
793
|
|
|
776
794
|
/** One named field plus its codec, in declaration order. @internal — see `FieldCodec`. */
|
|
777
795
|
export type StoreEntry = { readonly name: string; readonly codec: FieldCodec }
|
|
@@ -798,7 +816,12 @@ export type StoreCore = {
|
|
|
798
816
|
setLabels(map: Record<string, Record<string, string> | undefined>): void
|
|
799
817
|
optionsFor(entry: StoreEntry): readonly FieldOption[]
|
|
800
818
|
/** The resolution law, once: URL ⊳ localStorage ⊳ fallback. `search: null` skips the URL lane. */
|
|
801
|
-
resolve(
|
|
819
|
+
resolve(
|
|
820
|
+
entry: StoreEntry,
|
|
821
|
+
search: StoreRecord | null,
|
|
822
|
+
record: StoreRecord,
|
|
823
|
+
fallback?: unknown,
|
|
824
|
+
): unknown
|
|
802
825
|
writeField(setRecord: (next: StoreRecord) => void, name: string, value: unknown): void
|
|
803
826
|
/**
|
|
804
827
|
* The IN-MEMORY lane's `use()` for one field — a field with neither the URL nor the mirror.
|
|
@@ -841,14 +864,19 @@ function assignDefined(into: StoreRecord, from: StoreRecord): void {
|
|
|
841
864
|
* The resolution law, in one place: URL ⊳ localStorage ⊳ fallback (C4). `search: null` skips the
|
|
842
865
|
* URL lane — what the local lane and `linkSearch` pass.
|
|
843
866
|
*/
|
|
844
|
-
function resolveField(
|
|
867
|
+
function resolveField(
|
|
868
|
+
entry: StoreEntry,
|
|
869
|
+
search: StoreRecord | null,
|
|
870
|
+
record: StoreRecord,
|
|
871
|
+
fallback?: unknown,
|
|
872
|
+
): unknown {
|
|
845
873
|
const fromUrl = search === null ? null : entry.codec.fromSearch(search)
|
|
846
874
|
if (fromUrl !== null) return fromUrl
|
|
847
875
|
if (entry.codec.lane.persist) {
|
|
848
876
|
const stored = entry.codec.decode(record[entry.name])
|
|
849
877
|
if (stored !== null) return stored
|
|
850
878
|
}
|
|
851
|
-
return entry.codec.fallback
|
|
879
|
+
return fallback === undefined ? entry.codec.fallback : fallback
|
|
852
880
|
}
|
|
853
881
|
|
|
854
882
|
/**
|
|
@@ -865,7 +893,7 @@ function resolveField(entry: StoreEntry, search: StoreRecord | null, record: Sto
|
|
|
865
893
|
function createMemoryLane(): {
|
|
866
894
|
subscribe: (cb: () => void) => () => void
|
|
867
895
|
getVersion: () => number
|
|
868
|
-
resolve: (entry: StoreEntry) => unknown
|
|
896
|
+
resolve: (entry: StoreEntry, fallback?: unknown) => unknown
|
|
869
897
|
write: (name: string, value: unknown) => void
|
|
870
898
|
remove: (name: string) => void
|
|
871
899
|
clear: () => void
|
|
@@ -889,9 +917,10 @@ function createMemoryLane(): {
|
|
|
889
917
|
getVersion: () => version,
|
|
890
918
|
// `undefined` is the only "never written" marker the lane needs: no field kind has `undefined`
|
|
891
919
|
// as a legal value, so it can never collide with a written one.
|
|
892
|
-
resolve: (entry) => {
|
|
920
|
+
resolve: (entry, fallback) => {
|
|
893
921
|
const stored = values.get(entry.name)
|
|
894
|
-
|
|
922
|
+
if (stored !== undefined) return stored
|
|
923
|
+
return fallback === undefined ? entry.codec.fallback : fallback
|
|
895
924
|
},
|
|
896
925
|
write: (name, value) => {
|
|
897
926
|
values.set(name, value)
|
|
@@ -1036,11 +1065,14 @@ export function createStoreCore(o: StoreCoreOptions): StoreCore {
|
|
|
1036
1065
|
},
|
|
1037
1066
|
readRecord,
|
|
1038
1067
|
memoryUse(entry) {
|
|
1039
|
-
return () => {
|
|
1068
|
+
return (read) => {
|
|
1040
1069
|
// Subscribing to the VERSION (never to the value) is what re-renders every other mount of
|
|
1041
1070
|
// this field when one of them writes, with no snapshot-stability trap.
|
|
1042
1071
|
useSyncExternalStore(memory.subscribe, memory.getVersion, memory.getVersion)
|
|
1043
|
-
return [
|
|
1072
|
+
return [
|
|
1073
|
+
memory.resolve(entry, read?.fallback),
|
|
1074
|
+
(next: unknown) => memory.write(entry.name, next),
|
|
1075
|
+
] as const
|
|
1044
1076
|
}
|
|
1045
1077
|
},
|
|
1046
1078
|
readMemoryValue: (entry) => memory.resolve(entry),
|
|
@@ -1074,7 +1106,7 @@ export function createStoreCore(o: StoreCoreOptions): StoreCore {
|
|
|
1074
1106
|
|
|
1075
1107
|
/** `Pick<SearchStore<S>, 'field' | 'readStored' | 'labels'>` — the same handles, no router. */
|
|
1076
1108
|
export type LocalStore<S extends Record<string, AnyField>> = {
|
|
1077
|
-
readonly field: { [K in keyof S]:
|
|
1109
|
+
readonly field: { [K in keyof S]: LocalFieldHandle<S[K]> }
|
|
1078
1110
|
readStored: () => Partial<StoredValues<S>>
|
|
1079
1111
|
/**
|
|
1080
1112
|
* Option labels for enum / multi / range fields — the same chainable contract
|
|
@@ -1116,20 +1148,24 @@ export function createLocalStore<const S extends Record<string, AnyField>>(o: {
|
|
|
1116
1148
|
}): LocalStore<S> {
|
|
1117
1149
|
const core = createStoreCore({ key: o.key, fields: o.fields, version: o.version })
|
|
1118
1150
|
|
|
1119
|
-
const persistedUse =
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
core.
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1151
|
+
const persistedUse =
|
|
1152
|
+
(entry: StoreEntry): FieldUse =>
|
|
1153
|
+
(read) => {
|
|
1154
|
+
const [record, setRecord] = core.usePersistedRecord()
|
|
1155
|
+
return [
|
|
1156
|
+
// The call-site fallback is applied here, in render, after the store read — never inside
|
|
1157
|
+
// the snapshot — so React's out-of-render snapshot reads cannot disagree with it.
|
|
1158
|
+
core.resolve(entry, null, record, read?.fallback),
|
|
1159
|
+
(next: unknown) => core.writeField(setRecord, entry.name, next),
|
|
1160
|
+
] as const
|
|
1161
|
+
}
|
|
1126
1162
|
|
|
1127
|
-
const field = {} as { [K in keyof S]:
|
|
1163
|
+
const field = {} as { [K in keyof S]: LocalFieldHandle<S[K]> }
|
|
1128
1164
|
for (const entry of core.entries) {
|
|
1129
1165
|
const persisted = entry.codec.lane.persist
|
|
1130
1166
|
const use = persisted ? persistedUse(entry) : core.memoryUse(entry)
|
|
1131
1167
|
const clear = persisted ? () => core.clearField(entry.name) : () => core.clearMemory(entry.name)
|
|
1132
|
-
field[entry.name as keyof S] = core.handle(entry, use, clear) as unknown as
|
|
1168
|
+
field[entry.name as keyof S] = core.handle(entry, use, clear) as unknown as LocalFieldHandle<
|
|
1133
1169
|
S[keyof S]
|
|
1134
1170
|
>
|
|
1135
1171
|
}
|