From 0a9c38161b17ee9318284be5cbbc75d1e0ff02fa Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 20:12:36 +0200 Subject: [PATCH] =?UTF-8?q?feat(guides):=20add=20mana=20guides=20app=20?= =?UTF-8?q?=E2=80=94=20step-by-step=20playbook=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New app at apps/guides/ with local-first architecture (Dexie.js + mana-sync). - 5 collections: guides, sections, steps, collections, runs - Library view: card grid with search, category & difficulty filters - Guide detail: sections/steps overview, start-run buttons - Run mode: scroll (all steps) + focus (one step at a time) with note support - Collections view: learning paths with progress bars - History view: all runs with timestamps and duration - Guest seed: 3 demo guides (dev setup, pasta recipe, git basics) - GuideCard with run-status indicator (○◑●⟳) - GuideEditModal with emoji, color, difficulty, tags - Registered in shared-branding: port 5200, teal #0d9488, guides.mana.how - Plan doc: .claude/plans/mana-guides.md Co-Authored-By: Claude Sonnet 4.6 --- .claude/plans/mana-guides.md | 233 +++++++++++++++ apps/guides/apps/web/package.json | 61 ++++ apps/guides/apps/web/src/app.css | 34 +++ apps/guides/apps/web/src/app.d.ts | 2 + apps/guides/apps/web/src/app.html | 26 ++ apps/guides/apps/web/src/hooks.client.ts | 12 + .../web/src/lib/components/GuideCard.svelte | 79 +++++ .../src/lib/components/GuideEditModal.svelte | 251 ++++++++++++++++ .../apps/web/src/lib/data/guest-seed.ts | 245 +++++++++++++++ .../apps/web/src/lib/data/local-store.ts | 120 ++++++++ .../apps/web/src/lib/stores/auth.svelte.ts | 3 + .../apps/web/src/lib/stores/guides.svelte.ts | 135 +++++++++ .../apps/web/src/lib/stores/runs.svelte.ts | 87 ++++++ .../apps/web/src/routes/(app)/+layout.svelte | 118 ++++++++ .../apps/web/src/routes/(app)/+page.svelte | 149 ++++++++++ .../src/routes/(app)/collections/+page.svelte | 107 +++++++ .../src/routes/(app)/guide/[id]/+page.svelte | 252 ++++++++++++++++ .../routes/(app)/guide/[id]/run/+page.svelte | 278 ++++++++++++++++++ .../web/src/routes/(app)/history/+page.svelte | 114 +++++++ .../guides/apps/web/src/routes/+layout.svelte | 37 +++ apps/guides/apps/web/svelte.config.js | 14 + apps/guides/apps/web/tsconfig.json | 14 + apps/guides/apps/web/vite.config.ts | 49 +++ apps/guides/package.json | 14 + package.json | 5 + packages/shared-branding/src/app-icons.ts | 3 + packages/shared-branding/src/mana-apps.ts | 18 ++ 27 files changed, 2460 insertions(+) create mode 100644 .claude/plans/mana-guides.md create mode 100644 apps/guides/apps/web/package.json create mode 100644 apps/guides/apps/web/src/app.css create mode 100644 apps/guides/apps/web/src/app.d.ts create mode 100644 apps/guides/apps/web/src/app.html create mode 100644 apps/guides/apps/web/src/hooks.client.ts create mode 100644 apps/guides/apps/web/src/lib/components/GuideCard.svelte create mode 100644 apps/guides/apps/web/src/lib/components/GuideEditModal.svelte create mode 100644 apps/guides/apps/web/src/lib/data/guest-seed.ts create mode 100644 apps/guides/apps/web/src/lib/data/local-store.ts create mode 100644 apps/guides/apps/web/src/lib/stores/auth.svelte.ts create mode 100644 apps/guides/apps/web/src/lib/stores/guides.svelte.ts create mode 100644 apps/guides/apps/web/src/lib/stores/runs.svelte.ts create mode 100644 apps/guides/apps/web/src/routes/(app)/+layout.svelte create mode 100644 apps/guides/apps/web/src/routes/(app)/+page.svelte create mode 100644 apps/guides/apps/web/src/routes/(app)/collections/+page.svelte create mode 100644 apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte create mode 100644 apps/guides/apps/web/src/routes/(app)/guide/[id]/run/+page.svelte create mode 100644 apps/guides/apps/web/src/routes/(app)/history/+page.svelte create mode 100644 apps/guides/apps/web/src/routes/+layout.svelte create mode 100644 apps/guides/apps/web/svelte.config.js create mode 100644 apps/guides/apps/web/tsconfig.json create mode 100644 apps/guides/apps/web/vite.config.ts create mode 100644 apps/guides/package.json diff --git a/.claude/plans/mana-guides.md b/.claude/plans/mana-guides.md new file mode 100644 index 000000000..e4feec321 --- /dev/null +++ b/.claude/plans/mana-guides.md @@ -0,0 +1,233 @@ +# mana guides — Implementation Plan + +## Concept + +Guides ist eine **Playbook-App**: jeder Guide ist gleichzeitig eine schöne Karte zum Entdecken +(Bibliothek), ein ausführbarer Prozess mit Ausführungshistorie (Executer) und ein Baustein in +strukturierten Lernpfaden (Teacher). Drei Konzepte, ein konsistentes Datenmodell. + +**Positionierung:** Zwischen context (freie Dokumente) und todo (Tasks). Guides sind +strukturierte, ausführbare Schritt-für-Schritt-Anleitungen. + +--- + +## Architektur-Entscheidungen + +| Thema | Entscheidung | Begründung | +|-------|-------------|------------| +| Stack | SvelteKit 5 + Svelte Runes + Tailwind | Ecosystem-Standard | +| Daten | Local-first (Dexie.js + mana-sync) | Kein Server für MVP, offline-fähig | +| Server | Kein MVP-Server | Phase 2: Web-Import via mana-search, Sharing | +| Auth | mana-core-auth, allowGuest=true | Guides funktionieren als Gast | +| Tier | beta | Entwicklungsphase | +| Port | 5200 | Nächster freier Port nach calc (5198) | +| Farbe | #0d9488 (teal) | Einzigartig im Ecosystem | +| Subdomain | guides.mana.how | | + +--- + +## Datenmodell + +Alle Collections local-first via `createLocalStore()`, 5 Collections: + +```typescript +LocalGuide { + id, title, description?, coverEmoji?, coverColor?, + category, difficulty: 'easy'|'medium'|'hard', + estimatedMinutes?, tags: string[], + collectionId?, // Zugehörigkeit zu einer Collection + orderInCollection?, + xpReward?, // skilltree bridge (optional) + skillId?, + createdAt, updatedAt, deletedAt // BaseRecord +} + +LocalSection { + id, guideId, title, order +} + +LocalStep { + id, guideId, sectionId?, + order, title, content?, // markdown + type: 'instruction'|'warning'|'tip'|'checkpoint'|'code', + checkable: boolean +} + +LocalCollection { + id, title, description?, coverEmoji?, coverColor?, + type: 'path'|'library', // path=geordnet, library=ungeordnet + guideOrder: string[] // geordnete Guide-IDs für paths +} + +LocalRun { + id, guideId, + startedAt, completedAt?, + mode: 'scroll'|'focus', + stepStates: Record +} +``` + +--- + +## Route-Struktur + +``` +apps/guides/apps/web/src/routes/ + +layout.svelte root: auth init, theme, i18n + (app)/ + +layout.svelte auth gate, nav, FAB + +page.svelte Bibliothek: Grid aller Guides + guide/ + [id]/ + +page.svelte Guide-Detail: Sections, Steps, Run starten + run/ + +page.svelte Run-Modus: Scroll oder Fokus + collections/ + +page.svelte Collections-Liste + [id]/ + +page.svelte Collection-Detail / Pfad-View + history/ + +page.svelte Alle Runs, Ausführungshistorie + (auth)/ + login/+page.svelte + register/+page.svelte + forgot-password/+page.svelte + reset-password/+page.svelte +``` + +--- + +## UI-Konzept + +### Bibliothek (Hauptansicht) +- Masonry- oder gleichmäßiges Grid mit Guide-Karten +- Karte zeigt: Emoji/Color, Titel, Kategorie, Schwierigkeit (⭐), Zeit, Run-Status-Ring +- Filter: Kategorie / Schwierigkeit / Status (Neu/Aktiv/Abgeschlossen) +- FAB: "Guide erstellen" + +### Guide-Karte Status-Indikator +``` +○ Neu (nie ausgeführt) +◑ Aktiv (laufender Run) +● Abgeschlossen (letzter Run fertig) +⟳ Wiederholt (3+ Runs = SOP-Indikator) +``` + +### Run-Modi +**Scroll-Modus** (Tutorials, Anleitungen): +- Alle Steps sichtbar, fortlaufend scrollbar +- Erledigte Steps werden durchgestrichen/grün +- Fortschrittsbalken oben + +**Fokus-Modus** (SOPs, Routinen): +- Ein Step fullscreen +- Großer "Abschließen"-Button +- Notiz hinzufügen möglich +- Navigation: Zurück / Weiter + +### Collections / Pfad-View +- `path`-Collections: sequentielle Ansicht mit Fortschrittsbalken, XP-Belohnung +- `library`-Collections: Themed Kochbuch-Stil, ungeordneter Grid + +--- + +## Phasen + +### Phase 1 — MVP (jetzt implementiert) ✓ +- [x] Monorepo-Skelett (package.json, config-files) +- [x] Local-Store (5 Collections) +- [x] Guest-Seed (3 Demo-Guides) +- [x] Root-Layout + Auth-Layout +- [x] Bibliothek-View (+page.svelte) +- [x] Guide-Detail-View +- [x] Run-Modus (Scroll + Fokus) +- [x] Collections-View +- [x] Verlauf-View +- [x] GuideCard-Komponente +- [x] GuideEditModal +- [x] RunView-Komponente +- [x] Registrierung in mana-apps.ts + app-icons.ts + +### Phase 2 — Web-Import & Sharing +- [ ] Hono/Bun-Server (apps/guides/apps/server/) +- [ ] Web-Import: URL → Guide via mana-search +- [ ] Guide-Export: JSON / Markdown +- [ ] Guide-Sharing (öffentliche Guides) +- [ ] QR-Code für Guide-Sharing + +### Phase 3 — KI & Integration +- [ ] KI-Guide-Generator: Text/Paste → strukturierter Guide via mana-llm +- [ ] skilltree-XP-Bridge (completedRun → XP-Event) +- [ ] calendar-Integration: Guide als Kalender-Event einplanen +- [ ] todo-Integration: Guide-Schritt als Task erstellen + +### Phase 4 — Community +- [ ] Öffentliche Guide-Bibliothek (community guides) +- [ ] Guide-Bewertungen und Kommentare +- [ ] Guide-Templates (Starter-Vorlagen) + +--- + +## Dateien + +``` +apps/guides/ +├── package.json +└── apps/ + └── web/ + ├── package.json + ├── svelte.config.js + ├── vite.config.ts + ├── tsconfig.json + ├── src/ + │ ├── app.html + │ ├── app.css + │ ├── app.d.ts + │ ├── hooks.client.ts + │ ├── lib/ + │ │ ├── data/ + │ │ │ ├── local-store.ts + │ │ │ └── guest-seed.ts + │ │ ├── stores/ + │ │ │ ├── auth.svelte.ts + │ │ │ ├── guides.svelte.ts + │ │ │ └── runs.svelte.ts + │ │ └── components/ + │ │ ├── GuideCard.svelte + │ │ ├── GuideEditModal.svelte + │ │ ├── StepEditor.svelte + │ │ └── RunView.svelte + │ └── routes/ + │ ├── +layout.svelte + │ ├── (app)/ + │ │ ├── +layout.svelte + │ │ ├── +page.svelte + │ │ ├── guide/[id]/ + │ │ │ ├── +page.svelte + │ │ │ └── run/+page.svelte + │ │ ├── collections/ + │ │ │ ├── +page.svelte + │ │ │ └── [id]/+page.svelte + │ │ └── history/+page.svelte + │ └── (auth)/ + │ ├── login/+page.svelte + │ ├── register/+page.svelte + │ ├── forgot-password/+page.svelte + │ └── reset-password/+page.svelte + +Registrierung: + packages/shared-branding/src/app-icons.ts → guidesSvg + APP_ICONS.guides + packages/shared-branding/src/mana-apps.ts → MANA_APPS entry + APP_URLS entry +``` + +--- + +## Abgrenzung zu bestehenden Apps + +| App | Überlappung | Abgrenzung | +|-----|-------------|------------| +| context | Dokumente, Wissen | context = freie Dokumente, guides = strukturierte Ausführung | +| todo | Aufgaben, Checklisten | todo = Tasks, guides = Prozesse mit History | +| questions | Recherche | questions = Q&A, guides = How-To | +| manadeck | Lernen | manadeck = Karteikarten/Spaced-Repetition, guides = Schritt-für-Schritt | +| skilltree | Skill-Progression | skilltree = XP-Tracking, guides = Quelle von XP (optional) | diff --git a/apps/guides/apps/web/package.json b/apps/guides/apps/web/package.json new file mode 100644 index 000000000..1b27bd516 --- /dev/null +++ b/apps/guides/apps/web/package.json @@ -0,0 +1,61 @@ +{ + "name": "@guides/web", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint .", + "format": "prettier --write .", + "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@manacore/shared-pwa": "workspace:*", + "@manacore/shared-vite-config": "workspace:*", + "@sveltejs/adapter-node": "^5.0.0", + "@sveltejs/kit": "^2.47.1", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.1.7", + "@types/node": "^20.0.0", + "@vite-pwa/sveltekit": "^1.1.0", + "jsdom": "^25.0.1", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "svelte": "^5.41.0", + "svelte-check": "^4.3.3", + "tailwindcss": "^4.1.7", + "tslib": "^2.4.1", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vitest": "^4.1.0" + }, + "dependencies": { + "@manacore/local-store": "workspace:*", + "@manacore/shared-api-client": "workspace:*", + "@manacore/shared-app-onboarding": "workspace:*", + "@manacore/shared-auth": "workspace:*", + "@manacore/shared-auth-stores": "workspace:*", + "@manacore/shared-auth-ui": "workspace:*", + "@manacore/shared-branding": "workspace:*", + "@manacore/shared-error-tracking": "workspace:*", + "@manacore/feedback": "workspace:*", + "@manacore/shared-i18n": "workspace:*", + "@manacore/help": "workspace:*", + "@manacore/shared-icons": "workspace:*", + "@manacore/shared-profile-ui": "workspace:*", + "@manacore/shared-stores": "workspace:*", + "@manacore/shared-tailwind": "workspace:*", + "@manacore/shared-theme": "workspace:*", + "@manacore/shared-theme-ui": "workspace:*", + "@manacore/shared-types": "workspace:*", + "@manacore/shared-ui": "workspace:*", + "@manacore/shared-utils": "workspace:*" + }, + "type": "module" +} diff --git a/apps/guides/apps/web/src/app.css b/apps/guides/apps/web/src/app.css new file mode 100644 index 000000000..bf203e37c --- /dev/null +++ b/apps/guides/apps/web/src/app.css @@ -0,0 +1,34 @@ +@import 'tailwindcss'; +@import '@manacore/shared-tailwind/themes.css'; + +@source "../../../packages/shared/src"; +@source "../../../../../packages/shared-ui/src"; +@source "../../../../../packages/shared-theme-ui/src"; +@source "../../../../../packages/shared-theme-ui/src/components"; +@source "../../../../../packages/shared-theme-ui/src/pages"; + +:root { + /* Guides App - Teal Theme */ + --color-primary: #0d9488; + --color-primary-hover: #0f766e; + --color-primary-light: #14b8a6; + --color-primary-dark: #115e59; + + --color-secondary: #f0fdfa; + --color-secondary-hover: #ccfbf1; + + --color-accent: #2dd4bf; + --color-accent-hover: #14b8a6; + + /* Difficulty colors */ + --color-difficulty-easy: #22c55e; + --color-difficulty-medium: #f59e0b; + --color-difficulty-hard: #ef4444; + + /* Step type colors */ + --color-step-instruction: #0d9488; + --color-step-warning: #f97316; + --color-step-tip: #8b5cf6; + --color-step-checkpoint: #3b82f6; + --color-step-code: #1e293b; +} diff --git a/apps/guides/apps/web/src/app.d.ts b/apps/guides/apps/web/src/app.d.ts new file mode 100644 index 000000000..c269fca6f --- /dev/null +++ b/apps/guides/apps/web/src/app.d.ts @@ -0,0 +1,2 @@ +declare const __BUILD_HASH__: string; +declare const __BUILD_TIME__: string; diff --git a/apps/guides/apps/web/src/app.html b/apps/guides/apps/web/src/app.html new file mode 100644 index 000000000..4bb005387 --- /dev/null +++ b/apps/guides/apps/web/src/app.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + Guides + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/guides/apps/web/src/hooks.client.ts b/apps/guides/apps/web/src/hooks.client.ts new file mode 100644 index 000000000..474fa7a5a --- /dev/null +++ b/apps/guides/apps/web/src/hooks.client.ts @@ -0,0 +1,12 @@ +import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser'; +import type { HandleClientError } from '@sveltejs/kit'; + +initErrorTracking({ + serviceName: 'guides-web', + dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__, + environment: import.meta.env.MODE, +}); + +export const handleError: HandleClientError = ({ error }) => { + handleSvelteError(error); +}; diff --git a/apps/guides/apps/web/src/lib/components/GuideCard.svelte b/apps/guides/apps/web/src/lib/components/GuideCard.svelte new file mode 100644 index 000000000..7883aee0a --- /dev/null +++ b/apps/guides/apps/web/src/lib/components/GuideCard.svelte @@ -0,0 +1,79 @@ + + + + +
+ + {guide.coverEmoji ?? '📖'} + +
+ + +
+
+

