@squadbase/vantage 0.5.1 → 0.5.3-dev.71d2349

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.1).** APIs may still change between releases.
10
+ > **Status: prototype (v0.5.3).** APIs may still change between releases.
11
11
 
12
12
  ## Install
13
13
 
@@ -111,8 +111,9 @@ skipped with their location reported instead of copied a second time.
111
111
 
112
112
  ## Documentation
113
113
 
114
- Full guides live in the [Vantage docs](https://github.com/squadbase/vantage-framework/tree/main/docs)
115
- (bilingual — English under `docs/content/en`, Japanese under `docs/content`).
114
+ Full guides live in the
115
+ [Vantage docs](https://github.com/squadbase/squadbase-monorepo/tree/main/packages/vantage/site)
116
+ (bilingual — English under `site/content/en`, Japanese under `site/content`).
116
117
 
117
118
  ## License
118
119
 
@@ -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.1";
13
+ declare const VERSION = "0.5.3";
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.1";
9
+ var VERSION = "0.5.3";
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.1\"\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.3\"\n"]}
@@ -15,6 +15,7 @@ All of these target an agent that is **building a Vantage app**.
15
15
  | `vantage-app` | Creating an app, and growing a single-file `index.tsx` SPA into a fullstack app with `server/api`, nested layouts, dynamic routes, and 404/error pages |
16
16
  | `vantage-add-feature` | Adding a page / API / UI / block (`vantage add` through to `check` / `routes`) |
17
17
  | `vantage-pitfalls` | A pitfalls reference (Base UI ≠ Radix, `SelectValue` behavior, never importing `server/` from the client, and more) |
18
+ | `project-storage` | Working with Squadbase Project Storage (uploaded files) from the browser — listing, embedding images / video / PDFs, uploading. For apps running on Squadbase |
18
19
 
19
20
  ## Placing them
20
21
 
@@ -37,6 +38,7 @@ under the root, under `.claude/skills/`, under `.squadbase/skills/`, it doesn't
37
38
  ```text
38
39
  $ vantage add skill
39
40
  Bundled skills:
41
+ project-storage .claude/skills/project-storage/
40
42
  vantage-add-feature .claude/skills/vantage-add-feature/
41
43
  vantage-app .claude/skills/vantage-app/
42
44
  vantage-pitfalls not placed
@@ -56,7 +58,8 @@ your-app/
56
58
  └── skills/
57
59
  ├── vantage-app/SKILL.md
58
60
  ├── vantage-add-feature/SKILL.md
59
- └── vantage-pitfalls/SKILL.md
61
+ ├── vantage-pitfalls/SKILL.md
62
+ └── project-storage/SKILL.md
60
63
  ```
61
64
 
62
65
  Claude Code discovers `.claude/skills/` automatically. The agent loads and follows the matching
@@ -9,6 +9,44 @@ 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.3
13
+
14
+ Adds a bundled skill for working with Squadbase Project Storage from the browser.
15
+ Anything on `^0.5.0` picks this up as-is.
16
+
17
+ ### Added
18
+
19
+ - **A `project-storage` skill now ships with Vantage.** It covers reading and
20
+ writing Squadbase Project Storage — the files a user uploaded — from a page or
21
+ component through `useStorage()` from `@squadbase/react`: listing files,
22
+ embedding images / video / PDFs (including refreshing the short-lived URL),
23
+ reading file contents, and uploading, deleting or moving.
24
+
25
+ Place it like any other skill with `vantage add skill --all` (add `--dir` to
26
+ choose where it lands). No library code changed, so no app-side change is
27
+ needed.
28
+
29
+ ## v0.5.2
30
+
31
+ Fixes an already-open tab getting stuck on chunks from a build that is no longer
32
+ served. Anything on `^0.5.0` picks this up as-is.
33
+
34
+ ### Fixed
35
+
36
+ - **A tab left open across a re-publish now follows the new build on its own.**
37
+ A published app is served build by build, and its HTML points at
38
+ content-hashed chunks. Re-publish the app and a tab that stayed open asks for a
39
+ **chunk hash the newly-served build no longer has** — the dynamic import behind
40
+ a page transition fails with `Failed to fetch dynamically imported module`, and
41
+ the screen sits on the error until someone reloads by hand.
42
+
43
+ Vantage now listens for Vite's `vite:preloadError` (and for the same failure
44
+ arriving as an unhandled promise rejection) and **reloads the page once** to
45
+ pick up the new build. A 10-second cooldown in `sessionStorage` keeps that to a
46
+ single attempt, so a chunk that is genuinely unreachable — offline, or a build
47
+ that is gone for good — does not turn into a reload loop: the second failure
48
+ surfaces to the error boundary as before. No app-side change is needed.
49
+
12
50
  ## v0.5.1
