basalt-ui 1.23.1 → 1.25.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 +11 -6
- package/CHANGELOG.md +21 -0
- package/MIGRATING.md +138 -4
- package/README.md +123 -32
- package/agent/rules/basalt-data.md +104 -12
- package/agent/rules/basalt-query.md +57 -0
- package/agent/skills/basalt-app/SKILL.md +7 -0
- package/dist/cli/index.d.ts +4 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +101 -17
- package/dist/dashboard/empty-state.d.ts +6 -2
- package/dist/dashboard/empty-state.d.ts.map +1 -1
- package/dist/dashboard/empty-state.js +1 -1
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +8 -0
- package/dist/dashboard/query-state.d.ts +96 -0
- package/dist/dashboard/query-state.d.ts.map +1 -0
- package/dist/dashboard/query-state.js +123 -0
- package/dist/dashboard/query-state.test.d.ts +2 -0
- package/dist/dashboard/query-state.test.d.ts.map +1 -0
- package/dist/data/data-table.d.ts +125 -8
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +121 -13
- package/dist/data/data-table.test.d.ts +2 -0
- package/dist/data/data-table.test.d.ts.map +1 -0
- package/dist/data/table.d.ts +2 -2
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +21 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/query/error-message.d.ts +41 -0
- package/dist/query/error-message.d.ts.map +1 -0
- package/dist/query/error-message.js +30 -0
- package/dist/query/error-message.test.d.ts +2 -0
- package/dist/query/error-message.test.d.ts.map +1 -0
- package/dist/query/index.d.ts +1 -19
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +2 -14
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/cli/check-theme.test.ts +34 -0
- package/src/cli/index.ts +222 -44
- package/src/cli/tokens-css.test.ts +73 -6
- package/src/cli/toolchain-wiring.test.ts +146 -0
- package/src/dashboard/empty-state.tsx +9 -3
- package/src/dashboard/index.ts +11 -0
- package/src/dashboard/query-state.test.tsx +152 -0
- package/src/dashboard/query-state.tsx +272 -0
- package/src/data/data-table.test.tsx +349 -0
- package/src/data/data-table.tsx +359 -42
- package/src/data/table.ts +8 -1
- package/src/guard/check-source.test.ts +141 -0
- package/src/guard/index.ts +102 -8
- package/src/index.ts +9 -0
- package/src/query/error-message.test.ts +67 -0
- package/src/query/error-message.ts +70 -0
- package/src/query/index.ts +2 -34
- package/src/surfaces.ts +1 -1
|
@@ -59,6 +59,34 @@ const columns = [
|
|
|
59
59
|
| `initialSorting` | `SortingState` | `[]` | Initial sort state; drives `useState` initializer |
|
|
60
60
|
| `onSortingChange` | `(s: SortingState) => void` | — | Called whenever sorting changes; omit for uncontrolled |
|
|
61
61
|
|
|
62
|
+
Body chrome — all conditional pass-throughs to Mantine `Table` except `withTableBorder`, which
|
|
63
|
+
basalt defaults to `true`:
|
|
64
|
+
|
|
65
|
+
| Prop | Type | Notes |
|
|
66
|
+
| --------------------------------------- | ------------------------------- | -------------------------------------------------------------------- |
|
|
67
|
+
| `maxHeight` | `number \| string` | Caps the body height — renders `Table.ScrollContainer type="native"` |
|
|
68
|
+
| `minWidth` | `number \| string` | Same container; `maxHeight` alone passes `minWidth={0}` |
|
|
69
|
+
| `stickyHeader` / `stickyHeaderOffset` | `boolean` / `number \| string` | Forwarded to Mantine; pair with `maxHeight` |
|
|
70
|
+
| `verticalSpacing` / `horizontalSpacing` | `MantineSpacing` | Cell padding |
|
|
71
|
+
| `withRowBorders` / `withTableBorder` | `boolean` | `withTableBorder` defaults `true` here, overriding Mantine's `false` |
|
|
72
|
+
| `meta: { align }` (per column) | `'left' \| 'center' \| 'right'` | Sets `textAlign` on BOTH the `th` and the `td` |
|
|
73
|
+
| `meta: { numeral: false }` (per column) | `boolean` | Opts a numeric cell OUT of the mono-numeral style. Opt-out only |
|
|
74
|
+
|
|
75
|
+
`meta.align` is a `ColumnMeta` module augmentation, so a typo'd key is a tsc error and a value
|
|
76
|
+
outside the union **throws** naming the column — a money column never silently left-aligns. Set it
|
|
77
|
+
once on the column def instead of repeating `textAlign: 'right'` on the header and the cell.
|
|
78
|
+
|
|
79
|
+
**`meta.numeral: false` is needed less often than it looks.** The auto style sets `fontFamily`,
|
|
80
|
+
`fontSize`, `fontWeight`, `fontVariantNumeric` and `color` on the `<td>` whenever the raw value is a
|
|
81
|
+
number — but a child `Text` sets its own `color` and `fontWeight` and wins those, and never sets a
|
|
82
|
+
`fontFamily`. **Only `fontFamily` leaks.** So `c="dimmed"` is not a reason to opt out (and on a
|
|
83
|
+
percentage column the mono figures are what you want); `fw={600}` on an accent figure is, because
|
|
84
|
+
the `<td>`'s monospace still overrides the family the `Text` inherited. One consumer measured this
|
|
85
|
+
across three tables: **3 columns** genuinely needed the opt-out, not the 4 that were assumed. Reach
|
|
86
|
+
for it when the cell renders its own chrome — a coloured `Text`, a `Badge`, a sparkline — and check
|
|
87
|
+
what actually leaked before adding it. `null` is worth a thought too: `typeof null === 'object'`, so
|
|
88
|
+
a `number | null` column goes mono only on its non-null rows, which is worse than uniformly wrong.
|
|
89
|
+
|
|
62
90
|
### Controlled sorting and URL sync
|
|
63
91
|
|
|
64
92
|
`initialSorting` seeds the internal `useState` and `onSortingChange` is called on every sort
|
|
@@ -122,18 +150,57 @@ col.display({ id: 'actions', header: 'Actions', cell: (ctx) => <Button>Delete {c
|
|
|
122
150
|
`ColumnDef<T>` defaults `TValue` to `unknown`, not `any`. Never cast column defs to `any` — use
|
|
123
151
|
the correct `ColumnDef<T>` type or `ColumnDef<T, unknown>`.
|
|
124
152
|
|
|
125
|
-
###
|
|
153
|
+
### Also shipped, and easy to miss
|
|
154
|
+
|
|
155
|
+
Global search (`enableGlobalFilter`, `globalFilterPlaceholder`, `searchIcon`,
|
|
156
|
+
`initialGlobalFilter`, `onGlobalFilterChange`), faceted filters (`facets`), a toolbar slot
|
|
157
|
+
(`toolbarActions`), pagination client- or server-side (`enablePagination`, `pageSizeOptions`,
|
|
158
|
+
`initialPagination`, `onPaginationChange`, `manualPagination`, `rowCount`, `pageCount`) and column
|
|
159
|
+
pinning (`enablePinning`, `initialColumnPinning`). **Don't hand-roll any of these** — read the props
|
|
160
|
+
on the type, they are all there.
|
|
161
|
+
|
|
162
|
+
### `manualPagination` imposes a contract on the rest of the props
|
|
163
|
+
|
|
164
|
+
Adopting it makes `data` **one server page** while the bar still reads `Showing 1–25 of 412`. Every
|
|
165
|
+
other client-side control then becomes a claim about 412 rows it can only make about 25 — a sort
|
|
166
|
+
that reorders one page under a header chevron that says it sorted all of them is a plausible, wrong
|
|
167
|
+
answer with nothing on screen to give it away. So each control has to be resolved explicitly; there
|
|
168
|
+
is no safe default, because only the call site knows whether the server does the work.
|
|
169
|
+
|
|
170
|
+
| With `manualPagination` | Resolve it with |
|
|
171
|
+
| ------------------------- | ----------------------------------------------------------------------------------- |
|
|
172
|
+
| the pagination bar itself | `enablePagination` (without it `manualPagination` is inert) + `rowCount` |
|
|
173
|
+
| sorting | `manualSorting` + sort in the `onSortingChange` request, or `enableSorting={false}` |
|
|
174
|
+
| `enableGlobalFilter` | `manualFiltering` + filter in the `onGlobalFilterChange` request |
|
|
175
|
+
| `facets` | `manualFiltering` + `onColumnFiltersChange` |
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<BasaltDataTable
|
|
179
|
+
data={page.rows}
|
|
180
|
+
columns={columns}
|
|
181
|
+
enablePagination
|
|
182
|
+
manualPagination
|
|
183
|
+
rowCount={page.total}
|
|
184
|
+
manualSorting
|
|
185
|
+
onSortingChange={(s) => refetch({ sort: s[0] })}
|
|
186
|
+
onPaginationChange={(p) => refetch({ page: p })}
|
|
187
|
+
/>
|
|
188
|
+
```
|
|
126
189
|
|
|
127
|
-
|
|
190
|
+
`manualSorting` / `manualFiltering` don't switch the controls off — the headers and inputs stay
|
|
191
|
+
live and keep reporting through their `on*Change` callbacks. They stop the table from _recomputing_
|
|
192
|
+
the answer locally, so what renders is the order and the selection the server sent.
|
|
128
193
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
**Unresolved, it is not silent.** A contradiction throws in dev naming every breach at once, and a
|
|
195
|
+
production bundle (where the throw is constant-folded away) degrades to the honest table instead:
|
|
196
|
+
no sort headers, no filter controls, no `of N` it cannot stand behind, plus one `console.error`.
|
|
197
|
+
You will never get the plausible wrong answer, but you also won't get sorting you thought you had —
|
|
198
|
+
so read the throw, don't work around it.
|
|
133
199
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
200
|
+
Still deferred: row selection, row expansion / sub-rows, and fully controlled (external) sorting
|
|
201
|
+
STATE — `manualSorting` hands the _work_ to the server, but the table still owns the `SortingState`
|
|
202
|
+
and reports it through `onSortingChange`. Add them when a concrete consumer need arises; keep the API
|
|
203
|
+
additive.
|
|
137
204
|
|
|
138
205
|
### The blessed lane vs the raw escape hatch
|
|
139
206
|
|
|
@@ -166,9 +233,34 @@ const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowMo
|
|
|
166
233
|
// table.getHeaderGroups() / table.getRowModel().rows, rendered with flexRender(...) and your own markup
|
|
167
234
|
```
|
|
168
235
|
|
|
169
|
-
Reach for the escape hatch only when `BasaltDataTable`'s props genuinely can't express the shape
|
|
170
|
-
|
|
171
|
-
|
|
236
|
+
Reach for the escape hatch only when `BasaltDataTable`'s props genuinely can't express the shape.
|
|
237
|
+
Pagination, server-side data and filtering are **not** among those cases any more — see above.
|
|
238
|
+
|
|
239
|
+
**Both lanes scroll through the same node.** An escape-hatch table that needs a capped body wraps
|
|
240
|
+
it in `<Table.ScrollContainer type="native" minWidth={0} maxHeight={320}>` — and that is byte for
|
|
241
|
+
byte what `BasaltDataTable`'s `maxHeight`/`minWidth` render. The blessed lane and the escape cannot
|
|
242
|
+
contradict each other on scrolling, and neither should reach for `type="scrollarea"`: `ScrollArea`'s
|
|
243
|
+
custom viewport is the positioning context a sticky `<thead>` resolves against, so the default type
|
|
244
|
+
pins the header to the page viewport instead of the table's box. (`basalt/raw-scroll-container`
|
|
245
|
+
takes no view here — it steers raw `overflow: auto`, and `Table.ScrollContainer` is a Mantine
|
|
246
|
+
component, not a raw scroll box.)
|
|
247
|
+
|
|
248
|
+
**Adopt the blessed lane for ownership, not for a line count.** Porting one consumer's three tables
|
|
249
|
+
onto these props made them 341 → **370–379** lines, **29–38 longer**: a scratch port measured the
|
|
250
|
+
low end, the consumer's own shipped port the high end, because it kept the comment blocks
|
|
251
|
+
justifying each table's `numeral` opt-outs and its `enableSorting` decision. Accessor blocks at
|
|
252
|
+
4–6 lines each cost more than an eight-`<Table.Td>` row at ~3 when every cell is bespoke, and the
|
|
253
|
+
opt-outs are lines the hand-rolled version never had to write. Budget the high end. What the port
|
|
254
|
+
bought was the `type="native"` footgun, alignment stated once instead of on both `th` and `td` —
|
|
255
|
+
**4 literals in one table alone, 6 across the two that had alignment to state** — and
|
|
256
|
+
sorting/filtering/pagination no longer being yours to maintain. Expect that trade, not a shrink.
|
|
257
|
+
|
|
258
|
+
That consumer deliberately left the third table hand-rolled, and the reason generalises: four of
|
|
259
|
+
its six columns would have needed `enableSorting: false` (three sparkline cells over `number[]`
|
|
260
|
+
accessors, one status enum), and a numeric column rendering its own coloured `Text` would have
|
|
261
|
+
needed a `numeral: false` it did not need before. A table with no sorting, no pagination, no filter
|
|
262
|
+
and no alignment to declare is a layout grid that happens to use `<table>` — adopting the component
|
|
263
|
+
there buys nothing and taxes you to switch off things you never asked for.
|
|
172
264
|
|
|
173
265
|
## BasaltVirtualList
|
|
174
266
|
|
|
@@ -76,6 +76,63 @@ const { data } = useSuspenseQuery(resourceQueries.summary())
|
|
|
76
76
|
router's `ensureQueryData` (see basalt-router.md).
|
|
77
77
|
- Wrap pages or panels in `<Suspense fallback={…}>` at the route level.
|
|
78
78
|
|
|
79
|
+
## Rendering a query — use `QueryState`, don't write the switch
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { QueryState } from 'basalt-ui' // NOT basalt-ui/query — it renders Mantine
|
|
83
|
+
|
|
84
|
+
function Library() {
|
|
85
|
+
const q = useQuery(imageQueries.list())
|
|
86
|
+
return (
|
|
87
|
+
<QueryState
|
|
88
|
+
query={q}
|
|
89
|
+
variant="page" // or "section" for a panel inside a page
|
|
90
|
+
errorTitle="Could not load images"
|
|
91
|
+
empty={{ title: 'No images yet', description: 'Upload one to get started.' }}
|
|
92
|
+
isEmpty={(d) => d.items.length === 0}
|
|
93
|
+
>
|
|
94
|
+
{(data) => <ImageGrid items={data.items} />}
|
|
95
|
+
</QueryState>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Rendering only the empty branch is the bug this retires.** A consumer's library showed
|
|
101
|
+
`No images` on a **500**, and a share detail showed `Share not found` on a dropped connection —
|
|
102
|
+
"nothing here" is what an app renders when nobody wrote the other three branches. Swapping the
|
|
103
|
+
import replaced 204 hand-rolled lines across 10 call sites there, with no prop or JSX change.
|
|
104
|
+
|
|
105
|
+
Precedence, in evaluation order:
|
|
106
|
+
|
|
107
|
+
| Condition | Renders |
|
|
108
|
+
| ----------------------------------- | ------------------------------------------------------------------------- |
|
|
109
|
+
| `isError`, no `data` | `ErrorState` — `errorTitle` / `errorFallback` / `errorAction`, with Retry |
|
|
110
|
+
| no `data`, `fetchStatus === 'idle'` | the `empty` copy (nothing, if `empty` is omitted) |
|
|
111
|
+
| no `data` | `loading`, else `LoadingState` |
|
|
112
|
+
| `data`, `isEmpty(data)` | the `empty` copy |
|
|
113
|
+
| `data` | `children` — a node, or `(data) => node` |
|
|
114
|
+
| `data` **and** `isError` | a fixed _Showing cached data_ banner above `children` |
|
|
115
|
+
|
|
116
|
+
`errorTitle` / `errorFallback` / `errorAction` reach the no-data error branch only; the stale-data
|
|
117
|
+
banner carries its own copy. `empty` is `{ title, description?, icon?, action? }` — `description`
|
|
118
|
+
is optional, so a compact panel needs no invented second sentence.
|
|
119
|
+
|
|
120
|
+
**`useQuery`, not `useSuspenseQuery`.** A suspense read never hands you an undefined `data` or an
|
|
121
|
+
error branch — it throws to the nearest boundary — so `QueryState` has nothing to decide there.
|
|
122
|
+
Suspense + an error boundary and `QueryState` are the two ways to render a query; pick one per read.
|
|
123
|
+
|
|
124
|
+
`LoadingState` and `ErrorState` ship beside it as the escape hatch for a page that must place its
|
|
125
|
+
branches in different DOM positions.
|
|
126
|
+
|
|
127
|
+
**`query` is a structural subset, checked at runtime.** It needs `{ data, isError, error,
|
|
128
|
+
fetchStatus, refetch }` — a TanStack `UseQueryResult` satisfies it, and a hand-composed object is
|
|
129
|
+
legal. Because the subset gives up the compiler's help, a missing `isError` **throws** naming the
|
|
130
|
+
field rather than rendering a false claim about the data.
|
|
131
|
+
|
|
132
|
+
Decoding an unknown error into copy is `toErrorMessage(err, fallback?)` from `basalt-ui/query`,
|
|
133
|
+
with `errorStatus(err)` for the status; `toErrorMessage` folds the status into the fallback when the
|
|
134
|
+
body decodes to nothing readable, so an opaque envelope never renders as `{}`.
|
|
135
|
+
|
|
79
136
|
## Mutations
|
|
80
137
|
|
|
81
138
|
```ts
|
|
@@ -149,6 +149,13 @@ the single guard-exempt palette source — see `/basalt-charts`.
|
|
|
149
149
|
- `sync --check` makes no changes and exits non-zero on drift — wire it as a CI freshness gate so a
|
|
150
150
|
consumer can't silently fall behind the shipped rules.
|
|
151
151
|
|
|
152
|
+
**Run it anywhere in the repo.** `sync`, `check-theme` and `doctor` share one resolver:
|
|
153
|
+
`BASALT_CWD` → the cwd → declared workspace packages → a two-level descend → an **ascend** to the
|
|
154
|
+
nearest ancestor carrying a basalt project, bounded by the repo root. Both directions announce the
|
|
155
|
+
relocation in one sentence. From a sub-package `sync` now refreshes the parent install rather than
|
|
156
|
+
refusing — but it still **exits 1 rather than scaffolding a second consumer** where the resolved
|
|
157
|
+
project has no `.basalt/manifest.json`. Creating an install stays `basalt-ui init`'s decision.
|
|
158
|
+
|
|
152
159
|
Seeds (`DESIGN.md`, the toolchain files, the scaffolds) are never reconciled or drift-reported —
|
|
153
160
|
they are yours. The managed files (rules, skills, the CLAUDE block) are framework-owned and meant
|
|
154
161
|
to be overwritten; the sync diff is where you review a doctrine change before committing it.
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -454,8 +454,10 @@ type DoctorResult = {
|
|
|
454
454
|
* demonstrably not the installed package — see the check).
|
|
455
455
|
* 5. The running CLI's own version matches the installed basalt-ui (catches a stale
|
|
456
456
|
* `bunx basalt-ui` npm fetch; best-effort, skipped if node_modules is absent).
|
|
457
|
-
* 9. `basaltAppPlugin`
|
|
458
|
-
*
|
|
457
|
+
* 9. The icon files `basaltAppPlugin` (basalt-ui/vite) references exist under the app package's
|
|
458
|
+
* public/ — the package found via `basalt.roots`, not just cwd, so a root-invoked run still
|
|
459
|
+
* reaches it. Never silently omitted: no plugin call anywhere is a pass that says so, a
|
|
460
|
+
* plugin call with no public/ beside it is a SKIPPED.
|
|
459
461
|
* 10 (second half). A declared `basalt.aiMajorSkewReason` when the ai majors currently AGREE — the
|
|
460
462
|
* exemption is stale and can be deleted; an exemption nobody revisits is how a real, later
|
|
461
463
|
* skew slips through unnoticed.
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAwB,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE9E,OAAO,EAAE,UAAU,EAAE,WAAW,EAA2C,MAAM,aAAa,CAAA;AAI9F,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG;IACzB,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,aAAa,CAAA;IACnC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;wFACoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAA;IACpD,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC,CAAA;IACzD;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED;;;GAGG;AACH,KAAK,eAAe,GAAG,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -89,7 +89,7 @@ function readBasaltConfig(cwd) {
|
|
|
89
89
|
return {};
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
const SCANNABLE_EXT = /\.(?:tsx?|css|html?|webmanifest)$/;
|
|
92
|
+
const SCANNABLE_EXT = /\.(?:tsx?|jsx|astro|vue|css|html?|webmanifest)$/;
|
|
93
93
|
function walkSourceFiles(dir, out = []) {
|
|
94
94
|
let entries;
|
|
95
95
|
try {
|
|
@@ -170,6 +170,18 @@ function descendantProjects(cwd, depth = 2) {
|
|
|
170
170
|
}
|
|
171
171
|
return out;
|
|
172
172
|
}
|
|
173
|
+
function ascendantProject(cwd) {
|
|
174
|
+
const repoRoot = findRepoRoot(cwd);
|
|
175
|
+
if (repoRoot === cwd) return null;
|
|
176
|
+
let current = dirname(cwd);
|
|
177
|
+
for (; ; ) {
|
|
178
|
+
if (hasBasaltProject(current)) return current;
|
|
179
|
+
if (current === repoRoot) return null;
|
|
180
|
+
const parent = dirname(current);
|
|
181
|
+
if (parent === current) return null;
|
|
182
|
+
current = parent;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
173
185
|
function resolveProjectDir(cwd) {
|
|
174
186
|
const override = process.env["BASALT_CWD"];
|
|
175
187
|
if (override !== void 0 && override.length > 0) {
|
|
@@ -183,6 +195,8 @@ function resolveProjectDir(cwd) {
|
|
|
183
195
|
return { dir: candidates[0], relocatedFrom: cwd, ambiguous: null };
|
|
184
196
|
}
|
|
185
197
|
if (candidates.length > 1) return { dir: cwd, relocatedFrom: null, ambiguous: candidates };
|
|
198
|
+
const above = ascendantProject(cwd);
|
|
199
|
+
if (above !== null) return { dir: above, relocatedFrom: cwd, ambiguous: null };
|
|
186
200
|
return { dir: cwd, relocatedFrom: null, ambiguous: null };
|
|
187
201
|
}
|
|
188
202
|
function declaredProfile(cfg, flags) {
|
|
@@ -1883,28 +1897,37 @@ basalt-ui doctor \u2014 ${cwd}
|
|
|
1883
1897
|
`${lefthookGate.file} names no basalt gate and \`lefthook dump\` could not be run here, so this check cannot tell a missing gate from one wired through an \`include:\` or a remote config it never read \u2014 treat it as advisory. If the guard really is unwired, add "extends: [${lefthookCorrect}]" or your own command running \`${basaltBinCommand(install, cwd)} check-theme\`.`
|
|
1884
1898
|
);
|
|
1885
1899
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1900
|
+
if (profile === "framework") {
|
|
1901
|
+
const appDir = findAppPluginDir(cwd, cfg);
|
|
1902
|
+
const iconsIn = appDir ?? cwd;
|
|
1903
|
+
const icons = readAppIconsOption(iconsIn);
|
|
1904
|
+
const here = appDir === null || appDir === cwd ? "" : `${relativePosix(cwd, appDir)}/`;
|
|
1905
|
+
const publicDir = resolve(iconsIn, "public");
|
|
1906
|
+
if (appDir === null && !existsSync(publicDir)) {
|
|
1907
|
+
pass("app icons: no `basaltAppPlugin(` under this project and no public/ \u2014 nothing to check");
|
|
1908
|
+
} else if (icons.kind === "none") {
|
|
1890
1909
|
pass("basaltAppPlugin is configured with no icons \u2014 nothing to check");
|
|
1910
|
+
} else if (!existsSync(publicDir)) {
|
|
1911
|
+
skip(
|
|
1912
|
+
`app icons \u2014 ${here === "" ? "this package" : here.replace(/\/$/, "")} configures basaltAppPlugin, but there is no \`public/\` beside its vite config, so the files it references cannot be located. Use Vite's public-dir convention, or set \`icons: false\` if this app ships none.`
|
|
1913
|
+
);
|
|
1891
1914
|
} else if (icons.kind === "explicit") {
|
|
1892
1915
|
const missing = icons.sources.filter(
|
|
1893
1916
|
(src) => !existsSync(resolve(publicDir, src.replace(/^\//, "")))
|
|
1894
1917
|
);
|
|
1895
1918
|
if (missing.length > 0) {
|
|
1896
1919
|
warn(
|
|
1897
|
-
`basaltAppPlugin declares icon(s) that are not in public/: ${missing.join(", ")} \u2014 the manifest and the head links will point at a 404. Place them, or drop them from the \`icons\` array.`
|
|
1920
|
+
`basaltAppPlugin declares icon(s) that are not in ${here}public/: ${missing.join(", ")} \u2014 the manifest and the head links will point at a 404. Place them, or drop them from the \`icons\` array.`
|
|
1898
1921
|
);
|
|
1899
1922
|
} else {
|
|
1900
1923
|
pass(
|
|
1901
|
-
|
|
1924
|
+
`${here}public/ has all ${icons.sources.length} icon file(s) basaltAppPlugin's \`icons\` option names`
|
|
1902
1925
|
);
|
|
1903
1926
|
}
|
|
1904
1927
|
} else {
|
|
1905
1928
|
const dir = icons.kind === "default" ? icons.dir : null;
|
|
1906
1929
|
const iconRoot = dir === null ? publicDir : resolve(publicDir, dir.replace(/^\//, ""));
|
|
1907
|
-
const shown = dir === null ?
|
|
1930
|
+
const shown = dir === null ? `${here}public/` : `${here}public/${dir.replace(/^\//, "")}`;
|
|
1908
1931
|
const missingIcons = DEFAULT_APP_ICON_FILES.filter((f) => !existsSync(resolve(iconRoot, f)));
|
|
1909
1932
|
if (missingIcons.length > 0) {
|
|
1910
1933
|
warn(
|
|
@@ -2049,6 +2072,31 @@ const VITE_CONFIG_FILES = [
|
|
|
2049
2072
|
"vite.config.mjs",
|
|
2050
2073
|
"vite.config.cjs"
|
|
2051
2074
|
];
|
|
2075
|
+
function findAppPluginDir(cwd, cfg) {
|
|
2076
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2077
|
+
const candidates = [];
|
|
2078
|
+
const add = (dir) => {
|
|
2079
|
+
if (seen.has(dir)) return;
|
|
2080
|
+
seen.add(dir);
|
|
2081
|
+
candidates.push(dir);
|
|
2082
|
+
};
|
|
2083
|
+
add(cwd);
|
|
2084
|
+
for (const root of resolveRoots(cfg)) {
|
|
2085
|
+
let current = resolve(cwd, root);
|
|
2086
|
+
while (current !== cwd && !relative(cwd, current).startsWith("..")) {
|
|
2087
|
+
add(current);
|
|
2088
|
+
const parent = dirname(current);
|
|
2089
|
+
if (parent === current) break;
|
|
2090
|
+
current = parent;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
for (const dir of candidates) {
|
|
2094
|
+
const configPath = VITE_CONFIG_FILES.map((f) => resolve(dir, f)).find((f) => existsSync(f));
|
|
2095
|
+
if (configPath === void 0) continue;
|
|
2096
|
+
if (readIfExists(configPath)?.includes("basaltAppPlugin(") === true) return dir;
|
|
2097
|
+
}
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2052
2100
|
function balancedSpan(text, open) {
|
|
2053
2101
|
const pairs = { "(": ")", "{": "}", "[": "]" };
|
|
2054
2102
|
const closer = pairs[text[open]];
|
|
@@ -2108,12 +2156,36 @@ function generatedHeader(version, command, flags) {
|
|
|
2108
2156
|
/* basalt-ui ${version} \u2014 \`basalt-ui ${invocation}\` */
|
|
2109
2157
|
`;
|
|
2110
2158
|
}
|
|
2111
|
-
|
|
2159
|
+
const PROVENANCE_VERSION = /^(\/\* basalt-ui )(\d+\.\d+\.\d+\S*)( — `basalt-ui (?:tokens:css|fonts:css)[^`]*` \*\/)$/;
|
|
2160
|
+
function provenanceVersion(css) {
|
|
2161
|
+
const match = PROVENANCE_VERSION.exec(css.split("\n")[1] ?? "");
|
|
2162
|
+
return match === null ? null : match[2];
|
|
2163
|
+
}
|
|
2164
|
+
function withoutProvenanceVersion(css) {
|
|
2112
2165
|
const lines = css.split("\n");
|
|
2113
|
-
|
|
2114
|
-
|
|
2166
|
+
const line = lines[1];
|
|
2167
|
+
if (line === void 0) return css;
|
|
2168
|
+
const match = PROVENANCE_VERSION.exec(line);
|
|
2169
|
+
if (match === null) return css;
|
|
2170
|
+
lines[1] = `${match[1]}<version>${match[3]}`;
|
|
2115
2171
|
return lines.join("\n");
|
|
2116
2172
|
}
|
|
2173
|
+
function versionTriple(version) {
|
|
2174
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
2175
|
+
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2176
|
+
}
|
|
2177
|
+
function describeVersionSkew(onDisk, current) {
|
|
2178
|
+
const a = versionTriple(onDisk);
|
|
2179
|
+
const b = versionTriple(current);
|
|
2180
|
+
if (a === null || b === null) return "a different";
|
|
2181
|
+
for (let i = 0; i < 3; i++) {
|
|
2182
|
+
const left = a[i];
|
|
2183
|
+
const right = b[i];
|
|
2184
|
+
if (left < right) return "an older";
|
|
2185
|
+
if (left > right) return "a newer";
|
|
2186
|
+
}
|
|
2187
|
+
return "a different";
|
|
2188
|
+
}
|
|
2117
2189
|
function normalizeColorFunctions(css) {
|
|
2118
2190
|
return css.replace(/\b(rgba?|hsla?)\(([^()]*)\)/g, (_match, fn, args) => {
|
|
2119
2191
|
const parts = args.split(",").map((part) => normalizeCssNumber(part.trim()));
|
|
@@ -2148,15 +2220,27 @@ function emitGeneratedCss(content, flags, cwd, command) {
|
|
|
2148
2220
|
);
|
|
2149
2221
|
return 1;
|
|
2150
2222
|
}
|
|
2151
|
-
if (
|
|
2152
|
-
const
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
);
|
|
2223
|
+
if (withoutProvenanceVersion(onDisk) === withoutProvenanceVersion(content)) {
|
|
2224
|
+
const diskVersion = provenanceVersion(onDisk);
|
|
2225
|
+
const currentVersion = provenanceVersion(content);
|
|
2226
|
+
const skew = diskVersion !== null && currentVersion !== null && diskVersion !== currentVersion ? ` (Its provenance line names basalt-ui ${diskVersion}, ${describeVersionSkew(diskVersion, currentVersion)} release than the ${currentVersion} that just ran \u2014 only that version token is excluded from the comparison. Re-run without --check whenever you want it refreshed.)` : "";
|
|
2227
|
+
console.log(`\u2713 ${command} --check: ${shown} is up to date.${skew}`);
|
|
2156
2228
|
return 0;
|
|
2157
2229
|
}
|
|
2230
|
+
const diskLines = onDisk.split("\n");
|
|
2231
|
+
const wantLines = content.split("\n");
|
|
2232
|
+
const provenanceOnly = diskLines.length === wantLines.length && diskLines.every((line, i) => i === 1 || line === wantLines[i]) && diskLines[1] !== wantLines[1];
|
|
2233
|
+
if (provenanceOnly) {
|
|
2234
|
+
console.error(
|
|
2235
|
+
`\u2716 ${command} --check: ${shown}'s \`@generated\` provenance line does not match the command that produced this content \u2014 the token values agree, line 2 does not.
|
|
2236
|
+
on disk: ${diskLines[1]}
|
|
2237
|
+
emitted: ${wantLines[1]}
|
|
2238
|
+
Line 1 tells the reader to regenerate with line 2, so line 2 is gated: run the emitted invocation without --check and commit the result, or change the flags this check runs with to the ones the file was really built from. Only the version token is exempt.`
|
|
2239
|
+
);
|
|
2240
|
+
return 1;
|
|
2241
|
+
}
|
|
2158
2242
|
console.error(
|
|
2159
|
-
`\u2716 ${command} --check: ${shown} differs from what \`basalt-ui ${command}\` emits today (on disk ${
|
|
2243
|
+
`\u2716 ${command} --check: ${shown} differs from what \`basalt-ui ${command}\` emits today (on disk ${diskLines.length} lines, emitted ${wantLines.length}) \u2014 re-run the same command without --check and commit the result. Only the \`@generated\` provenance line's VERSION token is excluded from this comparison, so a release alone never trips it \u2014 but its flags do.`
|
|
2160
2244
|
);
|
|
2161
2245
|
return 1;
|
|
2162
2246
|
}
|
|
@@ -4,8 +4,12 @@ export type EmptyStateProps = {
|
|
|
4
4
|
icon?: ReactNode;
|
|
5
5
|
/** Head-font title (18px, weight 550, ink). */
|
|
6
6
|
title: string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Muted 15px description, capped at ~360px so it reads as a short explanation. Optional: a
|
|
9
|
+
* compact panel ("No data yet") should not have to invent a second sentence, and five argo
|
|
10
|
+
* features wrapped this component solely to avoid doing so.
|
|
11
|
+
*/
|
|
12
|
+
description?: string;
|
|
9
13
|
/** Optional call-to-action rendered below the description. */
|
|
10
14
|
action?: ReactNode;
|
|
11
15
|
/** `'page'` (default) = generous padding for a full-page state; `'section'` = compact. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empty-state.d.ts","sourceRoot":"","sources":["../../src/dashboard/empty-state.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,MAAM,eAAe,GAAG;IAC5B,uFAAuF;IACvF,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb
|
|
1
|
+
{"version":3,"file":"empty-state.d.ts","sourceRoot":"","sources":["../../src/dashboard/empty-state.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,MAAM,eAAe,GAAG;IAC5B,uFAAuF;IACvF,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B,CAAA;AAED,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,MAAM,EACN,OAAgB,GACjB,EAAE,eAAe,+BA4BjB"}
|
|
@@ -32,7 +32,7 @@ function EmptyState({
|
|
|
32
32
|
children: title
|
|
33
33
|
}
|
|
34
34
|
),
|
|
35
|
-
/* @__PURE__ */ jsx("span", { style: { fontSize: VX.text.md, color: VX.muted, maxWidth: 360 }, children: description }),
|
|
35
|
+
description !== void 0 && /* @__PURE__ */ jsx("span", { style: { fontSize: VX.text.md, color: VX.muted, maxWidth: 360 }, children: description }),
|
|
36
36
|
action
|
|
37
37
|
]
|
|
38
38
|
}
|
|
@@ -2,4 +2,5 @@ export { DeltaBadge, type DeltaBadgeProps } from './delta-badge.js';
|
|
|
2
2
|
export { StatCard, type StatCardProps, type StatCardTone } from './stat-card.js';
|
|
3
3
|
export { EmptyState, type EmptyStateProps } from './empty-state.js';
|
|
4
4
|
export { SettingsSection, type SettingsSectionProps, SettingsRow, type SettingsRowProps, DangerZone, type DangerZoneProps, } from './settings-section.js';
|
|
5
|
+
export { QueryState, type QueryStateProps, type QueryStateLike, type QueryStateVariant, type QueryEmptyCopy, LoadingState, type LoadingStateProps, ErrorState, type ErrorStateProps, } from './query-state.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAChE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAChE,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,EACzB,WAAW,EACX,KAAK,gBAAgB,EACrB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAChE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAChE,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,EACzB,WAAW,EACX,KAAK,gBAAgB,EACrB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,YAAY,EACZ,KAAK,iBAAiB,EACtB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA"}
|
package/dist/dashboard/index.js
CHANGED
|
@@ -6,10 +6,18 @@ import {
|
|
|
6
6
|
SettingsRow,
|
|
7
7
|
DangerZone
|
|
8
8
|
} from "./settings-section.js";
|
|
9
|
+
import {
|
|
10
|
+
QueryState,
|
|
11
|
+
LoadingState,
|
|
12
|
+
ErrorState
|
|
13
|
+
} from "./query-state.js";
|
|
9
14
|
export {
|
|
10
15
|
DangerZone,
|
|
11
16
|
DeltaBadge,
|
|
12
17
|
EmptyState,
|
|
18
|
+
ErrorState,
|
|
19
|
+
LoadingState,
|
|
20
|
+
QueryState,
|
|
13
21
|
SettingsRow,
|
|
14
22
|
SettingsSection,
|
|
15
23
|
StatCard
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** `'page'` = a full route body (generous padding). `'section'` = a card/panel region (compact). */
|
|
3
|
+
export type QueryStateVariant = 'page' | 'section';
|
|
4
|
+
/**
|
|
5
|
+
* The structural subset of a TanStack `UseQueryResult` these components read.
|
|
6
|
+
*
|
|
7
|
+
* Typed as a SUBSET rather than as `UseQueryResult<T>` on purpose: a composed, derived or
|
|
8
|
+
* hand-rolled result must be passable without a cast, and
|
|
9
|
+
* these components stay independent of which `@tanstack/react-query` major a consumer pins. This
|
|
10
|
+
* is NOT a claim that the root barrel is free of that package — `./connectivity` imports
|
|
11
|
+
* `onlineManager` as a value and is re-exported from `src/index.ts`. The cost of the subset is
|
|
12
|
+
* that the compiler stops policing the shape — which is why {@link QueryState} asserts it at
|
|
13
|
+
* runtime.
|
|
14
|
+
*/
|
|
15
|
+
export type QueryStateLike<TData> = {
|
|
16
|
+
data: TData | undefined;
|
|
17
|
+
isError: boolean;
|
|
18
|
+
error: unknown;
|
|
19
|
+
fetchStatus: 'fetching' | 'paused' | 'idle';
|
|
20
|
+
refetch: () => unknown;
|
|
21
|
+
};
|
|
22
|
+
/** Copy for the empty branch. Rendered through `EmptyState`, so it looks like every other one. */
|
|
23
|
+
export type QueryEmptyCopy = {
|
|
24
|
+
title: string;
|
|
25
|
+
/** Optional — a one-word empty panel should not have to invent a sentence. */
|
|
26
|
+
description?: string;
|
|
27
|
+
/** Optional glyph slot, rendered faint. */
|
|
28
|
+
icon?: ReactNode;
|
|
29
|
+
/** Call-to-action below the copy (e.g. "Reset filters"). */
|
|
30
|
+
action?: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
export type LoadingStateProps = {
|
|
33
|
+
variant?: QueryStateVariant;
|
|
34
|
+
/** Accessible name for the spinner. Say what is loading. */
|
|
35
|
+
label?: string;
|
|
36
|
+
};
|
|
37
|
+
/** A spinner on its own — `variant="page"` centres it in a generous block, `'section'` is bare. */
|
|
38
|
+
export declare function LoadingState({ variant, label, }: LoadingStateProps): ReactNode;
|
|
39
|
+
export type ErrorStateProps = {
|
|
40
|
+
/** Whatever was thrown — the raw Eden/fetch envelope is fine, it gets decoded. */
|
|
41
|
+
error: unknown;
|
|
42
|
+
/** Alert heading. Say what failed, e.g. `Could not load images`. */
|
|
43
|
+
title?: string;
|
|
44
|
+
/** Shown only when the server body carries no readable message. */
|
|
45
|
+
fallback?: string;
|
|
46
|
+
onRetry?: () => void;
|
|
47
|
+
/** Disables/spins the retry button while a refetch is in flight. */
|
|
48
|
+
retrying?: boolean;
|
|
49
|
+
variant?: QueryStateVariant;
|
|
50
|
+
/** Extra controls beside Retry (e.g. a "Back to shares" link). */
|
|
51
|
+
action?: ReactNode;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The error branch: the REAL server message (via `toErrorMessage`) plus a retry. Usable on its own,
|
|
55
|
+
* outside a query — an auth gate or a boundary fallback renders it directly.
|
|
56
|
+
*/
|
|
57
|
+
export declare function ErrorState({ error, title, fallback, onRetry, retrying, variant, action, }: ErrorStateProps): ReactNode;
|
|
58
|
+
export type QueryStateProps<TData> = {
|
|
59
|
+
query: QueryStateLike<TData>;
|
|
60
|
+
/** A render function gets `data` narrowed to non-undefined; a plain node also works. */
|
|
61
|
+
children: ReactNode | ((data: TData) => ReactNode);
|
|
62
|
+
/** Omit to render nothing when the result is empty. */
|
|
63
|
+
empty?: QueryEmptyCopy;
|
|
64
|
+
/** Override the default `[]` / `{ data: [] }` emptiness test — `() => false` disables it. */
|
|
65
|
+
isEmpty?: (data: TData) => boolean;
|
|
66
|
+
/** Alert heading on the error branch — say what failed. */
|
|
67
|
+
errorTitle?: string;
|
|
68
|
+
/** Shown only when the server body carries no readable message. */
|
|
69
|
+
errorFallback?: string;
|
|
70
|
+
/** Extra controls beside Retry on the error branch. */
|
|
71
|
+
errorAction?: ReactNode;
|
|
72
|
+
variant?: QueryStateVariant;
|
|
73
|
+
/** Replace the default spinner (e.g. with a skeleton grid). */
|
|
74
|
+
loading?: ReactNode;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Renders loading / error-with-retry / empty / children for a query result, in that order.
|
|
78
|
+
* Branch precedence is the whole point, and it is deliberate:
|
|
79
|
+
*
|
|
80
|
+
* - error AND no cached data → the full error state (the page cannot render)
|
|
81
|
+
* - error WITH cached data → children, plus a compact "showing cached data" banner on top
|
|
82
|
+
* (a background refetch failing must not blank a page that already works)
|
|
83
|
+
* - no data, fetch idle → empty (this is an `enabled: false` query, not a pending one)
|
|
84
|
+
* - no data, fetching → `loading` (or the default spinner)
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* <QueryState
|
|
88
|
+
* query={q}
|
|
89
|
+
* errorTitle="Could not load images"
|
|
90
|
+
* empty={{ title: 'No images match', description: 'Widen the capture dates.' }}
|
|
91
|
+
* >
|
|
92
|
+
* {(data) => <ImageGrid images={data.rows} />}
|
|
93
|
+
* </QueryState>
|
|
94
|
+
*/
|
|
95
|
+
export declare function QueryState<TData>({ query, children, empty, isEmpty, errorTitle, errorFallback, errorAction, variant, loading, }: QueryStateProps<TData>): ReactNode;
|
|
96
|
+
//# sourceMappingURL=query-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-state.d.ts","sourceRoot":"","sources":["../../src/dashboard/query-state.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,oGAAoG;AACpG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAAA;AAElD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI;IAClC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC3C,OAAO,EAAE,MAAM,OAAO,CAAA;CACvB,CAAA;AAED,kGAAkG;AAClG,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB,CAAA;AAID,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,EAC3B,OAAgB,EAChB,KAAiB,GAClB,EAAE,iBAAiB,GAAG,SAAS,CAI/B;AAID,MAAM,MAAM,eAAe,GAAG;IAC5B,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAA;IACd,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,kEAAkE;IAClE,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB,CAAA;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAA8B,EAC9B,QAAgC,EAChC,OAAO,EACP,QAAgB,EAChB,OAAgB,EAChB,MAAM,GACP,EAAE,eAAe,GAAG,SAAS,CAoB7B;AAyCD,MAAM,MAAM,eAAe,CAAC,KAAK,IAAI;IACnC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;IAC5B,wFAAwF;IACxF,QAAQ,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS,CAAC,CAAA;IAClD,uDAAuD;IACvD,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAA;IAClC,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uDAAuD;IACvD,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,EAChC,KAAK,EACL,QAAQ,EACR,KAAK,EACL,OAAO,EACP,UAA6B,EAC7B,aAAqC,EACrC,WAAW,EACX,OAAgB,EAChB,OAAO,GACR,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,SAAS,CAiDpC"}
|