@object-ui/app-shell 6.2.3 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (486) hide show
  1. package/CHANGELOG.md +1229 -0
  2. package/README.md +292 -0
  3. package/dist/assistant/assistantBus.d.ts +72 -0
  4. package/dist/assistant/assistantBus.js +133 -0
  5. package/dist/chrome/CommandPalette.d.ts +1 -1
  6. package/dist/chrome/CommandPalette.js +26 -22
  7. package/dist/chrome/ConditionalAuthWrapper.d.ts +1 -1
  8. package/dist/chrome/ConsoleToaster.d.ts +1 -1
  9. package/dist/chrome/ConsoleToaster.js +3 -1
  10. package/dist/chrome/ErrorBoundary.d.ts +1 -1
  11. package/dist/chrome/KeyboardShortcutsDialog.d.ts +1 -1
  12. package/dist/chrome/KeyboardShortcutsDialog.js +16 -5
  13. package/dist/chrome/LoadingScreen.d.ts +1 -1
  14. package/dist/chrome/LoadingScreen.js +22 -26
  15. package/dist/chrome/RouteFader.d.ts +1 -1
  16. package/dist/chrome/ThemeProvider.d.ts +1 -1
  17. package/dist/components/ManagedByBadge.d.ts +1 -1
  18. package/dist/console/AppContent.d.ts +1 -1
  19. package/dist/console/AppContent.js +184 -39
  20. package/dist/console/ConsoleShell.d.ts +7 -7
  21. package/dist/console/ConsoleShell.js +32 -3
  22. package/dist/console/ai/AiChatPage.d.ts +88 -1
  23. package/dist/console/ai/AiChatPage.js +747 -66
  24. package/dist/console/ai/ConversationsSidebar.d.ts +26 -1
  25. package/dist/console/ai/ConversationsSidebar.js +149 -34
  26. package/dist/console/ai/LiveCanvas.d.ts +28 -0
  27. package/dist/console/ai/LiveCanvas.js +80 -0
  28. package/dist/console/ai/reconcileTurn.d.ts +8 -0
  29. package/dist/console/ai/reconcileTurn.js +20 -0
  30. package/dist/console/auth/AuthPageLayout.d.ts +1 -1
  31. package/dist/console/auth/ForgotPasswordPage.d.ts +1 -1
  32. package/dist/console/auth/LoginPage.d.ts +1 -1
  33. package/dist/console/auth/RegisterPage.d.ts +1 -1
  34. package/dist/console/auth/RegisterPage.js +23 -3
  35. package/dist/console/cloud-connection/CloudConnectionPanel.d.ts +1 -0
  36. package/dist/console/cloud-connection/CloudConnectionPanel.js +169 -0
  37. package/dist/console/home/AppCard.d.ts +1 -1
  38. package/dist/console/home/AppCard.js +6 -12
  39. package/dist/console/home/HomeAppsStrip.d.ts +8 -0
  40. package/dist/console/home/HomeAppsStrip.js +61 -0
  41. package/dist/console/home/HomeLayout.d.ts +1 -1
  42. package/dist/console/home/HomeLayout.js +3 -1
  43. package/dist/console/home/HomePage.d.ts +1 -2
  44. package/dist/console/home/HomePage.js +149 -21
  45. package/dist/console/home/HomeRail.d.ts +22 -0
  46. package/dist/console/home/HomeRail.js +62 -0
  47. package/dist/console/home/QuickActions.d.ts +1 -1
  48. package/dist/console/home/QuickActions.js +3 -11
  49. package/dist/console/home/RecentApps.d.ts +1 -1
  50. package/dist/console/home/RecentApps.js +2 -2
  51. package/dist/console/home/StarredApps.d.ts +1 -1
  52. package/dist/console/home/StarredApps.js +2 -2
  53. package/dist/console/marketplace/InstalledListWidget.d.ts +1 -0
  54. package/dist/console/marketplace/InstalledListWidget.js +93 -0
  55. package/dist/console/marketplace/MarkdownText.d.ts +1 -1
  56. package/dist/console/marketplace/MarketplaceAccessDenied.d.ts +1 -1
  57. package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +8 -14
  58. package/dist/console/marketplace/MarketplaceInstalledPage.js +14 -66
  59. package/dist/console/marketplace/MarketplacePackagePage.d.ts +1 -1
  60. package/dist/console/marketplace/MarketplacePackagePage.js +249 -8
  61. package/dist/console/marketplace/MarketplacePage.d.ts +1 -1
  62. package/dist/console/marketplace/MarketplacePage.js +60 -3
  63. package/dist/console/marketplace/PackageIcon.d.ts +1 -1
  64. package/dist/console/marketplace/PluginDisclosure.d.ts +14 -0
  65. package/dist/console/marketplace/PluginDisclosure.js +38 -0
  66. package/dist/console/marketplace/marketplaceApi.d.ts +123 -0
  67. package/dist/console/marketplace/marketplaceApi.js +254 -1
  68. package/dist/console/organizations/CreateWorkspaceDialog.d.ts +1 -1
  69. package/dist/console/organizations/OrganizationsLayout.d.ts +1 -1
  70. package/dist/console/organizations/OrganizationsPage.d.ts +1 -1
  71. package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +1 -1
  72. package/dist/console/organizations/manage/InvitationsPage.d.ts +1 -1
  73. package/dist/console/organizations/manage/InviteMemberDialog.d.ts +1 -1
  74. package/dist/console/organizations/manage/MembersPage.d.ts +1 -1
  75. package/dist/console/organizations/manage/OrganizationLayout.d.ts +1 -1
  76. package/dist/console/organizations/manage/SettingsPage.d.ts +1 -1
  77. package/dist/context/CommandPaletteProvider.d.ts +44 -0
  78. package/dist/context/CommandPaletteProvider.js +71 -0
  79. package/dist/context/FavoritesProvider.d.ts +1 -1
  80. package/dist/context/NavigationContext.d.ts +1 -1
  81. package/dist/context/RecentItemsProvider.d.ts +2 -2
  82. package/dist/context/UserStateAdapters.d.ts +1 -1
  83. package/dist/context/index.d.ts +2 -0
  84. package/dist/context/index.js +1 -0
  85. package/dist/hooks/index.d.ts +5 -2
  86. package/dist/hooks/index.js +4 -1
  87. package/dist/hooks/useActionModal.d.ts +53 -0
  88. package/dist/hooks/useActionModal.js +111 -0
  89. package/dist/hooks/useChatConversation.d.ts +137 -4
  90. package/dist/hooks/useChatConversation.js +316 -25
  91. package/dist/hooks/useConsoleActionRuntime.d.ts +70 -0
  92. package/dist/hooks/useConsoleActionRuntime.js +564 -0
  93. package/dist/hooks/useConversationList.js +61 -3
  94. package/dist/hooks/useHomeInbox.d.ts +13 -0
  95. package/dist/hooks/useHomeInbox.js +142 -0
  96. package/dist/hooks/useNavPins.js +17 -23
  97. package/dist/hooks/useNavigationSync.d.ts +33 -0
  98. package/dist/hooks/useNavigationSync.js +98 -12
  99. package/dist/hooks/useReconcileOnError.d.ts +40 -0
  100. package/dist/hooks/useReconcileOnError.js +37 -0
  101. package/dist/hooks/useRecordApprovals.d.ts +18 -19
  102. package/dist/hooks/useRecordApprovals.js +24 -40
  103. package/dist/hooks/useResponsiveSidebar.js +14 -5
  104. package/dist/hooks/useSettleSignal.d.ts +19 -0
  105. package/dist/hooks/useSettleSignal.js +20 -0
  106. package/dist/hooks/useTrackRouteAsRecent.js +35 -0
  107. package/dist/hooks/useUrlOverlay.d.ts +62 -0
  108. package/dist/hooks/useUrlOverlay.js +88 -0
  109. package/dist/index.d.ts +18 -8
  110. package/dist/index.js +17 -5
  111. package/dist/layout/ActivityFeed.d.ts +1 -1
  112. package/dist/layout/AppHeader.d.ts +3 -2
  113. package/dist/layout/AppHeader.js +237 -72
  114. package/dist/layout/AppSidebar.d.ts +2 -1
  115. package/dist/layout/AppSidebar.js +26 -46
  116. package/dist/layout/AppSwitcher.d.ts +2 -1
  117. package/dist/layout/AppSwitcher.js +9 -5
  118. package/dist/layout/AuthPageLayout.d.ts +1 -1
  119. package/dist/layout/ConnectionStatus.d.ts +1 -1
  120. package/dist/layout/ConnectionStatus.js +9 -6
  121. package/dist/layout/ConsoleChatbotFab.d.ts +19 -1
  122. package/dist/layout/ConsoleChatbotFab.js +16 -2
  123. package/dist/layout/ConsoleFloatingChatbot.d.ts +34 -2
  124. package/dist/layout/ConsoleFloatingChatbot.js +391 -41
  125. package/dist/layout/ConsoleLayout.d.ts +1 -1
  126. package/dist/layout/ConsoleLayout.js +27 -11
  127. package/dist/layout/ContextSelectors.d.ts +44 -0
  128. package/dist/layout/ContextSelectors.js +242 -0
  129. package/dist/layout/InboxPopover.d.ts +6 -1
  130. package/dist/layout/InboxPopover.js +25 -6
  131. package/dist/layout/LocaleSwitcher.d.ts +1 -1
  132. package/dist/layout/LocalizedSidebarTrigger.d.ts +2 -0
  133. package/dist/layout/LocalizedSidebarTrigger.js +15 -0
  134. package/dist/layout/MobileViewSwitcherContext.d.ts +1 -1
  135. package/dist/layout/ModeToggle.d.ts +1 -1
  136. package/dist/layout/PageHeader.d.ts +1 -1
  137. package/dist/layout/UnifiedSidebar.d.ts +2 -1
  138. package/dist/layout/UnifiedSidebar.js +116 -15
  139. package/dist/layout/agentPicker.d.ts +56 -0
  140. package/dist/layout/agentPicker.js +40 -0
  141. package/dist/observability/index.d.ts +1 -0
  142. package/dist/observability/index.js +1 -0
  143. package/dist/observability/settleSignal.d.ts +64 -0
  144. package/dist/observability/settleSignal.js +131 -0
  145. package/dist/preview/CommitTimeline.d.ts +15 -0
  146. package/dist/preview/CommitTimeline.js +82 -0
  147. package/dist/preview/DraftChangesPanel.d.ts +19 -0
  148. package/dist/preview/DraftChangesPanel.js +114 -0
  149. package/dist/preview/DraftPreviewBar.d.ts +8 -0
  150. package/dist/preview/DraftPreviewBar.js +86 -0
  151. package/dist/preview/PreviewDraftEmptyState.d.ts +16 -0
  152. package/dist/preview/PreviewDraftEmptyState.js +47 -0
  153. package/dist/preview/PreviewModeContext.d.ts +57 -0
  154. package/dist/preview/PreviewModeContext.js +99 -0
  155. package/dist/preview/UnpublishedAppBar.d.ts +8 -0
  156. package/dist/preview/UnpublishedAppBar.js +83 -0
  157. package/dist/preview/commitHistory.d.ts +28 -0
  158. package/dist/preview/commitHistory.js +48 -0
  159. package/dist/preview/draftStatus.d.ts +20 -0
  160. package/dist/preview/draftStatus.js +27 -0
  161. package/dist/preview/usePublishAllDrafts.d.ts +18 -0
  162. package/dist/preview/usePublishAllDrafts.js +106 -0
  163. package/dist/providers/AdapterProvider.d.ts +1 -1
  164. package/dist/providers/AdapterProvider.js +6 -1
  165. package/dist/providers/ExpressionProvider.d.ts +1 -1
  166. package/dist/providers/MetadataProvider.d.ts +17 -2
  167. package/dist/providers/MetadataProvider.js +192 -12
  168. package/dist/runtime-config.d.ts +46 -2
  169. package/dist/runtime-config.js +39 -2
  170. package/dist/services/builtinComponents.js +68 -59
  171. package/dist/skeletons/SkeletonDashboard.d.ts +1 -1
  172. package/dist/skeletons/SkeletonDetail.d.ts +1 -1
  173. package/dist/skeletons/SkeletonGrid.d.ts +1 -1
  174. package/dist/utils/appRoute.d.ts +21 -0
  175. package/dist/utils/appRoute.js +25 -0
  176. package/dist/utils/deriveRelatedLists.d.ts +54 -0
  177. package/dist/utils/deriveRelatedLists.js +91 -0
  178. package/dist/utils/index.d.ts +4 -0
  179. package/dist/utils/index.js +3 -0
  180. package/dist/utils/managedByEmptyState.d.ts +8 -1
  181. package/dist/utils/managedByEmptyState.js +13 -7
  182. package/dist/utils/preferLocal.d.ts +18 -0
  183. package/dist/utils/preferLocal.js +24 -0
  184. package/dist/views/ActionConfirmDialog.d.ts +1 -1
  185. package/dist/views/ActionConfirmDialog.js +3 -1
  186. package/dist/views/ActionParamDialog.d.ts +6 -1
  187. package/dist/views/ActionParamDialog.js +9 -3
  188. package/dist/views/ActionResultDialog.d.ts +13 -0
  189. package/dist/views/ActionResultDialog.js +134 -0
  190. package/dist/views/ComponentNavView.d.ts +14 -1
  191. package/dist/views/CreateViewDialog.d.ts +1 -1
  192. package/dist/views/DashboardConfigPanel.d.ts +28 -0
  193. package/dist/views/DashboardConfigPanel.js +81 -0
  194. package/dist/views/DashboardView.d.ts +4 -3
  195. package/dist/views/DashboardView.js +38 -239
  196. package/dist/views/FlowRunner.d.ts +31 -0
  197. package/dist/views/FlowRunner.js +121 -0
  198. package/dist/views/InterfaceListPage.d.ts +49 -0
  199. package/dist/views/InterfaceListPage.js +347 -0
  200. package/dist/views/MetadataInspector.d.ts +2 -2
  201. package/dist/views/ObjectView.d.ts +1 -1
  202. package/dist/views/ObjectView.js +209 -532
  203. package/dist/views/PageView.d.ts +8 -3
  204. package/dist/views/PageView.js +45 -32
  205. package/dist/views/RecordDetailView.d.ts +1 -1
  206. package/dist/views/RecordDetailView.js +363 -148
  207. package/dist/views/RecordFormPage.d.ts +1 -1
  208. package/dist/views/RecordFormPage.js +26 -1
  209. package/dist/views/ReportConfigPanel.d.ts +37 -0
  210. package/dist/views/ReportConfigPanel.js +85 -0
  211. package/dist/views/ReportView.d.ts +1 -1
  212. package/dist/views/ReportView.js +116 -7
  213. package/dist/views/RuntimeDraftBar.d.ts +30 -0
  214. package/dist/views/RuntimeDraftBar.js +112 -0
  215. package/dist/views/ScreenView.d.ts +70 -0
  216. package/dist/views/ScreenView.js +73 -0
  217. package/dist/views/SearchResultsPage.d.ts +1 -1
  218. package/dist/views/SearchResultsPage.js +8 -18
  219. package/dist/views/ViewConfigPanel.d.ts +24 -17
  220. package/dist/views/ViewConfigPanel.js +121 -77
  221. package/dist/views/index.d.ts +1 -1
  222. package/dist/views/index.js +1 -1
  223. package/dist/views/metadata-admin/AuditPanel.d.ts +28 -0
  224. package/dist/views/metadata-admin/AuditPanel.js +79 -0
  225. package/dist/views/metadata-admin/DiagnosticsPage.d.ts +20 -0
  226. package/dist/views/metadata-admin/DiagnosticsPage.js +69 -0
  227. package/dist/views/metadata-admin/DirectoryPage.d.ts +16 -1
  228. package/dist/views/metadata-admin/DirectoryPage.js +101 -24
  229. package/dist/views/metadata-admin/DraftReviewPanel.d.ts +33 -0
  230. package/dist/views/metadata-admin/DraftReviewPanel.js +77 -0
  231. package/dist/views/metadata-admin/EmbeddedItemEditor.d.ts +17 -1
  232. package/dist/views/metadata-admin/EmbeddedItemEditor.js +15 -8
  233. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +39 -0
  234. package/dist/views/metadata-admin/JsonSourceEditor.js +196 -0
  235. package/dist/views/metadata-admin/LayeredDiff.d.ts +39 -1
  236. package/dist/views/metadata-admin/LayeredDiff.js +171 -5
  237. package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +15 -1
  238. package/dist/views/metadata-admin/MetadataTypeActions.d.ts +48 -0
  239. package/dist/views/metadata-admin/MetadataTypeActions.js +165 -0
  240. package/dist/views/metadata-admin/PackagesPage.d.ts +18 -0
  241. package/dist/views/metadata-admin/PackagesPage.js +403 -0
  242. package/dist/views/metadata-admin/PageShell.d.ts +1 -1
  243. package/dist/views/metadata-admin/PageShell.js +9 -4
  244. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +35 -1
  245. package/dist/views/metadata-admin/QuickFind.d.ts +21 -1
  246. package/dist/views/metadata-admin/QuickFind.js +6 -3
  247. package/dist/views/metadata-admin/RelatedPanel.d.ts +24 -1
  248. package/dist/views/metadata-admin/RelatedPanel.js +20 -18
  249. package/dist/views/metadata-admin/ResourceEditPage.d.ts +40 -1
  250. package/dist/views/metadata-admin/ResourceEditPage.js +1250 -60
  251. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +39 -1
  252. package/dist/views/metadata-admin/ResourceHistoryPage.js +66 -16
  253. package/dist/views/metadata-admin/ResourceListPage.d.ts +13 -1
  254. package/dist/views/metadata-admin/ResourceListPage.js +258 -30
  255. package/dist/views/metadata-admin/ResourceRouter.d.ts +23 -1
  256. package/dist/views/metadata-admin/SchemaForm.d.ts +34 -1
  257. package/dist/views/metadata-admin/SchemaForm.js +559 -49
  258. package/dist/views/metadata-admin/StudioHomePage.d.ts +22 -0
  259. package/dist/views/metadata-admin/StudioHomePage.js +205 -0
  260. package/dist/views/metadata-admin/anchors.js +255 -24
  261. package/dist/views/metadata-admin/clientValidation.d.ts +50 -0
  262. package/dist/views/metadata-admin/clientValidation.js +169 -0
  263. package/dist/views/metadata-admin/color-variant-field.d.ts +30 -0
  264. package/dist/views/metadata-admin/color-variant-field.js +38 -0
  265. package/dist/views/metadata-admin/createDerive.d.ts +75 -0
  266. package/dist/views/metadata-admin/createDerive.js +179 -0
  267. package/dist/views/metadata-admin/dashboard-schema.d.ts +12 -0
  268. package/dist/views/metadata-admin/dashboard-schema.js +80 -0
  269. package/dist/views/metadata-admin/datasource/DatasourceResourcePage.d.ts +35 -0
  270. package/dist/views/metadata-admin/datasource/DatasourceResourcePage.js +327 -0
  271. package/dist/views/metadata-admin/datasource/register.d.ts +1 -0
  272. package/dist/views/metadata-admin/datasource/register.js +24 -0
  273. package/dist/views/metadata-admin/default-inspector-registry.d.ts +49 -0
  274. package/dist/views/metadata-admin/default-inspector-registry.js +8 -0
  275. package/dist/views/metadata-admin/default-schemas.js +115 -10
  276. package/dist/views/metadata-admin/external/ExternalDatasourcePanel.d.ts +27 -0
  277. package/dist/views/metadata-admin/external/ExternalDatasourcePanel.js +69 -0
  278. package/dist/views/metadata-admin/external/ImportObjectDialog.d.ts +27 -0
  279. package/dist/views/metadata-admin/external/ImportObjectDialog.js +77 -0
  280. package/dist/views/metadata-admin/external/SchemaBrowser.d.ts +16 -0
  281. package/dist/views/metadata-admin/external/SchemaBrowser.js +74 -0
  282. package/dist/views/metadata-admin/external/ValidationPanel.d.ts +16 -0
  283. package/dist/views/metadata-admin/external/ValidationPanel.js +68 -0
  284. package/dist/views/metadata-admin/external/api.d.ts +100 -0
  285. package/dist/views/metadata-admin/external/api.js +124 -0
  286. package/dist/views/metadata-admin/i18n.d.ts +1 -0
  287. package/dist/views/metadata-admin/i18n.js +1252 -2
  288. package/dist/views/metadata-admin/index.d.ts +8 -5
  289. package/dist/views/metadata-admin/index.js +12 -2
  290. package/dist/views/metadata-admin/inspector-registry.d.ts +51 -0
  291. package/dist/views/metadata-admin/inspector-registry.js +11 -0
  292. package/dist/views/metadata-admin/inspectors/ActionDefaultInspector.d.ts +30 -0
  293. package/dist/views/metadata-admin/inspectors/ActionDefaultInspector.js +180 -0
  294. package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +16 -0
  295. package/dist/views/metadata-admin/inspectors/AppNavInspector.js +110 -0
  296. package/dist/views/metadata-admin/inspectors/ConditionBuilder.d.ts +29 -0
  297. package/dist/views/metadata-admin/inspectors/ConditionBuilder.js +154 -0
  298. package/dist/views/metadata-admin/inspectors/DashboardDefaultInspector.d.ts +28 -0
  299. package/dist/views/metadata-admin/inspectors/DashboardDefaultInspector.js +110 -0
  300. package/dist/views/metadata-admin/inspectors/DashboardWidgetInspector.d.ts +18 -0
  301. package/dist/views/metadata-admin/inspectors/DashboardWidgetInspector.js +139 -0
  302. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +21 -0
  303. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +221 -0
  304. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.d.ts +16 -0
  305. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +126 -0
  306. package/dist/views/metadata-admin/inspectors/FlowInspector.d.ts +12 -0
  307. package/dist/views/metadata-admin/inspectors/FlowInspector.js +9 -0
  308. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +30 -0
  309. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +125 -0
  310. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +18 -0
  311. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +40 -0
  312. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.d.ts +14 -0
  313. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +205 -0
  314. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +26 -0
  315. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +105 -0
  316. package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +83 -0
  317. package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +181 -0
  318. package/dist/views/metadata-admin/inspectors/FlowStringListField.d.ts +21 -0
  319. package/dist/views/metadata-admin/inspectors/FlowStringListField.js +60 -0
  320. package/dist/views/metadata-admin/inspectors/InspectorComboField.d.ts +40 -0
  321. package/dist/views/metadata-admin/inspectors/InspectorComboField.js +61 -0
  322. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.d.ts +21 -0
  323. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +55 -0
  324. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.d.ts +23 -0
  325. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +365 -0
  326. package/dist/views/metadata-admin/inspectors/PageBlockInspector.d.ts +48 -0
  327. package/dist/views/metadata-admin/inspectors/PageBlockInspector.js +332 -0
  328. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +58 -0
  329. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +218 -0
  330. package/dist/views/metadata-admin/inspectors/ViewColumnInspector.d.ts +19 -0
  331. package/dist/views/metadata-admin/inspectors/ViewColumnInspector.js +144 -0
  332. package/dist/views/metadata-admin/inspectors/ViewInspector.d.ts +19 -0
  333. package/dist/views/metadata-admin/inspectors/ViewInspector.js +21 -0
  334. package/dist/views/metadata-admin/inspectors/ViewVariantInspector.d.ts +54 -0
  335. package/dist/views/metadata-admin/inspectors/ViewVariantInspector.js +191 -0
  336. package/dist/views/metadata-admin/inspectors/_shared.d.ts +128 -0
  337. package/dist/views/metadata-admin/inspectors/_shared.js +113 -0
  338. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  339. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
  340. package/dist/views/metadata-admin/inspectors/expression-validate.d.ts +26 -0
  341. package/dist/views/metadata-admin/inspectors/expression-validate.js +66 -0
  342. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +143 -0
  343. package/dist/views/metadata-admin/inspectors/flow-node-config.js +506 -0
  344. package/dist/views/metadata-admin/inspectors/index.d.ts +1 -0
  345. package/dist/views/metadata-admin/inspectors/index.js +45 -0
  346. package/dist/views/metadata-admin/inspectors/json-schema-to-fields.d.ts +40 -0
  347. package/dist/views/metadata-admin/inspectors/json-schema-to-fields.js +227 -0
  348. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +72 -0
  349. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  350. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  351. package/dist/views/metadata-admin/issuePath.js +65 -0
  352. package/dist/views/metadata-admin/mergeServerFields.d.ts +65 -0
  353. package/dist/views/metadata-admin/mergeServerFields.js +56 -0
  354. package/dist/views/metadata-admin/package-scope.d.ts +26 -0
  355. package/dist/views/metadata-admin/package-scope.js +43 -0
  356. package/dist/views/metadata-admin/preview-registry.d.ts +55 -0
  357. package/dist/views/metadata-admin/previews/ActionPreview.d.ts +25 -0
  358. package/dist/views/metadata-admin/previews/ActionPreview.js +238 -0
  359. package/dist/views/metadata-admin/previews/AddWidgetPicker.d.ts +12 -0
  360. package/dist/views/metadata-admin/previews/AddWidgetPicker.js +56 -0
  361. package/dist/views/metadata-admin/previews/AgentPreview.d.ts +24 -0
  362. package/dist/views/metadata-admin/previews/AgentPreview.js +100 -0
  363. package/dist/views/metadata-admin/previews/AppNavCanvas.d.ts +31 -0
  364. package/dist/views/metadata-admin/previews/AppNavCanvas.js +260 -0
  365. package/dist/views/metadata-admin/previews/AppPreview.d.ts +16 -1
  366. package/dist/views/metadata-admin/previews/AppPreview.js +23 -14
  367. package/dist/views/metadata-admin/previews/BookPreview.d.ts +20 -0
  368. package/dist/views/metadata-admin/previews/BookPreview.js +132 -0
  369. package/dist/views/metadata-admin/previews/DashboardPreview.d.ts +16 -1
  370. package/dist/views/metadata-admin/previews/DashboardPreview.js +110 -8
  371. package/dist/views/metadata-admin/previews/DatasetPreview.d.ts +18 -0
  372. package/dist/views/metadata-admin/previews/DatasetPreview.js +105 -0
  373. package/dist/views/metadata-admin/previews/DatasourcePreview.d.ts +23 -0
  374. package/dist/views/metadata-admin/previews/DatasourcePreview.js +68 -0
  375. package/dist/views/metadata-admin/previews/EmailTemplatePreview.d.ts +14 -1
  376. package/dist/views/metadata-admin/previews/FieldStub.d.ts +30 -0
  377. package/dist/views/metadata-admin/previews/FieldStub.js +104 -0
  378. package/dist/views/metadata-admin/previews/FieldsListEditor.d.ts +50 -0
  379. package/dist/views/metadata-admin/previews/FieldsListEditor.js +97 -0
  380. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +49 -0
  381. package/dist/views/metadata-admin/previews/FlowCanvas.js +416 -0
  382. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +20 -0
  383. package/dist/views/metadata-admin/previews/FlowPreview.js +120 -0
  384. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +46 -0
  385. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +97 -0
  386. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.d.ts +25 -0
  387. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +204 -0
  388. package/dist/views/metadata-admin/previews/JobPreview.d.ts +28 -0
  389. package/dist/views/metadata-admin/previews/JobPreview.js +290 -0
  390. package/dist/views/metadata-admin/previews/ObjectFormCanvas.d.ts +30 -0
  391. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +547 -0
  392. package/dist/views/metadata-admin/previews/ObjectPreview.d.ts +14 -1
  393. package/dist/views/metadata-admin/previews/ObjectPreview.js +5 -30
  394. package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +32 -0
  395. package/dist/views/metadata-admin/previews/OutlineStrip.js +8 -0
  396. package/dist/views/metadata-admin/previews/PageBlockCanvas.d.ts +49 -0
  397. package/dist/views/metadata-admin/previews/PageBlockCanvas.js +510 -0
  398. package/dist/views/metadata-admin/previews/PagePreview.d.ts +10 -1
  399. package/dist/views/metadata-admin/previews/PagePreview.js +200 -5
  400. package/dist/views/metadata-admin/previews/PermissionPreview.d.ts +27 -0
  401. package/dist/views/metadata-admin/previews/PermissionPreview.js +115 -0
  402. package/dist/views/metadata-admin/previews/PreviewShell.d.ts +29 -6
  403. package/dist/views/metadata-admin/previews/PreviewShell.js +16 -3
  404. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +18 -1
  405. package/dist/views/metadata-admin/previews/ReportPreview.js +23 -15
  406. package/dist/views/metadata-admin/previews/RolePreview.d.ts +19 -0
  407. package/dist/views/metadata-admin/previews/RolePreview.js +14 -0
  408. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  409. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  410. package/dist/views/metadata-admin/previews/SkillPreview.d.ts +22 -0
  411. package/dist/views/metadata-admin/previews/SkillPreview.js +34 -0
  412. package/dist/views/metadata-admin/previews/ToolPreview.d.ts +25 -0
  413. package/dist/views/metadata-admin/previews/ToolPreview.js +122 -0
  414. package/dist/views/metadata-admin/previews/TranslationPreview.d.ts +25 -0
  415. package/dist/views/metadata-admin/previews/TranslationPreview.js +52 -0
  416. package/dist/views/metadata-admin/previews/ValidationPreview.d.ts +27 -0
  417. package/dist/views/metadata-admin/previews/ValidationPreview.js +110 -0
  418. package/dist/views/metadata-admin/previews/ViewColumnPanes.d.ts +62 -0
  419. package/dist/views/metadata-admin/previews/ViewColumnPanes.js +140 -0
  420. package/dist/views/metadata-admin/previews/ViewPreview.d.ts +23 -1
  421. package/dist/views/metadata-admin/previews/ViewPreview.js +101 -73
  422. package/dist/views/metadata-admin/previews/block-config.d.ts +82 -0
  423. package/dist/views/metadata-admin/previews/block-config.js +324 -0
  424. package/dist/views/metadata-admin/previews/block-types.d.ts +40 -0
  425. package/dist/views/metadata-admin/previews/block-types.js +110 -0
  426. package/dist/views/metadata-admin/previews/field-types.d.ts +53 -0
  427. package/dist/views/metadata-admin/previews/field-types.js +97 -0
  428. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +102 -0
  429. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +227 -0
  430. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +96 -0
  431. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +373 -0
  432. package/dist/views/metadata-admin/previews/form-preview.d.ts +24 -0
  433. package/dist/views/metadata-admin/previews/form-preview.js +29 -0
  434. package/dist/views/metadata-admin/previews/index.js +43 -0
  435. package/dist/views/metadata-admin/previews/object-fields-bridge.d.ts +66 -0
  436. package/dist/views/metadata-admin/previews/object-fields-bridge.js +171 -0
  437. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +130 -0
  438. package/dist/views/metadata-admin/previews/object-fields-io.js +243 -0
  439. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  440. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  441. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +102 -0
  442. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.js +2 -0
  443. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +15 -0
  444. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +185 -0
  445. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +73 -0
  446. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +426 -0
  447. package/dist/views/metadata-admin/previews/useDatasetCatalog.d.ts +47 -0
  448. package/dist/views/metadata-admin/previews/useDatasetCatalog.js +133 -0
  449. package/dist/views/metadata-admin/previews/useFlowNodePalette.d.ts +44 -0
  450. package/dist/views/metadata-admin/previews/useFlowNodePalette.js +124 -0
  451. package/dist/views/metadata-admin/previews/useMetaOptions.d.ts +8 -0
  452. package/dist/views/metadata-admin/previews/useMetaOptions.js +50 -0
  453. package/dist/views/metadata-admin/previews/useObjectFields.d.ts +23 -0
  454. package/dist/views/metadata-admin/previews/useObjectFields.js +79 -0
  455. package/dist/views/metadata-admin/previews/useObjectOptions.d.ts +8 -0
  456. package/dist/views/metadata-admin/previews/useObjectOptions.js +43 -0
  457. package/dist/views/metadata-admin/previews/view-column-io.d.ts +42 -0
  458. package/dist/views/metadata-admin/previews/view-column-io.js +73 -0
  459. package/dist/views/metadata-admin/previews/widget-types.d.ts +24 -0
  460. package/dist/views/metadata-admin/previews/widget-types.js +40 -0
  461. package/dist/views/metadata-admin/registry.d.ts +140 -19
  462. package/dist/views/metadata-admin/report-schema.d.ts +26 -0
  463. package/dist/views/metadata-admin/report-schema.js +121 -0
  464. package/dist/views/metadata-admin/useMetadata.d.ts +100 -2
  465. package/dist/views/metadata-admin/useMetadata.js +155 -4
  466. package/dist/views/metadata-admin/view-item-normalize.d.ts +20 -0
  467. package/dist/views/metadata-admin/view-item-normalize.js +68 -0
  468. package/dist/views/metadata-admin/view-schema.d.ts +16 -0
  469. package/dist/views/metadata-admin/view-schema.js +107 -0
  470. package/dist/views/metadata-admin/view-variant-model.d.ts +23 -0
  471. package/dist/views/metadata-admin/view-variant-model.js +64 -0
  472. package/dist/views/metadata-admin/widgets.d.ts +89 -1
  473. package/dist/views/metadata-admin/widgets.js +491 -17
  474. package/dist/views/runtime-metadata-persistence.d.ts +78 -0
  475. package/dist/views/runtime-metadata-persistence.js +89 -0
  476. package/dist/views/useOpenRecordList.d.ts +18 -0
  477. package/dist/views/useOpenRecordList.js +36 -0
  478. package/dist/views/userFilterUrlState.d.ts +15 -0
  479. package/dist/views/userFilterUrlState.js +53 -0
  480. package/dist/views/view-config-adapter.d.ts +38 -0
  481. package/dist/views/view-config-adapter.js +80 -0
  482. package/package.json +52 -34
  483. package/dist/views/DesignDrawer.d.ts +0 -28
  484. package/dist/views/DesignDrawer.js +0 -51
  485. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +0 -68
  486. package/dist/views/metadata-admin/DesignerEditorWrapper.js +0 -158
