@squadbase/vantage 0.5.0 → 0.5.2

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 CHANGED
@@ -7,7 +7,7 @@ You write `index.tsx`. Vantage owns routing, React, TanStack Query, Tailwind v4,
7
7
  the UI kit, the dev server, the optional API server, and the build. There is no
8
8
  config file.
9
9
 
10
- > **Status: prototype (v0.5.0).** APIs may still change between releases.
10
+ > **Status: prototype (v0.5.2).** APIs may still change between releases.
11
11
 
12
12
  ## Install
13
13
 
@@ -113,7 +113,42 @@ function layoutDepth(layout, manifest) {
113
113
  }
114
114
  return depth;
115
115
  }
116
+
117
+ // src/client/self-heal.ts
118
+ var RELOAD_COOLDOWN_MS = 1e4;
119
+ var RELOAD_STAMP_KEY = "__vantage_chunk_reload_at";
120
+ var DYNAMIC_IMPORT_FAILURE = /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/i;
121
+ function reloadOnceForStaleChunk() {
122
+ let last = 0;
123
+ try {
124
+ last = Number(window.sessionStorage.getItem(RELOAD_STAMP_KEY) ?? "0");
125
+ } catch {
126
+ }
127
+ if (Number.isFinite(last) && Date.now() - last < RELOAD_COOLDOWN_MS) {
128
+ return;
129
+ }
130
+ try {
131
+ window.sessionStorage.setItem(RELOAD_STAMP_KEY, String(Date.now()));
132
+ } catch {
133
+ }
134
+ window.location.reload();
135
+ }
136
+ function registerChunkErrorSelfHeal() {
137
+ if (typeof window === "undefined") return;
138
+ window.addEventListener("vite:preloadError", (event) => {
139
+ event.preventDefault();
140
+ reloadOnceForStaleChunk();
141
+ });
142
+ window.addEventListener("unhandledrejection", (event) => {
143
+ const reason = event.reason;
144
+ const message = reason instanceof Error ? reason.message : String(reason ?? "");
145
+ if (DYNAMIC_IMPORT_FAILURE.test(message)) {
146
+ reloadOnceForStaleChunk();
147
+ }
148
+ });
149
+ }
116
150
  function bootstrap(manifest) {
151
+ registerChunkErrorSelfHeal();
117
152
  setRoutes(manifest);
118
153
  const { router } = buildRouter(manifest);
119
154
  const queryClient = createQueryClient();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/states.tsx","../../src/client/error-boundary.tsx","../../src/client/build-router.tsx","../../src/client/bootstrap.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import type { ReactNode } from \"react\"\n\n/** Minimal internal states used as router-level defaults. */\n\nexport function DefaultPending(): ReactNode {\n return (\n <div className=\"flex min-h-[40vh] items-center justify-center\" role=\"status\" aria-live=\"polite\">\n <div className=\"flex items-center gap-3 text-sm text-[var(--muted-foreground,#71717a)]\">\n <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n Loading…\n </div>\n </div>\n )\n}\n\nexport function DefaultError({ error }: { error: unknown }): ReactNode {\n const message = error instanceof Error ? error.message : String(error)\n return (\n <div className=\"mx-auto max-w-2xl p-6\" role=\"alert\">\n <div className=\"rounded-lg border border-[var(--border,#e4e4e7)] bg-[var(--card,#fff)] p-6\">\n <h2 className=\"text-lg font-semibold text-[var(--foreground,#09090b)]\">Something went wrong</h2>\n <p className=\"mt-2 text-sm text-[var(--muted-foreground,#71717a)]\">{message}</p>\n </div>\n </div>\n )\n}\n\nexport function DefaultNotFound(): ReactNode {\n return (\n <div className=\"mx-auto max-w-2xl p-6 text-center\">\n <p className=\"text-5xl font-bold text-[var(--muted-foreground,#a1a1aa)]\">404</p>\n <h2 className=\"mt-3 text-lg font-semibold text-[var(--foreground,#09090b)]\">Page not found</h2>\n <p className=\"mt-1 text-sm text-[var(--muted-foreground,#71717a)]\">\n The page you are looking for does not exist.\n </p>\n </div>\n )\n}\n","import { Component, type ErrorInfo, type ReactNode } from \"react\"\nimport { DefaultError } from \"./states\"\n\ninterface Props {\n children: ReactNode\n fallback?: (error: unknown) => ReactNode\n}\n\ninterface State {\n error: unknown\n}\n\n/**\n * Top-level error boundary that wraps the entire application (outside the\n * router). Route-level errors are handled by the router's errorComponent; this\n * catches catastrophic render failures in the provider tree.\n */\nexport class AppErrorBoundary extends Component<Props, State> {\n state: State = { error: null }\n\n static getDerivedStateFromError(error: unknown): State {\n return { error }\n }\n\n componentDidCatch(error: unknown, info: ErrorInfo): void {\n // Forwarded to the terminal in dev via the console bridge.\n console.error(\"[vantage] Unhandled application error:\", error, info.componentStack)\n }\n\n render(): ReactNode {\n if (this.state.error != null) {\n return this.props.fallback\n ? this.props.fallback(this.state.error)\n : <DefaultError error={this.state.error} />\n }\n return this.props.children\n }\n}\n","import {\n createRootRoute,\n createRoute,\n createRouter,\n lazyRouteComponent,\n Outlet,\n} from \"@tanstack/react-router\"\nimport type { ClientManifest, Loader } from \"./manifest\"\nimport { DefaultError, DefaultNotFound, DefaultPending } from \"./states\"\n\n// The programmatic route tree fights TanStack's file-route generics, which are\n// designed for codegen. Internal assembly is intentionally untyped; the public\n// surface (pages, definePage) stays fully typed.\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyRoute = any\n\nfunction RootPassthrough() {\n return <Outlet />\n}\n\nfunction lazyComponent(load: Loader): AnyRoute {\n return lazyRouteComponent(load as () => Promise<{ default: any }>)\n}\n\nfunction lazyErrorComponent(load: Loader): AnyRoute {\n return lazyRouteComponent(load as () => Promise<{ default: any }>)\n}\n\nexport interface BuiltRouter {\n router: AnyRoute\n /** routeId -> page meta, for the title manager. */\n meta: Map<string, { title?: string; description?: string; routePath: string }>\n}\n\n/** Assemble a TanStack Router instance from the generated manifest. */\nexport function buildRouter(manifest: ClientManifest): BuiltRouter {\n const rootRoute = createRootRoute({\n component: manifest.rootLayout ? lazyComponent(manifest.rootLayout) : RootPassthrough,\n errorComponent: manifest.error ? lazyErrorComponent(manifest.error) : DefaultError,\n notFoundComponent: manifest.notFound ? lazyComponent(manifest.notFound) : DefaultNotFound,\n })\n\n const layoutById = new Map<string, AnyRoute>()\n const childrenByParent = new Map<string, AnyRoute[]>()\n const meta = new Map<string, { title?: string; description?: string; routePath: string }>()\n\n const pushChild = (parentKey: string, route: AnyRoute) => {\n const list = childrenByParent.get(parentKey) ?? []\n list.push(route)\n childrenByParent.set(parentKey, list)\n }\n\n // 1. Create layout routes (parents referenced lazily via getParentRoute).\n for (const layout of manifest.layouts) {\n const parentKey = layout.parentId ?? \"__root__\"\n const route: AnyRoute = createRoute({\n getParentRoute: () =>\n layout.parentId ? layoutById.get(layout.parentId)! : rootRoute,\n path: layout.path,\n component: lazyComponent(layout.load),\n } as AnyRoute)\n layoutById.set(layout.id, route)\n pushChild(parentKey, route)\n }\n\n // 2. Create page routes.\n for (const page of manifest.pages) {\n const parentKey = page.parentLayoutId ?? \"__root__\"\n const route: AnyRoute = createRoute({\n getParentRoute: () =>\n page.parentLayoutId ? layoutById.get(page.parentLayoutId)! : rootRoute,\n path: page.path,\n component: lazyComponent(page.load),\n staticData: { title: page.title, description: page.description, routePath: page.routePath },\n } as AnyRoute)\n pushChild(parentKey, route)\n // route.id is finalized after createRouter; index by routePath-derived key\n // is unreliable, so the title manager reads staticData off matches instead.\n }\n\n // 3. Attach children — deepest layouts first so nested trees are built before\n // their parent references them.\n const layoutsDeepFirst = [...manifest.layouts].sort(\n (a, b) => layoutDepth(b, manifest) - layoutDepth(a, manifest),\n )\n for (const layout of layoutsDeepFirst) {\n const route = layoutById.get(layout.id)!\n route.addChildren(childrenByParent.get(layout.id) ?? [])\n }\n rootRoute.addChildren(childrenByParent.get(\"__root__\") ?? [])\n\n const router = createRouter({\n routeTree: rootRoute,\n defaultPendingComponent: DefaultPending,\n defaultErrorComponent: DefaultError,\n defaultNotFoundComponent: DefaultNotFound,\n defaultPreload: \"intent\",\n scrollRestoration: true,\n } as AnyRoute)\n\n return { router, meta }\n}\n\nfunction layoutDepth(layout: { id: string; parentId: string | null }, manifest: ClientManifest): number {\n let depth = 0\n let current: string | null = layout.parentId\n const byId = new Map(manifest.layouts.map((l) => [l.id, l]))\n while (current) {\n depth++\n current = byId.get(current)?.parentId ?? null\n }\n return depth\n}\n","import { StrictMode } from \"react\"\nimport { createRoot } from \"react-dom/client\"\nimport { RouterProvider } from \"@tanstack/react-router\"\nimport { QueryClientProvider } from \"@tanstack/react-query\"\nimport { createQueryClient } from \"../query/client\"\nimport { setRoutes } from \"../router/registry\"\nimport { AppErrorBoundary } from \"./error-boundary\"\nimport { buildRouter } from \"./build-router\"\nimport type { ClientManifest } from \"./manifest\"\n\n/**\n * Client entrypoint. The generated `.vantage/client-entry.tsx` calls this with\n * the route manifest. Owns React root creation and the provider tree:\n *\n * <AppErrorBoundary>\n * <QueryClientProvider>\n * <RouterProvider />\n */\nexport function bootstrap(manifest: ClientManifest): void {\n // Publish the route list before the first render so `useRoutes()` is\n // populated for the very first page, not one paint late.\n setRoutes(manifest)\n\n const { router } = buildRouter(manifest)\n const queryClient = createQueryClient()\n\n // The document's initial title is the fallback for pages without a definePage\n // title, so navigating away from a titled page doesn't leave a stale title.\n const defaultTitle = document.title\n\n const updateTitle = () => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const matches: any[] = router.state?.matches ?? []\n const leaf = matches[matches.length - 1]\n const title = leaf?.staticData?.title\n document.title = typeof title === \"string\" && title.length > 0 ? title : defaultTitle\n } catch {\n /* title updates are best-effort */\n }\n }\n router.subscribe?.(\"onResolved\", updateTitle)\n\n const el = document.getElementById(\"root\")\n if (!el) {\n throw new Error('[vantage] Root element \"#root\" was not found in the document.')\n }\n\n createRoot(el).render(\n <StrictMode>\n <AppErrorBoundary>\n <QueryClientProvider client={queryClient}>\n <RouterProvider router={router} />\n </QueryClientProvider>\n </AppErrorBoundary>\n </StrictMode>,\n )\n\n updateTitle()\n}\n"]}
