@elytracms/studio 0.0.27 → 0.0.28
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.
- package/convex/assets.ts +19 -10
- package/convex/authority.ts +60 -0
- package/convex/cliTokens.ts +4 -4
- package/convex/cms.ts +12 -12
- package/convex/content.ts +10 -3
- package/convex/delivery.ts +22 -4
- package/convex/guard.ts +28 -11
- package/convex/members.ts +40 -7
- package/convex/redirects.ts +67 -0
- package/convex/references.ts +11 -11
- package/convex/schema.ts +30 -1
- package/convex/sync.ts +17 -10
- package/convex/users.ts +63 -0
- package/dist/components/app-views/default-modules.d.ts +33 -14
- package/dist/components/app-views/default-modules.js +59 -20
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +32 -0
- package/dist/components/app-views/module.js +16 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +15 -1
- package/dist/components/app-views/sidebar.js +10 -0
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/cms/asset-picker.js +4 -4
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell.js +16 -13
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +11 -26
- package/dist/components/cms/cms-store.js +25 -160
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +10 -0
- package/dist/components/cms/collection-table.js +72 -72
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +14 -0
- package/dist/components/cms/detail-editor.js +224 -72
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +26 -0
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/editable-cell.js +12 -7
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +2 -0
- package/dist/components/cms/field-control.js +40 -30
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +18 -0
- package/dist/components/cms/filter-builder.js +47 -6
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/import-export.js +5 -3
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +8 -9
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +13 -10
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/use-cell-edit.js +13 -7
- package/dist/components/cms/use-cell-edit.js.map +1 -1
- package/dist/components/cms/use-document-versions.d.ts +16 -0
- package/dist/components/cms/use-document-versions.js +31 -0
- package/dist/components/cms/use-document-versions.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +30 -0
- package/dist/components/cms/version-compare-text.js +113 -0
- package/dist/components/cms/version-compare-text.js.map +1 -0
- package/dist/components/cms/version-compare.d.ts +12 -0
- package/dist/components/cms/version-compare.js +271 -0
- package/dist/components/cms/version-compare.js.map +1 -0
- package/dist/components/cms/versions-pane.d.ts +3 -0
- package/dist/components/cms/versions-pane.js +47 -38
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.d.ts +21 -6
- package/dist/components/cms/view-bar.js +42 -57
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.d.ts +15 -0
- package/dist/components/collection-view/cms-collection-view-source.js +158 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -0
- package/dist/components/collection-view/collection-view-context.d.ts +10 -0
- package/dist/components/collection-view/collection-view-context.js +26 -0
- package/dist/components/collection-view/collection-view-context.js.map +1 -0
- package/dist/components/current-user-menu.js +8 -1
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +18 -0
- package/dist/components/redirects/redirects-collection-view-source.js +114 -0
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -0
- package/dist/components/redirects/redirects-module.d.ts +18 -0
- package/dist/components/redirects/redirects-module.js +43 -0
- package/dist/components/redirects/redirects-module.js.map +1 -0
- package/dist/components/redirects/redirects-schema.d.ts +10 -0
- package/dist/components/redirects/redirects-schema.js +42 -0
- package/dist/components/redirects/redirects-schema.js.map +1 -0
- package/dist/components/redirects/redirects-store.d.ts +46 -0
- package/dist/components/redirects/redirects-store.js +124 -0
- package/dist/components/redirects/redirects-store.js.map +1 -0
- package/dist/components/references/references-panel.js +2 -2
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +14 -5
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/users/user-search.d.ts +8 -0
- package/dist/components/users/user-search.js +13 -0
- package/dist/components/users/user-search.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +28 -0
- package/dist/components/users/users-collection-view-source.js +157 -0
- package/dist/components/users/users-collection-view-source.js.map +1 -0
- package/dist/components/users/users-module.d.ts +23 -0
- package/dist/components/users/users-module.js +61 -0
- package/dist/components/users/users-module.js.map +1 -0
- package/dist/components/users/users-schema.d.ts +19 -0
- package/dist/components/users/users-schema.js +38 -0
- package/dist/components/users/users-schema.js.map +1 -0
- package/dist/components/users/users-store.d.ts +40 -0
- package/dist/components/users/users-store.js +87 -0
- package/dist/components/users/users-store.js.map +1 -0
- package/dist/lib/cms-views/index.d.ts +4 -5
- package/dist/lib/cms-views/index.js +4 -5
- package/dist/lib/cms-views/index.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +48 -1
- package/dist/lib/cms-views/view-model.js +77 -1
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +251 -0
- package/dist/lib/collection-view/collection-view-source.js +10 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +7 -0
- package/dist/lib/operations/session-hydration.js +7 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +5 -1
- package/dist/lib/persistence/convex-adapter.js +148 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -8
- package/dist/lib/workspace/index.js +3 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +4 -0
- package/dist/router/match.js +8 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/popover.d.ts +13 -0
- package/dist/ui/popover.js +17 -0
- package/dist/ui/popover.js.map +1 -0
- package/dist/ui/row-item.d.ts +16 -0
- package/dist/ui/row-item.js +18 -0
- package/dist/ui/row-item.js.map +1 -0
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-modules.js","sourceRoot":"","sources":["../../../src/components/app-views/default-modules.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGjC,OAAO,EACL,MAAM,EACN,QAAQ,EACR,OAAO,EACP,KAAK,EACL,UAAU,EACV,MAAM,EACN,QAAQ,IAAI,YAAY,EACxB,QAAQ,GACT,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AAGhF,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrF,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAA;AAC/E,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,QAAQ,GAKT,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AA2DzD,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,MAAM,CAAA;AACjG,CAAC;AAED,0FAA0F;AAC1F,SAAS,SAAS,CAAC,MAAqB;IACtC,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,EAAE,CAAA;IAC/B,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,OAAO,MAAM,CAAC,UAAU,EAAE,CAAA;IACxD,IAAI,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxD,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IACnD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,SAAwB,EAAE;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAA;IAChC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAA;IACpD,OAAO;QACL,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;QAC5B,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,2CAA2C;QAC9E,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,QAAQ;QAC7B,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,IAAI,KAAK;QACxD,4EAA4E;QAC5E,sFAAsF;QACtF,SAAS,EAAE,MAAM;YACf,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CACX,QAAQ,CAAC,MAAM,KAAK,CAAC;gBACnB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE;gBAC5C,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;oBACrB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE;oBACpD,CAAC,CAAC,IAAI;YACd,CAAC,CAAC,WAAW;QACf,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CACf,KAAC,SAAS,IACR,OAAO,EAAE,GAAG,CAAC,OAAO,EACpB,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,YAAY,IAAI,IAAI,EAC/C,UAAU,EAAE,GAAG,CAAC,QAAQ,CAAC,UAAU,IAAI,IAAI,EAC3C,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EACvB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EACvB,gBAAgB,EAAE,KAAK,GACvB,CACH;KACF,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,UAAyB;IAClD,MAAM,SAAS,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;IACzC,OAAO,QAAQ,CACb,OAAO,CAAC;QACN,UAAU,EAAE,UAAU,CAAC,EAAE;QACzB,KAAK,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,IAAI,UAAU,CAAC,EAAE;QAC9C,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;KAC3E,CAAC,CACH,CAAA;AACH,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,cAAc,CAAC,SAA4B,EAAE;IAC3D,OAAO;QACL,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,YAAY;QACnC,WAAW,EAAE,2DAA2D;QACxE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,MAAM;QAC3B,mBAAmB,EAAE,YAAY;QACjC,SAAS,EAAE,kBAAkB;QAC7B,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CACf,KAAC,gBAAgB,IACf,SAAS,EAAE,GAAG,CAAC,SAAS,EACxB,WAAW,EAAE,GAAG,CAAC,QAAQ,CAAC,WAAW,IAAI,IAAI,EAC7C,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,GACvB,CACH;KACF,CAAA;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,SAA4B,EAAE;IACvD,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,QAAQ;QAC/B,WAAW,EAAE,yEAAyE;QACtF,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,QAAQ;QAC7B,2FAA2F;QAC3F,mBAAmB,EAAE,KAAK;QAC1B,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACd,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CACnD,KAAC,gBAAgB,IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAC,QAAQ,GAAG,CAC7D,CAAC,CAAC,CAAC,CACF,KAAC,WAAW,IAAC,OAAO,EAAE,GAAG,CAAC,OAAO,GAAI,CACtC;KACJ,CAAA;AACH,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,UAAU,CAAC,SAA4B,EAAE;IACvD,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,QAAQ;QAC/B,WAAW,EAAE,qCAAqC;QAClD,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,KAAK;QAC1B,mBAAmB,EAAE,QAAQ;QAC7B,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CACf,KAAC,cAAc,IAAC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAC,QAAQ,EAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,YAElF,KAAC,YAAY,IAAC,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,GAAI,GAC3D,CAClB;KACF,CAAA;AACH,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,cAAc,CAAC,SAA4B,EAAE;IAC3D,OAAO;QACL,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,YAAY;QACnC,WAAW,EAAE,gFAAgF;QAC7F,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,MAAM;QAC3B,mBAAmB,EAAE,YAAY;QACjC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACd,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CACnD,KAAC,gBAAgB,IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAC,YAAY,GAAG,CACjE,CAAC,CAAC,CAAC,CACF,KAAC,cAAc,KAAG,CACnB;KACJ,CAAA;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,YAAY,CAAC,SAA4B,EAAE;IACzD,OAAO;QACL,EAAE,EAAE,UAAU;QACd,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,UAAU;QACjC,WAAW,EAAE,yCAAyC;QACtD,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,YAAY;QACjC,mBAAmB,EAAE,UAAU;QAC/B,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CACf,KAAC,cAAc,IAAC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAC,UAAU,EAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,YACpF,KAAC,YAAY,IAAC,OAAO,EAAE,GAAG,CAAC,OAAO,GAAI,GACvB,CAClB;KACF,CAAA;AACH,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,SAAS;QAChB,WAAW,EAAE,+DAA+D;QAC5E,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;QACf,uEAAuE;QACvE,uEAAuE;QACvE,KAAC,QAAQ,IACP,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE;gBACN,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI;oBACrB,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;oBACjD,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,MAAM,CAAC;aACrC,EACD,OAAO,SACP,CACH;KACF,CAAA;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,UAAU,CAAC,MAAwB;IACjD,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QACtC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,KAAK;QAC/C,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,UAAU;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3F,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAA6B;IACrD,cAAc,EAAE;IAChB,OAAO,EAAE;IACT,UAAU,EAAE;IACZ,UAAU,EAAE;IACZ,cAAc,EAAE;IAChB,YAAY,EAAE;IACd,WAAW,EAAE;CACd,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB;;;;OAIG;IACH,GAAG,EAAE,CAAC,SAAwB,EAAE,EAAmB,EAAE;QACnD,0EAA0E;QAC1E,IAAI,MAAM,CAAC,UAAU;YAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QACvD,oFAAoF;QACpF,MAAM,IAAI,GAAoB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAA;QACrE,IAAI,MAAM,CAAC,QAAQ;YAAE,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;;YAElD,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,EAAE,CAC5B,mBAAmB,CAAC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;QAC/E,OAAO,IAAI,CAAA;IACb,CAAC;IACD,sCAAsC;IACtC,UAAU,EAAE,CAAC,SAA4B,EAAE,EAAmB,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACjG,yBAAyB;IACzB,MAAM,EAAE,CAAC,SAA4B,EAAE,EAAmB,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACzF,uCAAuC;IACvC,MAAM,EAAE,CAAC,SAA4B,EAAE,EAAmB,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACzF,wBAAwB;IACxB,QAAQ,EAAE,CAAC,SAA4B,EAAE,EAAmB,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAC7F,kCAAkC;IAClC,UAAU,EAAE,CAAC,SAA4B,EAAE,EAAmB,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACjG,+EAA+E;IAC/E,IAAI,EAAE,CAAC,MAAwB,EAAmB,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACjF,6DAA6D;IAC7D,KAAK,EAAE,CAAC,KAAa,EAAE,QAA2B,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC;IACjF,uCAAuC;IACvC,SAAS,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE;CACxB,CAAA;AAEV,oEAAoE;AAEpE;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,WAAW,EACX,KAAK,GAKN;IACC,IAAI,KAAK,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAC,YAAY,GAAG,CAAA;IAC9D,CAAC;IACD,OAAO,CACL,eAAK,SAAS,EAAC,qBAAqB,aAClC,gBAAO,SAAS,EAAC,0EAA0E,YACzF,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,IAAI,SAAS,GAAI,GAChF,EACR,cAAK,SAAS,EAAC,8BAA8B,YAC1C,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,CACtB,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,GAAI,CAC1C,CAAC,CAAC,CAAC,CACF,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,GAAI,CACpE,GACG,IACF,CACP,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,EACjB,OAAO,EACP,YAAY,EACZ,UAAU,EACV,KAAK,EACL,KAAK,EACL,gBAAgB,GAQjB;IACC,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAA;IAC5B,IAAI,KAAK,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAC,KAAK,GAAG,CAAA;IACvD,CAAC;IAED,8FAA8F;IAC9F,MAAM,GAAG,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAA;IAEzC,OAAO;IACL,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,KAAC,cAAc,cACZ,CAAC,GAAG,EAAE,EAAE,CAAC,CACR,KAAC,WAAW,IAEV,SAAS,EAAE,SAAS,EACpB,kBAAkB,EAAE,GAAG,EAAE,WAAW,IAAI,GAAG,EAAE,WAAW,EACxD,gBAAgB,EAAE,GAAG,EAAE,SAAS,IAAI,GAAG,EAAE,SAAS,EAClD,oBAAoB,EAAE,GAAG,EAAE,aAAa,EACxC,mBAAmB,EACjB,GAAG;gBACD,CAAC,CAAC,oEAAoE;oBACpE,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;gBACrF,CAAC,CAAC,GAAG,EAAE,YAAY,YAGvB,eAAK,SAAS,EAAC,8BAA8B,aAC3C,KAAC,qBAAqB,KAAG,EAIzB,cAAK,SAAS,EAAC,gCAAgC,YAC7C,KAAC,UAAU,IACT,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,KAAK,EACZ,gBAAgB,EAAE,gBAAgB,GAClC,GACE,IACF,IA1BD,SAAS,CA2BF,CACf,GACc,CAClB,CAAA;AACH,CAAC;AAED,iFAAiF;AACjF,SAAS,UAAU,CAAC,EAClB,SAAS,EACT,YAAY,EACZ,UAAU,EACV,KAAK,EACL,gBAAgB,GAOjB;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IAEpB,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,KAAC,QAAQ,IAAC,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,GAAI,CAAA;IAExG,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,YAAY,CAAC,CAAA;QAClD,4EAA4E;QAC5E,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,UAAU,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1C,OAAO,KAAC,aAAa,IAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAI,CAAA;QAC5E,CAAC;QACD,OAAO,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAI,CAAA;IAC9E,CAAC;IAED,OAAO,CACL,KAAC,YAAY,IACX,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,KAAK,GAChB,CACH,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,EAAE,SAAS,EAAE,YAAY,EAA+C;IAC7F,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,YAAY,CAAC,CAAA;IAClD,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,CAAC,YAAY,CAAC,CAAA;IAC1C,MAAM,UAAU,GAAG,UAAU;QAC3B,CAAC,CAAC,wBAAwB,CAAC,UAAU,EAAE,IAAI,CAAC;QAC5C,CAAC,CAAE,EAAE,IAAI,EAAE,OAAO,EAAY,CAAA;IAEhC,+EAA+E;IAC/E,0EAA0E;IAC1E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO;YAAE,GAAG,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAA;IAC5E,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAA;IAExC,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAChC,OAAO,CACL,KAAC,YAAY,IAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,GAAI,CAClG,CAAA;IACH,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAChC,OAAO,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAI,CAAA;IAC9E,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,oCAAoC,YACjD,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,yBACjB,UAAU,EAAE,IAAI,EAAE,KAAK,IAAI,YAAY,cAC3C,GACH,CACP,CAAA;AACH,CAAC;AAED,yEAAyE;AACzE,SAAS,qBAAqB;IAC5B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,IAAI,CAAC,GAAG,CAAC,YAAY;QAAE,OAAO,IAAI,CAAA;IAClC,OAAO,CACL,cAAK,SAAS,EAAC,qCAAqC,YAClD,KAAC,oBAAoB,IAAC,KAAK,EAAE,GAAG,CAAC,YAAY,EAAE,SAAS,EAAE,GAAG,CAAC,iBAAiB,GAAI,GAC/E,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Blocks,\n Database,\n FileCog,\n Image,\n LayoutGrid,\n Puzzle,\n Settings as SettingsIcon,\n Signpost,\n} from 'lucide-react'\nimport { isSingleton, resolveSingletonDocument } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { Project, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Navigate, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Text } from '@elytracms/studio/ui'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport {\n cmsDeepLink,\n componentsDeepLink,\n groupNode,\n separatorNode,\n viewNode,\n type AppViewContext,\n type AppViewDeepLink,\n type AppViewModule,\n type SidebarViewNode,\n} from './module'\nimport { SectionSurface } from '../section-surface'\nimport { SectionStateView } from '../section-state'\nimport { SettingsForm } from '../settings-form'\nimport { AssetLibrary } from '../assets/asset-library'\nimport { RoutesShell } from '../routes/routes-shell'\nimport { ExtensionsView } from '../extensions/extensions-view'\nimport { RegistryBrowser } from '../components/registry-browser'\nimport { ComponentsIndex } from '../components/components-index'\nimport { ComponentDetail } from '../components/component-detail'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { focusCollectionList } from '../cms/collection-focus'\nimport { CmsIndex } from '../cms/cms-index'\nimport { CollectionTable } from '../cms/collection-table'\nimport { DetailEditor } from '../cms/detail-editor'\nimport { OperationErrorBanner } from '../operation-error'\n\n/**\n * EC-237/239 — the studio's BUILT-IN views + the consumer-facing `nav` namespace,\n * an instance of the EC-236 customization seam.\n *\n * Each built-in surface (the former `StudioSectionResolver` case body) is promoted\n * to a self-contained {@link AppViewModule} factory. The `nav` namespace is the\n * single, typed builder a consuming repo's `studio.config.tsx` uses to declare its\n * section rail — `nav.cms(...)`, `nav.group(...)`, `nav.view(...)` — passing the\n * view objects (and a focus config) DIRECTLY, never a string id. The CMS surface\n * is a factory ({@link cmsView}) so a focused view (\"Catalog\", \"Categories\") is the\n * SAME surface instantiated with config, not a copy — the EC-239 reuse proof.\n */\n\n// --- The CMS surface, reusable via a config factory (the EC-239 reuse seam) ---\n\n/** Config for a CMS-backed view — `collections`/`collection` focus the lens (EC-239). */\nexport interface CmsViewConfig {\n /** Explicit stable id (the route segment). Omitted → derived from the focus. */\n id?: string\n label?: string\n description?: string\n icon?: LucideIcon\n /**\n * Scope the rail auto-expansion (EC-293) to these collection ids, in order. The\n * parent expands into one focused-lens child per id; the parent index shows the\n * same subset. Omitted → every collection. Ignored for a single-collection lens.\n */\n collections?: readonly string[]\n /** Single-collection lens: a LEAF rail item that opens straight into this collection. */\n collection?: string\n /**\n * EC-293 escape hatch: explicit rail children for this CMS parent, overriding the\n * default per-collection auto-expansion. Ignored for a single-collection lens.\n */\n children?: SidebarViewNode[]\n /** The availability section this view rides on (EC-221); defaults to `cms`. */\n availabilitySection?: SectionId\n}\n\n/** Common label/icon overrides for a built-in view (`nav.routes({ label: 'URLs' })`). */\nexport interface BuiltinViewConfig {\n label?: string\n icon?: LucideIcon\n}\n\n/** Config for a fully custom view (`nav.view({...})`) — the net-new surface escape hatch. */\nexport interface CustomViewConfig {\n /** Explicit stable id (the route segment). Omitted → a slug of the label. */\n id?: string\n label: string\n description?: string\n icon?: LucideIcon\n render: (ctx: AppViewContext) => ReactNode\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n availabilitySection?: SectionId\n}\n\nfunction slugify(label: string): string {\n return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'view'\n}\n\n/** Derive a stable, deterministic id from the CMS focus (overridable via `config.id`). */\nfunction cmsViewId(config: CmsViewConfig): string {\n if (config.id) return config.id\n if (config.collection) return `cms:${config.collection}`\n if (config.collections && config.collections.length > 0) {\n return `cms:${[...config.collections].join('-')}`\n }\n return 'cms'\n}\n\n/**\n * Build a CMS view — the default `cms` view, or a focused instance. Every instance\n * shares one surface ({@link CmsScreen}); `collections` narrows the sidebar + index\n * to a subset, `collection` is a single-collection lens that opens straight into\n * that collection's list. The id is derived from the focus (deterministic, stable\n * across rail reorders) unless `id` is given — so two focused views never collide.\n */\nexport function cmsView(config: CmsViewConfig = {}): AppViewModule {\n const single = config.collection\n const focus = single ? [single] : config.collections\n return {\n id: cmsViewId(config),\n label: config.label ?? 'CMS',\n description: config.description ?? 'Model collections, fields, and documents.',\n icon: config.icon ?? Database,\n availabilitySection: config.availabilitySection ?? 'cms',\n // A single-collection lens routes `[]`→that collection, `[d]`→its document;\n // an unfocused / multi-collection view uses the full index/collection/document arity.\n parsePath: single\n ? (segments) =>\n segments.length === 0\n ? { collectionId: single, documentId: null }\n : segments.length === 1\n ? { collectionId: single, documentId: segments[0]! }\n : null\n : cmsDeepLink,\n render: (ctx) => (\n <CmsScreen\n project={ctx.project}\n collectionId={ctx.deepLink.collectionId ?? null}\n documentId={ctx.deepLink.documentId ?? null}\n state={ctx.search.state}\n field={ctx.search.field}\n focusCollections={focus}\n />\n ),\n }\n}\n\n/**\n * A focused-lens rail child for one collection (EC-293) — the SAME `cms:<id>` view a\n * single-collection `nav.cms({ collection })` produces, carrying the collection's own\n * label + a singleton-aware icon (the former `CollectionsSidebar` row, lifted into\n * the rail). CMS auto-expansion maps the project's collections through this.\n */\nfunction cmsCollectionLeaf(collection: CollectionDef): SidebarViewNode {\n const singleton = isSingleton(collection)\n return viewNode(\n cmsView({\n collection: collection.id,\n label: collection.form?.label ?? collection.id,\n icon: singleton ? FileCog : collection.kind === 'asset' ? Image : Database,\n }),\n )\n}\n\n/** The components registry browser view. */\nexport function componentsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'components',\n label: config.label ?? 'Components',\n description: 'Browse the registry of primitives and project components.',\n icon: config.icon ?? Blocks,\n availabilitySection: 'components',\n parsePath: componentsDeepLink,\n render: (ctx) => (\n <ComponentsScreen\n projectId={ctx.projectId}\n componentId={ctx.deepLink.componentId ?? null}\n state={ctx.search.state}\n />\n ),\n }\n}\n\n/** The resolved routes & redirects view (read-only; routing is config-as-code). */\nexport function routesView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'routes',\n label: config.label ?? 'Routes',\n description: 'View the resolved URL routes & redirects (defined in elytra.config.ts).',\n icon: config.icon ?? Signpost,\n // Routes/redirects are CMS routing concerns, so the surface rides on the CMS availability.\n availabilitySection: 'cms',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Routes\" />\n ) : (\n <RoutesShell project={ctx.project} />\n ),\n }\n}\n\n/** The asset library view. */\nexport function assetsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'assets',\n label: config.label ?? 'Assets',\n description: 'Manage uploaded media and metadata.',\n icon: config.icon ?? Image,\n availabilitySection: 'assets',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"assets\" forcedState={ctx.search.state}>\n {/* The AssetsProvider is mounted at the project level (EC-195). */}\n <AssetLibrary projectId={ctx.projectId} focusAssetId={ctx.search.asset} />\n </SectionSurface>\n ),\n }\n}\n\n/** The extensions marketplace view. */\nexport function extensionsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'extensions',\n label: config.label ?? 'Extensions',\n description: 'Install components, field types, adapters, and templates from the marketplace.',\n icon: config.icon ?? Puzzle,\n availabilitySection: 'extensions',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Extensions\" />\n ) : (\n <ExtensionsView />\n ),\n }\n}\n\n/** The project settings view. */\nexport function settingsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'settings',\n label: config.label ?? 'Settings',\n description: 'Configure project metadata and locales.',\n icon: config.icon ?? SettingsIcon,\n availabilitySection: 'settings',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"settings\" forcedState={ctx.search.state}>\n <SettingsForm project={ctx.project} />\n </SectionSurface>\n ),\n }\n}\n\n/** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */\nexport function builderView(): AppViewModule {\n return {\n id: 'builder',\n label: 'Builder',\n description: 'Legacy builder route — redirects to the page document editor.',\n icon: Blocks,\n hidden: true,\n render: (ctx) => (\n // Retired surface (EC-187 slice 6): a page is a document in the `page`\n // collection. `?page=<id>` lands on that document; else the page list.\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{\n projectId: ctx.projectId,\n _splat: ctx.search.page\n ? studioPath.cmsDocument('page', ctx.search.page)\n : studioPath.cmsCollection('page'),\n }}\n replace\n />\n ),\n }\n}\n\n/** Build a fully custom view (the `nav.view(...)` escape hatch). */\nexport function customView(config: CustomViewConfig): AppViewModule {\n return {\n id: config.id ?? slugify(config.label),\n label: config.label,\n description: config.description ?? config.label,\n icon: config.icon ?? LayoutGrid,\n render: config.render,\n ...(config.parsePath ? { parsePath: config.parsePath } : {}),\n ...(config.availabilitySection ? { availabilitySection: config.availabilitySection } : {}),\n }\n}\n\n/**\n * The studio's built-in views in canonical rail order (components · CMS · routes ·\n * assets · extensions · settings), plus the hidden `builder` redirect (routable but\n * unlisted). The default rail (no `defineStudio({ sidebar })`) is these as plain\n * entries; the router registry always includes them so deep-links resolve even\n * when a consumer's rail omits one.\n */\nexport const DEFAULT_VIEWS: readonly AppViewModule[] = [\n componentsView(),\n cmsView(),\n routesView(),\n assetsView(),\n extensionsView(),\n settingsView(),\n builderView(),\n]\n\n/**\n * The `nav` namespace — the single typed builder for `studio.config.tsx`'s section\n * rail. Each call returns a {@link SidebarViewNode} (or a group / separator) that\n * carries its view DIRECTLY: `nav.cms({ collection })` is a focused CMS view,\n * `nav.view({ render })` a net-new surface, `nav.group(label, [...])` a header with\n * sub-views, `nav.separator()` a divider. No string ids, no `moduleId` — the rail\n * is the views.\n */\nexport const nav = {\n /**\n * The CMS view. `nav.cms()` / `nav.cms({ collections })` is a PARENT that nests its\n * collections in the rail (each a focused `cms:<id>` lens); `nav.cms({ collection })`\n * is a single-collection LEAF; `nav.cms({ children })` supplies explicit children.\n */\n cms: (config: CmsViewConfig = {}): SidebarViewNode => {\n // A single-collection lens is a leaf — it opens that collection directly.\n if (config.collection) return viewNode(cmsView(config))\n // Otherwise a parent that expands its collections as nested rail children (EC-293).\n const node: SidebarViewNode = { kind: 'view', view: cmsView(config) }\n if (config.children) node.children = config.children\n else\n node.expand = (collections) =>\n focusCollectionList(collections, config.collections).map(cmsCollectionLeaf)\n return node\n },\n /** The component registry browser. */\n components: (config: BuiltinViewConfig = {}): SidebarViewNode => viewNode(componentsView(config)),\n /** The asset library. */\n assets: (config: BuiltinViewConfig = {}): SidebarViewNode => viewNode(assetsView(config)),\n /** The resolved routes & redirects. */\n routes: (config: BuiltinViewConfig = {}): SidebarViewNode => viewNode(routesView(config)),\n /** Project settings. */\n settings: (config: BuiltinViewConfig = {}): SidebarViewNode => viewNode(settingsView(config)),\n /** The extensions marketplace. */\n extensions: (config: BuiltinViewConfig = {}): SidebarViewNode => viewNode(extensionsView(config)),\n /** A fully custom view — pass your own `render` (and optional `parsePath`). */\n view: (config: CustomViewConfig): SidebarViewNode => viewNode(customView(config)),\n /** A labelled group of views (a header with sub-entries). */\n group: (label: string, children: SidebarViewNode[]) => groupNode(label, children),\n /** A divider between rail sections. */\n separator: () => separatorNode(),\n} as const\n\n// --- Surfaces (the view render bodies; CMS gains a focus lens) ---\n\n/**\n * Components section (EC-050): a registry-browser sidebar beside the active\n * screen — the index (no selection) or a component's detail. Forced states\n * (?state=) replace the whole surface.\n */\nfunction ComponentsScreen({\n projectId,\n componentId,\n state,\n}: {\n projectId: string\n componentId: string | null\n state?: SectionState\n}) {\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"Components\" />\n }\n return (\n <div className=\"flex h-full min-h-0\">\n <aside className=\"hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block\">\n <RegistryBrowser projectId={projectId} activeComponentId={componentId ?? undefined} />\n </aside>\n <div className=\"min-h-0 flex-1 overflow-auto\">\n {componentId === null ? (\n <ComponentsIndex projectId={projectId} />\n ) : (\n <ComponentDetail projectId={projectId} componentId={componentId} />\n )}\n </div>\n </div>\n )\n}\n\n/**\n * CMS section: hydrates the persisted (or kit-seeded) collections + documents, then\n * renders the active screen full-width (index, a collection table, or a document\n * editor). EC-293: the collection list lives in the section rail (nested under the\n * CMS view), so there is no in-surface collections sidebar. Forced states (?state=)\n * replace the whole surface. `focusCollections` (EC-239) narrows the index to a\n * subset, so a focused view (\"Catalog\") reuses this surface with config.\n */\nfunction CmsScreen({\n project,\n collectionId,\n documentId,\n state,\n field,\n focusCollections,\n}: {\n project: Project\n collectionId: string | null\n documentId: string | null\n state?: SectionState\n field?: string\n focusCollections?: readonly string[]\n}) {\n const projectId = project.id\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"CMS\" />\n }\n\n // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.\n const kit = getInstantiatedKit(projectId)\n\n return (\n // EC-177 gap 1: in Convex mode every CMS mount loads the CURRENT persisted\n // schema + documents (fixtures only when nothing is persisted — first run).\n // Playground renders synchronously with `null` → fixture seeds, as before.\n <CmsSessionGate>\n {(cms) => (\n <CmsProvider\n key={projectId}\n projectId={projectId}\n initialCollections={cms?.collections ?? kit?.collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n initialLocaleConfig={\n cms\n ? // Hydrated sessions follow the project's persisted locale settings.\n { default: project.settings.defaultLocale, locales: [...project.settings.locales] }\n : kit?.localeConfig\n }\n >\n <div className=\"flex h-full min-h-0 flex-col\">\n <CmsPersistErrorBanner />\n {/* EC-293: the collection list now lives in the section rail (nested\n under the CMS view), so the surface is full-width — the old 240px\n CollectionsSidebar aside is gone. */}\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <CmsContent\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n field={field}\n focusCollections={focusCollections}\n />\n </div>\n </div>\n </CmsProvider>\n )}\n </CmsSessionGate>\n )\n}\n\n/** Resolves the active CMS screen inside the provider (needs the live store). */\nfunction CmsContent({\n projectId,\n collectionId,\n documentId,\n field,\n focusCollections,\n}: {\n projectId: string\n collectionId: string | null\n documentId: string | null\n field?: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n\n if (collectionId === null) return <CmsIndex projectId={projectId} focusCollections={focusCollections} />\n\n if (documentId === null) {\n const collection = cms.getCollection(collectionId)\n // EC-274: a singleton is a UI lens — at the collection URL it opens its one\n // document directly *while in its expected shape*, but degrades to the list\n // (with a \"for review\" lane) on drift. SingletonGate decides per the data.\n if (collection && isSingleton(collection)) {\n return <SingletonGate projectId={projectId} collectionId={collectionId} />\n }\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n\n return (\n <DetailEditor\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n focusPath={field}\n />\n )\n}\n\n/**\n * EC-274 — the singleton lens gate. At a singleton collection's URL, resolve its\n * lone document and open it directly; on drift (> 1 document) fall back to the\n * list with the \"for review\" lane so an editor can resolve the conflict.\n */\nfunction SingletonGate({ projectId, collectionId }: { projectId: string; collectionId: string }) {\n const cms = useCms()\n const collection = cms.getCollection(collectionId)\n const docs = cms.documentsOf(collectionId)\n const resolution = collection\n ? resolveSingletonDocument(collection, docs)\n : ({ kind: 'empty' } as const)\n\n // Materialize the lone document on first visit (an empty draft, persisted like\n // any create). Idempotent in the store; only fires while genuinely empty.\n useEffect(() => {\n if (resolution.kind === 'empty') cms.ensureSingletonDocument(collectionId)\n }, [resolution.kind, cms, collectionId])\n\n if (resolution.kind === 'ready') {\n return (\n <DetailEditor projectId={projectId} collectionId={collectionId} documentId={resolution.doc.id} />\n )\n }\n if (resolution.kind === 'drift') {\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n Opening {collection?.form?.label ?? collectionId}…\n </Text>\n </div>\n )\n}\n\n/** Failed/denied persisted CMS mutations are always visible (EC-141). */\nfunction CmsPersistErrorBanner() {\n const cms = useCms()\n if (!cms.persistError) return null\n return (\n <div className=\"shrink-0 border-b border-border p-2\">\n <OperationErrorBanner error={cms.persistError} onDismiss={cms.clearPersistError} />\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"default-modules.js","sourceRoot":"","sources":["../../../src/components/app-views/default-modules.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Blocks,\n Database,\n FileCog,\n Image,\n LayoutGrid,\n Puzzle,\n Settings as SettingsIcon,\n Signpost,\n Users,\n} from 'lucide-react'\nimport { isSingleton, resolveSingletonDocument } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { Project, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Navigate, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Text } from '@elytracms/studio/ui'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport {\n cmsDeepLink,\n componentsDeepLink,\n usersDeepLink,\n redirectsDeepLink,\n groupNode,\n separatorNode,\n viewNode,\n type AppViewContext,\n type AppViewDeepLink,\n type AppViewModule,\n type NavPermission,\n type SidebarViewNode,\n} from './module'\nimport { SectionSurface } from '../section-surface'\nimport { SectionStateView } from '../section-state'\nimport { SettingsForm } from '../settings-form'\nimport { AssetLibrary } from '../assets/asset-library'\nimport { UsersModule } from '../users/users-module'\nimport { RedirectsModule } from '../redirects/redirects-module'\nimport { ExtensionsView } from '../extensions/extensions-view'\nimport { RegistryBrowser } from '../components/registry-browser'\nimport { ComponentsIndex } from '../components/components-index'\nimport { ComponentDetail } from '../components/component-detail'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { focusCollectionList } from '../cms/collection-focus'\nimport { CmsIndex } from '../cms/cms-index'\nimport { CollectionTable } from '../cms/collection-table'\nimport { DetailEditor } from '../cms/detail-editor'\nimport { OperationErrorBanner } from '../operation-error'\n\n/**\n * EC-237/239 — the studio's BUILT-IN views + the consumer-facing `nav` namespace,\n * an instance of the EC-236 customization seam.\n *\n * Each built-in surface (the former `StudioSectionResolver` case body) is promoted\n * to a self-contained {@link AppViewModule} factory. The `nav` namespace is the\n * single, typed builder a consuming repo's `studio.config.tsx` uses to declare its\n * section rail — `nav.cms(...)`, `nav.group(...)`, `nav.view(...)` — passing the\n * view objects (and a focus config) DIRECTLY, never a string id. The CMS surface\n * is a factory ({@link cmsView}) so a focused view (\"Catalog\", \"Categories\") is the\n * SAME surface instantiated with config, not a copy — the EC-239 reuse proof.\n */\n\n// --- The CMS surface, reusable via a config factory (the EC-239 reuse seam) ---\n\n/** Config for a CMS-backed view — `collections`/`collection` focus the lens (EC-239). */\nexport interface CmsViewConfig {\n /** Explicit stable id (the route segment). Omitted → derived from the focus. */\n id?: string\n label?: string\n description?: string\n icon?: LucideIcon\n /**\n * Scope the rail auto-expansion (EC-293) to these collection ids, in order. The\n * parent expands into one focused-lens child per id; the parent index shows the\n * same subset. Omitted → every collection. Ignored for a single-collection lens.\n */\n collections?: readonly string[]\n /** Single-collection lens: a LEAF rail item that opens straight into this collection. */\n collection?: string\n /**\n * EC-293 escape hatch: explicit rail children for this CMS parent, overriding the\n * default per-collection auto-expansion. Ignored for a single-collection lens.\n */\n children?: SidebarViewNode[]\n /** The availability section this view rides on (EC-221); defaults to `cms`. */\n availabilitySection?: SectionId\n /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */\n visible?: NavPermission\n}\n\n/** Common label/icon overrides for a built-in view (`nav.redirects({ label: 'URLs' })`). */\nexport interface BuiltinViewConfig {\n label?: string\n icon?: LucideIcon\n /**\n * OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18): hide this\n * rail item for principals it returns `false` for; absent → always visible. The\n * policy (which roles) lives in the consuming project, never the engine. E.g.\n * `nav.users({ visible: ({ role }) => role === 'admin' })`.\n */\n visible?: NavPermission\n}\n\n/** Config for a fully custom view (`nav.view({...})`) — the net-new surface escape hatch. */\nexport interface CustomViewConfig {\n /** Explicit stable id (the route segment). Omitted → a slug of the label. */\n id?: string\n label: string\n description?: string\n icon?: LucideIcon\n render: (ctx: AppViewContext) => ReactNode\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n availabilitySection?: SectionId\n /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */\n visible?: NavPermission\n}\n\nfunction slugify(label: string): string {\n return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'view'\n}\n\n/** Derive a stable, deterministic id from the CMS focus (overridable via `config.id`). */\nfunction cmsViewId(config: CmsViewConfig): string {\n if (config.id) return config.id\n if (config.collection) return `cms:${config.collection}`\n if (config.collections && config.collections.length > 0) {\n return `cms:${[...config.collections].join('-')}`\n }\n return 'cms'\n}\n\n/**\n * Build a CMS view — the default `cms` view, or a focused instance. Every instance\n * shares one surface ({@link CmsScreen}); `collections` narrows the sidebar + index\n * to a subset, `collection` is a single-collection lens that opens straight into\n * that collection's list. The id is derived from the focus (deterministic, stable\n * across rail reorders) unless `id` is given — so two focused views never collide.\n */\nexport function cmsView(config: CmsViewConfig = {}): AppViewModule {\n const single = config.collection\n const focus = single ? [single] : config.collections\n return {\n id: cmsViewId(config),\n label: config.label ?? 'CMS',\n description: config.description ?? 'Model collections, fields, and documents.',\n icon: config.icon ?? Database,\n availabilitySection: config.availabilitySection ?? 'cms',\n // A single-collection lens routes `[]`→that collection, `[d]`→its document;\n // an unfocused / multi-collection view uses the full index/collection/document arity.\n parsePath: single\n ? (segments) =>\n segments.length === 0\n ? { collectionId: single, documentId: null }\n : segments.length === 1\n ? { collectionId: single, documentId: segments[0]! }\n : null\n : cmsDeepLink,\n render: (ctx) => (\n <CmsScreen\n project={ctx.project}\n collectionId={ctx.deepLink.collectionId ?? null}\n documentId={ctx.deepLink.documentId ?? null}\n state={ctx.search.state}\n field={ctx.search.field}\n focusCollections={focus}\n />\n ),\n }\n}\n\n/**\n * A focused-lens rail child for one collection (EC-293) — the SAME `cms:<id>` view a\n * single-collection `nav.cms({ collection })` produces, carrying the collection's own\n * label + a singleton-aware icon (the former `CollectionsSidebar` row, lifted into\n * the rail). CMS auto-expansion maps the project's collections through this.\n */\nfunction cmsCollectionLeaf(collection: CollectionDef): SidebarViewNode {\n const singleton = isSingleton(collection)\n return viewNode(\n cmsView({\n collection: collection.id,\n label: collection.form?.label ?? collection.id,\n icon: singleton ? FileCog : collection.kind === 'asset' ? Image : Database,\n }),\n )\n}\n\n/** The components registry browser view. */\nexport function componentsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'components',\n label: config.label ?? 'Components',\n description: 'Browse the registry of primitives and project components.',\n icon: config.icon ?? Blocks,\n availabilitySection: 'components',\n parsePath: componentsDeepLink,\n render: (ctx) => (\n <ComponentsScreen\n projectId={ctx.projectId}\n componentId={ctx.deepLink.componentId ?? null}\n state={ctx.search.state}\n />\n ),\n }\n}\n\n/**\n * The Redirects view (EC-303) — editorial redirects as DATA, an editor-owned CRUD\n * surface (create/edit/delete) over the portable list/edit surfaces. Routes are no\n * longer shown in the admin (they're code; native `next.config`/`vercel` own static\n * forwards, evaluated before these). Rides on the CMS availability (a content/\n * routing concern). `[]` is the list; `[redirectId]` opens that redirect's detail.\n */\nexport function redirectsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'redirects',\n label: config.label ?? 'Redirects',\n description: 'Create and manage URL redirects (301/302) for this site.',\n icon: config.icon ?? Signpost,\n availabilitySection: 'cms',\n parsePath: redirectsDeepLink,\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Redirects\" />\n ) : (\n <RedirectsModule projectId={ctx.projectId} redirectId={ctx.deepLink.redirectId ?? null} />\n ),\n }\n}\n\n/** The asset library view. */\nexport function assetsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'assets',\n label: config.label ?? 'Assets',\n description: 'Manage uploaded media and metadata.',\n icon: config.icon ?? Image,\n availabilitySection: 'assets',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"assets\" forcedState={ctx.search.state}>\n {/* The AssetsProvider is mounted at the project level (EC-195). */}\n <AssetLibrary projectId={ctx.projectId} focusAssetId={ctx.search.asset} />\n </SectionSurface>\n ),\n }\n}\n\n/**\n * The studio Users view (EC-240) — an admin-only roster of who has access to this\n * deployment and their role. A first-class section (its own rail entry), modeled\n * like the Assets library; the surface itself gates to admins and renders a\n * \"restricted\" state otherwise. Rides on the `settings` availability (an\n * admin/config concern, so it appears/hides with a project's settings surface).\n */\nexport function usersView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'users',\n label: config.label ?? 'Users',\n description: 'Manage who has access to this deployment and their role.',\n icon: config.icon ?? Users,\n availabilitySection: 'users',\n // EC-240: `[]` is the roster; `[userId]` opens that member in the portable edit\n // shell (AD-19 reuse — the same `DetailEditor` body over the users source).\n parsePath: usersDeepLink,\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Users\" />\n ) : (\n <UsersModule projectId={ctx.projectId} userId={ctx.deepLink.userId ?? null} />\n ),\n }\n}\n\n/** The extensions marketplace view. */\nexport function extensionsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'extensions',\n label: config.label ?? 'Extensions',\n description: 'Install components, field types, adapters, and templates from the marketplace.',\n icon: config.icon ?? Puzzle,\n availabilitySection: 'extensions',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Extensions\" />\n ) : (\n <ExtensionsView />\n ),\n }\n}\n\n/** The project settings view. */\nexport function settingsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'settings',\n label: config.label ?? 'Settings',\n description: 'Configure project metadata and locales.',\n icon: config.icon ?? SettingsIcon,\n availabilitySection: 'settings',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"settings\" forcedState={ctx.search.state}>\n <SettingsForm project={ctx.project} />\n </SectionSurface>\n ),\n }\n}\n\n/** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */\nexport function builderView(): AppViewModule {\n return {\n id: 'builder',\n label: 'Builder',\n description: 'Legacy builder route — redirects to the page document editor.',\n icon: Blocks,\n hidden: true,\n render: (ctx) => (\n // Retired surface (EC-187 slice 6): a page is a document in the `page`\n // collection. `?page=<id>` lands on that document; else the page list.\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{\n projectId: ctx.projectId,\n _splat: ctx.search.page\n ? studioPath.cmsDocument('page', ctx.search.page)\n : studioPath.cmsCollection('page'),\n }}\n replace\n />\n ),\n }\n}\n\n/** Build a fully custom view (the `nav.view(...)` escape hatch). */\nexport function customView(config: CustomViewConfig): AppViewModule {\n return {\n id: config.id ?? slugify(config.label),\n label: config.label,\n description: config.description ?? config.label,\n icon: config.icon ?? LayoutGrid,\n render: config.render,\n ...(config.parsePath ? { parsePath: config.parsePath } : {}),\n ...(config.availabilitySection ? { availabilitySection: config.availabilitySection } : {}),\n }\n}\n\n/**\n * The studio's built-in views in canonical rail order (components · CMS · routes ·\n * assets · extensions · settings), plus the hidden `builder` redirect (routable but\n * unlisted). The default rail (no `defineStudio({ sidebar })`) is these as plain\n * entries; the router registry always includes them so deep-links resolve even\n * when a consumer's rail omits one.\n */\nexport const DEFAULT_VIEWS: readonly AppViewModule[] = [\n componentsView(),\n cmsView(),\n redirectsView(),\n assetsView(),\n extensionsView(),\n usersView(),\n settingsView(),\n builderView(),\n]\n\n/**\n * The `nav` namespace — the single typed builder for `studio.config.tsx`'s section\n * rail. Each call returns a {@link SidebarViewNode} (or a group / separator) that\n * carries its view DIRECTLY: `nav.cms({ collection })` is a focused CMS view,\n * `nav.view({ render })` a net-new surface, `nav.group(label, [...])` a header with\n * sub-views, `nav.separator()` a divider. No string ids, no `moduleId` — the rail\n * is the views.\n */\n/**\n * Forward the OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18)\n * from a `nav.*` config onto the built view, so gating is settable on EVERY nav\n * item — not just Users. Absent → the view is unchanged (always visible).\n */\nfunction withNavPermission(view: AppViewModule, config: { visible?: NavPermission }): AppViewModule {\n return config.visible ? { ...view, visible: config.visible } : view\n}\n\nexport const nav = {\n /**\n * The CMS view. `nav.cms()` / `nav.cms({ collections })` is a PARENT that nests its\n * collections in the rail (each a focused `cms:<id>` lens); `nav.cms({ collection })`\n * is a single-collection LEAF; `nav.cms({ children })` supplies explicit children.\n */\n cms: (config: CmsViewConfig = {}): SidebarViewNode => {\n const view = withNavPermission(cmsView(config), config)\n // A single-collection lens is a leaf — it opens that collection directly.\n if (config.collection) return viewNode(view)\n // Otherwise a parent that expands its collections as nested rail children (EC-293).\n const node: SidebarViewNode = { kind: 'view', view }\n if (config.children) node.children = config.children\n else\n node.expand = (collections) =>\n focusCollectionList(collections, config.collections).map(cmsCollectionLeaf)\n return node\n },\n /** The component registry browser. */\n components: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(componentsView(config), config)),\n /** The asset library. */\n assets: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(assetsView(config), config)),\n /** Editorial redirects (EC-303). */\n redirects: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(redirectsView(config), config)),\n /** Project settings. */\n settings: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(settingsView(config), config)),\n /** The extensions marketplace. */\n extensions: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(extensionsView(config), config)),\n /** The admin users roster (EC-240). */\n users: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(usersView(config), config)),\n /** A fully custom view — pass your own `render` (and optional `parsePath`). */\n view: (config: CustomViewConfig): SidebarViewNode =>\n viewNode(withNavPermission(customView(config), config)),\n /** A labelled group of views (a header with sub-entries). */\n group: (label: string, children: SidebarViewNode[]) => groupNode(label, children),\n /** A divider between rail sections. */\n separator: () => separatorNode(),\n} as const\n\n// --- Surfaces (the view render bodies; CMS gains a focus lens) ---\n\n/**\n * Components section (EC-050): a registry-browser sidebar beside the active\n * screen — the index (no selection) or a component's detail. Forced states\n * (?state=) replace the whole surface.\n */\nfunction ComponentsScreen({\n projectId,\n componentId,\n state,\n}: {\n projectId: string\n componentId: string | null\n state?: SectionState\n}) {\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"Components\" />\n }\n return (\n <div className=\"flex h-full min-h-0\">\n <aside className=\"hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block\">\n <RegistryBrowser projectId={projectId} activeComponentId={componentId ?? undefined} />\n </aside>\n <div className=\"min-h-0 flex-1 overflow-auto\">\n {componentId === null ? (\n <ComponentsIndex projectId={projectId} />\n ) : (\n <ComponentDetail projectId={projectId} componentId={componentId} />\n )}\n </div>\n </div>\n )\n}\n\n/**\n * CMS section: hydrates the persisted (or kit-seeded) collections + documents, then\n * renders the active screen full-width (index, a collection table, or a document\n * editor). EC-293: the collection list lives in the section rail (nested under the\n * CMS view), so there is no in-surface collections sidebar. Forced states (?state=)\n * replace the whole surface. `focusCollections` (EC-239) narrows the index to a\n * subset, so a focused view (\"Catalog\") reuses this surface with config.\n */\nfunction CmsScreen({\n project,\n collectionId,\n documentId,\n state,\n field,\n focusCollections,\n}: {\n project: Project\n collectionId: string | null\n documentId: string | null\n state?: SectionState\n field?: string\n focusCollections?: readonly string[]\n}) {\n const projectId = project.id\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"CMS\" />\n }\n\n // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.\n const kit = getInstantiatedKit(projectId)\n\n return (\n // EC-177 gap 1: in Convex mode every CMS mount loads the CURRENT persisted\n // schema + documents (fixtures only when nothing is persisted — first run).\n // Playground renders synchronously with `null` → fixture seeds, as before.\n <CmsSessionGate>\n {(cms) => (\n <CmsProvider\n key={projectId}\n projectId={projectId}\n initialCollections={cms?.collections ?? kit?.collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n initialLocaleConfig={\n cms\n ? // Hydrated sessions follow the project's persisted locale settings.\n { default: project.settings.defaultLocale, locales: [...project.settings.locales] }\n : kit?.localeConfig\n }\n >\n <div className=\"flex h-full min-h-0 flex-col\">\n <CmsPersistErrorBanner />\n {/* EC-293: the collection list now lives in the section rail (nested\n under the CMS view), so the surface is full-width — the old 240px\n CollectionsSidebar aside is gone. */}\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <CmsContent\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n field={field}\n focusCollections={focusCollections}\n />\n </div>\n </div>\n </CmsProvider>\n )}\n </CmsSessionGate>\n )\n}\n\n/** Resolves the active CMS screen inside the provider (needs the live store). */\nfunction CmsContent({\n projectId,\n collectionId,\n documentId,\n field,\n focusCollections,\n}: {\n projectId: string\n collectionId: string | null\n documentId: string | null\n field?: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n\n if (collectionId === null) return <CmsIndex projectId={projectId} focusCollections={focusCollections} />\n\n if (documentId === null) {\n const collection = cms.getCollection(collectionId)\n // EC-274: a singleton is a UI lens — at the collection URL it opens its one\n // document directly *while in its expected shape*, but degrades to the list\n // (with a \"for review\" lane) on drift. SingletonGate decides per the data.\n if (collection && isSingleton(collection)) {\n return <SingletonGate projectId={projectId} collectionId={collectionId} />\n }\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n\n return (\n <DetailEditor\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n focusPath={field}\n />\n )\n}\n\n/**\n * EC-274 — the singleton lens gate. At a singleton collection's URL, resolve its\n * lone document and open it directly; on drift (> 1 document) fall back to the\n * list with the \"for review\" lane so an editor can resolve the conflict.\n */\nfunction SingletonGate({ projectId, collectionId }: { projectId: string; collectionId: string }) {\n const cms = useCms()\n const collection = cms.getCollection(collectionId)\n const docs = cms.documentsOf(collectionId)\n const resolution = collection\n ? resolveSingletonDocument(collection, docs)\n : ({ kind: 'empty' } as const)\n\n // Materialize the lone document on first visit (an empty draft, persisted like\n // any create). Idempotent in the store; only fires while genuinely empty.\n useEffect(() => {\n if (resolution.kind === 'empty') cms.ensureSingletonDocument(collectionId)\n }, [resolution.kind, cms, collectionId])\n\n if (resolution.kind === 'ready') {\n return (\n <DetailEditor projectId={projectId} collectionId={collectionId} documentId={resolution.doc.id} />\n )\n }\n if (resolution.kind === 'drift') {\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n Opening {collection?.form?.label ?? collectionId}…\n </Text>\n </div>\n )\n}\n\n/** Failed/denied persisted CMS mutations are always visible (EC-141). */\nfunction CmsPersistErrorBanner() {\n const cms = useCms()\n if (!cms.persistError) return null\n return (\n <div className=\"shrink-0 border-b border-border p-2\">\n <OperationErrorBanner error={cms.persistError} onDismiss={cms.clearPersistError} />\n </div>\n )\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core';
|
|
4
4
|
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
5
|
+
import type { ProjectRole } from '@elytracms/core/persistence';
|
|
5
6
|
import type { StudioSearch } from '@elytracms/studio/router/search';
|
|
6
7
|
/**
|
|
7
8
|
* The studio **view** contract + the **sidebar node** model — the internal types
|
|
@@ -28,6 +29,24 @@ import type { StudioSearch } from '@elytracms/studio/router/search';
|
|
|
28
29
|
*/
|
|
29
30
|
/** The deep-link a view parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */
|
|
30
31
|
export type AppViewDeepLink = Record<string, string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* What an OPTIONAL nav-permission predicate sees (EC-240, vision AD-18): the
|
|
34
|
+
* project + the signed-in user's resolved role (`null` while it loads, or for a
|
|
35
|
+
* non-member). Kept minimal — policy that needs more should live server-side.
|
|
36
|
+
*/
|
|
37
|
+
export interface NavPermissionContext {
|
|
38
|
+
project: Project;
|
|
39
|
+
role: ProjectRole | null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* An OPTIONAL, **default-open** per-nav-item visibility predicate (AD-18). Returns
|
|
43
|
+
* `false` to HIDE the item for this principal; absent ⇒ visible. Permissions
|
|
44
|
+
* RESTRICT — they never grant a baseline. This is the general mechanism (applies
|
|
45
|
+
* to EVERY nav item, not a Users special-case); the concrete policy ships in the
|
|
46
|
+
* starter project, never the engine. UI reflection only — the server chokepoint
|
|
47
|
+
* stays the real boundary, and hidden views remain URL-addressable (EC-221).
|
|
48
|
+
*/
|
|
49
|
+
export type NavPermission = (ctx: NavPermissionContext) => boolean;
|
|
31
50
|
/** Everything a view's surface needs to render (EC-239). */
|
|
32
51
|
export interface AppViewContext {
|
|
33
52
|
project: Project;
|
|
@@ -57,6 +76,15 @@ export interface AppViewModule {
|
|
|
57
76
|
* view defaults to visible).
|
|
58
77
|
*/
|
|
59
78
|
availabilitySection?: SectionId;
|
|
79
|
+
/**
|
|
80
|
+
* OPTIONAL, default-open per-nav-item visibility (EC-240, vision AD-18). Absent
|
|
81
|
+
* → visible. Evaluated with the signed-in user's role so devs can configure
|
|
82
|
+
* role-based sidebars for ANY nav item (not just Users). Distinct from
|
|
83
|
+
* {@link availabilitySection} (project-config gating): this is per-principal.
|
|
84
|
+
* See {@link NavPermission}. (Phase-0 contract; the rail wires it in EC-240c —
|
|
85
|
+
* `resolveSidebar`/`SectionNav` thread the role through and honour a `false`.)
|
|
86
|
+
*/
|
|
87
|
+
visible?: NavPermission;
|
|
60
88
|
/** A redirect-only / utility view the stock rail does not list (e.g. `builder`). */
|
|
61
89
|
hidden?: boolean;
|
|
62
90
|
}
|
|
@@ -109,6 +137,10 @@ export declare function flatPath(segments: string[]): AppViewDeepLink | null;
|
|
|
109
137
|
export declare function cmsDeepLink(segments: string[]): AppViewDeepLink | null;
|
|
110
138
|
/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */
|
|
111
139
|
export declare function componentsDeepLink(segments: string[]): AppViewDeepLink | null;
|
|
140
|
+
/** Users arity (EC-240): `[]` list · `[userId]` member detail; deeper is not-found. */
|
|
141
|
+
export declare function usersDeepLink(segments: string[]): AppViewDeepLink | null;
|
|
142
|
+
/** Redirects arity (EC-303): `[]` list · `[redirectId]` detail; deeper is not-found. */
|
|
143
|
+
export declare function redirectsDeepLink(segments: string[]): AppViewDeepLink | null;
|
|
112
144
|
/**
|
|
113
145
|
* A view's availability for a project (EC-221): the section it rides on, or always
|
|
114
146
|
* `available` when it declares none (a custom view). Drives both the rail (hide
|
|
@@ -34,6 +34,22 @@ export function componentsDeepLink(segments) {
|
|
|
34
34
|
return { componentId: segments[0] };
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
+
/** Users arity (EC-240): `[]` list · `[userId]` member detail; deeper is not-found. */
|
|
38
|
+
export function usersDeepLink(segments) {
|
|
39
|
+
if (segments.length === 0)
|
|
40
|
+
return { userId: null };
|
|
41
|
+
if (segments.length === 1)
|
|
42
|
+
return { userId: segments[0] };
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** Redirects arity (EC-303): `[]` list · `[redirectId]` detail; deeper is not-found. */
|
|
46
|
+
export function redirectsDeepLink(segments) {
|
|
47
|
+
if (segments.length === 0)
|
|
48
|
+
return { redirectId: null };
|
|
49
|
+
if (segments.length === 1)
|
|
50
|
+
return { redirectId: segments[0] };
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
37
53
|
// --- Availability (EC-221) ---
|
|
38
54
|
/**
|
|
39
55
|
* A view's availability for a project (EC-221): the section it rides on, or always
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/components/app-views/module.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AA0G5D,mCAAmC;AACnC,MAAM,UAAU,QAAQ,CAAC,IAAmB;IAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAC/B,CAAC;AACD,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,QAA2B;IAClE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AAC3C,CAAC;AACD,sBAAsB;AACtB,MAAM,UAAU,aAAa;IAC3B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;AAC9B,CAAC;AAED,iFAAiF;AAEjF,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CAAC,QAAkB;IACzC,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,QAAkB;IAC5C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IAC1E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IAClF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC1F,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,kBAAkB,CAAC,QAAkB;IACnD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IACvD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC/D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,gCAAgC;AAEhC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAqB;IACxE,OAAO,MAAM,CAAC,mBAAmB;QAC/B,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,CAAC;QAClF,CAAC,CAAC,WAAW,CAAA;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA+B,EAC/B,OAAgB;IAEhB,OAAO,KAAK,CAAC,IAAI,CACf,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,aAAa,CAC9E,CAAA;AACH,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { availabilityOf } from '@elytracms/core/studio-core'\nimport type { Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\n\n/**\n * The studio **view** contract + the **sidebar node** model — the internal types\n * the consumer-facing `nav.*` builders (EC-237/239, customization seam EC-236)\n * produce.\n *\n * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a\n * self-contained {@link AppViewModule}: a stable id + display metadata + a React\n * `render` + an optional deep-link parser. A {@link SidebarNode} is one rail entry\n * — a view, a labelled group of views, or a separator — and a **view node carries\n * its view object DIRECTLY**, not a string id. This is the redesign's core: there\n * is no separate \"register a view, then reference it by `moduleId`\" indirection\n * and no shared id-keyspace, so the section rail IS the set of views, the router\n * registry is DERIVED from it, and the duplicate-id / both-highlighted class of\n * bug is gone by construction.\n *\n * The boundary principle: everything here is studio-only live code passed by\n * reference (no string keys). A string only crosses the serializable boundary for\n * field controls (`form.control`), where the field lives in delivery-shared schema.\n *\n * Reuse-not-copy (EC-239): a focused view like \"Catalog\" is the CMS surface\n * instantiated with config (`nav.cms({ collections })`), not a fork — the same\n * surface code backs many views, each its own addressable node.\n */\n\n/** The deep-link a view parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */\nexport type AppViewDeepLink = Record<string, string | null>\n\n/** Everything a view's surface needs to render (EC-239). */\nexport interface AppViewContext {\n project: Project\n projectId: string\n /** The view's parsed deep-link; `{}` for a flat view with no nested resource. */\n deepLink: AppViewDeepLink\n /** The validated `?…` search (forced state, focused field/asset, …). */\n search: StudioSearch\n}\n\nexport interface AppViewModule {\n /** Stable id — the first path segment under the project + the registry key. */\n id: string\n label: string\n description: string\n icon: LucideIcon\n /** Render the surface; it mounts its own providers/gates. */\n render: (ctx: AppViewContext) => ReactNode\n /**\n * Parse this view's trailing path segments into a deep-link, or `null` for an\n * unroutable path (→ not-found). Omitted → a FLAT view ({@link flatPath}): only\n * the bare path is valid, any trailing segment is not-found.\n */\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n /**\n * The availability section this view rides on (EC-221 gating): an `unavailable`\n * section hides the view from the rail. Omitted → always available (a custom\n * view defaults to visible).\n */\n availabilitySection?: SectionId\n /** A redirect-only / utility view the stock rail does not list (e.g. `builder`). */\n hidden?: boolean\n}\n\n// --- The sidebar node model (what `nav.*` produces) ---\n\n/**\n * A rail entry that mounts a view. Carries the view object directly — no string id.\n *\n * EC-293: a view node may NEST one level. `children` is a static list of sub-views;\n * `expand` derives them at resolve time from the project's collections (CMS\n * auto-expand). Children render indented under the parent — indentation signals\n * parent→child, distinct from a flat labelled {@link SidebarGroupNode}. `children`\n * (explicit) wins over `expand` when both are set.\n */\nexport interface SidebarViewNode {\n kind: 'view'\n view: AppViewModule\n /** Static sub-views, rendered indented under this view (one level of nesting). */\n children?: SidebarViewNode[]\n /**\n * Resolve-time children from the project's collections (e.g. CMS expanding each\n * collection into a focused-lens child). Run by `resolveSidebar` with the\n * config-sourced collections; the result renders as {@link children}.\n */\n expand?: (collections: readonly CollectionDef[]) => SidebarViewNode[]\n}\n/**\n * A labelled group of view entries. Groups are presentation only — never routable,\n * and **flat**: their children render aligned with the top-level items (the heading\n * is the only grouping signal). Group children do not nest — use a parent-with-\n * children ({@link SidebarViewNode.expand}/`children`) for nesting (EC-293).\n */\nexport interface SidebarGroupNode {\n kind: 'group'\n label: string\n children: SidebarViewNode[]\n}\n/** A divider between rail sections. */\nexport interface SidebarSeparatorNode {\n kind: 'separator'\n}\nexport type SidebarNode = SidebarViewNode | SidebarGroupNode | SidebarSeparatorNode\n\n/** Wrap a view as a rail entry. */\nexport function viewNode(view: AppViewModule): SidebarViewNode {\n return { kind: 'view', view }\n}\n/** A labelled group of view entries (sub-views under a header). */\nexport function groupNode(label: string, children: SidebarViewNode[]): SidebarGroupNode {\n return { kind: 'group', label, children }\n}\n/** A rail divider. */\nexport function separatorNode(): SidebarSeparatorNode {\n return { kind: 'separator' }\n}\n\n// --- Deep-link parsers (shared by the built-in views + reused by factories) ---\n\n/** A flat view: only the bare path resolves; any trailing segment is not-found. */\nexport function flatPath(segments: string[]): AppViewDeepLink | null {\n return segments.length === 0 ? {} : null\n}\n\n/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */\nexport function cmsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { collectionId: null, documentId: null }\n if (segments.length === 1) return { collectionId: segments[0]!, documentId: null }\n if (segments.length === 2) return { collectionId: segments[0]!, documentId: segments[1]! }\n return null\n}\n\n/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */\nexport function componentsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { componentId: null }\n if (segments.length === 1) return { componentId: segments[0]! }\n return null\n}\n\n// --- Availability (EC-221) ---\n\n/**\n * A view's availability for a project (EC-221): the section it rides on, or always\n * `available` when it declares none (a custom view). Drives both the rail (hide\n * `unavailable`) and the bare-project landing target.\n */\nexport function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability {\n return module.availabilitySection\n ? availabilityOf(project.settings.sectionAvailability, module.availabilitySection)\n : 'available'\n}\n\n/**\n * The view a project should open to from its bare URL: the first listed\n * (non-hidden), non-`unavailable` view in rail order. Callers pass the resolved\n * registry's view list (rail order); `undefined` only if every view is\n * hidden/unavailable (the resolver guarantees Settings, so in practice never).\n */\nexport function firstAvailableView(\n views: readonly AppViewModule[],\n project: Project,\n): AppViewModule | undefined {\n return views.find(\n (view) => !view.hidden && moduleAvailability(project, view) !== 'unavailable',\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/components/app-views/module.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAwI5D,mCAAmC;AACnC,MAAM,UAAU,QAAQ,CAAC,IAAmB;IAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAC/B,CAAC;AACD,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,QAA2B;IAClE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AAC3C,CAAC;AACD,sBAAsB;AACtB,MAAM,UAAU,aAAa;IAC3B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;AAC9B,CAAC;AAED,iFAAiF;AAEjF,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CAAC,QAAkB;IACzC,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,QAAkB;IAC5C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IAC1E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IAClF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC1F,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,kBAAkB,CAAC,QAAkB;IACnD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IACvD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC/D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,aAAa,CAAC,QAAkB;IAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IAClD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC1D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,QAAkB;IAClD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,gCAAgC;AAEhC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAqB;IACxE,OAAO,MAAM,CAAC,mBAAmB;QAC/B,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,CAAC;QAClF,CAAC,CAAC,WAAW,CAAA;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA+B,EAC/B,OAAgB;IAEhB,OAAO,KAAK,CAAC,IAAI,CACf,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,aAAa,CAC9E,CAAA;AACH,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { availabilityOf } from '@elytracms/core/studio-core'\nimport type { Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\n\n/**\n * The studio **view** contract + the **sidebar node** model — the internal types\n * the consumer-facing `nav.*` builders (EC-237/239, customization seam EC-236)\n * produce.\n *\n * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a\n * self-contained {@link AppViewModule}: a stable id + display metadata + a React\n * `render` + an optional deep-link parser. A {@link SidebarNode} is one rail entry\n * — a view, a labelled group of views, or a separator — and a **view node carries\n * its view object DIRECTLY**, not a string id. This is the redesign's core: there\n * is no separate \"register a view, then reference it by `moduleId`\" indirection\n * and no shared id-keyspace, so the section rail IS the set of views, the router\n * registry is DERIVED from it, and the duplicate-id / both-highlighted class of\n * bug is gone by construction.\n *\n * The boundary principle: everything here is studio-only live code passed by\n * reference (no string keys). A string only crosses the serializable boundary for\n * field controls (`form.control`), where the field lives in delivery-shared schema.\n *\n * Reuse-not-copy (EC-239): a focused view like \"Catalog\" is the CMS surface\n * instantiated with config (`nav.cms({ collections })`), not a fork — the same\n * surface code backs many views, each its own addressable node.\n */\n\n/** The deep-link a view parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */\nexport type AppViewDeepLink = Record<string, string | null>\n\n/**\n * What an OPTIONAL nav-permission predicate sees (EC-240, vision AD-18): the\n * project + the signed-in user's resolved role (`null` while it loads, or for a\n * non-member). Kept minimal — policy that needs more should live server-side.\n */\nexport interface NavPermissionContext {\n project: Project\n role: ProjectRole | null\n}\n\n/**\n * An OPTIONAL, **default-open** per-nav-item visibility predicate (AD-18). Returns\n * `false` to HIDE the item for this principal; absent ⇒ visible. Permissions\n * RESTRICT — they never grant a baseline. This is the general mechanism (applies\n * to EVERY nav item, not a Users special-case); the concrete policy ships in the\n * starter project, never the engine. UI reflection only — the server chokepoint\n * stays the real boundary, and hidden views remain URL-addressable (EC-221).\n */\nexport type NavPermission = (ctx: NavPermissionContext) => boolean\n\n/** Everything a view's surface needs to render (EC-239). */\nexport interface AppViewContext {\n project: Project\n projectId: string\n /** The view's parsed deep-link; `{}` for a flat view with no nested resource. */\n deepLink: AppViewDeepLink\n /** The validated `?…` search (forced state, focused field/asset, …). */\n search: StudioSearch\n}\n\nexport interface AppViewModule {\n /** Stable id — the first path segment under the project + the registry key. */\n id: string\n label: string\n description: string\n icon: LucideIcon\n /** Render the surface; it mounts its own providers/gates. */\n render: (ctx: AppViewContext) => ReactNode\n /**\n * Parse this view's trailing path segments into a deep-link, or `null` for an\n * unroutable path (→ not-found). Omitted → a FLAT view ({@link flatPath}): only\n * the bare path is valid, any trailing segment is not-found.\n */\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n /**\n * The availability section this view rides on (EC-221 gating): an `unavailable`\n * section hides the view from the rail. Omitted → always available (a custom\n * view defaults to visible).\n */\n availabilitySection?: SectionId\n /**\n * OPTIONAL, default-open per-nav-item visibility (EC-240, vision AD-18). Absent\n * → visible. Evaluated with the signed-in user's role so devs can configure\n * role-based sidebars for ANY nav item (not just Users). Distinct from\n * {@link availabilitySection} (project-config gating): this is per-principal.\n * See {@link NavPermission}. (Phase-0 contract; the rail wires it in EC-240c —\n * `resolveSidebar`/`SectionNav` thread the role through and honour a `false`.)\n */\n visible?: NavPermission\n /** A redirect-only / utility view the stock rail does not list (e.g. `builder`). */\n hidden?: boolean\n}\n\n// --- The sidebar node model (what `nav.*` produces) ---\n\n/**\n * A rail entry that mounts a view. Carries the view object directly — no string id.\n *\n * EC-293: a view node may NEST one level. `children` is a static list of sub-views;\n * `expand` derives them at resolve time from the project's collections (CMS\n * auto-expand). Children render indented under the parent — indentation signals\n * parent→child, distinct from a flat labelled {@link SidebarGroupNode}. `children`\n * (explicit) wins over `expand` when both are set.\n */\nexport interface SidebarViewNode {\n kind: 'view'\n view: AppViewModule\n /** Static sub-views, rendered indented under this view (one level of nesting). */\n children?: SidebarViewNode[]\n /**\n * Resolve-time children from the project's collections (e.g. CMS expanding each\n * collection into a focused-lens child). Run by `resolveSidebar` with the\n * config-sourced collections; the result renders as {@link children}.\n */\n expand?: (collections: readonly CollectionDef[]) => SidebarViewNode[]\n}\n/**\n * A labelled group of view entries. Groups are presentation only — never routable,\n * and **flat**: their children render aligned with the top-level items (the heading\n * is the only grouping signal). Group children do not nest — use a parent-with-\n * children ({@link SidebarViewNode.expand}/`children`) for nesting (EC-293).\n */\nexport interface SidebarGroupNode {\n kind: 'group'\n label: string\n children: SidebarViewNode[]\n}\n/** A divider between rail sections. */\nexport interface SidebarSeparatorNode {\n kind: 'separator'\n}\nexport type SidebarNode = SidebarViewNode | SidebarGroupNode | SidebarSeparatorNode\n\n/** Wrap a view as a rail entry. */\nexport function viewNode(view: AppViewModule): SidebarViewNode {\n return { kind: 'view', view }\n}\n/** A labelled group of view entries (sub-views under a header). */\nexport function groupNode(label: string, children: SidebarViewNode[]): SidebarGroupNode {\n return { kind: 'group', label, children }\n}\n/** A rail divider. */\nexport function separatorNode(): SidebarSeparatorNode {\n return { kind: 'separator' }\n}\n\n// --- Deep-link parsers (shared by the built-in views + reused by factories) ---\n\n/** A flat view: only the bare path resolves; any trailing segment is not-found. */\nexport function flatPath(segments: string[]): AppViewDeepLink | null {\n return segments.length === 0 ? {} : null\n}\n\n/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */\nexport function cmsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { collectionId: null, documentId: null }\n if (segments.length === 1) return { collectionId: segments[0]!, documentId: null }\n if (segments.length === 2) return { collectionId: segments[0]!, documentId: segments[1]! }\n return null\n}\n\n/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */\nexport function componentsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { componentId: null }\n if (segments.length === 1) return { componentId: segments[0]! }\n return null\n}\n\n/** Users arity (EC-240): `[]` list · `[userId]` member detail; deeper is not-found. */\nexport function usersDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { userId: null }\n if (segments.length === 1) return { userId: segments[0]! }\n return null\n}\n\n/** Redirects arity (EC-303): `[]` list · `[redirectId]` detail; deeper is not-found. */\nexport function redirectsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { redirectId: null }\n if (segments.length === 1) return { redirectId: segments[0]! }\n return null\n}\n\n// --- Availability (EC-221) ---\n\n/**\n * A view's availability for a project (EC-221): the section it rides on, or always\n * `available` when it declares none (a custom view). Drives both the rail (hide\n * `unavailable`) and the bare-project landing target.\n */\nexport function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability {\n return module.availabilitySection\n ? availabilityOf(project.settings.sectionAvailability, module.availabilitySection)\n : 'available'\n}\n\n/**\n * The view a project should open to from its bare URL: the first listed\n * (non-hidden), non-`unavailable` view in rail order. Callers pass the resolved\n * registry's view list (rail order); `undefined` only if every view is\n * hidden/unavailable (the resolver guarantees Settings, so in practice never).\n */\nexport function firstAvailableView(\n views: readonly AppViewModule[],\n project: Project,\n): AppViewModule | undefined {\n return views.find(\n (view) => !view.hidden && moduleAvailability(project, view) !== 'unavailable',\n )\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LucideIcon } from 'lucide-react';
|
|
2
2
|
import type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core';
|
|
3
3
|
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
4
|
-
import { type AppViewModule, type SidebarNode } from './module';
|
|
4
|
+
import { type AppViewModule, type NavPermission, type NavPermissionContext, type SidebarNode } from './module';
|
|
5
5
|
/**
|
|
6
6
|
* EC-237 — resolving the configurable section rail, an instance of the EC-236
|
|
7
7
|
* customization seam.
|
|
@@ -33,6 +33,13 @@ export interface ResolvedRailView {
|
|
|
33
33
|
/** The `_splat` to navigate to (the view's home route = its id). */
|
|
34
34
|
path: string;
|
|
35
35
|
availability: SectionAvailability;
|
|
36
|
+
/**
|
|
37
|
+
* EC-240 (AD-18): the view's OPTIONAL, default-open nav-permission predicate,
|
|
38
|
+
* carried through resolution so the rail can evaluate it against the live role
|
|
39
|
+
* (which resolution — memoised on `project` alone — does not have). Absent →
|
|
40
|
+
* always visible.
|
|
41
|
+
*/
|
|
42
|
+
visible?: NavPermission;
|
|
36
43
|
/**
|
|
37
44
|
* EC-293: nested sub-views (e.g. a CMS view's collections), rendered indented
|
|
38
45
|
* under this view. Present only on a parent that expanded; leaves omit it.
|
|
@@ -74,6 +81,13 @@ export interface StudioNav {
|
|
|
74
81
|
* derived registry, so a nested `cms:<id>` lens routes and highlights like any view.
|
|
75
82
|
*/
|
|
76
83
|
export declare function resolveSidebar(entries: readonly SidebarNode[] | undefined, project: Project, collections?: readonly CollectionDef[]): StudioNav;
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate a rail view's OPTIONAL nav-permission predicate (EC-240, AD-18):
|
|
86
|
+
* default-open — absent → visible; present → the predicate decides against the
|
|
87
|
+
* live role. UI reflection only; the server chokepoint is the real boundary and
|
|
88
|
+
* a hidden view stays URL-addressable (EC-221).
|
|
89
|
+
*/
|
|
90
|
+
export declare function isNavViewVisible(view: Pick<ResolvedRailView, 'visible'>, ctx: NavPermissionContext): boolean;
|
|
77
91
|
/** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */
|
|
78
92
|
export declare function activeViewIdFromPath(pathname: string): string | null;
|
|
79
93
|
/** The view a bare project URL should open to: the first navigable rail view (EC-221). */
|
|
@@ -96,6 +96,7 @@ export function resolveSidebar(entries, project, collections = []) {
|
|
|
96
96
|
icon: resolvedView.icon,
|
|
97
97
|
path: resolvedView.id,
|
|
98
98
|
availability: moduleAvailability(project, resolvedView),
|
|
99
|
+
...(resolvedView.visible ? { visible: resolvedView.visible } : {}),
|
|
99
100
|
};
|
|
100
101
|
};
|
|
101
102
|
const railNodes = [];
|
|
@@ -127,6 +128,15 @@ export function resolveSidebar(entries, project, collections = []) {
|
|
|
127
128
|
function isRailView(view) {
|
|
128
129
|
return view !== null;
|
|
129
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Evaluate a rail view's OPTIONAL nav-permission predicate (EC-240, AD-18):
|
|
133
|
+
* default-open — absent → visible; present → the predicate decides against the
|
|
134
|
+
* live role. UI reflection only; the server chokepoint is the real boundary and
|
|
135
|
+
* a hidden view stays URL-addressable (EC-221).
|
|
136
|
+
*/
|
|
137
|
+
export function isNavViewVisible(view, ctx) {
|
|
138
|
+
return view.visible ? view.visible(ctx) : true;
|
|
139
|
+
}
|
|
130
140
|
/** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */
|
|
131
141
|
export function activeViewIdFromPath(pathname) {
|
|
132
142
|
return pathname.split('/')[2] || null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../src/components/app-views/sidebar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { resolveOverrides } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport {\n firstAvailableView,\n moduleAvailability,\n viewNode,\n type AppViewModule,\n type SidebarNode,\n type SidebarViewNode,\n} from './module'\nimport { DEFAULT_VIEWS } from './default-modules'\n\n/**\n * EC-237 — resolving the configurable section rail, an instance of the EC-236\n * customization seam.\n *\n * A consuming repo supplies its rail as a `SidebarNode[]` (built from `nav.*`);\n * omitted → the studio derives the default rail from its built-in views. The rail\n * IS the set of views — each view node carries its view object directly — so this\n * resolver does two things at once:\n *\n * 1. **Renders the rail.** Walk the nodes into display-ready entries (views,\n * groups, separators) with per-view availability (EC-221) and a route path.\n * 2. **Derives the router registry.** Collect every rail view + the always-on\n * built-ins (so deep-links resolve even for views a rail omits), keyed by id.\n *\n * Only VIEWS occupy the id-keyspace; groups + separators are presentation with no\n * id, so a group label can never collide with a view (the old duplicate-id bug).\n * Views resolve through the shared {@link resolveOverrides} core: a duplicate view\n * id or a view missing `render`/`label` is a visible {@link CustomizationIssue} and\n * is dropped — never silent. The active rail item is matched by the view id in the\n * URL, so two views backed by the same surface (Content vs. a focused Categories)\n * highlight independently.\n */\n\n// --- Resolved (rail-ready) shapes ---\n\nexport interface ResolvedRailView {\n kind: 'view'\n /** The view id — the route segment and the active-state key. */\n id: string\n label: string\n icon: LucideIcon\n /** The `_splat` to navigate to (the view's home route = its id). */\n path: string\n availability: SectionAvailability\n /**\n * EC-293: nested sub-views (e.g. a CMS view's collections), rendered indented\n * under this view. Present only on a parent that expanded; leaves omit it.\n */\n children?: ResolvedRailView[]\n}\nexport interface ResolvedRailGroup {\n kind: 'group'\n label: string\n views: ResolvedRailView[]\n}\nexport interface ResolvedRailSeparator {\n kind: 'separator'\n}\nexport type ResolvedRailNode = ResolvedRailView | ResolvedRailGroup | ResolvedRailSeparator\n\n/** The router registry derived from the rail: id → view, plus the landing order. */\nexport interface ViewRegistry {\n /** Resolve a view id to its module (rail views + always-on built-ins). */\n get(id: string): AppViewModule | undefined\n /** Views in landing order (rail order first, then non-rail built-ins). */\n views: AppViewModule[]\n}\n\nexport interface StudioNav {\n /** The display-ready rail (for {@link import('../section-nav').SectionNav}). */\n nodes: ResolvedRailNode[]\n /** The router registry (for the screen resolver + breadcrumb). */\n registry: ViewRegistry\n /** Customization issues (duplicate view id, invalid view) — visible, never silent. */\n issues: CustomizationIssue[]\n}\n\n/** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */\nfunction defaultRail(): SidebarNode[] {\n return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode)\n}\n\n/**\n * The sub-views a view node contributes (EC-293): explicit `children`, else\n * `expand(collections)` (CMS auto-expansion), else none. Groups never nest, so this\n * applies to top-level view nodes only.\n */\nfunction childNodesOf(\n node: SidebarViewNode,\n collections: readonly CollectionDef[],\n): SidebarViewNode[] {\n return node.children ?? node.expand?.(collections) ?? []\n}\n\n/**\n * Every view the rail references, in rail order: each top-level view, then its nested\n * children, then group children (groups are flat — their children never nest). This\n * is the set fed to `resolveOverrides` + the derived registry.\n */\nfunction railViews(\n nodes: readonly SidebarNode[],\n collections: readonly CollectionDef[],\n): AppViewModule[] {\n const views: AppViewModule[] = []\n for (const node of nodes) {\n if (node.kind === 'view') {\n views.push(node.view)\n for (const child of childNodesOf(node, collections)) views.push(child.view)\n } else if (node.kind === 'group') {\n for (const child of node.children) views.push(child.view)\n }\n }\n return views\n}\n\n/**\n * Resolve a consumer rail (or the default rail) into the display tree + the router\n * registry through the EC-236 seam. Views are resolved over the built-in defaults\n * (so an omitted built-in stays routable and a rail `cms` REPLACES the default\n * one); groups/separators pass through as presentation.\n *\n * EC-293: a view node may nest. `collections` (config-sourced) materialises a CMS\n * parent's per-collection children; every child view joins the override pass + the\n * derived registry, so a nested `cms:<id>` lens routes and highlights like any view.\n */\nexport function resolveSidebar(\n entries: readonly SidebarNode[] | undefined,\n project: Project,\n collections: readonly CollectionDef[] = [],\n): StudioNav {\n const nodes = entries ?? defaultRail()\n const railOrderViews = railViews(nodes, collections)\n\n // Resolve the rail views over the built-in registry: dedup by id + contract\n // check. Built-ins are the defaults (so omitted ones stay routable); rail views\n // are overrides (a same-id rail view replaces the default; a new id is added).\n const resolved = resolveOverrides<AppViewModule>({\n point: 'sidebar',\n defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),\n overrides: railOrderViews.map((view) => ({ id: view.id, value: view })),\n allowNew: true,\n validate: (entry) => {\n const view = entry.value\n if (typeof view.render !== 'function') return 'the view has no render function'\n if (typeof view.label !== 'string' || view.label.trim().length === 0)\n return 'the view has no label'\n return null\n },\n })\n\n // Rail order for landing: each rail view (resolved), deduped, then the built-ins\n // not in the rail (so Settings — always available — is the final landing fallback).\n const inRail = new Set<string>()\n const landing: AppViewModule[] = []\n for (const view of railOrderViews) {\n const resolvedView = resolved.get(view.id)\n if (resolvedView && !inRail.has(resolvedView.id)) {\n inRail.add(resolvedView.id)\n landing.push(resolvedView)\n }\n }\n for (const view of DEFAULT_VIEWS) if (!inRail.has(view.id)) landing.push(view)\n\n const registry: ViewRegistry = { get: (id) => resolved.get(id), views: landing }\n\n // Build the display tree. Each view id is emitted at most once (the first of a\n // duplicate wins, matching `resolveOverrides`); a view dropped by validation is\n // skipped here too.\n const emitted = new Set<string>()\n const toRailView = (view: AppViewModule): ResolvedRailView | null => {\n const resolvedView = resolved.get(view.id)\n if (!resolvedView || emitted.has(resolvedView.id)) return null\n emitted.add(resolvedView.id)\n return {\n kind: 'view',\n id: resolvedView.id,\n label: resolvedView.label,\n icon: resolvedView.icon,\n path: resolvedView.id,\n availability: moduleAvailability(project, resolvedView),\n }\n }\n\n const railNodes: ResolvedRailNode[] = []\n for (const node of nodes) {\n if (node.kind === 'separator') {\n railNodes.push({ kind: 'separator' })\n continue\n }\n if (node.kind === 'group') {\n const views = node.children.map((child) => toRailView(child.view)).filter(isRailView)\n if (views.length > 0) railNodes.push({ kind: 'group', label: node.label, views })\n continue\n }\n // A top-level view, with its nested children (CMS collections) attached. The\n // parent is emitted first so its id wins any duplicate; children follow in order.\n const view = toRailView(node.view)\n if (!view) continue\n const children = childNodesOf(node, collections)\n .map((child) => toRailView(child.view))\n .filter(isRailView)\n if (children.length > 0) view.children = children\n railNodes.push(view)\n }\n\n return { nodes: railNodes, registry, issues: resolved.issues }\n}\n\nfunction isRailView(view: ResolvedRailView | null): view is ResolvedRailView {\n return view !== null\n}\n\n/** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */\nexport function activeViewIdFromPath(pathname: string): string | null {\n return pathname.split('/')[2] || null\n}\n\n/** The view a bare project URL should open to: the first navigable rail view (EC-221). */\nexport function landingViewId(nav: StudioNav, project: Project): string {\n return firstAvailableView(nav.registry.views, project)?.id ?? 'settings'\n}\n\n/**\n * The active project's resolved studio nav — the default rail or\n * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam\n * (static per studio boot).\n */\nexport function useStudioNav(project: Project): StudioNav {\n // `resolveDeploymentSidebar`/`resolveDeploymentCollections` are config-holder reads\n // (stable per studio boot), so keying the memo on `project` alone is correct.\n return useMemo(\n () => resolveSidebar(resolveDeploymentSidebar(), project, resolveDeploymentCollections()),\n [project],\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../src/components/app-views/sidebar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { resolveOverrides } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport {\n firstAvailableView,\n moduleAvailability,\n viewNode,\n type AppViewModule,\n type NavPermission,\n type NavPermissionContext,\n type SidebarNode,\n type SidebarViewNode,\n} from './module'\nimport { DEFAULT_VIEWS } from './default-modules'\n\n/**\n * EC-237 — resolving the configurable section rail, an instance of the EC-236\n * customization seam.\n *\n * A consuming repo supplies its rail as a `SidebarNode[]` (built from `nav.*`);\n * omitted → the studio derives the default rail from its built-in views. The rail\n * IS the set of views — each view node carries its view object directly — so this\n * resolver does two things at once:\n *\n * 1. **Renders the rail.** Walk the nodes into display-ready entries (views,\n * groups, separators) with per-view availability (EC-221) and a route path.\n * 2. **Derives the router registry.** Collect every rail view + the always-on\n * built-ins (so deep-links resolve even for views a rail omits), keyed by id.\n *\n * Only VIEWS occupy the id-keyspace; groups + separators are presentation with no\n * id, so a group label can never collide with a view (the old duplicate-id bug).\n * Views resolve through the shared {@link resolveOverrides} core: a duplicate view\n * id or a view missing `render`/`label` is a visible {@link CustomizationIssue} and\n * is dropped — never silent. The active rail item is matched by the view id in the\n * URL, so two views backed by the same surface (Content vs. a focused Categories)\n * highlight independently.\n */\n\n// --- Resolved (rail-ready) shapes ---\n\nexport interface ResolvedRailView {\n kind: 'view'\n /** The view id — the route segment and the active-state key. */\n id: string\n label: string\n icon: LucideIcon\n /** The `_splat` to navigate to (the view's home route = its id). */\n path: string\n availability: SectionAvailability\n /**\n * EC-240 (AD-18): the view's OPTIONAL, default-open nav-permission predicate,\n * carried through resolution so the rail can evaluate it against the live role\n * (which resolution — memoised on `project` alone — does not have). Absent →\n * always visible.\n */\n visible?: NavPermission\n /**\n * EC-293: nested sub-views (e.g. a CMS view's collections), rendered indented\n * under this view. Present only on a parent that expanded; leaves omit it.\n */\n children?: ResolvedRailView[]\n}\nexport interface ResolvedRailGroup {\n kind: 'group'\n label: string\n views: ResolvedRailView[]\n}\nexport interface ResolvedRailSeparator {\n kind: 'separator'\n}\nexport type ResolvedRailNode = ResolvedRailView | ResolvedRailGroup | ResolvedRailSeparator\n\n/** The router registry derived from the rail: id → view, plus the landing order. */\nexport interface ViewRegistry {\n /** Resolve a view id to its module (rail views + always-on built-ins). */\n get(id: string): AppViewModule | undefined\n /** Views in landing order (rail order first, then non-rail built-ins). */\n views: AppViewModule[]\n}\n\nexport interface StudioNav {\n /** The display-ready rail (for {@link import('../section-nav').SectionNav}). */\n nodes: ResolvedRailNode[]\n /** The router registry (for the screen resolver + breadcrumb). */\n registry: ViewRegistry\n /** Customization issues (duplicate view id, invalid view) — visible, never silent. */\n issues: CustomizationIssue[]\n}\n\n/** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */\nfunction defaultRail(): SidebarNode[] {\n return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode)\n}\n\n/**\n * The sub-views a view node contributes (EC-293): explicit `children`, else\n * `expand(collections)` (CMS auto-expansion), else none. Groups never nest, so this\n * applies to top-level view nodes only.\n */\nfunction childNodesOf(\n node: SidebarViewNode,\n collections: readonly CollectionDef[],\n): SidebarViewNode[] {\n return node.children ?? node.expand?.(collections) ?? []\n}\n\n/**\n * Every view the rail references, in rail order: each top-level view, then its nested\n * children, then group children (groups are flat — their children never nest). This\n * is the set fed to `resolveOverrides` + the derived registry.\n */\nfunction railViews(\n nodes: readonly SidebarNode[],\n collections: readonly CollectionDef[],\n): AppViewModule[] {\n const views: AppViewModule[] = []\n for (const node of nodes) {\n if (node.kind === 'view') {\n views.push(node.view)\n for (const child of childNodesOf(node, collections)) views.push(child.view)\n } else if (node.kind === 'group') {\n for (const child of node.children) views.push(child.view)\n }\n }\n return views\n}\n\n/**\n * Resolve a consumer rail (or the default rail) into the display tree + the router\n * registry through the EC-236 seam. Views are resolved over the built-in defaults\n * (so an omitted built-in stays routable and a rail `cms` REPLACES the default\n * one); groups/separators pass through as presentation.\n *\n * EC-293: a view node may nest. `collections` (config-sourced) materialises a CMS\n * parent's per-collection children; every child view joins the override pass + the\n * derived registry, so a nested `cms:<id>` lens routes and highlights like any view.\n */\nexport function resolveSidebar(\n entries: readonly SidebarNode[] | undefined,\n project: Project,\n collections: readonly CollectionDef[] = [],\n): StudioNav {\n const nodes = entries ?? defaultRail()\n const railOrderViews = railViews(nodes, collections)\n\n // Resolve the rail views over the built-in registry: dedup by id + contract\n // check. Built-ins are the defaults (so omitted ones stay routable); rail views\n // are overrides (a same-id rail view replaces the default; a new id is added).\n const resolved = resolveOverrides<AppViewModule>({\n point: 'sidebar',\n defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),\n overrides: railOrderViews.map((view) => ({ id: view.id, value: view })),\n allowNew: true,\n validate: (entry) => {\n const view = entry.value\n if (typeof view.render !== 'function') return 'the view has no render function'\n if (typeof view.label !== 'string' || view.label.trim().length === 0)\n return 'the view has no label'\n return null\n },\n })\n\n // Rail order for landing: each rail view (resolved), deduped, then the built-ins\n // not in the rail (so Settings — always available — is the final landing fallback).\n const inRail = new Set<string>()\n const landing: AppViewModule[] = []\n for (const view of railOrderViews) {\n const resolvedView = resolved.get(view.id)\n if (resolvedView && !inRail.has(resolvedView.id)) {\n inRail.add(resolvedView.id)\n landing.push(resolvedView)\n }\n }\n for (const view of DEFAULT_VIEWS) if (!inRail.has(view.id)) landing.push(view)\n\n const registry: ViewRegistry = { get: (id) => resolved.get(id), views: landing }\n\n // Build the display tree. Each view id is emitted at most once (the first of a\n // duplicate wins, matching `resolveOverrides`); a view dropped by validation is\n // skipped here too.\n const emitted = new Set<string>()\n const toRailView = (view: AppViewModule): ResolvedRailView | null => {\n const resolvedView = resolved.get(view.id)\n if (!resolvedView || emitted.has(resolvedView.id)) return null\n emitted.add(resolvedView.id)\n return {\n kind: 'view',\n id: resolvedView.id,\n label: resolvedView.label,\n icon: resolvedView.icon,\n path: resolvedView.id,\n availability: moduleAvailability(project, resolvedView),\n ...(resolvedView.visible ? { visible: resolvedView.visible } : {}),\n }\n }\n\n const railNodes: ResolvedRailNode[] = []\n for (const node of nodes) {\n if (node.kind === 'separator') {\n railNodes.push({ kind: 'separator' })\n continue\n }\n if (node.kind === 'group') {\n const views = node.children.map((child) => toRailView(child.view)).filter(isRailView)\n if (views.length > 0) railNodes.push({ kind: 'group', label: node.label, views })\n continue\n }\n // A top-level view, with its nested children (CMS collections) attached. The\n // parent is emitted first so its id wins any duplicate; children follow in order.\n const view = toRailView(node.view)\n if (!view) continue\n const children = childNodesOf(node, collections)\n .map((child) => toRailView(child.view))\n .filter(isRailView)\n if (children.length > 0) view.children = children\n railNodes.push(view)\n }\n\n return { nodes: railNodes, registry, issues: resolved.issues }\n}\n\nfunction isRailView(view: ResolvedRailView | null): view is ResolvedRailView {\n return view !== null\n}\n\n/**\n * Evaluate a rail view's OPTIONAL nav-permission predicate (EC-240, AD-18):\n * default-open — absent → visible; present → the predicate decides against the\n * live role. UI reflection only; the server chokepoint is the real boundary and\n * a hidden view stays URL-addressable (EC-221).\n */\nexport function isNavViewVisible(\n view: Pick<ResolvedRailView, 'visible'>,\n ctx: NavPermissionContext,\n): boolean {\n return view.visible ? view.visible(ctx) : true\n}\n\n/** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */\nexport function activeViewIdFromPath(pathname: string): string | null {\n return pathname.split('/')[2] || null\n}\n\n/** The view a bare project URL should open to: the first navigable rail view (EC-221). */\nexport function landingViewId(nav: StudioNav, project: Project): string {\n return firstAvailableView(nav.registry.views, project)?.id ?? 'settings'\n}\n\n/**\n * The active project's resolved studio nav — the default rail or\n * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam\n * (static per studio boot).\n */\nexport function useStudioNav(project: Project): StudioNav {\n // `resolveDeploymentSidebar`/`resolveDeploymentCollections` are config-holder reads\n // (stable per studio boot), so keying the memo on `project` alone is correct.\n return useMemo(\n () => resolveSidebar(resolveDeploymentSidebar(), project, resolveDeploymentCollections()),\n [project],\n )\n}\n"]}
|
|
@@ -6,7 +6,7 @@ import { cn } from '@elytracms/studio/lib/utils';
|
|
|
6
6
|
import { filterAssets } from '../assets/asset-search';
|
|
7
7
|
import { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store';
|
|
8
8
|
import { newlyAddedAssetIds } from '../assets/focal-point';
|
|
9
|
-
import {
|
|
9
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
10
10
|
/**
|
|
11
11
|
* Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).
|
|
12
12
|
* Backed by the *real* asset library (the assets store over the operations
|
|
@@ -16,7 +16,7 @@ import { useCms } from './cms-store';
|
|
|
16
16
|
* auto-selects the new asset (EC-301).
|
|
17
17
|
*/
|
|
18
18
|
export function AssetPicker({ value, open, onOpenChange, onChange, accept, }) {
|
|
19
|
-
const
|
|
19
|
+
const source = useCollectionView();
|
|
20
20
|
const ambient = useOptionalAssets();
|
|
21
21
|
// Mount nothing while closed: the self-provided assets store would
|
|
22
22
|
// otherwise load the library eagerly for every asset field on the form.
|
|
@@ -25,12 +25,12 @@ export function AssetPicker({ value, open, onOpenChange, onChange, accept, }) {
|
|
|
25
25
|
if (ambient) {
|
|
26
26
|
return (_jsx(AssetPickerDialog, { value: value, onOpenChange: onOpenChange, onChange: onChange, accept: accept }));
|
|
27
27
|
}
|
|
28
|
-
if (
|
|
28
|
+
if (source.projectId === undefined) {
|
|
29
29
|
// No project scope (fixture-only CMS session): the library is
|
|
30
30
|
// unreachable — say so instead of silently showing nothing.
|
|
31
31
|
return (_jsx(Dialog, { open: true, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Pick an asset" }), _jsx(DialogDescription, { children: "Select from the project's asset library." })] }), _jsxs(Row, { gap: "2", "data-testid": "asset-picker-no-project", children: [_jsx(AlertTriangle, { className: "size-4 text-warning" }), _jsx(Text, { size: "sm", tone: "muted", children: "The asset library needs a project \u2014 this CMS session has none." })] })] }) }));
|
|
32
32
|
}
|
|
33
|
-
return (_jsx(AssetsProvider, { projectId:
|
|
33
|
+
return (_jsx(AssetsProvider, { projectId: source.projectId, children: _jsx(AssetPickerDialog, { value: value, onOpenChange: onOpenChange, onChange: onChange, accept: accept }) }));
|
|
34
34
|
}
|
|
35
35
|
/** The open dialog over a mounted assets store. */
|
|
36
36
|
function AssetPickerDialog({ value, onOpenChange, onChange, accept, }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Maximize2, Minimize2, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { filterAssets } from '../assets/asset-search'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\nimport { useCms } from './cms-store'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field. The dialog\n * is form-width with a maximize toggle and supports drag/click upload that\n * auto-selects the new asset (EC-301).\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** The file input's `accept` attribute for in-picker upload (field-derived). */\n accept?: string\n}) {\n const cms = useCms()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n )\n }\n if (cms.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Pick an asset</DialogTitle>\n <DialogDescription>Select from the project's asset library.</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n The asset library needs a project — this CMS session has none.\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={cms.projectId}>\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n </AssetsProvider>\n )\n}\n\n/** The open dialog over a mounted assets store. */\nfunction AssetPickerDialog({\n value,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [maximized, setMaximized] = useState(false)\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // Library ids when an upload started; the effect below diffs against it to\n // learn the new asset and select it (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = filterAssets(store.assets, query)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n\n // Auto-select an asset uploaded from within the picker, then close.\n useEffect(() => {\n if (!pendingRef.current) return\n const added = newlyAddedAssetIds(pendingRef.current, libraryIds)\n if (added.length === 0) return\n pendingRef.current = null\n onChange(added[0]!)\n onOpenChange(false)\n }, [libraryIds, onChange, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = new Set(libraryIds)\n store.upload(files)\n }\n // Radix portals the dialog in the DOM, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the dialog would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(maximized ? 'max-w-[95vw]' : 'max-w-2xl')}\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <DialogHeader>\n <Row gap=\"2\" align=\"center\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <DialogTitle>Pick an asset</DialogTitle>\n <DialogDescription>Select from the project's asset library.</DialogDescription>\n </Stack>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={maximized ? 'Restore dialog size' : 'Maximize dialog'}\n onClick={() => setMaximized((v) => !v)}\n className=\"mr-7\"\n >\n {maximized ? <Minimize2 className=\"size-4\" /> : <Maximize2 className=\"size-4\" />}\n </Button>\n </Row>\n </DialogHeader>\n\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search assets…\"\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> Upload\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className={cn(\n 'grid gap-2 overflow-auto',\n maximized\n ? 'max-h-[78vh] grid-cols-4 lg:grid-cols-6'\n : 'max-h-[60vh] grid-cols-3 sm:grid-cols-4',\n )}\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n Loading assets…\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? 'Assets failed to load.'}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim()\n ? 'No assets match your search.'\n : 'No assets yet — drop files here or use Upload.'}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => {\n onChange(asset.id)\n onOpenChange(false)\n }}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n onChange(null)\n onOpenChange(false)\n }}\n >\n Clear\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">Drop to upload</Text>\n </Row>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** One selectable asset: thumbnail + title/filename + alt text. The internal\n * asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n >\n <PickerThumbnail asset={asset} />\n <Row gap=\"1\">\n <Text size=\"xs\" className=\"min-w-0 flex-1 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {selected ? <Check className=\"size-3.5 text-primary\" /> : null}\n </Row>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? 'No alt text'}\n </Text>\n </button>\n )\n}\n\n/** Image preview from the stored serve URL; icon fallback for other types. */\nfunction PickerThumbnail({ asset }: { asset: AssetRecord }) {\n const isImage = asset.contentType.startsWith('image/')\n // In-memory/opaque locators (memory://, convex://) are not fetchable by <img>.\n const resolvable = asset.url?.startsWith('http') ?? false\n if (isImage && asset.url && resolvable) {\n return (\n <img\n src={asset.url}\n alt={asset.alt ?? asset.filename}\n className=\"aspect-video w-full rounded-md bg-muted object-cover\"\n />\n )\n }\n return (\n <span className=\"grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground\">\n <ImageIcon className=\"size-6\" />\n </span>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Maximize2, Minimize2, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { filterAssets } from '../assets/asset-search'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field. The dialog\n * is form-width with a maximize toggle and supports drag/click upload that\n * auto-selects the new asset (EC-301).\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** The file input's `accept` attribute for in-picker upload (field-derived). */\n accept?: string\n}) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Pick an asset</DialogTitle>\n <DialogDescription>Select from the project's asset library.</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n The asset library needs a project — this CMS session has none.\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n </AssetsProvider>\n )\n}\n\n/** The open dialog over a mounted assets store. */\nfunction AssetPickerDialog({\n value,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [maximized, setMaximized] = useState(false)\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // Library ids when an upload started; the effect below diffs against it to\n // learn the new asset and select it (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = filterAssets(store.assets, query)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n\n // Auto-select an asset uploaded from within the picker, then close.\n useEffect(() => {\n if (!pendingRef.current) return\n const added = newlyAddedAssetIds(pendingRef.current, libraryIds)\n if (added.length === 0) return\n pendingRef.current = null\n onChange(added[0]!)\n onOpenChange(false)\n }, [libraryIds, onChange, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = new Set(libraryIds)\n store.upload(files)\n }\n // Radix portals the dialog in the DOM, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the dialog would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(maximized ? 'max-w-[95vw]' : 'max-w-2xl')}\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <DialogHeader>\n <Row gap=\"2\" align=\"center\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <DialogTitle>Pick an asset</DialogTitle>\n <DialogDescription>Select from the project's asset library.</DialogDescription>\n </Stack>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={maximized ? 'Restore dialog size' : 'Maximize dialog'}\n onClick={() => setMaximized((v) => !v)}\n className=\"mr-7\"\n >\n {maximized ? <Minimize2 className=\"size-4\" /> : <Maximize2 className=\"size-4\" />}\n </Button>\n </Row>\n </DialogHeader>\n\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search assets…\"\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> Upload\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className={cn(\n 'grid gap-2 overflow-auto',\n maximized\n ? 'max-h-[78vh] grid-cols-4 lg:grid-cols-6'\n : 'max-h-[60vh] grid-cols-3 sm:grid-cols-4',\n )}\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n Loading assets…\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? 'Assets failed to load.'}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim()\n ? 'No assets match your search.'\n : 'No assets yet — drop files here or use Upload.'}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => {\n onChange(asset.id)\n onOpenChange(false)\n }}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n onChange(null)\n onOpenChange(false)\n }}\n >\n Clear\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">Drop to upload</Text>\n </Row>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** One selectable asset: thumbnail + title/filename + alt text. The internal\n * asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n >\n <PickerThumbnail asset={asset} />\n <Row gap=\"1\">\n <Text size=\"xs\" className=\"min-w-0 flex-1 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {selected ? <Check className=\"size-3.5 text-primary\" /> : null}\n </Row>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? 'No alt text'}\n </Text>\n </button>\n )\n}\n\n/** Image preview from the stored serve URL; icon fallback for other types. */\nfunction PickerThumbnail({ asset }: { asset: AssetRecord }) {\n const isImage = asset.contentType.startsWith('image/')\n // In-memory/opaque locators (memory://, convex://) are not fetchable by <img>.\n const resolvable = asset.url?.startsWith('http') ?? false\n if (isImage && asset.url && resolvable) {\n return (\n <img\n src={asset.url}\n alt={asset.alt ?? asset.filename}\n className=\"aspect-video w-full rounded-md bg-muted object-cover\"\n />\n )\n }\n return (\n <span className=\"grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground\">\n <ImageIcon className=\"size-6\" />\n </span>\n )\n}\n"]}
|