@@ -20,31 +20,113 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
20
20
  * EditPage via `registerMetadataResource()`.
21
21
  */
22
22
  import * as React from 'react';
23
- import { useNavigate, useParams } from 'react-router-dom';
24
- import { Save, RotateCcw, History, Link2, Loader2, AlertTriangle, Layers3, Eye, Pencil, X, } from 'lucide-react';
23
+ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
24
+ import { Save, RotateCcw, Trash2, History, Link2, Loader2, AlertTriangle, Layers3, GitCompareArrows, Boxes, Eye, Pencil, X, PanelRightClose, PanelRightOpen, Maximize2, Minimize2, MousePointer2, SlidersHorizontal, FileCode2, Zap, ZapOff, Send, Undo2, Lock, ShieldCheck, } from 'lucide-react';
25
25
  import { Button } from '@object-ui/components';
26
26
  import { Badge } from '@object-ui/components';
27
- import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@object-ui/components';
27
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from '@object-ui/components';
28
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from '@object-ui/components';
28
29
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@object-ui/components';
29
30
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
30
31
  import { PageShell } from './PageShell';
31
- import { LayeredDiff } from './LayeredDiff';
32
+ import { MetadataTypeActions } from './MetadataTypeActions';
33
+ import { LayeredDiff, countOverlaidFields } from './LayeredDiff';
34
+ import { DraftReviewPanel, computeDraftChangeCount } from './DraftReviewPanel';
32
35
  import { SchemaForm } from './SchemaForm';