+ {guide.title} +

+
+ + {#if guide.description} +

{guide.description}

+ {/if} + +
+
+ +
+ + {difficultyConfig[guide.difficulty].label} +
+ {#if guide.estimatedMinutes} + · {guide.estimatedMinutes}min + {/if} +
+ + + + {statusConfig[runStatus].icon} + {#if runStatus === 'repeated'} + {runCount}× + {/if} + +
+
+
diff --git a/apps/guides/apps/web/src/lib/components/GuideEditModal.svelte b/apps/guides/apps/web/src/lib/components/GuideEditModal.svelte new file mode 100644 index 000000000..7c48d41be --- /dev/null +++ b/apps/guides/apps/web/src/lib/components/GuideEditModal.svelte @@ -0,0 +1,251 @@ + + +{#if open} + +
+ + +
+{/if} diff --git a/apps/guides/apps/web/src/lib/data/guest-seed.ts b/apps/guides/apps/web/src/lib/data/guest-seed.ts new file mode 100644 index 000000000..a0ec1e1cb --- /dev/null +++ b/apps/guides/apps/web/src/lib/data/guest-seed.ts @@ -0,0 +1,245 @@ +/** + * Guest seed data for the Guides app. + * + * Three demo guides that showcase the app's capabilities: + * 1. "Entwicklungsumgebung einrichten" — tech SOP (medium, 3 sections) + * 2. "Perfekte Pasta kochen" — recipe (easy, flat steps) + * 3. "Git-Grundlagen" — learning path guide (hard, sections) + * + * One demo collection: "Dev Setup Pfad" (path type) + */ + +import type { LocalGuide, LocalSection, LocalStep, LocalCollection } from './local-store'; + +// ─── Guides ───────────────────────────────────────────────── + +export const guestGuides: LocalGuide[] = [ + { + id: 'guide-dev-setup', + title: 'Entwicklungsumgebung einrichten', + description: 'Node.js, Git und VS Code auf einem neuen Mac konfigurieren.', + coverEmoji: '💻', + coverColor: '#0d9488', + category: 'Technik', + difficulty: 'medium', + estimatedMinutes: 45, + tags: ['setup', 'mac', 'developer'], + collectionId: 'col-dev', + orderInCollection: 0, + }, + { + id: 'guide-pasta', + title: 'Perfekte Pasta alla Norma', + description: 'Klassisches sizilianisches Gericht mit Auberginen und Ricotta.', + coverEmoji: '🍝', + coverColor: '#f97316', + category: 'Kochen', + difficulty: 'easy', + estimatedMinutes: 30, + tags: ['italienisch', 'vegetarisch', 'pasta'], + }, + { + id: 'guide-git', + title: 'Git-Grundlagen verstehen', + description: 'Commits, Branches, Merges und Pull Requests — von Null auf solide Basis.', + coverEmoji: '🌿', + coverColor: '#8b5cf6', + category: 'Technik', + difficulty: 'hard', + estimatedMinutes: 90, + tags: ['git', 'versionskontrolle', 'developer'], + collectionId: 'col-dev', + orderInCollection: 1, + }, +]; + +// ─── Sections ─────────────────────────────────────────────── + +export const guestSections: LocalSection[] = [ + // Dev Setup sections + { id: 'sec-ds-1', guideId: 'guide-dev-setup', title: 'Homebrew & Basics', order: 0 }, + { id: 'sec-ds-2', guideId: 'guide-dev-setup', title: 'Node.js & npm', order: 1 }, + { id: 'sec-ds-3', guideId: 'guide-dev-setup', title: 'Git konfigurieren', order: 2 }, + + // Git guide sections + { id: 'sec-git-1', guideId: 'guide-git', title: 'Repository & Commits', order: 0 }, + { id: 'sec-git-2', guideId: 'guide-git', title: 'Branches & Merging', order: 1 }, + { id: 'sec-git-3', guideId: 'guide-git', title: 'Remote & GitHub', order: 2 }, +]; + +// ─── Steps ────────────────────────────────────────────────── + +export const guestSteps: LocalStep[] = [ + // ── Dev Setup: Homebrew ────────────────────────────────── + { + id: 'step-ds-1', + guideId: 'guide-dev-setup', + sectionId: 'sec-ds-1', + order: 0, + title: 'Homebrew installieren', + content: + 'Öffne das Terminal und führe folgenden Befehl aus:\n\n```bash\n/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n```', + type: 'code', + checkable: true, + }, + { + id: 'step-ds-2', + guideId: 'guide-dev-setup', + sectionId: 'sec-ds-1', + order: 1, + title: 'Installation prüfen', + content: 'Führe `brew --version` aus. Du solltest eine Versionsnummer sehen.', + type: 'checkpoint', + checkable: true, + }, + { + id: 'step-ds-3', + guideId: 'guide-dev-setup', + sectionId: 'sec-ds-2', + order: 0, + title: 'Node.js via nvm installieren', + content: '```bash\nbrew install nvm\nnvm install --lts\nnvm use --lts\n```', + type: 'code', + checkable: true, + }, + { + id: 'step-ds-4', + guideId: 'guide-dev-setup', + sectionId: 'sec-ds-2', + order: 1, + title: 'pnpm global installieren', + content: '```bash\nnpm install -g pnpm\n```', + type: 'instruction', + checkable: true, + }, + { + id: 'step-ds-5', + guideId: 'guide-dev-setup', + sectionId: 'sec-ds-3', + order: 0, + title: 'Git-Identität setzen', + content: + '```bash\ngit config --global user.name "Dein Name"\ngit config --global user.email "dein@email.de"\n```', + type: 'code', + checkable: true, + }, + { + id: 'step-ds-6', + guideId: 'guide-dev-setup', + sectionId: 'sec-ds-3', + order: 1, + title: 'SSH-Key für GitHub erstellen', + content: + '```bash\nssh-keygen -t ed25519 -C "dein@email.de"\ncat ~/.ssh/id_ed25519.pub\n```\n\nKopiere den Output und füge ihn in GitHub unter Settings → SSH Keys ein.', + type: 'tip', + checkable: true, + }, + + // ── Pasta steps (no sections) ──────────────────────────── + { + id: 'step-pasta-1', + guideId: 'guide-pasta', + order: 0, + title: 'Auberginen salzen', + content: + 'Auberginen in 1 cm Würfel schneiden, mit Salz bestreuen, 20 Minuten ziehen lassen. Dann abwaschen und trockentupfen.', + type: 'instruction', + checkable: true, + }, + { + id: 'step-pasta-2', + guideId: 'guide-pasta', + order: 1, + title: 'Tomatensauce kochen', + content: + 'Knoblauch in Olivenöl anschwitzen, Dosentomaten dazu, 15 Minuten köcheln lassen. Mit Salz, Pfeffer und Basilikum abschmecken.', + type: 'instruction', + checkable: true, + }, + { + id: 'step-pasta-3', + guideId: 'guide-pasta', + order: 2, + title: 'Auberginen frittieren', + content: 'Reichlich Olivenöl erhitzen (180°C), Auberginen goldbraun frittieren, auf Küchenpapier abtropfen lassen.', + type: 'warning', + checkable: true, + }, + { + id: 'step-pasta-4', + guideId: 'guide-pasta', + order: 3, + title: 'Pasta al dente kochen', + content: 'Rigatoni oder Penne in gut gesalzenem Wasser 1 Minute kürzer als Packungsanweisung kochen.', + type: 'tip', + checkable: true, + }, + { + id: 'step-pasta-5', + guideId: 'guide-pasta', + order: 4, + title: 'Alles vereinen & servieren', + content: + 'Pasta mit der Sauce vermengen, Auberginen darüber geben. Mit gesalzenem Ricotta und frischem Basilikum toppen.', + type: 'checkpoint', + checkable: false, + }, + + // ── Git sections ───────────────────────────────────────── + { + id: 'step-git-1', + guideId: 'guide-git', + sectionId: 'sec-git-1', + order: 0, + title: 'Repository initialisieren', + content: '```bash\nmkdir mein-projekt && cd mein-projekt\ngit init\n```', + type: 'code', + checkable: true, + }, + { + id: 'step-git-2', + guideId: 'guide-git', + sectionId: 'sec-git-1', + order: 1, + title: 'Ersten Commit erstellen', + content: + '```bash\necho "# Mein Projekt" > README.md\ngit add README.md\ngit commit -m "feat: initial commit"\n```', + type: 'code', + checkable: true, + }, + { + id: 'step-git-3', + guideId: 'guide-git', + sectionId: 'sec-git-2', + order: 0, + title: 'Feature-Branch erstellen', + content: '```bash\ngit checkout -b feature/mein-feature\n```', + type: 'instruction', + checkable: true, + }, + { + id: 'step-git-4', + guideId: 'guide-git', + sectionId: 'sec-git-3', + order: 0, + title: 'Remote hinzufügen & pushen', + content: + '```bash\ngit remote add origin git@github.com:user/repo.git\ngit push -u origin main\n```', + type: 'code', + checkable: true, + }, +]; + +// ─── Collections ──────────────────────────────────────────── + +export const guestCollections: LocalCollection[] = [ + { + id: 'col-dev', + title: 'Developer Starter Kit', + description: 'Alles was du als neuer Entwickler einrichten und lernen musst.', + coverEmoji: '🚀', + coverColor: '#0d9488', + type: 'path', + guideOrder: ['guide-dev-setup', 'guide-git'], + }, +]; diff --git a/apps/guides/apps/web/src/lib/data/local-store.ts b/apps/guides/apps/web/src/lib/data/local-store.ts new file mode 100644 index 000000000..a616d2dce --- /dev/null +++ b/apps/guides/apps/web/src/lib/data/local-store.ts @@ -0,0 +1,120 @@ +/** + * Guides App — Local-First Data Layer + * + * 5 Collections: guides, sections, steps, collections, runs + * All data lives in IndexedDB first, syncs to server in the background. + */ + +import { createLocalStore, type BaseRecord } from '@manacore/local-store'; +import { guestGuides, guestSections, guestSteps, guestCollections } from './guest-seed.js'; + +// ─── Types ────────────────────────────────────────────────── + +export type Difficulty = 'easy' | 'medium' | 'hard'; +export type StepType = 'instruction' | 'warning' | 'tip' | 'checkpoint' | 'code'; + +export interface LocalGuide extends BaseRecord { + title: string; + description?: string; + /** Single emoji used as cover when no image */ + coverEmoji?: string; + /** Tailwind-compatible hex color for the cover background */ + coverColor?: string; + category: string; + difficulty: Difficulty; + estimatedMinutes?: number; + tags: string[]; + /** Optional: belongs to a collection */ + collectionId?: string; + orderInCollection?: number; + /** Optional skilltree integration */ + xpReward?: number; + skillId?: string; +} + +export interface LocalSection extends BaseRecord { + guideId: string; + title: string; + order: number; +} + +export interface LocalStep extends BaseRecord { + guideId: string; + sectionId?: string; + order: number; + title: string; + /** Markdown content */ + content?: string; + type: StepType; + checkable: boolean; +} + +export interface LocalCollection extends BaseRecord { + title: string; + description?: string; + coverEmoji?: string; + coverColor?: string; + /** path = ordered learning path, library = unordered recipe book */ + type: 'path' | 'library'; + /** Ordered guide IDs (relevant for 'path' type) */ + guideOrder: string[]; +} + +export interface StepState { + done: boolean; + doneAt?: string; + notes?: string; +} + +export interface LocalRun extends BaseRecord { + guideId: string; + startedAt: string; + completedAt?: string; + /** scroll = all steps visible, focus = one step at a time */ + mode: 'scroll' | 'focus'; + stepStates: Record; +} + +// ─── Store ────────────────────────────────────────────────── + +const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050'; + +export const guidesStore = createLocalStore({ + appId: 'guides', + collections: [ + { + name: 'guides', + indexes: ['category', 'difficulty', 'collectionId', 'tags'], + guestSeed: guestGuides, + }, + { + name: 'sections', + indexes: ['guideId', 'order'], + guestSeed: guestSections, + }, + { + name: 'steps', + indexes: ['guideId', 'sectionId', 'order', '[guideId+order]'], + guestSeed: guestSteps, + }, + { + name: 'collections', + indexes: [], + guestSeed: guestCollections, + }, + { + name: 'runs', + indexes: ['guideId', 'startedAt', 'completedAt'], + }, + ], + sync: { + serverUrl: SYNC_SERVER_URL, + }, +}); + +// Typed collection accessors +export const guideCollection = guidesStore.collection('guides'); +export const sectionCollection = guidesStore.collection('sections'); +export const stepCollection = guidesStore.collection('steps'); +export const collectionCollection = guidesStore.collection('collections'); +export const runCollection = guidesStore.collection('runs'); diff --git a/apps/guides/apps/web/src/lib/stores/auth.svelte.ts b/apps/guides/apps/web/src/lib/stores/auth.svelte.ts new file mode 100644 index 000000000..c0de1a052 --- /dev/null +++ b/apps/guides/apps/web/src/lib/stores/auth.svelte.ts @@ -0,0 +1,3 @@ +import { createAuthStore } from '@manacore/shared-auth-stores'; + +export const authStore = createAuthStore(); diff --git a/apps/guides/apps/web/src/lib/stores/guides.svelte.ts b/apps/guides/apps/web/src/lib/stores/guides.svelte.ts new file mode 100644 index 000000000..74c0d02ab --- /dev/null +++ b/apps/guides/apps/web/src/lib/stores/guides.svelte.ts @@ -0,0 +1,135 @@ +/** + * Guides store — mutation layer for guides, sections, steps, and collections. + * Reads happen via Dexie liveQuery in components. + */ + +import { + guideCollection, + sectionCollection, + stepCollection, + collectionCollection, + type LocalGuide, + type LocalSection, + type LocalStep, + type LocalCollection, +} from '$lib/data/local-store.js'; + +// ─── Error state ───────────────────────────────────────────── + +let error = $state(null); + +async function withErrorHandling(fn: () => Promise, msg: string): Promise { + try { + return await fn(); + } catch (e) { + error = msg; + console.error(msg, e); + return null; + } +} + +// ─── Guides ────────────────────────────────────────────────── + +export const guidesStore = { + get error() { + return error; + }, + clearError() { + error = null; + }, + + async createGuide( + data: Omit + ): Promise { + return withErrorHandling(async () => { + return guideCollection.insert({ + id: crypto.randomUUID(), + ...data, + tags: data.tags ?? [], + }); + }, 'Guide konnte nicht erstellt werden'); + }, + + async updateGuide(id: string, data: Partial): Promise { + return withErrorHandling( + () => guideCollection.update(id, data), + 'Guide konnte nicht aktualisiert werden' + ); + }, + + async deleteGuide(id: string): Promise { + await withErrorHandling(async () => { + // Soft-delete guide + cascade to sections/steps + await guideCollection.delete(id); + const sections = await sectionCollection.getAll({ guideId: id }); + for (const s of sections) await sectionCollection.delete(s.id); + const steps = await stepCollection.getAll({ guideId: id }); + for (const s of steps) await stepCollection.delete(s.id); + }, 'Guide konnte nicht gelöscht werden'); + }, + + // ─── Sections ────────────────────────────────────────── + + async createSection(data: Omit): Promise { + return withErrorHandling( + () => sectionCollection.insert({ id: crypto.randomUUID(), ...data }), + 'Abschnitt konnte nicht erstellt werden' + ); + }, + + async updateSection(id: string, data: Partial): Promise { + return withErrorHandling( + () => sectionCollection.update(id, data), + 'Abschnitt konnte nicht aktualisiert werden' + ); + }, + + async deleteSection(id: string): Promise { + await withErrorHandling(async () => { + await sectionCollection.delete(id); + const steps = await stepCollection.getAll({ sectionId: id }); + for (const s of steps) await stepCollection.delete(s.id); + }, 'Abschnitt konnte nicht gelöscht werden'); + }, + + // ─── Steps ───────────────────────────────────────────── + + async createStep(data: Omit): Promise { + return withErrorHandling( + () => stepCollection.insert({ id: crypto.randomUUID(), ...data }), + 'Schritt konnte nicht erstellt werden' + ); + }, + + async updateStep(id: string, data: Partial): Promise { + return withErrorHandling( + () => stepCollection.update(id, data), + 'Schritt konnte nicht aktualisiert werden' + ); + }, + + async deleteStep(id: string): Promise { + await withErrorHandling( + () => stepCollection.delete(id), + 'Schritt konnte nicht gelöscht werden' + ); + }, + + // ─── Collections ─────────────────────────────────────── + + async createCollection( + data: Omit + ): Promise { + return withErrorHandling( + () => collectionCollection.insert({ id: crypto.randomUUID(), ...data }), + 'Sammlung konnte nicht erstellt werden' + ); + }, + + async updateCollection(id: string, data: Partial): Promise { + return withErrorHandling( + () => collectionCollection.update(id, data), + 'Sammlung konnte nicht aktualisiert werden' + ); + }, +}; diff --git a/apps/guides/apps/web/src/lib/stores/runs.svelte.ts b/apps/guides/apps/web/src/lib/stores/runs.svelte.ts new file mode 100644 index 000000000..e6a104574 --- /dev/null +++ b/apps/guides/apps/web/src/lib/stores/runs.svelte.ts @@ -0,0 +1,87 @@ +/** + * Runs store — create, update, and complete guide runs. + */ + +import { runCollection, type LocalRun, type StepState } from '$lib/data/local-store.js'; + +let error = $state(null); + +async function withErrorHandling(fn: () => Promise, msg: string): Promise { + try { + return await fn(); + } catch (e) { + error = msg; + console.error(msg, e); + return null; + } +} + +export const runsStore = { + get error() { + return error; + }, + clearError() { + error = null; + }, + + async startRun(guideId: string, mode: LocalRun['mode'] = 'scroll'): Promise { + return withErrorHandling( + () => + runCollection.insert({ + id: crypto.randomUUID(), + guideId, + startedAt: new Date().toISOString(), + mode, + stepStates: {}, + }), + 'Run konnte nicht gestartet werden' + ); + }, + + async setStepState(runId: string, stepId: string, state: Partial): Promise { + await withErrorHandling(async () => { + const run = await runCollection.get(runId); + if (!run) return; + const existing = run.stepStates[stepId] ?? { done: false }; + const updated: Record = { + ...run.stepStates, + [stepId]: { ...existing, ...state }, + }; + await runCollection.update(runId, { stepStates: updated }); + }, 'Schritt-Status konnte nicht aktualisiert werden'); + }, + + async completeRun(runId: string): Promise { + await withErrorHandling( + () => runCollection.update(runId, { completedAt: new Date().toISOString() }), + 'Run konnte nicht abgeschlossen werden' + ); + }, + + async deleteRun(runId: string): Promise { + await withErrorHandling( + () => runCollection.delete(runId), + 'Run konnte nicht gelöscht werden' + ); + }, + + /** Returns the most recent incomplete run for a guide, or null */ + async getActiveRun(guideId: string): Promise { + try { + const runs = await runCollection.getAll({ guideId }); + return runs.find((r) => !r.completedAt) ?? null; + } catch { + return null; + } + }, + + /** Count completed runs for a guide */ + async getRunCount(guideId: string): Promise { + try { + const runs = await runCollection.getAll({ guideId }); + return runs.filter((r) => r.completedAt).length; + } catch { + return 0; + } + }, +}; diff --git a/apps/guides/apps/web/src/routes/(app)/+layout.svelte b/apps/guides/apps/web/src/routes/(app)/+layout.svelte new file mode 100644 index 000000000..8ccf9ab94 --- /dev/null +++ b/apps/guides/apps/web/src/routes/(app)/+layout.svelte @@ -0,0 +1,118 @@ + + + +
+ + + + +
+
+ {@render children()} +
+
+
+ + + + + + +
+ +{#if showCreateModal} + + {#await import('$lib/components/GuideEditModal.svelte') then { default: GuideEditModal }} + (showCreateModal = false)} + onSave={async (data) => { + await guidesStore.createGuide(data); + showCreateModal = false; + }} + /> + {/await} +{/if} diff --git a/apps/guides/apps/web/src/routes/(app)/+page.svelte b/apps/guides/apps/web/src/routes/(app)/+page.svelte new file mode 100644 index 000000000..cba3f5697 --- /dev/null +++ b/apps/guides/apps/web/src/routes/(app)/+page.svelte @@ -0,0 +1,149 @@ + + +
+ +
+
+

Bibliothek

+

{allGuides.length} Anleitungen

+
+ +
+ + +
+ + +
+ + {#each categories as cat} + + {/each} + + + {#each Object.entries(difficultyLabels) as [val, label]} + + {/each} +
+
+ + + {#if filteredGuides.length === 0} +
+ {#if allGuides.length === 0} + 📖 +

Noch keine Anleitungen

+

+ Erstelle deine erste Anleitung — ein Rezept, eine SOP, ein Lernpfad. +

+ + {:else} + 🔍 +

Keine Ergebnisse für diese Filter.

+ + {/if} +
+ {:else} +
+ {#each filteredGuides as guide (guide.id)} + + {/each} +
+ {/if} +
diff --git a/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte b/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte new file mode 100644 index 000000000..9efc1cccd --- /dev/null +++ b/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte @@ -0,0 +1,107 @@ + + +
+
+

Sammlungen

+

Lernpfade und thematische Anleitungs-Sets

+
+ + {#if collections.length === 0} +
+ 📂 +

Noch keine Sammlungen

+

+ Sammlungen gruppieren Anleitungen zu Lernpfaden oder thematischen Bibliotheken. +

+
+ {:else} + + {/if} +
diff --git a/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte b/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte new file mode 100644 index 000000000..cd62fcdf3 --- /dev/null +++ b/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte @@ -0,0 +1,252 @@ + + +{#if !guide} +
+

Anleitung nicht gefunden.

+
+{:else} +
+ + + ← Bibliothek + + + +
+ {guide.coverEmoji ?? '📖'} +
+

{guide.title}

+ {#if guide.description} +

{guide.description}

+ {/if} +
+ + {difficultyConfig[guide.difficulty].label} + + {#if guide.estimatedMinutes} + ⏱ {guide.estimatedMinutes} min + {/if} + {totalSteps} Schritte + {#if completedRuns.length > 0} + ✓ {completedRuns.length}× abgeschlossen + {/if} +
+
+
+ +
+
+ + + {#if activeRun} +
+
+ Aktiver Durchlauf + {getActiveRunProgress()}% abgeschlossen +
+
+
+
+ +
+ {:else} + +
+ + +
+ {/if} + + +
+ {#if sections.length > 0} + {#each sections as section (section.id)} +
+

+ {section.title} +

+
+ {#each getStepsForSection(section.id) as step (step.id)} + {@const cfg = stepTypeConfig[step.type]} +
+
+ {cfg.icon} +
+

{step.title}

+ {#if step.content} +

{step.content}

+ {/if} +
+
+
+ {/each} +
+
+ {/each} + + {#if getUnsectionedSteps().length > 0} +
+ {#each getUnsectionedSteps() as step (step.id)} + {@const cfg = stepTypeConfig[step.type]} +
+

{step.title}

+
+ {/each} +
+ {/if} + {:else} +
+ {#each steps as step (step.id)} + {@const cfg = stepTypeConfig[step.type]} +
+
+ {cfg.icon} +
+

{step.title}

+ {#if step.content} +

{step.content}

+ {/if} +
+
+
+ {/each} +
+ {/if} +
+ + + {#if completedRuns.length > 0} +
+

Verlauf

+
+ {#each completedRuns.slice(0, 5) as run (run.id)} +
+
+ + + {new Date(run.startedAt).toLocaleDateString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric' + })} + +
+ + {Object.values(run.stepStates).filter((s) => s.done).length}/{totalSteps} Schritte + +
+ {/each} +
+
+ {/if} +
+{/if} diff --git a/apps/guides/apps/web/src/routes/(app)/guide/[id]/run/+page.svelte b/apps/guides/apps/web/src/routes/(app)/guide/[id]/run/+page.svelte new file mode 100644 index 000000000..42e7b85d1 --- /dev/null +++ b/apps/guides/apps/web/src/routes/(app)/guide/[id]/run/+page.svelte @@ -0,0 +1,278 @@ + + +{#if !guide || !run} +
+
+
+{:else if mode === 'scroll'} + +
+ +
+ + {doneCount}/{steps.length} · {progress}% +
+ + +
+
+
+ + +
+ {#each steps as step, i (step.id)} + {@const done = isStepDone(step.id)} + + {/each} +
+ + + {#if isComplete} +
+
🎉
+

Alle Schritte erledigt!

+

Möchtest du den Durchlauf abschließen?

+ +
+ {/if} +
+ +{:else} + + {#if steps.length === 0} +
+

Keine Schritte vorhanden.

+
+ {:else} + {@const currentStep = steps[focusIndex]} + {@const done = isStepDone(currentStep.id)} + +
+ +
+ +
+ + {focusIndex + 1} / {steps.length} + +
+
+
+
+ {progress}% +
+ + +
+
{stepTypeIcons[currentStep.type]}
+

+ {currentStep.title} +

+ {#if currentStep.content} +

+ {currentStep.content} +

+ {/if} + + {#if showNoteInput} +
+ +
+ + +
+
+ {/if} +
+ + +
+ {#if !done} + + {:else} +
+ ✓ Erledigt +
+ {/if} + +
+ + + + + {#if focusIndex < steps.length - 1} + + {:else if isComplete} + + {:else} + + {/if} +
+
+
+ {/if} +{/if} diff --git a/apps/guides/apps/web/src/routes/(app)/history/+page.svelte b/apps/guides/apps/web/src/routes/(app)/history/+page.svelte new file mode 100644 index 000000000..dbb854629 --- /dev/null +++ b/apps/guides/apps/web/src/routes/(app)/history/+page.svelte @@ -0,0 +1,114 @@ + + +
+
+

Verlauf

+

{completedRuns.length} abgeschlossene Durchläufe

+
+ + + {#if activeRuns.length > 0} +
+

Aktive Durchläufe

+
+ {#each activeRuns as run (run.id)} + {@const guide = guides.get(run.guideId)} + {@const { done, total } = getStepCount(run)} + + {guide?.coverEmoji ?? '📖'} +
+

{guide?.title ?? 'Unbekannte Anleitung'}

+

+ Gestartet {formatDate(run.startedAt)} · {done} von {total} Schritten +

+
+ Fortsetzen → +
+ {/each} +
+
+ {/if} + + + {#if completedRuns.length === 0} +
+ 🕐 +

Noch keine abgeschlossenen Durchläufe

+

+ Starte einen Durchlauf einer Anleitung, um hier den Verlauf zu sehen. +

+
+ {:else} +
+ {#each completedRuns as run (run.id)} + {@const guide = guides.get(run.guideId)} + {@const { done, total } = getStepCount(run)} +
+ {guide?.coverEmoji ?? '📖'} +
+ + {guide?.title ?? 'Unbekannte Anleitung'} + +

+ {formatDate(run.startedAt)} · {getDuration(run)} +

+
+
+ +

{done}/{total}

+
+
+ {/each} +
+ {/if} +
diff --git a/apps/guides/apps/web/src/routes/+layout.svelte b/apps/guides/apps/web/src/routes/+layout.svelte new file mode 100644 index 000000000..524379b18 --- /dev/null +++ b/apps/guides/apps/web/src/routes/+layout.svelte @@ -0,0 +1,37 @@ + + + + + + + + + +{#if !appReady} +
+
+
+{:else} +
+ {@render children()} +
+{/if} diff --git a/apps/guides/apps/web/svelte.config.js b/apps/guides/apps/web/svelte.config.js new file mode 100644 index 000000000..a7a917e4c --- /dev/null +++ b/apps/guides/apps/web/svelte.config.js @@ -0,0 +1,14 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + out: 'build', + }), + }, +}; + +export default config; diff --git a/apps/guides/apps/web/tsconfig.json b/apps/guides/apps/web/tsconfig.json new file mode 100644 index 000000000..a8f10c8e3 --- /dev/null +++ b/apps/guides/apps/web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/apps/guides/apps/web/vite.config.ts b/apps/guides/apps/web/vite.config.ts new file mode 100644 index 000000000..5f2d35288 --- /dev/null +++ b/apps/guides/apps/web/vite.config.ts @@ -0,0 +1,49 @@ +/// +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; +import { SvelteKitPWA } from '@vite-pwa/sveltekit'; +import { createPWAConfig } from '@manacore/shared-pwa'; +import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config'; + +export default defineConfig({ + plugins: [ + tailwindcss(), + sveltekit(), + SvelteKitPWA( + createPWAConfig({ + name: 'Guides - Schritt-für-Schritt Anleitungen', + shortName: 'Guides', + description: 'Erstelle und führe Schritt-für-Schritt Anleitungen, Playbooks und Lernpfade aus', + themeColor: '#0d9488', + devEnabled: false, + shortcuts: [ + { + name: 'Neue Anleitung', + short_name: 'Neu', + description: 'Neue Anleitung erstellen', + url: '/?action=new', + }, + ], + }) + ), + ], + server: { + port: 5200, + strictPort: true, + }, + ssr: { + noExternal: [...MANACORE_SHARED_PACKAGES], + }, + optimizeDeps: { + exclude: [...MANACORE_SHARED_PACKAGES], + }, + test: { + environment: 'jsdom', + include: ['src/**/*.test.ts'], + globals: true, + }, + define: { + ...getBuildDefines(), + }, +}); diff --git a/apps/guides/package.json b/apps/guides/package.json new file mode 100644 index 000000000..fce33145c --- /dev/null +++ b/apps/guides/package.json @@ -0,0 +1,14 @@ +{ + "name": "guides", + "version": "1.0.0", + "private": true, + "description": "Mana Guides - Schritt-für-Schritt Anleitungen & Playbooks", + "scripts": { + "dev": "pnpm run --filter=@guides/* --parallel dev", + "dev:web": "pnpm --filter @guides/web dev" + }, + "devDependencies": { + "typescript": "^5.9.3" + }, + "packageManager": "pnpm@9.15.0" +} diff --git a/package.json b/package.json index 591f8732f..fe56a0a13 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,11 @@ "dev:calc:web": "pnpm --filter @calc/web dev", "dev:calc:app": "pnpm dev:calc:web", "dev:calc:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:calc:web\"", + "guides:dev": "turbo run dev --filter=guides...", + "dev:guides:web": "pnpm --filter @guides/web dev", + "dev:guides:app": "pnpm dev:guides:web", + "dev:guides:local": "concurrently -n sync,web -c magenta,cyan \"pnpm dev:sync\" \"pnpm dev:guides:web\"", + "dev:guides:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:guides:web\"", "moodlit:dev": "turbo run dev --filter=moodlit...", "dev:moodlit:mobile": "pnpm --filter @moodlit/mobile dev", "dev:moodlit:app": "concurrently -n server,web -c yellow,cyan \"pnpm dev:moodlit:server\" \"pnpm dev:moodlit:web\"", diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index c2c2e63d1..cc01c21e9 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -123,6 +123,9 @@ export const APP_ICONS = { news: svgToDataUrl( `` ), + guides: svgToDataUrl( + `` + ), } as const; export type AppIconId = keyof typeof APP_ICONS; diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index f4efba5a9..5fe2a2847 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -513,6 +513,23 @@ export const MANA_APPS: ManaApp[] = [ status: 'development', requiredTier: 'beta', }, + { + id: 'guides', + name: 'Guides', + description: { + de: 'Schritt-für-Schritt Anleitungen', + en: 'Step-by-Step Guides', + }, + longDescription: { + de: 'Erstelle und führe strukturierte Anleitungen aus — Rezepte, SOPs, Lernpfade und Playbooks mit Ausführungshistorie.', + en: 'Create and execute structured guides — recipes, SOPs, learning paths, and playbooks with run history.', + }, + icon: APP_ICONS.guides, + color: '#0d9488', + comingSoon: false, + status: 'development', + requiredTier: 'beta', + }, ]; /** @@ -617,6 +634,7 @@ export const APP_URLS: Record = { reader: { dev: 'exp://localhost:8081', prod: 'https://reader.mana.how' }, news: { dev: 'http://localhost:5174', prod: 'https://news.mana.how' }, calc: { dev: 'http://localhost:5198', prod: 'https://calc.mana.how' }, + guides: { dev: 'http://localhost:5200', prod: 'https://guides.mana.how' }, }; /**