13
51
 
14
52
  Fixes `dark:` utilities not being wired to the same switch as the theme tokens.
@@ -15,6 +15,7 @@ Vantage は **Claude Code の Agent Skills** を同梱しています。Skill
15
15
  | `vantage-app` | アプリの新規作成と、`index.tsx` 単体の SPA から `server/api`・ネストレイアウト・動的ルート・404/error を備えた fullstack への拡張 |
16
16
  | `vantage-add-feature` | ページ / API / UI / ブロックの追加(`vantage add` + `check` / `routes` までの一連) |
17
17
  | `vantage-pitfalls` | 落とし穴リファレンス(Base UI ≠ Radix、`SelectValue` の挙動、クライアントから `server/` を import しない など) |
18
+ | `project-storage` | Squadbase の Project Storage(アップロード済みファイル)をブラウザから扱う(一覧、画像 / 動画 / PDF の埋め込み、アップロード)。Squadbase 上で動くアプリ向け |
18
19
 
19
20
  ## 配置する
20
21
 
@@ -37,6 +38,7 @@ vantage add skill --all --force # 配置済みのコピーをそ
37
38
  ```text
38
39
  $ vantage add skill
39
40
  Bundled skills:
41
+ project-storage .claude/skills/project-storage/
40
42
  vantage-add-feature .claude/skills/vantage-add-feature/
41
43
  vantage-app .claude/skills/vantage-app/
42
44
  vantage-pitfalls not placed
@@ -56,7 +58,8 @@ your-app/
56
58
  └── skills/
57
59
  ├── vantage-app/SKILL.md
58
60
  ├── vantage-add-feature/SKILL.md
59
- └── vantage-pitfalls/SKILL.md
61
+ ├── vantage-pitfalls/SKILL.md
62
+ └── project-storage/SKILL.md
60
63
  ```
61
64
 
62
65
  `.claude/skills/` は Claude Code が自動で認識します。エージェントはタスクに応じて、対応する
@@ -8,6 +8,41 @@ Vantage のリリースごとの変更点です。新しいものが上に来ま
8
8
  > まだプロトタイプ(**0.x**)です。0.x の間は **minor が実質のメジャー**扱いで、後方互換性の
9
9
  > ない変更は minor で入ることがあります。patch は後方互換の機能追加とバグ修正のみです。
10
10
 
