@horka/app-forge 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.
- package/LICENSE +32 -0
- package/README.md +99 -0
- package/bin/cli.js +371 -0
- package/bin/cli.test.js +91 -0
- package/package.json +43 -0
- package/templates/core/CLAUDE.md +36 -0
- package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
- package/templates/core/claude/memory/COMMANDS.md +13 -0
- package/templates/core/claude/memory/DECISIONS.md +5 -0
- package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
- package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
- package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
- package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
- package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
- package/templates/core/claude/skills/save-context/SKILL.md +35 -0
- package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
- package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
- package/templates/core/docs-architecture/DELIVERY.md +68 -0
- package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
- package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
- package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
- package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
- package/templates/core/gitignore +15 -0
- package/templates/core/mcp.json +8 -0
- package/templates/packs/nuxt-web/CLAUDE.md +74 -0
- package/templates/packs/nuxt-web/app/app.vue +5 -0
- package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
- package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
- package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
- package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
- package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
- package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
- package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
- package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
- package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
- package/templates/packs/nuxt-web/gitignore +18 -0
- package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
- package/templates/packs/nuxt-web/pack.json +11 -0
- package/templates/packs/nuxt-web/package.json +31 -0
- package/templates/packs/nuxt-web/playwright.config.ts +39 -0
- package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
- package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
- package/templates/packs/nuxt-web/tsconfig.json +4 -0
- package/templates/packs/nuxt-web/vitest.config.ts +23 -0
- package/templates/packs/swift-ios/CLAUDE.md +64 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
- package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
- package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
- package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
- package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
- package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
- package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
- package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
- package/templates/packs/swift-ios/gitignore +5 -0
- package/templates/packs/swift-ios/mcp.json +8 -0
- package/templates/packs/swift-ios/pack.json +11 -0
- package/templates/packs/swift-ios/project.yml +33 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
- package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
- package/templates/packs/ts-sdk/CLAUDE.md +72 -0
- package/templates/packs/ts-sdk/MIGRATION.md +28 -0
- package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
- package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
- package/templates/packs/ts-sdk/gitignore +6 -0
- package/templates/packs/ts-sdk/pack.json +11 -0
- package/templates/packs/ts-sdk/package.json +55 -0
- package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
- package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
- package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
- package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
- package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
- package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
- package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
- package/templates/packs/ts-sdk/src/index.ts +62 -0
- package/templates/packs/ts-sdk/src/types/index.ts +33 -0
- package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
- package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
- package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
- package/templates/packs/ts-sdk/tsconfig.json +15 -0
- package/templates/packs/ts-sdk/tsup.config.ts +22 -0
- package/templates/packs/ts-sdk/vitest.config.ts +8 -0
- package/templates/packs/vapor-api/CLAUDE.md +73 -0
- package/templates/packs/vapor-api/Dockerfile +80 -0
- package/templates/packs/vapor-api/Package.swift +68 -0
- package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
- package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
- package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
- package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
- package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
- package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
- package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
- package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
- package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
- package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
- package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
- package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
- package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
- package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
- package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
- package/templates/packs/vapor-api/env_dist +29 -0
- package/templates/packs/vapor-api/gitignore +7 -0
- package/templates/packs/vapor-api/pack.json +11 -0
- package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
- package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SEO & ROUTING — mixed public/private apps on SSR
|
|
2
|
+
|
|
3
|
+
The default product shape this pack assumes: public marketing pages (home, pricing, features,
|
|
4
|
+
changelog…) and a private app behind auth — in ONE Nuxt codebase. This file is the recipe
|
|
5
|
+
that keeps both worlds correct.
|
|
6
|
+
|
|
7
|
+
## 1. SSR is on and stays on
|
|
8
|
+
|
|
9
|
+
`ssr: true` is the pack default. Public pages must deliver their content **in the server
|
|
10
|
+
response** — crawlers and link previews don't run your client fetches reliably, and users
|
|
11
|
+
shouldn't watch spinners for static copy.
|
|
12
|
+
|
|
13
|
+
Proof command (run it on every public page before calling it done):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -s http://localhost:3000/pricing | grep -i "your headline text"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If the content isn't in the HTML, the page is fetching client-side — fix the data fetching
|
|
20
|
+
(CONVENTIONS.md §4: `useAsyncData` in setup, `onMounted` only with a `CLIENT-ONLY:` rationale).
|
|
21
|
+
|
|
22
|
+
## 2. Auth policy lives ON the route — default deny
|
|
23
|
+
|
|
24
|
+
Public pages declare their own policy, next to their own code:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// app/pages/pricing.vue
|
|
28
|
+
definePageMeta({ auth: false })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The global middleware reads route meta and **default-denies** everything else:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app/middleware/auth.global.ts — add the day auth lands; the meta convention starts day one
|
|
35
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
36
|
+
if (to.meta.auth === false) return // page opted out, explicitly
|
|
37
|
+
const { isAuthenticated } = useSession() // your auth provider's session check
|
|
38
|
+
if (!isAuthenticated.value) {
|
|
39
|
+
return navigateTo({ path: '/', query: { redirect: to.fullPath } })
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Absolute rules:
|
|
45
|
+
- **Never a name allowlist** (`['index', 'pricing', …]`) — it's a second route table that
|
|
46
|
+
silently drifts from the real one (ANTI_PATTERNS.md #2).
|
|
47
|
+
- New pages are **private by default**; making one public is a one-line, reviewable, greppable
|
|
48
|
+
diff on the page itself: `grep -rn "auth: false" app/pages` lists the public surface.
|
|
49
|
+
- The redirect target preserves `redirect=` so login returns the user where they were headed.
|
|
50
|
+
|
|
51
|
+
> 📖 **War story:** a production team used a hand-maintained array of public page names in the
|
|
52
|
+
> auth middleware. A public changelog page shipped but was never added to the list — anonymous
|
|
53
|
+
> visitors (and crawlers) were silently 302'd to the homepage **for weeks**. No error, no log,
|
|
54
|
+
> no failing test; the sitemap even advertised the URL. Route-meta + default-deny makes that
|
|
55
|
+
> failure impossible: the page carries its own policy, and forgetting it fails *closed* (a
|
|
56
|
+
> private page stays private) instead of breaking the public site invisibly.
|
|
57
|
+
|
|
58
|
+
## 3. Sitemap & robots — explicit allowlist, never the route table
|
|
59
|
+
|
|
60
|
+
- The sitemap lists **only an explicit array of public URLs**, maintained next to the sitemap
|
|
61
|
+
config. Never auto-generate from the route table — your route table contains the private app.
|
|
62
|
+
With `@nuxtjs/sitemap`: `excludeAppSources: true` + an explicit `urls` list.
|
|
63
|
+
- Adding a public page = page file + `auth: false` + sitemap entry + i18n module. Put this
|
|
64
|
+
4-item checklist in the PR template.
|
|
65
|
+
- Private sections also send `X-Robots-Tag` via route rules (belt and braces with the auth wall):
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
routeRules: {
|
|
69
|
+
'/': { prerender: true }, // pure-static marketing
|
|
70
|
+
'/pricing': { prerender: true },
|
|
71
|
+
'/changelog': { swr: 3600 }, // public but updated often
|
|
72
|
+
'/app/**': { headers: { 'X-Robots-Tag': 'noindex' } } // private app surface
|
|
73
|
+
},
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Staging must never be indexed**: gate a global `robots: noindex, nofollow` meta on the
|
|
77
|
+
environment from runtime config — not on `NODE_ENV`, which is `production` on staging builds.
|
|
78
|
+
|
|
79
|
+
## 4. Per-page meta
|
|
80
|
+
|
|
81
|
+
Every public page owns its meta in setup — no global defaults pretending to be content:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
useSeoMeta({
|
|
85
|
+
title: t('pricing.meta.title'),
|
|
86
|
+
description: t('pricing.meta.description'),
|
|
87
|
+
ogTitle: t('pricing.meta.title'),
|
|
88
|
+
ogImage: `${config.public.baseUrl}/og/pricing.png`, // ABSOLUTE URL — relative og:image is ignored
|
|
89
|
+
twitterCard: 'summary_large_image',
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- `og:image` and canonical URLs must be **absolute**, built from `runtimeConfig.public.baseUrl`
|
|
94
|
+
— never hardcoded hostnames (they rot across environments).
|
|
95
|
+
- One `<h1>` per page; headings follow document order — crawlers and screen readers share it.
|
|
96
|
+
- Dynamic public pages (blog posts…) set meta from the same `useAsyncData` payload that renders
|
|
97
|
+
the content, so meta and body can't diverge.
|
|
98
|
+
|
|
99
|
+
## 5. Routing hygiene
|
|
100
|
+
|
|
101
|
+
- File-based routes only; no `hashMode`. Deep links into the private app must survive the
|
|
102
|
+
login round-trip (the `redirect=` query above).
|
|
103
|
+
- Route params are validated in the page (`validate`) — a malformed id is a 404, not a blank
|
|
104
|
+
page with a console error.
|
|
105
|
+
- Error states route through `error.vue` with correct status codes:
|
|
106
|
+
`throw createError({ statusCode: 404, statusMessage: 'Not found' })` during SSR returns a
|
|
107
|
+
real 404 to crawlers — a soft-404 (200 + "not found" text) poisons indexing.
|
|
108
|
+
- i18n URL strategy: app-only products may hide locale (`no_prefix`); indexable multilingual
|
|
109
|
+
marketing needs per-locale URLs + `hreflang` (I18N.md §5). Record the choice in DECISIONS.md.
|
|
110
|
+
|
|
111
|
+
## 6. Pre-ship checklist (public pages)
|
|
112
|
+
|
|
113
|
+
- [ ] `definePageMeta({ auth: false })` present
|
|
114
|
+
- [ ] Content visible in `curl` output (SSR-rendered, no client fetch for primary content)
|
|
115
|
+
- [ ] `useSeoMeta` with title/description/absolute ogImage
|
|
116
|
+
- [ ] Sitemap entry added; staging still noindex
|
|
117
|
+
- [ ] i18n module exists in every locale (parity check green)
|
|
118
|
+
- [ ] Status codes correct for the page's error paths (404 via `createError`, not a soft-404)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Nuxt
|
|
2
|
+
.nuxt/
|
|
3
|
+
.output/
|
|
4
|
+
dist/
|
|
5
|
+
node_modules/
|
|
6
|
+
|
|
7
|
+
# env — never commit real env files
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
|
|
11
|
+
# test artifacts — generated reports are claims, not proof (ANTI_PATTERNS.md #10)
|
|
12
|
+
coverage/
|
|
13
|
+
playwright-report/
|
|
14
|
+
test-results/
|
|
15
|
+
|
|
16
|
+
# misc
|
|
17
|
+
*.log
|
|
18
|
+
.cache/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Layer mapping and the reasoning behind every block: docs-architecture/ARCHITECTURE.md
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
compatibilityDate: '2025-07-15',
|
|
4
|
+
|
|
5
|
+
// SSR is ON and stays on. Public pages must render their content server-side;
|
|
6
|
+
// data fetching happens in setup (useAsyncData), never in onMounted without a
|
|
7
|
+
// written CLIENT-ONLY rationale. See docs-architecture/SEO_AND_ROUTING.md §1.
|
|
8
|
+
ssr: true,
|
|
9
|
+
|
|
10
|
+
devtools: { enabled: true },
|
|
11
|
+
|
|
12
|
+
css: [
|
|
13
|
+
'~/assets/css/tokens.css', // L0 — the ONLY place visual values are defined
|
|
14
|
+
'~/assets/css/main.css',
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
// Module auto-registration: DS modules (L3, domain-blind) and feature modules (L4)
|
|
18
|
+
// expose their components/ subfolder. Adding a module requires zero config here.
|
|
19
|
+
components: [
|
|
20
|
+
// NOTE: glob goes in `pattern`, not `path` — a glob inside `path` is silently
|
|
21
|
+
// ignored by the scanner (zero components registered, no warning).
|
|
22
|
+
{ path: '~/designSystem', pattern: '**/components/**', pathPrefix: false },
|
|
23
|
+
{ path: '~/features', pattern: '**/components/**', pathPrefix: false },
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
imports: {
|
|
27
|
+
dirs: [
|
|
28
|
+
'~/designSystem/**/composables',
|
|
29
|
+
'~/features/**/composables',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
runtimeConfig: {
|
|
34
|
+
// Server-only values (override at runtime: NUXT_<KEY>). Secrets live HERE.
|
|
35
|
+
public: {
|
|
36
|
+
// Client-visible values (override at runtime: NUXT_PUBLIC_<KEY>) — never secrets.
|
|
37
|
+
apiBaseUrl: '',
|
|
38
|
+
baseUrl: 'http://localhost:3000',
|
|
39
|
+
environment: 'development',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
app: {
|
|
44
|
+
head: {
|
|
45
|
+
htmlAttrs: { lang: 'en' },
|
|
46
|
+
title: '{{PROJECT_NAME}}',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nuxt-web",
|
|
3
|
+
"label": "Web app — Nuxt 4 / Vue 3 / TypeScript",
|
|
4
|
+
"languages": ["typescript", "vue"],
|
|
5
|
+
"idPrompt": "App identifier (reverse-DNS)",
|
|
6
|
+
"requirements": [
|
|
7
|
+
"Node.js 20+ (LTS) and npm",
|
|
8
|
+
"Playwright browsers for e2e (one-time): npx playwright install"
|
|
9
|
+
],
|
|
10
|
+
"notes": "Skeleton builds and tests day one: npm install && npm run test && npm run build"
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{BUNDLE_ID}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=20"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "nuxt dev",
|
|
10
|
+
"build": "nuxt build",
|
|
11
|
+
"generate": "nuxt generate",
|
|
12
|
+
"preview": "nuxt preview",
|
|
13
|
+
"postinstall": "nuxt prepare",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:e2e": "playwright test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"nuxt": "^4.0.0",
|
|
20
|
+
"vue": "^3.5.0",
|
|
21
|
+
"vue-router": "^4.5.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@playwright/test": "^1.49.0",
|
|
25
|
+
"@vitejs/plugin-vue": "^5.2.0",
|
|
26
|
+
"@vue/test-utils": "^2.4.6",
|
|
27
|
+
"happy-dom": "^20.0.0",
|
|
28
|
+
"typescript": "^5.6.0",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// E2E suite — real dependency, real config, runs from a clean checkout
|
|
4
|
+
// (one-time per machine: `npx playwright install`). Specs live in tests/e2e ONLY;
|
|
5
|
+
// vitest owns app/** and tests/unit/**. Reports are gitignored — a committed
|
|
6
|
+
// report is a claim, a green run is proof (ANTI_PATTERNS.md #10).
|
|
7
|
+
|
|
8
|
+
// One source of truth for the e2e origin, so `use.baseURL` and `webServer.url` can never
|
|
9
|
+
// drift apart. Override with PLAYWRIGHT_BASE_URL to point at a deployed preview.
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ Port-collision trap: locally `reuseExistingServer` is true, so if ANYTHING is already
|
|
12
|
+
// listening on this port (a stale `nuxt dev`, another app, a previous crashed run) Playwright
|
|
13
|
+
// silently tests THAT process instead of a fresh build — a false green or false red that has
|
|
14
|
+
// nothing to do with your code. Port 3000 is heavily contended; this skeleton uses a less
|
|
15
|
+
// common 4173. If a run looks wrong, kill stragglers first: `lsof -ti:4173 | xargs kill`,
|
|
16
|
+
// or set PLAYWRIGHT_BASE_URL to a clean port. In CI, reuseExistingServer is false (always fresh).
|
|
17
|
+
const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 4173)
|
|
18
|
+
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${PORT}`
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
testDir: 'tests/e2e',
|
|
22
|
+
fullyParallel: true,
|
|
23
|
+
forbidOnly: !!process.env.CI,
|
|
24
|
+
retries: process.env.CI ? 2 : 0,
|
|
25
|
+
reporter: [['html', { open: 'never' }]],
|
|
26
|
+
use: {
|
|
27
|
+
baseURL: BASE_URL,
|
|
28
|
+
trace: 'on-first-retry',
|
|
29
|
+
},
|
|
30
|
+
projects: [
|
|
31
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
32
|
+
],
|
|
33
|
+
webServer: {
|
|
34
|
+
command: `npm run dev -- --port ${PORT}`,
|
|
35
|
+
url: BASE_URL,
|
|
36
|
+
reuseExistingServer: !process.env.CI,
|
|
37
|
+
timeout: 120_000,
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The SINGLE health endpoint — Docker HEALTHCHECK, orchestrator checks, deploy
|
|
2
|
+
// gates and uptime monitoring all point HERE and nowhere else (OPS_WEB.md §7).
|
|
3
|
+
export default defineEventHandler(() => ({
|
|
4
|
+
status: 'ok',
|
|
5
|
+
service: '{{BUNDLE_ID}}',
|
|
6
|
+
time: new Date().toISOString(),
|
|
7
|
+
}))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// Example e2e spec — proves the wiring is real (server boots, page SSRs, DS button reacts).
|
|
4
|
+
// One-time per machine: `npx playwright install`. Then: `npm run test:e2e`.
|
|
5
|
+
test('home page renders server-side and the DS button responds', async ({ page }) => {
|
|
6
|
+
await page.goto('/')
|
|
7
|
+
await expect(page.getByRole('heading', { level: 1 })).toContainText('{{PROJECT_NAME}}')
|
|
8
|
+
|
|
9
|
+
const button = page.getByRole('button', { name: /clicked/i })
|
|
10
|
+
await expect(button).toContainText('0')
|
|
11
|
+
await button.click()
|
|
12
|
+
await expect(button).toContainText('1')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('health endpoint answers', async ({ request }) => {
|
|
16
|
+
const response = await request.get('/api/health')
|
|
17
|
+
expect(response.ok()).toBeTruthy()
|
|
18
|
+
expect((await response.json()).status).toBe('ok')
|
|
19
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { defineConfig } from 'vitest/config'
|
|
4
|
+
|
|
5
|
+
// Unit tests run WITHOUT booting Nuxt: pure domain code and DS components mount
|
|
6
|
+
// standalone. Keep it that way — this is the fast feedback loop (ARCHITECTURE.md §7).
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [vue()],
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
'~': fileURLToPath(new URL('./app', import.meta.url)),
|
|
12
|
+
'@': fileURLToPath(new URL('./app', import.meta.url)),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
test: {
|
|
16
|
+
environment: 'happy-dom',
|
|
17
|
+
// tests/e2e belongs to Playwright — vitest must never pick it up
|
|
18
|
+
include: [
|
|
19
|
+
'app/**/*.{test,spec}.ts',
|
|
20
|
+
'tests/unit/**/*.{test,spec}.ts',
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — Claude Code Operating Manual
|
|
2
|
+
|
|
3
|
+
This project was scaffolded by **AppForge** (pack: iOS/Swift): a Claude-Code-first
|
|
4
|
+
architecture extracted from production apps. You (Claude) are the team lead AND the
|
|
5
|
+
primary developer. Follow this manual exactly — it encodes hard-won lessons, not preferences.
|
|
6
|
+
|
|
7
|
+
## Identity
|
|
8
|
+
- App: **{{PROJECT_NAME}}** · Bundle id: `{{BUNDLE_ID}}` · iOS 26+ · Swift 6.2 (strict concurrency)
|
|
9
|
+
- Backend: CloudKit-only by default (container `iCloud.{{BUNDLE_ID}}`) — adapt if the PRD says otherwise.
|
|
10
|
+
|
|
11
|
+
## Session protocol (MANDATORY)
|
|
12
|
+
1. **Session start**: run the `restore-context` skill — read `.claude/memory/*.md` before doing anything. Never invent project facts.
|
|
13
|
+
2. **Empty project / new idea**: run the `kickoff` skill — it interviews the user, writes the PRD, plans slices, then builds autonomously.
|
|
14
|
+
3. **After significant work**: update `.claude/memory/PROJECT_STATE.md` (and DECISIONS/NEXT_STEPS when relevant) — `save-context` skill.
|
|
15
|
+
|
|
16
|
+
## Architecture (read the docs before coding)
|
|
17
|
+
The knowledge base lives in `docs-architecture/`. Read the relevant doc BEFORE touching that area:
|
|
18
|
+
|
|
19
|
+
| You are about to… | Read first |
|
|
20
|
+
|---|---|
|
|
21
|
+
| understand the layer model (stack-agnostic) | `ARCHITECTURE_PRINCIPLES.md` |
|
|
22
|
+
| plan/deliver slices, validate, update memory | `DELIVERY.md` |
|
|
23
|
+
| product spans repos (API + SDK + clients) | `MULTI_REPO_CONTRACT.md` |
|
|
24
|
+
| ship/consume a typed SDK | `SDK_CONTRACT.md` |
|
|
25
|
+
| accept user-supplied URLs (webhooks…) | `SECURITY_USER_URLS.md` |
|
|
26
|
+
| write any documentation | `DOCS_PLACEMENT.md` |
|
|
27
|
+
| add caching/feature flags/auth shortcuts | `ANTI_PATTERNS.md` |
|
|
28
|
+
| add/move any file, create a feature | `ARCHITECTURE.md` |
|
|
29
|
+
| write any Swift code | `CONVENTIONS.md` |
|
|
30
|
+
| add a screen, sheet, deeplink, push routing | `NAVIGATION.md` |
|
|
31
|
+
| touch CloudKit, CKShare, sync, subscriptions | `CLOUDKIT_GUIDE.md` |
|
|
32
|
+
| style anything (colors, fonts, spacing) | `DESIGN_SYSTEM.md` |
|
|
33
|
+
| write or modify domain logic | `TESTING.md` |
|
|
34
|
+
| build, run, validate, debug on device | `WORKFLOW.md` |
|
|
35
|
+
|
|
36
|
+
Layer summary (universal contract in ARCHITECTURE_PRINCIPLES.md, Swift mapping in ARCHITECTURE.md):
|
|
37
|
+
L0 `{{PROJECT_NAME}}DS` tokens · L1 Ops (create when needed) · L2 `DataLayer` (implements L3 contracts) ·
|
|
38
|
+
L3 `{{PROJECT_NAME}}Core` (pure domain — **never imports SwiftUI**) + DS `Components/` (Core UI) ·
|
|
39
|
+
L4 app `Module/` (shared feature bricks) · L5 app `App/` + `Store/` + `Tools/`. Imports point downward only.
|
|
40
|
+
|
|
41
|
+
## Non-negotiable rules
|
|
42
|
+
- **Packages first**: `swift build`/`swift test --package-path Packages/<X>` before any `xcodebuild`. Fast, precise errors.
|
|
43
|
+
- **Never claim done without proof**: package tests green + app build green + (for UI) a simulator screenshot you actually looked at.
|
|
44
|
+
- **Design tokens only**: no hardcoded colors/fonts/spacing in app code — everything through `{{PROJECT_NAME}}DS`.
|
|
45
|
+
- **Pure domain logic**: engines are `nonisolated enum`s with injected `Calendar`/dates. Every rule ships with a test.
|
|
46
|
+
- **Logging**: `os.Logger` with `privacy: .public` interpolation (print() is invisible on device).
|
|
47
|
+
- **Memory is law**: contradictions between memory files and code → code wins, then fix the memory file.
|
|
48
|
+
|
|
49
|
+
## Build commands
|
|
50
|
+
```bash
|
|
51
|
+
# per-package loop (seconds)
|
|
52
|
+
swift build --package-path Packages/{{PROJECT_NAME}}Core
|
|
53
|
+
swift test --package-path Packages/{{PROJECT_NAME}}Core
|
|
54
|
+
|
|
55
|
+
# app target (after xcodegen generate, only when packages are green)
|
|
56
|
+
xcodebuild -project {{PROJECT_NAME}}.xcodeproj -scheme {{PROJECT_NAME}} \
|
|
57
|
+
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
|
|
58
|
+
-derivedDataPath /tmp/{{PROJECT_NAME}}_dd build 2>&1 | grep -E "error:|BUILD"
|
|
59
|
+
```
|
|
60
|
+
Simulator validation: use the `ios-simulator` MCP (install_app → launch_app → screenshot → look at it).
|
|
61
|
+
|
|
62
|
+
## Git
|
|
63
|
+
- Never push without explicit user approval. Feature branches; commit format `add/update/fix(scope) - description`.
|
|
64
|
+
- No AI attribution in commits or file headers.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// swift-tools-version: 6.2
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "DataLayer",
|
|
6
|
+
platforms: [.iOS(.v26), .macOS(.v15)],
|
|
7
|
+
products: [.library(name: "DataLayer", targets: ["DataLayer"])],
|
|
8
|
+
dependencies: [
|
|
9
|
+
.package(path: "../{{PROJECT_NAME}}Core"),
|
|
10
|
+
],
|
|
11
|
+
targets: [
|
|
12
|
+
// REAL repository implementations only (CloudKit, URLSession, …). Protocols AND their
|
|
13
|
+
// InMemory variant (tests/previews/offline) live in Core — DataLayer ships IO-backed
|
|
14
|
+
// impls of those contracts. See docs-architecture/ARCHITECTURE.md §2.
|
|
15
|
+
.target(
|
|
16
|
+
name: "DataLayer",
|
|
17
|
+
dependencies: [.product(name: "{{PROJECT_NAME}}Core", package: "{{PROJECT_NAME}}Core")],
|
|
18
|
+
swiftSettings: [.swiftLanguageMode(.v6)]
|
|
19
|
+
),
|
|
20
|
+
]
|
|
21
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {{PROJECT_NAME}}Core
|
|
2
|
+
|
|
3
|
+
/// PERMANENT file — do not delete (an SPM target with zero sources does not build).
|
|
4
|
+
/// DataLayer hosts the REAL repository implementations (CloudKit, URLSession, database…),
|
|
5
|
+
/// each conforming to a contract declared in {{PROJECT_NAME}}Core (ports & adapters).
|
|
6
|
+
/// Real implementations are added by slices that need them; until then this marker keeps
|
|
7
|
+
/// the target alive. See docs-architecture/CLOUDKIT_GUIDE.md before writing a CloudKit impl.
|
|
8
|
+
public enum DataLayer {
|
|
9
|
+
/// Bump when a real implementation lands, so the placeholder's job is visible in reviews.
|
|
10
|
+
public static let implementations: [String] = []
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// swift-tools-version: 6.2
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "{{PROJECT_NAME}}Core",
|
|
6
|
+
platforms: [.iOS(.v26), .macOS(.v15)],
|
|
7
|
+
products: [.library(name: "{{PROJECT_NAME}}Core", targets: ["{{PROJECT_NAME}}Core"])],
|
|
8
|
+
targets: [
|
|
9
|
+
// Pure domain logic. NEVER imports SwiftUI/UIKit. Fully testable with `swift test`.
|
|
10
|
+
.target(
|
|
11
|
+
name: "{{PROJECT_NAME}}Core",
|
|
12
|
+
swiftSettings: [.swiftLanguageMode(.v6)]
|
|
13
|
+
),
|
|
14
|
+
.testTarget(
|
|
15
|
+
name: "{{PROJECT_NAME}}CoreTests",
|
|
16
|
+
dependencies: ["{{PROJECT_NAME}}Core"],
|
|
17
|
+
swiftSettings: [.swiftLanguageMode(.v6)]
|
|
18
|
+
),
|
|
19
|
+
]
|
|
20
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Sample domain entity — replace at kickoff. Shows the conventions:
|
|
4
|
+
/// value type, Identifiable + Sendable, explicit dates injected (never read the clock here).
|
|
5
|
+
public struct SampleItem: Identifiable, Hashable, Sendable {
|
|
6
|
+
public let id: UUID
|
|
7
|
+
public var title: String
|
|
8
|
+
public var createdAt: Date
|
|
9
|
+
|
|
10
|
+
public init(id: UUID = UUID(), title: String, createdAt: Date) {
|
|
11
|
+
self.id = id
|
|
12
|
+
self.title = title
|
|
13
|
+
self.createdAt = createdAt
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Sample pure engine — replace at kickoff. Conventions on display:
|
|
4
|
+
/// `enum` (no instances), nonisolated pure functions, `Calendar` injected for determinism.
|
|
5
|
+
public enum SampleEngine {
|
|
6
|
+
/// Items created on the same calendar day as `reference`.
|
|
7
|
+
public static func items(
|
|
8
|
+
_ items: [SampleItem],
|
|
9
|
+
onSameDayAs reference: Date,
|
|
10
|
+
calendar: Calendar = .current
|
|
11
|
+
) -> [SampleItem] {
|
|
12
|
+
items.filter { calendar.isDate($0.createdAt, inSameDayAs: reference) }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Sample repository pair — replace at kickoff. The pattern (see docs-architecture/ARCHITECTURE.md §2):
|
|
4
|
+
/// the CONTRACT and the InMemory reference implementation live HERE in Core (testable without IO
|
|
5
|
+
/// frameworks); the real backend implementation (CloudKit/network) lives in DataLayer and imports
|
|
6
|
+
/// this protocol — the one sanctioned upward arrow (ports & adapters).
|
|
7
|
+
public protocol SampleItemRepository: Sendable {
|
|
8
|
+
func loadItems() async throws -> [SampleItem]
|
|
9
|
+
func save(_ item: SampleItem) async throws
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Backs tests, previews and offline-first development.
|
|
13
|
+
public actor InMemorySampleItemRepository: SampleItemRepository {
|
|
14
|
+
private var items: [SampleItem] = []
|
|
15
|
+
|
|
16
|
+
public init() {}
|
|
17
|
+
|
|
18
|
+
public func loadItems() async throws -> [SampleItem] { items }
|
|
19
|
+
|
|
20
|
+
public func save(_ item: SampleItem) async throws {
|
|
21
|
+
if let index = items.firstIndex(where: { $0.id == item.id }) {
|
|
22
|
+
items[index] = item // update in place — keeps stable order
|
|
23
|
+
} else {
|
|
24
|
+
items.append(item)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import {{PROJECT_NAME}}Core
|
|
4
|
+
|
|
5
|
+
/// Swift Testing (@Test/#expect), deterministic dates via a Fixture helper. This is the pattern
|
|
6
|
+
/// every engine test follows. Delete alongside SampleEngine at kickoff.
|
|
7
|
+
@Suite struct SampleEngineTests {
|
|
8
|
+
/// Fixed Gregorian/UTC calendar — never `Calendar.current` (its timezone moves day boundaries
|
|
9
|
+
/// between machines/CI and makes hour/day-boundary tests flaky). Inject it everywhere.
|
|
10
|
+
private static let calendar: Calendar = {
|
|
11
|
+
var calendar = Calendar(identifier: .gregorian)
|
|
12
|
+
calendar.timeZone = TimeZone(identifier: "UTC")!
|
|
13
|
+
return calendar
|
|
14
|
+
}()
|
|
15
|
+
|
|
16
|
+
private static func date(_ year: Int, _ month: Int, _ day: Int) -> Date {
|
|
17
|
+
var components = DateComponents()
|
|
18
|
+
(components.year, components.month, components.day) = (year, month, day)
|
|
19
|
+
return calendar.date(from: components)!
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test func sameDayFilterKeepsOnlyMatchingItems() {
|
|
23
|
+
let monday = Self.date(2026, 6, 1)
|
|
24
|
+
let tuesday = Self.date(2026, 6, 2)
|
|
25
|
+
let items = [
|
|
26
|
+
SampleItem(title: "a", createdAt: monday),
|
|
27
|
+
SampleItem(title: "b", createdAt: tuesday),
|
|
28
|
+
]
|
|
29
|
+
let result = SampleEngine.items(items, onSameDayAs: monday, calendar: Self.calendar)
|
|
30
|
+
#expect(result.map(\.title) == ["a"])
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// swift-tools-version: 6.2
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "{{PROJECT_NAME}}DS",
|
|
6
|
+
platforms: [.iOS(.v26), .macOS(.v15)],
|
|
7
|
+
products: [.library(name: "{{PROJECT_NAME}}DS", targets: ["{{PROJECT_NAME}}DS"])],
|
|
8
|
+
targets: [
|
|
9
|
+
.target(
|
|
10
|
+
name: "{{PROJECT_NAME}}DS",
|
|
11
|
+
swiftSettings: [
|
|
12
|
+
.swiftLanguageMode(.v6),
|
|
13
|
+
.defaultIsolation(MainActor.self),
|
|
14
|
+
]
|
|
15
|
+
),
|
|
16
|
+
]
|
|
17
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Semantic palette. Dark-first. Rename/extend at kickoff — never bypass with raw values.
|
|
4
|
+
public extension Color {
|
|
5
|
+
enum DS {
|
|
6
|
+
public static let accent = Color(red: 0.35, green: 0.45, blue: 1.00)
|
|
7
|
+
public static let accentSecondary = Color(red: 0.55, green: 0.30, blue: 0.95)
|
|
8
|
+
public static let background = Color(red: 0.039, green: 0.039, blue: 0.059)
|
|
9
|
+
public static let surface = Color.white.opacity(0.06)
|
|
10
|
+
public static let surfaceStrong = Color.white.opacity(0.10)
|
|
11
|
+
public static let stroke = Color.white.opacity(0.12)
|
|
12
|
+
public static let textPrimary = Color.white
|
|
13
|
+
public static let textSecondary = Color.white.opacity(0.6)
|
|
14
|
+
public static let textTertiary = Color.white.opacity(0.4)
|
|
15
|
+
public static let success = Color(red: 0.20, green: 0.85, blue: 0.45)
|
|
16
|
+
public static let danger = Color(red: 1.00, green: 0.27, blue: 0.36)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Starter Core-UI component (L3 — domain-blind). The Components/ folder grows at kickoff;
|
|
4
|
+
/// every component here uses tokens only and knows nothing about the domain.
|
|
5
|
+
public struct DSCard<Content: View>: View {
|
|
6
|
+
private let content: Content
|
|
7
|
+
|
|
8
|
+
public init(@ViewBuilder content: () -> Content) {
|
|
9
|
+
self.content = content()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public var body: some View {
|
|
13
|
+
content
|
|
14
|
+
.padding(DS.Padding.m)
|
|
15
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
16
|
+
.background(Color.DS.surface, in: RoundedRectangle(cornerRadius: DS.Radius.m))
|
|
17
|
+
.overlay(
|
|
18
|
+
RoundedRectangle(cornerRadius: DS.Radius.m)
|
|
19
|
+
.stroke(Color.DS.stroke, lineWidth: DS.Size.hairline)
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
}
|
package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Design system namespace. ALL visual tokens live here — app code never hardcodes values.
|
|
4
|
+
public enum DS {
|
|
5
|
+
/// Responsive scaling hook (1.0 for now; derive from screen size later if needed).
|
|
6
|
+
public static let ratio: CGFloat = 1.0
|
|
7
|
+
|
|
8
|
+
public enum Padding {
|
|
9
|
+
public static let xs: CGFloat = 4 * DS.ratio
|
|
10
|
+
public static let s: CGFloat = 8 * DS.ratio
|
|
11
|
+
public static let m: CGFloat = 16 * DS.ratio
|
|
12
|
+
public static let l: CGFloat = 24 * DS.ratio
|
|
13
|
+
public static let xl: CGFloat = 40 * DS.ratio
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public enum Radius {
|
|
17
|
+
public static let s: CGFloat = 10 * DS.ratio
|
|
18
|
+
public static let m: CGFloat = 16 * DS.ratio
|
|
19
|
+
public static let l: CGFloat = 24 * DS.ratio
|
|
20
|
+
public static let pill: CGFloat = 999
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public enum Size {
|
|
24
|
+
public static let hairline: CGFloat = 1
|
|
25
|
+
public static let icon: CGFloat = 24 * DS.ratio
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Signature gradients — customize at kickoff to match the app's brand.
|
|
29
|
+
public enum Gradients {
|
|
30
|
+
public static let brand = LinearGradient(
|
|
31
|
+
colors: [Color.DS.accent, Color.DS.accentSecondary],
|
|
32
|
+
startPoint: .topLeading,
|
|
33
|
+
endPoint: .bottomTrailing
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|