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