irgen 0.2.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 (244) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +312 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/dsl/aggregator.d.ts +8 -0
  9. package/dist/dsl/aggregator.d.ts.map +1 -0
  10. package/dist/dsl/aggregator.js +64 -0
  11. package/dist/dsl/aggregator.js.map +1 -0
  12. package/dist/dsl/frontend-runtime.d.ts +486 -0
  13. package/dist/dsl/frontend-runtime.d.ts.map +1 -0
  14. package/dist/dsl/frontend-runtime.js +232 -0
  15. package/dist/dsl/frontend-runtime.js.map +1 -0
  16. package/dist/dsl/runtime.d.ts +33 -0
  17. package/dist/dsl/runtime.d.ts.map +1 -0
  18. package/dist/dsl/runtime.js +120 -0
  19. package/dist/dsl/runtime.js.map +1 -0
  20. package/dist/emit/backend/adapters.d.ts +11 -0
  21. package/dist/emit/backend/adapters.d.ts.map +1 -0
  22. package/dist/emit/backend/adapters.js +374 -0
  23. package/dist/emit/backend/adapters.js.map +1 -0
  24. package/dist/emit/backend/backend-tsmorph.d.ts +5 -0
  25. package/dist/emit/backend/backend-tsmorph.d.ts.map +1 -0
  26. package/dist/emit/backend/backend-tsmorph.js +858 -0
  27. package/dist/emit/backend/backend-tsmorph.js.map +1 -0
  28. package/dist/emit/backend/fake-backend.d.ts +2 -0
  29. package/dist/emit/backend/fake-backend.d.ts.map +1 -0
  30. package/dist/emit/backend/fake-backend.js +19 -0
  31. package/dist/emit/backend/fake-backend.js.map +1 -0
  32. package/dist/emit/backend/packaging.d.ts +3 -0
  33. package/dist/emit/backend/packaging.d.ts.map +1 -0
  34. package/dist/emit/backend/packaging.js +71 -0
  35. package/dist/emit/backend/packaging.js.map +1 -0
  36. package/dist/emit/backend/server.d.ts +4 -0
  37. package/dist/emit/backend/server.d.ts.map +1 -0
  38. package/dist/emit/backend/server.js +169 -0
  39. package/dist/emit/backend/server.js.map +1 -0
  40. package/dist/emit/cli/cli-fake.d.ts +2 -0
  41. package/dist/emit/cli/cli-fake.d.ts.map +1 -0
  42. package/dist/emit/cli/cli-fake.js +33 -0
  43. package/dist/emit/cli/cli-fake.js.map +1 -0
  44. package/dist/emit/electron/electron-shell.d.ts +3 -0
  45. package/dist/emit/electron/electron-shell.d.ts.map +1 -0
  46. package/dist/emit/electron/electron-shell.js +454 -0
  47. package/dist/emit/electron/electron-shell.js.map +1 -0
  48. package/dist/emit/engine.d.ts +14 -0
  49. package/dist/emit/engine.d.ts.map +1 -0
  50. package/dist/emit/engine.js +25 -0
  51. package/dist/emit/engine.js.map +1 -0
  52. package/dist/emit/format.d.ts +2 -0
  53. package/dist/emit/format.d.ts.map +1 -0
  54. package/dist/emit/format.js +23 -0
  55. package/dist/emit/format.js.map +1 -0
  56. package/dist/emit/frontend/frontend-react.d.ts +4 -0
  57. package/dist/emit/frontend/frontend-react.d.ts.map +1 -0
  58. package/dist/emit/frontend/frontend-react.js +2021 -0
  59. package/dist/emit/frontend/frontend-react.js.map +1 -0
  60. package/dist/emit/frontend/registry.d.ts +20 -0
  61. package/dist/emit/frontend/registry.d.ts.map +1 -0
  62. package/dist/emit/frontend/registry.js +46 -0
  63. package/dist/emit/frontend/registry.js.map +1 -0
  64. package/dist/emit/frontend/runtime-emitter.d.ts +4 -0
  65. package/dist/emit/frontend/runtime-emitter.d.ts.map +1 -0
  66. package/dist/emit/frontend/runtime-emitter.js +435 -0
  67. package/dist/emit/frontend/runtime-emitter.js.map +1 -0
  68. package/dist/emit/frontend/runtime-template.d.ts +28 -0
  69. package/dist/emit/frontend/runtime-template.d.ts.map +1 -0
  70. package/dist/emit/frontend/runtime-template.js +218 -0
  71. package/dist/emit/frontend/runtime-template.js.map +1 -0
  72. package/dist/emit/frontend/ssg.d.ts +8 -0
  73. package/dist/emit/frontend/ssg.d.ts.map +1 -0
  74. package/dist/emit/frontend/ssg.js +219 -0
  75. package/dist/emit/frontend/ssg.js.map +1 -0
  76. package/dist/emit/registry.d.ts +17 -0
  77. package/dist/emit/registry.d.ts.map +1 -0
  78. package/dist/emit/registry.js +38 -0
  79. package/dist/emit/registry.js.map +1 -0
  80. package/dist/emit/static-site/css.d.ts +5 -0
  81. package/dist/emit/static-site/css.d.ts.map +1 -0
  82. package/dist/emit/static-site/css.js +872 -0
  83. package/dist/emit/static-site/css.js.map +1 -0
  84. package/dist/emit/static-site/enhancements.d.ts +11 -0
  85. package/dist/emit/static-site/enhancements.d.ts.map +1 -0
  86. package/dist/emit/static-site/enhancements.js +266 -0
  87. package/dist/emit/static-site/enhancements.js.map +1 -0
  88. package/dist/emit/static-site/static-site-html.d.ts +3 -0
  89. package/dist/emit/static-site/static-site-html.d.ts.map +1 -0
  90. package/dist/emit/static-site/static-site-html.js +1172 -0
  91. package/dist/emit/static-site/static-site-html.js.map +1 -0
  92. package/dist/emit/utils/sdk.d.ts +15 -0
  93. package/dist/emit/utils/sdk.d.ts.map +1 -0
  94. package/dist/emit/utils/sdk.js +34 -0
  95. package/dist/emit/utils/sdk.js.map +1 -0
  96. package/dist/extensions/context.d.ts +23 -0
  97. package/dist/extensions/context.d.ts.map +1 -0
  98. package/dist/extensions/context.js +43 -0
  99. package/dist/extensions/context.js.map +1 -0
  100. package/dist/index.d.ts +32 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +135 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/ir/decl/backend.raw.schema.d.ts +128 -0
  105. package/dist/ir/decl/backend.raw.schema.d.ts.map +1 -0
  106. package/dist/ir/decl/backend.raw.schema.js +24 -0
  107. package/dist/ir/decl/backend.raw.schema.js.map +1 -0
  108. package/dist/ir/decl/bundle.d.ts +15 -0
  109. package/dist/ir/decl/bundle.d.ts.map +1 -0
  110. package/dist/ir/decl/bundle.js +8 -0
  111. package/dist/ir/decl/bundle.js.map +1 -0
  112. package/dist/ir/decl/cli.raw.schema.d.ts +133 -0
  113. package/dist/ir/decl/cli.raw.schema.d.ts.map +1 -0
  114. package/dist/ir/decl/cli.raw.schema.js +20 -0
  115. package/dist/ir/decl/cli.raw.schema.js.map +1 -0
  116. package/dist/ir/decl/frontend.raw.schema.d.ts +6631 -0
  117. package/dist/ir/decl/frontend.raw.schema.d.ts.map +1 -0
  118. package/dist/ir/decl/frontend.raw.schema.js +272 -0
  119. package/dist/ir/decl/frontend.raw.schema.js.map +1 -0
  120. package/dist/ir/decl/index.d.ts +6 -0
  121. package/dist/ir/decl/index.d.ts.map +1 -0
  122. package/dist/ir/decl/index.js +6 -0
  123. package/dist/ir/decl/index.js.map +1 -0
  124. package/dist/ir/decl/normalize.schema.d.ts +9154 -0
  125. package/dist/ir/decl/normalize.schema.d.ts.map +1 -0
  126. package/dist/ir/decl/normalize.schema.js +71 -0
  127. package/dist/ir/decl/normalize.schema.js.map +1 -0
  128. package/dist/ir/domain/backend.d.ts +19 -0
  129. package/dist/ir/domain/backend.d.ts.map +1 -0
  130. package/dist/ir/domain/backend.js +3 -0
  131. package/dist/ir/domain/backend.js.map +1 -0
  132. package/dist/ir/domain/cli.d.ts +18 -0
  133. package/dist/ir/domain/cli.d.ts.map +1 -0
  134. package/dist/ir/domain/cli.js +2 -0
  135. package/dist/ir/domain/cli.js.map +1 -0
  136. package/dist/ir/domain/frontend/index.d.ts +190 -0
  137. package/dist/ir/domain/frontend/index.d.ts.map +1 -0
  138. package/dist/ir/domain/frontend/index.js +2 -0
  139. package/dist/ir/domain/frontend/index.js.map +1 -0
  140. package/dist/ir/domain/frontend.d.ts +2 -0
  141. package/dist/ir/domain/frontend.d.ts.map +1 -0
  142. package/dist/ir/domain/frontend.js +3 -0
  143. package/dist/ir/domain/frontend.js.map +1 -0
  144. package/dist/ir/frontend-contract.d.ts +187 -0
  145. package/dist/ir/frontend-contract.d.ts.map +1 -0
  146. package/dist/ir/frontend-contract.js +6 -0
  147. package/dist/ir/frontend-contract.js.map +1 -0
  148. package/dist/ir/target/backend.d.ts +11 -0
  149. package/dist/ir/target/backend.d.ts.map +1 -0
  150. package/dist/ir/target/backend.js +2 -0
  151. package/dist/ir/target/backend.js.map +1 -0
  152. package/dist/ir/target/backend.policy.d.ts +896 -0
  153. package/dist/ir/target/backend.policy.d.ts.map +1 -0
  154. package/dist/ir/target/backend.policy.js +106 -0
  155. package/dist/ir/target/backend.policy.js.map +1 -0
  156. package/dist/ir/target/cli.d.ts +3 -0
  157. package/dist/ir/target/cli.d.ts.map +1 -0
  158. package/dist/ir/target/cli.js +2 -0
  159. package/dist/ir/target/cli.js.map +1 -0
  160. package/dist/ir/target/electron.d.ts +99 -0
  161. package/dist/ir/target/electron.d.ts.map +1 -0
  162. package/dist/ir/target/electron.js +2 -0
  163. package/dist/ir/target/electron.js.map +1 -0
  164. package/dist/ir/target/electron.policy.d.ts +7015 -0
  165. package/dist/ir/target/electron.policy.d.ts.map +1 -0
  166. package/dist/ir/target/electron.policy.js +119 -0
  167. package/dist/ir/target/electron.policy.js.map +1 -0
  168. package/dist/ir/target/frontend.d.ts +12 -0
  169. package/dist/ir/target/frontend.d.ts.map +1 -0
  170. package/dist/ir/target/frontend.js +2 -0
  171. package/dist/ir/target/frontend.js.map +1 -0
  172. package/dist/ir/target/frontend.policy.d.ts +268 -0
  173. package/dist/ir/target/frontend.policy.d.ts.map +1 -0
  174. package/dist/ir/target/frontend.policy.js +33 -0
  175. package/dist/ir/target/frontend.policy.js.map +1 -0
  176. package/dist/ir/target/index.d.ts +6 -0
  177. package/dist/ir/target/index.d.ts.map +1 -0
  178. package/dist/ir/target/index.js +6 -0
  179. package/dist/ir/target/index.js.map +1 -0
  180. package/dist/ir/target/static-site.d.ts +18 -0
  181. package/dist/ir/target/static-site.d.ts.map +1 -0
  182. package/dist/ir/target/static-site.js +2 -0
  183. package/dist/ir/target/static-site.js.map +1 -0
  184. package/dist/ir/target/static-site.policy.d.ts +2911 -0
  185. package/dist/ir/target/static-site.policy.d.ts.map +1 -0
  186. package/dist/ir/target/static-site.policy.js +127 -0
  187. package/dist/ir/target/static-site.policy.js.map +1 -0
  188. package/dist/lowering/backend.d.ts +4 -0
  189. package/dist/lowering/backend.d.ts.map +1 -0
  190. package/dist/lowering/backend.js +57 -0
  191. package/dist/lowering/backend.js.map +1 -0
  192. package/dist/lowering/cli.d.ts +4 -0
  193. package/dist/lowering/cli.d.ts.map +1 -0
  194. package/dist/lowering/cli.js +22 -0
  195. package/dist/lowering/cli.js.map +1 -0
  196. package/dist/lowering/engine.d.ts +18 -0
  197. package/dist/lowering/engine.d.ts.map +1 -0
  198. package/dist/lowering/engine.js +47 -0
  199. package/dist/lowering/engine.js.map +1 -0
  200. package/dist/lowering/frontend.d.ts +9 -0
  201. package/dist/lowering/frontend.d.ts.map +1 -0
  202. package/dist/lowering/frontend.js +246 -0
  203. package/dist/lowering/frontend.js.map +1 -0
  204. package/dist/lowering/targets/to-backend.d.ts +9 -0
  205. package/dist/lowering/targets/to-backend.d.ts.map +1 -0
  206. package/dist/lowering/targets/to-backend.js +55 -0
  207. package/dist/lowering/targets/to-backend.js.map +1 -0
  208. package/dist/lowering/targets/to-cli.d.ts +4 -0
  209. package/dist/lowering/targets/to-cli.d.ts.map +1 -0
  210. package/dist/lowering/targets/to-cli.js +11 -0
  211. package/dist/lowering/targets/to-cli.js.map +1 -0
  212. package/dist/lowering/targets/to-electron.d.ts +30 -0
  213. package/dist/lowering/targets/to-electron.d.ts.map +1 -0
  214. package/dist/lowering/targets/to-electron.js +87 -0
  215. package/dist/lowering/targets/to-electron.js.map +1 -0
  216. package/dist/lowering/targets/to-frontend.d.ts +4 -0
  217. package/dist/lowering/targets/to-frontend.d.ts.map +1 -0
  218. package/dist/lowering/targets/to-frontend.js +30 -0
  219. package/dist/lowering/targets/to-frontend.js.map +1 -0
  220. package/dist/lowering/targets/to-static-site.d.ts +16 -0
  221. package/dist/lowering/targets/to-static-site.d.ts.map +1 -0
  222. package/dist/lowering/targets/to-static-site.js +30 -0
  223. package/dist/lowering/targets/to-static-site.js.map +1 -0
  224. package/dist/mappers/index.d.ts +12 -0
  225. package/dist/mappers/index.d.ts.map +1 -0
  226. package/dist/mappers/index.js +60 -0
  227. package/dist/mappers/index.js.map +1 -0
  228. package/dist/types/extension.d.ts +3 -0
  229. package/dist/types/extension.d.ts.map +1 -0
  230. package/dist/types/extension.js +2 -0
  231. package/dist/types/extension.js.map +1 -0
  232. package/dist/utils/array.d.ts +2 -0
  233. package/dist/utils/array.d.ts.map +1 -0
  234. package/dist/utils/array.js +4 -0
  235. package/dist/utils/array.js.map +1 -0
  236. package/dist/utils/index.d.ts +3 -0
  237. package/dist/utils/index.d.ts.map +1 -0
  238. package/dist/utils/index.js +3 -0
  239. package/dist/utils/index.js.map +1 -0
  240. package/dist/utils/string.d.ts +13 -0
  241. package/dist/utils/string.d.ts.map +1 -0
  242. package/dist/utils/string.js +56 -0
  243. package/dist/utils/string.js.map +1 -0
  244. package/package.json +112 -0
