@inneranimalmedia/agentsam-sdk 2.6.4 → 2.6.5

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 (418) hide show
  1. package/AGENTSAM.md +11 -0
  2. package/README.md +15 -9
  3. package/apps/client-cms-editor/AGENTS.md +12 -0
  4. package/apps/client-cms-editor/IMPORT_PROVENANCE.json +8 -0
  5. package/apps/client-cms-editor/README.md +72 -0
  6. package/apps/client-cms-editor/agentsam.app.json +57 -0
  7. package/apps/client-cms-editor/backend/README.md +5 -0
  8. package/apps/client-cms-editor/backend/package.json +22 -0
  9. package/apps/client-cms-editor/backend/src/api/client.ts +200 -0
  10. package/apps/client-cms-editor/backend/src/demo-bootstrap.ts +145 -0
  11. package/apps/client-cms-editor/backend/src/index.ts +6 -0
  12. package/apps/client-cms-editor/backend/src/model.ts +115 -0
  13. package/apps/client-cms-editor/backend/src/preview/bridge.ts +79 -0
  14. package/apps/client-cms-editor/backend/src/preview/urls.ts +55 -0
  15. package/apps/client-cms-editor/backend/src/routing/cms-route.js +293 -0
  16. package/apps/client-cms-editor/backend/src/routing/index.js +10 -0
  17. package/apps/client-cms-editor/backend/src/storefront.ts +52 -0
  18. package/apps/client-cms-editor/backend/tsconfig.json +15 -0
  19. package/apps/client-cms-editor/backend/worker/index.js +34 -0
  20. package/apps/client-cms-editor/backend/wrangler.jsonc +11 -0
  21. package/apps/client-cms-editor/bin/agentsam-cms.mjs +229 -0
  22. package/apps/client-cms-editor/frontend/index.html +12 -0
  23. package/apps/client-cms-editor/frontend/package.json +33 -0
  24. package/apps/client-cms-editor/frontend/src/AgentSamDrawer.tsx +188 -0
  25. package/apps/client-cms-editor/frontend/src/CmsAgentSurface.tsx +191 -0
  26. package/apps/client-cms-editor/frontend/src/CmsEditor.tsx +1345 -0
  27. package/apps/client-cms-editor/frontend/src/env.d.ts +14 -0
  28. package/apps/client-cms-editor/frontend/src/index.ts +8 -0
  29. package/apps/client-cms-editor/frontend/src/lib/content-safety.ts +63 -0
  30. package/apps/client-cms-editor/frontend/src/main.tsx +17 -0
  31. package/apps/client-cms-editor/frontend/src/mount.tsx +51 -0
  32. package/apps/client-cms-editor/frontend/src/styles/agentsam-drawer.css +214 -0
  33. package/apps/client-cms-editor/frontend/src/styles/cmsShell.css +1739 -0
  34. package/apps/client-cms-editor/frontend/src/styles/studio.css +106 -0
  35. package/apps/client-cms-editor/frontend/src/surfaces/CmsDashboard.tsx +313 -0
  36. package/apps/client-cms-editor/frontend/src/surfaces/CmsGuidedChatHero.tsx +121 -0
  37. package/apps/client-cms-editor/frontend/src/surfaces/CmsHubPage.tsx +132 -0
  38. package/apps/client-cms-editor/frontend/src/surfaces/CmsSiteSwitcher.tsx +176 -0
  39. package/apps/client-cms-editor/frontend/src/surfaces/index.ts +5 -0
  40. package/apps/client-cms-editor/frontend/src/surfaces/types.ts +48 -0
  41. package/apps/client-cms-editor/frontend/tsconfig.json +25 -0
  42. package/apps/client-cms-editor/frontend/vite.config.ts +13 -0
  43. package/apps/client-cms-editor/package-lock.json +3832 -0
  44. package/apps/client-cms-editor/package.json +23 -0
  45. package/apps/client-cms-editor/reference/DONOR_README.md +52 -0
  46. package/apps/client-cms-editor/reference/DONOR_package-lock.json +1253 -0
  47. package/apps/client-cms-editor/reference/DONOR_tsconfig.json +15 -0
  48. package/apps/client-cms-editor/shared/cms/package.json +21 -0
  49. package/apps/client-cms-editor/shared/cms/src/agent-context.ts +35 -0
  50. package/apps/client-cms-editor/shared/cms/src/cloudflare-bindings.ts +51 -0
  51. package/apps/client-cms-editor/shared/cms/src/editor-types.ts +58 -0
  52. package/apps/client-cms-editor/shared/cms/src/index.ts +5 -0
  53. package/apps/client-cms-editor/shared/cms/src/publication.ts +24 -0
  54. package/apps/client-cms-editor/shared/cms/src/routes.ts +11 -0
  55. package/apps/client-cms-editor/shared/cms/tsconfig.json +12 -0
  56. package/bin/agentsam-rapid-rust +36 -0
  57. package/docs/AUTH_IDENTITY_CONTRACT.md +2 -0
  58. package/docs/MACHINE_INTELLIGENCE.md +120 -0
  59. package/docs/WINDOWS_RUNTIME_INSTALL.md +30 -0
  60. package/docs/architecture/AGENTSAM_APPLICATION_CONTRACT.md +188 -0
  61. package/docs/architecture/AGENTSAM_RUNTIME_PROTOCOL.md +151 -0
  62. package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +12 -4
  63. package/docs/architecture/R2_SLUG_NAMESPACE.md +20 -0
  64. package/docs/architecture/WORK_PRODUCT_EXTRACTION.md +97 -0
  65. package/docs/brand/AGENT_SAM_ICON_GRAMMAR.md +70 -0
  66. package/docs/content-studio/HOST_MOUNT_CHECKLIST.md +71 -0
  67. package/docs/content-studio/REVISION_GATE_2026-09-27.md +501 -0
  68. package/docs/contracts/capability-install-planner.md +67 -0
  69. package/docs/contracts/environment-vocabulary.md +195 -0
  70. package/docs/contracts/google-cloud-oauth-setup.md +199 -0
  71. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +1 -1
  72. package/docs/plans/LOCAL-STUDIO-GRADUATION-2026-09-26.md +290 -0
  73. package/docs/plans/LOCAL-STUDIO-REAL-APP-CONTRACT-ADDENDUM-2026-09-27.md +394 -0
  74. package/docs/plans/MACHINE-FIRST-APPS-TUI-2026-09-24.md +227 -0
  75. package/docs/plans/TOMORROW-EXTENSIVE-TODO-2026-09-26.md +221 -0
  76. package/docs/plans/TOMORROW-MACHINE-FIRST-BRIEFING.md +145 -0
  77. package/docs/plans/UNIVERSAL-INGEST-MACHINE-FIRST-2026-09-24.md +215 -0
  78. package/docs/plugins.md +2 -2
  79. package/docs/product/AGENTSAM_PRODUCT_EXPERIENCE_STANDARD.md +280 -0
  80. package/docs/product/LOCAL_STUDIO_DESKTOP_EXPERIENCE.md +139 -0
  81. package/docs/product/README.md +8 -0
  82. package/docs/rapid-rust-wasm.md +35 -0
  83. package/migrations/d1/0016_cloudflare_oauth_user_oauth_tokens_ssot.sql +14 -0
  84. package/package.json +73 -14
  85. package/packages/agentsam-brand/README.md +106 -11
  86. package/packages/agentsam-brand/bin/agentsam-brand.mjs +9 -0
  87. package/packages/agentsam-brand/package.json +39 -7
  88. package/packages/agentsam-brand/src/adapters/cloudflare-images.js +453 -0
  89. package/packages/agentsam-brand/src/adapters/cloudflare-r2.js +74 -0
  90. package/packages/agentsam-brand/src/adapters/d1-registry.js +26 -0
  91. package/packages/agentsam-brand/src/adapters/filesystem.js +59 -0
  92. package/packages/agentsam-brand/src/adapters/memory.js +31 -0
  93. package/packages/agentsam-brand/src/cli/controller.js +503 -0
  94. package/packages/agentsam-brand/src/cli/wizard.js +526 -0
  95. package/packages/agentsam-brand/src/core/capabilities.js +201 -0
  96. package/packages/agentsam-brand/src/core/derivatives.js +211 -0
  97. package/packages/agentsam-brand/src/core/errors.js +12 -0
  98. package/packages/agentsam-brand/src/core/ingest.js +195 -0
  99. package/packages/agentsam-brand/src/core/inspect.js +109 -0
  100. package/packages/agentsam-brand/src/core/keys.js +83 -0
  101. package/packages/agentsam-brand/src/core/naming.js +142 -0
  102. package/packages/agentsam-brand/src/core/optimize.js +316 -0
  103. package/packages/agentsam-brand/src/core/pack.js +305 -0
  104. package/packages/agentsam-brand/src/core/plan.js +276 -0
  105. package/packages/agentsam-brand/src/core/preview.js +159 -0
  106. package/packages/agentsam-brand/src/core/processors/cloudflare.js +68 -0
  107. package/packages/agentsam-brand/src/core/processors/contract.js +203 -0
  108. package/packages/agentsam-brand/src/core/processors/index.js +148 -0
  109. package/packages/agentsam-brand/src/core/processors/native.js +82 -0
  110. package/packages/agentsam-brand/src/core/processors/sharp.js +113 -0
  111. package/packages/agentsam-brand/src/core/processors/squoosh-binary.js +195 -0
  112. package/packages/agentsam-brand/src/core/promote.js +288 -0
  113. package/packages/agentsam-brand/src/core/receipts.js +155 -0
  114. package/packages/agentsam-brand/src/core/v2/compile.js +280 -0
  115. package/packages/agentsam-brand/src/core/v2/derivatives-semantic.js +207 -0
  116. package/packages/agentsam-brand/src/core/v2/ingest-graph.js +387 -0
  117. package/packages/agentsam-brand/src/core/v2/roles.js +180 -0
  118. package/packages/agentsam-brand/src/core/v2/schema.js +209 -0
  119. package/packages/agentsam-brand/src/core/v2/templates.js +114 -0
  120. package/packages/agentsam-brand/src/index.js +193 -36
  121. package/packages/agentsam-brand/src/presets/app-icon.json +16 -0
  122. package/packages/agentsam-brand/src/presets/favicon.json +17 -0
  123. package/packages/agentsam-brand/src/presets/hero.json +12 -0
  124. package/packages/agentsam-brand/src/presets/index.js +40 -0
  125. package/packages/agentsam-brand/src/presets/logo.json +12 -0
  126. package/packages/agentsam-brand/src/presets/model.json +8 -0
  127. package/packages/agentsam-brand/src/presets/og-image.json +10 -0
  128. package/packages/agentsam-brand/src/presets/product-shot.json +11 -0
  129. package/packages/agentsam-brand/src/presets/social-image.json +10 -0
  130. package/packages/agentsam-brand/src/presets/web-image.json +10 -0
  131. package/packages/agentsam-contracts/src/artifacts.ts +41 -0
  132. package/packages/agentsam-knowledge/src/autorag/index.js +1 -1
  133. package/packages/agentsam-knowledge/src/providers/index.js +6 -4
  134. package/packages/agentsam-knowledge/src/providers/ollama-endpoint.js +31 -0
  135. package/packages/agentsam-knowledge/test/autorag.test.mjs +22 -1
  136. package/packages/agentsam-nav/package.json +2 -2
  137. package/packages/agentsam-rapid-rust/Cargo.lock +344 -0
  138. package/packages/agentsam-rapid-rust/Cargo.toml +18 -0
  139. package/packages/agentsam-rapid-rust/README.md +5 -0
  140. package/packages/agentsam-rapid-rust/src/main.rs +718 -0
  141. package/packages/agentsam-rapid-rust/templates/api/Cargo.toml.tmpl +18 -0
  142. package/packages/agentsam-rapid-rust/templates/api/src/lib.rs.tmpl +17 -0
  143. package/packages/agentsam-rapid-rust/templates/common/README.md.tmpl +22 -0
  144. package/packages/agentsam-rapid-rust/templates/common/package.json.tmpl +12 -0
  145. package/packages/agentsam-rapid-rust/templates/common/wrangler.toml.tmpl +6 -0
  146. package/packages/agentsam-rapid-rust/templates/crypto-auth/Cargo.toml.tmpl +21 -0
  147. package/packages/agentsam-rapid-rust/templates/crypto-auth/src/lib.rs.tmpl +32 -0
  148. package/packages/agentsam-rapid-rust/templates/data-processor/Cargo.toml.tmpl +18 -0
  149. package/packages/agentsam-rapid-rust/templates/data-processor/src/lib.rs.tmpl +41 -0
  150. package/packages/agentsam-rapid-rust/templates/shared-core/Cargo.toml.tmpl +9 -0
  151. package/packages/agentsam-rapid-rust/templates/shared-core/core/Cargo.toml.tmpl +7 -0
  152. package/packages/agentsam-rapid-rust/templates/shared-core/core/src/lib.rs.tmpl +23 -0
  153. package/packages/agentsam-rapid-rust/templates/shared-core/worker/Cargo.toml.tmpl +12 -0
  154. package/packages/agentsam-rapid-rust/templates/shared-core/worker/src/lib.rs.tmpl +16 -0
  155. package/packages/agentsam-rapid-rust/templates/shared-core/wrangler.toml.tmpl +6 -0
  156. package/packages/agentsam-repository/src/index.js +1 -0
  157. package/packages/agentsam-repository/src/merkle/semantic.js +3 -10
  158. package/packages/agentsam-repository/src/source-types.js +85 -0
  159. package/packages/agentsam-vault/README.md +96 -0
  160. package/packages/agentsam-vault/package.json +32 -0
  161. package/packages/agentsam-vault/src/contracts/credential.d.ts +78 -0
  162. package/packages/agentsam-vault/src/contracts/credential.js +132 -0
  163. package/packages/agentsam-vault/src/crypto/aes-gcm.d.ts +30 -0
  164. package/packages/agentsam-vault/src/crypto/aes-gcm.js +107 -0
  165. package/packages/agentsam-vault/src/crypto/oauth-envelope.js +150 -0
  166. package/packages/agentsam-vault/src/index.d.ts +9 -0
  167. package/packages/agentsam-vault/src/index.js +27 -0
  168. package/packages/agentsam-vault/src/providers/registry.js +90 -0
  169. package/packages/agentsam-vault/src/runtime/resolver.js +77 -0
  170. package/packages/agentsam-workbench/README.md +24 -0
  171. package/packages/agentsam-workbench/package.json +38 -0
  172. package/packages/agentsam-workbench/src/agent/AgentPanel.tsx +41 -0
  173. package/packages/agentsam-workbench/src/agent/ArtifactCard.tsx +9 -0
  174. package/packages/agentsam-workbench/src/agent/Composer.tsx +102 -0
  175. package/packages/agentsam-workbench/src/agent/ConnectedAgentPanel.tsx +194 -0
  176. package/packages/agentsam-workbench/src/agent/ContextualComposer.tsx +788 -0
  177. package/packages/agentsam-workbench/src/agent/GoalStatusStrip.tsx +38 -0
  178. package/packages/agentsam-workbench/src/agent/MiniAgentSam.tsx +86 -0
  179. package/packages/agentsam-workbench/src/agent/ModelSelect.tsx +19 -0
  180. package/packages/agentsam-workbench/src/agent/Thread.tsx +56 -0
  181. package/packages/agentsam-workbench/src/agent/ToolReceipt.tsx +11 -0
  182. package/packages/agentsam-workbench/src/agent/contextual-composer.css +611 -0
  183. package/packages/agentsam-workbench/src/agent/index.ts +10 -0
  184. package/packages/agentsam-workbench/src/agent/mini-agentsam.css +12 -0
  185. package/packages/agentsam-workbench/src/browser/BrowserFrame.tsx +37 -0
  186. package/packages/agentsam-workbench/src/browser/index.ts +1 -0
  187. package/packages/agentsam-workbench/src/index.ts +6 -0
  188. package/packages/agentsam-workbench/src/project-control.css +160 -0
  189. package/packages/agentsam-workbench/src/projects/ProjectsSurface.tsx +131 -0
  190. package/packages/agentsam-workbench/src/projects/index.ts +1 -0
  191. package/packages/agentsam-workbench/src/shell/Breadcrumb.tsx +35 -0
  192. package/packages/agentsam-workbench/src/shell/ResizableStage.tsx +99 -0
  193. package/packages/agentsam-workbench/src/shell/SidePanel.tsx +5 -0
  194. package/packages/agentsam-workbench/src/shell/index.ts +4 -0
  195. package/packages/agentsam-workbench/src/shell/surfaceRegistry.ts +29 -0
  196. package/packages/agentsam-workbench/src/terminal/TerminalSurface.tsx +5 -0
  197. package/packages/agentsam-workbench/src/terminal/index.ts +1 -0
  198. package/packages/agentsam-workbench/src/timeline/GanttBoard.tsx +258 -0
  199. package/packages/agentsam-workbench/src/timeline/index.ts +1 -0
  200. package/packages/agentsam-workbench/tsconfig.json +13 -0
  201. package/packages/connectors/cloudflare/OAUTH_STORAGE_AUDIT.md +53 -0
  202. package/packages/connectors/cloudflare/package.json +3 -1
  203. package/packages/connectors/cloudflare/src/api-client.js +279 -0
  204. package/packages/connectors/cloudflare/src/capabilities.js +585 -0
  205. package/packages/connectors/cloudflare/src/credential.js +318 -0
  206. package/packages/connectors/cloudflare/src/families/brand-protection.js +123 -0
  207. package/packages/connectors/cloudflare/src/families/keyless.js +114 -0
  208. package/packages/connectors/cloudflare/src/families/pages.js +151 -0
  209. package/packages/connectors/cloudflare/src/families/scanner.js +123 -0
  210. package/packages/connectors/cloudflare/src/families/snippets.js +207 -0
  211. package/packages/connectors/cloudflare/src/families/tag-gateway.js +46 -0
  212. package/packages/connectors/cloudflare/src/families/tags.js +124 -0
  213. package/packages/connectors/cloudflare/src/families/token-validation.js +95 -0
  214. package/packages/connectors/cloudflare/src/families/workflows.js +156 -0
  215. package/packages/connectors/cloudflare/src/index.js +102 -265
  216. package/packages/connectors/cloudflare/src/oauth-persist.js +336 -0
  217. package/packages/connectors/cloudflare/src/owner.js +42 -12
  218. package/packages/connectors/cloudflare/src/permission-options.js +1064 -0
  219. package/packages/connectors/cloudflare/src/routes.js +309 -144
  220. package/packages/connectors/cloudflare/src/scope-catalog.js +345 -0
  221. package/packages/connectors/cloudflare/src/vault.js +2 -2
  222. package/packages/connectors/cloudflare/tests/capabilities.test.mjs +276 -0
  223. package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -30
  224. package/packages/connectors/cloudflare/tests/oauth-persist.test.mjs +125 -0
  225. package/packages/identity/.agentsam/features/oauth-login-portal/providers.json +22 -2
  226. package/packages/identity/.agentsam/features/oauth-login-portal/resources.json +10 -4
  227. package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +20 -0
  228. package/packages/identity/README.md +1 -1
  229. package/packages/identity/backend/README.md +1 -1
  230. package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +3 -2
  231. package/packages/identity/migrations/sqlite/004_identity_oauth_consent_catalog.sql +48 -0
  232. package/packages/identity/package.json +4 -3
  233. package/packages/identity/scripts/local-identity-bridge.mjs +184 -0
  234. package/packages/identity/scripts/preview-auth-portal.mjs +7 -7
  235. package/packages/identity/src/adapters/cloudflare-d1/index.js +31 -2
  236. package/packages/identity/src/adapters/sqlite/index.js +4 -1
  237. package/packages/identity/src/contracts/auth-config.js +28 -8
  238. package/packages/identity/src/contracts/external-identity.js +1 -1
  239. package/packages/identity/src/contracts/provider.js +1 -0
  240. package/packages/identity/src/frontend/auth-portal/README.md +2 -1
  241. package/packages/identity/src/frontend/auth-portal/pages/login.html +6 -5
  242. package/packages/identity/src/frontend/auth-portal/pages/reset.html +6 -5
  243. package/packages/identity/src/frontend/auth-portal/pages/signup.html +6 -5
  244. package/packages/identity/src/frontend/auth-portal/shared/company-branding.js +1 -1
  245. package/packages/identity/src/frontend/auth-portal/shared/desktop-identity-bridge.js +207 -0
  246. package/packages/identity/src/index.js +16 -0
  247. package/packages/identity/src/migrations/0001_identity_core.sql +19 -2
  248. package/packages/identity/src/migrations/0003_accounts_ssot.sql +20 -0
  249. package/packages/identity/src/migrations/README.md +5 -2
  250. package/packages/identity/src/oauth/README.md +10 -5
  251. package/packages/identity/src/oauth/consent-view.js +388 -0
  252. package/packages/identity/src/oauth/credentials.js +31 -11
  253. package/packages/identity/src/oauth/google-desktop-exchange.js +173 -0
  254. package/packages/identity/src/providers/cloudflare/oauth.js +3 -4
  255. package/packages/identity/src/providers/cloudflare/profile.js +49 -14
  256. package/packages/identity/src/providers/iam/index.js +27 -5
  257. package/packages/identity/src/server/identity-service.js +31 -2
  258. package/packages/identity/src/server/worker-router.js +111 -81
  259. package/packages/identity/tests/auth-config.test.mjs +3 -2
  260. package/packages/identity/tests/identity-init-scaffold.test.mjs +3 -0
  261. package/packages/identity/tests/identity-service.test.mjs +16 -0
  262. package/packages/identity/tests/native-session-worker.test.mjs +64 -0
  263. package/packages/identity/tests/oauth-consent-view.test.mjs +106 -0
  264. package/packages/identity/tests/oauth-credentials.test.mjs +17 -4
  265. package/packages/identity/tests/oauth-presets-accounts-ssot.test.mjs +58 -0
  266. package/packages/identity/tests/preview-auth-portal.test.mjs +3 -3
  267. package/packages/runtime-protocol/src/index.js +154 -0
  268. package/packages/theme-scenes/README.md +164 -0
  269. package/packages/theme-scenes/package.json +53 -0
  270. package/packages/theme-scenes/src/blocks/index.js +30 -0
  271. package/packages/theme-scenes/src/contracts/index.js +286 -0
  272. package/packages/theme-scenes/src/demos/index.js +65 -0
  273. package/packages/theme-scenes/src/index.js +75 -0
  274. package/packages/theme-scenes/src/presets/index.js +263 -0
  275. package/packages/theme-scenes/src/runtime/compile.js +119 -0
  276. package/packages/theme-scenes/src/scenes/index.js +146 -0
  277. package/packages/theme-scenes/src/shell/index.js +47 -0
  278. package/packages/theme-scenes/src/themes/index.js +129 -0
  279. package/packages/theme-scenes/src/visuals/index.js +41 -0
  280. package/packages/theme-scenes/test/theme-scenes.test.mjs +179 -0
  281. package/packages/work-graph/src/index.js +34 -1
  282. package/packages/work-graph/src/renderers/gantt-model.js +24 -0
  283. package/packages/work-graph/test/work-graph.test.mjs +27 -7
  284. package/protocol/README.md +15 -7
  285. package/protocol/apps/agentsam.app.v1.schema.json +89 -7
  286. package/protocol/apps/agentsam.host-install.v1.schema.json +36 -0
  287. package/protocol/assets/README.md +9 -0
  288. package/protocol/assets/asset-record.v1.schema.json +64 -0
  289. package/protocol/runtime/agentsam.runtime.v1.schema.json +75 -0
  290. package/protocol/scoring/README.md +10 -0
  291. package/protocol/scoring/scorecard.v1.schema.json +65 -0
  292. package/protocol/ui/icon-registry.mjs +1 -0
  293. package/registry/cms-rust-machine-20260928/installable-products.json +31 -0
  294. package/registry/cms-rust-machine-20260928/manifest.json +22 -0
  295. package/registry/cms-rust-machine-20260928/migration.sql +237 -0
  296. package/registry/cms-rust-machine-20260928/receipt.json +21 -0
  297. package/registry/cms-rust-machine-20260928/receipt.sql +27 -0
  298. package/scripts/build_rapid_rust_cli.py +83 -0
  299. package/src/attachments/index.js +6 -0
  300. package/src/attachments/intake.js +235 -0
  301. package/src/capabilities/repository-snapshot.js +19 -2
  302. package/src/cli/command-catalog.js +8 -0
  303. package/src/cli.js +182 -26
  304. package/src/commands/account-auth.js +94 -8
  305. package/src/commands/app.js +307 -3
  306. package/src/commands/billing.js +127 -0
  307. package/src/commands/brand.js +74 -60
  308. package/src/commands/cheat-sheet.js +154 -0
  309. package/src/commands/cloudflare-login.js +222 -0
  310. package/src/commands/cloudflare.js +453 -23
  311. package/src/commands/codebaseindex.js +6 -1
  312. package/src/commands/compute.js +4 -0
  313. package/src/commands/connections.js +11 -5
  314. package/src/commands/credentials.js +116 -0
  315. package/src/commands/gcloud-auth.js +427 -0
  316. package/src/commands/google-cloud.js +375 -0
  317. package/src/commands/identity-init.js +2 -2
  318. package/src/commands/mcp.js +226 -33
  319. package/src/commands/ollama.js +3 -2
  320. package/src/commands/plugins.js +7 -2
  321. package/src/commands/product.js +36 -0
  322. package/src/commands/runtime-status.js +28 -9
  323. package/src/commands/runtime.js +313 -0
  324. package/src/commands/rust.js +24 -0
  325. package/src/commands/setup.js +279 -0
  326. package/src/commands/shell.js +16 -3
  327. package/src/commands/start-local.js +2 -2
  328. package/src/commands/terminal.js +171 -0
  329. package/src/commands/update.js +140 -0
  330. package/src/commands/whoami.js +329 -65
  331. package/src/features/resolve.js +1 -0
  332. package/src/index.js +4 -0
  333. package/src/indexing/ingest/inventory.js +254 -19
  334. package/src/indexing/ingest/materials.js +3 -9
  335. package/src/indexing/ingest/style-evidence.js +181 -0
  336. package/src/knowledge/source.js +3 -3
  337. package/src/lib/account-session.js +11 -4
  338. package/src/lib/app-authority.js +172 -0
  339. package/src/lib/auth.js +94 -21
  340. package/src/lib/core-client.js +5 -6
  341. package/src/lib/credentials-audit.js +451 -0
  342. package/src/lib/deploy/health.js +24 -6
  343. package/src/lib/detect-context.js +7 -1
  344. package/src/lib/google-cloud-connection.js +270 -0
  345. package/src/lib/google-cloud-inventory.js +385 -0
  346. package/src/lib/google-desktop-oauth.js +372 -0
  347. package/src/lib/google-studio-brokered-oauth.js +158 -0
  348. package/src/lib/identity-scaffold.js +24 -6
  349. package/src/lib/init-options.js +192 -0
  350. package/src/lib/local-scaffold.js +48 -16
  351. package/src/lib/provider-command-remediation.js +308 -0
  352. package/src/lib/provider-credentials.js +8 -0
  353. package/src/lib/scaffold/index.js +8 -2
  354. package/src/lib/scaffold/mcp-server.js +43 -0
  355. package/src/lib/scaffold/templates/mcp-server/index.js +397 -0
  356. package/src/lib/scaffold/templates/mcp-server/presets.js +39 -0
  357. package/src/lib/scaffold/wizards/cms.js +93 -0
  358. package/src/lib/scaffold/wizards/mcp-server.js +61 -0
  359. package/src/lib/setup/contract.js +26 -0
  360. package/src/lib/setup/discover.js +116 -0
  361. package/src/lib/setup/execute.js +205 -0
  362. package/src/lib/setup/planner.js +134 -0
  363. package/src/lib/setup/recipes.js +235 -0
  364. package/src/lib/setup/runtime-persist.js +197 -0
  365. package/src/lib/setup/runtime.js +394 -0
  366. package/src/lib/slash-commands.js +2 -0
  367. package/src/lib/terminal/machine-identity.js +95 -0
  368. package/src/lib/terminal-local.js +164 -0
  369. package/src/lib/terminal-scopes.js +52 -0
  370. package/src/lib/whoami-capabilities.js +184 -0
  371. package/src/local-pty/server.js +34 -5
  372. package/src/mcp/authority.js +58 -108
  373. package/src/mcp/client-adapters.js +68 -144
  374. package/src/mcp/client.js +142 -122
  375. package/src/mcp/cloudflare-bundles.js +225 -0
  376. package/src/mcp/index.js +12 -0
  377. package/src/plugins/cloudflare.js +1 -1
  378. package/src/providers/ollama-chat.js +2 -1
  379. package/src/sam/types.js +27 -2
  380. package/src/security/inventory.js +12 -2
  381. package/src/security/local-vault.js +2 -0
  382. package/src/security/scan.js +1 -1
  383. package/test/account-session.test.mjs +3 -3
  384. package/test/app-authority-context.test.mjs +32 -0
  385. package/test/cli/codebaseindex-plan-ux.test.mjs +1 -1
  386. package/test/cli/mcp-client.test.mjs +21 -10
  387. package/test/cli/runtime-persist.test.mjs +56 -0
  388. package/test/cli/rust-command.test.mjs +21 -0
  389. package/test/credentials-audit.test.mjs +113 -0
  390. package/test/deploy-health-scan.test.mjs +29 -1
  391. package/test/gcloud-auth.test.mjs +111 -0
  392. package/test/google-cloud-connection.test.mjs +83 -0
  393. package/test/google-cloud-inventory.test.mjs +64 -0
  394. package/test/init-options.test.mjs +35 -0
  395. package/test/integration/app-command.test.mjs +30 -0
  396. package/test/integration/cad-project.test.mjs +4 -1
  397. package/test/integration/cms-app-scaffold.test.mjs +71 -0
  398. package/test/integration/cms-site-tenancy-contract.test.mjs +3 -2
  399. package/test/integration/codebaseindex-ingest.test.mjs +48 -0
  400. package/test/integration/features-resolve.test.mjs +30 -11
  401. package/test/integration/inspect-path-cli.test.mjs +46 -0
  402. package/test/integration/inventory-evidence.test.mjs +125 -0
  403. package/test/integration/runtime-status.integration.test.mjs +2 -1
  404. package/test/knowledge.test.mjs +35 -1
  405. package/test/loopback-oauth.test.mjs +53 -0
  406. package/test/ollama.test.mjs +22 -0
  407. package/test/security.test.mjs +23 -0
  408. package/test/setup-planner.test.mjs +74 -0
  409. package/test/setup-runtime.test.mjs +51 -0
  410. package/test/terminal/local-pty.mock.test.mjs +3 -1
  411. package/test/terminal-machine-identity.test.mjs +88 -0
  412. package/test/whoami-resume.test.mjs +27 -6
  413. package/packages/agentsam-brand/test/brand-scan.test.mjs +0 -106
  414. package/packages/work-graph/prototype/README.md +0 -32
  415. package/packages/work-graph/prototype/data/demo-workgraph.js +0 -47
  416. package/packages/work-graph/prototype/renderers/timeline-model.js +0 -21
  417. package/packages/work-graph/prototype/renderers/workgraph-runtime.js +0 -38
  418. package/packages/work-graph/prototype-gnantt.html +0 -655