33
36
  import { useMetadataClient, useMetadataTypes, } from './useMetadata';
34
37
  import { getMetadataResource, resolveResourceConfig, listAnchorsFor, } from './registry';
38
+ import { useCreateDerive, deriveDefaultCreateFields } from './createDerive';
35
39
  import { RelatedPanel } from './RelatedPanel';
36
40
  import { MetadataDetailDrawer } from './MetadataDetailDrawer';
41
+ import { HistoryPanel } from './ResourceHistoryPage';
42
+ import { AuditPanel } from './AuditPanel';
37
43
  import { getMetadataPreview } from './preview-registry';
44
+ import { readFields } from './previews/object-fields-io';
45
+ import { useRegisterAssistantEditor } from '../../assistant/assistantBus';
46
+ import { getMetadataInspector } from './inspector-registry';
47
+ import { getMetadataDefaultInspector } from './default-inspector-registry';
48
+ import { detectLocale, t, tFormat, translateValidationMessage } from './i18n';
49
+ import { JsonSourceEditor } from './JsonSourceEditor';
50
+ import { validateMetadataDraft, hasClientValidator } from './clientValidation';
51
+ import { describeIssuePath } from './issuePath';
52
+ // react-resizable-panels' `direction` prop type does not always narrow
53
+ // cleanly in our TS config; cast at the boundary (precedent:
54
+ // packages/components/src/custom/navigation-overlay.tsx).
55
+ const PanelGroup = ResizablePanelGroup;
56
+ /**
57
+ * Metadata types whose canvas IS the primary create-time authoring
58
+ * surface, so we render the preview/inspector split during create
59
+ * instead of the centered basic-info form. Object-level basics stay
60
+ * editable via the no-selection default inspector. Other types keep
61
+ * the conventional "name it first, design after save" create flow.
62
+ */
63
+ const CREATE_MODE_CANVAS_TYPES = new Set(['object', 'report', 'dataset']);
64
+ /**
65
+ * Top-level metadata keys that a type's canvas PreviewComponent owns and
66
+ * edits visually (e.g. the object designer owns `fields` + `fieldGroups`).
67
+ * These must never surface in the inspector's fallback SchemaForm — the
68
+ * no-selection panel would otherwise render a raw JSON editor for data
69
+ * the user is already editing on the canvas.
70
+ */
71
+ const CANVAS_OWNED_KEYS = {
72
+ object: ['fields', 'fieldGroups'],
73
+ };
74
+ /**
75
+ * Normalize the framework's draft envelope into either the draft body or
76
+ * `null` (no pending draft). The envelope is:
77
+ *
78
+ * - `{ type, name, item: {...} }` when a draft exists,
79
+ * - `{ type, name, label }` when no draft exists (HTTP 200, item absent).
80
+ *
81
+ * The presence of the `item` key is the single signal; we do NOT fall back
82
+ * to using the envelope itself as the body — doing so would mis-identify the
83
+ * "no draft" stub (which still has `type`/`name`/`label` keys) as a real
84
+ * pending draft and would corrupt the editor baseline.
85
+ */
86
+ function extractDraftBody(draftResp) {
87
+ if (!draftResp || typeof draftResp !== 'object')
88
+ return null;
89
+ const env = draftResp;
90
+ if (!('item' in env))
91
+ return null;
92
+ const body = env.item;
93
+ if (!body || typeof body !== 'object')
94
+ return null;
95
+ return Object.keys(body).length > 0
96
+ ? body
97
+ : null;
98
+ }
99
+ /**
100
+ * Decide whether the validation-diagnostics banner should render at all.
101
+ *
102
+ * The gate has two reasons to stay hidden:
103
+ * - `loadFailed` — the layered/draft fetch itself failed, so the form is
104
+ * sitting on empty defaults. Any required-field issues the client
105
+ * validator produces are an artefact of the empty form, not a verdict on
106
+ * the item; the explicit "failed to load" banner already tells the real
107
+ * story. Suppress so a transport failure never masquerades as a broken
108
+ * item.
109
+ * - no diagnostics source — there is neither a server `_diagnostics`
110
+ * payload nor a client-side validator for this type, so there is nothing
111
+ * to show.
112
+ */
113
+ export function shouldRenderDiagnostics(opts) {
114
+ if (opts.loadFailed)
115
+ return false;
116
+ return opts.hasDiag || opts.hasClientValidator;
117
+ }
38
118
  export function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode = false, embedded = false, }) {
119
+ // Tiny dispatcher: a registered Custom EditPage / CreatePage is a
120
+ // different component type than MetadataResourceEditPageImpl, so React
121
+ // will unmount/remount when the registry-driven branch wins or loses
122
+ // (e.g. navigating from `/object/new` → `/object/sales_order`). Doing
123
+ // the dispatch INSIDE the impl below would leak hooks between
124
+ // branches and trigger "Rendered more hooks than during the previous
125
+ // render". We therefore keep this outer dispatcher hook-free apart
126
+ // from `useParams`, which is unconditional.
39
127
  const params = useParams();
40
128
  const type = typeProp ?? params.type ?? '';
41
129
  const name = nameProp ?? params.name ?? '';
42
- const navigate = useNavigate();
43
- const client = useMetadataClient();
44
- const { entries } = useMetadataTypes(client);
45
- const entry = entries.find((t) => t.type === type);
46
- const config = resolveResourceConfig(type, entry);
47
- // Custom editor takes over.
48
130
  const customConfig = getMetadataResource(type);
49
131
  if (customConfig?.EditPage && !createMode) {
50
132
  const Custom = customConfig.EditPage;
@@ -54,24 +136,238 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
54
136
  const Custom = customConfig.CreatePage;
55
137
  return _jsx(Custom, { type: type });
56
138
  }
139
+ return (_jsx(MetadataResourceEditPageImpl, { type: type, name: name, createMode: createMode, embedded: embedded }));
140
+ }
141
+ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
142
+ const navigate = useNavigate();
143
+ const [searchParams, setSearchParams] = useSearchParams();
144
+ // ADR-0048 — the owning package of the item being edited, carried on the
145
+ // edit URL as `?package=` (emitted by the metadata list links). Scopes the
146
+ // layered/draft read so a same-name collision resolves to the right
147
+ // package's item. NOT the active Studio app's package — Studio edits items
148
+ // across all installed packages.
149
+ const ownerPackageId = searchParams.get('package') ?? undefined;
150
+ const client = useMetadataClient();
151
+ const { entries } = useMetadataTypes(client);
152
+ const entry = entries.find((t) => t.type === type);
153
+ const config = resolveResourceConfig(type, entry);
154
+ // Hoist `schema` to the top: it's a pure derivation of entry/config
155
+ // and several create-mode hooks below need it. Keeping it down here
156
+ // would put those hooks *after* the loading early-return, which
157
+ // breaks the rules of hooks when navigating new→edit (a different
158
+ // number of hooks runs across renders of the same instance).
159
+ const schema = (createMode && config.createSchema
160
+ ? config.createSchema
161
+ : entry?.schema) ??
162
+ config.defaultSchema;
163
+ const locale = React.useMemo(() => detectLocale(), []);
57
164
  const [layered, setLayered] = React.useState(null);
58
- const [draft, setDraft] = React.useState(createMode ? { name: '' } : {});
165
+ const identityField = config.identityField ?? 'name';
166
+ const [draft, setDraft] = React.useState(() => createMode ? { ...(config.createDefaults ?? {}), [identityField]: '' } : {});
59
167
  const [refs, setRefs] = React.useState(null);
60
168
  const [loading, setLoading] = React.useState(!createMode);
61
169
  const [saving, setSaving] = React.useState(false);
62
170
  const [error, setError] = React.useState(null);
171
+ // Distinguishes "the layered/draft fetch itself failed" (network/500/
172
+ // timeout) from "we loaded an item that fails validation". Without it a
173
+ // failed load renders the form with empty defaults and the client
174
+ // validator fires spurious "name/label/regions required" diagnostics,
175
+ // making a transport failure look like a structurally broken item. Set
176
+ // in the load catch block, reset at the start of each load.
177
+ const [loadFailed, setLoadFailed] = React.useState(false);
63
178
  const [issues, setIssues] = React.useState([]);
179
+ // In create mode, hold back validation noise until the author has actually
180
+ // edited a field. A blank new-item form firing 3 red "required" errors before
181
+ // the user types anything reads as broken, not helpful (the save path still
182
+ // validates). Flips true on the first real edit.
183
+ const [createDirty, setCreateDirty] = React.useState(false);
184
+ // Wrap setDraft so that editing a field clears any *server-side*
185
+ // diagnostic issues whose path begins with that field. The user
186
+ // gets immediate visual feedback — the red ring disappears as
187
+ // they type — and the form re-validates on save. We diff at the
188
+ // top-level segment, which matches how Zod's `issue.path[0]`
189
+ // identifies the offending field.
190
+ const handleDraftChange = React.useCallback((next) => {
191
+ setDraft((prev) => {
192
+ const resolved = typeof next === 'function' ? next(prev) : next;
193
+ const changed = new Set();
194
+ const keys = new Set([...Object.keys(prev ?? {}), ...Object.keys(resolved ?? {})]);
195
+ for (const k of keys) {
196
+ if (!Object.is(prev?.[k], resolved?.[k]))
197
+ changed.add(k);
198
+ }
199
+ if (changed.size > 0) {
200
+ setCreateDirty(true);
201
+ setIssues((prevIssues) => prevIssues.filter((i) => {
202
+ const head = (i.path ?? '').split('.')[0];
203
+ return !changed.has(head);
204
+ }));
205
+ }
206
+ return resolved;
207
+ });
208
+ }, []);
64
209
  const [destructiveIssues, setDestructiveIssues] = React.useState(null);
65
210
  const [pendingItem, setPendingItem] = React.useState(null);
