@cosmicdrift/kumiko-renderer 0.373.0 → 0.377.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/app/action-form-support.d.ts +58 -0
- package/dist/app/action-form-support.js +96 -0
- package/dist/app/client-actions.d.ts +46 -0
- package/dist/app/client-actions.js +60 -0
- package/dist/app/column-renderers.js +1 -1
- package/dist/app/config-edit-shim.js +1 -1
- package/dist/app/content-preview.js +1 -6
- package/dist/app/custom-screens.js +1 -1
- package/dist/app/dashboard-body.d.ts +1 -1
- package/dist/app/entity-list-expanded-row.d.ts +1 -1
- package/dist/app/entity-list-expanded-row.js +4 -0
- package/dist/app/form-schema.d.ts +1 -1
- package/dist/app/form-schema.js +2 -2
- package/dist/app/header-actions-bar.d.ts +19 -0
- package/dist/app/header-actions-bar.js +30 -0
- package/dist/app/kumiko-screen.d.ts +1 -1
- package/dist/app/kumiko-screen.js +134 -66
- package/dist/app/list-empty-content.d.ts +2 -2
- package/dist/app/list-empty-content.js +3 -2
- package/dist/app/list-facets.d.ts +1 -1
- package/dist/app/nav.js +2 -2
- package/dist/app/query-state-banners.d.ts +1 -1
- package/dist/app/reference-link.d.ts +1 -1
- package/dist/app/row-actions.d.ts +10 -2
- package/dist/app/row-actions.js +85 -6
- package/dist/app/secret-mint-body.d.ts +1 -1
- package/dist/app/secrets-edit-body.d.ts +1 -1
- package/dist/app/use-record-actions.d.ts +12 -0
- package/dist/app/use-record-actions.js +30 -0
- package/dist/app/write-failed-error.d.ts +1 -1
- package/dist/components/availability-options-select.d.ts +1 -1
- package/dist/components/embedded-list-field.d.ts +1 -1
- package/dist/components/embedded-list-field.js +1 -1
- package/dist/components/file-list-field.d.ts +1 -1
- package/dist/components/grid-cell-for-field.d.ts +4 -2
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/multi-select-checkboxes.d.ts +1 -1
- package/dist/components/multi-select-checkboxes.js +1 -1
- package/dist/components/query-options-select.d.ts +1 -1
- package/dist/components/related-list-section.d.ts +1 -1
- package/dist/components/related-list-section.js +16 -3
- package/dist/components/render-edit-logic.d.ts +1 -1
- package/dist/components/render-edit-types.d.ts +2 -2
- package/dist/components/render-edit.d.ts +1 -1
- package/dist/components/render-edit.js +5 -3
- package/dist/components/render-field.d.ts +5 -2
- package/dist/components/render-field.js +103 -9
- package/dist/components/render-list.d.ts +1 -1
- package/dist/components/render-list.js +20 -4
- package/dist/components/write-form-section.d.ts +1 -1
- package/dist/components/write-form-section.js +1 -1
- package/dist/context/dispatcher-context.d.ts +1 -1
- package/dist/format-when.js +1 -1
- package/dist/headless/apex/css.d.ts +3 -0
- package/dist/headless/apex/css.js +298 -0
- package/dist/headless/apex/index.d.ts +236 -0
- package/dist/headless/apex/index.js +377 -0
- package/dist/headless/apex/lightbox.d.ts +4 -0
- package/dist/headless/apex/lightbox.js +81 -0
- package/dist/headless/contracts/asset.d.ts +15 -0
- package/dist/headless/contracts/asset.js +17 -0
- package/dist/headless/contracts/index.d.ts +3 -0
- package/dist/headless/contracts/index.js +1 -0
- package/dist/headless/contracts/locale.d.ts +7 -0
- package/dist/headless/contracts/locale.js +11 -0
- package/dist/headless/contracts/primitives.d.ts +93 -0
- package/dist/headless/contracts/primitives.js +21 -0
- package/dist/headless/dispatcher/index.d.ts +2 -0
- package/dist/headless/dispatcher/index.js +1 -0
- package/dist/headless/dispatcher/types.d.ts +86 -0
- package/dist/headless/dispatcher/types.js +34 -0
- package/dist/headless/form/form-controller.d.ts +2 -0
- package/dist/headless/form/form-controller.js +409 -0
- package/dist/headless/form/index.d.ts +3 -0
- package/dist/headless/form/index.js +2 -0
- package/dist/headless/form/types.d.ts +76 -0
- package/dist/headless/form/types.js +1 -0
- package/dist/headless/form/zod-bridge.d.ts +10 -0
- package/dist/headless/form/zod-bridge.js +103 -0
- package/dist/headless/format/document-values.d.ts +12 -0
- package/dist/headless/format/document-values.js +70 -0
- package/dist/headless/format/escape.d.ts +5 -0
- package/dist/headless/format/escape.js +75 -0
- package/dist/headless/format/html-template.d.ts +9 -0
- package/dist/headless/format/html-template.js +39 -0
- package/dist/headless/format/index.d.ts +12 -0
- package/dist/headless/format/index.js +237 -0
- package/dist/headless/format/money.d.ts +3 -0
- package/dist/headless/format/money.js +39 -0
- package/dist/headless/index.d.ts +14 -0
- package/dist/headless/index.js +9 -0
- package/dist/headless/locale-routing/index.d.ts +41 -0
- package/dist/headless/locale-routing/index.js +145 -0
- package/dist/headless/nav/index.d.ts +2 -0
- package/dist/headless/nav/index.js +1 -0
- package/dist/headless/nav/resolve.d.ts +2 -0
- package/dist/headless/nav/resolve.js +71 -0
- package/dist/headless/nav/types.d.ts +26 -0
- package/dist/headless/nav/types.js +1 -0
- package/dist/headless/store/create-store.d.ts +2 -0
- package/dist/headless/store/create-store.js +43 -0
- package/dist/headless/store/equality.d.ts +1 -0
- package/dist/headless/store/equality.js +34 -0
- package/dist/headless/store/index.d.ts +3 -0
- package/dist/headless/store/index.js +2 -0
- package/dist/headless/store/types.d.ts +7 -0
- package/dist/headless/store/types.js +10 -0
- package/dist/headless/view-model/edit.d.ts +18 -0
- package/dist/headless/view-model/edit.js +501 -0
- package/dist/headless/view-model/embedded-list.d.ts +27 -0
- package/dist/headless/view-model/embedded-list.js +63 -0
- package/dist/headless/view-model/index.d.ts +7 -0
- package/dist/headless/view-model/index.js +3 -0
- package/dist/headless/view-model/list-file-display.d.ts +3 -0
- package/dist/headless/view-model/list-file-display.js +27 -0
- package/dist/headless/view-model/list.d.ts +16 -0
- package/dist/headless/view-model/list.js +229 -0
- package/dist/headless/view-model/types.d.ts +382 -0
- package/dist/headless/view-model/types.js +1 -0
- package/dist/hooks/use-ai-text.d.ts +1 -1
- package/dist/hooks/use-form.d.ts +1 -1
- package/dist/hooks/use-form.js +1 -1
- package/dist/hooks/use-mutation.d.ts +1 -1
- package/dist/hooks/use-query.d.ts +1 -1
- package/dist/hooks/use-store.d.ts +1 -1
- package/dist/hooks/use-store.js +7 -7
- package/dist/hooks/use-stream-handler.d.ts +1 -1
- package/dist/i18n-defaults.js +7 -0
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -4
- package/dist/live/csrf.d.ts +2 -0
- package/dist/live/csrf.js +60 -0
- package/dist/live/dispatcher-live.d.ts +7 -0
- package/dist/live/dispatcher-live.js +286 -0
- package/dist/live/envelopes.d.ts +101 -0
- package/dist/live/envelopes.js +35 -0
- package/dist/live/error-mapping.d.ts +16 -0
- package/dist/live/error-mapping.js +82 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +5 -0
- package/dist/live/locale.d.ts +2 -0
- package/dist/live/locale.js +25 -0
- package/dist/live/sse-stream.d.ts +14 -0
- package/dist/live/sse-stream.js +128 -0
- package/dist/primitives.d.ts +53 -36
- package/dist/primitives.js +1 -1
- package/dist/session/session-ended.d.ts +1 -1
- package/dist/tokens.d.ts +3 -4
- package/dist/tokens.js +4 -5
- package/dist/web/app/app-schema-boot.d.ts +19 -0
- package/dist/web/app/app-schema-boot.js +124 -0
- package/dist/web/app/browser-locale.d.ts +22 -0
- package/dist/web/app/browser-locale.js +78 -0
- package/dist/web/app/client-plugin.d.ts +106 -0
- package/dist/web/app/client-plugin.js +12 -0
- package/dist/web/app/create-app.d.ts +104 -0
- package/dist/web/app/create-app.js +498 -0
- package/dist/web/app/create-public-surface.d.ts +36 -0
- package/dist/web/app/create-public-surface.js +40 -0
- package/dist/web/app/dashboard-body.d.ts +3 -0
- package/dist/web/app/dashboard-body.js +865 -0
- package/dist/web/app/dashboard-refresh.d.ts +6 -0
- package/dist/web/app/dashboard-refresh.js +15 -0
- package/dist/web/app/document-lang-sync.d.ts +4 -0
- package/dist/web/app/document-lang-sync.js +27 -0
- package/dist/web/app/draft-storage.d.ts +11 -0
- package/dist/web/app/draft-storage.js +53 -0
- package/dist/web/app/nav-providers-context.d.ts +13 -0
- package/dist/web/app/nav-providers-context.js +18 -0
- package/dist/web/app/nav.d.ts +40 -0
- package/dist/web/app/nav.js +231 -0
- package/dist/web/app/plain-content-editor.d.ts +3 -0
- package/dist/web/app/plain-content-editor.js +30 -0
- package/dist/web/app/relative-time.d.ts +10 -0
- package/dist/web/app/relative-time.js +56 -0
- package/dist/web/app/resolvers-context.d.ts +12 -0
- package/dist/web/app/resolvers-context.js +10 -0
- package/dist/web/app/rich-content-editor.d.ts +3 -0
- package/dist/web/app/rich-content-editor.js +15 -0
- package/dist/web/app/root-error-fallback.d.ts +2 -0
- package/dist/web/app/root-error-fallback.js +21 -0
- package/dist/web/app/screen-error-fallback.d.ts +6 -0
- package/dist/web/app/screen-error-fallback.js +17 -0
- package/dist/web/app/tiptap-editor.d.ts +3 -0
- package/dist/web/app/tiptap-editor.js +120 -0
- package/dist/web/app/toast-action-notifier.d.ts +5 -0
- package/dist/web/app/toast-action-notifier.js +17 -0
- package/dist/web/components/config-cascade.d.ts +13 -0
- package/dist/web/components/config-cascade.js +155 -0
- package/dist/web/components/config-source-badge.d.ts +6 -0
- package/dist/web/components/config-source-badge.js +55 -0
- package/dist/web/default-color-scheme.d.ts +2 -0
- package/dist/web/default-color-scheme.js +3 -0
- package/dist/web/icons.d.ts +94 -0
- package/dist/web/icons.js +100 -0
- package/dist/web/index.d.ts +78 -0
- package/dist/web/index.js +53 -0
- package/dist/web/layout/app-layout.d.ts +21 -0
- package/dist/web/layout/app-layout.js +8 -0
- package/dist/web/layout/avatar.d.ts +12 -0
- package/dist/web/layout/avatar.js +63 -0
- package/dist/web/layout/default-app-shell.d.ts +41 -0
- package/dist/web/layout/default-app-shell.js +16 -0
- package/dist/web/layout/editor-panel.d.ts +10 -0
- package/dist/web/layout/editor-panel.js +29 -0
- package/dist/web/layout/fill-classes.d.ts +9 -0
- package/dist/web/layout/fill-classes.js +15 -0
- package/dist/web/layout/filter-app-schema-navs.d.ts +7 -0
- package/dist/web/layout/filter-app-schema-navs.js +68 -0
- package/dist/web/layout/header-action-group.d.ts +6 -0
- package/dist/web/layout/header-action-group.js +8 -0
- package/dist/web/layout/header-overflow-menu.d.ts +4 -0
- package/dist/web/layout/header-overflow-menu.js +8 -0
- package/dist/web/layout/language-menu-items.d.ts +8 -0
- package/dist/web/layout/language-menu-items.js +16 -0
- package/dist/web/layout/language-switcher.d.ts +25 -0
- package/dist/web/layout/language-switcher.js +46 -0
- package/dist/web/layout/nav-tree.d.ts +19 -0
- package/dist/web/layout/nav-tree.js +536 -0
- package/dist/web/layout/page-header-slot.d.ts +25 -0
- package/dist/web/layout/page-header-slot.js +50 -0
- package/dist/web/layout/profile-menu.d.ts +13 -0
- package/dist/web/layout/profile-menu.js +6 -0
- package/dist/web/layout/shell-breadcrumb.d.ts +11 -0
- package/dist/web/layout/shell-breadcrumb.js +34 -0
- package/dist/web/layout/shell-header.d.ts +12 -0
- package/dist/web/layout/shell-header.js +159 -0
- package/dist/web/layout/sidebar-brand.d.ts +15 -0
- package/dist/web/layout/sidebar-brand.js +9 -0
- package/dist/web/layout/sidebar-panel.d.ts +41 -0
- package/dist/web/layout/sidebar-panel.js +107 -0
- package/dist/web/layout/sidebar-user.d.ts +8 -0
- package/dist/web/layout/sidebar-user.js +17 -0
- package/dist/web/layout/sidebar.d.ts +17 -0
- package/dist/web/layout/sidebar.js +11 -0
- package/dist/web/layout/target-resolver-stub.d.ts +13 -0
- package/dist/web/layout/target-resolver-stub.js +61 -0
- package/dist/web/layout/target-url.d.ts +12 -0
- package/dist/web/layout/target-url.js +77 -0
- package/dist/web/layout/theme-label.d.ts +15 -0
- package/dist/web/layout/theme-label.js +35 -0
- package/dist/web/layout/theme-menu-item.d.ts +12 -0
- package/dist/web/layout/theme-menu-item.js +16 -0
- package/dist/web/layout/theme-toggle.d.ts +17 -0
- package/dist/web/layout/theme-toggle.js +22 -0
- package/dist/web/layout/topbar.d.ts +8 -0
- package/dist/web/layout/topbar.js +4 -0
- package/dist/web/layout/workspace-shell.d.ts +63 -0
- package/dist/web/layout/workspace-shell.js +220 -0
- package/dist/web/layout/workspace-switcher.d.ts +15 -0
- package/dist/web/layout/workspace-switcher.js +36 -0
- package/dist/web/lib/accept-attr.d.ts +1 -0
- package/dist/web/lib/accept-attr.js +6 -0
- package/dist/web/lib/clamp.d.ts +1 -0
- package/dist/web/lib/clamp.js +3 -0
- package/dist/web/lib/cn.d.ts +6 -0
- package/dist/web/lib/cn.js +9 -0
- package/dist/web/lib/download.d.ts +2 -0
- package/dist/web/lib/download.js +24 -0
- package/dist/web/lib/file-ref-download.d.ts +15 -0
- package/dist/web/lib/file-ref-download.js +62 -0
- package/dist/web/lib/file-upload-form-data.d.ts +7 -0
- package/dist/web/lib/file-upload-form-data.js +11 -0
- package/dist/web/lib/resize-image.d.ts +1 -0
- package/dist/web/lib/resize-image.js +66 -0
- package/dist/web/lib/upload-file-ref.d.ts +9 -0
- package/dist/web/lib/upload-file-ref.js +29 -0
- package/dist/web/primitives/action-menu.d.ts +34 -0
- package/dist/web/primitives/action-menu.js +30 -0
- package/dist/web/primitives/calendar-popover.d.ts +13 -0
- package/dist/web/primitives/calendar-popover.js +76 -0
- package/dist/web/primitives/combobox.d.ts +56 -0
- package/dist/web/primitives/combobox.js +127 -0
- package/dist/web/primitives/date-field.d.ts +18 -0
- package/dist/web/primitives/date-field.js +74 -0
- package/dist/web/primitives/date-input.d.ts +1 -0
- package/dist/web/primitives/date-input.js +5 -0
- package/dist/web/primitives/date-parse.d.ts +11 -0
- package/dist/web/primitives/date-parse.js +159 -0
- package/dist/web/primitives/dialog.d.ts +3 -0
- package/dist/web/primitives/dialog.js +42 -0
- package/dist/web/primitives/drawer.d.ts +3 -0
- package/dist/web/primitives/drawer.js +17 -0
- package/dist/web/primitives/dropdown-menu.d.ts +15 -0
- package/dist/web/primitives/dropdown-menu.js +56 -0
- package/dist/web/primitives/embedded-list-input.d.ts +9 -0
- package/dist/web/primitives/embedded-list-input.js +382 -0
- package/dist/web/primitives/file-thumbnail.d.ts +25 -0
- package/dist/web/primitives/file-thumbnail.js +90 -0
- package/dist/web/primitives/file-upload.d.ts +21 -0
- package/dist/web/primitives/file-upload.js +56 -0
- package/dist/web/primitives/index.d.ts +34 -0
- package/dist/web/primitives/index.js +2021 -0
- package/dist/web/primitives/json-view.d.ts +7 -0
- package/dist/web/primitives/json-view.js +84 -0
- package/dist/web/primitives/layout.d.ts +27 -0
- package/dist/web/primitives/layout.js +30 -0
- package/dist/web/primitives/lightbox.d.ts +3 -0
- package/dist/web/primitives/lightbox.js +36 -0
- package/dist/web/primitives/locale-number.d.ts +2 -0
- package/dist/web/primitives/locale-number.js +54 -0
- package/dist/web/primitives/located-timestamp-input.d.ts +22 -0
- package/dist/web/primitives/located-timestamp-input.js +22 -0
- package/dist/web/primitives/metric.d.ts +5 -0
- package/dist/web/primitives/metric.js +18 -0
- package/dist/web/primitives/modal-shell.d.ts +17 -0
- package/dist/web/primitives/modal-shell.js +9 -0
- package/dist/web/primitives/modal.d.ts +3 -0
- package/dist/web/primitives/modal.js +13 -0
- package/dist/web/primitives/money-input.d.ts +18 -0
- package/dist/web/primitives/money-input.js +122 -0
- package/dist/web/primitives/narrow-pinned-footer.d.ts +12 -0
- package/dist/web/primitives/narrow-pinned-footer.js +153 -0
- package/dist/web/primitives/number-input.d.ts +18 -0
- package/dist/web/primitives/number-input.js +53 -0
- package/dist/web/primitives/option-groups.d.ts +11 -0
- package/dist/web/primitives/option-groups.js +27 -0
- package/dist/web/primitives/page-header.d.ts +3 -0
- package/dist/web/primitives/page-header.js +52 -0
- package/dist/web/primitives/promo-panel.d.ts +3 -0
- package/dist/web/primitives/promo-panel.js +11 -0
- package/dist/web/primitives/segmented-select.d.ts +21 -0
- package/dist/web/primitives/segmented-select.js +62 -0
- package/dist/web/primitives/share-actions.d.ts +4 -0
- package/dist/web/primitives/share-actions.js +56 -0
- package/dist/web/primitives/status-badge.d.ts +3 -0
- package/dist/web/primitives/status-badge.js +5 -0
- package/dist/web/primitives/sticky-action-bar.d.ts +5 -0
- package/dist/web/primitives/sticky-action-bar.js +15 -0
- package/dist/web/primitives/tabs.d.ts +3 -0
- package/dist/web/primitives/tabs.js +107 -0
- package/dist/web/primitives/timestamp-input.d.ts +27 -0
- package/dist/web/primitives/timestamp-input.js +87 -0
- package/dist/web/primitives/toast.d.ts +19 -0
- package/dist/web/primitives/toast.js +63 -0
- package/dist/web/primitives/tz-input.d.ts +11 -0
- package/dist/web/primitives/tz-input.js +8 -0
- package/dist/web/primitives/tz-options.d.ts +4 -0
- package/dist/web/primitives/tz-options.js +17 -0
- package/dist/web/primitives/use-keyboard-inset.d.ts +1 -0
- package/dist/web/primitives/use-keyboard-inset.js +28 -0
- package/dist/web/primitives/use-narrow-viewport.d.ts +4 -0
- package/dist/web/primitives/use-narrow-viewport.js +46 -0
- package/dist/web/react-css-custom-properties.d.ts +6 -0
- package/dist/web/react-css-custom-properties.js +1 -0
- package/dist/web/sse/live-events.d.ts +15 -0
- package/dist/web/sse/live-events.js +148 -0
- package/dist/web/theme-plugin.d.ts +41 -0
- package/dist/web/theme-plugin.js +123 -0
- package/dist/web/tokens.d.ts +24 -0
- package/dist/web/tokens.js +203 -0
- package/dist/web/ui/avatar.d.ts +11 -0
- package/dist/web/ui/avatar.js +25 -0
- package/dist/web/ui/badge.d.ts +9 -0
- package/dist/web/ui/badge.js +26 -0
- package/dist/web/ui/breadcrumb.d.ts +11 -0
- package/dist/web/ui/breadcrumb.js +29 -0
- package/dist/web/ui/button.d.ts +10 -0
- package/dist/web/ui/button.js +37 -0
- package/dist/web/ui/checkbox.d.ts +4 -0
- package/dist/web/ui/checkbox.js +11 -0
- package/dist/web/ui/input.d.ts +3 -0
- package/dist/web/ui/input.js +8 -0
- package/dist/web/ui/label.d.ts +4 -0
- package/dist/web/ui/label.js +10 -0
- package/dist/web/ui/resizable.d.ts +7 -0
- package/dist/web/ui/resizable.js +16 -0
- package/dist/web/ui/separator.d.ts +4 -0
- package/dist/web/ui/separator.js +10 -0
- package/dist/web/ui/sheet.d.ts +14 -0
- package/dist/web/ui/sheet.js +42 -0
- package/dist/web/ui/sidebar.d.ts +73 -0
- package/dist/web/ui/sidebar.js +211 -0
- package/dist/web/ui/skeleton.d.ts +2 -0
- package/dist/web/ui/skeleton.js +7 -0
- package/dist/web/ui/switch.d.ts +6 -0
- package/dist/web/ui/switch.js +10 -0
- package/dist/web/ui/table.d.ts +10 -0
- package/dist/web/ui/table.js +30 -0
- package/dist/web/ui/tabs.d.ts +11 -0
- package/dist/web/ui/tabs.js +31 -0
- package/dist/web/ui/textarea.d.ts +3 -0
- package/dist/web/ui/textarea.js +8 -0
- package/dist/web/ui/tooltip.d.ts +7 -0
- package/dist/web/ui/tooltip.js +19 -0
- package/dist/web/ui/use-mobile.d.ts +1 -0
- package/dist/web/ui/use-mobile.js +16 -0
- package/dist/web/version/update-checker.d.ts +9 -0
- package/dist/web/version/update-checker.js +88 -0
- package/dist/web/widgets/ai-text-field.d.ts +36 -0
- package/dist/web/widgets/ai-text-field.js +137 -0
- package/dist/web/widgets/chart-window.d.ts +58 -0
- package/dist/web/widgets/chart-window.js +190 -0
- package/dist/web/widgets/charts.d.ts +208 -0
- package/dist/web/widgets/charts.js +685 -0
- package/dist/web/widgets/collapsible-section.d.ts +10 -0
- package/dist/web/widgets/collapsible-section.js +17 -0
- package/dist/web/widgets/dashboard-list.d.ts +21 -0
- package/dist/web/widgets/dashboard-list.js +45 -0
- package/dist/web/widgets/detail-list.d.ts +15 -0
- package/dist/web/widgets/detail-list.js +8 -0
- package/dist/web/widgets/drawer.d.ts +65 -0
- package/dist/web/widgets/drawer.js +142 -0
- package/dist/web/widgets/feed-list.d.ts +13 -0
- package/dist/web/widgets/feed-list.js +9 -0
- package/dist/web/widgets/floating-panel.d.ts +30 -0
- package/dist/web/widgets/floating-panel.js +242 -0
- package/dist/web/widgets/form-fields.d.ts +93 -0
- package/dist/web/widgets/form-fields.js +50 -0
- package/dist/web/widgets/index.d.ts +27 -0
- package/dist/web/widgets/index.js +30 -0
- package/dist/web/widgets/infinity-list.d.ts +46 -0
- package/dist/web/widgets/infinity-list.js +211 -0
- package/dist/web/widgets/mode-switch.d.ts +21 -0
- package/dist/web/widgets/mode-switch.js +25 -0
- package/dist/web/widgets/photo-slots.d.ts +29 -0
- package/dist/web/widgets/photo-slots.js +52 -0
- package/dist/web/widgets/plan-card.d.ts +36 -0
- package/dist/web/widgets/plan-card.js +13 -0
- package/dist/web/widgets/progress-bar.d.ts +19 -0
- package/dist/web/widgets/progress-bar.js +24 -0
- package/dist/web/widgets/progress-list.d.ts +19 -0
- package/dist/web/widgets/progress-list.js +11 -0
- package/dist/web/widgets/public-shell.d.ts +22 -0
- package/dist/web/widgets/public-shell.js +15 -0
- package/dist/web/widgets/query-table.d.ts +29 -0
- package/dist/web/widgets/query-table.js +30 -0
- package/dist/web/widgets/result-panel.d.ts +69 -0
- package/dist/web/widgets/result-panel.js +30 -0
- package/dist/web/widgets/section-card.d.ts +12 -0
- package/dist/web/widgets/section-card.js +9 -0
- package/dist/web/widgets/sheet-parts.d.ts +9 -0
- package/dist/web/widgets/sheet-parts.js +11 -0
- package/dist/web/widgets/side-by-side-table.d.ts +31 -0
- package/dist/web/widgets/side-by-side-table.js +11 -0
- package/dist/web/widgets/stat.d.ts +54 -0
- package/dist/web/widgets/stat.js +65 -0
- package/dist/web/widgets/states.d.ts +23 -0
- package/dist/web/widgets/states.js +20 -0
- package/dist/web/widgets/status-badge.d.ts +15 -0
- package/dist/web/widgets/status-badge.js +24 -0
- package/dist/web/widgets/step-bar.d.ts +35 -0
- package/dist/web/widgets/step-bar.js +60 -0
- package/dist/web/widgets/upload-zone.d.ts +24 -0
- package/dist/web/widgets/upload-zone.js +108 -0
- package/dist/web/widgets/use-draft.d.ts +20 -0
- package/dist/web/widgets/use-draft.js +21 -0
- package/dist/web/widgets/use-pointer-drag.d.ts +21 -0
- package/dist/web/widgets/use-pointer-drag.js +53 -0
- package/package.json +105 -7
- package/src/changes.json +77 -0
- package/src/web/fonts/OFL.txt +93 -0
- package/src/web/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
- package/src/web/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
- package/src/web/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
- package/src/web/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
- package/src/web/fonts/ibm-plex-sans-latin-400.woff2 +0 -0
- package/src/web/fonts/ibm-plex-sans-latin-500.woff2 +0 -0
- package/src/web/fonts/ibm-plex-sans-latin-600.woff2 +0 -0
- package/src/web/fonts/ibm-plex-sans-latin-ext-400.woff2 +0 -0
- package/src/web/fonts/ibm-plex-sans-latin-ext-500.woff2 +0 -0
- package/src/web/fonts/ibm-plex-sans-latin-ext-600.woff2 +0 -0
- package/src/web/styles.css +401 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronRight, Plus, Search } from "lucide-react";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
4
|
+
import { resolveNavigation } from "../../headless/index.js";
|
|
5
|
+
import { lastSegment, toAppSchema, useLiveEvents, useNav, useTranslation } from "../../index.js";
|
|
6
|
+
import { KumikoLink } from "../app/nav.js";
|
|
7
|
+
import { useNavEntities, useNavProviders } from "../app/nav-providers-context.js";
|
|
8
|
+
import { NAV_ICONS } from "../icons.js";
|
|
9
|
+
import { cn } from "../lib/cn.js";
|
|
10
|
+
import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarInput, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useSidebar, } from "../ui/sidebar.js";
|
|
11
|
+
import { resolveParentScreenId } from "./shell-breadcrumb.js";
|
|
12
|
+
import { useDispatchTarget } from "./target-resolver-stub.js";
|
|
13
|
+
import { parseTargetFromSearchParams } from "./target-url.js";
|
|
14
|
+
// Widened alias for the two lookup sites below, which index by the plain
|
|
15
|
+
// `string` icon key of the resolved NavNode/TreeAction tree — not the
|
|
16
|
+
// closed NavIconKey union NAV_ICONS itself is typed against.
|
|
17
|
+
const NAV_ICON_LOOKUP = NAV_ICONS;
|
|
18
|
+
const EMPTY_BADGES = new Map();
|
|
19
|
+
const NavBadgesContext = createContext(EMPTY_BADGES);
|
|
20
|
+
const NavFilterContext = createContext({ q: "", matches: () => true });
|
|
21
|
+
const ActiveScreenMarkerContext = createContext(undefined);
|
|
22
|
+
// Walks only the static `children` — provider-emitted nodes (treeNodeToNavNode
|
|
23
|
+
// below) never set `screen`, so they can't hide a match this walk would miss.
|
|
24
|
+
function treeContainsScreen(nodes, screenId) {
|
|
25
|
+
return nodes.some((n) => (n.screen !== undefined && lastSegment(n.screen) === screenId) ||
|
|
26
|
+
treeContainsScreen(n.children, screenId));
|
|
27
|
+
}
|
|
28
|
+
// Ein Knoten überlebt den Filter, wenn er selbst oder ein Nachfahre matcht.
|
|
29
|
+
// Provider-Kinder sind zur Filterzeit schon materialisiert (Provider-Knoten
|
|
30
|
+
// sind default-expanded → eager geladen), darum reicht die statische
|
|
31
|
+
// children-Rekursion. ponytail: ein zur Filterzeit noch NICHT geladener
|
|
32
|
+
// (user-zugeklappter) Provider wird über den force-expand in useNavNodeState
|
|
33
|
+
// aufgeklappt, lädt und filtert dann mit.
|
|
34
|
+
function subtreeMatches(node, matches) {
|
|
35
|
+
return matches(node.label) || node.children.some((c) => subtreeMatches(c, matches));
|
|
36
|
+
}
|
|
37
|
+
export function NavTree({ schema, user, testId, allowedNavQns, navBadges, }) {
|
|
38
|
+
const app = useMemo(() => toAppSchema(schema), [schema]);
|
|
39
|
+
const tree = useMemo(() => {
|
|
40
|
+
const source = buildNavRegistrySliceForApp(app, allowedNavQns);
|
|
41
|
+
return resolveNavigation({ source, ...(user !== undefined && { user }) });
|
|
42
|
+
}, [app, user, allowedNavQns]);
|
|
43
|
+
const nav = useNav();
|
|
44
|
+
const activeScreenId = nav.route?.screenId;
|
|
45
|
+
const activeEntityId = nav.route?.entityId;
|
|
46
|
+
// Same "no nav entry of its own" gap the breadcrumb closes (fw#2724):
|
|
47
|
+
// when the routed screen has no node in {tree}, fall back to its
|
|
48
|
+
// resolved parent so a detail/edit/form screen still orients the user.
|
|
49
|
+
const activeMarker = useMemo(() => {
|
|
50
|
+
if (activeScreenId === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const allScreens = app.features.flatMap((f) => f.screens);
|
|
53
|
+
const parentScreenId = resolveParentScreenId(allScreens, activeScreenId);
|
|
54
|
+
// A nav entry on an edit screen means "create"; an existing record (entityId
|
|
55
|
+
// in the route) belongs to its list, same rule as the breadcrumb.
|
|
56
|
+
if (activeEntityId !== undefined &&
|
|
57
|
+
parentScreenId !== undefined &&
|
|
58
|
+
treeContainsScreen(tree, parentScreenId)) {
|
|
59
|
+
return { screenId: parentScreenId, exact: false };
|
|
60
|
+
}
|
|
61
|
+
if (treeContainsScreen(tree, activeScreenId))
|
|
62
|
+
return { screenId: activeScreenId, exact: true };
|
|
63
|
+
return parentScreenId !== undefined ? { screenId: parentScreenId, exact: false } : undefined;
|
|
64
|
+
}, [tree, app.features, activeScreenId, activeEntityId]);
|
|
65
|
+
// Collapsed-Set: nur die explizit zugeklappten qualified-names. Default
|
|
66
|
+
// ist also "alles auf" — neue Features tauchen sofort offen auf, ohne
|
|
67
|
+
// dass der User erst klicken muss.
|
|
68
|
+
const [collapsed, setCollapsed] = useState(() => new Set());
|
|
69
|
+
const onToggle = useCallback((qn) => {
|
|
70
|
+
setCollapsed((prev) => {
|
|
71
|
+
const next = new Set(prev);
|
|
72
|
+
if (next.has(qn))
|
|
73
|
+
next.delete(qn);
|
|
74
|
+
else
|
|
75
|
+
next.add(qn);
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
}, []);
|
|
79
|
+
const t = useTranslation();
|
|
80
|
+
const { state: sidebarState } = useSidebar();
|
|
81
|
+
const [filter, setFilter] = useState("");
|
|
82
|
+
// The search box is only CSS-hidden (`group-data-[collapsible=icon]:hidden`)
|
|
83
|
+
// when the sidebar collapses to icon rail, not unmounted — without this,
|
|
84
|
+
// a stale filter re-applies invisibly on the next expand (fw#1816).
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (sidebarState === "collapsed")
|
|
87
|
+
setFilter("");
|
|
88
|
+
}, [sidebarState]);
|
|
89
|
+
const q = filter.trim().toLowerCase();
|
|
90
|
+
const matches = useCallback((raw) => {
|
|
91
|
+
const label = raw.includes(".") ? t(raw) : raw;
|
|
92
|
+
return label.toLowerCase().includes(q);
|
|
93
|
+
}, [t, q]);
|
|
94
|
+
const navFilter = useMemo(() => ({ q, matches }), [q, matches]);
|
|
95
|
+
return (_jsx(ActiveScreenMarkerContext.Provider, { value: activeMarker, children: _jsx(NavFilterContext.Provider, { value: navFilter, children: _jsx(NavBadgesContext.Provider, { value: navBadges ?? EMPTY_BADGES, children: _jsxs("div", { "data-testid": testId, "data-kumiko-layout": "nav-tree", className: "flex w-full flex-col gap-3", children: [_jsxs("div", { className: "relative mb-0 group-data-[collapsible=icon]:hidden", children: [_jsx(Search, { "aria-hidden": "true", className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-sidebar-muted" }), _jsx(SidebarInput, { className: "pl-8", value: filter, onChange: (e) => setFilter(e.target.value), placeholder: t("kumiko.nav.search"), "aria-label": t("kumiko.nav.search") })] }), tree.map((node) => isPureSection(node) ? (_jsx(NavSection, { node: node, collapsed: collapsed, onToggle: onToggle }, node.qualifiedName)) : (_jsx(SidebarMenu, { children: _jsx(NavMenuNode, { node: node, collapsed: collapsed, onToggle: onToggle }) }, node.qualifiedName)))] }) }) }) }));
|
|
96
|
+
}
|
|
97
|
+
// Runtime-Badge-Slot rechts am Label, gekeyt auf die bare nav-id. Kein
|
|
98
|
+
// Eintrag → nichts (silent, kein Layout-Shift). shrink-0 + die ml-auto
|
|
99
|
+
// schiebt ihn ans rechte Ende; das truncate-Label gibt nach.
|
|
100
|
+
// ponytail: nur an screen/target-Leaves verdrahtet, nicht an Container-
|
|
101
|
+
// Knoten (die tragen den ml-auto-Chevron) — Tier-Badge ist ein Leaf.
|
|
102
|
+
// Defense-in-depth (555/4): every current call site already guards on
|
|
103
|
+
// node.screen/node.target !== undefined before rendering <NavBadge>, so this
|
|
104
|
+
// early-return changes no current behavior — it's here so a future call
|
|
105
|
+
// site added without that guard can't silently show a leaf's badge on an
|
|
106
|
+
// unrelated container node that happens to share its last-segment.
|
|
107
|
+
function NavBadge({ node }) {
|
|
108
|
+
const badges = useContext(NavBadgesContext);
|
|
109
|
+
if (node.screen === undefined && node.target === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
const badge = badges.get(lastSegment(node.qualifiedName));
|
|
112
|
+
if (badge === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
return _jsx("span", { className: "ml-auto shrink-0", children: badge });
|
|
115
|
+
}
|
|
116
|
+
// Pure section = ein Gruppen-Header ohne eigenen Screen, der nur seine
|
|
117
|
+
// children gruppiert. Auf Top-Level wird daraus eine SidebarGroup mit
|
|
118
|
+
// togglebarem GroupLabel; ein Node mit Screen (auch wenn er children hat)
|
|
119
|
+
// ist dagegen ein klickbarer Menu-Eintrag.
|
|
120
|
+
function isPureSection(node) {
|
|
121
|
+
return (node.screen === undefined &&
|
|
122
|
+
node.target === undefined &&
|
|
123
|
+
node.provider !== true &&
|
|
124
|
+
node.createAction === undefined &&
|
|
125
|
+
node.children.length > 0);
|
|
126
|
+
}
|
|
127
|
+
// i18n-Key Konvention: enthält das label einen Punkt, durchs t() laufen
|
|
128
|
+
// lassen (Bundle kennt den Key → übersetzt, sonst bleibt der Key sichtbar
|
|
129
|
+
// = vergessene Übersetzung). Reine String-Labels bleiben unangetastet.
|
|
130
|
+
function useLabel(node) {
|
|
131
|
+
const t = useTranslation();
|
|
132
|
+
return node.label.includes(".") ? t(node.label) : node.label;
|
|
133
|
+
}
|
|
134
|
+
// Without an icon only the collapsed rail shows the initial letter; expanded,
|
|
135
|
+
// the label stands alone.
|
|
136
|
+
function NavLeadingIcon({ node, active, expanded = false, label, }) {
|
|
137
|
+
const iconKey = expanded && node.icon === "folder" ? "folder-open" : node.icon;
|
|
138
|
+
const isKnownIcon = iconKey !== undefined && Object.hasOwn(NAV_ICONS, iconKey);
|
|
139
|
+
const NavIcon = iconKey !== undefined && isKnownIcon ? NAV_ICON_LOOKUP[iconKey] : undefined;
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (iconKey !== undefined && !isKnownIcon) {
|
|
142
|
+
// biome-ignore lint/suspicious/noConsole: diagnostic for unregistered nav icon keys
|
|
143
|
+
console.warn(`[kumiko] Nav entry "${node.qualifiedName}" references icon "${iconKey}", which is not ` +
|
|
144
|
+
`registered in NAV_ICONS — falling back to the dot indicator. Check the icon key spelling.`);
|
|
145
|
+
}
|
|
146
|
+
}, [iconKey, isKnownIcon, node.qualifiedName]);
|
|
147
|
+
if (NavIcon !== undefined)
|
|
148
|
+
return _jsx(NavIcon, { "aria-hidden": "true", className: "shrink-0" });
|
|
149
|
+
const initial = label?.trim().charAt(0).toUpperCase();
|
|
150
|
+
if (initial === undefined || initial === "")
|
|
151
|
+
return null;
|
|
152
|
+
return (_jsx("span", { "aria-hidden": "true", className: cn("hidden size-4 shrink-0 items-center justify-center font-medium text-xs group-data-[collapsible=icon]:flex", active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"), children: initial }));
|
|
153
|
+
}
|
|
154
|
+
// Stable-reference empty-list — sonst destabilisiert ein neues `[]` pro
|
|
155
|
+
// Render die useEffect-deps der SSE-Subscription.
|
|
156
|
+
const EMPTY_ENTITY_LIST = [];
|
|
157
|
+
// Active-Target-Vergleich: featureId + action exakt, args shallow-equal
|
|
158
|
+
// (heute nur primitives). Gespiegelt aus dem alten TreeNodeRenderer.
|
|
159
|
+
function targetsEqual(a, b) {
|
|
160
|
+
if (b === undefined)
|
|
161
|
+
return false;
|
|
162
|
+
if (a.featureId !== b.featureId || a.action !== b.action)
|
|
163
|
+
return false;
|
|
164
|
+
const aKeys = a.args ? Object.keys(a.args) : [];
|
|
165
|
+
const bKeys = b.args ? Object.keys(b.args) : [];
|
|
166
|
+
if (aKeys.length !== bKeys.length)
|
|
167
|
+
return false;
|
|
168
|
+
for (const k of aKeys) {
|
|
169
|
+
if (a.args?.[k] !== b.args?.[k])
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
// Adapter: provider-emittierte TreeNodes → renderbare NavNodes. Synthetische
|
|
175
|
+
// QN aus parent-QN + idx-label (stabil über Emit-Order). Statische children
|
|
176
|
+
// rekursiv. ponytail: eine TreeChildrenSubscribe-Function als children wird
|
|
177
|
+
// als Leaf behandelt — nested-dynamic-Provider nutzt heute kein Consumer;
|
|
178
|
+
// upgrade-path = synthetische QN registrieren wenn einer sie emittet.
|
|
179
|
+
function treeNodeToNavNode(tn, parentQn, idx) {
|
|
180
|
+
const qualifiedName = `${parentQn}/${idx}-${tn.label}`;
|
|
181
|
+
const children = Array.isArray(tn.children)
|
|
182
|
+
? tn.children.map((c, i) => treeNodeToNavNode(c, qualifiedName, i))
|
|
183
|
+
: [];
|
|
184
|
+
return {
|
|
185
|
+
qualifiedName,
|
|
186
|
+
label: tn.label,
|
|
187
|
+
order: idx,
|
|
188
|
+
children,
|
|
189
|
+
...(tn.icon !== undefined && { icon: tn.icon }),
|
|
190
|
+
...(tn.target !== undefined && { target: tn.target }),
|
|
191
|
+
...(tn.actions !== undefined && { actions: tn.actions }),
|
|
192
|
+
...(tn.createAction !== undefined && { createAction: tn.createAction }),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Lazy provider-children + SSE-Live-Refresh für einen `provider: true`-Knoten.
|
|
196
|
+
// Subscribe läuft erst wenn der Knoten ausgeklappt ist (enabled); die
|
|
197
|
+
// treeEntities-Liste (per QN) re-fired den Provider bei Entity-Events → neu
|
|
198
|
+
// erstellte Knoten erscheinen live. Logik gespiegelt aus VisualTree.ProviderBranch.
|
|
199
|
+
function useNavProviderChildren(qn, enabled) {
|
|
200
|
+
const providers = useNavProviders();
|
|
201
|
+
const entitiesMap = useNavEntities();
|
|
202
|
+
const subscribeLive = useLiveEvents();
|
|
203
|
+
const provider = providers.get(qn);
|
|
204
|
+
const entities = entitiesMap.get(qn) ?? EMPTY_ENTITY_LIST;
|
|
205
|
+
const [raw, setRaw] = useState(null);
|
|
206
|
+
const [error, setError] = useState(null);
|
|
207
|
+
const [attempt, setAttempt] = useState(0);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!enabled || entities.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
const unsubs = entities.map((e) => subscribeLive(e, () => setAttempt((n) => n + 1)));
|
|
212
|
+
return () => {
|
|
213
|
+
for (const u of unsubs)
|
|
214
|
+
u();
|
|
215
|
+
};
|
|
216
|
+
}, [enabled, entities, subscribeLive]);
|
|
217
|
+
// `attempt` triggert SSE-Refresh + erneutes Subscribe — Biome sieht es
|
|
218
|
+
// nicht im body, semantisch ist es der re-fire-Trigger.
|
|
219
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: attempt = re-fire trigger
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!enabled) {
|
|
222
|
+
setRaw(null);
|
|
223
|
+
setError(null);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (provider === undefined) {
|
|
227
|
+
setError("Kein nav-provider registriert.");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
setError(null);
|
|
231
|
+
setRaw(null);
|
|
232
|
+
try {
|
|
233
|
+
const subscribe = provider();
|
|
234
|
+
try {
|
|
235
|
+
return subscribe(setRaw, (e) => setError(e instanceof Error ? e.message : String(e)));
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
setError(e instanceof Error ? e.message : "Subscribe fehlgeschlagen.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
setError(e instanceof Error ? e.message : "Provider-Init fehlgeschlagen.");
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}, [enabled, provider, attempt]);
|
|
246
|
+
const nodes = useMemo(() => (raw === null ? null : raw.map((tn, i) => treeNodeToNavNode(tn, qn, i))), [raw, qn]);
|
|
247
|
+
return { nodes, error };
|
|
248
|
+
}
|
|
249
|
+
// Geteilte Knoten-Logik für Top-Level- (NavMenuNode) und Sub-Knoten
|
|
250
|
+
// (NavSubNode): Label, Expand/Collapse, active-State (screen ODER target),
|
|
251
|
+
// und die lazy Provider-Children. Die zwei Renderer unterscheiden sich nur
|
|
252
|
+
// in den shadcn-Primitives, nicht in dieser Logik.
|
|
253
|
+
function useNavNodeState(node, collapsed) {
|
|
254
|
+
const nav = useNav();
|
|
255
|
+
const { q: filterQ, matches } = useContext(NavFilterContext);
|
|
256
|
+
const filterActive = filterQ !== "";
|
|
257
|
+
const displayLabel = useLabel(node);
|
|
258
|
+
const isCollapsed = collapsed.has(node.qualifiedName);
|
|
259
|
+
const expandable = node.children.length > 0 || node.provider === true;
|
|
260
|
+
// Aktiver Filter klappt jeden Container auf, damit Treffer unter zugeklappten
|
|
261
|
+
// Ordnern sichtbar werden (und Provider-Kinder nachladen).
|
|
262
|
+
const isExpanded = expandable && (!isCollapsed || filterActive);
|
|
263
|
+
const { nodes: providerChildren, error: providerError } = useNavProviderChildren(node.qualifiedName, node.provider === true && isExpanded);
|
|
264
|
+
const activeTarget = useMemo(() => parseTargetFromSearchParams(nav.searchParams), [nav.searchParams]);
|
|
265
|
+
const activeMarker = useContext(ActiveScreenMarkerContext);
|
|
266
|
+
const screenActive = node.screen !== undefined &&
|
|
267
|
+
activeMarker !== undefined &&
|
|
268
|
+
lastSegment(node.screen) === activeMarker.screenId;
|
|
269
|
+
const screenIsExactMatch = screenActive && activeMarker?.exact === true;
|
|
270
|
+
const targetActive = node.target !== undefined && targetsEqual(node.target, activeTarget);
|
|
271
|
+
const childNodes = node.provider === true ? (providerChildren ?? []) : node.children;
|
|
272
|
+
const visibleChildNodes = filterActive
|
|
273
|
+
? childNodes.filter((c) => subtreeMatches(c, matches))
|
|
274
|
+
: childNodes;
|
|
275
|
+
return {
|
|
276
|
+
displayLabel,
|
|
277
|
+
isCollapsed,
|
|
278
|
+
expandable,
|
|
279
|
+
isExpanded,
|
|
280
|
+
active: screenActive || targetActive,
|
|
281
|
+
ariaCurrent: screenIsExactMatch || targetActive,
|
|
282
|
+
childNodes: visibleChildNodes,
|
|
283
|
+
// Ein gefilterter Knoten verschwindet nur, wenn weder er selbst noch ein
|
|
284
|
+
// sichtbar gebliebenes Kind matcht — sonst bliebe ein leerer Ordner-Header.
|
|
285
|
+
hidden: filterActive && !matches(node.label) && visibleChildNodes.length === 0,
|
|
286
|
+
providerLoading: node.provider === true && isExpanded && providerChildren === null,
|
|
287
|
+
providerError,
|
|
288
|
+
workspaceId: nav.route?.workspaceId,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// Action icon lookup: registered NAV_ICONS key → Lucide icon, otherwise the
|
|
292
|
+
// raw string as text (provider convention, no boot-fail on an unknown key).
|
|
293
|
+
function ActionGlyph({ icon }) {
|
|
294
|
+
const Icon = Object.hasOwn(NAV_ICONS, icon) ? NAV_ICON_LOOKUP[icon] : undefined;
|
|
295
|
+
if (Icon !== undefined)
|
|
296
|
+
return _jsx(Icon, { "aria-hidden": true, className: "size-3.5" });
|
|
297
|
+
return (_jsx("span", { "aria-hidden": true, className: "text-xs", children: icon }));
|
|
298
|
+
}
|
|
299
|
+
// Boot validates screen XOR target for NavDefinition actions, but a
|
|
300
|
+
// provider-emitted TreeNode reaches here without going through boot — warn
|
|
301
|
+
// once per label instead of rendering a dead button.
|
|
302
|
+
const warnedTreeActionLabels = new Set();
|
|
303
|
+
function warnTreeActionMisconfigured(label, problem) {
|
|
304
|
+
const key = `${label}:${problem}`;
|
|
305
|
+
// skip: already warned for this label and problem — suppresses the repeat, not the warning itself.
|
|
306
|
+
if (warnedTreeActionLabels.has(key))
|
|
307
|
+
return;
|
|
308
|
+
warnedTreeActionLabels.add(key);
|
|
309
|
+
// biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
|
|
310
|
+
console.warn(`[kumiko] Nav action "${label}" ${problem}`);
|
|
311
|
+
}
|
|
312
|
+
// An action button carries screen XOR target (see TreeAction) — `screen`
|
|
313
|
+
// renders a KumikoLink to the route, `target` a dispatch button. Same
|
|
314
|
+
// classes/size either way, only the element changes.
|
|
315
|
+
function TreeActionControl({ action, ariaLabel, className, dispatch, workspaceId, children, }) {
|
|
316
|
+
// Authored actions are typed screen-XOR-target, but schemas can arrive
|
|
317
|
+
// untyped over the wire — read both fields to stay defensive at runtime.
|
|
318
|
+
const loose = action;
|
|
319
|
+
if (loose.screen !== undefined && loose.target !== undefined) {
|
|
320
|
+
warnTreeActionMisconfigured(action.label, 'has both "screen" and "target" set — using "screen", "target" is ignored.');
|
|
321
|
+
}
|
|
322
|
+
if (loose.screen !== undefined) {
|
|
323
|
+
const screenId = lastSegment(loose.screen);
|
|
324
|
+
return (_jsx(KumikoLink, { to: { ...(workspaceId !== undefined && { workspaceId }), screenId }, "aria-label": ariaLabel, className: className, onClick: (e) => e.stopPropagation(), children: children }));
|
|
325
|
+
}
|
|
326
|
+
if (loose.target !== undefined) {
|
|
327
|
+
const target = loose.target;
|
|
328
|
+
return (_jsx("button", { type: "button", "aria-label": ariaLabel, className: className, onClick: (e) => {
|
|
329
|
+
e.stopPropagation();
|
|
330
|
+
dispatch(target);
|
|
331
|
+
}, children: children }));
|
|
332
|
+
}
|
|
333
|
+
warnTreeActionMisconfigured(action.label, 'has neither "screen" nor "target" set — it will not render.');
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
// Hover-actions + the "+" affordance, positioned absolute right.
|
|
337
|
+
// createAction is persistent (a user-visible "+"), the other actions only
|
|
338
|
+
// show on hover. stopPropagation so the row click (toggle/dispatch) doesn't
|
|
339
|
+
// also fire. `expandable` comes from useNavNodeState (s.expandable) instead
|
|
340
|
+
// of being re-derived here — a node without a chevron doesn't need the
|
|
341
|
+
// right-7 gap, otherwise the "+" hangs 28px off the edge.
|
|
342
|
+
function NodeActions({ node, expandable, workspaceId, }) {
|
|
343
|
+
const dispatch = useDispatchTarget();
|
|
344
|
+
const t = useTranslation();
|
|
345
|
+
const create = node.createAction;
|
|
346
|
+
const actions = node.actions ?? [];
|
|
347
|
+
if (create === undefined && actions.length === 0)
|
|
348
|
+
return null;
|
|
349
|
+
const label = (s) => (s.includes(".") ? t(s) : s);
|
|
350
|
+
const btn = "flex size-5 items-center justify-center rounded-md text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-accent-foreground";
|
|
351
|
+
const hover = "opacity-0 group-hover/menu-item:opacity-100 group-hover/menu-sub-item:opacity-100 group-focus-within/menu-item:opacity-100";
|
|
352
|
+
return (_jsxs("div", { className: cn("absolute top-1 flex items-center gap-0.5", expandable ? "right-7" : "right-1"), children: [create !== undefined && (_jsx(TreeActionControl, { action: create, ariaLabel: label(create.label), className: btn, dispatch: dispatch, workspaceId: workspaceId, children: _jsx(Plus, { className: "size-3.5" }) })), actions.map((a) => (_jsx(TreeActionControl, { action: a, ariaLabel: label(a.label), className: cn(btn, hover), dispatch: dispatch, workspaceId: workspaceId, children: _jsx(ActionGlyph, { icon: a.icon }) }, a.label)))] }));
|
|
353
|
+
}
|
|
354
|
+
// Status-Zeilen innerhalb einer Provider-Sub-Liste (lazy-loading / error).
|
|
355
|
+
function ProviderStatus({ loading, error, }) {
|
|
356
|
+
if (error !== null) {
|
|
357
|
+
return _jsx("li", { className: "px-2 py-1 text-xs text-destructive", children: error });
|
|
358
|
+
}
|
|
359
|
+
if (loading) {
|
|
360
|
+
return _jsx("li", { className: "px-2 py-1 text-xs text-sidebar-muted italic", children: "L\u00E4dt \u2026" });
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
// Top-Level-Section: SidebarGroup mit STATISCHEM Label (sidebar-07-Muster —
|
|
365
|
+
// "Platform"/"Projects" sind feste Überschriften, keine Toggles). Collapse
|
|
366
|
+
// gehört auf Items MIT children, nicht auf die Section selbst.
|
|
367
|
+
function NavSection({ node, collapsed, onToggle }) {
|
|
368
|
+
const displayLabel = useLabel(node);
|
|
369
|
+
const { q, matches } = useContext(NavFilterContext);
|
|
370
|
+
// Provider-Kinder (statisch leer bis geladen) halten die Section offen — ihr
|
|
371
|
+
// Match lässt sich hier nicht statisch prüfen; der Knoten selbst self-hidet.
|
|
372
|
+
if (q !== "" &&
|
|
373
|
+
!matches(node.label) &&
|
|
374
|
+
!node.children.some((c) => c.provider === true || subtreeMatches(c, matches))) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: displayLabel }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: node.children.map((child) => (_jsx(NavMenuNode, { node: child, collapsed: collapsed, onToggle: onToggle }, child.qualifiedName))) }) })] }));
|
|
378
|
+
}
|
|
379
|
+
// Ein Menu-Eintrag (Top-Level-Item oder Section-Child). Drei Klick-Modi am
|
|
380
|
+
// EINEN Knoten: `screen` → Route-Link (KumikoLink), `target` → EditorPanel-
|
|
381
|
+
// Dispatch (useDispatchTarget), keins → Container/Provider-Knoten der nur
|
|
382
|
+
// auf-/zuklappt. Provider-Knoten laden ihre Children lazy + SSE-live. Der
|
|
383
|
+
// Collapse-Chevron sitzt als SidebarMenuAction daneben (separater Button,
|
|
384
|
+
// sonst <button> im <a> = invalides HTML); createAction/actions als
|
|
385
|
+
// absolute NodeActions links davon.
|
|
386
|
+
function NavMenuNode({ node, collapsed, onToggle }) {
|
|
387
|
+
const t = useTranslation();
|
|
388
|
+
const dispatch = useDispatchTarget();
|
|
389
|
+
const s = useNavNodeState(node, collapsed);
|
|
390
|
+
if (s.hidden)
|
|
391
|
+
return null;
|
|
392
|
+
const sub = s.isExpanded ? (_jsxs(SidebarMenuSub, { children: [_jsx(ProviderStatus, { loading: s.providerLoading, error: s.providerError }), s.childNodes.map((child) => (_jsx(NavSubNode, { node: child, collapsed: collapsed, onToggle: onToggle }, child.qualifiedName)))] })) : null;
|
|
393
|
+
const chevron = s.expandable ? (_jsx(SidebarMenuAction, { "aria-label": t(s.isExpanded ? "kumiko.nav.collapse" : "kumiko.nav.expand"), "aria-expanded": s.isExpanded, onClick: () => onToggle(node.qualifiedName), children: s.isExpanded ? _jsx(ChevronDown, {}) : _jsx(ChevronRight, {}) })) : null;
|
|
394
|
+
if (node.screen !== undefined) {
|
|
395
|
+
const screenId = lastSegment(node.screen);
|
|
396
|
+
return (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, isActive: s.active, tooltip: s.displayLabel, children: _jsxs(KumikoLink, { to: { ...(s.workspaceId !== undefined && { workspaceId: s.workspaceId }), screenId }, ...(s.ariaCurrent && { "aria-current": "page" }), children: [_jsx(NavLeadingIcon, { node: node, active: s.active, label: s.displayLabel }), _jsx("span", { className: "min-w-0 truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel }), _jsx(NavBadge, { node: node })] }) }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), chevron, sub] }));
|
|
397
|
+
}
|
|
398
|
+
if (node.target !== undefined) {
|
|
399
|
+
const target = node.target;
|
|
400
|
+
return (_jsxs(SidebarMenuItem, { children: [_jsxs(SidebarMenuButton, { isActive: s.active, tooltip: s.displayLabel, onClick: () => dispatch(target), children: [_jsx(NavLeadingIcon, { node: node, active: s.active, label: s.displayLabel }), _jsx("span", { className: "min-w-0 truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel }), _jsx(NavBadge, { node: node })] }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), chevron, sub] }));
|
|
401
|
+
}
|
|
402
|
+
return (_jsxs(SidebarMenuItem, { children: [_jsxs(SidebarMenuButton, { onClick: () => {
|
|
403
|
+
if (s.expandable)
|
|
404
|
+
onToggle(node.qualifiedName);
|
|
405
|
+
}, ...(s.expandable && { "aria-expanded": s.isExpanded }), children: [_jsx(NavLeadingIcon, { node: node, active: false, expanded: s.isExpanded, label: s.displayLabel }), _jsx("span", { className: "truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel }), s.expandable &&
|
|
406
|
+
(s.isExpanded ? (_jsx(ChevronDown, { className: "ml-auto" })) : (_jsx(ChevronRight, { className: "ml-auto" })))] }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), sub] }));
|
|
407
|
+
}
|
|
408
|
+
// Verschachtelte Einträge (innerhalb SidebarMenuSub). Gleiche drei Klick-Modi
|
|
409
|
+
// wie NavMenuNode, nur mit den Sub-Primitives. Folder-/Container-Knoten (kein
|
|
410
|
+
// screen/target) klappen auf — Provider-Children + tiefere Statics rekursiv.
|
|
411
|
+
// Non-Link-Klickziele brauchen asChild+<button> (SidebarMenuSubButton ist ein
|
|
412
|
+
// <a>); der Chevron sitzt als eigener absoluter Button (kein Button-im-<a>).
|
|
413
|
+
function NavSubNode({ node, collapsed, onToggle }) {
|
|
414
|
+
const t = useTranslation();
|
|
415
|
+
const dispatch = useDispatchTarget();
|
|
416
|
+
const s = useNavNodeState(node, collapsed);
|
|
417
|
+
if (s.hidden)
|
|
418
|
+
return null;
|
|
419
|
+
// Kinder in ein eigenes SidebarMenuSub (<ul>) — ein Sub-Item ist ein <li>,
|
|
420
|
+
// verschachtelte <li> ohne <ul> dazwischen sind invalides HTML (nested
|
|
421
|
+
// Content-Ordner: Marketing/ > Block). Der <ul> trägt zugleich die
|
|
422
|
+
// Einrück-/Border-Stufe pro Tiefe.
|
|
423
|
+
const deeper = s.isExpanded ? (_jsxs(SidebarMenuSub, { children: [_jsx(ProviderStatus, { loading: s.providerLoading, error: s.providerError }), s.childNodes.map((child) => (_jsx(NavSubNode, { node: child, collapsed: collapsed, onToggle: onToggle }, child.qualifiedName)))] })) : null;
|
|
424
|
+
const chevron = s.expandable ? (_jsx("button", { type: "button", "aria-label": t(s.isExpanded ? "kumiko.nav.collapse" : "kumiko.nav.expand"), "aria-expanded": s.isExpanded, onClick: (e) => {
|
|
425
|
+
e.stopPropagation();
|
|
426
|
+
onToggle(node.qualifiedName);
|
|
427
|
+
}, className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded-md text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", children: s.isExpanded ? _jsx(ChevronDown, { className: "size-3.5" }) : _jsx(ChevronRight, { className: "size-3.5" }) })) : null;
|
|
428
|
+
if (node.screen !== undefined) {
|
|
429
|
+
const screenId = lastSegment(node.screen);
|
|
430
|
+
return (_jsxs(SidebarMenuSubItem, { children: [_jsx(SidebarMenuSubButton, { asChild: true, isActive: s.active, children: _jsxs(KumikoLink, { to: { ...(s.workspaceId !== undefined && { workspaceId: s.workspaceId }), screenId }, ...(s.ariaCurrent && { "aria-current": "page" }), children: [_jsx(NavLeadingIcon, { node: node, active: s.active, label: s.displayLabel }), _jsx("span", { className: "min-w-0 truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel }), _jsx(NavBadge, { node: node })] }) }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), chevron, deeper] }));
|
|
431
|
+
}
|
|
432
|
+
if (node.target !== undefined) {
|
|
433
|
+
const target = node.target;
|
|
434
|
+
return (_jsxs(SidebarMenuSubItem, { children: [_jsx(SidebarMenuSubButton, { asChild: true, isActive: s.active, children: _jsxs("button", { type: "button", onClick: () => dispatch(target), children: [_jsx(NavLeadingIcon, { node: node, active: s.active, label: s.displayLabel }), _jsx("span", { className: "min-w-0 truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel }), _jsx(NavBadge, { node: node })] }) }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), chevron, deeper] }));
|
|
435
|
+
}
|
|
436
|
+
return (_jsxs(SidebarMenuSubItem, { children: [_jsx(SidebarMenuSubButton, { asChild: true, children: _jsxs("button", { type: "button", onClick: () => {
|
|
437
|
+
if (s.expandable)
|
|
438
|
+
onToggle(node.qualifiedName);
|
|
439
|
+
}, children: [_jsx(NavLeadingIcon, { node: node, active: false, expanded: s.isExpanded, label: s.displayLabel }), _jsx("span", { className: "truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel })] }) }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), chevron, deeper] }));
|
|
440
|
+
}
|
|
441
|
+
// Backwards-kompatibler Single-Feature-Builder. Convenience-Wrapper um
|
|
442
|
+
// buildNavRegistrySliceForApp — bestehende Tests die direkt mit
|
|
443
|
+
// FeatureSchema rufen brauchen keine Änderung.
|
|
444
|
+
export function buildNavRegistrySlice(schema, allowedNavQns) {
|
|
445
|
+
return buildNavRegistrySliceForApp(toAppSchema(schema), allowedNavQns);
|
|
446
|
+
}
|
|
447
|
+
// Multi-Feature-Variante. Iteriert alle Features, qualifiziert pro
|
|
448
|
+
// Feature mit dem eigenen featureName. Reihenfolge: Features in der
|
|
449
|
+
// AppSchema-Ordnung, navs in der vom Feature deklarierten Reihenfolge.
|
|
450
|
+
//
|
|
451
|
+
// Cross-Feature-Workspaces sind hier nativ unterstützt — `navMembers`
|
|
452
|
+
// referenzieren QNs, der Filter trifft die jeweils richtigen Einträge
|
|
453
|
+
// egal in welchem Feature sie deklariert sind.
|
|
454
|
+
export function buildNavRegistrySliceForApp(app, allowedNavQns) {
|
|
455
|
+
const screenAccessByQn = buildScreenAccessMap(app);
|
|
456
|
+
const qualified = [];
|
|
457
|
+
for (const feature of app.features) {
|
|
458
|
+
for (const n of feature.navs ?? []) {
|
|
459
|
+
const screen = n.screen !== undefined ? qualifyScreenId(feature.featureName, n.screen) : undefined;
|
|
460
|
+
// Nav entries without their own `access` inherit the referenced
|
|
461
|
+
// screen's access rule — otherwise a broader workspace/parent access
|
|
462
|
+
// lets a role see a nav entry whose target screen 403s them (#1099).
|
|
463
|
+
// An explicit `n.access` always wins; inheritance only fills the gap.
|
|
464
|
+
const inheritedAccess = n.access ?? (screen !== undefined ? screenAccessByQn.get(screen) : undefined);
|
|
465
|
+
qualified.push({
|
|
466
|
+
...n,
|
|
467
|
+
id: qualifyNavId(feature.featureName, n.id),
|
|
468
|
+
...(n.parent !== undefined && { parent: qualifyNavId(feature.featureName, n.parent) }),
|
|
469
|
+
...(screen !== undefined && { screen }),
|
|
470
|
+
...(inheritedAccess !== undefined && { access: inheritedAccess }),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const existing = dropNavsWithNonexistentParent(qualified);
|
|
475
|
+
// Workspace filter: drop nav entries whose qualified id isn't in the
|
|
476
|
+
// allow-set. A child whose parent gets dropped surfaces as a top-level
|
|
477
|
+
// entry — the workspace owner should list parents explicitly if they
|
|
478
|
+
// want the grouping preserved.
|
|
479
|
+
const filtered = allowedNavQns !== undefined ? existing.filter((n) => allowedNavQns.has(n.id)) : existing;
|
|
480
|
+
const allowedQnSet = new Set(filtered.map((n) => n.id));
|
|
481
|
+
const topLevel = [];
|
|
482
|
+
const byParentMap = new Map();
|
|
483
|
+
for (const nav of filtered) {
|
|
484
|
+
// Treat the entry as top-level when its parent isn't visible — keeps
|
|
485
|
+
// children visible after filtering, even if the workspace omits the
|
|
486
|
+
// parent group from its members list.
|
|
487
|
+
const hasVisibleParent = nav.parent !== undefined && allowedQnSet.has(nav.parent);
|
|
488
|
+
if (hasVisibleParent && nav.parent !== undefined) {
|
|
489
|
+
const list = byParentMap.get(nav.parent) ?? [];
|
|
490
|
+
list.push(nav);
|
|
491
|
+
byParentMap.set(nav.parent, list);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
topLevel.push(nav);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
topLevel,
|
|
499
|
+
byParent: (parent) => byParentMap.get(parent) ?? [],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// A parent absent from the (role-projected) schema is not a workspace
|
|
503
|
+
// filter result: the child must not leak to the top level. Transitive.
|
|
504
|
+
// Parents removed by an allowlist (filterAppSchemaNavsByAllowlist) are not
|
|
505
|
+
// absent here: that helper already lifted their children to the top level.
|
|
506
|
+
function dropNavsWithNonexistentParent(navs) {
|
|
507
|
+
let current = [...navs];
|
|
508
|
+
for (;;) {
|
|
509
|
+
const ids = new Set(current.map((n) => n.id));
|
|
510
|
+
const kept = current.filter((n) => n.parent === undefined || ids.has(n.parent));
|
|
511
|
+
if (kept.length === current.length)
|
|
512
|
+
return current;
|
|
513
|
+
current = kept;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Exported (not part of the package's public index.ts surface) so other
|
|
517
|
+
// modules under layout/ — e.g. filter-app-schema-navs.ts — qualify nav ids
|
|
518
|
+
// the same way instead of keeping a second copy in sync.
|
|
519
|
+
export function qualifyNavId(feature, id) {
|
|
520
|
+
return id.includes(":nav:") ? id : `${feature}:nav:${id}`;
|
|
521
|
+
}
|
|
522
|
+
function qualifyScreenId(feature, id) {
|
|
523
|
+
return id.includes(":screen:") ? id : `${feature}:screen:${id}`;
|
|
524
|
+
}
|
|
525
|
+
function buildScreenAccessMap(app) {
|
|
526
|
+
const map = new Map();
|
|
527
|
+
for (const feature of app.features) {
|
|
528
|
+
for (const s of feature.screens) {
|
|
529
|
+
map.set(qualifyScreenId(feature.featureName, s.id), s.access);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return map;
|
|
533
|
+
}
|
|
534
|
+
// `lastSegment` lebt jetzt in @cosmicdrift/kumiko-renderer (./app/qn) — eine
|
|
535
|
+
// Quelle, beide Pakete teilen sie.
|
|
536
|
+
export { lastSegment };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type ActionMenuItemSpec } from "../../index.ts";
|
|
3
|
+
type SlotElement = HTMLElement | null;
|
|
4
|
+
export type PageHeaderSlot = {
|
|
5
|
+
readonly statusElement: SlotElement;
|
|
6
|
+
readonly actionsElement: SlotElement;
|
|
7
|
+
readonly setStatusElement: (node: SlotElement) => void;
|
|
8
|
+
readonly setActionsElement: (node: SlotElement) => void;
|
|
9
|
+
readonly title: string | undefined;
|
|
10
|
+
readonly setTitle: (title: string | undefined) => void;
|
|
11
|
+
readonly recordTitle: string | undefined;
|
|
12
|
+
readonly setRecordTitle: (recordTitle: string | undefined) => void;
|
|
13
|
+
readonly breadcrumbHidden: boolean;
|
|
14
|
+
readonly setBreadcrumbHidden: (hidden: boolean) => void;
|
|
15
|
+
readonly breadcrumbScreenId: string | undefined;
|
|
16
|
+
readonly setBreadcrumbScreenId: (screenId: string | undefined) => void;
|
|
17
|
+
readonly overflowItems: readonly ActionMenuItemSpec[] | undefined;
|
|
18
|
+
readonly setOverflowItems: (items: readonly ActionMenuItemSpec[] | undefined) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function usePageHeaderSlot(): PageHeaderSlot | null;
|
|
21
|
+
/** Shells wrap ShellHeader and the content area in it. */
|
|
22
|
+
export declare function PageHeaderSlotProvider({ children }: {
|
|
23
|
+
readonly children: ReactNode;
|
|
24
|
+
}): ReactNode;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Two portal targets (status, actions) plus a title override and a record title inside the
|
|
3
|
+
// ShellHeader, filled by the PageHeader primitive of whatever screen renders
|
|
4
|
+
// below it.
|
|
5
|
+
//
|
|
6
|
+
// Why a slot: the header sits above the content and is owned by the shell; a
|
|
7
|
+
// screen can only reach it through a portal. Same inversion as SidebarPanel.
|
|
8
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
9
|
+
import { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, } from "../../index.js";
|
|
10
|
+
import { useIsNarrowViewport } from "../primitives/use-narrow-viewport.js";
|
|
11
|
+
const PageHeaderSlotContext = createContext(null);
|
|
12
|
+
export function usePageHeaderSlot() {
|
|
13
|
+
return useContext(PageHeaderSlotContext);
|
|
14
|
+
}
|
|
15
|
+
/** Shells wrap ShellHeader and the content area in it. */
|
|
16
|
+
export function PageHeaderSlotProvider({ children }) {
|
|
17
|
+
const [statusElement, setStatusElement] = useState(null);
|
|
18
|
+
const [actionsElement, setActionsElement] = useState(null);
|
|
19
|
+
const [title, setTitle] = useState(undefined);
|
|
20
|
+
const [recordTitle, setRecordTitle] = useState(undefined);
|
|
21
|
+
const [breadcrumbHidden, setBreadcrumbHidden] = useState(false);
|
|
22
|
+
const [breadcrumbScreenId, setBreadcrumbScreenId] = useState(undefined);
|
|
23
|
+
const [overflowItems, setOverflowItems] = useState(undefined);
|
|
24
|
+
const compact = useIsNarrowViewport();
|
|
25
|
+
const value = useMemo(() => ({
|
|
26
|
+
statusElement,
|
|
27
|
+
actionsElement,
|
|
28
|
+
setStatusElement,
|
|
29
|
+
setActionsElement,
|
|
30
|
+
title,
|
|
31
|
+
setTitle,
|
|
32
|
+
recordTitle,
|
|
33
|
+
setRecordTitle,
|
|
34
|
+
breadcrumbHidden,
|
|
35
|
+
setBreadcrumbHidden,
|
|
36
|
+
breadcrumbScreenId,
|
|
37
|
+
setBreadcrumbScreenId,
|
|
38
|
+
overflowItems,
|
|
39
|
+
setOverflowItems,
|
|
40
|
+
}), [
|
|
41
|
+
statusElement,
|
|
42
|
+
actionsElement,
|
|
43
|
+
title,
|
|
44
|
+
recordTitle,
|
|
45
|
+
breadcrumbHidden,
|
|
46
|
+
breadcrumbScreenId,
|
|
47
|
+
overflowItems,
|
|
48
|
+
]);
|
|
49
|
+
return (_jsx(PageHeaderSlotContext.Provider, { value: value, children: _jsx(PageHeaderSlotAvailableProvider, { value: true, children: _jsx(PageHeaderCompactProvider, { value: compact, children: children }) }) }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type MenuItemDef } from "../primitives/action-menu.tsx";
|
|
3
|
+
export type ProfileMenuItem = MenuItemDef;
|
|
4
|
+
export type ProfileMenuProps = {
|
|
5
|
+
readonly user: {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
/** Display-Name oder Email — Quelle für Initials. */
|
|
8
|
+
readonly label: string;
|
|
9
|
+
};
|
|
10
|
+
readonly items: readonly ProfileMenuItem[];
|
|
11
|
+
readonly testId?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function ProfileMenu({ user, items, testId }: ProfileMenuProps): ReactNode;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ActionMenu } from "../primitives/action-menu.js";
|
|
3
|
+
import { Avatar } from "./avatar.js";
|
|
4
|
+
export function ProfileMenu({ user, items, testId }) {
|
|
5
|
+
return (_jsx(ActionMenu, { trigger: _jsx(Avatar, { id: user.id, label: user.label, size: "md" }), triggerLabel: `Open ${user.label} menu`, items: items, align: "end", testId: testId ?? "profile-menu-trigger" }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
export type BreadcrumbCrumb = {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly screenId?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function screenTitleKey(screenShortId: string): string;
|
|
7
|
+
/** The short id of {screenId}'s parent list screen, resolved via the same
|
|
8
|
+
* logic as `resolveDetailBreadcrumb` — `undefined` when nothing resolves
|
|
9
|
+
* (unknown screen, or no explicit/heuristic parent). */
|
|
10
|
+
export declare function resolveParentScreenId(screens: readonly ScreenDefinition[], screenId: string): string | undefined;
|
|
11
|
+
export declare function resolveDetailBreadcrumb(screens: readonly ScreenDefinition[], detailScreenId: string, t: (key: string) => string): readonly BreadcrumbCrumb[] | undefined;
|