1
+ {"version":3,"sources":["../../src/client/states.tsx","../../src/client/error-boundary.tsx","../../src/client/build-router.tsx","../../src/client/self-heal.ts","../../src/client/bootstrap.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import type { ReactNode } from \"react\"\n\n/** Minimal internal states used as router-level defaults. */\n\nexport function DefaultPending(): ReactNode {\n return (\n <div className=\"flex min-h-[40vh] items-center justify-center\" role=\"status\" aria-live=\"polite\">\n <div className=\"flex items-center gap-3 text-sm text-[var(--muted-foreground,#71717a)]\">\n <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n Loading…\n </div>\n </div>\n )\n}\n\nexport function DefaultError({ error }: { error: unknown }): ReactNode {\n const message = error instanceof Error ? error.message : String(error)\n return (\n <div className=\"mx-auto max-w-2xl p-6\" role=\"alert\">\n <div className=\"rounded-lg border border-[var(--border,#e4e4e7)] bg-[var(--card,#fff)] p-6\">\n <h2 className=\"text-lg font-semibold text-[var(--foreground,#09090b)]\">Something went wrong</h2>\n <p className=\"mt-2 text-sm text-[var(--muted-foreground,#71717a)]\">{message}</p>\n </div>\n </div>\n )\n}\n\nexport function DefaultNotFound(): ReactNode {\n return (\n <div className=\"mx-auto max-w-2xl p-6 text-center\">\n <p className=\"text-5xl font-bold text-[var(--muted-foreground,#a1a1aa)]\">404</p>\n <h2 className=\"mt-3 text-lg font-semibold text-[var(--foreground,#09090b)]\">Page not found</h2>\n <p className=\"mt-1 text-sm text-[var(--muted-foreground,#71717a)]\">\n The page you are looking for does not exist.\n </p>\n </div>\n )\n}\n","import { Component, type ErrorInfo, type ReactNode } from \"react\"\nimport { DefaultError } from \"./states\"\n\ninterface Props {\n children: ReactNode\n fallback?: (error: unknown) => ReactNode\n}\n\ninterface State {\n error: unknown\n}\n\n/**\n * Top-level error boundary that wraps the entire application (outside the\n * router). Route-level errors are handled by the router's errorComponent; this\n * catches catastrophic render failures in the provider tree.\n */\nexport class AppErrorBoundary extends Component<Props, State> {\n state: State = { error: null }\n\n static getDerivedStateFromError(error: unknown): State {\n return { error }\n }\n\n componentDidCatch(error: unknown, info: ErrorInfo): void {\n // Forwarded to the terminal in dev via the console bridge.\n console.error(\"[vantage] Unhandled application error:\", error, info.componentStack)\n }\n\n render(): ReactNode {\n if (this.state.error != null) {\n return this.props.fallback\n ? this.props.fallback(this.state.error)\n : <DefaultError error={this.state.error} />\n }\n return this.props.children\n }\n}\n","import {\n createRootRoute,\n createRoute,\n createRouter,\n lazyRouteComponent,\n Outlet,\n} from \"@tanstack/react-router\"\nimport type { ClientManifest, Loader } from \"./manifest\"\nimport { DefaultError, DefaultNotFound, DefaultPending } from \"./states\"\n\n// The programmatic route tree fights TanStack's file-route generics, which are\n// designed for codegen. Internal assembly is intentionally untyped; the public\n// surface (pages, definePage) stays fully typed.\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyRoute = any\n\nfunction RootPassthrough() {\n return <Outlet />\n}\n\nfunction lazyComponent(load: Loader): AnyRoute {\n return lazyRouteComponent(load as () => Promise<{ default: any }>)\n}\n\nfunction lazyErrorComponent(load: Loader): AnyRoute {\n return lazyRouteComponent(load as () => Promise<{ default: any }>)\n}\n\nexport interface BuiltRouter {\n router: AnyRoute\n /** routeId -> page meta, for the title manager. */\n meta: Map<string, { title?: string; description?: string; routePath: string }>\n}\n\n/** Assemble a TanStack Router instance from the generated manifest. */\nexport function buildRouter(manifest: ClientManifest): BuiltRouter {\n const rootRoute = createRootRoute({\n component: manifest.rootLayout ? lazyComponent(manifest.rootLayout) : RootPassthrough,\n errorComponent: manifest.error ? lazyErrorComponent(manifest.error) : DefaultError,\n notFoundComponent: manifest.notFound ? lazyComponent(manifest.notFound) : DefaultNotFound,\n })\n\n const layoutById = new Map<string, AnyRoute>()\n const childrenByParent = new Map<string, AnyRoute[]>()\n const meta = new Map<string, { title?: string; description?: string; routePath: string }>()\n\n const pushChild = (parentKey: string, route: AnyRoute) => {\n const list = childrenByParent.get(parentKey) ?? []\n list.push(route)\n childrenByParent.set(parentKey, list)\n }\n\n // 1. Create layout routes (parents referenced lazily via getParentRoute).\n for (const layout of manifest.layouts) {\n const parentKey = layout.parentId ?? \"__root__\"\n const route: AnyRoute = createRoute({\n getParentRoute: () =>\n layout.parentId ? layoutById.get(layout.parentId)! : rootRoute,\n path: layout.path,\n component: lazyComponent(layout.load),\n } as AnyRoute)\n layoutById.set(layout.id, route)\n pushChild(parentKey, route)\n }\n\n // 2. Create page routes.\n for (const page of manifest.pages) {\n const parentKey = page.parentLayoutId ?? \"__root__\"\n const route: AnyRoute = createRoute({\n getParentRoute: () =>\n page.parentLayoutId ? layoutById.get(page.parentLayoutId)! : rootRoute,\n path: page.path,\n component: lazyComponent(page.load),\n staticData: { title: page.title, description: page.description, routePath: page.routePath },\n } as AnyRoute)\n pushChild(parentKey, route)\n // route.id is finalized after createRouter; index by routePath-derived key\n // is unreliable, so the title manager reads staticData off matches instead.\n }\n\n // 3. Attach children — deepest layouts first so nested trees are built before\n // their parent references them.\n const layoutsDeepFirst = [...manifest.layouts].sort(\n (a, b) => layoutDepth(b, manifest) - layoutDepth(a, manifest),\n )\n for (const layout of layoutsDeepFirst) {\n const route = layoutById.get(layout.id)!\n route.addChildren(childrenByParent.get(layout.id) ?? [])\n }\n rootRoute.addChildren(childrenByParent.get(\"__root__\") ?? [])\n\n const router = createRouter({\n routeTree: rootRoute,\n defaultPendingComponent: DefaultPending,\n defaultErrorComponent: DefaultError,\n defaultNotFoundComponent: DefaultNotFound,\n defaultPreload: \"intent\",\n scrollRestoration: true,\n } as AnyRoute)\n\n return { router, meta }\n}\n\nfunction layoutDepth(layout: { id: string; parentId: string | null }, manifest: ClientManifest): number {\n let depth = 0\n let current: string | null = layout.parentId\n const byId = new Map(manifest.layouts.map((l) => [l.id, l]))\n while (current) {\n depth++\n current = byId.get(current)?.parentId ?? null\n }\n return depth\n}\n","/**\n * Stale-chunk self-healing.\n *\n * A published app is served build-by-build: its HTML references content-hashed\n * JS chunks that belong to one build. When the app is re-published, an\n * already-open tab can end up requesting a chunk hash that the newly-served\n * build no longer has (the routing pin that keeps a session on one build is\n * short-lived by design), which fails the dynamic import. Vite emits\n * `vite:preloadError` for exactly that case.\n *\n * We reload once to pick up the new build. A short sessionStorage cooldown keeps\n * a genuinely-unreachable chunk (offline, or a build that was garbage-collected)\n * from turning into an infinite reload loop — after the first failed reload the\n * error is allowed to surface to the error boundary instead.\n */\nconst RELOAD_COOLDOWN_MS = 10_000\nconst RELOAD_STAMP_KEY = \"__vantage_chunk_reload_at\"\n\n// Messages browsers use when a dynamic import / module preload fails to load.\nconst DYNAMIC_IMPORT_FAILURE = /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/i\n\nfunction reloadOnceForStaleChunk(): void {\n let last = 0\n try {\n last = Number(window.sessionStorage.getItem(RELOAD_STAMP_KEY) ?? \"0\")\n } catch {\n // sessionStorage can throw in private/embedded contexts — treat as \"no prior reload\".\n }\n if (Number.isFinite(last) && Date.now() - last < RELOAD_COOLDOWN_MS) {\n // Already reloaded very recently; the chunk is still unreachable, so stop\n // reloading and let the error surface rather than looping.\n return\n }\n try {\n window.sessionStorage.setItem(RELOAD_STAMP_KEY, String(Date.now()))\n } catch {\n // best-effort — a failed write just means we can't dedupe, not a reason to skip healing.\n }\n window.location.reload()\n}\n\n/**\n * Register global listeners that reload the page when a chunk from a\n * now-replaced build fails to load. Safe to call once at bootstrap; a no-op\n * outside the browser.\n */\nexport function registerChunkErrorSelfHeal(): void {\n if (typeof window === \"undefined\") return\n\n // Vite's canonical signal for a failed dynamic-import preload. Calling\n // preventDefault() stops Vite from re-throwing; we reload instead.\n window.addEventListener(\"vite:preloadError\", (event) => {\n event.preventDefault()\n reloadOnceForStaleChunk()\n })\n\n // Belt-and-suspenders: a dynamic import that fails without a modulepreload\n // surfaces as an unhandled rejection. Only act on the specific stale-module\n // messages so unrelated rejections are left alone.\n window.addEventListener(\"unhandledrejection\", (event) => {\n const reason = event.reason\n const message = reason instanceof Error ? reason.message : String(reason ?? \"\")\n if (DYNAMIC_IMPORT_FAILURE.test(message)) {\n reloadOnceForStaleChunk()\n }\n })\n}\n","import { StrictMode } from \"react\"\nimport { createRoot } from \"react-dom/client\"\nimport { RouterProvider } from \"@tanstack/react-router\"\nimport { QueryClientProvider } from \"@tanstack/react-query\"\nimport { createQueryClient } from \"../query/client\"\nimport { setRoutes } from \"../router/registry\"\nimport { AppErrorBoundary } from \"./error-boundary\"\nimport { buildRouter } from \"./build-router\"\nimport { registerChunkErrorSelfHeal } from \"./self-heal\"\nimport type { ClientManifest } from \"./manifest\"\n\n/**\n * Client entrypoint. The generated `.vantage/client-entry.tsx` calls this with\n * the route manifest. Owns React root creation and the provider tree:\n *\n * <AppErrorBoundary>\n * <QueryClientProvider>\n * <RouterProvider />\n */\nexport function bootstrap(manifest: ClientManifest): void {\n // Recover from stale chunks after a re-publish: reload once when a dynamic\n // import fails because the served build changed under an open tab.\n registerChunkErrorSelfHeal()\n\n // Publish the route list before the first render so `useRoutes()` is\n // populated for the very first page, not one paint late.\n setRoutes(manifest)\n\n const { router } = buildRouter(manifest)\n const queryClient = createQueryClient()\n\n // The document's initial title is the fallback for pages without a definePage\n // title, so navigating away from a titled page doesn't leave a stale title.\n const defaultTitle = document.title\n\n const updateTitle = () => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const matches: any[] = router.state?.matches ?? []\n const leaf = matches[matches.length - 1]\n const title = leaf?.staticData?.title\n document.title = typeof title === \"string\" && title.length > 0 ? title : defaultTitle\n } catch {\n /* title updates are best-effort */\n }\n }\n router.subscribe?.(\"onResolved\", updateTitle)\n\n const el = document.getElementById(\"root\")\n if (!el) {\n throw new Error('[vantage] Root element \"#root\" was not found in the document.')\n }\n\n createRoot(el).render(\n <StrictMode>\n <AppErrorBoundary>\n <QueryClientProvider client={queryClient}>\n <RouterProvider router={router} />\n </QueryClientProvider>\n </AppErrorBoundary>\n </StrictMode>,\n )\n\n updateTitle()\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -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.5.0";
13
+ declare const VERSION = "0.5.2";
14
14
 
15
15
  export { VERSION };
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ function definePage(config) {
6
6
  }
7
7
 
8
8
  // src/index.ts
9
- var VERSION = "0.5.0";
9
+ var VERSION = "0.5.2";
10
10
 
11
11
  export { VERSION, definePage };
12
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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\"\nexport type { PageConfig, PageComponent } from \"./runtime/define-page\"\n\n/** Framework version, kept in sync with package.json at build time. */\nexport const VERSION = \"0.5.0\"\n"]}
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\"\nexport type { PageConfig, PageComponent } from \"./runtime/define-page\"\n\n/** Framework version, kept in sync with package.json at build time. */\nexport const VERSION = \"0.5.2\"\n"]}
@@ -9,6 +9,55 @@ What changed in each Vantage release. Newest is on top.
9
9
  > minor release may include backwards-incompatible changes. A patch is limited to