211
+ // ── Create-mode form harness ──────────────────────────────────────
212
+ //
213
+ // Apply the registry's `createDerive` rules live (label→name slug,
214
+ // singular→plural, etc.). The hook is a no-op when not in create
215
+ // mode or when no rules are declared, so we always mount it.
216
+ const onCreatePatch = React.useCallback((patch) => {
217
+ handleDraftChange((d) => ({ ...d, ...patch }));
218
+ }, [handleDraftChange]);
219
+ const { markTouched: markCreateFieldTouched } = useCreateDerive({
220
+ rules: config.createDerive,
221
+ draft,
222
+ onPatch: onCreatePatch,
223
+ enabled: !!createMode,
224
+ });
225
+ // Effective hidden-fields for create mode: collapse the form to just
226
+ // the identity inputs declared by the type (or required-fields ∪
227
+ // label/name as a sensible default). Edit mode keeps the full form.
228
+ //
229
+ // The complement-set is what SchemaForm consumes (it hides paths
230
+ // listed in `hiddenFields`), so we invert the allowlist here.
231
+ const createFieldList = React.useMemo(() => {
232
+ if (!createMode)
233
+ return undefined;
234
+ if (config.createFields && config.createFields.length > 0)
235
+ return config.createFields;
236
+ const props = schema?.properties ?? undefined;
237
+ const required = schema?.required ?? undefined;
238
+ return deriveDefaultCreateFields(props, required);
239
+ }, [createMode, config.createFields, schema]);
240
+ const effectiveHiddenFields = React.useMemo(() => {
241
+ // Keys edited on the canvas (fields, fieldGroups) are never shown in
242
+ // the inspector's SchemaForm fallback — otherwise deselecting reveals
243
+ // a raw JSON editor for data the canvas already owns.
244
+ const canvasOwned = CANVAS_OWNED_KEYS[type] ?? [];
245
+ if (!createMode || !createFieldList) {
246
+ if (canvasOwned.length === 0)
247
+ return config.hiddenFields;
248
+ return Array.from(new Set([...(config.hiddenFields ?? []), ...canvasOwned]));
249
+ }
250
+ const props = schema?.properties ?? {};
251
+ const allow = new Set(createFieldList);
252
+ const hidden = Object.keys(props).filter((k) => !allow.has(k));
253
+ // Preserve any registry-declared `hiddenFields` too — they remain
254
+ // hidden in create mode even if they appeared in `createFields`.
255
+ if (config.hiddenFields) {
256
+ for (const k of config.hiddenFields)
257
+ if (!hidden.includes(k))
258
+ hidden.push(k);
259
+ }
260
+ // Canvas-owned keys are hidden regardless of the create allowlist.
261
+ for (const k of canvasOwned)
262
+ if (!hidden.includes(k))
263
+ hidden.push(k);
264
+ return hidden;
265
+ }, [createMode, createFieldList, schema, config.hiddenFields, type]);
266
+ const effectiveFieldOrder = React.useMemo(() => {
267
+ if (createMode && createFieldList)
268
+ return createFieldList;
269
+ return config.fieldOrder;
270
+ }, [createMode, createFieldList, config.fieldOrder]);
271
+ // Mark a top-level field as user-touched so create-mode derivations
272
+ // (label→name slug, etc.) leave it alone going forward. Wraps the
273
+ // standard onChange so the rest of the form is unaffected.
274
+ const handleCreateAwareChange = React.useCallback((next) => {
275
+ if (createMode) {
276
+ const before = draft;
277
+ const resolved = typeof next === 'function' ? next(before) : next;
278
+ const keys = new Set([...Object.keys(before ?? {}), ...Object.keys(resolved ?? {})]);
279
+ for (const k of keys) {
280
+ if (!Object.is(before?.[k], resolved?.[k]))
281
+ markCreateFieldTouched(k);
282
+ }
283
+ }
284
+ handleDraftChange(next);
285
+ }, [createMode, draft, handleDraftChange, markCreateFieldTouched]);
286
+ // Live client-side Zod validation. Debounced 200ms so we don't run
287
+ // on every keystroke through a complex AutoForm tree. When a client
288
+ // schema exists for `type` (spec 7.x exports per-type schemas under
289
+ // /data, /ui, /automation, /ai, /system, /kernel), we replace the
290
+ // `issues` state with Zod's output — same schemas the server runs,
291
+ // so behavior matches the post-save diagnostics but appears live.
292
+ // Types without a client schema keep the existing server-only flow.
293
+ React.useEffect(() => {
294
+ if (!hasClientValidator(type))
295
+ return;
296
+ let cancelled = false;
297
+ const handle = window.setTimeout(() => {
298
+ // Pass the live server schema so the client never flags fields the
299
+ // running server now treats as optional (cross-repo spec-skew root-cure).
300
+ void validateMetadataDraft(type, draft, entry?.schema).then((res) => {
301
+ if (cancelled)
302
+ return;
303
+ setIssues(res.issues);
304
+ });
305
+ }, 200);
306
+ return () => {
307
+ cancelled = true;
308
+ window.clearTimeout(handle);
309
+ };
310
+ }, [type, draft, entry?.schema]);
311
+ // Issues to DISPLAY (banner + inline). Suppressed on a pristine create form
312
+ // so a blank new item doesn't open covered in required-field errors.
313
+ const displayIssues = React.useMemo(() => (createMode && !createDirty ? [] : issues), [createMode, createDirty, issues]);
314
+ // Per-item draft pending publish (mode=draft saves land here).
315
+ // When non-null, the editor is "viewing the draft" and we surface
316
+ // Publish / Discard-draft actions.
317
+ const [hasDraft, setHasDraft] = React.useState(false);
318
+ const [publishing, setPublishing] = React.useState(false);
319
+ // Bumped by destructive operations (rollback / discard-draft) to
320
+ // force the load effect to refetch layered + draft state.
321
+ const [reloadKey, setReloadKey] = React.useState(0);
66
322
  // Form edit mode. The form is read-only by default — admins land in a
67
323
  // "view" state and must click Edit to mutate, mirroring the Salesforce /
68
324
  // Notion convention. createMode is always editing (you can't view what
69
325
  // doesn't exist yet). Truly read-only types (no allowOrgOverride) stay
70
326
  // read-only regardless.
71
327
  const [editing, setEditing] = React.useState(!!createMode);
328
+ // Currently selected sub-element (e.g. a dashboard widget). The
329
+ // preview emits this; the inspector consumes it. Must live above
330
+ // any early returns to preserve hook order — reset on item
331
+ // navigation or when leaving edit mode below.
332
+ const [selection, setSelection] = React.useState(null);
333
+ React.useEffect(() => {
334
+ setSelection(null);
335
+ }, [type, name]);
336
+ React.useEffect(() => {
337
+ if (!editing)
338
+ setSelection(null);
339
+ }, [editing]);
72
340
  // Snapshot of the last saved draft. Used by Cancel to revert in-flight
73
341
  // edits, and as the source-of-truth when entering edit mode.
74
342
  const draftSnapshotRef = React.useRef(null);
343
+ // Last successful save timestamp — surfaced as "Saved HH:MM" indicator
344
+ // next to the icon-only Save button.
345
+ const [lastSavedAt, setLastSavedAt] = React.useState(null);
346
+ // Auto-save toggle, persisted per-browser. Defaults to on for an
347
+ // "it just works" experience; users can disable it from the toolbar.
348
+ const [autoSaveEnabled, setAutoSaveEnabled] = React.useState(() => {
349
+ if (typeof window === 'undefined')
350
+ return true;
351
+ try {
352
+ const v = window.localStorage.getItem('metadata-admin:autosave');
353
+ return v === null ? true : v === '1';
354
+ }
355
+ catch {
356
+ return true;
357
+ }
358
+ });
359
+ React.useEffect(() => {
360
+ try {
361
+ window.localStorage.setItem('metadata-admin:autosave', autoSaveEnabled ? '1' : '0');
362
+ }
363
+ catch {
364
+ /* ignore */
365
+ }
366
+ }, [autoSaveEnabled]);
367
+ // Tracks the last draft snapshot we attempted to auto-save, so a
368
+ // validation failure does not loop on the same payload — auto-save
369
+ // only retries once the user mutates the draft again.
370
+ const lastAutoSaveSnapshotRef = React.useRef(null);
75
371
  // Prefetch object name list once — fuels the `ref:object` widget.
76
372
  // We don't block render on it; the widget shows a "Loading…" state.
77
373
  const [objectNames, setObjectNames] = React.useState([]);
@@ -98,7 +394,99 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
98
394
  cancelled = true;
99
395
  };
100
396
  }, [client]);
101
- const widgetContext = React.useMemo(() => ({ objectNames, objectsLoading }), [objectNames, objectsLoading]);
397
+ // Field catalog of the draft's bound/source object fuels field-picker
398
+ // widgets (e.g. the interface-page filter-mode selector). For a page the
399
+ // source is `interfaceConfig.source` (interface mode) or the bound
400
+ // `object`; other types fall back to their own `object`/`objectName`.
401
+ const sourceObjectName = draft?.interfaceConfig?.source ||
402
+ draft?.object ||
403
+ draft?.objectName;
404
+ const [objectFields, setObjectFields] = React.useState([]);
405
+ const [objectFieldsLoading, setObjectFieldsLoading] = React.useState(false);
406
+ // Action catalog of the source object — fuels the `action-multi` picker so
407
+ // interface-page `buttons` reference the object's real actions.
408
+ const [objectActions, setObjectActions] = React.useState([]);
409
+ React.useEffect(() => {
410
+ let cancelled = false;
411
+ if (!sourceObjectName) {
412
+ setObjectFields([]);
413
+ setObjectActions([]);
414
+ return;
415
+ }
416
+ setObjectFieldsLoading(true);
417
+ (async () => {
418
+ try {
419
+ const obj = (await client.get('object', sourceObjectName));
420
+ if (cancelled)
421
+ return;
422
+ const raw = obj?.fields;
423
+ const list = Array.isArray(raw)
424
+ ? raw.map((f) => ({ name: f?.name, label: f?.label, type: f?.type }))
425
+ : raw && typeof raw === 'object'
426
+ ? Object.entries(raw).map(([name, f]) => ({ name, label: f?.label, type: f?.type }))
427
+ : [];
428
+ setObjectFields(list.filter((f) => !!f.name));
429
+ const rawActions = obj?.actions;
430
+ const acts = Array.isArray(rawActions)
431
+ ? rawActions.map((a) => ({ name: a?.name, label: a?.label, locations: a?.locations })).filter((a) => !!a.name)
432
+ : [];
433
+ if (!cancelled)
434
+ setObjectActions(acts);
435
+ }
436
+ catch {
437
+ if (!cancelled) {
438
+ setObjectFields([]);
439
+ setObjectActions([]);
440
+ }
441
+ }
442
+ finally {
443
+ if (!cancelled)
444
+ setObjectFieldsLoading(false);
445
+ }
446
+ })();
447
+ return () => { cancelled = true; };
448
+ }, [client, sourceObjectName]);
449
+ // View catalog of the source object — fuels the `view-ref` picker for
450
+ // `interfaceConfig.sourceView` so the author chooses an existing view
451
+ // instead of typing (and mistyping) a name. Views are standalone metadata
452
+ // keyed to their object via `objectName`/`object`; the LIST endpoint returns
453
+ // name + label, which is all the picker needs.
454
+ const [objectViews, setObjectViews] = React.useState([]);
455
+ const [objectViewsLoading, setObjectViewsLoading] = React.useState(false);
456
+ React.useEffect(() => {
457
+ let cancelled = false;
458
+ if (!sourceObjectName) {
459
+ setObjectViews([]);
460
+ return;
461
+ }
462
+ setObjectViewsLoading(true);
463
+ (async () => {
464
+ try {
465
+ const all = (await client.list('view'));
466
+ if (cancelled)
467
+ return;
468
+ const forObject = (all || []).filter((v) => {
469
+ const obj = v?.objectName ?? v?.object ?? v?.object_name;
470
+ return obj === sourceObjectName;
471
+ });
472
+ const seen = new Set();
473
+ const list = forObject
474
+ .map((v) => ({ name: v?.name, label: v?.label || undefined }))
475
+ .filter((v) => !!v.name && !seen.has(v.name) && seen.add(v.name));
476
+ setObjectViews(list);
477
+ }
478
+ catch {
479
+ if (!cancelled)
480
+ setObjectViews([]);
481
+ }
482
+ finally {
483
+ if (!cancelled)
484
+ setObjectViewsLoading(false);
485
+ }
486
+ })();
487
+ return () => { cancelled = true; };
488
+ }, [client, sourceObjectName]);
489
+ const widgetContext = React.useMemo(() => ({ objectNames, objectsLoading, objectFields, objectFieldsLoading, objectViews, objectViewsLoading, objectActions }), [objectNames, objectsLoading, objectFields, objectFieldsLoading, objectViews, objectViewsLoading, objectActions]);
102
490
  // Load layered view + initial draft.
