@squadbase/vantage 0.1.0 → 0.1.1
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/README.md +4 -1
- package/dist/{chunk-PRFBSQA4.js → chunk-5L2MH2NG.js} +9 -4
- package/dist/chunk-5L2MH2NG.js.map +1 -0
- package/dist/chunk-7IPAXPPY.js +51 -0
- package/dist/chunk-7IPAXPPY.js.map +1 -0
- package/dist/chunk-DTDVSFRY.js +29 -0
- package/dist/chunk-DTDVSFRY.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +23 -2
- package/dist/components/index.js +115 -1
- package/dist/components/index.js.map +1 -1
- package/dist/{define-page-B6y9TOfZ.d.ts → define-page-BfhrK99G.d.ts} +5 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query/index.d.ts +69 -2
- package/dist/query/index.js +85 -2
- package/dist/query/index.js.map +1 -1
- package/dist/router/index.d.ts +86 -1
- package/dist/router/index.js +71 -7
- package/dist/router/index.js.map +1 -1
- package/dist/vite/index.js +1 -1
- package/docs/en/components.md +2 -0
- package/docs/en/data-fetching.md +77 -13
- package/docs/en/pages-and-metadata.md +6 -3
- package/docs/en/parts/placeholder.md +38 -0
- package/docs/en/parts/sparkline.md +47 -0
- package/docs/en/routing.md +82 -0
- package/docs/index.json +50 -10
- package/docs/ja/components.md +2 -0
- package/docs/ja/data-fetching.md +76 -12
- package/docs/ja/pages-and-metadata.md +6 -3
- package/docs/ja/parts/placeholder.md +37 -0
- package/docs/ja/parts/sparkline.md +47 -0
- package/docs/ja/routing.md +80 -0
- package/package.json +1 -1
- package/skills/vantage-add-feature/SKILL.md +14 -11
- package/skills/vantage-app/SKILL.md +54 -21
- package/skills/vantage-pitfalls/SKILL.md +26 -2
- package/templates/AGENTS.md +62 -17
- package/dist/chunk-ATYZ45XL.js +0 -19
- package/dist/chunk-ATYZ45XL.js.map +0 -1
- package/dist/chunk-PRFBSQA4.js.map +0 -1
|
@@ -9,6 +9,11 @@ interface PageConfig {
|
|
|
9
9
|
title?: string;
|
|
10
10
|
/** Description metadata for the page. */
|
|
11
11
|
description?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Short label for navigation built from `useRoutes()`. Defaults to `title`,
|
|
14
|
+
* which is often too long for a nav ("Overview · Acme Analytics").
|
|
15
|
+
*/
|
|
16
|
+
navLabel?: string;
|
|
12
17
|
/**
|
|
13
18
|
* Component rendered while the route is loading.
|
|
14
19
|
* (Reserved — not yet applied to Vantage's code-split routes in the prototype.)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { P as PageComponent, a as PageConfig, d as definePage } from './define-page-
|
|
1
|
+
export { P as PageComponent, a as PageConfig, d as definePage } from './define-page-BfhrK99G.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -10,6 +10,6 @@ import 'react';
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/** Framework version, kept in sync with package.json at build time. */
|
|
13
|
-
declare const VERSION = "0.1.
|
|
13
|
+
declare const VERSION = "0.1.1";
|
|
14
14
|
|
|
15
15
|
export { VERSION };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/define-page.ts","../src/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/runtime/define-page.ts","../src/index.ts"],"names":[],"mappings":";;;AAyCO,SAAS,WAAW,MAAA,EAAgC;AACzD,EAAA,OAAO,MAAA;AACT;;;AChCO,IAAM,OAAA,GAAU","file":"index.js","sourcesContent":["import type { ComponentType, ReactNode } from \"react\"\n\n/**\n * Optional per-page configuration. The only required part of a page module is\n * its default-exported component; `definePage` covers everything else.\n */\nexport interface PageConfig {\n /** Document title applied when this route is active. */\n title?: string\n /** Description metadata for the page. */\n description?: string\n /**\n * Short label for navigation built from `useRoutes()`. Defaults to `title`,\n * which is often too long for a nav (\"Overview · Acme Analytics\").\n */\n navLabel?: string\n /**\n * Component rendered while the route is loading.\n * (Reserved — not yet applied to Vantage's code-split routes in the prototype.)\n */\n pendingComponent?: ComponentType\n /**\n * Component rendered when the route throws.\n * (Reserved — not yet applied to Vantage's code-split routes in the prototype.)\n */\n errorComponent?: ComponentType<{ error: unknown }>\n /**\n * Validate/transform the URL search params for this route.\n * (Reserved — not yet applied to Vantage's code-split routes in the prototype.)\n */\n validateSearch?: (search: Record<string, unknown>) => Record<string, unknown>\n}\n\n/**\n * Declare optional page configuration.\n *\n * ```tsx\n * export const page = definePage({ title: \"Monthly Analysis\" })\n * export default function MonthlyAnalysis() { return <div/> }\n * ```\n */\nexport function definePage(config: PageConfig): PageConfig {\n return config\n}\n\n/** Convenience type for a Vantage page component. */\nexport type PageComponent = ComponentType & { (): ReactNode }\n","/**\n * `@squadbase/vantage` — public runtime surface.\n *\n * The only import most pages need. Server state lives in\n * `@squadbase/vantage/query`, navigation in `@squadbase/vantage/router`,\n * UI in `@squadbase/vantage/ui`, and API types in `@squadbase/vantage/server`.\n */\nexport { definePage } from \"./runtime/define-page.js\"\nexport type { PageConfig, PageComponent } from \"./runtime/define-page.js\"\n\n/** Framework version, kept in sync with package.json at build time. */\nexport const VERSION = \"0.1.1\"\n"]}
|
package/dist/query/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QueryClient } from '@tanstack/react-query';
|
|
1
|
+
import { QueryClient, UseMutationOptions, UseQueryOptions, QueryKey, UseMutationResult, UseQueryResult } from '@tanstack/react-query';
|
|
2
2
|
export { QueryClient, QueryKey, UseInfiniteQueryOptions, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult, keepPreviousData, useInfiniteQuery, useIsFetching, useIsMutating, useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -33,5 +33,72 @@ declare function apiUrl(path: string): string;
|
|
|
33
33
|
* existing `queryFn` style.
|
|
34
34
|
*/
|
|
35
35
|
declare function apiFetch(path: string, init?: RequestInit): Promise<Response>;
|
|
36
|
+
/** Query-string values accepted by the API helpers. */
|
|
37
|
+
type ApiSearch = Record<string, string | number | boolean | null | undefined | (string | number)[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Append a query string to an API path. `undefined`/`null` entries are dropped
|
|
40
|
+
* so an unset filter leaves the URL — and the query key — unchanged. Array
|
|
41
|
+
* values repeat the key, matching `URLSearchParams.getAll` on the server.
|
|
42
|
+
*/
|
|
43
|
+
declare function apiPath(path: string, search?: ApiSearch): string;
|
|
44
|
+
/**
|
|
45
|
+
* A non-2xx response from `server/api`. `message` is the server's `HttpError`
|
|
46
|
+
* message when there was one; anything else surfaces as a generic status text,
|
|
47
|
+
* mirroring what the server chose to disclose.
|
|
48
|
+
*/
|
|
49
|
+
declare class ApiError extends Error {
|
|
50
|
+
/** HTTP status of the response. */
|
|
51
|
+
readonly status: number;
|
|
52
|
+
/** Parsed response body, when there was one. */
|
|
53
|
+
readonly body: unknown;
|
|
54
|
+
/** `x-vantage-request-id`, to correlate with the server's terminal log. */
|
|
55
|
+
readonly requestId: string | undefined;
|
|
56
|
+
constructor(message: string, status: number, body: unknown, requestId?: string);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Fetch an API route and parse its JSON, throwing `ApiError` on a non-2xx.
|
|
60
|
+
* This is what the API hooks call; use it directly in a loader or an event
|
|
61
|
+
* handler where a hook doesn't fit.
|
|
62
|
+
*/
|
|
63
|
+
declare function apiJson<T = unknown>(path: string, init?: RequestInit): Promise<T>;
|
|
64
|
+
|
|
65
|
+
interface ApiQueryOptions<TData> extends Omit<UseQueryOptions<TData, ApiError>, "queryKey" | "queryFn"> {
|
|
66
|
+
/** Query-string values; `undefined` entries are omitted. */
|
|
67
|
+
search?: ApiSearch;
|
|
68
|
+
/** Extra `fetch` init (headers, credentials, …). */
|
|
69
|
+
init?: RequestInit;
|
|
70
|
+
/** Override the derived `["api", url]` key. */
|
|
71
|
+
queryKey?: QueryKey;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* GET an API route.
|
|
75
|
+
*
|
|
76
|
+
* ```tsx
|
|
77
|
+
* const { data, isPending, error } = useApiQuery<Monthly>("/api/monthly-analysis")
|
|
78
|
+
* const filtered = useApiQuery<Row[]>("/api/customers", { search: { segment } })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* The key defaults to `["api", <url with query string>]`, so changing `search`
|
|
82
|
+
* refetches and `invalidateQueries({ queryKey: ["api"] })` clears everything.
|
|
83
|
+
*/
|
|
84
|
+
declare function useApiQuery<TData = unknown>(path: string, options?: ApiQueryOptions<TData>): UseQueryResult<TData, ApiError>;
|
|
85
|
+
interface ApiMutationOptions<TData, TVariables> extends Omit<UseMutationOptions<TData, ApiError, TVariables>, "mutationFn"> {
|
|
86
|
+
/** HTTP method (default `"POST"`). */
|
|
87
|
+
method?: "POST" | "PUT" | "PATCH" | "DELETE";
|
|
88
|
+
/** Extra `fetch` init (headers, credentials, …). */
|
|
89
|
+
init?: RequestInit;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write to an API route. The mutation variables are sent as the JSON body.
|
|
93
|
+
*
|
|
94
|
+
* ```tsx
|
|
95
|
+
* const save = useApiMutation<Customer, { id: string; name: string }>(
|
|
96
|
+
* (vars) => `/api/customers/${vars.id}`,
|
|
97
|
+
* { method: "PUT" },
|
|
98
|
+
* )
|
|
99
|
+
* save.mutate({ id: "1", name: "Northwind" })
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare function useApiMutation<TData = unknown, TVariables = void>(path: string | ((variables: TVariables) => string), options?: ApiMutationOptions<TData, TVariables>): UseMutationResult<TData, ApiError, TVariables>;
|
|
36
103
|
|
|
37
|
-
export { apiBaseUrl, apiFetch, apiUrl, createQueryClient };
|
|
104
|
+
export { ApiError, type ApiMutationOptions, type ApiQueryOptions, type ApiSearch, apiBaseUrl, apiFetch, apiJson, apiPath, apiUrl, createQueryClient, useApiMutation, useApiQuery };
|
package/dist/query/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { createQueryClient } from '../chunk-
|
|
1
|
+
export { createQueryClient } from '../chunk-DTDVSFRY.js';
|
|
2
|
+
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
2
3
|
export { QueryClient, keepPreviousData, useInfiniteQuery, useIsFetching, useIsMutating, useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
4
|
|
|
4
5
|
// @squadbase/vantage — generated build. Do not edit.
|
|
@@ -16,7 +17,89 @@ function apiUrl(path) {
|
|
|
16
17
|
function apiFetch(path, init) {
|
|
17
18
|
return fetch(apiUrl(path), init);
|
|
18
19
|
}
|
|
20
|
+
function apiPath(path, search) {
|
|
21
|
+
if (!search) return path;
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
for (const key of Object.keys(search).sort()) {
|
|
24
|
+
const value = search[key];
|
|
25
|
+
if (value === void 0 || value === null) continue;
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
for (const item of value) params.append(key, String(item));
|
|
28
|
+
} else {
|
|
29
|
+
params.append(key, String(value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const qs = params.toString();
|
|
33
|
+
if (!qs) return path;
|
|
34
|
+
return path + (path.includes("?") ? "&" : "?") + qs;
|
|
35
|
+
}
|
|
36
|
+
var ApiError = class extends Error {
|
|
37
|
+
/** HTTP status of the response. */
|
|
38
|
+
status;
|
|
39
|
+
/** Parsed response body, when there was one. */
|
|
40
|
+
body;
|
|
41
|
+
/** `x-vantage-request-id`, to correlate with the server's terminal log. */
|
|
42
|
+
requestId;
|
|
43
|
+
constructor(message, status, body, requestId) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "ApiError";
|
|
46
|
+
this.status = status;
|
|
47
|
+
this.body = body;
|
|
48
|
+
this.requestId = requestId;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
async function apiJson(path, init) {
|
|
52
|
+
const res = await apiFetch(path, init);
|
|
53
|
+
const requestId = res.headers.get("x-vantage-request-id") ?? void 0;
|
|
54
|
+
const text = await res.text();
|
|
55
|
+
let body;
|
|
56
|
+
if (text) {
|
|
57
|
+
try {
|
|
58
|
+
body = JSON.parse(text);
|
|
59
|
+
} catch {
|
|
60
|
+
body = text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const serverMessage = typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : void 0;
|
|
65
|
+
throw new ApiError(
|
|
66
|
+
serverMessage ?? `${res.status} ${res.statusText || "Request failed"}`,
|
|
67
|
+
res.status,
|
|
68
|
+
body,
|
|
69
|
+
requestId
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return body;
|
|
73
|
+
}
|
|
74
|
+
var API_KEY = "api";
|
|
75
|
+
function useApiQuery(path, options = {}) {
|
|
76
|
+
const { search, init, queryKey, ...rest } = options;
|
|
77
|
+
const url = apiPath(path, search);
|
|
78
|
+
return useQuery({
|
|
79
|
+
queryKey: queryKey ?? [API_KEY, url],
|
|
80
|
+
// The signal comes from TanStack so an unmounted/refetched query aborts;
|
|
81
|
+
// a caller-supplied signal wins.
|
|
82
|
+
queryFn: ({ signal }) => apiJson(url, { ...init, signal: init?.signal ?? signal }),
|
|
83
|
+
...rest
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function useApiMutation(path, options = {}) {
|
|
87
|
+
const { method = "POST", init, ...rest } = options;
|
|
88
|
+
return useMutation({
|
|
89
|
+
mutationFn: (variables) => {
|
|
90
|
+
const url = typeof path === "function" ? path(variables) : path;
|
|
91
|
+
const hasBody = variables !== void 0 && variables !== null;
|
|
92
|
+
return apiJson(url, {
|
|
93
|
+
method,
|
|
94
|
+
...init,
|
|
95
|
+
headers: hasBody ? { "content-type": "application/json", ...init?.headers } : init?.headers,
|
|
96
|
+
...hasBody ? { body: JSON.stringify(variables) } : {}
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
...rest
|
|
100
|
+
});
|
|
101
|
+
}
|
|
19
102
|
|
|
20
|
-
export { apiBaseUrl, apiFetch, apiUrl };
|
|
103
|
+
export { ApiError, apiBaseUrl, apiFetch, apiJson, apiPath, apiUrl, useApiMutation, useApiQuery };
|
|
21
104
|
//# sourceMappingURL=index.js.map
|
|
22
105
|
//# sourceMappingURL=index.js.map
|
package/dist/query/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/query/api.ts"],"names":[],"mappings":";;;;;;AAaO,SAAS,UAAA,GAAqB;AAEnC,EAAA,MAAM,MAAO,MAAA,CAAA,IAAA,CAA6D,GAAA;AAC1E,EAAA,MAAM,GAAA,GAAM,KAAK,mBAAA,IAAuB,EAAA;AACxC,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAC/B;AAOO,SAAS,OAAO,IAAA,EAAsB;AAC3C,EAAA,MAAM,MAAM,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,GAAI,IAAA,GAAO,IAAI,IAAI,CAAA,CAAA;AAClD,EAAA,OAAO,YAAW,GAAI,GAAA;AACxB;AAOO,SAAS,QAAA,CAAS,MAAc,IAAA,EAAuC;AAC5E,EAAA,OAAO,KAAA,CAAM,MAAA,CAAO,IAAI,CAAA,EAAG,IAAI,CAAA;AACjC","file":"index.js","sourcesContent":["/**\n * The one seam for calling the API from client code. All `/api/*` requests go\n * through here so a split deployment (client on S3/CloudFront, server on Lambda)\n * can point the client at a different origin without touching page code.\n *\n * The base is read from `import.meta.env.PUBLIC_API_BASE_URL` — a `PUBLIC_`\n * prefixed var so it is inlined into the client bundle (see envPrefix in the\n * Vite plugin). It can be set in `.env` or injected at build time via\n * `vantage build --api-base-url <url>`. When unset it is empty, so requests stay\n * same-origin relative paths — identical to hand-written `fetch(\"/api/...\")`.\n */\n\n/** The configured API base URL (no trailing slash), or \"\" for same-origin. */\nexport function apiBaseUrl(): string {\n // `import.meta.env` is typed in the generated .vantage/types, not here.\n const env = (import.meta as { env?: Record<string, string | undefined> }).env\n const raw = env?.PUBLIC_API_BASE_URL ?? \"\"\n return raw.replace(/\\/+$/, \"\")\n}\n\n/**\n * Build a full API URL. Pass the path exactly as it maps to `server/api/**`,\n * including the `/api` prefix — e.g. `apiUrl(\"/api/customers\")`. With no base\n * configured this returns the relative path unchanged.\n */\nexport function apiUrl(path: string): string {\n const rel = path.startsWith(\"/\") ? path : `/${path}`\n return apiBaseUrl() + rel\n}\n\n/**\n * Thin `fetch` wrapper that resolves the path against the API base. Returns the\n * raw `Response` — parsing and error handling stay with the caller, matching the\n * existing `queryFn` style.\n */\nexport function apiFetch(path: string, init?: RequestInit): Promise<Response> {\n return fetch(apiUrl(path), init)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/query/api.ts","../../src/query/hooks.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The one seam for calling the API from client code. All `/api/*` requests go\n * through here so a split deployment (client on S3/CloudFront, server on Lambda)\n * can point the client at a different origin without touching page code.\n *\n * The base is read from `import.meta.env.PUBLIC_API_BASE_URL` — a `PUBLIC_`\n * prefixed var so it is inlined into the client bundle (see envPrefix in the\n * Vite plugin). It can be set in `.env` or injected at build time via\n * `vantage build --api-base-url <url>`. When unset it is empty, so requests stay\n * same-origin relative paths — identical to hand-written `fetch(\"/api/...\")`.\n */\n\n/** The configured API base URL (no trailing slash), or \"\" for same-origin. */\nexport function apiBaseUrl(): string {\n // `import.meta.env` is typed in the generated .vantage/types, not here.\n const env = (import.meta as { env?: Record<string, string | undefined> }).env\n const raw = env?.PUBLIC_API_BASE_URL ?? \"\"\n return raw.replace(/\\/+$/, \"\")\n}\n\n/**\n * Build a full API URL. Pass the path exactly as it maps to `server/api/**`,\n * including the `/api` prefix — e.g. `apiUrl(\"/api/customers\")`. With no base\n * configured this returns the relative path unchanged.\n */\nexport function apiUrl(path: string): string {\n const rel = path.startsWith(\"/\") ? path : `/${path}`\n return apiBaseUrl() + rel\n}\n\n/**\n * Thin `fetch` wrapper that resolves the path against the API base. Returns the\n * raw `Response` — parsing and error handling stay with the caller, matching the\n * existing `queryFn` style.\n */\nexport function apiFetch(path: string, init?: RequestInit): Promise<Response> {\n return fetch(apiUrl(path), init)\n}\n\n/** Query-string values accepted by the API helpers. */\nexport type ApiSearch = Record<\n string,\n string | number | boolean | null | undefined | (string | number)[]\n>\n\n/**\n * Append a query string to an API path. `undefined`/`null` entries are dropped\n * so an unset filter leaves the URL — and the query key — unchanged. Array\n * values repeat the key, matching `URLSearchParams.getAll` on the server.\n */\nexport function apiPath(path: string, search?: ApiSearch): string {\n if (!search) return path\n const params = new URLSearchParams()\n // Sorted so two calls with the same filters produce the same query key.\n for (const key of Object.keys(search).sort()) {\n const value = search[key]\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, String(item))\n } else {\n params.append(key, String(value))\n }\n }\n const qs = params.toString()\n if (!qs) return path\n return path + (path.includes(\"?\") ? \"&\" : \"?\") + qs\n}\n\n/**\n * A non-2xx response from `server/api`. `message` is the server's `HttpError`\n * message when there was one; anything else surfaces as a generic status text,\n * mirroring what the server chose to disclose.\n */\nexport class ApiError extends Error {\n /** HTTP status of the response. */\n readonly status: number\n /** Parsed response body, when there was one. */\n readonly body: unknown\n /** `x-vantage-request-id`, to correlate with the server's terminal log. */\n readonly requestId: string | undefined\n\n constructor(message: string, status: number, body: unknown, requestId?: string) {\n super(message)\n this.name = \"ApiError\"\n this.status = status\n this.body = body\n this.requestId = requestId\n }\n}\n\n/**\n * Fetch an API route and parse its JSON, throwing `ApiError` on a non-2xx.\n * This is what the API hooks call; use it directly in a loader or an event\n * handler where a hook doesn't fit.\n */\nexport async function apiJson<T = unknown>(path: string, init?: RequestInit): Promise<T> {\n const res = await apiFetch(path, init)\n const requestId = res.headers.get(\"x-vantage-request-id\") ?? undefined\n\n // Read as text first: an error response may be HTML from a proxy, and a 204\n // has no body at all — neither should throw a JSON parse error at the caller.\n const text = await res.text()\n let body: unknown\n if (text) {\n try {\n body = JSON.parse(text)\n } catch {\n body = text\n }\n }\n\n if (!res.ok) {\n const serverMessage =\n typeof body === \"object\" && body !== null && typeof (body as { error?: unknown }).error === \"string\"\n ? (body as { error: string }).error\n : undefined\n throw new ApiError(\n serverMessage ?? `${res.status} ${res.statusText || \"Request failed\"}`,\n res.status,\n body,\n requestId,\n )\n }\n\n return body as T\n}\n","/**\n * Hooks for the app's own `server/api` routes. They are `useQuery`/`useMutation`\n * with the parts every page was repeating — base URL, JSON parsing, non-2xx to\n * `ApiError`, a query key derived from the URL — moved into the framework.\n * Everything else about TanStack Query stays available through the options.\n */\nimport { useMutation, useQuery } from \"@tanstack/react-query\"\nimport type {\n QueryKey,\n UseMutationOptions,\n UseMutationResult,\n UseQueryOptions,\n UseQueryResult,\n} from \"@tanstack/react-query\"\nimport { apiJson, apiPath, type ApiError, type ApiSearch } from \"./api.js\"\n\n/** Query-key namespace, so `invalidateQueries({ queryKey: [\"api\"] })` works. */\nconst API_KEY = \"api\"\n\nexport interface ApiQueryOptions<TData>\n extends Omit<UseQueryOptions<TData, ApiError>, \"queryKey\" | \"queryFn\"> {\n /** Query-string values; `undefined` entries are omitted. */\n search?: ApiSearch\n /** Extra `fetch` init (headers, credentials, …). */\n init?: RequestInit\n /** Override the derived `[\"api\", url]` key. */\n queryKey?: QueryKey\n}\n\n/**\n * GET an API route.\n *\n * ```tsx\n * const { data, isPending, error } = useApiQuery<Monthly>(\"/api/monthly-analysis\")\n * const filtered = useApiQuery<Row[]>(\"/api/customers\", { search: { segment } })\n * ```\n *\n * The key defaults to `[\"api\", <url with query string>]`, so changing `search`\n * refetches and `invalidateQueries({ queryKey: [\"api\"] })` clears everything.\n */\nexport function useApiQuery<TData = unknown>(\n path: string,\n options: ApiQueryOptions<TData> = {},\n): UseQueryResult<TData, ApiError> {\n const { search, init, queryKey, ...rest } = options\n const url = apiPath(path, search)\n return useQuery<TData, ApiError>({\n queryKey: queryKey ?? [API_KEY, url],\n // The signal comes from TanStack so an unmounted/refetched query aborts;\n // a caller-supplied signal wins.\n queryFn: ({ signal }) => apiJson<TData>(url, { ...init, signal: init?.signal ?? signal }),\n ...rest,\n })\n}\n\nexport interface ApiMutationOptions<TData, TVariables>\n extends Omit<UseMutationOptions<TData, ApiError, TVariables>, \"mutationFn\"> {\n /** HTTP method (default `\"POST\"`). */\n method?: \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"\n /** Extra `fetch` init (headers, credentials, …). */\n init?: RequestInit\n}\n\n/**\n * Write to an API route. The mutation variables are sent as the JSON body.\n *\n * ```tsx\n * const save = useApiMutation<Customer, { id: string; name: string }>(\n * (vars) => `/api/customers/${vars.id}`,\n * { method: \"PUT\" },\n * )\n * save.mutate({ id: \"1\", name: \"Northwind\" })\n * ```\n */\nexport function useApiMutation<TData = unknown, TVariables = void>(\n path: string | ((variables: TVariables) => string),\n options: ApiMutationOptions<TData, TVariables> = {},\n): UseMutationResult<TData, ApiError, TVariables> {\n const { method = \"POST\", init, ...rest } = options\n return useMutation<TData, ApiError, TVariables>({\n mutationFn: (variables: TVariables) => {\n const url = typeof path === \"function\" ? path(variables) : path\n const hasBody = variables !== undefined && variables !== null\n return apiJson<TData>(url, {\n method,\n ...init,\n headers: hasBody\n ? { \"content-type\": \"application/json\", ...(init?.headers as Record<string, string>) }\n : init?.headers,\n ...(hasBody ? { body: JSON.stringify(variables) } : {}),\n })\n },\n ...rest,\n })\n}\n"]}
|
package/dist/router/index.d.ts
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
export { Outlet, notFound, redirect, useRouterState } from '@tanstack/react-router';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The app's route list, published once by the client bootstrap and read by the
|
|
6
|
+
* route hooks. A module-level store rather than React context: the list is
|
|
7
|
+
* fixed for the lifetime of the bundle — the generated route tree is imported
|
|
8
|
+
* by `.vantage/client-entry.tsx`, which accepts no HMR update, so adding a page
|
|
9
|
+
* full-reloads the dev server — and pages can read it without a provider.
|
|
10
|
+
*
|
|
11
|
+
* `client/bootstrap` and `router/` are separate published subpaths but tsup
|
|
12
|
+
* hoists this module into a shared chunk, so both see the same instance.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** One page route, as described by the generated manifest. */
|
|
16
|
+
interface RouteInfo {
|
|
17
|
+
/** Display path with `:param` placeholders — e.g. `/sales/:customerId`. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** The same path in `Link`'s token form — e.g. `/sales/$customerId`. */
|
|
20
|
+
to: string;
|
|
21
|
+
/** Dynamic parameter names in order; a catch-all is reported as `_splat`. */
|
|
22
|
+
params: string[];
|
|
23
|
+
/** True when the route carries at least one dynamic parameter. */
|
|
24
|
+
dynamic: boolean;
|
|
25
|
+
/** True for a directory's index route (`index.tsx`). */
|
|
26
|
+
index: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Ready-to-render nav label: `navLabel`, else `title`, else the path. Always
|
|
29
|
+
* a string, so a nav needs no fallback of its own.
|
|
30
|
+
*/
|
|
31
|
+
label: string;
|
|
32
|
+
/** `definePage({ title })`, extracted statically at generate time. */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** `definePage({ description })`. */
|
|
35
|
+
description?: string;
|
|
36
|
+
/** `definePage({ navLabel })`. */
|
|
37
|
+
navLabel?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every page route in the app, in the framework's scan order (shallow before
|
|
42
|
+
* deep, static before dynamic, alphabetical) — the order a nav wants.
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* // Dynamic routes have no single URL, so a nav lists only static ones.
|
|
46
|
+
* const routes = useRoutes().filter((route) => !route.dynamic)
|
|
47
|
+
* routes.map((route) => <Link key={route.path} to={route.to}>{route.label}</Link>)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function useRoutes(): readonly RouteInfo[];
|
|
51
|
+
/**
|
|
52
|
+
* The route currently rendered, or `undefined` on the 404 route (no page
|
|
53
|
+
* matched). Use it for breadcrumbs or a page header that mirrors `definePage`.
|
|
54
|
+
*/
|
|
55
|
+
declare function useCurrentRoute(): RouteInfo | undefined;
|
|
56
|
+
|
|
57
|
+
interface SearchStateOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Replace the current history entry instead of pushing a new one.
|
|
60
|
+
* Defaults to `true` so dragging a slider doesn't fill the back button.
|
|
61
|
+
*/
|
|
62
|
+
replace?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A string search param, as `useState`. The value is always a string —
|
|
66
|
+
* TanStack parses `?year=2024` into a number, and this hook normalizes it back.
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const [region, setRegion] = useSearchParam("region", "all")
|
|
70
|
+
* <SegmentedControl value={region} onValueChange={setRegion} options={REGIONS} />
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Writing the default value (or `null`) drops the key from the URL.
|
|
74
|
+
*/
|
|
75
|
+
declare function useSearchParam(key: string, defaultValue?: string, options?: SearchStateOptions): [string, (value: string | null) => void];
|
|
76
|
+
/**
|
|
77
|
+
* Any JSON-serializable search param, as `useState` — arrays for a multi-select,
|
|
78
|
+
* an object for a whole filter bar. Supports a functional update.
|
|
79
|
+
*
|
|
80
|
+
* ```tsx
|
|
81
|
+
* const [segments, setSegments] = useSearchState<string[]>("segments", [])
|
|
82
|
+
* setSegments((prev) => [...prev, "enterprise"])
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* Writing a value deep-equal to the default drops the key from the URL.
|
|
86
|
+
*/
|
|
87
|
+
declare function useSearchState<T>(key: string, defaultValue: T, options?: SearchStateOptions): [T, (value: T | ((prev: T) => T)) => void];
|
|
88
|
+
|
|
4
89
|
/**
|
|
5
90
|
* Vantage's thin router API. Users depend on `@squadbase/vantage/router`
|
|
6
91
|
* instead of `@tanstack/react-router`, so the underlying router can evolve
|
|
@@ -33,4 +118,4 @@ declare function useNavigate(): (opts: {
|
|
|
33
118
|
replace?: boolean;
|
|
34
119
|
}) => void;
|
|
35
120
|
|
|
36
|
-
export { Link, type LinkProps, useNavigate, useParams, useSearch };
|
|
121
|
+
export { Link, type LinkProps, type RouteInfo, type SearchStateOptions, useCurrentRoute, useNavigate, useParams, useRoutes, useSearch, useSearchParam, useSearchState };
|
package/dist/router/index.js
CHANGED
|
@@ -1,9 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getRoutes, findRoute } from '../chunk-7IPAXPPY.js';
|
|
2
|
+
import { useRouterState, Link as Link$1, useParams as useParams$1, useSearch, useNavigate } from '@tanstack/react-router';
|
|
2
3
|
export { Outlet, notFound, redirect, useRouterState } from '@tanstack/react-router';
|
|
3
|
-
import { createElement } from 'react';
|
|
4
|
+
import { useCallback, createElement } from 'react';
|
|
4
5
|
|
|
5
6
|
// @squadbase/vantage — generated build. Do not edit.
|
|
7
|
+
function useRoutes() {
|
|
8
|
+
return getRoutes();
|
|
9
|
+
}
|
|
10
|
+
function useCurrentRoute() {
|
|
11
|
+
const path = useRouterState({
|
|
12
|
+
select: (state) => {
|
|
13
|
+
const matches = state.matches;
|
|
14
|
+
const leaf = matches[matches.length - 1];
|
|
15
|
+
return leaf?.staticData?.routePath;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return path ? findRoute(path) : void 0;
|
|
19
|
+
}
|
|
20
|
+
function useRawSearch() {
|
|
21
|
+
return useSearch({ strict: false });
|
|
22
|
+
}
|
|
23
|
+
function useSearchWriter(key, options) {
|
|
24
|
+
const navigate = useNavigate();
|
|
25
|
+
const replace = options.replace ?? true;
|
|
26
|
+
return useCallback(
|
|
27
|
+
(next) => {
|
|
28
|
+
navigate({
|
|
29
|
+
search: (prev) => {
|
|
30
|
+
const merged = { ...prev };
|
|
31
|
+
if (next === void 0) delete merged[key];
|
|
32
|
+
else merged[key] = next;
|
|
33
|
+
return merged;
|
|
34
|
+
},
|
|
35
|
+
replace
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
[navigate, key, replace]
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function useSearchParam(key, defaultValue = "", options = {}) {
|
|
42
|
+
const search = useRawSearch();
|
|
43
|
+
const write = useSearchWriter(key, options);
|
|
44
|
+
const raw = search[key];
|
|
45
|
+
const value = raw === void 0 || raw === null ? defaultValue : String(raw);
|
|
46
|
+
const set = useCallback(
|
|
47
|
+
(next) => {
|
|
48
|
+
write(next === null || next === defaultValue ? void 0 : next);
|
|
49
|
+
},
|
|
50
|
+
[write, defaultValue]
|
|
51
|
+
);
|
|
52
|
+
return [value, set];
|
|
53
|
+
}
|
|
54
|
+
function useSearchState(key, defaultValue, options = {}) {
|
|
55
|
+
const search = useRawSearch();
|
|
56
|
+
const write = useSearchWriter(key, options);
|
|
57
|
+
const raw = search[key];
|
|
58
|
+
const value = raw === void 0 || raw === null ? defaultValue : raw;
|
|
59
|
+
const set = useCallback(
|
|
60
|
+
(next) => {
|
|
61
|
+
const resolved = typeof next === "function" ? next(value) : next;
|
|
62
|
+
const isDefault = JSON.stringify(resolved) === JSON.stringify(defaultValue);
|
|
63
|
+
write(isDefault ? void 0 : resolved);
|
|
64
|
+
},
|
|
65
|
+
[write, value, defaultValue]
|
|
66
|
+
);
|
|
67
|
+
return [value, set];
|
|
68
|
+
}
|
|
6
69
|
|
|
70
|
+
// src/router/index.ts
|
|
7
71
|
function Link({ activeClassName, className, ...props }) {
|
|
8
72
|
const anyLink = Link$1;
|
|
9
73
|
return createElement(anyLink, {
|
|
@@ -15,13 +79,13 @@ function Link({ activeClassName, className, ...props }) {
|
|
|
15
79
|
function useParams() {
|
|
16
80
|
return useParams$1({ strict: false });
|
|
17
81
|
}
|
|
18
|
-
function
|
|
19
|
-
return useSearch
|
|
82
|
+
function useSearch2() {
|
|
83
|
+
return useSearch({ strict: false });
|
|
20
84
|
}
|
|
21
|
-
function
|
|
22
|
-
return useNavigate
|
|
85
|
+
function useNavigate2() {
|
|
86
|
+
return useNavigate();
|
|
23
87
|
}
|
|
24
88
|
|
|
25
|
-
export { Link, useNavigate, useParams, useSearch };
|
|
89
|
+
export { Link, useCurrentRoute, useNavigate2 as useNavigate, useParams, useRoutes, useSearch2 as useSearch, useSearchParam, useSearchState };
|
|
26
90
|
//# sourceMappingURL=index.js.map
|
|
27
91
|
//# sourceMappingURL=index.js.map
|
package/dist/router/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/router/index.ts"],"names":["TanstackLink","useParamsBase","useSearchBase","useNavigateBase"],"mappings":";;;;;;AAiCO,SAAS,KAAK,EAAE,eAAA,EAAiB,SAAA,EAAW,GAAG,OAAM,EAAyB;AAEnF,EAAA,MAAM,OAAA,GAAUA,MAAA;AAChB,EAAA,OAAO,cAAc,OAAA,EAAS;AAAA,IAC5B,GAAG,KAAA;AAAA,IACH,SAAA;AAAA,IACA,WAAA,EAAa,eAAA,GAAkB,EAAE,SAAA,EAAW,CAAA,EAAG,SAAA,IAAa,EAAE,CAAA,CAAA,EAAI,eAAe,CAAA,CAAA,CAAG,IAAA,EAAK,EAAE,GAAI;AAAA,GAChG,CAAA;AACH;AAGO,SAAS,SAAA,GAAoC;AAElD,EAAA,OAAOC,WAAA,CAAc,EAAE,MAAA,EAAQ,KAAA,EAAO,CAAA;AACxC;AAGO,SAAS,SAAA,GAAqC;AAEnD,EAAA,OAAOC,WAAA,CAAc,EAAE,MAAA,EAAQ,KAAA,EAAO,CAAA;AACxC;AAGO,SAAS,WAAA,GAAoI;AAElJ,EAAA,OAAOC,aAAA,EAAgB;AACzB","file":"index.js","sourcesContent":["/**\n * Vantage's thin router API. Users depend on `@squadbase/vantage/router`\n * instead of `@tanstack/react-router`, so the underlying router can evolve\n * without touching user code. Only the ergonomic, SPA-relevant surface is\n * exposed; typed-route generics are intentionally relaxed to plain strings.\n */\nimport {\n Link as TanstackLink,\n Outlet,\n useNavigate as useNavigateBase,\n useParams as useParamsBase,\n useSearch as useSearchBase,\n useRouterState,\n redirect,\n notFound,\n} from \"@tanstack/react-router\"\nimport { createElement, type ReactNode } from \"react\"\n\nexport { Outlet, useRouterState, redirect, notFound }\n\nexport interface LinkProps {\n to: string\n params?: Record<string, string>\n search?: Record<string, unknown>\n hash?: string\n replace?: boolean\n className?: string\n activeClassName?: string\n children?: ReactNode\n [key: string]: unknown\n}\n\n/** Client-side navigation link. */\nexport function Link({ activeClassName, className, ...props }: LinkProps): ReactNode {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const anyLink = TanstackLink as any\n return createElement(anyLink, {\n ...props,\n className,\n activeProps: activeClassName ? { className: `${className ?? \"\"} ${activeClassName}`.trim() } : undefined,\n })\n}\n\n/** Route params for the current route (e.g. `{ customerId: \"42\" }`). */\nexport function useParams(): Record<string, string> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useParamsBase({ strict: false }) as any\n}\n\n/** Validated/raw search params for the current route. */\nexport function useSearch(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useSearchBase({ strict: false }) as any\n}\n\n/** Imperative navigation. `navigate({ to: \"/sales/42\" })`. */\nexport function useNavigate(): (opts: { to: string; params?: Record<string, string>; search?: Record<string, unknown>; replace?: boolean }) => void {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useNavigateBase() as any\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/router/routes.ts","../../src/router/search.ts","../../src/router/index.ts"],"names":["TanstackLink","useParamsBase","useSearch","useSearchBase","useNavigate","useNavigateBase"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Route-introspection hooks. The route list comes from the generated manifest,\n * so a nav built on `useRoutes()` picks up a new page file the moment it is\n * created — no hand-maintained link array.\n */\nimport { useRouterState } from \"@tanstack/react-router\"\nimport { findRoute, getRoutes, type RouteInfo } from \"./registry.js\"\n\n/**\n * Every page route in the app, in the framework's scan order (shallow before\n * deep, static before dynamic, alphabetical) — the order a nav wants.\n *\n * ```tsx\n * // Dynamic routes have no single URL, so a nav lists only static ones.\n * const routes = useRoutes().filter((route) => !route.dynamic)\n * routes.map((route) => <Link key={route.path} to={route.to}>{route.label}</Link>)\n * ```\n */\nexport function useRoutes(): readonly RouteInfo[] {\n return getRoutes()\n}\n\n/**\n * The route currently rendered, or `undefined` on the 404 route (no page\n * matched). Use it for breadcrumbs or a page header that mirrors `definePage`.\n */\nexport function useCurrentRoute(): RouteInfo | undefined {\n // Selecting the path string (not the match object) keeps the subscription\n // stable — the component re-renders on navigation, not on every router tick.\n const path = useRouterState({\n select: (state) => {\n const matches = state.matches\n const leaf = matches[matches.length - 1] as\n | { staticData?: { routePath?: string } }\n | undefined\n return leaf?.staticData?.routePath\n },\n })\n return path ? findRoute(path) : undefined\n}\n","/**\n * URL-backed component state. A dashboard's filters belong in the URL: the view\n * survives a reload and can be pasted to a colleague. These hooks read and write\n * a single search-param key with a `useState`-shaped API.\n */\nimport { useCallback } from \"react\"\nimport { useNavigate, useSearch } from \"@tanstack/react-router\"\n\nexport interface SearchStateOptions {\n /**\n * Replace the current history entry instead of pushing a new one.\n * Defaults to `true` so dragging a slider doesn't fill the back button.\n */\n replace?: boolean\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nfunction useRawSearch(): Record<string, unknown> {\n return useSearch({ strict: false }) as any\n}\n\n/**\n * Writer for one key. Passing `undefined` removes the key so defaults never\n * show up in the URL.\n */\nfunction useSearchWriter(key: string, options: SearchStateOptions): (next: unknown) => void {\n const navigate = useNavigate() as any\n const replace = options.replace ?? true\n return useCallback(\n (next: unknown) => {\n // No `to`: TanStack keeps the current route and its path params, so this\n // works unchanged on a dynamic route like `/sales/$customerId`.\n navigate({\n search: (prev: Record<string, unknown>) => {\n const merged = { ...prev }\n if (next === undefined) delete merged[key]\n else merged[key] = next\n return merged\n },\n replace,\n })\n },\n [navigate, key, replace],\n )\n}\n\n/**\n * A string search param, as `useState`. The value is always a string —\n * TanStack parses `?year=2024` into a number, and this hook normalizes it back.\n *\n * ```tsx\n * const [region, setRegion] = useSearchParam(\"region\", \"all\")\n * <SegmentedControl value={region} onValueChange={setRegion} options={REGIONS} />\n * ```\n *\n * Writing the default value (or `null`) drops the key from the URL.\n */\nexport function useSearchParam(\n key: string,\n defaultValue = \"\",\n options: SearchStateOptions = {},\n): [string, (value: string | null) => void] {\n const search = useRawSearch()\n const write = useSearchWriter(key, options)\n\n const raw = search[key]\n const value = raw === undefined || raw === null ? defaultValue : String(raw)\n\n const set = useCallback(\n (next: string | null) => {\n write(next === null || next === defaultValue ? undefined : next)\n },\n [write, defaultValue],\n )\n\n return [value, set]\n}\n\n/**\n * Any JSON-serializable search param, as `useState` — arrays for a multi-select,\n * an object for a whole filter bar. Supports a functional update.\n *\n * ```tsx\n * const [segments, setSegments] = useSearchState<string[]>(\"segments\", [])\n * setSegments((prev) => [...prev, \"enterprise\"])\n * ```\n *\n * Writing a value deep-equal to the default drops the key from the URL.\n */\nexport function useSearchState<T>(\n key: string,\n defaultValue: T,\n options: SearchStateOptions = {},\n): [T, (value: T | ((prev: T) => T)) => void] {\n const search = useRawSearch()\n const write = useSearchWriter(key, options)\n\n const raw = search[key]\n const value = raw === undefined || raw === null ? defaultValue : (raw as T)\n\n const set = useCallback(\n (next: T | ((prev: T) => T)) => {\n const resolved = typeof next === \"function\" ? (next as (prev: T) => T)(value) : next\n // Structural comparison: the parsed value is plain JSON, so stringify is\n // both sufficient and cheap at filter sizes.\n const isDefault = JSON.stringify(resolved) === JSON.stringify(defaultValue)\n write(isDefault ? undefined : resolved)\n },\n [write, value, defaultValue],\n )\n\n return [value, set]\n}\n","/**\n * Vantage's thin router API. Users depend on `@squadbase/vantage/router`\n * instead of `@tanstack/react-router`, so the underlying router can evolve\n * without touching user code. Only the ergonomic, SPA-relevant surface is\n * exposed; typed-route generics are intentionally relaxed to plain strings.\n */\nimport {\n Link as TanstackLink,\n Outlet,\n useNavigate as useNavigateBase,\n useParams as useParamsBase,\n useSearch as useSearchBase,\n useRouterState,\n redirect,\n notFound,\n} from \"@tanstack/react-router\"\nimport { createElement, type ReactNode } from \"react\"\n\nexport { Outlet, useRouterState, redirect, notFound }\n\nexport { useRoutes, useCurrentRoute } from \"./routes.js\"\nexport type { RouteInfo } from \"./registry.js\"\n\nexport { useSearchParam, useSearchState } from \"./search.js\"\nexport type { SearchStateOptions } from \"./search.js\"\n\nexport interface LinkProps {\n to: string\n params?: Record<string, string>\n search?: Record<string, unknown>\n hash?: string\n replace?: boolean\n className?: string\n activeClassName?: string\n children?: ReactNode\n [key: string]: unknown\n}\n\n/** Client-side navigation link. */\nexport function Link({ activeClassName, className, ...props }: LinkProps): ReactNode {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const anyLink = TanstackLink as any\n return createElement(anyLink, {\n ...props,\n className,\n activeProps: activeClassName ? { className: `${className ?? \"\"} ${activeClassName}`.trim() } : undefined,\n })\n}\n\n/** Route params for the current route (e.g. `{ customerId: \"42\" }`). */\nexport function useParams(): Record<string, string> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useParamsBase({ strict: false }) as any\n}\n\n/** Validated/raw search params for the current route. */\nexport function useSearch(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useSearchBase({ strict: false }) as any\n}\n\n/** Imperative navigation. `navigate({ to: \"/sales/42\" })`. */\nexport function useNavigate(): (opts: { to: string; params?: Record<string, string>; search?: Record<string, unknown>; replace?: boolean }) => void {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useNavigateBase() as any\n}\n"]}
|
package/dist/vite/index.js
CHANGED
package/docs/en/components.md
CHANGED
|
@@ -124,6 +124,7 @@ Dashboard parts composed from `ui/`.
|
|
|
124
124
|
| [`DataTable`](parts/data-table) | Sorting, search, pagination, selection |
|
|
125
125
|
| [`EChart`](parts/echart) | The Apache ECharts wrapper |
|
|
126
126
|
| [`FunnelSteps`](parts/funnel-steps) | A funnel drawn as bars |
|
|
127
|
+
| [`Sparkline`](parts/sparkline) | An inline trend chart for cells and tiles |
|
|
127
128
|
| [`FilterBar`](parts/filter-bar) | A row of filters driven by one value |
|
|
128
129
|
| [`DateRangePicker`](parts/date-range-picker) | Date range with presets |
|
|
129
130
|
| [`SegmentedControl`](parts/segmented-control) | Single-choice segments |
|
|
@@ -132,6 +133,7 @@ Dashboard parts composed from `ui/`.
|
|
|
132
133
|
| [`MetricValue`](parts/metric-value) | A KPI's number and unit |
|
|
133
134
|
| [`TrendIndicator`](parts/trend-indicator) | An arrow and a percentage |
|
|
134
135
|
| [`StatusBadge`](parts/status-badge) | A dot-and-label status badge |
|
|
136
|
+
| [`Placeholder`](parts/placeholder) | An inline marker for sample data |
|
|
135
137
|
|
|
136
138
|
## `@squadbase/vantage/markdown`
|
|
137
139
|
|
package/docs/en/data-fetching.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
> Handle server state with a managed TanStack Query (useQuery).
|
|
4
4
|
|
|
5
5
|
Vantage ships a **managed TanStack Query** for server state. There is no `QueryClient` to set up —
|
|
6
|
-
just import `useQuery` and friends from `@squadbase/vantage/query`.
|
|
6
|
+
just import `useQuery` and friends from `@squadbase/vantage/query`. To call your own `server/api`,
|
|
7
|
+
[`useApiQuery`](#call-your-own-api--useapiquery) below is the shortcut.
|
|
7
8
|
|
|
8
9
|
```tsx
|
|
9
10
|
import { useQuery } from "@squadbase/vantage/query";
|
|
@@ -39,6 +40,12 @@ Vantage provides exactly one `QueryClient` with these defaults:
|
|
|
39
40
|
| `staleTime` | 30 seconds |
|
|
40
41
|
| `retry` | 1 |
|
|
41
42
|
| `refetchOnWindowFocus` | `false` |
|
|
43
|
+
| `networkMode` | `"always"` (queries and mutations) |
|
|
44
|
+
|
|
45
|
+
`networkMode: "always"` requests and retries regardless of the browser's offline heuristic. Under
|
|
46
|
+
the default `"online"`, a failed query is *paused* the moment that heuristic reads offline — no
|
|
47
|
+
error, just a loading state that never resolves. A dashboard talks to its own origin (or the
|
|
48
|
+
configured API base), where that heuristic says nothing useful.
|
|
42
49
|
|
|
43
50
|
Customize by **overriding per query**. Swapping the whole `QueryClient` is intentionally not exposed,
|
|
44
51
|
to keep the stack managed.
|
|
@@ -57,21 +64,78 @@ useQuery({
|
|
|
57
64
|
> Do not import `@tanstack/react-query` directly. Using the curated re-exports from
|
|
58
65
|
> `@squadbase/vantage/query` lets Vantage evolve the underlying library safely.
|
|
59
66
|
|
|
60
|
-
##
|
|
67
|
+
## Call your own API — useApiQuery
|
|
68
|
+
|
|
69
|
+
For routes under `server/api`, use `useApiQuery`. It is `useQuery` plus the boilerplate every page
|
|
70
|
+
was repeating: base-URL resolution, JSON parsing, non-2xx to an error, and a query key.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { useApiQuery } from "@squadbase/vantage/query";
|
|
74
|
+
|
|
75
|
+
interface Monthly {
|
|
76
|
+
monthly: { month: string; revenue: number }[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function MonthlyAnalysis() {
|
|
80
|
+
const { data, isPending, error } = useApiQuery<Monthly>("/api/monthly-analysis");
|
|
81
|
+
|
|
82
|
+
if (isPending) return <p>Loading…</p>;
|
|
83
|
+
if (error) return <p>{error.message}</p>;
|
|
84
|
+
return <Chart data={data.monthly} />;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Pass the query string as `search`. `undefined` entries are dropped, so an unset filter appears
|
|
89
|
+
neither in the URL nor in the query key.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
const query = useApiQuery<Row[]>("/api/customers", {
|
|
93
|
+
search: { segment, from: range.from, to: range.to },
|
|
94
|
+
enabled: Boolean(segment), // any useQuery option still works
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The query key defaults to `["api", <url with query string>]`, so changing `search` refetches, and
|
|
99
|
+
`invalidateQueries({ queryKey: ["api"] })` drops everything that came from the API.
|
|
100
|
+
|
|
101
|
+
### Errors arrive as ApiError
|
|
102
|
+
|
|
103
|
+
A non-2xx response is thrown as an `ApiError`. Its `message` is what the API chose to disclose with
|
|
104
|
+
[`HttpError`](api-and-server); anything else surfaces as generic status text.
|
|
105
|
+
|
|
106
|
+
| Property | Contents |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `message` | The message the server set with `HttpError` |
|
|
109
|
+
| `status` | HTTP status |
|
|
110
|
+
| `body` | Parsed response body |
|
|
111
|
+
| `requestId` | `x-vantage-request-id`, to correlate with the dev terminal log |
|
|
112
|
+
|
|
113
|
+
### Writes with useApiMutation
|
|
114
|
+
|
|
115
|
+
The mutation variables are sent as the JSON body. The path may be a function.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { useApiMutation, useQueryClient } from "@squadbase/vantage/query";
|
|
119
|
+
|
|
120
|
+
const queryClient = useQueryClient();
|
|
121
|
+
const save = useApiMutation<Customer, { id: string; name: string }>(
|
|
122
|
+
(vars) => `/api/customers/${vars.id}`,
|
|
123
|
+
{
|
|
124
|
+
method: "PUT",
|
|
125
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["api"] }),
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
save.mutate({ id: "1", name: "Northwind" });
|
|
130
|
+
```
|
|
61
131
|
|
|
62
|
-
|
|
63
|
-
|
|
132
|
+
Where a hook doesn't fit (an event handler, say), call `apiJson(path, init)` directly. For the raw
|
|
133
|
+
`Response`, use `apiFetch`.
|
|
64
134
|
|
|
65
135
|
> [!TIP]
|
|
66
|
-
>
|
|
67
|
-
>
|
|
68
|
-
>
|
|
69
|
-
>
|
|
70
|
-
> ```tsx
|
|
71
|
-
> import { apiFetch } from "@squadbase/vantage/query";
|
|
72
|
-
>
|
|
73
|
-
> queryFn: () => apiFetch("/api/customers").then((r) => r.json());
|
|
74
|
-
> ```
|
|
136
|
+
> For an external API, keep using `useQuery` + `fetch`. `useApiQuery` goes through `apiFetch`, so
|
|
137
|
+
> setting [`PUBLIC_API_BASE_URL`](build-and-deploy) lets you deploy the client and server on
|
|
138
|
+
> different origins without touching page code.
|
|
75
139
|
|
|
76
140
|
## What to read next
|
|
77
141
|
|
|
@@ -22,7 +22,8 @@ To add a title or description, use `definePage`. It is optional.
|
|
|
22
22
|
import { definePage } from "@squadbase/vantage";
|
|
23
23
|
|
|
24
24
|
export const page = definePage({
|
|
25
|
-
title: "Monthly analysis",
|
|
25
|
+
title: "Monthly analysis · Acme Analytics",
|
|
26
|
+
navLabel: "Monthly analysis",
|
|
26
27
|
description: "A monthly summary of revenue and key metrics",
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -33,8 +34,10 @@ export default function MonthlyAnalysis() {
|
|
|
33
34
|
|
|
34
35
|
- `title` drives `document.title` (the deepest route match's title wins; otherwise it falls back to
|
|
35
36
|
the document's initial `<title>`).
|
|
36
|
-
- `
|
|
37
|
-
|
|
37
|
+
- `navLabel` is the name a nav built with [`useRoutes()`](routing#build-a-nav-from-the-route-list)
|
|
38
|
+
renders. It defaults to `title`; set it separately when `title` carries the site name.
|
|
39
|
+
- `title`/`description`/`navLabel` are **read statically at generation time**. Vantage parses the
|
|
40
|
+
source rather than executing your page, so keep the values **literal**.
|
|
38
41
|
|
|
39
42
|
> [!WARNING]
|
|
40
43
|
> The **export name must be `page`** in `export const page = definePage({...})`. A different name means
|