forgedev 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/package.json +25 -7
  4. package/src/chainproof-bridge.js +330 -0
  5. package/src/ci-mode.js +85 -0
  6. package/src/claude-configurator.js +86 -49
  7. package/src/cli.js +30 -7
  8. package/src/composer.js +159 -34
  9. package/src/doctor-checks-chainproof.js +106 -0
  10. package/src/doctor-checks.js +39 -20
  11. package/src/doctor-prompts.js +9 -9
  12. package/src/doctor.js +37 -4
  13. package/src/guided.js +3 -3
  14. package/src/index.js +31 -10
  15. package/src/init-mode.js +64 -11
  16. package/src/menu.js +178 -0
  17. package/src/prompts.js +5 -12
  18. package/src/recommender.js +134 -10
  19. package/src/scanner.js +57 -2
  20. package/src/uat-generator.js +204 -189
  21. package/src/update-check.js +9 -4
  22. package/src/update.js +1 -1
  23. package/src/utils.js +64 -5
  24. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  25. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  29. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  34. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  35. package/templates/backend/express/Dockerfile.template +18 -0
  36. package/templates/backend/express/package.json.template +33 -0
  37. package/templates/backend/express/src/index.ts.template +34 -0
  38. package/templates/backend/express/src/routes/health.ts.template +27 -0
  39. package/templates/backend/express/tsconfig.json +17 -0
  40. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  41. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  42. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  44. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  45. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  46. package/templates/backend/hono/Dockerfile.template +18 -0
  47. package/templates/backend/hono/package.json.template +31 -0
  48. package/templates/backend/hono/src/index.ts.template +32 -0
  49. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  50. package/templates/backend/hono/tsconfig.json +18 -0
  51. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  52. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  53. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  54. package/templates/chainproof/base/.mcp.json +9 -0
  55. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  56. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  57. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  58. package/templates/claude-code/agents/architect.md +25 -11
  59. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  60. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  61. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  62. package/templates/claude-code/agents/database-reviewer.md +15 -1
  63. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  64. package/templates/claude-code/agents/doc-updater.md +19 -5
  65. package/templates/claude-code/agents/docs-lookup.md +19 -5
  66. package/templates/claude-code/agents/e2e-runner.md +26 -12
  67. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  68. package/templates/claude-code/agents/frontend-builder.md +188 -0
  69. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  70. package/templates/claude-code/agents/loop-operator.md +27 -13
  71. package/templates/claude-code/agents/planner.md +21 -7
  72. package/templates/claude-code/agents/product-strategist.md +24 -10
  73. package/templates/claude-code/agents/production-readiness.md +14 -0
  74. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  75. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  76. package/templates/claude-code/agents/security-reviewer.md +14 -0
  77. package/templates/claude-code/agents/spec-validator.md +15 -1
  78. package/templates/claude-code/agents/tdd-guide.md +21 -7
  79. package/templates/claude-code/agents/uat-validator.md +14 -0
  80. package/templates/claude-code/claude-md/base.md +14 -7
  81. package/templates/claude-code/claude-md/fastapi.md +8 -8
  82. package/templates/claude-code/claude-md/fullstack.md +6 -6
  83. package/templates/claude-code/claude-md/hono.md +18 -0
  84. package/templates/claude-code/claude-md/nextjs.md +5 -5
  85. package/templates/claude-code/claude-md/remix.md +18 -0
  86. package/templates/claude-code/commands/audit-security.md +14 -0
  87. package/templates/claude-code/commands/audit-spec.md +14 -0
  88. package/templates/claude-code/commands/audit-wiring.md +14 -0
  89. package/templates/claude-code/commands/build-fix.md +28 -0
  90. package/templates/claude-code/commands/build-ui.md +59 -0
  91. package/templates/claude-code/commands/code-review.md +53 -31
  92. package/templates/claude-code/commands/fix-loop.md +211 -0
  93. package/templates/claude-code/commands/full-audit.md +36 -8
  94. package/templates/claude-code/commands/generate-prd.md +1 -1
  95. package/templates/claude-code/commands/generate-sdd.md +74 -0
  96. package/templates/claude-code/commands/generate-uat.md +107 -35
  97. package/templates/claude-code/commands/help.md +68 -0
  98. package/templates/claude-code/commands/live-uat.md +268 -0
  99. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  100. package/templates/claude-code/commands/plan.md +3 -3
  101. package/templates/claude-code/commands/pre-pr.md +57 -19
  102. package/templates/claude-code/commands/product-strategist.md +21 -0
  103. package/templates/claude-code/commands/resume-session.md +10 -10
  104. package/templates/claude-code/commands/run-uat.md +59 -2
  105. package/templates/claude-code/commands/save-session.md +10 -10
  106. package/templates/claude-code/commands/simplify.md +36 -0
  107. package/templates/claude-code/commands/tdd.md +17 -18
  108. package/templates/claude-code/commands/verify-all.md +24 -0
  109. package/templates/claude-code/commands/verify-intent.md +55 -0
  110. package/templates/claude-code/commands/workflows.md +52 -40
  111. package/templates/claude-code/hooks/polyglot.json +10 -1
  112. package/templates/claude-code/hooks/python.json +10 -1
  113. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  114. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  115. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  116. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  117. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  118. package/templates/claude-code/hooks/typescript.json +10 -1
  119. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  120. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  121. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  122. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  123. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  124. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  125. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  126. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  127. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  128. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  129. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  132. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  133. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  136. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  137. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  139. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  140. package/templates/frontend/nextjs/package.json.template +3 -1
  141. package/templates/frontend/react/index.html.template +12 -0
  142. package/templates/frontend/react/package.json.template +34 -0
  143. package/templates/frontend/react/src/App.tsx.template +10 -0
  144. package/templates/frontend/react/src/index.css +1 -0
  145. package/templates/frontend/react/src/main.tsx +10 -0
  146. package/templates/frontend/react/tsconfig.json +17 -0
  147. package/templates/frontend/react/vite.config.ts.template +15 -0
  148. package/templates/frontend/react/vitest.config.ts +9 -0
  149. package/templates/frontend/remix/app/root.tsx.template +31 -0
  150. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  151. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  152. package/templates/frontend/remix/app/tailwind.css +1 -0
  153. package/templates/frontend/remix/package.json.template +39 -0
  154. package/templates/frontend/remix/tsconfig.json +18 -0
  155. package/templates/frontend/remix/vite.config.ts.template +7 -0
  156. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  157. package/docs/00-README.md +0 -310
  158. package/docs/01-universal-prompt-library.md +0 -1049
  159. package/docs/02-claude-code-mastery-playbook.md +0 -283
  160. package/docs/03-multi-agent-verification.md +0 -565
  161. package/docs/04-errata-and-verification-checklist.md +0 -284
  162. package/docs/05-universal-scaffolder-vision.md +0 -452
  163. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  164. package/docs/errata.md +0 -58
  165. package/docs/multi-agent-verification.md +0 -66
  166. package/docs/playbook.md +0 -95
  167. package/docs/prompt-library.md +0 -160
  168. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  169. package/docs/uat/UAT_TEMPLATE.md +0 -163
  170. package/templates/claude-code/commands/done.md +0 -19
  171. /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