103
491
  React.useEffect(() => {
104
492
  if (createMode) {
@@ -108,21 +496,70 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
108
496
  let cancelled = false;
109
497
  setLoading(true);
110
498
  setError(null);
499
+ setLoadFailed(false);
111
500
  (async () => {
112
501
  try {
113
- const lay = await client.layered(type, name);
502
+ const scope = ownerPackageId ? { packageId: ownerPackageId } : {};
503
+ const [lay, draftResp] = await Promise.all([
504
+ client.layered(type, name, scope),
505
+ // Draft reads are best-effort — a 404/error must not block
506
+ // the page; readers without overlay-write permission still
507
+ // see the published item.
508
+ client.getDraft(type, name, scope).catch(() => null),
509
+ ]);
114
510
  if (cancelled)
115
511
  return;
116
512
  setLayered(lay);
117
- // Initial draft = effective if available, otherwise code.
118
- const initial = (lay.effective ?? lay.code ?? {});
513
+ // Surface server-computed load-time validation errors as inline
514
+ // SchemaForm issues operators see what's wrong with the
515
+ // saved metadata immediately, not just on the next Save round-trip.
516
+ const loadDiag = lay?._diagnostics;
517
+ if (loadDiag && loadDiag.valid === false && Array.isArray(loadDiag.errors)) {
518
+ setIssues(loadDiag.errors.map((e) => ({
519
+ path: e.path || '',
520
+ message: e.message,
521
+ })));
522
+ }
523
+ else {
524
+ setIssues([]);
525
+ }
526
+ // Draft envelope from the framework is `{ type, name, item }`;
527
+ // an empty/missing item means "no pending draft".
528
+ const draftReal = extractDraftBody(draftResp);
529
+ // Prefer the pending draft as the editing baseline — the
530
+ // operator is mid-flight on this item and should see their
531
+ // own in-progress state, not the last published version.
532
+ // A pending draft overlay can carry only the edited fields, so using
533
+ // it wholesale would drop inherited fields that were never touched —
534
+ // notably `type`, which section-level `visibleOn` predicates depend on
535
+ // (ADR-0047 hides Data Context / Layout when `data.type == 'list'`).
536
+ // Merge the draft over the effective baseline so those fields survive;
537
+ // the draft still wins for anything it does carry.
538
+ const baseline = (lay.effective ?? lay.code ?? {});
539
+ const rawInitial = draftReal
540
+ ? { ...baseline, ...draftReal }
541
+ : baseline;
542
+ // Normalise the wire shape into the editor's draft shape (e.g.
543
+ // `view` unwraps an expanded ViewItem's `config` into a
544
+ // `{ list | form }` family key). No-op for types without a hook.
545
+ const initial = config.toDraft ? config.toDraft(rawInitial) : rawInitial;
119
546
  setDraft(initial);
120
547
  draftSnapshotRef.current = initial;
548
+ setHasDraft(!!draftReal);
121
549
  setLoading(false);
122
550
  }
123
551
  catch (err) {
124
552
  if (!cancelled) {
125
- setError(err?.message ?? String(err));
553
+ // A failed fetch is a LOAD error, not a validation error: flag it
554
+ // so the diagnostics banner suppresses the spurious required-field
555
+ // issues the empty-default form would otherwise produce, and make
556
+ // the top error banner explicit about what actually went wrong.
557
+ setLoadFailed(true);
558
+ setError(tFormat('engine.edit.loadFailed', locale, {
559
+ type,
560
+ name: name ?? '',
561
+ message: err?.message ?? String(err),
562
+ }));
126
563
  setLoading(false);
127
564
  }
128
565
  }
@@ -130,8 +567,8 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
130
567
  return () => {
131
568
  cancelled = true;
132
569
  };
133
- }, [client, type, name, createMode]);
134
- // Lazy-load references when the tab is opened.
570
+ }, [client, type, name, ownerPackageId, createMode, reloadKey, locale]);
571
+ // Lazy-load references the first time the References sheet opens.
135
572
  const [refsLoading, setRefsLoading] = React.useState(false);
136
573
  async function loadReferences() {
137
574
  if (refs != null || refsLoading)
@@ -160,13 +597,202 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
160
597
  // the parent payload to materialise) so we only restore metadata
161
598
  // targets here.
162
599
  const initialTabRef = React.useRef(null);
600
+ const [openSheet, setOpenSheet] = React.useState(null);
601
+ // ADR-0033 Phase B — `?review=1` arrival (from the chat's "Review N change(s)"
602
+ // affordance). The AI may have drafted this item *after* the page mounted, so
603
+ // we first force a fresh fetch, then — once the draft is loaded — open the
604
+ // generic review/diff sheet and consume the query param (so a refresh/back
605
+ // doesn't re-trigger it). The same-item-already-open case is covered by the
606
+ // reload bump (the load effect keys off `reloadKey`, not the search string).
607
+ const reviewParam = searchParams.get('review');
608
+ const reviewBumpedRef = React.useRef(false);
609
+ React.useEffect(() => {
610
+ if (reviewParam !== '1' || createMode)
611
+ return;
612
+ if (!reviewBumpedRef.current) {
613
+ reviewBumpedRef.current = true;
614
+ setReloadKey((k) => k + 1);
615
+ return; // wait for the reload to settle before reading hasDraft
616
+ }
617
+ if (!loading) {
618
+ if (hasDraft)
619
+ setOpenSheet('review');
620
+ const next = new URLSearchParams(searchParams);
621
+ next.delete('review');
622
+ setSearchParams(next, { replace: true });
623
+ reviewBumpedRef.current = false;
624
+ }
625
+ // eslint-disable-next-line react-hooks/exhaustive-deps
626
+ }, [reviewParam, createMode, loading, hasDraft]);
627
+ // Inspector tabs: properties form vs raw JSON source view. Source view
628
+ // is for power users who need to edit fields the form doesn't expose
629
+ // (e.g. nested arrays). Tracked locally — not persisted between
630
+ // navigations since most users live in the form 99% of the time.
631
+ const [inspectorTab, setInspectorTab] = React.useState('properties');
632
+ // When the References sheet opens, lazy-load the data (idempotent).
633
+ // Also keep the URL `?tab=` query in sync so deep-links round-trip.
634
+ React.useEffect(() => {
635
+ if (openSheet === 'references') {
636
+ void loadReferences();
637
+ }
638
+ if (typeof window !== 'undefined' && !embedded) {
639
+ const url = new URL(window.location.href);
640
+ if (openSheet)
641
+ url.searchParams.set('tab', openSheet);
642
+ else
643
+ url.searchParams.delete('tab');
644
+ window.history.replaceState({}, '', url.toString());
645
+ }
646
+ // eslint-disable-next-line react-hooks/exhaustive-deps
647
+ }, [openSheet, embedded]);
648
+ // Designer-style split-panel state. The inspector (right form panel)
649
+ // can collapse to give the preview the full canvas. The collapsed
650
+ // state is persisted in localStorage so the user's preference sticks
651
+ // across navigations.
652
+ const inspectorStorageKey = 'metadata-edit:inspector-collapsed';
653
+ const inspectorSizeStorageKey = 'metadata-edit:inspector-size';
654
+ const [inspectorCollapsed, setInspectorCollapsed] = React.useState(() => {
655
+ if (typeof window === 'undefined')
656
+ return false;
657
+ return window.localStorage.getItem(inspectorStorageKey) === '1';
658
+ });
659
+ // Remember the user's preferred inspector size so collapsing then
660
+ // re-expanding restores it instead of leaving a sliver. react-resizable-
661
+ // panels' built-in expand() returns to the size right before collapse
662
+ // which is often near 0, hence the explicit memory.
663
+ const lastInspectorSizeRef = React.useRef(38);
664
+ // Hydrate from localStorage on mount.
665
+ React.useEffect(() => {
666
+ if (typeof window === 'undefined')
667
+ return;
668
+ const v = Number(window.localStorage.getItem(inspectorSizeStorageKey));
669
+ if (Number.isFinite(v) && v >= 22 && v <= 80) {
670
+ lastInspectorSizeRef.current = v;
671
+ }
672
+ // eslint-disable-next-line react-hooks/exhaustive-deps
673
+ }, []);
674
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
675
+ const inspectorPanelRef = React.useRef(null);
676
+ const toggleInspector = React.useCallback(() => {
677
+ setInspectorCollapsed((prev) => {
678
+ const next = !prev;
679
+ if (typeof window !== 'undefined') {
680
+ window.localStorage.setItem(inspectorStorageKey, next ? '1' : '0');
681
+ }
682
+ return next;
683
+ });
684
+ }, []);
685
+ // Drive the imperative panel resize from a state-change effect rather
686
+ // than inside the setter — the latter runs before React has committed
687
+ // the new state and react-resizable-panels can race with its own
688
+ // onResize observer, producing tiny re-expanded sizes.
689
+ // ⚠️ resize() treats numeric values as **pixels**; pass a string to
690
+ // get a percentage. resize(38) → 38px (~2.7%); resize('38%') → 38%.
691
+ React.useEffect(() => {
692
+ const handle = inspectorPanelRef.current;
693
+ if (!handle)
694
+ return;
695
+ if (inspectorCollapsed) {
696
+ handle.resize?.('0%');
697
+ }
698
+ else {
699
+ const target = lastInspectorSizeRef.current || 38;
700
+ handle.resize?.(`${target}%`);
701
+ }
702
+ }, [inspectorCollapsed]);
703
+ // Canvas-local UX state — preview-only view (hides design chrome
704
+ // without dropping dirty edits) and fullscreen (canvas takes over the
705
+ // viewport so designers can focus). Both are session-scoped.
706
+ const [previewOnly, setPreviewOnly] = React.useState(false);
707
+ const [isFullscreen, setIsFullscreen] = React.useState(false);
708
+ // Lock body scroll while fullscreen so the underlying page can't peek
709
+ // through and the user's scroll position is preserved on exit.
710
+ React.useEffect(() => {
711
+ if (typeof document === 'undefined')
712
+ return;
713
+ if (!isFullscreen)
714
+ return;
715
+ const prev = document.body.style.overflow;
716
+ document.body.style.overflow = 'hidden';
717
+ return () => {
718
+ document.body.style.overflow = prev;
719
+ };
720
+ }, [isFullscreen]);
721
+ // Escape exits fullscreen.
722
+ React.useEffect(() => {
723
+ if (typeof window === 'undefined' || !isFullscreen)
724
+ return;
725
+ function onKey(e) {
726
+ if (e.key === 'Escape') {
727
+ e.preventDefault();
728
+ setIsFullscreen(false);
729
+ }
730
+ }
731
+ window.addEventListener('keydown', onKey);
732
+ return () => window.removeEventListener('keydown', onKey);
733
+ }, [isFullscreen]);
734
+ // Auto-enable design mode for designer-capable types. We do this once
735
+ // per (type,name) navigation so the user lands in the productive
736
+ // state instead of having to click "Edit". Truly read-only types
737
+ // (canWrite=false) keep the old behavior. The check happens inside
738
+ // the effect to avoid hook-order issues with the early `loading`
739
+ // return below.
740
+ const designerAutoOnRef = React.useRef(null);
741
+ React.useEffect(() => {
742
+ designerAutoOnRef.current = null;
743
+ }, [type, name]);
744
+ React.useEffect(() => {
745
+ if (createMode || embedded || loading)
746
+ return;
747
+ const key = `${type}/${name ?? ''}`;
748
+ if (designerAutoOnRef.current === key)
749
+ return;
750
+ const PC = getMetadataPreview(type);
751
+ if (!PC)
752
+ return;
753
+ // See `isArtifactItem` below — a `sys_metadata`-tagged code layer is a
754
+ // published org object, NOT a packaged artifact, so it stays editable.
755
+ const isArtifact = layered?.code != null
756
+ && layered.code?._packageId !== 'sys_metadata';
757
+ const cw = isArtifact
758
+ ? !!entry?.allowOrgOverride
759
+ : !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate);
760
+ if (!cw)
761
+ return;
762
+ designerAutoOnRef.current = key;
763
+ setEditing(true);
764
+ }, [type, name, createMode, embedded, loading, entry, layered]);
765
+ // Keyboard shortcut: Cmd/Ctrl+\ toggles the inspector. This is the
766
+ // designer convention shared by Figma, VS Code (Cmd+B), Sketch — `\`
767
+ // sits next to Return so it's reachable one-handed.
768
+ React.useEffect(() => {
769
+ if (typeof window === 'undefined' || embedded)
770
+ return;
771
+ function onKey(e) {
772
+ const mod = e.metaKey || e.ctrlKey;
773
+ if (!mod || e.shiftKey || e.altKey)
774
+ return;
775
+ if (e.key !== '\\')
776
+ return;
777
+ // Ignore when typing in an editor (textarea / contenteditable).
778
+ const t = e.target;
779
+ if (t && (t.tagName === 'TEXTAREA' || t.isContentEditable))
780
+ return;
781
+ e.preventDefault();
782
+ toggleInspector();
783
+ }
784
+ window.addEventListener('keydown', onKey);
785
+ return () => window.removeEventListener('keydown', onKey);
786
+ }, [embedded, toggleInspector]);
163
787
  React.useEffect(() => {
164
788
  if (typeof window === 'undefined' || embedded)
165
789
  return;
166
790
  const sp = new URLSearchParams(window.location.search);
167
791
  const tab = sp.get('tab');
168
- if (tab)
169
- initialTabRef.current = tab;
792
+ if (tab === 'layers' || tab === 'references' || tab === 'related' || tab === 'audit') {
793
+ setOpenSheet(tab);
794
+ }
795
+ initialTabRef.current = tab;
170
796
  const open = sp.get('open');
171
797
  if (open && open.includes(':')) {
172
798
  const [t, n] = open.split(':', 2);
@@ -189,36 +815,140 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
189
815
  }
190
816
  window.history.replaceState({}, '', url.toString());
191
817
  }, [relatedTarget, embedded]);
818
+ function labelForIssuePath(path) {
819
+ const key = path.split('.')[0];
820
+ if (!key)
821
+ return path;
822
+ // Resolve the human label for the HEAD segment from the form/schema.
823
+ const headLabel = (() => {
824
+ const formForLabels = (createMode && config.createSchema ? undefined : entry?.form);
825
+ const sections = Array.isArray(formForLabels?.sections) ? formForLabels.sections : [];
826
+ for (const section of sections) {
827
+ const fields = Array.isArray(section?.fields) ? section.fields : [];
828
+ for (const field of fields) {
829
+ if (typeof field === 'string') {
830
+ if (field === key)
831
+ return field;
832
+ }
833
+ else if (field?.field === key) {
834
+ return String(field.label ?? key);
835
+ }
836
+ }
837
+ }
838
+ const props = (schema?.properties ?? {});
839
+ return String(props[key]?.title ?? key);
840
+ })();
841
+ // For a NESTED path (e.g. `widgets.2.layout`) append a readable trail naming
842
+ // the offending element + sub-field, so a terse "Widgets: Invalid input"
843
+ // becomes "Widgets → priority_split → layout".
844
+ return describeIssuePath(headLabel, path, draft);
845
+ }
192
846
  async function doSave(force) {
193
847
  setSaving(true);
194
848
  setError(null);
195
849
  setIssues([]);
196
850
  try {
197
- // Ensure `name` is set on create.
851
+ // Ensure identity is set on create, and that any `createDefaults`
852
+ // / `createBuildBody` shape (e.g. `{ fields: {} }` for object,
853
+ // or `{ list: { data: { object } } }` for view) is present so
854
+ // the saved body satisfies its JSONSchema. User-supplied values
855
+ // always win over the defaults.
856
+ let builtBody = createMode
857
+ ? (config.createBuildBody
858
+ ? config.createBuildBody(draft)
859
+ : { ...(config.createDefaults ?? {}), ...draft })
860
+ // Edit mode: serialise the editor draft back to the wire shape
861
+ // (inverse of `toDraft` — e.g. `view` folds the `{ list | form }`
862
+ // family key back into the ViewItem `config` wrapper).
863
+ : (config.fromDraft ? config.fromDraft(draft) : draft);
864
+ // Async create-time augmentation (e.g. seed a record page's regions from
865
+ // the bound object's synthesized default). Best-effort — a failure leaves
866
+ // the un-augmented body. User/builder-supplied keys win over the seed.
867
+ if (createMode && config.createSeed) {
868
+ try {
869
+ const seeded = await config.createSeed(draft, { client });
870
+ if (seeded && typeof seeded === 'object') {
871
+ // Seed wins over the empty defaults (`builtBody` already folded the
872
+ // user's draft in, which only carries default-empty `regions`).
873
+ builtBody = { ...builtBody, ...seeded };
874
+ }
875
+ }
876
+ catch { /* seed is best-effort; proceed with the un-augmented body */ }
877
+ }
878
+ const savedName = String(builtBody[identityField] ?? draft[identityField] ?? name);
198
879
  const itemToSave = createMode
199
- ? { ...draft, name: String(draft.name ?? name) }
200
- : draft;
201
- const savedName = String(itemToSave.name ?? name);
880
+ ? { ...builtBody, [identityField]: savedName }
881
+ : builtBody;
202
882
  if (!savedName) {
203
- setError('A name is required.');
883
+ setError(t('engine.validation.nameRequired', locale));
204
884
  setSaving(false);
205
885
  return;
206
886
  }
207
- const result = await client.save(type, savedName, itemToSave, { force });
208
- // Refresh layered after save.
209
- const lay = await client.layered(type, savedName);
887
+ // Save lands in the draft buffer the runtime keeps serving the
888
+ // last published version until the operator clicks Publish. The
889
+ // backend defaults to publish mode for backward-compatibility, so
890
+ // Studio must opt into draft explicitly.
891
+ // Bind to the active software package (sys_metadata.package_id) when a
892
+ // real package scope is carried in the URL (`?package=`). The backend
893
+ // stamps it on create and preserves an existing binding on update, so
894
+ // env-local overlays (no `?package=`) are unaffected.
895
+ const activePackage = (() => {
896
+ try {
897
+ const p = new URLSearchParams(window.location.search).get('package');
898
+ return p && p !== 'all' ? p : undefined;
899
+ }
900
+ catch {
901
+ return undefined;
902
+ }
903
+ })();
904
+ await client.save(type, savedName, itemToSave, {
905
+ force,
906
+ mode: 'draft',
907
+ ...(activePackage ? { packageId: activePackage } : {}),
908
+ });
909
+ // Refresh layered + draft state after save — scope to the same package
910
+ // as the initial load (ADR-0048) so a same-name collision re-reads this
911
+ // package's own row, not another's.
912
+ const refreshScope = ownerPackageId ? { packageId: ownerPackageId } : {};
913
+ const [lay, draftResp] = await Promise.all([
914
+ client.layered(type, savedName, refreshScope),
915
+ client.getDraft(type, savedName, refreshScope).catch(() => null),
916
+ ]);
210
917
  setLayered(lay);
211
- const fresh = (lay.effective ?? itemToSave);
918
+ const draftReal = extractDraftBody(draftResp);
919
+ setHasDraft(!!draftReal);
920
+ // Merge the draft over the effective baseline (see the load effect):
921
+ // a partial draft overlay must not drop inherited fields like `type`.
922
+ const freshBaseline = (lay.effective ?? itemToSave);
923
+ const rawFresh = draftReal
924
+ ? { ...freshBaseline, ...draftReal }
925
+ : freshBaseline;
926
+ // Re-normalise the refreshed wire shape so the editor keeps showing
927
+ // the canonical draft shape after a save (e.g. the backend re-expands
928
+ // a view into the ViewItem `config` wrapper).
929
+ const fresh = config.toDraft ? config.toDraft(rawFresh) : rawFresh;
212
930
  setDraft(fresh);
213
931
  draftSnapshotRef.current = fresh;
932
+ setLastSavedAt(new Date());
933
+ lastAutoSaveSnapshotRef.current = JSON.stringify(fresh);
214
934
  setDestructiveIssues(null);
215
935
  setPendingItem(null);
216
- // Exit edit mode on successful save (unless we were creating
217
- // navigation to the new record's URL will reset state anyway).
218
- if (!createMode)
936
+ // Stay in design mode after save for designer-capable types so the
937
+ // user keeps their inspector context. Non-designer types fall back
938
+ // to the previous "exit edit on save" UX.
939
+ const stayInEditing = !createMode && !!getMetadataPreview(type);
940
+ if (!createMode && !stayInEditing)
219
941
  setEditing(false);
220
942
  if (createMode) {
221
- navigate(`../${encodeURIComponent(savedName)}`);
943
+ // Preserve the active query string (notably `?package=…`) so the
944
+ // post-create navigation lands on the item in the SAME package the
945
+ // author was working in. Without this the param is dropped and the
946
+ // editor falls back to the user's default package, where the freshly
947
+ // saved draft doesn't exist — so it reloads a blank form.
948
+ const qs = searchParams.toString();
949
+ navigate(`../${encodeURIComponent(savedName)}${qs ? `?${qs}` : ''}`, {
950
+ relative: 'path',
951
+ });
222
952
  }
223
953
  }
224
954
  catch (err) {
@@ -233,7 +963,7 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
233
963
  const i = err?.body?.issues ?? [];
234
964
  let mapped = (Array.isArray(i) ? i : []).map((x) => ({
235
965
  path: Array.isArray(x.path) ? x.path.join('.') : String(x.path ?? ''),
236
- message: String(x.message ?? 'Invalid'),
966
+ message: translateValidationMessage(String(x.message ?? 'Invalid'), locale),
237
967
  }));
238
968
  // Backend's invalid_metadata sometimes returns a flat string like
239
969
  // "<type>/<name> failed spec validation: <path>: <message>".
@@ -243,10 +973,10 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
243
973
  if (mapped.length === 0 && raw) {
244
974
  const m = raw.match(/failed spec validation:\s*(.+?):\s*(.+)$/);
245
975
  if (m) {
246
- mapped = [{ path: m[1].trim(), message: m[2].trim() }];
976
+ mapped = [{ path: m[1].trim(), message: translateValidationMessage(m[2].trim(), locale) }];
247
977
  }
248
978
  else {
249
- mapped = [{ path: '', message: raw }];
979
+ mapped = [{ path: '', message: translateValidationMessage(raw, locale) }];
250
980
  }
251
981
  }
252
982
  setIssues(mapped);
@@ -254,10 +984,10 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
254
984
  setError(mapped[0].message);
255
985
  }
256
986
  else if (mapped.length === 1) {
257
- setError(`${mapped[0].path}: ${mapped[0].message}`);
987
+ setError(`${labelForIssuePath(mapped[0].path)}: ${mapped[0].message}`);
258
988
  }
259
989
  else {
260
- setError(`Validation failed (${mapped.length} issues).`);
990
+ setError(tFormat('engine.validation.failed', locale, { count: mapped.length }));
261
991
  }
262
992
  }
263
993
  else {
@@ -269,19 +999,96 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
269
999
  }
270
1000
  }