10
10
  > backwards-compatible additions and bug fixes.
11
11
 
12
+ ## v0.5.2
13
+
14
+ Fixes an already-open tab getting stuck on chunks from a build that is no longer
15
+ served. Anything on `^0.5.0` picks this up as-is.
16
+
17
+ ### Fixed
18
+
19
+ - **A tab left open across a re-publish now follows the new build on its own.**
20
+ A published app is served build by build, and its HTML points at
21
+ content-hashed chunks. Re-publish the app and a tab that stayed open asks for a
22
+ **chunk hash the newly-served build no longer has** — the dynamic import behind
23
+ a page transition fails with `Failed to fetch dynamically imported module`, and
24
+ the screen sits on the error until someone reloads by hand.
25
+
26
+ Vantage now listens for Vite's `vite:preloadError` (and for the same failure
27
+ arriving as an unhandled promise rejection) and **reloads the page once** to
28
+ pick up the new build. A 10-second cooldown in `sessionStorage` keeps that to a
29
+ single attempt, so a chunk that is genuinely unreachable — offline, or a build
30
+ that is gone for good — does not turn into a reload loop: the second failure
31
+ surfaces to the error boundary as before. No app-side change is needed.
32
+
33
+ ## v0.5.1
34
+
35
+ Fixes `dark:` utilities not being wired to the same switch as the theme tokens.
36
+ Anything on `^0.5.0` picks this up as-is.
37
+
38
+ ### Fixed
39
+
40
+ - **`dark:` now follows the `.dark` class, not the OS setting.** Tailwind's stock
41
+ `dark:` is `@media (prefers-color-scheme: dark)`, but Vantage's tokens only flip
42
+ when an ancestor carries `.dark` / `[data-theme="dark"]`. **In a browser set to
43
+ OS dark mode** that split the theme in two — background and text tokens stayed
44
+ light while every `dark:` class fired. Text vanished on white cards, badges
45
+ inverted on their own, and so on.
46
+
47
+ ```tsx
48
+ // 0.5.0: on an OS-dark browser, this text disappeared against the white card
49
+ <div className="text-slate-900 dark:text-slate-100">{name}</div>
50
+ ```
51
+
52
+ `theme.css` now binds `dark:` to the same class as the tokens. **Unless you add
53
+ `.dark` / `[data-theme="dark"]` yourself, the app always renders light**
54
+ (→ [UI and theming](ui-and-theming)). If your app meant to follow the OS
55
+ setting, set the class on your root element explicitly.
56
+
57
+ - **`color-scheme` is now declared** — browser-owned UI (scrollbars, `<select>`
58
+ popups, autofill) no longer goes dark on an OS-dark machine while the app itself
59
+ stays light.
60
+
12
61
  ## v0.5.0
