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.
Files changed (63) hide show
  1. package/AGENTS.md +11 -6
  2. package/CHANGELOG.md +21 -0
  3. package/MIGRATING.md +138 -4
  4. package/README.md +123 -32
  5. package/agent/rules/basalt-data.md +104 -12
  6. package/agent/rules/basalt-query.md +57 -0
  7. package/agent/skills/basalt-app/SKILL.md +7 -0
  8. package/dist/cli/index.d.ts +4 -2
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +101 -17
  11. package/dist/dashboard/empty-state.d.ts +6 -2
  12. package/dist/dashboard/empty-state.d.ts.map +1 -1
  13. package/dist/dashboard/empty-state.js +1 -1
  14. package/dist/dashboard/index.d.ts +1 -0
  15. package/dist/dashboard/index.d.ts.map +1 -1
  16. package/dist/dashboard/index.js +8 -0
  17. package/dist/dashboard/query-state.d.ts +96 -0
  18. package/dist/dashboard/query-state.d.ts.map +1 -0
  19. package/dist/dashboard/query-state.js +123 -0
  20. package/dist/dashboard/query-state.test.d.ts +2 -0
  21. package/dist/dashboard/query-state.test.d.ts.map +1 -0
  22. package/dist/data/data-table.d.ts +125 -8
  23. package/dist/data/data-table.d.ts.map +1 -1
  24. package/dist/data/data-table.js +121 -13
  25. package/dist/data/data-table.test.d.ts +2 -0
  26. package/dist/data/data-table.test.d.ts.map +1 -0
  27. package/dist/data/table.d.ts +2 -2
  28. package/dist/data/table.d.ts.map +1 -1
  29. package/dist/guard/index.d.ts.map +1 -1
  30. package/dist/guard/index.js +21 -6
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -1
  34. package/dist/query/error-message.d.ts +41 -0
  35. package/dist/query/error-message.d.ts.map +1 -0
  36. package/dist/query/error-message.js +30 -0
  37. package/dist/query/error-message.test.d.ts +2 -0
  38. package/dist/query/error-message.test.d.ts.map +1 -0
  39. package/dist/query/index.d.ts +1 -19
  40. package/dist/query/index.d.ts.map +1 -1
  41. package/dist/query/index.js +2 -14
  42. package/dist/surfaces.d.ts +1 -1
  43. package/dist/surfaces.js +1 -1
  44. package/llms.txt +3 -3
  45. package/package.json +1 -1
  46. package/src/cli/check-theme.test.ts +34 -0
  47. package/src/cli/index.ts +222 -44
  48. package/src/cli/tokens-css.test.ts +73 -6
  49. package/src/cli/toolchain-wiring.test.ts +146 -0
  50. package/src/dashboard/empty-state.tsx +9 -3
  51. package/src/dashboard/index.ts +11 -0
  52. package/src/dashboard/query-state.test.tsx +152 -0
  53. package/src/dashboard/query-state.tsx +272 -0
  54. package/src/data/data-table.test.tsx +349 -0
  55. package/src/data/data-table.tsx +359 -42
  56. package/src/data/table.ts +8 -1
  57. package/src/guard/check-source.test.ts +141 -0
  58. package/src/guard/index.ts +102 -8
  59. package/src/index.ts +9 -0
  60. package/src/query/error-message.test.ts +67 -0
  61. package/src/query/error-message.ts +70 -0
  62. package/src/query/index.ts +2 -34
  63. 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
- ### Deferred for later versions
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
- The 1.0 table ships sorting only. Natural extensions are **not** in scope yet:
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
- - Filtering (column or global search)
130
- - Pagination (client-side or server-side)
131
- - Row selection (checkboxes)
132
- - Row expansion / sub-rows
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
- Add them when a concrete consumer need arises; keep the API additive. Controlled (fully external)
135
- sorting for `BasaltDataTable` itself is likewise deferred — `onSortingChange` above is the current
136
- sync seam.
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
- grid feature growth (pagination, server-side data, column visibility, filtering) is consumer-pulled
171
- future work, not a reason to bypass the blessed lane by default.
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.
@@ -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`'s (basalt-ui/vite) default icon filenames exist under public/ (only when
458
- * a public/ dir exists at all — skipped otherwise).
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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
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
- const publicDir = resolve(cwd, "public");
1887
- if (profile === "framework" && existsSync(publicDir)) {
1888
- const icons = readAppIconsOption(cwd);
1889
- if (icons.kind === "none") {
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
- `public/ has all ${icons.sources.length} icon file(s) basaltAppPlugin's \`icons\` option names`
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 ? "public/" : `public/${dir.replace(/^\//, "")}`;
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
- function withoutProvenanceLine(css) {
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
- if (lines.length < 2) return css;
2114
- lines[1] = "";
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 (withoutProvenanceLine(onDisk) === withoutProvenanceLine(content)) {
2152
- const staleProvenance = onDisk.split("\n")[1] !== content.split("\n")[1];
2153
- console.log(
2154
- `\u2713 ${command} --check: ${shown} is up to date.` + (staleProvenance ? " (Its provenance line still names an older basalt-ui; that line is deliberately not gated \u2014 re-run without --check whenever you want it refreshed.)" : "")
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 ${onDisk.split("\n").length} lines, emitted ${content.split("\n").length}) \u2014 re-run the same command without --check and commit the result. The \`@generated\` header's provenance line is excluded from this comparison, so a version bump alone never trips it.`
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
- /** Muted 15px description, capped at ~360px so it reads as a short explanation. */
8
- description: string;
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,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAA;IACnB,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,+BA0BjB"}
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"}
@@ -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"}