11
+ ## v0.5.3
12
+
13
+ Squadbase の Project Storage をブラウザから扱うための Skill を同梱しました。`^0.5.0` を
14
+ 指定していればそのまま入ります。
15
+
16
+ ### 追加
17
+
18
+ - **`project-storage` skill を同梱しました。** Squadbase の Project Storage(ユーザーが
19
+ アップロードしたファイル)を、`@squadbase/react` の `useStorage()` でページやコンポーネント
20
+ から扱う手順書です。ファイル一覧、画像 / 動画 / PDF の埋め込み(短命な URL の取り直しを
21
+ 含む)、中身の読み取り、アップロードと削除・移動を扱います。
22
+
23
+ 配置は他の skill と同じで `vantage add skill --all`(置き場を選ぶなら `--dir`)。
24
+ ライブラリのコードは変わっていないので、アプリ側の変更は要りません。
25
+
26
+ ## v0.5.2
27
+
28
+ 再公開でビルドが差し替わったとき、開いたままのタブが古い chunk を取りに行って動かなくなる
29
+ 不具合の修正です。`^0.5.0` を指定していればそのまま入ります。
30
+
31
+ ### 修正
32
+
33
+ - **再公開したあと、開きっぱなしのタブが自動で新しいビルドに追従するようになりました。**
34
+ 公開されたアプリはビルド単位で配信され、HTML は content hash の付いた chunk を指しています。
35
+ アプリを再公開すると、開いたままだったタブは**もう配信されていない古いハッシュの chunk**を
36
+ 要求してしまい、ページ遷移(dynamic import)が
37
+ `Failed to fetch dynamically imported module` で失敗していました ― 画面はエラー表示のまま
38
+ 止まり、手でリロードするまで戻りません。
39
+
40
+ Vite が出す `vite:preloadError`(と、同じ失敗が未捕捉の Promise リジェクションとして出る
41
+ 場合)を捕捉して、**一度だけページをリロード**し、新しいビルドを読み直すようにしました。
42
+ リロードは `sessionStorage` に置いた 10 秒の cooldown で 1 回に制限しているので、chunk が
43
+ 本当に取得できないとき(オフライン、あるいは配信から消えたビルド)でもリロードループには
44
+ ならず、2 回目からは従来どおりエラーバウンダリに落ちます。アプリ側の変更は要りません。
45
+
11
46
  ## v0.5.1
12
47
 
13
48
  `dark:` ユーティリティがテーマトークンと同じスイッチに繋がっていなかった不具合の修正です。
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@squadbase/vantage",
3
- "version": "0.5.1",
3
+ "version": "0.5.3-dev.71d2349",
4
4
  "description": "Data dashboard framework for Squadbase Editor. Vite-powered, config-free, one-file first.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Squadbase",
8
- "homepage": "https://github.com/squadbase/vantage-framework/tree/main/packages/vantage#readme",
8
+ "homepage": "https://github.com/squadbase/squadbase-monorepo/tree/main/packages/vantage#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/squadbase/vantage-framework.git",
11
+ "url": "git+https://github.com/squadbase/squadbase-monorepo.git",
12
12
  "directory": "packages/vantage"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/squadbase/vantage-framework/issues"
15
+ "url": "https://github.com/squadbase/squadbase-monorepo/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "vantage",
@@ -25,10 +25,11 @@
25
25
  "tanstack"
26
26
  ],
27
27
  "engines": {
28
- "node": ">=20"
28
+ "node": ">=22"
29
29
  },
30
30
  "publishConfig": {
31
- "access": "public"
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org"
32
33
  },
33
34
  "bin": {
34
35
  "vantage": "./dist/cli.js"
@@ -93,7 +94,12 @@
93
94
  "dev": "tsup --watch --no-dts",
94
95
  "dev:types": "tsc --noEmit --watch",
95
96
  "typecheck": "tsc --noEmit",
96
- "prepublishOnly": "node scripts/build-docs.mjs && node scripts/build.mjs"
97
+ "type-check": "tsc --noEmit",
98
+ "prepublishOnly": "node scripts/build-docs.mjs && node scripts/build.mjs",
99
+ "release": "npm publish --@squadbase:registry=https://registry.npmjs.org",
100
+ "site:install": "npm --prefix site install",
101
+ "site:dev": "npm --prefix site run dev",
102
+ "site:build": "npm --prefix site run build"
97
103
  },