13
62
 
14
63
  Browser-to-terminal forwarding now runs on Vite's native mechanism. Which levels
@@ -0,0 +1,199 @@
1
+ # Roadmap
2
+
3
+ > Where Vantage is heading — ideas under consideration for giving agents eyes and memory, plus what we deliberately will not do.
4
+
5
+ Where Vantage is heading. For what already works, see the [changelog](changelog);
6
+ for what you can write today, see the guides.
7
+
8
+ > [!WARNING]
9
+ > Nothing on this page is implemented yet. The order and the contents will change, and
10
+ > some of it will be dropped entirely. **Do not write apps that depend on anything here.**
11
+
12
+ ## The premise — who is this framework for?
13
+
14
+ Vantage treats **AI coding agents as first-class citizens**. Being pleasant for humans
15
+ to read matters less than letting an agent **write it without mistakes and verify it on
16
+ its own**. That is why config files are banned, why the framework owns the UI kit and
17
+ the data layer, and why `vantage docs` / `vantage search` live in the CLI —
18
+ **the CLI is the agent's interface, and conventions remove the room for judgment.**
19
+
20
+ Seen from that angle, today's Vantage supports agents well in two places:
21
+
22
+ - **Before writing** — `vantage docs` / `vantage search` / `vantage add skill` / `AGENTS.md`
23
+ - **Verifying statically** — `vantage check` / `vantage routes`
24
+
25
+ And there is one large gap.
26
+
27
+ > **An agent cannot see the screen it just wrote.**
28
+
29
+ Browser-to-terminal forwarding in `vantage dev` (`console.warn` / `console.error` and
30
+ uncaught errors) is the first small window into that gap. Most of the ideas below exist
31
+ to close it.
32
+
33
+ ## In design
34
+
35
+ ### Element inspector — from the screen to the agent
36
+
37
+ Hover and click an element in the browser, then tell the agent "fix **this**". A staple
38
+ of vibe-coding tools, but here it belongs to the **framework** rather than to each app.
39
+
40
+ Three parts:
41
+
42
+ - **Inject source locations into JSX in development only** —
43
+ `data-vtg="src/sales/index.tsx:42:7"` plus the component name. Vantage owns its UI kit,
44
+ and most of it spreads `{...props}` straight onto the DOM, so an attribute placed on
45
+ something like `<DataTable />` still reaches a real DOM node.
46
+ - **A development-only overlay** that outlines the element, labels it, and emits the selection.
47
+ - **`vantage select`** so the agent picks the selection up from the CLI, with `--json` and
48
+ a `--wait` that blocks until something is selected.
49
+
50
+ Production builds drop both the attributes and the overlay **entirely** — leaving source
51
+ paths in a bundle is a straight information leak.
52
+
53
+ For tools that embed the preview in an iframe, such as Squadbase Editor, the selection is
54
+ also posted over `postMessage`, and the overlay itself can be taken over by the host tool.
55
+ Vantage owns the correctness of the position data and a minimal default UI; the polished
56
+ experience belongs to the tool.
57
+
58
+ ## Under consideration — giving agents eyes
59
+
60
+ ### `vantage snap` — a structured snapshot of a page
61
+
62
+ Render each route headlessly and return **structured text rather than an image**.
63
+
64
+ ```jsonc
65
+ {
66
+ "route": "/sales",
67
+ "state": "ready", // loading | ready | error | empty
68
+ "regions": [
69
+ { "type": "kpi", "labels": ["Revenue", "MoM"], "values": ["$12.4M", "+8.2%"] },
70
+ { "type": "chart", "component": "EChart", "series": ["actual", "target"], "points": 12 },
71
+ { "type": "table", "component": "DataTable", "columns": ["date", "store", "amount"], "rows": 0 }
72
+ ],
73
+ "console": [{ "level": "warn", "text": "…" }],
74
+ "network": [{ "url": "/api/sales", "status": 500 }]
75
+ }
76
+ ```
77
+
78
+ For an agent this is orders of magnitude more readable than a screenshot. "The table has
79
+ zero rows" and "the API returned 500" are exactly the failures **nobody notices until a
80
+ human pastes a screenshot**.
81
+
82
+ Those `regions` can carry meaning only because Vantage owns the UI kit, the routing, and
83
+ the data layer. A general-purpose browser automation tool cannot produce this granularity,
84
+ which is why we think this is the strongest bet on the list.
85
+
86
+ ### `vantage check --runtime` — diagnostics that need a render
87
+
88
+ Today's [diagnostics](cli-reference) only read source. This would put the failures that
89
+ require an actual render behind the same command and the same exit code.
90
+
91
+ - A query stuck pending, so the page never leaves its loading state
92
+ - Fetching succeeded but the table or chart is empty
93
+ - An API returned 4xx / 5xx during the render
94
+ - Unhandled promise rejections
95
+ - An element overflowing its parent
96
+
97
+ The goal is to **close the agent's self-verification loop with an exit code** — one line
98
+ that says: if `vantage check` passes, it is safe to ship.
99
+
100
+ ### `vantage dev --agent` — a structured event stream
101
+
102
+ The dev server would emit HMR results, render errors, API responses, and slow queries as
103
+ NDJSON on a separate channel. Human-facing terminal output stays as it is, while an agent
104
+ tails the stream and **learns it broke something the moment it edits**. A natural extension
105
+ of today's console forwarding.
106
+
107
+ ## Under consideration — giving agents memory
108
+
109
+ ### An `intent` field on `definePage`
110
+
111
+ Let the framework hold **what a page is for** as structure.
112
+
113
+ ```tsx
114
+ export const page = definePage({
115
+ title: "Sales dashboard",
116
+ intent: "Spot stores missing their target by looking at daily revenue per store",
117
+ });
118
+ ```
119
+
120
+ Comments get lost as code is rewritten; this shows up in `vantage routes` and lands in the
121
+ manifest. The readers are "yourself thirty turns from now" and "the next agent to pick this
122
+ up". It is the per-page version of the same idea behind `AGENTS.md` and Agent Skills:
123
+ **the framework decides where memory lives.**
124
+
125
+ ### `vantage diff` — diffing the app, not the code
126
+
127
+ ```text
128
+ routes + /reports/monthly - /legacy
129
+ api ~ GET /api/sales (POST is gone)
130
+ render /sales table columns 5 → 4 ("owner" disappeared)
131
+ bundle +11.2 MB
132
+ ```
133
+
134
+ Agents systematically miss the **side effects** of their own edits. This confronts them with
135
+ the difference between the change they intended and the change that happened.
136
+
137
+ ### `vantage explain <route|file>`
138
+
139
+ Documentation in reverse, for an agent inheriting an existing app: the route, the components
140
+ in use, the APIs it calls, the environment variables it depends on, and the parent layouts,
141
+ on one screen. An extension of `vantage routes --detail`, aimed at **a granularity that
142
+ replaces reading the source** and cuts context consumption sharply.
143
+
144
+ ## Under consideration — preventing the mistake
145
+
146
+ ### Turn errors into entry points to the docs
147
+
148
+ Vantage knows its own stack inside out — TanStack Query, Base UI, ECharts, Hono. When it
149
+ forwards a browser error to the terminal, it should be able to **append the next move**.
150
+
151
+ ```text
152
+ [console.error] Cannot read properties of undefined (reading 'map')
153
+ > src/sales/index.tsx:42
154
+ ↳ data from useApiQuery is undefined on the first render. Use `data?.map`
155
+ or branch on isLoading → vantage docs data-fetching
156
+ ```
157
+
158
+ Agents stumble hardest at "what should I even search for", so the error itself should pull
159
+ up the documentation.
160
+
161
+ ### Absorb common mistakes instead of diagnosing them
162
+
163
+ A generalization of how the UI kit already absorbs Base UI's `Select` quirks. **A diagnostic
164
+ that fires often is a candidate for being made to just work.** We want to treat "how often
165
+ agents get this wrong" as a metric for improving the framework.
166
+
167
+ ### `vantage check --budget`
168
+
169
+ Fail the build on bundle-size or first-render regressions. Agents add heavy dependencies
170
+ without hesitation — a single Markdown renderer can add more than 10 MB of output. This turns
171
+ the unease a human would feel into a threshold a machine can read.
172
+
173
+ ### Scaffolding from the shape of the data
174
+
175
+ Hand it an API response or a SQL result and get back a page skeleton suited to that shape:
176
+ a line chart for time series, a table plus KPIs for category × value, and so on. Agents redo
177
+ this judgment from scratch every time; the conventional answers belong in the framework.
178
+
179
+ ## What we deliberately will not do
180
+
181
+ What is *not* on a roadmap matters just as much, so here is the current position.
182
+
183
+ - **Config files.** There will be no `vantage.config`. When something seems to need
184
+ configuration, we first ask whether a convention or a flag can solve it. Room for judgment
185
+ is a liability for an agent.
186
+ - **A swappable stack.** React, the router, the data layer, and the UI kit are fixed by
187
+ Vantage. The moment swapping is allowed, the docs, the skills, and the diagnostics all have
188
+ to assume they do not know what you are using — and nearly every idea above stops working.
189
+ - **Replacing the `QueryClient` wholesale.** Customization happens per query (see
190
+ [data fetching](data-fetching)).
191
+ - **Becoming a general-purpose application framework.** Vantage is a framework for
192
+ **dashboards**. Rather than broadening the scope, we would rather step on every landmine
193
+ dashboards have to offer.
194
+
195
+ ## Feedback
196
+
197
+ We would rather prioritize the list above from real friction. If you hit a spot where the
198
+ agent always gets it wrong, or where you were stuck because something was not discoverable,
199
+ please tell us.
@@ -78,6 +78,25 @@ and `--sidebar-*` for the sidebar. Changing `--chart-*` also changes
78
78
  [`EChart`](parts/echart)'s palette — charts build their theme by reading those CSS