271
1001
  async function doReset() {
272
- if (!confirm(`Reset overlay for ${type}/${name}? Code-level value will be restored.`)) {
1002
+ // Two semantics:
1003
+ // - artifact-backed item: "Reset overlay" — keep the code default.
1004
+ // - DB-only item: "Delete" — the item disappears entirely (no
1005
+ // artifact baseline to fall back to). Navigate back to the list
1006
+ // since the current URL no longer refers to anything.
1007
+ const itemIsArtifact = !createMode && layered?.code != null;
1008
+ const confirmKey = itemIsArtifact
1009
+ ? 'engine.edit.resetConfirm'
1010
+ : 'engine.edit.deleteConfirm';
1011
+ if (!confirm(tFormat(confirmKey, locale, { type, name: name ?? '' }))) {
273
1012
  return;
274
1013
  }
275
1014
  setSaving(true);
276
1015
  setError(null);
277
1016
  try {
278
1017
  await client.reset(type, name);
1018
+ if (itemIsArtifact) {
1019
+ const lay = await client.layered(type, name);
1020
+ setLayered(lay);
1021
+ const fresh = (lay.effective ?? lay.code ?? {});
1022
+ setDraft(fresh);
1023
+ draftSnapshotRef.current = fresh;
1024
+ // Designer-capable types stay in design mode; allow the auto-on
1025
+ // effect to re-trigger after this reset.
1026
+ if (getMetadataPreview(type)) {
1027
+ designerAutoOnRef.current = null;
1028
+ }
1029
+ else {
1030
+ setEditing(false);
1031
+ }
1032
+ }
1033
+ else {
1034
+ // No artifact baseline → return to the list view.
1035
+ navigate(`../`, { relative: 'path' });
1036
+ }
1037
+ }
1038
+ catch (err) {
1039
+ setError(err?.message ?? String(err));
1040
+ }
1041
+ finally {
1042
+ setSaving(false);
1043
+ }
1044
+ }
1045
+ // Promote the pending draft to the active overlay. Mirrors `doSave`'s
1046
+ // refresh pattern so the editor stays in sync with the new baseline.
1047
+ async function doPublish() {
1048
+ setPublishing(true);
1049
+ setError(null);
1050
+ try {
1051
+ await client.publish(type, name);
1052
+ const [lay, draftResp] = await Promise.all([
1053
+ client.layered(type, name),
1054
+ client.getDraft(type, name).catch(() => null),
1055
+ ]);
1056
+ setLayered(lay);
1057
+ const draftReal = extractDraftBody(draftResp);
1058
+ setHasDraft(!!draftReal);
1059
+ // Merge the draft over the effective baseline so a partial draft overlay
1060
+ // doesn't drop inherited fields like `type` (section visibleOn depends
1061
+ // on it — ADR-0047).
1062
+ const freshBaseline = (lay.effective ?? draft);
1063
+ const fresh = draftReal
1064
+ ? { ...freshBaseline, ...draftReal }
1065
+ : freshBaseline;
1066
+ setDraft(fresh);
1067
+ draftSnapshotRef.current = fresh;
1068
+ }
1069
+ catch (err) {
1070
+ setError(err?.message ?? String(err));
1071
+ }
1072
+ finally {
1073
+ setPublishing(false);
1074
+ }
1075
+ }
1076
+ // Discard the pending draft (`DELETE ?state=draft`). The published
1077
+ // overlay is untouched; the editor reverts to showing the live body.
1078
+ async function doDiscardDraft() {
1079
+ if (!confirm(tFormat('engine.edit.discardDraftConfirm', locale, { type, name: name ?? '' }))) {
1080
+ return;
1081
+ }
1082
+ setSaving(true);
1083
+ setError(null);
1084
+ try {
1085
+ await client.reset(type, name, { state: 'draft' });
279
1086
  const lay = await client.layered(type, name);
280
1087
  setLayered(lay);
281
1088
  const fresh = (lay.effective ?? lay.code ?? {});
282
1089
  setDraft(fresh);
283
1090
  draftSnapshotRef.current = fresh;
284
- setEditing(false);
1091
+ setHasDraft(false);
285
1092
  }
286
1093
  catch (err) {
287
1094
  setError(err?.message ?? String(err));
@@ -290,18 +1097,214 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
290
1097
  setSaving(false);
291
1098
  }
292
1099
  }
1100
+ // Dirty detection: cheap structural comparison via JSON. The draft is
1101
+ // small (a single metadata record) so this is fine on each render.
1102
+ // Used to surface an "unsaved" indicator next to the Save button.
1103
+ // Must be declared BEFORE any early returns to preserve hook order.
1104
+ const isDirty = React.useMemo(() => {
1105
+ if (createMode)
1106
+ return Object.keys(draft).length > 0;
1107
+ const snap = draftSnapshotRef.current;
1108
+ if (!snap)
1109
+ return false;
1110
+ try {
1111
+ return JSON.stringify(draft) !== JSON.stringify(snap);
1112
+ }
1113
+ catch {
1114
+ return false;
1115
+ }
1116
+ }, [draft, createMode]);
1117
+ // Two-tier authorization (PR-10d.7) — hoisted above the early `loading`
1118
+ // return so the auto-save / keyboard / blocker effects below can read
1119
+ // them. Recomputed cheaply on every render.
1120
+ // - artifact-backed items (layered.code != null) need allowOrgOverride
1121
+ // - DB-only items (no artifact) need allowOrgOverride OR allowRuntimeCreate
1122
+ // - createMode is always writable (the server will gate on intent)
1123
+ // A non-null `code` layer alone is NOT proof of a code (artifact) package:
1124
+ // a published org object also surfaces its active version in `code`, but
1125
+ // tagged with the `sys_metadata` provenance sentinel. Mirror the server's
1126
+ // `isArtifactBacked` (which excludes `_packageId === 'sys_metadata'`) so an
1127
+ // org-authored object stays editable after publish instead of being mis-read
1128
+ // as a read-only packaged item.
1129
+ const isArtifactItem = !createMode
1130
+ && layered?.code != null
1131
+ && layered.code?._packageId !== 'sys_metadata';
1132
+ // ADR-0010 — server-computed lock flags. undefined means "no opinion"
1133
+ // (older server / non-lockable item) → preserve legacy behaviour.
1134
+ const lockEditable = layered?.editable !== false;
1135
+ const lockDeletable = layered?.deletable !== false;
1136
+ const lockResettable = layered?.resettable !== false;
1137
+ const lockReason = layered?.lockReason;
1138
+ const isLocked = layered?.lock && layered.lock !== 'none';
1139
+ const canWriteByType = createMode
1140
+ ? !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate)
1141
+ : isArtifactItem
1142
+ ? !!entry?.allowOrgOverride
1143
+ : !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate);
1144
+ const canWrite = canWriteByType && (createMode || lockEditable);
1145
+ const readOnly = !canWrite && !createMode;
1146
+ // Auto-save: debounce edits and persist silently once the user pauses
1147
+ // for AUTOSAVE_DEBOUNCE_MS. Skipped for create mode (need an explicit
1148
+ // name first), read-only forms, and while a save is already in flight.
1149
+ // We track the last attempted snapshot so a validation failure doesn't
1150
+ // loop on the same payload — the user has to mutate the draft again.
1151
+ const AUTOSAVE_DEBOUNCE_MS = 1500;
1152
+ // Keep doSave fresh inside the effect without re-arming the timer on
1153
+ // every render.
1154
+ const doSaveRef = React.useRef(doSave);
1155
+ React.useEffect(() => {
1156
+ doSaveRef.current = doSave;
1157
+ });
1158
+ React.useEffect(() => {
1159
+ if (!autoSaveEnabled)
1160
+ return;
1161
+ if (createMode || readOnly || !editing || !isDirty || saving)
1162
+ return;
1163
+ let snap;
1164
+ try {
1165
+ snap = JSON.stringify(draft);
1166
+ }
1167
+ catch {
1168
+ return;
1169
+ }
1170
+ if (snap === lastAutoSaveSnapshotRef.current)
1171
+ return;
1172
+ const handle = window.setTimeout(() => {
1173
+ lastAutoSaveSnapshotRef.current = snap;
1174
+ doSaveRef.current(false);
1175
+ }, AUTOSAVE_DEBOUNCE_MS);
1176
+ return () => window.clearTimeout(handle);
1177
+ }, [draft, isDirty, editing, saving, createMode, readOnly, autoSaveEnabled]);
1178
+ // Keyboard shortcut — ⌘S / Ctrl+S triggers save when dirty.
1179
+ React.useEffect(() => {
1180
+ const handler = (e) => {
1181
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
1182
+ if (!canWrite || readOnly)
1183
+ return;
1184
+ if (!editing && !createMode)
1185
+ return;
1186
+ e.preventDefault();
1187
+ if (!saving && (createMode || isDirty)) {
1188
+ doSaveRef.current(false);
1189
+ }
1190
+ }
1191
+ };
1192
+ window.addEventListener('keydown', handler);
1193
+ return () => window.removeEventListener('keydown', handler);
1194
+ }, [canWrite, readOnly, editing, createMode, saving, isDirty]);
1195
+ // Beforeunload guard — browser-native "leave site?" prompt when the
1196
+ // user closes the tab / reloads with unsaved changes.
1197
+ React.useEffect(() => {
1198
+ if (!isDirty)
1199
+ return;
1200
+ const handler = (e) => {
1201
+ e.preventDefault();
1202
+ // Required for Chrome to actually show the prompt.
1203
+ e.returnValue = '';
1204
+ };
1205
+ window.addEventListener('beforeunload', handler);
1206
+ return () => window.removeEventListener('beforeunload', handler);
1207
+ }, [isDirty]);
1208
+ // In-app navigation guard — intercept anchor / link clicks before the
1209
+ // router consumes them. Cheaper and more compatible than useBlocker,
1210
+ // which requires a data router (the host app uses BrowserRouter).
1211
+ React.useEffect(() => {
1212
+ if (!isDirty)
1213
+ return;
1214
+ const handler = (e) => {
1215
+ if (e.defaultPrevented)
1216
+ return;
1217
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
1218
+ return;
1219
+ const target = e.target;
1220
+ const anchor = target?.closest?.('a[href]');
1221
+ if (!anchor)
1222
+ return;
1223
+ // Allow new-tab / download / external links — they don't replace
1224
+ // the current page.
1225
+ if (anchor.target && anchor.target !== '_self')
1226
+ return;
1227
+ if (anchor.hasAttribute('download'))
1228
+ return;
1229
+ try {
1230
+ const url = new URL(anchor.href, window.location.href);
1231
+ if (url.origin !== window.location.origin)
1232
+ return;
1233
+ if (url.pathname === window.location.pathname)
1234
+ return;
1235
+ }
1236
+ catch {
1237
+ return;
1238
+ }
1239
+ if (!confirm(t('engine.edit.unsavedLeaveConfirm', locale))) {
1240
+ e.preventDefault();
1241
+ e.stopPropagation();
1242
+ }
1243
+ };
1244
+ document.addEventListener('click', handler, true);
1245
+ return () => document.removeEventListener('click', handler, true);
1246
+ }, [isDirty, locale]);
1247
+ // Publish "what's being edited" to the global AI chat so the agent can
1248
+ // act on the open item (and offer item-specific starter prompts). Kept
1249
+ // to a light summary — the agent can `describe_object` for full detail.
1250
+ // Declared above the early returns to satisfy the Rules of Hooks.
1251
+ const assistantEditorCtx = React.useMemo(() => {
1252
+ if (embedded)
1253
+ return null;
1254
+ const itemName = String(draft.name ?? name ?? '');
1255
+ if (!itemName)
1256
+ return null;
1257
+ const ctx = {
1258
+ type,
1259
+ name: itemName,
1260
+ label: typeof draft.label === 'string' ? draft.label : undefined,
1261
+ };
1262
+ if (type === 'object') {
1263
+ ctx.fields = readFields(draft.fields).entries.slice(0, 60).map((e) => ({
1264
+ name: e.name,
1265
+ type: typeof e.def.type === 'string' ? e.def.type : undefined,
1266
+ label: typeof e.def.label === 'string' ? e.def.label : undefined,
1267
+ required: !!e.def.required || undefined,
1268
+ }));
1269
+ }
1270
+ return ctx;
1271
+ }, [embedded, type, name, draft]);
1272
+ useRegisterAssistantEditor(assistantEditorCtx);
293
1273
  if (loading) {
294
1274
  return (_jsx(PageShell, { entry: entry, itemName: name, children: _jsxs("div", { className: "p-6 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading ", type, "/", name, "\u2026"] }) }));
295
1275
  }
296
- const schema = entry?.schema ??
297
- config.defaultSchema;
298
- const readOnly = !entry?.allowOrgOverride && !createMode;
299
- const DesignerTab = !createMode ? customConfig?.DesignerTab : undefined;
300
- const designerTabLabel = customConfig?.designerTabLabel ?? 'Designer';
1276
+ // `schema`, `createFieldList`, `effectiveHiddenFields`,
1277
+ // `effectiveFieldOrder`, and `handleCreateAwareChange` are all
1278
+ // hoisted to the top of the component (next to the rest of the
1279
+ // create-mode harness) to avoid placing hooks *after* the loading
1280
+ // early-return.
1281
+ // Banner variant: when type ships with allowRuntimeCreate but this
1282
+ // specific item is locked because it comes from a code package, we
1283
+ // show a different message inviting the user to create their own.
1284
+ const showArtifactLockedBanner = readOnly && isArtifactItem && !!entry?.allowRuntimeCreate;
301
1285
  // Preview tab — opt-in via `registerMetadataPreview()`. Hidden in
302
1286
  // create mode (nothing to preview yet) and inside the embedded
303
1287
  // drawer (the parent context owns the preview surface).