@@ -0,0 +1,1345 @@
1
+ "use client";
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import type { AgentPrincipal, AgentWorkbenchAdapter } from "@inneranimalmedia/agentsam-contracts";
5
+ import { MiniAgentSam, type AnnotationSelection } from "@inneranimalmedia/agentsam-workbench/agent";
6
+ import "@inneranimalmedia/agentsam-workbench/agent/mini-agentsam.css";
7
+ import { AgentSamDrawer } from "./AgentSamDrawer";
8
+ import { escapeCmsText, safeCmsAssetUrl, safeCmsCssValue, sanitizeCmsRichText } from "./lib/content-safety";
9
+ import {
10
+ createCmsEditorBlock,
11
+ createCmsEditorPage as createIamPage,
12
+ createCmsEditorSection as createIamSection,
13
+ getCmsEditorAssets as getAssets,
14
+ getCmsEditorBootstrap as getBootstrap,
15
+ getCmsEditorContacts as getContacts,
16
+ getCmsEditorTemplates as getTemplates,
17
+ applyCmsEditorTemplate as applyTemplateApi,
18
+ publishCmsEditorPage as publishIamPage,
19
+ renameCmsEditorSection as renameSection,
20
+ reorderCmsEditorSections as reorderSections,
21
+ saveCmsEditorSection as saveIamSection,
22
+ saveCmsEditorBlock,
23
+ saveCmsEditorPageMeta as savePageMeta,
24
+ saveCmsEditorThemeVars as saveThemeVars,
25
+ setCmsEditorBlockVisibility,
26
+ setCmsEditorSectionVisibility as setSectionVisibility,
27
+ } from "@inneranimalmedia/agentsam-cms-backend/api";
28
+ import type {
29
+ CmsEditorBlock as BlockData,
30
+ CmsEditorPage as PageData,
31
+ CmsEditorSection as Section,
32
+ CmsEditorSite as Site,
33
+ } from "@inneranimalmedia/agentsam-cms-shared";
34
+ import { mapCmsEditorPage, mapCmsEditorSection } from "@inneranimalmedia/agentsam-cms-backend/model";
35
+ import {
36
+ CMS_EDITOR_PREVIEW_TYPES,
37
+ cmsEditorSelectionFromPreview,
38
+ normalizeCmsEditorPreviewMessage,
39
+ postCmsEditorPreviewMessage,
40
+ } from "@inneranimalmedia/agentsam-cms-backend/preview";
41
+
42
+ type RailMode = "pages" | "sections" | "blocks" | "templates" | "media" | "crm" | "settings";
43
+ type InspectorTab = "content" | "style" | "page" | "theme" | "crm";
44
+ type Viewport = "phone" | "tablet" | "desktop";
45
+ type Toast = { id: number; type: "success" | "warning" | "error" | "info"; message: string; action?: string };
46
+
47
+ const railItems: { id: RailMode; label: string; icon: string }[] = [
48
+ { id: "pages", label: "Pages", icon: "file" }, { id: "sections", label: "Sections", icon: "layers" },
49
+ { id: "blocks", label: "Blocks", icon: "puzzle" }, { id: "templates", label: "Templates", icon: "grid" },
50
+ { id: "media", label: "Media", icon: "image" }, { id: "crm", label: "CRM", icon: "users" },
51
+ { id: "settings", label: "Settings", icon: "settings" },
52
+ ];
53
+
54
+ const iconPaths: Record<string, React.ReactNode> = {
55
+ file: <><path d="M6 2.75h7l5 5v13.5H6z"/><path d="M13 2.75v5h5M9 12h6M9 16h6"/></>,
56
+ layers: <><path d="m12 3 9 5-9 5-9-5z"/><path d="m3 12 9 5 9-5M3 16l9 5 9-5"/></>,
57
+ puzzle: <path d="M19 13h-2.1a2.9 2.9 0 1 0 0-2H15V6h-4V4.1a2.9 2.9 0 1 0-2 0V6H4v5h2.1a2.9 2.9 0 1 1 0 2H4v5h5v2a3 3 0 0 0 6 0v-2h4z"/>,
58
+ grid: <><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></>,
59
+ image: <><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m3 17 5-5 4 4 3-3 6 5"/></>,
60
+ users: <><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M18 8a3 3 0 0 1 0 6M22 20v-2a4 4 0 0 0-3-3.87"/></>,
61
+ settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.03 1.55V21h-4v-.08A1.7 1.7 0 0 0 8.95 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.52-1H3v-4h.08A1.7 1.7 0 0 0 4.6 8.95a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6 1.7 1.7 0 0 0 10 3.08V3h4v.08A1.7 1.7 0 0 0 15.05 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.13.62.75 1 1.52 1H21v4h-.08A1.7 1.7 0 0 0 19.4 15Z"/></>,
62
+ plus: <path d="M12 5v14M5 12h14"/>, search: <><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></>,
63
+ chevron: <path d="m9 18 6-6-6-6"/>, down: <path d="m6 9 6 6 6-6"/>, more: <><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></>,
64
+ eye: <><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12"/><circle cx="12" cy="12" r="2.5"/></>, eyeoff: <><path d="m3 3 18 18"/><path d="M10.6 6.2A10 10 0 0 1 12 6c6.5 0 10 6 10 6a18 18 0 0 1-2.1 2.8M6.6 6.6C3.7 8.4 2 12 2 12s3.5 6 10 6a10 10 0 0 0 5.4-1.5"/></>,
65
+ phone: <rect x="7" y="2" width="10" height="20" rx="2"/>, tablet: <rect x="5" y="2" width="14" height="20" rx="2"/>, desktop: <><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></>,
66
+ undo: <><path d="m9 7-5 5 5 5"/><path d="M20 17a7 7 0 0 0-7-7H4"/></>, redo: <><path d="m15 7 5 5-5 5"/><path d="M4 17a7 7 0 0 1 7-7h9"/></>,
67
+ external: <><path d="M14 3h7v7M21 3l-10 10"/><path d="M18 13v7H4V6h7"/></>, publish: <><path d="M12 16V3M7 8l5-5 5 5"/><path d="M5 14v7h14v-7"/></>,
68
+ collapse: <><path d="M10 4 4 12l6 8M20 4l-6 8 6 8"/></>, close: <path d="m5 5 14 14M19 5 5 19"/>,
69
+ check: <path d="m4 12 5 5L20 6"/>, warning: <><path d="M12 3 2 21h20z"/><path d="M12 9v5M12 18h.01"/></>, info: <><circle cx="12" cy="12" r="10"/><path d="M12 11v6M12 7h.01"/></>,
70
+ copy: <><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V4H4v12h4"/></>, trash: <><path d="M4 7h16M9 7V4h6v3M7 7l1 14h8l1-14"/></>,
71
+ upload: <><path d="M12 16V4M7 9l5-5 5 5"/><path d="M4 15v6h16v-6"/></>, filter: <path d="M3 5h18l-7 8v6l-4 2v-8z"/>,
72
+ link: <><path d="M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1-1"/></>,
73
+ code: <><path d="m8 9-4 3 4 3M16 9l4 3-4 3M14 5l-4 14"/></>, menu: <path d="M4 7h16M4 12h16M4 17h16"/>,
74
+ };
75
+
76
+ function Icon({ name, size = 16 }: { name: string; size?: number }) {
77
+ return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>{iconPaths[name] || iconPaths.info}</svg>;
78
+ }
79
+
80
+ const initialSites: Site[] = [];
81
+
82
+
83
+ function useStored<T>(key: string, initial: T) {
84
+ const [value, setValue] = useState<T>(() => { if (typeof window === "undefined") return initial; try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : initial; } catch { return initial; } });
85
+ useEffect(() => { try { localStorage.setItem(key, JSON.stringify(value)); } catch {} }, [key, value]);
86
+ return [value, setValue] as const;
87
+ }
88
+
89
+ function Button({ children, icon, kind = "ghost", onClick, disabled, className = "", title, ...rest }: any) {
90
+ return <button className={`button ${kind} ${className}`} onClick={onClick} disabled={disabled} title={title} {...rest}>{icon && <Icon name={icon} />}{children}</button>;
91
+ }
92
+
93
+ function Search({ value, onChange, placeholder = "Search" }: any) {
94
+ return <div className="search"><Icon name="search" size={14}/><input value={value} onChange={e => onChange(e.target.value)} placeholder={placeholder}/>{value && <button onClick={() => onChange("")} aria-label="Clear"><Icon name="close" size={12}/></button>}<kbd>⌘F</kbd></div>;
95
+ }
96
+
97
+ function EmptyState({ icon = "search", title, copy, action, onAction }: any) {
98
+ return <div className="empty"><span><Icon name={icon} size={22}/></span><h4>{title}</h4><p>{copy}</p>{action && <Button kind="secondary" onClick={onAction}>{action}</Button>}</div>;
99
+ }
100
+
101
+ function Wireframe({ variant = 0 }: { variant?: number }) {
102
+ return <svg className="wireframe" viewBox="0 0 220 120" aria-hidden><rect x="1" y="1" width="218" height="118" rx="7" fill="#ffffff" stroke="rgba(43,39,31,.14)"/><rect x="15" y="14" width="42" height="5" rx="2" fill="#c7c3b5"/><rect x="155" y="14" width="18" height="4" rx="2" fill="#DCD5C2"/><rect x="179" y="14" width="25" height="4" rx="2" fill="#1e6a6f"/>{variant % 3 === 0 ? <><rect x="15" y="42" width="86" height="8" rx="2" fill="#8a8a95"/><rect x="15" y="56" width="70" height="8" rx="2" fill="#8a8a95"/><rect x="15" y="73" width="75" height="4" rx="2" fill="#DCD5C2"/><rect x="15" y="83" width="44" height="11" rx="3" fill="#1e6a6f"/><rect x="124" y="36" width="80" height="63" rx="5" fill="#E8E2D2"/></> : variant % 3 === 1 ? <><rect x="55" y="40" width="110" height="8" rx="2" fill="#8a8a95"/><rect x="75" y="55" width="70" height="4" rx="2" fill="#DCD5C2"/><rect x="22" y="76" width="52" height="25" rx="4" fill="#E8E2D2"/><rect x="84" y="76" width="52" height="25" rx="4" fill="#E8E2D2"/><rect x="146" y="76" width="52" height="25" rx="4" fill="#E8E2D2"/></> : <><rect x="15" y="37" width="190" height="18" rx="4" fill="#E8E2D2"/><rect x="15" y="64" width="90" height="40" rx="4" fill="#E8E2D2"/><rect x="115" y="64" width="90" height="40" rx="4" fill="#E8E2D2"/></>}</svg>;
103
+ }
104
+
105
+ function Modal({ title, children, onClose, wide = false, focusKey = "open" }: any) {
106
+ const ref = useRef<HTMLDivElement>(null);
107
+ const onCloseRef = useRef(onClose);
108
+ onCloseRef.current = onClose;
109
+ useEffect(() => {
110
+ const fn = (e: KeyboardEvent) => {
111
+ if (e.key === "Escape") onCloseRef.current();
112
+ };
113
+ window.addEventListener("keydown", fn);
114
+ return () => window.removeEventListener("keydown", fn);
115
+ }, []);
116
+ useEffect(() => {
117
+ const el = ref.current?.querySelector("input,button,textarea") as HTMLElement | null;
118
+ el?.focus();
119
+ }, [focusKey]);
120
+ return <div className="modal-backdrop" role="dialog" aria-modal="true"><div ref={ref} className={`modal ${wide ? "modal-wide" : ""}`}><header><div><span className="modal-kicker">Studio CMS</span><h2>{title}</h2></div><Button icon="close" onClick={onClose} title="Close"/></header>{children}</div></div>;
121
+ }
122
+
123
+ function ToastStack({ toasts, dismiss }: { toasts: Toast[]; dismiss: (id: number) => void }) {
124
+ return <div className="toast-stack">{toasts.slice(-3).map(t => <div className={`toast ${t.type}`} key={t.id}><span className="toast-icon"><Icon name={t.type === "success" ? "check" : t.type === "warning" ? "warning" : t.type === "error" ? "close" : "info"}/></span><span>{t.message}</span>{t.action && <button>{t.action}</button>}<button className="toast-x" onClick={() => dismiss(t.id)}><Icon name="close" size={12}/></button>{t.type !== "error" && <i/>}</div>)}</div>;
125
+ }
126
+
127
+ export type CmsEditorAgentConfig = {
128
+ adapter: AgentWorkbenchAdapter;
129
+ principal: AgentPrincipal;
130
+ conversationId?: string;
131
+ };
132
+
133
+ export type CmsEditorProps = {
134
+ projectSlug?: string;
135
+ initialPageId?: string | null;
136
+ initialPanel?: "pages" | "sections" | "templates" | "imports" | "theme";
137
+ siteCatalog?: Array<{ slug: string; name?: string; domain?: string | null }>;
138
+ onSiteChange?: (slug: string) => void;
139
+ agent?: CmsEditorAgentConfig;
140
+ basePath?: string;
141
+ onNavigate?: (path: string) => void;
142
+ };
143
+
144
+ function exportContactsCsv(contacts: any[]) {
145
+ if (!contacts.length) return;
146
+ const quote = (value: unknown) => `"${String(value ?? "").replace(/"/g, '""')}"`;
147
+ const rows = [["name","email","source","date"], ...contacts.map((contact) => [contact.name,contact.email,contact.source,contact.date])];
148
+ const blob = new Blob([rows.map((row) => row.map(quote).join(",")).join("\n")], { type: "text/csv;charset=utf-8" });
149
+ const link = document.createElement("a");
150
+ link.href = URL.createObjectURL(blob);
151
+ link.download = "cms-contacts.csv";
152
+ link.click();
153
+ URL.revokeObjectURL(link.href);
154
+ }
155
+
156
+ export default function CmsEditor({
157
+ projectSlug = "",
158
+ initialPageId = null,
159
+ initialPanel = "sections",
160
+ siteCatalog = [],
161
+ onSiteChange,
162
+ agent,
163
+ basePath = "/cms",
164
+ onNavigate,
165
+ }: CmsEditorProps) {
166
+ const [sites, setSites] = useState<Site[]>(initialSites);
167
+ const [siteId, setSiteId] = useStored("cms-active-site", projectSlug);
168
+ const site = sites.find((s) => s.id === siteId) || sites.find((s) => s.pages?.length) || sites[0] || null;
169
+ const [pageId, setPageId] = useState(initialPageId || site?.pages?.[0]?.id || "");
170
+ const page = site?.pages?.find((p) => p.id === pageId) || site?.pages?.[0] || null;
171
+ const [selectedId, setSelectedId] = useState(page?.sections?.[1]?.id || page?.sections?.[0]?.id || "");
172
+ const selected = page?.sections?.find((s) => s.id === selectedId) || page?.sections?.[0] || null;
173
+ const [selectedBlockId, setSelectedBlockId] = useState("");
174
+ const selectedBlock = selected?.blocks?.find((block) => block.id === selectedBlockId) || null;
175
+ const [rail, setRail] = useState<RailMode>(initialPanel === "theme" ? "sections" : initialPanel === "imports" ? "templates" : initialPanel);
176
+ const [tab, setTab] = useState<InspectorTab>(initialPanel === "theme" ? "theme" : "content");
177
+ const [sidebarCollapsed, setSidebarCollapsed] = useStored("cms-sidebar-collapsed", false);
178
+ const [inspectorCollapsed, setInspectorCollapsed] = useStored("cms-inspector-collapsed", false);
179
+ const [agentSamOpen, setAgentSamOpen] = useStored("cms-agentsam-open-v2", false);
180
+ const [inspectorSheetOpen, setInspectorSheetOpen] = useState(false);
181
+ const [annotateSelecting, setAnnotateSelecting] = useState(false);
182
+ const [annotatePrompt, setAnnotatePrompt] = useState<string | null>(null);
183
+ const canvasScopeRef = useRef<HTMLElement | null>(null);
184
+ const iframeSandbox = "allow-scripts allow-same-origin";
185
+ const [viewport, setViewport] = useStored<Viewport>("cms-viewport", "phone");
186
+ const [zoom, setZoom] = useState("Fit");
187
+ const [responsive, setResponsive] = useState(false);
188
+ const [dirty, setDirty] = useState(false);
189
+ const [saving, setSaving] = useState(false);
190
+ const [preview, setPreview] = useState(false);
191
+ const [search, setSearch] = useState("");
192
+ const [siteSwitcher, setSiteSwitcher] = useState(false);
193
+ const [publishMenu, setPublishMenu] = useState(false);
194
+ const [modal, setModal] = useState<string | null>(null);
195
+ const [newPage, setNewPage] = useState({ type: "Interior", title: "", slug: "", parent: "None" });
196
+ const [toasts, setToasts] = useState<Toast[]>([]);
197
+ const [history, setHistory] = useState<Section[][]>([]);
198
+ const [future, setFuture] = useState<Section[][]>([]);
199
+ const [mediaView, setMediaView] = useState<"grid" | "list">("grid");
200
+ const [mediaFilter, setMediaFilter] = useState("All");
201
+ const [contactId, setContactId] = useState<number | null>(null);
202
+ type TemplateCard = { id: string; name: string; type?: string; category?: string; hasHtml?: boolean };
203
+ const FALLBACK_TEMPLATE_CARDS: TemplateCard[] = [
204
+ { id: "tpl_baseline_landing", name: "Landing page", type: "page", category: "Marketing", hasHtml: false },
205
+ { id: "tpl_baseline_about", name: "About page", type: "page", category: "Marketing", hasHtml: false },
206
+ { id: "tpl_baseline_services", name: "Services grid", type: "section", category: "Agency", hasHtml: false },
207
+ { id: "tpl_baseline_contact", name: "Contact split", type: "section", category: "Agency", hasHtml: false },
208
+ { id: "tpl_baseline_hero", name: "Editorial hero", type: "section", category: "Portfolio", hasHtml: false },
209
+ ];
210
+ const [templateCards, setTemplateCards] = useState<TemplateCard[]>([]);
211
+ const [schemaCatalog, setSchemaCatalog] = useState<{
212
+ protocol_version: number;
213
+ sections: Array<{ key: string; type: string; version: number; label: string; fields?: Record<string, unknown>; allowedBlocks?: string[]; defaults?: Record<string, unknown> }>;
214
+ blocks: Array<{ key: string; type: string; version: number; label: string; fields?: Record<string, unknown> }>;
215
+ }>({ protocol_version: 1, sections: [], blocks: [] });
216
+ const [mediaItems, setMediaItems] = useState<any[]>([]);
217
+ const [contacts, setContacts] = useState<any[]>([]);
218
+
219
+ useEffect(() => {
220
+ const syncPanels = () => {
221
+ if (window.innerWidth < 1100) setSidebarCollapsed(true);
222
+ };
223
+ syncPanels();
224
+ window.addEventListener("resize", syncPanels);
225
+ return () => window.removeEventListener("resize", syncPanels);
226
+ }, [setSidebarCollapsed]);
227
+ const [bootstrapLoading, setBootstrapLoading] = useState(true);
228
+ const [bootstrapError, setBootstrapError] = useState<string | null>(null);
229
+ const [bootstrapNonce, setBootstrapNonce] = useState(0);
230
+ const [theme, setTheme] = useState<Record<string, string>>({ "--brand-primary": "#6358ff", "--brand-secondary": "#1c5e46", "--brand-accent": "#cfef5b", "--color-bg": "#f5f2ea", "--color-surface": "#ffffff", "--color-text": "#101014", "--color-text-muted": "#73737f", "--font-heading": "Inter", "--font-body": "Inter", "--font-mono": "JetBrains Mono", "--font-size-base": "16px", "--radius-md": "12px", "--radius-lg": "22px", "--space-6": "24px", "--section-padding-y": "92px", "--container-max-width": "1200px" });
231
+ const iframeRef = useRef<HTMLIFrameElement>(null);
232
+ const themeSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
233
+ const pageSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
234
+ const pageSaveReadyRef = useRef(false);
235
+
236
+ const toast = useCallback((message: string, type: Toast["type"] = "success", action?: string) => {
237
+ const id = Date.now(); setToasts(v => [...v, { id, message, type, action }]);
238
+ if (type !== "error") setTimeout(() => setToasts(v => v.filter(x => x.id !== id)), type === "warning" ? 5000 : 3000);
239
+ }, []);
240
+ const dismissToast = (id: number) => setToasts(v => v.filter(x => x.id !== id));
241
+
242
+ useEffect(() => {
243
+ setSiteId(projectSlug);
244
+ }, [projectSlug, setSiteId]);
245
+
246
+ useEffect(() => {
247
+ if (!siteCatalog.length) return;
248
+ setSites((current) => {
249
+ const byId = new Map(current.map((entry) => [entry.id, entry]));
250
+ return siteCatalog.map((entry) => {
251
+ const existing = byId.get(entry.slug);
252
+ const name = entry.name || entry.slug;
253
+ if (existing) {
254
+ return {
255
+ ...existing,
256
+ name: entry.name || existing.name,
257
+ domain: entry.domain || existing.domain,
258
+ initials: existing.initials || name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase(),
259
+ };
260
+ }
261
+ return {
262
+ id: entry.slug,
263
+ name,
264
+ initials: name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase(),
265
+ domain: entry.domain || "",
266
+ edited: "Available",
267
+ color: "#6358ff",
268
+ pages: [],
269
+ };
270
+ });
271
+ });
272
+ }, [siteCatalog, projectSlug]);
273
+
274
+ useEffect(() => {
275
+ let cancelled = false;
276
+ setBootstrapLoading(true);
277
+ setBootstrapError(null);
278
+ getBootstrap(projectSlug, initialPageId)
279
+ .then(({ site: loadedSite, themeVars, templates, homePageId, schemas }) => {
280
+ if (cancelled) return;
281
+ setSites((current) => {
282
+ const withoutCurrent = current.filter((entry) => entry.id !== loadedSite.id);
283
+ return [loadedSite, ...withoutCurrent];
284
+ });
285
+ setSiteId(loadedSite.id);
286
+ const targetPage = loadedSite.pages.find((entry) => entry.id === initialPageId)
287
+ || loadedSite.pages.find((entry) => entry.id === homePageId)
288
+ || loadedSite.pages[0];
289
+ if (targetPage) {
290
+ setPageId(targetPage.id);
291
+ setSelectedId(targetPage.sections[0]?.id || "");
292
+ }
293
+ if (Object.keys(themeVars).length) setTheme((current) => ({ ...current, ...themeVars }));
294
+ if (templates.length) {
295
+ const mapped = templates.map((entry: any) => ({ id: String(entry.id || ""), name: String(entry.template_name || entry.iam_label || entry.slug || "Template"), type: String(entry.template_type || "page"), category: String(entry.category || "General"), hasHtml: Boolean(entry.source_html_r2_key || entry.source_html_r2_key) })).filter((entry: TemplateCard) => entry.id);
296
+ setTemplateCards(mapped.length ? mapped : FALLBACK_TEMPLATE_CARDS);
297
+ }
298
+ if (schemas) {
299
+ setSchemaCatalog({
300
+ protocol_version: Number(schemas.protocol_version || 1),
301
+ sections: Array.isArray(schemas.sections) ? schemas.sections : [],
302
+ blocks: Array.isArray(schemas.blocks) ? schemas.blocks : [],
303
+ });
304
+ }
305
+ setBootstrapLoading(false);
306
+ if (initialPanel === "imports") setModal("add-section");
307
+ })
308
+ .catch((error) => {
309
+ if (cancelled) return;
310
+ setBootstrapError(error?.message || "Could not load this CMS site");
311
+ setBootstrapLoading(false);
312
+ });
313
+ return () => { cancelled = true; };
314
+ }, [projectSlug, initialPageId, initialPanel, bootstrapNonce, setSiteId]);
315
+
316
+ useEffect(() => {
317
+ if (rail !== "media" || mediaItems.length) return;
318
+ getAssets().then(setMediaItems).catch((error) => toast(error?.message || "Could not load assets", "error"));
319
+ }, [rail, mediaItems.length, toast]);
320
+
321
+ useEffect(() => {
322
+ if (rail !== "templates" || templateCards.length) return;
323
+ getTemplates().then((items) => setTemplateCards(items.length ? items : FALLBACK_TEMPLATE_CARDS)).catch(() => setTemplateCards(FALLBACK_TEMPLATE_CARDS));
324
+ }, [rail, templateCards.length]);
325
+
326
+ useEffect(() => {
327
+ if ((rail !== "crm" && tab !== "crm") || contacts.length) return;
328
+ getContacts().then(setContacts).catch((error) => toast(error?.message || "Could not load contacts", "error"));
329
+ }, [rail, tab, contacts.length, toast]);
330
+
331
+ useEffect(() => {
332
+ if (!pageSaveReadyRef.current) {
333
+ pageSaveReadyRef.current = true;
334
+ return;
335
+ }
336
+ if (bootstrapLoading || !page?.id || String(page.id).startsWith("temp-") || modal === "page") return;
337
+ if (pageSaveTimerRef.current) clearTimeout(pageSaveTimerRef.current);
338
+ pageSaveTimerRef.current = setTimeout(() => {
339
+ savePageMeta(page).catch((error) => toast(error?.message || "Page details could not be saved", "error"));
340
+ }, 700);
341
+ return () => { if (pageSaveTimerRef.current) clearTimeout(pageSaveTimerRef.current); };
342
+ }, [page?.title, page?.slug, page?.type, page?.metaTitle, page?.metaDescription, page?.id, bootstrapLoading, modal, toast]);
343
+
344
+ const updatePages = useCallback((fn: (pages: PageData[]) => PageData[]) => {
345
+ setSites((all) => all.map((s) => (s.id === siteId ? { ...s, pages: fn(s.pages) } : s)));
346
+ }, [siteId]);
347
+ const mutateSections = (fn: (items: Section[]) => Section[]) => {
348
+ if (!page) return;
349
+ setHistory((h) => [...h.slice(-19), page.sections.map((s) => ({ ...s, fields: { ...s.fields } }))]);
350
+ setFuture([]);
351
+ updatePages((pages) => pages.map((p) => (p.id === page.id ? { ...p, sections: fn(p.sections) } : p)));
352
+ setDirty(true);
353
+ };
354
+ const updateField = (key: string, value: any) => mutateSections(items => items.map(s => s.id === selected.id ? { ...s, fields: { ...s.fields, [key]: value } } : s));
355
+ const updateCss = (key: string, value: any) => { mutateSections(items => items.map(s => s.id === selected.id ? { ...s, css: { ...(s.css || {}), [key]: value } } : s)); postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.STYLE, section_id: selected.id, css: { [key]: value } }); };
356
+ const mutateBlocks = (fn: (items: BlockData[]) => BlockData[]) => {
357
+ if (!selected) return;
358
+ mutateSections((items) => items.map((section) => section.id === selected.id ? { ...section, blocks: fn(section.blocks || []) } : section));
359
+ };
360
+ const chooseBlock = (id: string) => { setSelectedBlockId(id); setRail("blocks"); setTab("content"); };
361
+ const updateBlockField = (key: string, value: any) => mutateBlocks((items) => items.map((block) => block.id === selectedBlockId ? { ...block, data: { ...block.data, [key]: value } } : block));
362
+ const addBlock = async () => {
363
+ if (!selected?.id) return;
364
+ setSaving(true);
365
+ try {
366
+ const block = await createCmsEditorBlock(selected.id, "text", { text: "New block" }, (selected.blocks?.length + 1) * 10);
367
+ mutateBlocks((items) => [...items, block]);
368
+ setSelectedBlockId(block.id);
369
+ setRail("blocks");
370
+ setDirty(false);
371
+ toast("Block created");
372
+ } catch (error: any) { toast(error?.message || "Block could not be created", "error"); }
373
+ finally { setSaving(false); }
374
+ };
375
+
376
+ const save = useCallback(() => {
377
+ if (!dirty) return;
378
+ setSaving(true);
379
+ const operation = tab === "theme"
380
+ ? saveThemeVars(projectSlug, theme)
381
+ : selectedBlock
382
+ ? saveCmsEditorBlock(selectedBlock)
383
+ : selected?.id
384
+ ? Promise.all([saveIamSection(selected.id, selected.fields, selected.css), renameSection(selected.id, selected.name)])
385
+ : Promise.reject(new Error("Select a section or block to save"));
386
+ operation
387
+ .then(() => { setDirty(false); toast(tab === "theme" ? "Theme saved" : selectedBlock ? "Block saved" : "Section saved"); })
388
+ .catch((error) => toast(error?.message || "Save failed", "error"))
389
+ .finally(() => setSaving(false));
390
+ }, [dirty, selected, selectedBlock, tab, projectSlug, theme, toast]);
391
+ const undo = useCallback(() => { if (!page) return; const prev = history.at(-1); if (!prev) return; setFuture(f => [page.sections, ...f]); setHistory(h => h.slice(0, -1)); updatePages(ps => ps.map(p => p.id === page.id ? { ...p, sections: prev } : p)); toast("Change undone", "info"); }, [history, page, updatePages, toast]);
392
+ const redo = useCallback(() => { if (!page) return; const next = future[0]; if (!next) return; setHistory(h => [...h, page.sections]); setFuture(f => f.slice(1)); updatePages(ps => ps.map(p => p.id === page.id ? { ...p, sections: next } : p)); toast("Change restored", "info"); }, [future, page, updatePages, toast]);
393
+
394
+ const chooseSection = (id: string) => { if (dirty && id !== selectedId && !window.confirm("You have unsaved changes. Discard and continue?")) return; setSelectedId(id); setSelectedBlockId(""); setDirty(false); setRail("sections"); setTab("content"); postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.HIGHLIGHT, section_id: id }); };
395
+
396
+ const selectAnnotatedResource = useCallback((id: string) => {
397
+ if (!page) return;
398
+ const resourceId = id.startsWith("page:") ? id.slice(5) : id;
399
+ if (resourceId === page.id || id === `page:${page.id}`) {
400
+ setSelectedId(page.sections[0]?.id || "");
401
+ setSelectedBlockId("");
402
+ return;
403
+ }
404
+ const section = page.sections.find((row) => row.id === resourceId || row.id === id);
405
+ if (section) {
406
+ chooseSection(section.id);
407
+ setInspectorSheetOpen(true);
408
+ return;
409
+ }
410
+ for (const row of page.sections) {
411
+ const block = (row.blocks || []).find((item) => item.id === resourceId || item.id === id);
412
+ if (block) {
413
+ setSelectedId(row.id);
414
+ chooseBlock(block.id);
415
+ setInspectorSheetOpen(true);
416
+ postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.HIGHLIGHT, section_id: row.id });
417
+ return;
418
+ }
419
+ }
420
+ }, [page, chooseBlock]);
421
+
422
+ const onAnnotateSubmit = useCallback(async (prompt: string, annotation: AnnotationSelection) => {
423
+ selectAnnotatedResource(annotation.id);
424
+ setAgentSamOpen(true);
425
+ setInspectorSheetOpen(false);
426
+ setAnnotatePrompt(prompt);
427
+ toast(`Annotated “${annotation.label || annotation.tag}”`, "info");
428
+ window.dispatchEvent(new CustomEvent("agentsam:cms-annotate", { detail: { prompt, annotation } }));
429
+ }, [selectAnnotatedResource, setAgentSamOpen, toast]);
430
+ const choosePage = (id: string) => { if (dirty && !window.confirm("You have unsaved changes. Discard and continue?")) return; const p = site.pages.find(x => x.id === id); if (!p) return; setPageId(id); setSelectedId(p.sections[0]?.id || ""); setSelectedBlockId(""); setDirty(false); setTab("content"); toast(`Opened ${p.title}`, "info"); };
431
+ const chooseSite = (id: string) => { const s = sites.find(x => x.id === id); if (!s) return; setSiteSwitcher(false); if (onSiteChange) { onSiteChange(id); toast(`Opening ${s.name}`, "info"); return; } setSiteId(id); if (s.pages[0]) { setPageId(s.pages[0].id); setSelectedId(s.pages[0].sections[0]?.id || ""); setSelectedBlockId(""); } toast(`Switched to ${s.name}`, "info"); };
432
+
433
+ useEffect(() => { const t = dirty ? setTimeout(() => { save(); toast("Auto-save started", "info"); }, 30000) : undefined; return () => { if (t !== undefined) clearTimeout(t); }; }, [dirty, selected?.fields, save, toast]);
434
+ useEffect(() => { const before = (e: BeforeUnloadEvent) => { if (dirty) { e.preventDefault(); e.returnValue = ""; } }; window.addEventListener("beforeunload", before); return () => window.removeEventListener("beforeunload", before); }, [dirty]);
435
+ useEffect(() => { const receive = (e: MessageEvent) => {
436
+ const m = normalizeCmsEditorPreviewMessage(e.data);
437
+ if (!m) return;
438
+ const target = cmsEditorSelectionFromPreview(m, page?.id || null);
439
+ if (target?.sectionId) chooseSection(target.sectionId);
440
+ if (m.type === CMS_EDITOR_PREVIEW_TYPES.READY && selectedId) {
441
+ postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.HIGHLIGHT, section_id: selectedId });
442
+ }
443
+ }; window.addEventListener("message", receive); return () => window.removeEventListener("message", receive); });
444
+ useEffect(() => { const down = (e: KeyboardEvent) => {
445
+ const cmd = e.metaKey || e.ctrlKey; if (cmd && e.key.toLowerCase() === "s") { e.preventDefault(); save(); }
446
+ if (cmd && !e.shiftKey && e.key.toLowerCase() === "z") { e.preventDefault(); undo(); }
447
+ if (cmd && e.shiftKey && e.key.toLowerCase() === "z") { e.preventDefault(); redo(); }
448
+ if (cmd && e.key.toLowerCase() === "p" && !e.shiftKey) { e.preventDefault(); setPreview(v => !v); }
449
+ if (cmd && e.key.toLowerCase() === "k") { e.preventDefault(); setModal("palette"); }
450
+ if (cmd && e.key === "/") { e.preventDefault(); setModal("shortcuts"); }
451
+ if (cmd && e.shiftKey && e.key === ".") { e.preventDefault(); setSidebarCollapsed(v => !v); }
452
+ if (cmd && e.shiftKey && e.key === ",") { e.preventDefault(); setInspectorCollapsed(v => !v); }
453
+ if (cmd && /^[1-7]$/.test(e.key)) { e.preventDefault(); setRail(railItems[Number(e.key)-1].id); }
454
+ if (e.key === "Escape") {
455
+ if (annotateSelecting) { setAnnotateSelecting(false); return; }
456
+ if (agentSamOpen) { setAgentSamOpen(false); return; }
457
+ if (inspectorSheetOpen) { setInspectorSheetOpen(false); return; }
458
+ if (preview) setPreview(false);
459
+ else if (modal) setModal(null);
460
+ else { setSelectedId(""); postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.DESELECT }); }
461
+ }
462
+ }; window.addEventListener("keydown", down); return () => window.removeEventListener("keydown", down); }, [annotateSelecting, agentSamOpen, inspectorSheetOpen, modal, preview, save, undo, redo, setSidebarCollapsed, setInspectorCollapsed, setAgentSamOpen]);
463
+
464
+ const frameHtml = useMemo(() => {
465
+ if (!page) return "<!doctype html><html><body></body></html>";
466
+ const visible = page.sections.filter(s => s.visible);
467
+ const content = visible.map((s, i) => {
468
+ const f = s.fields || {};
469
+ const typeLower = String(s.type || "").toLowerCase();
470
+ const nameLower = String(s.name || "").toLowerCase();
471
+ const isNav = typeLower === "navigation" || typeLower === "header" || typeLower === "nav" || nameLower.includes("header") || nameLower.includes("nav");
472
+ const isFooter = typeLower === "footer" || nameLower.includes("footer");
473
+ const isHero = typeLower === "hero" || nameLower.includes("hero");
474
+ const img = f.hero_image_url || f.image_url;
475
+ const sectionId = escapeCmsText(s.id);
476
+ if (isNav) {
477
+ const links = Array.isArray(f.nav_links) ? f.nav_links : Array.isArray(f.links) ? f.links : [];
478
+ const brand = f.brand_name || f.title || f.brand || site?.name || "Agent Sam";
479
+ const cta = f.nav_cta_label || f.cta_text || f.cta_label || "Get Started";
480
+ return `<section data-cms-id="${sectionId}" data-agentsam-resource="${sectionId}" class="nav"><b>${escapeCmsText(brand)}</b><nav>${links.map((x: any) => {
481
+ if (x && typeof x === "object") {
482
+ const lbl = escapeCmsText(x.label || x.title || x.text || x.name || "Link");
483
+ const href = escapeCmsText(x.href || x.url || x.path || "#");
484
+ return `<a href="${href}">${lbl}</a>`;
485
+ }
486
+ return `<span>${escapeCmsText(x)}</span>`;
487
+ }).join("")}</nav><button>${escapeCmsText(cta)}</button></section>`;
488
+ }
489
+ if (isHero) {
490
+ const rawImg = typeof f.image === "string" ? f.image : (f.image && typeof f.image === "object" ? String((f.image as any).url || (f.image as any).src || "") : img || "");
491
+ const imgUrl = safeCmsAssetUrl(rawImg);
492
+ const cta = f.primaryCta && typeof f.primaryCta === "object" ? f.primaryCta as any : null;
493
+ const ctaLabel = cta ? String(cta.label || cta.text || "") : String(f.cta_text || f.cta_label || f.hero_primary_cta || f.button_label || "");
494
+ const heading = String(f.headline || f.heading || f.hero_title || f.title || s.name || "Hero");
495
+ const body = String(f.subline || f.subhead || f.body || f.hero_body || f.description || "");
496
+ const eyebrow = String(f.eyebrow || f.section_label || "");
497
+ const bg = safeCmsCssValue(f.bg_color || f.background || "") || "#0d0d12";
498
+ return `<section data-cms-id="${sectionId}" data-agentsam-resource="${sectionId}" class="hero" style="background-color:${bg};background-image:linear-gradient(90deg,rgba(7,7,10,.8),rgba(7,7,10,.15))${imgUrl ? `,url('${imgUrl}')` : ''}"><div><small>${escapeCmsText(eyebrow)}</small><h1>${escapeCmsText(heading)}</h1><p>${escapeCmsText(body)}</p>${ctaLabel ? `<button>${escapeCmsText(ctaLabel)}</button>` : ""}${f.hero_secondary_cta ? `<a>${escapeCmsText(f.hero_secondary_cta)} →</a>` : ""}</div></section>`;
499
+ }
500
+ if (isFooter) {
501
+ const links = Array.isArray(f.footer_links) ? f.footer_links : Array.isArray(f.links) ? f.links : [];
502
+ const brand = f.brand_name || site?.name || "Agent Sam";
503
+ const copyright = f.copyright || f.copyright_text || `© ${new Date().getFullYear()} ${brand}`;
504
+ const blockCopy = (s.blocks || [])
505
+ .filter((b) => b.visible !== false)
506
+ .map((b) => {
507
+ const blockId = escapeCmsText(b.id);
508
+ const text = escapeCmsText(String(b.data?.text || b.data?.title || b.type || ""));
509
+ return text ? `<p class="footer-block" data-agentsam-resource="${blockId}">${text}</p>` : "";
510
+ })
511
+ .filter(Boolean)
512
+ .join("");
513
+ return `<section data-cms-id="${sectionId}" data-agentsam-resource="${sectionId}" class="footer"><b>${escapeCmsText(brand)}</b>${blockCopy}<p>${escapeCmsText(copyright)}</p><span>${links.map((x: any) => {
514
+ if (x && typeof x === "object") {
515
+ const lbl = escapeCmsText(x.label || x.title || x.text || x.name || "Link");
516
+ const href = escapeCmsText(x.href || x.url || x.path || "#");
517
+ return `<a href="${href}">${lbl}</a>`;
518
+ }
519
+ return escapeCmsText(x);
520
+ }).join(" · ")}</span></section>`;
521
+ }
522
+ if (typeLower.includes("product") || nameLower.includes("product")) {
523
+ const cards = (s.blocks || []).filter((b) => b.visible !== false);
524
+ const title = String(f.title || s.name || "Products");
525
+ const grid = cards.length
526
+ ? cards.map((b) => `<article data-agentsam-resource="${escapeCmsText(b.id)}"><div class="thumb"></div><b>${escapeCmsText(String(b.data?.title || "Product"))}</b><span>${escapeCmsText(String(b.data?.price || ""))}</span></article>`).join("")
527
+ : `<article><div class="thumb"></div><b>Product</b><span>$19.99</span></article>`.repeat(4);
528
+ return `<section data-cms-id="${sectionId}" data-agentsam-resource="${sectionId}" class="products"><h2>${escapeCmsText(title)}</h2><div class="product-grid">${grid}</div></section>`;
529
+ }
530
+ const title = String(f.title || f.heading || f.headline || f.quote || s.name || "");
531
+ const description = String(f.description || f.body || f.subline || f.author_name || "");
532
+ const label = String(f.section_label || s.type || "");
533
+ const cta = String(f.button_label || f.cta_text || "");
534
+ const email = f.email ? `<p><a href="mailto:${escapeCmsText(f.email)}" style="color:var(--brand-primary);">${escapeCmsText(f.email)}</a></p>` : "";
535
+ return `<section data-cms-id="${sectionId}" data-agentsam-resource="${sectionId}" class="content s${i}" style="background:${safeCmsCssValue(s.color) || 'transparent'}"><small>${escapeCmsText(label)}</small><h2>${escapeCmsText(title)}</h2><p>${escapeCmsText(description)}</p>${email}${cta ? `<button>${escapeCmsText(cta)}</button>` : ""}</section>`;
536
+ }).join("");
537
+ const vars = Object.entries(theme)
538
+ .filter(([key]) => /^--[a-z0-9-]+$/i.test(key))
539
+ .map(([key, value]) => `${key}:${safeCmsCssValue(value)}`)
540
+ .filter((entry) => !entry.endsWith(':'))
541
+ .join(";");
542
+ return `<!doctype html><html><head><meta charset="utf-8"><style>:root{${vars}}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font-family:var(--font-body),Arial;background:#f5f2ea;color:#101014}section{position:relative;transition:.18s;cursor:default}.nav{height:68px;display:flex;align-items:center;padding:0 clamp(24px,6vw,84px);gap:30px;background:#0c0c10;color:white}.nav b{font-size:18px;margin-right:auto}.nav nav{display:flex;gap:22px;font-size:12px;color:#aaa}.nav nav a{color:#aaa;text-decoration:none;transition:color .15s}.nav nav a:hover{color:#fff}.nav button,.hero button,.content button{border:0;border-radius:999px;padding:11px 18px;background:var(--brand-primary);color:white;cursor:pointer}.hero{min-height:520px;background-size:cover;background-position:center;display:flex;align-items:end;padding:clamp(48px,9vw,100px);color:white;background-color:#0d0d12}.hero>div{max-width:760px}.hero small,.content small{text-transform:uppercase;letter-spacing:.18em;font-size:11px;font-weight:700;color:var(--brand-primary)}.hero h1{font-size:clamp(40px,6vw,84px);line-height:1.02;letter-spacing:-.045em;margin:16px 0}.hero p{font-size:18px;max-width:580px;line-height:1.6;color:#d8d8de}.hero a{margin-left:18px;font-size:13px;color:#fff}.content{min-height:300px;padding:clamp(48px,8vw,90px);display:flex;flex-direction:column;justify-content:center}.content h2{font-size:clamp(28px,4.5vw,56px);line-height:1.05;margin:16px 0;max-width:880px;letter-spacing:-.03em}.content p{max-width:660px;line-height:1.7;color:#444}.products{padding:clamp(48px,8vw,90px)}.products h2{font-size:clamp(28px,4vw,48px);margin:0 0 28px;letter-spacing:-.03em}.product-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.product-grid article{background:#fff;border:1px solid rgba(43,39,31,.1);border-radius:14px;padding:14px}.product-grid .thumb{height:140px;border-radius:10px;background:linear-gradient(135deg,#EFEBDD,#E8E2D2);margin-bottom:12px}.product-grid b{display:block;font-size:15px}.product-grid span{color:#73737f;font-size:13px}.footer{min-height:180px;padding:50px clamp(24px,6vw,84px);background:#09090b;color:white;display:grid;gap:16px;align-content:center}.footer-block{font-size:22px;font-weight:600;margin:0}.footer a{color:#aaa;text-decoration:none}.footer a:hover{color:#fff}.cms-highlight{outline:3px solid #4d8dff!important;outline-offset:-3px}.cms-highlight:after{content:attr(data-cms-name);position:absolute;top:5px;left:5px;background:#3b82f6;color:#fff;padding:4px 7px;border-radius:4px;font:11px Arial;z-index:10}</style></head><body>${content}<script>window.parent.postMessage({type:'cms:ready'},'*');document.querySelectorAll('[data-cms-id]').forEach(el=>{el.dataset.cmsName=el.querySelector('h1,h2,b')?.textContent || 'Section';el.addEventListener('click',e=>{e.stopPropagation();window.parent.postMessage({type:'cms:section-click',section_id:el.dataset.cmsId},'*')})});addEventListener('message',e=>{const m=e.data;document.querySelectorAll('[data-cms-id]').forEach(x=>x.classList.remove('cms-highlight'));if(m.type==='cms:highlight'){const x=document.querySelector('[data-cms-id="'+m.sectionId+'"]');x&&x.classList.add('cms-highlight')}if(m.type==='cms:scroll-to'){document.querySelector('[data-cms-id="'+m.sectionId+'"]')?.scrollIntoView({behavior:'smooth'})}if(m.type==='cms:theme-vars'){Object.entries(m.vars||{}).forEach(([k,v])=>document.documentElement.style.setProperty(k,v))}if(m.type==='cms:style'){const x=document.querySelector('[data-cms-id="'+m.sectionId+'"]');if(x)Object.assign(x.style,(m.css&&typeof m.css==='object')?m.css:{})}});addEventListener('scroll',()=>window.parent.postMessage({type:'cms:scroll',scrollY:scrollY},'*'))</script></body></html>`;
543
+ }, [page, theme, site?.name]);
544
+
545
+ const filteredPages = page && site ? site.pages.filter(p => p.title.toLowerCase().includes(search.toLowerCase())) : [];
546
+ const filteredSections = page ? page.sections.filter(s => s.name.toLowerCase().includes(search.toLowerCase())) : [];
547
+ const pageWordCount = page ? page.sections.reduce((total, section) => {
548
+ const sectionText = Object.values(section.fields || {}).filter((value) => typeof value === "string").join(" ");
549
+ const blockText = (section.blocks || []).flatMap((block) => Object.values(block.data || {}).filter((value) => typeof value === "string")).join(" ");
550
+ const words = `${sectionText} ${blockText}`.trim().split(/\s+/).filter(Boolean).length;
551
+ return total + words;
552
+ }, 0) : 0;
553
+
554
+
555
+ const createPage = () => {
556
+ if (saving) return;
557
+ const title = String(newPage.title || "").trim() || "Untitled Page";
558
+ const slug = String(newPage.slug || "").trim() || title;
559
+ setSaving(true);
560
+ createIamPage(projectSlug, { title, slug, type: newPage.type })
561
+ .then((created) => {
562
+ updatePages((items) => [...items, created as PageData]);
563
+ setPageId(created.id);
564
+ setSelectedId("");
565
+ setModal(null);
566
+ setNewPage({ type: "Interior", title: "", slug: "", parent: "None" });
567
+ toast("New page created");
568
+ })
569
+ .catch((error) => {
570
+ const code = String(error?.message || error?.payload?.error || "");
571
+ if (code === "route_exists" || code.includes("route_exists")) {
572
+ toast("That slug is already in use — pick another", "error");
573
+ } else if (code === "slug_required" || code.includes("slug_required")) {
574
+ toast("Add a page title or slug before creating", "error");
575
+ } else {
576
+ toast(code || "Could not create page", "error");
577
+ }
578
+ })
579
+ .finally(() => setSaving(false));
580
+ };
581
+ const addSection = (name: string) => {
582
+ const key = String(name || "").trim();
583
+ const schema = schemaCatalog.sections.find((row) =>
584
+ row.type === key
585
+ || row.type === key.toLowerCase()
586
+ || row.label === key
587
+ || String(row.key || "") === key
588
+ );
589
+ const sectionType = String(schema?.type || key).trim() || "custom";
590
+ const sectionLabel = String(schema?.label || key).trim() || sectionType;
591
+ const fields = schema?.defaults && typeof schema.defaults === "object"
592
+ ? { ...(schema.defaults as Record<string, unknown>) }
593
+ : { eyebrow: sectionLabel, title: `A new ${sectionLabel.toLowerCase()} section`, description: "Select this section to replace the content and shape its presentation." };
594
+ const tempId = `temp-${Date.now()}`;
595
+ const pending: Section = { id: tempId, name: sectionLabel, type: sectionType, zone: "BODY", visible: true, color: "#f1efe7", fields: fields as Section["fields"], blocks: [] };
596
+ mutateSections((items) => [...items, pending]);
597
+ setSelectedId(tempId);
598
+ setModal(null);
599
+ createIamSection(page.id, sectionType, fields as Record<string, any>, (page.sections.length + 1) * 10)
600
+ .then((created) => {
601
+ updatePages((pages) => pages.map((item) => item.id === page.id ? { ...item, sections: item.sections.map((section) => section.id === tempId ? created as Section : section) } : item));
602
+ setSelectedId(created.id);
603
+ setDirty(false);
604
+ toast(`${sectionLabel} added`);
605
+ })
606
+ .catch((error) => {
607
+ updatePages((pages) => pages.map((item) => item.id === page.id ? { ...item, sections: item.sections.filter((section) => section.id !== tempId) } : item));
608
+ toast(error?.message || "Could not add section", "error");
609
+ });
610
+ };
611
+
612
+ if (preview) {
613
+ return (
614
+ <div className="preview-mode" data-agentsam-resource={`page:${page?.id || "preview"}`}>
615
+ <iframe sandbox={iframeSandbox} srcDoc={frameHtml} title="Page preview"/>
616
+ <div className="preview-toolbar" data-annotation-control="">
617
+ <Button onClick={() => setPreview(false)}>Exit preview</Button>
618
+ <ViewportSwitcher viewport={viewport} setViewport={setViewport}/>
619
+ <Button icon="external" onClick={() => toast("Preview opened in a new tab", "info")}>Open</Button>
620
+ </div>
621
+ </div>
622
+ );
623
+ }
624
+
625
+
626
+ const applyTemplate = async (template: TemplateCard) => {
627
+ if (!template?.id) {
628
+ toast("Template id is missing", "error");
629
+ return;
630
+ }
631
+ setSaving(true);
632
+ try {
633
+ // Client fallback cards are examples only until D1 catalog rows exist.
634
+ if (String(template.id).startsWith("tpl_baseline_")) {
635
+ const slugBase = String(template.name || "template").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "template";
636
+ const created = await createIamPage(projectSlug, {
637
+ title: template.name || "Untitled Page",
638
+ slug: `/${slugBase}`,
639
+ type: String(template.type || "").toLowerCase().includes("section") ? "Landing" : "Landing",
640
+ });
641
+ updatePages((items) => [...items, created as PageData]);
642
+ setPageId(created.id);
643
+ setSelectedId("");
644
+ setModal(null);
645
+ setRail("pages");
646
+ toast(`Started "${template.name}" as a draft page (baseline example)`);
647
+ return;
648
+ }
649
+ const result = await applyTemplateApi(template.id, { pageId: page?.id, projectSlug }) as any;
650
+ if (result.mode === "page" && result.page?.id) {
651
+ const created = mapCmsEditorPage(result.page);
652
+ updatePages((items) => [...items, created as PageData]);
653
+ setPageId(created.id);
654
+ setSelectedId("");
655
+ setModal(null);
656
+ toast(`Created page from ${template.name}`);
657
+ return;
658
+ }
659
+ if (result.mode === "section" && result.section) {
660
+ const created = mapCmsEditorSection(result.section);
661
+ if (!page?.id) { toast("Open or create a page before applying a section template", "error"); return; }
662
+ updatePages((pages) => pages.map((item) => item.id === page.id ? { ...item, sections: [...item.sections, created as Section] } : item));
663
+ setSelectedId(created.id);
664
+ setRail("sections");
665
+ setModal(null);
666
+ toast(`Added ${created.name || template.name}`);
667
+ return;
668
+ }
669
+ toast(result.error || "Template could not be applied", "error");
670
+
671
+ } catch (error: any) {
672
+ toast(error?.message || "Template could not be applied", "error");
673
+ } finally {
674
+ setSaving(false);
675
+ }
676
+ };
677
+
678
+
679
+ if (bootstrapLoading) {
680
+ return (
681
+ <main className="cms-shell">
682
+ <div className="canvas-stage" style={{ display: "grid", placeItems: "center", minHeight: "100vh", background: "#F9F7F2", color: "#1a1a1a" }}>
683
+ <div className="canvas-loading"><div className="skeleton-topbar"/><div className="skeleton-hero"/><div className="skeleton-block"/></div>
684
+ </div>
685
+ </main>
686
+ );
687
+ }
688
+
689
+ if (bootstrapError || !site) {
690
+ return (
691
+ <main className="cms-shell">
692
+ <div className="canvas-stage" style={{ display: "grid", placeItems: "center", minHeight: "100vh", background: "#F9F7F2", color: "#1a1a1a" }}>
693
+ <div className="canvas-error">
694
+ <h3>Could not load site</h3>
695
+ <p>{bootstrapError || "CMS site context is missing for this workspace."}</p>
696
+ <Button kind="accent" onClick={() => setBootstrapNonce((value) => value + 1)}>Retry</Button>
697
+ </div>
698
+ </div>
699
+ </main>
700
+ );
701
+ }
702
+
703
+ if (!page) {
704
+ return (
705
+ <main className="cms-shell">
706
+ <header className="topbar">
707
+ <div className="topbar-left">
708
+ <button
709
+ className="button ghost hub-exit"
710
+ onClick={() => {
711
+ const target = basePath || "/cms";
712
+ if (onNavigate) {
713
+ onNavigate(target);
714
+ } else {
715
+ window.parent.postMessage({ type: "iam-studio-cms-navigate", path: target }, window.location.origin);
716
+ }
717
+ }}
718
+ title="Back to CMS overview"
719
+ ><Icon name="collapse" size={14}/> Overview</button>
720
+ <div className="site-trigger-wrap">
721
+ <button className="site-trigger" onClick={() => setSiteSwitcher((v) => !v)}>
722
+ <span className="site-avatar" style={{ background: site.color }}>{site.initials}</span>
723
+ <span><b>{site.name}</b><small>{site.domain}</small></span>
724
+ <Icon name="down" size={13}/>
725
+ </button>
726
+ </div>
727
+ </div>
728
+ <div className="breadcrumb"><span>{site.name}</span><Icon name="chevron" size={11}/><b>No pages yet</b></div>
729
+ </header>
730
+ <div className="workspace">
731
+ <nav className="rail">
732
+ {railItems.map((item, i) => (
733
+ <button key={item.id} className={rail === item.id ? "active" : ""} onClick={() => { setRail(item.id); setSidebarCollapsed(false); }} data-tip={`${item.label} ⌘${i + 1}`}>
734
+ <Icon name={item.icon}/>
735
+ </button>
736
+ ))}
737
+ </nav>
738
+ <aside className={`sidebar ${sidebarCollapsed ? "collapsed" : ""}`}>
739
+ <div className="sidebar-scroll">
740
+ {rail === "templates" ? (
741
+ <TemplatesSidebar items={templateCards} schemas={schemaCatalog} preview={(tpl: TemplateCard) => setModal(`template:${tpl.id}`)} apply={applyTemplate} addSection={(type: string) => addSection(type)}/>
742
+ ) : (
743
+ <div className="sidebar-section">
744
+ <EmptyState icon="file" title="Create your first page" copy="This site has no CMS pages yet. Create a blank page or apply a template." action="New page" onAction={() => setModal("page")}/>
745
+ <Button kind="accent" className="wide-action" onClick={() => setRail("templates")}>Browse templates</Button>
746
+ </div>
747
+ )}
748
+ </div>
749
+ </aside>
750
+ <section className="canvas-area">
751
+ <div className="canvas-stage" style={{ display: "grid", placeItems: "center" }}>
752
+ <div className="canvas-error">
753
+ <h3>No pages on {site.name}</h3>
754
+ <p>Start from a blank page or apply a baseline template. Examples beat an empty editor.</p>
755
+ <div style={{ display: "flex", gap: 8, justifyContent: "center", marginTop: 12 }}>
756
+ <Button kind="accent" onClick={() => setModal("page")}>Create page</Button>
757
+ <Button onClick={() => setRail("templates")}>Browse templates</Button>
758
+ </div>
759
+ </div>
760
+ </div>
761
+ </section>
762
+ </div>
763
+ <ToastStack toasts={toasts} dismiss={dismissToast}/>
764
+ {modal === "page" && <PageModal data={newPage} setData={setNewPage} close={() => setModal(null)} create={createPage} saving={saving}/>}
765
+ {modal?.startsWith("template:") && (
766
+ <TemplatePreview
767
+ template={templateCards.find((entry) => entry.id === modal.slice(9)) || { id: modal.slice(9), name: modal.slice(9) }}
768
+ close={() => setModal(null)}
769
+ apply={applyTemplate}
770
+ />
771
+ )}
772
+ </main>
773
+ );
774
+ }
775
+
776
+
777
+ return <main className={`cms-shell cms-shell--fnf${agentSamOpen ? " is-agentsam-open" : ""}${inspectorSheetOpen ? " is-inspector-open" : ""}`}>
778
+ <header className="fnf-topbar" data-annotation-control="">
779
+ <div className="fnf-topbar-left">
780
+ <button
781
+ className="fnf-topbar-ghost"
782
+ onClick={() => {
783
+ const target = basePath || "/cms";
784
+ if (onNavigate) onNavigate(target);
785
+ else window.parent.postMessage({ type: "iam-studio-cms-navigate", path: target }, window.location.origin);
786
+ }}
787
+ title="Back to CMS overview"
788
+ ><Icon name="collapse" size={14}/><span>Overview</span></button>
789
+ <div className="site-trigger-wrap">
790
+ <button className="site-trigger" onClick={() => setSiteSwitcher(v => !v)}>
791
+ <span className="site-avatar" style={{ background: site.color }}>{site.initials}</span>
792
+ <span><b>{site.name}</b><small>{site.domain}</small></span>
793
+ <Icon name="down" size={13}/>
794
+ </button>
795
+ {siteSwitcher && <SiteSwitcher sites={sites} active={site.id} choose={chooseSite} close={() => setSiteSwitcher(false)} newSite={() => { setSiteSwitcher(false); toast("Create sites from the CMS hub", "info"); }}/>}
796
+ </div>
797
+ </div>
798
+ <button className="fnf-search" type="button" onClick={() => setModal("palette")} aria-label="Search">
799
+ <Icon name="search" size={14}/>
800
+ <span>Search pages, sections…</span>
801
+ <kbd>⌘K</kbd>
802
+ </button>
803
+ <div className="fnf-topbar-actions">
804
+ <ViewportSwitcher viewport={viewport} setViewport={setViewport}/>
805
+ <button
806
+ type="button"
807
+ className={`fnf-topbar-btn${annotateSelecting ? " is-active" : ""}`}
808
+ data-annotation-control=""
809
+ onClick={() => setAnnotateSelecting((v) => !v)}
810
+ title="Annotate canvas"
811
+ >{annotateSelecting ? "Selecting…" : "Annotate"}</button>
812
+ <button type="button" className="fnf-topbar-icon" onClick={undo} disabled={!history.length} title="Undo ⌘Z"><Icon name="undo" size={14}/></button>
813
+ <button type="button" className="fnf-topbar-icon" onClick={redo} disabled={!future.length} title="Redo ⌘⇧Z"><Icon name="redo" size={14}/></button>
814
+ <button type="button" className="fnf-topbar-btn" onClick={() => setPreview(true)}>Preview</button>
815
+ <div className="publish-wrap">
816
+ <Button icon="publish" kind="accent" onClick={() => setPublishMenu(v => !v)}>Publish live <Icon name="down" size={11}/></Button>
817
+ {publishMenu && <PublishMenu action={(m: string) => { if (m === "Schedule") { setModal("schedule"); } else { setSaving(true); publishIamPage(page.id).then(() => toast("Page published")).catch((error) => toast(error?.message || "Publish failed", "error")).finally(() => setSaving(false)); } setPublishMenu(false); }}/>}
818
+ </div>
819
+ <button
820
+ type="button"
821
+ className={`fnf-topbar-icon${agentSamOpen ? " is-agentsam-active" : ""}`}
822
+ data-annotation-control=""
823
+ aria-label="AgentSam"
824
+ aria-expanded={agentSamOpen}
825
+ title="AgentSam Side Assistant"
826
+ onClick={() => setAgentSamOpen((v) => !v)}
827
+ >
828
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
829
+ </button>
830
+ </div>
831
+ <div className="fnf-mobile-top" data-annotation-control="">
832
+ <button type="button" className="fnf-topbar-ghost" onClick={() => { setSidebarCollapsed(false); setRail("sections"); }} aria-label="Library"><Icon name="menu" size={16}/></button>
833
+ <b>{page.title}</b>
834
+ <div className="fnf-mobile-top-actions">
835
+ <button type="button" className={`fnf-topbar-btn${inspectorSheetOpen ? " is-active" : ""}`} onClick={() => setInspectorSheetOpen((v) => !v)}>{inspectorSheetOpen ? "Preview" : "Edit"}</button>
836
+ <button
837
+ type="button"
838
+ className={`fnf-topbar-icon${agentSamOpen ? " is-agentsam-active" : ""}`}
839
+ aria-label="AgentSam"
840
+ aria-expanded={agentSamOpen}
841
+ onClick={() => setAgentSamOpen((v) => !v)}
842
+ >
843
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
844
+ </button>
845
+ </div>
846
+ </div>
847
+ </header>
848
+
849
+ <div className="fnf-workspace">
850
+ <nav className="rail fnf-rail" data-annotation-control="">
851
+ {railItems.map((item, i) => <button key={item.id} className={rail === item.id ? "active" : ""} onClick={() => { setRail(item.id); setSidebarCollapsed(false); }} data-tip={`${item.label} ⌘${i + 1}`}><Icon name={item.icon}/></button>)}
852
+ <button className="rail-collapse" onClick={() => setSidebarCollapsed(v => !v)} data-tip={sidebarCollapsed ? "Expand library" : "Collapse library"}><Icon name="collapse"/></button>
853
+ </nav>
854
+
855
+ <aside className={`sidebar fnf-library ${sidebarCollapsed ? "collapsed" : ""}`} data-annotation-control="">
856
+ <SidebarHeader rail={rail} search={search} setSearch={setSearch} action={() => rail === "pages" ? setModal("page") : rail === "sections" ? setModal("add-section") : rail === "blocks" ? addBlock() : rail === "media" ? setModal("upload") : toast("No create action is available for this panel", "info")}/>
857
+ <div className="sidebar-scroll">
858
+ {rail === "pages" && <PagesSidebar pages={filteredPages} active={page.id} choose={choosePage} create={() => setModal("page")} toast={toast}/>}
859
+ {rail === "sections" && <SectionsSidebar sections={filteredSections} selected={selectedId} choose={chooseSection} mutate={mutateSections} add={() => setModal("add-section")} toast={toast} onReorder={(ordered: Section[]) => reorderSections(page.id, ordered).catch((error) => toast(error?.message || "Reorder failed", "error"))} onVisibility={(section: Section, visible: boolean) => setSectionVisibility(section.id, visible).catch((error) => toast(error?.message || "Visibility update failed", "error"))} onRename={(section: Section, name: string) => renameSection(section.id, name).catch((error) => toast(error?.message || "Rename failed", "error"))}/>}
860
+ {rail === "blocks" && <BlocksSidebar section={selected} selected={selectedBlockId} choose={chooseBlock} add={addBlock} onVisibility={(block: BlockData, visible: boolean) => { mutateBlocks((items) => items.map((item) => item.id === block.id ? { ...item, visible } : item)); setCmsEditorBlockVisibility(block.id, visible).catch((error) => toast(error?.message || "Block visibility update failed", "error")); }}/>}
861
+ {rail === "templates" && <TemplatesSidebar items={templateCards} schemas={schemaCatalog} preview={(tpl: TemplateCard) => { setModal(`template:${tpl.id}`); }} apply={applyTemplate} addSection={(type: string) => addSection(type)}/>}
862
+ {rail === "media" && <MediaSidebar view={mediaView} setView={setMediaView} filter={mediaFilter} setFilter={setMediaFilter} items={mediaItems} upload={() => setModal("upload")} toast={toast}/>}
863
+ {rail === "crm" && <CrmSidebar contacts={contacts} search={search} select={setContactId} exportCsv={() => exportContactsCsv(contacts)}/>}
864
+ {rail === "settings" && <SettingsSidebar site={site} toast={toast}/>}
865
+ </div>
866
+ </aside>
867
+
868
+ <div className="fnf-editor">
869
+ <aside className={`fnf-inspector ${inspectorCollapsed ? "collapsed" : ""}`} data-annotation-control="">
870
+ <div className="fnf-inspector-head">
871
+ <div>
872
+ <strong>{selectedBlock ? selectedBlock.type : selected?.name || "Section"}</strong>
873
+ <span className={`badge ${dirty ? "draft" : "active"}`}>{dirty ? "draft" : "saved"}</span>
874
+ </div>
875
+ <button type="button" className="fnf-topbar-icon" onClick={() => setInspectorCollapsed((v) => !v)} title="Collapse inspector"><Icon name="collapse" size={14}/></button>
876
+ </div>
877
+ {selected && (
878
+ <div className="selected-head">
879
+ <div>
880
+ <input
881
+ value={selectedBlock ? selectedBlock.type : selected.name}
882
+ onChange={(e) => selectedBlock ? undefined : mutateSections((items) => items.map((s) => s.id === selected.id ? { ...s, name: e.target.value } : s))}
883
+ readOnly={!!selectedBlock}
884
+ />
885
+ <span>{selectedBlock ? `Block · ${selected.name}` : selected.type}</span>
886
+ </div>
887
+ <Button disabled={!dirty} onClick={() => { setDirty(false); toast("Local changes reverted", "info"); }}>Revert</Button>
888
+ <Button kind="accent" disabled={!dirty || saving} onClick={save}>{saving ? <span className="spinner"/> : "Save draft"}</Button>
889
+ </div>
890
+ )}
891
+ <div className="inspector-tabs">{(["content", "style", "page", "theme", "crm"] as InspectorTab[]).map((t) => <button key={t} className={tab === t ? "active" : ""} onClick={() => setTab(t)}>{t}</button>)}</div>
892
+ <div className="inspector-scroll">
893
+ {tab === "content" && selectedBlock ? <BlockInspector block={selectedBlock} update={updateBlockField} toast={toast}/> : null}
894
+ {tab === "content" && !selectedBlock && selected ? <ContentInspector section={selected} schemas={schemaCatalog.sections} update={updateField} toast={toast}/> : null}
895
+ {tab === "style" && selected ? <StyleInspector section={selected} update={updateCss}/> : null}
896
+ {tab === "page" ? <PageInspector page={page} site={site} updatePages={updatePages} history={() => setModal("history")} toast={toast}/> : null}
897
+ {tab === "theme" ? <ThemeInspector vars={theme} update={(k: string, v: string) => { const next = { ...theme, [k]: v }; setTheme(next); postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.THEME_VARS, vars: next }); setDirty(true); if (themeSaveTimerRef.current) clearTimeout(themeSaveTimerRef.current); themeSaveTimerRef.current = setTimeout(() => saveThemeVars(projectSlug, next).then(() => { setDirty(false); toast("Theme saved", "info"); }).catch((error) => toast(error?.message || "Theme save failed", "error")), 500); }} toast={toast}/> : null}
898
+ {tab === "crm" ? <InspectorCrm contacts={contacts} viewAll={() => setRail("crm")} select={setContactId}/> : null}
899
+ {!selected && tab === "content" ? <EmptyState icon="layers" title="Select a section" copy="Choose a section from the library or canvas markers to edit fields."/> : null}
900
+ </div>
901
+ <p className="fnf-live-note">{dirty ? "Unsaved changes" : "Live editing connected."}</p>
902
+ </aside>
903
+
904
+ <section className="canvas-area fnf-canvas" ref={canvasScopeRef} data-agentsam-resource={`page:${page.id}`} aria-label={`${page.title} canvas`}>
905
+ <div className="fnf-preview-bar" data-annotation-control="">
906
+ <span>Preview — {page.slug || page.title}</span>
907
+ <div className="fnf-preview-bar-actions">
908
+ <button type="button" className="fnf-topbar-btn" onClick={() => setPreview(true)}>Open in tab</button>
909
+ </div>
910
+ </div>
911
+ <div className="canvas-stage">
912
+ {bootstrapLoading && <div className="canvas-loading"><div className="skeleton-topbar"/><div className="skeleton-hero"/><div className="skeleton-block"/></div>}
913
+ {bootstrapError && !bootstrapLoading && <div className="canvas-error"><h3>Could not load site</h3><p>{bootstrapError}</p><Button kind="accent" onClick={() => setBootstrapNonce((value) => value + 1)}>Retry</Button></div>}
914
+ <div className="section-markers">
915
+ {page.sections.map((s, i) => (
916
+ <button
917
+ key={s.id}
918
+ type="button"
919
+ data-agentsam-resource={s.id}
920
+ className={s.id === selectedId ? "active" : ""}
921
+ onClick={() => {
922
+ chooseSection(s.id);
923
+ postCmsEditorPreviewMessage(iframeRef.current?.contentWindow, { type: CMS_EDITOR_PREVIEW_TYPES.SCROLL_TO, section_id: s.id });
924
+ }}
925
+ >
926
+ <span>{i + 1}</span><b>{s.name}</b>
927
+ </button>
928
+ ))}
929
+ </div>
930
+ {responsive
931
+ ? <div className="responsive-three">{(["phone", "tablet", "desktop"] as Viewport[]).map((v) => <div key={v}><label><Icon name={v}/> {v}</label><iframe sandbox={iframeSandbox} srcDoc={frameHtml}/></div>)}</div>
932
+ : (
933
+ <div className={`device-frame ${viewport}`} data-agentsam-resource={selected?.id || `page:${page.id}`} style={{ transform: `scale(${zoom === "50%" ? .5 : zoom === "75%" ? .75 : 1})` }}>
934
+ <div className="loading-line"/>
935
+ <iframe ref={iframeRef} sandbox={iframeSandbox} title={`${page.title} preview`} srcDoc={frameHtml}/>
936
+ </div>
937
+ )}
938
+ </div>
939
+ <footer className="canvas-footer" data-annotation-control="">
940
+ <div><button className={`toggle ${responsive ? "on" : ""}`} onClick={() => setResponsive((v) => !v)}><span/></button><span>Responsive preview</span></div>
941
+ <div className="canvas-metrics"><span>{pageWordCount} words</span><span>{dirty ? "Unsaved changes" : "Saved state"}</span></div>
942
+ <div className="zoom"><button onClick={() => setZoom("50%")} className={zoom === "50%" ? "active" : ""}>50</button><button onClick={() => setZoom("75%")} className={zoom === "75%" ? "active" : ""}>75</button><button onClick={() => setZoom("100%")} className={zoom === "100%" ? "active" : ""}>100</button><button onClick={() => setZoom("Fit")} className={zoom === "Fit" ? "active" : ""}>Fit</button></div>
943
+ </footer>
944
+ <button className="mobile-fab" data-annotation-control="" onClick={() => setModal("add-section")}><Icon name="plus"/></button>
945
+ </section>
946
+ </div>
947
+
948
+ <AgentSamDrawer
949
+ open={agentSamOpen}
950
+ onClose={() => setAgentSamOpen(false)}
951
+ adapter={agent?.adapter}
952
+ principal={agent?.principal}
953
+ projectId={site.id || projectSlug}
954
+ conversationId={agent?.conversationId || `cms:${site.id || projectSlug}`}
955
+ route={page.slug}
956
+ pageId={page.id}
957
+ sectionId={selected?.id || null}
958
+ blockId={selectedBlock?.id || null}
959
+ selectionLabel={selectedBlock ? String(selectedBlock.data?.text || selectedBlock.data?.title || selectedBlock.type) : selected?.name || null}
960
+ pendingPrompt={annotatePrompt}
961
+ onPendingPromptConsumed={() => setAnnotatePrompt(null)}
962
+ />
963
+ </div>
964
+
965
+ <MiniAgentSam
966
+ selecting={annotateSelecting}
967
+ onSelectingChange={setAnnotateSelecting}
968
+ scope={() => canvasScopeRef.current}
969
+ onSubmit={onAnnotateSubmit}
970
+ />
971
+
972
+ <footer className="statusbar"><div><i/> {annotateSelecting ? "Annotate mode" : agentSamOpen ? "AgentSam open" : "Connected"}</div><span>{dirty ? "Unsaved changes" : saving ? "Saving…" : "Saved"}</span><span>{page.slug}</span><span>{page.sections.length} sections · {page.sections.reduce((total, section) => total + (section.blocks?.length || 0), 0)} blocks</span><button onClick={() => setModal("shortcuts")}>Shortcuts <kbd>⌘/</kbd></button></footer>
973
+ <nav className="mobile-tabs">{railItems.slice(0, 5).map((i) => <button key={i.id} className={rail === i.id ? "active" : ""} onClick={() => setRail(i.id)}><Icon name={i.icon}/><span>{i.label}</span></button>)}</nav>
974
+ <ToastStack toasts={toasts} dismiss={dismissToast}/>
975
+
976
+ {modal === "add-section" && (initialPanel === "imports" ? <ImportSectionModal close={() => setModal(null)} add={addSection}/> : <AddSectionModal close={() => setModal(null)} add={addSection} schemas={schemaCatalog.sections}/>)}
977
+ {modal === "page" && <PageModal data={newPage} setData={setNewPage} close={() => setModal(null)} create={createPage} saving={saving}/>}
978
+ {modal === "upload" && <UploadModal close={() => setModal(null)}/>}
979
+ {modal === "schedule" && <ScheduleModal close={() => setModal(null)}/>}
980
+ {modal === "shortcuts" && <ShortcutsModal close={() => setModal(null)}/>}
981
+ {modal === "palette" && <CommandPalette close={() => setModal(null)} pages={site.pages} sections={page.sections} action={(type: string, id?: string) => { setModal(null); if (type === "page" && id) choosePage(id); else if (type === "section" && id) chooseSection(id); else if (type === "publish") setPublishMenu(true); else if (type === "add") setModal("add-section"); else if (type === "media") setRail("media"); }}/>}
982
+ {modal === "history" && <HistoryModal close={() => setModal(null)} restore={() => { setModal(null); toast("Revision restored", "info"); }}/>}
983
+ {modal?.startsWith("template:") && <TemplatePreview template={templateCards.find((entry) => entry.id === modal.slice(9)) || { id: modal.slice(9), name: modal.slice(9) }} close={() => setModal(null)} apply={applyTemplate}/>}
984
+ {contactId !== null && <ContactDrawer contact={contacts[contactId]} close={() => setContactId(null)} toast={toast}/>}
985
+ </main>;
986
+ }
987
+
988
+ function ViewportSwitcher({ viewport, setViewport }: any) { return <div className="viewport-switcher">{(["phone","tablet","desktop"] as Viewport[]).map(v => <button key={v} className={viewport===v?"active":""} onClick={() => setViewport(v)} title={`${v} viewport`}><Icon name={v}/></button>)}</div>; }
989
+
990
+ function SiteSwitcher({ sites, active, choose, close, newSite }: any) { const [q,setQ]=useState(""); return <div className="popover site-switcher"><div className="popover-title"><b>Switch site</b><Button icon="close" onClick={close}/></div><Search value={q} onChange={setQ} placeholder="Search client sites"/><div>{sites.filter((s:Site)=>s.name.toLowerCase().includes(q.toLowerCase())).map((s:Site)=><button className="site-option" key={s.id} onClick={()=>choose(s.id)}><span className="site-avatar" style={{background:s.color}}>{s.initials}</span><span><b>{s.name}</b><small>{s.domain} · {s.edited}</small></span>{s.id===active&&<Icon name="check"/>}</button>)}</div><Button icon="plus" kind="secondary" onClick={newSite}>New site</Button></div>; }
991
+ function PublishMenu({ action }: any) { const items=[{name:"Publish Page",copy:"Publish the current page through the canonical lifecycle",icon:"publish"},{name:"Schedule",copy:"Scheduling is not connected yet",icon:"settings"}]; return <div className="popover publish-menu">{items.map((item)=><button key={item.name} onClick={()=>action(item.name)}><span><Icon name={item.icon}/></span><div><b>{item.name}</b><small>{item.copy}</small></div></button>)}</div>; }
992
+ function SidebarHeader({ rail, search, setSearch, action }: any) { return <header className="sidebar-head"><div><span className="eyebrow">Workspace</span><h2>{rail[0].toUpperCase()+rail.slice(1)}</h2></div><Button icon="plus" kind="secondary" onClick={action} title={`Add ${rail}`}/><Search value={search} onChange={setSearch} placeholder={`Search ${rail}`}/></header>; }
993
+
994
+ function PagesSidebar({ pages, active, choose, create, toast }: any) { return <div className="sidebar-section"><div className="group-title"><span>Top-level pages</span><b>{pages.filter((p:PageData)=>!p.parent).length}</b></div>{!pages.length?<EmptyState icon="file" title="No pages found" copy="Try another search or create a new page." action="New page" onAction={create}/>:pages.map((p:PageData)=><div className={`page-row ${active===p.id?"selected":""}`} key={p.id} onClick={()=>choose(p.id)}><i className={`status ${p.status}`}/><span><b>{p.title}</b><small>{p.slug}</small></span><button onClick={e=>{e.stopPropagation();toast("Page actions are not connected yet","info")}}><Icon name="more"/></button></div>)}<div className="group-title"><span>Nested</span><b>{pages.filter((p:PageData)=>p.parent).length}</b></div>{pages.filter((p:PageData)=>p.parent).map((p:PageData)=><div className="page-row nested" key={`nested-${p.id}`}><Icon name="chevron" size={12}/><span><b>{p.title}</b><small>{p.slug}</small></span></div>)}</div>; }
995
+ function SectionsSidebar({ sections, selected, choose, mutate, add, toast, onReorder, onVisibility, onRename }: any) { const [open,setOpen]=useState<Record<string,boolean>>({HEADER:true,BODY:true,FOOTER:true,TEMPLATE:true}); const drag=useRef<string|null>(null); return <div className="sidebar-section">{["HEADER","BODY","FOOTER","TEMPLATE"].map(zone=>{const items=sections.filter((s:Section)=>s.zone===zone);return <div key={zone} className="zone"><button className="group-title zone-title" onClick={()=>setOpen({...open,[zone]:!open[zone]})}><Icon name={open[zone]?"down":"chevron"} size={12}/><span>{zone}</span><b>{items.length}</b></button>{open[zone]&&items.map((s:Section)=><div key={s.id} draggable onDragStart={()=>drag.current=s.id} onDragOver={e=>e.preventDefault()} onDrop={()=>{const from=sections.findIndex((x:Section)=>x.id===drag.current),to=sections.findIndex((x:Section)=>x.id===s.id);if(from<0||to<0)return;const copy=[...sections];const [m]=copy.splice(from,1);copy.splice(to,0,m);mutate(()=>copy);onReorder(copy);toast("Section order updated","info")}} className={`section-row ${selected===s.id?"selected":""}`} onClick={()=>choose(s.id)}><span className="drag">⠿</span><i className="swatch" style={{background:s.color}}/><span onDoubleClick={e=>{e.stopPropagation();(e.currentTarget as HTMLElement).contentEditable="true";e.currentTarget.focus()}} onBlur={e=>{(e.currentTarget as HTMLElement).contentEditable="false";const name=(e.currentTarget.childNodes[0]?.textContent||s.name).trim();mutate((list:Section[])=>list.map(x=>x.id===s.id?{...x,name}:x));onRename(s,name)}}>{s.name}<small>{s.zone}</small></span><button onClick={e=>{e.stopPropagation();const visible=!s.visible;mutate((list:Section[])=>list.map(x=>x.id===s.id?{...x,visible}:x));onVisibility(s,visible)}}><Icon name={s.visible?"eye":"eyeoff"} size={14}/></button></div>)}</div>})}<Button icon="plus" kind="accent" className="wide-action" onClick={add}>Add section</Button></div>; }
996
+
997
+ function BlocksSidebar({ section, selected, choose, add, onVisibility }: any) {
998
+ const blocks: BlockData[] = section?.blocks || [];
999
+ return <div className="sidebar-section"><div className="group-title"><span>{section ? `${section.name} blocks` : "Blocks"}</span><b>{blocks.length}</b></div>{!section ? <EmptyState icon="puzzle" title="Select a section" copy="Choose a section to inspect its blocks."/> : !blocks.length ? <EmptyState icon="puzzle" title="No blocks yet" copy="This section has no canonical blocks." action="Add block" onAction={add}/> : blocks.map((block) => <button className={`page-row ${selected===block.id?"selected":""}`} key={block.id} onClick={() => choose(block.id)}><i className={`status ${block.visible?"live":"draft"}`}/><span><b>{block.type}</b><small>{block.id}</small></span><button onClick={(event) => { event.stopPropagation(); onVisibility(block, !block.visible); }} title={block.visible?"Hide block":"Show block"}><Icon name={block.visible?"eye":"eyeoff"}/></button></button>)}{section && <Button icon="plus" className="wide-action" onClick={add}>Add block</Button>}</div>;
1000
+ }
1001
+ function TemplatesSidebar({ items, schemas, preview, apply, addSection }: any) {
1002
+ const [tab, setTab] = useState<"Models" | "Pages" | "Sections">("Models");
1003
+ const [industry, setIndustry] = useState("All");
1004
+ const sectionSchemas = schemas?.sections || [];
1005
+ const blockSchemas = schemas?.blocks || [];
1006
+ const list = (items || []).filter((row: any) => {
1007
+ const type = String(row.type || "").toLowerCase();
1008
+ if (tab === "Pages" && type.includes("section")) return false;
1009
+ if (tab === "Sections" && type.includes("page")) return false;
1010
+ if (industry !== "All" && row.category && row.category !== industry) return false;
1011
+ return true;
1012
+ });
1013
+ return (
1014
+ <div className="sidebar-section">
1015
+ <div className="segmented">
1016
+ <button className={tab === "Models" ? "active" : ""} onClick={() => setTab("Models")}>Content models</button>
1017
+ <button className={tab === "Pages" ? "active" : ""} onClick={() => setTab("Pages")}>Full pages</button>
1018
+ <button className={tab === "Sections" ? "active" : ""} onClick={() => setTab("Sections")}>Sections</button>
1019
+ </div>
1020
+ {tab === "Models" ? (
1021
+ <>
1022
+ <div className="panel-note" style={{ margin: "8px 0 12px" }}>
1023
+ <Icon name="info"/>
1024
+ <p>Registry section/block schemas — one definition for editor fields, preview, and publish.</p>
1025
+ </div>
1026
+ <div className="group-title"><span>Sections</span><b>{sectionSchemas.length}</b></div>
1027
+ {!sectionSchemas.length ? (
1028
+ <EmptyState icon="layers" title="No schemas yet" copy="Bootstrap did not return registry schemas. Check /api/cms/bootstrap schemas payload."/>
1029
+ ) : sectionSchemas.map((schema: any) => (
1030
+ <article key={schema.key || schema.type} className="page-row" style={{ alignItems: "flex-start", cursor: "default" }}>
1031
+ <span>
1032
+ <b>{schema.label || schema.type}</b>
1033
+ <small>{schema.key || schema.type} · {Object.keys(schema.fields || {}).length} fields{(schema.allowedBlocks || []).length ? ` · blocks: ${(schema.allowedBlocks || []).join(", ")}` : ""}</small>
1034
+ </span>
1035
+ <Button kind="accent" onClick={() => addSection?.(schema.type || schema.label)}>Add</Button>
1036
+ </article>
1037
+ ))}
1038
+ <div className="group-title"><span>Blocks</span><b>{blockSchemas.length}</b></div>
1039
+ {blockSchemas.map((schema: any) => (
1040
+ <div className="page-row nested" key={schema.key || schema.type}>
1041
+ <span><b>{schema.label || schema.type}</b><small>{schema.key || schema.type}</small></span>
1042
+ </div>
1043
+ ))}
1044
+ </>
1045
+ ) : (
1046
+ <>
1047
+ <select value={industry} onChange={(e) => setIndustry(e.target.value)}>
1048
+ <option>All industries</option>
1049
+ {["Agency", "Restaurant", "Nonprofit", "Portfolio", "Ecommerce", "SaaS"].map((x) => <option key={x}>{x}</option>)}
1050
+ </select>
1051
+ <div className="template-grid">
1052
+ {list.map((tpl: any, i: number) => (
1053
+ <article key={tpl.id || tpl.name}>
1054
+ <Wireframe variant={i} />
1055
+ <b>{tpl.name}</b>
1056
+ <small>{tpl.category || ["Agency", "Portfolio", "Editorial"][i % 3]}{tpl.hasHtml ? " · HTML" : ""}</small>
1057
+ <div>
1058
+ <Button onClick={() => preview(tpl)}>Preview</Button>
1059
+ <Button kind="accent" onClick={() => apply(tpl)}>Apply</Button>
1060
+ </div>
1061
+ </article>
1062
+ ))}
1063
+ </div>
1064
+ </>
1065
+ )}
1066
+ </div>
1067
+ );
1068
+ }
1069
+
1070
+ function MediaSidebar({ view,setView,filter,setFilter,items,upload,toast }: any) { return <div className="sidebar-section"><button className="mini-dropzone" onClick={upload}><Icon name="upload"/><span><b>Upload media</b><small>Drop files or browse</small></span></button><div className="media-controls"><div className="filter-tabs">{["All","Image","Video","Document","Font"].map(x=><button className={filter===x?"active":""} onClick={()=>setFilter(x)} key={x}>{x}</button>)}</div><div><button className={view==="grid"?"active":""} onClick={()=>setView("grid")}><Icon name="grid"/></button><button className={view==="list"?"active":""} onClick={()=>setView("list")}><Icon name="layers"/></button></div></div><div className={`media-grid ${view}`}>{items.filter((x:any)=>filter==="All"||x.type===filter).map((x:any)=><article key={x.id}><div className="media-thumb" style={{background:x.color}}>{x.type==="Image"?<Icon name="image" size={24}/>:<Icon name="file" size={24}/>}<input type="checkbox"/><button onClick={()=>{const url=x.previewUrl||"";if(url){navigator.clipboard?.writeText(url);toast("Media URL copied")}else toast("This asset has no public URL","info")}}><Icon name="copy"/></button></div><span><b>{x.name}</b><small>{x.size}</small></span></article>)}</div><div className="storage"><div><span>Storage</span><b>Usage unavailable</b></div></div></div>; }
1071
+ function CrmSidebar({ contacts,search,select,exportCsv }: any) { const list=contacts.filter((x:any)=>(x.name+x.email).toLowerCase().includes(search.toLowerCase())); return <div className="sidebar-section"><div className="crm-stats"><div><b>{contacts.length}</b><span>Loaded contacts</span></div></div><div className="filter-row"><Button icon="filter">Source</Button><Button icon="publish" onClick={exportCsv}>Export CSV</Button></div>{!list.length ? <EmptyState icon="users" title="No contacts" copy="No matching contacts are available."/> : list.map((c:any)=><button className="contact-row" key={c.id} onClick={()=>select(c.id)}><span className="contact-avatar">{c.name.split(" ").map((x:string)=>x[0]).join("")}</span><span><b>{c.name}</b><small>{c.email}</small><em>{c.source}</em></span><time>{c.date}</time></button>)}</div>; }
1072
+ function SettingsSidebar({ site }: any) { return <div className="sidebar-section settings-form"><div className="panel-note"><Icon name="info"/><p>Site provisioning and integration settings are managed from the CMS hub. This editor only shows the resolved site identity.</p></div><SectionLabel title="Site details"/><Field label="Site name"><input value={site.name} readOnly/></Field><Field label="Site slug"><input value={site.id} readOnly/></Field><Field label="Custom domain"><input value={site.domain || "Not configured"} readOnly/></Field></div>; }
1073
+
1074
+ function SectionLabel({title}:any){return <div className="section-label"><span>{title}</span><i/></div>}
1075
+ function Field({label,raw,children}:any){return <label className="field"><span>{label}{raw&&<i title={raw}>?</i>}</span>{children}</label>}
1076
+ function ToggleRow({label,copy,value,set}:any){return <div className="toggle-row"><span><b>{label}</b><small>{copy}</small></span><button className={`toggle ${value?"on":""}`} onClick={()=>set(!value)}><span/></button></div>}
1077
+
1078
+ function BlockInspector({ block, update, toast }: any) { return <div className="panel-form"><div className="panel-note"><Icon name="info"/><p>Editing canonical <b>{block.type}</b> block data.</p></div>{Object.entries(block.data || {}).map(([key,value]) => { const label=key.replace(/_/g," ").replace(/\b\w/g,x=>x.toUpperCase()); if(typeof value==="boolean")return <Field key={key} label={label}><ToggleRow label={value?"Enabled":"Disabled"} copy="Boolean field" value={value} set={(v:boolean)=>update(key,v)}/></Field>; if(typeof value==="number")return <Field key={key} label={label}><input type="number" value={value} onChange={e=>update(key,Number(e.target.value))}/></Field>; if(Array.isArray(value)) return <ListField key={key} label={label} raw={key} value={value} update={(v: any) => update(key, v)}/>; if(typeof value==="object")return <Field key={key} label={label}><textarea className="code-area" rows={6} value={JSON.stringify(value,null,2)} onChange={e=>{try{update(key,JSON.parse(e.target.value))}catch{}}} onBlur={e=>{try{JSON.parse(e.target.value)}catch{toast("This block field contains invalid JSON","error")}}}/></Field>; return <Field key={key} label={label}><input value={String(value??"")} onChange={e=>update(key,e.target.value)}/></Field>; })}{!Object.keys(block.data || {}).length && <EmptyState icon="puzzle" title="Empty block" copy="This block has no editable data fields yet."/>}</div>; }
1079
+ function ContentInspector({ section, schemas, update, toast }: any) {
1080
+ const [raw, setRaw] = useState(false);
1081
+ const [rich, setRich] = useState<string>("");
1082
+ const schema = (schemas || []).find((row: any) => row.type === section.type || row.type === String(section.type || "").toLowerCase() || row.label === section.name);
1083
+ const fieldDefs = schema?.fields && typeof schema.fields === "object" ? schema.fields as Record<string, any> : null;
1084
+ const sectionFieldKeys = Object.keys(section.fields || {});
1085
+ const allKeys = fieldDefs
1086
+ ? Array.from(new Set([...Object.keys(fieldDefs), ...sectionFieldKeys]))
1087
+ : sectionFieldKeys;
1088
+ const entries = allKeys.map((key) => [
1089
+ key,
1090
+ section.fields?.[key] ?? schema?.defaults?.[key] ?? "",
1091
+ ] as const);
1092
+ return <div className="panel-form"><div className="panel-note"><Icon name="info"/><p>{schema ? <>Fields from registry schema <b>{schema.type}</b> (v{schema.version}).</> : <>Fields are mapped to <b>{section.name}</b>.</>} Changes preview instantly and save as structured content.</p></div>{entries.map(([key, value]) => {
1093
+ const def = fieldDefs?.[key];
1094
+ const label = (def?.label as string) || key.replace(/_/g, " ").replace(/\b\w/g, (x: string) => x.toUpperCase());
1095
+ const kind = String(def?.type || "").toLowerCase();
1096
+ if (typeof value === "boolean" || kind === "boolean") return <Field key={key} label={label} raw={key}><ToggleRow label={value ? "Enabled" : "Disabled"} copy="Boolean field" value={!!value} set={(v: boolean) => update(key, v)}/></Field>;
1097
+ if (kind === "link" || (value && typeof value === "object" && !Array.isArray(value) && ("href" in (value as any) || "label" in (value as any)))) {
1098
+ const link = (value && typeof value === "object" ? value : { label: "", href: String(value || "") }) as any;
1099
+ return <Field key={key} label={label} raw={key}><div className="two-fields"><input placeholder="Label" value={String(link.label || "")} onChange={(e) => update(key, { ...link, label: e.target.value })}/><input placeholder="https://" value={String(link.href || "")} onChange={(e) => update(key, { ...link, href: e.target.value })}/></div></Field>;
1100
+ }
1101
+ if (kind === "asset" || key.includes("image")) {
1102
+ const url = typeof value === "string" ? value : value && typeof value === "object" ? String((value as any).url || (value as any).src || "") : "";
1103
+ return <Field key={key} label={label} raw={key}><div className="image-field"><img src={url || undefined} alt=""/><div><input value={url} onChange={(e) => update(key, kind === "asset" ? { url: e.target.value } : e.target.value)}/><span><Button icon="image" onClick={() => toast("Use the Media panel to inspect available assets", "info")}>Media</Button></span></div></div></Field>;
1104
+ }
1105
+ if (Array.isArray(value)) return <ListField key={key} label={label} raw={key} value={value} update={(v: any) => update(key, v)}/>;
1106
+ if (kind === "richtext" || key === "body" || key === "description" || key.includes("body") || key.includes("quote")) return <Field key={key} label={label} raw={key}><div className="rich-editor"><div className="rich-tools">{["B","I","Link","• List","H2"].map((x) => <button key={x} onMouseDown={(e) => { e.preventDefault(); document.execCommand(x === "B" ? "bold" : x === "I" ? "italic" : x === "H2" ? "formatBlock" : x === "• List" ? "insertUnorderedList" : "createLink", false, x === "Link" ? "https://" : "h2"); }}>{x}</button>)}</div><div contentEditable suppressContentEditableWarning onPaste={(e) => { e.preventDefault(); document.execCommand("insertText", false, e.clipboardData.getData("text/plain")); }} onInput={(e) => { const clean = sanitizeCmsRichText(e.currentTarget.innerHTML); setRich(clean); update(key, clean); }} dangerouslySetInnerHTML={{ __html: sanitizeCmsRichText(rich || String(value || "")) }}/></div></Field>;
1107
+ if (typeof value === "number" || kind === "number") return <Field key={key} label={label} raw={key}><input type="number" value={Number(value || 0)} onChange={(e) => update(key, Number(e.target.value))}/></Field>;
1108
+ if (value && typeof value === "object") return <Field key={key} label={label} raw={key}><textarea className="code-area" rows={6} value={JSON.stringify(value, null, 2)} onChange={(e) => { try { update(key, JSON.parse(e.target.value)); } catch {} }} onBlur={(e) => { try { JSON.parse(e.target.value); } catch { toast("This field contains invalid JSON", "error"); } }}/></Field>;
1109
+ return <Field key={key} label={label} raw={key}>{String(value ?? "").length > 70 ? <textarea rows={4} value={String(value ?? "")} onChange={(e) => update(key, e.target.value)}/> : <input value={String(value ?? "")} onChange={(e) => update(key, e.target.value)}/>}</Field>;
1110
+ })}<button className="disclosure" onClick={() => setRaw((v) => !v)}><Icon name={raw ? "down" : "chevron"}/><span>Raw fields JSON</span></button>{raw && <div className="raw-json"><button onClick={() => { navigator.clipboard?.writeText(JSON.stringify(section.fields, null, 2)); toast("Fields JSON copied"); }}><Icon name="copy"/></button><pre>{JSON.stringify(section.fields, null, 2)}</pre></div>}</div>;
1111
+ }
1112
+
1113
+ function ListField({ label, raw, value, update }: any) {
1114
+ const drag = useRef<number | null>(null);
1115
+ const arr = Array.isArray(value) ? value : [];
1116
+ const isObjectArray = arr.some((item: any) => item && typeof item === "object" && !Array.isArray(item));
1117
+
1118
+ const isLinkLike = isObjectArray && (
1119
+ String(raw || "").toLowerCase().includes("link") ||
1120
+ String(raw || "").toLowerCase().includes("nav") ||
1121
+ String(label || "").toLowerCase().includes("link") ||
1122
+ String(label || "").toLowerCase().includes("nav") ||
1123
+ arr.some((x: any) => x && typeof x === "object" && ("label" in x || "href" in x || "url" in x || "title" in x))
1124
+ );
1125
+
1126
+ const getNewItem = () => {
1127
+ if (!isObjectArray && !isLinkLike) return "";
1128
+ const existing = arr.find((x: any) => x && typeof x === "object" && !Array.isArray(x));
1129
+ if (existing) {
1130
+ return Object.fromEntries(Object.keys(existing).map((k) => [k, ""]));
1131
+ }
1132
+ return isLinkLike ? { label: "", href: "" } : { title: "", text: "" };
1133
+ };
1134
+
1135
+ return (
1136
+ <Field label={label} raw={raw}>
1137
+ <div className="list-field">
1138
+ {arr.map((v: any, i: number) => {
1139
+ const isItemObject = v && typeof v === "object" && !Array.isArray(v);
1140
+
1141
+ return (
1142
+ <div
1143
+ key={i}
1144
+ draggable
1145
+ onDragStart={() => (drag.current = i)}
1146
+ onDragOver={(e) => e.preventDefault()}
1147
+ onDrop={() => {
1148
+ const a = [...arr];
1149
+ const [m] = a.splice(drag.current!, 1);
1150
+ a.splice(i, 0, m);
1151
+ update(a);
1152
+ }}
1153
+ style={{
1154
+ alignItems: isItemObject && Object.keys(v).length > 2 ? "flex-start" : "center",
1155
+ }}
1156
+ >
1157
+ <span title="Drag to reorder" style={{ cursor: "grab", userSelect: "none" }}>
1158
+ ⠿
1159
+ </span>
1160
+
1161
+ {isItemObject ? (
1162
+ ("label" in v || "href" in v || "url" in v || "title" in v || Object.keys(v).length <= 2) ? (
1163
+ <div
1164
+ className="two-fields"
1165
+ style={{ flex: 1, padding: 0, gap: 4, display: "grid", gridTemplateColumns: "1fr 1fr" }}
1166
+ >
1167
+ <input
1168
+ placeholder="Title / Label"
1169
+ value={String(v.label ?? v.title ?? v.name ?? v.text ?? "")}
1170
+ onChange={(e) => {
1171
+ const a = [...arr];
1172
+ const key = "label" in v ? "label" : "title" in v ? "title" : "name" in v ? "name" : "text" in v ? "text" : "label";
1173
+ a[i] = { ...v, [key]: e.target.value };
1174
+ update(a);
1175
+ }}
1176
+ />
1177
+ <input
1178
+ placeholder="Link (href / url)"
1179
+ value={String(v.href ?? v.url ?? v.path ?? "")}
1180
+ onChange={(e) => {
1181
+ const a = [...arr];
1182
+ const key = "href" in v ? "href" : "url" in v ? "url" : "path" in v ? "path" : "href";
1183
+ a[i] = { ...v, [key]: e.target.value };
1184
+ update(a);
1185
+ }}
1186
+ />
1187
+ </div>
1188
+ ) : (
1189
+ <div
1190
+ style={{
1191
+ flex: 1,
1192
+ display: "flex",
1193
+ flexDirection: "column",
1194
+ gap: 4,
1195
+ background: "var(--surface-1, rgba(0,0,0,0.03))",
1196
+ border: "1px solid var(--border-2, #e5e5e5)",
1197
+ borderRadius: 6,
1198
+ padding: "6px 8px",
1199
+ }}
1200
+ >
1201
+ {Object.keys(v).map((propKey) => (
1202
+ <div key={propKey} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
1203
+ <span
1204
+ style={{
1205
+ fontSize: "8px",
1206
+ fontWeight: 600,
1207
+ textTransform: "uppercase",
1208
+ color: "var(--text-3, #888)",
1209
+ }}
1210
+ >
1211
+ {propKey.replace(/_/g, " ")}
1212
+ </span>
1213
+ <input
1214
+ value={String(v[propKey] ?? "")}
1215
+ onChange={(e) => {
1216
+ const a = [...arr];
1217
+ a[i] = { ...v, [propKey]: e.target.value };
1218
+ update(a);
1219
+ }}
1220
+ />
1221
+ </div>
1222
+ ))}
1223
+ </div>
1224
+ )
1225
+ ) : (
1226
+ <input
1227
+ value={String(v ?? "")}
1228
+ onChange={(e) => {
1229
+ const a = [...arr];
1230
+ a[i] = e.target.value;
1231
+ update(a);
1232
+ }}
1233
+ />
1234
+ )}
1235
+
1236
+ <button
1237
+ type="button"
1238
+ title="Remove item"
1239
+ onClick={() => update(arr.filter((_: any, x: number) => x !== i))}
1240
+ >
1241
+ <Icon name="close" />
1242
+ </button>
1243
+ </div>
1244
+ );
1245
+ })}
1246
+
1247
+ <Button icon="plus" onClick={() => update([...arr, getNewItem()])}>
1248
+ Add {isLinkLike ? "link" : "item"}
1249
+ </Button>
1250
+ </div>
1251
+ </Field>
1252
+ );
1253
+ }
1254
+
1255
+ function StyleInspector({ section, update }: any) { const css=section.css||{}; const [linked,setLinked]=useState(true); const [bg,setBg]=useState("Color"); const [display,setDisplay]=useState("block"); return <div className="panel-form"><SectionLabel title="Spacing"/><div className="box-control"><div className="box-grid">{["Top","Right","Bottom","Left"].map((x,i)=><Field key={x} label={x}><input type="number" value={parseInt(css[`padding${x}`]||"24")} onChange={e=>{const v=e.target.value+"px";if(linked)["Top","Right","Bottom","Left"].forEach(y=>update(`padding${y}`,v));else update(`padding${x}`,v)}}/></Field>)}</div><button className={linked?"active":""} onClick={()=>setLinked(v=>!v)}><Icon name="link"/></button></div><SectionLabel title="Background"/><Field label="Type"><select value={bg} onChange={e=>setBg(e.target.value)}>{["None","Color","Gradient","Image","Video"].map(x=><option key={x}>{x}</option>)}</select></Field>{bg==="Color"&&<Field label="Background color"><div className="color-input"><input type="color" value={css.backgroundColor||section.color} onChange={e=>update("backgroundColor",e.target.value)}/><input value={css.backgroundColor||section.color} onChange={e=>update("backgroundColor",e.target.value)}/></div></Field>}{bg==="Gradient"&&<><div className="gradient-preview"/><div className="two-fields"><input type="color" defaultValue="#6358ff"/><input type="color" defaultValue="#cfef5b"/></div><Field label="Angle"><input type="range" min="0" max="360" defaultValue="135"/></Field></>}<SectionLabel title="Typography"/><Field label="Font family"><select onChange={e=>update("fontFamily",e.target.value)}>{["Inter","Manrope","DM Sans","Space Grotesk","Playfair Display","JetBrains Mono"].map(x=><option key={x}>{x}</option>)}</select></Field><div className="two-fields"><Field label="Size"><input type="number" defaultValue="16" onChange={e=>update("fontSize",e.target.value+"px")}/></Field><Field label="Weight"><select onChange={e=>update("fontWeight",e.target.value)}>{[400,500,600,700,800].map(x=><option key={x}>{x}</option>)}</select></Field></div><Field label="Alignment"><div className="button-group">{["left","center","right","justify"].map(x=><button key={x} onClick={()=>update("textAlign",x)}>{x[0].toUpperCase()}</button>)}</div></Field><SectionLabel title="Border & shadow"/><div className="two-fields"><Field label="Width"><input type="number" defaultValue="0" onChange={e=>update("borderWidth",e.target.value+"px")}/></Field><Field label="Style"><select onChange={e=>update("borderStyle",e.target.value)}>{["none","solid","dashed","dotted"].map(x=><option key={x}>{x}</option>)}</select></Field></div><Field label="Radius"><input type="range" min="0" max="64" defaultValue="0" onChange={e=>update("borderRadius",e.target.value+"px")}/></Field><div className="shadow-builder"><b>Shadow 1</b><div className="four-fields">{["X","Y","Blur","Spread"].map((x,i)=><Field key={x} label={x}><input type="number" defaultValue={[0,12,32,0][i]}/></Field>)}</div><ToggleRow label="Inset" copy="Draw inside the element" value={false} set={()=>{}}/></div><SectionLabel title="Layout"/><Field label="Display"><select value={display} onChange={e=>{setDisplay(e.target.value);update("display",e.target.value)}}>{["block","flex","grid"].map(x=><option key={x}>{x}</option>)}</select></Field>{display==="flex"&&<><Field label="Direction"><div className="button-group">{["row","column"].map(x=><button key={x} onClick={()=>update("flexDirection",x)}>{x}</button>)}</div></Field><Field label="Align"><select onChange={e=>update("alignItems",e.target.value)}>{["stretch","start","center","end"].map(x=><option key={x}>{x}</option>)}</select></Field></>}{display==="grid"&&<div className="two-fields"><Field label="Columns"><input type="number" defaultValue="3"/></Field><Field label="Gap"><input type="number" defaultValue="24"/></Field></div>}<SectionLabel title="Effects & motion"/><Field label="Opacity"><div className="range-input"><input type="range" min="0" max="100" defaultValue="100" onChange={e=>update("opacity",Number(e.target.value)/100)}/><output>100%</output></div></Field><Field label="Transition"><select>{["None","Fade in","Slide up","Slide left","Zoom in"].map(x=><option key={x}>{x}</option>)}</select></Field></div>; }
1256
+
1257
+ function PageInspector({ page,site,updatePages,history,toast }: any){const update=(k:string,v:any)=>updatePages((ps:PageData[])=>ps.map(p=>p.id===page.id?{...p,[k]:v}:p));return <div className="panel-form"><SectionLabel title="Page details"/><Field label="Page title"><input value={page.title} onChange={e=>update("title",e.target.value)}/></Field><Field label="Slug"><div className="input-action"><input value={page.slug} onChange={e=>update("slug",e.target.value.toLowerCase().replace(/\s+/g,"-"))}/><button onClick={()=>{navigator.clipboard?.writeText(`https://${site.domain}${page.slug}`);toast("Page URL copied")}}><Icon name="copy"/></button></div><small className="url-preview">https://{site.domain}{page.slug}</small></Field><SectionLabel title="Search & sharing"/><Field label="Meta title"><input value={page.metaTitle} onChange={e=>update("metaTitle",e.target.value)}/><CharCount value={page.metaTitle} max={60}/></Field><Field label="Meta description"><textarea rows={4} value={page.metaDescription} onChange={e=>update("metaDescription",e.target.value)}/><CharCount value={page.metaDescription} max={160}/></Field><Field label="Open Graph image"><button className="og-preview"><div><Icon name="image" size={22}/><span>Choose social image</span></div></button></Field><Field label="Canonical URL"><input placeholder={`https://${site.domain}${page.slug}`}/></Field><div className="check-row"><label><input type="checkbox" defaultChecked/> Index</label><label><input type="checkbox" defaultChecked/> Follow</label></div><SectionLabel title="Publishing"/><div className="publish-status"><span className={`status ${page.status}`}/><span><b>{page.status==="live"?"Published":"Draft"}</b><small>{page.status==="live"?"Published timestamp available from revision history":"Not currently published"}</small></span></div><Field label="Page type"><select value={page.type} onChange={e=>update("type",e.target.value)}>{["Home","Interior","Landing","Blog post","Product"].map(x=><option key={x}>{x}</option>)}</select></Field><div className="page-metrics"><div><b>{page.sections.reduce((total: number, section: Section) => total + Object.values(section.fields || {}).filter((value) => typeof value === "string").join(" ").trim().split(/\s+/).filter(Boolean).length, 0)}</b><span>Words</span></div><div><b>{page.sections.length}</b><span>Sections</span></div><div><b>{page.sections.reduce((total: number, section: Section) => total + (section.blocks?.length || 0), 0)}</b><span>Blocks</span></div></div><Button icon="layers" className="wide-action" onClick={history}>View page history</Button></div>}
1258
+ function CharCount({value,max}:any){const n=value.length;return <small className={`char-count ${n>max+10?"bad":n>max?"warn":"good"}`}>{n} / {max}</small>}
1259
+
1260
+ function ThemeInspector({vars,update,toast}:any){const [open,setOpen]=useState<Record<string,boolean>>({Brand:true,Typography:true,Colors:false,Spacing:false,Radius:false,Shadows:false,Motion:false,Layout:false});const groups:any={Brand:["--brand-primary","--brand-secondary","--brand-accent"],Typography:["--font-heading","--font-body","--font-mono","--font-size-base"],Colors:["--color-bg","--color-surface","--color-text","--color-text-muted"],Spacing:["--space-6","--section-padding-y"],Radius:["--radius-md","--radius-lg"],Shadows:["--shadow-sm","--shadow-md"],Motion:["--duration-fast","--duration-normal"],Layout:["--container-max-width"]};return <div className="panel-form"><div className="theme-actions"><Button onClick={()=>toast("Theme reset is not connected yet","info")}>Reset all</Button><Button icon="publish" onClick={()=>{const blob=new Blob([JSON.stringify(vars,null,2)],{type:"application/json"});const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="theme.json";a.click()}}>Export</Button><Button icon="copy" onClick={()=>{navigator.clipboard?.writeText(`:root {\n${Object.entries(vars).map(([k,v])=>` ${k}: ${v};`).join("\n")}\n}`);toast("CSS variables copied")}}>CSS</Button></div><div className="theme-preview"><span style={{background:vars["--brand-primary"]}}/><div><b>Live brand preview</b><small>Buttons, links, highlights and focus states</small></div><button style={{background:vars["--brand-primary"]}}>Action</button></div>{Object.entries(groups).map(([g,keys]:any)=><div className="theme-group" key={g}><button className="theme-group-head" onClick={()=>setOpen({...open,[g]:!open[g]})}><Icon name={open[g]?"down":"chevron"}/><span>{g}</span><small>{keys.length}</small><em onClick={e=>{e.stopPropagation();toast(`${g} reset is not connected yet`,"info")}}>Reset</em></button>{open[g]&&<div className="theme-group-body">{keys.map((k:string)=>{const val=vars[k]||({"--shadow-sm":"0 2px 8px rgba(0,0,0,.08)","--shadow-md":"0 12px 32px rgba(0,0,0,.12)","--duration-fast":"120ms","--duration-normal":"200ms"} as any)[k]||"16px";const color=k.includes("brand")||k.includes("color");return <Field key={k} label={k.replace("--","").replace(/-/g," ")} raw={k}><div className={color?"color-input":"input-action"}>{color&&<input type="color" value={val} onChange={e=>update(k,e.target.value)}/>}<input value={val} onChange={e=>update(k,e.target.value)}/><button onClick={()=>{navigator.clipboard?.writeText(val);toast("Variable copied")}}><Icon name="copy"/></button></div></Field>})}</div>}</div>)}</div>}
1261
+
1262
+ function InspectorCrm({contacts,viewAll,select}:any){return <div className="panel-form"><div className="crm-page-stats"><div><b>{contacts.length}</b><span>Loaded contacts</span></div></div><div className="list-head"><b>Recent contacts</b><button onClick={viewAll}>View all</button></div>{!contacts.length ? <EmptyState icon="users" title="No CRM data" copy="No contact records are loaded for this site."/> : contacts.slice(0,6).map((c:any)=><button className="mini-contact" key={c.id} onClick={()=>select(c.id)}><span>{c.name.split(" ").map((x:string)=>x[0]).join("")}</span><div><b>{c.name}</b><small>{c.email}</small></div><time>{c.date}</time></button>)}</div>}
1263
+ function ImportSectionModal({close,add}:any){const [mode,setMode]=useState<"URL"|"HTML">("URL");return <Modal title="Import content" onClose={close} wide><div className="modal-toolbar"><div className="segmented"><button className={mode==="URL"?"active":""} onClick={()=>setMode("URL")}>Import from URL</button><button className={mode==="HTML"?"active":""} onClick={()=>setMode("HTML")}>Paste HTML</button></div></div>{mode==="URL"?<div className="import-pane"><Icon name="external" size={28}/><h3>Import a section from a URL</h3><p>Bring public content into the CMS import workflow and create an editable section.</p><div className="input-action"><input autoFocus placeholder="https://example.com/page"/><Button kind="accent" onClick={()=>add("Imported Section")}>Import</Button></div></div>:<div className="import-pane"><Icon name="code" size={28}/><h3>Paste custom HTML</h3><p>Scripts are removed before the section is created.</p><textarea autoFocus rows={12} placeholder={'<section class="custom">\n <h2>Your section</h2>\n</section>'}/><Button kind="accent" onClick={()=>add("Custom HTML")}>Add custom section</Button></div>}</Modal>}
1264
+ function AddSectionModal({close,add,schemas}:any){const [cat,setCat]=useState("All");const [q,setQ]=useState("");const [tab,setTab]=useState("Library");const cats=["All","Hero","Navigation","Feature","Testimonial","CTA","Gallery","Pricing","FAQ","Contact","Footer","Custom"];const registryCards=(schemas||[]).map((s:any)=>s.label||s.type).filter(Boolean);const cards=registryCards.length?registryCards:["Editorial Hero","Immersive Hero","Minimal Nav","Split Feature","Numbered Services","Quote Spotlight","Project Gallery","Pricing Matrix","FAQ Stack","Contact Split","Bold CTA","Utility Footer"];return <Modal title="Add a section" onClose={close} wide><div className="modal-toolbar"><div className="segmented"><button className={tab==="Library"?"active":""} onClick={()=>setTab("Library")}>Section library</button><button className={tab==="URL"?"active":""} onClick={()=>setTab("URL")}>Import from URL</button><button className={tab==="HTML"?"active":""} onClick={()=>setTab("HTML")}>Paste HTML</button></div><Search value={q} onChange={setQ} placeholder="Search section templates"/></div>{tab==="Library"?<div className="add-section-layout"><nav>{cats.map(x=><button className={cat===x?"active":""} key={x} onClick={()=>setCat(x)}>{x}<span>{x==="All"?cards.length:Math.floor(Math.random()*5)+1}</span></button>)}</nav><div className="section-card-grid">{cards.filter((x:string)=>x.toLowerCase().includes(q.toLowerCase())).map((x:string,i:number)=><article key={x}><Wireframe variant={i}/><div><b>{x}</b><span>{cats[1+(i%(cats.length-1))]}</span></div><Button kind="accent" icon="plus" onClick={()=>add(x)}>Add</Button></article>)}</div></div>:tab==="URL"?<div className="import-pane"><Icon name="external" size={28}/><h3>Import a section from a URL</h3><p>We will fetch the public page, isolate a region, sanitize the markup, and create an editable custom section.</p><div className="input-action"><input autoFocus placeholder="https://example.com/page"/><Button kind="accent" onClick={()=>add("Imported Section")}>Import</Button></div></div>:<div className="import-pane"><Icon name="code" size={28}/><h3>Paste custom HTML</h3><p>Scripts are removed before the section is added. You can refine HTML and styles in the inspector.</p><textarea autoFocus rows={12} placeholder={'<section class="custom">\n <h2>Your section</h2>\n</section>'}/><Button kind="accent" onClick={()=>add("Custom HTML")}>Add custom section</Button></div>}</Modal>}
1265
+
1266
+ function slugFromPageTitle(title: string) {
1267
+ const base = String(title || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1268
+ return base ? `/${base}` : "/untitled";
1269
+ }
1270
+
1271
+ function PageModal({ data, setData, close, create, saving }: any) {
1272
+ const [slugEdited, setSlugEdited] = useState(false);
1273
+ return (
1274
+ <Modal title="Create a new page" onClose={close} wide focusKey="create-page">
1275
+ <div className="wizard-body">
1276
+ <div className="panel-note" style={{ marginBottom: 16 }}>
1277
+ <Icon name="info"/>
1278
+ <p>Enter a title — slug updates automatically unless you edit it. New pages start as drafts with default sections from the platform starter.</p>
1279
+ </div>
1280
+ <div className="details-form">
1281
+ <Field label="Page title">
1282
+ <input
1283
+ autoFocus
1284
+ value={data.title}
1285
+ onChange={(e) => {
1286
+ const title = e.target.value;
1287
+ setData((prev: typeof data) => ({
1288
+ ...prev,
1289
+ title,
1290
+ slug: slugEdited ? prev.slug : slugFromPageTitle(title),
1291
+ }));
1292
+ }}
1293
+ />
1294
+ </Field>
1295
+ <Field label="Slug">
1296
+ <input
1297
+ value={data.slug}
1298
+ onChange={(e) => {
1299
+ setSlugEdited(true);
1300
+ setData({ ...data, slug: e.target.value });
1301
+ }}
1302
+ />
1303
+ </Field>
1304
+ <Field label="Page type">
1305
+ <select value={data.type} onChange={(e) => setData({ ...data, type: e.target.value })}>
1306
+ {["Interior", "Home", "Landing", "Blog", "Product"].map((x) => <option key={x}>{x}</option>)}
1307
+ </select>
1308
+ </Field>
1309
+ </div>
1310
+ </div>
1311
+ <footer className="modal-footer">
1312
+ <Button onClick={close}>Cancel</Button>
1313
+ <div>
1314
+ <Button kind="accent" disabled={saving || !String(data.title || "").trim()} onClick={create}>
1315
+ {saving ? "Creating…" : "Create page"}
1316
+ </Button>
1317
+ </div>
1318
+ </footer>
1319
+ </Modal>
1320
+ );
1321
+ }
1322
+
1323
+ function UploadModal({close}:any){return <Modal title="Upload media" onClose={close}><EmptyState icon="upload" title="Media upload is not connected yet" copy="The canonical Assets API currently provides the media library read path. Upload transport will be enabled only when a real asset registration/upload capability is connected."/><footer className="modal-footer"><Button onClick={close}>Close</Button></footer></Modal>}
1324
+ function ScheduleModal({close}:any){return <Modal title="Schedule publishing" onClose={close}><EmptyState icon="info" title="Scheduling is not connected yet" copy="Immediate publishing uses the canonical lifecycle pipeline. A scheduling endpoint has not been connected to this editor, so no date or time will be fabricated here."/><footer className="modal-footer"><Button onClick={close}>Close</Button></footer></Modal>}
1325
+ function ShortcutsModal({close}:any){const rows=[["Save active section","⌘ S"],["Undo / Redo","⌘ Z / ⌘ ⇧ Z"],["Toggle preview mode","⌘ P"],["Command palette","⌘ K"],["Publish page","⌘ ⇧ P"],["Collapse sidebar","⌘ ⇧ ."],["Collapse inspector","⌘ ⇧ ,"],["Switch workspace mode","⌘ 1–7"],["Move selected section","⌘ ↑ / ⌘ ↓"],["Close / deselect","Esc"],["Focus sidebar search","⌘ F"],["Open shortcuts","⌘ /"]];return <Modal title="Keyboard shortcuts" onClose={close}><div className="shortcut-list">{rows.map(([x,k])=><div key={x}><span>{x}</span><kbd>{k}</kbd></div>)}</div></Modal>}
1326
+ function CommandPalette({close,pages,sections,action}:any){const [q,setQ]=useState("");const items=[...pages.map((p:PageData)=>({type:"page",id:p.id,name:p.title,desc:`Open ${p.slug}`,icon:"file"})),...sections.map((s:Section)=>({type:"section",id:s.id,name:s.name,desc:s.type,icon:"layers"})),{type:"publish",name:"Publish current page",desc:"Send pending changes live",icon:"publish"},{type:"add",name:"Add a section",desc:"Browse the section library",icon:"plus"},{type:"media",name:"Open media library",desc:"Browse assets and uploads",icon:"image"}].filter(x=>(x.name+x.desc).toLowerCase().includes(q.toLowerCase()));return <div className="palette-backdrop"><div className="command-palette"><div className="palette-search"><Icon name="search" size={20}/><input autoFocus value={q} onChange={e=>setQ(e.target.value)} placeholder="Search pages, sections, and actions…"/><kbd>ESC</kbd></div><div className="palette-results"><span>Results</span>{items.map((x:any,i)=><button className={i===0?"active":""} key={`${x.type}-${x.id||x.name}`} onClick={()=>action(x.type,x.id)}><i><Icon name={x.icon}/></i><span><b>{x.name}</b><small>{x.desc}</small></span>{x.type==="page"&&<em>Page</em>}{x.type==="section"&&<em>Section</em>}</button>)}</div><footer><span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span><span><kbd>↵</kbd> Select</span></footer></div></div>}
1327
+ function HistoryModal({close}:any){return <Modal title="Page history" onClose={close}><EmptyState icon="layers" title="Revision history" copy="Revision history is available through the CMS lifecycle API and will be loaded here when this panel opens."/></Modal>}
1328
+ function TemplatePreview({ template, close, apply }: any) {
1329
+ const name = template?.name || "Template";
1330
+ return (
1331
+ <Modal title={name} onClose={close} wide>
1332
+ <EmptyState
1333
+ icon="grid"
1334
+ title={name}
1335
+ copy={template?.hasHtml ? "Apply creates a new draft page from this template HTML." : "This catalog entry may lack source HTML; apply will fail loud if R2 source is missing."}
1336
+ />
1337
+ <footer className="modal-footer">
1338
+ <Button onClick={close}>Close</Button>
1339
+ <Button kind="accent" onClick={() => apply(template)} disabled={!template?.id}>Apply template</Button>
1340
+ </footer>
1341
+ </Modal>
1342
+ );
1343
+ }
1344
+
1345
+ function ContactDrawer({contact,close}:any){return <div className="drawer-backdrop" onClick={close}><aside className="contact-drawer" onClick={e=>e.stopPropagation()}><header><div className="contact-avatar large">{contact.name.split(" ").map((x:string)=>x[0]).join("")}</div><div><span>CRM contact</span><h2>{contact.name}</h2><p>{contact.email}</p></div><Button icon="close" onClick={close}/></header><div className="drawer-body"><SectionLabel title="Details"/><dl><dt>Email</dt><dd>{contact.email || "—"}</dd><dt>Source</dt><dd>{contact.source || "—"}</dd><dt>Date</dt><dd>{contact.date || "—"}</dd></dl><div className="panel-note"><Icon name="info"/><p>Notes, activity, phone details, and contact mutations are shown only when those fields are supplied by the CRM API.</p></div></div></aside></div>}