98
104
  "dependencies": {
99
105
  "@base-ui/react": "^1.6.0",
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: project-storage
3
+ description: Squadbase の Project Storage(アップロード済みファイル)を @squadbase/react の useStorage() でブラウザから扱う手順。ファイル一覧、画像 / 動画 / PDF の埋め込み、中身の読み取り、アップロードと削除・移動。ページやコンポーネントがアップロード済みファイルを表示・埋め込み・アップロードするときに参照する。
4
+ ---
5
+
6
+ # Project Storage(ブラウザ側)
7
+
8
+ Squadbase の **Project Storage** を React コンポーネントから読み書きする手順。Squadbase 上で
9
+ 動くアプリでは資格情報も設定も要らず、そのまま動く。サーバー(`server/api/**`)からストレージを
10
+ 触る API は別物で、ここでは扱わない。
11
+
12
+ ## import は `@squadbase/react` から直接
13
+
14
+ 「import は `@squadbase/vantage` のサブパス経由」はフレームワークが所有するものの話。
15
+ **`@squadbase/react` は Squadbase プラットフォームの SDK なので直接 import する**
16
+ (`SquadbaseProvider` / `useUser` と同じ)。
17
+
18
+ ```tsx
19
+ import { useStorage } from "@squadbase/react"
20
+
21
+ const storage = useStorage()
22
+ // 呼び出しはすべて storage.uploads.* の下
23
+ ```
24
+
25
+ `useStorage()` が返すクライアントは安定なので、コンポーネント先頭で呼んで `useQuery` /
26
+ `useMutation` の中で使ってよい。データ取得のフックは `@squadbase/vantage/query` から取る
27
+ (`@tanstack/*` は直接 import しない)。
28
+
29
+ ## API
30
+
31
+ すべて async。`key` はストレージ内の**相対**パス(例: `reports/2025-q1.pdf`)で、先頭に
32
+ スラッシュを付けない。
33
+
34
+ 読み取り:
35
+
36
+ - `list(prefix?, options?)` → `{ objects, commonPrefixes, nextCursor? }`。`objects` は
37
+ `{ key, size, lastModified }[]`。`options.delimiter = "/"` でフォルダ表示(サブフォルダが
38
+ `commonPrefixes` に入る)、省略すると再帰的なフラット一覧。`options.limit` / `options.cursor`
39
+ でページング(`cursor` は前回の `nextCursor`)
40
+ - `head(key)` → `{ key, size, contentType, createdAt }`。メタ情報だけで本体は取らない
41
+ - `getUrl(key)` → `{ url, expiresAt }`。埋め込み用の一時 URL
42
+ - `get(key)` → 生の `Response`。`.json()` / `.text()` / `.blob()` / `.arrayBuffer()` で読む
43
+
44
+ 書き込み:
45
+
46
+ - `put(key, data, options?)` → `void`。`data` は `Blob`(`File` を含む) / `ArrayBuffer` /
47
+ `Uint8Array` / `string`
48
+ - `delete(key)` → `void`
49
+ - `copy(srcKey, destKey, options?)` / `move(srcKey, destKey, options?)` → `void`
50
+
51
+ ## 一覧を出す
52
+
53
+ ```tsx
54
+ import { useStorage } from "@squadbase/react"
55
+ import { useQuery } from "@squadbase/vantage/query"
56
+
57
+ const storage = useStorage()
58
+ const { data, isPending, isError } = useQuery({
59
+ queryKey: ["storage", "list", prefix],
60
+ queryFn: () => storage.uploads.list(prefix, { delimiter: "/" }),
61
+ })
62
+ ```
63
+
64
+ `data.commonPrefixes` をフォルダ、`data.objects` をファイルとして描く。プレフィックス配下を
65
+ まとめて出すなら `delimiter` を省く。
66
+
67
+ ## 画像 / 動画 / PDF を埋め込む
68
+
69
+ 埋め込みには URL が要るので `getUrl` を使う。**URL には期限がある**ので、切れる前に取り直す:
70
+
71
+ ```tsx
72
+ import { useStorage } from "@squadbase/react"
73
+ import { useQuery } from "@squadbase/vantage/query"
74
+
75
+ function useStorageUrl(key: string) {
76
+ const storage = useStorage()
77
+ return useQuery({
78
+ queryKey: ["storage", "url", key],
79
+ queryFn: () => storage.uploads.getUrl(key),
80
+ refetchInterval: (query) => {
81
+ const expiresAt = query.state.data?.expiresAt
82
+ if (!expiresAt) return false
83
+ // 期限の 60 秒前に取り直す
84
+ return Math.max(0, new Date(expiresAt).getTime() - Date.now() - 60_000)
85
+ },
86
+ })
87
+ }
88
+ ```
89
+
90
+ ```tsx
91
+ import { ErrorState, Loading } from "@squadbase/vantage/ui"
92
+
93
+ const { data, isPending, isError } = useStorageUrl("reports/cover.png")
94
+ if (isPending) return <Loading />
95
+ if (isError) return <ErrorState message="ファイルを読み込めませんでした" />
96
+ return <img src={data.url} alt="" className="w-full rounded-lg" />
97
+ ```
98
+
99
+ 動画は `<video src={data.url} controls className="w-full rounded-lg" />`、PDF は
100
+ `<iframe src={data.url} className="h-[80vh] w-full rounded-lg border border-border" />`。
101
+
102
+ ## 中身を JS で読む
103
+
104
+ ```tsx
105
+ const { data } = useQuery({
106
+ queryKey: ["storage", "content", key],
107
+ queryFn: async () => {
108
+ const res = await storage.uploads.get(key)
109
+ return (await res.json()) as Row[] // または res.text()
110
+ },
111
+ })
112
+ ```
113
+
114
+ ## アップロードする
115
+
116
+ ```tsx
117
+ import { useStorage } from "@squadbase/react"
118
+ import { useMutation, useQueryClient } from "@squadbase/vantage/query"
119
+
120
+ const storage = useStorage()
121
+ const queryClient = useQueryClient()
122
+ const upload = useMutation({
123
+ mutationFn: (file: File) =>
124
+ storage.uploads.put(`uploads/${file.name}`, file, { contentType: file.type }),
125
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage", "list"] }),
126
+ })
127
+
128
+ <input
129
+ type="file"
130
+ onChange={(e) => {
131
+ const file = e.target.files?.[0]
132
+ if (file) upload.mutate(file)
133
+ }}
134
+ />
135
+ ```
136
+
137
+ `File` はそのまま `put` に渡す。書き込み後は該当する `["storage", "list", …]` を invalidate して
138
+ UI を追従させる。
139
+
140
+ ## 落とし穴
141
+
142
+ - **`getUrl` の URL は短命。** 結果を永続化したり長寿命のキャッシュに入れたりしない。
143
+ `expiresAt` の前に取り直す(上のフック)。`<img src>` に一度入れて放置すると、開いたままの
144
+ 画面で後から読み込まれた分が失敗する
145
+ - **`put` の `contentType` の既定は `application/octet-stream`。** 後で埋め込むもの(画像・動画・
146
+ PDF)には `file.type` を必ず渡す。付けないとブラウザが表示せずダウンロードに落ちる
147
+ - **`key` は相対パス。** 先頭にスラッシュを付けない。`getUrl` を通さず `<img src="/foo.png">` の
148
+ ような静的パスで参照しない ― ストレージはアプリの `public/` ではない
149
+ - **`copy` / `move` は既定で既存の宛先を上書きする。** 残したいときは `{ overwrite: false }`
150
+ - **サーバー(`server/api/**`)からは呼ばない。** `useStorage` はブラウザ側の API
151
+
152
+ ---
153
+
154
+ ## 関連スキル
155
+
156
+ - `vantage-add-feature` — ページ / API / コンポーネントを 1 つ足す手順
157
+ - `vantage-pitfalls` — UI・配色・ルーティングの落とし穴
@@ -379,6 +379,7 @@ body・レスポンスの status と形)まで出る。`vantage check --json` /
379
379
  - **`vantage-app`** — アプリを一から作る / 構成を広げるときの進め方と検証の順序
380
380
  - **`vantage-add-feature`** — 既存アプリに page / api / ui を 1 つ足す定型
381
381
  - **`vantage-pitfalls`** — Base UI(≠ Radix)の癖など、静かに壊れる落とし穴のリファレンス
382
+ - **`project-storage`** — Squadbase の Project Storage(アップロード済みファイル)をブラウザから読み書きする
382
383
 
383
384
  手順書の本体は `SKILL.md` というファイルで、置き場所はアプリによって違う(ルート直下・
384
385
  `.claude/skills/`・`.squadbase/skills/` など)。**探す必要はない ― CLI が探す:**