davaux 0.8.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 (333) hide show
  1. package/BASELINE.md +169 -0
  2. package/CLAUDE.md +518 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -0
  5. package/ROADMAP.md +198 -0
  6. package/build.mjs +101 -0
  7. package/client/control.ts +247 -0
  8. package/client/hydrate.ts +37 -0
  9. package/client/index.ts +19 -0
  10. package/client/jsx-runtime.ts +209 -0
  11. package/client/resource.ts +122 -0
  12. package/client/signal.ts +211 -0
  13. package/client/store.ts +110 -0
  14. package/client/useHead.ts +63 -0
  15. package/dist/build/config.d.ts +3 -0
  16. package/dist/build/config.d.ts.map +1 -0
  17. package/dist/build/config.js +38 -0
  18. package/dist/build/config.js.map +7 -0
  19. package/dist/build/index.d.ts +2 -0
  20. package/dist/build/index.d.ts.map +1 -0
  21. package/dist/build/index.js +13 -0
  22. package/dist/build/index.js.map +7 -0
  23. package/dist/build/plugins.d.ts +7 -0
  24. package/dist/build/plugins.d.ts.map +1 -0
  25. package/dist/build/plugins.js +85 -0
  26. package/dist/build/plugins.js.map +7 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +427 -0
  30. package/dist/cli.js.map +7 -0
  31. package/dist/client/control.d.ts +49 -0
  32. package/dist/client/control.d.ts.map +1 -0
  33. package/dist/client/control.js +154 -0
  34. package/dist/client/control.js.map +7 -0
  35. package/dist/client/hydrate.d.ts +7 -0
  36. package/dist/client/hydrate.d.ts.map +1 -0
  37. package/dist/client/hydrate.js +23 -0
  38. package/dist/client/hydrate.js.map +7 -0
  39. package/dist/client/index.d.ts +12 -0
  40. package/dist/client/index.d.ts.map +1 -0
  41. package/dist/client/index.js +32 -0
  42. package/dist/client/index.js.map +7 -0
  43. package/dist/client/jsx-runtime.d.ts +40 -0
  44. package/dist/client/jsx-runtime.d.ts.map +1 -0
  45. package/dist/client/jsx-runtime.js +139 -0
  46. package/dist/client/jsx-runtime.js.map +7 -0
  47. package/dist/client/resource.d.ts +31 -0
  48. package/dist/client/resource.d.ts.map +1 -0
  49. package/dist/client/resource.js +64 -0
  50. package/dist/client/resource.js.map +7 -0
  51. package/dist/client/signal.d.ts +90 -0
  52. package/dist/client/signal.d.ts.map +1 -0
  53. package/dist/client/signal.js +115 -0
  54. package/dist/client/signal.js.map +7 -0
  55. package/dist/client/store.d.ts +26 -0
  56. package/dist/client/store.d.ts.map +1 -0
  57. package/dist/client/store.js +63 -0
  58. package/dist/client/store.js.map +7 -0
  59. package/dist/client/useHead.d.ts +28 -0
  60. package/dist/client/useHead.d.ts.map +1 -0
  61. package/dist/client/useHead.js +33 -0
  62. package/dist/client/useHead.js.map +7 -0
  63. package/dist/config.d.ts +182 -0
  64. package/dist/config.d.ts.map +1 -0
  65. package/dist/config.js +21 -0
  66. package/dist/config.js.map +7 -0
  67. package/dist/create-multisite.d.ts +2 -0
  68. package/dist/create-multisite.d.ts.map +1 -0
  69. package/dist/create-multisite.js +291 -0
  70. package/dist/create-multisite.js.map +7 -0
  71. package/dist/create.d.ts +2 -0
  72. package/dist/create.d.ts.map +1 -0
  73. package/dist/create.js +179 -0
  74. package/dist/create.js.map +7 -0
  75. package/dist/dev/blueprints.d.ts +11 -0
  76. package/dist/dev/blueprints.d.ts.map +1 -0
  77. package/dist/dev/blueprints.js +65 -0
  78. package/dist/dev/blueprints.js.map +7 -0
  79. package/dist/dev/components.d.ts +19 -0
  80. package/dist/dev/components.d.ts.map +1 -0
  81. package/dist/dev/components.js +87 -0
  82. package/dist/dev/components.js.map +7 -0
  83. package/dist/dev/insert.d.ts +11 -0
  84. package/dist/dev/insert.d.ts.map +1 -0
  85. package/dist/dev/insert.js +160 -0
  86. package/dist/dev/insert.js.map +7 -0
  87. package/dist/dev/remove.d.ts +53 -0
  88. package/dist/dev/remove.d.ts.map +1 -0
  89. package/dist/dev/remove.js +518 -0
  90. package/dist/dev/remove.js.map +7 -0
  91. package/dist/dev/watch.d.ts +26 -0
  92. package/dist/dev/watch.d.ts.map +1 -0
  93. package/dist/dev/watch.js +2905 -0
  94. package/dist/dev/watch.js.map +7 -0
  95. package/dist/errors.d.ts +6 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +63 -0
  98. package/dist/errors.js.map +7 -0
  99. package/dist/generate.d.ts +2 -0
  100. package/dist/generate.d.ts.map +1 -0
  101. package/dist/generate.js +191 -0
  102. package/dist/generate.js.map +7 -0
  103. package/dist/index.d.ts +9 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +57 -0
  106. package/dist/index.js.map +7 -0
  107. package/dist/island.d.ts +24 -0
  108. package/dist/island.d.ts.map +1 -0
  109. package/dist/island.js +15 -0
  110. package/dist/island.js.map +7 -0
  111. package/dist/jsx-runtime.d.ts +406 -0
  112. package/dist/jsx-runtime.d.ts.map +1 -0
  113. package/dist/jsx-runtime.js +90 -0
  114. package/dist/jsx-runtime.js.map +7 -0
  115. package/dist/link.d.ts +27 -0
  116. package/dist/link.d.ts.map +1 -0
  117. package/dist/link.js +29 -0
  118. package/dist/link.js.map +7 -0
  119. package/dist/oml/fragment.d.ts +16 -0
  120. package/dist/oml/fragment.d.ts.map +1 -0
  121. package/dist/oml/fragment.js +26 -0
  122. package/dist/oml/fragment.js.map +7 -0
  123. package/dist/oml/index.d.ts +11 -0
  124. package/dist/oml/index.d.ts.map +1 -0
  125. package/dist/oml/index.js +21 -0
  126. package/dist/oml/index.js.map +7 -0
  127. package/dist/oml/jsx-runtime.d.ts +34 -0
  128. package/dist/oml/jsx-runtime.d.ts.map +1 -0
  129. package/dist/oml/jsx-runtime.js +59 -0
  130. package/dist/oml/jsx-runtime.js.map +7 -0
  131. package/dist/oml/jsx.d.ts +14 -0
  132. package/dist/oml/jsx.d.ts.map +1 -0
  133. package/dist/oml/jsx.js +96 -0
  134. package/dist/oml/jsx.js.map +7 -0
  135. package/dist/oml/page.d.ts +7 -0
  136. package/dist/oml/page.d.ts.map +1 -0
  137. package/dist/oml/page.js +6 -0
  138. package/dist/oml/page.js.map +7 -0
  139. package/dist/oml/render.d.ts +13 -0
  140. package/dist/oml/render.d.ts.map +1 -0
  141. package/dist/oml/render.js +117 -0
  142. package/dist/oml/render.js.map +7 -0
  143. package/dist/oml/types.d.ts +79 -0
  144. package/dist/oml/types.d.ts.map +1 -0
  145. package/dist/oml/types.js +64 -0
  146. package/dist/oml/types.js.map +7 -0
  147. package/dist/router/handler.d.ts +53 -0
  148. package/dist/router/handler.d.ts.map +1 -0
  149. package/dist/router/handler.js +342 -0
  150. package/dist/router/handler.js.map +7 -0
  151. package/dist/router/matcher.d.ts +21 -0
  152. package/dist/router/matcher.d.ts.map +1 -0
  153. package/dist/router/matcher.js +28 -0
  154. package/dist/router/matcher.js.map +7 -0
  155. package/dist/router/scanner.d.ts +17 -0
  156. package/dist/router/scanner.d.ts.map +1 -0
  157. package/dist/router/scanner.js +197 -0
  158. package/dist/router/scanner.js.map +7 -0
  159. package/dist/server/index.d.ts +23 -0
  160. package/dist/server/index.d.ts.map +1 -0
  161. package/dist/server/index.js +29 -0
  162. package/dist/server/index.js.map +7 -0
  163. package/dist/signal.d.ts +15 -0
  164. package/dist/signal.d.ts.map +1 -0
  165. package/dist/signal.js +29 -0
  166. package/dist/signal.js.map +7 -0
  167. package/dist/ssg.d.ts +45 -0
  168. package/dist/ssg.d.ts.map +1 -0
  169. package/dist/ssg.js +175 -0
  170. package/dist/ssg.js.map +7 -0
  171. package/dist/test/actions.test.d.ts +2 -0
  172. package/dist/test/actions.test.d.ts.map +1 -0
  173. package/dist/test/body-limits.test.d.ts +2 -0
  174. package/dist/test/body-limits.test.d.ts.map +1 -0
  175. package/dist/test/errors.test.d.ts +2 -0
  176. package/dist/test/errors.test.d.ts.map +1 -0
  177. package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
  178. package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
  179. package/dist/test/fixtures/routes/_error.d.ts +3 -0
  180. package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
  181. package/dist/test/fixtures/routes/_global.d.ts +3 -0
  182. package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
  183. package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
  184. package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
  185. package/dist/test/fixtures/routes/_layout.d.ts +3 -0
  186. package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
  187. package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
  188. package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
  189. package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
  190. package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
  191. package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
  192. package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
  193. package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
  194. package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
  195. package/dist/test/fixtures/routes/about.page.d.ts +3 -0
  196. package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
  197. package/dist/test/fixtures/routes/action.page.d.ts +6 -0
  198. package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
  199. package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
  200. package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
  201. package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
  202. package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
  203. package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
  204. package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
  205. package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
  206. package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
  207. package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
  208. package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
  209. package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
  210. package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
  211. package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
  212. package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
  213. package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
  214. package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
  215. package/dist/test/fixtures/routes/index.page.d.ts +3 -0
  216. package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
  217. package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
  218. package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
  219. package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
  220. package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
  221. package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
  222. package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
  223. package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
  224. package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
  225. package/dist/test/fixtures/routes/state.page.d.ts +3 -0
  226. package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
  227. package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
  228. package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
  229. package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
  230. package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
  231. package/dist/test/helpers.d.ts +37 -0
  232. package/dist/test/helpers.d.ts.map +1 -0
  233. package/dist/test/layouts.test.d.ts +2 -0
  234. package/dist/test/layouts.test.d.ts.map +1 -0
  235. package/dist/test/middleware.test.d.ts +2 -0
  236. package/dist/test/middleware.test.d.ts.map +1 -0
  237. package/dist/test/multipart.test.d.ts +2 -0
  238. package/dist/test/multipart.test.d.ts.map +1 -0
  239. package/dist/test/oml-routing.test.d.ts +2 -0
  240. package/dist/test/oml-routing.test.d.ts.map +1 -0
  241. package/dist/test/oml.test.d.ts +2 -0
  242. package/dist/test/oml.test.d.ts.map +1 -0
  243. package/dist/test/redirects.test.d.ts +2 -0
  244. package/dist/test/redirects.test.d.ts.map +1 -0
  245. package/dist/test/routing.test.d.ts +2 -0
  246. package/dist/test/routing.test.d.ts.map +1 -0
  247. package/dist/test/ssg.test.d.ts +2 -0
  248. package/dist/test/ssg.test.d.ts.map +1 -0
  249. package/dist/test/web-response.test.d.ts +2 -0
  250. package/dist/test/web-response.test.d.ts.map +1 -0
  251. package/dist/types.d.ts +314 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +292 -0
  254. package/dist/types.js.map +7 -0
  255. package/package.json +103 -0
  256. package/pka.config.json +32 -0
  257. package/src/build/config.ts +42 -0
  258. package/src/build/index.ts +6 -0
  259. package/src/build/plugins.ts +118 -0
  260. package/src/cli.ts +502 -0
  261. package/src/config.ts +197 -0
  262. package/src/create-multisite.ts +310 -0
  263. package/src/create.ts +194 -0
  264. package/src/dev/blueprints.ts +75 -0
  265. package/src/dev/components.ts +108 -0
  266. package/src/dev/insert.ts +221 -0
  267. package/src/dev/remove.ts +677 -0
  268. package/src/dev/watch.ts +3098 -0
  269. package/src/env.d.ts +5 -0
  270. package/src/errors.ts +64 -0
  271. package/src/generate.ts +228 -0
  272. package/src/index.ts +67 -0
  273. package/src/island.ts +47 -0
  274. package/src/jsx-runtime.d.ts +408 -0
  275. package/src/jsx-runtime.d.ts.map +1 -0
  276. package/src/jsx-runtime.ts +536 -0
  277. package/src/link.ts +49 -0
  278. package/src/oml/fragment.ts +54 -0
  279. package/src/oml/index.ts +21 -0
  280. package/src/oml/jsx-runtime.ts +121 -0
  281. package/src/oml/jsx.ts +151 -0
  282. package/src/oml/page.ts +13 -0
  283. package/src/oml/render.ts +181 -0
  284. package/src/oml/types.ts +159 -0
  285. package/src/router/handler.ts +515 -0
  286. package/src/router/matcher.ts +52 -0
  287. package/src/router/scanner.ts +272 -0
  288. package/src/server/index.ts +49 -0
  289. package/src/signal.ts +39 -0
  290. package/src/ssg.ts +253 -0
  291. package/src/test/actions.test.ts +40 -0
  292. package/src/test/body-limits.test.ts +83 -0
  293. package/src/test/errors.test.ts +53 -0
  294. package/src/test/fixtures/routes/[id].page.ts +3 -0
  295. package/src/test/fixtures/routes/_error.ts +6 -0
  296. package/src/test/fixtures/routes/_global.ts +8 -0
  297. package/src/test/fixtures/routes/_layout-template.ts +7 -0
  298. package/src/test/fixtures/routes/_layout.ts +7 -0
  299. package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
  300. package/src/test/fixtures/routes/_middleware.ts +8 -0
  301. package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
  302. package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
  303. package/src/test/fixtures/routes/about.page.ts +6 -0
  304. package/src/test/fixtures/routes/action.page.ts +11 -0
  305. package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
  306. package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
  307. package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
  308. package/src/test/fixtures/routes/api/upload.post.ts +14 -0
  309. package/src/test/fixtures/routes/api/users.get.ts +3 -0
  310. package/src/test/fixtures/routes/api/xml.get.ts +5 -0
  311. package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
  312. package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
  313. package/src/test/fixtures/routes/index.page.ts +3 -0
  314. package/src/test/fixtures/routes/oml.page.ts +7 -0
  315. package/src/test/fixtures/routes/redirect.page.ts +3 -0
  316. package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
  317. package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
  318. package/src/test/fixtures/routes/state.page.ts +4 -0
  319. package/src/test/fixtures/routes/throw.page.ts +5 -0
  320. package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
  321. package/src/test/helpers.ts +132 -0
  322. package/src/test/layouts.test.ts +76 -0
  323. package/src/test/middleware.test.ts +69 -0
  324. package/src/test/multipart.test.ts +91 -0
  325. package/src/test/oml-routing.test.ts +59 -0
  326. package/src/test/oml.test.ts +429 -0
  327. package/src/test/redirects.test.ts +32 -0
  328. package/src/test/routing.test.ts +118 -0
  329. package/src/test/ssg.test.ts +273 -0
  330. package/src/test/web-response.test.ts +33 -0
  331. package/src/types.ts +670 -0
  332. package/tsconfig.client.json +17 -0
  333. package/tsconfig.json +20 -0
