@burojs/ui-shadcn 0.6.0 → 0.7.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 (152) hide show
  1. package/README.md +9 -0
  2. package/dist/bulk-summary-BWSAS7k2.js +143 -0
  3. package/dist/bulk-summary-BWSAS7k2.js.map +1 -0
  4. package/dist/bulk-summary-CrEij5vq.cjs +143 -0
  5. package/dist/bulk-summary-CrEij5vq.cjs.map +1 -0
  6. package/dist/{candidates-relation-picker-B116Bmqi.js → candidates-relation-picker-CS_JZ4Fd.js} +3 -3
  7. package/dist/{candidates-relation-picker-B116Bmqi.js.map → candidates-relation-picker-CS_JZ4Fd.js.map} +1 -1
  8. package/dist/{candidates-relation-picker-Cy0rTxvI.cjs → candidates-relation-picker-y5M6_oe4.cjs} +4 -4
  9. package/dist/{candidates-relation-picker-Cy0rTxvI.cjs.map → candidates-relation-picker-y5M6_oe4.cjs.map} +1 -1
  10. package/dist/{check-confirm-dialog-bRT8lt0d.js → check-confirm-dialog-2j0gOd7x.js} +384 -108
  11. package/dist/check-confirm-dialog-2j0gOd7x.js.map +1 -0
  12. package/dist/{check-confirm-dialog--gPFnEeg.cjs → check-confirm-dialog-C-NHgMST.cjs} +468 -107
  13. package/dist/check-confirm-dialog-C-NHgMST.cjs.map +1 -0
  14. package/dist/{dialog-B_rt9S1c.js → dialog-C123sVQ8.js} +4 -4
  15. package/dist/dialog-C123sVQ8.js.map +1 -0
  16. package/dist/{dialog-DRNUq9EA.cjs → dialog-vmMdfUWs.cjs} +3 -3
  17. package/dist/dialog-vmMdfUWs.cjs.map +1 -0
  18. package/dist/{filter-builder-nXqtM_h4.cjs → filter-builder-DFu0JP-1.cjs} +923 -120
  19. package/dist/filter-builder-DFu0JP-1.cjs.map +1 -0
  20. package/dist/{filter-builder-Bhn-dhZV.js → filter-builder-DaWHUZuV.js} +839 -108
  21. package/dist/filter-builder-DaWHUZuV.js.map +1 -0
  22. package/dist/{flow-runtime-D3avVH0I.cjs → flow-runtime-BpmrIUb6.cjs} +61 -29
  23. package/dist/flow-runtime-BpmrIUb6.cjs.map +1 -0
  24. package/dist/{flow-runtime-s6lkRPKF.js → flow-runtime-DyqTmK3H.js} +62 -30
  25. package/dist/flow-runtime-DyqTmK3H.js.map +1 -0
  26. package/dist/{flow-screen-Ci_Sik4Y.cjs → flow-screen-Bcz9NF2V.cjs} +128 -87
  27. package/dist/flow-screen-Bcz9NF2V.cjs.map +1 -0
  28. package/dist/{flow-screen-CCvgDI9G.js → flow-screen-DCdli6Ha.js} +128 -87
  29. package/dist/flow-screen-DCdli6Ha.js.map +1 -0
  30. package/dist/index.cjs +731 -906
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +455 -247
  33. package/dist/index.d.ts +455 -247
  34. package/dist/index.js +647 -828
  35. package/dist/index.js.map +1 -1
  36. package/dist/{inline-flow-host-DV1et3c6.cjs → inline-flow-host-3vw7LzzD.cjs} +5 -5
  37. package/dist/inline-flow-host-3vw7LzzD.cjs.map +1 -0
  38. package/dist/{inline-flow-host-B6FI0ctQ.js → inline-flow-host-C0rI9B7f.js} +5 -5
  39. package/dist/inline-flow-host-C0rI9B7f.js.map +1 -0
  40. package/dist/{candidates-gate-D33jXLHO.cjs → lazy-relation-picker-dialog-Bt8lrEIu.cjs} +10 -10
  41. package/dist/lazy-relation-picker-dialog-Bt8lrEIu.cjs.map +1 -0
  42. package/dist/{candidates-gate-Bb70LPAy.js → lazy-relation-picker-dialog-CAScLPb2.js} +11 -11
  43. package/dist/lazy-relation-picker-dialog-CAScLPb2.js.map +1 -0
  44. package/dist/{page-flow-route-i3ZfJ1sd.js → page-flow-route-BHUEpn0I.js} +36 -23
  45. package/dist/page-flow-route-BHUEpn0I.js.map +1 -0
  46. package/dist/{page-flow-route-hdnULWqd.cjs → page-flow-route-DS8IVTVH.cjs} +41 -28
  47. package/dist/page-flow-route-DS8IVTVH.cjs.map +1 -0
  48. package/dist/{place-node-C5SmIAYU.cjs → place-node-CNb3w-c7.cjs} +1869 -947
  49. package/dist/place-node-CNb3w-c7.cjs.map +1 -0
  50. package/dist/{place-node-NraNYKPx.js → place-node-CfzQvh4Y.js} +1824 -926
  51. package/dist/place-node-CfzQvh4Y.js.map +1 -0
  52. package/dist/{related-list-cards-BWZ9GfOG.js → related-list-cards-BIqqY69Y.js} +9 -4
  53. package/dist/{related-list-cards-BWZ9GfOG.js.map → related-list-cards-BIqqY69Y.js.map} +1 -1
  54. package/dist/{related-list-cards-CFq3meUn.cjs → related-list-cards-CelMxl4q.cjs} +9 -4
  55. package/dist/{related-list-cards-CFq3meUn.cjs.map → related-list-cards-CelMxl4q.cjs.map} +1 -1
  56. package/dist/{related-list-timeline-yrPsjA3F.js → related-list-timeline-CRh7xwLD.js} +6 -7
  57. package/dist/related-list-timeline-CRh7xwLD.js.map +1 -0
  58. package/dist/{related-list-timeline-CCzl71aA.cjs → related-list-timeline-DsLdi8sv.cjs} +5 -6
  59. package/dist/related-list-timeline-DsLdi8sv.cjs.map +1 -0
  60. package/dist/{related-rows-o0_t-Yce.js → related-rows--uP2iKQC.js} +25 -14
  61. package/dist/related-rows--uP2iKQC.js.map +1 -0
  62. package/dist/{related-rows-ea43qhhy.cjs → related-rows-MPnLCmPF.cjs} +27 -16
  63. package/dist/related-rows-MPnLCmPF.cjs.map +1 -0
  64. package/dist/{search-access-CPPmfuvy.js → relation-create-C43cdPQt.js} +595 -13
  65. package/dist/relation-create-C43cdPQt.js.map +1 -0
  66. package/dist/{search-access-Dugvttlg.cjs → relation-create-DDEYcKfw.cjs} +633 -14
  67. package/dist/relation-create-DDEYcKfw.cjs.map +1 -0
  68. package/dist/{relation-many-picker-bvkjkv-k.js → relation-many-picker-BJhaEqxz.js} +203 -102
  69. package/dist/relation-many-picker-BJhaEqxz.js.map +1 -0
  70. package/dist/{relation-many-picker-C8zFEjwd.cjs → relation-many-picker-DzuGJpEl.cjs} +211 -110
  71. package/dist/relation-many-picker-DzuGJpEl.cjs.map +1 -0
  72. package/dist/{relation-picker-Z0ZJJ5uS.js → relation-picker-D-tBBZW_.js} +23 -59
  73. package/dist/relation-picker-D-tBBZW_.js.map +1 -0
  74. package/dist/{relation-picker-gag0qSR5.cjs → relation-picker-DaIMzf2H.cjs} +29 -71
  75. package/dist/relation-picker-DaIMzf2H.cjs.map +1 -0
  76. package/dist/{relation-picker-dialog-0IzkjmiA.cjs → relation-picker-dialog-9Vg5vvR1.cjs} +133 -35
  77. package/dist/relation-picker-dialog-9Vg5vvR1.cjs.map +1 -0
  78. package/dist/{relation-picker-dialog-B6nIiJBf.js → relation-picker-dialog-DoEY8EkH.js} +134 -36
  79. package/dist/relation-picker-dialog-DoEY8EkH.js.map +1 -0
  80. package/dist/relation-select-B8Fq4o-t.js +48 -0
  81. package/dist/relation-select-B8Fq4o-t.js.map +1 -0
  82. package/dist/relation-select-Cr3CfNLy.cjs +55 -0
  83. package/dist/relation-select-Cr3CfNLy.cjs.map +1 -0
  84. package/dist/{resource-page-router-PXfMW9nh.cjs → resource-page-router-C_kGW2o_.cjs} +543 -300
  85. package/dist/resource-page-router-C_kGW2o_.cjs.map +1 -0
  86. package/dist/{resource-page-router-3RUf9d1j.js → resource-page-router-Diw6S6TK.js} +498 -255
  87. package/dist/resource-page-router-Diw6S6TK.js.map +1 -0
  88. package/dist/{rich-text-vTyVjDoa.cjs → rich-text-C_7JCvRo.cjs} +2 -2
  89. package/dist/{rich-text-vTyVjDoa.cjs.map → rich-text-C_7JCvRo.cjs.map} +1 -1
  90. package/dist/{rich-text-DFerywgz.js → rich-text-RcPBf6hr.js} +2 -2
  91. package/dist/{rich-text-DFerywgz.js.map → rich-text-RcPBf6hr.js.map} +1 -1
  92. package/dist/success-notices-Dl3M1fZP.cjs +46 -0
  93. package/dist/success-notices-Dl3M1fZP.cjs.map +1 -0
  94. package/dist/success-notices-LDEgEkc4.js +41 -0
  95. package/dist/success-notices-LDEgEkc4.js.map +1 -0
  96. package/dist/{toaster-Cr51X7bz.cjs → toaster-BbByOytP.cjs} +60 -15
  97. package/dist/toaster-BbByOytP.cjs.map +1 -0
  98. package/dist/{toaster-C02XbB9r.js → toaster-Ct_YHGs6.js} +43 -16
  99. package/dist/toaster-Ct_YHGs6.js.map +1 -0
  100. package/dist/{use-dialog-filter-state-C--oMAZG.js → use-dialog-filter-state-B_MqzHaT.js} +5 -4
  101. package/dist/use-dialog-filter-state-B_MqzHaT.js.map +1 -0
  102. package/dist/{use-dialog-filter-state-Dhe8PcB5.cjs → use-dialog-filter-state-tNvqAAjh.cjs} +6 -5
  103. package/dist/use-dialog-filter-state-tNvqAAjh.cjs.map +1 -0
  104. package/package.json +11 -11
  105. package/dist/bulk-summary-CwSd6Yjb.cjs +0 -108
  106. package/dist/bulk-summary-CwSd6Yjb.cjs.map +0 -1
  107. package/dist/bulk-summary-GxMDP4bw.js +0 -108
  108. package/dist/bulk-summary-GxMDP4bw.js.map +0 -1
  109. package/dist/candidates-gate-Bb70LPAy.js.map +0 -1
  110. package/dist/candidates-gate-D33jXLHO.cjs.map +0 -1
  111. package/dist/check-confirm-dialog--gPFnEeg.cjs.map +0 -1
  112. package/dist/check-confirm-dialog-bRT8lt0d.js.map +0 -1
  113. package/dist/dialog-B_rt9S1c.js.map +0 -1
  114. package/dist/dialog-DRNUq9EA.cjs.map +0 -1
  115. package/dist/filter-builder-Bhn-dhZV.js.map +0 -1
  116. package/dist/filter-builder-nXqtM_h4.cjs.map +0 -1
  117. package/dist/flow-runtime-D3avVH0I.cjs.map +0 -1
  118. package/dist/flow-runtime-s6lkRPKF.js.map +0 -1
  119. package/dist/flow-screen-CCvgDI9G.js.map +0 -1
  120. package/dist/flow-screen-Ci_Sik4Y.cjs.map +0 -1
  121. package/dist/inline-flow-host-B6FI0ctQ.js.map +0 -1
  122. package/dist/inline-flow-host-DV1et3c6.cjs.map +0 -1
  123. package/dist/page-flow-route-hdnULWqd.cjs.map +0 -1
  124. package/dist/page-flow-route-i3ZfJ1sd.js.map +0 -1
  125. package/dist/place-node-C5SmIAYU.cjs.map +0 -1
  126. package/dist/place-node-NraNYKPx.js.map +0 -1
  127. package/dist/related-list-timeline-CCzl71aA.cjs.map +0 -1
  128. package/dist/related-list-timeline-yrPsjA3F.js.map +0 -1
  129. package/dist/related-rows-ea43qhhy.cjs.map +0 -1
  130. package/dist/related-rows-o0_t-Yce.js.map +0 -1
  131. package/dist/relation-create-IA40t1hi.cjs +0 -364
  132. package/dist/relation-create-IA40t1hi.cjs.map +0 -1
  133. package/dist/relation-create-h6F9B4C4.js +0 -339
  134. package/dist/relation-create-h6F9B4C4.js.map +0 -1
  135. package/dist/relation-many-picker-C8zFEjwd.cjs.map +0 -1
  136. package/dist/relation-many-picker-bvkjkv-k.js.map +0 -1
  137. package/dist/relation-picker-Z0ZJJ5uS.js.map +0 -1
  138. package/dist/relation-picker-dialog-0IzkjmiA.cjs.map +0 -1
  139. package/dist/relation-picker-dialog-B6nIiJBf.js.map +0 -1
  140. package/dist/relation-picker-gag0qSR5.cjs.map +0 -1
  141. package/dist/resource-page-router-3RUf9d1j.js.map +0 -1
  142. package/dist/resource-page-router-PXfMW9nh.cjs.map +0 -1
  143. package/dist/search-access-CPPmfuvy.js.map +0 -1
  144. package/dist/search-access-Dugvttlg.cjs.map +0 -1
  145. package/dist/shell-BPlk8hEa.js +0 -205
  146. package/dist/shell-BPlk8hEa.js.map +0 -1
  147. package/dist/shell-DxhlsPwm.cjs +0 -271
  148. package/dist/shell-DxhlsPwm.cjs.map +0 -1
  149. package/dist/toaster-C02XbB9r.js.map +0 -1
  150. package/dist/toaster-Cr51X7bz.cjs.map +0 -1
  151. package/dist/use-dialog-filter-state-C--oMAZG.js.map +0 -1
  152. package/dist/use-dialog-filter-state-Dhe8PcB5.cjs.map +0 -1