@@ -0,0 +1,81 @@
1
+ import Link from 'next/link';
2
+ import { notFound } from 'next/navigation';
3
+ import { getCategories, getDoc } from '@/lib/docs';
4
+ import { MarkdownRenderer } from '@/components/portal/markdown-renderer';
5
+
6
+ export function generateStaticParams() {
7
+ const categories = getCategories();
8
+ const params: { category: string; slug: string }[] = [];
9
+ for (const cat of categories) {
10
+ for (const doc of cat.docs) {
11
+ params.push({ category: cat.id, slug: doc.slug });
12
+ }
13
+ }
14
+ return params;
15
+ }
16
+
17
+ export default async function DocPage({ params }: { params: Promise<{ category: string; slug: string }> }) {
18
+ const { category, slug } = await params;
19
+ const doc = getDoc(category, slug);
20
+
21
+ if (!doc) notFound();
22
+
23
+ const categories = getCategories();
24
+ const cat = categories.find((c) => c.id === category);
25
+
26
+ // Find prev/next docs within the same category
27
+ const catDocs = cat?.docs || [];
28
+ const currentIndex = catDocs.findIndex((d) => d.slug === slug);
29
+ const prevDoc = currentIndex > 0 ? catDocs[currentIndex - 1] : null;
30
+ const nextDoc = currentIndex < catDocs.length - 1 ? catDocs[currentIndex + 1] : null;
31
+
32
+ return (
33
+ <div className="max-w-4xl mx-auto">
34
+ {/* Breadcrumb */}
35
+ <div className="flex items-center gap-2 text-sm text-gray-500 mb-6">
36
+ <Link href="/portal" className="hover:text-gray-700 transition-colors">Portal</Link>
37
+ <span>/</span>
38
+ <Link href={`/portal/${category}`} className="hover:text-gray-700 transition-colors">
39
+ {doc.categoryLabel}
40
+ </Link>
41
+ <span>/</span>
42
+ <span className="text-gray-900">{doc.title}</span>
43
+ </div>
44
+
45
+ {/* Document */}
46
+ <article className="bg-white rounded-xl border border-gray-200 p-8 md:p-12">
47
+ <div className="mb-8 pb-6 border-b border-gray-200">
48
+ <h1 className="text-3xl font-bold text-gray-900">{doc.title}</h1>
49
+ <p className="text-sm text-gray-400 mt-2">
50
+ Last updated {`${doc.lastModified.getUTCFullYear()}-${String(doc.lastModified.getUTCMonth() + 1).padStart(2, '0')}-${String(doc.lastModified.getUTCDate()).padStart(2, '0')}`}
51
+ </p>
52
+ </div>
53
+ <MarkdownRenderer content={doc.content} />
54
+ </article>
55
+
56
+ {/* Prev/Next navigation */}
57
+ {(prevDoc || nextDoc) && (
58
+ <div className="flex justify-between mt-6 gap-4">
59
+ {prevDoc ? (
60
+ <Link
61
+ href={`/portal/${category}/${prevDoc.slug}`}
62
+ className="flex-1 bg-white rounded-xl border border-gray-200 p-4 hover:shadow-md transition-shadow"
63
+ >
64
+ <span className="text-xs text-gray-400">Previous</span>
65
+ <p className="text-sm font-medium text-gray-900 mt-1">{prevDoc.title}</p>
66
+ </Link>
67
+ ) : <div className="flex-1" />}
68
+ {nextDoc ? (
69
+ <Link
70
+ href={`/portal/${category}/${nextDoc.slug}`}
71
+ className="flex-1 bg-white rounded-xl border border-gray-200 p-4 hover:shadow-md transition-shadow text-right"
72
+ >
73
+ <span className="text-xs text-gray-400">Next</span>
74
+ <p className="text-sm font-medium text-gray-900 mt-1">{nextDoc.title}</p>
75
+ </Link>
76
+ ) : <div className="flex-1" />}
77
+ </div>
78
+ )}
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,65 @@
1
+ import Link from 'next/link';
2
+ import { notFound } from 'next/navigation';
3
+ import { getCategories, getDoc } from '@/lib/docs';
4
+
5
+ export function generateStaticParams() {
6
+ return getCategories().map((cat) => ({ category: cat.id }));
7
+ }
8
+
9
+ export default async function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
10
+ const { category } = await params;
11
+ const categories = getCategories();
12
+ const cat = categories.find((c) => c.id === category);
13
+
14
+ if (!cat) notFound();
15
+
16
+ return (
17
+ <div className="space-y-6">
18
+ <div>
19
+ <div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
20
+ <Link href="/portal" className="hover:text-gray-700 transition-colors">Portal</Link>
21
+ <span>/</span>
22
+ <span className="text-gray-900">{cat.label}</span>
23
+ </div>
24
+ <h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
25
+ <span>{cat.icon}</span>
26
+ <span>{cat.label}</span>
27
+ </h1>
28
+ {cat.description && (
29
+ <p className="mt-2 text-gray-600">{cat.description}</p>
30
+ )}
31
+ </div>
32
+
33
+ <div className="grid grid-cols-1 gap-3">
34
+ {cat.docs.map((doc) => {
35
+ const full = getDoc(category, doc.slug);
36
+ // Get first paragraph as preview
37
+ const firstLine = full?.content
38
+ .split('\n')
39
+ .filter((line) => line.trim() && !line.startsWith('#'))
40
+ [0] ?? '';
41
+ const preview = firstLine.slice(0, 200);
42
+ const isTruncated = firstLine.length > 200;
43
+
44
+ return (
45
+ <Link
46
+ key={doc.slug}
47
+ href={`/portal/${category}/${doc.slug}`}
48
+ className="bg-white rounded-xl border border-gray-200 p-5 hover:shadow-md transition-shadow block"
49
+ >
50
+ <h2 className="text-lg font-semibold text-gray-900 mb-1">{doc.title}</h2>
51
+ {preview && (
52
+ <p className="text-sm text-gray-500 line-clamp-2">{preview}{isTruncated ? '...' : ''}</p>
53
+ )}
54
+ {full && (
55
+ <p className="text-xs text-gray-400 mt-2">
56
+ Updated {`${full.lastModified.getUTCFullYear()}-${String(full.lastModified.getUTCMonth() + 1).padStart(2, '0')}-${String(full.lastModified.getUTCDate()).padStart(2, '0')}`}
57
+ </p>
58
+ )}
59
+ </Link>
60
+ );
61
+ })}
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,54 @@
1
+ import Link from 'next/link';
2
+ import { getCategories } from '@/lib/docs';
3
+ import { PortalNav } from '@/components/portal/portal-nav';
4
+ import { MobilePortalNav } from '@/components/portal/mobile-portal-nav';
5
+
6
+ export const metadata = {
7
+ title: 'Project Portal — {{PROJECT_NAME_PASCAL}}',
8
+ description: 'Project documentation, requirements, architecture, and test results',
9
+ };
10
+
11
+ export default function PortalLayout({ children }: { children: React.ReactNode }) {
12
+ const categories = getCategories();
13
+
14
+ return (
15
+ <div className="min-h-screen bg-gray-50">
16
+ {/* Header */}
17
+ <header className="bg-white border-b border-gray-200 sticky top-0 z-40">
18
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
19
+ <div className="flex items-center justify-between h-16">
20
+ <div className="flex items-center gap-6">
21
+ <Link href="/portal" className="flex items-center gap-2.5">
22
+ <div className="w-8 h-8 bg-gray-900 rounded-lg flex items-center justify-center">
23
+ <span className="text-white text-sm font-bold">
24
+ {'{{PROJECT_NAME_PASCAL}}'.charAt(0)}
25
+ </span>
26
+ </div>
27
+ <span className="font-semibold text-gray-900">{{PROJECT_NAME_PASCAL}}</span>
28
+ </Link>
29
+ <div className="hidden md:block">
30
+ <PortalNav categories={categories} />
31
+ </div>
32
+ </div>
33
+ <div className="flex items-center gap-3">
34
+ <Link
35
+ href="/"
36
+ className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
37
+ >
38
+ Back to app
39
+ </Link>
40
+ <div className="md:hidden">
41
+ <MobilePortalNav categories={categories} />
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </header>
47
+
48
+ {/* Content */}
49
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
50
+ {children}
51
+ </main>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,85 @@
1
+ import Link from 'next/link';
2
+ import { getCategories, getAllDocs } from '@/lib/docs';
3
+
4
+ export default function PortalHome() {
5
+ const categories = getCategories();
6
+ const allDocs = getAllDocs();
7
+
8
+ const recentDocs = [...allDocs]
9
+ .sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
10
+ .slice(0, 5);
11
+
12
+ if (categories.length === 0) {
13
+ return (
14
+ <div className="text-center py-20">
15
+ <div className="text-5xl mb-4">📄</div>
16
+ <h1 className="text-2xl font-bold text-gray-900 mb-2">No documentation yet</h1>
17
+ <p className="text-gray-600 max-w-md mx-auto">
18
+ Documentation will appear here once it has been generated.
19
+ Ask your development team to run the documentation generators.
20
+ </p>
21
+ </div>
22
+ );
23
+ }
24
+
25
+ return (
26
+ <div className="space-y-8">
27
+ {/* Welcome */}
28
+ <div>
29
+ <h1 className="text-3xl font-bold text-gray-900">Project Documentation</h1>
30
+ <p className="mt-2 text-gray-600">
31
+ Everything you need to know about this project — requirements, architecture, test results, and more.
32
+ </p>
33
+ </div>
34
+
35
+ {/* Category cards */}
36
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
37
+ {categories.map((cat) => (
38
+ <div
39
+ key={cat.id}
40
+ className="bg-white rounded-xl border border-gray-200 p-6 hover:shadow-md transition-shadow"
41
+ >
42
+ <div className="text-3xl mb-3">{cat.icon}</div>
43
+ <h2 className="text-lg font-semibold text-gray-900 mb-1">{cat.label}</h2>
44
+ <p className="text-sm text-gray-500 mb-4">{cat.description}</p>
45
+ <div className="space-y-1">
46
+ {cat.docs.map((doc) => (
47
+ <Link
48
+ key={doc.slug}
49
+ href={`/portal/${cat.id}/${doc.slug}`}
50
+ className="block text-sm text-blue-600 hover:text-blue-800 transition-colors py-0.5"
51
+ >
52
+ {doc.title}
53
+ </Link>
54
+ ))}
55
+ </div>
56
+ </div>
57
+ ))}
58
+ </div>
59
+
60
+ {/* Recently updated */}
61
+ {recentDocs.length > 0 && (
62
+ <div className="bg-white rounded-xl border border-gray-200 p-6">
63
+ <h2 className="text-lg font-semibold text-gray-900 mb-4">Recently Updated</h2>
64
+ <div className="divide-y divide-gray-100">
65
+ {recentDocs.map((doc) => (
66
+ <Link
67
+ key={`${doc.category}/${doc.slug}`}
68
+ href={`/portal/${doc.category}/${doc.slug}`}
69
+ className="flex items-center justify-between py-3 hover:bg-gray-50 -mx-3 px-3 rounded-lg transition-colors"
70
+ >
71
+ <div>
72
+ <span className="text-sm font-medium text-gray-900">{doc.title}</span>
73
+ <span className="ml-2 text-xs text-gray-400">{doc.categoryLabel}</span>
74
+ </div>
75
+ <span className="text-xs text-gray-400">
76
+ {`${doc.lastModified.getUTCFullYear()}-${String(doc.lastModified.getUTCMonth() + 1).padStart(2, '0')}-${String(doc.lastModified.getUTCDate()).padStart(2, '0')}`}
77
+ </span>
78
+ </Link>
79
+ ))}
80
+ </div>
81
+ </div>
82
+ )}
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import ReactMarkdown from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+
6
+ export function MarkdownRenderer({ content }: { content: string }) {
7
+ return (
8
+ <div className="prose prose-gray max-w-none">
9
+ <ReactMarkdown
10
+ remarkPlugins={[remarkGfm]}
11
+ components={{
12
+ // Clean table rendering
13
+ table: ({ children }) => (
14
+ <div className="overflow-x-auto my-6 rounded-lg border border-gray-200">
15
+ <table className="min-w-full divide-y divide-gray-200">{children}</table>
16
+ </div>
17
+ ),
18
+ thead: ({ children }) => (
19
+ <thead className="bg-gray-50">{children}</thead>
20
+ ),
21
+ th: ({ children }) => (
22
+ <th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
23
+ {children}
24
+ </th>
25
+ ),
26
+ td: ({ children }) => (
27
+ <td className="px-4 py-3 text-sm text-gray-700 border-t border-gray-100">{children}</td>
28
+ ),
29
+ // Status badges
30
+ code: ({ children, className }) => {
31
+ const isBlock = className?.startsWith('language-');
32
+ if (isBlock) {
33
+ return (
34
+ <code className={`${className} block overflow-x-auto`}>{children}</code>
35
+ );
36
+ }
37
+ // Inline status badges
38
+ const text = String(children);
39
+ if (text === 'PASS' || text === '✅ Built') {
40
+ return <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">{text}</span>;
41
+ }
42
+ if (text === 'FAIL' || text === 'CRITICAL') {
43
+ return <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">{text}</span>;
44
+ }
45
+ if (text === '🚧 In Progress') {
46
+ return <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">{text}</span>;
47
+ }
48
+ if (text === '📋 Planned') {
49
+ return <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">{text}</span>;
50
+ }
51
+ return <code className="px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono text-gray-800">{children}</code>;
52
+ },
53
+ // Headings with anchors
54
+ h1: ({ children }) => {
55
+ const id = String(children).toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
56
+ return <h1 id={id} className="text-3xl font-bold text-gray-900 mt-8 mb-4 first:mt-0">{children}</h1>;
57
+ },
58
+ h2: ({ children }) => {
59
+ const id = String(children).toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
60
+ return <h2 id={id} className="text-2xl font-semibold text-gray-900 mt-8 mb-3 pb-2 border-b border-gray-200">{children}</h2>;
61
+ },
62
+ h3: ({ children }) => {
63
+ const id = String(children).toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
64
+ return <h3 id={id} className="text-xl font-semibold text-gray-800 mt-6 mb-2">{children}</h3>;
65
+ },
66
+ // Clean list rendering
67
+ ul: ({ children }) => (
68
+ <ul className="list-disc list-outside ml-5 space-y-1.5 text-gray-700">{children}</ul>
69
+ ),
70
+ ol: ({ children }) => (
71
+ <ol className="list-decimal list-outside ml-5 space-y-1.5 text-gray-700">{children}</ol>
72
+ ),
73
+ // Paragraphs
74
+ p: ({ children }) => (
75
+ <p className="text-gray-700 leading-relaxed my-3">{children}</p>
76
+ ),
77
+ // Links — only allow safe protocols
78
+ a: ({ href, children }) => {
79
+ const safeHref = href && /^(https?:\/\/|mailto:|\/|#)/.test(href) ? href : undefined;
80
+ const isExternal = safeHref?.startsWith('http');
81
+ return (
82
+ <a
83
+ href={safeHref}
84
+ className="text-blue-600 hover:text-blue-800 underline decoration-blue-300 hover:decoration-blue-500 transition-colors"
85
+ {...(isExternal && { target: '_blank', rel: 'noopener noreferrer' })}
86
+ >
87
+ {children}
88
+ </a>
89
+ );
90
+ },
91
+ // Blockquotes
92
+ blockquote: ({ children }) => (
93
+ <blockquote className="border-l-4 border-blue-200 bg-blue-50 pl-4 py-2 my-4 rounded-r-lg text-gray-700">
94
+ {children}
95
+ </blockquote>
96
+ ),
97
+ }}
98
+ />
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { usePathname } from 'next/navigation';
6
+
7
+ interface NavCategory {
8
+ id: string;
9
+ label: string;
10
+ icon: string;
11
+ docs: { slug: string; title: string }[];
12
+ }
13
+
14
+ export function MobilePortalNav({ categories }: { categories: NavCategory[] }) {
15
+ const pathname = usePathname();
16
+ const [open, setOpen] = useState(false);
17
+
18
+ return (
19
+ <>
20
+ <button
21
+ onClick={() => setOpen(!open)}
22
+ className="p-2 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors"
23
+ aria-label="Toggle navigation menu"
24
+ >
25
+ {open ? (
26
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
27
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
28
+ </svg>
29
+ ) : (
30
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
32
+ </svg>
33
+ )}
34
+ </button>
35
+
36
+ {open && (
37
+ <div className="absolute top-full left-0 right-0 bg-white border-b border-gray-200 shadow-lg z-50">
38
+ <div className="max-w-7xl mx-auto px-4 py-4 space-y-2">
39
+ <Link
40
+ href="/portal"
41
+ onClick={() => setOpen(false)}
42
+ className={`block px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
43
+ pathname === '/portal'
44
+ ? 'bg-gray-100 text-gray-900'
45
+ : 'text-gray-600 hover:bg-gray-50'
46
+ }`}
47
+ >
48
+ Overview
49
+ </Link>
50
+ {categories.map((cat) => (
51
+ <div key={cat.id} className="space-y-1">
52
+ <Link
53
+ href={`/portal/${cat.id}`}
54
+ onClick={() => setOpen(false)}
55
+ className={`block px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
56
+ pathname.startsWith(`/portal/${cat.id}`)
57
+ ? 'bg-gray-100 text-gray-900'
58
+ : 'text-gray-600 hover:bg-gray-50'
59
+ }`}
60
+ >
61
+ <span className="mr-2">{cat.icon}</span>
62
+ {cat.label}
63
+ </Link>
64
+ {cat.docs.map((doc) => (
65
+ <Link
66
+ key={doc.slug}
67
+ href={`/portal/${cat.id}/${doc.slug}`}
68
+ onClick={() => setOpen(false)}
69
+ className="block pl-8 pr-3 py-1.5 text-sm text-gray-500 hover:text-gray-900 hover:bg-gray-50 rounded-lg transition-colors"
70
+ >
71
+ {doc.title}
72
+ </Link>
73
+ ))}
74
+ </div>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ )}
79
+ </>
80
+ );
81
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useState, useRef, useEffect } from 'react';
6
+
7
+ interface NavCategory {
8
+ id: string;
9
+ label: string;
10
+ icon: string;
11
+ docs: { slug: string; title: string }[];
12
+ }
13
+
14
+ export function PortalNav({ categories }: { categories: NavCategory[] }) {
15
+ const pathname = usePathname();
16
+ const [openMenu, setOpenMenu] = useState<string | null>(null);
17
+ const navRef = useRef<HTMLElement>(null);
18
+
19
+ // Close dropdown when clicking outside
20
+ useEffect(() => {
21
+ function handleClickOutside(e: MouseEvent) {
22
+ if (navRef.current && !navRef.current.contains(e.target as Node)) {
23
+ setOpenMenu(null);
24
+ }
25
+ }
26
+ document.addEventListener('mousedown', handleClickOutside);
27
+ return () => document.removeEventListener('mousedown', handleClickOutside);
28
+ }, []);
29
+
30
+ return (
31
+ <nav ref={navRef} className="flex items-center gap-1">
32
+ <Link
33
+ href="/portal"
34
+ className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
35
+ pathname === '/portal'
36
+ ? 'bg-gray-100 text-gray-900'
37
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
38
+ }`}
39
+ >
40
+ Overview
41
+ </Link>
42
+ {categories.map((cat) => (
43
+ <div
44
+ key={cat.id}
45
+ className="relative"
46
+ onMouseEnter={() => setOpenMenu(cat.id)}
47
+ onMouseLeave={() => setOpenMenu(null)}
48
+ >
49
+ <button
50
+ aria-expanded={openMenu === cat.id}
51
+ aria-haspopup="true"
52
+ onClick={() => setOpenMenu(openMenu === cat.id ? null : cat.id)}
53
+ onKeyDown={(e) => {
54
+ if (e.key === 'Escape') setOpenMenu(null);
55
+ }}
56
+ className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-1.5 ${
57
+ pathname.startsWith(`/portal/${cat.id}`)
58
+ ? 'bg-gray-100 text-gray-900'
59
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
60
+ }`}
61
+ >
62
+ <span>{cat.icon}</span>
63
+ <span>{cat.label}</span>
64
+ <svg className="w-3.5 h-3.5 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
65
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
66
+ </svg>
67
+ </button>
68
+ {openMenu === cat.id && cat.docs.length > 0 && (
69
+ <div role="menu" className="absolute top-full left-0 mt-1 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50">
70
+ {cat.docs.map((doc) => (
71
+ <Link
72
+ key={doc.slug}
73
+ role="menuitem"
74
+ href={`/portal/${cat.id}/${doc.slug}`}
75
+ className="block px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 transition-colors"
76
+ >
77
+ {doc.title}
78
+ </Link>
79
+ ))}
80
+ </div>
81
+ )}
82
+ </div>
83
+ ))}
84
+ </nav>
85
+ );
86
+ }