@@ -0,0 +1,3098 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { createReadStream, existsSync, watch as fsWatch, readFileSync, statSync, writeFileSync } from 'node:fs'
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+ import { createRequire } from 'node:module'
5
+ import { extname, join, relative } from 'node:path'
6
+ import { context, type Plugin } from 'esbuild'
7
+ import { cssCollectorPlugin, generateIslandsEntry, islandServerPlugin } from '../build/plugins.js'
8
+ import {
9
+ collectEsbuildPlugins,
10
+ collectScannerSuffixes,
11
+ type DavauxPlugin,
12
+ pathsToAlias,
13
+ } from '../config.js'
14
+ import { formatBuildErrors } from '../errors.js'
15
+ import type { OmlPropSchema } from '../oml/types.js'
16
+ import { buildApp } from '../router/handler.js'
17
+ import { scanIslands, scanRoutes } from '../router/scanner.js'
18
+ import { startServer } from '../server/index.js'
19
+ import type { IslandFile, ScanResult } from '../types.js'
20
+ import { scanBlueprints } from './blueprints.js'
21
+ import { saveBlueprint, scanComponents, updateBlueprint } from './components.js'
22
+ import { insertAfterElement, insertJsx } from './insert.js'
23
+ import { getComponentFragment, getElementFragment, moveNode, removeElement, removeJsx, replaceComponentFragment, replaceElementAttrs, replaceElementFragment, replaceJsx, replaceTextContent } from './remove.js'
24
+
25
+ const MIME: Record<string, string> = {
26
+ '.js': 'application/javascript',
27
+ '.mjs': 'application/javascript',
28
+ '.css': 'text/css',
29
+ '.html': 'text/html',
30
+ '.json': 'application/json',
31
+ '.png': 'image/png',
32
+ '.jpg': 'image/jpeg',
33
+ '.jpeg': 'image/jpeg',
34
+ '.gif': 'image/gif',
35
+ '.svg': 'image/svg+xml',
36
+ '.ico': 'image/x-icon',
37
+ '.woff': 'font/woff',
38
+ '.woff2': 'font/woff2',
39
+ }
40
+
41
+ let _partialUpdatesScript: string | null = null
42
+ function getPartialUpdatesScript(): string {
43
+ if (_partialUpdatesScript !== null) return _partialUpdatesScript
44
+ try {
45
+ const r = createRequire(import.meta.url)
46
+ const templateForCjs = r.resolve('template-for-polyfill')
47
+ const templateForJs = readFileSync(
48
+ templateForCjs.replace(/template-for-polyfill\.cjs$/, 'template-for-polyfill.js'),
49
+ 'utf-8',
50
+ )
51
+ const htmlSettersMain = r.resolve('html-setters-polyfill')
52
+ const htmlSettersJs = readFileSync(
53
+ htmlSettersMain.replace(/index\.js$/, 'index.min.js'),
54
+ 'utf-8',
55
+ )
56
+ _partialUpdatesScript = `${templateForJs}\n${htmlSettersJs}`
57
+ } catch {
58
+ _partialUpdatesScript = '/* partial-updates polyfills unavailable */'
59
+ }
60
+ return _partialUpdatesScript
61
+ }
62
+
63
+ function serveStatic(
64
+ publicDir: string,
65
+ ): ((req: IncomingMessage, res: ServerResponse) => boolean) | null {
66
+ if (!existsSync(publicDir)) return null
67
+ return (req, res): boolean => {
68
+ const filePath = join(publicDir, new URL(req.url ?? '/', 'http://x').pathname)
69
+ if (!filePath.startsWith(`${publicDir}/`) && filePath !== publicDir) return false
70
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) return false
71
+ const mime = MIME[extname(filePath)] ?? 'application/octet-stream'
72
+ res.writeHead(200, { 'Content-Type': mime })
73
+ createReadStream(filePath).pipe(res)
74
+ return true
75
+ }
76
+ }
77
+
78
+ // ─── Live reload (SSE) ────────────────────────────────────────────────────────
79
+
80
+ const sseClients = new Set<ServerResponse>()
81
+ let serverReady = false
82
+ let reloadTimer: ReturnType<typeof setTimeout> | undefined
83
+
84
+ function sendToClients(data: string): void {
85
+ for (const client of sseClients) {
86
+ try {
87
+ client.write(`data: ${data}\n\n`)
88
+ } catch {
89
+ sseClients.delete(client)
90
+ }
91
+ }
92
+ }
93
+
94
+ function scheduleReload(): void {
95
+ if (!serverReady) return
96
+ if (reloadTimer) clearTimeout(reloadTimer)
97
+ reloadTimer = setTimeout(() => {
98
+ reloadTimer = undefined
99
+ sendToClients('reload')
100
+ }, 50)
101
+ }
102
+
103
+ // SharedWorker script: one SSE connection per origin, fans messages to all tab ports.
104
+ // Falls back to a per-tab EventSource when SharedWorker is unavailable.
105
+ const SHARED_WORKER_SCRIPT = `var ports=[];var source=null;
106
+ self.onconnect=function(ev){
107
+ var port=ev.ports[0];port.start();ports.push(port);
108
+ if(!source){
109
+ source=new EventSource('/_davaux/livereload');
110
+ source.onmessage=function(e){
111
+ ports=ports.filter(function(p){try{p.postMessage(e.data);return true}catch(_){return false}});
112
+ };
113
+ }
114
+ };`
115
+
116
+ const LIVERELOAD_SCRIPT = `;(function(){
117
+ var overlay=null
118
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
119
+ function show(errors){
120
+ if(!overlay){
121
+ overlay=document.createElement('div')
122
+ overlay.style.cssText='position:fixed;inset:0;z-index:99999;overflow:auto;background:#0d0d0d;color:#e8e8e8;font:13px/1.6 monospace;padding:2rem;box-sizing:border-box'
123
+ document.body.appendChild(overlay)
124
+ }
125
+ overlay.innerHTML='<p style="color:#ff5555;font-size:1.1em;margin:0 0 1.5rem 0"><b>[davaux] Build Error</b></p>'+
126
+ errors.map(function(e){
127
+ var loc=e.file?(e.file+(e.line?':'+e.line+':'+e.column:'')):'unknown'
128
+ return '<div style="margin-bottom:1.25rem;border:1px solid #ff3333;border-radius:6px;padding:1rem">'+
129
+ '<div style="color:#888;font-size:0.9em;margin-bottom:0.4rem">'+esc(loc)+'</div>'+
130
+ '<div style="color:#ff6b6b">'+esc(e.text)+'</div>'+
131
+ (e.lineText?'<pre style="margin:0.75rem 0 0;padding:0.5rem;background:#1a1a1a;border-radius:4px;overflow:auto;color:#aaa">'+esc(e.lineText)+'</pre>':'')+
132
+ '</div>'
133
+ }).join('')
134
+ }
135
+ function handle(data){
136
+ if(data==='reload'){location.reload()}
137
+ else if(data.slice(0,6)==='error:'){show(JSON.parse(data.slice(6)))}
138
+ }
139
+ if(typeof SharedWorker!=='undefined'){
140
+ var w=new SharedWorker('/_davaux/livereload-worker.js')
141
+ w.port.onmessage=function(ev){handle(ev.data)}
142
+ w.port.start()
143
+ } else {
144
+ new EventSource('/_davaux/livereload').onmessage=function(ev){handle(ev.data)}
145
+ }
146
+ })()`
147
+
148
+ const INSPECTOR_SCRIPT = `;(function(){
149
+ if(window!==window.top)return
150
+ var KEY='_dv_ins'
151
+ var isOpen=sessionStorage.getItem(KEY)==='1'
152
+ var host=document.createElement('div')
153
+ var shadow=host.attachShadow({mode:'open'})
154
+ document.body.appendChild(host)
155
+ var st=document.createElement('style')
156
+ st.textContent=':host{all:initial;display:block;position:fixed;bottom:12px;right:12px;z-index:2147483647;font:13px/1.5 ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace}:host(.open){inset:0}*{box-sizing:border-box}.badge{display:block;padding:4px 10px;background:#0f0f1a;color:#7cc5f0;border:1px solid #2a2a4a;border-radius:20px;cursor:pointer;font:700 11px/1 inherit;letter-spacing:.05em;box-shadow:0 2px 8px rgba(0,0,0,.4);white-space:nowrap}.badge:hover{background:#1a1a2e}.edbtn{display:block;margin-top:6px;padding:4px 10px;background:#1a2a1a;color:#7eca9c;border:1px solid #2a4a2a;border-radius:20px;font:700 11px/1 inherit;letter-spacing:.05em;text-decoration:none;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.4);white-space:nowrap}.edbtn:hover{background:#243a24}:host(.open) .badge,:host(.open) .edbtn{display:none}.panel{display:none;position:absolute;inset:0;background:#0f0f1a;flex-direction:column;overflow:hidden}:host(.open) .panel{display:flex}.hd{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#080816;border-bottom:1px solid #2a2a4a;flex-shrink:0}.title{color:#7cc5f0;font-weight:700;font-size:11px;letter-spacing:.1em;flex:1}.chip{font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;letter-spacing:.05em;display:none}.chip.oml{display:inline;background:#1a2a1a;color:#7eca9c;border:1px solid #2a4a2a}.chip.dom{display:inline;background:#0f0f1a;color:#aaa;border:1px solid #333}.xb{background:none;border:none;color:#555;cursor:pointer;font:inherit;padding:0;line-height:1}.xb:hover{color:#e8e8e8}.bd{overflow:auto;flex:1;padding:6px 0}.row{display:flex;align-items:baseline;padding:1px 8px;white-space:nowrap;border-radius:3px;color:#e8e8e8;cursor:default;-webkit-user-select:none;user-select:none}.row.c:hover{background:#1a1a2e;cursor:pointer}.tog{width:14px;font-size:9px;color:#666;flex-shrink:0;text-align:center}.el{color:#7cc5f0}.co{color:#7eca9c}.fr{color:#666}.tx{color:#888;font-style:italic}.pk{color:#b89eff}.pv{color:#f1c27d}.empty{padding:12px;color:#555;font-style:italic;text-align:center}'
157
+ shadow.appendChild(st)
158
+ ;(function(){var p=(window.__DVX__&&window.__DVX__.pos)||'bottom-right';if(p!=='bottom-right'){var ps=document.createElement('style');var css=':host:not(.open){';css+=p.indexOf('top')>-1?'top:12px;bottom:auto;':'bottom:12px;top:auto;';css+=p.indexOf('left')>-1?'left:12px;right:auto;':'right:12px;left:auto;';css+='}';ps.textContent=css;shadow.appendChild(ps)}})()
159
+ var badge=document.createElement('button')
160
+ badge.className='badge'
161
+ badge.textContent=(window.__DVX__&&window.__DVX__.label)||'Inspector'
162
+ badge.title=((window.__DVX__&&window.__DVX__.label)||'Inspector')+' (Ctrl+Shift+O)'
163
+ shadow.appendChild(badge)
164
+ var edbtn=document.createElement('a')
165
+ edbtn.className='edbtn'
166
+ edbtn.textContent='Edit'
167
+ edbtn.title='Visual Editor (Ctrl+Shift+E)'
168
+ edbtn.href='/_davaux/editor?page='+encodeURIComponent(location.pathname+location.search)
169
+ shadow.appendChild(edbtn)
170
+ var panel=document.createElement('div')
171
+ panel.className='panel'
172
+ var hd=document.createElement('div')
173
+ hd.className='hd'
174
+ var ttl=document.createElement('span')
175
+ ttl.className='title'
176
+ ttl.textContent='INSPECTOR'
177
+ var chip=document.createElement('span')
178
+ chip.className='chip'
179
+ var xb=document.createElement('button')
180
+ xb.className='xb'
181
+ xb.textContent='✕'
182
+ xb.title='Close'
183
+ hd.appendChild(ttl)
184
+ hd.appendChild(chip)
185
+ hd.appendChild(xb)
186
+ var bd=document.createElement('div')
187
+ bd.className='bd'
188
+ panel.appendChild(hd)
189
+ panel.appendChild(bd)
190
+ shadow.appendChild(panel)
191
+ function setOpen(v){
192
+ isOpen=!!v
193
+ sessionStorage.setItem(KEY,v?'1':'0')
194
+ host.classList.toggle('open',!!v)
195
+ if(v)load()
196
+ }
197
+ badge.onclick=function(){setOpen(!isOpen)}
198
+ xb.onclick=function(){setOpen(false)}
199
+ document.addEventListener('keydown',function(e){
200
+ if(e.ctrlKey&&e.shiftKey&&(e.key==='O'||e.key==='o')){e.preventDefault();setOpen(!isOpen)}
201
+ if(e.ctrlKey&&e.shiftKey&&(e.key==='E'||e.key==='e')){e.preventDefault();window.location.href='/_davaux/editor?page='+encodeURIComponent(location.pathname+location.search)}
202
+ })
203
+ if(isOpen)setOpen(true)
204
+ function setMode(mode){
205
+ badge.textContent=mode
206
+ chip.textContent=mode
207
+ chip.className='chip '+mode.toLowerCase()
208
+ }
209
+ function load(){
210
+ bd.innerHTML='<div class="empty">Loading…</div>'
211
+ fetch('/_davaux/inspector?url='+encodeURIComponent(location.pathname+location.search))
212
+ .then(function(r){return r.json()})
213
+ .then(function(d){
214
+ bd.innerHTML=''
215
+ if(d.error){
216
+ setMode('DOM')
217
+ var bodySnap=domSnap(document.body)
218
+ if(bodySnap)bd.appendChild(tree(bodySnap,0))
219
+ return
220
+ }
221
+ setMode('OML')
222
+ bd.appendChild(tree(d.node,0))
223
+ })
224
+ .catch(function(){bd.innerHTML='<div class="empty">No tree available</div>'})
225
+ }
226
+ function domSnap(el){
227
+ if(el===host)return null
228
+ if(el.nodeType===3){var v=(el.textContent||'').trim();return v?{type:'#text',value:v}:null}
229
+ if(el.nodeType!==1)return null
230
+ var tag=el.tagName.toLowerCase()
231
+ var p={};for(var i=0;i<el.attributes.length;i++){var a=el.attributes[i];p[a.name]=a.value}
232
+ var cs=[];for(var j=0;j<el.childNodes.length;j++){var c=domSnap(el.childNodes[j]);if(c!==null)cs.push(c)}
233
+ return{type:'element',tag:tag,id:undefined,props:p,children:cs}
234
+ }
235
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
236
+ function propHtml(p){
237
+ if(!p)return''
238
+ var ks=Object.keys(p).filter(function(k){return!/^on[A-Z]/.test(k)&&k!=='children'&&k!=='dangerouslySetInnerHTML'})
239
+ var out=ks.slice(0,3).map(function(k){
240
+ var v=p[k],s=typeof v==='string'?'"'+(v.length>18?v.slice(0,18)+'…':esc(v))+'"':esc(String(v))
241
+ return' <span class="pk">'+esc(k)+'</span>=<span class="pv">'+s+'</span>'
242
+ }).join('')
243
+ if(ks.length>3)out+=' <span style="color:#555">+'+(ks.length-3)+'</span>'
244
+ return out
245
+ }
246
+ function tree(node,depth){
247
+ var w=document.createElement('div')
248
+ var pad=(depth*14+8)+'px'
249
+ if(node===null){
250
+ var r=document.createElement('div')
251
+ r.className='row'
252
+ r.style.paddingLeft=pad
253
+ r.innerHTML='<span class="tog"> </span><span class="fr">null</span>'
254
+ w.appendChild(r)
255
+ return w
256
+ }
257
+ if(node.type==='#text'){
258
+ var v=String(node.value||'')
259
+ var r=document.createElement('div')
260
+ r.className='row'
261
+ r.style.paddingLeft=pad
262
+ r.innerHTML='<span class="tog"> </span><span class="tx">"'+(v.length>60?esc(v.slice(0,60))+'…':esc(v))+'"</span>'
263
+ w.appendChild(r)
264
+ return w
265
+ }
266
+ if(node.type==='#raw'){
267
+ var v=String(node.value||'')
268
+ var r=document.createElement('div')
269
+ r.className='row'
270
+ r.style.paddingLeft=pad
271
+ r.innerHTML='<span class="tog"> </span><span class="fr">[raw html] </span><span class="tx">'+(v.length>60?esc(v.slice(0,60))+'…':esc(v))+'</span>'
272
+ w.appendChild(r)
273
+ return w
274
+ }
275
+ var cs=node.type==='#component'
276
+ ?(node.output&&node.output.type!=='#raw'?[node.output]:(node.children||[]).filter(function(c){return c!==null}))
277
+ :(node.children||[]).filter(function(c){return c!==null})
278
+ var hk=cs.length>0
279
+ var closed=false
280
+ var row=document.createElement('div')
281
+ row.className='row'+(hk?' c':'')
282
+ row.style.paddingLeft=pad
283
+ var tog=document.createElement('span')
284
+ tog.className='tog'
285
+ tog.textContent=hk?'▼':' '
286
+ var lbl=document.createElement('span')
287
+ if(node.type==='element'){
288
+ lbl.innerHTML='<span class="el">&lt;'+esc(node.tag)+'</span>'+propHtml(node.props)+'<span class="el">&gt;</span>'
289
+ }else if(node.type==='#component'){
290
+ lbl.innerHTML='<span class="co">◈ '+esc(node.name)+'</span>'+propHtml(node.props)
291
+ }else{
292
+ lbl.innerHTML='<span class="fr">&lt;&gt;</span>'
293
+ }
294
+ row.appendChild(tog)
295
+ row.appendChild(lbl)
296
+ w.appendChild(row)
297
+ if(hk){
298
+ var kc=document.createElement('div')
299
+ cs.forEach(function(c){kc.appendChild(tree(c,depth+1))})
300
+ w.appendChild(kc)
301
+ row.onclick=function(){
302
+ closed=!closed
303
+ kc.style.display=closed?'none':''
304
+ tog.textContent=closed?'▶':'▼'
305
+ }
306
+ }
307
+ return w
308
+ }
309
+ })()`
310
+
311
+ const EDITOR_HTML = `<!DOCTYPE html>
312
+ <html lang="en">
313
+ <head>
314
+ <meta charset="utf-8">
315
+ <title>Davaux Visual Editor</title>
316
+ <script type="importmap">
317
+ {
318
+ "imports": {
319
+ "https://esm.sh/@codemirror/state@^6.6.0?target=es2022": "https://esm.sh/@codemirror/state@6.6.0/es2022/state.mjs",
320
+ "https://esm.sh/@codemirror/state@^6.0.0?target=es2022": "https://esm.sh/@codemirror/state@6.6.0/es2022/state.mjs",
321
+ "https://esm.sh/@codemirror/view@^6.27.0?target=es2022": "https://esm.sh/@codemirror/view@6.43.0/es2022/view.mjs",
322
+ "https://esm.sh/@codemirror/view@^6.23.0?target=es2022": "https://esm.sh/@codemirror/view@6.43.0/es2022/view.mjs",
323
+ "https://esm.sh/@codemirror/language@^6.0.0?target=es2022": "https://esm.sh/@codemirror/language@6.12.3/es2022/language.mjs",
324
+ "https://esm.sh/@codemirror/language@^6.3.0?target=es2022": "https://esm.sh/@codemirror/language@6.12.3/es2022/language.mjs",
325
+ "https://esm.sh/@codemirror/autocomplete@^6.7.1?target=es2022": "https://esm.sh/@codemirror/autocomplete@6.20.2/es2022/autocomplete.mjs",
326
+ "https://esm.sh/@codemirror/lang-html@^6.0.0?target=es2022": "https://esm.sh/@codemirror/lang-html@6.4.11/es2022/lang-html.mjs",
327
+ "https://esm.sh/@lezer/common@^1.1.0?target=es2022": "https://esm.sh/@lezer/common@1.5.2/es2022/common.mjs",
328
+ "https://esm.sh/@lezer/common@^1.5.0?target=es2022": "https://esm.sh/@lezer/common@1.5.2/es2022/common.mjs",
329
+ "https://esm.sh/@lezer/highlight@^1.0.0?target=es2022": "https://esm.sh/@lezer/highlight@1.2.3/es2022/highlight.mjs",
330
+ "https://esm.sh/crelt@^1.0.6?target=es2022": "https://esm.sh/crelt@1.0.6/es2022/crelt.mjs"
331
+ }
332
+ }
333
+ </script>
334
+ <style>
335
+ :root{
336
+ --dvx-bg:#0d0d1a;--dvx-surf:#0f0f1a;--dvx-hd:#080816;--dvx-inp:#1a1a2e;
337
+ --dvx-bdr:#2a2a4a;--dvx-bdr-hi:#4a4a8a;
338
+ --dvx-txt:#e8e8e8;--dvx-txt-dim:#aaa;--dvx-txt-faint:#666;--dvx-txt-ghost:#555;
339
+ --dvx-row-hover:#1a1a2e;--dvx-row-sel:#1a2a3a;
340
+ }
341
+ [data-dvx-theme="light"]{
342
+ --dvx-bg:#f8f9fa;--dvx-surf:#ffffff;--dvx-hd:#e9ecef;--dvx-inp:#f1f3f5;
343
+ --dvx-bdr:#ced4da;--dvx-bdr-hi:#4c6ef5;
344
+ --dvx-txt:#212529;--dvx-txt-dim:#343a40;--dvx-txt-faint:#495057;--dvx-txt-ghost:#6c757d;
345
+ --dvx-row-hover:#f1f3f5;--dvx-row-sel:#dbe4ff;
346
+ }
347
+ *{box-sizing:border-box;margin:0;padding:0}
348
+ body{display:flex;flex-direction:column;height:100vh;background:var(--dvx-bg);color:var(--dvx-txt);font:13px/1.5 ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace}
349
+ header{display:flex;align-items:center;gap:12px;padding:8px 16px;background:var(--dvx-hd);border-bottom:1px solid var(--dvx-bdr);flex-shrink:0}
350
+ .logo{color:#7cc5f0;font-weight:700;font-size:11px;letter-spacing:.1em}
351
+ .page-bar{display:flex;align-items:center;gap:8px;flex:1}
352
+ .page-bar span{color:var(--dvx-txt-faint);font-size:11px}
353
+ .pg-wrap{position:relative;display:inline-flex;align-items:center}
354
+ .page-input{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);color:var(--dvx-txt);padding:3px 22px 3px 8px;border-radius:4px;font:inherit;width:240px}
355
+ .page-input:focus{outline:none;border-color:var(--dvx-bdr-hi)}
356
+ .pg-clear{position:absolute;right:6px;background:none;border:none;color:var(--dvx-txt-ghost);cursor:pointer;font:inherit;font-size:13px;line-height:1;padding:0;display:none}
357
+ .pg-clear:hover{color:var(--dvx-txt-dim)}
358
+ .hbtn{background:#1a2a3a;border:1px solid #2a4a6a;color:#7cc5f0;padding:3px 10px;border-radius:4px;cursor:pointer;font:inherit;text-decoration:none;display:inline-block}
359
+ .hbtn:hover{background:#243a4a}
360
+ .main{display:flex;flex:1;overflow:hidden}
361
+ .palette{width:260px;flex-shrink:0;background:var(--dvx-surf);border-right:1px solid var(--dvx-bdr);display:flex;flex-direction:column;overflow:hidden}
362
+ .section-hd{color:var(--dvx-txt-faint);font-size:10px;font-weight:700;letter-spacing:.1em;padding:4px 4px 8px;flex-shrink:0}
363
+ .bp-card{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);border-radius:6px;margin-bottom:6px;cursor:pointer;overflow:hidden;flex-shrink:0}
364
+ .bp-card:hover{border-color:var(--dvx-bdr-hi)}
365
+ .bp-card.sel{border-color:#7cc5f0}
366
+ .bp-prev{padding:8px;min-height:36px;background:var(--dvx-surf);overflow:hidden;max-height:72px;font-size:12px}
367
+ .bp-lbl{padding:5px 8px;font-size:11px;font-weight:700;color:#7eca9c;border-top:1px solid var(--dvx-bdr)}
368
+ .empty{color:var(--dvx-txt-ghost);font-style:italic;padding:8px 4px;font-size:11px}
369
+ .center{flex:1;background:var(--dvx-bg);display:flex;flex-direction:column;overflow:hidden}
370
+ .ctoolbar{display:flex;align-items:center;gap:4px;padding:4px 8px;background:var(--dvx-hd);border-bottom:1px solid var(--dvx-bdr);flex-shrink:0}
371
+ .tool-btn{background:none;border:1px solid transparent;color:var(--dvx-txt-ghost);cursor:pointer;font:700 10px/1 inherit;letter-spacing:.05em;padding:4px 10px;border-radius:4px}
372
+ .tool-btn:hover{color:var(--dvx-txt-dim);border-color:var(--dvx-bdr)}
373
+ .tool-btn.active{background:var(--dvx-inp);color:#7cc5f0;border-color:var(--dvx-bdr-hi)}
374
+ .center iframe{flex:1;width:100%;border:none;background:#fff}
375
+ .details{width:260px;flex-shrink:0;background:var(--dvx-surf);border-left:1px solid var(--dvx-bdr);overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}
376
+ .d-title{color:#7cc5f0;font-weight:700;font-size:13px;margin-bottom:12px}
377
+ .d-lbl{color:var(--dvx-txt-faint);font-size:10px;font-weight:700;letter-spacing:.1em;margin:12px 0 6px}
378
+ .prop-row{margin-bottom:4px;font-size:12px}
379
+ .pn{color:#b89eff}
380
+ .pt{color:#888}
381
+ .pr{color:#ff6b6b;font-size:10px}
382
+ .jsx-block{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);border-radius:4px;padding:8px;white-space:pre;overflow-x:auto;font-size:11px;color:var(--dvx-txt);margin-bottom:8px}
383
+ .cbtn{background:#1a3a1a;border:1px solid #2a6a2a;color:#7eca9c;padding:4px 12px;border-radius:4px;cursor:pointer;font:inherit;width:100%}
384
+ .cbtn:hover{background:#243a24}
385
+ .cbtn.ok{background:#1a4a1a;color:#5ef05e}
386
+ .ibtn{background:#1a2a3a;border-color:#2a4a6a;color:#7cc5f0;margin-top:4px}
387
+ .ibtn:hover{background:#243a4a}
388
+ .ins-st{font-size:10px;margin-top:5px;min-height:14px;color:#888;word-break:break-all}
389
+ .ins-st.ok{color:#7eca9c}
390
+ .ins-st.warn{color:#f5a623}
391
+ .ins-st.err{color:#ff8888}
392
+ .no-sel{color:var(--dvx-txt-ghost);font-style:italic;padding:8px 0;font-size:12px}
393
+ .src-ed{width:100%;font:12px/1.6 ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;background:var(--dvx-hd);border:1px solid var(--dvx-bdr);color:var(--dvx-txt);padding:12px;tab-size:2;white-space:pre;resize:none}
394
+ .src-ed:focus{outline:none;border-color:var(--dvx-bdr-hi)}
395
+ .src-file-lbl{font-size:10px;color:var(--dvx-txt-ghost);word-break:break-all;padding:0 2px}
396
+ .src-modal{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center}
397
+ .src-modal-box{display:flex;flex-direction:column;width:80vw;height:82vh;background:var(--dvx-surf);border:1px solid var(--dvx-bdr);border-radius:8px;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,.4)}
398
+ .src-modal-hd{display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--dvx-hd);border-bottom:1px solid var(--dvx-bdr);flex-shrink:0}
399
+ .src-modal-hd .cbtn{width:auto;flex-shrink:0}
400
+ .src-modal-hd #src-modal-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:normal}
401
+ .src-editor-area{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
402
+ .src-modal-ta{flex:1;border-radius:0;border:none;border-top:1px solid var(--dvx-bdr);padding:12px 16px;min-height:0;background:var(--dvx-surf);color:var(--dvx-txt)}
403
+ #src-cm-host{flex:1;min-height:0;overflow:hidden;display:none;border-top:1px solid var(--dvx-bdr)}
404
+ #src-cm-host .cm-editor{height:100%!important;background:var(--dvx-bg)}
405
+ #src-cm-host .cm-scroller{overflow:auto!important}
406
+ .src-modal-ft{display:flex;align-items:center;gap:8px;padding:4px 8px;background:var(--dvx-hd);border-top:1px solid var(--dvx-bdr);flex-shrink:0}
407
+ .src-modal-ft .ins-st{flex:1;margin:0}
408
+ .xbtn{background:none;border:none;color:var(--dvx-txt-ghost);cursor:pointer;font:inherit;font-size:14px;line-height:1;padding:0 2px}.xbtn:hover{color:var(--dvx-txt)}
409
+ .pi-row{margin-bottom:6px;display:flex;flex-direction:column;gap:2px}
410
+ .pi-lbl{font-size:11px}.pi-lbl .pn{color:#b89eff}.pi-lbl .pt{color:var(--dvx-txt-faint)}.pi-lbl .pr{color:#ff6b6b}
411
+ .pi{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);color:var(--dvx-txt);padding:3px 6px;border-radius:3px;font:inherit;font-size:11px;width:100%}
412
+ .pi:focus{outline:none;border-color:var(--dvx-bdr-hi)}
413
+ .pi-cb{accent-color:#7cc5f0;width:14px;height:14px;cursor:pointer}
414
+ .el-chip{display:inline-block;background:#1a2a3a;border:1px solid #2a4a6a;border-radius:4px;padding:2px 7px;font-size:11px;color:#7cc5f0;margin-bottom:8px}
415
+ .comp-chip{display:inline-block;background:#1a2a1a;border:1px solid #2a4a2a;border-radius:4px;padding:2px 7px;font-size:11px;color:#7eca9c;margin-bottom:4px}
416
+ .det-tabs{display:flex;border-bottom:1px solid var(--dvx-bdr);flex-shrink:0;margin-bottom:4px}
417
+ .dtab{background:none;border:none;border-bottom:2px solid transparent;color:var(--dvx-txt-faint);cursor:pointer;font:700 10px/1 inherit;letter-spacing:.1em;padding:7px 12px;flex:1}
418
+ .dtab.active{color:#7cc5f0;border-bottom-color:#7cc5f0}
419
+ .dtab:hover:not(.active){color:var(--dvx-txt-dim)}
420
+ .cv-row{margin-bottom:10px}
421
+ .cv-name{color:#b89eff;font-size:10px;margin-bottom:3px;word-break:break-all}
422
+ .cv-color-row{display:flex;gap:4px;align-items:center}
423
+ .cv-color{width:32px;height:24px;padding:1px 2px;border:1px solid var(--dvx-bdr);border-radius:3px;background:var(--dvx-inp);cursor:pointer;flex-shrink:0}
424
+ .cv-text{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);color:var(--dvx-txt);padding:3px 6px;border-radius:3px;font:inherit;font-size:11px;width:100%;min-width:0}
425
+ .cv-text:focus{outline:none;border-color:var(--dvx-bdr-hi)}
426
+ .cv-color-row .cv-text{flex:1;width:auto}
427
+ .pal-tabs{display:flex;border-bottom:1px solid var(--dvx-bdr);flex-shrink:0}
428
+ .ptab{background:none;border:none;border-bottom:2px solid transparent;color:var(--dvx-txt-faint);cursor:pointer;font:700 10px/1 inherit;letter-spacing:.1em;padding:7px 8px;flex:1}
429
+ .ptab.active{color:#7cc5f0;border-bottom-color:#7cc5f0}
430
+ .ptab:hover:not(.active){color:var(--dvx-txt-dim)}
431
+ .pal-panel{display:none;flex:1;overflow-y:auto;padding:8px;flex-direction:column}
432
+ .pal-panel.active{display:flex}
433
+ .conv-hd{display:flex;gap:4px;margin-bottom:8px;align-items:center;flex-shrink:0}
434
+ .conv-dir{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);color:var(--dvx-txt);padding:3px 6px;border-radius:3px;font:inherit;font-size:11px;flex:1;min-width:0}
435
+ .conv-dir:focus{outline:none;border-color:var(--dvx-bdr-hi)}
436
+ .conv-go{background:#1a2a3a;border:1px solid #2a4a6a;color:#7cc5f0;padding:3px 8px;border-radius:3px;cursor:pointer;font:inherit;font-size:11px;flex-shrink:0}
437
+ .conv-go:hover{background:#243a4a}
438
+ .conv-item{background:var(--dvx-inp);border:1px solid var(--dvx-bdr);border-radius:4px;padding:6px 8px;margin-bottom:4px;flex-shrink:0}
439
+ .conv-name{color:#7eca9c;font-weight:700;font-size:11px;margin-bottom:2px}
440
+ .conv-meta{color:var(--dvx-txt-ghost);font-size:10px;margin-bottom:4px;word-break:break-all}
441
+ .conv-btn{background:#1a3a1a;border:1px solid #2a5a2a;color:#7eca9c;padding:2px 8px;border-radius:3px;cursor:pointer;font:inherit;font-size:10px;letter-spacing:.05em}
442
+ .conv-btn:hover:not([disabled]){background:#243a24}
443
+ .conv-btn.done{background:#1a4a1a;color:#5ef05e;border-color:#2a6a2a;cursor:default}
444
+ .conv-btn.err{background:#3a1a1a;color:#ff8888;border-color:#5a2a2a}
445
+ .conv-acts{display:flex;gap:4px;align-items:center}
446
+ .conv-exists{font-size:10px;color:#5ef05e;flex:1}
447
+ .conv-upd{background:#1a2a3a;border:1px solid #2a4a6a;color:#7cc5f0}
448
+ .conv-upd:hover:not([disabled]){background:#243a4a}
449
+ .ebtn{background:#1a2a3a;border:1px solid #2a4a6a;color:#7cc5f0}
450
+ .ebtn:hover{background:#243a4a}
451
+ .ep-row{display:flex;gap:3px;align-items:center;margin-bottom:3px}
452
+ .ep-name{width:68px;flex-shrink:0}
453
+ .ep-type{width:64px;flex-shrink:0;background:var(--dvx-inp);border:1px solid var(--dvx-bdr);color:var(--dvx-txt);padding:3px 4px;border-radius:3px;font:inherit;font-size:11px}
454
+ .ep-def{flex:1;min-width:0}
455
+ .ep-req-lbl{display:flex;align-items:center;gap:2px;font-size:10px;color:var(--dvx-txt-faint);flex-shrink:0;cursor:pointer}
456
+ .ep-rm{background:none;border:none;color:var(--dvx-txt-ghost);cursor:pointer;font:inherit;font-size:11px;padding:0 2px;flex-shrink:0;line-height:1}
457
+ .ep-rm:hover{color:#ff8888}
458
+ .tr-row{display:flex;align-items:center;gap:3px;padding:2px 4px;border-radius:3px;cursor:pointer;user-select:none;min-width:0}
459
+ .tr-row:hover{background:var(--dvx-row-hover)}
460
+ .tr-row.tr-sel{background:var(--dvx-row-sel)}
461
+ .tr-tog{width:12px;flex-shrink:0;text-align:center;font-size:9px;color:var(--dvx-txt-ghost);cursor:pointer;padding-top:1px}
462
+ .tr-tog:hover{color:var(--dvx-txt-dim)}
463
+ .tr-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:11px}
464
+ .tr-tag{color:#7cc5f0}
465
+ .tr-comp{color:#7eca9c}
466
+ .tr-frag{color:var(--dvx-txt-ghost)}
467
+ .tr-txt{color:var(--dvx-txt-ghost);font-style:italic;font-size:10px}
468
+ </style>
469
+ </head>
470
+ <body>
471
+ <header>
472
+ <div class="logo">&#9826; DAVAUX EDITOR</div>
473
+ <div class="page-bar">
474
+ <span>Page:</span>
475
+ <div class="pg-wrap">
476
+ <input id="pg" class="page-input" value="/" list="route-list" autocomplete="off" />
477
+ <button id="pg-clear" class="pg-clear" type="button">&#x2715;</button>
478
+ </div>
479
+ <datalist id="route-list"></datalist>
480
+ <button class="hbtn" onclick="goPage()">Go</button>
481
+ </div>
482
+ <a id="back" href="/" class="hbtn">&#8592; Back to page</a>
483
+ </header>
484
+ <div class="main">
485
+ <div class="palette">
486
+ <div class="pal-tabs">
487
+ <button class="ptab active" data-pal="bps" onclick="switchPal('bps')">BLUEPRINTS</button>
488
+ <button class="ptab" data-pal="conv" onclick="switchPal('conv')">CONVERT</button>
489
+ <button class="ptab" data-pal="tree" onclick="switchPal('tree')">TREE</button>
490
+ </div>
491
+ <div id="pal-bps" class="pal-panel active">
492
+ <div id="bplist"><div class="empty">Loading&#8230;</div></div>
493
+ </div>
494
+ <div id="pal-tree" class="pal-panel">
495
+ <div id="tree-root"><div class="empty">Load a page to see the tree.</div></div>
496
+ </div>
497
+ <div id="pal-conv" class="pal-panel">
498
+ <div class="conv-hd">
499
+ <input id="conv-dir" class="conv-dir" value="src/components" title="Component folder (relative to project root)" />
500
+ <button class="conv-go" onclick="loadComponents()">Scan</button>
501
+ </div>
502
+ <div id="conv-list"><div class="empty">Click Scan to load components.</div></div>
503
+ </div>
504
+ </div>
505
+ <div class="center">
506
+ <div class="ctoolbar">
507
+ <button id="sel-toggle" class="tool-btn" title="Inspect elements (I)" onclick="toggleInspect()">⌖ Inspect</button>
508
+ <button id="edit-src-btn" class="tool-btn" style="display:none" onclick="openPageSrcModal()">&#9998; Edit Source</button>
509
+ <div id="layout-btns"></div>
510
+ </div>
511
+ <iframe id="frame" src="/"></iframe>
512
+ <div id="src-modal" class="src-modal" style="display:none" onclick="if(event.target===this)closeSrcModal()">
513
+ <div class="src-modal-box">
514
+ <div class="src-modal-hd">
515
+ <span id="src-modal-title" class="src-file-lbl" style="flex:1;font-size:11px;color:#aaa"></span>
516
+ <button class="cbtn" id="src-save-btn" style="padding:3px 12px">Save</button>
517
+ <button class="xbtn" onclick="closeSrcModal()" title="Close (Esc)">✕</button>
518
+ </div>
519
+ <div class="src-editor-area">
520
+ <textarea class="src-ed src-modal-ta" id="src-ta" spellcheck="false"></textarea>
521
+ <div id="src-cm-host"></div>
522
+ </div>
523
+ <div class="src-modal-ft">
524
+ <div id="src-st" class="ins-st"></div>
525
+ <button id="src-hl-btn" class="tool-btn" style="font-size:10px;padding:3px 8px" onclick="toggleHighlight()">✦ Highlight</button>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ <div class="details">
531
+ <div class="det-tabs">
532
+ <button class="dtab active" data-mode="comp" onclick="switchMode('comp')">PROPS</button>
533
+ <button class="dtab" data-mode="styles" onclick="switchMode('styles')">STYLES</button>
534
+ </div>
535
+ <div id="det"><div class="no-sel">Select a component to see details.</div></div>
536
+ </div>
537
+ </div>
538
+ <script>
539
+ ;(function(){
540
+ var NL=String.fromCharCode(10)
541
+ var bps=[]
542
+ var sel=-1
543
+ var overrides={}
544
+ var omlTree=null
545
+ var pageMode='oml'
546
+ var pageFilePath=null
547
+ var pageFileType=null
548
+ var pageSourceContent=''
549
+ var cmEditor=null
550
+ var cmModules=null
551
+ var srcHighlight=localStorage.getItem('dvx-src-hl')==='1'
552
+ var themeObserver=null
553
+ var insertTarget=null
554
+ var pageLayouts=[]
555
+ var pageOwnFilePath=null
556
+ var srcModalMode='file'
557
+ var srcFragmentComp=null
558
+ var srcFragmentTag=null
559
+ var srcFragmentIdx=0
560
+ var prevHovered=null
561
+ var selected=null
562
+ var selectedTreeRow=null
563
+ var cssVarMap={}
564
+ var styleEdits={}
565
+ var detMode='comp'
566
+ var frame=document.getElementById('frame')
567
+ var params=new URLSearchParams(location.search)
568
+ var pg=params.get('page')||'/'
569
+ document.getElementById('pg').value=pg
570
+ setFrame(pg)
571
+ document.getElementById('back').href=pg
572
+ var pgInp=document.getElementById('pg')
573
+ var pgClear=document.getElementById('pg-clear')
574
+ function syncPgClear(){pgClear.style.display=pgInp.value?'block':'none'}
575
+ pgInp.addEventListener('keydown',function(e){if(e.key==='Enter')goPage()})
576
+ pgInp.addEventListener('input',syncPgClear)
577
+ pgClear.addEventListener('click',function(){pgInp.value='';syncPgClear();pgInp.focus()})
578
+ syncPgClear()
579
+
580
+ function setFrame(path){
581
+ var sep=path.indexOf('?')>=0?'&':'?'
582
+ frame.src=path+sep+'_editor=1'
583
+ }
584
+ function goPage(){
585
+ pg=document.getElementById('pg').value.trim()||'/'
586
+ setFrame(pg)
587
+ document.getElementById('back').href=pg
588
+ history.replaceState(null,'','/_davaux/editor?page='+encodeURIComponent(pg))
589
+ }
590
+ window.goPage=goPage
591
+
592
+ // ── Click-to-select bridge ────────────────────────────────────────────────────
593
+ // ── Inspect mode ─────────────────────────────────────────────────────────────
594
+ var selectMode=false
595
+ function setSelectMode(v){
596
+ selectMode=!!v
597
+ var btn=document.getElementById('sel-toggle')
598
+ if(btn)btn.classList.toggle('active',selectMode)
599
+ if(!selectMode&&prevHovered){prevHovered.style.outline='';prevHovered=null}
600
+ if(!selectMode&&selected){try{selected.style.outline=''}catch(_){}selected=null}
601
+ try{frame.contentDocument.body.style.cursor=selectMode?'crosshair':''}catch(_){}
602
+ }
603
+ window.setSelectMode=setSelectMode
604
+ window.toggleInspect=function(){setSelectMode(!selectMode)}
605
+ window.openSrcModal=openSrcModal
606
+ window.openLayoutModal=openLayoutModal
607
+ window.openPageSrcModal=openPageSrcModal
608
+ window.closeSrcModal=closeSrcModal
609
+ window.toggleHighlight=toggleHighlight
610
+ window.openFragmentModal=openFragmentModal
611
+ window.openElementFragmentModal=openElementFragmentModal
612
+ document.addEventListener('keydown',function(e){
613
+ if(e.key==='Escape'){var m=document.getElementById('src-modal');if(m&&m.style.display!=='none'){closeSrcModal();return}}
614
+ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT'||e.target.isContentEditable)return
615
+ if(e.key==='i'||e.key==='I'){e.preventDefault();setSelectMode(!selectMode)}
616
+ })
617
+
618
+ frame.addEventListener('load',function(){
619
+ try{
620
+ var doc=frame.contentDocument
621
+ if(!doc||!doc.body)return
622
+ // Fetch OML tree for this page
623
+ var fp=frame.contentWindow.location
624
+ var rawSearch=fp.search.replace(/[?&]_editor=1/,'')
625
+ var treeUrl=fp.pathname+(rawSearch||'')
626
+ omlTree=null
627
+ ocState=null
628
+ oeState=null
629
+ selected=null
630
+ insertTarget=null
631
+ styleEdits={}
632
+ var det=document.getElementById('det')
633
+ if(det)det.innerHTML='<div class="no-sel">Select a component to see details.</div>'
634
+ fetch('/_davaux/inspector?url='+encodeURIComponent(treeUrl))
635
+ .then(function(r){return r.json()})
636
+ .then(function(d){
637
+ pageFileType=d.fileType||'tsx'
638
+ pageFilePath=d.filePath||null
639
+ pageOwnFilePath=d.filePath||null
640
+ pageLayouts=d.layouts||[]
641
+ omlTree=d.node||null
642
+ pageMode=(pageFileType==='mdx'||pageFileType==='md')?'source':'oml'
643
+ renderLayoutButtons()
644
+ if(pageMode==='source'){
645
+ enterSourceMode()
646
+ }else{
647
+ var esb=document.getElementById('edit-src-btn')
648
+ if(esb)esb.style.display=pageFilePath?'':'none'
649
+ pageSourceContent=''
650
+ if(pageFilePath){
651
+ fetch('/_davaux/get-source?file='+encodeURIComponent(pageFilePath))
652
+ .then(function(r){return r.json()})
653
+ .then(function(d2){pageSourceContent=d2.content||''})
654
+ .catch(function(){})
655
+ }
656
+ closeSrcModal()
657
+ renderTreePanel()
658
+ }
659
+ })
660
+ .catch(function(){renderTreePanel()})
661
+ scanCssVars(doc)
662
+ detectPageTheme(doc)
663
+ if(themeObserver)themeObserver.disconnect()
664
+ themeObserver=new MutationObserver(function(){detectPageTheme(doc)})
665
+ themeObserver.observe(doc.documentElement,{attributes:true,attributeFilter:['data-mantine-color-scheme','data-color-scheme','data-theme']})
666
+ if(detMode==='styles')renderStylesPanel()
667
+ if(selectMode)doc.body.style.cursor='crosshair'
668
+ // Hover — blue outline (inspect mode only)
669
+ doc.addEventListener('mouseover',function(e){
670
+ if(!selectMode)return
671
+ // Restore selection outline on previously hovered element (if it was selected)
672
+ if(prevHovered){
673
+ prevHovered.style.outline=prevHovered===selected?'2px solid #7cc5f0':''
674
+ prevHovered.style.outlineOffset=prevHovered===selected?'-1px':''
675
+ }
676
+ if(e.target===doc.body||e.target===doc.documentElement){prevHovered=null;return}
677
+ prevHovered=e.target
678
+ e.target.style.outline='2px solid rgba(124,197,240,0.7)'
679
+ e.target.style.outlineOffset='-1px'
680
+ })
681
+ doc.addEventListener('mouseout',function(){
682
+ if(!selectMode)return
683
+ if(prevHovered){
684
+ // Restore selection outline if this element is still selected
685
+ prevHovered.style.outline=prevHovered===selected?'2px solid #7cc5f0':''
686
+ prevHovered.style.outlineOffset=prevHovered===selected?'-1px':''
687
+ prevHovered=null
688
+ }
689
+ })
690
+ // Click — toggle selection and show details (inspect mode only)
691
+ doc.addEventListener('click',function(e){
692
+ if(!selectMode)return
693
+ e.preventDefault()
694
+ e.stopPropagation()
695
+ var target=e.target
696
+ // Clear previous selection outline
697
+ if(selected){selected.style.outline='';selected.style.outlineOffset=''}
698
+ if(selected===target){
699
+ // Same element clicked again — deselect
700
+ selected=null
701
+ ocState=null
702
+ oeState=null
703
+ var det=document.getElementById('det')
704
+ if(det)det.innerHTML='<div class="no-sel">Select a component to see details.</div>'
705
+ return
706
+ }
707
+ // Select the new element
708
+ selected=target
709
+ selected.style.outline='2px solid #7cc5f0'
710
+ selected.style.outlineOffset='-1px'
711
+ showElemDet(target)
712
+ },true)
713
+ }catch(_){}
714
+ })
715
+
716
+ function showElemDet(el){
717
+ detMode='comp'
718
+ document.querySelectorAll('.dtab').forEach(function(t){t.classList.toggle('active',t.dataset.mode==='comp')})
719
+ sel=-1
720
+ ocState=null
721
+ oeState=null
722
+ styleEdits={}
723
+ document.querySelectorAll('.bp-card').forEach(function(c){c.classList.remove('sel')})
724
+ var tag=el.tagName?el.tagName.toLowerCase():'?'
725
+ var cls=el.getAttribute?el.getAttribute('class')||'':''
726
+ var txt=(el.textContent||'').trim().slice(0,80)
727
+ var det=document.getElementById('det')
728
+ // Islands use data-island; library components use data-oml-comp injected at render time.
729
+ // Walk up from the clicked element to find the nearest marker.
730
+ var islandName=null
731
+ var omlCompEl=null
732
+ var n=el
733
+ while(n){
734
+ var di=n.getAttribute&&n.getAttribute('data-island');if(di){islandName=di;break}
735
+ var dc=n.getAttribute&&n.getAttribute('data-oml-comp');if(dc){omlCompEl=n;break}
736
+ n=n.parentElement
737
+ }
738
+ var match=islandName&&omlTree?findByName(omlTree,islandName)
739
+ :omlCompEl&&omlTree?findByOmlComp(omlTree,omlCompEl.getAttribute('data-oml-comp'),parseInt(omlCompEl.getAttribute('data-oml-inst')||'0',10))
740
+ :omlTree?findInTree(omlTree,tag,txt,cls):null
741
+ if(match&&match.type==='#component'){
742
+ var compIdx=countNameBefore(omlTree,match,match.name)
743
+ var rawProps=Object.assign({},match.props||{})
744
+ var chi=compChildrenInfo(match)
745
+ ocState={name:match.name,props:rawProps,instanceIndex:compIdx,childrenText:chi.childrenText,hasChildren:chi.hasChildren}
746
+ insertTarget={type:'comp',name:match.name,instanceIndex:compIdx}
747
+ renderOcPanel(det)
748
+ syncTreeRow(match)
749
+ return
750
+ }
751
+ if(match&&match.type==='element'){
752
+ var elemText=null
753
+ if(match.children&&match.children.length>0){
754
+ var allTxt=true
755
+ var accum=''
756
+ for(var ci=0;ci<match.children.length;ci++){
757
+ var ch=match.children[ci]
758
+ if(ch&&ch.type==='#text'){accum+=ch.value||''}
759
+ else{allTxt=false;break}
760
+ }
761
+ if(allTxt&&accum.trim())elemText=accum
762
+ }
763
+ var idx=countTagBefore(omlTree,match,match.tag)
764
+ oeState={tag:match.tag,props:Object.assign({},match.props||{}),instanceIndex:idx,textContent:elemText}
765
+ insertTarget={type:'elem',tag:match.tag,instanceIndex:idx}
766
+ renderOePanel(det)
767
+ syncTreeRow(match)
768
+ return
769
+ }
770
+ syncTreeRow(null)
771
+ var h='<span class="el-chip">&lt;'+esc(tag)+'&gt;</span>'
772
+ if(cls)h+='<div class="prop-row"><span class="pn">class</span><span class="pt"> "'+esc(cls)+'"</span></div>'
773
+ if(txt)h+='<div class="d-lbl">TEXT</div><div style="color:#888;font-size:11px;word-break:break-all;line-height:1.4">'+esc(txt)+'</div>'
774
+ det.innerHTML=h
775
+ }
776
+ function compChildrenInfo(node){
777
+ var kids=node.children||[]
778
+ if(kids.length===0)return{childrenText:null,hasChildren:false}
779
+ if(kids.length===1&&kids[0]&&kids[0].type==='#text')return{childrenText:kids[0].value||null,hasChildren:true}
780
+ return{childrenText:null,hasChildren:true}
781
+ }
782
+ function isOmlVal(v){
783
+ if(!v||typeof v!=='object')return false
784
+ if(Array.isArray(v))return v.some(isOmlVal)
785
+ var t=v.type
786
+ return t==='element'||t==='#component'||t==='#fragment'||t==='#text'||t==='#raw'
787
+ }
788
+ function renderOcPanel(det){
789
+ if(!ocState)return
790
+ var entries=Object.entries(ocState.props).filter(function(e){return typeof e[1]!=='function'&&!isOmlVal(e[1])})
791
+ var readonlyEntries=Object.entries(ocState.props).filter(function(e){return isOmlVal(e[1])})
792
+ // Find matching blueprint to surface optional props not currently in the JSX
793
+ var bp=bps.find(function(b){return b.name===ocState.name})
794
+ var bpSchema=bp?bp.props||{}:{}
795
+ var missing=Object.keys(bpSchema).filter(function(k){return!(k in ocState.props)})
796
+ var h='<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">'
797
+ h+='<span class="comp-chip" style="margin:0">&#9826; '+esc(ocState.name)+'</span>'
798
+ h+='<button class="cbtn mv-up" style="padding:2px 6px;font-size:12px;width:auto" title="Move up">↑</button>'
799
+ h+='<button class="cbtn mv-dn" style="padding:2px 6px;font-size:12px;width:auto" title="Move down">↓</button>'
800
+ h+='<button class="cbtn rem-btn" style="background:#2a1a1a;border-color:#4a2a2a;color:#ff8888;padding:2px 8px;font-size:10px;width:auto;margin-left:auto" data-comp="'+esc(ocState.name)+'" data-idx="'+ocState.instanceIndex+'">Remove</button>'
801
+ h+='</div>'
802
+ h+='<div id="rem-st" class="ins-st"></div>'
803
+ if(entries.length||readonlyEntries.length){
804
+ h+='<div class="d-lbl">PROPS</div>'
805
+ entries.forEach(function(e){
806
+ var k=e[0],v=e[1]
807
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">'+esc(k)+'</span>'
808
+ h+='<span class="pt"> '+esc(typeof v)+'</span></label>'
809
+ if(typeof v==='boolean'){
810
+ h+='<input type="checkbox" class="pi pi-cb oc-inp" data-prop="'+esc(k)+'" data-type="boolean"'+(v?' checked':'')+'>'
811
+ }else if(typeof v==='number'){
812
+ h+='<input type="number" class="pi oc-inp" data-prop="'+esc(k)+'" data-type="number" value="'+esc(String(v))+'">'
813
+ }else if(typeof v==='object'&&v!==null){
814
+ h+='<input type="text" class="pi oc-inp" data-prop="'+esc(k)+'" data-type="json" value="'+esc(JSON.stringify(v))+'">'
815
+ }else{
816
+ h+='<input type="text" class="pi oc-inp" data-prop="'+esc(k)+'" data-type="string" value="'+esc(String(v!==null&&v!==undefined?v:''))+'">'
817
+ }
818
+ h+='</div>'
819
+ })
820
+ readonlyEntries.forEach(function(e){
821
+ var k=e[0]
822
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">'+esc(k)+'</span><span class="pt"> jsx</span></label>'
823
+ h+='<input type="text" class="pi" value="[JSX]" readonly style="opacity:0.4;cursor:default"></div>'
824
+ })
825
+ }else{
826
+ h+='<div class="no-sel" style="font-size:11px">No props</div>'
827
+ }
828
+ if(ocState.childrenText!==null&&ocState.childrenText!==undefined){
829
+ h+='<div class="d-lbl" style="margin-top:8px">CONTENT</div>'
830
+ h+='<textarea class="pi oc-txt" rows="2" style="width:100%;resize:vertical;font-family:monospace;font-size:11px">'+esc(ocState.childrenText)+'</textarea>'
831
+ }else if(ocState.hasChildren){
832
+ h+='<div class="d-lbl" style="margin-top:8px">CHILDREN</div>'
833
+ h+='<button class="cbtn ibtn" style="font-size:10px;padding:3px 8px;width:auto" onclick="openFragmentModal()">&#9998; Edit Children</button>'
834
+ }
835
+ // Blueprint optional props not yet in JSX
836
+ if(missing.length){
837
+ h+='<div class="d-lbl" style="margin-top:8px">OPTIONAL</div>'
838
+ missing.forEach(function(k){
839
+ var schema=bpSchema[k]
840
+ h+='<div class="pi-row"><span class="pn" style="opacity:0.5;font-size:11px;flex:1">'+esc(k)+'</span>'
841
+ h+='<span class="pt" style="opacity:0.4;font-size:10px;flex-shrink:0;margin-right:4px">'+esc(schema.type||'string')+'</span>'
842
+ h+='<button class="conv-btn oc-add-bp" data-prop="'+esc(k)+'" data-type="'+(schema.type||'string')+'" style="font-size:10px;padding:2px 6px">+ Add</button>'
843
+ h+='</div>'
844
+ })
845
+ }
846
+ // Manual add prop
847
+ h+='<button class="cbtn oc-add-manual" style="margin-top:6px;font-size:10px;background:#1a1a2e;color:#666">+ Add prop</button>'
848
+ h+='<div style="display:flex;gap:4px;margin-top:8px"><button class="cbtn" id="oc-save-btn">Save changes</button></div>'
849
+ h+='<div id="oc-st" class="ins-st"></div>'
850
+ det.innerHTML=h
851
+ det.querySelectorAll('.oc-inp').forEach(function(inp){
852
+ inp.addEventListener('input',function(){
853
+ var p=this.dataset.prop,t=this.dataset.type
854
+ if(t==='boolean')ocState.props[p]=this.checked
855
+ else if(t==='number')ocState.props[p]=Number(this.value)
856
+ else if(t==='json'){try{ocState.props[p]=JSON.parse(this.value)}catch(_){}}
857
+ else ocState.props[p]=this.value
858
+ })
859
+ })
860
+ var ocTxt=det.querySelector('.oc-txt')
861
+ if(ocTxt)ocTxt.addEventListener('input',function(){ocState.childrenText=this.value})
862
+ det.querySelectorAll('.oc-add-bp').forEach(function(btn){
863
+ btn.addEventListener('click',function(){
864
+ var k=this.dataset.prop,t=this.dataset.type||'string'
865
+ ocState.props[k]=defVal(t)
866
+ renderOcPanel(document.getElementById('det'))
867
+ })
868
+ })
869
+ var addManual=det.querySelector('.oc-add-manual')
870
+ if(addManual)addManual.addEventListener('click',function(){
871
+ var row=document.createElement('div')
872
+ row.className='pi-row'
873
+ row.style.gap='4px'
874
+ row.innerHTML='<input type="text" class="pi" placeholder="name" style="flex:1"><input type="text" class="pi" placeholder="value" style="flex:1"><button class="conv-btn" style="padding:2px 6px;font-size:10px;flex-shrink:0">OK</button>'
875
+ var saveArea=det.querySelector('#oc-save-btn')
876
+ if(saveArea&&saveArea.parentElement)saveArea.parentElement.before(row)
877
+ row.querySelector('input').focus()
878
+ row.querySelector('button').addEventListener('click',function(){
879
+ var ins=row.querySelectorAll('input')
880
+ var name=ins[0].value.trim(),val=ins[1].value
881
+ if(!name)return
882
+ ocState.props[name]=val
883
+ renderOcPanel(document.getElementById('det'))
884
+ })
885
+ })
886
+ var rb=det.querySelector('.rem-btn')
887
+ if(rb)rb.addEventListener('click',function(){remComp(this.getAttribute('data-comp'),parseInt(this.getAttribute('data-idx')||'0',10))})
888
+ var sb=det.querySelector('#oc-save-btn')
889
+ if(sb)sb.addEventListener('click',saveOmlComp)
890
+ var mvup=det.querySelector('.mv-up')
891
+ if(mvup)mvup.addEventListener('click',function(){movePanelNode(true,ocState.name,ocState.instanceIndex,'up')})
892
+ var mvdn=det.querySelector('.mv-dn')
893
+ if(mvdn)mvdn.addEventListener('click',function(){movePanelNode(true,ocState.name,ocState.instanceIndex,'down')})
894
+ }
895
+ function buildJsxFromObj(name,props){
896
+ var entries=Object.entries(props).filter(function(e){return typeof e[1]!=='function'})
897
+ if(!entries.length)return'<'+name+' />'
898
+ var lines=entries.map(function(e){
899
+ var k=e[0],v=e[1]
900
+ if(typeof v==='string')return' '+k+'='+JSON.stringify(v)
901
+ if(typeof v==='boolean')return' '+k+'={'+(v?'true':'false')+'}'
902
+ if(typeof v==='number')return' '+k+'={'+v+'}'
903
+ return' '+k+'={'+JSON.stringify(v)+'}'
904
+ })
905
+ return'<'+name+NL+lines.join(NL)+NL+'/>'
906
+ }
907
+ function saveOmlComp(){
908
+ if(!ocState)return
909
+ var st=document.getElementById('oc-st')
910
+ if(st){st.textContent='Saving...';st.className='ins-st'}
911
+ fetch('/_davaux/update-comp-props',{
912
+ method:'POST',headers:{'Content-Type':'application/json'},
913
+ body:JSON.stringify({page:pg.split('?')[0],component:ocState.name,newProps:ocState.props,instanceIndex:ocState.instanceIndex,childrenText:ocState.childrenText})
914
+ })
915
+ .then(function(r){return r.json()})
916
+ .then(function(res){
917
+ var s=document.getElementById('oc-st');if(!s)return
918
+ if(res.replaced){
919
+ s.textContent='Saved to '+res.file;s.className='ins-st ok'
920
+ var b=document.getElementById('oc-save-btn')
921
+ if(b){b.textContent='Saved!';b.classList.add('ok');setTimeout(function(){b.textContent='Save changes';b.classList.remove('ok')},2000)}
922
+ }else{s.textContent=res.error;s.className='ins-st err'}
923
+ })
924
+ .catch(function(){var s=document.getElementById('oc-st');if(s){s.textContent='Request failed';s.className='ins-st err'}})
925
+ }
926
+ window.saveOmlComp=saveOmlComp
927
+
928
+ function countTagBefore(root,target,tag){
929
+ var state={n:0,found:false}
930
+ walkTagCount(root,target,tag,state)
931
+ return state.found?state.n:0
932
+ }
933
+ function walkTagCount(node,target,tag,state){
934
+ if(state.found)return
935
+ if(node===target){state.found=true;return}
936
+ if(node.type==='element'){
937
+ if(node.tag===tag)state.n++
938
+ var kids=node.children||[]
939
+ for(var i=0;i<kids.length;i++)walkTagCount(kids[i],target,tag,state)
940
+ }else if(node.type==='#fragment'){
941
+ var kids=node.children||[]
942
+ for(var i=0;i<kids.length;i++)walkTagCount(kids[i],target,tag,state)
943
+ }else if(node.type==='#component'){
944
+ var sub=(node.output&&node.output.type!=='#raw')?[node.output]:(node.children||[])
945
+ for(var i=0;i<sub.length;i++)walkTagCount(sub[i],target,tag,state)
946
+ }
947
+ }
948
+ function countNameBefore(root,target,name){
949
+ var state={n:0,found:false}
950
+ walkNameCount(root,target,name,state)
951
+ return state.found?state.n:0
952
+ }
953
+ function walkNameCount(node,target,name,state){
954
+ if(!node||state.found)return
955
+ if(node===target){state.found=true;return}
956
+ if(node.type==='#component'){
957
+ if(node.name===name)state.n++
958
+ var sub=(node.output&&node.output.type!=='#raw')?[node.output]:(node.children||[])
959
+ for(var i=0;i<sub.length;i++)walkNameCount(sub[i],target,name,state)
960
+ }else if(node.type==='element'||node.type==='#fragment'){
961
+ var kids=node.children||[]
962
+ for(var i=0;i<kids.length;i++)walkNameCount(kids[i],target,name,state)
963
+ }
964
+ }
965
+ function renderOePanel(det){
966
+ if(!oeState)return
967
+ var entries=Object.entries(oeState.props).filter(function(e){return typeof e[1]!=='function'})
968
+ var h='<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">'
969
+ h+='<span class="el-chip" style="margin:0">&lt;'+esc(oeState.tag)+'&gt;</span>'
970
+ h+='<button class="cbtn mv-up" style="padding:2px 6px;font-size:12px;width:auto" title="Move up">↑</button>'
971
+ h+='<button class="cbtn mv-dn" style="padding:2px 6px;font-size:12px;width:auto" title="Move down">↓</button>'
972
+ h+='<button class="cbtn oe-rem-btn" style="background:#2a1a1a;border-color:#4a2a2a;color:#ff8888;padding:2px 8px;font-size:10px;width:auto;margin-left:auto">Remove</button>'
973
+ h+='</div>'
974
+ if(entries.length){
975
+ h+='<div class="d-lbl">ATTRIBUTES</div>'
976
+ entries.forEach(function(e){
977
+ var k=e[0],v=e[1]
978
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">'+esc(k)+'</span>'
979
+ if(typeof v==='boolean'){
980
+ h+='<span class="pt"> bool</span></label>'
981
+ h+='<input type="checkbox" class="pi pi-cb oe-inp" data-attr="'+esc(k)+'" data-type="boolean"'+(v?' checked':'')+'>'
982
+ }else if(typeof v==='object'&&v!==null){
983
+ h+='<span class="pt"> object</span></label>'
984
+ h+='<input type="text" class="pi oe-inp" data-attr="'+esc(k)+'" data-type="json" value="'+esc(JSON.stringify(v))+'">'
985
+ }else{
986
+ h+='<span class="pt"> string</span></label>'
987
+ h+='<input type="text" class="pi oe-inp" data-attr="'+esc(k)+'" data-type="string" value="'+esc(String(v!==null&&v!==undefined?v:''))+'">'
988
+ }
989
+ h+='</div>'
990
+ })
991
+ }else{
992
+ h+='<div class="no-sel" style="font-size:11px">No attributes</div>'
993
+ }
994
+ if(oeState.textContent!==null&&oeState.textContent!==undefined){
995
+ h+='<div class="d-lbl" style="margin-top:8px">TEXT</div>'
996
+ h+='<textarea class="pi oe-txt" rows="2" style="width:100%;resize:vertical;font-family:monospace;font-size:11px">'+esc(oeState.textContent)+'</textarea>'
997
+ }else{
998
+ h+='<div class="d-lbl" style="margin-top:8px">CHILDREN</div>'
999
+ h+='<button class="cbtn ibtn" style="font-size:10px;padding:3px 8px;width:auto" onclick="openElementFragmentModal()">&#9998; Edit Children</button>'
1000
+ }
1001
+ h+='<button class="cbtn oe-add-manual" style="margin-top:6px;font-size:10px;background:#1a1a2e;color:#666">+ Add attr</button>'
1002
+ h+='<div style="display:flex;gap:4px;margin-top:8px"><button class="cbtn" id="oe-save-btn">Save changes</button></div>'
1003
+ h+='<div id="oe-st" class="ins-st"></div>'
1004
+ h+='<div style="margin-top:8px">'
1005
+ h+='<button class="cbtn oe-add-sib-btn" style="font-size:10px;background:#1a2a1a;border-color:#2a4a2a;color:#88cc88;width:auto;padding:2px 8px">+ Add sibling</button>'
1006
+ h+='<div id="oe-sib-form" style="display:none;margin-top:6px">'
1007
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">tag</span></label>'
1008
+ h+='<select class="pi" id="sib-tag"><option>div</option><option>p</option><option>span</option><option>h1</option><option>h2</option><option>h3</option><option>section</option><option>article</option><option>button</option><option>a</option><option>ul</option><option>li</option></select>'
1009
+ h+='</div>'
1010
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">class</span></label><input type="text" class="pi" id="sib-cls" placeholder="optional"></div>'
1011
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">text</span></label><input type="text" class="pi" id="sib-txt" placeholder="optional"></div>'
1012
+ h+='<button class="cbtn" id="sib-create-btn" style="margin-top:4px">Create</button>'
1013
+ h+='</div></div>'
1014
+ det.innerHTML=h
1015
+ det.querySelectorAll('.oe-inp').forEach(function(inp){
1016
+ inp.addEventListener('input',function(){
1017
+ var a=this.dataset.attr,t=this.dataset.type
1018
+ if(t==='boolean')oeState.props[a]=this.checked
1019
+ else if(t==='json'){try{oeState.props[a]=JSON.parse(this.value)}catch(_){}}
1020
+ else oeState.props[a]=this.value
1021
+ })
1022
+ })
1023
+ var txtArea=det.querySelector('.oe-txt')
1024
+ if(txtArea)txtArea.addEventListener('input',function(){oeState.textContent=this.value})
1025
+ var addManual=det.querySelector('.oe-add-manual')
1026
+ if(addManual)addManual.addEventListener('click',function(){
1027
+ var row=document.createElement('div')
1028
+ row.className='pi-row'
1029
+ row.style.gap='4px'
1030
+ row.innerHTML='<input type="text" class="pi" placeholder="attr" style="flex:1"><input type="text" class="pi" placeholder="value" style="flex:1"><button class="conv-btn" style="padding:2px 6px;font-size:10px;flex-shrink:0">OK</button>'
1031
+ var saveArea=det.querySelector('#oe-save-btn')
1032
+ if(saveArea&&saveArea.parentElement)saveArea.parentElement.before(row)
1033
+ row.querySelector('input').focus()
1034
+ row.querySelector('button').addEventListener('click',function(){
1035
+ var ins=row.querySelectorAll('input')
1036
+ var name=ins[0].value.trim(),val=ins[1].value
1037
+ if(!name)return
1038
+ oeState.props[name]=val
1039
+ renderOePanel(document.getElementById('det'))
1040
+ })
1041
+ })
1042
+ var sb=det.querySelector('#oe-save-btn')
1043
+ if(sb)sb.addEventListener('click',saveOmlElem)
1044
+ var mvup=det.querySelector('.mv-up')
1045
+ if(mvup)mvup.addEventListener('click',function(){movePanelNode(false,oeState.tag,oeState.instanceIndex,'up')})
1046
+ var mvdn=det.querySelector('.mv-dn')
1047
+ if(mvdn)mvdn.addEventListener('click',function(){movePanelNode(false,oeState.tag,oeState.instanceIndex,'down')})
1048
+ var remBtn=det.querySelector('.oe-rem-btn')
1049
+ if(remBtn)remBtn.addEventListener('click',removeOmlElem)
1050
+ var addSibBtn=det.querySelector('.oe-add-sib-btn')
1051
+ if(addSibBtn)addSibBtn.addEventListener('click',function(){
1052
+ var form=document.getElementById('oe-sib-form')
1053
+ if(form)form.style.display=form.style.display==='none'?'block':'none'
1054
+ })
1055
+ var createSibBtn=det.querySelector('#sib-create-btn')
1056
+ if(createSibBtn)createSibBtn.addEventListener('click',insertSiblingElem)
1057
+ }
1058
+ function insertSiblingElem(){
1059
+ if(!oeState)return
1060
+ var tagEl=document.getElementById('sib-tag')
1061
+ var clsEl=document.getElementById('sib-cls')
1062
+ var txtEl=document.getElementById('sib-txt')
1063
+ var tag=tagEl?tagEl['value']:'div'
1064
+ var cls=clsEl?clsEl['value'].trim():''
1065
+ var txt=txtEl?txtEl['value'].trim():''
1066
+ var attrs=cls?' class="'+cls+'"':''
1067
+ var jsx=txt?'<'+tag+attrs+'>'+txt+'</'+tag+'>':'<'+tag+attrs+' />'
1068
+ var st=document.getElementById('oe-st')
1069
+ if(st){st.textContent='Creating...';st.className='ins-st'}
1070
+ fetch('/_davaux/insert-after',{
1071
+ method:'POST',headers:{'Content-Type':'application/json'},
1072
+ body:JSON.stringify({page:pg.split('?')[0],tag:oeState.tag,instanceIndex:oeState.instanceIndex,newJsx:jsx})
1073
+ })
1074
+ .then(function(r){return r.json()})
1075
+ .then(function(res){
1076
+ var s=document.getElementById('oe-st');if(!s)return
1077
+ if(res.inserted){
1078
+ s.textContent='Inserted into '+res.file;s.className='ins-st ok'
1079
+ var cb=document.getElementById('sib-create-btn')
1080
+ if(cb){cb['disabled']=true;cb.textContent='Done'}
1081
+ }else{s.textContent=res.error;s.className='ins-st err'}
1082
+ })
1083
+ .catch(function(){var s=document.getElementById('oe-st');if(s){s.textContent='Request failed';s.className='ins-st err'}})
1084
+ }
1085
+ function removeOmlElem(){
1086
+ if(!oeState)return
1087
+ var st=document.getElementById('oe-st')
1088
+ if(st){st.textContent='Removing...';st.className='ins-st'}
1089
+ fetch('/_davaux/remove',{
1090
+ method:'POST',headers:{'Content-Type':'application/json'},
1091
+ body:JSON.stringify({page:pg.split('?')[0],tag:oeState.tag,instanceIndex:oeState.instanceIndex})
1092
+ })
1093
+ .then(function(r){return r.json()})
1094
+ .then(function(res){
1095
+ var s=document.getElementById('oe-st');if(!s)return
1096
+ if(res.removed){
1097
+ s.textContent='Removed from '+res.file;s.className='ins-st ok'
1098
+ var rb=document.querySelector('.oe-rem-btn')
1099
+ if(rb){rb.disabled=true;rb.textContent='Removed'}
1100
+ }else{s.textContent=res.error;s.className='ins-st err'}
1101
+ })
1102
+ .catch(function(){var s=document.getElementById('oe-st');if(s){s.textContent='Request failed';s.className='ins-st err'}})
1103
+ }
1104
+ function saveOmlElem(){
1105
+ if(!oeState)return
1106
+ var st=document.getElementById('oe-st')
1107
+ if(st){st.textContent='Saving...';st.className='ins-st'}
1108
+ fetch('/_davaux/update-element',{
1109
+ method:'POST',headers:{'Content-Type':'application/json'},
1110
+ body:JSON.stringify({page:pg.split('?')[0],tag:oeState.tag,newProps:oeState.props,instanceIndex:oeState.instanceIndex,textContent:oeState.textContent})
1111
+ })
1112
+ .then(function(r){return r.json()})
1113
+ .then(function(res){
1114
+ var s=document.getElementById('oe-st');if(!s)return
1115
+ if(res.replaced){
1116
+ s.textContent='Saved to '+res.file;s.className='ins-st ok'
1117
+ var b=document.getElementById('oe-save-btn')
1118
+ if(b){b.textContent='Saved!';b.classList.add('ok');setTimeout(function(){b.textContent='Save changes';b.classList.remove('ok')},2000)}
1119
+ }else{s.textContent=res.error;s.className='ins-st err'}
1120
+ })
1121
+ .catch(function(){var s=document.getElementById('oe-st');if(s){s.textContent='Request failed';s.className='ins-st err'}})
1122
+ }
1123
+ window.saveOmlElem=saveOmlElem
1124
+
1125
+ function movePanelNode(isComp,name,instanceIndex,dir){
1126
+ var stId=isComp?'oc-st':'oe-st'
1127
+ var st=document.getElementById(stId)
1128
+ if(st){st.textContent='Moving...';st.className='ins-st'}
1129
+ fetch('/_davaux/move',{
1130
+ method:'POST',headers:{'Content-Type':'application/json'},
1131
+ body:JSON.stringify({page:pg.split('?')[0],name:name,isComponent:isComp,instanceIndex:instanceIndex,direction:dir})
1132
+ })
1133
+ .then(function(r){return r.json()})
1134
+ .then(function(res){
1135
+ var s=document.getElementById(stId);if(!s)return
1136
+ if(res.moved){s.textContent='';s.className='ins-st'}
1137
+ else{s.textContent=res.error;s.className='ins-st err'}
1138
+ })
1139
+ .catch(function(){var s=document.getElementById(stId);if(s){s.textContent='Request failed';s.className='ins-st err'}})
1140
+ }
1141
+ window.movePanelNode=movePanelNode
1142
+
1143
+ function findByName(node,name){
1144
+ if(!node)return null
1145
+ if(node.type==='#component'&&node.name===name)return node
1146
+ var kids
1147
+ if(node.type==='element'||node.type==='#fragment'){kids=node.children||[]}
1148
+ else if(node.type==='#component'){kids=(node.output&&node.output.type!=='#raw')?[node.output]:(node.children||[])}
1149
+ else{kids=[]}
1150
+ for(var i=0;i<kids.length;i++){var f=findByName(kids[i],name);if(f)return f}
1151
+ return null
1152
+ }
1153
+ function findByOmlComp(root,name,inst){
1154
+ var state={n:0,found:null}
1155
+ walkFindOmlComp(root,name,inst,state)
1156
+ return state.found
1157
+ }
1158
+ function walkFindOmlComp(node,name,inst,state){
1159
+ if(!node||state.found)return
1160
+ if(node.type==='#component'){
1161
+ if(node.name===name){
1162
+ if(state.n===inst){state.found=node;return}
1163
+ state.n++
1164
+ }
1165
+ var sub=(node.output&&node.output.type!=='#raw')?[node.output]:(node.children||[])
1166
+ for(var i=0;i<sub.length;i++)walkFindOmlComp(sub[i],name,inst,state)
1167
+ }else if(node.type==='element'||node.type==='#fragment'){
1168
+ var kids=node.children||[]
1169
+ for(var i=0;i<kids.length;i++)walkFindOmlComp(kids[i],name,inst,state)
1170
+ }
1171
+ }
1172
+ function nodeMatchesCriteria(node,tag,txt,cls){
1173
+ if(node.tag!==tag)return false
1174
+ var nc=(node.props&&node.props.class)||''
1175
+ // Class must match when provided — prevents div.grid from matching div.card.
1176
+ if(cls&&nc!==cls)return false
1177
+ // Text is always checked as a tiebreaker — prevents the first div.card from
1178
+ // matching when clicking a different div.card that shares the same class.
1179
+ if(txt){var nt=extractText(node).trim();if(nt.slice(0,30)!==txt.slice(0,30))return false}
1180
+ return true
1181
+ }
1182
+ function findInTree(node,tag,txt,cls){
1183
+ if(!node)return null
1184
+ // Elements: check children FIRST so we always return the deepest (most specific) match.
1185
+ if(node.type==='element'){
1186
+ var ekids=node.children||[]
1187
+ for(var i=0;i<ekids.length;i++){var ef=findInTree(ekids[i],tag,txt,cls);if(ef)return ef}
1188
+ if(nodeMatchesCriteria(node,tag,txt,cls))return node
1189
+ return null
1190
+ }
1191
+ if(node.type==='#component'){
1192
+ // For #raw-returning components, search in JSX children (the hierarchy the user wrote)
1193
+ if(node.output&&node.output.type==='#raw'){
1194
+ var jkids=node.children||[]
1195
+ for(var i=0;i<jkids.length;i++){var jf=findInTree(jkids[i],tag,txt,cls);if(jf)return jf}
1196
+ return null
1197
+ }
1198
+ if(node.output){
1199
+ var r=node.output
1200
+ if(r.type==='element'&&nodeMatchesCriteria(r,tag,txt,cls))return node
1201
+ var fc=findInTree(r,tag,txt,cls)
1202
+ if(fc)return node
1203
+ }
1204
+ return null
1205
+ }
1206
+ if(node.type==='#fragment'){
1207
+ var fkids=node.children||[]
1208
+ for(var i=0;i<fkids.length;i++){var ff=findInTree(fkids[i],tag,txt,cls);if(ff)return ff}
1209
+ return null
1210
+ }
1211
+ return null
1212
+ }
1213
+ function extractText(node){
1214
+ if(!node)return''
1215
+ if(node.type==='#text')return node.value
1216
+ if(node.type==='element'||node.type==='#fragment')return(node.children||[]).map(extractText).join('')
1217
+ if(node.type==='#component'){
1218
+ if(node.output&&node.output.type!=='#raw')return extractText(node.output)
1219
+ return(node.children||[]).map(extractText).join('')
1220
+ }
1221
+ return''
1222
+ }
1223
+
1224
+ // ── Tree Panel ────────────────────────────────────────────────────────────────
1225
+ function renderLayoutButtons(){
1226
+ var container=document.getElementById('layout-btns')
1227
+ if(!container)return
1228
+ container.innerHTML=''
1229
+ if(!pageLayouts.length)return
1230
+ pageLayouts.forEach(function(l){
1231
+ var btn=document.createElement('button')
1232
+ btn.className='tool-btn'
1233
+ btn.title=l.filePath
1234
+ var parts=l.filePath.replace(/\\\\/g,'/').split('/')
1235
+ var label=pageLayouts.length===1?'Layout':(parts.length>2?parts.slice(-2).join('/'):l.filePath)
1236
+ btn.textContent='♰ '+label
1237
+ btn.onclick=function(){openLayoutModal(l.filePath)}
1238
+ container.appendChild(btn)
1239
+ })
1240
+ }
1241
+ function openLayoutModal(filePath){
1242
+ srcModalMode='file'
1243
+ pageFilePath=filePath
1244
+ fetch('/_davaux/get-source?file='+encodeURIComponent(filePath))
1245
+ .then(function(r){return r.json()})
1246
+ .then(function(d){pageSourceContent=d.content||'';openSrcModal('file')})
1247
+ .catch(function(){})
1248
+ }
1249
+ function openPageSrcModal(){
1250
+ if(!pageOwnFilePath)return
1251
+ pageFilePath=pageOwnFilePath
1252
+ srcModalMode='file'
1253
+ fetch('/_davaux/get-source?file='+encodeURIComponent(pageOwnFilePath))
1254
+ .then(function(r){return r.json()})
1255
+ .then(function(d){pageSourceContent=d.content||'';openSrcModal('file')})
1256
+ .catch(function(){})
1257
+ }
1258
+ function enterSourceMode(){
1259
+ var root=document.getElementById('tree-root')
1260
+ if(root)root.innerHTML='<div class="empty">'+esc(pageFileType.toUpperCase())+' page</div>'
1261
+ var det=document.getElementById('det')
1262
+ if(det)det.innerHTML='<div class="no-sel">Click <b>Edit Source</b> in the toolbar to edit this page.</div>'
1263
+ var btn=document.getElementById('edit-src-btn')
1264
+ if(btn)btn.style.display=''
1265
+ if(!pageFilePath)return
1266
+ fetch('/_davaux/get-source?file='+encodeURIComponent(pageFilePath))
1267
+ .then(function(r){return r.json()})
1268
+ .then(function(d){pageSourceContent=d.content||''})
1269
+ .catch(function(){})
1270
+ }
1271
+ function openSrcModal(mode){
1272
+ if(mode)srcModalMode=mode
1273
+ var modal=document.getElementById('src-modal')
1274
+ var title=document.getElementById('src-modal-title')
1275
+ var ta=document.getElementById('src-ta')
1276
+ var saveBtn=document.getElementById('src-save-btn')
1277
+ var st=document.getElementById('src-st')
1278
+ var hlBtn=document.getElementById('src-hl-btn')
1279
+ if(!modal||!ta)return
1280
+ if(title)title.textContent=srcModalMode==='fragment'?('<'+srcFragmentComp+'> children'):srcModalMode==='element-fragment'?('<'+srcFragmentTag+'> children'):(pageFilePath||'')
1281
+ if(st)st.textContent=''
1282
+ if(saveBtn){saveBtn.disabled=false;saveBtn.onclick=saveSource}
1283
+ if(hlBtn)hlBtn.classList.toggle('active',srcHighlight)
1284
+ if(cmEditor&&cmEditor._dvxMode!==srcModalMode){cmEditor.destroy();cmEditor=null}
1285
+ if(cmEditor){
1286
+ try{cmEditor.dispatch({changes:{from:0,to:cmEditor.state.doc.length,insert:pageSourceContent}})}
1287
+ catch(_){cmEditor.destroy();cmEditor=null}
1288
+ }
1289
+ if(!cmEditor){
1290
+ ta.value=pageSourceContent
1291
+ ta.onkeydown=function(e){
1292
+ if(e.key==='Tab'){e.preventDefault();var s=this.selectionStart,en=this.selectionEnd;this.value=this.value.substring(0,s)+' '+this.value.substring(en);this.selectionStart=this.selectionEnd=s+2;pageSourceContent=this.value}
1293
+ if(e.key==='Escape'){closeSrcModal()}
1294
+ }
1295
+ ta.oninput=function(){pageSourceContent=this.value}
1296
+ if(srcHighlight)enableHighlight()
1297
+ }
1298
+ modal.style.display='flex'
1299
+ if(!cmEditor)ta.focus()
1300
+ }
1301
+ function closeSrcModal(){
1302
+ var modal=document.getElementById('src-modal')
1303
+ if(modal)modal.style.display='none'
1304
+ }
1305
+ function saveSource(){
1306
+ var btn=document.getElementById('src-save-btn'),st=document.getElementById('src-st')
1307
+ if(btn)btn.disabled=true
1308
+ if(st)st.textContent='Saving…'
1309
+ var body=srcModalMode==='fragment'
1310
+ ?JSON.stringify({file:pageFilePath,component:srcFragmentComp,instanceIndex:srcFragmentIdx,content:pageSourceContent})
1311
+ :srcModalMode==='element-fragment'
1312
+ ?JSON.stringify({file:pageFilePath,tag:srcFragmentTag,instanceIndex:srcFragmentIdx,content:pageSourceContent})
1313
+ :JSON.stringify({file:pageFilePath,content:pageSourceContent})
1314
+ var url=srcModalMode==='fragment'?'/_davaux/update-fragment':srcModalMode==='element-fragment'?'/_davaux/update-element-fragment':'/_davaux/update-source'
1315
+ fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:body})
1316
+ .then(function(r){return r.json()})
1317
+ .then(function(){if(st){st.textContent='Saved';setTimeout(function(){st.textContent=''},2000)}if(btn)btn.disabled=false})
1318
+ .catch(function(){if(st)st.textContent='Error saving';if(btn)btn.disabled=false})
1319
+ }
1320
+ async function enableHighlight(){
1321
+ var hlBtn=document.getElementById('src-hl-btn')
1322
+ var ta=document.getElementById('src-ta')
1323
+ var host=document.getElementById('src-cm-host')
1324
+ if(!ta||!host)return
1325
+ if(hlBtn){hlBtn.disabled=true;hlBtn.textContent='⟳ Loading…'}
1326
+ if(!cmModules){
1327
+ try{
1328
+ var [view,cmds,lang,langMd,langJs,thm]=await Promise.all([
1329
+ import('https://esm.sh/@codemirror/view@6'),
1330
+ import('https://esm.sh/@codemirror/commands@6'),
1331
+ import('https://esm.sh/@codemirror/language@6'),
1332
+ import('https://esm.sh/@codemirror/lang-markdown@6'),
1333
+ import('https://esm.sh/@codemirror/lang-javascript@6'),
1334
+ import('https://esm.sh/@codemirror/theme-one-dark@6'),
1335
+ ])
1336
+ cmModules={EditorView:view.EditorView,lineNumbers:view.lineNumbers,keymap:view.keymap,drawSelection:view.drawSelection,highlightActiveLine:view.highlightActiveLine,history:cmds.history,defaultKeymap:cmds.defaultKeymap,historyKeymap:cmds.historyKeymap,syntaxHighlighting:lang.syntaxHighlighting,defaultHighlightStyle:lang.defaultHighlightStyle,indentOnInput:lang.indentOnInput,LanguageDescription:lang.LanguageDescription,markdown:langMd.markdown,markdownLanguage:langMd.markdownLanguage,javascript:langJs.javascript,oneDark:thm.oneDark}
1337
+ }catch(e){
1338
+ console.error('CodeMirror load failed',e)
1339
+ srcHighlight=false
1340
+ localStorage.setItem('dvx-src-hl','0')
1341
+ if(hlBtn){hlBtn.disabled=false;hlBtn.textContent='✦ Highlight';hlBtn.classList.remove('active')}
1342
+ return
1343
+ }
1344
+ }
1345
+ var isDark=document.documentElement.dataset.dvxTheme!=='light'
1346
+ var isFragment=srcModalMode==='fragment'||srcModalMode==='element-fragment'
1347
+ var isTsx=!isFragment&&pageFilePath&&/\\.tsx?$/.test(pageFilePath)
1348
+ var m=cmModules
1349
+ var LD=m.LanguageDescription
1350
+ var activeLang=(isFragment||isTsx)
1351
+ ?m.javascript({jsx:true,typescript:true})
1352
+ :m.markdown({base:m.markdownLanguage,codeLanguages:[
1353
+ LD.of({name:'JavaScript',alias:['js','javascript'],extensions:['js'],support:m.javascript()}),
1354
+ LD.of({name:'TypeScript',alias:['ts','typescript'],extensions:['ts'],support:m.javascript({typescript:true})}),
1355
+ LD.of({name:'JSX',alias:['jsx'],extensions:['jsx'],support:m.javascript({jsx:true})}),
1356
+ LD.of({name:'TSX',alias:['tsx'],extensions:['tsx'],support:m.javascript({jsx:true,typescript:true})}),
1357
+ ]})
1358
+ cmEditor=new m.EditorView({
1359
+ doc:pageSourceContent,
1360
+ extensions:[
1361
+ m.lineNumbers(),
1362
+ m.history(),
1363
+ m.keymap.of([...m.defaultKeymap,...m.historyKeymap]),
1364
+ m.drawSelection(),
1365
+ m.highlightActiveLine(),
1366
+ m.indentOnInput(),
1367
+ m.syntaxHighlighting(m.defaultHighlightStyle),
1368
+ ...(isDark?[m.oneDark]:[]),
1369
+ activeLang,
1370
+ m.EditorView.updateListener.of(function(upd){if(upd.docChanged)pageSourceContent=upd.state.doc.toString()}),
1371
+ m.EditorView.domEventHandlers({keydown:function(e){if(e.key==='Escape'){closeSrcModal();return true}}}),
1372
+ ],
1373
+ parent:host
1374
+ })
1375
+ cmEditor._dvxDark=isDark
1376
+ cmEditor._dvxMode=srcModalMode
1377
+ ta.style.display='none'
1378
+ host.style.display='flex'
1379
+ host.style.flexDirection='column'
1380
+ if(hlBtn){hlBtn.disabled=false;hlBtn.textContent='✦ Highlight';hlBtn.classList.add('active')}
1381
+ }
1382
+ function openFragmentModal(){
1383
+ if(!ocState||!pageOwnFilePath)return
1384
+ pageFilePath=pageOwnFilePath
1385
+ srcFragmentComp=ocState.name
1386
+ srcFragmentIdx=ocState.instanceIndex
1387
+ srcModalMode='fragment'
1388
+ var det=document.getElementById('det')
1389
+ var st=document.getElementById('src-st')
1390
+ if(st)st.textContent=''
1391
+ fetch('/_davaux/get-fragment?file='+encodeURIComponent(pageOwnFilePath)+'&component='+encodeURIComponent(srcFragmentComp)+'&instanceIndex='+srcFragmentIdx)
1392
+ .then(function(r){return r.json()})
1393
+ .then(function(d){
1394
+ if(!d.found){if(det)det.innerHTML='<div class="no-sel" style="color:#ff8888">'+esc(d.error)+'</div>';return}
1395
+ pageSourceContent=d.content||''
1396
+ openSrcModal('fragment')
1397
+ })
1398
+ .catch(function(){if(det)det.innerHTML='<div class="no-sel" style="color:#ff8888">Could not load fragment.</div>'})
1399
+ }
1400
+ function openElementFragmentModal(){
1401
+ if(!oeState||!pageOwnFilePath)return
1402
+ pageFilePath=pageOwnFilePath
1403
+ srcFragmentTag=oeState.tag
1404
+ srcFragmentIdx=oeState.instanceIndex
1405
+ srcModalMode='element-fragment'
1406
+ var det=document.getElementById('det')
1407
+ fetch('/_davaux/get-element-fragment?file='+encodeURIComponent(pageOwnFilePath)+'&tag='+encodeURIComponent(srcFragmentTag)+'&instanceIndex='+srcFragmentIdx)
1408
+ .then(function(r){return r.json()})
1409
+ .then(function(d){
1410
+ if(!d.found){if(det)det.innerHTML='<div class="no-sel" style="color:#ff8888">'+esc(d.error)+'</div>';return}
1411
+ pageSourceContent=d.content||''
1412
+ openSrcModal('element-fragment')
1413
+ })
1414
+ .catch(function(){if(det)det.innerHTML='<div class="no-sel" style="color:#ff8888">Could not load fragment.</div>'})
1415
+ }
1416
+ function disableHighlight(){
1417
+ var ta=document.getElementById('src-ta')
1418
+ var host=document.getElementById('src-cm-host')
1419
+ if(cmEditor){pageSourceContent=cmEditor.state.doc.toString();cmEditor.destroy();cmEditor=null}
1420
+ if(ta){ta.value=pageSourceContent;ta.style.display='';ta.focus()}
1421
+ if(host){host.style.display='none';host.innerHTML=''}
1422
+ var hlBtn=document.getElementById('src-hl-btn')
1423
+ if(hlBtn){hlBtn.classList.remove('active');hlBtn.textContent='✦ Highlight'}
1424
+ }
1425
+ function toggleHighlight(){
1426
+ srcHighlight=!srcHighlight
1427
+ localStorage.setItem('dvx-src-hl',srcHighlight?'1':'0')
1428
+ if(srcHighlight)enableHighlight()
1429
+ else disableHighlight()
1430
+ }
1431
+ function renderTreePanel(){
1432
+ var root=document.getElementById('tree-root')
1433
+ if(!root)return
1434
+ if(!omlTree){root.innerHTML='<div class="empty">Load a page to see the tree.</div>';return}
1435
+ root.innerHTML=''
1436
+ var dom=buildTreeNode(omlTree,0,true)
1437
+ if(dom)root.appendChild(dom)
1438
+ else root.innerHTML='<div class="empty">Empty page.</div>'
1439
+ }
1440
+
1441
+ function buildTreeNode(node,depth,isRoot){
1442
+ if(!node)return null
1443
+ if(node.type==='#raw')return null
1444
+ if(node.type==='#text'){
1445
+ var v=String(node.value||'').trim()
1446
+ if(!v)return null
1447
+ var w=document.createElement('div')
1448
+ var row=document.createElement('div')
1449
+ row.className='tr-row'
1450
+ row.style.paddingLeft=(depth*12+4)+'px'
1451
+ row.innerHTML='<span class="tr-tog"> </span><span class="tr-lbl"><span class="tr-txt">"'+esc(v.length>28?v.slice(0,28)+'…':v)+'"</span></span>'
1452
+ w.appendChild(row)
1453
+ return w
1454
+ }
1455
+ var cs
1456
+ if(node.type==='#fragment'){
1457
+ cs=(node.children||[]).filter(Boolean)
1458
+ if(isRoot||depth===0){
1459
+ var w=document.createElement('div')
1460
+ cs.forEach(function(c){var ch=buildTreeNode(c,depth,false);if(ch)w.appendChild(ch)})
1461
+ return w
1462
+ }
1463
+ }else if(node.type==='#component'){
1464
+ // If return is #raw (library component), show JSX children hierarchy instead
1465
+ if(node.output&&node.output.type!=='#raw'){cs=[node.output]}
1466
+ else{cs=(node.children||[]).filter(Boolean)}
1467
+ }else{
1468
+ var allCs=(node.children||[]).filter(Boolean)
1469
+ var hasElemKids=allCs.some(function(c){return c.type!=='#text'&&c.type!=='#raw'})
1470
+ cs=hasElemKids?allCs.filter(function(c){return c.type!=='#text'&&c.type!=='#raw'}):[]
1471
+ }
1472
+ var hk=cs&&cs.length>0
1473
+ var collapsed=false
1474
+ var w=document.createElement('div')
1475
+ var row=document.createElement('div')
1476
+ row.className='tr-row'
1477
+ row.style.paddingLeft=(depth*12+4)+'px'
1478
+ var togSpan=document.createElement('span')
1479
+ togSpan.className='tr-tog'
1480
+ togSpan.textContent=hk?'▼':' '
1481
+ var lblSpan=document.createElement('span')
1482
+ lblSpan.className='tr-lbl'
1483
+ if(node.type==='element'){
1484
+ var cls=(node.props&&node.props.class)||''
1485
+ var fc=cls?'.'+cls.split(' ')[0]:''
1486
+ lblSpan.innerHTML='<span class="tr-tag">&lt;'+esc(node.tag+fc)+'&gt;</span>'
1487
+ row._omlNode=node
1488
+ row.addEventListener('click',function(e){if(e.target===togSpan)return;selectFromTree(node)})
1489
+ }else if(node.type==='#component'){
1490
+ lblSpan.innerHTML='<span class="tr-comp">&#9826; '+esc(node.name)+'</span>'
1491
+ row._omlNode=node
1492
+ row.addEventListener('click',function(e){if(e.target===togSpan)return;selectFromTree(node)})
1493
+ }else{
1494
+ lblSpan.innerHTML='<span class="tr-frag">&lt;&gt;</span>'
1495
+ row.addEventListener('click',function(){if(!hk)return;collapsed=!collapsed;kidsDiv.style.display=collapsed?'none':'';togSpan.textContent=collapsed?'▶':'▼'})
1496
+ }
1497
+ row.appendChild(togSpan)
1498
+ row.appendChild(lblSpan)
1499
+ w.appendChild(row)
1500
+ if(hk){
1501
+ var kidsDiv=document.createElement('div')
1502
+ cs.forEach(function(c){var ch=buildTreeNode(c,depth+1,false);if(ch)kidsDiv.appendChild(ch)})
1503
+ w.appendChild(kidsDiv)
1504
+ togSpan.addEventListener('click',function(e){
1505
+ e.stopPropagation()
1506
+ collapsed=!collapsed
1507
+ kidsDiv.style.display=collapsed?'none':''
1508
+ togSpan.textContent=collapsed?'▶':'▼'
1509
+ })
1510
+ }
1511
+ return w
1512
+ }
1513
+
1514
+ function selectFromTree(node){
1515
+ if(!node)return
1516
+ var det=document.getElementById('det')
1517
+ if(!det)return
1518
+ if(selected){try{selected.style.outline='';selected.style.outlineOffset=''}catch(_){}selected=null}
1519
+ detMode='comp'
1520
+ document.querySelectorAll('.dtab').forEach(function(t){t.classList.toggle('active',t.dataset.mode==='comp')})
1521
+ sel=-1
1522
+ document.querySelectorAll('.bp-card').forEach(function(c){c.classList.remove('sel')})
1523
+ if(node.type==='#component'){
1524
+ var treeCompIdx=countNameBefore(omlTree,node,node.name)
1525
+ var treeRawProps=Object.assign({},node.props||{})
1526
+ var chi=compChildrenInfo(node)
1527
+ ocState={name:node.name,props:treeRawProps,instanceIndex:treeCompIdx,childrenText:chi.childrenText,hasChildren:chi.hasChildren}
1528
+ oeState=null
1529
+ renderOcPanel(det)
1530
+ }else if(node.type==='element'){
1531
+ var idx=countTagBefore(omlTree,node,node.tag)
1532
+ var elemText=null
1533
+ var allCs=(node.children||[]).filter(Boolean)
1534
+ var onlyTxt=allCs.length>0&&allCs.every(function(c){return c.type==='#text'})
1535
+ if(onlyTxt){var acc='';allCs.forEach(function(c){acc+=c.value||''});if(acc.trim())elemText=acc}
1536
+ oeState={tag:node.tag,props:Object.assign({},node.props||{}),instanceIndex:idx,textContent:elemText}
1537
+ ocState=null
1538
+ renderOePanel(det)
1539
+ }
1540
+ try{
1541
+ var domEl=findDomElForNode(node)
1542
+ if(domEl){
1543
+ selected=domEl
1544
+ domEl.style.outline='2px solid #7cc5f0'
1545
+ domEl.style.outlineOffset='-1px'
1546
+ domEl.scrollIntoView({behavior:'smooth',block:'nearest'})
1547
+ }
1548
+ }catch(_){}
1549
+ syncTreeRow(node)
1550
+ }
1551
+
1552
+ function findDomElForNode(node){
1553
+ if(!node)return null
1554
+ var doc=frame.contentDocument
1555
+ if(!doc)return null
1556
+ if(node.type==='#component'){
1557
+ var islandEl=doc.querySelector('[data-island="'+node.name+'"]')
1558
+ if(islandEl)return islandEl
1559
+ var compInst=countNameBefore(omlTree,node,node.name)
1560
+ var compEls=doc.querySelectorAll('[data-oml-comp="'+node.name+'"]')
1561
+ return compEls[compInst]||null
1562
+ }
1563
+ if(node.type==='element'){
1564
+ var tag=node.tag
1565
+ var cls=(node.props&&node.props.class)||''
1566
+ var txt=extractText(node).trim().slice(0,30)
1567
+ var els=Array.from(doc.querySelectorAll(tag))
1568
+ for(var i=0;i<els.length;i++){
1569
+ var el=els[i]
1570
+ var elCls=el.getAttribute?el.getAttribute('class')||'':''
1571
+ var elTxt=(el.textContent||'').trim().slice(0,30)
1572
+ if((!cls||elCls===cls)&&(!txt||elTxt===txt))return el
1573
+ }
1574
+ }
1575
+ return null
1576
+ }
1577
+
1578
+ function syncTreeRow(node){
1579
+ if(selectedTreeRow){selectedTreeRow.classList.remove('tr-sel');selectedTreeRow=null}
1580
+ if(!node)return
1581
+ var root=document.getElementById('tree-root')
1582
+ if(!root)return
1583
+ var rows=root.querySelectorAll('.tr-row')
1584
+ for(var i=0;i<rows.length;i++){
1585
+ if(rows[i]._omlNode===node){
1586
+ selectedTreeRow=rows[i]
1587
+ selectedTreeRow.classList.add('tr-sel')
1588
+ try{selectedTreeRow.scrollIntoView({block:'nearest'})}catch(_){}
1589
+ break
1590
+ }
1591
+ }
1592
+ }
1593
+
1594
+ // ── Route picker ─────────────────────────────────────────────────────────────
1595
+ fetch('/_davaux/routes').then(function(r){return r.json()}).then(function(routes){
1596
+ var dl=document.getElementById('route-list')
1597
+ routes.forEach(function(r){
1598
+ var opt=document.createElement('option')
1599
+ opt.value=r.urlPattern
1600
+ dl.appendChild(opt)
1601
+ })
1602
+ }).catch(function(){})
1603
+
1604
+ // ── Blueprint palette ─────────────────────────────────────────────────────────
1605
+ function refreshBps(){
1606
+ fetch('/_davaux/blueprints').then(function(r){return r.json()}).then(function(data){
1607
+ bps=data
1608
+ var list=document.getElementById('bplist')
1609
+ if(!bps.length){list.innerHTML='<div class="empty">No blueprints found.<br>Add .oml.json files to<br>src/blueprints/</div>';return}
1610
+ list.innerHTML=''
1611
+ bps.forEach(function(bp,i){
1612
+ var card=document.createElement('div')
1613
+ card.className='bp-card'
1614
+ if(bp.previewHtml){var pv=document.createElement('div');pv.className='bp-prev';pv.innerHTML=bp.previewHtml;card.appendChild(pv)}
1615
+ var lbl=document.createElement('div');lbl.className='bp-lbl';lbl.textContent=bp.name;card.appendChild(lbl)
1616
+ card.onclick=function(){pick(i)}
1617
+ list.appendChild(card)
1618
+ })
1619
+ }).catch(function(){document.getElementById('bplist').innerHTML='<div class="empty">Failed to load.</div>'})
1620
+ }
1621
+ refreshBps()
1622
+
1623
+ // ── Blueprint editor ──────────────────────────────────────────────────────────
1624
+ var editBp=null
1625
+ var ocState=null
1626
+ var oeState=null
1627
+ function startEdit(i){
1628
+ var bp=bps[i]
1629
+ editBp={idx:i,id:bp.id,name:bp.name,rows:Object.entries(bp.props||{}).map(function(e){
1630
+ var s=e[1];return{name:e[0],type:s.type||'string',required:!!s.required,def:s.default!==undefined?String(s.default):''}
1631
+ })}
1632
+ renderEditPanel()
1633
+ }
1634
+ window.startEdit=startEdit
1635
+ function renderEditPanel(){
1636
+ if(!editBp)return
1637
+ var det=document.getElementById('det')
1638
+ det.innerHTML=
1639
+ '<div style="display:flex;align-items:center;gap:6px;margin-bottom:10px">'
1640
+ +'<div id="ep-title" class="d-title" style="margin:0;flex:1">'+esc(editBp.name)+'</div>'
1641
+ +'</div>'
1642
+ +'<div class="d-lbl">NAME</div>'
1643
+ +'<input id="ep-bpname" class="pi" style="margin-bottom:8px" value="'+esc(editBp.name)+'">'
1644
+ +'<div class="d-lbl">PROPS</div>'
1645
+ +'<div id="ep-list"></div>'
1646
+ +'<button class="cbtn" style="margin-bottom:8px" onclick="epAdd()">+ Add prop</button>'
1647
+ +'<div style="display:flex;gap:4px;margin-bottom:4px">'
1648
+ +'<button class="cbtn ebtn" onclick="epSave()">Save</button>'
1649
+ +'<button class="cbtn" style="background:#1a1a2e;flex:1" onclick="epCancel()">Cancel</button>'
1650
+ +'</div>'
1651
+ +'<div id="ep-st" class="ins-st"></div>'
1652
+ renderEpRows()
1653
+ document.getElementById('ep-bpname').addEventListener('input',function(){
1654
+ editBp.name=this.value
1655
+ var t=document.getElementById('ep-title');if(t)t.textContent=this.value
1656
+ })
1657
+ }
1658
+ function renderEpRows(){
1659
+ var list=document.getElementById('ep-list');if(!list)return
1660
+ list.innerHTML=''
1661
+ editBp.rows.forEach(function(row,idx){
1662
+ var d=document.createElement('div');d.className='ep-row'
1663
+ var ni=document.createElement('input');ni.type='text';ni.className='pi ep-name';ni.value=row.name;ni.placeholder='name'
1664
+ ni.addEventListener('input',function(){editBp.rows[idx].name=this.value})
1665
+ var ts=document.createElement('select');ts.className='ep-type'
1666
+ ;['string','number','boolean','function','node','array'].forEach(function(t){
1667
+ var o=document.createElement('option');o.value=t;o.textContent=t;if(t===row.type)o.selected=true;ts.appendChild(o)
1668
+ })
1669
+ ts.addEventListener('change',function(){editBp.rows[idx].type=this.value})
1670
+ var rl=document.createElement('label');rl.className='ep-req-lbl'
1671
+ var rc=document.createElement('input');rc.type='checkbox';rc.className='pi-cb';rc.checked=row.required
1672
+ rc.addEventListener('change',function(){editBp.rows[idx].required=this.checked})
1673
+ rl.appendChild(rc);rl.appendChild(Object.assign(document.createElement('span'),{textContent:'req'}))
1674
+ var di=document.createElement('input');di.type='text';di.className='pi ep-def';di.value=row.def;di.placeholder='default'
1675
+ di.addEventListener('input',function(){editBp.rows[idx].def=this.value})
1676
+ var rm=document.createElement('button');rm.className='ep-rm';rm.innerHTML='✕';rm.title='Remove'
1677
+ rm.addEventListener('click',function(){editBp.rows.splice(idx,1);renderEpRows()})
1678
+ d.appendChild(ni);d.appendChild(ts);d.appendChild(rl);d.appendChild(di);d.appendChild(rm)
1679
+ list.appendChild(d)
1680
+ })
1681
+ }
1682
+ window.epAdd=function(){
1683
+ editBp.rows.push({name:'',type:'string',required:false,def:''})
1684
+ renderEpRows()
1685
+ var list=document.getElementById('ep-list')
1686
+ if(list){var ins=list.querySelectorAll('.ep-name');if(ins.length)ins[ins.length-1].focus()}
1687
+ }
1688
+ window.epCancel=function(){
1689
+ editBp=null
1690
+ if(sel>=0)renderBpDet(bps[sel])
1691
+ else document.getElementById('det').innerHTML='<div class="no-sel">Select a component to see details.</div>'
1692
+ }
1693
+ window.epSave=function(){
1694
+ var st=document.getElementById('ep-st')
1695
+ if(st){st.textContent='Saving...';st.className='ins-st'}
1696
+ var props={}
1697
+ editBp.rows.forEach(function(row){
1698
+ var n=row.name.trim();if(!n)return
1699
+ var s={type:row.type,required:row.required}
1700
+ if(row.def!==''){
1701
+ if(row.type==='boolean')s.default=row.def==='true'
1702
+ else if(row.type==='number')s.default=Number(row.def)
1703
+ else s.default=row.def
1704
+ }
1705
+ props[n]=s
1706
+ })
1707
+ fetch('/_davaux/update-blueprint',{
1708
+ method:'POST',headers:{'Content-Type':'application/json'},
1709
+ body:JSON.stringify({id:editBp.id,name:editBp.name,props:props})
1710
+ })
1711
+ .then(function(r){return r.json()})
1712
+ .then(function(res){
1713
+ if(res.saved){
1714
+ editBp=null;sel=-1
1715
+ document.getElementById('det').innerHTML='<div class="no-sel">Blueprint saved.</div>'
1716
+ refreshBps()
1717
+ }else{if(st){st.textContent=res.error||'Save failed';st.className='ins-st err'}}
1718
+ })
1719
+ .catch(function(){if(st){st.textContent='Request failed';st.className='ins-st err'}})
1720
+ }
1721
+
1722
+ // ── Prop editing ──────────────────────────────────────────────────────────────
1723
+ function pick(i){
1724
+ detMode='comp'
1725
+ document.querySelectorAll('.dtab').forEach(function(t){t.classList.toggle('active',t.dataset.mode==='comp')})
1726
+ sel=i
1727
+ overrides={}
1728
+ ocState=null
1729
+ document.querySelectorAll('.bp-card').forEach(function(c,j){c.classList.toggle('sel',j===i)})
1730
+ renderBpDet(bps[i])
1731
+ }
1732
+
1733
+ function renderBpDet(bp){
1734
+ var pe=Object.entries(bp.props||{})
1735
+ var h='<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px"><div class="d-title" style="margin:0;flex:1">'+esc(bp.name)+'</div><button class="cbtn ebtn" style="padding:2px 8px;font-size:10px;width:auto" onclick="startEdit('+sel+')">Edit</button></div>'
1736
+ if(pe.length){
1737
+ h+='<div class="d-lbl">PROPS</div>'
1738
+ pe.forEach(function(e){
1739
+ var name=e[0],schema=e[1]
1740
+ var def=schema.default!==undefined?schema.default:defVal(schema.type)
1741
+ h+='<div class="pi-row"><label class="pi-lbl"><span class="pn">'+esc(name)+'</span>'
1742
+ +'<span class="pt"> '+esc(schema.type)+'</span>'
1743
+ +(schema.required?'<span class="pr"> *</span>':'')+'</label>'
1744
+ if(schema.type==='boolean'){
1745
+ h+='<input type="checkbox" class="pi pi-cb" data-prop="'+esc(name)+'" data-type="boolean"'+(def?' checked':'')+'>'
1746
+ }else if(schema.type==='number'){
1747
+ h+='<input type="number" class="pi" data-prop="'+esc(name)+'" data-type="number" value="'+esc(String(def||0))+'">'
1748
+ }else{
1749
+ h+='<input type="text" class="pi" data-prop="'+esc(name)+'" data-type="string" value="'+esc(String(def||''))+'">'
1750
+ }
1751
+ h+='</div>'
1752
+ })
1753
+ }
1754
+ h+='<div class="d-lbl">JSX</div>'
1755
+ h+='<div class="jsx-block" id="jsx-out">'+esc(buildJsx(bp))+'</div>'
1756
+ h+='<button class="cbtn" id="cpbtn" onclick="cp()">Copy JSX</button>'
1757
+ var insLabel=insertTarget?('Insert after &lt;'+(insertTarget.name||insertTarget.tag)+'&gt;'):'Insert into page'
1758
+ h+='<button class="cbtn ibtn" id="ibtn" onclick="ins()">'+insLabel+'</button>'
1759
+ h+='<div id="ins-st" class="ins-st"></div>'
1760
+ var det=document.getElementById('det')
1761
+ det.innerHTML=h
1762
+ det.querySelectorAll('.pi').forEach(function(inp){
1763
+ inp.addEventListener('input',function(){
1764
+ var prop=this.dataset.prop,type=this.dataset.type
1765
+ overrides[prop]=type==='boolean'?this.checked:type==='number'?Number(this.value):this.value
1766
+ var out=document.getElementById('jsx-out')
1767
+ if(out)out.textContent=buildJsx(bps[sel])
1768
+ })
1769
+ })
1770
+ }
1771
+
1772
+ function buildJsx(bp){
1773
+ var pe=Object.entries(bp.props||{})
1774
+ if(!pe.length)return'<'+bp.name+' />'
1775
+ var lines=pe.map(function(e){
1776
+ var name=e[0],schema=e[1]
1777
+ var val=overrides[name]!==undefined?overrides[name]:(schema.default!==undefined?schema.default:defVal(schema.type))
1778
+ return' '+name+'='+fmtVal(schema.type,val)
1779
+ })
1780
+ return'<'+bp.name+NL+lines.join(NL)+NL+'/>'
1781
+ }
1782
+ function fmtVal(type,val){
1783
+ if(type==='string')return JSON.stringify(String(val))
1784
+ if(type==='boolean')return'{'+(val?'true':'false')+'}'
1785
+ if(type==='number')return'{'+Number(val)+'}'
1786
+ return'{'+JSON.stringify(val)+'}'
1787
+ }
1788
+ function defVal(type){
1789
+ return{string:'example',number:0,boolean:false,function:'() => {}',node:'content',array:[]}[type]??'example'
1790
+ }
1791
+
1792
+ function switchMode(m){
1793
+ detMode=m
1794
+ document.querySelectorAll('.dtab').forEach(function(t){t.classList.toggle('active',t.dataset.mode===m)})
1795
+ if(m==='styles'){renderStylesPanel()}
1796
+ else{
1797
+ if(sel>=0)renderBpDet(bps[sel])
1798
+ else document.getElementById('det').innerHTML='<div class="no-sel">Select a component to see details.</div>'
1799
+ }
1800
+ }
1801
+ function detectPageTheme(doc){
1802
+ try{
1803
+ var forced=window.__DVX_EDITOR__&&window.__DVX_EDITOR__.theme
1804
+ if(forced==='dark'||forced==='light'){document.documentElement.dataset.dvxTheme=forced;return}
1805
+ var html=doc&&doc.documentElement
1806
+ if(!html)return
1807
+ var scheme=html.dataset.mantineColorScheme||html.dataset.colorScheme||html.dataset.theme||''
1808
+ var dark=scheme==='dark'||(!scheme&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)
1809
+ document.documentElement.dataset.dvxTheme=dark?'dark':'light'
1810
+ // Swap CodeMirror theme if active
1811
+ if(cmEditor&&cmModules){
1812
+ var wasDark=cmEditor._dvxDark
1813
+ var nowDark=dark
1814
+ if(wasDark!==nowDark){
1815
+ disableHighlight()
1816
+ if(srcHighlight)enableHighlight()
1817
+ }
1818
+ }
1819
+ }catch(_){}
1820
+ }
1821
+ function scanCssVars(doc){
1822
+ var vars={}
1823
+ try{
1824
+ for(var i=0;i<doc.styleSheets.length;i++){
1825
+ var sheet=doc.styleSheets[i]
1826
+ try{
1827
+ var rules=sheet.cssRules
1828
+ for(var j=0;j<rules.length;j++){
1829
+ var rule=rules[j]
1830
+ if(rule.selectorText===':root'||rule.selectorText==='html'){
1831
+ var style=rule.style
1832
+ for(var k=0;k<style.length;k++){
1833
+ var prop=style[k]
1834
+ if(prop.charAt(0)==='-'&&prop.charAt(1)==='-'){
1835
+ var cv=style.getPropertyValue(prop).trim()
1836
+ vars[prop]={value:cv,type:detectVarType(cv)}
1837
+ }
1838
+ }
1839
+ }
1840
+ }
1841
+ }catch(_){}
1842
+ }
1843
+ }catch(_){}
1844
+ cssVarMap=vars
1845
+ }
1846
+ function detectVarType(val){
1847
+ var v=val.trim()
1848
+ if(/^#[0-9a-fA-F]{3,8}$/.test(v))return'color'
1849
+ if(v.indexOf('rgb')===0||v.indexOf('hsl')===0)return'color-fn'
1850
+ if(/^-?[0-9]+([.][0-9]+)?(px|rem|em|%|vw|vh|ch|ex|pt|fr)$/.test(v))return'size'
1851
+ if(/^-?[0-9]+([.][0-9]+)?$/.test(v))return'number'
1852
+ return'text'
1853
+ }
1854
+ var STYLE_GROUPS=[
1855
+ {label:'TYPOGRAPHY',props:['color','font-size','font-weight','line-height','text-align']},
1856
+ {label:'SPACING',props:['padding','margin','gap']},
1857
+ {label:'BACKGROUND',props:['background-color']},
1858
+ {label:'BORDER',props:['border-radius','border-width','border-color']},
1859
+ {label:'LAYOUT',props:['display','width','height','max-width']},
1860
+ ]
1861
+ function isVisibleColor(v){var t=v.trim();return(/^#/.test(t)||/^rgb/.test(t)||/^hsl/.test(t))&&t!=='rgba(0, 0, 0, 0)'&&t!=='transparent'}
1862
+ function renderStylePropRow(prop,computed,current){
1863
+ var showPicker=isVisibleColor(computed||'')&&/color|background/.test(prop)
1864
+ var pick=current&&isVisibleColor(current)?current:isVisibleColor(computed||'')?computed:''
1865
+ var h='<div class="cv-row">'
1866
+ h+='<div class="cv-name">'+esc(prop)+'</div>'
1867
+ if(showPicker){
1868
+ h+='<div class="cv-color-row">'
1869
+ h+='<input type="color" class="sty-inp cv-color" data-prop="'+esc(prop)+'" data-role="cp" value="'+esc(pick)+'">'
1870
+ h+='<input type="text" class="sty-inp cv-text" data-prop="'+esc(prop)+'" data-role="tx" placeholder="'+esc((computed||'').slice(0,24))+'" value="'+esc(current)+'">'
1871
+ h+='</div>'
1872
+ }else{
1873
+ h+='<input type="text" class="sty-inp cv-text" data-prop="'+esc(prop)+'" placeholder="'+esc((computed||'').slice(0,28))+'" value="'+esc(current)+'">'
1874
+ }
1875
+ h+='</div>'
1876
+ return h
1877
+ }
1878
+ function renderElementStylesSection(cs){
1879
+ var h='<div class="d-lbl">ELEMENT STYLES</div>'
1880
+ STYLE_GROUPS.forEach(function(g){
1881
+ var rows=g.props.map(function(p){
1882
+ var computed=''
1883
+ try{computed=cs.getPropertyValue(p)||''}catch(_){}
1884
+ var current=styleEdits[p]!==undefined?styleEdits[p]:(selected.style.getPropertyValue(p)||'')
1885
+ return renderStylePropRow(p,computed,current)
1886
+ }).join('')
1887
+ h+='<div class="d-lbl" style="font-size:9px;margin:8px 0 4px">'+g.label+'</div>'+rows
1888
+ })
1889
+ h+='<div style="display:flex;gap:4px;margin-top:8px">'
1890
+ h+='<button class="cbtn" id="style-save-btn" onclick="saveInlineStyles()">Apply Styles</button>'
1891
+ h+='</div>'
1892
+ h+='<div id="style-st" class="ins-st"></div>'
1893
+ h+='<div class="d-lbl" style="margin-top:16px">CSS VARIABLES</div>'
1894
+ return h
1895
+ }
1896
+ function renderStylesPanel(){
1897
+ var det=document.getElementById('det')
1898
+ var h=''
1899
+ if(selected){
1900
+ try{
1901
+ var cs=frame.contentDocument.defaultView.getComputedStyle(selected)
1902
+ h+=renderElementStylesSection(cs)
1903
+ }catch(_){}
1904
+ }
1905
+ var names=Object.keys(cssVarMap)
1906
+ if(!names.length&&!h){det.innerHTML='<div class="no-sel">Select an element to inspect its styles,<br>or add CSS custom properties on :root.</div>';return}
1907
+ if(names.length){
1908
+ if(!selected)h+='<div class="d-lbl">CSS VARIABLES</div>'
1909
+ var colors=names.filter(function(n){return cssVarMap[n].type==='color'})
1910
+ var colorFns=names.filter(function(n){return cssVarMap[n].type==='color-fn'})
1911
+ var sizes=names.filter(function(n){return cssVarMap[n].type==='size'||cssVarMap[n].type==='number'})
1912
+ var texts=names.filter(function(n){return cssVarMap[n].type==='text'})
1913
+ if(colors.length+colorFns.length){h+='<div class="d-lbl" style="font-size:9px;margin:8px 0 4px">COLORS</div>';colors.concat(colorFns).forEach(function(n){h+=renderVarRow(n)})}
1914
+ if(sizes.length){h+='<div class="d-lbl" style="font-size:9px;margin:8px 0 4px">SIZES</div>';sizes.forEach(function(n){h+=renderVarRow(n)})}
1915
+ if(texts.length){h+='<div class="d-lbl" style="font-size:9px;margin:8px 0 4px">OTHER</div>';texts.forEach(function(n){h+=renderVarRow(n)})}
1916
+ h+='<button class="cbtn" id="css-cpbtn" onclick="ccss()" style="margin-top:12px">Copy CSS</button>'
1917
+ }
1918
+ det.innerHTML=h
1919
+ // Wire CSS var inputs
1920
+ det.querySelectorAll('.cv-inp').forEach(function(inp){
1921
+ inp.addEventListener('input',function(){
1922
+ var name=this.dataset.var
1923
+ var val=this.value
1924
+ cssVarMap[name].value=val
1925
+ var companion=det.querySelector('[data-var="'+name+'"][data-role="'+(this.dataset.role==='cp'?'tx':'cp')+'"]')
1926
+ if(companion)companion.value=val
1927
+ try{frame.contentDocument.documentElement.style.setProperty(name,val)}catch(_){}
1928
+ })
1929
+ })
1930
+ // Wire element style inputs — live preview
1931
+ det.querySelectorAll('.sty-inp').forEach(function(inp){
1932
+ inp.addEventListener('input',function(){
1933
+ var prop=this.dataset.prop
1934
+ var val=this.value.trim()
1935
+ styleEdits[prop]=val
1936
+ if(this.dataset.role){
1937
+ var companion=det.querySelector('[data-prop="'+prop+'"][data-role="'+(this.dataset.role==='cp'?'tx':'cp')+'"]')
1938
+ if(companion)companion.value=val
1939
+ }
1940
+ try{if(val)selected.style.setProperty(prop,val);else selected.style.removeProperty(prop)}catch(_){}
1941
+ })
1942
+ })
1943
+ }
1944
+ function saveInlineStyles(){
1945
+ if(!oeState&&!ocState)return
1946
+ var det=document.getElementById('det')
1947
+ var st=document.getElementById('style-st')
1948
+ if(st){st.textContent='Saving...';st.className='ins-st'}
1949
+ // Build style string from edits
1950
+ var styleObj={}
1951
+ var existing=(oeState&&oeState.props&&oeState.props.style)||(ocState&&ocState.props&&ocState.props.style)||''
1952
+ if(typeof existing==='string'){existing.split(';').forEach(function(p){var i=p.indexOf(':');if(i>-1){var k=p.slice(0,i).trim(),v=p.slice(i+1).trim();if(k)styleObj[k]=v}})}
1953
+ det.querySelectorAll('.sty-inp[data-role="tx"],.sty-inp:not([data-role])').forEach(function(inp){
1954
+ var prop=inp.dataset.prop,val=inp.value.trim()
1955
+ if(val)styleObj[prop]=val;else delete styleObj[prop]
1956
+ })
1957
+ var styleStr=Object.entries(styleObj).map(function(e){return e[0]+': '+e[1]}).join('; ')
1958
+ var url,body
1959
+ if(oeState){
1960
+ var np=Object.assign({},oeState.props);if(styleStr)np.style=styleStr;else delete np.style
1961
+ url='/_davaux/update-element'
1962
+ body=JSON.stringify({page:pg.split('?')[0],tag:oeState.tag,newProps:np,instanceIndex:oeState.instanceIndex,textContent:oeState.textContent})
1963
+ }else{
1964
+ var np=Object.assign({},ocState.props);if(styleStr)np.style=styleStr;else delete np.style
1965
+ url='/_davaux/update-comp-props'
1966
+ body=JSON.stringify({page:pg.split('?')[0],component:ocState.name,newProps:np,instanceIndex:ocState.instanceIndex,childrenText:ocState.childrenText})
1967
+ }
1968
+ fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:body})
1969
+ .then(function(r){return r.json()})
1970
+ .then(function(res){
1971
+ var ok=res.replaced||res.inserted
1972
+ if(st){st.textContent=ok?'Saved':(res.error||'Error');st.className='ins-st '+(ok?'ok':'err');setTimeout(function(){st.textContent=''},2000)}
1973
+ })
1974
+ .catch(function(){if(st){st.textContent='Error';st.className='ins-st err'}})
1975
+ }
1976
+ window.saveInlineStyles=saveInlineStyles
1977
+ function renderVarRow(name){
1978
+ var v=cssVarMap[name]
1979
+ var h='<div class="cv-row"><div class="cv-name">'+esc(name)+'</div>'
1980
+ if(v.type==='color'){
1981
+ h+='<div class="cv-color-row">'
1982
+ h+='<input type="color" class="cv-inp cv-color" data-var="'+esc(name)+'" data-role="cp" value="'+esc(v.value)+'">'
1983
+ h+='<input type="text" class="cv-inp cv-text" data-var="'+esc(name)+'" data-role="tx" value="'+esc(v.value)+'">'
1984
+ h+='</div>'
1985
+ }else{
1986
+ h+='<input type="text" class="cv-inp cv-text" data-var="'+esc(name)+'" value="'+esc(v.value)+'">'
1987
+ }
1988
+ h+='</div>'
1989
+ return h
1990
+ }
1991
+ window.switchMode=switchMode
1992
+
1993
+ // ── Palette tabs ──────────────────────────────────────────────────────────────
1994
+ function switchPal(m){
1995
+ document.querySelectorAll('.ptab').forEach(function(t){t.classList.toggle('active',t.dataset.pal===m)})
1996
+ document.querySelectorAll('.pal-panel').forEach(function(p){p.classList.toggle('active',p.id==='pal-'+m)})
1997
+ }
1998
+ window.switchPal=switchPal
1999
+
2000
+ // ── Convert panel ─────────────────────────────────────────────────────────────
2001
+ var comps=[]
2002
+ function loadComponents(){
2003
+ var dir=document.getElementById('conv-dir').value.trim()||'src/components'
2004
+ var cl=document.getElementById('conv-list')
2005
+ cl.innerHTML='<div class="empty">Scanning…</div>'
2006
+ fetch('/_davaux/components?dir='+encodeURIComponent(dir))
2007
+ .then(function(r){return r.json()})
2008
+ .then(function(data){comps=data;renderConvList()})
2009
+ .catch(function(){cl.innerHTML='<div class="empty">Failed to scan.</div>'})
2010
+ }
2011
+ window.loadComponents=loadComponents
2012
+ function renderConvList(){
2013
+ var cl=document.getElementById('conv-list')
2014
+ if(!comps.length){cl.innerHTML='<div class="empty">No components found.<br>Create .tsx files starting with<br>an uppercase letter.</div>';return}
2015
+ cl.innerHTML=''
2016
+ comps.forEach(function(c){
2017
+ var div=document.createElement('div')
2018
+ div.className='conv-item'
2019
+ var nc=Object.keys(c.props||{}).length
2020
+ var nd=document.createElement('div');nd.className='conv-name';nd.textContent=c.name
2021
+ var md=document.createElement('div');md.className='conv-meta';md.textContent=c.file+(nc?' · '+nc+' prop'+(nc!==1?'s':''):'')
2022
+ var acts=document.createElement('div');acts.className='conv-acts'
2023
+ if(c.hasBlueprintAlready){
2024
+ var ex=document.createElement('span');ex.className='conv-exists';ex.textContent='✓ exists'
2025
+ var upd=document.createElement('button');upd.className='conv-btn conv-upd';upd.innerHTML='↻ Update'
2026
+ upd.onclick=(function(comp,b){return function(){saveBp(comp,b,true)}})(c,upd)
2027
+ acts.appendChild(ex);acts.appendChild(upd)
2028
+ }else{
2029
+ var btn=document.createElement('button');btn.className='conv-btn';btn.innerHTML='→ Blueprint'
2030
+ btn.onclick=(function(comp,b){return function(){saveBp(comp,b,false)}})(c,btn)
2031
+ acts.appendChild(btn)
2032
+ }
2033
+ div.appendChild(nd);div.appendChild(md);div.appendChild(acts)
2034
+ cl.appendChild(div)
2035
+ })
2036
+ }
2037
+ function saveBp(comp,btn,isUpdate){
2038
+ btn.disabled=true;btn.textContent='Saving...'
2039
+ fetch('/_davaux/save-blueprint',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:comp.name,props:comp.props,importPath:comp.importPath})})
2040
+ .then(function(r){return r.json()})
2041
+ .then(function(res){
2042
+ if(res.saved){
2043
+ if(isUpdate){
2044
+ btn.innerHTML='✓ Updated'
2045
+ setTimeout(function(){btn.innerHTML='↻ Update';btn.disabled=false},1500)
2046
+ }else{
2047
+ comps.forEach(function(c){if(c.name===comp.name)c.hasBlueprintAlready=true})
2048
+ renderConvList()
2049
+ refreshBps()
2050
+ }
2051
+ }else{btn.innerHTML='Error';btn.className='conv-btn conv-upd err';btn.disabled=false}
2052
+ })
2053
+ .catch(function(){btn.innerHTML='Error';btn.className='conv-btn conv-upd err';btn.disabled=false})
2054
+ }
2055
+
2056
+ function buildCss(){
2057
+ var lines=Object.keys(cssVarMap).map(function(n){return' '+n+': '+cssVarMap[n].value+';'})
2058
+ return':root {'+NL+lines.join(NL)+NL+'}'
2059
+ }
2060
+ window.ccss=function(){
2061
+ navigator.clipboard.writeText(buildCss()).then(function(){
2062
+ var b=document.getElementById('css-cpbtn')
2063
+ if(!b)return
2064
+ b.textContent='Copied!'
2065
+ b.classList.add('ok')
2066
+ setTimeout(function(){b.textContent='Copy CSS';b.classList.remove('ok')},1500)
2067
+ })
2068
+ }
2069
+ window.cp=function(){
2070
+ if(sel<0)return
2071
+ navigator.clipboard.writeText(buildJsx(bps[sel])).then(function(){
2072
+ var b=document.getElementById('cpbtn')
2073
+ if(!b)return
2074
+ b.textContent='Copied!'
2075
+ b.classList.add('ok')
2076
+ setTimeout(function(){b.textContent='Copy JSX';b.classList.remove('ok')},1500)
2077
+ })
2078
+ }
2079
+ window.ins=function(){
2080
+ if(sel<0)return
2081
+ var bp=bps[sel]
2082
+ var st=document.getElementById('ins-st')
2083
+ if(st){st.textContent='Inserting...';st.className='ins-st'}
2084
+ var url,body
2085
+ if(insertTarget){
2086
+ url='/_davaux/insert-after'
2087
+ body=JSON.stringify({page:pg.split('?')[0],tag:insertTarget.name||insertTarget.tag,instanceIndex:insertTarget.instanceIndex,newJsx:buildJsx(bp),imports:bp.imports||{}})
2088
+ }else{
2089
+ url='/_davaux/insert'
2090
+ body=JSON.stringify({page:pg.split('?')[0],jsx:buildJsx(bp),imports:bp.imports||{}})
2091
+ }
2092
+ fetch(url,{
2093
+ method:'POST',
2094
+ headers:{'Content-Type':'application/json'},
2095
+ body:body
2096
+ })
2097
+ .then(function(r){return r.json()})
2098
+ .then(function(res){
2099
+ var st2=document.getElementById('ins-st')
2100
+ if(!st2)return
2101
+ if(res.inserted){
2102
+ st2.textContent=res.warning?res.warning:'Inserted into '+res.file
2103
+ st2.className='ins-st '+(res.warning?'warn':'ok')
2104
+ var b=document.getElementById('ibtn')
2105
+ if(b){b.textContent='Inserted!';b.classList.add('ok');setTimeout(function(){b.textContent='Insert into page';b.classList.remove('ok')},2000)}
2106
+ }else{
2107
+ st2.textContent=res.error
2108
+ st2.className='ins-st err'
2109
+ }
2110
+ })
2111
+ .catch(function(e){
2112
+ var st3=document.getElementById('ins-st')
2113
+ if(st3){st3.textContent='Request failed';st3.className='ins-st err'}
2114
+ })
2115
+ }
2116
+ window.remComp=function(name,instanceIndex){
2117
+ if(!name)return
2118
+ var st=document.getElementById('rem-st')
2119
+ if(st){st.textContent='Removing...';st.className='ins-st'}
2120
+ fetch('/_davaux/remove',{
2121
+ method:'POST',headers:{'Content-Type':'application/json'},
2122
+ body:JSON.stringify({page:pg.split('?')[0],component:name,instanceIndex:instanceIndex??0})
2123
+ })
2124
+ .then(function(r){return r.json()})
2125
+ .then(function(res){
2126
+ var s=document.getElementById('rem-st');if(!s)return
2127
+ if(res.removed){
2128
+ s.textContent='Removed from '+res.file
2129
+ s.className='ins-st ok'
2130
+ var rb=document.querySelector('.rem-btn')
2131
+ if(rb){rb.disabled=true;rb.textContent='Removed'}
2132
+ }else{
2133
+ s.textContent=res.error
2134
+ s.className='ins-st err'
2135
+ }
2136
+ })
2137
+ .catch(function(){var s=document.getElementById('rem-st');if(s){s.textContent='Request failed';s.className='ins-st err'}})
2138
+ }
2139
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
2140
+ })()
2141
+ </script>
2142
+ </body>
2143
+ </html>`
2144
+
2145
+ type ClientError = {
2146
+ text: string
2147
+ file?: string
2148
+ line?: number
2149
+ column?: number
2150
+ lineText?: string
2151
+ }
2152
+
2153
+ const reloadPlugin: Plugin = {
2154
+ name: 'davaux-reload',
2155
+ setup(build) {
2156
+ build.onEnd((result) => {
2157
+ if (result.errors.length > 0) {
2158
+ console.error(`\n[davaux] Build error:\n${formatBuildErrors(result.errors)}`)
2159
+ if (serverReady) {
2160
+ const payload: ClientError[] = result.errors.map((e) => ({
2161
+ text: e.text,
2162
+ file: e.location?.file,
2163
+ line: e.location?.line,
2164
+ column: e.location?.column,
2165
+ lineText: e.location?.lineText?.trim(),
2166
+ }))
2167
+ sendToClients(`error:${JSON.stringify(payload)}`)
2168
+ }
2169
+ return
2170
+ }
2171
+ scheduleReload()
2172
+ })
2173
+ },
2174
+ }
2175
+
2176
+ // ─── Dev server ───────────────────────────────────────────────────────────────
2177
+
2178
+ export interface DevOptions {
2179
+ cwd: string
2180
+ port?: number
2181
+ hostname?: string
2182
+ routesDir?: string
2183
+ publicDir?: string
2184
+ islandsDir?: string
2185
+ /** Path to the client-side entry file for custom client code. Default: <cwd>/src/client.ts */
2186
+ clientEntry?: string
2187
+ /** Path aliases forwarded to esbuild `alias` in every build context. Same glob syntax as tsconfig `paths`. */
2188
+ paths?: Record<string, string>
2189
+ /** Davaux plugins from davaux.config.ts — contribute esbuild transforms and scanner extensions. */
2190
+ plugins?: DavauxPlugin[]
2191
+ /**
2192
+ * Additional packages to exclude from esbuild bundling. Merged with the default
2193
+ * `['node:*', 'davaux']` externals. Useful for native or CJS-only packages.
2194
+ */
2195
+ external?: string[]
2196
+ /** Absolute path to `src/middleware.ts` if it exists — compiled and run before route matching. */
2197
+ middlewareSrc?: string
2198
+ /** Visual editor configuration from davaux.config.ts. */
2199
+ editor?: import('../config.js').EditorConfig
2200
+ }
2201
+
2202
+ export async function startDev(options: DevOptions): Promise<void> {
2203
+ const {
2204
+ cwd,
2205
+ port = 3000,
2206
+ hostname = 'localhost',
2207
+ routesDir = join(cwd, 'src', 'routes'),
2208
+ publicDir = join(cwd, 'public'),
2209
+ islandsDir = join(cwd, 'src', 'islands'),
2210
+ clientEntry = join(cwd, 'src', 'client.ts'),
2211
+ paths,
2212
+ plugins: davauxPlugins = [],
2213
+ external: userExternal = [],
2214
+ middlewareSrc,
2215
+ editor: editorConfig,
2216
+ } = options
2217
+ const editorEnabled = editorConfig?.enabled === true
2218
+
2219
+ const serverExternal = ['node:*', 'davaux', ...userExternal]
2220
+
2221
+ const userAlias = pathsToAlias(paths ?? {})
2222
+ const extraPlugins = collectEsbuildPlugins(davauxPlugins)
2223
+ const scannerSuffixes = collectScannerSuffixes(davauxPlugins)
2224
+
2225
+ const dauxDir = join(cwd, '.davaux')
2226
+ const outDir = join(dauxDir, 'routes')
2227
+ const clientOutFile = join(dauxDir, 'client.js')
2228
+ const islandsOutFile = join(dauxDir, 'islands.js')
2229
+ const stylesOutFile = join(dauxDir, 'styles.css')
2230
+ const hasClient = existsSync(clientEntry)
2231
+
2232
+ function toCompiledPath(src: string): string {
2233
+ return join(outDir, relative(routesDir, src).replace(/\.(tsx?|jsx?|mdx?)$/, '.js'))
2234
+ }
2235
+ function toCompiledDir(srcDir: string): string {
2236
+ return join(outDir, relative(routesDir, srcDir))
2237
+ }
2238
+
2239
+ function collectSourceFiles(scan: ScanResult): string[] {
2240
+ return [
2241
+ ...scan.routes.map((r) => r.filePath),
2242
+ ...scan.layouts.map((l) => l.filePath),
2243
+ ...scan.middlewares.map((m) => m.filePath),
2244
+ ...(scan.errorPage ? [scan.errorPage] : []),
2245
+ ]
2246
+ }
2247
+
2248
+ function makeApp(scan: ScanResult, islandFiles: IslandFile[]) {
2249
+ const injectedScripts: string[] = []
2250
+ if (islandFiles.length > 0) injectedScripts.push('/_davaux/islands.js')
2251
+ if (hasClient) injectedScripts.push('/_davaux/client.js')
2252
+ // src/middleware.ts compiles to .davaux/middleware.js (one level above the routes outDir)
2253
+ const appMiddlewarePath = middlewareSrc ? join(dauxDir, 'middleware.js') : undefined
2254
+ return buildApp(
2255
+ {
2256
+ routes: scan.routes.map((r) => ({ ...r, filePath: toCompiledPath(r.filePath) })),
2257
+ layouts: scan.layouts.map((l) => ({
2258
+ filePath: toCompiledPath(l.filePath),
2259
+ dirPath: toCompiledDir(l.dirPath),
2260
+ })),
2261
+ middlewares: scan.middlewares.map((m) => ({
2262
+ filePath: toCompiledPath(m.filePath),
2263
+ dirPath: toCompiledDir(m.dirPath),
2264
+ })),
2265
+ errorPage: scan.errorPage ? toCompiledPath(scan.errorPage) : undefined,
2266
+ },
2267
+ true,
2268
+ injectedScripts,
2269
+ ['/_davaux/styles.css'],
2270
+ appMiddlewarePath,
2271
+ '',
2272
+ editorEnabled,
2273
+ )
2274
+ }
2275
+
2276
+ // Mutable state — updated on structural rebuild (file add/remove)
2277
+ let scan = await scanRoutes(routesDir, scannerSuffixes)
2278
+ let islands = await scanIslands(islandsDir)
2279
+ let sourceFiles = collectSourceFiles(scan)
2280
+
2281
+ if (sourceFiles.length === 0) {
2282
+ console.warn('[davaux] No routes found in', routesDir)
2283
+ }
2284
+
2285
+ // Routes context — server JSX, islands aliased to server stubs + auto-wrapped
2286
+ let ctx = await context({
2287
+ entryPoints: sourceFiles,
2288
+ outdir: outDir,
2289
+ outbase: routesDir,
2290
+ format: 'esm',
2291
+ platform: 'node',
2292
+ target: 'node22',
2293
+ bundle: true,
2294
+ external: serverExternal,
2295
+ jsx: 'automatic',
2296
+ jsxImportSource: 'davaux/oml',
2297
+ sourcemap: 'inline',
2298
+ alias: { ...userAlias, 'davaux/client': 'davaux/signal' },
2299
+ plugins: [
2300
+ reloadPlugin,
2301
+ islandServerPlugin(islandsDir),
2302
+ cssCollectorPlugin(dauxDir, stylesOutFile),
2303
+ ...extraPlugins,
2304
+ ],
2305
+ })
2306
+ await ctx.rebuild()
2307
+
2308
+ // Islands client context — client JSX, one bundle with all islands + hydration
2309
+ let islandsCtx: Awaited<ReturnType<typeof context>> | undefined
2310
+ if (islands.length > 0) {
2311
+ islandsCtx = await context({
2312
+ stdin: {
2313
+ contents: generateIslandsEntry(islands),
2314
+ loader: 'ts',
2315
+ resolveDir: cwd,
2316
+ },
2317
+ outfile: islandsOutFile,
2318
+ format: 'esm',
2319
+ platform: 'browser',
2320
+ target: 'es2022',
2321
+ bundle: true,
2322
+ jsx: 'automatic',
2323
+ jsxImportSource: 'davaux/client',
2324
+ sourcemap: 'inline',
2325
+ alias: userAlias,
2326
+ tsconfigRaw: JSON.stringify({
2327
+ compilerOptions: { jsx: 'react-jsx', jsxImportSource: 'davaux/client' },
2328
+ }),
2329
+ plugins: [reloadPlugin, cssCollectorPlugin(dauxDir, stylesOutFile), ...extraPlugins],
2330
+ })
2331
+ await islandsCtx.rebuild()
2332
+ }
2333
+
2334
+ // Optional user-authored client bundle (never needs a structural rebuild)
2335
+ let clientCtx: Awaited<ReturnType<typeof context>> | undefined
2336
+ if (hasClient) {
2337
+ clientCtx = await context({
2338
+ entryPoints: [clientEntry],
2339
+ outfile: clientOutFile,
2340
+ format: 'esm',
2341
+ platform: 'browser',
2342
+ target: 'es2022',
2343
+ bundle: true,
2344
+ jsx: 'automatic',
2345
+ jsxImportSource: 'davaux/client',
2346
+ sourcemap: 'inline',
2347
+ alias: userAlias,
2348
+ tsconfigRaw: JSON.stringify({
2349
+ compilerOptions: { jsx: 'react-jsx', jsxImportSource: 'davaux/client' },
2350
+ }),
2351
+ plugins: [reloadPlugin, ...extraPlugins],
2352
+ })
2353
+ await clientCtx.rebuild()
2354
+ }
2355
+
2356
+ // src/middleware.ts — compiled separately so it lands at .davaux/middleware.js
2357
+ // (not in the routes context, which uses outbase: src/routes and would mis-place it)
2358
+ const middlewareOutFile = join(dauxDir, 'middleware.js')
2359
+ let middlewareCtx: Awaited<ReturnType<typeof context>> | undefined
2360
+ if (middlewareSrc) {
2361
+ middlewareCtx = await context({
2362
+ entryPoints: [middlewareSrc],
2363
+ outfile: middlewareOutFile,
2364
+ format: 'esm',
2365
+ platform: 'node',
2366
+ target: 'node22',
2367
+ bundle: true,
2368
+ external: serverExternal,
2369
+ jsx: 'automatic',
2370
+ jsxImportSource: 'davaux',
2371
+ sourcemap: 'inline',
2372
+ alias: userAlias,
2373
+ plugins: [reloadPlugin, ...extraPlugins],
2374
+ })
2375
+ await middlewareCtx.rebuild()
2376
+ }
2377
+
2378
+ // Mutable app — hot-swapped by the structural rebuild without restarting the server
2379
+ let app = makeApp(scan, islands)
2380
+ const staticHandler = serveStatic(publicDir)
2381
+
2382
+ startServer(() => app, {
2383
+ port,
2384
+ hostname,
2385
+ onRequest: async (req, res) => {
2386
+ const path = req.url?.split('?')[0]
2387
+
2388
+ // SSE live reload stream
2389
+ if (path === '/_davaux/livereload') {
2390
+ res.writeHead(200, {
2391
+ 'Content-Type': 'text/event-stream',
2392
+ 'Cache-Control': 'no-cache',
2393
+ Connection: 'keep-alive',
2394
+ })
2395
+ res.write('\n')
2396
+ sseClients.add(res)
2397
+ req.on('close', () => sseClients.delete(res))
2398
+ return Promise.resolve(true)
2399
+ }
2400
+
2401
+ if (path === '/_davaux/livereload.js') {
2402
+ res.writeHead(200, { 'Content-Type': 'application/javascript' })
2403
+ res.end(LIVERELOAD_SCRIPT)
2404
+ return Promise.resolve(true)
2405
+ }
2406
+
2407
+ if (path === '/_davaux/livereload-worker.js') {
2408
+ res.writeHead(200, { 'Content-Type': 'application/javascript' })
2409
+ res.end(SHARED_WORKER_SCRIPT)
2410
+ return Promise.resolve(true)
2411
+ }
2412
+
2413
+ if (path === '/_davaux/partial-updates.js') {
2414
+ res.writeHead(200, { 'Content-Type': 'application/javascript' })
2415
+ res.end(getPartialUpdatesScript())
2416
+ return Promise.resolve(true)
2417
+ }
2418
+
2419
+ // Security gate — editor endpoints require editor.enabled: true.
2420
+ // Infrastructure paths (livereload, styles, islands, client) are always served.
2421
+ if (path?.startsWith('/_davaux/') && !editorEnabled) {
2422
+ const alwaysOn = [
2423
+ '/_davaux/livereload',
2424
+ '/_davaux/livereload.js',
2425
+ '/_davaux/livereload-worker.js',
2426
+ '/_davaux/partial-updates.js',
2427
+ '/_davaux/styles.css',
2428
+ '/_davaux/islands.js',
2429
+ '/_davaux/client.js',
2430
+ ]
2431
+ if (!alwaysOn.includes(path)) {
2432
+ res.writeHead(404)
2433
+ res.end()
2434
+ return Promise.resolve(true)
2435
+ }
2436
+ }
2437
+
2438
+ if (path === '/_davaux/inspector.js') {
2439
+ res.writeHead(200, {
2440
+ 'Content-Type': 'application/javascript',
2441
+ 'Cache-Control': 'no-store',
2442
+ })
2443
+ const dvx = JSON.stringify({
2444
+ pos: editorConfig?.badge?.position ?? 'bottom-right',
2445
+ label: editorConfig?.badge?.label ?? 'Inspector',
2446
+ })
2447
+ res.end(`window.__DVX__=${dvx};\n${INSPECTOR_SCRIPT}`)
2448
+ return Promise.resolve(true)
2449
+ }
2450
+
2451
+ if (path === '/_davaux/inspector') {
2452
+ const url = new URL(req.url ?? '/', 'http://x').searchParams.get('url') ?? '/'
2453
+ const node = app.devOmlStore?.get(url)
2454
+ const pageUrl = url.split('?')[0]
2455
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2456
+ const filePath = route?.filePath ? relative(cwd, route.filePath) : null
2457
+ const ext = route?.filePath ? extname(route.filePath) : ''
2458
+ const fileType = ext === '.mdx' ? 'mdx' : ext === '.md' ? 'md' : 'tsx'
2459
+ // Applicable layouts — same matching logic as findLayouts() in handler.ts
2460
+ const layouts = route?.filePath
2461
+ ? scan.layouts
2462
+ .filter((l) => {
2463
+ const d = route.filePath.replace(/[\\/][^\\/]+$/, '')
2464
+ return d === l.dirPath || d.startsWith(`${l.dirPath}/`)
2465
+ })
2466
+ .sort((a, b) => b.dirPath.length - a.dirPath.length)
2467
+ .map((l) => ({ filePath: relative(cwd, l.filePath) }))
2468
+ : []
2469
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2470
+ res.end(
2471
+ node !== undefined
2472
+ ? JSON.stringify({ node, fileType, filePath, layouts })
2473
+ : JSON.stringify({ error: 'No OML tree for this URL. Load the page first.', fileType, filePath, layouts }),
2474
+ )
2475
+ return Promise.resolve(true)
2476
+ }
2477
+
2478
+ if (path === '/_davaux/editor') {
2479
+ res.writeHead(200, {
2480
+ 'Content-Type': 'text/html; charset=utf-8',
2481
+ 'Cache-Control': 'no-store',
2482
+ })
2483
+ const dvxEditor = JSON.stringify({ theme: editorConfig?.theme ?? 'auto' })
2484
+ let cssContent = editorConfig?.css ?? ''
2485
+ if (editorConfig?.cssFile) {
2486
+ try { cssContent += readFileSync(join(cwd, editorConfig.cssFile), 'utf-8') } catch {}
2487
+ }
2488
+ const extras = `<script>window.__DVX_EDITOR__=${dvxEditor}</script>${cssContent ? `<style>${cssContent}</style>` : ''}`
2489
+ res.end(EDITOR_HTML.replace('</head>', `${extras}</head>`))
2490
+ return Promise.resolve(true)
2491
+ }
2492
+
2493
+ if (path === '/_davaux/blueprints') {
2494
+ try {
2495
+ const entries = scanBlueprints(cwd)
2496
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2497
+ res.end(JSON.stringify(entries))
2498
+ } catch (err) {
2499
+ console.error('[davaux] Failed to scan blueprints:', err)
2500
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2501
+ res.end('[]')
2502
+ }
2503
+ return Promise.resolve(true)
2504
+ }
2505
+
2506
+ if (path === '/_davaux/get-source' && req.method === 'GET') {
2507
+ const file = new URL(req.url ?? '/', 'http://x').searchParams.get('file') ?? ''
2508
+ const absPath = join(cwd, file)
2509
+ if (!file || !absPath.startsWith(`${cwd}/`)) {
2510
+ res.writeHead(403, { 'Content-Type': 'application/json' })
2511
+ res.end(JSON.stringify({ error: 'Forbidden' }))
2512
+ return Promise.resolve(true)
2513
+ }
2514
+ try {
2515
+ const content = readFileSync(absPath, 'utf-8')
2516
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2517
+ res.end(JSON.stringify({ content }))
2518
+ } catch {
2519
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2520
+ res.end(JSON.stringify({ error: 'File not found' }))
2521
+ }
2522
+ return Promise.resolve(true)
2523
+ }
2524
+
2525
+ if (path === '/_davaux/update-source' && req.method === 'POST') {
2526
+ const body = await new Promise<string>((resolve) => {
2527
+ const chunks: Buffer[] = []
2528
+ req.on('data', (c: Buffer) => chunks.push(c))
2529
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2530
+ req.on('error', () => resolve('{}'))
2531
+ })
2532
+ const { file, content } = JSON.parse(body) as { file: string; content: string }
2533
+ const absPath = join(cwd, file)
2534
+ if (!file || !absPath.startsWith(`${cwd}/`)) {
2535
+ res.writeHead(403, { 'Content-Type': 'application/json' })
2536
+ res.end(JSON.stringify({ error: 'Forbidden' }))
2537
+ return Promise.resolve(true)
2538
+ }
2539
+ writeFileSync(absPath, content, 'utf-8')
2540
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2541
+ res.end(JSON.stringify({ ok: true }))
2542
+ return Promise.resolve(true)
2543
+ }
2544
+
2545
+ if (path === '/_davaux/get-fragment' && req.method === 'GET') {
2546
+ const params = new URL(req.url ?? '/', 'http://x').searchParams
2547
+ const file = params.get('file') ?? ''
2548
+ const component = params.get('component') ?? ''
2549
+ const instanceIndex = parseInt(params.get('instanceIndex') ?? '0', 10)
2550
+ const absPath = join(cwd, file)
2551
+ if (!file || !absPath.startsWith(`${cwd}/`)) {
2552
+ res.writeHead(403, { 'Content-Type': 'application/json' })
2553
+ res.end(JSON.stringify({ error: 'Forbidden' }))
2554
+ return Promise.resolve(true)
2555
+ }
2556
+ const result = getComponentFragment(absPath, component, instanceIndex)
2557
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2558
+ res.end(JSON.stringify(result))
2559
+ return Promise.resolve(true)
2560
+ }
2561
+
2562
+ if (path === '/_davaux/update-fragment' && req.method === 'POST') {
2563
+ const body = await new Promise<string>((resolve) => {
2564
+ const chunks: Buffer[] = []
2565
+ req.on('data', (c: Buffer) => chunks.push(c))
2566
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2567
+ req.on('error', () => resolve('{}'))
2568
+ })
2569
+ const { file, component, instanceIndex, content } = JSON.parse(body) as {
2570
+ file: string; component: string; instanceIndex: number; content: string
2571
+ }
2572
+ const absPath = join(cwd, file)
2573
+ if (!file || !absPath.startsWith(`${cwd}/`)) {
2574
+ res.writeHead(403, { 'Content-Type': 'application/json' })
2575
+ res.end(JSON.stringify({ error: 'Forbidden' }))
2576
+ return Promise.resolve(true)
2577
+ }
2578
+ const result = replaceComponentFragment(absPath, component, instanceIndex, content)
2579
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2580
+ res.end(JSON.stringify(result))
2581
+ return Promise.resolve(true)
2582
+ }
2583
+
2584
+ if (path === '/_davaux/get-element-fragment' && req.method === 'GET') {
2585
+ const params = new URL(req.url ?? '/', 'http://x').searchParams
2586
+ const file = params.get('file') ?? ''
2587
+ const tag = params.get('tag') ?? ''
2588
+ const instanceIndex = parseInt(params.get('instanceIndex') ?? '0', 10)
2589
+ const absPath = join(cwd, file)
2590
+ if (!file || !absPath.startsWith(`${cwd}/`)) {
2591
+ res.writeHead(403, { 'Content-Type': 'application/json' })
2592
+ res.end(JSON.stringify({ error: 'Forbidden' }))
2593
+ return Promise.resolve(true)
2594
+ }
2595
+ const result = getElementFragment(absPath, tag, instanceIndex)
2596
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2597
+ res.end(JSON.stringify(result))
2598
+ return Promise.resolve(true)
2599
+ }
2600
+
2601
+ if (path === '/_davaux/update-element-fragment' && req.method === 'POST') {
2602
+ const body = await new Promise<string>((resolve) => {
2603
+ const chunks: Buffer[] = []
2604
+ req.on('data', (c: Buffer) => chunks.push(c))
2605
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2606
+ req.on('error', () => resolve('{}'))
2607
+ })
2608
+ const { file, tag, instanceIndex, content } = JSON.parse(body) as {
2609
+ file: string; tag: string; instanceIndex: number; content: string
2610
+ }
2611
+ const absPath = join(cwd, file)
2612
+ if (!file || !absPath.startsWith(`${cwd}/`)) {
2613
+ res.writeHead(403, { 'Content-Type': 'application/json' })
2614
+ res.end(JSON.stringify({ error: 'Forbidden' }))
2615
+ return Promise.resolve(true)
2616
+ }
2617
+ const result = replaceElementFragment(absPath, tag, instanceIndex, content)
2618
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2619
+ res.end(JSON.stringify(result))
2620
+ return Promise.resolve(true)
2621
+ }
2622
+
2623
+ if (path === '/_davaux/routes') {
2624
+ const pageRoutes = scan.routes
2625
+ .filter((r) => r.type === 'page')
2626
+ .map((r) => ({ urlPattern: r.urlPattern }))
2627
+ .sort((a, b) => a.urlPattern.localeCompare(b.urlPattern))
2628
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2629
+ res.end(JSON.stringify(pageRoutes))
2630
+ return Promise.resolve(true)
2631
+ }
2632
+
2633
+ if (path === '/_davaux/insert' && req.method === 'POST') {
2634
+ const body = await new Promise<string>((resolve) => {
2635
+ const chunks: Buffer[] = []
2636
+ req.on('data', (c: Buffer) => chunks.push(c))
2637
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2638
+ req.on('error', () => resolve('{}'))
2639
+ })
2640
+ const { page, jsx, imports } = JSON.parse(body) as {
2641
+ page: string
2642
+ jsx: string
2643
+ imports: Record<string, string>
2644
+ }
2645
+ const pageUrl = (page ?? '/').split('?')[0]
2646
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2647
+ if (!route) {
2648
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2649
+ res.end(JSON.stringify({ inserted: false, error: `No page route found for: ${pageUrl}` }))
2650
+ return Promise.resolve(true)
2651
+ }
2652
+ const result = insertJsx(route.filePath, jsx, imports ?? {})
2653
+ const display = result.inserted ? { ...result, file: relative(cwd, result.file) } : result
2654
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2655
+ res.end(JSON.stringify(display))
2656
+ return Promise.resolve(true)
2657
+ }
2658
+
2659
+ if (path === '/_davaux/insert-after' && req.method === 'POST') {
2660
+ const body = await new Promise<string>((resolve) => {
2661
+ const chunks: Buffer[] = []
2662
+ req.on('data', (c: Buffer) => chunks.push(c))
2663
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2664
+ req.on('error', () => resolve('{}'))
2665
+ })
2666
+ const { page, tag, instanceIndex, newJsx, imports } = JSON.parse(body) as {
2667
+ page: string; tag: string; instanceIndex: number; newJsx: string
2668
+ imports?: Record<string, string>
2669
+ }
2670
+ const pageUrl = (page ?? '/').split('?')[0]
2671
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2672
+ if (!route) {
2673
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2674
+ res.end(JSON.stringify({ inserted: false, error: `No page route found for: ${pageUrl}` }))
2675
+ return Promise.resolve(true)
2676
+ }
2677
+ const result = insertAfterElement(route.filePath, tag, instanceIndex ?? 0, newJsx, imports ?? {})
2678
+ const display = result.inserted ? { ...result, file: relative(cwd, result.file) } : result
2679
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2680
+ res.end(JSON.stringify(display))
2681
+ return Promise.resolve(true)
2682
+ }
2683
+
2684
+ if (path === '/_davaux/components') {
2685
+ const qs = new URL(req.url ?? '/', 'http://x').searchParams
2686
+ const dir = qs.get('dir') || 'src/components'
2687
+ const absDir = dir.startsWith('/') ? dir : join(cwd, dir)
2688
+ const blueprintsDir = join(cwd, 'src', 'blueprints')
2689
+ try {
2690
+ const entries = scanComponents(absDir, cwd, blueprintsDir)
2691
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2692
+ res.end(JSON.stringify(entries))
2693
+ } catch (err) {
2694
+ console.error('[davaux] Failed to scan components:', err)
2695
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
2696
+ res.end('[]')
2697
+ }
2698
+ return Promise.resolve(true)
2699
+ }
2700
+
2701
+ if (path === '/_davaux/save-blueprint' && req.method === 'POST') {
2702
+ const body = await new Promise<string>((resolve) => {
2703
+ const chunks: Buffer[] = []
2704
+ req.on('data', (c: Buffer) => chunks.push(c))
2705
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2706
+ req.on('error', () => resolve('{}'))
2707
+ })
2708
+ const { name, props, importPath } = JSON.parse(body) as {
2709
+ name: string
2710
+ props: Record<string, OmlPropSchema>
2711
+ importPath?: string
2712
+ }
2713
+ const blueprintsDir = join(cwd, 'src', 'blueprints')
2714
+ const imports = importPath ? { [name]: importPath } : {}
2715
+ const result = saveBlueprint(name, props, imports, blueprintsDir)
2716
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2717
+ res.end(JSON.stringify(result))
2718
+ return Promise.resolve(true)
2719
+ }
2720
+
2721
+ if (path === '/_davaux/update-blueprint' && req.method === 'POST') {
2722
+ const body = await new Promise<string>((resolve) => {
2723
+ const chunks: Buffer[] = []
2724
+ req.on('data', (c: Buffer) => chunks.push(c))
2725
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2726
+ req.on('error', () => resolve('{}'))
2727
+ })
2728
+ const { id, name, props } = JSON.parse(body) as {
2729
+ id: string
2730
+ name: string
2731
+ props: Record<string, OmlPropSchema>
2732
+ }
2733
+ const blueprintsDir = join(cwd, 'src', 'blueprints')
2734
+ const result = updateBlueprint(id, name, props, blueprintsDir)
2735
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2736
+ res.end(JSON.stringify(result))
2737
+ return Promise.resolve(true)
2738
+ }
2739
+
2740
+ if (path === '/_davaux/remove' && req.method === 'POST') {
2741
+ const body = await new Promise<string>((resolve) => {
2742
+ const chunks: Buffer[] = []
2743
+ req.on('data', (c: Buffer) => chunks.push(c))
2744
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2745
+ req.on('error', () => resolve('{}'))
2746
+ })
2747
+ const { page, component, tag, instanceIndex } = JSON.parse(body) as {
2748
+ page: string
2749
+ component?: string
2750
+ tag?: string
2751
+ instanceIndex?: number
2752
+ }
2753
+ const pageUrl = (page ?? '/').split('?')[0]
2754
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2755
+ if (!route) {
2756
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2757
+ res.end(JSON.stringify({ removed: false, error: `No page route found for: ${pageUrl}` }))
2758
+ return Promise.resolve(true)
2759
+ }
2760
+ const result = tag !== undefined
2761
+ ? removeElement(route.filePath, tag, instanceIndex ?? 0)
2762
+ : removeJsx(route.filePath, component ?? '', instanceIndex)
2763
+ const display = result.removed ? { ...result, file: relative(cwd, result.file) } : result
2764
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2765
+ res.end(JSON.stringify(display))
2766
+ return Promise.resolve(true)
2767
+ }
2768
+
2769
+ if (path === '/_davaux/update-component' && req.method === 'POST') {
2770
+ const body = await new Promise<string>((resolve) => {
2771
+ const chunks: Buffer[] = []
2772
+ req.on('data', (c: Buffer) => chunks.push(c))
2773
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2774
+ req.on('error', () => resolve('{}'))
2775
+ })
2776
+ const { page, component, jsx, instanceIndex } = JSON.parse(body) as {
2777
+ page: string
2778
+ component: string
2779
+ jsx: string
2780
+ instanceIndex?: number
2781
+ }
2782
+ const pageUrl = (page ?? '/').split('?')[0]
2783
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2784
+ if (!route) {
2785
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2786
+ res.end(JSON.stringify({ replaced: false, error: `No page route found for: ${pageUrl}` }))
2787
+ return Promise.resolve(true)
2788
+ }
2789
+ const result = replaceJsx(route.filePath, component, jsx, instanceIndex ?? 0)
2790
+ const display = result.replaced ? { ...result, file: relative(cwd, result.file) } : result
2791
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2792
+ res.end(JSON.stringify(display))
2793
+ return Promise.resolve(true)
2794
+ }
2795
+
2796
+ if (path === '/_davaux/update-element' && req.method === 'POST') {
2797
+ const body = await new Promise<string>((resolve) => {
2798
+ const chunks: Buffer[] = []
2799
+ req.on('data', (c: Buffer) => chunks.push(c))
2800
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2801
+ req.on('error', () => resolve('{}'))
2802
+ })
2803
+ const { page, tag, newProps, instanceIndex, textContent } = JSON.parse(body) as {
2804
+ page: string
2805
+ tag: string
2806
+ newProps: Record<string, unknown>
2807
+ instanceIndex?: number
2808
+ textContent?: string | null
2809
+ }
2810
+ if (!page || !tag) {
2811
+ res.writeHead(400, { 'Content-Type': 'application/json' })
2812
+ res.end(JSON.stringify({ replaced: false, error: 'Missing page or tag' }))
2813
+ return Promise.resolve(true)
2814
+ }
2815
+ const pageUrl = (page ?? '/').split('?')[0]
2816
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2817
+ if (!route) {
2818
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2819
+ res.end(JSON.stringify({ replaced: false, error: `No page route found for: ${pageUrl}` }))
2820
+ return Promise.resolve(true)
2821
+ }
2822
+ const attrsResult = replaceElementAttrs(route.filePath, tag, newProps, instanceIndex ?? 0)
2823
+ if (!attrsResult.replaced) {
2824
+ const display = attrsResult
2825
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2826
+ res.end(JSON.stringify(display))
2827
+ return Promise.resolve(true)
2828
+ }
2829
+ if (textContent != null) {
2830
+ const textResult = replaceTextContent(route.filePath, tag, textContent, instanceIndex ?? 0)
2831
+ if (!textResult.replaced) {
2832
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2833
+ res.end(JSON.stringify(textResult))
2834
+ return Promise.resolve(true)
2835
+ }
2836
+ }
2837
+ const display = { ...attrsResult, file: relative(cwd, attrsResult.file) }
2838
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2839
+ res.end(JSON.stringify(display))
2840
+ return Promise.resolve(true)
2841
+ }
2842
+
2843
+ if (path === '/_davaux/update-comp-props' && req.method === 'POST') {
2844
+ const body = await new Promise<string>((resolve) => {
2845
+ const chunks: Buffer[] = []
2846
+ req.on('data', (c: Buffer) => chunks.push(c))
2847
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2848
+ req.on('error', () => resolve('{}'))
2849
+ })
2850
+ const { page, component, newProps, instanceIndex, childrenText } = JSON.parse(body) as {
2851
+ page: string
2852
+ component: string
2853
+ newProps: Record<string, unknown>
2854
+ instanceIndex?: number
2855
+ childrenText?: string | null
2856
+ }
2857
+ const pageUrl = (page ?? '/').split('?')[0]
2858
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2859
+ if (!route) {
2860
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2861
+ res.end(JSON.stringify({ replaced: false, error: `No page route found for: ${pageUrl}` }))
2862
+ return Promise.resolve(true)
2863
+ }
2864
+ const attrsResult = replaceElementAttrs(route.filePath, component, newProps ?? {}, instanceIndex ?? 0)
2865
+ if (!attrsResult.replaced) {
2866
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2867
+ res.end(JSON.stringify(attrsResult))
2868
+ return Promise.resolve(true)
2869
+ }
2870
+ if (childrenText != null) {
2871
+ replaceTextContent(route.filePath, component, childrenText, instanceIndex ?? 0)
2872
+ }
2873
+ const display = { ...attrsResult, file: relative(cwd, attrsResult.file) }
2874
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2875
+ res.end(JSON.stringify(display))
2876
+ return Promise.resolve(true)
2877
+ }
2878
+
2879
+ if (path === '/_davaux/move' && req.method === 'POST') {
2880
+ const body = await new Promise<string>((resolve) => {
2881
+ const chunks: Buffer[] = []
2882
+ req.on('data', (c: Buffer) => chunks.push(c))
2883
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
2884
+ req.on('error', () => resolve('{}'))
2885
+ })
2886
+ const { page, name, isComponent, instanceIndex, direction } = JSON.parse(body) as {
2887
+ page: string
2888
+ name: string
2889
+ isComponent: boolean
2890
+ instanceIndex?: number
2891
+ direction: 'up' | 'down'
2892
+ }
2893
+ if (!page || !name || !direction) {
2894
+ res.writeHead(400, { 'Content-Type': 'application/json' })
2895
+ res.end(JSON.stringify({ moved: false, error: 'Missing required fields' }))
2896
+ return Promise.resolve(true)
2897
+ }
2898
+ const pageUrl = (page ?? '/').split('?')[0]
2899
+ const route = scan.routes.find((r) => r.urlPattern === pageUrl && r.type === 'page')
2900
+ if (!route) {
2901
+ res.writeHead(404, { 'Content-Type': 'application/json' })
2902
+ res.end(JSON.stringify({ moved: false, error: `No page route found for: ${pageUrl}` }))
2903
+ return Promise.resolve(true)
2904
+ }
2905
+ const result = moveNode(route.filePath, name, isComponent ?? false, instanceIndex ?? 0, direction)
2906
+ const display = result.moved ? { ...result, file: relative(cwd, result.file) } : result
2907
+ res.writeHead(200, { 'Content-Type': 'application/json' })
2908
+ res.end(JSON.stringify(display))
2909
+ return Promise.resolve(true)
2910
+ }
2911
+
2912
+ if (path === '/_davaux/styles.css') {
2913
+ res.writeHead(200, { 'Content-Type': 'text/css' })
2914
+ if (existsSync(stylesOutFile)) {
2915
+ createReadStream(stylesOutFile).pipe(res)
2916
+ } else {
2917
+ res.end()
2918
+ }
2919
+ return Promise.resolve(true)
2920
+ }
2921
+
2922
+ if (path === '/_davaux/islands.js') {
2923
+ res.writeHead(200, { 'Content-Type': 'application/javascript' })
2924
+ createReadStream(islandsOutFile).pipe(res)
2925
+ return Promise.resolve(true)
2926
+ }
2927
+
2928
+ if (hasClient && path === '/_davaux/client.js') {
2929
+ res.writeHead(200, { 'Content-Type': 'application/javascript' })
2930
+ createReadStream(clientOutFile).pipe(res)
2931
+ return Promise.resolve(true)
2932
+ }
2933
+
2934
+ if (staticHandler) return Promise.resolve(staticHandler(req, res))
2935
+ return Promise.resolve(false)
2936
+ },
2937
+ })
2938
+
2939
+ serverReady = true
2940
+
2941
+ // ─── Config hot reload ────────────────────────────────────────────────────────
2942
+ const CONFIG_NAMES = ['davaux.config.ts', 'davaux.config.js', 'davaux.config.mjs']
2943
+ const activeConfigFile = CONFIG_NAMES.map((f) => join(cwd, f)).find(existsSync)
2944
+
2945
+ if (activeConfigFile) {
2946
+ let configReloadTimer: ReturnType<typeof setTimeout> | undefined
2947
+ fsWatch(activeConfigFile, () => {
2948
+ if (configReloadTimer) clearTimeout(configReloadTimer)
2949
+ configReloadTimer = setTimeout(async () => {
2950
+ configReloadTimer = undefined
2951
+ try {
2952
+ app = makeApp(scan, islands)
2953
+ console.log('[davaux] Config reloaded')
2954
+ scheduleReload()
2955
+ } catch (err) {
2956
+ console.error('[davaux] Failed to reload config:', err)
2957
+ }
2958
+ }, 300)
2959
+ })
2960
+ }
2961
+
2962
+ await ctx.watch()
2963
+ if (islandsCtx) await islandsCtx.watch()
2964
+ if (clientCtx) await clientCtx.watch()
2965
+ if (middlewareCtx) await middlewareCtx.watch()
2966
+ console.log(' Watching for changes...')
2967
+
2968
+ // ─── Structural rebuild on file addition / removal ────────────────────────
2969
+ // esbuild watch() only tracks changes to existing entry points. Adding or
2970
+ // removing a route/island file requires disposing the old context and
2971
+ // creating a new one with the updated entry list.
2972
+
2973
+ let isRebuilding = false
2974
+ let rebuildTimer: ReturnType<typeof setTimeout> | undefined
2975
+
2976
+ async function handleStructuralChange(): Promise<void> {
2977
+ const newScan = await scanRoutes(routesDir, scannerSuffixes)
2978
+ const newIslands = await scanIslands(islandsDir)
2979
+ const newSourceFiles = collectSourceFiles(newScan)
2980
+
2981
+ // Skip if the file set is identical
2982
+ const same =
2983
+ newSourceFiles.length === sourceFiles.length &&
2984
+ newSourceFiles.every((f) => sourceFiles.includes(f)) &&
2985
+ newIslands.length === islands.length &&
2986
+ newIslands.every((i) => islands.some((j) => j.filePath === i.filePath))
2987
+ if (same) return
2988
+
2989
+ sourceFiles = newSourceFiles
2990
+ scan = newScan
2991
+ islands = newIslands
2992
+
2993
+ ctx = await context({
2994
+ entryPoints: sourceFiles,
2995
+ outdir: outDir,
2996
+ outbase: routesDir,
2997
+ format: 'esm',
2998
+ platform: 'node',
2999
+ target: 'node22',
3000
+ bundle: true,
3001
+ external: serverExternal,
3002
+ jsx: 'automatic',
3003
+ jsxImportSource: 'davaux/oml',
3004
+ sourcemap: 'inline',
3005
+ alias: { ...userAlias, 'davaux/client': 'davaux/signal' },
3006
+ plugins: [
3007
+ reloadPlugin,
3008
+ islandServerPlugin(islandsDir),
3009
+ cssCollectorPlugin(dauxDir, stylesOutFile),
3010
+ ...extraPlugins,
3011
+ ],
3012
+ })
3013
+ await ctx.rebuild()
3014
+
3015
+ if (islands.length > 0) {
3016
+ islandsCtx = await context({
3017
+ stdin: {
3018
+ contents: generateIslandsEntry(islands),
3019
+ loader: 'ts',
3020
+ resolveDir: cwd,
3021
+ },
3022
+ outfile: islandsOutFile,
3023
+ format: 'esm',
3024
+ platform: 'browser',
3025
+ target: 'es2022',
3026
+ bundle: true,
3027
+ jsx: 'automatic',
3028
+ jsxImportSource: 'davaux/client',
3029
+ sourcemap: 'inline',
3030
+ alias: userAlias,
3031
+ tsconfigRaw: JSON.stringify({
3032
+ compilerOptions: { jsx: 'react-jsx', jsxImportSource: 'davaux/client' },
3033
+ }),
3034
+ plugins: [reloadPlugin, cssCollectorPlugin(dauxDir, stylesOutFile), ...extraPlugins],
3035
+ })
3036
+ await islandsCtx.rebuild()
3037
+ } else {
3038
+ islandsCtx = undefined
3039
+ }
3040
+
3041
+ app = makeApp(scan, islands)
3042
+
3043
+ await ctx.watch()
3044
+ if (islandsCtx) await islandsCtx.watch()
3045
+
3046
+ scheduleReload()
3047
+ console.log('[davaux] Routes updated')
3048
+ }
3049
+
3050
+ function onStructuralChange(): void {
3051
+ // Immediately stop esbuild's watch so it cannot attempt a rebuild with the
3052
+ // now-stale entry list (e.g. an entry point file that was just deleted).
3053
+ // Errors are swallowed — the context may already be disposed.
3054
+ ctx.dispose().catch(() => {})
3055
+ islandsCtx?.dispose().catch(() => {})
3056
+
3057
+ if (rebuildTimer) clearTimeout(rebuildTimer)
3058
+ rebuildTimer = setTimeout(() => {
3059
+ if (isRebuilding) return
3060
+ isRebuilding = true
3061
+ handleStructuralChange()
3062
+ .catch((err) => console.error('[davaux] Route rebuild failed:', err))
3063
+ .finally(() => {
3064
+ isRebuilding = false
3065
+ })
3066
+ }, 200)
3067
+ }
3068
+
3069
+ fsWatch(routesDir, { recursive: true }, (event, filename) => {
3070
+ if (event !== 'rename' || !filename) return
3071
+ if (!/\.(tsx?|jsx?|mdx?)$/.test(filename)) return
3072
+ onStructuralChange()
3073
+ })
3074
+
3075
+ if (existsSync(islandsDir)) {
3076
+ fsWatch(islandsDir, { recursive: true }, (event, filename) => {
3077
+ if (event !== 'rename' || !filename) return
3078
+ if (!/\.(tsx?|jsx?|mdx?)$/.test(filename)) return
3079
+ onStructuralChange()
3080
+ })
3081
+ }
3082
+
3083
+ startTypeChecker(cwd)
3084
+ }
3085
+
3086
+ // ─── Type checker ─────────────────────────────────────────────────────────────
3087
+
3088
+ function startTypeChecker(cwd: string): void {
3089
+ const tscBin = join(cwd, 'node_modules', '.bin', 'tsc')
3090
+ if (!existsSync(tscBin) || !existsSync(join(cwd, 'tsconfig.json'))) return
3091
+
3092
+ const proc = spawn(tscBin, ['--noEmit', '--watch', '--preserveWatchOutput'], {
3093
+ cwd,
3094
+ stdio: ['ignore', 'inherit', 'inherit'],
3095
+ })
3096
+ proc.on('error', () => {})
3097
+ process.on('exit', () => proc.kill())
3098
+ }