@cosmicdrift/kumiko-renderer 0.374.0 → 0.378.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 +9 -2
- package/dist/app/action-form-support.js +24 -2
- package/dist/app/client-actions.d.ts +2 -2
- 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/form-schema.d.ts +1 -1
- package/dist/app/form-schema.js +2 -2
- package/dist/app/kumiko-screen.d.ts +1 -1
- package/dist/app/kumiko-screen.js +10 -7
- package/dist/app/list-empty-content.d.ts +1 -1
- 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 +1 -1
- 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 +1 -1
- 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/form-derivation-patch.d.ts +8 -0
- package/dist/components/form-derivation-patch.js +24 -0
- package/dist/components/form-screen-layout.d.ts +13 -0
- package/dist/components/form-screen-layout.js +13 -0
- package/dist/components/grid-cell-for-field.d.ts +1 -1
- 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/render-edit-logic.d.ts +1 -1
- package/dist/components/render-edit-types.d.ts +6 -3
- package/dist/components/render-edit.d.ts +1 -1
- package/dist/components/render-edit.js +18 -3
- package/dist/components/render-field.d.ts +1 -1
- package/dist/components/render-field.js +12 -2
- 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/file-ids-of.d.ts +1 -0
- package/dist/file-ids-of.js +7 -0
- package/dist/flow-diagram/icons.d.ts +3 -0
- package/dist/flow-diagram/icons.js +14 -0
- package/dist/flow-diagram/index.d.ts +2 -0
- package/dist/flow-diagram/index.js +1 -0
- package/dist/flow-diagram/render.d.ts +3 -0
- package/dist/flow-diagram/render.js +234 -0
- package/dist/flow-diagram/types.d.ts +18 -0
- package/dist/flow-diagram/types.js +1 -0
- 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 +21 -0
- package/dist/headless/view-model/edit.js +512 -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 +6 -0
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -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 +45 -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 +31 -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/file-value-list.d.ts +3 -0
- package/dist/web/primitives/file-value-list.js +22 -0
- package/dist/web/primitives/index.d.ts +34 -0
- package/dist/web/primitives/index.js +2023 -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/bounded-load-cache.d.ts +1 -0
- package/dist/web/widgets/bounded-load-cache.js +25 -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 +177 -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 +28 -0
- package/dist/web/widgets/index.js +31 -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/pdf-preview.d.ts +10 -0
- package/dist/web/widgets/pdf-preview.js +140 -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 +114 -7
- package/src/changes.json +92 -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 +405 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { entityFromQueryType, useDispatcher, useLiveEvents, useTranslation } from "../../index.js";
|
|
4
|
+
import { EmptyState, ErrorState, LoadingState } from "./states.js";
|
|
5
|
+
/** Cursor-paginated scroll list (mail inbox, activity feeds) — loads the
|
|
6
|
+
* next page when the end sentinel becomes visible (IntersectionObserver),
|
|
7
|
+
* instead of a pager bar like QueryTable/entityList. The query handler
|
|
8
|
+
* receives `{ ...payload, limit, cursor? }` and returns rows plus the next
|
|
9
|
+
* cursor (same cursor convention as the audit-log screen). */
|
|
10
|
+
export function InfinityList({ query, payload, pageSize = 50, rows, nextCursor, rowId, renderRow, emptyState, className, testId, live = false, selectedId, onSelectionChange, }) {
|
|
11
|
+
const dispatcher = useDispatcher();
|
|
12
|
+
const t = useTranslation();
|
|
13
|
+
const subscribeLive = useLiveEvents();
|
|
14
|
+
const [state, setState] = useState({ kind: "loading" });
|
|
15
|
+
const sentinelRef = useRef(null);
|
|
16
|
+
const payloadKey = JSON.stringify(payload ?? {});
|
|
17
|
+
// rows/nextCursor/rowId are fresh closures on every caller render
|
|
18
|
+
// (inline arrow props). As useCallback deps that would recreate
|
|
19
|
+
// `load`/`refreshFirstPage` every render → the effects below would
|
|
20
|
+
// refetch in a loop. Refs keep them stable while always reading the
|
|
21
|
+
// current selector.
|
|
22
|
+
const rowsRef = useRef(rows);
|
|
23
|
+
rowsRef.current = rows;
|
|
24
|
+
const nextCursorRef = useRef(nextCursor);
|
|
25
|
+
nextCursorRef.current = nextCursor;
|
|
26
|
+
const rowIdRef = useRef(rowId);
|
|
27
|
+
rowIdRef.current = rowId;
|
|
28
|
+
// Discards a response whose request was superseded by a newer one before
|
|
29
|
+
// it resolved (e.g. two searches fired in quick succession) — without
|
|
30
|
+
// this, a slow earlier response can land after a faster later one and
|
|
31
|
+
// overwrite it, leaving stale rows on screen (fw#1705).
|
|
32
|
+
const requestSeq = useRef(0);
|
|
33
|
+
// IDs of the most recently loaded first page. refreshFirstPage() needs
|
|
34
|
+
// this to tell "row dropped from page 1 elsewhere" (must be pruned) apart
|
|
35
|
+
// from "row belongs to an already-accumulated later page" (must survive)
|
|
36
|
+
// — both are simply absent from the fresh page's id set.
|
|
37
|
+
const firstPageIdsRef = useRef(new Set());
|
|
38
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
|
|
39
|
+
const load = useCallback(async (cursor) => {
|
|
40
|
+
const mySeq = ++requestSeq.current;
|
|
41
|
+
const res = await dispatcher.query(query, {
|
|
42
|
+
...payload,
|
|
43
|
+
limit: pageSize,
|
|
44
|
+
...(cursor !== null && { cursor }),
|
|
45
|
+
});
|
|
46
|
+
if (mySeq !== requestSeq.current)
|
|
47
|
+
return;
|
|
48
|
+
if (!res.isSuccess) {
|
|
49
|
+
setState({ kind: "error", error: res.error });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const nextRows = rowsRef.current(res.data);
|
|
53
|
+
if (cursor === null) {
|
|
54
|
+
// Same identity as refreshFirstPage's freshIds/staleRows diff below
|
|
55
|
+
// — this ref is what that diff's previousFirstPageIds reads, so it
|
|
56
|
+
firstPageIdsRef.current = new Set(nextRows.map((row, index) => rowIdRef.current(row, index)));
|
|
57
|
+
}
|
|
58
|
+
setState((prev) => {
|
|
59
|
+
if (cursor === null || prev.kind !== "ready") {
|
|
60
|
+
return { kind: "ready", rows: nextRows, cursor: nextCursorRef.current(res.data) };
|
|
61
|
+
}
|
|
62
|
+
// The live-merge above can re-sort a row to the front of page 1; an
|
|
63
|
+
// offset-based cursor then re-serves that same row in a later page.
|
|
64
|
+
// Dedupe on append so it doesn't land twice under duplicate React
|
|
65
|
+
// keys.
|
|
66
|
+
const existingIds = new Set(prev.rows.map((row, index) => rowIdRef.current(row, index)));
|
|
67
|
+
const newRows = nextRows.filter((row, index) => !existingIds.has(rowIdRef.current(row, index)));
|
|
68
|
+
return {
|
|
69
|
+
kind: "ready",
|
|
70
|
+
rows: [...prev.rows, ...newRows],
|
|
71
|
+
cursor: nextCursorRef.current(res.data),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}, [dispatcher, query, pageSize, payloadKey]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
setState({ kind: "loading" });
|
|
77
|
+
void load(null);
|
|
78
|
+
return () => {
|
|
79
|
+
requestSeq.current += 1;
|
|
80
|
+
};
|
|
81
|
+
}, [load]);
|
|
82
|
+
// Live-mode: on an SSE event for the query's entity, refetch only the
|
|
83
|
+
// first page and merge it in — rows the fresh page still contains move
|
|
84
|
+
// to the front (newest-first feeds), rows it dropped (edited/deleted
|
|
85
|
+
// elsewhere) are pruned, and everything beyond page 1 stays untouched.
|
|
86
|
+
// A full reload would collapse already-accumulated pages and jump the
|
|
87
|
+
// scroll position; see fw#1827.
|
|
88
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
|
|
89
|
+
const refreshFirstPage = useCallback(async () => {
|
|
90
|
+
// Read, don't bump: a concurrent load() (e.g. the mount fetch still
|
|
91
|
+
// in flight) must still land. Bumping here would make load()'s own
|
|
92
|
+
// sequence check discard it, and the refresh below then bails on
|
|
93
|
+
// `prev.kind !== "ready"` — the list gets stuck in loading forever.
|
|
94
|
+
const seqAtStart = requestSeq.current;
|
|
95
|
+
const res = await dispatcher.query(query, { ...payload, limit: pageSize });
|
|
96
|
+
if (seqAtStart !== requestSeq.current)
|
|
97
|
+
return;
|
|
98
|
+
// skip: background live refresh failed, keep showing the current rows
|
|
99
|
+
if (!res.isSuccess)
|
|
100
|
+
return;
|
|
101
|
+
const freshRows = rowsRef.current(res.data);
|
|
102
|
+
const freshIds = new Set(freshRows.map((row, index) => rowIdRef.current(row, index)));
|
|
103
|
+
// A row absent from freshIds is stale for one of two reasons: it was
|
|
104
|
+
// dropped from page 1 (deleted, or filtered out elsewhere) and must be
|
|
105
|
+
// pruned, or it belongs to an already-accumulated later page and must
|
|
106
|
+
// survive. previousFirstPageIds — captured before it's overwritten below
|
|
107
|
+
// — is what tells those two cases apart.
|
|
108
|
+
const previousFirstPageIds = firstPageIdsRef.current;
|
|
109
|
+
firstPageIdsRef.current = freshIds;
|
|
110
|
+
setState((prev) => {
|
|
111
|
+
// skip: the initial load() is still in flight and owns the eventual
|
|
112
|
+
// state — a concurrent refresh landing here has nothing accumulated
|
|
113
|
+
// to merge into yet.
|
|
114
|
+
if (prev.kind === "loading")
|
|
115
|
+
return prev;
|
|
116
|
+
if (prev.kind !== "ready") {
|
|
117
|
+
// prev.kind === "error": this refresh succeeded — promote straight
|
|
118
|
+
// to "ready" instead of leaving the list stuck on the earlier
|
|
119
|
+
// failure forever (a live event should be able to recover it).
|
|
120
|
+
return { kind: "ready", rows: freshRows, cursor: nextCursorRef.current(res.data) };
|
|
121
|
+
}
|
|
122
|
+
const staleRows = prev.rows.filter((row, index) => {
|
|
123
|
+
const id = rowIdRef.current(row, index);
|
|
124
|
+
return !freshIds.has(id) && !previousFirstPageIds.has(id);
|
|
125
|
+
});
|
|
126
|
+
return { kind: "ready", rows: [...freshRows, ...staleRows], cursor: prev.cursor };
|
|
127
|
+
});
|
|
128
|
+
}, [dispatcher, query, pageSize, payloadKey]);
|
|
129
|
+
// Trailing debounce + in-flight coalesce: bulk SSE storms must not fan out
|
|
130
|
+
// into one query per event (fw#1829). A refresh already running schedules
|
|
131
|
+
// exactly one trailing pass after it settles.
|
|
132
|
+
const liveTimerRef = useRef(null);
|
|
133
|
+
const liveInFlightRef = useRef(false);
|
|
134
|
+
const liveTrailingRef = useRef(false);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
// skip: live mode off, no SSE subscription needed
|
|
137
|
+
if (!live)
|
|
138
|
+
return;
|
|
139
|
+
const entity = entityFromQueryType(query);
|
|
140
|
+
// skip: query type has no mapped entity, nothing to subscribe to
|
|
141
|
+
if (entity === undefined)
|
|
142
|
+
return;
|
|
143
|
+
const runRefresh = () => {
|
|
144
|
+
liveInFlightRef.current = true;
|
|
145
|
+
void refreshFirstPage().finally(() => {
|
|
146
|
+
liveInFlightRef.current = false;
|
|
147
|
+
if (liveTrailingRef.current) {
|
|
148
|
+
liveTrailingRef.current = false;
|
|
149
|
+
runRefresh();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const scheduleRefresh = () => {
|
|
154
|
+
if (liveTimerRef.current !== null)
|
|
155
|
+
clearTimeout(liveTimerRef.current);
|
|
156
|
+
liveTimerRef.current = setTimeout(() => {
|
|
157
|
+
liveTimerRef.current = null;
|
|
158
|
+
if (liveInFlightRef.current) {
|
|
159
|
+
liveTrailingRef.current = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
runRefresh();
|
|
163
|
+
}, 250);
|
|
164
|
+
};
|
|
165
|
+
const unsubscribe = subscribeLive(entity, scheduleRefresh);
|
|
166
|
+
return () => {
|
|
167
|
+
unsubscribe();
|
|
168
|
+
if (liveTimerRef.current !== null) {
|
|
169
|
+
clearTimeout(liveTimerRef.current);
|
|
170
|
+
liveTimerRef.current = null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}, [live, query, refreshFirstPage, subscribeLive]);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const sentinel = sentinelRef.current;
|
|
176
|
+
// skip: no further page or not ready yet — observer not needed
|
|
177
|
+
if (sentinel === null || state.kind !== "ready" || state.cursor === null)
|
|
178
|
+
return;
|
|
179
|
+
const cursor = state.cursor;
|
|
180
|
+
const observer = new IntersectionObserver((entries) => {
|
|
181
|
+
if (entries[0]?.isIntersecting === true)
|
|
182
|
+
void load(cursor);
|
|
183
|
+
});
|
|
184
|
+
observer.observe(sentinel);
|
|
185
|
+
return () => observer.disconnect();
|
|
186
|
+
}, [state, load]);
|
|
187
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
188
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (selectedId === undefined || selectedId === null || state.kind !== "ready")
|
|
191
|
+
return;
|
|
192
|
+
const ids = state.rows.map((row, index) => rowIdRef.current(row, index));
|
|
193
|
+
const index = ids.indexOf(selectedId);
|
|
194
|
+
if (index === -1)
|
|
195
|
+
return;
|
|
196
|
+
onSelectionChangeRef.current?.({
|
|
197
|
+
index,
|
|
198
|
+
total: ids.length,
|
|
199
|
+
prevId: ids[index - 1] ?? null,
|
|
200
|
+
nextId: ids[index + 1] ?? null,
|
|
201
|
+
hasMore: state.cursor !== null,
|
|
202
|
+
});
|
|
203
|
+
}, [state, selectedId]);
|
|
204
|
+
if (state.kind === "loading")
|
|
205
|
+
return _jsx(LoadingState, { rows: 4, testId: testId });
|
|
206
|
+
if (state.kind === "error")
|
|
207
|
+
return _jsx(ErrorState, { error: state.error, onRetry: () => void load(null), testId: testId });
|
|
208
|
+
if (state.rows.length === 0)
|
|
209
|
+
return _jsx(_Fragment, { children: emptyState ?? _jsx(EmptyState, { title: t("kumiko.list.no-entries"), testId: testId }) });
|
|
210
|
+
return (_jsxs("div", { "data-testid": testId, className: className ?? "flex flex-col overflow-y-auto", children: [state.rows.map((row, index) => (_jsx("div", { children: renderRow(row) }, rowId(row, index)))), _jsx("div", { ref: sentinelRef, className: "h-px" })] }));
|
|
211
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ModeSwitchVariant = "outline" | "pill";
|
|
3
|
+
/** Segmented-Control für sich ausschließende Modi — die prominente
|
|
4
|
+
* Alternative zum vergrabenen <select>. */
|
|
5
|
+
export declare function ModeSwitch<T extends string>({ value, options, onChange, ariaLabel, variant, className, testId, }: {
|
|
6
|
+
/** `null`: no option active. */
|
|
7
|
+
readonly value: T | null;
|
|
8
|
+
readonly options: readonly {
|
|
9
|
+
readonly value: T;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
/** Muted tabular counter after the label. */
|
|
12
|
+
readonly count?: number;
|
|
13
|
+
}[];
|
|
14
|
+
readonly onChange: (value: T) => void;
|
|
15
|
+
readonly ariaLabel?: string;
|
|
16
|
+
/** `outline` (default): bordered segments, active tinted. `pill`: grey
|
|
17
|
+
* track with a raised active segment. */
|
|
18
|
+
readonly variant?: ModeSwitchVariant;
|
|
19
|
+
readonly className?: string;
|
|
20
|
+
readonly testId?: string;
|
|
21
|
+
}): ReactNode;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
/** Segmented-Control für sich ausschließende Modi — die prominente
|
|
4
|
+
* Alternative zum vergrabenen <select>. */
|
|
5
|
+
export function ModeSwitch({ value, options, onChange, ariaLabel, variant = "outline", className, testId, }) {
|
|
6
|
+
const pill = variant === "pill";
|
|
7
|
+
return (_jsx("div", { "data-testid": testId, role: "group", "aria-label": ariaLabel, className: cn("flex min-h-8", pill
|
|
8
|
+
? "gap-0.5 rounded-lg bg-muted p-0.5"
|
|
9
|
+
: "overflow-hidden rounded-md border border-input bg-background", className), children: options.map((o) => {
|
|
10
|
+
const active = o.value === value;
|
|
11
|
+
return (_jsxs("button", { type: "button", "aria-pressed": active, onClick: () => onChange(o.value), className: cn("grow px-3 text-sm transition-colors", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", pill
|
|
12
|
+
? [
|
|
13
|
+
"rounded-md",
|
|
14
|
+
active
|
|
15
|
+
? "bg-card font-semibold text-foreground shadow-sm dark:bg-foreground/10"
|
|
16
|
+
: "text-muted-foreground hover:text-foreground",
|
|
17
|
+
]
|
|
18
|
+
: [
|
|
19
|
+
"border-l border-input first:border-l-0",
|
|
20
|
+
active
|
|
21
|
+
? "bg-primary/10 font-semibold text-primary"
|
|
22
|
+
: "text-foreground hover:bg-muted",
|
|
23
|
+
]), children: [o.label, o.count !== undefined && (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-count-${o.value}` : undefined, className: cn("ml-1.5 font-normal tabular-nums", !active && "text-muted-foreground"), children: o.count }))] }, o.value));
|
|
24
|
+
}) }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type PdfPreviewSource = {
|
|
3
|
+
readonly base64: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly url: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function PdfPreview({ source, testId, }: {
|
|
8
|
+
readonly source: PdfPreviewSource;
|
|
9
|
+
readonly testId?: string;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, use, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useTranslation } from "../../i18n.js";
|
|
4
|
+
import { createBoundedLoadCache } from "./bounded-load-cache.js";
|
|
5
|
+
// pdf.js runs in the main thread ("fake worker"): the worker module is bundled
|
|
6
|
+
// with the app and handed over through globalThis.pdfjsWorker, so no worker or
|
|
7
|
+
// CDN URL is ever fetched (self-hosting and CSP stay intact).
|
|
8
|
+
const RENDER_SCALE = 96 / 72;
|
|
9
|
+
const MAX_CACHED_DOCUMENTS = 4;
|
|
10
|
+
const cachedDocument = createBoundedLoadCache(MAX_CACHED_DOCUMENTS, (loaded) => {
|
|
11
|
+
if (loaded.ok)
|
|
12
|
+
void loaded.loadingTask.destroy();
|
|
13
|
+
});
|
|
14
|
+
function decodeBase64(base64) {
|
|
15
|
+
const binary = atob(base64);
|
|
16
|
+
const bytes = new Uint8Array(binary.length);
|
|
17
|
+
for (let index = 0; index < binary.length; index++)
|
|
18
|
+
bytes[index] = binary.charCodeAt(index);
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
async function loadBytes(source) {
|
|
22
|
+
if ("base64" in source)
|
|
23
|
+
return decodeBase64(source.base64);
|
|
24
|
+
const res = await fetch(source.url, { credentials: "same-origin" });
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
throw new Error(`pdf fetch failed (${res.status})`);
|
|
27
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
28
|
+
}
|
|
29
|
+
async function loadDocument(source) {
|
|
30
|
+
try {
|
|
31
|
+
const [pdfjs, worker, data] = await Promise.all([
|
|
32
|
+
import("pdfjs-dist"),
|
|
33
|
+
import("pdfjs-dist/build/pdf.worker.min.mjs"),
|
|
34
|
+
loadBytes(source),
|
|
35
|
+
]);
|
|
36
|
+
Object.assign(globalThis, { pdfjsWorker: worker });
|
|
37
|
+
const loadingTask = pdfjs.getDocument({ data });
|
|
38
|
+
return { ok: true, document: await loadingTask.promise, loadingTask };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The promise must be stable across renders for use().
|
|
45
|
+
function documentFor(source) {
|
|
46
|
+
const key = "base64" in source ? `base64:${source.base64}` : `url:${source.url}`;
|
|
47
|
+
return cachedDocument(key, () => loadDocument(source));
|
|
48
|
+
}
|
|
49
|
+
function PageView({ document, pageNumber, }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
const page = use(pageFor(document, pageNumber));
|
|
52
|
+
// Stable identity: the mount callbacks below depend on it, and a new viewport per render re-mounts canvas and text layer.
|
|
53
|
+
const viewport = useMemo(() => page.getViewport({ scale: RENDER_SCALE }), [page]);
|
|
54
|
+
const [availableWidth, setAvailableWidth] = useState(null);
|
|
55
|
+
const measureFrame = useCallback((frame) => {
|
|
56
|
+
if (frame === null)
|
|
57
|
+
return;
|
|
58
|
+
const observer = new ResizeObserver(() => setAvailableWidth(frame.clientWidth));
|
|
59
|
+
observer.observe(frame);
|
|
60
|
+
setAvailableWidth(frame.clientWidth);
|
|
61
|
+
return () => observer.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
// The sheet keeps its natural pixel size and is scaled as a whole, so canvas and text layer stay aligned.
|
|
64
|
+
const fitScale = availableWidth === null || availableWidth <= 0
|
|
65
|
+
? 1
|
|
66
|
+
: Math.min(1, availableWidth / viewport.width);
|
|
67
|
+
const mountCanvas = useCallback((canvas) => {
|
|
68
|
+
if (canvas === null)
|
|
69
|
+
return;
|
|
70
|
+
const pixelRatio = globalThis.devicePixelRatio || 1;
|
|
71
|
+
canvas.width = Math.floor(viewport.width * pixelRatio);
|
|
72
|
+
canvas.height = Math.floor(viewport.height * pixelRatio);
|
|
73
|
+
const task = page.render({
|
|
74
|
+
canvas,
|
|
75
|
+
viewport,
|
|
76
|
+
transform: pixelRatio === 1 ? undefined : [pixelRatio, 0, 0, pixelRatio, 0, 0],
|
|
77
|
+
});
|
|
78
|
+
task.promise.catch(() => undefined);
|
|
79
|
+
return () => task.cancel();
|
|
80
|
+
}, [page, viewport]);
|
|
81
|
+
const mountTextLayer = useCallback((container) => {
|
|
82
|
+
if (container === null)
|
|
83
|
+
return;
|
|
84
|
+
container.style.setProperty("--total-scale-factor", String(RENDER_SCALE));
|
|
85
|
+
container.style.setProperty("--scale-factor", String(RENDER_SCALE));
|
|
86
|
+
let layer = null;
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
void import("pdfjs-dist").then(({ TextLayer }) => {
|
|
89
|
+
// skip: cleanup ran before pdfjs finished loading
|
|
90
|
+
if (cancelled)
|
|
91
|
+
return undefined;
|
|
92
|
+
// TextLayer.cancel() keeps rendered spans; a re-mount must not append a second layer.
|
|
93
|
+
container.replaceChildren();
|
|
94
|
+
const textLayer = new TextLayer({
|
|
95
|
+
textContentSource: page.streamTextContent(),
|
|
96
|
+
container,
|
|
97
|
+
viewport,
|
|
98
|
+
});
|
|
99
|
+
layer = textLayer;
|
|
100
|
+
return textLayer.render().catch(() => undefined);
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
layer?.cancel();
|
|
105
|
+
};
|
|
106
|
+
}, [page, viewport]);
|
|
107
|
+
return (_jsxs("figure", { className: "m-0 flex w-full flex-col items-center gap-2 rounded-lg bg-muted p-4", "data-testid": `pdf-page-${pageNumber}`, children: [_jsx("div", { ref: measureFrame, className: "w-full", children: _jsx("div", { className: "mx-auto", style: { width: viewport.width * fitScale, height: viewport.height * fitScale }, children: _jsxs("div", { className: "relative border border-border bg-white shadow-md", style: {
|
|
108
|
+
width: viewport.width,
|
|
109
|
+
height: viewport.height,
|
|
110
|
+
transform: `scale(${fitScale})`,
|
|
111
|
+
transformOrigin: "top left",
|
|
112
|
+
}, children: [_jsx("canvas", { ref: mountCanvas, style: { width: viewport.width, height: viewport.height }, className: "block" }), _jsx("div", { ref: mountTextLayer, className: "absolute inset-0 overflow-hidden leading-none [&_span]:absolute [&_span]:origin-top-left [&_span]:cursor-text [&_span]:whitespace-pre [&_span]:text-transparent [&_span::selection]:bg-primary/30" })] }) }) }), _jsx("figcaption", { className: "text-xs text-muted-foreground", children: t("kumiko.widget.pdf-preview.pageOf", { page: pageNumber, pages: document.numPages }) })] }));
|
|
113
|
+
}
|
|
114
|
+
const pageCache = new WeakMap();
|
|
115
|
+
function pageFor(document, pageNumber) {
|
|
116
|
+
let pages = pageCache.get(document);
|
|
117
|
+
if (pages === undefined) {
|
|
118
|
+
pages = new Map();
|
|
119
|
+
pageCache.set(document, pages);
|
|
120
|
+
}
|
|
121
|
+
let page = pages.get(pageNumber);
|
|
122
|
+
if (page === undefined) {
|
|
123
|
+
page = document.getPage(pageNumber);
|
|
124
|
+
pages.set(pageNumber, page);
|
|
125
|
+
}
|
|
126
|
+
return page;
|
|
127
|
+
}
|
|
128
|
+
function Pages({ source }) {
|
|
129
|
+
const t = useTranslation();
|
|
130
|
+
const loaded = use(documentFor(source));
|
|
131
|
+
if (!loaded.ok) {
|
|
132
|
+
return _jsx("p", { className: "text-sm text-status-bad", children: t("kumiko.widget.pdf-preview.renderFailed") });
|
|
133
|
+
}
|
|
134
|
+
const pageNumbers = Array.from({ length: loaded.document.numPages }, (_, index) => index + 1);
|
|
135
|
+
return (_jsx(_Fragment, { children: pageNumbers.map((pageNumber) => (_jsx(Suspense, { fallback: null, children: _jsx(PageView, { document: loaded.document, pageNumber: pageNumber }) }, pageNumber))) }));
|
|
136
|
+
}
|
|
137
|
+
export function PdfPreview({ source, testId = "pdf-preview", }) {
|
|
138
|
+
const t = useTranslation();
|
|
139
|
+
return (_jsx("div", { className: "flex flex-col items-stretch gap-4 overflow-auto", "data-testid": testId, children: _jsx(Suspense, { fallback: _jsx("p", { className: "text-sm text-muted-foreground", children: t("kumiko.widget.pdf-preview.loading") }), children: _jsx(Pages, { source: source }) }) }));
|
|
140
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type PhotoSlotSpec = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
/** The shot this slot asks for ("Front, angled") — the empty slot's
|
|
5
|
+
* caption and the filled thumbnail's alt text. */
|
|
6
|
+
readonly label: string;
|
|
7
|
+
/** Set once the app holds the photo; the slot then shows the thumbnail. */
|
|
8
|
+
readonly previewUrl?: string;
|
|
9
|
+
/** Corner tag on a filled slot, e.g. "Cover photo". */
|
|
10
|
+
readonly badge?: string;
|
|
11
|
+
};
|
|
12
|
+
export type PhotoSlotsProps = {
|
|
13
|
+
readonly slots: readonly PhotoSlotSpec[];
|
|
14
|
+
/** Stores the photo for `slotId`. Throws on failure — the message is shown
|
|
15
|
+
* under the slot. The app then passes the slot's `previewUrl`. */
|
|
16
|
+
readonly onUpload: (slotId: string, file: File) => Promise<void>;
|
|
17
|
+
/** Second line on an empty slot, e.g. "Camera or gallery". */
|
|
18
|
+
readonly pickHint?: ReactNode;
|
|
19
|
+
readonly accept?: readonly string[];
|
|
20
|
+
/** Opens that camera directly on phones; unset keeps camera and gallery. */
|
|
21
|
+
readonly capture?: "environment" | "user";
|
|
22
|
+
/** The first slot spans the full row (cover photo). Defaults to true. */
|
|
23
|
+
readonly featureFirst?: boolean;
|
|
24
|
+
readonly disabled?: boolean;
|
|
25
|
+
readonly testId?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Guided photo capture: one tile per requested shot in a two-column grid,
|
|
28
|
+
* each its own camera/gallery picker, filled tiles show the thumbnail. */
|
|
29
|
+
export declare function PhotoSlots({ slots, onUpload, pickHint, accept, capture, featureFirst, disabled, testId, }: PhotoSlotsProps): ReactNode;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Camera, Check, Loader2 } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useTranslation } from "../../index.js";
|
|
5
|
+
import { toAcceptAttr } from "../lib/accept-attr.js";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { resizeImageBeforeUpload } from "../lib/resize-image.js";
|
|
8
|
+
import { matchesAccept } from "./upload-zone.js";
|
|
9
|
+
const DEFAULT_ACCEPT = ["image/*"];
|
|
10
|
+
/** Guided photo capture: one tile per requested shot in a two-column grid,
|
|
11
|
+
* each its own camera/gallery picker, filled tiles show the thumbnail. */
|
|
12
|
+
export function PhotoSlots({ slots, onUpload, pickHint, accept = DEFAULT_ACCEPT, capture, featureFirst = true, disabled, testId, }) {
|
|
13
|
+
const t = useTranslation();
|
|
14
|
+
const [uploadStates, setUploadStates] = useState({});
|
|
15
|
+
function setSlotState(slotId, state) {
|
|
16
|
+
setUploadStates((prev) => {
|
|
17
|
+
const { [slotId]: _previous, ...rest } = prev;
|
|
18
|
+
return state === undefined ? rest : { ...rest, [slotId]: state };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function uploadIntoSlot(slotId, file) {
|
|
22
|
+
if (!matchesAccept(file, accept)) {
|
|
23
|
+
setSlotState(slotId, { kind: "error", message: t("kumiko.widget.upload.rejected-type") });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setSlotState(slotId, { kind: "uploading" });
|
|
27
|
+
try {
|
|
28
|
+
await onUpload(slotId, await resizeImageBeforeUpload(file));
|
|
29
|
+
setSlotState(slotId, undefined);
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
const message = cause instanceof Error ? cause.message : t("kumiko.widget.upload.error");
|
|
33
|
+
setSlotState(slotId, { kind: "error", message });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const acceptAttr = toAcceptAttr(accept);
|
|
37
|
+
return (_jsx("div", { "data-testid": testId, className: "grid grid-cols-2 gap-2.5", children: slots.map((slot, index) => {
|
|
38
|
+
const uploadState = uploadStates[slot.id];
|
|
39
|
+
const isUploading = uploadState?.kind === "uploading";
|
|
40
|
+
const isFeatured = featureFirst && index === 0;
|
|
41
|
+
const slotTestId = testId !== undefined ? `${testId}-slot-${slot.id}` : undefined;
|
|
42
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-1", isFeatured && "col-span-2"), children: [_jsxs("label", { "data-testid": slotTestId, "data-filled": slot.previewUrl !== undefined ? "true" : undefined, className: cn("relative flex cursor-pointer flex-col items-center justify-center gap-1 overflow-hidden rounded-lg border-2 text-center text-sm transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2", isFeatured ? "h-52" : "h-32", slot.previewUrl !== undefined
|
|
43
|
+
? "border-transparent"
|
|
44
|
+
: "border-dashed border-primary/60 bg-primary/5 text-primary", (disabled === true || isUploading) && "cursor-not-allowed opacity-60"), children: [slot.previewUrl !== undefined ? (_jsxs(_Fragment, { children: [_jsx("img", { src: slot.previewUrl, alt: slot.label, className: "absolute inset-0 size-full object-cover" }), slot.badge !== undefined && (_jsx("span", { className: "absolute bottom-2 left-2 rounded-full bg-background/90 px-2 py-0.5 text-xs font-semibold text-foreground", children: slot.badge })), _jsx("span", { className: "absolute top-2 right-2 flex size-6 items-center justify-center rounded-full bg-status-ok text-background", children: _jsx(Check, { "aria-hidden": "true", className: "size-3.5" }) })] })) : (_jsxs(_Fragment, { children: [isUploading ? (_jsx(Loader2, { "aria-hidden": "true", className: "size-6 animate-spin" })) : (_jsx(Camera, { "aria-hidden": "true", className: "size-6" })), _jsx("span", { className: "font-semibold", children: slot.label }), pickHint !== undefined && _jsx("span", { className: "text-xs", children: pickHint })] })), isUploading && (_jsx("span", { className: "sr-only", children: t("kumiko.widget.upload.uploading") })), _jsx("input", { type: "file", className: "sr-only", "data-testid": slotTestId !== undefined ? `${slotTestId}-input` : undefined, "aria-label": slot.label, disabled: disabled === true || isUploading, ...(acceptAttr !== undefined && { accept: acceptAttr }), ...(capture !== undefined && { capture }), onChange: (e) => {
|
|
45
|
+
const file = e.target.files?.[0];
|
|
46
|
+
// Reset so picking the same file again after an error still fires change.
|
|
47
|
+
e.target.value = "";
|
|
48
|
+
if (file !== undefined)
|
|
49
|
+
void uploadIntoSlot(slot.id, file);
|
|
50
|
+
} })] }), uploadState?.kind === "error" && (_jsx("p", { role: "alert", className: "text-xs text-destructive", children: uploadState.message }))] }, slot.id));
|
|
51
|
+
}) }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type PlanCardPrice = {
|
|
3
|
+
readonly amount: string;
|
|
4
|
+
readonly period?: string;
|
|
5
|
+
};
|
|
6
|
+
export type PlanCardActionSlot = {
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly onClick: () => void;
|
|
9
|
+
readonly disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type PlanCardLinkSlot = {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly href: string;
|
|
14
|
+
readonly target?: "_blank";
|
|
15
|
+
};
|
|
16
|
+
export type PlanCardProps = {
|
|
17
|
+
readonly title: string;
|
|
18
|
+
readonly description?: string;
|
|
19
|
+
/** null renders the price-unavailable state; undefined renders no price row. */
|
|
20
|
+
readonly price?: PlanCardPrice | null;
|
|
21
|
+
readonly features?: readonly ReactNode[];
|
|
22
|
+
readonly current?: boolean;
|
|
23
|
+
readonly cta?: PlanCardActionSlot;
|
|
24
|
+
/** Link-styled cta (e.g. contact sales); wins over `cta` when both are set. */
|
|
25
|
+
readonly ctaLink?: PlanCardLinkSlot;
|
|
26
|
+
readonly secondaryAction?: PlanCardActionSlot;
|
|
27
|
+
/** compact: one-off purchases (e.g. credit packs) — title+price in one row, no feature emphasis, smaller padding. */
|
|
28
|
+
readonly variant?: "default" | "compact";
|
|
29
|
+
readonly testId?: string;
|
|
30
|
+
};
|
|
31
|
+
export type PlanGridProps = {
|
|
32
|
+
readonly children: ReactNode;
|
|
33
|
+
readonly testId?: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function PlanCard({ title, description, price, features, current, cta, ctaLink, secondaryAction, variant, testId, }: PlanCardProps): ReactNode;
|
|
36
|
+
export declare function PlanGrid({ children, testId }: PlanGridProps): ReactNode;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePrimitives, useTranslation } from "../../index.js";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
export function PlanCard({ title, description, price, features, current = false, cta, ctaLink, secondaryAction, variant = "default", testId, }) {
|
|
5
|
+
const { Card, Button, Link } = usePrimitives();
|
|
6
|
+
const t = useTranslation();
|
|
7
|
+
const compact = variant === "compact";
|
|
8
|
+
const footer = cta !== undefined || ctaLink !== undefined || secondaryAction !== undefined ? (_jsxs("div", { className: "flex w-full flex-col gap-2", children: [ctaLink !== undefined && (_jsx(Link, { href: ctaLink.href, variant: "button", target: ctaLink.target, className: "w-full justify-center", testId: testId !== undefined ? `${testId}-cta` : "plan-card-cta", children: ctaLink.label })), ctaLink === undefined && cta !== undefined && (_jsx(Button, { width: "full", disabled: cta.disabled, onClick: cta.onClick, testId: testId !== undefined ? `${testId}-cta` : "plan-card-cta", children: cta.label })), secondaryAction !== undefined && (_jsx(Button, { variant: "secondary", width: "full", disabled: secondaryAction.disabled, onClick: secondaryAction.onClick, children: secondaryAction.label }))] })) : undefined;
|
|
9
|
+
return (_jsxs(Card, { testId: testId ?? "plan-card", className: cn(current && "border-primary"), slots: { footer }, children: [_jsxs("div", { className: cn("flex items-start justify-between gap-2", compact && "items-center"), children: [_jsx("h2", { className: cn("font-semibold", compact ? "text-base" : "text-lg"), children: title }), current && (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-current` : "plan-card-current", className: "inline-block rounded-xl bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary", children: t("kumiko.planCard.current") }))] }), description !== undefined && !compact && (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description })), price !== undefined && (_jsx("div", { className: cn(compact ? "mt-1" : "mt-1 mb-3"), children: price === null ? (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-price-unavailable` : "plan-card-price-unavailable", className: "text-sm text-muted-foreground", children: t("kumiko.planCard.priceUnavailable") })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-2xl font-bold", children: price.amount }), price.period !== undefined && (_jsx("span", { className: "ml-1 text-sm text-muted-foreground", children: price.period }))] })) })), !compact && features !== undefined && features.length > 0 && (_jsx("ul", { className: "space-y-1 text-sm", children: features.map((feature, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { "aria-hidden": "true", children: "\u2713" }), _jsx("span", { children: feature })] }, index))) }))] }));
|
|
10
|
+
}
|
|
11
|
+
export function PlanGrid({ children, testId }) {
|
|
12
|
+
return (_jsx("div", { "data-testid": testId, className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: children }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ProgressTone } from "../../index.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Progress bar, `value` 0..1 (clamped).
|
|
5
|
+
*
|
|
6
|
+
* The track lives in its own inner element so a parent that pads or
|
|
7
|
+
* stretches its direct children (e.g. RenderEdit's wizard chrome) never
|
|
8
|
+
* touches the track's own `h-2` — a caller's `className` reaches the
|
|
9
|
+
* outer wrapper, not the height-bearing track.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ProgressBar({ value, tone, size, ariaLabel, className, testId, }: {
|
|
12
|
+
readonly value: number;
|
|
13
|
+
readonly tone?: ProgressTone;
|
|
14
|
+
/** `"thin"` is a 4px bar on the border-token track for quiet inline progress. */
|
|
15
|
+
readonly size?: "default" | "thin";
|
|
16
|
+
readonly ariaLabel?: string;
|
|
17
|
+
readonly className?: string;
|
|
18
|
+
readonly testId?: string;
|
|
19
|
+
}): ReactNode;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
// Fully spelled out (no interpolation) — Tailwind's scanner only picks up
|
|
4
|
+
// literal class strings, not `bg-status-${tone}`.
|
|
5
|
+
const FILL_TONE = {
|
|
6
|
+
default: "bg-primary",
|
|
7
|
+
success: "bg-status-ok",
|
|
8
|
+
warn: "bg-status-warn",
|
|
9
|
+
danger: "bg-status-critical",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Progress bar, `value` 0..1 (clamped).
|
|
13
|
+
*
|
|
14
|
+
* The track lives in its own inner element so a parent that pads or
|
|
15
|
+
* stretches its direct children (e.g. RenderEdit's wizard chrome) never
|
|
16
|
+
* touches the track's own `h-2` — a caller's `className` reaches the
|
|
17
|
+
* outer wrapper, not the height-bearing track.
|
|
18
|
+
*/
|
|
19
|
+
export function ProgressBar({ value, tone = "default", size = "default", ariaLabel, className, testId, }) {
|
|
20
|
+
// `value` is typically `done / total`; total:0 produces NaN, which would
|
|
21
|
+
// otherwise leak into `width: "NaN%"` and an invalid aria-valuenow.
|
|
22
|
+
const pct = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
23
|
+
return (_jsx("div", { className: cn("w-full", className), children: _jsx("div", { "data-testid": testId, role: "progressbar", "aria-valuenow": Math.round(pct * 100), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": ariaLabel, className: cn("relative w-full overflow-hidden rounded-full", size === "thin" ? "h-1 bg-border" : "h-2 bg-muted"), children: _jsx("div", { className: cn("absolute inset-y-0 left-0 rounded-full", FILL_TONE[tone]), style: { width: `${pct * 100}%` } }) }) }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ProgressListRow = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly value: string;
|
|
6
|
+
/** null renders no bar (nothing to measure against, e.g. an unlimited cap). */
|
|
7
|
+
readonly fraction: number | null;
|
|
8
|
+
/** danger colors bar and value. */
|
|
9
|
+
readonly tone?: "default" | "danger";
|
|
10
|
+
/** Small line under the bar, e.g. "42 % repaid". */
|
|
11
|
+
readonly sub?: string;
|
|
12
|
+
};
|
|
13
|
+
/** Liste aus Label/Wert-Kopfzeile + Fortschrittsbalken pro Eintrag (z.B.
|
|
14
|
+
* Tilgungsfortschritt pro Kredit). */
|
|
15
|
+
export declare function ProgressList({ rows, emptyContent, testId, }: {
|
|
16
|
+
readonly rows: readonly ProgressListRow[];
|
|
17
|
+
readonly emptyContent?: ReactNode;
|
|
18
|
+
readonly testId?: string;
|
|
19
|
+
}): ReactNode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { ProgressBar } from "./progress-bar.js";
|
|
4
|
+
/** Liste aus Label/Wert-Kopfzeile + Fortschrittsbalken pro Eintrag (z.B.
|
|
5
|
+
* Tilgungsfortschritt pro Kredit). */
|
|
6
|
+
export function ProgressList({ rows, emptyContent, testId, }) {
|
|
7
|
+
if (rows.length === 0) {
|
|
8
|
+
return _jsx("div", { "data-testid": testId, children: emptyContent });
|
|
9
|
+
}
|
|
10
|
+
return (_jsx("ul", { "data-testid": testId, className: "flex max-h-64 flex-col gap-3 overflow-y-auto pr-1", children: rows.map((row) => (_jsxs("li", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [_jsx("span", { className: "font-medium", children: row.label }), _jsx("span", { className: cn("tabular-nums", row.tone === "danger" ? "text-status-critical" : "text-muted-foreground"), children: row.value })] }), row.fraction !== null && _jsx(ProgressBar, { value: row.fraction, tone: row.tone }), row.sub !== undefined && _jsx("div", { className: "text-xs text-muted-foreground", children: row.sub })] }, row.id))) }));
|
|
11
|
+
}
|