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.
- package/CHANGELOG.md +11 -0
- package/README.md +190 -0
- package/admin/index.tsx +19 -13
- package/admin/ui/BootScreen.tsx +201 -0
- package/dist/admin.js +156 -18
- package/dist/deploy/ci.js +2 -2
- package/dist/deploy/io.js +2 -2
- package/dist/dev/harness.d.ts +1 -1
- package/dist/dev/harness.js +21 -5
- package/dist/external/cache.d.ts +28 -0
- package/dist/external/cache.js +35 -0
- package/dist/external/envelope.d.ts +35 -0
- package/dist/external/envelope.js +48 -0
- package/dist/external/graphql.d.ts +25 -0
- package/dist/external/graphql.js +78 -0
- package/dist/external/handler.d.ts +52 -0
- package/dist/external/handler.js +139 -0
- package/dist/external/index.d.ts +19 -0
- package/dist/external/index.js +38 -0
- package/dist/external/lambda.d.ts +49 -0
- package/dist/external/lambda.js +86 -0
- package/dist/external/rest.d.ts +17 -0
- package/dist/external/rest.js +46 -0
- package/dist/external/route.d.ts +12 -0
- package/dist/external/route.js +33 -0
- package/dist/external/service.d.ts +50 -0
- package/dist/external/service.js +59 -0
- package/dist/external/types.d.ts +107 -0
- package/dist/external/types.js +22 -0
- package/dist/external/util.d.ts +16 -0
- package/dist/external/util.js +58 -0
- package/dist/rastack-dev.js +200 -4
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +6 -6
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/external.ts +10 -0
- package/hooks/query/delete.ts +12 -11
- package/hooks/query/fetch.ts +6 -8
- package/hooks/query/interfaces.ts +1 -4
- package/hooks/query/list.ts +16 -9
- package/hooks/query/update.ts +8 -6
- package/package.json +25 -2
- package/scripts/copy-templates.js +1 -1
- package/scripts/copy-wasm.js +1 -1
- package/scripts/dev-install.mjs +63 -0
- package/src/deploy/ci.ts +2 -2
- package/src/deploy/io.ts +2 -2
- package/src/dev/harness.ts +21 -5
- package/src/external/cache.ts +53 -0
- package/src/external/envelope.ts +78 -0
- package/src/external/graphql.ts +98 -0
- package/src/external/handler.ts +207 -0
- package/src/external/index.ts +25 -0
- package/src/external/lambda.ts +108 -0
- package/src/external/rest.ts +60 -0
- package/src/external/route.ts +33 -0
- package/src/external/service.ts +86 -0
- package/src/external/types.ts +121 -0
- package/src/external/util.ts +57 -0
- package/src/rastack-dev.ts +233 -6
- package/src/rastack-wasm-build.ts +6 -6
- package/jest.config.cjs +0 -12
- package/test/auth.spec.ts +0 -207
- package/test/cache.spec.ts +0 -263
- package/test/cognito.spec.ts +0 -281
- package/test/compile.spec.ts +0 -240
- package/test/components.spec.ts +0 -524
- package/test/csv-schema.spec.ts +0 -143
- package/test/deploy.spec.ts +0 -571
- package/test/dev.spec.ts +0 -414
- package/test/entities.spec.ts +0 -597
- package/test/import.spec.ts +0 -241
- package/test/plugin.spec.ts +0 -315
- package/test/runtime-manifest.spec.ts +0 -309
- package/test/schema-entities.spec.ts +0 -688
- package/test/tokens.spec.ts +0 -302
- package/test/transitions.spec.ts +0 -372
- package/test/typed-hooks.spec.ts +0 -412
- package/test/update.spec.ts +0 -152
- package/test/validate.spec.ts +0 -319
- package/tsconfig.json +0 -27
- package/wasm/package.json +0 -4
- package/wasm/rastack_wasm.d.ts +0 -100
- package/wasm/rastack_wasm.js +0 -745
- package/wasm/rastack_wasm_bg.wasm +0 -0
- 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
|
|
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
|
-
<
|
|
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
|
|
112
|
-
|
|
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
|
+
}
|