rastack 0.0.57 → 0.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +190 -0
  3. package/admin/index.tsx +19 -13
  4. package/admin/ui/BootScreen.tsx +201 -0
  5. package/dist/admin.js +156 -18
  6. package/dist/deploy/ci.js +2 -2
  7. package/dist/deploy/io.js +2 -2
  8. package/dist/dev/harness.d.ts +1 -1
  9. package/dist/dev/harness.js +21 -5
  10. package/dist/external/cache.d.ts +28 -0
  11. package/dist/external/cache.js +35 -0
  12. package/dist/external/envelope.d.ts +35 -0
  13. package/dist/external/envelope.js +48 -0
  14. package/dist/external/graphql.d.ts +25 -0
  15. package/dist/external/graphql.js +78 -0
  16. package/dist/external/handler.d.ts +52 -0
  17. package/dist/external/handler.js +139 -0
  18. package/dist/external/index.d.ts +19 -0
  19. package/dist/external/index.js +38 -0
  20. package/dist/external/lambda.d.ts +49 -0
  21. package/dist/external/lambda.js +86 -0
  22. package/dist/external/rest.d.ts +17 -0
  23. package/dist/external/rest.js +46 -0
  24. package/dist/external/route.d.ts +12 -0
  25. package/dist/external/route.js +33 -0
  26. package/dist/external/service.d.ts +50 -0
  27. package/dist/external/service.js +59 -0
  28. package/dist/external/types.d.ts +107 -0
  29. package/dist/external/types.js +22 -0
  30. package/dist/external/util.d.ts +16 -0
  31. package/dist/external/util.js +58 -0
  32. package/dist/rastack-dev.js +200 -4
  33. package/dist/rastack-wasm-build.d.ts +1 -1
  34. package/dist/rastack-wasm-build.js +6 -6
  35. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  36. package/external.ts +10 -0
  37. package/hooks/query/delete.ts +12 -11
  38. package/hooks/query/fetch.ts +6 -8
  39. package/hooks/query/interfaces.ts +1 -4
  40. package/hooks/query/list.ts +16 -9
  41. package/hooks/query/update.ts +8 -6
  42. package/package.json +25 -2
  43. package/scripts/copy-templates.js +1 -1
  44. package/scripts/copy-wasm.js +1 -1
  45. package/scripts/dev-install.mjs +63 -0
  46. package/src/deploy/ci.ts +2 -2
  47. package/src/deploy/io.ts +2 -2
  48. package/src/dev/harness.ts +21 -5
  49. package/src/external/cache.ts +53 -0
  50. package/src/external/envelope.ts +78 -0
  51. package/src/external/graphql.ts +98 -0
  52. package/src/external/handler.ts +207 -0
  53. package/src/external/index.ts +25 -0
  54. package/src/external/lambda.ts +108 -0
  55. package/src/external/rest.ts +60 -0
  56. package/src/external/route.ts +33 -0
  57. package/src/external/service.ts +86 -0
  58. package/src/external/types.ts +121 -0
  59. package/src/external/util.ts +57 -0
  60. package/src/rastack-dev.ts +233 -6
  61. package/src/rastack-wasm-build.ts +6 -6
  62. package/jest.config.cjs +0 -12
  63. package/test/auth.spec.ts +0 -207
  64. package/test/cache.spec.ts +0 -263
  65. package/test/cognito.spec.ts +0 -281
  66. package/test/compile.spec.ts +0 -240
  67. package/test/components.spec.ts +0 -524
  68. package/test/csv-schema.spec.ts +0 -143
  69. package/test/deploy.spec.ts +0 -571
  70. package/test/dev.spec.ts +0 -414
  71. package/test/entities.spec.ts +0 -597
  72. package/test/import.spec.ts +0 -241
  73. package/test/plugin.spec.ts +0 -315
  74. package/test/runtime-manifest.spec.ts +0 -309
  75. package/test/schema-entities.spec.ts +0 -688
  76. package/test/tokens.spec.ts +0 -302
  77. package/test/transitions.spec.ts +0 -372
  78. package/test/typed-hooks.spec.ts +0 -412
  79. package/test/update.spec.ts +0 -152
  80. package/test/validate.spec.ts +0 -319
  81. package/tsconfig.json +0 -27
  82. package/wasm/package.json +0 -4
  83. package/wasm/rastack_wasm.d.ts +0 -100
  84. package/wasm/rastack_wasm.js +0 -745
  85. package/wasm/rastack_wasm_bg.wasm +0 -0
  86. package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.59](https://github.com/theserverkid/reactapistack/compare/v0.0.58...v0.0.59) (2026-07-18)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **ci:** valid pnpm-workspace.yaml (pnpm 11 moved overrides/onlyBuiltDependencies out of package.json); fixes ERR_PNPM_INVALID_WORKSPACE_CONFIGURATION ([b739531](https://github.com/theserverkid/reactapistack/commit/b739531a34d323afd2a0df3394d95563cfa175a0))
11
+ * **hooks:** type the query compat layer against @tanstack/react-query v5, drop the dead react-query v3 dependency ([6642bc6](https://github.com/theserverkid/reactapistack/commit/6642bc68c917465bd763deaf4a5696983e1dde45))
12
+ * **pkg:** add files allowlist so npm publishes only the framework surface (root-package layout no longer leaks engine/web/docs) ([a6bc9ee](https://github.com/theserverkid/reactapistack/commit/a6bc9ee93d794628be42bac14eef12575a5cdfba))
13
+
14
+ ### [0.0.58](https://github.com/theserverkid/reactapistack/compare/v0.0.57...v0.0.58) (2026-07-18)
15
+
5
16
  ### [0.0.57](https://github.com/theserverkid/reactapistack/compare/v0.0.56...v0.0.57) (2026-07-18)
6
17
 
7
18
  ### [0.0.56](https://github.com/theserverkid/reactapistack/compare/v0.0.55...v0.0.56) (2026-07-18)
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # React API Stack
2
+
3
+ **A full-stack framework on top of object storage. Scale to millions.**
4
+
5
+ React API Stack is not a UI library and not "just" an API package — it is the
6
+ **whole vertical**: the API exposure surface, the database storage layer, and the
7
+ devops that ships them. You author **one TypeScript resource** and the framework
8
+ derives everything from the React data hooks at the top to the Parquet files on
9
+ S3 at the bottom, with a stateless Rust API in between.
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────┐
13
+ │ React / Expo │ API EXPOSURE
14
+ │ useData(Airport) / useForm(Flight) │ SURFACE
15
+ └───────────────────────▲─────────────────────┘
16
+ │ your types, resolved via the manifest
17
+ ┌───────────────────────┴─────────────────────┐
18
+ │ TypeScript resources → `rastack compile` │ API EXPOSURE
19
+ │ FK inferred from types → manifest+OpenAPI │ SURFACE
20
+ └───────────────────────▲─────────────────────┘
21
+ │ schema.rastack.json (manifest)
22
+ ┌───────────────────────┴─────────────────────┐
23
+ │ Rust API (rastack-server, axum) │ API + ADMIN
24
+ │ manifest-driven CRUD · FK validation · │
25
+ │ Django-admin-style /admin DB browser │
26
+ └───────────────────────▲─────────────────────┘
27
+ │ object_store
28
+ ┌───────────────────────┴─────────────────────┐
29
+ │ Apache Iceberg tables on S3 (Parquet) │ DATABASE
30
+ │ snapshots · ACID on object storage │ STORAGE
31
+ └───────────────────────▲─────────────────────┘
32
+ │
33
+ ┌───────────────────────┴─────────────────────┐
34
+ │ AWS Lambda (stateless) + S3 (state) │ DEVOPS
35
+ │ compute/storage separation, IaC, scale-to- │
36
+ │ zero — modelled on SurrealDB's Gen-3 │
37
+ └─────────────────────────────────────────────┘
38
+ ```
39
+
40
+ ## The three surfaces, one framework
41
+
42
+ | Surface | What it is | Package(s) |
43
+ | --- | --- | --- |
44
+ | **API exposure** | TypeScript resources → `rastack compile` (FK inferred from types) → manifest + OpenAPI → pass your type to `useData` / `useForm` (typed TanStack Query hooks, no codegen) | the repo root, published as `rastack` (npm) — `components/`, `hooks/`, `theme/`, … |
45
+ | **Database storage** | A stateless Rust API that stores every model as an Apache Iceberg table on object storage — snapshots, ACID on S3, columnar Parquet | `engine/` (`serve/rastack-server`) |
46
+ | **DevOps** | Serverless Rust on AWS Lambda + S3: stateless compute, state in object storage, infrastructure-as-code, scale-to-zero — driven by `rastack init` / `deploy` / `ci` | `deploy/cloudformation/`, `examples/serverless/` |
47
+
48
+ ## Install the CLI
49
+
50
+ One command, no clone — installs the `rastack` CLI into a per-user dir
51
+ (`~/.rastack`, no sudo), the way you'd install Poetry or rustup. Node.js is the
52
+ only prerequisite.
53
+
54
+ ```bash
55
+ # macOS / Linux
56
+ curl -fsSL https://reactapistack.com/setup.sh | sh
57
+
58
+ # Windows (PowerShell) — from cmd, prefix with: powershell -c "…"
59
+ irm https://reactapistack.com/setup.ps1 | iex
60
+ ```
61
+
62
+ Prefer a package manager, or adding it to a single project? `npm install -g rastack`
63
+ (global CLI) or `npm install -D rastack` (project dependency) work too. Pin a
64
+ version with `RASTACK_VERSION=x.y.z` before the installer.
65
+
66
+ ## Author once, compile the rest
67
+
68
+ ```ts
69
+ // resources/airports.ts
70
+ import { resource, s } from "rastack/define";
71
+
72
+ export const Airport = resource("airports", "airport", {
73
+ code: s.string({ maxLength: 3, unique: true }),
74
+ name: s.string({ maxLength: 120 }),
75
+ });
76
+
77
+ export const Terminal = resource("airports", "terminal", {
78
+ airport: s.ref(() => Airport), // ← foreign key, detected from the type
79
+ label: s.string({ maxLength: 20 }),
80
+ });
81
+ ```
82
+
83
+ ```bash
84
+ rastack compile resources .rastack # TS → schema.rastack.json + openapi.json (FK detected)
85
+ rastack dev # the app + admin, in-browser over the WASM engine
86
+ rastack admin # Django-admin-style DB browser
87
+ ```
88
+
89
+ ```tsx
90
+ // Then just pass your type to the hooks — nothing is generated into your repo.
91
+ const airports = useData(Airport);
92
+ <DataTable query={airports} />
93
+
94
+ const form = useForm(Flight, { id });
95
+ form.transition("board"); // a typed, state-machine-safe save
96
+ ```
97
+
98
+ The compile step runs a real TypeScript type checker and infers relationships
99
+ straight from the field types — see
100
+ [docs/compile-and-relations.md](docs/compile-and-relations.md); the hooks
101
+ resolve your types against the compiled manifest at runtime — see
102
+ [docs/typed-hooks.md](docs/typed-hooks.md). Resources can declare **state
103
+ transitions** (`scheduled → boarding → departed`), compiled into the manifest
104
+ and enforced on every write surface — an illegal jump is a 400 from the form,
105
+ the importer, the server, and the in-browser WASM engine alike.
106
+
107
+ ## Ship it — one command per stage
108
+
109
+ The DevOps surface is a CLI too. A new repo is scaffolded, configured, and
110
+ deployed with three commands, and GitHub Actions stays a one-liner because the
111
+ pipeline lives in the CLI, not the YAML:
112
+
113
+ ```bash
114
+ rastack init # template .github/workflows + .rastack/deploy.json
115
+ rastack deploy # configure the deployment; print the stack launch command
116
+ rastack ci deploy # what CI runs: build the arm64 Lambda, bake the JWKS, ship the code
117
+ ```
118
+
119
+ `init` scaffolds the base GitHub Actions and a deploy config (defaulting the
120
+ GitHub coordinates from your `origin` remote); `deploy` fills that config in and
121
+ prints the one-time `aws cloudformation deploy` plus the repo Variables to wire;
122
+ `ci` runs the build/deploy pipeline so a workflow is just
123
+ `run: rastack ci deploy` (`rastack ci --plan deploy` prints every step). Full
124
+ guide: **[docs/serverless-deploy.md](docs/serverless-deploy.md)**.
125
+
126
+ ## Run the whole API in the browser
127
+
128
+ The Rust API is stateless compute over object storage, so it also compiles to
129
+ **WebAssembly and runs entirely in the browser** — no server, no network. One
130
+ setting on the React provider picks where the API runs; the hooks never
131
+ change:
132
+
133
+ ```tsx
134
+ // Local-first dev: the entire API in the browser, over the data/ Iceberg files.
135
+ <RAStackProvider mode="local" manifest={schema} warehouseBaseUrl="/data/warehouse/">
136
+ <App />
137
+ </RAStackProvider>
138
+
139
+ // Production: the same code on AWS Lambda + S3.
140
+ <RAStackProvider mode="remote" baseURL="https://api.example.com">…</RAStackProvider>
141
+
142
+ // Client-only, reading and writing S3 directly.
143
+ <RAStackProvider mode="s3" manifest={schema} warehouseBaseUrl="https://bucket.s3.amazonaws.com/warehouse/">…</RAStackProvider>
144
+ ```
145
+
146
+ `local` and `s3` run the same `rastack-api-core` request logic (CRUD, foreign-key
147
+ validation, DRF-shaped lists) that Lambda runs — and, because Arrow/Parquet
148
+ compile to WebAssembly, the browser reads and writes the **same Parquet Iceberg
149
+ files** the server does. The local database is just a committed directory of
150
+ those files under `data/`. Full guide:
151
+ **[docs/local-wasm-dev.md](docs/local-wasm-dev.md)**.
152
+
153
+ ## Why "on top of object storage"?
154
+
155
+ Traditional stacks pin you to a server-bound RDBMS. React API Stack instead puts
156
+ **S3 at the bottom of the stack as the system of record** and runs the API as
157
+ **stateless, elastic compute** on top — the same compute/storage-separation
158
+ principle behind [SurrealDB's Gen-3 architecture](https://surrealdb.com/platform/surrealds).
159
+ That gives you:
160
+
161
+ - **Scale to millions** — compute scales horizontally and independently of data;
162
+ storage is S3's 11-nines durability and effectively unbounded capacity.
163
+ - **Scale to zero** — when no requests are in flight, no compute runs and you pay
164
+ only for object storage.
165
+ - **No vendor lock-in** — data lives in open Apache Iceberg / Parquet, readable by
166
+ Spark, DuckDB, Trino, and Polars.
167
+
168
+ ## Start here
169
+
170
+ - **[docs/compile-and-relations.md](docs/compile-and-relations.md)** — the
171
+ TypeScript-first compile step and foreign-key inference.
172
+ - **[docs/typed-hooks.md](docs/typed-hooks.md)** — pass your type to
173
+ `useData` / `useForm`, and state transitions as typesafe backend functions.
174
+ - **[docs/architecture.md](docs/architecture.md)** — the full serverless,
175
+ object-storage architecture and how the three surfaces compose.
176
+ - **[docs/local-wasm-dev.md](docs/local-wasm-dev.md)** — running the whole API in
177
+ the browser via WebAssembly, and the `RAStackProvider` mode switch.
178
+ - **[engine/](engine/)** — the Rust engine: `core/` (manifest, Iceberg storage, request brain) and `serve/` (axum API, server binary, wasm).
179
+ - **[examples/serverless/](examples/serverless/)** — Lambda + S3 + IaC.
180
+ - **[CLAUDE.md](CLAUDE.md)** — codebase guide for contributors.
181
+ - **Building with an LLM?** The documentation site is mirrored as plain Markdown
182
+ for agents — [reactapistack.com/llms.txt](https://reactapistack.com/llms.txt)
183
+ (curated index), [llms-full.txt](https://reactapistack.com/llms-full.txt)
184
+ (every page in one file), or any page at `/docs/<slug>.md`.
185
+
186
+ > **Performance note.** Apache Iceberg over S3 is currently the portable,
187
+ > correct-by-default storage path, not the fastest one. S3 first-byte latency
188
+ > dominates small OLTP reads. The roadmap closes this with a custom S3 backend
189
+ > fronted by a DynamoDB metadata/cache layer (and optionally EFS for hot IO) —
190
+ > see [docs/architecture.md](docs/architecture.md#performance-roadmap).
package/admin/index.tsx CHANGED
@@ -17,12 +17,12 @@ import React, { useEffect, useMemo, useState } from "react";
17
17
  import { createRoot } from "react-dom/client";
18
18
  import { RAStackProvider, useRastackClient } from "../provider";
19
19
  import type { RastackWasmModule } from "../provider/types";
20
- import { Spinner } from "../components";
21
20
  import { ThemeProvider } from "../theme";
22
21
  import { defineTheme } from "../tokens";
23
22
  import { AdminApp } from "./AdminApp";
24
23
  import { parseManifest, type AdminResource } from "./adapters";
25
24
  import { createHttpEngine } from "./httpEngine";
25
+ import { BootScreen } from "./ui/BootScreen";
26
26
 
27
27
  interface AdminConfig {
28
28
  mode: "local" | "remote";
@@ -55,22 +55,19 @@ const loadEngine = async (): Promise<RastackWasmModule> => {
55
55
  return mod;
56
56
  };
57
57
 
58
- function Loading({ label }: { label: string }): React.JSX.Element {
59
- return (
60
- <div className="ras-boot">
61
- <Spinner /> {label}
62
- </div>
63
- );
64
- }
65
-
66
58
  /** Local (WASM) admin: wait for the provider's engine, then render. */
67
59
  function LocalAdmin({ resources }: { resources: AdminResource[] }): React.JSX.Element {
68
60
  const client = useRastackClient();
69
61
  if (client.status === "error")
70
- return <Loading label="The WASM engine failed to start — check the console." />;
62
+ return (
63
+ <BootScreen
64
+ error
65
+ label="The WASM engine failed to start — check the console."
66
+ />
67
+ );
71
68
  if (!client.engine || client.status !== "ready")
72
69
  return (
73
- <Loading
70
+ <BootScreen
74
71
  label={
75
72
  client.status === "seeding"
76
73
  ? "Seeding the local warehouse…"
@@ -108,8 +105,17 @@ function Boot(): React.JSX.Element {
108
105
  }, []);
109
106
 
110
107
  if (error)
111
- return <div className="ras-boot">Failed to load the schema: {error}</div>;
112
- if (!resources || !manifest) return <Loading label="Loading the schema…" />;
108
+ return (
109
+ <ThemeProvider tokens={theme}>
110
+ <BootScreen error label={`Failed to load the schema: ${error}`} />
111
+ </ThemeProvider>
112
+ );
113
+ if (!resources || !manifest)
114
+ return (
115
+ <ThemeProvider tokens={theme}>
116
+ <BootScreen label="Loading the schema…" />
117
+ </ThemeProvider>
118
+ );
113
119
 
114
120
  if (cfg.mode === "remote") {
115
121
  return (
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The admin's boot / loading screen — what you see while the schema loads and
3
+ * the WASM engine spins up. It is deliberately **self-contained**: the main
4
+ * `AdminStyles` sheet is only injected once `<AdminApp>` mounts, so this screen
5
+ * ships its own scoped `<style>` (guarded against double-injection) and leans on
6
+ * the design-token CSS variables with graceful fallbacks, so it looks right in
7
+ * every phase — before the `<ThemeProvider>` is even in the tree, and in both
8
+ * light and dark. It reuses the product `<BrandMark>` and honours
9
+ * `prefers-reduced-motion`.
10
+ */
11
+ import React from "react";
12
+ import { BrandMark } from "./icons";
13
+
14
+ const STYLE_ID = "ras-boot-style";
15
+
16
+ const BOOT_CSS = String.raw`
17
+ .ras-loader {
18
+ min-height: 100vh;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ padding: 24px;
23
+ background:
24
+ radial-gradient(120% 90% at 50% -10%,
25
+ color-mix(in srgb, var(--color-accent, #4F6BFF) 12%, transparent) 0%,
26
+ transparent 60%),
27
+ var(--color-bg, transparent);
28
+ }
29
+ .ras-loader-inner {
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ gap: 22px;
34
+ animation: ras-boot-in 0.5s cubic-bezier(0.22, 0.7, 0.24, 1) both;
35
+ }
36
+ .ras-loader-mark {
37
+ position: relative;
38
+ display: grid;
39
+ place-items: center;
40
+ width: 76px;
41
+ height: 76px;
42
+ }
43
+ /* Soft pulsing halo behind the logo. */
44
+ .ras-loader-mark::before {
45
+ content: "";
46
+ position: absolute;
47
+ inset: 0;
48
+ border-radius: 22px;
49
+ background: color-mix(in srgb, var(--color-accent, #4F6BFF) 45%, transparent);
50
+ filter: blur(16px);
51
+ animation: ras-boot-glow 1.8s var(--ras-ease, cubic-bezier(0.22, 0.7, 0.24, 1)) infinite;
52
+ }
53
+ /* Rotating conic ring that frames the mark. */
54
+ .ras-loader-mark::after {
55
+ content: "";
56
+ position: absolute;
57
+ inset: -7px;
58
+ border-radius: 26px;
59
+ padding: 2px;
60
+ background: conic-gradient(from 0deg,
61
+ transparent 0deg,
62
+ var(--color-accent, #4F6BFF) 90deg,
63
+ transparent 200deg);
64
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
65
+ -webkit-mask-composite: xor;
66
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
67
+ mask-composite: exclude;
68
+ animation: ras-boot-spin 1.4s linear infinite;
69
+ }
70
+ .ras-loader-logo {
71
+ position: relative;
72
+ display: grid;
73
+ place-items: center;
74
+ width: 62px;
75
+ height: 62px;
76
+ border-radius: 18px;
77
+ background: var(--color-surface, var(--color-bg, #fff));
78
+ box-shadow:
79
+ 0 8px 24px color-mix(in srgb, var(--color-accent, #4F6BFF) 22%, transparent),
80
+ 0 1px 0 color-mix(in srgb, var(--color-text, #0B1020) 6%, transparent) inset;
81
+ animation: ras-boot-bob 3s ease-in-out infinite;
82
+ }
83
+ .ras-loader-title {
84
+ font-size: 17px;
85
+ font-weight: 650;
86
+ letter-spacing: -0.01em;
87
+ color: var(--color-text, #0B1020);
88
+ font-family: var(--font-family-sans, "Inter", system-ui, sans-serif);
89
+ }
90
+ .ras-loader-title .ras-loader-admin {
91
+ color: var(--color-accent, #4F6BFF);
92
+ font-weight: 600;
93
+ }
94
+ /* Indeterminate track. */
95
+ .ras-loader-track {
96
+ position: relative;
97
+ width: 172px;
98
+ height: 4px;
99
+ border-radius: 999px;
100
+ overflow: hidden;
101
+ background: color-mix(in srgb, var(--color-text, #0B1020) 10%, transparent);
102
+ }
103
+ .ras-loader-track::after {
104
+ content: "";
105
+ position: absolute;
106
+ top: 0;
107
+ left: 0;
108
+ height: 100%;
109
+ width: 40%;
110
+ border-radius: inherit;
111
+ background: linear-gradient(90deg,
112
+ transparent,
113
+ var(--color-accent, #4F6BFF),
114
+ color-mix(in srgb, var(--color-accent, #4F6BFF) 40%, transparent));
115
+ animation: ras-boot-slide 1.25s cubic-bezier(0.5, 0.1, 0.3, 1) infinite;
116
+ }
117
+ .ras-loader-label {
118
+ margin: -6px 0 0;
119
+ font-size: 13.5px;
120
+ color: var(--color-text-muted, #6b7590);
121
+ font-family: var(--font-family-sans, "Inter", system-ui, sans-serif);
122
+ text-align: center;
123
+ max-width: 320px;
124
+ }
125
+ .ras-loader.is-error .ras-loader-mark::before,
126
+ .ras-loader.is-error .ras-loader-mark::after { animation-play-state: paused; opacity: 0.35; }
127
+ .ras-loader.is-error .ras-loader-label { color: var(--color-danger, #d64550); }
128
+
129
+ @keyframes ras-boot-in {
130
+ from { opacity: 0; transform: translateY(8px) scale(0.985); }
131
+ to { opacity: 1; transform: none; }
132
+ }
133
+ @keyframes ras-boot-spin { to { transform: rotate(360deg); } }
134
+ @keyframes ras-boot-glow {
135
+ 0%, 100% { opacity: 0.45; transform: scale(0.9); }
136
+ 50% { opacity: 0.85; transform: scale(1.08); }
137
+ }
138
+ @keyframes ras-boot-bob {
139
+ 0%, 100% { transform: translateY(0); }
140
+ 50% { transform: translateY(-3px); }
141
+ }
142
+ @keyframes ras-boot-slide {
143
+ 0% { left: -42%; }
144
+ 100% { left: 100%; }
145
+ }
146
+ @media (prefers-reduced-motion: reduce) {
147
+ .ras-loader-inner,
148
+ .ras-loader-mark::before,
149
+ .ras-loader-mark::after,
150
+ .ras-loader-logo,
151
+ .ras-loader-track::after { animation: none; }
152
+ .ras-loader-track::after { left: 0; width: 100%; opacity: 0.7; }
153
+ }
154
+ `;
155
+
156
+ /** Inject the scoped stylesheet exactly once. */
157
+ function useBootStyles(): void {
158
+ if (typeof document === "undefined") return;
159
+ if (document.getElementById(STYLE_ID)) return;
160
+ const el = document.createElement("style");
161
+ el.id = STYLE_ID;
162
+ el.textContent = BOOT_CSS;
163
+ document.head.appendChild(el);
164
+ }
165
+
166
+ export interface BootScreenProps {
167
+ /** The status line shown beneath the mark. */
168
+ label: string;
169
+ /** Render in the error palette (paused halo, danger-coloured label). */
170
+ error?: boolean;
171
+ }
172
+
173
+ /**
174
+ * A branded, animated boot screen — the product logo in a floating tile with a
175
+ * pulsing halo and a rotating ring, the wordmark, an indeterminate progress
176
+ * track, and the current status. Matches the polish of the rest of the admin.
177
+ */
178
+ export function BootScreen({ label, error }: BootScreenProps): React.JSX.Element {
179
+ useBootStyles();
180
+ return (
181
+ <div
182
+ className={`ras-loader${error ? " is-error" : ""}`}
183
+ role="status"
184
+ aria-live="polite"
185
+ aria-busy={!error}
186
+ >
187
+ <div className="ras-loader-inner">
188
+ <div className="ras-loader-mark">
189
+ <div className="ras-loader-logo">
190
+ <BrandMark size={34} />
191
+ </div>
192
+ </div>
193
+ <div className="ras-loader-title">
194
+ React API Stack <span className="ras-loader-admin">Admin</span>
195
+ </div>
196
+ {!error ? <div className="ras-loader-track" aria-hidden="true" /> : null}
197
+ <p className="ras-loader-label">{label}</p>
198
+ </div>
199
+ </div>
200
+ );
201
+ }