79
79
  variables directly.
80
80
 
81
+ ## Dark mode is opt-in
82
+
83
+ The dark tokens apply only when an ancestor carries `.dark` or `[data-theme="dark"]`. Vantage
84
+ **does not read the OS/browser setting (`prefers-color-scheme`)** — a dashboard reads better when it
85
+ doesn't flip with whatever it happens to be embedded in, so the switch stays with the app.
86
+
87
+ `dark:` utilities are wired to the same switch (`theme.css` declares it via `@custom-variant dark`).
88
+ Without a `.dark` ancestor no `dark:` utility applies, so the theme can't split in two — light tokens
89
+ under dark-mode utilities.
90
+
91
+ ```tsx
92
+ // With no `.dark` ancestor, the second class does nothing
93
+ <div className="text-foreground dark:text-slate-100" />
94
+ ```
95
+
96
+ > [!NOTE]
97
+ > Prefer the semantic tokens (`text-foreground`, `text-muted-foreground`, `bg-card`) over the raw
98
+ > Tailwind palette (`text-slate-900`). Written with tokens, you rarely need a `dark:` pair at all.
99
+
81
100
  ## When you want to edit the source: `vantage add`
82
101
 
83
102
  To make a component's internals your own, copy an editable source into `components/`.
package/docs/index.json CHANGED
@@ -123,6 +123,17 @@
123
123
  "order": 10,
124
124
  "searchable": false
125
125
  },
126
+ {
127
+ "slug": "roadmap",
128
+ "lang": "ja",
129
+ "section": "guide",
130
+ "sectionTitle": "ガイド",
131
+ "title": "ロードマップ",
132
+ "description": "Vantage がこれから向かう方向。エージェントに「目」と「記憶」を与えるための検討中のアイデアと、意図的にやらないこと。",
133
+ "file": "ja/roadmap.md",
134
+ "order": 11,
135
+ "searchable": false
136
+ },
126
137
  {
127
138
  "slug": "components",
128
139
  "lang": "ja",
@@ -804,6 +815,17 @@
804
815
  "order": 10,
805
816
  "searchable": false
806
817
  },
