@multiplatform.one/cli 6.1.0 → 6.4.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/README.md +72 -0
- package/bin/createMultiplatformApp.mjs +77 -0
- package/lib/bin/multiplatformOne.mjs +35 -10
- package/lib/commands/e2e.mjs +3 -3
- package/lib/commands/init.mjs +16 -12
- package/lib/commands/initApp.mjs +175 -0
- package/lib/commands/updateApp.mjs +260 -0
- package/lib/generateVscode.mjs +27 -10
- package/package.json +23 -12
- package/src/bin/multiplatformOne.ts +83 -20
- package/src/commands/init.ts +7 -18
- package/src/commands/initApp.spec.ts +210 -0
- package/src/commands/initApp.ts +274 -0
- package/src/commands/updateApp.ts +277 -0
- package/src/generateVscode.ts +31 -9
- package/templates/app/AGENTS.md +142 -0
- package/templates/app/README.md +62 -0
- package/templates/app/apps/__NAME__/index.html +36 -0
- package/templates/app/apps/__NAME__/package.json +30 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +9 -0
- package/templates/app/apps/__NAME__/src/main.tsx +14 -0
- package/templates/app/apps/__NAME__/tsconfig.json +8 -0
- package/templates/app/apps/__NAME__/vite.config.ts +17 -0
- package/templates/app/features/__NAME__/package.json +34 -0
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +70 -0
- package/templates/app/features/__NAME__/src/index.ts +1 -0
- package/templates/app/features/__NAME__/tsconfig.json +5 -0
- package/templates/app/gitignore +8 -0
- package/templates/app/npmrc +2 -0
- package/templates/app/oxlintrc.json +39 -0
- package/templates/app/package.json +20 -0
- package/templates/app/packages/config/config.json +4 -0
- package/templates/app/packages/config/index.ts +5 -0
- package/templates/app/packages/config/package.json +17 -0
- package/templates/app/packages/config/tsconfig.json +5 -0
- package/templates/app/pnpm-workspace.yaml +4 -0
- package/templates/app/tsconfig.base.json +16 -0
- package/templates/universal/AGENTS.md +142 -0
- package/templates/universal/README.md +97 -0
- package/templates/universal/apps/__NAME__/app.config.js +31 -0
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +4 -0
- package/templates/universal/apps/__NAME__/package.json +61 -0
- package/templates/universal/apps/__NAME__/polyfills.native.ts +23 -0
- package/templates/universal/apps/__NAME__/polyfills.ts +3 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +22 -0
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +21 -0
- package/templates/universal/apps/__NAME__/routes/about.tsx +3 -0
- package/templates/universal/apps/__NAME__/routes/index.tsx +3 -0
- package/templates/universal/apps/__NAME__/src/app.ts +10 -0
- package/templates/universal/apps/__NAME__/tamagui.css +3 -0
- package/templates/universal/apps/__NAME__/tsconfig.json +8 -0
- package/templates/universal/apps/__NAME__/types.d.ts +9 -0
- package/templates/universal/apps/__NAME__/vite.config.ts +66 -0
- package/templates/universal/features/__NAME__/package.json +35 -0
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +27 -0
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +50 -0
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +22 -0
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +55 -0
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +40 -0
- package/templates/universal/features/__NAME__/src/index.ts +4 -0
- package/templates/universal/features/__NAME__/src/screens.spec.ts +12 -0
- package/templates/universal/features/__NAME__/tsconfig.json +5 -0
- package/templates/universal/features/__NAME__/vitest.config.mjs +7 -0
- package/templates/universal/gitignore +12 -0
- package/templates/universal/npmrc +3 -0
- package/templates/universal/oxlintrc.json +39 -0
- package/templates/universal/package.json +27 -0
- package/templates/universal/packages/config/config.json +4 -0
- package/templates/universal/packages/config/index.ts +5 -0
- package/templates/universal/packages/config/package.json +17 -0
- package/templates/universal/packages/config/tsconfig.json +5 -0
- package/templates/universal/packages/i18n/en/common.json +30 -0
- package/templates/universal/packages/i18n/es/common.json +30 -0
- package/templates/universal/packages/i18n/i18n.d.ts +16 -0
- package/templates/universal/packages/i18n/index.ts +26 -0
- package/templates/universal/packages/i18n/package.json +20 -0
- package/templates/universal/packages/i18n/resources.ts +13 -0
- package/templates/universal/packages/i18n/tsconfig.json +5 -0
- package/templates/universal/packages/themes/accent.ts +32 -0
- package/templates/universal/packages/themes/base.ts +50 -0
- package/templates/universal/packages/themes/fonts.ts +8 -0
- package/templates/universal/packages/themes/index.ts +33 -0
- package/templates/universal/packages/themes/package.json +23 -0
- package/templates/universal/packages/themes/tamagui.config.ts +14 -0
- package/templates/universal/packages/themes/tsconfig.json +5 -0
- package/templates/universal/pnpm-workspace.yaml +4 -0
- package/templates/universal/tsconfig.base.json +21 -0
- package/templates/universal/types/env.d.ts +9 -0
- package/types/bin/multiplatformOne.d.ts +11 -0
- package/types/bin/multiplatformOne.d.ts.map +1 -0
- package/types/commands/e2e.d.ts +14 -0
- package/types/commands/e2e.d.ts.map +1 -0
- package/types/commands/init.d.ts +16 -0
- package/types/commands/init.d.ts.map +1 -0
- package/types/commands/initApp.d.ts +41 -0
- package/types/commands/initApp.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +24 -0
- package/types/commands/updateApp.d.ts.map +1 -0
- package/types/generateVscode.d.ts +7 -0
- package/types/generateVscode.d.ts.map +1 -0
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -0
- package/types/types.d.ts +6 -0
- package/types/types.d.ts.map +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "@tamagui/core/reset.css";
|
|
2
|
+
import "../tamagui.css";
|
|
3
|
+
import resources from "virtual:i18next-loader";
|
|
4
|
+
import { AppLayout } from "@features/__NAME__";
|
|
5
|
+
import { createRootLayout } from "@multiplatform.one/core";
|
|
6
|
+
import { defaultLanguage, defaultNamespace, languages, namespaces } from "@package/i18n";
|
|
7
|
+
import { AppProvider } from "../src/app";
|
|
8
|
+
|
|
9
|
+
// Web root layout (native resolves _layout.tsx). Translations come from the
|
|
10
|
+
// vite i18next loader (packages/i18n) so they hot-reload in dev.
|
|
11
|
+
export default createRootLayout({
|
|
12
|
+
AppProvider,
|
|
13
|
+
RootLayout: AppLayout,
|
|
14
|
+
i18n: {
|
|
15
|
+
languages,
|
|
16
|
+
namespaces,
|
|
17
|
+
defaultLanguage,
|
|
18
|
+
defaultNamespace,
|
|
19
|
+
resources,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createApp } from "@multiplatform.one/core";
|
|
2
|
+
import { themeConfig } from "@package/themes";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* App-wide provider stack. Add keycloak / frappe / tanstack / web3 here as the
|
|
6
|
+
* app grows — see CreateAppConfig in @multiplatform.one/core.
|
|
7
|
+
*/
|
|
8
|
+
export const { AppProvider, useAppConfig, withApp } = createApp({
|
|
9
|
+
theme: themeConfig,
|
|
10
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference types="../../packages/i18n/i18n.d.ts" />
|
|
2
|
+
/// <reference types="@multiplatform.one/platform/rnw-overrides" />
|
|
3
|
+
|
|
4
|
+
declare module "virtual:*";
|
|
5
|
+
|
|
6
|
+
// Published core@6.1.0 sources carry a type skew against published
|
|
7
|
+
// frappe-ui@6.1.0 (FrappeProvider auth.token). The app never imports
|
|
8
|
+
// frappe-ui directly; treat it as untyped until fixed packages publish.
|
|
9
|
+
declare module "@multiplatform.one/frappe-ui";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { createViteConfig } from "@multiplatform.one/config/vite";
|
|
4
|
+
import { one } from "one/vite";
|
|
5
|
+
import type { Plugin } from "vite";
|
|
6
|
+
import { public as publicConfigKeys } from "../../packages/config/config.json";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Some @multiplatform.one packages at 6.1.0 publish web export targets that
|
|
12
|
+
* don't exist in their tarballs (exports say dist/esm/…/index.js; the build
|
|
13
|
+
* emitted index.mjs), which breaks resolution for anything importing them —
|
|
14
|
+
* e.g. forms → router and frappe-ui → frappe. Resolve those bare imports to
|
|
15
|
+
* the real web builds. Native is unaffected (vxrn's native resolver uses the
|
|
16
|
+
* packages' valid react-native export condition). Remove this plugin once
|
|
17
|
+
* fixed packages are published.
|
|
18
|
+
*/
|
|
19
|
+
const BROKEN_PUBLISHED_ENTRIES: Record<string, { pkg: string; entry: string }> = {
|
|
20
|
+
"@multiplatform.one/router": {
|
|
21
|
+
pkg: "@multiplatform.one/router",
|
|
22
|
+
entry: "dist/esm/index.mjs",
|
|
23
|
+
},
|
|
24
|
+
"@multiplatform.one/frappe": {
|
|
25
|
+
pkg: "@multiplatform.one/frappe",
|
|
26
|
+
entry: "dist/esm/index.mjs",
|
|
27
|
+
},
|
|
28
|
+
"@multiplatform.one/frappe/devtools": {
|
|
29
|
+
pkg: "@multiplatform.one/frappe",
|
|
30
|
+
entry: "dist/esm/devtools/index.mjs",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function fixPublishedEsmEntries(): Plugin {
|
|
35
|
+
return {
|
|
36
|
+
name: "fix-multiplatform-published-esm-entries",
|
|
37
|
+
enforce: "pre",
|
|
38
|
+
resolveId(id) {
|
|
39
|
+
const broken = BROKEN_PUBLISHED_ENTRIES[id];
|
|
40
|
+
if (!broken) return null;
|
|
41
|
+
try {
|
|
42
|
+
const pkgJson = require.resolve(`${broken.pkg}/package.json`);
|
|
43
|
+
const entry = pkgJson.replace(/package\.json$/, broken.entry);
|
|
44
|
+
return existsSync(entry) ? entry : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default createViteConfig({
|
|
53
|
+
publicConfigKeys,
|
|
54
|
+
one: true,
|
|
55
|
+
tamagui: { config: "./config/tamagui.config.ts", outputCSS: "./tamagui.css" },
|
|
56
|
+
i18n: { paths: ["../../packages/i18n"] },
|
|
57
|
+
plugins: [
|
|
58
|
+
one({
|
|
59
|
+
router: { root: "routes" },
|
|
60
|
+
web: { deploy: "node", defaultRenderMode: "ssr" },
|
|
61
|
+
react: { compiler: true },
|
|
62
|
+
native: { key: "__NAME_PASCAL__" },
|
|
63
|
+
}),
|
|
64
|
+
fixPublishedEsmEntries(),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@features/__NAME__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./*": "./src/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@multiplatform.one/components": "__MPO_VERSION__",
|
|
17
|
+
"@multiplatform.one/forms": "__MPO_VERSION__",
|
|
18
|
+
"@multiplatform.one/theme": "__MPO_VERSION__",
|
|
19
|
+
"@package/config": "workspace:*",
|
|
20
|
+
"one": "1.16.5",
|
|
21
|
+
"react": "19.2.5",
|
|
22
|
+
"react-i18next": "^16.6.6"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@multiplatform.one/config": "__MPO_VERSION__",
|
|
26
|
+
"@multiplatform.one/test-utils": "__MPO_VERSION__",
|
|
27
|
+
"@types/react": "~19.2.14",
|
|
28
|
+
"jsdom": "^26.0.0",
|
|
29
|
+
"typescript": "~5.9.3",
|
|
30
|
+
"vitest": "^4.1.5"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "^19.1.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button, H2, Paragraph, Screen, YStack } from "@multiplatform.one/components";
|
|
2
|
+
import { useRouter } from "one";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* About screen (route: /about) — demonstrates programmatic navigation with
|
|
7
|
+
* the router hooks from `one` (same pattern as the framework's own layouts).
|
|
8
|
+
*/
|
|
9
|
+
export function AboutScreen() {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Screen>
|
|
15
|
+
<YStack gap="$3" alignItems="flex-start">
|
|
16
|
+
<H2>{t("screens.about.title", "About")}</H2>
|
|
17
|
+
<Paragraph color="$color11">
|
|
18
|
+
{t(
|
|
19
|
+
"screens.about.message",
|
|
20
|
+
"This screen lives in features/__NAME__ and is wired to the /about route by apps/__NAME__/routes/about.tsx.",
|
|
21
|
+
)}
|
|
22
|
+
</Paragraph>
|
|
23
|
+
<Button onPress={() => router.navigate("/")}>{t("screens.about.back", "Back home")}</Button>
|
|
24
|
+
</YStack>
|
|
25
|
+
</Screen>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Button, SizableText, XStack, YStack } from "@multiplatform.one/components";
|
|
2
|
+
import { usePathname, useRouter } from "one";
|
|
3
|
+
import type { PropsWithChildren } from "react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Root layout: top navigation + content. Wired into the router by
|
|
8
|
+
* apps/__NAME__/routes/_layout(.web).tsx via createRootLayout. Uses router
|
|
9
|
+
* hooks (not window.location) so it works on native as well as web.
|
|
10
|
+
*/
|
|
11
|
+
export function AppLayout({ children }: PropsWithChildren) {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const path = usePathname() || "/";
|
|
15
|
+
|
|
16
|
+
const items = [
|
|
17
|
+
{ label: t("nav.home", "Home"), href: "/", active: path === "/" },
|
|
18
|
+
{ label: t("nav.about", "About"), href: "/about", active: path.startsWith("/about") },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<YStack flex={1} backgroundColor="$background">
|
|
23
|
+
<XStack
|
|
24
|
+
alignItems="center"
|
|
25
|
+
gap="$4"
|
|
26
|
+
paddingHorizontal="$4"
|
|
27
|
+
paddingVertical="$3"
|
|
28
|
+
borderBottomWidth={1}
|
|
29
|
+
borderBottomColor="$borderColor"
|
|
30
|
+
>
|
|
31
|
+
<SizableText fontWeight="700" size="$5">
|
|
32
|
+
{t("app.name", "__NAME_PASCAL__")}
|
|
33
|
+
</SizableText>
|
|
34
|
+
<XStack gap="$2" marginLeft="auto">
|
|
35
|
+
{items.map((item) => (
|
|
36
|
+
<Button
|
|
37
|
+
key={item.href}
|
|
38
|
+
size="$3"
|
|
39
|
+
chromeless={!item.active}
|
|
40
|
+
onPress={() => router.navigate(item.href)}
|
|
41
|
+
>
|
|
42
|
+
{item.label}
|
|
43
|
+
</Button>
|
|
44
|
+
))}
|
|
45
|
+
</XStack>
|
|
46
|
+
</XStack>
|
|
47
|
+
{children}
|
|
48
|
+
</YStack>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { ContactForm } from "./ContactForm";
|
|
5
|
+
|
|
6
|
+
describe("ContactForm", () => {
|
|
7
|
+
it("renders the starter form fields", () => {
|
|
8
|
+
const result = renderWithProviders(<ContactForm />);
|
|
9
|
+
expect(result.findTextElement("Contact")).toBeDefined();
|
|
10
|
+
expect(result.findTextElement("Name")).toBeDefined();
|
|
11
|
+
expect(result.findTextElement("Message")).toBeDefined();
|
|
12
|
+
expect(result.getButton("Send")).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("shows required errors when submitting empty", () => {
|
|
16
|
+
const result = renderWithProviders(<ContactForm />);
|
|
17
|
+
const send = result.getButton("Send");
|
|
18
|
+
expect(send).toBeDefined();
|
|
19
|
+
fireEvent.click(send as Element);
|
|
20
|
+
expect(result.findTextElement("This field is required")).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { H2, Paragraph, YStack } from "@multiplatform.one/components";
|
|
2
|
+
import { Button, Input, TextArea } from "@multiplatform.one/forms";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starter form demonstrating @multiplatform.one/forms fields (Input, TextArea,
|
|
8
|
+
* Button) with the canonical onChangeText/value contract, helper/error slots,
|
|
9
|
+
* and a submit handler. Replace the console.info with a real mutation.
|
|
10
|
+
*/
|
|
11
|
+
export function ContactForm() {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const [name, setName] = useState("");
|
|
14
|
+
const [message, setMessage] = useState("");
|
|
15
|
+
const [submitted, setSubmitted] = useState(false);
|
|
16
|
+
const [sent, setSent] = useState(false);
|
|
17
|
+
|
|
18
|
+
const requiredError = t("screens.contact.required", "This field is required");
|
|
19
|
+
|
|
20
|
+
function handleSend() {
|
|
21
|
+
setSubmitted(true);
|
|
22
|
+
if (!name.trim() || !message.trim()) return;
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.info("[__NAME__] contact form:", { name, message });
|
|
25
|
+
setSent(true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<YStack gap="$4" maxWidth={480}>
|
|
30
|
+
<H2>{t("screens.contact.title", "Contact")}</H2>
|
|
31
|
+
<Input
|
|
32
|
+
label={t("screens.contact.name", "Name")}
|
|
33
|
+
placeholder={t("screens.contact.namePlaceholder", "Your name")}
|
|
34
|
+
value={name}
|
|
35
|
+
onChangeText={setName}
|
|
36
|
+
required
|
|
37
|
+
error={submitted && !name.trim() ? requiredError : undefined}
|
|
38
|
+
/>
|
|
39
|
+
<TextArea
|
|
40
|
+
label={t("screens.contact.message", "Message")}
|
|
41
|
+
placeholder={t("screens.contact.messagePlaceholder", "What do you want to say?")}
|
|
42
|
+
value={message}
|
|
43
|
+
onChangeText={setMessage}
|
|
44
|
+
required
|
|
45
|
+
error={submitted && !message.trim() ? requiredError : undefined}
|
|
46
|
+
/>
|
|
47
|
+
<Button onPress={handleSend}>{t("screens.contact.send", "Send")}</Button>
|
|
48
|
+
{sent ? (
|
|
49
|
+
<Paragraph color="$color11">
|
|
50
|
+
{t("screens.contact.sent", "Message sent (demo) — check the console.")}
|
|
51
|
+
</Paragraph>
|
|
52
|
+
) : null}
|
|
53
|
+
</YStack>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { H1, Paragraph, Screen, Separator, YStack } from "@multiplatform.one/components";
|
|
2
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
3
|
+
import { Link } from "one";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { ContactForm } from "./ContactForm";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Home screen (route: /). Keep product UI in features/<name>; the app shell
|
|
9
|
+
* (apps/<name>) only wires routes and providers.
|
|
10
|
+
*/
|
|
11
|
+
export function HomeScreen() {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
// Knob wiring: spread complete knobProps fragments — never cherry-pick
|
|
14
|
+
// resolved tokens (see AGENTS.md, knobs system).
|
|
15
|
+
const { knobProps } = useResolvedKnobs({ component: "Card" });
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Screen>
|
|
19
|
+
<YStack
|
|
20
|
+
{...knobProps.surface}
|
|
21
|
+
{...knobProps.borderRadius}
|
|
22
|
+
{...knobProps.panelPadding}
|
|
23
|
+
{...knobProps.gap}
|
|
24
|
+
backgroundColor="$color2"
|
|
25
|
+
borderColor="$borderColor"
|
|
26
|
+
>
|
|
27
|
+
<H1 {...knobProps.heading}>{t("screens.home.welcome", "Welcome to __NAME_PASCAL__")}</H1>
|
|
28
|
+
<Paragraph {...knobProps.body} color="$color11">
|
|
29
|
+
{t(
|
|
30
|
+
"screens.home.message",
|
|
31
|
+
"One codebase — web, iOS, and Android. Edit features/__NAME__/src/HomeScreen.tsx to get started.",
|
|
32
|
+
)}
|
|
33
|
+
</Paragraph>
|
|
34
|
+
<Link href="/about">{t("nav.about", "About")}</Link>
|
|
35
|
+
</YStack>
|
|
36
|
+
<Separator />
|
|
37
|
+
<ContactForm />
|
|
38
|
+
</Screen>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AboutScreen, AppLayout, ContactForm, HomeScreen } from "./index";
|
|
3
|
+
|
|
4
|
+
// Source-shape smoke: every starter screen is exported and renderable
|
|
5
|
+
// (component functions). Full render coverage lives next to each component.
|
|
6
|
+
describe("feature exports", () => {
|
|
7
|
+
it("exports the starter screens", () => {
|
|
8
|
+
for (const Component of [AboutScreen, AppLayout, ContactForm, HomeScreen]) {
|
|
9
|
+
expect(typeof Component).toBe("function");
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createVitestConfig } from "@multiplatform.one/config/vitest";
|
|
2
|
+
|
|
3
|
+
export default createVitestConfig({
|
|
4
|
+
tamaguiConfig: "../../packages/themes/tamagui.config.ts",
|
|
5
|
+
setupFiles: ["@multiplatform.one/test-utils/setup"],
|
|
6
|
+
include: ["src/**/*.spec.{ts,tsx}"],
|
|
7
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"rules": {
|
|
4
|
+
"no-unused-vars": [
|
|
5
|
+
"warn",
|
|
6
|
+
{
|
|
7
|
+
"argsIgnorePattern": "^_",
|
|
8
|
+
"varsIgnorePattern": "^_"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"no-console": "warn",
|
|
12
|
+
"eqeqeq": "error",
|
|
13
|
+
"no-var": "error",
|
|
14
|
+
"prefer-const": "error"
|
|
15
|
+
},
|
|
16
|
+
"overrides": [
|
|
17
|
+
{
|
|
18
|
+
"files": [
|
|
19
|
+
"apps/**/*.{ts,tsx,js,jsx}",
|
|
20
|
+
"features/**/*.{ts,tsx,js,jsx}",
|
|
21
|
+
"packages/**/*.{ts,tsx,js,jsx}"
|
|
22
|
+
],
|
|
23
|
+
"rules": {
|
|
24
|
+
"no-restricted-imports": [
|
|
25
|
+
"error",
|
|
26
|
+
{
|
|
27
|
+
"paths": [
|
|
28
|
+
{
|
|
29
|
+
"name": "tamagui",
|
|
30
|
+
"message": "Raw tamagui is only allowed inside framework packages. Import from @multiplatform.one/components, @multiplatform.one/forms, or @multiplatform.one/theme. See AGENTS.md."
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "__NAME__ — multiplatform.one universal app (web + iOS + Android)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "pnpm --filter @app/__NAME__ dev",
|
|
9
|
+
"build": "pnpm --filter @app/__NAME__ build",
|
|
10
|
+
"serve": "pnpm --filter @app/__NAME__ serve",
|
|
11
|
+
"native": "pnpm --filter @app/__NAME__ prebuild:native",
|
|
12
|
+
"ios": "pnpm --filter @app/__NAME__ ios",
|
|
13
|
+
"android": "pnpm --filter @app/__NAME__ android",
|
|
14
|
+
"test": "pnpm -r test",
|
|
15
|
+
"lint": "oxlint -c oxlintrc.json .",
|
|
16
|
+
"typecheck": "pnpm -r typecheck"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/chrome": "^0.2.5",
|
|
20
|
+
"@types/firefox-webext-browser": "^143.0.0",
|
|
21
|
+
"oxlint": "^1.62.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"packageManager": "pnpm@10.32.1"
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@package/config",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts",
|
|
9
|
+
"./config.json": "./config.json"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "~5.9.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app": {
|
|
3
|
+
"name": "__NAME_PASCAL__"
|
|
4
|
+
},
|
|
5
|
+
"nav": {
|
|
6
|
+
"home": "Home",
|
|
7
|
+
"about": "About"
|
|
8
|
+
},
|
|
9
|
+
"screens": {
|
|
10
|
+
"home": {
|
|
11
|
+
"welcome": "Welcome to __NAME_PASCAL__",
|
|
12
|
+
"message": "One codebase — web, iOS, and Android. Edit features/__NAME__/src/HomeScreen.tsx to get started."
|
|
13
|
+
},
|
|
14
|
+
"about": {
|
|
15
|
+
"title": "About",
|
|
16
|
+
"message": "This screen lives in features/__NAME__ and is wired to the /about route by apps/__NAME__/routes/about.tsx.",
|
|
17
|
+
"back": "Back home"
|
|
18
|
+
},
|
|
19
|
+
"contact": {
|
|
20
|
+
"title": "Contact",
|
|
21
|
+
"name": "Name",
|
|
22
|
+
"namePlaceholder": "Your name",
|
|
23
|
+
"message": "Message",
|
|
24
|
+
"messagePlaceholder": "What do you want to say?",
|
|
25
|
+
"send": "Send",
|
|
26
|
+
"required": "This field is required",
|
|
27
|
+
"sent": "Message sent (demo) — check the console."
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app": {
|
|
3
|
+
"name": "__NAME_PASCAL__"
|
|
4
|
+
},
|
|
5
|
+
"nav": {
|
|
6
|
+
"home": "Inicio",
|
|
7
|
+
"about": "Acerca de"
|
|
8
|
+
},
|
|
9
|
+
"screens": {
|
|
10
|
+
"home": {
|
|
11
|
+
"welcome": "Bienvenido a __NAME_PASCAL__",
|
|
12
|
+
"message": "Un solo código — web, iOS y Android. Edita features/__NAME__/src/HomeScreen.tsx para empezar."
|
|
13
|
+
},
|
|
14
|
+
"about": {
|
|
15
|
+
"title": "Acerca de",
|
|
16
|
+
"message": "Esta pantalla vive en features/__NAME__ y se conecta a la ruta /about desde apps/__NAME__/routes/about.tsx.",
|
|
17
|
+
"back": "Volver al inicio"
|
|
18
|
+
},
|
|
19
|
+
"contact": {
|
|
20
|
+
"title": "Contacto",
|
|
21
|
+
"name": "Nombre",
|
|
22
|
+
"namePlaceholder": "Tu nombre",
|
|
23
|
+
"message": "Mensaje",
|
|
24
|
+
"messagePlaceholder": "¿Qué quieres decir?",
|
|
25
|
+
"send": "Enviar",
|
|
26
|
+
"required": "Este campo es obligatorio",
|
|
27
|
+
"sent": "Mensaje enviado (demo) — mira la consola."
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18next TypeScript type augmentations.
|
|
3
|
+
*
|
|
4
|
+
* Key typing is intentionally loose (t() accepts any string; missing keys fall
|
|
5
|
+
* back at runtime). To restore strict typed keys, add
|
|
6
|
+
* `resources: { common: typeof import("./en/common.json") }` to
|
|
7
|
+
* CustomTypeOptions.
|
|
8
|
+
*/
|
|
9
|
+
import "i18next";
|
|
10
|
+
|
|
11
|
+
declare module "i18next" {
|
|
12
|
+
interface CustomTypeOptions {
|
|
13
|
+
defaultNS: "common";
|
|
14
|
+
returnNull: false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported languages. Add a language by creating `<lng>/common.json` and
|
|
3
|
+
* listing the code here (and in resources.ts for native).
|
|
4
|
+
*/
|
|
5
|
+
export const languages = ["en", "es"] as const;
|
|
6
|
+
|
|
7
|
+
/** Translation namespaces — one JSON file per namespace per language. */
|
|
8
|
+
export const namespaces = ["common"] as const;
|
|
9
|
+
|
|
10
|
+
/** Fallback language when a translation is missing. */
|
|
11
|
+
export const defaultLanguage = "en" as const;
|
|
12
|
+
|
|
13
|
+
/** Namespace used when t() calls do not specify one. */
|
|
14
|
+
export const defaultNamespace = "common" as const;
|
|
15
|
+
|
|
16
|
+
export type Language = (typeof languages)[number];
|
|
17
|
+
export type Namespace = (typeof namespaces)[number];
|
|
18
|
+
|
|
19
|
+
// Re-export framework i18n utilities
|
|
20
|
+
export {
|
|
21
|
+
createI18nConfig,
|
|
22
|
+
detectLanguage,
|
|
23
|
+
getPersistedLanguage,
|
|
24
|
+
persistLanguage,
|
|
25
|
+
} from "@multiplatform.one/i18n";
|
|
26
|
+
export type { I18nConfigOptions } from "@multiplatform.one/i18n";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@package/i18n",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts",
|
|
9
|
+
"./resources": "./resources.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@multiplatform.one/i18n": "__MPO_VERSION__"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "~5.9.3"
|
|
19
|
+
}
|
|
20
|
+
}
|