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.
- package/AGENTS.md +11 -6
- package/CHANGELOG.md +14 -0
- package/MIGRATING.md +138 -4
- package/README.md +123 -32
- package/agent/rules/basalt-data.md +45 -13
- 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 +61 -4
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +41 -4
- 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 +1 -1
- 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 +158 -0
- package/src/data/data-table.tsx +123 -5
- package/src/data/table.ts +6 -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
|
@@ -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.
|
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 (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 @@
|
|
|
1
|
+
{"version":3,"file":"query-state.test.d.ts","sourceRoot":"","sources":["../../src/dashboard/query-state.test.tsx"],"names":[],"mappings":""}
|