@@ -0,0 +1,2021 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { Project, QuoteKind, IndentationText, ScriptTarget } from "ts-morph";
4
+ import { emitterEngine } from "../engine.js";
5
+ import { registerTargetEmitter } from "../registry.js";
6
+ import { pascal, kebab } from "../../utils/string.js";
7
+ import { emitSsgSupport } from "./ssg.js";
8
+ import { emitRuntime } from "./runtime-emitter.js";
9
+ function hasMarkdownCodeBlocks(ir) {
10
+ const hasFence = (text) => typeof text === "string" && /```/.test(text);
11
+ const checkComponent = (component) => {
12
+ if (!component)
13
+ return false;
14
+ if (hasFence(component.content))
15
+ return true;
16
+ if (component.layout?.tabs?.some((tab) => hasFence(tab.content)))
17
+ return true;
18
+ return false;
19
+ };
20
+ for (const page of ir.pages ?? []) {
21
+ for (const comp of page.components ?? []) {
22
+ if (checkComponent(comp))
23
+ return true;
24
+ }
25
+ }
26
+ for (const comp of ir.components ?? []) {
27
+ if (checkComponent(comp))
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ function hasMarkdownMermaid(ir) {
33
+ const hasMermaidFence = (text) => typeof text === "string" && /```mermaid/.test(text);
34
+ const checkComponent = (component) => {
35
+ if (!component)
36
+ return false;
37
+ if (hasMermaidFence(component.content))
38
+ return true;
39
+ if (component.layout?.tabs?.some((tab) => hasMermaidFence(tab.content)))
40
+ return true;
41
+ return false;
42
+ };
43
+ for (const page of ir.pages ?? []) {
44
+ for (const comp of page.components ?? []) {
45
+ if (checkComponent(comp))
46
+ return true;
47
+ }
48
+ }
49
+ for (const comp of ir.components ?? []) {
50
+ if (checkComponent(comp))
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ function escapeHtml(input) {
56
+ return input
57
+ .replace(/&/g, "&")
58
+ .replace(/</g, "&lt;")
59
+ .replace(/>/g, "&gt;")
60
+ .replace(/"/g, "&quot;")
61
+ .replace(/'/g, "&#39;");
62
+ }
63
+ function renderInlineMarkdown(input) {
64
+ let out = escapeHtml(input);
65
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, href) => {
66
+ return `<a href="${escapeHtml(href)}">${escapeHtml(text)}</a>`;
67
+ });
68
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
69
+ out = out.replace(/\*([^*]+)\*/g, "<em>$1</em>");
70
+ out = out.replace(/`([^`]+)`/g, "<code>$1</code>");
71
+ return out;
72
+ }
73
+ function slugifyHeading(input) {
74
+ return input
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9\s-]/g, "")
77
+ .trim()
78
+ .replace(/\s+/g, "-")
79
+ .replace(/-+/g, "-");
80
+ }
81
+ function renderMarkdownToHtml(input) {
82
+ const lines = input.replace(/\r\n/g, "\n").split("\n");
83
+ const parts = [];
84
+ const usedIds = new Map();
85
+ let i = 0;
86
+ while (i < lines.length) {
87
+ const line = lines[i];
88
+ if (/^```/.test(line.trim())) {
89
+ const lang = line.trim().slice(3).trim().toLowerCase();
90
+ const codeLines = [];
91
+ i += 1;
92
+ while (i < lines.length && !/^```/.test(lines[i].trim())) {
93
+ codeLines.push(lines[i]);
94
+ i += 1;
95
+ }
96
+ i += 1;
97
+ const code = escapeHtml(codeLines.join("\n"));
98
+ if (lang === "mermaid") {
99
+ parts.push(`<div class="mermaid">${code}</div>`);
100
+ }
101
+ else {
102
+ const cls = lang ? ` class="language-${escapeHtml(lang)}"` : "";
103
+ parts.push(`<pre><code${cls}>${code}</code></pre>`);
104
+ }
105
+ continue;
106
+ }
107
+ if (/^#{1,6}\s+/.test(line)) {
108
+ const level = line.match(/^#+/)?.[0]?.length ?? 1;
109
+ const text = line.replace(/^#{1,6}\s+/, "");
110
+ const baseId = slugifyHeading(text) || `section-${level}`;
111
+ const next = (usedIds.get(baseId) ?? 0) + 1;
112
+ usedIds.set(baseId, next);
113
+ const id = next > 1 ? `${baseId}-${next}` : baseId;
114
+ parts.push(`<h${level} id="${escapeHtml(id)}">${renderInlineMarkdown(text)}</h${level}>`);
115
+ i += 1;
116
+ continue;
117
+ }
118
+ if (/^(\*|-)\s+/.test(line) || /^\d+\.\s+/.test(line)) {
119
+ const isOrdered = /^\d+\.\s+/.test(line);
120
+ const items = [];
121
+ while (i < lines.length && (/^(\*|-)\s+/.test(lines[i]) || /^\d+\.\s+/.test(lines[i]))) {
122
+ const raw = lines[i].replace(/^(\*|-)\s+/, "").replace(/^\d+\.\s+/, "");
123
+ items.push(`<li>${renderInlineMarkdown(raw)}</li>`);
124
+ i += 1;
125
+ }
126
+ parts.push(isOrdered ? `<ol>${items.join("")}</ol>` : `<ul>${items.join("")}</ul>`);
127
+ continue;
128
+ }
129
+ if (line.trim() === "") {
130
+ i += 1;
131
+ continue;
132
+ }
133
+ const paragraphLines = [];
134
+ while (i < lines.length
135
+ && lines[i].trim() !== ""
136
+ && !/^#{1,6}\s+/.test(lines[i])
137
+ && !/^```/.test(lines[i].trim())
138
+ && !/^(\*|-)\s+/.test(lines[i])
139
+ && !/^\d+\.\s+/.test(lines[i])) {
140
+ paragraphLines.push(lines[i]);
141
+ i += 1;
142
+ }
143
+ const paragraph = paragraphLines.join(" ").trim();
144
+ if (paragraph)
145
+ parts.push(`<p>${renderInlineMarkdown(paragraph)}</p>`);
146
+ }
147
+ return parts.join("\n");
148
+ }
149
+ function ensureDir(p) {
150
+ fs.mkdirSync(p, { recursive: true });
151
+ }
152
+ function emitFrontendPackageJson(outDir, ir) {
153
+ const policy = ir.policies.frontend;
154
+ const mode = policy.framework.rendering.mode;
155
+ const isSsg = mode === "ssg" || mode === "hybrid";
156
+ const hasMarkdownCode = hasMarkdownCodeBlocks(ir);
157
+ const hasMermaid = hasMarkdownMermaid(ir);
158
+ const pkg = {
159
+ name: ir?.appName ? `${ir.appName.toLowerCase()}-frontend` : "generated-frontend",
160
+ version: "0.1.0",
161
+ private: true,
162
+ type: "module",
163
+ scripts: {
164
+ format: "prettier --write .",
165
+ "build:css": "tailwindcss -i src/index.css -o dist/index.css --minify",
166
+ dev: "vite",
167
+ build: isSsg ? "npm run build:ssg" : "vite build",
168
+ preview: "vite preview",
169
+ },
170
+ dependencies: {
171
+ react: "^18.2.0",
172
+ "react-dom": "^18.2.0",
173
+ "lucide-react": "^0.263.1",
174
+ "react-router-dom": "^6.14.0",
175
+ },
176
+ devDependencies: {
177
+ "@types/react": "^18.0.0",
178
+ "@types/react-dom": "^18.0.0",
179
+ tailwindcss: "^3.3.0",
180
+ postcss: "^8.4.0",
181
+ autoprefixer: "^10.4.0",
182
+ "@tailwindcss/forms": "^0.5.0",
183
+ prettier: "^2.8.8",
184
+ typescript: "^5.6.3",
185
+ tsx: "^4.19.2",
186
+ vite: "^5.4.8",
187
+ "@vitejs/plugin-react": "^4.3.2",
188
+ },
189
+ };
190
+ if (isSsg) {
191
+ pkg.scripts["build:ssg"] = "vite build && npm run build:ssr && npm run prerender";
192
+ pkg.scripts["build:ssr"] = "vite build --ssr src/entry-server.tsx --outDir .ssg";
193
+ pkg.scripts["prerender"] = "node scripts/prerender.mjs";
194
+ }
195
+ // Add syntax highlighter if needed
196
+ const hasCode = ir.pages.some(p => p.components.some(c => c.codeBlock)) || ir.components.some(c => c.codeBlock);
197
+ if (hasCode) {
198
+ pkg.dependencies["react-syntax-highlighter"] = "^15.5.0";
199
+ pkg.devDependencies["@types/react-syntax-highlighter"] = "^15.5.0";
200
+ }
201
+ if (hasMarkdownCode) {
202
+ pkg.dependencies["prismjs"] = "^1.29.0";
203
+ }
204
+ if (hasMermaid) {
205
+ pkg.dependencies["mermaid"] = "^10.9.1";
206
+ }
207
+ fs.mkdirSync(outDir, { recursive: true });
208
+ fs.writeFileSync(path.join(outDir, "package.json"), JSON.stringify(pkg, null, 2), "utf-8");
209
+ }
210
+ function collectComponentText(component) {
211
+ const parts = [];
212
+ if (component.name)
213
+ parts.push(String(component.name));
214
+ if (component.content)
215
+ parts.push(String(component.content));
216
+ if (component.codeBlock?.snippet)
217
+ parts.push(String(component.codeBlock.snippet));
218
+ if (component.button?.label)
219
+ parts.push(String(component.button.label));
220
+ if (component.layout?.title)
221
+ parts.push(String(component.layout.title));
222
+ if (component.agentChat?.title)
223
+ parts.push(String(component.agentChat.title));
224
+ if (Array.isArray(component.agentChat?.messages)) {
225
+ for (const msg of component.agentChat.messages) {
226
+ if (msg?.content)
227
+ parts.push(String(msg.content));
228
+ }
229
+ }
230
+ if (component.cliUsage?.title)
231
+ parts.push(String(component.cliUsage.title));
232
+ if (component.cliUsage?.command)
233
+ parts.push(String(component.cliUsage.command));
234
+ if (Array.isArray(component.cliUsage?.options)) {
235
+ for (const opt of component.cliUsage.options) {
236
+ if (opt?.flag)
237
+ parts.push(String(opt.flag));
238
+ if (opt?.description)
239
+ parts.push(String(opt.description));
240
+ }
241
+ }
242
+ if (component.marketing?.title)
243
+ parts.push(String(component.marketing.title));
244
+ if (component.marketing?.subtitle)
245
+ parts.push(String(component.marketing.subtitle));
246
+ if (Array.isArray(component.marketing?.items)) {
247
+ for (const item of component.marketing.items) {
248
+ if (item?.title)
249
+ parts.push(String(item.title));
250
+ if (item?.description)
251
+ parts.push(String(item.description));
252
+ if (item?.value)
253
+ parts.push(String(item.value));
254
+ if (item?.label)
255
+ parts.push(String(item.label));
256
+ }
257
+ }
258
+ if (Array.isArray(component.marketing?.actions)) {
259
+ for (const action of component.marketing.actions) {
260
+ if (action?.label)
261
+ parts.push(String(action.label));
262
+ }
263
+ }
264
+ return parts.join(" ");
265
+ }
266
+ function buildSearchIndex(ir) {
267
+ return (ir.pages ?? []).map((page) => {
268
+ const contentParts = [];
269
+ for (const comp of page.components ?? []) {
270
+ contentParts.push(collectComponentText(comp));
271
+ }
272
+ return {
273
+ title: page.name,
274
+ path: page.path,
275
+ description: page.description ?? "",
276
+ content: contentParts.join(" "),
277
+ };
278
+ });
279
+ }
280
+ function emitPwaAssets(outDir, ir) {
281
+ if (!ir.pwa?.enabled)
282
+ return;
283
+ const pwa = ir.pwa;
284
+ const manifest = {
285
+ name: pwa.name,
286
+ short_name: pwa.shortName,
287
+ description: pwa.description ?? `${ir.appName} PWA`,
288
+ start_url: pwa.startUrl,
289
+ scope: pwa.scope,
290
+ display: pwa.display,
291
+ background_color: pwa.backgroundColor,
292
+ theme_color: pwa.themeColor,
293
+ orientation: pwa.orientation,
294
+ icons: pwa.icons ?? [
295
+ { src: "/icons/icon.svg", sizes: "any", type: "image/svg+xml" },
296
+ ],
297
+ };
298
+ const publicDir = path.join(outDir, "public");
299
+ const iconsDir = path.join(publicDir, "icons");
300
+ ensureDir(iconsDir);
301
+ fs.writeFileSync(path.join(publicDir, "manifest.webmanifest"), JSON.stringify(manifest, null, 2), "utf-8");
302
+ const svgIcon = `
303
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
304
+ <defs>
305
+ <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
306
+ <stop offset="0%" style="stop-color:${pwa.themeColor};stop-opacity:1" />
307
+ <stop offset="100%" style="stop-color:${pwa.backgroundColor};stop-opacity:1" />
308
+ </linearGradient>
309
+ </defs>
310
+ <rect width="512" height="512" rx="64" fill="url(#grad)"/>
311
+ <text x="50%" y="55%" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="180" fill="#ffffff" font-weight="700">IR</text>
312
+ </svg>
313
+ `.trim();
314
+ fs.writeFileSync(path.join(iconsDir, "icon.svg"), svgIcon, "utf-8");
315
+ const sw = `
316
+ const CACHE_NAME = "irgen-pwa-v1";
317
+ const ASSETS = ["/", "/index.html", "/manifest.webmanifest"];
318
+
319
+ self.addEventListener("install", (event) => {
320
+ event.waitUntil(
321
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)).then(() => self.skipWaiting())
322
+ );
323
+ });
324
+
325
+ self.addEventListener("activate", (event) => {
326
+ event.waitUntil(
327
+ caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))).then(() => self.clients.claim())
328
+ );
329
+ });
330
+
331
+ self.addEventListener("fetch", (event) => {
332
+ if (event.request.method !== "GET") return;
333
+ event.respondWith(
334
+ caches.match(event.request).then((cached) => {
335
+ if (cached) return cached;
336
+ return fetch(event.request).then((resp) => {
337
+ const copy = resp.clone();
338
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy));
339
+ return resp;
340
+ }).catch(() => caches.match("/index.html"));
341
+ })
342
+ );
343
+ });
344
+ `.trim();
345
+ fs.writeFileSync(path.join(publicDir, "pwa-sw.js"), sw, "utf-8");
346
+ }
347
+ function emitViteConfig(project, outDir, policy) {
348
+ const mode = policy.framework.rendering.mode;
349
+ const isSsg = mode === "ssg" || mode === "hybrid";
350
+ const buildOutDir = policy.framework.rendering.prerender.outDir;
351
+ const config = `
352
+ import { defineConfig } from "vite";
353
+ import react from "@vitejs/plugin-react";
354
+
355
+ export default defineConfig({
356
+ base: "/",
357
+ plugins: [react()],
358
+ resolve: {
359
+ dedupe: ["react", "react-dom", "react-router", "react-router-dom"],
360
+ },
361
+ server: { port: 5173 },
362
+ preview: { port: 4173 },
363
+ publicDir: "public",${isSsg ? `
364
+ build: {
365
+ outDir: "${buildOutDir}",
366
+ manifest: true,
367
+ },
368
+ ssr: {
369
+ noExternal: ["react", "react-dom", "react-router", "react-router-dom"],
370
+ },` : ""}
371
+ });
372
+ `.trim();
373
+ project.createSourceFile(path.join(outDir, "vite.config.ts"), config, { overwrite: true });
374
+ }
375
+ export function emitFrontend(project, outDir, ir) {
376
+ const frontendDir = path.join(outDir, "src");
377
+ ensureDir(frontendDir);
378
+ const policy = ir.policies.frontend;
379
+ const mode = policy.framework.rendering.mode;
380
+ const isSsg = mode === "ssg" || mode === "hybrid";
381
+ const hasMarkdownCode = hasMarkdownCodeBlocks(ir);
382
+ const hasMermaid = hasMarkdownMermaid(ir);
383
+ const docsLinks = (ir.pages ?? [])
384
+ .filter((page) => page.docsLayout)
385
+ .map((page) => ({ name: page.name, path: page.path, groupLabel: page.docsGroupLabel }));
386
+ const docsGroupLabels = Array.from(new Set(docsLinks.map((link) => link.groupLabel).filter((label) => Boolean(label))));
387
+ const docsGroupLabel = docsGroupLabels.length === 1 ? docsGroupLabels[0] : "Docs";
388
+ const navbarLinks = (ir.pages ?? [])
389
+ .filter((page) => !page.docsLayout)
390
+ .map((page) => ({ name: page.name, path: page.path }));
391
+ if (docsLinks.length > 0) {
392
+ navbarLinks.push({ name: docsGroupLabel, path: docsLinks[0].path });
393
+ }
394
+ emitFrontendPackageJson(outDir, ir);
395
+ emitPwaAssets(outDir, ir);
396
+ emitViteConfig(project, outDir, policy);
397
+ emitSharedLogic(project, frontendDir);
398
+ emitRuntime(project, frontendDir, ir);
399
+ const searchIndex = buildSearchIndex(ir);
400
+ project.createSourceFile(path.join(frontendDir, "lib", "search-index.ts"), `export const SEARCH_INDEX = ${JSON.stringify(searchIndex, null, 2)} as const;\n`, { overwrite: true });
401
+ // client entry (CSR + optional hydration for hybrid)
402
+ const clientEntry = project.createSourceFile(path.join(frontendDir, "entry-client.tsx"), "", { overwrite: true });
403
+ clientEntry.addImportDeclaration({ moduleSpecifier: "react", defaultImport: "React" });
404
+ clientEntry.addImportDeclaration({
405
+ moduleSpecifier: "react-dom/client",
406
+ namedImports: mode === "hybrid" ? ["hydrateRoot", "createRoot"] : ["createRoot"],
407
+ });
408
+ clientEntry.addImportDeclaration({ moduleSpecifier: "react-router-dom", namedImports: ["BrowserRouter"] });
409
+ clientEntry.addImportDeclaration({ moduleSpecifier: "./index.css" });
410
+ clientEntry.addImportDeclaration({ moduleSpecifier: "./App", namedImports: ["App"] });
411
+ if (hasMarkdownCode) {
412
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/themes/prism.css" });
413
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-markup" });
414
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-markup-templating" });
415
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-clike" });
416
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-javascript" });
417
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-typescript" });
418
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-jsx" });
419
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-tsx" });
420
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-json" });
421
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-bash" });
422
+ clientEntry.addImportDeclaration({ moduleSpecifier: "prismjs/components/prism-css" });
423
+ }
424
+ const basePath = ir.basePath || policy.framework.rendering.basePath || "/";
425
+ const hasBasePath = basePath !== "/";
426
+ if (mode === "hybrid") {
427
+ clientEntry.addStatements(`
428
+ const rootElement = document.getElementById('root') as HTMLElement | null;
429
+ if (rootElement) {
430
+ const modeFlag = rootElement.dataset.irgenInteractive;
431
+ if (modeFlag === "false") {
432
+ // no hydrate for static-only pages
433
+ } else if (modeFlag === "csr" || !modeFlag) {
434
+ const root = createRoot(rootElement);
435
+ root.render(
436
+ <React.StrictMode>
437
+ <BrowserRouter${hasBasePath ? ` basename="${basePath}"` : ""}>
438
+ <App />
439
+ </BrowserRouter>
440
+ </React.StrictMode>
441
+ );
442
+ } else {
443
+ hydrateRoot(
444
+ rootElement,
445
+ <React.StrictMode>
446
+ <BrowserRouter${hasBasePath ? ` basename="${basePath}"` : ""}>
447
+ <App />
448
+ </BrowserRouter>
449
+ </React.StrictMode>
450
+ );
451
+ }
452
+ }
453
+ `.trim());
454
+ }
455
+ else {
456
+ clientEntry.addStatements(`
457
+ const root = createRoot(document.getElementById('root') as HTMLElement);
458
+ root.render(
459
+ <React.StrictMode>
460
+ <BrowserRouter${hasBasePath ? ` basename="${basePath}"` : ""}>
461
+ <App />
462
+ </BrowserRouter>
463
+ </React.StrictMode>
464
+ );
465
+ `.trim());
466
+ }
467
+ if (ir.pwa?.enabled) {
468
+ clientEntry.addStatements(`
469
+ if ('serviceWorker' in navigator) {
470
+ window.addEventListener('load', () => {
471
+ navigator.serviceWorker.register('/pwa-sw.js').catch(err => {
472
+ console.error('Service worker registration failed', err);
473
+ });
474
+ });
475
+ }
476
+ `.trim());
477
+ }
478
+ // compatibility entry for legacy tooling/tests
479
+ project.createSourceFile(path.join(frontendDir, "index.tsx"), `import "./entry-client";`, { overwrite: true });
480
+ if (isSsg) {
481
+ emitSsgSupport(project, outDir, frontendDir, ir);
482
+ }
483
+ // App.tsx
484
+ const appFile = project.createSourceFile(path.join(frontendDir, "App.tsx"), "", { overwrite: true });
485
+ const appReactImports = ["useEffect", "useMemo", "useState"];
486
+ appFile.addImportDeclaration({ moduleSpecifier: "react", namedImports: appReactImports });
487
+ const routerImports = ["Routes", "Route", "Link", "useLocation"];
488
+ appFile.addImportDeclaration({ moduleSpecifier: "react-router-dom", namedImports: routerImports });
489
+ appFile.addImportDeclaration({ moduleSpecifier: "lucide-react", namespaceImport: "Icons" });
490
+ if (hasMarkdownCode) {
491
+ appFile.addImportDeclaration({ moduleSpecifier: "prismjs", defaultImport: "Prism" });
492
+ }
493
+ if (hasMermaid) {
494
+ appFile.addImportDeclaration({ moduleSpecifier: "mermaid", defaultImport: "mermaid" });
495
+ }
496
+ appFile.addImportDeclaration({ moduleSpecifier: "./lib/search-index", namedImports: ["SEARCH_INDEX"] });
497
+ // Import all pages
498
+ ir.pages.forEach(p => {
499
+ appFile.addImportDeclaration({ moduleSpecifier: `./pages/${kebab(p.name)}`, namedImports: [`${pascal(p.name)}Page`] });
500
+ });
501
+ const appFn = appFile.addFunction({ name: "App", isExported: true });
502
+ appFn.setBodyText(writer => {
503
+ writer.writeLine("const [isDark, setIsDark] = useState(() => {");
504
+ writer.writeLine(" if (typeof window !== 'undefined') {");
505
+ writer.writeLine(" return document.documentElement.classList.contains('dark') || localStorage.getItem('theme') === 'dark';");
506
+ writer.writeLine(" }");
507
+ writer.writeLine(" return false;");
508
+ writer.writeLine("});");
509
+ writer.writeLine("const [searchOpen, setSearchOpen] = useState(false);");
510
+ writer.writeLine("const [searchQuery, setSearchQuery] = useState(\"\");");
511
+ writer.writeLine("const [tocItems, setTocItems] = useState([] as Array<{ id: string; text: string; level: number }>);");
512
+ writer.writeLine("const [activeToc, setActiveToc] = useState(\"\" as string);");
513
+ writer.writeLine("const location = useLocation();");
514
+ writer.writeLine(`const docsLinks = ${JSON.stringify(docsLinks, null, 2)};`);
515
+ writer.writeLine(`const defaultDocsGroupLabel = ${JSON.stringify(docsGroupLabel)};`);
516
+ writer.writeLine("const docsSidebarGroups = docsLinks.reduce((acc, link) => {");
517
+ writer.writeLine(" const label = link.groupLabel || defaultDocsGroupLabel;");
518
+ writer.writeLine(" let group = acc.find((g) => g.label === label);");
519
+ writer.writeLine(" if (!group) { group = { label, items: [] as typeof docsLinks }; acc.push(group); }");
520
+ writer.writeLine(" group.items.push(link);");
521
+ writer.writeLine(" return acc;");
522
+ writer.writeLine("}, [] as Array<{ label: string; items: typeof docsLinks }>)");
523
+ writer.writeLine("const docsPaths = docsLinks.map((link) => link.path);");
524
+ writer.writeLine("const isDocsRoute = docsPaths.includes(location.pathname);");
525
+ writer.writeLine("");
526
+ writer.writeLine("useEffect(() => {");
527
+ writer.writeLine(" if (isDark) {");
528
+ writer.writeLine(" document.documentElement.classList.add('dark');");
529
+ writer.writeLine(" localStorage.setItem('theme', 'dark');");
530
+ writer.writeLine(" } else {");
531
+ writer.writeLine(" document.documentElement.classList.remove('dark');");
532
+ writer.writeLine(" localStorage.setItem('theme', 'light');");
533
+ writer.writeLine(" }");
534
+ writer.writeLine("}, [isDark]);");
535
+ writer.writeLine("useEffect(() => {");
536
+ writer.writeLine(" const root = document.querySelector('[data-irgen-content]');");
537
+ writer.writeLine(" if (!root) { setTocItems([]); return; }");
538
+ writer.writeLine(" const headings = Array.from(root.querySelectorAll('h2, h3')) as HTMLElement[];");
539
+ writer.writeLine(" const next = headings.map((el) => ({ id: el.id, text: el.textContent || '', level: Number(el.tagName.replace('H','')) }));");
540
+ writer.writeLine(" setTocItems(next.filter((item) => item.id && item.text));");
541
+ writer.writeLine("}, [location.pathname]);");
542
+ writer.writeLine("");
543
+ writer.writeLine("useEffect(() => {");
544
+ writer.writeLine(" const root = document.querySelector('[data-irgen-content]');");
545
+ writer.writeLine(" if (!root) return;");
546
+ writer.writeLine(" const headings = Array.from(root.querySelectorAll('h2, h3')) as HTMLElement[];");
547
+ writer.writeLine(" if (!headings.length) return;");
548
+ writer.writeLine(" const observer = new IntersectionObserver((entries) => {");
549
+ writer.writeLine(" entries.forEach((entry) => {");
550
+ writer.writeLine(" if (entry.isIntersecting) {");
551
+ writer.writeLine(" setActiveToc(entry.target.id);");
552
+ writer.writeLine(" }");
553
+ writer.writeLine(" });");
554
+ writer.writeLine(" }, { rootMargin: '0px 0px -70% 0px', threshold: 0.1 });");
555
+ writer.writeLine(" headings.forEach((h) => observer.observe(h));");
556
+ writer.writeLine(" return () => observer.disconnect();");
557
+ writer.writeLine("}, [location.pathname, tocItems.length]);");
558
+ writer.writeLine("");
559
+ writer.writeLine("const searchResults = useMemo(() => {");
560
+ writer.writeLine(" const q = searchQuery.trim().toLowerCase();");
561
+ writer.writeLine(" if (!q) return [];");
562
+ writer.writeLine(" return SEARCH_INDEX.filter((item) => {");
563
+ writer.writeLine(" return (`${item.title} ${item.description} ${item.content}`.toLowerCase()).includes(q);");
564
+ writer.writeLine(" }).slice(0, 20);");
565
+ writer.writeLine("}, [searchQuery]);");
566
+ writer.writeLine("");
567
+ if (hasMarkdownCode) {
568
+ writer.writeLine("useEffect(() => {");
569
+ writer.writeLine(" Prism.highlightAll();");
570
+ writer.writeLine("}, [location.pathname]);");
571
+ }
572
+ if (hasMermaid) {
573
+ writer.writeLine("");
574
+ writer.writeLine("useEffect(() => {");
575
+ writer.writeLine(" mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'default' });");
576
+ writer.writeLine(" mermaid.run({ querySelector: '.mermaid' });");
577
+ writer.writeLine("}, [isDark, location.pathname]);");
578
+ }
579
+ writer.writeLine("");
580
+ writer.writeLine("return (");
581
+ writer.writeLine(" <div className=\"min-h-screen bg-slate-50/50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 font-sans selection:bg-slate-900 selection:text-white transition-colors duration-300\">");
582
+ writer.writeLine(" {/* Decorative background gradients */}");
583
+ writer.writeLine(" <div className=\"fixed inset-0 -z-10 pointer-events-none opacity-40\">");
584
+ writer.writeLine(" <div className=\"absolute top-0 left-1/4 w-96 h-96 bg-slate-200 rounded-full blur-3xl\"></div>");
585
+ writer.writeLine(" <div className=\"absolute bottom-0 right-1/4 w-96 h-96 bg-slate-100 rounded-full blur-3xl\"></div>");
586
+ writer.writeLine(" </div>");
587
+ writer.writeLine("");
588
+ writer.writeLine(" {/* Navigation Bar - Glassmorphism */}");
589
+ writer.writeLine(" <nav className=\"sticky top-0 z-50 bg-white/70 dark:bg-slate-900/70 backdrop-blur-xl border-b border-slate-200/60 dark:border-slate-800/60\">");
590
+ writer.writeLine(" <div className=\"max-w-7xl mx-auto px-6 lg:px-10\">");
591
+ writer.writeLine(" <div className=\"flex justify-between h-20\">");
592
+ writer.writeLine(" <div className=\"flex items-center gap-10\">");
593
+ writer.writeLine(" <div className=\"flex-shrink-0\">");
594
+ writer.writeLine(` <Link to=\"/\" className=\"group flex items-center gap-2\">`);
595
+ writer.writeLine(` <div className=\"w-10 h-10 rounded-xl flex items-center justify-center text-white shadow-lg shadow-slate-900/20 active:scale-95 transition-all\" style={{ backgroundColor: \"${ir.policies.frontend.styling.theme.primaryColor}\" }}>`);
596
+ writer.writeLine(` <Icons.Box size={24} />`);
597
+ writer.writeLine(` </div>`);
598
+ writer.writeLine(` <span className=\"font-black text-2xl tracking-tighter text-slate-900 dark:text-white\">${ir.appName}</span>`);
599
+ writer.writeLine(` </Link>`);
600
+ writer.writeLine(" </div>");
601
+ writer.writeLine(` <div className="hidden sm:flex items-center gap-1">`);
602
+ writer.writeLine(` {${JSON.stringify(navbarLinks)}.map((link) => (`);
603
+ writer.writeLine(` <Link key={link.path} to={link.path} className="px-4 py-2 rounded-lg text-sm font-bold text-slate-500 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100/50 dark:hover:bg-slate-800/50 transition-all">`);
604
+ writer.writeLine(` {link.name}`);
605
+ writer.writeLine(` </Link>`);
606
+ writer.writeLine(` ))}`);
607
+ writer.writeLine(" </div>");
608
+ writer.writeLine(" </div>");
609
+ writer.writeLine(" <div className=\"flex items-center gap-4\">");
610
+ writer.writeLine(" <button onClick={() => setSearchOpen(true)} className=\"p-2 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors\" aria-label=\"Search\"><Icons.Search size={20}/></button>");
611
+ writer.writeLine(" <button className=\"p-2 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors\"><Icons.Bell size={20}/></button>");
612
+ writer.writeLine(" <button ");
613
+ writer.writeLine(" onClick={() => setIsDark(!isDark)}");
614
+ writer.writeLine(" className=\"p-2 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-all active:scale-90\"");
615
+ writer.writeLine(" aria-label=\"Toggle theme\"");
616
+ writer.writeLine(" >");
617
+ writer.writeLine(" <div className=\"relative w-5 h-5\">");
618
+ writer.writeLine(" <Icons.Sun className=\"absolute inset-0 rotate-0 scale-100 dark:-rotate-90 dark:scale-0 transition-all text-amber-500\" size={20} />");
619
+ writer.writeLine(" <Icons.Moon className=\"absolute inset-0 rotate-90 scale-0 dark:rotate-0 dark:scale-100 transition-all text-indigo-400\" size={20} />");
620
+ writer.writeLine(" </div>");
621
+ writer.writeLine(" </button>");
622
+ writer.writeLine(" <div className=\"w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-800 border-2 border-white dark:border-slate-700 overflow-hidden shadow-sm\">");
623
+ writer.writeLine(" <img src=\"https://i.pravatar.cc/100?u=user\" alt=\"Avatar\" className=\"w-full h-full object-cover\" />");
624
+ writer.writeLine(" </div>");
625
+ writer.writeLine(" </div>");
626
+ writer.writeLine(" </div>");
627
+ writer.writeLine(" </div>");
628
+ writer.writeLine(" </nav>");
629
+ writer.writeLine("");
630
+ writer.writeLine(" {/* Content Area */}");
631
+ writer.writeLine(" <main className={isDocsRoute ? \"max-w-[1400px] mx-auto px-6 lg:px-10 py-10 md:py-12 animate-in fade-in duration-700\" : \"max-w-7xl mx-auto px-6 lg:px-10 py-12 md:py-20 animate-in fade-in duration-700\"}>");
632
+ writer.writeLine(" {isDocsRoute ? (");
633
+ writer.writeLine(" <div className={tocItems.length > 0 ? \"grid grid-cols-1 lg:grid-cols-[220px_minmax(0,1fr)_260px] gap-10\" : \"grid grid-cols-1 lg:grid-cols-[220px_minmax(0,1fr)] gap-10\"}>");
634
+ writer.writeLine(" <aside className=\"hidden lg:block\">");
635
+ writer.writeLine(" <div className=\"sticky top-28\">");
636
+ writer.writeLine(" <p className=\"text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-3\">Documentation</p>");
637
+ writer.writeLine(" <nav className=\"space-y-6 text-sm\">");
638
+ writer.writeLine(" {docsSidebarGroups.map((group) => (");
639
+ writer.writeLine(" <div key={group.label} className=\"space-y-2\">");
640
+ writer.writeLine(" <p className=\"text-[11px] font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500\">{group.label}</p>");
641
+ writer.writeLine(" <div className=\"space-y-0\">");
642
+ writer.writeLine(" {group.items.map((link) => (");
643
+ writer.writeLine(" <Link key={link.path} to={link.path} className={link.path === location.pathname ? \"block px-3 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white font-semibold\" : \"block px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100/60 dark:hover:bg-slate-800/60\"}>");
644
+ writer.writeLine(" {link.name}");
645
+ writer.writeLine(" </Link>");
646
+ writer.writeLine(" ))}");
647
+ writer.writeLine(" </div>");
648
+ writer.writeLine(" </div>");
649
+ writer.writeLine(" ))}");
650
+ writer.writeLine(" </nav>");
651
+ writer.writeLine(" </div>");
652
+ writer.writeLine(" </aside>");
653
+ writer.writeLine(" <div className=\"min-w-0\" data-irgen-content>");
654
+ writer.writeLine(" <Routes>");
655
+ ir.pages.forEach(p => {
656
+ writer.writeLine(` <Route path=\"${p.path}\" element={<${pascal(p.name)}Page />} />`);
657
+ });
658
+ if (ir.pages.length > 0) {
659
+ writer.writeLine(` <Route path=\"*\" element={<${pascal(ir.pages[0].name)}Page />} />`);
660
+ }
661
+ writer.writeLine(" </Routes>");
662
+ writer.writeLine(" </div>");
663
+ writer.writeLine(" {tocItems.length > 0 && (");
664
+ writer.writeLine(" <aside className=\"hidden lg:block\">");
665
+ writer.writeLine(" <div className=\"sticky top-28 space-y-3 text-sm\">");
666
+ writer.writeLine(" <p className=\"text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500\">On this page</p>");
667
+ writer.writeLine(" <ul className=\"space-y-2\">");
668
+ writer.writeLine(" {tocItems.map((item) => (");
669
+ writer.writeLine(" <li key={item.id} className={item.level === 3 ? \"pl-3\" : \"\"}>");
670
+ writer.writeLine(" <a href={`#${item.id}`} className={item.id === activeToc ? \"text-slate-900 dark:text-white font-semibold\" : \"text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white\"}>{item.text}</a>");
671
+ writer.writeLine(" </li>");
672
+ writer.writeLine(" ))}");
673
+ writer.writeLine(" </ul>");
674
+ writer.writeLine(" </div>");
675
+ writer.writeLine(" </aside>");
676
+ writer.writeLine(" )}");
677
+ writer.writeLine(" </div>");
678
+ writer.writeLine(" ) : (");
679
+ writer.writeLine(" <div className=\"min-w-0\" data-irgen-content>");
680
+ writer.writeLine(" <Routes>");
681
+ ir.pages.forEach(p => {
682
+ writer.writeLine(` <Route path=\"${p.path}\" element={<${pascal(p.name)}Page />} />`);
683
+ });
684
+ if (ir.pages.length > 0) {
685
+ writer.writeLine(` <Route path=\"*\" element={<${pascal(ir.pages[0].name)}Page />} />`);
686
+ }
687
+ writer.writeLine(" </Routes>");
688
+ writer.writeLine(" </div>");
689
+ writer.writeLine(" )}");
690
+ writer.writeLine(" </main>");
691
+ writer.writeLine("");
692
+ writer.writeLine(" {searchOpen && (");
693
+ writer.writeLine(" <div className=\"fixed inset-0 z-[60] bg-slate-900/50 backdrop-blur-sm flex items-start justify-center pt-24\" onClick={() => setSearchOpen(false)}>");
694
+ writer.writeLine(" <div className=\"bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-2xl w-full max-w-2xl p-6\" onClick={(e) => e.stopPropagation()}>");
695
+ writer.writeLine(" <div className=\"flex items-center gap-3 mb-4\">");
696
+ writer.writeLine(" <Icons.Search size={18} className=\"text-slate-400\" />");
697
+ writer.writeLine(" <input autoFocus value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder=\"Search docs...\" className=\"w-full bg-transparent outline-none text-slate-900 dark:text-white\" />");
698
+ writer.writeLine(" </div>");
699
+ writer.writeLine(" <div className=\"max-h-[420px] overflow-auto divide-y divide-slate-100 dark:divide-slate-800\">");
700
+ writer.writeLine(" {searchResults.length === 0 ? (");
701
+ writer.writeLine(" <p className=\"text-sm text-slate-500 dark:text-slate-400 py-6 text-center\">No results</p>");
702
+ writer.writeLine(" ) : (");
703
+ writer.writeLine(" searchResults.map((item) => (");
704
+ writer.writeLine(" <Link key={item.path} to={item.path} onClick={() => setSearchOpen(false)} className=\"block py-3 hover:bg-slate-50 dark:hover:bg-slate-800/40 px-2 rounded-lg\">");
705
+ writer.writeLine(" <p className=\"text-sm font-semibold text-slate-900 dark:text-white\">{item.title}</p>");
706
+ writer.writeLine(" {item.description && <p className=\"text-xs text-slate-500 dark:text-slate-400 mt-1\">{item.description}</p>}");
707
+ writer.writeLine(" </Link>");
708
+ writer.writeLine(" ))");
709
+ writer.writeLine(" )}");
710
+ writer.writeLine(" </div>");
711
+ writer.writeLine(" </div>");
712
+ writer.writeLine(" </div>");
713
+ writer.writeLine(" )}");
714
+ writer.writeLine("");
715
+ writer.writeLine(" {/* Footer */}");
716
+ writer.writeLine(" <footer className=\"border-t border-slate-200 dark:border-slate-800 mt-20 py-12 bg-white/30 dark:bg-slate-900/30 backdrop-blur-sm\">");
717
+ writer.writeLine(" <div className=\"max-w-7xl mx-auto px-6 lg:px-10 flex flex-col md:flex-row justify-between items-center gap-6\">");
718
+ writer.writeLine(` <div className=\"text-slate-400 dark:text-slate-500 text-sm font-medium\">© 2026 ${ir.appName}. Powered by <span className=\"font-bold text-slate-900 dark:text-white\">irgen</span></div>`);
719
+ writer.writeLine(" <div className=\"flex gap-8 text-slate-400 dark:text-slate-500 text-sm font-bold uppercase tracking-widest\">");
720
+ writer.writeLine(" <a href=\"#\" className=\"hover:text-slate-900 dark:hover:text-white transition-colors\">Terms</a>");
721
+ writer.writeLine(" <a href=\"#\" className=\"hover:text-slate-900 dark:hover:text-white transition-colors\">Privacy</a>");
722
+ writer.writeLine(" <a href=\"#\" className=\"hover:text-slate-900 dark:hover:text-white transition-colors\">Contact</a>");
723
+ writer.writeLine(" </div>");
724
+ writer.writeLine(" </div>");
725
+ writer.writeLine(" </footer>");
726
+ writer.writeLine(" </div>");
727
+ writer.writeLine(" );");
728
+ });
729
+ // index.html (SPA fallback / CSR entry)
730
+ project.createSourceFile(path.join(outDir, "index.html"), `
731
+ <!DOCTYPE html>
732
+ <html lang="en">
733
+ <head>
734
+ <meta charset="UTF-8" />
735
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
736
+ ${ir.pwa?.enabled ? `<link rel="manifest" href="/manifest.webmanifest" />` : ""}
737
+ ${ir.pwa?.enabled ? `<meta name="theme-color" content="${ir.pwa.themeColor}" />` : ""}
738
+ <title>${ir.appName}</title>
739
+ </head>
740
+ <body>
741
+ <div id="root" data-irgen-interactive="csr"></div>
742
+ <script type="module" src="/src/entry-client.tsx"></script>
743
+ </body>
744
+ </html>
745
+ `.trim(), { overwrite: true });
746
+ // TAILWIND SETUP
747
+ const cssPath = path.join(frontendDir, "index.css");
748
+ project.createSourceFile(cssPath, `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* Markdown prose styling */\n.prose { color: #0f172a; }\n.dark .prose { color: #e2e8f0; }\n.prose p { margin: 0.75rem 0; line-height: 1.75; }\n.prose h1, .prose h2, .prose h3, .prose h4 { font-weight: 700; color: inherit; margin: 1.25rem 0 0.5rem; }\n.prose h1 { font-size: 2rem; }\n.prose h2 { font-size: 1.5rem; }\n.prose h3 { font-size: 1.25rem; }\n.prose a { color: #2563eb; text-decoration: underline; text-underline-offset: 3px; }\n.dark .prose a { color: #93c5fd; }\n.prose ul, .prose ol { margin: 0.75rem 0 0.75rem 1.25rem; }\n.prose li { margin: 0.25rem 0; }\n.prose code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; background: rgba(15, 23, 42, 0.08); padding: 0.1rem 0.35rem; border-radius: 0.375rem; font-size: 0.85em; }\n.dark .prose code { background: rgba(148, 163, 184, 0.2); }\n.prose pre { background: #0f172a; color: #e2e8f0; padding: 1rem 1.25rem; border-radius: 0.75rem; overflow-x: auto; overflow-y: hidden; font-size: 0.85rem; line-height: 1.6; }\n.prose pre code { background: transparent; padding: 0; color: inherit; }\n`, { overwrite: true });
749
+ emitTailwindConfig(project, outDir);
750
+ // pages barrel
751
+ const pagesBarrel = project.createSourceFile(path.join(frontendDir, "pages.ts"), "", { overwrite: true });
752
+ pagesBarrel.addStatements([`// Re-exports for generated pages`]);
753
+ // components barrel
754
+ const compsBarrel = project.createSourceFile(path.join(frontendDir, "components.ts"), "", { overwrite: true });
755
+ compsBarrel.addStatements([`// Re-exports for generated components`]);
756
+ for (const p of ir.pages) {
757
+ emitPage(project, frontendDir, p);
758
+ pagesBarrel.addStatements([`export * from "./pages/${p.name.toLowerCase()}";`]);
759
+ }
760
+ for (const c of ir.components) {
761
+ emitComponent(project, frontendDir, c, ir);
762
+ compsBarrel.addStatements([`export * from "./components/${c.name.toLowerCase()}";`]);
763
+ }
764
+ }
765
+ function emitTailwindConfig(project, outDir) {
766
+ // We need to write these to the ROOT of output, not just frontend dir, usually.
767
+ // But let's put them in outDir which is where package.json lives.
768
+ // tailwind.config.js
769
+ project.createSourceFile(path.join(outDir, "tailwind.config.js"), `
770
+ /** @type {import('tailwindcss').Config} */
771
+ module.exports = {
772
+ content: [
773
+ "./frontend/**/*.{js,jsx,ts,tsx}",
774
+ "./src/**/*.{js,jsx,ts,tsx}",
775
+ ],
776
+ theme: {
777
+ extend: {},
778
+ },
779
+ darkMode: 'class',
780
+ plugins: [
781
+ require('@tailwindcss/forms'),
782
+ ],
783
+ }
784
+ `.trim(), { overwrite: true });
785
+ // postcss.config.js
786
+ project.createSourceFile(path.join(outDir, "postcss.config.js"), `
787
+ export default {
788
+ plugins: {
789
+ tailwindcss: {},
790
+ autoprefixer: {},
791
+ },
792
+ };
793
+ `.trim(), { overwrite: true });
794
+ }
795
+ function emitSharedLogic(project, srcDir) {
796
+ const libDir = path.join(srcDir, "lib");
797
+ ensureDir(libDir);
798
+ const filePath = path.join(libDir, "logic.ts");
799
+ const sf = project.createSourceFile(filePath, "", { overwrite: true });
800
+ sf.addStatements([
801
+ `export const getByPath = (obj: any, path?: string) => { if (!path) return undefined; return path.split(".").reduce((acc, key) => (acc && typeof acc === "object") ? acc[key] : undefined, obj); };`,
802
+ `export const isEmptyVal = (v: any): boolean => {
803
+ if (Array.isArray(v)) return v.length === 0;
804
+ if (typeof v === "object" && v !== null) { const vals = Object.values(v); return vals.length === 0 ? true : vals.every(isEmptyVal); }
805
+ if (typeof v === "boolean") return !v;
806
+ return (!v || v.toString().trim() === "");
807
+ };`,
808
+ `export const evalLogic = (logic: any, fallback?: any, logicCtx: any = {}): any => {
809
+ const evalNode = (node: any): any => {
810
+ if (node === undefined || node === null) return undefined;
811
+ if (typeof node === "string") {
812
+ const trimmed = node.trim();
813
+ try { const parsed = JSON.parse(trimmed); if (parsed && typeof parsed === "object") return evalNode(parsed); } catch (_) {}
814
+ const match = trimmed.match(/^([A-Za-z0-9_\\.]+)\\s*(==|===|!=|!==|>=|<=|>|<)\\s*(.+)$/);
815
+ if (match) {
816
+ const [, lhsKey, opSym, rhsRaw] = match;
817
+ const lhs = getByPath(logicCtx, lhsKey);
818
+ let rhs: any = rhsRaw;
819
+ if (rhsRaw === "true") rhs = true; else if (rhsRaw === "false") rhs = false; else if (!isNaN(Number(rhsRaw))) rhs = Number(rhsRaw); else rhs = rhsRaw.replace(/^['"]|['"]$/g, "");
820
+ switch (opSym) {
821
+ case "==": return lhs == rhs;
822
+ case "===": return lhs === rhs;
823
+ case "!=": return lhs != rhs;
824
+ case "!==": return lhs !== rhs;
825
+ case ">": return lhs > rhs;
826
+ case "<": return lhs < rhs;
827
+ case ">=": return lhs >= rhs;
828
+ case "<=": return lhs <= rhs;
829
+ }
830
+ }
831
+ return getByPath(logicCtx, trimmed) ?? trimmed;
832
+ }
833
+ if (Array.isArray(node)) return node.map(evalNode);
834
+ if (typeof node !== "object") return node;
835
+ const entries = Object.entries(node); if (entries.length === 0) return undefined;
836
+ const [op, valRaw] = entries[0];
837
+ const list = Array.isArray(valRaw) ? valRaw : [valRaw];
838
+ const values = list.map(evalNode);
839
+ switch (op) {
840
+ case "var": return getByPath(logicCtx, values[0]);
841
+ case "==": return values[0] == values[1];
842
+ case "===": return values[0] === values[1];
843
+ case "!=": return values[0] != values[1];
844
+ case "!==": return values[0] !== values[1];
845
+ case ">": return values[0] > values[1];
846
+ case "<": return values[0] < values[1];
847
+ case ">=": return values[0] >= values[1];
848
+ case "<=": return values[0] <= values[1];
849
+ case "and": return values.every(Boolean);
850
+ case "or": return values.some(Boolean);
851
+ case "!": return !values[0];
852
+ case "!!": return !!values[0];
853
+ case "if": return values[0] ? values[1] : values[2];
854
+ case "in": return Array.isArray(values[1]) ? values[1].includes(values[0]) : false;
855
+ case "+": return values.reduce((a,b) => (Number(a) || 0) + (Number(b) || 0), 0);
856
+ case "-": return values.length === 1 ? -(Number(values[0]) || 0) : (Number(values[0]) || 0) - (Number(values[1]) || 0);
857
+ case "*": return values.reduce((a,b) => (Number(a) || 0) * (Number(b) || 0), 1);
858
+ case "/": return values.length === 1 ? (Number(values[0]) || 0) : (Number(values[1]) ? (Number(values[0]) || 0) / (Number(values[1]) || 1) : undefined);
859
+ case "%": return values.length === 1 ? Number(values[0]) % 1 : (Number(values[0]) || 0) % (Number(values[1]) || 1);
860
+ default: return undefined;
861
+ }
862
+ };
863
+ const res = evalNode(logic);
864
+ return (typeof res === "undefined") ? fallback : res;
865
+ };`
866
+ ]);
867
+ }
868
+ // Register frontend emitter with the engine
869
+ try {
870
+ emitterEngine.registerEmitter("frontend-tsmorph", async (ir, outDir) => {
871
+ const project = new Project({
872
+ useInMemoryFileSystem: false,
873
+ manipulationSettings: {
874
+ quoteKind: QuoteKind.Double,
875
+ indentationText: IndentationText.TwoSpaces,
876
+ },
877
+ compilerOptions: { target: ScriptTarget.ES2022 },
878
+ });
879
+ // create/ensure out dir
880
+ fs.mkdirSync(outDir, { recursive: true });
881
+ emitFrontend(project, outDir, ir);
882
+ project.saveSync();
883
+ }, { force: true });
884
+ }
885
+ catch (e) {
886
+ // ignore double registration
887
+ }
888
+ // register default target mapping
889
+ try {
890
+ registerTargetEmitter("frontend", "frontend-tsmorph", { force: true });
891
+ }
892
+ catch (e) {
893
+ // ignore
894
+ }
895
+ function emitPage(project, frontendDir, page) {
896
+ const dir = path.join(frontendDir, "pages");
897
+ project.createDirectory(dir);
898
+ const filePath = path.join(dir, `${kebab(page.name)}.tsx`);
899
+ const sf = project.createSourceFile(filePath, "", { overwrite: true });
900
+ sf.addImportDeclaration({ moduleSpecifier: "react", defaultImport: "React" });
901
+ sf.addImportDeclaration({ moduleSpecifier: "react", namedImports: ["useEffect", "useState"] });
902
+ // import referenced components
903
+ for (const c of page.components) {
904
+ sf.addImportDeclaration({ moduleSpecifier: `../components/${kebab(c.name)}`, namedImports: [pascal(c.name)] });
905
+ }
906
+ const compName = `${pascal(page.name)}Page`;
907
+ const fn = sf.addFunction({ name: compName, isExported: true });
908
+ fn.setBodyText((writer) => {
909
+ const sectionGap = page.docsLayout ? "space-y-6" : "space-y-12";
910
+ const gridGap = page.docsLayout ? "gap-6" : "gap-12";
911
+ writer.writeLine("return (");
912
+ writer.writeLine(` <div className="${sectionGap} animate-in fade-in slide-in-from-bottom-4 duration-500">`);
913
+ if (!page.hideHeader) {
914
+ writer.writeLine(" <header className=\"border-b border-slate-200 dark:border-slate-800 pb-10\">");
915
+ writer.writeLine(` <div className=\"flex items-center gap-4 text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.3em] mb-4\">`);
916
+ writer.writeLine(` <div className=\"w-10 h-[1px] bg-slate-200 dark:bg-slate-800\"></div>`);
917
+ writer.writeLine(` <span>Resource: ${page.name}</span>`);
918
+ writer.writeLine(` </div>`);
919
+ writer.writeLine(` <h1 className=\"text-5xl md:text-6xl font-black text-slate-950 dark:text-white tracking-tighter\">${page.name}</h1>`);
920
+ const description = page.description || `Manage your ${page.name.toLowerCase()} assets and application state in this unified view.`;
921
+ writer.writeLine(` <p className=\"mt-4 text-slate-500 dark:text-slate-400 text-lg max-w-3xl leading-relaxed font-medium\">${description}</p>`);
922
+ writer.writeLine(" </header>");
923
+ }
924
+ writer.writeLine(` <div className="grid ${gridGap}">`);
925
+ for (const c of page.components) {
926
+ const sectionClass = page.docsLayout
927
+ ? "relative shrink-0 overflow-x-hidden"
928
+ : "relative shrink-0";
929
+ writer.writeLine(` <section className=\"${sectionClass}\">`);
930
+ writer.writeLine(` <${pascal(c.name)} />`);
931
+ writer.writeLine(` </section>`);
932
+ }
933
+ writer.writeLine(" </div>");
934
+ writer.writeLine(" </div>");
935
+ writer.writeLine(");");
936
+ });
937
+ }
938
+ function emitComponent(project, frontendDir, component, ir) {
939
+ const dir = path.join(frontendDir, "components");
940
+ project.createDirectory(dir);
941
+ const filePath = path.join(dir, `${kebab(component.name)}.tsx`);
942
+ const sf = project.createSourceFile(filePath, "", { overwrite: true });
943
+ const hasIpcButton = Boolean(component.props && component.props["ipcChannel"]);
944
+ const needsHooks = !!component.themeToggle || !!(component.form && component.form.fields && component.form.fields.length > 0) || (component.layout?.kind === "tabs") || hasIpcButton || !!component.table;
945
+ if (needsHooks) {
946
+ sf.addImportDeclaration({ moduleSpecifier: "react", defaultImport: "React", namedImports: ["useEffect", "useState"] });
947
+ }
948
+ else {
949
+ sf.addImportDeclaration({ moduleSpecifier: "react", defaultImport: "React" });
950
+ }
951
+ sf.addImportDeclaration({ moduleSpecifier: "lucide-react", namespaceImport: "Icons" });
952
+ sf.addImportDeclaration({ moduleSpecifier: "../lib/logic", namedImports: ["evalLogic", "getByPath", "isEmptyVal"] });
953
+ sf.addImportDeclaration({ moduleSpecifier: "../lib/hooks", namedImports: ["useOperation", "useResource"] });
954
+ if (component.codeBlock) {
955
+ sf.addImportDeclaration({
956
+ moduleSpecifier: "react-syntax-highlighter",
957
+ namedImports: ["Prism as SyntaxHighlighter"],
958
+ });
959
+ sf.addImportDeclaration({
960
+ moduleSpecifier: "react-syntax-highlighter/dist/esm/styles/prism",
961
+ namedImports: ["oneDark"],
962
+ });
963
+ }
964
+ // Import layout child components if any
965
+ // Import layout child components if any and valid identifier
966
+ if (component.layout) {
967
+ const childNames = new Set();
968
+ if (component.layout.items)
969
+ component.layout.items.forEach((c) => childNames.add(c));
970
+ component.layout.tabs?.forEach((t) => t.items?.forEach((c) => childNames.add(c)));
971
+ const isValidIdent = (name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
972
+ for (const childName of childNames) {
973
+ if (childName === component.name)
974
+ continue; // avoid self-import
975
+ if (!isValidIdent(childName))
976
+ continue; // skip placeholder labels
977
+ const safeChildName = pascal(childName);
978
+ sf.addImportDeclaration({
979
+ moduleSpecifier: `./${kebab(childName)}`,
980
+ namedImports: [safeChildName],
981
+ });
982
+ }
983
+ }
984
+ const compName = `${pascal(component.name)}`;
985
+ const fn = sf.addFunction({ name: compName, isExported: true });
986
+ fn.setBodyText((writer) => {
987
+ // Utility classes
988
+ // Utility classes - Modern & Premium
989
+ const primaryColor = ir.policies.frontend.styling.theme.primaryColor || "#000000";
990
+ const labelClass = "block text-sm font-semibold text-slate-900 dark:text-slate-200 mb-1.5";
991
+ const inputClass = `mt-1 block w-full rounded-lg border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 shadow-sm transition-all duration-200 focus:border-slate-900 dark:focus:border-slate-100 focus:bg-white dark:focus:bg-slate-900 focus:ring-2 focus:ring-slate-900/10 dark:focus:ring-white/10 sm:text-sm dark:text-slate-100`;
992
+ const checkboxClass = "h-4 w-4 rounded border-slate-300 dark:border-slate-700 text-slate-900 dark:text-white focus:ring-slate-900 bg-white dark:bg-slate-900";
993
+ const radioClass = "h-4 w-4 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-white focus:ring-slate-900 bg-white dark:bg-slate-900";
994
+ const btnClass = `inline-flex items-center justify-center rounded-lg border border-transparent py-2.5 px-5 text-sm font-semibold text-white shadow-xl transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-offset-2`;
995
+ const errorClass = "mt-2 text-xs font-medium text-red-500 dark:text-red-400 flex items-center gap-1";
996
+ const formClass = "space-y-8 bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-2xl shadow-slate-200/50 dark:shadow-none px-6 py-8 sm:rounded-2xl";
997
+ // Theme Toggle component
998
+ if (component.themeToggle) {
999
+ writer.writeLine(`const [isDark, setIsDark] = useState(() => {`);
1000
+ writer.writeLine(` if (typeof window !== 'undefined') {`);
1001
+ writer.writeLine(` return document.documentElement.classList.contains('dark') || localStorage.getItem('theme') === 'dark';`);
1002
+ writer.writeLine(` }`);
1003
+ writer.writeLine(` return false;`);
1004
+ writer.writeLine(`});`);
1005
+ writer.writeLine("");
1006
+ writer.writeLine(`useEffect(() => {`);
1007
+ writer.writeLine(` if (isDark) {`);
1008
+ writer.writeLine(` document.documentElement.classList.add('dark');`);
1009
+ writer.writeLine(` localStorage.setItem('theme', 'dark');`);
1010
+ writer.writeLine(` } else {`);
1011
+ writer.writeLine(` document.documentElement.classList.remove('dark');`);
1012
+ writer.writeLine(` localStorage.setItem('theme', 'light');`);
1013
+ writer.writeLine(` }`);
1014
+ writer.writeLine(`}, [isDark]);`);
1015
+ writer.writeLine("");
1016
+ writer.writeLine(`return (`);
1017
+ writer.writeLine(` <button `);
1018
+ writer.writeLine(` onClick={() => setIsDark(!isDark)} `);
1019
+ writer.writeLine(` className="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-md transition-all active:scale-95 group" `);
1020
+ writer.writeLine(` aria-label="Toggle dark mode"`);
1021
+ writer.writeLine(` >`);
1022
+ writer.writeLine(` <div className="relative w-5 h-5">`);
1023
+ writer.writeLine(` <Icons.Sun className="absolute inset-0 rotate-0 scale-100 dark:-rotate-90 dark:scale-0 transition-all text-amber-500" size={20} />`);
1024
+ writer.writeLine(` <Icons.Moon className="absolute inset-0 rotate-90 scale-0 dark:rotate-0 dark:scale-100 transition-all text-indigo-400" size={20} />`);
1025
+ writer.writeLine(` </div>`);
1026
+ writer.writeLine(` </button>`);
1027
+ writer.writeLine(`);`);
1028
+ return;
1029
+ }
1030
+ if (component.codeBlock) {
1031
+ const { snippet, language, showLineNumbers } = component.codeBlock;
1032
+ writer.writeLine(`const codeBlock = (`);
1033
+ writer.writeLine(` <div className="rounded-xl overflow-x-auto overflow-y-hidden border border-slate-200 dark:border-slate-800 shadow-sm">`);
1034
+ writer.writeLine(` <SyntaxHighlighter `);
1035
+ writer.writeLine(` language="${language}" `);
1036
+ writer.writeLine(` style={oneDark} `);
1037
+ writer.writeLine(` showLineNumbers={${showLineNumbers}}`);
1038
+ writer.writeLine(` customStyle={{ margin: 0, padding: '1.5rem', fontSize: '0.875rem' }}`);
1039
+ writer.writeLine(` >`);
1040
+ writer.writeLine(` {\`${snippet.replace(/`/g, "\\`").replace(/\${/g, "\\${")}\`}`);
1041
+ writer.writeLine(` </SyntaxHighlighter>`);
1042
+ writer.writeLine(` </div>`);
1043
+ writer.writeLine(`);`);
1044
+ }
1045
+ const hasInlineContent = !!(component.content || component.codeBlock || component.button);
1046
+ // Marketing components
1047
+ if (component.marketing) {
1048
+ writer.writeLine(`return (`);
1049
+ writer.writeLine(` <>`);
1050
+ emitMarketingComponent(writer, component.marketing, ir.policies.frontend);
1051
+ writer.writeLine(` </>`);
1052
+ writer.writeLine(`);`);
1053
+ return;
1054
+ }
1055
+ if (component.agentChat) {
1056
+ const title = component.agentChat.title ?? "AI Copilot Integration";
1057
+ const messages = component.agentChat.messages ?? [];
1058
+ writer.writeLine(`const messages = ${JSON.stringify(messages)};`);
1059
+ writer.writeLine(`return (`);
1060
+ writer.writeLine(` <div className="max-w-2xl mx-auto rounded-3xl border border-slate-100 dark:border-slate-800 bg-white/70 dark:bg-slate-900/50 backdrop-blur-xl p-8 shadow-2xl space-y-6">`);
1061
+ writer.writeLine(` <p className="text-xs font-bold text-slate-400 uppercase tracking-widest text-center">{${JSON.stringify(title)}}</p>`);
1062
+ writer.writeLine(` <div className="space-y-6">`);
1063
+ writer.writeLine(` {messages.map((msg: any, idx: number) => (`);
1064
+ writer.writeLine(` <div key={idx} className="flex gap-4">`);
1065
+ writer.writeLine(` <div className={\`h-10 w-10 shrink-0 rounded-full flex items-center justify-center text-sm font-bold text-white \${msg.role === 'agent' ? 'bg-sky-500' : 'bg-slate-900'}\`}>{msg.label ?? (msg.role === 'agent' ? 'A' : 'U')}</div>`);
1066
+ writer.writeLine(` <div className={\`flex-1 rounded-2xl p-4 text-sm shadow-sm whitespace-pre-line \${msg.role === 'agent' ? 'bg-sky-50 dark:bg-sky-900/20 text-sky-900 dark:text-sky-100 border border-sky-100/50 dark:border-sky-500/10' : 'bg-slate-50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-300'}\`}>`);
1067
+ writer.writeLine(` {msg.content}`);
1068
+ writer.writeLine(` </div>`);
1069
+ writer.writeLine(` </div>`);
1070
+ writer.writeLine(` ))}`);
1071
+ writer.writeLine(` </div>`);
1072
+ writer.writeLine(` </div>`);
1073
+ writer.writeLine(`);`);
1074
+ return;
1075
+ }
1076
+ if (component.cliUsage) {
1077
+ const title = component.cliUsage.title ?? "Standard Usage";
1078
+ const command = component.cliUsage.command ?? "";
1079
+ const options = component.cliUsage.options ?? [];
1080
+ writer.writeLine(`const options = ${JSON.stringify(options)};`);
1081
+ writer.writeLine(`return (`);
1082
+ writer.writeLine(` <div className="max-w-3xl mx-auto space-y-6">`);
1083
+ writer.writeLine(` <h3 className="text-2xl font-bold text-slate-900 dark:text-white">{${JSON.stringify(title)}}</h3>`);
1084
+ writer.writeLine(` <div className="bg-slate-900 rounded-xl p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">{${JSON.stringify(command)}}</div>`);
1085
+ writer.writeLine(` {options.length > 0 && (`);
1086
+ writer.writeLine(` <div className="grid gap-4 mt-8">`);
1087
+ writer.writeLine(` {options.map((opt: any, idx: number) => (`);
1088
+ writer.writeLine(` <div key={idx} className="p-6 border border-slate-100 dark:border-slate-800 rounded-2xl">`);
1089
+ writer.writeLine(` <h4 className="font-bold mb-2">{opt.flag}</h4>`);
1090
+ writer.writeLine(` <p className="text-sm text-slate-500 italic">{opt.description}</p>`);
1091
+ writer.writeLine(` </div>`);
1092
+ writer.writeLine(` ))}`);
1093
+ writer.writeLine(` </div>`);
1094
+ writer.writeLine(` )}`);
1095
+ writer.writeLine(` </div>`);
1096
+ writer.writeLine(`);`);
1097
+ return;
1098
+ }
1099
+ // Layout components (real child components)
1100
+ if (hasIpcButton) {
1101
+ const channel = component.props["ipcChannel"];
1102
+ const title = component.props["title"] ?? "IPC Demo";
1103
+ const description = component.props["description"] ?? "Invoke IPC channel from renderer";
1104
+ writer.writeLine(`const [result, setResult] = useState<string | null>(null);`);
1105
+ writer.writeLine(`const [error, setError] = useState<string | null>(null);`);
1106
+ writer.writeLine(`const handleClick = async () => {`);
1107
+ writer.writeLine(` try {`);
1108
+ writer.writeLine(` // @ts-ignore - bridge injected by Electron preload`);
1109
+ writer.writeLine(` const api = (window as any).api;`);
1110
+ writer.writeLine(` if (!api?.invoke) { setError("IPC bridge unavailable"); return; }`);
1111
+ writer.writeLine(` const res = await api.invoke("${channel}");`);
1112
+ writer.writeLine(` setResult(res ?? "No selection");`);
1113
+ writer.writeLine(` setError(null);`);
1114
+ writer.writeLine(` } catch (err:any) {`);
1115
+ writer.writeLine(` setError(err?.message ?? String(err));`);
1116
+ writer.writeLine(` }`);
1117
+ writer.writeLine(`};`);
1118
+ writer.writeLine(`return (`);
1119
+ writer.writeLine(` <div className="bg-slate-50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-2xl p-6 space-y-4 shadow-sm">`);
1120
+ writer.writeLine(` <div className="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4">`);
1121
+ writer.writeLine(` <div>`);
1122
+ writer.writeLine(` <h3 className="text-lg font-bold text-slate-900 dark:text-white flex items-center gap-2">`);
1123
+ writer.writeLine(` <Icons.Activity size={20} className="text-slate-400 dark:text-slate-500" />`);
1124
+ writer.writeLine(` {${JSON.stringify(title)}}`);
1125
+ writer.writeLine(` </h3>`);
1126
+ writer.writeLine(` <p className="text-slate-500 dark:text-slate-400 text-sm mt-0.5">{${JSON.stringify(description)}}</p>`);
1127
+ writer.writeLine(` </div>`);
1128
+ writer.writeLine(` <div className="px-2 py-1 rounded bg-slate-900/5 dark:bg-white/5 text-[10px] font-mono text-slate-500 dark:text-slate-400 uppercase tracking-widest border border-slate-950/5 dark:border-white/5">Bridge: ${channel}</div>`);
1129
+ writer.writeLine(` </div>`);
1130
+ writer.writeLine(` <button onClick={handleClick} className="${btnClass} w-full sm:w-auto" style={{ backgroundColor: "${primaryColor}" }}>`);
1131
+ writer.writeLine(` <Icons.Cpu size={16} className="mr-2" />`);
1132
+ writer.writeLine(` Invoke IPC Hook`);
1133
+ writer.writeLine(` </button>`);
1134
+ writer.writeLine(` {(result || error) && (`);
1135
+ writer.writeLine(` <div className={\`rounded-xl p-4 font-mono text-xs border \${error ? 'bg-red-50 border-red-100 text-red-600' : 'bg-slate-900 text-slate-300 border-white/10 shadow-inner'}\`}>`);
1136
+ writer.writeLine(` <div className="flex items-center gap-2 mb-2 opacity-50">`);
1137
+ writer.writeLine(` <div className={\`w-2 h-2 rounded-full \${error ? 'bg-red-500' : 'bg-green-500 animate-pulse'}\`}></div>`);
1138
+ writer.writeLine(` <span>\${error ? 'EXECUTION ERROR' : 'TERMINAL OUTPUT'}</span>`);
1139
+ writer.writeLine(` </div>`);
1140
+ writer.writeLine(` {error ? error : String(result)}`);
1141
+ writer.writeLine(` </div>`);
1142
+ writer.writeLine(` )}`);
1143
+ writer.writeLine(` </div>`);
1144
+ writer.writeLine(`);`);
1145
+ return;
1146
+ }
1147
+ if (component.layout) {
1148
+ const kind = component.layout.kind;
1149
+ if (kind === "tabs") {
1150
+ writer.writeLine(`const [active, setActive] = useState(0);`);
1151
+ writer.writeLine(`const tabs = [`);
1152
+ for (const t of component.layout.tabs ?? []) {
1153
+ const validItems = (t.items ?? []).filter((n) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(n));
1154
+ const tabItems = validItems.map((n) => pascal(n));
1155
+ writer.writeLine(` { label: "${t.label}", content: ${JSON.stringify(t.content ?? "")}, items: [${tabItems.join(", ")}] },`);
1156
+ }
1157
+ writer.writeLine(`];`);
1158
+ writer.writeLine(`return (`);
1159
+ writer.writeLine(` <div className=\"bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm\">`);
1160
+ if (component.layout.title) {
1161
+ const titleId = slugifyHeading(component.layout.title);
1162
+ writer.writeLine(` <div className=\"px-5 py-4 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50\"><h3 className=\"text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider\"${titleId ? ` id="${titleId}"` : ""}>{${JSON.stringify(component.layout.title)}}</h3></div>`);
1163
+ }
1164
+ writer.writeLine(` <div className=\"p-2 flex gap-1 bg-slate-100/50 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800\">`);
1165
+ writer.writeLine(` {tabs.map((t:any, idx:number) => (`);
1166
+ writer.writeLine(` <button key={idx} onClick={() => setActive(idx)} className={\`flex-1 px-4 py-2 text-sm font-semibold rounded-lg transition-all \${active === idx ? 'bg-white dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm' : 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'}\`}>{t.label}</button>`);
1167
+ writer.writeLine(` ))}`);
1168
+ writer.writeLine(` </div>`);
1169
+ writer.writeLine(` <div className=\"p-6\">`);
1170
+ writer.writeLine(` {tabs[active] ? (`);
1171
+ writer.writeLine(` <div className=\"space-y-4\">`);
1172
+ writer.writeLine(` {tabs[active].content && <p className=\"text-slate-600 dark:text-slate-400 leading-relaxed\">{tabs[active].content}</p>}`);
1173
+ writer.writeLine(` {tabs[active].items && tabs[active].items.length > 0 ? (`);
1174
+ writer.writeLine(` <div className=\"grid gap-4\">`);
1175
+ writer.writeLine(` {tabs[active].items.map((Comp: any, idx: number) => <div key={idx}><Comp /></div>)}`);
1176
+ writer.writeLine(` </div>`);
1177
+ writer.writeLine(` ) : (!tabs[active].content && <div className=\"text-center py-8 text-slate-400 dark:text-slate-500 text-sm italic border border-dashed border-slate-200 dark:border-slate-800 rounded-xl\">Empty tab</div>)}`);
1178
+ writer.writeLine(` </div>`);
1179
+ writer.writeLine(` ) : <p className=\"text-slate-400 dark:text-slate-500 text-sm\">No content.</p>}`);
1180
+ writer.writeLine(` </div>`);
1181
+ writer.writeLine(` </div>`);
1182
+ writer.writeLine(`);`);
1183
+ return;
1184
+ }
1185
+ else if (kind === "panel") {
1186
+ const docsVariant = component.props?.docsLayout === "true";
1187
+ if (docsVariant) {
1188
+ writer.writeLine(`return (`);
1189
+ writer.writeLine(` <div className="space-y-4">`);
1190
+ if (component.layout.title) {
1191
+ const titleId = slugifyHeading(component.layout.title);
1192
+ writer.writeLine(` <h3 className="text-xl font-semibold text-slate-900 dark:text-white"${titleId ? ` id="${titleId}"` : ""}>{${JSON.stringify(component.layout.title)}}</h3>`);
1193
+ }
1194
+ if (component.content || component.codeBlock || component.button) {
1195
+ writer.writeLine(` <div className="space-y-4">`);
1196
+ if (component.content)
1197
+ writer.writeLine(` <div className="prose dark:prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(renderMarkdownToHtml(component.content))} }} />`);
1198
+ if (component.codeBlock)
1199
+ writer.writeLine(` {codeBlock}`);
1200
+ if (component.button) {
1201
+ const variant = component.button.label ? (component.button.variant ?? "primary") : "primary";
1202
+ const baseBtn = "inline-flex items-center justify-center px-6 py-2.5 rounded-xl text-sm font-bold transition-all active:scale-95 shadow-lg shadow-slate-900/5";
1203
+ const variantClass = variant === "secondary"
1204
+ ? "bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700"
1205
+ : (variant === "ghost"
1206
+ ? "bg-transparent text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
1207
+ : "text-white hover:opacity-90");
1208
+ const style = variant === "primary" ? { backgroundColor: ir.policies.frontend.styling.theme.primaryColor } : {};
1209
+ writer.writeLine(` <button className="${baseBtn} ${variantClass}" style={${JSON.stringify(style)}} onClick={() => { /* TODO: wire action */ }}>`);
1210
+ if (component.button.icon) {
1211
+ writer.writeLine(` {(Icons as any)["${component.button.icon}"] && React.createElement((Icons as any)["${component.button.icon}"], { size: 16, className: "mr-2" })}`);
1212
+ }
1213
+ writer.writeLine(` ${component.button.label}`);
1214
+ writer.writeLine(` </button>`);
1215
+ }
1216
+ writer.writeLine(` </div>`);
1217
+ }
1218
+ if (component.layout.items?.length) {
1219
+ writer.writeLine(` <div className="space-y-4">`);
1220
+ for (const item of component.layout.items ?? []) {
1221
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(item)) {
1222
+ writer.writeLine(` <${pascal(item)} />`);
1223
+ }
1224
+ else {
1225
+ writer.writeLine(` <div className="text-slate-400 dark:text-slate-500 text-sm italic">Placeholder: ${item}</div>`);
1226
+ }
1227
+ }
1228
+ writer.writeLine(` </div>`);
1229
+ }
1230
+ writer.writeLine(` </div>`);
1231
+ writer.writeLine(`);`);
1232
+ return;
1233
+ }
1234
+ writer.writeLine(`return (`);
1235
+ writer.writeLine(` <div className=\"bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-xl shadow-slate-200/50 dark:shadow-none rounded-2xl overflow-hidden px-1 py-1\">`);
1236
+ if (component.layout.title) {
1237
+ const titleId = slugifyHeading(component.layout.title);
1238
+ writer.writeLine(` <h3 className=\"px-5 py-4 text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-widest border-b border-slate-50 dark:border-slate-800 bg-slate-50/30 dark:bg-slate-900/30\"${titleId ? ` id="${titleId}"` : ""}>{${JSON.stringify(component.layout.title)}}</h3>`);
1239
+ }
1240
+ writer.writeLine(` <div className=\"p-5 space-y-6\">`);
1241
+ if (component.content || component.codeBlock || component.button) {
1242
+ writer.writeLine(` <div className="space-y-4">`);
1243
+ if (component.content)
1244
+ writer.writeLine(` <div className="prose dark:prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(renderMarkdownToHtml(component.content))} }} />`);
1245
+ if (component.codeBlock)
1246
+ writer.writeLine(` {codeBlock}`);
1247
+ if (component.button) {
1248
+ const variant = component.button.label ? (component.button.variant ?? "primary") : "primary";
1249
+ const baseBtn = "inline-flex items-center justify-center px-6 py-2.5 rounded-xl text-sm font-bold transition-all active:scale-95 shadow-lg shadow-slate-900/5";
1250
+ const variantClass = variant === "secondary"
1251
+ ? "bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700"
1252
+ : (variant === "ghost"
1253
+ ? "bg-transparent text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
1254
+ : "text-white hover:opacity-90");
1255
+ const style = variant === "primary" ? { backgroundColor: ir.policies.frontend.styling.theme.primaryColor } : {};
1256
+ writer.writeLine(` <button className="${baseBtn} ${variantClass}" style={${JSON.stringify(style)}} onClick={() => { /* TODO: wire action */ }}>`);
1257
+ if (component.button.icon) {
1258
+ writer.writeLine(` {(Icons as any)["${component.button.icon}"] && React.createElement((Icons as any)["${component.button.icon}"], { size: 16, className: "mr-2" })}`);
1259
+ }
1260
+ writer.writeLine(` ${component.button.label}`);
1261
+ writer.writeLine(` </button>`);
1262
+ }
1263
+ writer.writeLine(` </div>`);
1264
+ }
1265
+ if (component.layout.items?.length) {
1266
+ for (const item of component.layout.items ?? []) {
1267
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(item)) {
1268
+ writer.writeLine(` <${pascal(item)} />`);
1269
+ }
1270
+ else {
1271
+ writer.writeLine(` <div className=\"p-10 text-center border-2 border-dashed border-slate-100 dark:border-slate-800 rounded-2xl text-slate-300 dark:text-slate-700 text-xs font-medium uppercase tracking-tighter italic\">Placeholder: ${item}</div>`);
1272
+ }
1273
+ }
1274
+ }
1275
+ else if (!hasInlineContent) {
1276
+ writer.writeLine(` <p className=\"text-slate-400 dark:text-slate-500 text-sm italic text-center py-10\">Empty panel</p>`);
1277
+ }
1278
+ writer.writeLine(` </div>`);
1279
+ writer.writeLine(` </div>`);
1280
+ writer.writeLine(`);`);
1281
+ return;
1282
+ }
1283
+ else if (kind === "row" || kind === "column") {
1284
+ const cols = component.layout.columns ?? 2;
1285
+ const grid = kind === "row" ? `grid-cols-${Math.min(4, Math.max(1, cols))}` : "grid-cols-1";
1286
+ const validItems = (component.layout.items ?? []).filter((n) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(n));
1287
+ const items = validItems.map((n) => pascal(n));
1288
+ writer.writeLine(`const items = [${items.join(", ")}];`);
1289
+ writer.writeLine(`return (`);
1290
+ writer.writeLine(` <div className=\"space-y-6\">`);
1291
+ if (component.layout.title) {
1292
+ const titleId = slugifyHeading(component.layout.title);
1293
+ writer.writeLine(` <h3 className=\"text-2xl font-black text-slate-900 dark:text-white tracking-tight\"${titleId ? ` id="${titleId}"` : ""}>{${JSON.stringify(component.layout.title)}}</h3>`);
1294
+ }
1295
+ if (component.content || component.codeBlock || component.button) {
1296
+ writer.writeLine(` <div className="space-y-4">`);
1297
+ if (component.content)
1298
+ writer.writeLine(` <div className="prose dark:prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(renderMarkdownToHtml(component.content))} }} />`);
1299
+ if (component.codeBlock)
1300
+ writer.writeLine(` {codeBlock}`);
1301
+ if (component.button) {
1302
+ const variant = component.button.label ? (component.button.variant ?? "primary") : "primary";
1303
+ const baseBtn = "inline-flex items-center justify-center px-6 py-2.5 rounded-xl text-sm font-bold transition-all active:scale-95 shadow-lg shadow-slate-900/5";
1304
+ const variantClass = variant === "secondary"
1305
+ ? "bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700"
1306
+ : (variant === "ghost"
1307
+ ? "bg-transparent text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
1308
+ : "text-white hover:opacity-90");
1309
+ const style = variant === "primary" ? { backgroundColor: ir.policies.frontend.styling.theme.primaryColor } : {};
1310
+ writer.writeLine(` <button className="${baseBtn} ${variantClass}" style={${JSON.stringify(style)}} onClick={() => { /* TODO: wire action */ }}>`);
1311
+ if (component.button.icon) {
1312
+ writer.writeLine(` {(Icons as any)["${component.button.icon}"] && React.createElement((Icons as any)["${component.button.icon}"], { size: 16, className: "mr-2" })}`);
1313
+ }
1314
+ writer.writeLine(` ${component.button.label}`);
1315
+ writer.writeLine(` </button>`);
1316
+ }
1317
+ writer.writeLine(` </div>`);
1318
+ }
1319
+ writer.writeLine(" <div className={`grid gap-6 " + grid + "`}>");
1320
+ writer.writeLine(` {items.length ? items.map((Comp: any, idx: number) => (`);
1321
+ writer.writeLine(` <div key={idx} className=\"transition-all duration-300 hover:-translate-y-1\"><Comp /></div>`);
1322
+ if (hasInlineContent) {
1323
+ writer.writeLine(` )) : null}`);
1324
+ }
1325
+ else {
1326
+ writer.writeLine(` )) : <div className=\"col-span-full py-20 bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-2xl text-center text-slate-400 dark:text-slate-500 italic\">No items</div>}`);
1327
+ }
1328
+ writer.writeLine(` </div>`);
1329
+ writer.writeLine(` </div>`);
1330
+ writer.writeLine(`);`);
1331
+ return;
1332
+ }
1333
+ }
1334
+ // Non-form content/button components
1335
+ if (component.content || component.button || component.codeBlock) {
1336
+ writer.writeLine(`return (`);
1337
+ writer.writeLine(` <div className="p-6 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm rounded-2xl space-y-4">`);
1338
+ if (component.content)
1339
+ writer.writeLine(` <div className="prose dark:prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(renderMarkdownToHtml(component.content))} }} />`);
1340
+ if (component.codeBlock)
1341
+ writer.writeLine(` {codeBlock}`);
1342
+ if (component.button) {
1343
+ const variant = component.button.label ? (component.button.variant ?? "primary") : "primary";
1344
+ const baseBtn = "inline-flex items-center justify-center px-6 py-2.5 rounded-xl text-sm font-bold transition-all active:scale-95 shadow-lg shadow-slate-900/5";
1345
+ const variantClass = variant === "secondary"
1346
+ ? "bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700"
1347
+ : (variant === "ghost"
1348
+ ? "bg-transparent text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
1349
+ : "text-white hover:opacity-90");
1350
+ const style = variant === "primary" ? { backgroundColor: ir.policies.frontend.styling.theme.primaryColor } : {};
1351
+ writer.writeLine(` <button className="${baseBtn} ${variantClass}" style={${JSON.stringify(style)}} onClick={() => { /* TODO: wire action */ }}>`);
1352
+ if (component.button.icon) {
1353
+ writer.writeLine(` {(Icons as any)["${component.button.icon}"] && React.createElement((Icons as any)["${component.button.icon}"], { size: 16, className: "mr-2" })}`);
1354
+ }
1355
+ writer.writeLine(` ${component.button.label}`);
1356
+ writer.writeLine(` </button>`);
1357
+ }
1358
+ writer.writeLine(` </div>`);
1359
+ writer.writeLine(`);`);
1360
+ return;
1361
+ }
1362
+ if (component.form && component.form.fields && component.form.fields.length > 0) {
1363
+ // 1. STATE DEFINITIONS
1364
+ const stateVars = [];
1365
+ for (const f of component.form.fields) {
1366
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1367
+ stateVars.push(varName);
1368
+ const initialVal = f.type === "checkbox" ? "false /* boolean */" :
1369
+ (f.type === "select" && f.multiple) ? "[]" :
1370
+ (f.type === "tags" ? "[]" :
1371
+ (f.type === "file" && f.multiple ? "[]" :
1372
+ (f.type === "file" ? "null" :
1373
+ (f.type === "daterange" ? "{ start: \"\", end: \"\" }" :
1374
+ "\"\""))));
1375
+ writer.writeLine(`const [${varName}, set_${varName}] = useState(${initialVal});`);
1376
+ if (f.dataSource) {
1377
+ writer.writeLine(`const [options_${varName}, setOptions_${varName}] = useState<{label:string, value:string}[]>([]);`);
1378
+ writer.writeLine(`const [loading_${varName}, setLoading_${varName}] = useState(false);`);
1379
+ writer.writeLine(`const [error_${varName}, setError_${varName}] = useState<string | null>(null);`);
1380
+ writer.writeLine(`const [search_${varName}, setSearch_${varName}] = useState("");`);
1381
+ writer.writeLine(`const [page_${varName}, setPage_${varName}] = useState(1);`);
1382
+ writer.writeLine(`const [hasMore_${varName}, setHasMore_${varName}] = useState(true);`);
1383
+ }
1384
+ }
1385
+ writer.writeLine(`const [errors, set_errors] = useState({} as Record<string,string>);`);
1386
+ writer.writeLine(`const ctx = { ${stateVars.map(s => `${s}: ${s}`).join(", ")} };`);
1387
+ writer.writeLine(`const getFieldVal = (field: string) => getByPath(ctx, field.replace(/[^a-zA-Z0-9_]/g, "_"));`);
1388
+ // 2. EFFECTS (Data Fetching, defaults, computed)
1389
+ for (const f of component.form.fields) {
1390
+ if (f.dataSource) {
1391
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1392
+ const searchParam = f.dataSource.searchParam ?? "q";
1393
+ const pageParam = f.dataSource.pageParam ?? "page";
1394
+ const pageSizeParam = f.dataSource.pageSizeParam ?? "pageSize";
1395
+ const pageSize = f.dataSource.pageSize ?? 20;
1396
+ const debounceMs = f.dataSource.debounceMs ?? 300;
1397
+ writer.writeLine(`const op_${varName} = useOperation("${f.dataSource.url}"); // In future, this will be an operationId`);
1398
+ writer.writeLine(`useEffect(() => {`);
1399
+ writer.writeLine(` const handle = setTimeout(async () => {`);
1400
+ writer.writeLine(` await op_${varName}.execute({ [ "${searchParam}" ]: search_${varName}, ["${pageParam}"]: page_${varName}, ["${pageSizeParam}"]: ${pageSize} });`);
1401
+ writer.writeLine(` }, ${debounceMs});`);
1402
+ writer.writeLine(` return () => clearTimeout(handle);`);
1403
+ writer.writeLine(`}, [search_${varName}, page_${varName}]);`);
1404
+ writer.writeLine(`useEffect(() => {`);
1405
+ writer.writeLine(` if (op_${varName}.data) setOptions_${varName}(op_${varName}.data as any);`);
1406
+ writer.writeLine(`}, [op_${varName}.data]);`);
1407
+ }
1408
+ if (f.loweredDefaultValue) {
1409
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1410
+ const deps = f.loweredDefaultValue.dependencies.map(d => d.replace(/[^a-zA-Z0-9_]/g, "_"));
1411
+ writer.writeLine(`useEffect(() => { const v = evalLogic(${JSON.stringify(f.loweredDefaultValue.logic)}, ${varName}, ctx); if (typeof v !== "undefined" && ${varName} === "" ) set_${varName}(v); }, [${deps.join(", ")}]);`);
1412
+ }
1413
+ if (f.loweredComputeValue) {
1414
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1415
+ const deps = f.loweredComputeValue.dependencies.map(d => d.replace(/[^a-zA-Z0-9_]/g, "_"));
1416
+ writer.writeLine(`useEffect(() => { const next = evalLogic(${JSON.stringify(f.loweredComputeValue.logic)}, ${varName}, ctx); if (typeof next !== "undefined" && next !== ${varName}) set_${varName}(next); }, [${deps.join(", ")}]);`);
1417
+ }
1418
+ }
1419
+ // 3. VALIDATION
1420
+ writer.writeLine(`const validate = () => {`);
1421
+ writer.writeLine(` const n: Record<string,string> = {};`);
1422
+ for (const f of component.form.fields) {
1423
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1424
+ const rules = f.loweredValidators ?? [];
1425
+ if (rules.length === 0)
1426
+ continue;
1427
+ writer.writeLine(` // ${f.name} validation`);
1428
+ for (const rule of rules) {
1429
+ const msg = JSON.stringify(rule.message);
1430
+ writer.writeLine(` if (!n["${f.name}"]) {`);
1431
+ switch (rule.type) {
1432
+ case "required":
1433
+ writer.writeLine(` if (isEmptyVal(${varName})) n["${f.name}"] = ${msg};`);
1434
+ break;
1435
+ case "requiredIf":
1436
+ writer.writeLine(` if (evalLogic(${JSON.stringify(rule.logic)}, false) && isEmptyVal(${varName})) n["${f.name}"] = ${msg};`);
1437
+ break;
1438
+ case "min":
1439
+ if (rule.params?.isDate) {
1440
+ writer.writeLine(` const d = Date.parse(${varName}); const min = Date.parse("${rule.params.value}");`);
1441
+ writer.writeLine(` if (!isNaN(d) && !isNaN(min) && d < min) n["${f.name}"] = ${msg};`);
1442
+ }
1443
+ else {
1444
+ writer.writeLine(` if (Number(${varName}) < ${rule.params?.value}) n["${f.name}"] = ${msg};`);
1445
+ }
1446
+ break;
1447
+ case "max":
1448
+ if (rule.params?.isDate) {
1449
+ writer.writeLine(` const d = Date.parse(${varName}); const max = Date.parse("${rule.params.value}");`);
1450
+ writer.writeLine(` if (!isNaN(d) && !isNaN(max) && d > max) n["${f.name}"] = ${msg};`);
1451
+ }
1452
+ else {
1453
+ writer.writeLine(` if (Number(${varName}) > ${rule.params?.value}) n["${f.name}"] = ${msg};`);
1454
+ }
1455
+ break;
1456
+ case "minLength":
1457
+ writer.writeLine(` if (${varName}.toString().length < ${rule.params?.value}) n["${f.name}"] = ${msg};`);
1458
+ break;
1459
+ case "maxLength":
1460
+ writer.writeLine(` if (${varName}.toString().length > ${rule.params?.value}) n["${f.name}"] = ${msg};`);
1461
+ break;
1462
+ case "pattern":
1463
+ writer.writeLine(` try { const re = new RegExp(${JSON.stringify(rule.params?.value)}); if (!re.test(${varName}.toString())) n["${f.name}"] = ${msg}; } catch (_) {}`);
1464
+ break;
1465
+ case "format":
1466
+ if (rule.params?.value === "email") {
1467
+ writer.writeLine(` if (${varName} && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${varName}.toString())) n["${f.name}"] = ${msg};`);
1468
+ }
1469
+ else if (rule.params?.value === "url") {
1470
+ writer.writeLine(` if (${varName}) { try { new URL(${varName}.toString()); } catch (_) { n["${f.name}"] = ${msg}; } }`);
1471
+ }
1472
+ break;
1473
+ case "equalsField":
1474
+ writer.writeLine(` if (${varName} != getFieldVal("${rule.params?.value}")) n["${f.name}"] = ${msg};`);
1475
+ break;
1476
+ case "notEqualsField":
1477
+ writer.writeLine(` if (${varName} == getFieldVal("${rule.params?.value}")) n["${f.name}"] = ${msg};`);
1478
+ break;
1479
+ case "greaterThanField":
1480
+ writer.writeLine(` { const other = getFieldVal("${rule.params?.value}"); const lhs = Number(${varName}); const rhs = Number(other); if (!isNaN(lhs) && !isNaN(rhs) && lhs <= rhs) n["${f.name}"] = ${msg}; }`);
1481
+ break;
1482
+ case "lessThanField":
1483
+ writer.writeLine(` { const other = getFieldVal("${rule.params?.value}"); const lhs = Number(${varName}); const rhs = Number(other); if (!isNaN(lhs) && !isNaN(rhs) && lhs >= rhs) n["${f.name}"] = ${msg}; }`);
1484
+ break;
1485
+ case "custom":
1486
+ writer.writeLine(` if (!evalLogic(${JSON.stringify(rule.logic)}, false)) n["${f.name}"] = ${msg};`);
1487
+ break;
1488
+ case "uniqueIn":
1489
+ writer.writeLine(` if (${JSON.stringify(rule.params?.value)}.includes(${varName})) n["${f.name}"] = ${msg};`);
1490
+ break;
1491
+ }
1492
+ writer.writeLine(` }`);
1493
+ }
1494
+ }
1495
+ writer.writeLine(` set_errors(n);`);
1496
+ writer.writeLine(` return Object.keys(n).length === 0;`);
1497
+ writer.writeLine(`};`);
1498
+ writer.writeLine(`const [submitSuccess, setSubmitSuccess] = useState<string | null>(null);`);
1499
+ writer.writeLine(`const [submitError, setSubmitError] = useState<string | null>(null);`);
1500
+ if (component.form.submit?.draftKey) {
1501
+ writer.writeLine(`useEffect(() => {`);
1502
+ writer.writeLine(` try { const raw = localStorage.getItem("${component.form.submit.draftKey}"); if (raw) { const obj = JSON.parse(raw);`);
1503
+ for (const f of component.form.fields) {
1504
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1505
+ writer.writeLine(` if (obj["${varName}"] !== undefined) set_${varName}(obj["${varName}"]);`);
1506
+ }
1507
+ writer.writeLine(` } } catch (_) {}`);
1508
+ writer.writeLine(`}, []);`);
1509
+ writer.writeLine(`useEffect(() => {`);
1510
+ writer.writeLine(` const data = { ${stateVars.map(s => `${s}: ${s}`).join(", ")} };`);
1511
+ writer.writeLine(` try { localStorage.setItem("${component.form.submit.draftKey}", JSON.stringify(data)); } catch (_) {}`);
1512
+ writer.writeLine(`}, [${stateVars.join(", ")}]);`);
1513
+ }
1514
+ writer.writeLine(`const submitOp = useOperation("${component.form.submit?.url ?? ""}");`);
1515
+ writer.writeLine(`const onSubmit = async (e: any) => { e.preventDefault(); setSubmitSuccess(null); setSubmitError(null);`);
1516
+ if (component.form.submit?.confirmMessage) {
1517
+ writer.writeLine(` if (!window.confirm(${JSON.stringify(component.form.submit.confirmMessage)})) return;`);
1518
+ }
1519
+ writer.writeLine(` if (!validate()) return;`);
1520
+ writer.writeLine(` const payload = { ${stateVars.map(s => `${s}: ${s}`).join(", ")} };`);
1521
+ writer.writeLine(` if (${component.form.submit?.beforeSubmit ? "true" : "false"}) {`);
1522
+ writer.writeLine(` const hookCtx = { ...ctx, payload };`);
1523
+ writer.writeLine(` const shouldContinue = evalLogic(${component.form.submit?.beforeSubmit ? JSON.stringify(component.form.submit.beforeSubmit) : "null"}, true, hookCtx);`);
1524
+ writer.writeLine(` if (shouldContinue === false) { setSubmitError("Submission cancelled"); return; }`);
1525
+ writer.writeLine(` }`);
1526
+ writer.writeLine(` if (!${component.form.submit ? "true" : "false"}) { setSubmitSuccess("Saved (mock)"); return; }`);
1527
+ writer.writeLine(` const res = await submitOp.execute(payload);`);
1528
+ writer.writeLine(` if (res.ok) {`);
1529
+ writer.writeLine(` setSubmitSuccess(${component.form.submit?.successMessage ? JSON.stringify(component.form.submit.successMessage) : `"Saved"`});`);
1530
+ writer.writeLine(` const hookCtx = { ...ctx, payload, response: res.data };`);
1531
+ if (component.form.submit?.onSuccess) {
1532
+ writer.writeLine(` evalLogic(${JSON.stringify(component.form.submit.onSuccess)}, undefined, hookCtx);`);
1533
+ }
1534
+ if (component.form.submit?.redirect) {
1535
+ writer.writeLine(` window.location.href = ${JSON.stringify(component.form.submit.redirect)};`);
1536
+ }
1537
+ writer.writeLine(` } else {`);
1538
+ writer.writeLine(` setSubmitError(res.error?.message ?? ${component.form.submit?.errorMessage ? JSON.stringify(component.form.submit.errorMessage) : `"Submit error"`});`);
1539
+ writer.writeLine(` const hookCtx = { ...ctx, payload, error: res.error };`);
1540
+ if (component.form.submit?.onError) {
1541
+ writer.writeLine(` evalLogic(${JSON.stringify(component.form.submit.onError)}, undefined, hookCtx);`);
1542
+ }
1543
+ writer.writeLine(` }`);
1544
+ if (component.form.submit?.afterSubmit) {
1545
+ writer.writeLine(` const hookCtx = { ...ctx, payload };`);
1546
+ writer.writeLine(` evalLogic(${JSON.stringify(component.form.submit.afterSubmit)}, undefined, hookCtx);`);
1547
+ }
1548
+ writer.writeLine(`};`);
1549
+ // 4. RENDER
1550
+ writer.writeLine(`return (`);
1551
+ writer.writeLine(` <form className=\"${formClass}\" onSubmit={onSubmit}>`);
1552
+ for (const f of component.form.fields) {
1553
+ const varName = f.name.replace(/[^a-zA-Z0-9_]/g, "_");
1554
+ const label = f.label ?? f.name;
1555
+ const visibleExpr = f.loweredVisibleIf ? JSON.stringify(f.loweredVisibleIf.logic) : undefined;
1556
+ writer.writeLine(` {(() => {`);
1557
+ if (visibleExpr) {
1558
+ writer.writeLine(` const visible = evalLogic(${visibleExpr}, true, ctx);`);
1559
+ writer.writeLine(` if (!visible) return null;`);
1560
+ }
1561
+ const disabledExpr = f.loweredDisabledIf ? JSON.stringify(f.loweredDisabledIf.logic) : undefined;
1562
+ writer.writeLine(` const disabledVal = ${disabledExpr ? `evalLogic(${disabledExpr}, false, ctx)` : "false"};`);
1563
+ writer.writeLine(` return (`);
1564
+ writer.writeLine(` <div className="${f.className ?? ""}">`);
1565
+ writer.writeLine(` <div className="flex items-center gap-2">`);
1566
+ writer.writeLine(` <label className=\"${labelClass}\">${label}</label>`);
1567
+ if (f.tooltip) {
1568
+ writer.writeLine(` <span className="text-gray-400 dark:text-gray-500" title="${f.tooltip}">ℹ️</span>`);
1569
+ }
1570
+ writer.writeLine(` </div>`);
1571
+ // Icon wrapper
1572
+ if (f.icon) {
1573
+ writer.writeLine(` <div className="relative mt-1 rounded-md shadow-sm">`);
1574
+ writer.writeLine(` <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">`);
1575
+ writer.writeLine(` {(Icons as any)["${f.icon}"] && React.createElement((Icons as any)["${f.icon}"], { size: 16, className: "text-gray-400 dark:text-gray-500" })}`);
1576
+ writer.writeLine(` </div>`);
1577
+ }
1578
+ else {
1579
+ writer.writeLine(` <div className="${(f.prefix || f.suffix) ? "relative mt-1" : "mt-1"}">`);
1580
+ }
1581
+ const baseInput = f.className ? `${inputClass} ${f.className}` : inputClass;
1582
+ const paddedInput = f.icon ? `${baseInput} pl-10` : baseInput;
1583
+ const inputClassName = (f.prefix ? `${paddedInput} pl-10` : paddedInput) + (f.suffix ? " pr-10" : "");
1584
+ if (f.prefix) {
1585
+ writer.writeLine(` <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 text-sm pointer-events-none">${f.prefix}</span>`);
1586
+ }
1587
+ if (f.type === "select") {
1588
+ if (f.dataSource) {
1589
+ writer.writeLine(` {loading_${varName} && <div className="animate-pulse space-y-2 mb-2" aria-busy="true">`);
1590
+ writer.writeLine(` <div className="h-3 bg-slate-100 dark:bg-slate-800 rounded-full"></div>`);
1591
+ writer.writeLine(` <div className="h-3 bg-slate-100 dark:bg-slate-800 rounded-full w-2/3"></div>`);
1592
+ writer.writeLine(` </div>}`);
1593
+ writer.writeLine(` {error_${varName} && <p className="text-xs font-semibold text-red-500 mb-2">{error_${varName}}</p>}`);
1594
+ writer.writeLine(` <div className="relative mb-3">`);
1595
+ writer.writeLine(` <Icons.Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500" size={14} />`);
1596
+ writer.writeLine(` <input className="${inputClass} pl-9" placeholder="${f.searchPlaceholder ?? "Type to filter..."}" value={search_${varName}} onChange={e => { setSearch_${varName}(e.target.value); setPage_${varName}(1); }} aria-label="${f.ariaLabel ?? label} search" />`);
1597
+ writer.writeLine(` </div>`);
1598
+ writer.writeLine(` {(() => { const filtered = options_${varName}; return (`);
1599
+ writer.writeLine(` <div className="relative">`);
1600
+ writer.writeLine(` <select className=\"appearance-none ${inputClassName}\" name=\"${f.name}\" ${f.multiple ? "multiple" : ""} value={${f.multiple ? varName : `${varName} || ""`}} onChange={(e) => {`);
1601
+ writer.writeLine(` set_${varName}(${f.multiple ? "Array.from(e.target.selectedOptions).map(o => o.value)" : "e.target.value"});`);
1602
+ writer.writeLine(` }} disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" aria-busy={loading_${varName}} aria-invalid={Boolean(errors["${f.name}"])}>`);
1603
+ if (!f.multiple)
1604
+ writer.writeLine(` <option value="">Select an option...</option>`);
1605
+ writer.writeLine(` {filtered.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}`);
1606
+ writer.writeLine(` </select>`);
1607
+ if (!f.multiple) {
1608
+ writer.writeLine(` <Icons.ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500 pointer-events-none" size={16} />`);
1609
+ }
1610
+ writer.writeLine(` </div>`);
1611
+ writer.writeLine(` ); })()}`);
1612
+ writer.writeLine(` <div className="flex items-center justify-between mt-3 px-1">`);
1613
+ writer.writeLine(` <div className="flex gap-2">`);
1614
+ writer.writeLine(` <button type="button" className="p-1.5 border border-slate-200 dark:border-slate-800 rounded-lg bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-400 disabled:opacity-30 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors" onClick={() => setPage_${varName}(p => Math.max(1, p - 1))} disabled={page_${varName} <= 1}><Icons.ChevronLeft size={16}/></button>`);
1615
+ writer.writeLine(` <button type="button" className="p-1.5 border border-slate-200 dark:border-slate-800 rounded-lg bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-400 disabled:opacity-30 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors" onClick={() => setPage_${varName}(p => hasMore_${varName} ? p + 1 : p)} disabled={!hasMore_${varName}}><Icons.ChevronRight size={16}/></button>`);
1616
+ writer.writeLine(` </div>`);
1617
+ writer.writeLine(` <span className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">Page {page_${varName}}</span>`);
1618
+ if (f.clearable) {
1619
+ writer.writeLine(` <button type="button" className="text-xs font-bold text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors" onClick={() => set_${varName}(${f.multiple ? "[]" : '""'})}>Reset</button>`);
1620
+ }
1621
+ writer.writeLine(` </div>`);
1622
+ }
1623
+ else {
1624
+ writer.writeLine(` <div className="relative">`);
1625
+ writer.writeLine(` <select className=\"appearance-none ${inputClassName}\" name=\"${f.name}\" ${f.multiple ? "multiple" : ""} value={${f.multiple ? varName : `${varName} || ""`}} onChange={(e) => {`);
1626
+ if (f.multiple) {
1627
+ writer.writeLine(` const vals = Array.from(e.target.selectedOptions).map(o => o.value); set_${varName}(vals);`);
1628
+ }
1629
+ else {
1630
+ writer.writeLine(` set_${varName}(e.target.value);`);
1631
+ }
1632
+ writer.writeLine(` }} disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" aria-invalid={Boolean(errors["${f.name}"])}>`);
1633
+ if (!f.multiple)
1634
+ writer.writeLine(` <option value="">Select...</option>`);
1635
+ if (f.options) {
1636
+ for (const opt of f.options) {
1637
+ writer.writeLine(` <option value="${opt.value}">${opt.label}</option>`);
1638
+ }
1639
+ }
1640
+ writer.writeLine(` </select>`);
1641
+ if (!f.multiple) {
1642
+ writer.writeLine(` <Icons.ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500 pointer-events-none" size={16} />`);
1643
+ }
1644
+ writer.writeLine(` </div>`);
1645
+ if (f.clearable) {
1646
+ writer.writeLine(` <button type="button" className="mt-2 text-xs font-bold text-slate-400 dark:text-slate-500 hover:text-slate-900 dark:hover:text-white transition-colors uppercase tracking-widest" onClick={() => set_${varName}(${f.multiple ? "[]" : '""'})}>Reset selection</button>`);
1647
+ }
1648
+ }
1649
+ }
1650
+ else if (f.type === "tags") {
1651
+ writer.writeLine(` <div className="flex flex-wrap gap-2 mb-2">`);
1652
+ writer.writeLine(` {${varName}.map((tag: string, idx: number) => (`);
1653
+ writer.writeLine(` <span key={idx} className="inline-flex items-center gap-1.5 bg-slate-900 text-white pl-2.5 pr-1.5 py-1 rounded-full text-xs font-semibold shadow-sm animate-in zoom-in-50">`);
1654
+ writer.writeLine(` {tag}`);
1655
+ writer.writeLine(` <button type="button" onClick={() => set_${varName}(${varName}.filter((_: any,i: number)=>i!==idx))} className="w-4 h-4 rounded-full bg-white/20 hover:bg-white/40 flex items-center justify-center transition-colors" aria-label="Remove tag">`);
1656
+ writer.writeLine(` <Icons.X size={10} />`);
1657
+ writer.writeLine(` </button>`);
1658
+ writer.writeLine(` </span>`);
1659
+ writer.writeLine(` ))}`);
1660
+ writer.writeLine(` </div>`);
1661
+ writer.writeLine(` <div className="relative">`);
1662
+ writer.writeLine(` <Icons.Plus className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500" size={14} />`);
1663
+ writer.writeLine(` <input className="${inputClassName} pl-9" placeholder="${f.placeholder ?? "New tag..."}" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); const val = (e.target as HTMLInputElement).value.trim(); if (val) { set_${varName}([...${varName}, val]); (e.target as HTMLInputElement).value = ""; } } }} disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" />`);
1664
+ writer.writeLine(` </div>`);
1665
+ writer.writeLine(` <div className="relative group/file">`);
1666
+ writer.writeLine(` <input className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" name="${f.name}" type="file" ${f.accept ? `accept="${f.accept}"` : ""} ${f.multiple ? "multiple" : ""} onChange={(e) => { const files = e.target.files; if (!files) return; ${f.multiple ? `set_${varName}(Array.from(files));` : `set_${varName}(files[0] ?? null);`} }} disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" />`);
1667
+ writer.writeLine(` <div className="\${inputClassName} flex items-center justify-center p-8 border-2 border-dashed border-slate-200 dark:border-slate-800 group-hover/file:border-slate-400 dark:group-hover/file:border-slate-600 transition-colors text-slate-500 bg-slate-50/50 dark:bg-slate-900/50">`);
1668
+ writer.writeLine(` <div className="text-center">`);
1669
+ writer.writeLine(` <Icons.UploadCloud size={24} className="mx-auto mb-2 opacity-50" />`);
1670
+ writer.writeLine(` <p className="text-xs font-bold uppercase tracking-widest text-slate-400">Click or drag to upload</p>`);
1671
+ writer.writeLine(` <p className="text-[10px] mt-1 italic text-slate-400">${f.multiple ? 'Multiple files supported' : (f.accept ? `Accepted: ${f.accept}` : 'All file types')}</p>`);
1672
+ writer.writeLine(` </div>`);
1673
+ writer.writeLine(` </div>`);
1674
+ writer.writeLine(` </div>`);
1675
+ writer.writeLine(` {${varName} && (`);
1676
+ writer.writeLine(` <div className="mt-3 space-y-2">`);
1677
+ if (f.multiple) {
1678
+ writer.writeLine(` {(${varName} as File[]).map((file, i) => (`);
1679
+ writer.writeLine(` <div key={i} className="flex items-center gap-2 p-2 bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-lg text-xs font-medium text-slate-600 dark:text-slate-400">`);
1680
+ writer.writeLine(` <Icons.File size={14} className="text-slate-400 dark:text-slate-500" />`);
1681
+ writer.writeLine(` <span className="truncate flex-1">{file.name}</span>`);
1682
+ writer.writeLine(` <span className="text-[10px] text-slate-300 dark:text-slate-600">{(file.size / 1024).toFixed(1)}KB</span>`);
1683
+ writer.writeLine(` </div>`);
1684
+ writer.writeLine(` ))}`);
1685
+ }
1686
+ else {
1687
+ writer.writeLine(` <div className="flex items-center gap-2 p-2 bg-slate-100/50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg text-xs font-semibold text-slate-700 dark:text-slate-200">`);
1688
+ writer.writeLine(` <Icons.FileCheck size={14} className="text-emerald-500 dark:text-emerald-400" />`);
1689
+ writer.writeLine(` <span className="truncate flex-1">{(${varName} as File).name}</span>`);
1690
+ writer.writeLine(` <button type="button" onClick={() => set_${varName}(null)} className="text-slate-400 dark:text-slate-500 hover:text-red-500 transition-colors"><Icons.X size={14}/></button>`);
1691
+ writer.writeLine(` </div>`);
1692
+ }
1693
+ writer.writeLine(` </div>`);
1694
+ writer.writeLine(` )}`);
1695
+ const minVal = (f.validators && typeof f.validators.min !== "undefined") ? f.validators.min : 0;
1696
+ const maxVal = (f.validators && typeof f.validators.max !== "undefined") ? f.validators.max : 100;
1697
+ const stepVal = f.step ?? 1;
1698
+ writer.writeLine(` <div className="py-4 px-1">`);
1699
+ writer.writeLine(` <input className="w-full h-1.5 bg-slate-200 dark:bg-slate-800 rounded-lg appearance-none cursor-pointer accent-slate-900 dark:accent-white" type="range" min="${minVal}" max="${maxVal}" step="${stepVal}" value={${varName} || ${minVal}} onChange={(e) => set_${varName}(e.target.value)} disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" />`);
1700
+ writer.writeLine(` <div className="flex justify-between mt-3">`);
1701
+ writer.writeLine(` <span className="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase underline decoration-slate-200 dark:decoration-slate-800 decoration-2 underline-offset-4">${minVal}</span>`);
1702
+ writer.writeLine(` <span className="bg-slate-900 dark:bg-white text-white dark:text-slate-950 text-[10px] font-bold px-2 py-0.5 rounded-full shadow-lg shadow-slate-900/20 dark:shadow-white/10">{${varName} || ${minVal}}</span>`);
1703
+ writer.writeLine(` <span className="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase underline decoration-slate-200 dark:decoration-slate-800 decoration-2 underline-offset-4">${maxVal}</span>`);
1704
+ writer.writeLine(` </div>`);
1705
+ writer.writeLine(` </div>`);
1706
+ writer.writeLine(` <div className="relative group/currency">`);
1707
+ writer.writeLine(` <div className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 font-bold text-xs pointer-events-none group-focus-within/currency:text-slate-900 dark:group-focus-within/currency:text-white transition-colors">${f.defaultCurrency ?? "Rp"}</div>`);
1708
+ writer.writeLine(` <input className="${inputClassName} pl-10 font-bold text-slate-900 dark:text-white" name="${f.name}" type="number" value={${varName}} onChange={(e) => set_${varName}(e.target.value)} placeholder="${f.placeholder ?? '0.00'}" disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" />`);
1709
+ writer.writeLine(` </div>`);
1710
+ writer.writeLine(` <div className="flex gap-3">`);
1711
+ writer.writeLine(` <div className="relative flex-1">`);
1712
+ writer.writeLine(` <Icons.Calendar className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500" size={14} />`);
1713
+ writer.writeLine(` <input className="${inputClassName} pl-9" type="date" value={${varName}.start} onChange={(e)=> set_${varName}({...${varName}, start: e.target.value})} disabled={disabledVal} aria-label="${f.ariaLabel ?? label} start" />`);
1714
+ writer.writeLine(` </div>`);
1715
+ writer.writeLine(` <div className="flex items-center text-slate-300 dark:text-slate-600 font-bold">→</div>`);
1716
+ writer.writeLine(` <div className="relative flex-1">`);
1717
+ writer.writeLine(` <Icons.Calendar className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500" size={14} />`);
1718
+ writer.writeLine(` <input className="${inputClassName} pl-9" type="date" value={${varName}.end} onChange={(e)=> set_${varName}({...${varName}, end: e.target.value})} disabled={disabledVal} aria-label="${f.ariaLabel ?? label} end" />`);
1719
+ writer.writeLine(` </div>`);
1720
+ writer.writeLine(` </div>`);
1721
+ writer.writeLine(` <div className="relative group/sig">`);
1722
+ writer.writeLine(` <div className="absolute top-3 right-3 opacity-20 group-hover/sig:opacity-40 transition-opacity">`);
1723
+ writer.writeLine(` <Icons.PenTool size={32} className="text-slate-300 dark:text-slate-600" />`);
1724
+ writer.writeLine(` </div>`);
1725
+ writer.writeLine(` <textarea className="${inputClassName} min-h-[120px] font-mono text-xs border-2 border-slate-100 dark:border-slate-800 italic" name="${f.name}" value={${varName}} onChange={(e)=> set_${varName}(e.target.value)} placeholder="${f.placeholder ?? 'Signature trace or base64...'}" disabled={disabledVal} aria-label="${f.ariaLabel ?? label}" />`);
1726
+ writer.writeLine(` <div className="mt-2 text-[10px] font-bold text-slate-400 dark:text-slate-500 flex items-center gap-1 uppercase tracking-tighter">`);
1727
+ writer.writeLine(` <Icons.ShieldCheck size={12} className="text-emerald-500 dark:text-emerald-400" /> Secure digital verification trace`);
1728
+ writer.writeLine(` </div>`);
1729
+ writer.writeLine(` </div>`);
1730
+ }
1731
+ else {
1732
+ const inputType = (["number", "email", "password", "date", "datetime", "time", "url", "phone"].includes(f.type)) ? (f.type === "phone" ? "tel" : f.type) : "text";
1733
+ if (f.type === "textarea") {
1734
+ writer.writeLine(` <textarea className=\"${inputClassName} min-h-[140px] resize-none\" name=\"${f.name}\" value={${varName}} onChange={(e) => set_${varName}(e.target.value)} placeholder=\"${f.placeholder ?? ''}\" disabled={disabledVal} />`);
1735
+ }
1736
+ else if (f.type === "checkbox") {
1737
+ writer.writeLine(` <div className="flex items-center gap-3 bg-slate-50 border border-slate-100 rounded-xl p-4 transition-all hover:bg-slate-100/50">`);
1738
+ writer.writeLine(` <input className=\"${checkboxClass} w-5 h-5 cursor-pointer\" name=\"${f.name}\" checked={${varName}} onChange={(e) => set_${varName}(e.target.checked)} type="checkbox" disabled={disabledVal} />`);
1739
+ writer.writeLine(` <div className="flex-1">`);
1740
+ writer.writeLine(` <p className="text-sm font-bold text-slate-900 dark:text-white leading-none mb-1">Confirm Selection</p>`);
1741
+ writer.writeLine(` <p className="text-[10px] text-slate-500 dark:text-slate-400 font-medium">Check to acknowledge that the data above is correct.</p>`);
1742
+ writer.writeLine(` </div>`);
1743
+ writer.writeLine(` </div>`);
1744
+ }
1745
+ else if (f.type === "radio") {
1746
+ if (f.options) {
1747
+ writer.writeLine(` <div className="grid gap-3">`);
1748
+ for (const opt of (f.options ?? [])) {
1749
+ writer.writeLine(` <label className={\`relative flex items-center p-4 border-2 rounded-2xl cursor-pointer transition-all \${${varName} === "${opt.value}" ? 'border-slate-950 dark:border-white bg-slate-50 dark:bg-slate-900 shadow-inner' : 'border-slate-100 dark:border-slate-800 hover:border-slate-200 dark:hover:border-slate-700'}\`}>`);
1750
+ writer.writeLine(` <input className="sr-only" type="radio" name="${f.name}" value="${opt.value}" checked={${varName} === "${opt.value}"} onChange={(e) => set_${varName}(e.target.value)} disabled={disabledVal} />`);
1751
+ writer.writeLine(` <div className="flex-1">`);
1752
+ writer.writeLine(` <p className={\`text-sm font-bold \${${varName} === "${opt.value}" ? 'text-slate-950 dark:text-white' : 'text-slate-600 dark:text-slate-400'}\`}>${opt.label}</p>`);
1753
+ writer.writeLine(` <p className="text-[10px] text-slate-400 dark:text-slate-500">Option preference identifier: ${opt.value}</p>`);
1754
+ writer.writeLine(` </div>`);
1755
+ writer.writeLine(` <div className={\`w-5 h-5 rounded-full border-2 flex items-center justify-center \${${varName} === "${opt.value}" ? 'border-slate-950 dark:border-white bg-white dark:bg-slate-900' : 'border-slate-200 dark:border-slate-700'}\`}>`);
1756
+ writer.writeLine(` {${varName} === "${opt.value}" && <div className="w-2.5 h-2.5 rounded-full bg-slate-950 dark:bg-white animate-in zoom-in-50" />}`);
1757
+ writer.writeLine(` </div>`);
1758
+ writer.writeLine(` </label>`);
1759
+ }
1760
+ writer.writeLine(` </div>`);
1761
+ }
1762
+ else {
1763
+ writer.writeLine(` <input className=\"${inputClassName}\" name=\"${f.name}\" value={${varName}} onChange={(e) => set_${varName}(e.target.value)} type="text" placeholder=\"${f.placeholder ?? ''}\" disabled={disabledVal} />`);
1764
+ }
1765
+ }
1766
+ else {
1767
+ writer.writeLine(` <input className=\"${inputClassName} h-11\" name=\"${f.name}\" value={${varName}} onChange={(e) => set_${varName}(e.target.value)} type=\"${inputType}\" placeholder=\"${f.placeholder ?? ''}\" disabled={disabledVal} />`);
1768
+ }
1769
+ }
1770
+ if (f.suffix) {
1771
+ writer.writeLine(` <span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 text-sm pointer-events-none">${f.suffix}</span>`);
1772
+ }
1773
+ writer.writeLine(` </div>`); // End relative wrapper/mt-1
1774
+ if (f.description) {
1775
+ writer.writeLine(` <p className="mt-2.5 text-xs font-medium text-slate-400 dark:text-slate-500 leading-relaxed">${f.description}</p>`);
1776
+ }
1777
+ writer.writeLine(` {errors["${f.name}"] && <div className=\"${errorClass}\"><Icons.AlertCircle size={12}/> {errors["${f.name}"]}</div>}`);
1778
+ if (f.helpHtml) {
1779
+ writer.writeLine(` <div className="mt-3 p-3 bg-slate-900/5 dark:bg-white/5 rounded-lg border border-slate-950/5 dark:border-white/5 text-[10px] font-medium text-slate-500 dark:text-slate-400 leading-normal italic" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(f.helpHtml)} }} />`);
1780
+ }
1781
+ writer.writeLine(` </div>`);
1782
+ writer.writeLine(` );`);
1783
+ writer.writeLine(` })()}`);
1784
+ writer.writeLine("");
1785
+ }
1786
+ writer.writeLine(` {submitSuccess && <div className="text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950/20 border border-emerald-100 dark:border-emerald-900/50 px-4 py-3 rounded-xl text-sm font-medium animate-in fade-in slide-in-from-top-2">{submitSuccess}</div>}`);
1787
+ writer.writeLine(` {submitError && <div className="text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/20 border border-red-100 dark:border-red-900/50 px-4 py-3 rounded-xl text-sm font-medium animate-in fade-in slide-in-from-top-2">{submitError}</div>}`);
1788
+ writer.writeLine(` <button className="${btnClass} w-full shadow-lg" style={{ backgroundColor: "${primaryColor}" }} type="submit" disabled={submitOp.loading}>`);
1789
+ writer.writeLine(` {submitOp.loading ? (`);
1790
+ writer.writeLine(` <span className="flex items-center gap-2">`);
1791
+ writer.writeLine(` <Icons.Loader2 className="animate-spin" size={18} />`);
1792
+ writer.writeLine(` Submitting...`);
1793
+ writer.writeLine(` </span>`);
1794
+ writer.writeLine(` ) : "Submit Application"}`);
1795
+ writer.writeLine(` </button>`);
1796
+ writer.writeLine(` </form>`);
1797
+ writer.writeLine(`);`);
1798
+ }
1799
+ else {
1800
+ if (component.table) {
1801
+ const opId = component.table.operationId || (component.table.resourceId ? `${component.table.resourceId}.list` : "");
1802
+ writer.writeLine(`const op = useOperation("${opId}");`);
1803
+ writer.writeLine(`useEffect(() => { op.execute(); }, []);`);
1804
+ writer.writeLine(`const data = op.data || [];`);
1805
+ writer.writeLine(`return (`);
1806
+ writer.writeLine(` <div className="bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-xl rounded-2xl overflow-hidden">`);
1807
+ writer.writeLine(` <div className="overflow-x-auto">`);
1808
+ writer.writeLine(` <table className="min-w-full divide-y divide-slate-100 dark:divide-slate-800">`);
1809
+ writer.writeLine(` <thead className="bg-slate-50/50 dark:bg-slate-900/50">`);
1810
+ writer.writeLine(` <tr>`);
1811
+ for (const col of component.table.columns ?? []) {
1812
+ writer.writeLine(` <th className="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">${col.header}</th>`);
1813
+ }
1814
+ writer.writeLine(` </tr>`);
1815
+ writer.writeLine(` </thead>`);
1816
+ writer.writeLine(` <tbody className="divide-y divide-slate-100 dark:divide-slate-800 bg-white dark:bg-slate-900">`);
1817
+ writer.writeLine(` {op.loading && <tr><td colSpan={${component.table.columns?.length ?? 1}} className="px-6 py-10 text-center text-slate-400">Loading...</td></tr>}`);
1818
+ writer.writeLine(` {!op.loading && data.length === 0 && <tr><td colSpan={${component.table.columns?.length ?? 1}} className="px-6 py-10 text-center text-slate-400">No data available</td></tr>}`);
1819
+ writer.writeLine(` {data.map((item: any, i: number) => (`);
1820
+ writer.writeLine(` <tr key={i} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors duration-150">`);
1821
+ for (const col of component.table.columns ?? []) {
1822
+ writer.writeLine(` <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-700 dark:text-slate-300 font-medium">{String(item["${col.accessor}"])}</td>`);
1823
+ }
1824
+ writer.writeLine(` </tr>`);
1825
+ writer.writeLine(` ))}`);
1826
+ writer.writeLine(` </tbody>`);
1827
+ writer.writeLine(` </table>`);
1828
+ writer.writeLine(` </div>`);
1829
+ writer.writeLine(` </div>`);
1830
+ writer.writeLine(`);`);
1831
+ return;
1832
+ }
1833
+ writer.writeLine(`return (`);
1834
+ writer.writeLine(` <div className=\"p-6 bg-white shadow rounded-lg\">`);
1835
+ if (component.entityRef) {
1836
+ writer.writeLine(` <h3 className=\"text-lg font-medium leading-6 text-gray-900 dark:text-white\">${component.name}</h3>`);
1837
+ writer.writeLine(` <p className=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Entity: ${component.entityRef}</p>`);
1838
+ }
1839
+ else {
1840
+ writer.writeLine(` <h3 className=\"text-lg font-medium leading-6 text-gray-900\">${component.name}</h3>`);
1841
+ writer.writeLine(` <div className=\"mt-4 border-t border-gray-200 pt-4\">`);
1842
+ if (component.props) {
1843
+ writer.writeLine(` <dl className=\"grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2\">`);
1844
+ for (const [key, type] of Object.entries(component.props)) {
1845
+ writer.writeLine(` <div className=\"sm:col-span-1\">`);
1846
+ writer.writeLine(` <dt className=\"text-sm font-medium text-gray-500 dark:text-gray-400\">${key}</dt>`);
1847
+ writer.writeLine(` <dd className=\"mt-1 text-sm text-gray-900 dark:text-gray-100\">{${JSON.stringify(type)}}</dd>`); // Escape via JSX expression
1848
+ writer.writeLine(` </div>`);
1849
+ }
1850
+ writer.writeLine(` </dl>`);
1851
+ }
1852
+ writer.writeLine(` </div>`);
1853
+ }
1854
+ writer.writeLine(` </div>`);
1855
+ writer.writeLine(`);`);
1856
+ }
1857
+ });
1858
+ }
1859
+ function emitMarketingComponent(writer, m, policy) {
1860
+ const primaryColor = policy.styling.theme.primaryColor;
1861
+ const radiusMap = { none: "rounded-none", sm: "rounded-sm", md: "rounded-md", lg: "rounded-lg", full: "rounded-full" };
1862
+ const radius = radiusMap[policy.styling.theme.borderRadius] || "rounded-xl";
1863
+ if (m.kind === "hero") {
1864
+ writer.writeLine(` <div className="relative overflow-hidden ${radius} bg-slate-950 dark:bg-black text-white p-8 md:p-20">`);
1865
+ writer.writeLine(` <div className="absolute inset-0 opacity-20" style={{ backgroundImage: 'radial-gradient(circle at 20% 20%, ${primaryColor}, transparent)' }}></div>`);
1866
+ writer.writeLine(` <div className="relative max-w-4xl space-y-8">`);
1867
+ if (m.badge) {
1868
+ writer.writeLine(` <div className="inline-flex items-center rounded-full bg-white/10 px-3 py-1 text-sm font-medium border border-white/10 text-slate-300">`);
1869
+ writer.writeLine(` {${JSON.stringify(m.badge)}}`);
1870
+ writer.writeLine(` </div>`);
1871
+ }
1872
+ writer.writeLine(` <h1 className="text-4xl md:text-6xl font-bold tracking-tight leading-tight">`);
1873
+ writer.writeLine(` {${JSON.stringify(m.title)}}`);
1874
+ writer.writeLine(` </h1>`);
1875
+ writer.writeLine(` <p className="text-lg md:text-xl text-slate-300 max-w-2xl leading-relaxed">`);
1876
+ writer.writeLine(` {${JSON.stringify(m.subtitle)}}`);
1877
+ writer.writeLine(` </p>`);
1878
+ if (m.actions && m.actions.length > 0) {
1879
+ writer.writeLine(` <div className="flex flex-wrap gap-4">`);
1880
+ for (const a of m.actions) {
1881
+ const btnCls = a.variant === "primary" ? `bg-[${primaryColor}] text-white` : "bg-white/10 text-white border border-white/20 hover:bg-white/20";
1882
+ writer.writeLine(` <a href="${a.href}" className="inline-flex items-center gap-2 px-6 py-3 font-semibold ${radius} transition-all ${btnCls}">`);
1883
+ if (a.icon)
1884
+ writer.writeLine(` {React.createElement((Icons as any)["${a.icon}"], { size: 20 })}`);
1885
+ writer.writeLine(` {${JSON.stringify(a.label)}}`);
1886
+ writer.writeLine(` </a>`);
1887
+ }
1888
+ writer.writeLine(` </div>`);
1889
+ }
1890
+ writer.writeLine(` </div>`);
1891
+ writer.writeLine(` </div>`);
1892
+ }
1893
+ else if (m.kind === "features") {
1894
+ const align = m.align ?? "left";
1895
+ const headerAlign = align === "center" ? "text-center" : "text-left";
1896
+ const cardAlign = align === "center" ? "text-center items-center" : "text-left";
1897
+ const iconWrap = align === "center" ? "mx-auto" : "";
1898
+ writer.writeLine(` <div className="py-12 px-2">`);
1899
+ if (m.title || m.subtitle) {
1900
+ writer.writeLine(` <div className="${headerAlign} mb-12 space-y-2">`);
1901
+ if (m.title)
1902
+ writer.writeLine(` <h2 className="text-3xl font-bold text-slate-900 dark:text-white">{${JSON.stringify(m.title)}}</h2>`);
1903
+ if (m.subtitle) {
1904
+ const subtitleClass = align === "center" ? "max-w-2xl mx-auto" : "max-w-2xl";
1905
+ writer.writeLine(` <p className="text-slate-500 dark:text-slate-400 ${subtitleClass}">{${JSON.stringify(m.subtitle)}}</p>`);
1906
+ }
1907
+ writer.writeLine(` </div>`);
1908
+ }
1909
+ writer.writeLine(` <div className="grid grid-cols-1 md:grid-cols-3 gap-8">`);
1910
+ for (const item of (m.items || [])) {
1911
+ writer.writeLine(` <div className="p-6 border border-slate-100 dark:border-slate-800 ${radius} bg-white dark:bg-slate-900 shadow-sm hover:shadow-md transition-shadow ${cardAlign}">`);
1912
+ if (item.icon) {
1913
+ writer.writeLine(` <div className="w-12 h-12 rounded-lg bg-slate-50 dark:bg-slate-800 flex items-center justify-center mb-4 text-[${primaryColor}] ${iconWrap}">`);
1914
+ writer.writeLine(` {React.createElement((Icons as any)["${item.icon}"], { size: 24 })}`);
1915
+ writer.writeLine(` </div>`);
1916
+ }
1917
+ writer.writeLine(` <h3 className="text-lg font-semibold mb-2 dark:text-white">{${JSON.stringify(item.title)}}</h3>`);
1918
+ writer.writeLine(` <p className="text-slate-600 dark:text-slate-400 leading-relaxed">{${JSON.stringify(item.description)}}</p>`);
1919
+ writer.writeLine(` </div>`);
1920
+ }
1921
+ writer.writeLine(` </div>`);
1922
+ writer.writeLine(` </div>`);
1923
+ }
1924
+ else if (m.kind === "logos") {
1925
+ writer.writeLine(` <div className="py-12 border-y border-slate-100 dark:border-slate-800">`);
1926
+ if (m.title)
1927
+ writer.writeLine(` <p className="text-center text-xs font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-8">{${JSON.stringify(m.title)}}</p>`);
1928
+ writer.writeLine(` <div className="flex flex-wrap items-center justify-center gap-x-12 gap-y-10 opacity-50 grayscale hover:grayscale-0 transition-all">`);
1929
+ for (const item of (m.items || [])) {
1930
+ writer.writeLine(` <div className="flex items-center gap-2 group text-slate-900 dark:text-white">`);
1931
+ if (item.icon)
1932
+ writer.writeLine(` {React.createElement((Icons as any)["${item.icon}"], { size: 24, className: "opacity-60 group-hover:opacity-100 transition-opacity" })}`);
1933
+ writer.writeLine(` <span className="font-black text-2xl group-hover:text-[${primaryColor}] transition-colors">{${JSON.stringify(item.title)}}</span>`);
1934
+ writer.writeLine(` </div>`);
1935
+ }
1936
+ writer.writeLine(` </div>`);
1937
+ writer.writeLine(` </div>`);
1938
+ }
1939
+ else if (m.kind === "testimonials") {
1940
+ writer.writeLine(` <div className="py-12 space-y-8">`);
1941
+ if (m.title)
1942
+ writer.writeLine(` <h2 className="text-3xl font-bold text-center mb-12 dark:text-white">{${JSON.stringify(m.title)}}</h2>`);
1943
+ writer.writeLine(` <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">`);
1944
+ for (const item of (m.items || [])) {
1945
+ writer.writeLine(` <div className="p-6 ${radius} border border-slate-100 dark:border-slate-800 bg-slate-50 dark:bg-slate-900/50 space-y-4">`);
1946
+ writer.writeLine(` <p className="text-slate-700 dark:text-slate-300 italic leading-relaxed">"{${JSON.stringify(item.description)}}"</p>`);
1947
+ writer.writeLine(` <div className="flex items-center gap-3">`);
1948
+ if (item.image)
1949
+ writer.writeLine(` <img src="${item.image}" className="w-10 h-10 rounded-full border border-white/20" alt="" />`);
1950
+ writer.writeLine(` <div>`);
1951
+ writer.writeLine(` <p className="font-bold text-sm text-slate-800 dark:text-slate-200">{${JSON.stringify(item.author)}}</p>`);
1952
+ writer.writeLine(` <p className="text-xs text-slate-500 dark:text-slate-400">{${JSON.stringify(item.role)}}</p>`);
1953
+ writer.writeLine(` </div>`);
1954
+ writer.writeLine(` </div>`);
1955
+ writer.writeLine(` </div>`);
1956
+ }
1957
+ writer.writeLine(` </div>`);
1958
+ writer.writeLine(` </div>`);
1959
+ }
1960
+ else if (m.kind === "faq") {
1961
+ writer.writeLine(` <div className="max-w-3xl mx-auto py-12">`);
1962
+ if (m.title)
1963
+ writer.writeLine(` <h2 className="text-3xl font-bold text-center mb-10 dark:text-white">{${JSON.stringify(m.title)}}</h2>`);
1964
+ writer.writeLine(` <div className="space-y-4">`);
1965
+ for (const item of (m.items || [])) {
1966
+ writer.writeLine(` <details className="border border-slate-100 dark:border-slate-800 ${radius} bg-white dark:bg-slate-900 px-6 py-4 group transition-colors">`);
1967
+ writer.writeLine(` <summary className="flex items-center justify-between font-semibold cursor-pointer list-none dark:text-slate-200">`);
1968
+ writer.writeLine(` {${JSON.stringify(item.title)}}`);
1969
+ writer.writeLine(` <Icons.ChevronDown size={20} className="group-open:rotate-180 transition-transform text-slate-400 dark:text-slate-500" />`);
1970
+ writer.writeLine(` </summary>`);
1971
+ writer.writeLine(` <p className="mt-4 text-slate-600 dark:text-slate-400 leading-relaxed text-sm">{${JSON.stringify(item.description)}}</p>`);
1972
+ writer.writeLine(` </details>`);
1973
+ }
1974
+ writer.writeLine(` </div>`);
1975
+ writer.writeLine(` </div>`);
1976
+ }
1977
+ else if (m.kind === "cta") {
1978
+ writer.writeLine(` <div className="my-12 p-8 md:p-16 ${radius} bg-slate-900 text-center space-y-6">`);
1979
+ writer.writeLine(` <h2 className="text-3xl md:text-5xl font-black text-white">{${JSON.stringify(m.title)}}</h2>`);
1980
+ if (m.subtitle)
1981
+ writer.writeLine(` <p className="text-lg text-slate-400 max-w-2xl mx-auto">{${JSON.stringify(m.subtitle)}}</p>`);
1982
+ if (m.actions && m.actions.length > 0) {
1983
+ writer.writeLine(` <div className="flex justify-center gap-4 pt-4">`);
1984
+ for (const a of m.actions) {
1985
+ writer.writeLine(` <a href="${a.href}" className="inline-flex items-center gap-2 px-8 py-3.5 font-bold ${radius} bg-[${primaryColor}] text-white hover:scale-105 active:scale-95 shadow-xl shadow-[${primaryColor}]/20 transition-all">`);
1986
+ if (a.icon)
1987
+ writer.writeLine(` {React.createElement((Icons as any)["${a.icon}"], { size: 20 })}`);
1988
+ writer.writeLine(` {${JSON.stringify(a.label)}}`);
1989
+ writer.writeLine(` </a>`);
1990
+ }
1991
+ writer.writeLine(` </div>`);
1992
+ }
1993
+ writer.writeLine(` </div>`);
1994
+ }
1995
+ else if (m.kind === "stats") {
1996
+ writer.writeLine(` <div className="grid grid-cols-2 lg:grid-cols-4 gap-8 py-16 border-y border-slate-100 dark:border-slate-800">`);
1997
+ for (const item of (m.items || [])) {
1998
+ writer.writeLine(` <div className="text-center space-y-1 group">`);
1999
+ writer.writeLine(` <p className="text-5xl font-black text-slate-900 dark:text-white group-hover:text-[${primaryColor}] transition-colors">{${JSON.stringify(item.value)}}</p>`);
2000
+ writer.writeLine(` <p className="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">{${JSON.stringify(item.label)}}</p>`);
2001
+ writer.writeLine(` </div>`);
2002
+ }
2003
+ writer.writeLine(` </div>`);
2004
+ }
2005
+ else if (m.kind === "timeline") {
2006
+ writer.writeLine(` <div className="py-12 px-4">`);
2007
+ if (m.title)
2008
+ writer.writeLine(` <h2 className="text-3xl font-bold text-center mb-16 dark:text-white">{${JSON.stringify(m.title)}}</h2>`);
2009
+ writer.writeLine(` <div className="relative border-l-2 border-slate-100 dark:border-slate-800 ml-3 md:ml-0 md:border-l-0 md:flex md:justify-between md:gap-4 md:before:absolute md:before:top-6 md:before:left-0 md:before:w-full md:before:h-0.5 md:before:bg-slate-100 dark:md:before:bg-slate-800">`);
2010
+ for (const item of (m.items || [])) {
2011
+ writer.writeLine(` <div className="relative pl-8 pb-10 md:pl-0 md:pt-12 md:pb-0 md:flex-1 text-left md:text-center">`);
2012
+ writer.writeLine(` <div className="absolute top-0 left-[-9px] md:left-1/2 md:-translate-x-1/2 w-4 h-4 rounded-full bg-white dark:bg-slate-900 border-4 border-[${primaryColor}] z-10 transition-transform hover:scale-125"></div>`);
2013
+ writer.writeLine(` <h3 className="font-bold text-slate-900 dark:text-slate-100 mb-1">{${JSON.stringify(item.title)}}</h3>`);
2014
+ writer.writeLine(` <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed">{${JSON.stringify(item.description)}}</p>`);
2015
+ writer.writeLine(` </div>`);
2016
+ }
2017
+ writer.writeLine(` </div>`);
2018
+ writer.writeLine(` </div>`);
2019
+ }
2020
+ }
2021
+ //# sourceMappingURL=frontend-react.js.map