robodev 0.11.0 → 0.12.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.
- package/package.json +1 -1
- package/src/collect-files.test.ts +19 -0
- package/src/collect-files.ts +1 -0
- package/src/config.test.ts +26 -0
- package/src/config.ts +34 -5
- package/src/emit-starter.test.ts +54 -0
- package/src/emit-starter.ts +47 -13
- package/src/index.ts +37 -4
- package/templates/auth-chat/package.json +1 -1
- package/templates/auth-chat/src/main.tsx +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/catalog.json +1 -1
- package/templates/empty/package.json +1 -1
- package/templates/space/.config/local.spa.template.yml +27 -0
- package/templates/space/.oxlint-base.json +29 -0
- package/templates/space/.oxlintrc.json +78 -0
- package/templates/space/.rulesync/rules/frontend-api-boundary.md +36 -0
- package/templates/space/.rulesync/rules/frontend-component-structure.md +38 -0
- package/templates/space/.rulesync/rules/frontend-forms.md +32 -0
- package/templates/space/.rulesync/rules/frontend-notifications.md +24 -0
- package/templates/space/.rulesync/rules/frontend-povio-components.md +42 -0
- package/templates/space/.rulesync/rules/frontend-povio-ui.md +45 -0
- package/templates/space/.rulesync/rules/frontend-query-autocomplete.md +29 -0
- package/templates/space/.rulesync/rules/frontend-tables-and-lists.md +34 -0
- package/templates/space/.rulesync/rules/frontend-translations.md +26 -0
- package/templates/space/.rulesync/rules/project-overview.md +32 -0
- package/templates/space/.rulesync/rules/robodev-api.md +30 -0
- package/templates/space/.rulesync/rules/robodev-auth.md +40 -0
- package/templates/space/.rulesync/rules/robodev-database.md +22 -0
- package/templates/space/.rulesync/rules/role-based-app-structure.md +36 -0
- package/templates/space/.rulesync/skills/media-feature/SKILL.md +21 -0
- package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
- package/templates/space/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
- package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
- package/templates/space/.rulesync/skills/robodev-table/SKILL.md +12 -0
- package/templates/space/README.md +26 -7
- package/templates/space/api/_lib.ts +359 -0
- package/templates/space/api/aliens/labels.ts +19 -0
- package/templates/space/api/files/presigned-url.ts +48 -0
- package/templates/space/api/files/upload.ts +29 -0
- package/templates/space/api/planets/[id]/like.ts +36 -0
- package/templates/space/api/planets/[id].ts +56 -13
- package/templates/space/api/planets/paginate.ts +36 -0
- package/templates/space/api/planets.ts +52 -44
- package/templates/space/apps/fe/index.html +24 -0
- package/templates/space/apps/fe/openapi-codegen.config.ts +37 -0
- package/templates/space/apps/fe/package.json +82 -0
- package/templates/space/apps/fe/public/apple-touch-icon.png +0 -0
- package/templates/space/apps/fe/public/favicon-96x96.png +0 -0
- package/templates/space/apps/fe/public/favicon.ico +0 -0
- package/templates/space/apps/fe/public/favicon.svg +3 -0
- package/templates/space/apps/fe/public/site.webmanifest +21 -0
- package/templates/space/apps/fe/public/web-app-manifest-192x192.png +0 -0
- package/templates/space/apps/fe/public/web-app-manifest-512x512.png +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
- package/templates/space/apps/fe/src/assets/locales/en/translation.json +301 -0
- package/templates/space/apps/fe/src/assets/locales/sl/translation.json +301 -0
- package/templates/space/apps/fe/src/clients/app-rest-client.ts +15 -0
- package/templates/space/apps/fe/src/clients/auth-token-store.ts +101 -0
- package/templates/space/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
- package/templates/space/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
- package/templates/space/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
- package/templates/space/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
- package/templates/space/apps/fe/src/components/404.tsx +18 -0
- package/templates/space/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
- package/templates/space/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
- package/templates/space/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
- package/templates/space/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
- package/templates/space/apps/fe/src/components/features/home/HomePage.tsx +21 -0
- package/templates/space/apps/fe/src/components/features/planets/details/PlanetDetailsPage.tsx +139 -0
- package/templates/space/apps/fe/src/components/features/planets/details/PlanetEditPage.tsx +266 -0
- package/templates/space/apps/fe/src/components/features/planets/feed/PlanetFeedCard.tsx +131 -0
- package/templates/space/apps/fe/src/components/features/planets/feed/PlanetsFeedPage.tsx +119 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetCard.tsx +89 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx +179 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.css +3 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.tsx +73 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsGridPage.tsx +51 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsInfiniteTablePage.tsx +79 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsPage.tsx +65 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTablePage.tsx +72 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTableSummary.tsx +29 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsViewTabs.tsx +79 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTable.tsx +30 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableActions.tsx +64 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableInfinite.tsx +36 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/planetsTableColumns.tsx +72 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/usePlanetTableFilters.tsx +102 -0
- package/templates/space/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
- package/templates/space/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
- package/templates/space/apps/fe/src/components/layout/AppLayout.tsx +19 -0
- package/templates/space/apps/fe/src/components/layout/app-header/AppHeader.tsx +93 -0
- package/templates/space/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +53 -0
- package/templates/space/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
- package/templates/space/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
- package/templates/space/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
- package/templates/space/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
- package/templates/space/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
- package/templates/space/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
- package/templates/space/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
- package/templates/space/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
- package/templates/space/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
- package/templates/space/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
- package/templates/space/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
- package/templates/space/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
- package/templates/space/apps/fe/src/components/shared/ui/Card.tsx +60 -0
- package/templates/space/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
- package/templates/space/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
- package/templates/space/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
- package/templates/space/apps/fe/src/config/app.config.ts +35 -0
- package/templates/space/apps/fe/src/config/i18n.ts +43 -0
- package/templates/space/apps/fe/src/config/inits/a11y.ts +14 -0
- package/templates/space/apps/fe/src/config/inits/logger.ts +7 -0
- package/templates/space/apps/fe/src/config/inits/sentry.ts +21 -0
- package/templates/space/apps/fe/src/config/jwt.config.ts +2 -0
- package/templates/space/apps/fe/src/config/query.config.ts +20 -0
- package/templates/space/apps/fe/src/hooks/useAuth.ts +5 -0
- package/templates/space/apps/fe/src/main.tsx +52 -0
- package/templates/space/apps/fe/src/pages/(guest)/login.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(guest)/register.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(guest)/route.tsx +18 -0
- package/templates/space/apps/fe/src/pages/(private)/index.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(private)/planets/$id/edit.tsx +53 -0
- package/templates/space/apps/fe/src/pages/(private)/planets/$id/index.tsx +41 -0
- package/templates/space/apps/fe/src/pages/(private)/planets/index.tsx +24 -0
- package/templates/space/apps/fe/src/pages/(private)/planets-feed.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(private)/profile.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(private)/route.tsx +18 -0
- package/templates/space/apps/fe/src/pages/(public)/auth.tsx +37 -0
- package/templates/space/apps/fe/src/pages/(public)/route.tsx +9 -0
- package/templates/space/apps/fe/src/pages/__root.tsx +164 -0
- package/templates/space/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
- package/templates/space/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
- package/templates/space/apps/fe/src/providers/index.tsx +44 -0
- package/templates/space/apps/fe/src/providers/jwt.provider.tsx +95 -0
- package/templates/space/apps/fe/src/routeTree.gen.ts +309 -0
- package/templates/space/apps/fe/src/styles/base.css +103 -0
- package/templates/space/apps/fe/src/styles/fonts/fonts.tsx +10 -0
- package/templates/space/apps/fe/src/styles/fonts/general-sans.css +31 -0
- package/templates/space/apps/fe/src/styles/globals.css +28 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
- package/templates/space/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
- package/templates/space/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
- package/templates/space/apps/fe/src/styles/theme.css +2177 -0
- package/templates/space/apps/fe/src/types/i18next.d.ts +12 -0
- package/templates/space/apps/fe/src/types/table.d.ts +17 -0
- package/templates/space/apps/fe/src/types/ui.d.ts +26 -0
- package/templates/space/apps/fe/src/types/vite-env.d.ts +20 -0
- package/templates/space/apps/fe/src/utils/date.utils.ts +17 -0
- package/templates/space/apps/fe/src/utils/image-fallback.ts +9 -0
- package/templates/space/apps/fe/src/utils/media-upload.ts +63 -0
- package/templates/space/apps/fe/src/utils/number.utils.ts +12 -0
- package/templates/space/apps/fe/src/utils/string.utils.ts +5 -0
- package/templates/space/apps/fe/src/vite-env.d.ts +1 -0
- package/templates/space/apps/fe/tsconfig.app.json +32 -0
- package/templates/space/apps/fe/tsconfig.json +9 -0
- package/templates/space/apps/fe/tsconfig.node.json +31 -0
- package/templates/space/apps/fe/vite.config.ts +94 -0
- package/templates/space/database.ts +33 -13
- package/templates/space/openapi.json +1819 -0
- package/templates/space/oxfmt.config.js +23 -0
- package/templates/space/package.json +6 -13
- package/templates/space/rulesync.jsonc +18 -0
- package/templates/space/tsconfig.json +2 -4
- package/templates/space/api/health.ts +0 -7
- package/templates/space/api/launch.ts +0 -29
- package/templates/space/api/me.ts +0 -14
- package/templates/space/api/motto.ts +0 -9
- package/templates/space/api/rockets.ts +0 -43
- package/templates/space/index.html +0 -138
- package/templates/space/src/main.tsx +0 -229
- package/templates/space/vite.config.ts +0 -7
package/package.json
CHANGED
|
@@ -45,3 +45,22 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
45
45
|
await rm(root, { recursive: true, force: true });
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
test("collectFiles skips nested apps/fe sources", async () => {
|
|
50
|
+
const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
|
|
51
|
+
try {
|
|
52
|
+
await writeFile(join(root, "database.ts"), "export default {}");
|
|
53
|
+
await mkdir(join(root, "apps/fe/src"), { recursive: true });
|
|
54
|
+
await writeFile(join(root, "apps/fe/src/main.tsx"), "export {}");
|
|
55
|
+
await writeFile(join(root, "apps/fe/index.html"), "<html></html>");
|
|
56
|
+
await writeFile(join(root, "apps/fe/package.json"), "{}");
|
|
57
|
+
const files = await collectFiles(root);
|
|
58
|
+
const paths = new Set(files.map((file) => file.path));
|
|
59
|
+
assert.ok(paths.has("database.ts"));
|
|
60
|
+
assert.ok(!paths.has("apps/fe/src/main.tsx"));
|
|
61
|
+
assert.ok(!paths.has("apps/fe/index.html"));
|
|
62
|
+
assert.ok(!paths.has("apps/fe/package.json"));
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(root, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
package/src/collect-files.ts
CHANGED
|
@@ -86,6 +86,7 @@ function isUnsafe(path: string): boolean {
|
|
|
86
86
|
function isDenied(path: string): boolean {
|
|
87
87
|
const top = path.split("/")[0];
|
|
88
88
|
if (SKIP_DIRS.has(top)) return true;
|
|
89
|
+
if (path === "apps" || path.startsWith("apps/")) return true;
|
|
89
90
|
if (path === ".env" || path.startsWith(".env.")) return true;
|
|
90
91
|
const name = basename(path);
|
|
91
92
|
if (name.startsWith(".") && name !== ".gitignore") return true;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { writeViteApiUrl } from "./config.js";
|
|
7
|
+
|
|
8
|
+
test("writeViteApiUrl sets Vite and Povio public API URLs", async () => {
|
|
9
|
+
const root = await mkdtemp(join(tmpdir(), "rd-env-"));
|
|
10
|
+
try {
|
|
11
|
+
await mkdir(join(root, ".config"), { recursive: true });
|
|
12
|
+
await writeFile(
|
|
13
|
+
join(root, ".config/local.spa.template.yml"),
|
|
14
|
+
`APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "http://localhost:4000"\nVITE_PUBLIC_API_URL: *APP_PUBLIC_API_URL\n`,
|
|
15
|
+
);
|
|
16
|
+
await writeViteApiUrl("https://prj.example.test", root);
|
|
17
|
+
const env = await readFile(join(root, ".env"), "utf8");
|
|
18
|
+
assert.match(env, /^VITE_API_URL=https:\/\/prj\.example\.test$/m);
|
|
19
|
+
assert.match(env, /^VITE_PUBLIC_API_URL=https:\/\/prj\.example\.test$/m);
|
|
20
|
+
assert.match(env, /^APP_PUBLIC_API_URL=https:\/\/prj\.example\.test$/m);
|
|
21
|
+
const spa = await readFile(join(root, ".config/local.spa.yml"), "utf8");
|
|
22
|
+
assert.match(spa, /APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "https:\/\/prj\.example\.test"/);
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(root, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -65,7 +65,17 @@ export async function writeLink(link: ProjectLink, cwd = process.cwd()): Promise
|
|
|
65
65
|
await writeFile(linkPath(cwd), `${JSON.stringify(link, null, 2)}\n`);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
const PROJECT_API_ENV_KEYS = ["VITE_API_URL", "VITE_PUBLIC_API_URL", "APP_PUBLIC_API_URL"] as const;
|
|
69
|
+
|
|
70
|
+
function upsertEnvKey(content: string, key: string, value: string): string {
|
|
71
|
+
const line = `${key}=${value}`;
|
|
72
|
+
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
73
|
+
if (pattern.test(content)) return content.replace(pattern, line);
|
|
74
|
+
const trimmed = content.replace(/\s*$/, "");
|
|
75
|
+
return `${trimmed}${trimmed ? "\n" : ""}${line}\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Sets Vite / Povio API URLs so starter UIs talk to the deployed project host. */
|
|
69
79
|
export async function writeViteApiUrl(projectApiUrl: string, cwd = process.cwd()): Promise<void> {
|
|
70
80
|
const envPath = join(cwd, ".env");
|
|
71
81
|
let content = "";
|
|
@@ -74,10 +84,29 @@ export async function writeViteApiUrl(projectApiUrl: string, cwd = process.cwd()
|
|
|
74
84
|
} catch {
|
|
75
85
|
content = "";
|
|
76
86
|
}
|
|
77
|
-
|
|
78
|
-
content = content
|
|
79
|
-
} else {
|
|
80
|
-
content = `${content.replace(/\s*$/, "")}${content ? "\n" : ""}VITE_API_URL=${projectApiUrl}\n`;
|
|
87
|
+
for (const key of PROJECT_API_ENV_KEYS) {
|
|
88
|
+
content = upsertEnvKey(content, key, projectApiUrl);
|
|
81
89
|
}
|
|
82
90
|
await writeFile(envPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
91
|
+
await rewriteLocalSpaConfig(projectApiUrl, cwd);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function rewriteLocalSpaConfig(projectApiUrl: string, cwd: string): Promise<void> {
|
|
95
|
+
const configDir = join(cwd, ".config");
|
|
96
|
+
const templatePath = join(configDir, "local.spa.template.yml");
|
|
97
|
+
const destPath = join(configDir, "local.spa.yml");
|
|
98
|
+
let source: string;
|
|
99
|
+
try {
|
|
100
|
+
source = await readFile(templatePath, "utf8");
|
|
101
|
+
} catch {
|
|
102
|
+
try {
|
|
103
|
+
source = await readFile(destPath, "utf8");
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const quoted = JSON.stringify(projectApiUrl);
|
|
109
|
+
const next = source.replace(/^(APP_PUBLIC_API_URL:\s*&APP_PUBLIC_API_URL\s+).+$/m, `$1${quoted}`);
|
|
110
|
+
await mkdir(configDir, { recursive: true });
|
|
111
|
+
await writeFile(destPath, next.endsWith("\n") ? next : `${next}\n`);
|
|
83
112
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { collectFiles } from "./collect-files.js";
|
|
7
|
+
import { cliRoot, emitStarter } from "./emit-starter.js";
|
|
8
|
+
|
|
9
|
+
const versions = { robodev: "0.12.0", sdk: "0.12.0", client: "0.12.0" };
|
|
10
|
+
|
|
11
|
+
test("emitStarter rewrites root package.json name only", async () => {
|
|
12
|
+
const dest = await mkdtemp(join(tmpdir(), "rd-emit-"));
|
|
13
|
+
try {
|
|
14
|
+
await emitStarter(join(cliRoot(), "..", "starters", "space"), dest, {
|
|
15
|
+
name: "Orbit",
|
|
16
|
+
packageName: "orbit-app",
|
|
17
|
+
title: "Space",
|
|
18
|
+
versions,
|
|
19
|
+
});
|
|
20
|
+
const rootPkg = JSON.parse(await readFile(join(dest, "package.json"), "utf8")) as {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
const fePkg = JSON.parse(await readFile(join(dest, "apps/fe/package.json"), "utf8")) as {
|
|
24
|
+
name: string;
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
devDependencies?: Record<string, string>;
|
|
27
|
+
};
|
|
28
|
+
assert.equal(rootPkg.name, "orbit-app");
|
|
29
|
+
assert.equal(fePkg.name, "fe");
|
|
30
|
+
assert.equal(fePkg.dependencies?.robodev, undefined);
|
|
31
|
+
assert.equal(fePkg.devDependencies?.robodev, undefined);
|
|
32
|
+
const overview = await readFile(join(dest, ".rulesync/rules/project-overview.md"), "utf8");
|
|
33
|
+
assert.match(overview, /^# Space starter AI rules/m);
|
|
34
|
+
const readme = await readFile(join(dest, "README.md"), "utf8");
|
|
35
|
+
assert.match(readme, /^# Orbit/m);
|
|
36
|
+
} finally {
|
|
37
|
+
await rm(dest, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
|
|
42
|
+
const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
|
|
43
|
+
const paths = new Set(files.map((file) => file.path));
|
|
44
|
+
assert.ok(paths.has("database.ts"));
|
|
45
|
+
assert.ok(paths.has("api/planets.ts"));
|
|
46
|
+
assert.ok(paths.has("api/planets/paginate.ts"));
|
|
47
|
+
assert.ok(paths.has("api/aliens/labels.ts"));
|
|
48
|
+
assert.ok(paths.has("api/files/presigned-url.ts"));
|
|
49
|
+
assert.ok(paths.has("api/files/upload.ts"));
|
|
50
|
+
assert.ok(!paths.has("index.html"));
|
|
51
|
+
assert.ok(!paths.has("src/main.tsx"));
|
|
52
|
+
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
|
53
|
+
assert.ok(files.length < 200);
|
|
54
|
+
});
|
package/src/emit-starter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
export type Starter = {
|
|
@@ -23,6 +23,20 @@ export type EmitContext = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const SKIP_DIRS = new Set(["node_modules", ".robodev", ".git"]);
|
|
26
|
+
const BINARY_COPY_EXTENSIONS = new Set([
|
|
27
|
+
".png",
|
|
28
|
+
".jpg",
|
|
29
|
+
".jpeg",
|
|
30
|
+
".gif",
|
|
31
|
+
".webp",
|
|
32
|
+
".ico",
|
|
33
|
+
".bmp",
|
|
34
|
+
".woff",
|
|
35
|
+
".woff2",
|
|
36
|
+
".ttf",
|
|
37
|
+
".eot",
|
|
38
|
+
".otf",
|
|
39
|
+
]);
|
|
26
40
|
|
|
27
41
|
type DepMap = Record<string, string>;
|
|
28
42
|
|
|
@@ -103,22 +117,35 @@ export async function loadPackageVersions(): Promise<PackageVersions> {
|
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
/** Copies a starter tree and applies package + display rewrites used by create and prepack. */
|
|
106
|
-
export async function emitStarter(
|
|
120
|
+
export async function emitStarter(
|
|
121
|
+
src: string,
|
|
122
|
+
dest: string,
|
|
123
|
+
ctx: EmitContext,
|
|
124
|
+
isRoot = true,
|
|
125
|
+
): Promise<void> {
|
|
107
126
|
await mkdir(dest, { recursive: true });
|
|
108
127
|
const entries = await readdir(src, { withFileTypes: true });
|
|
109
128
|
for (const entry of entries) {
|
|
110
129
|
if (entry.isDirectory()) {
|
|
111
130
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
112
|
-
await emitStarter(join(src, entry.name), join(dest, entry.name), ctx);
|
|
131
|
+
await emitStarter(join(src, entry.name), join(dest, entry.name), ctx, false);
|
|
113
132
|
continue;
|
|
114
133
|
}
|
|
115
134
|
if (!entry.isFile() || entry.name === ".env") continue;
|
|
116
|
-
const
|
|
135
|
+
const srcPath = join(src, entry.name);
|
|
136
|
+
const destPath = join(dest, entry.name);
|
|
137
|
+
if (BINARY_COPY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
138
|
+
await copyFile(srcPath, destPath);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const content = await readFile(srcPath, "utf8");
|
|
117
142
|
const next =
|
|
118
143
|
entry.name === "package.json"
|
|
119
|
-
? rewritePackageJson(content, ctx)
|
|
120
|
-
:
|
|
121
|
-
|
|
144
|
+
? rewritePackageJson(content, ctx, isRoot)
|
|
145
|
+
: shouldRewriteDisplay(entry.name, isRoot)
|
|
146
|
+
? rewriteDisplay(content, ctx)
|
|
147
|
+
: content;
|
|
148
|
+
await writeFile(destPath, next);
|
|
122
149
|
}
|
|
123
150
|
}
|
|
124
151
|
|
|
@@ -154,9 +181,14 @@ async function readPackageVersion(path: string, fallback?: string): Promise<stri
|
|
|
154
181
|
throw new Error(`Missing version in ${path}`);
|
|
155
182
|
}
|
|
156
183
|
|
|
157
|
-
function
|
|
184
|
+
function shouldRewriteDisplay(fileName: string, isRoot: boolean): boolean {
|
|
185
|
+
if (!isRoot) return false;
|
|
186
|
+
return fileName === "README.md" || fileName.endsWith(".html");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function rewritePackageJson(raw: string, ctx: EmitContext, isRoot: boolean): string {
|
|
158
190
|
const pkg = JSON.parse(raw) as PackageJson;
|
|
159
|
-
pkg.name = ctx.packageName;
|
|
191
|
+
if (isRoot) pkg.name = ctx.packageName;
|
|
160
192
|
|
|
161
193
|
const pins: Record<string, string> = {
|
|
162
194
|
robodev: `^${ctx.versions.robodev}`,
|
|
@@ -172,9 +204,11 @@ function rewritePackageJson(raw: string, ctx: EmitContext): string {
|
|
|
172
204
|
}
|
|
173
205
|
|
|
174
206
|
const moved = pkg.dependencies?.robodev;
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
207
|
+
if (isRoot) {
|
|
208
|
+
if (pkg.dependencies) delete pkg.dependencies.robodev;
|
|
209
|
+
pkg.devDependencies = pkg.devDependencies ?? {};
|
|
210
|
+
pkg.devDependencies.robodev = pkg.devDependencies.robodev ?? moved ?? pins.robodev;
|
|
211
|
+
}
|
|
178
212
|
|
|
179
213
|
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
180
214
|
}
|
package/src/index.ts
CHANGED
|
@@ -397,6 +397,28 @@ async function npmInstall(root: string): Promise<void> {
|
|
|
397
397
|
});
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
async function bunAvailable(): Promise<boolean> {
|
|
401
|
+
try {
|
|
402
|
+
await execFileAsync("bun", ["--version"]);
|
|
403
|
+
return true;
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function printSpaceNextSteps(displayPath: string): void {
|
|
410
|
+
if (displayPath !== ".") {
|
|
411
|
+
console.log(` cd ${displayPath}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(" cd apps/fe");
|
|
414
|
+
console.log(" bun install");
|
|
415
|
+
console.log(" bun openapi:gen");
|
|
416
|
+
console.log(" bun dev");
|
|
417
|
+
console.log(
|
|
418
|
+
" Open the Vite URL (default http://localhost:3000). The project App URL is APIs and Swagger, not this UI.",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
400
422
|
/** Creates a Starbase project, copies the chosen starter, links, deploys, and installs deps. */
|
|
401
423
|
async function runCreate(args: string[]): Promise<void> {
|
|
402
424
|
const parsed = parseCreateArgs(args);
|
|
@@ -442,6 +464,9 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
442
464
|
"npm install failed. Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.",
|
|
443
465
|
);
|
|
444
466
|
console.log("https://robodev.povio.dev/docs/frontend");
|
|
467
|
+
} else if (starter.id === "space") {
|
|
468
|
+
console.log("Run `npm install` in the project folder.");
|
|
469
|
+
console.log("bun is required for apps/fe. APIs are already deployed.");
|
|
445
470
|
} else {
|
|
446
471
|
console.log("Run `npm install` in the project folder, then `npm run dev`.");
|
|
447
472
|
}
|
|
@@ -451,14 +476,22 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
451
476
|
const displayPath = relative(process.cwd(), root) || ".";
|
|
452
477
|
console.log("");
|
|
453
478
|
console.log("Next:");
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
479
|
+
if (starter.id === "space") {
|
|
480
|
+
if (!(await bunAvailable())) {
|
|
481
|
+
console.log("bun is required for apps/fe. APIs are already deployed.");
|
|
482
|
+
}
|
|
483
|
+
printSpaceNextSteps(displayPath);
|
|
484
|
+
} else if (starter.kind === "backend") {
|
|
485
|
+
if (displayPath !== ".") {
|
|
486
|
+
console.log(` cd ${displayPath}`);
|
|
487
|
+
}
|
|
458
488
|
console.log(" Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.");
|
|
459
489
|
console.log(" Do not run robodev deploy or npm run build inside those tools.");
|
|
460
490
|
console.log(" https://robodev.povio.dev/docs/frontend");
|
|
461
491
|
} else {
|
|
492
|
+
if (displayPath !== ".") {
|
|
493
|
+
console.log(` cd ${displayPath}`);
|
|
494
|
+
}
|
|
462
495
|
console.log(" npm run dev");
|
|
463
496
|
}
|
|
464
497
|
}
|
|
@@ -160,7 +160,7 @@ function App() {
|
|
|
160
160
|
return (
|
|
161
161
|
<main className="narrow">
|
|
162
162
|
<p className="eyebrow">Robodev Auth</p>
|
|
163
|
-
<h1>
|
|
163
|
+
<h1>Auth chat</h1>
|
|
164
164
|
<p className="lede">
|
|
165
165
|
Sign in with email or Google. Protected APIs live at {apiUrl}. Unauthenticated GET
|
|
166
166
|
/api/messages: {unauthStatus ?? "checking…"}.
|
package/templates/catalog.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
STAGE: "local"
|
|
2
|
+
OUTPUT: "export"
|
|
3
|
+
|
|
4
|
+
ANALYZE: "false"
|
|
5
|
+
NEXT_TELEMETRY_DISABLED: "1"
|
|
6
|
+
|
|
7
|
+
VITE_PUBLIC_RELEASE: ${env:RELEASE}
|
|
8
|
+
VITE_PUBLIC_STAGE: ${func:stage}
|
|
9
|
+
|
|
10
|
+
APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "http://localhost:4000"
|
|
11
|
+
APP_PUBLIC_API_MODE: &APP_PUBLIC_API_MODE "real"
|
|
12
|
+
VITE_PUBLIC_API_URL: *APP_PUBLIC_API_URL
|
|
13
|
+
VITE_PUBLIC_API_MODE: *APP_PUBLIC_API_MODE
|
|
14
|
+
|
|
15
|
+
VITE_PUBLIC_LOG_LEVEL: "trace"
|
|
16
|
+
|
|
17
|
+
VITE_PUBLIC_SENTRY_DSN: ""
|
|
18
|
+
VITE_PUBLIC_SENTRY_ENVIRONMENT: ${func:stage}
|
|
19
|
+
VITE_PUBLIC_SENTRY_TRACES_SAMPLE_RATE: "0"
|
|
20
|
+
VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: "0"
|
|
21
|
+
VITE_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: "0"
|
|
22
|
+
|
|
23
|
+
VITE_PUBLIC_AUTH_CUSTOM_JWT_ENABLE_MAGIC_LINK: "true"
|
|
24
|
+
|
|
25
|
+
VITE_PUBLIC_GOOGLE_ANALYTICS_MEASUREMENT_ID: ""
|
|
26
|
+
|
|
27
|
+
VITE_DEV_PORT: "3000"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "apps/fe/node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"plugins": ["eslint", "typescript", "unicorn", "import", "oxc", "promise", "vitest"],
|
|
4
|
+
"categories": {
|
|
5
|
+
"correctness": "error",
|
|
6
|
+
"perf": "error",
|
|
7
|
+
"pedantic": "off",
|
|
8
|
+
"style": "off",
|
|
9
|
+
"restriction": "off",
|
|
10
|
+
"suspicious": "off",
|
|
11
|
+
"nursery": "off"
|
|
12
|
+
},
|
|
13
|
+
"env": {
|
|
14
|
+
"builtin": true,
|
|
15
|
+
"node": true,
|
|
16
|
+
"shared-node-browser": true
|
|
17
|
+
},
|
|
18
|
+
"rules": {
|
|
19
|
+
"eslint/eqeqeq": [
|
|
20
|
+
"error",
|
|
21
|
+
"always",
|
|
22
|
+
{
|
|
23
|
+
"null": "ignore"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"typescript/no-floating-promises": "off",
|
|
27
|
+
"eslint/capitalized-comments": "off"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "apps/fe/node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"extends": [".oxlint-base.json"],
|
|
4
|
+
"plugins": [
|
|
5
|
+
"eslint",
|
|
6
|
+
"typescript",
|
|
7
|
+
"unicorn",
|
|
8
|
+
"import",
|
|
9
|
+
"vitest",
|
|
10
|
+
"react",
|
|
11
|
+
"react-perf",
|
|
12
|
+
"jsx-a11y"
|
|
13
|
+
],
|
|
14
|
+
"categories": {
|
|
15
|
+
"style": "error"
|
|
16
|
+
},
|
|
17
|
+
"ignorePatterns": [
|
|
18
|
+
"openapi-codegen.config.ts",
|
|
19
|
+
"postcss.config.js",
|
|
20
|
+
"out",
|
|
21
|
+
"dist",
|
|
22
|
+
"public",
|
|
23
|
+
"scripts",
|
|
24
|
+
".vscode",
|
|
25
|
+
".turbo",
|
|
26
|
+
"node_modules",
|
|
27
|
+
"apps/fe/src/routeTree.gen.ts",
|
|
28
|
+
"apps/fe/src/openapi",
|
|
29
|
+
"apps/fe/public"
|
|
30
|
+
],
|
|
31
|
+
"overrides": [
|
|
32
|
+
{
|
|
33
|
+
"files": ["vite-plugin-*.ts", "apps/fe/vite-plugin-*.ts"],
|
|
34
|
+
"rules": {
|
|
35
|
+
"import/no-nodejs-modules": "off"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"rules": {
|
|
40
|
+
"eslint/no-implicit-coercion": "off",
|
|
41
|
+
"no-map-spread": "off",
|
|
42
|
+
"eslint/sort-keys": "off",
|
|
43
|
+
"eslint/sort-imports": "off",
|
|
44
|
+
"eslint/no-ternary": "off",
|
|
45
|
+
"eslint/id-length": "off",
|
|
46
|
+
"eslint/no-magic-numbers": "off",
|
|
47
|
+
"eslint/arrow-body-style": "off",
|
|
48
|
+
"eslint/max-params": "off",
|
|
49
|
+
"eslint/func-style": "off",
|
|
50
|
+
"eslint/new-cap": "off",
|
|
51
|
+
"eslint/no-continue": "off",
|
|
52
|
+
"eslint/max-statements": "off",
|
|
53
|
+
"eslint/no-duplicate-imports": [
|
|
54
|
+
"error",
|
|
55
|
+
{
|
|
56
|
+
"allowSeparateTypeImports": true
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"react/exhaustive-deps": "off",
|
|
60
|
+
"react/jsx-props-no-spreading": "off",
|
|
61
|
+
"react/jsx-max-depth": "off",
|
|
62
|
+
"react-perf/jsx-no-new-object-as-prop": "off",
|
|
63
|
+
"react-perf/jsx-no-new-function-as-prop": "off",
|
|
64
|
+
"react-perf/jsx-no-new-array-as-prop": "off",
|
|
65
|
+
"react-perf/jsx-no-jsx-as-prop": "off",
|
|
66
|
+
"typescript/no-empty-interface": "off",
|
|
67
|
+
"import/no-named-export": "off",
|
|
68
|
+
"import/group-exports": "off",
|
|
69
|
+
"import/prefer-default-export": "off",
|
|
70
|
+
"import/exports-last": "off",
|
|
71
|
+
"import/consistent-type-specifier-style": "off",
|
|
72
|
+
"jest/require-hook": "off",
|
|
73
|
+
"unicorn/filename-case": "off",
|
|
74
|
+
"unicorn/prefer-global-this": "off",
|
|
75
|
+
"unicorn/no-await-expression-member": "off",
|
|
76
|
+
"unicorn/no-null": "off"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
root: false
|
|
3
|
+
targets: ["claudecode", "codexcli", "cursor"]
|
|
4
|
+
description: "Frontend must consume generated OpenAPI queries and models, not backend internals."
|
|
5
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}"]
|
|
6
|
+
cursor:
|
|
7
|
+
alwaysApply: false
|
|
8
|
+
description: "Apply when editing frontend components, hooks, providers, pages, or clients that call the API."
|
|
9
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Frontend API Boundary
|
|
13
|
+
|
|
14
|
+
Frontend application code must consume the API through generated files in `apps/fe/src/openapi`.
|
|
15
|
+
|
|
16
|
+
Use generated modules such as:
|
|
17
|
+
|
|
18
|
+
- `@/openapi/<domain>/<domain>.queries`
|
|
19
|
+
- `@/openapi/<domain>/<domain>.models`
|
|
20
|
+
- `@/openapi/queryModules`
|
|
21
|
+
|
|
22
|
+
For generated mutations, pass invalidation through the mutation options instead of importing `queryClient` or calling `invalidateQueries` with generated query keys. The current module is invalidated by default through `invalidateCurrentModule: true`; invalidate dependent modules explicitly with `invalidateModules: [QueryModule.someModule]`.
|
|
23
|
+
|
|
24
|
+
After mutations, derive follow-up UI state from canonical API response or query state instead of leaving action controls unchanged. Disable duplicate submissions when the API state already represents a pending or completed action, and surface mutation errors through the default generated-client error handler or a feature-specific message.
|
|
25
|
+
|
|
26
|
+
Do not import `@robodev-ai/sdk`, `database.ts`, Drizzle tables, or API helper modules into frontend features.
|
|
27
|
+
|
|
28
|
+
`apps/fe/src/openapi` is generated by `@povio/openapi-codegen-cli` from root `openapi.json` (or a live project `/openapi.json` when `OPENAPI_LIVE=true`). Do not hand-edit generated OpenAPI client files.
|
|
29
|
+
|
|
30
|
+
When API contracts change:
|
|
31
|
+
|
|
32
|
+
1. Update `api/` handlers and `openapi.json` if the documented contract changed.
|
|
33
|
+
2. Run `bun openapi:gen` from `apps/fe`.
|
|
34
|
+
3. Update frontend code to use the regenerated queries and models.
|
|
35
|
+
|
|
36
|
+
Point `VITE_PUBLIC_API_URL` at the Starbase project host origin only. Do not append `/api`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
root: false
|
|
3
|
+
targets: ["claudecode", "codexcli", "cursor"]
|
|
4
|
+
description: "Frontend component extraction and feature subcomponent structure conventions."
|
|
5
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}"]
|
|
6
|
+
cursor:
|
|
7
|
+
alwaysApply: false
|
|
8
|
+
description: "Apply when organizing frontend feature components, page sections, mapped items, or reusable UI."
|
|
9
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Frontend Component Structure
|
|
13
|
+
|
|
14
|
+
Extract repeatable UI into subcomponents. Every named React component should live in its own file, even when it is currently used only once. Keep the component file near the feature, layout, page, or shared UI area that owns it.
|
|
15
|
+
|
|
16
|
+
Extract large or complex sections of pages into named components so route and page components stay readable. Anything mapped from an array should usually be a component, such as cards, table row actions, repeated sections, or list items.
|
|
17
|
+
|
|
18
|
+
The planets feature is the reference example while the template is still a prototype:
|
|
19
|
+
|
|
20
|
+
- `PlanetCard` for mapped grid cards.
|
|
21
|
+
- `PlanetsFilters` for shared filter controls.
|
|
22
|
+
- `PlanetsTableActions` for repeated row actions.
|
|
23
|
+
- `PlanetsTable` and `PlanetsTableInfinite` for table wrappers.
|
|
24
|
+
- `PlanetDetailsPage` and `PlanetEditPage` for large route-owned views.
|
|
25
|
+
|
|
26
|
+
When implementing a real app, remove the planets and aliens links from app navigation such as `apps/fe/src/components/layout/AppHeader.tsx` so these examples are not directly accessible in the product UI, but keep the example implementation available as a reference unless the team intentionally deletes the demo layer.
|
|
27
|
+
|
|
28
|
+
Route files should own routing, document metadata, and route data boundaries. Feature components should own the page UI and interaction details.
|
|
29
|
+
|
|
30
|
+
Exception: route-local wrappers such as `PageComponent` or layout guard components may stay in route files when they only connect routing primitives to feature components.
|
|
31
|
+
|
|
32
|
+
## Shared Utilities
|
|
33
|
+
|
|
34
|
+
Keep generic formatting, parsing, calculation, and data-shaping helpers in `apps/fe/src/utils/*.utils.ts` files instead of defining them inside component files. Export helpers through namespaces such as `DateUtils.formatDate(...)` or `NumberUtils.formatInteger(...)` so call sites stay explicit and related helpers stay grouped.
|
|
35
|
+
|
|
36
|
+
Component files may keep UI-local glue helpers only when the logic is tightly coupled to that component's JSX, such as adapting one component's prop shape or handling a local event. If the helper describes dates, numbers, arrays, strings, IDs, or other domain-neutral data, move it into a shared utility file.
|
|
37
|
+
|
|
38
|
+
For apps with multiple user roles that require distinct UI or business workflows, also apply the role-based app structure rule.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
root: false
|
|
3
|
+
targets: ["claudecode", "codexcli", "cursor"]
|
|
4
|
+
description: "Frontend form patterns with Povio UI useForm, generated schemas, and formControl wiring."
|
|
5
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}"]
|
|
6
|
+
cursor:
|
|
7
|
+
alwaysApply: false
|
|
8
|
+
description: "Apply when building or changing frontend forms, modals, edit pages, or input components."
|
|
9
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Frontend Forms
|
|
13
|
+
|
|
14
|
+
Use `useForm` and form-aware inputs from `@povio/ui/tanstack` with generated OpenAPI Zod schemas for API-backed forms. The planets feature is the reference example while the template is still a prototype:
|
|
15
|
+
|
|
16
|
+
- Create form: `apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx`
|
|
17
|
+
- Edit form: `apps/fe/src/components/features/planets/details/PlanetEditPage.tsx`
|
|
18
|
+
|
|
19
|
+
Prefer this shape:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const form = useForm({
|
|
23
|
+
zodSchema: PlanetsModels.PlanetsCreateInputSchema,
|
|
24
|
+
defaultValues: { name: "" },
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Pass form state into Povio UI inputs with `field={{ form, name: "fieldName" }}`. Use `form.handleSubmit`, `form.reset`, and `form.setFieldValue` for form actions, and `useFormValue(form, selector)` for reactive field reads.
|
|
29
|
+
|
|
30
|
+
Avoid local `useState`, custom `value`, and custom `onChange` plumbing for fields that belong to the form. Use TanStack Form's `form.Field` adapter only when a component cannot accept the Povio UI `field` binding.
|
|
31
|
+
|
|
32
|
+
Use generated OpenAPI queries and mutations for submit handlers. Keep toast feedback and navigation close to the feature interaction that owns them.
|