304
- const PreviewComponent = !createMode && !embedded ? getMetadataPreview(type) : undefined;
1288
+ //
1289
+ // Exception: a few types host their primary authoring surface IN the
1290
+ // canvas (object → field designer). For those we light the canvas up
1291
+ // during create too, so authors design fields immediately instead of
1292
+ // round-tripping through a save first. Object-level basics (name,
1293
+ // label, …) stay editable via the default inspector shown when no
1294
+ // field is selected, so naming still works before any field exists.
1295
+ const showPreviewInCreate = CREATE_MODE_CANVAS_TYPES.has(type);
1296
+ const PreviewComponent = !embedded && (!createMode || showPreviewInCreate)
1297
+ ? getMetadataPreview(type)
1298
+ : undefined;
1299
+ // Optional scoped inspector for the selected sub-element (e.g. a
1300
+ // dashboard widget). Registered separately via
1301
+ // `registerMetadataInspector()` so a type can opt in independently
1302
+ // of having a Preview, and so plugins can swap implementations.
1303
+ const InspectorComponent = getMetadataInspector(type);
1304
+ // Optional "home" inspector shown when there is NO selection, replacing
1305
+ // the generic whole-draft SchemaForm with a curated panel (e.g. the View
1306
+ // type + fields manager). Falls back to SchemaForm when unregistered.
1307
+ const DefaultInspectorComponent = getMetadataDefaultInspector(type);
305
1308
  // Cancel edits: revert the draft to the last saved snapshot and exit
306
1309
  // edit mode. Safe to call even with no snapshot (no-op).
307
1310
  function doCancelEdit() {
@@ -316,20 +1319,170 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
316
1319
  // read-only. createMode is always editing; truly read-only types
317
1320
  // (no allowOrgOverride) ignore the editing toggle entirely.
318
1321
  const formReadOnly = readOnly || (!editing && !createMode);
319
- // Default tab priority:
320
- // 1. URL ?tab= (explicit user nav / deep link)
321
- // 2. Designer (custom rich editor present)
322
- // 3. Preview (live preview present most informative landing)
323
- // 4. Form
324
- const defaultTab = initialTabRef.current ??
325
- (DesignerTab ? 'designer' : PreviewComponent ? 'preview' : 'form');
326
- return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? 'Create new' : 'Edit overlay', actions: _jsxs(_Fragment, { children: [!createMode && entry?.allowOrgOverride && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset overlay"] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(History, { className: "h-4 w-4 mr-1" }), "History"] })), entry?.allowOrgOverride && !createMode && !editing && (_jsxs(Button, { size: "sm", onClick: () => setEditing(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-1" }), "Edit"] })), entry?.allowOrgOverride && !createMode && editing && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doCancelEdit, disabled: saving, children: [_jsx(X, { className: "h-4 w-4 mr-1" }), "Cancel"] })), entry?.allowOrgOverride && (editing || createMode) && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), "Save"] }))] }), children: [_jsxs("div", { className: "p-6 space-y-6 max-w-7xl", children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), readOnly && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3", children: ["This type is read-only. To enable runtime editing, set", ' ', _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }), " to include ", _jsx("code", { className: "font-mono", children: type }), ", or flip", ' ', _jsx("code", { className: "font-mono", children: "allowOrgOverride" }), " in the registry."] })), _jsxs(Tabs, { defaultValue: defaultTab, className: "w-full", onValueChange: (v) => {
327
- if (typeof window === 'undefined' || embedded)
328
- return;
329
- const url = new URL(window.location.href);
330
- url.searchParams.set('tab', v);
331
- window.history.replaceState({}, '', url.toString());
332
- }, children: [_jsxs(TabsList, { children: [DesignerTab && (_jsx(TabsTrigger, { value: "designer", children: designerTabLabel })), PreviewComponent && (_jsxs(TabsTrigger, { value: "preview", children: [_jsx(Eye, { className: "h-3.5 w-3.5 mr-1" }), "Preview"] })), _jsx(TabsTrigger, { value: "form", children: "Detail" }), !createMode && (_jsxs(TabsTrigger, { value: "layers", children: ["Layers", layered?.overlay && (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: "overlay" }))] })), !createMode && (_jsxs(TabsTrigger, { value: "references", onClick: loadReferences, children: [_jsx(Link2, { className: "h-3.5 w-3.5 mr-1" }), "References", refs && (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: refs.length }))] })), hasAnchors && (_jsxs(TabsTrigger, { value: "related", children: [_jsx(Layers3, { className: "h-3.5 w-3.5 mr-1" }), "Related"] }))] }), DesignerTab && (_jsx(TabsContent, { value: "designer", className: "mt-4", children: _jsx(DesignerTab, { type: type, name: name }) })), _jsxs(TabsContent, { value: "form", className: "mt-4 space-y-3", children: [formReadOnly && !readOnly && entry?.allowOrgOverride && !createMode && (_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs text-muted-foreground border rounded p-2.5 bg-muted/30", children: [_jsxs("span", { children: ["Viewing in read-only mode. Click ", _jsx("strong", { children: "Edit" }), " to make changes."] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: [_jsx(Pencil, { className: "h-3.5 w-3.5 mr-1" }), "Edit"] })] })), _jsx(SchemaForm, { schema: schema, form: entry?.form, value: draft, onChange: setDraft, issues: issues, hiddenFields: config.hiddenFields, fieldOrder: config.fieldOrder, readOnly: formReadOnly, createMode: createMode, widgetContext: widgetContext })] }), PreviewComponent && (_jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsx(PreviewComponent, { type: type, name: name, draft: draft }) })), !createMode && (_jsx(TabsContent, { value: "layers", className: "mt-4", children: _jsx(LayeredDiff, { layered: layered }) })), !createMode && (_jsx(TabsContent, { value: "references", className: "mt-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })), hasAnchors && (_jsx(TabsContent, { value: "related", className: "mt-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) }))] })] }), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
1322
+ // Note: URL `?tab=` deep-links were repurposed to open side-panel
1323
+ // sheets (Layers / References / Related). Anything else is ignored —
1324
+ // the main work area is always the form+preview.
1325
+ // Action group rendered identically in either the PageShell header
1326
+ // (form-only types) or the canvas toolbar (types with a PreviewComponent).
1327
+ // Centralising it lets us merge the two top bars into one when a
1328
+ // designer is present, saving a full row of vertical chrome.
1329
+ const actionsNode = (_jsxs(_Fragment, { children: [!createMode && (_jsx(MetadataTypeActions, { entry: entry, location: "record_header", recordId: name, onAfter: () => setReloadKey((k) => k + 1) })), (!createMode || hasAnchors) && (_jsxs("div", { className: "flex items-center rounded-md border bg-background p-0.5", children: [!createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('layers'), title: t('engine.edit.layers', locale), className: "h-7 w-7 p-0 relative", children: [_jsx(Layers3, { className: "h-3.5 w-3.5" }), layered?.overlay && (() => {
1330
+ const n = countOverlaidFields(layered.code, layered.effective);
1331
+ return n > 0 ? (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-emerald-600 text-emerald-50 text-[9px] leading-[14px] text-center font-medium", title: t('engine.layers.diff', locale), children: n })) : null;
1332
+ })()] })), !createMode && hasDraft && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('review'), title: t('designer.draftReview.title', locale), className: "h-7 w-7 p-0 relative", "data-testid": "resource-review-trigger", children: [_jsx(GitCompareArrows, { className: "h-3.5 w-3.5" }), (() => {
1333
+ const n = computeDraftChangeCount(layered?.effective ?? null, draft);
1334
+ return n > 0 ? (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-amber-500 text-amber-50 text-[9px] leading-[14px] text-center font-medium", title: tFormat('designer.draftReview.badge', locale, { n }), children: n })) : null;
1335
+ })()] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('references'), title: t('engine.edit.references', locale), className: "h-7 w-7 p-0 relative", children: [_jsx(Link2, { className: "h-3.5 w-3.5" }), refs && refs.length > 0 && (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-muted text-foreground text-[9px] leading-[14px] text-center font-medium border", children: refs.length }))] })), hasAnchors && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('related'), title: t('engine.edit.related', locale), className: "h-7 w-7 p-0", children: _jsx(Boxes, { className: "h-3.5 w-3.5" }) })), !createMode && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('history'), title: t('engine.edit.history', locale), className: "h-7 w-7 p-0", children: _jsx(History, { className: "h-3.5 w-3.5" }) })), !createMode && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('audit'), title: t('engine.edit.auditTab', locale), className: "h-7 w-7 p-0", children: _jsx(ShieldCheck, { className: "h-3.5 w-3.5" }) }))] })), !createMode && canWrite && layered?.overlay && (isArtifactItem ? lockResettable : lockDeletable) && (_jsx(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, title: isArtifactItem
1336
+ ? t('engine.edit.reset', locale)
1337
+ : t('engine.edit.delete', locale), className: "h-7 w-7 p-0 text-muted-foreground hover:text-destructive", children: isArtifactItem ? (_jsx(RotateCcw, { className: "h-3.5 w-3.5" })) : (_jsx(Trash2, { className: "h-3.5 w-3.5" })) })), canWrite && !createMode && !editing && !PreviewComponent && (_jsxs(Button, { size: "sm", onClick: () => setEditing(true), className: "h-7", children: [_jsx(Pencil, { className: "h-3.5 w-3.5 mr-1" }), t('engine.edit.edit', locale)] })), canWrite && (editing || createMode) && (_jsx(SaveStatusIndicator, { saving: saving, isDirty: isDirty, autoSaveEnabled: autoSaveEnabled, lastSavedAt: lastSavedAt, createMode: !!createMode, locale: locale })), canWrite && (editing || createMode) && !createMode && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setAutoSaveEnabled((v) => !v), className: "h-7 w-7 p-0 text-muted-foreground", title: autoSaveEnabled
1338
+ ? t('engine.edit.autoSaveOn', locale)
1339
+ : t('engine.edit.autoSaveOff', locale), children: autoSaveEnabled ? (_jsx(Zap, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx(ZapOff, { className: "h-3.5 w-3.5" })) })), canWrite && !createMode && editing && !PreviewComponent && (_jsx(Button, { variant: "ghost", size: "sm", onClick: doCancelEdit, disabled: saving, className: "h-7 w-7 p-0", title: t('engine.cancel', locale), children: _jsx(X, { className: "h-3.5 w-3.5" }) })), canWrite && (editing || createMode) && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving || (!createMode && !isDirty), className: "h-7 w-7 p-0 relative", title: saving
1340
+ ? t('engine.edit.saving', locale)
1341
+ : !createMode && !isDirty
1342
+ ? t('engine.edit.noChanges', locale)
1343
+ : `${t('engine.edit.save', locale)} (⌘S)`, children: [saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(Save, { className: "h-3.5 w-3.5" })), isDirty && !saving && (_jsx("span", { "aria-hidden": true, className: "absolute -top-0.5 -right-0.5 inline-block h-2 w-2 rounded-full bg-amber-300 ring-2 ring-background" }))] })), canWrite && !createMode && hasDraft && (_jsx(Button, { variant: "ghost", size: "sm", onClick: doDiscardDraft, disabled: saving || publishing, className: "h-7 w-7 p-0 text-muted-foreground", title: t('engine.edit.discardDraft', locale), children: _jsx(Undo2, { className: "h-3.5 w-3.5" }) })), canWrite && !createMode && hasDraft && (_jsx(Button, { size: "sm", onClick: doPublish, disabled: saving || publishing || isDirty, className: "h-7 px-2 relative bg-emerald-600 hover:bg-emerald-700 text-emerald-50", title: isDirty
1344
+ ? t('engine.edit.publishBlockedDirty', locale)
1345
+ : t('engine.edit.publish', locale), children: publishing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(Send, { className: "h-3.5 w-3.5 mr-1" }), _jsx("span", { className: "text-xs", children: t('engine.edit.publish', locale) })] })) }))] }));
1346
+ return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? t('engine.edit.createNew', locale) : undefined, actions: PreviewComponent ? null : actionsNode, children: [_jsxs("div", { className: PreviewComponent
1347
+ ? 'flex h-full min-h-0 flex-col'
1348
+ : 'p-6 space-y-6 max-w-7xl', children: [(error || readOnly || hasDraft || isLocked) && (_jsxs("div", { className: PreviewComponent
1349
+ ? 'px-6 pt-4 space-y-3'
1350
+ : 'space-y-3', children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), isLocked && (_jsxs("div", { className: "text-xs text-amber-900 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-2.5", children: [_jsx(Lock, { className: "h-3.5 w-3.5 mt-0.5 shrink-0 opacity-80" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "font-medium", children: [layered?.lock === 'full' && t('engine.edit.lockFull', locale), layered?.lock === 'no-overlay' && t('engine.edit.lockNoOverlay', locale), layered?.lock === 'no-delete' && t('engine.edit.lockNoDelete', locale)] }), lockReason && _jsx("div", { className: "mt-0.5 opacity-90", children: lockReason }), layered?.lockDocsUrl && (_jsxs("a", { href: layered.lockDocsUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex items-center gap-1 text-amber-800 underline hover:text-amber-900 dark:text-amber-200 dark:hover:text-amber-100", children: [locale === 'zh-CN' ? '查看文档' : 'View docs', " \u2192"] })), layered?.packageId && (_jsx("div", { className: "mt-0.5 text-amber-700 dark:text-amber-300/80", children: _jsxs("code", { className: "font-mono", children: [layered.packageId, layered.packageVersion ? `@${layered.packageVersion}` : ''] }) }))] }), showArtifactLockedBanner && (_jsx(Button, { size: "sm", variant: "outline", className: "shrink-0 h-7 bg-background/60", onClick: () => navigate(`../new`, { relative: 'path' }), children: t('engine.list.create', locale) }))] })), hasDraft && !createMode && (_jsxs("div", { className: "text-xs text-emerald-900 border border-emerald-300 bg-emerald-50 rounded p-3 dark:text-emerald-200 dark:border-emerald-700/50 dark:bg-emerald-950/30 flex items-center gap-3", children: [_jsx(Send, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1", children: t('engine.edit.draftPending', locale) }), canWrite && (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: doDiscardDraft, disabled: saving || publishing, className: "h-7", children: t('engine.edit.discardDraft', locale) }), _jsx(Button, { size: "sm", onClick: doPublish, disabled: saving || publishing || isDirty, className: "h-7 bg-emerald-600 hover:bg-emerald-700 text-emerald-50", children: publishing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (t('engine.edit.publish', locale)) })] }))] })), readOnly && !isLocked && (_jsxs("div", { "data-testid": "readonly-banner", className: "text-xs text-amber-800 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-3", children: [_jsx("div", { className: "flex-1", children: showArtifactLockedBanner ? (
1351
+ /* Type allows runtime-create but THIS item ships from
1352
+ a code package. Tell the user clearly and provide
1353
+ a CTA to author their own. */
1354
+ t('engine.edit.artifactLockedBanner', locale)
1355
+ .split(/(\{type\})/)
1356
+ .map((part, i) => {
1357
+ if (part === '{type}')
1358
+ return _jsx("code", { className: "font-mono", children: type }, i);
1359
+ return _jsx(React.Fragment, { children: part }, i);
1360
+ })) : (
1361
+ /* The platform i18n bundle ships `engine.edit.readOnlyTypeBanner`
1362
+ with `{flag} / {type} / {override}` placeholders so the
1363
+ monospace tokens are inlined inside the translated sentence
1364
+ in any locale. Splitting on the three tokens preserves the
1365
+ sentence order across translations. */
1366
+ t('engine.edit.readOnlyTypeBanner', locale)
1367
+ .split(/(\{flag\}|\{type\}|\{override\})/)
1368
+ .map((part, i) => {
1369
+ if (part === '{flag}')
1370
+ return _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }, i);
1371
+ if (part === '{type}')
1372
+ return _jsx("code", { className: "font-mono", children: type }, i);
1373
+ if (part === '{override}')
1374
+ return _jsx("code", { className: "font-mono", children: "allowOrgOverride" }, i);
1375
+ return _jsx(React.Fragment, { children: part }, i);
1376
+ })) }), showArtifactLockedBanner && (_jsx(Button, { size: "sm", variant: "outline", className: "shrink-0", onClick: () => navigate(`../new`, { relative: 'path' }), children: t('engine.list.create', locale) }))] }))] })), _jsx("div", { className: PreviewComponent
1377
+ ? 'flex w-full flex-1 min-h-0 flex-col'
1378
+ : 'w-full', children: _jsxs("div", { className: PreviewComponent
1379
+ ? 'mt-2 flex-1 min-h-0 flex flex-col px-6 pb-4'
1380
+ : 'mt-4 space-y-3', children: [!PreviewComponent && formReadOnly && !readOnly && canWrite && !createMode && (_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs text-muted-foreground border rounded p-2.5 bg-muted/30", children: [_jsx("span", { children: t('engine.edit.readOnlyBanner', locale).split(/\{edit\}/).map((part, i, arr) => (_jsxs(React.Fragment, { children: [part, i < arr.length - 1 && _jsx("strong", { children: t('engine.edit.edit', locale) })] }, i))) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: [_jsx(Pencil, { className: "h-3.5 w-3.5 mr-1" }), t('engine.edit.edit', locale)] })] })), (() => {
1381
+ // Server-computed load-time validation errors on the
1382
+ // effective payload — surfaced here so operators can see
1383
+ // a structural problem without saving first. The same
1384
+ // errors are also threaded into SchemaForm as `issues`
1385
+ // and rendered inline next to each broken field.
1386
+ const diag = layered?._diagnostics;
1387
+ // When client-side Zod validation is available for this
1388
+ // type, drive the error portion of the banner from the
1389
+ // live `issues` state instead of the stale load-time
1390
+ // diagnostics, so it stays in sync with every keystroke.
1391
+ // Warnings remain server-sourced (Zod doesn't model them).
1392
+ const liveErrors = hasClientValidator(type)
1393
+ ? issues.map((i) => ({
1394
+ path: i.path,
1395
+ message: translateValidationMessage(i.message, locale),
1396
+ }))
1397
+ : (diag?.errors ?? []).map((i) => ({
1398
+ ...i,
1399
+ message: translateValidationMessage(i.message, locale),
1400
+ }));
1401
+ const liveValid = hasClientValidator(type)
1402
+ ? liveErrors.length === 0
1403
+ : diag?.valid !== false;
1404
+ // Gate the whole diagnostics block on a successful load with
1405
+ // a diagnostics source. A failed load shows the explicit
1406
+ // "failed to load" banner above instead; the empty-default
1407
+ // form's required-field issues here would be noise, not a
1408
+ // verdict on the item (see shouldRenderDiagnostics).
1409
+ if (createMode && !createDirty) {
1410
+ return null;
1411
+ }
1412
+ if (!shouldRenderDiagnostics({
1413
+ loadFailed,
1414
+ hasDiag: !!diag,
1415
+ hasClientValidator: hasClientValidator(type),
1416
+ })) {
1417
+ return null;
1418
+ }
1419
+ const errs = liveErrors;
1420
+ const warns = diag?.warnings ?? [];
1421
+ const hasErrs = !liveValid && errs.length > 0;
1422
+ const hasWarns = warns.length > 0;
1423
+ if (!hasErrs && !hasWarns)
1424
+ return null;
1425
+ const renderBlock = (kind, items) => {
1426
+ const head = items.slice(0, 3);
1427
+ const rest = Math.max(0, items.length - head.length);
1428
+ const cls = kind === 'error'
1429
+ ? 'border-destructive/40 bg-destructive/[0.06] text-destructive'
1430
+ : 'border-amber-500/40 bg-amber-500/[0.08] text-amber-800 dark:text-amber-200';
1431
+ const titleKey = kind === 'error'
1432
+ ? 'engine.edit.diagnostics.title'
1433
+ : 'engine.edit.diagnostics.warnTitle';
1434
+ return (_jsxs("div", { "data-testid": kind === 'error' ? 'metadata-validation-banner' : undefined, className: `flex items-start gap-2 text-xs border rounded p-2.5 ${cls}`, children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium", children: tFormat(titleKey, locale, { count: items.length }) }), _jsxs("ul", { className: "mt-1 space-y-0.5 font-mono text-[11px]", children: [head.map((e, i) => (_jsxs("li", { className: "truncate", children: [_jsx("span", { className: "opacity-70", children: e.path ? labelForIssuePath(e.path) : '(root)' }), ": ", e.message] }, i))), rest > 0 && (_jsx("li", { className: "opacity-70", children: tFormat('engine.edit.diagnostics.more', locale, { count: rest }) }))] })] })] }, kind));
1435
+ };
1436
+ return (_jsxs("div", { className: "space-y-2", children: [hasErrs && renderBlock('error', errs), hasWarns && renderBlock('warning', warns)] }));
1437
+ })(), PreviewComponent ? (_jsx("div", { className: isFullscreen
1438
+ ? 'fixed inset-0 z-50 bg-background flex flex-col p-3'
1439
+ : 'relative flex-1 min-h-0 flex', children: _jsxs(PanelGroup, { direction: "horizontal", className: "flex-1 min-h-0 rounded-md border bg-background overflow-hidden", id: `metadata-edit-${type}`, children: [_jsx(ResizablePanel, { defaultSize: 62, minSize: 30, children: _jsxs("div", { className: "relative h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b bg-background/95 backdrop-blur px-3 py-2 sticky top-0 z-10", children: [_jsx("div", { className: "flex items-center gap-1", children: canWrite && (_jsxs("div", { role: "tablist", "aria-label": t('engine.edit.designer', locale), className: "inline-flex items-center rounded-md border bg-muted/40 p-0.5", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": !previewOnly, onClick: () => setPreviewOnly(false), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
1440
+ (!previewOnly
1441
+ ? 'bg-background shadow-sm text-foreground'
1442
+ : 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.designMode', locale), children: [_jsx(MousePointer2, { className: "h-3.5 w-3.5" }), t('engine.edit.designMode', locale)] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": previewOnly, onClick: () => setPreviewOnly(true), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
1443
+ (previewOnly
1444
+ ? 'bg-background shadow-sm text-foreground'
1445
+ : 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.previewMode', locale), children: [_jsx(Eye, { className: "h-3.5 w-3.5" }), t('engine.edit.previewMode', locale)] })] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [actionsNode, _jsx("span", { className: "mx-1 h-5 w-px bg-border", "aria-hidden": true }), PreviewComponent && (_jsx(Button, { variant: "ghost", size: "sm", onClick: toggleInspector, className: "h-7 w-7 p-0", title: (inspectorCollapsed
1446
+ ? t('engine.edit.showInspector', locale)
1447
+ : t('engine.edit.hideInspector', locale)) + ' (⌘\\)', children: inspectorCollapsed ? (_jsx(PanelRightOpen, { className: "h-3.5 w-3.5" })) : (_jsx(PanelRightClose, { className: "h-3.5 w-3.5" })) })), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setIsFullscreen((v) => !v), className: "h-7 w-7 p-0", title: isFullscreen
1448
+ ? t('engine.edit.exitFullscreen', locale)
1449
+ : t('engine.edit.fullscreen', locale), children: isFullscreen ? (_jsx(Minimize2, { className: "h-3.5 w-3.5" })) : (_jsx(Maximize2, { className: "h-3.5 w-3.5" })) })] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4 bg-[radial-gradient(circle_at_1px_1px,theme(colors.border)_1px,transparent_0)] [background-size:16px_16px] bg-muted/30", children: _jsx(PreviewComponent, { type: type, name: name, draft: draft, baseline: !createMode
1450
+ ? (layered?.effective ?? undefined)
1451
+ : undefined, editing: editing && !previewOnly, selection: previewOnly ? null : selection, onSelectionChange: setSelection, locale: locale, onPatch: (patch) => handleDraftChange((d) => ({ ...d, ...patch })) }) })] }) }), _jsx(ResizableHandle, { withHandle: true, className: inspectorCollapsed
1452
+ ? 'hidden'
1453
+ : 'w-1.5 bg-border/40 hover:bg-primary/40 active:bg-primary/60 transition-colors' }), _jsx(ResizablePanel, { panelRef: inspectorPanelRef, defaultSize: lastInspectorSizeRef.current, minSize: 22, collapsible: true, collapsedSize: 0, onResize: (size) => {
1454
+ const pct = size.asPercentage;
1455
+ const collapsed = pct <= 0.5;
1456
+ if (!collapsed) {
1457
+ lastInspectorSizeRef.current = pct;
1458
+ if (typeof window !== 'undefined') {
1459
+ window.localStorage.setItem(inspectorSizeStorageKey, String(Math.round(pct)));
1460
+ }
1461
+ }
1462
+ setInspectorCollapsed((prev) => {
1463
+ if (prev === collapsed)
1464
+ return prev;
1465
+ if (typeof window !== 'undefined') {
1466
+ window.localStorage.setItem(inspectorStorageKey, collapsed ? '1' : '0');
1467
+ }
1468
+ return collapsed;
1469
+ });
1470
+ }, children: _jsxs("div", { className: "h-full overflow-auto", children: [_jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b bg-background/95 backdrop-blur px-3 py-2", children: [_jsxs("div", { role: "tablist", className: "inline-flex items-center rounded-md border bg-muted/40 p-0.5", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": inspectorTab === 'properties', onClick: () => setInspectorTab('properties'), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
1471
+ (inspectorTab === 'properties'
1472
+ ? 'bg-background shadow-sm text-foreground'
1473
+ : 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.inspector.properties', locale), children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), t('engine.edit.inspector.properties', locale)] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": inspectorTab === 'source', onClick: () => setInspectorTab('source'), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
1474
+ (inspectorTab === 'source'
1475
+ ? 'bg-background shadow-sm text-foreground'
1476
+ : 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.inspector.source', locale), children: [_jsx(FileCode2, { className: "h-3.5 w-3.5" }), t('engine.edit.inspector.source', locale)] })] }), isDirty && (_jsxs(Badge, { variant: "outline", className: "text-[10px] border-amber-400/60 text-amber-600 dark:text-amber-300", children: [_jsx("span", { className: "mr-1 inline-block h-1.5 w-1.5 rounded-full bg-amber-400" }), t('engine.edit.unsaved', locale)] }))] }), _jsx("div", { className: "p-4", children: inspectorTab === 'source' ? (_jsx(JsonSourceEditor, { value: draft, onChange: handleDraftChange, readOnly: formReadOnly, issues: displayIssues.map((i) => ({
1477
+ path: i.path ?? '',
1478
+ message: i.message,
1479
+ })) })) : selection && InspectorComponent ? (_jsx(InspectorComponent, { type: type, name: name, draft: draft, selection: selection, onPatch: (patch) => handleDraftChange((d) => ({
1480
+ ...d,
1481
+ ...patch,
1482
+ })), onClearSelection: () => setSelection(null), onSelectionChange: setSelection, readOnly: formReadOnly, locale: locale })) : !selection && DefaultInspectorComponent ? (_jsx(DefaultInspectorComponent, { type: type, name: name, draft: draft, onPatch: (patch) => handleDraftChange((d) => ({
1483
+ ...d,
1484
+ ...patch,
1485
+ })), onSelectionChange: setSelection, readOnly: formReadOnly, locale: locale, serverSchema: entry?.schema })) : (_jsx(SchemaForm, { schema: schema, form: createMode && config.createSchema ? undefined : entry?.form, value: draft, onChange: handleCreateAwareChange, issues: displayIssues, hiddenFields: effectiveHiddenFields, fieldOrder: effectiveFieldOrder, readOnly: formReadOnly, createMode: createMode, widgetContext: widgetContext })) })] }) })] }) })) : (_jsx(SchemaForm, { schema: schema, form: createMode && config.createSchema ? undefined : entry?.form, value: draft, onChange: handleCreateAwareChange, issues: displayIssues, hiddenFields: effectiveHiddenFields, fieldOrder: effectiveFieldOrder, readOnly: formReadOnly, createMode: createMode, widgetContext: widgetContext }))] }) })] }), _jsx(Sheet, { open: openSheet === 'layers', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.layers', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(LayeredDiff, { layered: layered, locale: locale }) })] }) }), _jsx(Sheet, { open: openSheet === 'review', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('designer.draftReview.title', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name, " \u00B7 ", t('designer.canvas.reviewVsPublished', locale)] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(DraftReviewPanel, { published: layered?.effective ?? null, draft: draft, locale: locale }) })] }) }), _jsx(Sheet, { open: openSheet === 'references', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsxs(SheetTitle, { className: "text-base", children: [t('engine.edit.references', locale), refs && (_jsx(Badge, { variant: "outline", className: "ml-2 text-[10px]", children: refs.length }))] }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })] }) }), hasAnchors && (_jsx(Sheet, { open: openSheet === 'related', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[860px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.related', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) })] }) })), !createMode && (_jsx(Sheet, { open: openSheet === 'history', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.history', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(HistoryPanel, { type: type, name: name, client: client, onRollback: () => setReloadKey((k) => k + 1), rollbackLabel: t('engine.edit.rollback', locale), rollbackConfirm: (version) => t('engine.edit.rollbackConfirm', locale).replace('{version}', String(version)) }) })] }) })), !createMode && (_jsx(Sheet, { open: openSheet === 'audit', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[860px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.auditTab', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-hidden p-3", children: _jsx(AuditPanel, { type: type, name: name, client: client, locale: locale }) })] }) })), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
333
1486
  if (!open) {
334
1487
  setDestructiveIssues(null);
335
1488
  setPendingItem(null);
@@ -345,3 +1498,40 @@ function ReferencesPanel({ refs, loading, }) {
345
1498
  }
346
1499
  return (_jsx("div", { className: "border rounded-lg overflow-hidden", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-xs uppercase tracking-wider text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left", children: "From type" }), _jsx("th", { className: "px-3 py-2 text-left", children: "From name" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Path" })] }) }), _jsx("tbody", { className: "divide-y", children: refs.map((r, i) => (_jsxs("tr", { className: "hover:bg-accent/50", children: [_jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: "text-[10px] font-mono", children: r.fromType }) }), _jsx("td", { className: "px-3 py-2 font-mono text-xs", children: r.fromName }), _jsx("td", { className: "px-3 py-2 font-mono text-xs text-muted-foreground", children: r.path })] }, i))) })] }) }));
