@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,685 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import { NARROW_VIEWPORT_BREAKPOINT_PX, useIsViewportBelow, } from "../primitives/use-narrow-viewport.js";
|
|
5
|
+
import { bucketIndexAtFraction, initialWindowSelection, nearestIndex, pointsWithin, resolveStackedAreaWindow, snapBrushWindow, snapMovedBrushWindow, stackedAreaAxisTicks, stackedAreaDateFormatFor, } from "./chart-window.js";
|
|
6
|
+
import { ModeSwitch } from "./mode-switch.js";
|
|
7
|
+
import { STATUS_TONE_TEXT } from "./status-badge.js";
|
|
8
|
+
// Inline-SVG-Charts — kein Chart-Dep. Farben ausschließlich über die
|
|
9
|
+
// --color-status-* / --color-foreground Theme-Tokens; Achsen-Labels
|
|
10
|
+
// kommen translated vom Caller (keine Locale-Annahmen im Widget).
|
|
11
|
+
const HOUR_MS = 3_600_000;
|
|
12
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
13
|
+
const TONE_VAR = {
|
|
14
|
+
ok: "var(--color-status-ok)",
|
|
15
|
+
warn: "var(--color-status-warn)",
|
|
16
|
+
bad: "var(--color-status-bad)",
|
|
17
|
+
critical: "var(--color-status-critical)",
|
|
18
|
+
muted: "var(--color-muted-foreground)",
|
|
19
|
+
};
|
|
20
|
+
/** Status bar strip (e.g. 90-day uptime): variable-height bars with
|
|
21
|
+
* gradient fade + tick line on top; the last entry gets a "now"
|
|
22
|
+
* accent stripe. `dense` swaps this for a flat-fill, fixed-size
|
|
23
|
+
* variant sized to fit a table cell instead of stretching to `w-full`. */
|
|
24
|
+
export function StatusBarChart({ entries, ariaLabel, startLabel, endLabel, highlightLast = true, dense = false, testId, }) {
|
|
25
|
+
const gradPrefix = useId();
|
|
26
|
+
if (entries.length === 0)
|
|
27
|
+
return _jsx("div", { className: dense ? "h-3" : "h-9", "aria-hidden": true });
|
|
28
|
+
const chartHeight = dense ? 12 : 36;
|
|
29
|
+
const tickHeight = dense ? 0 : 1;
|
|
30
|
+
const barWidth = dense ? 3 : 1;
|
|
31
|
+
const barGap = 1;
|
|
32
|
+
const lastIdx = entries.length - 1;
|
|
33
|
+
const totalWidth = entries.length * (barWidth + barGap);
|
|
34
|
+
return (_jsxs("div", { "data-testid": testId, children: [_jsxs("svg", { viewBox: `0 0 ${totalWidth} ${chartHeight}`, preserveAspectRatio: dense ? undefined : "none", width: dense ? totalWidth : undefined, height: dense ? chartHeight : undefined, className: dense ? "block" : "block h-9 w-full", role: "img", "aria-label": ariaLabel, children: [_jsx("title", { children: ariaLabel }), entries.map((entry, idx) => {
|
|
35
|
+
const x = idx * (barWidth + barGap);
|
|
36
|
+
const level = Math.max(0, Math.min(1, entry.level));
|
|
37
|
+
// Dense has no tick rect, so level 0 would otherwise render nothing and lose its tooltip.
|
|
38
|
+
const barHeight = Math.max((chartHeight - tickHeight) * level, dense ? 1 : 0);
|
|
39
|
+
const barY = chartHeight - barHeight;
|
|
40
|
+
const isLast = highlightLast && idx === lastIdx;
|
|
41
|
+
const color = TONE_VAR[entry.tone];
|
|
42
|
+
const gradId = `${gradPrefix}-${idx}`;
|
|
43
|
+
return (_jsxs("g", { children: [!dense && (_jsx("defs", { children: _jsxs("linearGradient", { id: gradId, x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: color, stopOpacity: isLast ? 0.85 : 0.5 }), _jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0.05 })] }) })), isLast && (_jsx("rect", { x: x - barGap / 2, y: 0, width: barWidth + barGap, height: chartHeight, fill: "var(--color-foreground)", fillOpacity: 0.06 })), _jsx("rect", { x: x, y: barY, width: barWidth, height: barHeight, fill: dense ? color : `url(#${gradId})`, fillOpacity: dense ? (isLast ? 1 : 0.75) : undefined, children: entry.label !== undefined && _jsx("title", { children: entry.label }) }), !dense && (_jsx("rect", { x: x, y: barY - tickHeight, width: barWidth, height: tickHeight, fill: "var(--color-foreground)", fillOpacity: isLast ? 1.0 : 0.7 }))] }, entry.key));
|
|
44
|
+
})] }), (startLabel !== undefined || endLabel !== undefined) && (_jsxs("div", { className: "mt-0.5 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: startLabel }), _jsx("span", { children: endLabel })] }))] }));
|
|
45
|
+
}
|
|
46
|
+
const SINGLE_POINT_RADIUS = 3;
|
|
47
|
+
function clampDotY(y, height) {
|
|
48
|
+
return Math.max(SINGLE_POINT_RADIUS, Math.min(height - SINGLE_POINT_RADIUS, y));
|
|
49
|
+
}
|
|
50
|
+
/** Quadratic-durch-Mittelpunkte-Trick: glättet die Zick-Zack-Linie ohne
|
|
51
|
+
* Overshoot (~5 Zeilen statt Catmull-Rom/Bezier-Fit). */
|
|
52
|
+
export function smoothPath(pts) {
|
|
53
|
+
const first = pts[0];
|
|
54
|
+
if (!first)
|
|
55
|
+
return "";
|
|
56
|
+
let d = `M ${first.x.toFixed(1)} ${first.y.toFixed(1)}`;
|
|
57
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
58
|
+
const cur = pts[i];
|
|
59
|
+
const next = pts[i + 1];
|
|
60
|
+
if (!cur || !next)
|
|
61
|
+
break;
|
|
62
|
+
const midX = (cur.x + next.x) / 2;
|
|
63
|
+
const midY = (cur.y + next.y) / 2;
|
|
64
|
+
d += ` Q ${cur.x.toFixed(1)} ${cur.y.toFixed(1)}, ${midX.toFixed(1)} ${midY.toFixed(1)}`;
|
|
65
|
+
}
|
|
66
|
+
const last = pts[pts.length - 1];
|
|
67
|
+
if (!last)
|
|
68
|
+
return d;
|
|
69
|
+
return `${d} L ${last.x.toFixed(1)} ${last.y.toFixed(1)}`;
|
|
70
|
+
}
|
|
71
|
+
const DEFAULT_TIMESERIES_HEIGHT_CLASS = "h-16";
|
|
72
|
+
const MIN_AXIS_TICKS = 2;
|
|
73
|
+
const MAX_AXIS_TICKS = 20;
|
|
74
|
+
function clampTickCount(requested) {
|
|
75
|
+
return Math.min(MAX_AXIS_TICKS, Math.max(MIN_AXIS_TICKS, Math.floor(requested) || MIN_AXIS_TICKS));
|
|
76
|
+
}
|
|
77
|
+
function evenlySpaced(count) {
|
|
78
|
+
const n = clampTickCount(count);
|
|
79
|
+
return Array.from({ length: n }, (_, i) => i / (n - 1));
|
|
80
|
+
}
|
|
81
|
+
function yTickPosition(index, count) {
|
|
82
|
+
if (index === 0)
|
|
83
|
+
return "-translate-y-full";
|
|
84
|
+
if (index === count - 1)
|
|
85
|
+
return "";
|
|
86
|
+
return "-translate-y-1/2";
|
|
87
|
+
}
|
|
88
|
+
/** Zeitreihen-Linien-Chart (geglättete Linie + Flächen-Verlauf). x-Achse =
|
|
89
|
+
* ZEIT im Fenster windowStartMs..windowEndMs, nicht Index — 5 Min Daten in
|
|
90
|
+
* einem 30-Tage-Fenster ergeben ehrlich einen schmalen Streifen rechts. */
|
|
91
|
+
export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok", ariaLabel, axisLabels, xAxis, yAxis, height, markers, formatMarkerTime, referenceLines, emptyContent, testId, }) {
|
|
92
|
+
const gradientId = useId();
|
|
93
|
+
const descId = useId();
|
|
94
|
+
const chartWidth = 300;
|
|
95
|
+
const chartHeight = 64;
|
|
96
|
+
const isNarrow = useIsViewportBelow(typeof height === "object"
|
|
97
|
+
? (height.narrowBelowPx ?? NARROW_VIEWPORT_BREAKPOINT_PX)
|
|
98
|
+
: NARROW_VIEWPORT_BREAKPOINT_PX);
|
|
99
|
+
const heightPx = typeof height === "object" ? (isNarrow ? height.narrow : height.wide) : height;
|
|
100
|
+
const heightClass = heightPx === undefined ? DEFAULT_TIMESERIES_HEIGHT_CLASS : undefined;
|
|
101
|
+
const heightStyle = heightPx === undefined ? undefined : { height: heightPx };
|
|
102
|
+
const values = points.map((p) => p.value).filter((v) => v !== null);
|
|
103
|
+
if (values.length === 0) {
|
|
104
|
+
return (_jsx("div", { className: cn("flex items-center justify-center text-[13px] text-muted-foreground", heightClass), style: heightStyle, children: emptyContent }));
|
|
105
|
+
}
|
|
106
|
+
const drawableLines = (referenceLines ?? []).filter((line) => Number.isFinite(line.value) && line.value >= 0);
|
|
107
|
+
const singleValue = values.length === 1 ? values[0] : undefined;
|
|
108
|
+
// A lone value at the top edge would sit on the clip boundary; doubling the
|
|
109
|
+
// scale centres it.
|
|
110
|
+
const dataMax = Math.max(...values.map((value) => (singleValue === undefined ? value : value * 2)), ...drawableLines.map((line) => line.value), 1);
|
|
111
|
+
const yTicks = yAxis === undefined ? [] : niceYTicks(dataMax, yAxis.ticks);
|
|
112
|
+
const maxValue = yTicks.length > 0 ? (yTicks[yTicks.length - 1] ?? dataMax) : dataMax;
|
|
113
|
+
const yOf = (value) => chartHeight - (value / maxValue) * chartHeight;
|
|
114
|
+
const span = Math.max(1, windowEndMs - windowStartMs);
|
|
115
|
+
const xOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span)) * chartWidth;
|
|
116
|
+
const chartPoints = points.map((p) => ({
|
|
117
|
+
x: xOf(p.atMs),
|
|
118
|
+
y: p.value === null ? chartHeight : yOf(p.value),
|
|
119
|
+
}));
|
|
120
|
+
const linePath = smoothPath(chartPoints);
|
|
121
|
+
const firstPoint = chartPoints[0];
|
|
122
|
+
const lastPoint = chartPoints[chartPoints.length - 1];
|
|
123
|
+
const areaPath = firstPoint && lastPoint
|
|
124
|
+
? `${linePath} L ${lastPoint.x.toFixed(1)} ${chartHeight} L ${firstPoint.x.toFixed(1)} ${chartHeight} Z`
|
|
125
|
+
: "";
|
|
126
|
+
const color = TONE_VAR[tone];
|
|
127
|
+
const hasReferenceLines = drawableLines.length > 0;
|
|
128
|
+
const svg = (_jsxs("svg", { viewBox: `0 0 ${chartWidth} ${chartHeight}`, preserveAspectRatio: "none", className: cn("block w-full", heightClass), style: heightStyle, role: "img", "aria-label": ariaLabel, "aria-describedby": hasReferenceLines ? descId : undefined, children: [_jsx("title", { children: ariaLabel }), hasReferenceLines && _jsx("desc", { id: descId, children: drawableLines.map((l) => l.label).join(", ") }), _jsx("defs", { children: _jsxs("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: color, stopOpacity: 0.3 }), _jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0.02 })] }) }), yTicks.map((tick) => (_jsx("line", { "data-grid-line": "", x1: 0, x2: chartWidth, y1: yOf(tick), y2: yOf(tick), stroke: "var(--color-border)", strokeWidth: 1, vectorEffect: "non-scaling-stroke" }, `grid-${tick}`))), singleValue === undefined ? (_jsxs(_Fragment, { children: [_jsx("path", { d: areaPath, fill: `url(#${gradientId})`, stroke: "none" }), _jsx("path", { d: linePath, fill: "none", stroke: color, strokeWidth: 1.5, strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" })] })) : (_jsx("circle", { "data-testid": "timeseries-single-point", cx: chartWidth / 2, cy: clampDotY(yOf(singleValue), chartHeight), r: SINGLE_POINT_RADIUS, fill: color })), drawableLines.map((line) => (_jsx("line", { "data-reference-line": "", x1: 0, x2: chartWidth, y1: yOf(line.value), y2: yOf(line.value), stroke: TONE_VAR[line.tone ?? "muted"], strokeWidth: 1, strokeDasharray: "4 3", vectorEffect: "non-scaling-stroke" }, `${line.value}:${line.label}`)))] }));
|
|
129
|
+
const plot = (_jsxs(_Fragment, { children: [hasReferenceLines ? (_jsxs("div", { className: "relative", children: [svg, drawableLines.map((line) => (_jsx("span", { "aria-hidden": "true", className: cn("absolute right-0 rounded bg-background/80 px-1 text-[11px]",
|
|
130
|
+
// A line near the top would push its label above the chart box.
|
|
131
|
+
yOf(line.value) >= chartHeight / 4 && "-translate-y-full", STATUS_TONE_TEXT[line.tone ?? "muted"]), style: { top: `${(yOf(line.value) / chartHeight) * 100}%` }, children: line.label }, `${line.value}:${line.label}`)))] })) : (svg), xAxis !== undefined ? (_jsx("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: evenlySpaced(xAxis.ticks).map((fraction) => (_jsx("span", { "data-x-label": "", children: xAxis.format(windowStartMs + fraction * (windowEndMs - windowStartMs)) }, fraction))) })) : (axisLabels !== undefined && (_jsxs("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: axisLabels.start }), axisLabels.mid !== undefined && _jsx("span", { children: axisLabels.mid }), _jsx("span", { children: axisLabels.end })] })))] }));
|
|
132
|
+
return (_jsxs("div", { "data-testid": testId, className: STATUS_TONE_TEXT[tone], children: [yAxis === undefined ? (plot) : (_jsxs("div", { className: cn("flex gap-1.5", yAxis.unit !== undefined && "pt-4"), children: [_jsxs("div", { "aria-hidden": "true", className: cn("relative w-12 shrink-0 text-right text-[11px] text-muted-foreground", heightClass), style: heightStyle, children: [yAxis.unit !== undefined && (_jsx("span", { "data-testid": testId === undefined ? undefined : `${testId}-y-unit`, "data-y-unit": "", className: "absolute right-0 bottom-full mb-1 whitespace-nowrap leading-none", children: yAxis.unit })), yTicks.map((tick, index) => (_jsx("span", { "data-y-label": "", className: cn("absolute right-0 whitespace-nowrap leading-none", yTickPosition(index, yTicks.length)), style: { top: `${(yOf(tick) / chartHeight) * 100}%` }, children: (yAxis.format ?? String)(tick) }, tick)))] }), _jsx("div", { className: "min-w-0 flex-1", children: plot })] })), markers !== undefined && markers.length > 0 && (_jsxs("div", { className: "text-foreground", children: [_jsx(MarkerPins, { markers: markers, windowStartMs: windowStartMs, windowEndMs: windowEndMs }), _jsx("div", { className: "mt-2", children: _jsx(MarkerLegend, { markers: markers, formatMarkerTime: formatMarkerTime }) })] }))] }));
|
|
133
|
+
}
|
|
134
|
+
const CHART_TONE_VAR = {
|
|
135
|
+
positive: "var(--color-status-ok)",
|
|
136
|
+
negative: "var(--color-status-bad)",
|
|
137
|
+
active: "var(--color-status-active)",
|
|
138
|
+
neutral: "var(--color-status-neutral)",
|
|
139
|
+
};
|
|
140
|
+
export function chartToneColor(tone) {
|
|
141
|
+
return CHART_TONE_VAR[tone];
|
|
142
|
+
}
|
|
143
|
+
const CHART_FALLBACK_TONES = ["active", "positive", "negative", "neutral"];
|
|
144
|
+
function chartColor(key, index, tones, colors) {
|
|
145
|
+
const color = colors?.[key];
|
|
146
|
+
if (color !== undefined)
|
|
147
|
+
return color;
|
|
148
|
+
const tone = tones?.[key] ?? CHART_FALLBACK_TONES[index % CHART_FALLBACK_TONES.length] ?? "active";
|
|
149
|
+
return CHART_TONE_VAR[tone];
|
|
150
|
+
}
|
|
151
|
+
const Y_TICK_COUNT = 3;
|
|
152
|
+
function niceCeil(max) {
|
|
153
|
+
if (max <= 0)
|
|
154
|
+
return 1;
|
|
155
|
+
const magnitude = 10 ** Math.floor(Math.log10(max));
|
|
156
|
+
const normalized = max / magnitude;
|
|
157
|
+
const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
|
|
158
|
+
return step * magnitude;
|
|
159
|
+
}
|
|
160
|
+
const FINE_NICE_STEPS = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
|
|
161
|
+
// Finer than niceCeil so a 1.1 M peak tops out at 1.2 M instead of 2 M.
|
|
162
|
+
function fineNiceCeil(max) {
|
|
163
|
+
if (max <= 0)
|
|
164
|
+
return 1;
|
|
165
|
+
const magnitude = 10 ** Math.floor(Math.log10(max));
|
|
166
|
+
const normalized = max / magnitude;
|
|
167
|
+
const step = FINE_NICE_STEPS.find((candidate) => normalized <= candidate) ?? 10;
|
|
168
|
+
return step * magnitude;
|
|
169
|
+
}
|
|
170
|
+
function niceYTicks(max, tickCount) {
|
|
171
|
+
const count = clampTickCount(tickCount);
|
|
172
|
+
const step = niceCeil(max / (count - 1));
|
|
173
|
+
// toPrecision strips float noise such as 3 * 0.2 = 0.6000000000000001 from labels and keys.
|
|
174
|
+
return Array.from({ length: count }, (_, i) => Number((i * step).toPrecision(12)));
|
|
175
|
+
}
|
|
176
|
+
function bucketTimes(series) {
|
|
177
|
+
const times = new Set();
|
|
178
|
+
for (const s of series)
|
|
179
|
+
for (const p of s.points)
|
|
180
|
+
times.add(p.atMs);
|
|
181
|
+
return [...times].sort((a, b) => a - b);
|
|
182
|
+
}
|
|
183
|
+
function valueAt(series, atMs) {
|
|
184
|
+
return series.points.find((p) => p.atMs === atMs)?.value ?? 0;
|
|
185
|
+
}
|
|
186
|
+
function linePathSegments(line, xOf, yOf) {
|
|
187
|
+
const commands = [];
|
|
188
|
+
let penDown = false;
|
|
189
|
+
for (const point of [...line.points].sort((a, b) => a.atMs - b.atMs)) {
|
|
190
|
+
if (point.value === null) {
|
|
191
|
+
penDown = false;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
commands.push(`${penDown ? "L" : "M"} ${xOf(point.atMs).toFixed(1)} ${yOf(point.value).toFixed(1)}`);
|
|
195
|
+
penDown = true;
|
|
196
|
+
}
|
|
197
|
+
return commands.join(" ");
|
|
198
|
+
}
|
|
199
|
+
function seriesTotal(series) {
|
|
200
|
+
return series.points.reduce((sum, p) => sum + (p.value ?? 0), 0);
|
|
201
|
+
}
|
|
202
|
+
function percent(fraction) {
|
|
203
|
+
return `${(fraction * 100).toFixed(2)}%`;
|
|
204
|
+
}
|
|
205
|
+
function ChartLegend({ items, formatValue, }) {
|
|
206
|
+
return (_jsx("ul", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => (_jsxs("li", { "data-testid": `chart-legend-${item.testIdKey ?? item.key}`, className: "flex items-center gap-1.5", children: [item.swatch === undefined || item.swatch === "area" ? (_jsx("span", { "aria-hidden": "true", className: "size-2 rounded-[2px]", style: { backgroundColor: item.color } })) : (_jsx("span", { "aria-hidden": "true", className: "w-3 border-t-2", style: {
|
|
207
|
+
borderColor: item.color,
|
|
208
|
+
borderTopStyle: item.swatch === "dashed-line" ? "dashed" : "solid",
|
|
209
|
+
} })), _jsx("span", { children: item.label }), item.total !== undefined && (_jsx("span", { className: "tabular-nums text-foreground", children: formatValue(item.total) }))] }, item.key))) }));
|
|
210
|
+
}
|
|
211
|
+
function evenYTickValues(max) {
|
|
212
|
+
return Array.from({ length: Y_TICK_COUNT }, (_, i) => (max / (Y_TICK_COUNT - 1)) * i);
|
|
213
|
+
}
|
|
214
|
+
function YTicks({ values, formatValue, className, }) {
|
|
215
|
+
return (_jsx("div", { "aria-hidden": "true", className: cn("flex flex-col-reverse justify-between text-right text-[11px] tabular-nums text-muted-foreground", className), children: values.map((value, i) => (_jsx("span", { children: formatValue(value) }, i))) }));
|
|
216
|
+
}
|
|
217
|
+
const BAR_LABEL_TARGET = 8;
|
|
218
|
+
const STACKED_AREA_PLOT_HEIGHT_CLASS = "h-56 @2xl:h-64";
|
|
219
|
+
// A 16px pin on a ~230px-wide plot (390px viewport) collides when markers sit
|
|
220
|
+
// within ~7% of each other, so close pins are staggered into extra rows.
|
|
221
|
+
const MARKER_PIN_MIN_GAP_FRACTION = 0.07;
|
|
222
|
+
const MARKER_PIN_MAX_ROWS = 3;
|
|
223
|
+
const MARKER_PIN_ROW_HEIGHT_PX = 18;
|
|
224
|
+
const TOOLTIP_WIDTH_REM = 14;
|
|
225
|
+
const TOOLTIP_OFFSET_PX = 8;
|
|
226
|
+
// A swipe over the plot (scrolling the page or the chart) must not open the card.
|
|
227
|
+
const TOOLTIP_TAP_MAX_MOVEMENT_PX = 8;
|
|
228
|
+
export function assignPinRows(fractions) {
|
|
229
|
+
const lastFractionPerRow = [];
|
|
230
|
+
return fractions.map((fraction) => {
|
|
231
|
+
let row = lastFractionPerRow.findIndex((last) => fraction - last >= MARKER_PIN_MIN_GAP_FRACTION);
|
|
232
|
+
if (row === -1) {
|
|
233
|
+
row =
|
|
234
|
+
lastFractionPerRow.length < MARKER_PIN_MAX_ROWS
|
|
235
|
+
? lastFractionPerRow.length
|
|
236
|
+
: MARKER_PIN_MAX_ROWS - 1;
|
|
237
|
+
}
|
|
238
|
+
lastFractionPerRow[row] = fraction;
|
|
239
|
+
return row;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// Container-query density tiers for the x axis: label budget per tier.
|
|
243
|
+
const AXIS_TIERS = [
|
|
244
|
+
{ maxLabels: 4, visibilityClass: "@xl:hidden" },
|
|
245
|
+
{ maxLabels: 7, visibilityClass: "hidden @xl:block @4xl:hidden" },
|
|
246
|
+
{ maxLabels: 12, visibilityClass: "hidden @4xl:block" },
|
|
247
|
+
];
|
|
248
|
+
const MARKER_LABEL_FLIP_FRACTION = 0.97;
|
|
249
|
+
// Markers sharing a timestamp become one label ("A, B") in the first one's color.
|
|
250
|
+
function groupMarkerLabels(markers) {
|
|
251
|
+
const groups = new Map();
|
|
252
|
+
for (const marker of [...markers].sort((a, b) => a.atMs - b.atMs)) {
|
|
253
|
+
groups.set(marker.atMs, [...(groups.get(marker.atMs) ?? []), marker]);
|
|
254
|
+
}
|
|
255
|
+
return [...groups.entries()].map(([atMs, members]) => ({
|
|
256
|
+
atMs,
|
|
257
|
+
text: members.map((m) => m.label).join(", "),
|
|
258
|
+
color: members[0]?.color ?? "var(--color-foreground)",
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
/** Vertically stacked bars per bucket (x axis = bucket, not time).
|
|
262
|
+
* The last bucket carries `todayLabel` when it contains `windowEndMs` and
|
|
263
|
+
* has day granularity. */
|
|
264
|
+
export function StackedBarChart({ series, windowEndMs, tones, colors, ariaLabel, todayLabel, formatBucketLabel, formatValue = String, formatAxisValue = formatValue, emptyContent, testId, }) {
|
|
265
|
+
const times = bucketTimes(series);
|
|
266
|
+
if (times.length === 0)
|
|
267
|
+
return _jsx("div", { "data-testid": testId, children: emptyContent });
|
|
268
|
+
const totals = times.map((atMs) => series.reduce((sum, s) => sum + valueAt(s, atMs), 0));
|
|
269
|
+
const max = niceCeil(Math.max(...totals));
|
|
270
|
+
const lastIdx = times.length - 1;
|
|
271
|
+
const lastAtMs = times[lastIdx] ?? 0;
|
|
272
|
+
const prevAtMs = times[lastIdx - 1];
|
|
273
|
+
const stepMs = prevAtMs !== undefined ? lastAtMs - prevAtMs : DAY_MS;
|
|
274
|
+
const lastIsToday = stepMs >= DAY_MS - HOUR_MS && windowEndMs - lastAtMs <= stepMs;
|
|
275
|
+
const labelEvery = Math.max(1, Math.ceil(times.length / BAR_LABEL_TARGET));
|
|
276
|
+
return (_jsxs("div", { "data-testid": testId, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx(YTicks, { values: evenYTickValues(max), formatValue: formatAxisValue, className: "h-40 pb-5 pt-0" }), _jsx("div", { role: "img", "aria-label": ariaLabel, className: "flex h-40 grow gap-1 border-b border-border-row", children: times.map((atMs, i) => {
|
|
277
|
+
const label = i === lastIdx && lastIsToday ? todayLabel : formatBucketLabel(atMs);
|
|
278
|
+
return (_jsxs("div", { title: `${label}: ${formatValue(totals[i] ?? 0)}`, className: "flex min-w-0 flex-1 flex-col", children: [_jsx("div", { className: "relative flex-1", children: _jsx("div", { className: "absolute inset-x-0 bottom-0 flex flex-col-reverse gap-px", style: { height: percent((totals[i] ?? 0) / max) }, children: series.map((s, si) => {
|
|
279
|
+
const value = valueAt(s, atMs);
|
|
280
|
+
if (value <= 0)
|
|
281
|
+
return null;
|
|
282
|
+
return (_jsx("div", { "data-testid": `chart-bar-segment-${s.key}`, style: {
|
|
283
|
+
height: percent(value / (totals[i] ?? 1)),
|
|
284
|
+
backgroundColor: chartColor(s.key, si, tones, colors),
|
|
285
|
+
} }, s.key));
|
|
286
|
+
}) }) }), _jsx("div", { className: "h-5 pt-1 text-center text-[11px] leading-4 whitespace-nowrap text-muted-foreground", children: (lastIdx - i) % labelEvery === 0 ? label : "" })] }, atMs));
|
|
287
|
+
}) })] }), _jsx(ChartLegend, { formatValue: formatValue, items: series.map((s, i) => ({
|
|
288
|
+
key: s.key,
|
|
289
|
+
label: s.label,
|
|
290
|
+
color: chartColor(s.key, i, tones, colors),
|
|
291
|
+
total: seriesTotal(s),
|
|
292
|
+
})) })] }));
|
|
293
|
+
}
|
|
294
|
+
/** One horizontally stacked bar per row; bar length is relative to the
|
|
295
|
+
* largest row so rows stay comparable. */
|
|
296
|
+
export function SegmentBarChart({ rows, tones, colors, ariaLabel, formatValue = String, testId, }) {
|
|
297
|
+
const rowTotal = (row) => row.segments.length > 0 ? row.segments.reduce((sum, s) => sum + s.value, 0) : row.value;
|
|
298
|
+
const maxTotal = Math.max(1, ...rows.map(rowTotal));
|
|
299
|
+
const segmentTotals = new Map();
|
|
300
|
+
for (const row of rows) {
|
|
301
|
+
for (const seg of row.segments) {
|
|
302
|
+
const known = segmentTotals.get(seg.key);
|
|
303
|
+
segmentTotals.set(seg.key, {
|
|
304
|
+
label: known?.label ?? seg.label,
|
|
305
|
+
total: (known?.total ?? 0) + seg.value,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const legendKeys = [...segmentTotals.keys()];
|
|
310
|
+
return (_jsxs("div", { "data-testid": testId, role: "img", "aria-label": ariaLabel, className: "flex flex-col gap-3", children: [_jsx("ul", { className: "flex flex-col gap-3", children: rows.map((row) => (_jsxs("li", { "data-testid": `chart-row-${row.key}`, 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: "tabular-nums text-muted-foreground", children: formatValue(row.value) })] }), _jsx("div", { className: "flex h-2 gap-px overflow-hidden rounded-full bg-muted", children: row.segments.map((seg) => (_jsx("div", { title: `${seg.label}: ${formatValue(seg.value)}`, className: "h-full", style: {
|
|
311
|
+
width: percent(seg.value / maxTotal),
|
|
312
|
+
backgroundColor: chartColor(seg.key, legendKeys.indexOf(seg.key), tones, colors),
|
|
313
|
+
} }, seg.key))) })] }, row.key))) }), _jsx(ChartLegend, { formatValue: formatValue, items: legendKeys.map((key, i) => ({
|
|
314
|
+
key,
|
|
315
|
+
label: segmentTotals.get(key)?.label ?? key,
|
|
316
|
+
color: chartColor(key, i, tones, colors),
|
|
317
|
+
total: segmentTotals.get(key)?.total ?? 0,
|
|
318
|
+
})) })] }));
|
|
319
|
+
}
|
|
320
|
+
// Two markers may share atMs and label, so the occurrence count keeps React keys unique.
|
|
321
|
+
function sortMarkers(markers) {
|
|
322
|
+
const seen = new Map();
|
|
323
|
+
return [...markers]
|
|
324
|
+
.sort((a, b) => a.atMs - b.atMs)
|
|
325
|
+
.map((marker) => {
|
|
326
|
+
const base = `${marker.atMs}-${marker.label}`;
|
|
327
|
+
const occurrence = seen.get(base) ?? 0;
|
|
328
|
+
seen.set(base, occurrence + 1);
|
|
329
|
+
return { ...marker, key: `${base}-${occurrence}` };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function MarkerPins({ markers, windowStartMs, windowEndMs, numbered = true, formatMarkerTime, className, }) {
|
|
333
|
+
const span = Math.max(1, windowEndMs - windowStartMs);
|
|
334
|
+
const sorted = sortMarkers(markers);
|
|
335
|
+
const fractions = sorted.map((marker) => Math.max(0, Math.min(1, (marker.atMs - windowStartMs) / span)));
|
|
336
|
+
const rows = assignPinRows(fractions);
|
|
337
|
+
const rowCount = Math.max(1, ...rows.map((row) => row + 1));
|
|
338
|
+
return (_jsx("div", { "aria-hidden": "true", className: cn("relative mt-1", className), style: { height: rowCount * MARKER_PIN_ROW_HEIGHT_PX }, children: sorted.map((marker, i) => (_jsx("span", { "data-testid": "chart-marker-pin", title: numbered
|
|
339
|
+
? undefined
|
|
340
|
+
: formatMarkerTime === undefined
|
|
341
|
+
? marker.label
|
|
342
|
+
: `${marker.label} · ${formatMarkerTime(marker.atMs)}`, className: cn("absolute flex -translate-x-1/2 items-center justify-center rounded-full text-[10px] font-medium text-background", numbered ? "size-4" : "size-2", marker.color === undefined && "bg-foreground"), style: {
|
|
343
|
+
left: percent(fractions[i] ?? 0),
|
|
344
|
+
top: (rows[i] ?? 0) * MARKER_PIN_ROW_HEIGHT_PX + (numbered ? 0 : 4),
|
|
345
|
+
...(marker.color !== undefined && { backgroundColor: marker.color }),
|
|
346
|
+
}, children: numbered ? i + 1 : null }, marker.key))) }));
|
|
347
|
+
}
|
|
348
|
+
function markerLegendItems(markers) {
|
|
349
|
+
const items = new Map();
|
|
350
|
+
for (const marker of markers) {
|
|
351
|
+
const label = marker.legendLabel ?? marker.label;
|
|
352
|
+
const color = marker.color ?? "var(--color-foreground)";
|
|
353
|
+
const key = `marker-${label}-${color}`;
|
|
354
|
+
if (!items.has(key))
|
|
355
|
+
items.set(key, { key, label, color });
|
|
356
|
+
}
|
|
357
|
+
return [...items.values()];
|
|
358
|
+
}
|
|
359
|
+
function MarkerLegend({ markers, formatMarkerTime, className, }) {
|
|
360
|
+
return (_jsx("ol", { className: cn("flex flex-col gap-1 text-xs text-muted-foreground", className), children: sortMarkers(markers).map((marker, i) => (_jsxs("li", { "data-testid": "chart-marker-item", className: "flex gap-2", children: [_jsx("span", { className: "w-4 shrink-0 text-center tabular-nums text-foreground", style: marker.color !== undefined ? { color: marker.color } : undefined, children: i + 1 }), _jsx("span", { children: marker.label }), formatMarkerTime !== undefined && (_jsx("span", { className: "ml-auto tabular-nums", children: formatMarkerTime(marker.atMs) }))] }, marker.key))) }));
|
|
361
|
+
}
|
|
362
|
+
const SCROLLABLE_BUCKET_WIDTH_PX = 48;
|
|
363
|
+
function bucketLabelTransform(index, count) {
|
|
364
|
+
if (index === 0)
|
|
365
|
+
return "";
|
|
366
|
+
return index === count - 1 ? "-translate-x-full" : "-translate-x-1/2";
|
|
367
|
+
}
|
|
368
|
+
const DEFAULT_BRUSH_LABELS = { start: "Window start", end: "Window end" };
|
|
369
|
+
const BRUSH_OVERVIEW_WIDTH = 100;
|
|
370
|
+
const BRUSH_OVERVIEW_HEIGHT = 24;
|
|
371
|
+
function brushKeyTarget(key, index, min, max) {
|
|
372
|
+
if (key === "ArrowLeft")
|
|
373
|
+
return index - 1;
|
|
374
|
+
if (key === "ArrowRight")
|
|
375
|
+
return index + 1;
|
|
376
|
+
if (key === "Home")
|
|
377
|
+
return min;
|
|
378
|
+
return key === "End" ? max : undefined;
|
|
379
|
+
}
|
|
380
|
+
function StackedAreaBrush({ times, totals, window, labels, onChange, testId, }) {
|
|
381
|
+
const trackRef = useRef(null);
|
|
382
|
+
const dragRef = useRef(null);
|
|
383
|
+
const firstMs = times[0] ?? 0;
|
|
384
|
+
const lastMs = times[times.length - 1] ?? 1;
|
|
385
|
+
const span = Math.max(1, lastMs - firstMs);
|
|
386
|
+
const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - firstMs) / span));
|
|
387
|
+
const startIndex = nearestIndex(times, window.startMs);
|
|
388
|
+
const endIndex = nearestIndex(times, window.endMs);
|
|
389
|
+
const lastIndex = times.length - 1;
|
|
390
|
+
const max = Math.max(...totals, 1);
|
|
391
|
+
const overviewPoints = times.map((atMs, i) => `${(fractionOf(atMs) * BRUSH_OVERVIEW_WIDTH).toFixed(1)} ${(BRUSH_OVERVIEW_HEIGHT - ((totals[i] ?? 0) / max) * BRUSH_OVERVIEW_HEIGHT).toFixed(1)}`);
|
|
392
|
+
const overviewPath = `M 0 ${BRUSH_OVERVIEW_HEIGHT} L ${overviewPoints.join(" L ")} L ${BRUSH_OVERVIEW_WIDTH} ${BRUSH_OVERVIEW_HEIGHT} Z`;
|
|
393
|
+
const commit = (startMs, endMs, anchor) => onChange(snapBrushWindow(times, startMs, endMs, anchor));
|
|
394
|
+
const beginDrag = (mode) => (e) => {
|
|
395
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
396
|
+
dragRef.current = {
|
|
397
|
+
mode,
|
|
398
|
+
originX: e.clientX,
|
|
399
|
+
originStartMs: window.startMs,
|
|
400
|
+
originEndMs: window.endMs,
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const moveDrag = (e) => {
|
|
404
|
+
const drag = dragRef.current;
|
|
405
|
+
const trackWidth = trackRef.current?.getBoundingClientRect().width ?? 0;
|
|
406
|
+
if (drag === null || trackWidth <= 0)
|
|
407
|
+
return;
|
|
408
|
+
const deltaMs = ((e.clientX - drag.originX) / trackWidth) * span;
|
|
409
|
+
if (drag.mode === "move") {
|
|
410
|
+
const shift = Math.max(firstMs - drag.originStartMs, Math.min(lastMs - drag.originEndMs, deltaMs));
|
|
411
|
+
onChange(snapMovedBrushWindow(times, drag.originStartMs, drag.originEndMs, shift));
|
|
412
|
+
}
|
|
413
|
+
else if (drag.mode === "start") {
|
|
414
|
+
commit(drag.originStartMs + deltaMs, drag.originEndMs, "start");
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
commit(drag.originStartMs, drag.originEndMs + deltaMs, "end");
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const endDrag = () => {
|
|
421
|
+
dragRef.current = null;
|
|
422
|
+
};
|
|
423
|
+
const dragHandlers = (mode) => ({
|
|
424
|
+
onPointerDown: beginDrag(mode),
|
|
425
|
+
onPointerMove: moveDrag,
|
|
426
|
+
onPointerUp: endDrag,
|
|
427
|
+
onPointerCancel: endDrag,
|
|
428
|
+
});
|
|
429
|
+
const handleKeyDown = (handle) => (e) => {
|
|
430
|
+
const isStart = handle === "start";
|
|
431
|
+
const index = isStart ? startIndex : endIndex;
|
|
432
|
+
const min = isStart ? 0 : startIndex + 1;
|
|
433
|
+
const maxIndex = isStart ? endIndex - 1 : lastIndex;
|
|
434
|
+
const target = brushKeyTarget(e.key, index, min, maxIndex);
|
|
435
|
+
if (target === undefined)
|
|
436
|
+
return;
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
const next = Math.max(min, Math.min(maxIndex, target));
|
|
439
|
+
if (next === index)
|
|
440
|
+
return;
|
|
441
|
+
const nextStart = times[isStart ? next : startIndex];
|
|
442
|
+
const nextEnd = times[isStart ? endIndex : next];
|
|
443
|
+
if (nextStart !== undefined && nextEnd !== undefined) {
|
|
444
|
+
onChange({ startMs: nextStart, endMs: nextEnd });
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const startFraction = fractionOf(window.startMs);
|
|
448
|
+
const endFraction = fractionOf(window.endMs);
|
|
449
|
+
const handleClass = "absolute top-0 z-10 h-full w-6 -translate-x-1/2 cursor-ew-resize touch-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
|
|
450
|
+
const renderHandle = (handle) => (_jsx("div", { "data-testid": `${testId}-${handle}`, role: "slider", tabIndex: 0, "aria-label": labels[handle], "aria-valuemin": handle === "start" ? 0 : startIndex + 1, "aria-valuemax": handle === "start" ? endIndex - 1 : lastIndex, "aria-valuenow": handle === "start" ? startIndex : endIndex, className: handleClass, style: { left: percent(handle === "start" ? startFraction : endFraction) }, onKeyDown: handleKeyDown(handle), ...dragHandlers(handle), children: _jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-sm bg-foreground/60" }) }));
|
|
451
|
+
return (_jsxs("div", { ref: trackRef, "data-testid": testId, className: "relative h-8 w-full select-none rounded-sm border border-border", children: [_jsx("svg", { viewBox: `0 0 ${BRUSH_OVERVIEW_WIDTH} ${BRUSH_OVERVIEW_HEIGHT}`, preserveAspectRatio: "none", className: "absolute inset-0 size-full", role: "presentation", children: _jsx("path", { d: overviewPath, fill: "var(--color-muted-foreground)", fillOpacity: 0.25 }) }), _jsx("div", { "data-testid": `${testId}-window`, className: "absolute top-0 h-full cursor-grab touch-none border-x border-primary bg-primary/10", style: {
|
|
452
|
+
left: percent(startFraction),
|
|
453
|
+
width: percent(Math.max(0, endFraction - startFraction)),
|
|
454
|
+
}, ...dragHandlers("move") }), renderHandle("start"), renderHandle("end")] }));
|
|
455
|
+
}
|
|
456
|
+
/** Stacked bands over time. Right of `todayMs` is the forecast: lighter
|
|
457
|
+
* fill plus a vertical "today" line. */
|
|
458
|
+
export function StackedAreaChart({ series: allSeries, windowStartMs: fullWindowStartMs, windowEndMs: fullWindowEndMs, todayMs, tones, colors, markers: allMarkers, lines: allLines = [], ranges, initialWindow, brush = false, windowSelection, onWindowSelectionChange, brushLabels = DEFAULT_BRUSH_LABELS, ariaLabel, todayLabel, formatBucketLabel, formatValue = String, formatAxisValue = formatValue, formatMarkerTime, dateFormat, timeZone, markerLegend = "list", markerLabels = false, scrollable = false, showLegendTotals = true, emptyContent, testId, }) {
|
|
459
|
+
const clipId = useId();
|
|
460
|
+
const scrollRef = useRef(null);
|
|
461
|
+
const [internalSelection, setInternalSelection] = useState(initialWindowSelection(ranges, initialWindow));
|
|
462
|
+
const isControlled = onWindowSelectionChange !== undefined;
|
|
463
|
+
const selection = isControlled ? windowSelection : internalSelection;
|
|
464
|
+
const changeSelection = (next) => {
|
|
465
|
+
if (isControlled)
|
|
466
|
+
onWindowSelectionChange(next);
|
|
467
|
+
else
|
|
468
|
+
setInternalSelection(next);
|
|
469
|
+
};
|
|
470
|
+
const allTimes = bucketTimes([...allSeries, ...allLines]);
|
|
471
|
+
const isWindowing = ranges !== undefined || brush || selection !== undefined;
|
|
472
|
+
const fullWindow = { startMs: fullWindowStartMs, endMs: fullWindowEndMs };
|
|
473
|
+
const visibleWindow = isWindowing
|
|
474
|
+
? resolveStackedAreaWindow({
|
|
475
|
+
selection,
|
|
476
|
+
ranges,
|
|
477
|
+
brush,
|
|
478
|
+
todayMs,
|
|
479
|
+
bucketTimes: allTimes,
|
|
480
|
+
fullWindow,
|
|
481
|
+
})
|
|
482
|
+
: fullWindow;
|
|
483
|
+
const windowStartMs = visibleWindow.startMs;
|
|
484
|
+
const windowEndMs = visibleWindow.endMs;
|
|
485
|
+
const effectiveDateFormat = dateFormat ?? stackedAreaDateFormatFor(visibleWindow);
|
|
486
|
+
const formatBucket = (atMs) => formatBucketLabel(atMs, effectiveDateFormat);
|
|
487
|
+
const tierTicks = useMemo(() => AXIS_TIERS.map((tier) => stackedAreaAxisTicks({ startMs: windowStartMs, endMs: windowEndMs }, tier.maxLabels, effectiveDateFormat, timeZone)), [windowStartMs, windowEndMs, effectiveDateFormat, timeZone]);
|
|
488
|
+
const formatMarkerTimeInWindow = formatMarkerTime === undefined
|
|
489
|
+
? undefined
|
|
490
|
+
: (atMs) => formatMarkerTime(atMs, effectiveDateFormat);
|
|
491
|
+
const isNarrowed = windowStartMs !== fullWindowStartMs || windowEndMs !== fullWindowEndMs;
|
|
492
|
+
const series = isNarrowed
|
|
493
|
+
? allSeries.map((s) => ({ ...s, points: pointsWithin(s.points, visibleWindow) }))
|
|
494
|
+
: allSeries;
|
|
495
|
+
const lines = isNarrowed
|
|
496
|
+
? allLines.map((line) => ({ ...line, points: pointsWithin(line.points, visibleWindow) }))
|
|
497
|
+
: allLines;
|
|
498
|
+
const markers = isNarrowed && allMarkers !== undefined ? pointsWithin(allMarkers, visibleWindow) : allMarkers;
|
|
499
|
+
const times = bucketTimes([...series, ...lines]);
|
|
500
|
+
const bucketCount = times.length;
|
|
501
|
+
const span = Math.max(1, windowEndMs - windowStartMs);
|
|
502
|
+
const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span));
|
|
503
|
+
const todayFraction = todayMs !== undefined ? fractionOf(todayMs) : undefined;
|
|
504
|
+
const [selectedTooltipAtMs, setSelectedTooltipAtMs] = useState(null);
|
|
505
|
+
// Stored as a time, not an index: a shifted window must not retarget the card to another bucket.
|
|
506
|
+
const tooltipAtMs = selectedTooltipAtMs !== null && times.includes(selectedTooltipAtMs)
|
|
507
|
+
? selectedTooltipAtMs
|
|
508
|
+
: undefined;
|
|
509
|
+
const tooltipOverlayRef = useRef(null);
|
|
510
|
+
const touchStartRef = useRef(null);
|
|
511
|
+
const isTooltipOpen = tooltipAtMs !== undefined;
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
if (!isTooltipOpen)
|
|
514
|
+
return;
|
|
515
|
+
const closeOnEscape = (event) => {
|
|
516
|
+
if (event.key === "Escape")
|
|
517
|
+
setSelectedTooltipAtMs(null);
|
|
518
|
+
};
|
|
519
|
+
const closeOnOutsidePointerDown = (event) => {
|
|
520
|
+
const overlay = tooltipOverlayRef.current;
|
|
521
|
+
if (overlay !== null && event.target instanceof Node && overlay.contains(event.target))
|
|
522
|
+
return;
|
|
523
|
+
setSelectedTooltipAtMs(null);
|
|
524
|
+
};
|
|
525
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
526
|
+
document.addEventListener("pointerdown", closeOnOutsidePointerDown);
|
|
527
|
+
return () => {
|
|
528
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
529
|
+
document.removeEventListener("pointerdown", closeOnOutsidePointerDown);
|
|
530
|
+
};
|
|
531
|
+
}, [isTooltipOpen]);
|
|
532
|
+
useEffect(() => {
|
|
533
|
+
const el = scrollRef.current;
|
|
534
|
+
if (!scrollable || el === null || bucketCount === 0)
|
|
535
|
+
return;
|
|
536
|
+
el.scrollLeft =
|
|
537
|
+
todayFraction === undefined
|
|
538
|
+
? el.scrollWidth
|
|
539
|
+
: Math.max(0, todayFraction * el.scrollWidth - el.clientWidth / 2);
|
|
540
|
+
}, [scrollable, todayFraction, bucketCount]);
|
|
541
|
+
if (times.length === 0)
|
|
542
|
+
return _jsx("div", { "data-testid": testId, children: emptyContent });
|
|
543
|
+
const width = 300;
|
|
544
|
+
const height = 120;
|
|
545
|
+
const totals = times.map((atMs) => series.reduce((sum, s) => sum + valueAt(s, atMs), 0));
|
|
546
|
+
const lineValues = lines.flatMap((line) => line.points.flatMap((p) => (p.value === null ? [] : [p.value])));
|
|
547
|
+
const isSingleBucket = times.length === 1;
|
|
548
|
+
if (isSingleBucket && totals[0] === 0) {
|
|
549
|
+
return _jsx("div", { "data-testid": testId, children: emptyContent });
|
|
550
|
+
}
|
|
551
|
+
// Doubling keeps a lone bucket away from the top edge of the y-scale.
|
|
552
|
+
const max = fineNiceCeil(Math.max(...totals, ...lineValues) * (isSingleBucket ? 2 : 1));
|
|
553
|
+
const yTickValues = evenYTickValues(max);
|
|
554
|
+
const yOf = (value) => height - (value / max) * height;
|
|
555
|
+
const lower = times.map(() => 0);
|
|
556
|
+
const bands = series.map((s, si) => {
|
|
557
|
+
const upper = times.map((atMs, ti) => (lower[ti] ?? 0) + valueAt(s, atMs));
|
|
558
|
+
const top = times.map((atMs, ti) => `${(fractionOf(atMs) * width).toFixed(1)} ${yOf(upper[ti] ?? 0).toFixed(1)}`);
|
|
559
|
+
const bottom = times
|
|
560
|
+
.map((atMs, ti) => `${(fractionOf(atMs) * width).toFixed(1)} ${yOf(lower[ti] ?? 0).toFixed(1)}`)
|
|
561
|
+
.reverse();
|
|
562
|
+
const bucketTop = upper[0] ?? 0;
|
|
563
|
+
const bucketHeight = bucketTop - (lower[0] ?? 0);
|
|
564
|
+
for (let ti = 0; ti < times.length; ti++)
|
|
565
|
+
lower[ti] = upper[ti] ?? 0;
|
|
566
|
+
return {
|
|
567
|
+
key: s.key,
|
|
568
|
+
color: chartColor(s.key, si, tones, colors),
|
|
569
|
+
edge: `M ${top.join(" L ")}`,
|
|
570
|
+
d: `M ${top.join(" L ")} L ${bottom.join(" L ")} Z`,
|
|
571
|
+
dot: isSingleBucket && bucketHeight > 0 ? clampDotY(yOf(bucketTop), height) : undefined,
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
const lineColorOffset = series.length;
|
|
575
|
+
const linePaths = lines.map((line, li) => ({
|
|
576
|
+
key: line.key,
|
|
577
|
+
label: line.label,
|
|
578
|
+
dashed: line.dashed === true,
|
|
579
|
+
color: chartColor(line.key, lineColorOffset + li, tones, colors),
|
|
580
|
+
d: linePathSegments(line, (atMs) => fractionOf(atMs) * width, yOf),
|
|
581
|
+
}));
|
|
582
|
+
const coloredMarkers = sortMarkers(markers ?? []).filter((marker) => marker.color !== undefined);
|
|
583
|
+
const todayX = (todayFraction ?? 1) * width;
|
|
584
|
+
const bandGradientId = (index) => `${clipId}-band-${index}`;
|
|
585
|
+
const renderBands = (fillOpacity, edgeOpacity) => bands.map((band, bi) => band.dot === undefined ? (_jsxs("g", { children: [_jsx("path", { d: band.d, fill: `url(#${bandGradientId(bi)})`, fillOpacity: fillOpacity, stroke: "none" }), _jsx("path", { "data-testid": `chart-band-edge-${band.key}`, d: band.edge, fill: "none", stroke: band.color, strokeOpacity: edgeOpacity, strokeWidth: 1.5, vectorEffect: "non-scaling-stroke" })] }, band.key)) : (_jsx("circle", { "data-testid": "stacked-area-single-point", cx: width / 2, cy: band.dot, r: SINGLE_POINT_RADIUS, fill: band.color, fillOpacity: fillOpacity }, band.key)));
|
|
586
|
+
const tooltipFraction = tooltipAtMs === undefined ? 0 : fractionOf(tooltipAtMs);
|
|
587
|
+
const tooltipRows = tooltipAtMs === undefined
|
|
588
|
+
? []
|
|
589
|
+
: [
|
|
590
|
+
...series.map((s, i) => ({
|
|
591
|
+
key: s.key,
|
|
592
|
+
label: s.label,
|
|
593
|
+
color: chartColor(s.key, i, tones, colors),
|
|
594
|
+
swatch: "area",
|
|
595
|
+
value: valueAt(s, tooltipAtMs),
|
|
596
|
+
})),
|
|
597
|
+
...lines.flatMap((line, li) => {
|
|
598
|
+
const value = line.points.find((p) => p.atMs === tooltipAtMs)?.value;
|
|
599
|
+
return value === null || value === undefined
|
|
600
|
+
? []
|
|
601
|
+
: [
|
|
602
|
+
{
|
|
603
|
+
key: line.key,
|
|
604
|
+
label: line.label,
|
|
605
|
+
color: linePaths[li]?.color ?? "var(--color-foreground)",
|
|
606
|
+
swatch: line.dashed === true ? "dashed-line" : "line",
|
|
607
|
+
value,
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
}),
|
|
611
|
+
];
|
|
612
|
+
const tooltipOnLeft = tooltipFraction > 0.5;
|
|
613
|
+
const tooltipEdge = `${percent(tooltipFraction)} ${tooltipOnLeft ? "-" : "+"} ${TOOLTIP_OFFSET_PX}px`;
|
|
614
|
+
const tooltipLeft = tooltipOnLeft
|
|
615
|
+
? `max(0px, calc(${tooltipEdge} - ${TOOLTIP_WIDTH_REM}rem))`
|
|
616
|
+
: `max(0px, min(calc(${tooltipEdge}), calc(100% - ${TOOLTIP_WIDTH_REM}rem)))`;
|
|
617
|
+
const bucketAtMsUnderPointer = (event) => {
|
|
618
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
619
|
+
return (times[bucketIndexAtFraction(times, visibleWindow, rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)] ?? null);
|
|
620
|
+
};
|
|
621
|
+
const tooltipOverlay = (_jsx("div", { ref: tooltipOverlayRef, "data-testid": "chart-tooltip-overlay", className: cn("absolute inset-x-0 top-0", STACKED_AREA_PLOT_HEIGHT_CLASS), onPointerMove: (event) => {
|
|
622
|
+
if (event.pointerType === "mouse")
|
|
623
|
+
setSelectedTooltipAtMs(bucketAtMsUnderPointer(event));
|
|
624
|
+
}, onPointerLeave: (event) => {
|
|
625
|
+
if (event.pointerType === "mouse")
|
|
626
|
+
setSelectedTooltipAtMs(null);
|
|
627
|
+
}, onPointerDown: (event) => {
|
|
628
|
+
if (event.pointerType === "mouse")
|
|
629
|
+
return;
|
|
630
|
+
touchStartRef.current = { x: event.clientX, y: event.clientY };
|
|
631
|
+
}, onPointerCancel: () => {
|
|
632
|
+
touchStartRef.current = null;
|
|
633
|
+
}, onPointerUp: (event) => {
|
|
634
|
+
const start = touchStartRef.current;
|
|
635
|
+
touchStartRef.current = null;
|
|
636
|
+
if (event.pointerType === "mouse" || start === null)
|
|
637
|
+
return;
|
|
638
|
+
const moved = Math.hypot(event.clientX - start.x, event.clientY - start.y);
|
|
639
|
+
if (moved >= TOOLTIP_TAP_MAX_MOVEMENT_PX)
|
|
640
|
+
return;
|
|
641
|
+
const next = bucketAtMsUnderPointer(event);
|
|
642
|
+
setSelectedTooltipAtMs((current) => (current === next ? null : next));
|
|
643
|
+
}, children: tooltipAtMs !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 w-px bg-foreground/40", style: { left: percent(tooltipFraction) } }), _jsxs("div", { "data-testid": "chart-tooltip", "data-side": tooltipOnLeft ? "left" : "right", className: "pointer-events-none absolute top-2 rounded-md border bg-popover p-2 text-xs text-popover-foreground shadow-sm", style: { left: tooltipLeft, width: `min(${TOOLTIP_WIDTH_REM}rem, 100%)` }, children: [_jsx("div", { className: "mb-1 font-medium", children: formatBucket(tooltipAtMs) }), tooltipRows.map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [row.swatch === "area" ? (_jsx("span", { "aria-hidden": "true", className: "size-2 shrink-0 rounded-[2px]", style: { backgroundColor: row.color } })) : (_jsx("span", { "aria-hidden": "true", className: "w-3 shrink-0 border-t-2", style: {
|
|
644
|
+
borderColor: row.color,
|
|
645
|
+
borderTopStyle: row.swatch === "dashed-line" ? "dashed" : "solid",
|
|
646
|
+
} })), _jsx("span", { className: "min-w-0 truncate text-muted-foreground", children: row.label }), _jsx("span", { className: "ml-auto tabular-nums", children: formatAxisValue(row.value) })] }, row.key)))] })] })) }));
|
|
647
|
+
const plot = (_jsxs(_Fragment, { children: [todayFraction !== undefined && (_jsx("span", { "data-testid": "chart-today-label", className: "absolute -top-4 -translate-x-1/2 text-[11px] text-foreground", style: { left: percent(todayFraction) }, children: todayLabel })), _jsxs("svg", { viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", className: cn("block w-full", STACKED_AREA_PLOT_HEIGHT_CLASS), role: "img", "aria-label": ariaLabel, children: [_jsx("title", { children: ariaLabel }), _jsxs("defs", { children: [bands.map((band, bi) => (_jsxs("linearGradient", { id: bandGradientId(bi), x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: band.color, stopOpacity: 1 }), _jsx("stop", { offset: "100%", stopColor: band.color, stopOpacity: 0.35 })] }, band.key))), _jsx("clipPath", { id: `${clipId}-past`, children: _jsx("rect", { x: 0, y: 0, width: todayX, height: height }) }), _jsx("clipPath", { id: `${clipId}-future`, children: _jsx("rect", { x: todayX, y: 0, width: width - todayX, height: height }) })] }), yTickValues.map((tickValue) => (_jsx("line", { "data-testid": "chart-grid-line", x1: 0, x2: width, y1: yOf(tickValue), y2: yOf(tickValue), stroke: "var(--color-border)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }, tickValue))), _jsx("g", { clipPath: `url(#${clipId}-past)`, children: renderBands(0.55, 0.9) }), todayFraction !== undefined && (_jsx("g", { "data-testid": "chart-forecast-region", clipPath: `url(#${clipId}-future)`, children: renderBands(0.25, 0.5) })), coloredMarkers.map((marker) => {
|
|
648
|
+
const x = fractionOf(marker.atMs) * width;
|
|
649
|
+
return (_jsx("line", { "data-testid": "chart-marker-guide", x1: x, x2: x, y1: 0, y2: height, stroke: marker.color, strokeWidth: 1, strokeDasharray: "2 3", vectorEffect: "non-scaling-stroke" }, marker.key));
|
|
650
|
+
}), linePaths.map((line) => (_jsx("path", { "data-testid": `chart-line-${line.key}`, d: line.d, fill: "none", stroke: line.color, strokeWidth: 2, strokeDasharray: line.dashed ? "6 4" : undefined, vectorEffect: "non-scaling-stroke" }, line.key))), todayFraction !== undefined && (_jsx("line", { "data-testid": "chart-today-line", x1: todayX, x2: todayX, y1: 0, y2: height, stroke: "var(--color-foreground)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }))] }), scrollable ? (_jsx("div", { "data-testid": "chart-bucket-labels", className: "relative mt-1 h-4 text-[11px] text-muted-foreground", children: times.map((atMs, i) => (_jsx("span", { className: cn("absolute whitespace-nowrap", bucketLabelTransform(i, times.length)), style: { left: percent(fractionOf(atMs)) }, children: formatBucket(atMs) }, atMs))) })) : (_jsxs("div", { className: "relative mt-1 h-4 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "absolute left-0 whitespace-nowrap", children: formatBucket(windowStartMs) }), _jsx("span", { className: "absolute right-0 whitespace-nowrap", children: formatBucket(windowEndMs) }), AXIS_TIERS.map((tier, tierIndex) => (_jsx("div", { className: cn("absolute inset-0", tier.visibilityClass), children: (tierTicks[tierIndex] ?? []).map((tickMs) => (_jsx("span", { "data-testid": "chart-axis-tick", className: "absolute -translate-x-1/2 whitespace-nowrap", style: { left: percent(fractionOf(tickMs)) }, children: formatBucket(tickMs) }, tickMs))) }, tier.maxLabels)))] })), tooltipOverlay, markerLabels &&
|
|
651
|
+
groupMarkerLabels(markers ?? []).map((group) => {
|
|
652
|
+
const fraction = fractionOf(group.atMs);
|
|
653
|
+
const flipped = fraction > MARKER_LABEL_FLIP_FRACTION;
|
|
654
|
+
return (_jsx("div", { className: cn("pointer-events-none absolute top-0 hidden @2xl:block", flipped ? "-translate-x-full pr-1" : "pl-1"), style: { left: percent(fraction) }, children: _jsx("span", { "data-testid": "chart-marker-label", className: "block max-h-44 rotate-180 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] leading-none [writing-mode:vertical-rl] @2xl:max-h-60", style: { color: group.color }, children: group.text }) }, group.atMs));
|
|
655
|
+
}), markers !== undefined && markers.length > 0 && (_jsx(MarkerPins, { markers: markers, windowStartMs: windowStartMs, windowEndMs: windowEndMs, numbered: markerLegend === "list" || markerLabels, formatMarkerTime: formatMarkerTimeInWindow, ...(markerLabels && { className: "@2xl:hidden" }) }))] }));
|
|
656
|
+
const ownRangeSwitch = !isControlled && ranges !== undefined ? (_jsx("div", { className: "flex justify-end", children: _jsx(ModeSwitch, { value: selection?.kind === "range" ? selection.value : null, options: ranges.options.map((option) => ({
|
|
657
|
+
value: option.value,
|
|
658
|
+
label: option.label,
|
|
659
|
+
})), onChange: (value) => changeSelection({ kind: "range", value }), ariaLabel: ariaLabel, variant: "pill", testId: testId !== undefined ? `${testId}-range` : undefined }) })) : undefined;
|
|
660
|
+
const brushBar = brush && allTimes.length >= 2 ? (_jsx(StackedAreaBrush, { times: allTimes, totals: allTimes.map((atMs) => allSeries.reduce((sum, s) => sum + valueAt(s, atMs), 0)), window: visibleWindow, labels: brushLabels, onChange: (next) => changeSelection({ kind: "brush", startMs: next.startMs, endMs: next.endMs }), testId: `${testId ?? "stacked-area"}-brush` })) : undefined;
|
|
661
|
+
return (_jsxs("div", { "data-testid": testId, className: "@container flex flex-col gap-3", children: [ownRangeSwitch, _jsxs("div", { className: "flex gap-2", children: [_jsx(YTicks, { values: yTickValues, formatValue: formatAxisValue, className: cn("shrink-0 pb-0", STACKED_AREA_PLOT_HEIGHT_CLASS, scrollable && "mt-4") }), scrollable ? (_jsx("div", { ref: scrollRef, "data-testid": "chart-scroll-container", className: "grow overflow-x-auto pt-4", children: _jsx("div", { "data-testid": "chart-scroll-content", className: "relative", style: {
|
|
662
|
+
minWidth: "100%",
|
|
663
|
+
width: `${times.length * SCROLLABLE_BUCKET_WIDTH_PX}px`,
|
|
664
|
+
}, children: plot }) })) : (_jsx("div", { className: "relative grow", children: plot }))] }), brushBar, _jsx(ChartLegend, { formatValue: formatValue, items: [
|
|
665
|
+
...series.map((s, i) => ({
|
|
666
|
+
key: s.key,
|
|
667
|
+
label: s.label,
|
|
668
|
+
color: chartColor(s.key, i, tones, colors),
|
|
669
|
+
...(showLegendTotals && { total: seriesTotal(s) }),
|
|
670
|
+
})),
|
|
671
|
+
...linePaths.map((line) => ({
|
|
672
|
+
key: `line:${line.key}`,
|
|
673
|
+
testIdKey: `line-${line.key}`,
|
|
674
|
+
label: line.label,
|
|
675
|
+
color: line.color,
|
|
676
|
+
swatch: line.dashed ? "dashed-line" : "line",
|
|
677
|
+
})),
|
|
678
|
+
...(markerLegend === "legend"
|
|
679
|
+
? markerLegendItems(markers ?? []).map((item) => ({
|
|
680
|
+
...item,
|
|
681
|
+
swatch: "dashed-line",
|
|
682
|
+
}))
|
|
683
|
+
: []),
|
|
684
|
+
] }), markers !== undefined && markers.length > 0 && (markerLegend === "list" || markerLabels) && (_jsx(MarkerLegend, { markers: markers, formatMarkerTime: formatMarkerTimeInWindow, ...(markerLabels && { className: "@2xl:hidden" }) }))] }));
|
|
685
|
+
}
|