@@ -1,64 +1,121 @@
1
- import { C as loadFailedText, E as loadPageFlowRoute, O as useLoaded, _ as FlowBoundary, o as canonicalResourceName, t as CheckConfirmDialog } from "./check-confirm-dialog-bRT8lt0d.js";
1
+ import { l as cn } from "./use-theme-dnE3Od3I.js";
2
+ import { l as useLinkComponent } from "./toaster-Ct_YHGs6.js";
3
+ import { D as loadPageFlowRoute, k as useLoaded, o as canonicalResourceName, t as CheckConfirmDialog, v as FlowBoundary, w as loadFailedText } from "./check-confirm-dialog-2j0gOd7x.js";
2
4
  import { t as Button } from "./button-MWVeZaqj.js";
5
+ import { b as RecordState, m as useRowTitle, v as InvalidRouteId, y as LoadError } from "./relation-create-C43cdPQt.js";
3
6
  import { t as Skeleton } from "./skeleton-U2noefj5.js";
4
- import { Et as DeepLinkScope, G as RouteScreenContext, H as useTabsUrlState, K as useIsRouteScreen, St as useShownItemActions, U as QuickScreenClaimContext, V as ScreenTree, W as RouteScreenClaimContext, _ as FormActions, _t as listTarget, b as FrameChromeContext, bt as showTarget, dt as PageHeader, f as ChangeTypeButton, gt as isShowAvailable, h as UndeclaredTypeNotice, i as RouteListContext, kt as FieldDisplay, l as isBuiltinRecordView, m as TypeChromeDrawnContext, n as viewOf, p as HeaderTypeNotice, t as selectImplementation, u as resolveFrameView, vt as recordReturnTarget, x as useFrameChrome, yt as recordRouteAvailable } from "./place-node-NraNYKPx.js";
5
- import { FORM_SLOT, addressesCapability, builtinFrames, canonicalEntityNodeKey, compilePageScreen, declaredText, dtoFields, evaluateVisible, fieldSensitivity, formatEntityRef, hasListPage, hrefFor, isCustomPageDef, isScreenFailure, matchPath, navHrefFor, nodeKind, normalizeDataError, normalizeEntityRef, pageParamNames, parsePageParams, resolveNavLabel, resolveResourceNavLabel, snapshotAppFrames, snapshotBlockLookup, snapshotFrameLookup } from "@burojs/core";
6
- import { ComponentsContext, FrameCheckContext, RecordFrameContext, RecordScopeProvider, ResourceContext, RouteSourceContext, ScreenStateProvider, pageAccessFor, policyRecordOf, resolveRoute, retryRecordRead, useApp, useApplicationSnapshot, useAvailabilityResolver, useComponent, useCtx, useCurrentUserId, useFieldAccess, useFormController, useFrameImplementations, useFrameT, useI18nCaps, useNavigationGuardText, useOne, useOptionalDataRuntime, useOptionalRouterAdapter, useOptionalT, usePageAccess, usePageScreen, usePermissions, useProjectedRecord, usePublishDocVariant, useQuickEntry, useRecordFrame, useRecordTitle, useRouteListImplementation, useRouteNavigate, useT, useTestIds, useTranslate, useVisibilityCtx } from "@burojs/react";
7
+ import { Ct as recordReturnTarget, Dt as useShownItemActions, G as RouteScreenClaimContext, H as ScreenTree, J as FormControlProvider, K as RouteScreenContext, Pt as FieldDisplay, S as useFrameChrome, St as listTarget, Tt as showTarget, U as useTabsUrlState, W as QuickScreenClaimContext, X as useFormControlOf, d as resolveFrameView, g as UndeclaredTypeNotice, h as TypeChromeDrawnContext, ht as PageHeader, i as RouteListContext, jt as DeepLinkScope, m as HeaderTypeNotice, n as viewOf, p as ChangeTypeButton, q as useIsRouteScreen, t as selectImplementation, u as isBuiltinRecordView, v as FormActions, wt as recordRouteAvailable, x as FrameChromeContext, xt as isShowAvailable } from "./place-node-CfzQvh4Y.js";
8
+ import { t as useNoticeSink } from "./success-notices-LDEgEkc4.js";
9
+ import { FORM_SLOT, addressesCapability, builtinFrames, canonicalEntityNodeKey, compilePageScreen, declaredText, dtoFields, evaluateVisible, fieldSensitivity, formatEntityRef, hasListPage, hrefFor, humanizeKey, isCustomPageDef, isScreenFailure, issueOfInput, matchPath, navHrefFor, normalizeEntityRef, normalizeIssueInputs, pageParamNames, parsePageParams, resolveNavLabel, resourceKey, routeResourceKey, routeServerIssues, scopeColumnOf, snapshotAppFrames, snapshotBlockLookup, snapshotFrameLookup, successNoticesOf } from "@burojs/core";
10
+ import { ActionRouteContext, ComponentsContext, FrameCheckContext, RecordFrameContext, RecordScopeProvider, ResourceContext, RouteSourceContext, ScreenStateProvider, maybeThen, pageAccessFor, policyRecordOf, recordKeyOf, resolveRoute, retryRecordRead, settle, useApp, useApplicationSnapshot, useAvailabilityResolver, useChromeT, useComponent, useCtx, useCurrentUserId, useFieldAccess, useFormController, useFrameImplementations, useFrameT, useI18nCaps, useList, useNavigationGuardText, useOne, useOptionalDataRuntime, useOptionalRouterAdapter, useOptionalT, usePageAccess, usePageScreen, usePermissions, usePlatformStorage, useProjectedRecord, usePublishDocVariant, useQuickEntry, useRecordFrame, useRecordTitle, useRouteListImplementation, useRouteNavigate, useRouteRecordId, useScope, useT, useTestIds, useTranslate, useVisibilityCtx } from "@burojs/react";
7
11
  import { Suspense, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
12
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
13
  //#region src/resource-key.ts
10
14
  function canonicalNavKey(node) {
11
15
  return canonicalEntityNodeKey(node);
12
16
  }
13
- function canonicalResourceKey(def) {
17
+ function canonicalResourceKey$1(def) {
14
18
  return canonicalNavKey(def);
15
19
  }
16
20
  //#endregion
17
- //#region src/layout/build-nav-items.ts
18
- function buildNavItems(args) {
19
- const { registry, caps, groupName, match, isVisible, entryFilter, icons } = args;
20
- const chain = match?.groupChain ?? [];
21
- const qualifiedMatch = match;
22
- const activeLeaf = qualifiedMatch == null ? void 0 : qualifiedMatch.kind === "group-index" ? canonicalNavKey({
23
- name: qualifiedMatch.target ?? qualifiedMatch.name,
24
- ref: qualifiedMatch.targetRef
25
- }) : canonicalNavKey(qualifiedMatch);
26
- const isEligible = (node) => isVisible(node) && (entryFilter ? entryFilter(node) : true);
27
- const out = [];
28
- for (const child of registry.groups.childrenOf(groupName)) {
29
- if (!isEligible(child)) continue;
30
- const kind = nodeKind(child);
31
- const childKey = canonicalNavKey(child);
32
- const href = navHrefFor(registry, childKey, { isVisible: isEligible });
33
- if (kind === "group") {
34
- const label = resolveNavLabel(caps, `groups.${childKey}`, declaredText("chrome", child, "label"));
35
- const active = chain.some((g) => canonicalNavKey(g) === childKey);
36
- out.push({
37
- kind: "group",
38
- name: childKey,
39
- label,
40
- href,
41
- icon: icons?.group?.(childKey),
42
- active
43
- });
44
- } else if (kind === "custom-page") out.push({
45
- kind: "custom-page",
46
- name: childKey,
47
- label: resolveNavLabel(caps, `pages.${childKey}`, declaredText("chrome", child, "label")),
48
- href,
49
- icon: icons?.page?.(childKey),
50
- active: activeLeaf === childKey
51
- });
52
- else out.push({
53
- kind: "resource",
54
- name: childKey,
55
- label: resolveResourceNavLabel(caps, childKey, child),
56
- href,
57
- icon: icons?.resource?.(childKey),
58
- active: activeLeaf === childKey
59
- });
60
- }
61
- return out;
21
+ //#region src/layout/layouts/secondary-sidebar-layout.tsx
22
+ function Row({ it }) {
23
+ const RouterLink = useLinkComponent();
24
+ return jsx("li", { children: jsxs(RouterLink, {
25
+ href: it.href,
26
+ "data-testid": `nav-secondary-item:${it.name}`,
27
+ "aria-current": it.active ? "page" : void 0,
28
+ className: cn("flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium outline-hidden ring-offset-background transition-colors", "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", it.active ? "bg-secondary text-secondary-foreground" : "text-muted-foreground hover:bg-secondary/50 hover:text-foreground"),
29
+ children: [it.icon != null && jsx("span", {
30
+ className: "inline-flex h-4 w-4 shrink-0 items-center justify-center",
31
+ "aria-hidden": "true",
32
+ children: it.icon
33
+ }), jsx("span", {
34
+ className: "truncate",
35
+ title: it.label,
36
+ children: it.label
37
+ })]
38
+ }) });
39
+ }
40
+ function SecondarySidebarLayout({ group, groupKey, items, children }) {
41
+ const RouterLink = useLinkComponent();
42
+ const caps = useI18nCaps();
43
+ const id = groupKey ?? group.name;
44
+ const label = resolveNavLabel(caps, `groups.${id}`, declaredText("chrome", group, "label"));
45
+ return jsxs("div", {
46
+ className: "md:-m-6 md:flex md:min-h-[calc(100%+3rem)]",
47
+ "data-testid": `nav-secondary:${id}`,
48
+ children: [
49
+ jsx("aside", {
50
+ className: "hidden w-64 shrink-0 border-r bg-background text-foreground md:block",
51
+ children: jsxs("div", {
52
+ className: "space-y-4 p-4",
53
+ children: [jsx("p", {
54
+ className: "px-3 text-base font-semibold tracking-tight",
55
+ children: label
56
+ }), jsx("nav", {
57
+ "aria-label": label,
58
+ children: jsx("ul", {
59
+ className: "space-y-0.5",
60
+ children: items.map((it) => jsx(Row, { it }, it.name))
61
+ })
62
+ })]
63
+ })
64
+ }),
65
+ jsx("nav", {
66
+ "aria-label": label,
67
+ "data-testid": `nav-secondary-select:${id}`,
68
+ className: "-mx-1 mb-4 flex snap-x snap-proximity gap-1.5 overflow-x-auto scroll-smooth px-1 pb-1 [-ms-overflow-style:none] [mask-image:linear-gradient(to_right,transparent,black_8px,black_calc(100%-8px),transparent)] [scrollbar-width:none] md:hidden [&::-webkit-scrollbar]:hidden",
69
+ children: items.map((it) => jsx(RouterLink, {
70
+ href: it.href,
71
+ "aria-current": it.active ? "page" : void 0,
72
+ className: cn("snap-start whitespace-nowrap rounded-full border px-3 py-1.5 text-sm outline-hidden ring-offset-background transition-colors duration-150", "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", it.active ? "border-primary/40 bg-primary/10 font-medium text-foreground" : "border-border text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground"),
73
+ children: it.label
74
+ }, it.name))
75
+ }),
76
+ jsx("div", {
77
+ className: "min-w-0 flex-1 md:overflow-x-hidden md:p-6",
78
+ children
79
+ })
80
+ ]
81
+ });
82
+ }
83
+ //#endregion
84
+ //#region src/layout/layouts/tabs-layout.tsx
85
+ function TabsLayout({ group, groupKey, items, children }) {
86
+ const RouterLink = useLinkComponent();
87
+ const caps = useI18nCaps();
88
+ const id = groupKey ?? group.name;
89
+ const label = resolveNavLabel(caps, `groups.${id}`, declaredText("chrome", group, "label"));
90
+ return jsxs("div", {
91
+ className: "flex flex-col gap-4",
92
+ "data-testid": `nav-tabs:${id}`,
93
+ children: [jsx("nav", {
94
+ "aria-label": label,
95
+ className: "flex gap-1 overflow-x-auto scroll-smooth border-b border-border [-ms-overflow-style:none] [mask-image:linear-gradient(to_right,transparent,black_8px,black_calc(100%-8px),transparent)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
96
+ children: items.map((it) => jsxs(RouterLink, {
97
+ href: it.href,
98
+ "aria-current": it.active ? "page" : void 0,
99
+ "data-testid": `nav-tab:${it.name}`,
100
+ className: cn("group/tab relative flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-t-sm px-3 py-2.5 text-sm outline-hidden ring-offset-background transition-colors duration-150", "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", it.active ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"),
101
+ children: [
102
+ it.icon != null && jsx("span", {
103
+ className: "inline-flex h-4 w-4 shrink-0 items-center justify-center",
104
+ "aria-hidden": "true",
105
+ children: it.icon
106
+ }),
107
+ it.label,
108
+ jsx("span", {
109
+ "aria-hidden": "true",
110
+ className: cn("pointer-events-none absolute inset-x-2 -bottom-px h-0.5 origin-center scale-x-0 rounded-full bg-primary opacity-0 transition-all duration-300 ease-out", it.active ? "scale-x-100 opacity-100" : "group-hover/tab:scale-x-100 group-hover/tab:opacity-30")
111
+ })
112
+ ]
113
+ }, it.name))
114
+ }), jsx("div", {
115
+ className: "min-w-0",
116
+ children
117
+ })]
118
+ });
62
119
  }
63
120
  //#endregion
64
121
  //#region src/layout/nav-slots.ts
@@ -68,9 +125,33 @@ function layoutSlot(key) {
68
125
  function useLayout(key) {
69
126
  return useComponent(layoutSlot(key));
70
127
  }
128
+ const BUILTIN_LAYOUTS = {
129
+ [layoutSlot("secondary-sidebar")]: SecondarySidebarLayout,
130
+ [layoutSlot("tabs")]: TabsLayout
131
+ };
71
132
  function useLayoutResolver() {
72
133
  const components = useContext(ComponentsContext);
73
- return (key) => components[layoutSlot(key)];
134
+ return (key) => components[layoutSlot(key)] ?? BUILTIN_LAYOUTS[layoutSlot(key)];
135
+ }
136
+ const unresolvedLayoutReported = new WeakMap();
137
+ function useReportUnresolvedLayouts(groups, resolve) {
138
+ const app = useApp();
139
+ useEffect(() => {
140
+ if (process.env.NODE_ENV === "production") return;
141
+ for (const group of groups) {
142
+ const layout = group.layout;
143
+ if (layout === "flat" || layout === "collapsible" || resolve(layout)) continue;
144
+ const key = canonicalNavKey(group);
145
+ let seen = unresolvedLayoutReported.get(app);
146
+ if (!seen) {
147
+ seen = new Set();
148
+ unresolvedLayoutReported.set(app, seen);
149
+ }
150
+ if (seen.has(key)) continue;
151
+ seen.add(key);
152
+ console.error(`[burojs] Nav group "${key}" declares layout "${layout}", and no renderer is registered for it. Register one as components["layout.${layout}"] on <BuroProvider>, or use a built-in layout: "secondary-sidebar", "tabs", "flat", "collapsible".`);
153
+ }
154
+ });
74
155
  }
75
156
  //#endregion
76
157
  //#region src/layout/use-nav-routing.ts
@@ -79,6 +160,10 @@ function useNavVisible() {
79
160
  const allowed = usePageAllowed();
80
161
  return useCallback((node) => evaluateVisible(node.visible, ctx) && (!isCustomPageDef(node) || allowed(node)), [ctx, allowed]);
81
162
  }
163
+ function useResourceVisible() {
164
+ const ctx = useVisibilityCtx();
165
+ return useCallback((def) => evaluateVisible(def.visible, ctx), [ctx]);
166
+ }
82
167
  function usePageAllowed() {
83
168
  const permissions = usePermissions();
84
169
  return useCallback((page) => pageAccessFor(page, permissions) === "allowed", [permissions]);
@@ -98,99 +183,17 @@ function useMatch() {
98
183
  return path === void 0 ? null : matchPath(app.registry, path, { isVisible });
99
184
  }
100
185
  //#endregion
101
- //#region src/layout/nav-chrome-boundary.tsx
102
- const NavChromeBoundaryContext = createContext(new Set());
103
- function useNavChromeBoundary() {
104
- return useContext(NavChromeBoundaryContext);
105
- }
106
- function NavChromeBoundaryProvider({ claim, children }) {
107
- const parent = useContext(NavChromeBoundaryContext);
108
- const key = claim.join(" ");
109
- const merged = useMemo(() => {
110
- const s = new Set(parent);
111
- for (const n of claim) s.add(n);
112
- return s;
113
- }, [parent, key]);
114
- return jsx(NavChromeBoundaryContext.Provider, {
115
- value: merged,
116
- children
117
- });
118
- }
119
- //#endregion
120
- //#region src/layout/nav-chrome.tsx
121
- const DETAIL_ACTIONS = new Set([
122
- "show",
123
- "edit",
124
- "create"
125
- ]);
126
- function NavChrome({ children, entryFilter, icons }) {
127
- const app = useApp();
128
- const caps = useI18nCaps();
129
- const isVisible = useNavVisible();
130
- const match = useMatch();
131
- const resolveLayout = useLayoutResolver();
132
- const boundary = useNavChromeBoundary();
133
- if (!match) return jsx(Fragment$1, { children });
134
- let content = children;
135
- for (let i = match.groupChain.length - 1; i >= 0; i--) {
136
- const group = match.groupChain[i];
137
- if (!group) continue;
138
- const groupKey = canonicalNavKey(group);
139
- if (boundary.has(groupKey)) continue;
140
- if (group.chromeOn === "list" && match.kind === "resource" && DETAIL_ACTIONS.has(match.action)) continue;
141
- const Layout = resolveLayout(group.layout);
142
- if (!Layout) continue;
143
- const items = buildNavItems({
144
- registry: app.registry,
145
- caps,
146
- groupName: groupKey,
147
- match,
148
- isVisible,
149
- entryFilter,
150
- icons
151
- });
152
- content = jsx(Layout, {
153
- group,
154
- groupKey,
155
- items,
156
- children: content
157
- });
158
- }
159
- return jsx(Fragment$1, { children: content });
160
- }
161
- //#endregion
162
- //#region src/record/record-state.tsx
163
- function RecordState({ state, onRetry }) {
164
- const t = useT();
165
- if (state === "loading") return jsx("div", {
166
- role: "status",
167
- "aria-busy": "true",
168
- "data-testid": "record-page:loading",
169
- className: "p-6 text-sm text-muted-foreground",
170
- children: t("router.recordLoading")
171
- });
172
- const message = state === "not-found" ? t("router.recordNotFound") : state === "session-expired" ? t("router.recordSessionExpired") : t("router.recordError");
173
- return jsxs("div", {
174
- role: "alert",
175
- "data-testid": `record-page:${state}`,
176
- className: "flex flex-col items-center justify-center gap-3 px-6 py-24 text-center",
177
- children: [jsx("p", {
178
- className: "max-w-sm text-sm text-muted-foreground",
179
- children: message
180
- }), onRetry ? jsx(Button, {
181
- type: "button",
182
- variant: "outline",
183
- size: "sm",
184
- "data-testid": "record-page:retry",
185
- onClick: onRetry,
186
- children: t("actions.retry")
187
- }) : null]
188
- });
186
+ //#region src/layout/nav-chrome-mounted.ts
187
+ const NavChromeMountedContext = createContext(false);
188
+ const NavChromeMountedProvider = NavChromeMountedContext.Provider;
189
+ function useNavChromeMounted() {
190
+ return useContext(NavChromeMountedContext);
189
191
  }
190
192
  //#endregion
191
193
  //#region src/flows/page-flow-loader.tsx
192
194
  function PageFlowRouteLoader({ match }) {
193
195
  const t = useOptionalT();
196
+ const chromeT = useChromeT();
194
197
  const loaded = useLoaded(loadPageFlowRoute);
195
198
  const failed = (retry) => jsxs("div", {
196
199
  role: "alert",
@@ -199,7 +202,7 @@ function PageFlowRouteLoader({ match }) {
199
202
  children: [jsx("p", { children: loadFailedText(t) }), jsx("div", { children: jsx(Button, {
200
203
  type: "button",
201
204
  onClick: retry,
202
- children: t("actions.retry") === "actions.retry" ? "Retry" : t("actions.retry")
205
+ children: chromeT("actions.retry", { defaultValue: "Retry" })
203
206
  }) })]
204
207
  });
205
208
  if (loaded.error !== void 0) return failed(loaded.retry);
@@ -212,7 +215,8 @@ function PageFlowRouteLoader({ match }) {
212
215
  }
213
216
  //#endregion
214
217
  //#region src/list-kit/resource-list-page.tsx
215
- function ResourceListPage({ resource, meta, renderCell, emptyState }) {
218
+ function ResourceListPage({ resource: resourceInput, meta, renderCell, emptyState }) {
219
+ const resource = routeResourceKey(resourceInput);
216
220
  const app = useRouteListImplementation();
217
221
  const implementation = selectImplementation("buro:related-list", app);
218
222
  const View = implementation === void 0 ? void 0 : viewOf(implementation);
@@ -311,8 +315,9 @@ function PageRouteScreen({ children }) {
311
315
  })
312
316
  });
313
317
  }
314
- function PlannedBody({ screen, name, label }) {
315
- const page = usePageScreen(screen);
318
+ function PlannedBody({ screen, name, label, record }) {
319
+ const pageOptions = useMemo(() => record === void 0 ? {} : { record }, [record]);
320
+ const page = usePageScreen(screen, pageOptions);
316
321
  const { PlacementScope } = page;
317
322
  const route = useContext(RouteSourceContext);
318
323
  const catalog = useFrameImplementations();
@@ -337,27 +342,23 @@ function PlannedBody({ screen, name, label }) {
337
342
  };
338
343
  return jsx(RouteSourceContext.Provider, {
339
344
  value: null,
340
- children: jsx(PlacementScope, { children: jsx(DeepLinkScope, {
341
- plan: page.plan,
342
- enabled: true,
343
- children: jsx("div", {
344
- "data-buro-region": "page-body",
345
- children: jsx(Suspense, {
346
- fallback: null,
347
- children: jsx(View, { ...viewProps })
345
+ children: jsx(ActionRouteContext.Provider, {
346
+ value: route,
347
+ children: jsx(PlacementScope, { children: jsx(DeepLinkScope, {
348
+ plan: page.plan,
349
+ enabled: true,
350
+ children: jsx("div", {
351
+ "data-buro-region": "page-body",
352
+ children: jsx(Suspense, {
353
+ fallback: null,
354
+ children: jsx(View, { ...viewProps })
355
+ })
348
356
  })
349
- })
350
- }) })
357
+ }) })
358
+ })
351
359
  });
352
360
  }
353
361
  //#endregion
354
- //#region src/resource/form-context.tsx
355
- const FormControlContext = createContext(null);
356
- const FormControlProvider = FormControlContext.Provider;
357
- function useFormControl() {
358
- return useContext(FormControlContext);
359
- }
360
- //#endregion
361
362
  //#region src/frames/obligations.tsx
362
363
  const SlotMountsContext = createContext(null);
363
364
  function useSlotOnce(slot) {
@@ -487,9 +488,9 @@ function timeAgo(savedAt, t) {
487
488
  if (diff < 864e5) return t("draft.timeAgoHours", { count: Math.round(diff / 36e5) });
488
489
  return t("draft.timeAgoDays", { count: Math.round(diff / 864e5) });
489
490
  }
490
- function frameRecordId(frame) {
491
+ function frameRecordId(frame, def) {
491
492
  if (frame.recordId !== void 0) return frame.recordId;
492
- const id = frame.record?.id;
493
+ const id = recordKeyOf(def, frame.record);
493
494
  return typeof id === "string" || typeof id === "number" ? String(id) : void 0;
494
495
  }
495
496
  function BodySlot() {
@@ -497,7 +498,7 @@ function BodySlot() {
497
498
  const { resource, mode, controller } = frame;
498
499
  const chrome = useFrameChrome();
499
500
  const tid = useTestIds();
500
- const id = frameRecordId(frame);
501
+ const id = frameRecordId(frame, useApp().registry.get(resource));
501
502
  const tree = jsx("div", {
502
503
  "data-buro-region": "record-body",
503
504
  "data-buro-slot": FORM_SLOT,
@@ -646,7 +647,7 @@ function RecordFrameView({ frame: instance }) {
646
647
  ...policyRecord ?? {},
647
648
  id: typeof rowId === "string" || typeof rowId === "number" ? String(rowId) : ""
648
649
  }), [policyRecord, rowId]);
649
- const showItemActions = useShownItemActions(resource, actionRow, "show");
650
+ const showItemActions = useShownItemActions(resource, actionRow, "show", void 0, controller.sources);
650
651
  const showActions = mode === "show" && record !== void 0 && showItemActions.length > 0;
651
652
  const View = resolved.view;
652
653
  const builtin = isBuiltinRecordView(resolved);
@@ -664,6 +665,156 @@ function RecordFrameView({ frame: instance }) {
664
665
  })
665
666
  })] });
666
667
  }
668
+ function useResourceDimensionOptions(dimension, resource, labelField) {
669
+ const def = useApp().registry.get(resource);
670
+ const rowTitle = useRowTitle(resource, labelField ?? def?.displayField);
671
+ const rows = useList(resource, {
672
+ pagination: {
673
+ kind: "offset",
674
+ page: 1,
675
+ pageSize: 100
676
+ },
677
+ total: false,
678
+ scope: {
679
+ default: "session",
680
+ dimensions: { [dimension]: "none" }
681
+ }
682
+ }).data?.data;
683
+ return useMemo(() => (rows ?? []).flatMap((row) => {
684
+ const key = recordKeyOf(def, row);
685
+ return key === null || key === void 0 ? [] : [{
686
+ value: key,
687
+ label: rowTitle(row)
688
+ }];
689
+ }), [
690
+ rows,
691
+ def,
692
+ rowTitle
693
+ ]);
694
+ }
695
+ //#endregion
696
+ //#region src/record/scope-mismatch-banner.tsx
697
+ const asList = (value) => value === null ? [] : Array.isArray(value) ? value : [value];
698
+ const isScalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
699
+ function ScopeMismatchBanner(props) {
700
+ const app = useApp();
701
+ const mapping = app.registry.get(props.resource)?.data?.scope;
702
+ if (app.scope === void 0 || mapping === void 0 || props.record === void 0) return null;
703
+ return jsx(Banner, {
704
+ ...props,
705
+ record: props.record,
706
+ mapping
707
+ });
708
+ }
709
+ function Banner({ record, mapping, kind = "record" }) {
710
+ const scope = useScope();
711
+ const chromeT = useChromeT();
712
+ const differences = useMemo(() => {
713
+ const out = [];
714
+ for (const [dimension, target] of Object.entries(mapping)) {
715
+ const definition = scope.definition?.dimensions[dimension];
716
+ if (definition === void 0 || definition.select === "none") continue;
717
+ const selected = asList(scope.selection[dimension] ?? null);
718
+ if (selected.length === 0) continue;
719
+ const column = scopeColumnOf(target);
720
+ if (!Object.hasOwn(record, column)) continue;
721
+ const held = record[column];
722
+ if (held === null || held === void 0 || held === "") {
723
+ if (kind === "created" || "field" in target && target.empty === "shared") continue;
724
+ out.push({
725
+ dimension,
726
+ value: null,
727
+ selected,
728
+ select: definition.select
729
+ });
730
+ continue;
731
+ }
732
+ if (!isScalar(held)) continue;
733
+ if (selected.some((value) => String(value) === String(held))) continue;
734
+ out.push({
735
+ dimension,
736
+ value: held,
737
+ selected,
738
+ select: definition.select
739
+ });
740
+ }
741
+ return out;
742
+ }, [
743
+ mapping,
744
+ record,
745
+ scope.definition,
746
+ scope.selection,
747
+ kind
748
+ ]);
749
+ if (differences.length === 0) return null;
750
+ const apply = () => {
751
+ for (const difference of differences) scope.select(difference.dimension, difference.value === null ? null : difference.select === "multiple" ? [difference.value] : difference.value);
752
+ };
753
+ return jsxs("div", {
754
+ role: "status",
755
+ "data-testid": "scope-mismatch:banner",
756
+ className: "flex flex-wrap items-center justify-between gap-3 rounded-md border bg-muted/50 px-3 py-2 text-sm",
757
+ children: [jsxs("div", {
758
+ className: "space-y-1",
759
+ children: [jsx("p", {
760
+ className: "font-medium",
761
+ children: kind === "created" ? chromeT("scope.mismatch.createdOutside", { defaultValue: "Created outside the current scope" }) : chromeT("scope.mismatch.title", { defaultValue: "This record is outside the current scope" })
762
+ }), jsx("ul", {
763
+ className: "text-muted-foreground",
764
+ children: differences.map((difference) => jsx(DifferenceLine, {
765
+ difference,
766
+ definition: scope.definition.dimensions[difference.dimension]
767
+ }, difference.dimension))
768
+ })]
769
+ }), jsx(Button, {
770
+ type: "button",
771
+ size: "sm",
772
+ "data-testid": "scope-mismatch:switch",
773
+ onClick: apply,
774
+ children: chromeT("scope.mismatch.switch", { defaultValue: "Switch" })
775
+ })]
776
+ });
777
+ }
778
+ function DifferenceLine(props) {
779
+ const source = props.definition.values;
780
+ if (source !== void 0 && !Array.isArray(source)) {
781
+ const { resource, label } = source;
782
+ return jsx(ResourceLine, {
783
+ ...props,
784
+ resource,
785
+ ...label ? { labelField: label } : {}
786
+ });
787
+ }
788
+ return jsx(LineText, {
789
+ difference: props.difference,
790
+ options: source ?? []
791
+ });
792
+ }
793
+ function ResourceLine({ difference, resource, labelField }) {
794
+ const options = useResourceDimensionOptions(difference.dimension, resource, labelField);
795
+ return jsx(LineText, {
796
+ difference,
797
+ options
798
+ });
799
+ }
800
+ function LineText({ difference, options }) {
801
+ const chromeT = useChromeT();
802
+ const labelOf = (value) => options.find((option) => String(option.value) === String(value))?.label ?? String(value);
803
+ const field = humanizeKey(difference.dimension);
804
+ const item = difference.value === null ? chromeT("scope.mismatch.empty", { defaultValue: "not set" }) : labelOf(difference.value);
805
+ const items = difference.selected.map(labelOf).join(", ");
806
+ return jsx("li", {
807
+ "data-testid": `scope-mismatch:line:${difference.dimension}`,
808
+ children: chromeT("scope.mismatch.line", {
809
+ params: {
810
+ field,
811
+ item,
812
+ items
813
+ },
814
+ defaultValue: `${field}: ${item} (selected: ${items})`
815
+ })
816
+ });
817
+ }
667
818
  //#endregion
668
819
  //#region src/record/type-offer.tsx
669
820
  function TypeOffer({ resource, offer, onChoose }) {
@@ -726,18 +877,41 @@ function isAbort(error) {
726
877
  return false;
727
878
  }
728
879
  }
729
- function canonicalName(resource) {
730
- const ref = normalizeEntityRef(typeof resource === "string" ? resource : resource.ref);
731
- return canonicalEntityNodeKey({
732
- name: ref.id,
733
- ref
734
- });
880
+ const NO_ISSUES = Object.freeze([]);
881
+ const APPLIED_UNREADABLE = Object.freeze({
882
+ code: "applied-unreadable",
883
+ message: Object.freeze({
884
+ key: "errors.appliedUnreadable",
885
+ default: "Saved. The server's answer could not be read, so the screen may not show the latest values."
886
+ }),
887
+ path: Object.freeze([]),
888
+ source: "server",
889
+ severity: "info"
890
+ });
891
+ function issuesOf(error) {
892
+ if (error === null || typeof error !== "object") return void 0;
893
+ try {
894
+ const descriptor = Object.getOwnPropertyDescriptor(error, "issues");
895
+ return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
896
+ } catch {
897
+ return;
898
+ }
735
899
  }
736
- function frameId(frame) {
737
- return frameRecordId(frame);
900
+ const NO_EDITED = new Set();
901
+ function sameValue(a, b) {
902
+ if (Object.is(a, b)) return true;
903
+ try {
904
+ return JSON.stringify(a) === JSON.stringify(b);
905
+ } catch {
906
+ return false;
907
+ }
738
908
  }
739
909
  function RecordFrameHost(props) {
740
910
  const { resource, mode } = props;
911
+ const [failed, setFailed] = useState(NO_ISSUES);
912
+ const named = useMemo(() => failed.length === 0 ? NO_ISSUES : routeServerIssues({ issues: failed }, (segment) => segment, (field) => field !== "_form").fields, [failed]);
913
+ const atFailure = useRef({});
914
+ const [edited, setEdited] = useState(NO_EDITED);
741
915
  const controller = useFormController({
742
916
  resource,
743
917
  mode,
@@ -746,7 +920,8 @@ function RecordFrameHost(props) {
746
920
  ...props.record === void 0 ? {} : { record: props.record },
747
921
  ...props.defaults === void 0 ? {} : { defaults: props.defaults },
748
922
  ...props.onSubmit === void 0 ? {} : { onSubmit: props.onSubmit },
749
- ...props.hostTitle ? { entry: "quick" } : {}
923
+ ...props.hostTitle ? { entry: "quick" } : {},
924
+ serverIssues: named
750
925
  });
751
926
  const inputApplied = useRef(false);
752
927
  const { input } = props;
@@ -765,13 +940,26 @@ function RecordFrameHost(props) {
765
940
  const formId = `record-form-${useId()}`;
766
941
  const root = useRef(null);
767
942
  const [submitError, setSubmitError] = useState(void 0);
943
+ const [submitApplied, setSubmitApplied] = useState(void 0);
944
+ useEffect(() => {
945
+ if (plan === null) return;
946
+ const now = Object.keys(atFailure.current).filter((name) => !edited.has(name) && plan.fields[name] === "active" && !sameValue(controller.field(name).value, atFailure.current[name]));
947
+ if (now.length > 0) setEdited((previous) => new Set([...previous, ...now]));
948
+ }, [
949
+ plan,
950
+ controller,
951
+ edited
952
+ ]);
953
+ const notify = useNoticeSink(resource);
768
954
  const latest = useRef({
769
955
  controller,
770
- onSuccess: props.onSuccess
956
+ onSuccess: props.onSuccess,
957
+ notify
771
958
  });
772
959
  latest.current = {
773
960
  controller,
774
- onSuccess: props.onSuccess
961
+ onSuccess: props.onSuccess,
962
+ notify
775
963
  };
776
964
  const mounted = useRef(true);
777
965
  useEffect(() => {
@@ -782,21 +970,45 @@ function RecordFrameHost(props) {
782
970
  }, []);
783
971
  const submit = useCallback(async () => {
784
972
  setSubmitError(void 0);
973
+ setSubmitApplied(void 0);
974
+ setFailed(NO_ISSUES);
975
+ setEdited(NO_EDITED);
976
+ atFailure.current = {};
785
977
  const outcome = await latest.current.controller.submit();
786
978
  if (!mounted.current) return;
787
- if (outcome.status === "submitted") latest.current.onSuccess?.(outcome.result);
788
- else if (outcome.status === "failed" && !isAbort(outcome.error)) setSubmitError(outcome.error ?? null);
979
+ if (outcome.status === "submitted") {
980
+ const notices = successNoticesOf(outcome.result);
981
+ if (notices.length > 0) latest.current.notify(notices);
982
+ latest.current.onSuccess?.(outcome.result);
983
+ } else if (outcome.status === "failed" && outcome.applied === "yes") {
984
+ latest.current.notify([APPLIED_UNREADABLE]);
985
+ latest.current.onSuccess?.(void 0);
986
+ } else if (outcome.status === "failed" && !isAbort(outcome.error)) {
987
+ setSubmitError(outcome.error ?? null);
988
+ setSubmitApplied(outcome.applied);
989
+ const issues = normalizeIssueInputs(issuesOf(outcome.error)).issues;
990
+ const next = issues.length === 0 ? NO_ISSUES : Object.freeze(issues.map(issueOfInput));
991
+ const held = {};
992
+ const now = latest.current.controller;
993
+ for (const issue of next) {
994
+ const head = issue.path[0];
995
+ if (typeof head === "string" && head !== "_form" && now.plan?.fields[head] === "active") held[head] = now.field(head).value;
996
+ }
997
+ atFailure.current = held;
998
+ setFailed(next);
999
+ }
789
1000
  }, []);
790
1001
  const record = controller.record;
1002
+ const recordText = props.recordId === void 0 ? void 0 : String(props.recordId);
791
1003
  const frame = useMemo(() => ({
792
1004
  resource,
793
- recordId: props.recordId,
1005
+ recordId: recordText,
794
1006
  mode,
795
1007
  record,
796
1008
  controller
797
1009
  }), [
798
1010
  resource,
799
- props.recordId,
1011
+ recordText,
800
1012
  mode,
801
1013
  record,
802
1014
  controller
@@ -817,7 +1029,8 @@ function RecordFrameHost(props) {
817
1029
  guardText.title,
818
1030
  guardText.body
819
1031
  ]);
820
- const typesDecl = useApp().registry.get(resource)?.types;
1032
+ const app = useApp();
1033
+ const typesDecl = app.registry.get(resource)?.types;
821
1034
  const loaded = controller.record;
822
1035
  const unsupportedType = useMemo(() => {
823
1036
  if (mode === "create" || typesDecl === void 0 || loaded === void 0) return void 0;
@@ -829,6 +1042,10 @@ function RecordFrameHost(props) {
829
1042
  typesDecl,
830
1043
  loaded
831
1044
  ]);
1045
+ const shownFailed = useMemo(() => edited.size === 0 ? failed : Object.freeze(failed.filter((issue) => {
1046
+ const head = issue.path[0];
1047
+ return typeof head !== "string" || !edited.has(head);
1048
+ })), [failed, edited]);
832
1049
  const chrome = useMemo(() => ({
833
1050
  ...props.onCancel === void 0 ? {} : { onCancel: props.onCancel },
834
1051
  ...props.submitLabel === void 0 ? {} : { submitLabel: props.submitLabel },
@@ -838,6 +1055,8 @@ function RecordFrameHost(props) {
838
1055
  ...props.hostTitle ? { hostTitle: true } : {},
839
1056
  formId,
840
1057
  submitError,
1058
+ ...submitApplied === void 0 ? {} : { submitApplied },
1059
+ serverIssues: shownFailed,
841
1060
  submit,
842
1061
  root
843
1062
  }), [
@@ -849,6 +1068,8 @@ function RecordFrameHost(props) {
849
1068
  props.hostTitle,
850
1069
  formId,
851
1070
  submitError,
1071
+ submitApplied,
1072
+ shownFailed,
852
1073
  submit
853
1074
  ]);
854
1075
  usePublishDocVariant(mode === "show" ? resource : void 0, record);
@@ -863,45 +1084,51 @@ function RecordFrameHost(props) {
863
1084
  resource,
864
1085
  mode
865
1086
  ]);
866
- const setValues = useCallback((patch) => {
867
- for (const [name, value] of Object.entries(patch)) latest.current.controller.setValue(name, value);
868
- }, []);
869
- const formControl = useMemo(() => ({ setValues }), [setValues]);
870
- const id = frameId(frame);
1087
+ const formControl = useFormControlOf(controller);
1088
+ const id = frameRecordId(frame, app.registry.get(frame.resource));
871
1089
  const rootId = mode === "show" ? tid.pageShow(resource, id ?? "") : mode === "edit" ? tid.pageEdit(resource, id ?? "") : tid.pageCreate(resource);
1090
+ const claimedRoute = useContext(RouteSourceContext);
872
1091
  const scopeValues = mode === "show" ? controller.record : controller.values;
873
1092
  return jsx(RecordScopeProvider, {
874
1093
  resource,
875
- ...props.recordId === void 0 ? {} : { recordId: props.recordId },
1094
+ ...recordText === void 0 ? {} : { recordId: recordText },
876
1095
  values: scopeValues,
1096
+ create: mode === "create",
877
1097
  children: jsx(RouteSourceContext.Provider, {
878
1098
  value: null,
879
- children: jsx(RecordFrameContext.Provider, {
880
- value: frame,
881
- children: jsx(FrameChromeContext.Provider, {
882
- value: chrome,
883
- children: jsxs(FormControlProvider, {
884
- value: formControl,
885
- children: [jsx(DeepLinkScope, {
886
- plan: controller.plan,
887
- enabled: routeScreen && mode !== "create",
888
- children: jsx("div", {
889
- ref: root,
890
- "data-testid": rootId,
891
- className: "flex flex-col gap-[var(--buro-section-gap)]",
892
- children: offering && controller.typeOffer !== null ? jsx(TypeOffer, {
893
- resource,
894
- offer: controller.typeOffer,
895
- onChoose: (type) => chooseType(type)
896
- }) : failure !== null ? jsxs(Fragment$1, { children: [jsx(RecordState, { state: "error" }), jsx(FormActions, {})] }) : mode === "edit" && unsupportedType !== void 0 ? jsx(UndeclaredTypeNotice, {
897
- resource,
898
- typeValue: unsupportedType
899
- }) : jsx(RecordFrameView, { ...props.frame === void 0 ? {} : { frame: props.frame } })
900
- })
901
- }), controller.checkGate === null ? null : jsx(CheckConfirmDialog, {
902
- state: controller.checkGate,
903
- testid: `${tid.form(resource, mode, id)}:checks`
904
- })]
1099
+ children: jsx(ActionRouteContext.Provider, {
1100
+ value: claimedRoute,
1101
+ children: jsx(RecordFrameContext.Provider, {
1102
+ value: frame,
1103
+ children: jsx(FrameChromeContext.Provider, {
1104
+ value: chrome,
1105
+ children: jsxs(FormControlProvider, {
1106
+ value: formControl,
1107
+ children: [jsx(DeepLinkScope, {
1108
+ plan: controller.plan,
1109
+ enabled: routeScreen && mode !== "create",
1110
+ children: jsx("div", {
1111
+ ref: root,
1112
+ "data-testid": rootId,
1113
+ className: "flex flex-col gap-[var(--buro-section-gap)]",
1114
+ children: offering && controller.typeOffer !== null ? jsx(TypeOffer, {
1115
+ resource,
1116
+ offer: controller.typeOffer,
1117
+ onChoose: (type) => chooseType(type)
1118
+ }) : failure !== null ? jsxs(Fragment$1, { children: [jsx(RecordState, { state: "error" }), jsx(FormActions, {})] }) : mode === "edit" && unsupportedType !== void 0 ? jsx(UndeclaredTypeNotice, {
1119
+ resource,
1120
+ typeValue: unsupportedType
1121
+ }) : jsxs(Fragment$1, { children: [jsx(ScopeMismatchBanner, {
1122
+ resource,
1123
+ record: mode === "create" ? controller.trustedSeeds : controller.record,
1124
+ kind: mode === "create" ? "created" : "record"
1125
+ }), jsx(RecordFrameView, { ...props.frame === void 0 ? {} : { frame: props.frame } })] })
1126
+ })
1127
+ }), controller.checkGate === null ? null : jsx(CheckConfirmDialog, {
1128
+ state: controller.checkGate,
1129
+ testid: `${tid.form(resource, mode, id)}:checks`
1130
+ })]
1131
+ })
905
1132
  })
906
1133
  })
907
1134
  })
@@ -913,7 +1140,8 @@ function RecordScreen(props) {
913
1140
  const routeScreen = useContext(RouteScreenClaimContext);
914
1141
  const quickScreen = useContext(QuickScreenClaimContext);
915
1142
  const route = useContext(RouteSourceContext);
916
- const resource = canonicalName(props.resource);
1143
+ const resourceInput = props.resource;
1144
+ const resource = resourceKey(resourceInput);
917
1145
  const tabs = useTabsUrlState(props.mode, routeScreen);
918
1146
  const adapter = useOptionalRouterAdapter();
919
1147
  const router = routeScreen ? adapter : null;
@@ -1175,24 +1403,18 @@ function parse(raw) {
1175
1403
  return [];
1176
1404
  }
1177
1405
  }
1178
- function readRecentRecords(userId) {
1179
- if (typeof window === "undefined") return [];
1180
- try {
1181
- return parse(window.localStorage.getItem(recentStorageKey(userId)));
1182
- } catch {
1183
- return [];
1184
- }
1406
+ function readRecentRecords(store, userId) {
1407
+ return maybeThen(settle(() => store.get(recentStorageKey(userId)), null), parse);
1185
1408
  }
1186
- function writeRecentRecord(userId, entry) {
1187
- if (typeof window === "undefined") return;
1188
- try {
1189
- const key = recentStorageKey(userId);
1409
+ function writeRecentRecord(store, userId, entry) {
1410
+ const key = recentStorageKey(userId);
1411
+ return maybeThen(settle(() => store.get(key), null), (raw) => {
1190
1412
  const envelope = {
1191
1413
  version: 2,
1192
- items: [entry, ...parse(window.localStorage.getItem(key)).filter((item) => !(item.canonicalRef === entry.canonicalRef && item.id === entry.id))].slice(0, 5)
1414
+ items: [entry, ...parse(raw).filter((item) => !(item.canonicalRef === entry.canonicalRef && item.id === entry.id))].slice(0, 5)
1193
1415
  };
1194
- window.localStorage.setItem(key, JSON.stringify(envelope));
1195
- } catch {}
1416
+ return settle(() => store.set(key, JSON.stringify(envelope)), void 0);
1417
+ });
1196
1418
  }
1197
1419
  function text(value) {
1198
1420
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -1218,10 +1440,12 @@ function recentLabel(record, id, isGuarded = () => false) {
1218
1440
  //#region src/layout/use-record-visit.ts
1219
1441
  function useRecordVisit(resource, record, opts) {
1220
1442
  const userId = useCurrentUserId();
1221
- const raw = record?.id;
1222
- const id = typeof raw === "string" || typeof raw === "number" ? String(raw) : void 0;
1443
+ const store = usePlatformStorage().local;
1223
1444
  const app = useApp();
1224
- const fields = app.registry.get(resource)?.fields;
1445
+ const def = app.registry.get(resource);
1446
+ const raw = recordKeyOf(def, record);
1447
+ const id = typeof raw === "string" || typeof raw === "number" ? String(raw) : void 0;
1448
+ const fields = def?.fields;
1225
1449
  const rules = app.ruleSource?.rules(resource) ?? [];
1226
1450
  const isGuarded = (field) => fields !== void 0 && Object.hasOwn(fields, field) && fieldSensitivity(fields[field]) !== void 0 || addressesCapability(rules, "view", {
1227
1451
  kind: "field",
@@ -1235,7 +1459,7 @@ function useRecordVisit(resource, record, opts) {
1235
1459
  useEffect(() => {
1236
1460
  if (userId === null || !isCurrentData || id === void 0 || id === "") return;
1237
1461
  if (label === void 0) return;
1238
- writeRecentRecord(userId, {
1462
+ writeRecentRecord(store, userId, {
1239
1463
  canonicalRef,
1240
1464
  id,
1241
1465
  label,
@@ -1243,6 +1467,7 @@ function useRecordVisit(resource, record, opts) {
1243
1467
  at: new Date().toISOString()
1244
1468
  });
1245
1469
  }, [
1470
+ store,
1246
1471
  userId,
1247
1472
  isCurrentData,
1248
1473
  canonicalRef,
@@ -1253,12 +1478,6 @@ function useRecordVisit(resource, record, opts) {
1253
1478
  }
1254
1479
  //#endregion
1255
1480
  //#region src/layout/record-page.tsx
1256
- function recordErrorState(error) {
1257
- const { kind } = normalizeDataError(error);
1258
- if (kind === "not-found" || kind === "forbidden") return "not-found";
1259
- if (kind === "auth") return "session-expired";
1260
- return "error";
1261
- }
1262
1481
  function useMountedRef() {
1263
1482
  const mounted = useRef(true);
1264
1483
  useEffect(() => {
@@ -1281,7 +1500,7 @@ function RecordRouteHeader({ resource, action, id, record }) {
1281
1500
  ...action === "create" ? {} : { recordName }
1282
1501
  });
1283
1502
  }
1284
- function RecordShow({ resource, id, record, shown }) {
1503
+ function RecordShow({ resource, id, recordId, record, shown }) {
1285
1504
  return jsxs("div", { children: [jsx(RecordRouteHeader, {
1286
1505
  resource,
1287
1506
  action: "show",
@@ -1290,11 +1509,11 @@ function RecordShow({ resource, id, record, shown }) {
1290
1509
  }), jsx(RecordScreen, {
1291
1510
  resource,
1292
1511
  mode: "show",
1293
- recordId: id,
1512
+ recordId,
1294
1513
  record
1295
1514
  })] });
1296
1515
  }
1297
- function RecordEdit({ resource, id, record, shown }) {
1516
+ function RecordEdit({ resource, id, recordId, record, shown }) {
1298
1517
  const app = useApp();
1299
1518
  const navigate = useRouteNavigate();
1300
1519
  const mounted = useMountedRef();
@@ -1307,7 +1526,7 @@ function RecordEdit({ resource, id, record, shown }) {
1307
1526
  }), jsx(RecordScreen, {
1308
1527
  resource,
1309
1528
  mode: "edit",
1310
- recordId: id,
1529
+ recordId,
1311
1530
  record,
1312
1531
  onCancel: () => navigate(back),
1313
1532
  onSuccess: () => {
@@ -1346,12 +1565,12 @@ function RecordCreate({ resource, defaults }) {
1346
1565
  })
1347
1566
  })] });
1348
1567
  }
1349
- function RecordRead({ resource, action, id }) {
1568
+ function RecordRead({ resource, action, id, recordId }) {
1350
1569
  const app = useApp();
1351
1570
  const def = app.registry.get(resource);
1352
1571
  const projection = useMemo(() => def === void 0 ? void 0 : dtoFields(def, "record"), [def]);
1353
1572
  const query = useOne(resource, {
1354
- id,
1573
+ id: recordId,
1355
1574
  retry: retryRecordRead,
1356
1575
  ...projection === void 0 ? {} : { projection }
1357
1576
  });
@@ -1391,30 +1610,39 @@ function RecordRead({ resource, action, id }) {
1391
1610
  children: action === "edit" ? jsx(RecordEdit, {
1392
1611
  resource,
1393
1612
  id,
1613
+ recordId,
1394
1614
  record: loaded,
1395
1615
  shown
1396
1616
  }) : jsx(RecordShow, {
1397
1617
  resource,
1398
1618
  id,
1619
+ recordId,
1399
1620
  record: loaded,
1400
1621
  shown
1401
1622
  })
1402
1623
  })
1403
1624
  });
1404
1625
  }
1405
- if (query.error != null) {
1406
- const state = recordErrorState(query.error);
1407
- return jsx(RecordState, {
1408
- state,
1409
- onRetry: state === "error" ? () => void query.refetch() : void 0
1410
- });
1411
- }
1626
+ if (query.error != null) return jsx(LoadError, {
1627
+ surface: "record",
1628
+ resource,
1629
+ error: query.error,
1630
+ retry: () => void query.refetch()
1631
+ });
1412
1632
  return jsx(RecordState, { state: "loading" });
1413
1633
  }
1414
1634
  function RecordRouteBody(props) {
1415
1635
  if (props.action === "create") return jsx(RecordCreate, { ...props });
1416
1636
  if (props.id === "") return jsx(RecordState, { state: "not-found" });
1417
- return jsx(RecordRead, { ...props });
1637
+ return jsx(RecordReadRoute, { ...props });
1638
+ }
1639
+ function RecordReadRoute(props) {
1640
+ const recordId = useRouteRecordId(props.resource, props.id);
1641
+ if (recordId === void 0) return jsx(InvalidRouteId, { resource: props.resource });
1642
+ return jsx(RecordRead, {
1643
+ ...props,
1644
+ recordId
1645
+ });
1418
1646
  }
1419
1647
  function RecordRoute(props) {
1420
1648
  const missingRuntime = useOptionalDataRuntime() === null;
@@ -1437,7 +1665,8 @@ function RecordPage(props) {
1437
1665
  const app = useApp();
1438
1666
  const match = useMatch();
1439
1667
  const id = match === null ? null : match.id ?? "";
1440
- const { resource, action } = props;
1668
+ const { action } = props;
1669
+ const resource = props.resource === void 0 ? void 0 : routeResourceKey(props.resource);
1441
1670
  const onRecordRoute = recordActionOf(match) !== void 0;
1442
1671
  const problem = recordPageProblem({
1443
1672
  props,
@@ -1567,10 +1796,24 @@ function NavAreaNotFound() {
1567
1796
  });
1568
1797
  }
1569
1798
  const adapterNoticeGiven = new WeakSet();
1799
+ const chromeNoticeGiven = new WeakSet();
1570
1800
  function NavAreaPage() {
1571
1801
  const match = useMatch();
1572
1802
  const app = useApp();
1573
1803
  const noAdapter = useOptionalRouterAdapter() === null;
1804
+ const chromeMounted = useNavChromeMounted();
1805
+ const resolveLayout = useLayoutResolver();
1806
+ const chromeGroup = match?.groupChain.find((g) => resolveLayout(g.layout)) ?? null;
1807
+ useEffect(() => {
1808
+ if (process.env.NODE_ENV === "production") return;
1809
+ if (chromeMounted || !chromeGroup || chromeNoticeGiven.has(app)) return;
1810
+ chromeNoticeGiven.add(app);
1811
+ console.error(`[burojs] <NavAreaPage> is inside the nav group "${canonicalNavKey(chromeGroup)}" whose layout "${chromeGroup.layout}" draws area chrome, and no <NavChrome> is mounted above it. <AppShell> mounts one by default; with your own frame, wrap your routes in <NavChrome>.`);
1812
+ }, [
1813
+ app,
1814
+ chromeMounted,
1815
+ chromeGroup
1816
+ ]);
1574
1817
  useEffect(() => {
1575
1818
  if (!noAdapter || process.env.NODE_ENV === "production") return;
1576
1819
  if (adapterNoticeGiven.has(app)) return;
@@ -1579,10 +1822,10 @@ function NavAreaPage() {
1579
1822
  }, [app, noAdapter]);
1580
1823
  return jsx(Suspense, {
1581
1824
  fallback: jsx(NavAreaPageSkeleton, {}),
1582
- children: match ? jsx(NavChrome, { children: jsx(ResourcePageRouter, { match }) }) : jsx(NavAreaNotFound, {})
1825
+ children: match ? jsx(ResourcePageRouter, { match }) : jsx(NavAreaNotFound, {})
1583
1826
  });
1584
1827
  }
1585
1828
  //#endregion
1586
- export { useLayoutResolver as A, useNavChromeBoundary as C, usePageAllowed as D, useNavVisible as E, canonicalNavKey as M, canonicalResourceKey as N, layoutSlot as O, NavChromeBoundaryProvider as S, useMatch as T, useFormControl as _, recordErrorState as a, RecordState as b, readRecentRecords as c, recordTrailRoute as d, useTrailContext as f, RecordScreen as g, usePublishedRecordTrail as h, RecordPage as i, buildNavItems as j, useLayout as k, headerTrail as l, usePublishRecordTrail as m, NavAreaPage as n, useRecordVisit as o, recordTrailKey as p, ResourcePageRouter as r, RECENT_LABEL_KEYS as s, NavAreaNotFound as t, recordTrail as u, PlannedBody as v, useHref as w, NavChrome as x, ResourceListPage as y };
1829
+ export { TabsLayout as A, usePageAllowed as C, useLayout as D, layoutSlot as E, canonicalNavKey as M, canonicalResourceKey$1 as N, useLayoutResolver as O, useNavVisible as S, BUILTIN_LAYOUTS as T, PlannedBody as _, useRecordVisit as a, useHref as b, headerTrail as c, useTrailContext as d, recordTrailKey as f, useResourceDimensionOptions as g, RecordScreen as h, RecordPage as i, SecondarySidebarLayout as j, useReportUnresolvedLayouts as k, recordTrail as l, usePublishedRecordTrail as m, NavAreaPage as n, RECENT_LABEL_KEYS as o, usePublishRecordTrail as p, ResourcePageRouter as r, readRecentRecords as s, NavAreaNotFound as t, recordTrailRoute as u, ResourceListPage as v, useResourceVisible as w, useMatch as x, NavChromeMountedProvider as y };
1587
1830
 
1588
- //# sourceMappingURL=resource-page-router-3RUf9d1j.js.map
1831
+ //# sourceMappingURL=resource-page-router-Diw6S6TK.js.map