@fluid-app/fluid-cli-portal 0.1.0

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 (45) hide show
  1. package/dist/index.d.mts +680 -0
  2. package/dist/index.d.mts.map +1 -0
  3. package/dist/index.mjs +2230 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/package.json +54 -0
  6. package/templates/base/index.html +14 -0
  7. package/templates/base/src/App.tsx +18 -0
  8. package/templates/base/src/index.css +80 -0
  9. package/templates/base/src/main.tsx +42 -0
  10. package/templates/base/src/navigation.config.ts +21 -0
  11. package/templates/base/src/portal.config.ts +32 -0
  12. package/templates/base/src/screens/Dashboard.tsx +133 -0
  13. package/templates/base/src/screens/ExampleForm.tsx +187 -0
  14. package/templates/base/src/vite-env.d.ts +1 -0
  15. package/templates/base/tsconfig.json +26 -0
  16. package/templates/fullstack/.dockerignore +9 -0
  17. package/templates/fullstack/.env.example +15 -0
  18. package/templates/fullstack/.github/workflows/ci.yml +47 -0
  19. package/templates/fullstack/.github/workflows/deploy.yml +54 -0
  20. package/templates/fullstack/Dockerfile +44 -0
  21. package/templates/fullstack/README.md.template +176 -0
  22. package/templates/fullstack/drizzle/0000_initial.sql +7 -0
  23. package/templates/fullstack/drizzle/meta/0000_snapshot.json +63 -0
  24. package/templates/fullstack/drizzle/meta/_journal.json +13 -0
  25. package/templates/fullstack/drizzle.config.ts +13 -0
  26. package/templates/fullstack/esbuild.config.js +14 -0
  27. package/templates/fullstack/eslint.config.js +13 -0
  28. package/templates/fullstack/package.json.template +63 -0
  29. package/templates/fullstack/src/fluid.config.ts.template +69 -0
  30. package/templates/fullstack/src/server/db/index.ts +10 -0
  31. package/templates/fullstack/src/server/db/migrate.ts +12 -0
  32. package/templates/fullstack/src/server/db/schema.ts +14 -0
  33. package/templates/fullstack/src/server/entry.ts +59 -0
  34. package/templates/fullstack/src/server/index.ts +33 -0
  35. package/templates/fullstack/src/server/routes/index.test.ts +123 -0
  36. package/templates/fullstack/src/server/routes/index.ts +109 -0
  37. package/templates/fullstack/src/server/routes/schemas.ts +7 -0
  38. package/templates/fullstack/src/test/setup.ts +9 -0
  39. package/templates/fullstack/vite.config.ts +39 -0
  40. package/templates/fullstack/vitest.config.ts +9 -0
  41. package/templates/starter/.env.example +1 -0
  42. package/templates/starter/README.md.template +218 -0
  43. package/templates/starter/package.json.template +40 -0
  44. package/templates/starter/src/fluid.config.ts.template +69 -0
  45. package/templates/starter/vite.config.ts +12 -0
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import app from "../index";
3
+
4
+ describe("API Routes", () => {
5
+ // Health check
6
+ it("GET /api/health returns 200", async () => {
7
+ const res = await app.request("/api/health");
8
+ expect(res.status).toBe(200);
9
+ const body = await res.json();
10
+ expect(body).toEqual({ status: "ok" });
11
+ });
12
+
13
+ // List todos (empty)
14
+ it("GET /api/todos returns empty array", async () => {
15
+ const res = await app.request("/api/todos");
16
+ expect(res.status).toBe(200);
17
+ const body = await res.json();
18
+ expect(body).toEqual([]);
19
+ });
20
+
21
+ // Create todo
22
+ it("POST /api/todos creates a todo", async () => {
23
+ const res = await app.request("/api/todos", {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({ title: "Test todo" }),
27
+ });
28
+ expect(res.status).toBe(201);
29
+ const body = await res.json();
30
+ expect(body.title).toBe("Test todo");
31
+ expect(body.completed).toBe(false);
32
+ expect(body.id).toBeDefined();
33
+ });
34
+
35
+ // Create invalid (empty body)
36
+ it("POST /api/todos with empty body returns 400", async () => {
37
+ const res = await app.request("/api/todos", {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify({}),
41
+ });
42
+ expect(res.status).toBe(400);
43
+ const body = await res.json();
44
+ expect(body.error).toBe("Validation failed");
45
+ expect(body.details).toBeDefined();
46
+ });
47
+
48
+ // Create invalid (empty title)
49
+ it("POST /api/todos with empty title returns 400", async () => {
50
+ const res = await app.request("/api/todos", {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ title: "" }),
54
+ });
55
+ expect(res.status).toBe(400);
56
+ const body = await res.json();
57
+ expect(body.error).toBe("Validation failed");
58
+ });
59
+
60
+ // Toggle todo
61
+ it("PATCH /api/todos/:id toggles completion", async () => {
62
+ // Create first
63
+ const createRes = await app.request("/api/todos", {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({ title: "Toggle me" }),
67
+ });
68
+ const created = await createRes.json();
69
+
70
+ // Toggle
71
+ const res = await app.request(`/api/todos/${created.id}`, {
72
+ method: "PATCH",
73
+ });
74
+ expect(res.status).toBe(200);
75
+ const body = await res.json();
76
+ expect(body.completed).toBe(true);
77
+ });
78
+
79
+ // Toggle non-existent
80
+ it("PATCH /api/todos/999 returns 404", async () => {
81
+ const res = await app.request("/api/todos/999", { method: "PATCH" });
82
+ expect(res.status).toBe(404);
83
+ });
84
+
85
+ // Toggle bad ID
86
+ it("PATCH /api/todos/abc returns 400", async () => {
87
+ const res = await app.request("/api/todos/abc", { method: "PATCH" });
88
+ expect(res.status).toBe(400);
89
+ const body = await res.json();
90
+ expect(body.error).toBe("Invalid ID");
91
+ });
92
+
93
+ // Delete todo
94
+ it("DELETE /api/todos/:id deletes a todo", async () => {
95
+ const createRes = await app.request("/api/todos", {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({ title: "Delete me" }),
99
+ });
100
+ const created = await createRes.json();
101
+
102
+ const res = await app.request(`/api/todos/${created.id}`, {
103
+ method: "DELETE",
104
+ });
105
+ expect(res.status).toBe(200);
106
+ const body = await res.json();
107
+ expect(body.success).toBe(true);
108
+ });
109
+
110
+ // Delete non-existent
111
+ it("DELETE /api/todos/999 returns 404", async () => {
112
+ const res = await app.request("/api/todos/999", { method: "DELETE" });
113
+ expect(res.status).toBe(404);
114
+ });
115
+
116
+ // Delete bad ID
117
+ it("DELETE /api/todos/abc returns 400", async () => {
118
+ const res = await app.request("/api/todos/abc", { method: "DELETE" });
119
+ expect(res.status).toBe(400);
120
+ const body = await res.json();
121
+ expect(body.error).toBe("Invalid ID");
122
+ });
123
+ });
@@ -0,0 +1,109 @@
1
+ import { Hono } from "hono";
2
+ import { eq, sql } from "drizzle-orm";
3
+ import { db } from "../db/index";
4
+ import { todos } from "../db/schema";
5
+ import { createTodoSchema, todoIdSchema } from "./schemas";
6
+
7
+ export const apiRoutes = new Hono();
8
+
9
+ // GET /api/todos — List all todos
10
+ apiRoutes.get("/todos", async (c) => {
11
+ try {
12
+ const allTodos = await db.select().from(todos).all();
13
+ return c.json(allTodos);
14
+ } catch (error) {
15
+ console.error(error);
16
+ return c.json({ error: "Internal server error" }, 500);
17
+ }
18
+ });
19
+
20
+ // POST /api/todos — Create a new todo
21
+ apiRoutes.post("/todos", async (c) => {
22
+ try {
23
+ const body = await c.req.json();
24
+ const result = createTodoSchema.safeParse(body);
25
+
26
+ if (!result.success) {
27
+ return c.json(
28
+ {
29
+ error: "Validation failed",
30
+ details: result.error.flatten().fieldErrors,
31
+ },
32
+ 400,
33
+ );
34
+ }
35
+
36
+ const newTodo = await db
37
+ .insert(todos)
38
+ .values({ title: result.data.title })
39
+ .returning()
40
+ .get();
41
+ return c.json(newTodo, 201);
42
+ } catch (error) {
43
+ console.error(error);
44
+ return c.json({ error: "Internal server error" }, 500);
45
+ }
46
+ });
47
+
48
+ // PATCH /api/todos/:id — Toggle a todo's completion status
49
+ apiRoutes.patch("/todos/:id", async (c) => {
50
+ try {
51
+ const idResult = todoIdSchema.safeParse(c.req.param("id"));
52
+
53
+ if (!idResult.success) {
54
+ return c.json({ error: "Invalid ID" }, 400);
55
+ }
56
+
57
+ const id = idResult.data;
58
+ const existing = await db
59
+ .select()
60
+ .from(todos)
61
+ .where(eq(todos.id, id))
62
+ .get();
63
+
64
+ if (!existing) {
65
+ return c.json({ error: "Todo not found" }, 404);
66
+ }
67
+
68
+ const updated = await db
69
+ .update(todos)
70
+ .set({
71
+ completed: !existing.completed,
72
+ updatedAt: sql`(current_timestamp)`,
73
+ })
74
+ .where(eq(todos.id, id))
75
+ .returning()
76
+ .get();
77
+ return c.json(updated);
78
+ } catch (error) {
79
+ console.error(error);
80
+ return c.json({ error: "Internal server error" }, 500);
81
+ }
82
+ });
83
+
84
+ // DELETE /api/todos/:id — Delete a todo
85
+ apiRoutes.delete("/todos/:id", async (c) => {
86
+ try {
87
+ const idResult = todoIdSchema.safeParse(c.req.param("id"));
88
+
89
+ if (!idResult.success) {
90
+ return c.json({ error: "Invalid ID" }, 400);
91
+ }
92
+
93
+ const id = idResult.data;
94
+ const deleted = await db
95
+ .delete(todos)
96
+ .where(eq(todos.id, id))
97
+ .returning()
98
+ .get();
99
+
100
+ if (!deleted) {
101
+ return c.json({ error: "Todo not found" }, 404);
102
+ }
103
+
104
+ return c.json({ success: true });
105
+ } catch (error) {
106
+ console.error(error);
107
+ return c.json({ error: "Internal server error" }, 500);
108
+ }
109
+ });
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+
3
+ export const createTodoSchema = z.object({
4
+ title: z.string().min(1).max(500),
5
+ });
6
+
7
+ export const todoIdSchema = z.coerce.number().int().positive();
@@ -0,0 +1,9 @@
1
+ process.env.DATABASE_URL = "file:test.db";
2
+
3
+ import { beforeEach } from "vitest";
4
+ import { db } from "../server/db/index";
5
+ import { todos } from "../server/db/schema";
6
+
7
+ beforeEach(async () => {
8
+ await db.delete(todos);
9
+ });
@@ -0,0 +1,39 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+ import devServer from "@hono/vite-dev-server";
5
+
6
+ export default defineConfig({
7
+ plugins: [
8
+ react(),
9
+ tailwindcss(),
10
+ devServer({
11
+ entry: "src/server/index.ts",
12
+ // Only send /api/* requests to Hono — let Vite handle everything else
13
+ // (static assets, SPA fallback, HMR, etc.)
14
+ exclude: [/^\/(?!api\/).*/, /^\/$/],
15
+ injectClientScript: false,
16
+ }),
17
+ ],
18
+ build: {
19
+ target: "esnext",
20
+ outDir: "dist/public",
21
+ chunkSizeWarningLimit: 600,
22
+ rollupOptions: {
23
+ output: {
24
+ manualChunks(id) {
25
+ if (
26
+ id.includes("node_modules/react-dom") ||
27
+ id.includes("node_modules/react/") ||
28
+ id.includes("node_modules/scheduler")
29
+ ) {
30
+ return "vendor";
31
+ }
32
+ if (id.includes("node_modules/@tanstack/react-query")) {
33
+ return "query";
34
+ }
35
+ },
36
+ },
37
+ },
38
+ },
39
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ include: ["src/**/*.test.ts"],
7
+ setupFiles: ["src/test/setup.ts"],
8
+ },
9
+ });
@@ -0,0 +1 @@
1
+ VITE_API_URL=https://api.fluid.app
@@ -0,0 +1,218 @@
1
+ # {{projectName}}
2
+
3
+ A custom Fluid portal built with [@fluid-app/portal-sdk](https://github.com/fluidcommerce/portal-sdk).
4
+
5
+ This is the **Starter** template - includes a dashboard with multiple widgets plus all core screens.
6
+
7
+ ## What's Included
8
+
9
+ ### Core Screens (Built-in)
10
+ - **Messages** - Conversation management (MessagingScreen)
11
+ - **Contacts** - Contact directory (ContactsScreen)
12
+
13
+ ### Custom Screens
14
+ - **Dashboard** - Overview screen demonstrating:
15
+ - TextWidget - Welcome message and tips
16
+ - ToDoWidget - Task list (uses `useTodos` hook)
17
+ - RecentActivityWidget - Activity feed (uses `useActivities` hook)
18
+ - ChartWidget - Performance visualization
19
+ - CalendarWidget - Event calendar (uses `useCalendarEvents` hook)
20
+
21
+ ## Getting Started
22
+
23
+ ### Development
24
+
25
+ Start the development server:
26
+
27
+ ```bash
28
+ pnpm dev
29
+ ```
30
+
31
+ Open [http://localhost:5173](http://localhost:5173) in your browser.
32
+
33
+ ### Building for Production
34
+
35
+ ```bash
36
+ pnpm build
37
+ ```
38
+
39
+ The output will be in the `dist/` folder.
40
+
41
+ ### Preview Production Build
42
+
43
+ ```bash
44
+ pnpm preview
45
+ ```
46
+
47
+ ## Project Structure
48
+
49
+ ```
50
+ src/
51
+ ├── main.tsx # App entry with FluidProvider setup
52
+ ├── App.tsx # Main app with screen routing
53
+ ├── index.css # Global styles (Tailwind CSS)
54
+ ├── fluid.config.ts # API and auth configuration
55
+ ├── portal.config.ts # Navigation and screen registration
56
+ ├── navigation/
57
+ │ └── index.tsx # Sidebar navigation component
58
+ └── screens/
59
+ └── Dashboard.tsx # Dashboard with multiple widgets
60
+ ```
61
+
62
+ ## Customization
63
+
64
+ ### Modify Navigation
65
+
66
+ Edit `src/portal.config.ts` to add, remove, or reorder navigation items:
67
+
68
+ ```typescript
69
+ navigation: [
70
+ // Your custom screens
71
+ { id: "dashboard", label: "Dashboard", icon: "LayoutDashboard", screen: "custom:dashboard" },
72
+
73
+ // Built-in core screens
74
+ { id: "messaging", label: "Messages", icon: "MessageSquare", screen: "core:messaging" },
75
+ // ...
76
+ ],
77
+ ```
78
+
79
+ ### Add Custom Screens
80
+
81
+ 1. Create a screen component in `src/screens/`:
82
+
83
+ ```tsx
84
+ // src/screens/Orders.tsx
85
+ import { TableWidget, TextWidget } from "@fluid-app/portal-sdk";
86
+
87
+ export function OrdersScreen() {
88
+ return (
89
+ <div className="space-y-6">
90
+ <TextWidget title="Orders" titleEnabled padding={4} borderRadius="lg" />
91
+ {/* Add more widgets */}
92
+ </div>
93
+ );
94
+ }
95
+ ```
96
+
97
+ 2. Register in `portal.config.ts`:
98
+
99
+ ```typescript
100
+ import { OrdersScreen } from "./screens/Orders";
101
+
102
+ customScreens: {
103
+ dashboard: DashboardScreen,
104
+ orders: OrdersScreen, // Add here
105
+ },
106
+ ```
107
+
108
+ 3. Add to navigation:
109
+
110
+ ```typescript
111
+ { id: "orders", label: "Orders", icon: "ShoppingCart", screen: "custom:orders" },
112
+ ```
113
+
114
+ ### Connect to Fluid API
115
+
116
+ Edit `src/fluid.config.ts` to configure your API connection:
117
+
118
+ ```typescript
119
+ export const fluidConfig: FluidSDKConfig = {
120
+ baseUrl: import.meta.env.VITE_API_URL ?? "https://api.fluid.app",
121
+ getAuthToken: () => getStoredToken() ?? null,
122
+ };
123
+ ```
124
+
125
+ ## Available Widgets
126
+
127
+ Import from `@fluid-app/portal-sdk`:
128
+
129
+ | Widget | Description |
130
+ |--------|-------------|
131
+ | `TextWidget` | Text content with optional title |
132
+ | `ChartWidget` | Bar, line, area, pie charts |
133
+ | `TableWidget` | Data tables with columns |
134
+ | `ListWidget` | Lists with icons and actions |
135
+ | `ToDoWidget` | Task list (uses `useTodos`) |
136
+ | `RecentActivityWidget` | Activity feed (uses `useActivities`) |
137
+ | `CalendarWidget` | Calendar view (uses `useCalendarEvents`) |
138
+ | `CatchUpWidget` | Catch-up items |
139
+ | `AlertWidget` | Info, warning, error alerts |
140
+ | `ImageWidget` | Images with sizing options |
141
+ | `VideoWidget` | Embedded video players |
142
+ | `CarouselWidget` | Image/content carousel |
143
+ | `ContainerWidget` | Layout containers |
144
+ | `SpacerWidget` | Vertical spacing |
145
+
146
+ ## Building Forms
147
+
148
+ For data-entry screens (order forms, customer intake, settings), we recommend [React Hook Form](https://react-hook-form.com/) + [Zod](https://zod.dev/) for validation.
149
+
150
+ ### Install form dependencies
151
+
152
+ ```bash
153
+ pnpm add react-hook-form zod @hookform/resolvers
154
+ ```
155
+
156
+ ### Example
157
+
158
+ ```tsx
159
+ import { useForm } from "react-hook-form";
160
+ import { z } from "zod";
161
+ import { zodResolver } from "@hookform/resolvers/zod";
162
+
163
+ const schema = z.object({
164
+ name: z.string().min(1, "Name is required"),
165
+ email: z.string().email("Invalid email"),
166
+ });
167
+
168
+ type FormData = z.infer<typeof schema>;
169
+
170
+ export function MyFormScreen() {
171
+ const {
172
+ register,
173
+ handleSubmit,
174
+ formState: { errors, isSubmitting },
175
+ } = useForm<FormData>({
176
+ resolver: zodResolver(schema),
177
+ });
178
+
179
+ const onSubmit = async (data: FormData) => {
180
+ // Call your API here
181
+ console.log(data);
182
+ };
183
+
184
+ return (
185
+ <form onSubmit={handleSubmit(onSubmit)}>
186
+ <input {...register("name")} placeholder="Name" />
187
+ {errors.name && <p>{errors.name.message}</p>}
188
+
189
+ <input {...register("email")} placeholder="Email" />
190
+ {errors.email && <p>{errors.email.message}</p>}
191
+
192
+ <button type="submit" disabled={isSubmitting}>
193
+ {isSubmitting ? "Submitting..." : "Submit"}
194
+ </button>
195
+ </form>
196
+ );
197
+ }
198
+ ```
199
+
200
+ ### Starter example
201
+
202
+ A complete form example is included at `src/screens/ExampleForm.tsx`. To enable it:
203
+
204
+ 1. Install the form dependencies above
205
+ 2. Uncomment the import and nav entry in `src/portal.config.ts`
206
+
207
+ ## Styling
208
+
209
+ This project uses [Tailwind CSS v4](https://tailwindcss.com/).
210
+
211
+ Edit `src/index.css` to customize the theme or add global styles.
212
+
213
+ ## Learn More
214
+
215
+ - [Fluid Commerce Documentation](https://docs.fluidcommerce.com)
216
+ - [portal-sdk GitHub](https://github.com/fluidcommerce/portal-sdk)
217
+ - [Vite Documentation](https://vite.dev)
218
+ - [React Documentation](https://react.dev)
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@fluid-app/portal-sdk": "{{sdkVersion}}",
14
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
15
+ "@fortawesome/pro-regular-svg-icons": "^6.7.2",
16
+ "@fortawesome/react-fontawesome": "^0.2.2",
17
+ "@tanstack/react-query": "^5.90.0",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0",
20
+ "react-hook-form": "^7.55.0",
21
+ "@hookform/resolvers": "^4.1.3",
22
+ "zod": "^3.24.0"
23
+ },
24
+ "devDependencies": {
25
+ "@tailwindcss/vite": "^4.0.0",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "@vitejs/plugin-react": "^4.3.0",
29
+ "tailwindcss": "^4.0.0",
30
+ "typescript": "^5.6.0",
31
+ "vite": "^6.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "packageManager": "pnpm@10.4.1",
37
+ "pnpm": {
38
+ "onlyBuiltDependencies": ["esbuild"]
39
+ }
40
+ }
@@ -0,0 +1,69 @@
1
+ import type { FluidSDKConfig, FluidAuthConfig } from "@fluid-app/portal-sdk";
2
+ import { getStoredToken } from "@fluid-app/portal-sdk";
3
+
4
+ /**
5
+ * Fluid SDK Configuration
6
+ *
7
+ * This file contains the configuration for connecting to your Fluid Commerce account.
8
+ * Authentication is handled by FluidAuthProvider, which automatically:
9
+ * - Extracts tokens from URL parameters (passed by parent Fluid app)
10
+ * - Stores tokens in cookies and localStorage
11
+ * - Validates token expiration
12
+ */
13
+
14
+ /**
15
+ * Authentication configuration
16
+ *
17
+ * Pattern: safety-satisfies-over-type-annotation
18
+ * Using `satisfies FluidAuthConfig` validates the object shape against the type
19
+ * WITHOUT widening the inferred types. This means:
20
+ * - TypeScript checks that all required properties are present
21
+ * - TypeScript checks that property types are compatible
22
+ * - The actual inferred types remain as specific as possible
23
+ *
24
+ * Compare to type annotation (`const authConfig: FluidAuthConfig = {...}`):
25
+ * - Type annotation widens types to match the interface exactly
26
+ * - `satisfies` preserves literal types while validating structure
27
+ */
28
+ export const authConfig = {
29
+ /**
30
+ * Enable dev-mode auth bypass for local development.
31
+ * This lets you run the portal without a real JWT token.
32
+ * Only active in Vite dev mode (pnpm dev) - automatically disabled in production builds.
33
+ */
34
+ devBypass: true,
35
+
36
+ // Auth failure defaults: SDK automatically redirects to https://auth.fluid.app
37
+ // with a redirect_url back to the current page. To customize:
38
+ // onAuthFailure: () => { window.location.href = "/custom-login"; },
39
+ // authUrl: "https://custom-auth.example.com",
40
+ } satisfies FluidAuthConfig;
41
+
42
+ /**
43
+ * API client configuration
44
+ *
45
+ * Pattern: safety-satisfies-over-type-annotation
46
+ * See authConfig above for explanation of the `satisfies` pattern.
47
+ *
48
+ * This config is passed to FluidProvider to configure the API client.
49
+ * The callbacks (getAuthToken, onAuthError) are called by the SDK
50
+ * when making API requests.
51
+ */
52
+ export const fluidConfig = {
53
+ /**
54
+ * Base URL for the Fluid API
55
+ * Change this to your Fluid instance URL if self-hosted
56
+ */
57
+ baseUrl: import.meta.env.VITE_API_URL ?? "https://api.fluid.app",
58
+
59
+ /**
60
+ * Returns the authentication token for API requests
61
+ * This reads from the token stored by FluidAuthProvider
62
+ */
63
+ getAuthToken: () => {
64
+ return getStoredToken() ?? null;
65
+ },
66
+
67
+ // 401 errors default: SDK automatically redirects to https://auth.fluid.app
68
+ // To customize: onAuthError: () => { clearTokens(); window.location.reload(); },
69
+ } satisfies FluidSDKConfig;
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ build: {
9
+ // Target modern browsers for smaller bundles
10
+ target: "esnext",
11
+ },
12
+ });