347
1500
  }
1501
+ /**
1502
+ * SaveStatusIndicator — small inline label next to the Save icon that
1503
+ * communicates auto-save state so the icon-only button is not a black
1504
+ * box. Five states:
1505
+ * - saving → "Saving…" with spinner
1506
+ * - dirty + on → "Auto-saving in 1.5s" (subtle, amber)
1507
+ * - dirty + off → "Unsaved" (amber)
1508
+ * - clean + ts → "Saved 14:32" (muted)
1509
+ * - createMode → hidden until first save
1510
+ */
1511
+ function SaveStatusIndicator({ saving, isDirty, autoSaveEnabled, lastSavedAt, createMode, locale, }) {
1512
+ // Re-render every 30s so "Saved 14:32" stays accurate without
1513
+ // requiring the caller to manage a ticker.
1514
+ const [, force] = React.useReducer((n) => n + 1, 0);
1515
+ React.useEffect(() => {
1516
+ if (!lastSavedAt)
1517
+ return;
1518
+ const id = window.setInterval(force, 30000);
1519
+ return () => window.clearInterval(id);
1520
+ }, [lastSavedAt]);
1521
+ if (saving) {
1522
+ return (_jsxs("span", { className: "text-xs text-muted-foreground hidden md:inline-flex items-center gap-1", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), t('engine.edit.saving', locale)] }));
1523
+ }
1524
+ if (isDirty) {
1525
+ if (createMode)
1526
+ return null;
1527
+ return (_jsxs("span", { className: "text-xs text-amber-600 dark:text-amber-300 hidden md:inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-1.5 w-1.5 rounded-full bg-amber-400" }), autoSaveEnabled
1528
+ ? t('engine.edit.autoSavingShortly', locale)
1529
+ : t('engine.edit.unsaved', locale)] }));
1530
+ }
1531
+ if (lastSavedAt) {
1532
+ const hh = String(lastSavedAt.getHours()).padStart(2, '0');
1533
+ const mm = String(lastSavedAt.getMinutes()).padStart(2, '0');
1534
+ return (_jsx("span", { className: "text-xs text-muted-foreground hidden md:inline-flex items-center gap-1", children: tFormat('engine.edit.savedAt', locale, { time: `${hh}:${mm}` }) }));
1535
+ }
1536
+ return null;
1537
+ }