basalt-ui 1.23.1 → 1.24.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 +14 -0
  3. package/MIGRATING.md +138 -4
  4. package/README.md +123 -32
  5. package/agent/rules/basalt-data.md +45 -13
  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 +61 -4
  23. package/dist/data/data-table.d.ts.map +1 -1
  24. package/dist/data/data-table.js +41 -4
  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 +1 -1
  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 +158 -0
  55. package/src/data/data-table.tsx +123 -5
  56. package/src/data/table.ts +6 -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
@@ -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. One import
103
+ replaced 204 hand-rolled lines across 10 call sites there.
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 (3 of image-share's 10 call sites are), 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"}
@@ -0,0 +1,123 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Alert, Button, Center, Group, Loader, Stack, Text } from "@mantine/core";
3
+ import { toErrorMessage } from "../query/error-message.js";
4
+ import { EmptyState } from "./empty-state.js";
5
+ function LoadingState({
6
+ variant = "page",
7
+ label = "Loading"
8
+ }) {
9
+ const loader = /* @__PURE__ */ jsx(Loader, { size: "sm", "aria-label": label });
10
+ if (variant === "section") return loader;
11
+ return /* @__PURE__ */ jsx(Center, { py: 64, children: loader });
12
+ }
13
+ function ErrorState({
14
+ error,
15
+ title = "Something went wrong",
16
+ fallback = "The request failed.",
17
+ onRetry,
18
+ retrying = false,
19
+ variant = "page",
20
+ action
21
+ }) {
22
+ const alert = /* @__PURE__ */ jsx(Alert, { color: "red", variant: "light", title, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
23
+ /* @__PURE__ */ jsx(Text, { size: "sm", children: toErrorMessage(error, fallback) }),
24
+ (onRetry ?? action) !== void 0 && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
25
+ onRetry && /* @__PURE__ */ jsx(Button, { size: "xs", variant: "default", loading: retrying, onClick: onRetry, children: "Retry" }),
26
+ action
27
+ ] })
28
+ ] }) });
29
+ if (variant === "section") return alert;
30
+ return /* @__PURE__ */ jsx(Stack, { py: "md", children: alert });
31
+ }
32
+ function defaultIsEmpty(data) {
33
+ if (data === null || data === void 0) return true;
34
+ if (Array.isArray(data)) return data.length === 0;
35
+ if (typeof data === "object" && Array.isArray(data.data)) {
36
+ return data.data.length === 0;
37
+ }
38
+ return false;
39
+ }
40
+ const FETCH_STATUSES = /* @__PURE__ */ new Set(["fetching", "paused", "idle"]);
41
+ function assertQueryStateLike(query) {
42
+ const bad = (detail) => {
43
+ throw new Error(
44
+ `QueryState: \`query\` ${detail}. It must carry { data, isError, error, fetchStatus, refetch } \u2014 a TanStack UseQueryResult does; a hand-composed one must spell every field, because a missing branch flag renders a false claim about the data instead of an error.`
45
+ );
46
+ };
47
+ if (query === null || typeof query !== "object")
48
+ bad(`is ${query === null ? "null" : typeof query}`);
49
+ const q = query;
50
+ if (!("data" in q)) bad("has no `data` key");
51
+ if (typeof q["isError"] !== "boolean") bad("has no boolean `isError`");
52
+ if (typeof q["fetchStatus"] !== "string" || !FETCH_STATUSES.has(q["fetchStatus"]))
53
+ bad(`has fetchStatus=${JSON.stringify(q["fetchStatus"])}, not 'fetching' | 'paused' | 'idle'`);
54
+ if (typeof q["refetch"] !== "function") bad("has no `refetch()`");
55
+ }
56
+ function QueryState({
57
+ query,
58
+ children,
59
+ empty,
60
+ isEmpty,
61
+ errorTitle = "Could not load",
62
+ errorFallback = "The request failed.",
63
+ errorAction,
64
+ variant = "page",
65
+ loading
66
+ }) {
67
+ assertQueryStateLike(query);
68
+ const { data, isError, error, fetchStatus } = query;
69
+ const retrying = fetchStatus === "fetching";
70
+ const retry = () => void query.refetch();
71
+ if (isError && data === void 0) {
72
+ return /* @__PURE__ */ jsx(
73
+ ErrorState,
74
+ {
75
+ error,
76
+ title: errorTitle,
77
+ fallback: errorFallback,
78
+ onRetry: retry,
79
+ retrying,
80
+ variant,
81
+ ...errorAction !== void 0 && { action: errorAction }
82
+ }
83
+ );
84
+ }
85
+ if (data === void 0) {
86
+ if (fetchStatus === "idle" && !isError) return empty ? renderEmpty(empty, variant) : null;
87
+ return loading ?? /* @__PURE__ */ jsx(LoadingState, { variant });
88
+ }
89
+ const emptyNow = isEmpty ? isEmpty(data) : defaultIsEmpty(data);
90
+ const body = emptyNow ? empty ? renderEmpty(empty, variant) : null : typeof children === "function" ? children(data) : children;
91
+ if (!isError) return body;
92
+ return /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
93
+ /* @__PURE__ */ jsx(
94
+ ErrorState,
95
+ {
96
+ error,
97
+ title: "Showing cached data",
98
+ fallback: "The last refresh failed.",
99
+ onRetry: retry,
100
+ retrying,
101
+ variant: "section"
102
+ }
103
+ ),
104
+ body
105
+ ] });
106
+ }
107
+ function renderEmpty(empty, variant) {
108
+ return /* @__PURE__ */ jsx(
109
+ EmptyState,
110
+ {
111
+ title: empty.title,
112
+ variant,
113
+ ...empty.description !== void 0 && { description: empty.description },
114
+ ...empty.icon !== void 0 && { icon: empty.icon },
115
+ ...empty.action !== void 0 && { action: empty.action }
116
+ }
117
+ );
118
+ }
119
+ export {
120
+ ErrorState,
121
+ LoadingState,
122
+ QueryState
123
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=query-state.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-state.test.d.ts","sourceRoot":"","sources":["../../src/dashboard/query-state.test.tsx"],"names":[],"mappings":""}