818
+ {
819
+ "slug": "roadmap",
820
+ "lang": "en",
821
+ "section": "guide",
822
+ "sectionTitle": "Guide",
823
+ "title": "Roadmap",
824
+ "description": "Where Vantage is heading — ideas under consideration for giving agents eyes and memory, plus what we deliberately will not do.",
825
+ "file": "en/roadmap.md",
826
+ "order": 11,
827
+ "searchable": false
828
+ },
807
829
  {
808
830
  "slug": "components",
809
831
  "lang": "en",
@@ -8,6 +8,54 @@ Vantage のリリースごとの変更点です。新しいものが上に来ま
8
8
  > まだプロトタイプ(**0.x**)です。0.x の間は **minor が実質のメジャー**扱いで、後方互換性の
9
9
  > ない変更は minor で入ることがあります。patch は後方互換の機能追加とバグ修正のみです。
10
10
 
11
+ ## v0.5.2
12
+
13
+ 再公開でビルドが差し替わったとき、開いたままのタブが古い chunk を取りに行って動かなくなる
14
+ 不具合の修正です。`^0.5.0` を指定していればそのまま入ります。
15
+
16
+ ### 修正
17
+
18
+ - **再公開したあと、開きっぱなしのタブが自動で新しいビルドに追従するようになりました。**
19
+ 公開されたアプリはビルド単位で配信され、HTML は content hash の付いた chunk を指しています。
20
+ アプリを再公開すると、開いたままだったタブは**もう配信されていない古いハッシュの chunk**を
21
+ 要求してしまい、ページ遷移(dynamic import)が
22
+ `Failed to fetch dynamically imported module` で失敗していました ― 画面はエラー表示のまま
23
+ 止まり、手でリロードするまで戻りません。
24
+
25
+ Vite が出す `vite:preloadError`(と、同じ失敗が未捕捉の Promise リジェクションとして出る
26
+ 場合)を捕捉して、**一度だけページをリロード**し、新しいビルドを読み直すようにしました。
27
+ リロードは `sessionStorage` に置いた 10 秒の cooldown で 1 回に制限しているので、chunk が
28
+ 本当に取得できないとき(オフライン、あるいは配信から消えたビルド)でもリロードループには
29
+ ならず、2 回目からは従来どおりエラーバウンダリに落ちます。アプリ側の変更は要りません。
30
+
31
+ ## v0.5.1
32
+
33
+ `dark:` ユーティリティがテーマトークンと同じスイッチに繋がっていなかった不具合の修正です。
34
+ `^0.5.0` を指定していればそのまま入ります。
35
+
36
+ ### 修正
37
+
38
+ - **`dark:` が OS の設定ではなく `.dark` クラスに従うようになりました。** Tailwind の既定の
39
+ `dark:` は `@media (prefers-color-scheme: dark)` ですが、Vantage のトークンは祖先の
40
+ `.dark` / `[data-theme="dark"]` でしか切り替わりません。そのため **OS がダークのブラウザ**では
41
+ テーマが二つに割れていました ― 背景・文字色のトークンはライトのまま、`dark:` の付いた
42
+ クラスだけが全部効いてしまう。白いカードの上で `dark:text-slate-100` の文字が消える、
43
+ バッジだけ黒地に反転する、といった症状が出ます。
44
+
45
+ ```tsx
46
+ // 0.5.0: OS がダークだと、白いカードの上でこの文字が見えなくなっていた
47
+ <div className="text-slate-900 dark:text-slate-100">{name}</div>
48
+ ```
49
+
50
+ `theme.css` で `dark:` をトークンと同じクラスに繋ぎ直しました。**`.dark` /
51
+ `[data-theme="dark"]` を自分で付けない限り、アプリは常にライトで描かれます**
52
+ (→ [UI とテーマ](ui-and-theming))。OS の設定に追従させていたつもりのアプリは、
53
+ ルート要素へ明示的にクラスを付けてください。
54
+
55
+ - **`color-scheme` を宣言するようになりました** ― スクロールバー、`<select>` のポップアップ、
56
+ オートフィルといったブラウザ側の UI が、アプリはライトなのに OS 設定でダークになる、という
57
+ ちぐはぐが解消します。
58
+
11
59
  ## v0.5.0
12
60
 
13
61
  ブラウザから開発ターミナルへの転送を Vite ネイティブの仕組みに置き換えました。転送される
@@ -0,0 +1,196 @@
1
+ # ロードマップ
2
+
3
+ > Vantage がこれから向かう方向。エージェントに「目」と「記憶」を与えるための検討中のアイデアと、意図的にやらないこと。
4
+
5
+ Vantage がこれから向かおうとしている方向をまとめたページです。**すでに動くもの**については
6
+ [変更履歴](changelog)を、いま書けることについては各ガイドを参照してください。
7
+
8
+ > [!WARNING]
9
+ > ここに書かれているものは**まだ実装されていません**。順序も内容も変わりますし、途中で丸ごと
10
+ > 取りやめることもあります。**このページの機能を前提にアプリを書かないでください。**
11
+
12
+ ## 前提 ― 誰のためのフレームワークか
13
+
14
+ Vantage は **AI コーディングエージェントを一級市民**として設計されています。人間が読みやすいこと
15
+ より、エージェントが**間違えずに書けること・自分で検証できること**を優先します。設定ファイルを
16
+ 禁止しているのも、UI キットとデータ層をフレームワークが所有しているのも、`vantage docs` /
17
+ `vantage search` が CLI にあるのも、すべて同じ理由です ―
18
+ **エージェントの窓口は CLI であり、判断の余地は規約で潰す。**
19
+
20
+ この視点で今の Vantage を眺めると、支援が厚いのは 2 箇所です。
21
+
22
+ - **書く前** ― `vantage docs` / `vantage search` / `vantage add skill` / `AGENTS.md`
23
+ - **静的に検証する** ― `vantage check` / `vantage routes`
24
+
25
+ そして、大きな空白が 1 つあります。
26
+
27
+ > **エージェントは、自分が書いた画面を見られない。**
28
+
29
+ `vantage dev` のブラウザ → ターミナル転送(`console.warn` / `console.error` / 未捕捉エラー)は、
30
+ その穴に開いた最初の小さな窓です。以下のアイデアは、ほとんどがこの穴を埋めるためのものです。
31
+
32
+ ## 設計中
33
+
34
+ ### 要素インスペクタ ― 画面からエージェントへ
35
+
36
+ ブラウザ上の要素をホバー・クリックで選び、「**これ**を直して」とエージェントに渡す仕組み。
37
+ バイブコーディングツールでは定番の体験ですが、これをアプリ側の実装ではなく
38
+ **フレームワークの機能**として持ちます。
39
+
40
+ 構成は 3 つです。
41
+
42
+ - **開発時だけ JSX にソース位置を注入する** ― `data-vtg="src/sales/index.tsx:42:7"` と
43
+ コンポーネント名。Vantage は UI キットを自分で所有していて、その大半が `{...props}` を
44
+ そのまま DOM へ渡すため、`<DataTable />` のような要素に付けた属性も実際の DOM まで届きます。
45
+ - **開発時だけのオーバーレイ** ― 要素の輪郭とラベルを描き、選択結果を送出する。
46
+ - **`vantage select`** ― 選択されたものをエージェントが CLI から受け取る。
47
+ `--json` と、選択されるまで待つ `--wait`。
48
+
49
+ 本番ビルドでは属性もオーバーレイも**完全に落とします**(ソースパスがバンドルに残るのは
50
+ 情報漏洩そのものなので)。
51
+
52
+ Squadbase Editor のようにプレビューを iframe で埋め込むツール向けには、選択結果を
53
+ `postMessage` でも流し、オーバーレイの見た目自体をツール側が乗っ取れるようにする方針です。
54
+ Vantage が持つのは「位置情報の正しさ」と最小限の既定 UI までで、体験の作り込みはツール側に
55
+ 譲ります。
56
+
57
+ ## 検討中 ― エージェントに「目」を与える
58
+
59
+ ### `vantage snap` ― ページの構造化スナップショット
60
+
61
+ 各ルートをヘッドレスで描画し、**画像ではなく構造化テキスト**で返すコマンド。
62
+
63
+ ```jsonc
64
+ {
65
+ "route": "/sales",
66
+ "state": "ready", // loading | ready | error | empty
67
+ "regions": [
68
+ { "type": "kpi", "labels": ["今月の売上", "前月比"], "values": ["¥12.4M", "+8.2%"] },
69
+ { "type": "chart", "component": "EChart", "series": ["売上", "目標"], "points": 12 },
70
+ { "type": "table", "component": "DataTable", "columns": ["日付", "店舗", "金額"], "rows": 0 }
71
+ ],
72
+ "console": [{ "level": "warn", "text": "…" }],
73
+ "network": [{ "url": "/api/sales", "status": 500 }]
74
+ }
75
+ ```
76
+
77
+ エージェントにとっては、スクリーンショットの画像よりこの形のほうが桁違いに読めます。
78
+ 「表が 0 行」「API が 500 を返している」は、**人間がスクリーンショットを貼るまで誰も気づかない**
79
+ 種類の失敗です。
80
+
81
+ `regions` に意味づけができるのは、Vantage が UI キット・ルーティング・データ層をすべて
82
+ 所有しているからです。汎用のブラウザ自動化ツールには出せない粒度で、ここが最大の勝ち筋だと
83
+ 考えています。
84
+
85
+ ### `vantage check --runtime` ― 描画して初めてわかる診断
86
+
87
+ いまの [診断](cli-reference) はソースを読むだけの静的なものです。実際に描画しないと
88
+ 分からない失敗を、同じコマンド・同じ終了コードに載せます。
89
+
90
+ - クエリが pending のまま抜けず、ローディング表示から戻らない
91
+ - データ取得は成功しているのに表やチャートが空
92
+ - 描画中に叩いた API が 4xx / 5xx を返した
93
+ - 未処理の Promise リジェクション
94
+ - 要素が親の幅をはみ出している
95
+
96
+ 狙いは「**エージェントの自己検証ループが終了コードで閉じる**」ことです。`vantage check` が
97
+ 通れば出荷してよい、という一本の線を引きたい。
98
+
99
+ ### `vantage dev --agent` ― 構造化イベントストリーム
100
+
101
+ 開発サーバーが、HMR の成否・描画エラー・API のレスポンス・遅いクエリを NDJSON で別チャネルへ
102
+ 出す案。人間向けのターミナル出力は今のままにして、エージェントは追記を読み続けることで
103
+ **編集した直後に壊れたことを知れる**ようにします。いまのコンソール転送の自然な延長線です。
104
+
105
+ ## 検討中 ― エージェントに「記憶」を与える
106
+
107
+ ### `definePage` の `intent`
108
+
109
+ そのページが**何のためにあるか**をフレームワークが構造として持つ案。
110
+
111
+ ```tsx
112
+ export const page = definePage({
113
+ title: "売上ダッシュボード",
114
+ intent: "店舗別の日次売上を見て、目標未達の店舗を早く見つける",
115
+ });
116
+ ```
117
+
118
+ コメントは書き換えの過程で消えますが、これは `vantage routes` に出てマニフェストに載る
119
+ **構造**です。読者は「30 ターン後の自分」や「引き継いだ別のエージェント」。
120
+ `AGENTS.md` や Agent Skills と同じ「**フレームワークが記憶の置き場所を規定する**」という
121
+ 考え方の、ページ単位版にあたります。
122
+
123
+ ### `vantage diff` ― コードの差分ではなくアプリの差分
124
+
125
+ ```text
126
+ routes + /reports/monthly - /legacy
127
+ api ~ GET /api/sales (POST が消えた)
128
+ render /sales 表の列 5 → 4 ("担当者" が消えた)
129
+ bundle +11.2 MB
130
+ ```
131
+
132
+ エージェントは自分の変更の**副作用**を系統的に見落とします。「意図した変更」と「実際に起きた
133
+ 変更」の差を突きつけるためのコマンドです。
134
+
135
+ ### `vantage explain <route|file>`
136
+
137
+ 既存のアプリを引き継いだエージェント向けの、逆向きのドキュメント。そのページのルート・使って
138
+ いるコンポーネント・叩いている API・依存している環境変数・親レイアウトを 1 画面にまとめます。
139
+ `vantage routes --detail` の延長ですが、狙いは「**ソースを読む代わりになる粒度**」で、
140
+ コンテキストの消費を大きく減らすことです。
141
+
142
+ ## 検討中 ― そもそも間違えさせない
143
+
144
+ ### エラーをドキュメントへの入口にする
145
+
146
+ Vantage は自分のスタック(TanStack Query・Base UI・ECharts・Hono)を全部知っています。
147
+ ブラウザのエラーをターミナルへ転送するときに、**次の一手を添える**ことができるはずです。
148
+
149
+ ```text
150
+ [console.error] Cannot read properties of undefined (reading 'map')
151
+ > src/sales/index.tsx:42
152
+ ↳ useApiQuery の data は初回 undefined です。`data?.map` にするか
153
+ isLoading で分岐してください → vantage docs data-fetching
154
+ ```
155
+
156
+ エージェントが最もつまずくのは「何を検索すればいいか分からない」場面なので、
157
+ **エラーが自動的にドキュメントを引く**形にします。
158
+
159
+ ### 「よく間違える」を診断ではなく吸収で潰す
160
+
161
+ Base UI の `Select` の癖を UI キット側で吸収しているのと同じ方針の一般化です。
162
+ **診断が頻繁に鳴るパターンは、診断ではなく「そのまま書いても動く」ようにする候補**と考えます。
163
+ エージェントが間違えた回数そのものを、フレームワークの改善指標として扱いたい。
164
+
165
+ ### `vantage check --budget`
166
+
167
+ バンドルサイズや初期描画の回帰でビルドを止める案。エージェントは重い依存を平気で追加します
168
+ (Markdown レンダラを 1 つ足しただけで出力が 10 MB 以上増えることがあります)。人間なら気づく
169
+ 違和感を、機械が読める閾値に落とします。
170
+
171
+ ### データの形から骨格を作る
172
+
173
+ API のレスポンスや SQL の結果を渡すと、その形に合った可視化の骨格を出す案
174
+ (時系列なら折れ線、カテゴリ × 数値なら表と KPI、など)。エージェントは毎回この判断を
175
+ ゼロからやり直しているので、定石はフレームワークが持つべきだと考えています。
176
+
177
+ ## 意図的にやらないこと
178
+
179
+ ロードマップに載らないことも同じくらい重要なので、現時点の方針を書いておきます。
180
+
181
+ - **設定ファイルの導入** ― `vantage.config` は作りません。設定を足したくなったら、それは
182
+ 規約かフラグで解けないかをまず疑います。判断の余地はエージェントにとって負債です。
183
+ - **スタックの差し替えを可能にすること** ― React・ルーター・データ層・UI キットは
184
+ Vantage が固定します。差し替えを許した瞬間、ドキュメントも Skill も診断も「相手が何を
185
+ 使っているか分からない」前提になり、上に挙げたアイデアはほぼ全部成立しなくなります。
186
+ - **`QueryClient` の丸ごと差し替え** ― カスタマイズの手段はクエリ単位の上書きです
187
+ ([データ取得](data-fetching)を参照)。
188
+ - **汎用のアプリケーションフレームワーク化** ― Vantage は**ダッシュボード**のための
189
+ フレームワークです。用途を広げるより、ダッシュボードで踏む地雷を全部踏み抜いておくことに
190
+ 投資します。
191
+
192
+ ## フィードバック
193
+
194
+ 上のアイデアの優先順位は、実際に詰まった場所で決めたいと考えています。使っていて
195
+ 「エージェントがここで必ず間違える」「これが分からず手が止まった」という箇所があれば、
196
+ ぜひ教えてください。
@@ -78,6 +78,26 @@ import { cn } from "@squadbase/vantage/ui";
78
78
  [`EChart`](parts/echart) の配色も一緒に変わります。チャートは CSS 変数を実際に読み取って
79
79
  テーマを組み立てるためです。
80
80
 
81
+ ## ダークモードは opt-in
82
+
83
+ ダークのトークンが効くのは、祖先に `.dark` か `[data-theme="dark"]` が付いているときだけです。
84
+ **OS やブラウザの設定(`prefers-color-scheme`)は見ません** ― ダッシュボードは埋め込み先の
85
+ 明暗に勝手に追従しないほうが読めるため、切り替えはアプリ側の明示的な指定に任せています。
86
+
87
+ `dark:` ユーティリティも同じスイッチに繋がっています(`theme.css` が
88
+ `@custom-variant dark` でそう定義しています)。`.dark` が無ければ `dark:` は一切効かないので、
89
+ トークンだけライトで `dark:` だけダーク、という食い違いは起きません。
90
+
91
+ ```tsx
92
+ // `.dark` が付いていなければ、下段のクラスは効かない
93
+ <div className="text-foreground dark:text-slate-100" />
94
+ ```
95
+
96
+ > [!NOTE]
97
+ > 色は素の Tailwind パレット(`text-slate-900` など)ではなく、
98
+ > セマンティックトークン(`text-foreground` / `text-muted-foreground` / `bg-card`)で書くのが
99
+ > おすすめです。トークンで書けば `dark:` を並べる必要がそもそも無くなります。
100
+
81
101
  ## ソースを編集したいとき: `vantage add`
82
102
 
83
103
  コンポーネントの中身を自分のものにしたいときは、編集可能なソースを `components/` にコピーします。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squadbase/vantage",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Data dashboard framework for Squadbase Editor. Vite-powered, config-free, one-file first.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,28 @@ Base UI の `SelectValue` は選択中の **value をそのまま描き**、`Sel
35
35
 
36
36
  トリガーに `kanto` や `/sales` と生の値が出たら、まずこれを疑う。
37
37
 
38
+ ## 配色:素のパレット + `dark:` ではなく、セマンティックトークンで書く
39
+
40
+ 色は `text-foreground` / `text-muted-foreground` / `bg-card` / `border-border` などの
41
+ トークンで書く。トークンはライト / ダークの両方を持っているので、`dark:` を並べる必要が無い。
42
+
43
+ `text-slate-900 dark:text-slate-100` のような素の Tailwind パレット + `dark:` の組は、
44
+ **トークンで塗られた周囲と食い違う**。ダークトークンが効くのは祖先に `.dark` /
45
+ `[data-theme="dark"]` が付いているときだけで、`dark:` ユーティリティも同じスイッチに
46
+ 繋がっている(`theme.css` の `@custom-variant dark`)。つまり自分でクラスを付けない限り
47
+ アプリは常にライトで動くのに、素のパレットを使うとその 1 か所だけ手動で両モードを
48
+ 管理することになる。
49
+
50
+ ```tsx
51
+ // ✗ 手動で 2 モードを管理することになる
52
+ <div className="font-medium text-slate-900 dark:text-slate-100">{name}</div>
53
+ // ✓ トークンなら 1 つで済む
54
+ <div className="font-medium text-foreground">{name}</div>
55
+ ```
56
+
57
+ `--muted` / `--accent` / `--secondary` / `--border` は**背景・境界用**で、ライトではほぼ白。
58
+ 文字色に使うと読めなくなる ― 弱い文字色は `text-muted-foreground`。
59
+
38
60
  ## チャート:`EChart` の配色はトークン追従。上書きは `option.color`
39
61
 
40
62
  系列色は `--chart-1..5`、軸・凡例・ツールチップは文字色/境界色のトークンから組まれる
@@ -36,6 +36,11 @@ UI キット・開発サーバー・API サーバー・ビルドはすべて Van
36
36
  - **クライアントに届く env は `PUBLIC_` 接頭辞のものだけ。** `import.meta.env.PUBLIC_FOO`。
37
37
  それ以外を `import.meta.env` で読むと `PUBLIC_ENV_MISUSE` 警告。シークレットは
38
38
  `ApiContext.env`(server/ 内)にだけ届く。
39
+ - **色はセマンティックトークンで書く。** `text-foreground`・`text-muted-foreground`・`bg-card`・
40
+ `border-border`。トークンがライト / ダークの両方を持つので `dark:` を並べる必要が無い。
41
+ 素のパレット + `dark:`(`text-slate-900 dark:text-slate-100`)を混ぜると、その 1 か所だけ
42
+ 手動で 2 モードを管理することになる(→「ダークモードは opt-in」)。`--muted`・`--accent`・
43
+ `--secondary`・`--border` は背景・境界用で、文字色に使うと読めない。
39
44
  - **`.vantage/` と `dist/` は生成物。** 編集しない・読みにいかない(gitignore 済み)。任意の CLI
40
45
  コマンド、または `vantage upgrade` で再生成される。
41
46
  - **`definePage` の値はリテラルで書く。** title/description/navLabel はビルド時に静的抽出される
@@ -318,6 +323,16 @@ const save = useApiMutation<Customer, Payload>("/api/customers", { method: "POST
318
323
  `ui: data-table` と `block: sales-overview` の 2 つだけ。それ以外の名前は import 専用で
319
324
  `add` の対象ではない(未知名で実行すると候補が一覧表示される)。
320
325
 
326
+ ### ダークモードは opt-in
327
+
328
+ ダークのトークンが効くのは、祖先に `.dark` か `[data-theme="dark"]` が付いているときだけ。
329
+ **OS / ブラウザの設定(`prefers-color-scheme`)は見ない。** `dark:` ユーティリティも同じ
330
+ スイッチに繋いであるので(`theme.css` の `@custom-variant dark`)、自分でクラスを付けない限り
331
+ アプリは常にライトで描かれる ― `dark:` を書いてもそこだけダークになることはない。
332
+
333
+ 明暗を切り替えたいときは、レイアウトのルート要素などに `.dark` / `data-theme="dark"` を
334
+ 自分で付ける。
335
+
321
336
  ## ドキュメントを引く(`vantage docs` / `vantage search`)
322
337
 
323
338
  ```bash
package/theme.css CHANGED
@@ -12,6 +12,12 @@
12
12
  on top — charts need distinguishable colours, not a greyscale ramp. */
13
13
 
14
14
  :root {
15
+ /* Native UI (scrollbars, form control popups, autofill) follows the OS unless
16
+ told otherwise. The tokens below don't, so pin it to the same switch —
17
+ otherwise a light app in an OS-dark browser gets dark scrollbars and a dark
18
+ `<select>` popup. */
19
+ color-scheme: light;
20
+
15
21
  --background: oklch(1 0 0);
16
22
  --foreground: oklch(0.145 0 0);
17
23
  --card: oklch(1 0 0);
@@ -51,6 +57,8 @@
51
57
 
52
58
  .dark,
53
59
  [data-theme="dark"] {
60
+ color-scheme: dark;
61
+
54
62
  --background: oklch(0.145 0 0);
55
63
  --foreground: oklch(0.985 0 0);
56
64
  --card: oklch(0.205 0 0);
@@ -145,6 +153,17 @@
145
153
  }
146
154
 
147
155
  /* ------------------------------------------------------- custom variants */
156
+
157
+ /* `dark:` must follow the same switch as the tokens above — the class, not the
158
+ OS setting. Tailwind v4's stock `dark` variant is `@media (prefers-color-scheme:
159
+ dark)`, so leaving it alone splits the theme in two: a browser in OS dark mode
160
+ activates every `dark:` utility while `--background`/`--foreground` stay light,
161
+ because no `.dark` class is present. The result is light-on-light — `dark:text-
162
+ slate-100` over a white card is invisible — and it hits Vantage's own kit too
163
+ (`dark:data-active:bg-input/30` and friends). `:is()` keeps the specificity
164
+ above the unprefixed utility, so `dark:` still wins when the class is set. */
165
+ @custom-variant dark (&:is(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *));
166
+
148
167
  /* Base UI reports state through `data-open`, `data-checked`, … rather than
149
168
  Radix's `data-state="open"`. These variants accept both spellings so the
150
169
  component classes read the same either way. */