From 3eb8969c8774dc626858f50b0097d6e2a5dadbb2 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 19 Jul 2026 22:31:00 -0700 Subject: [PATCH] [eric] chat: vendor tool-ui component library (21 components, MIT) behind ShowUI; scoped Tailwind v4 utilities, upstream zod contracts, React 18 ports --- backend/apps/agents/show_ui_mcp_server.py | 24 + frontend/package-lock.json | 2841 ++++++++++++++++- frontend/package.json | 14 +- .../AgentChat/tool-ui/ShowUiWidgetView.tsx | 2 + .../pages/AgentChat/tool-ui/showUiPayload.ts | 12 +- frontend/src/toolui/LICENSE.md | 21 + frontend/src/toolui/VendoredToolUi.tsx | 59 + .../toolui/components/approval-card/README.md | 19 + .../components/approval-card/_adapter.tsx | 11 + .../approval-card/approval-card.tsx | 212 ++ .../toolui/components/approval-card/index.tsx | 7 + .../toolui/components/approval-card/schema.ts | 54 + .../src/toolui/components/citation/README.md | 19 + .../toolui/components/citation/_adapter.tsx | 18 + .../components/citation/citation-list.tsx | 460 +++ .../toolui/components/citation/citation.tsx | 259 ++ .../src/toolui/components/citation/index.ts | 9 + .../src/toolui/components/citation/schema.ts | 52 + .../toolui/components/data-table/README.md | 19 + .../toolui/components/data-table/_adapter.tsx | 44 + .../components/data-table/data-table.tsx | 936 ++++++ .../components/data-table/formatters.tsx | 473 +++ .../toolui/components/data-table/index.tsx | 29 + .../toolui/components/data-table/schema.ts | 345 ++ .../src/toolui/components/data-table/types.ts | 262 ++ .../toolui/components/data-table/utilities.ts | 299 ++ .../toolui/components/image-gallery/README.md | 19 + .../components/image-gallery/_adapter.tsx | 13 + .../components/image-gallery/context.tsx | 184 ++ .../components/image-gallery/gallery-grid.tsx | 133 + .../image-gallery/gallery-lightbox.tsx | 146 + .../image-gallery/image-gallery.tsx | 75 + .../toolui/components/image-gallery/index.tsx | 6 + .../toolui/components/image-gallery/schema.ts | 59 + .../components/image-gallery/styles.css | 25 + .../src/toolui/components/image/README.md | 19 + .../src/toolui/components/image/_adapter.tsx | 11 + .../src/toolui/components/image/image.tsx | 206 ++ frontend/src/toolui/components/image/index.ts | 3 + .../src/toolui/components/image/schema.ts | 50 + .../components/instagram-post/README.md | 19 + .../components/instagram-post/_adapter.tsx | 19 + .../toolui/components/instagram-post/index.ts | 8 + .../instagram-post/instagram-post.tsx | 305 ++ .../components/instagram-post/schema.ts | 57 + .../toolui/components/item-carousel/README.md | 19 + .../components/item-carousel/_adapter.tsx | 12 + .../toolui/components/item-carousel/index.tsx | 8 + .../components/item-carousel/item-card.tsx | 110 + .../item-carousel/item-carousel.tsx | 404 +++ .../toolui/components/item-carousel/schema.ts | 77 + .../toolui/components/link-preview/README.md | 19 + .../components/link-preview/_adapter.tsx | 6 + .../toolui/components/link-preview/index.ts | 3 + .../components/link-preview/link-preview.tsx | 141 + .../toolui/components/link-preview/schema.ts | 43 + .../toolui/components/linkedin-post/README.md | 19 + .../components/linkedin-post/_adapter.tsx | 19 + .../toolui/components/linkedin-post/index.ts | 9 + .../linkedin-post/linkedin-post.tsx | 283 ++ .../toolui/components/linkedin-post/schema.ts | 59 + .../toolui/components/message-draft/README.md | 19 + .../components/message-draft/_adapter.tsx | 12 + .../toolui/components/message-draft/index.tsx | 10 + .../message-draft/message-draft.tsx | 511 +++ .../toolui/components/message-draft/schema.ts | 83 + .../toolui/components/option-list/README.md | 19 + .../components/option-list/_adapter.tsx | 14 + .../toolui/components/option-list/index.tsx | 7 + .../components/option-list/option-list.tsx | 625 ++++ .../toolui/components/option-list/schema.ts | 210 ++ .../components/option-list/selection.ts | 35 + .../toolui/components/order-summary/README.md | 19 + .../components/order-summary/_adapter.tsx | 16 + .../toolui/components/order-summary/index.tsx | 14 + .../order-summary/order-summary.tsx | 296 ++ .../toolui/components/order-summary/schema.ts | 108 + .../components/parameter-slider/README.md | 19 + .../components/parameter-slider/_adapter.tsx | 16 + .../components/parameter-slider/index.tsx | 7 + .../components/parameter-slider/math.ts | 42 + .../parameter-slider/parameter-slider.tsx | 821 +++++ .../components/parameter-slider/schema.ts | 114 + frontend/src/toolui/components/plan/README.md | 19 + .../src/toolui/components/plan/_adapter.tsx | 32 + frontend/src/toolui/components/plan/index.tsx | 7 + frontend/src/toolui/components/plan/plan.tsx | 428 +++ .../src/toolui/components/plan/progress.ts | 29 + frontend/src/toolui/components/plan/schema.ts | 69 + .../components/preferences-panel/README.md | 19 + .../components/preferences-panel/_adapter.tsx | 28 + .../components/preferences-panel/index.tsx | 10 + .../preferences-panel/preferences-panel.tsx | 681 ++++ .../components/preferences-panel/schema.ts | 144 + .../components/preferences-panel/signature.ts | 37 + .../components/progress-tracker/README.md | 19 + .../components/progress-tracker/_adapter.tsx | 10 + .../components/progress-tracker/index.tsx | 7 + .../progress-tracker/progress-tracker.tsx | 381 +++ .../components/progress-tracker/schema.ts | 76 + .../toolui/components/question-flow/README.md | 19 + .../components/question-flow/_adapter.tsx | 14 + .../toolui/components/question-flow/index.tsx | 15 + .../question-flow/question-flow.tsx | 793 +++++ .../toolui/components/question-flow/schema.ts | 131 + .../src/toolui/components/shared/README.md | 19 + .../src/toolui/components/shared/_adapter.tsx | 12 + .../components/shared/action-buttons.tsx | 100 + .../components/shared/actions-config.ts | 48 + .../src/toolui/components/shared/contract.ts | 19 + .../components/shared/decision-actions.tsx | 87 + .../components/shared/embedded-actions.ts | 17 + .../src/toolui/components/shared/index.ts | 27 + .../components/shared/local-actions.tsx | 54 + .../components/shared/media/aspect-ratio.ts | 27 + .../components/shared/media/format-utils.ts | 35 + .../toolui/components/shared/media/index.ts | 19 + .../shared/media/overlay-gradient.ts | 25 + .../shared/media/safe-navigation.ts | 23 + .../components/shared/media/sanitize-href.ts | 36 + .../src/toolui/components/shared/parse.ts | 51 + .../components/shared/pierre-dark-theme.js | 1374 ++++++++ .../components/shared/pierre-light-theme.js | 1374 ++++++++ .../src/toolui/components/shared/schema.ts | 159 + .../components/shared/tool-ui-context.tsx | 27 + .../src/toolui/components/shared/tool-ui.tsx | 89 + .../src/toolui/components/shared/toolkit.tsx | 20 + .../components/shared/use-action-buttons.tsx | 153 + .../shared/use-controllable-state.ts | 54 + .../shared/use-copy-to-clipboard.ts | 59 + .../components/shared/use-signature-reset.ts | 16 + .../src/toolui/components/shared/utils.ts | 29 + .../toolui/components/stats-display/README.md | 19 + .../components/stats-display/_adapter.tsx | 18 + .../toolui/components/stats-display/index.tsx | 10 + .../toolui/components/stats-display/schema.ts | 91 + .../components/stats-display/sparkline.tsx | 128 + .../stats-display/stats-display.tsx | 281 ++ .../src/toolui/components/terminal/README.md | 19 + .../toolui/components/terminal/_adapter.tsx | 14 + .../src/toolui/components/terminal/index.tsx | 2 + .../src/toolui/components/terminal/schema.ts | 43 + .../toolui/components/terminal/terminal.tsx | 283 ++ .../src/toolui/components/video/README.md | 19 + .../src/toolui/components/video/_adapter.tsx | 7 + .../src/toolui/components/video/context.tsx | 53 + frontend/src/toolui/components/video/index.ts | 3 + .../src/toolui/components/video/schema.ts | 50 + .../toolui/components/video/video-helpers.ts | 57 + .../src/toolui/components/video/video.tsx | 283 ++ .../weather-runtime-core.generated.ts | 67 + .../components/weather-widget/runtime.ts | 18 + .../weather-widget/schema-runtime.ts | 72 + .../weather-widget/weather-data-overlay.tsx | 586 ++++ .../weather-widget-container.tsx | 144 + .../src/toolui/components/x-post/README.md | 19 + .../src/toolui/components/x-post/_adapter.tsx | 19 + .../src/toolui/components/x-post/index.ts | 9 + .../src/toolui/components/x-post/schema.ts | 68 + .../src/toolui/components/x-post/x-post.tsx | 348 ++ frontend/src/toolui/lib/utils.ts | 6 + frontend/src/toolui/registry.tsx | 102 + frontend/src/toolui/toolui.css | 141 + frontend/src/toolui/ui/accordion.tsx | 76 + frontend/src/toolui/ui/alert.tsx | 68 + frontend/src/toolui/ui/avatar.tsx | 53 + frontend/src/toolui/ui/badge.tsx | 48 + frontend/src/toolui/ui/button-group.tsx | 83 + frontend/src/toolui/ui/button.tsx | 63 + frontend/src/toolui/ui/card.tsx | 92 + frontend/src/toolui/ui/collapsible.tsx | 39 + frontend/src/toolui/ui/dialog.tsx | 143 + frontend/src/toolui/ui/dropdown-menu.tsx | 257 ++ frontend/src/toolui/ui/input-group.tsx | 170 + frontend/src/toolui/ui/input.tsx | 21 + frontend/src/toolui/ui/item.tsx | 193 ++ frontend/src/toolui/ui/label.tsx | 24 + frontend/src/toolui/ui/popover.tsx | 48 + frontend/src/toolui/ui/radio-group.tsx | 45 + frontend/src/toolui/ui/select.tsx | 187 ++ frontend/src/toolui/ui/separator.tsx | 28 + frontend/src/toolui/ui/sheet.tsx | 139 + frontend/src/toolui/ui/skeleton.tsx | 13 + frontend/src/toolui/ui/slider.tsx | 63 + frontend/src/toolui/ui/switch.tsx | 31 + frontend/src/toolui/ui/table.tsx | 81 + frontend/src/toolui/ui/tabs.tsx | 66 + frontend/src/toolui/ui/textarea.tsx | 18 + frontend/src/toolui/ui/toggle-group.tsx | 83 + frontend/src/toolui/ui/toggle.tsx | 47 + frontend/src/toolui/ui/tooltip.tsx | 61 + frontend/tsconfig.json | 3 +- frontend/webpack.config.js | 12 +- 193 files changed, 24236 insertions(+), 24 deletions(-) create mode 100644 frontend/src/toolui/LICENSE.md create mode 100644 frontend/src/toolui/VendoredToolUi.tsx create mode 100644 frontend/src/toolui/components/approval-card/README.md create mode 100644 frontend/src/toolui/components/approval-card/_adapter.tsx create mode 100644 frontend/src/toolui/components/approval-card/approval-card.tsx create mode 100644 frontend/src/toolui/components/approval-card/index.tsx create mode 100644 frontend/src/toolui/components/approval-card/schema.ts create mode 100644 frontend/src/toolui/components/citation/README.md create mode 100644 frontend/src/toolui/components/citation/_adapter.tsx create mode 100644 frontend/src/toolui/components/citation/citation-list.tsx create mode 100644 frontend/src/toolui/components/citation/citation.tsx create mode 100644 frontend/src/toolui/components/citation/index.ts create mode 100644 frontend/src/toolui/components/citation/schema.ts create mode 100644 frontend/src/toolui/components/data-table/README.md create mode 100644 frontend/src/toolui/components/data-table/_adapter.tsx create mode 100644 frontend/src/toolui/components/data-table/data-table.tsx create mode 100644 frontend/src/toolui/components/data-table/formatters.tsx create mode 100644 frontend/src/toolui/components/data-table/index.tsx create mode 100644 frontend/src/toolui/components/data-table/schema.ts create mode 100644 frontend/src/toolui/components/data-table/types.ts create mode 100644 frontend/src/toolui/components/data-table/utilities.ts create mode 100644 frontend/src/toolui/components/image-gallery/README.md create mode 100644 frontend/src/toolui/components/image-gallery/_adapter.tsx create mode 100644 frontend/src/toolui/components/image-gallery/context.tsx create mode 100644 frontend/src/toolui/components/image-gallery/gallery-grid.tsx create mode 100644 frontend/src/toolui/components/image-gallery/gallery-lightbox.tsx create mode 100644 frontend/src/toolui/components/image-gallery/image-gallery.tsx create mode 100644 frontend/src/toolui/components/image-gallery/index.tsx create mode 100644 frontend/src/toolui/components/image-gallery/schema.ts create mode 100644 frontend/src/toolui/components/image-gallery/styles.css create mode 100644 frontend/src/toolui/components/image/README.md create mode 100644 frontend/src/toolui/components/image/_adapter.tsx create mode 100644 frontend/src/toolui/components/image/image.tsx create mode 100644 frontend/src/toolui/components/image/index.ts create mode 100644 frontend/src/toolui/components/image/schema.ts create mode 100644 frontend/src/toolui/components/instagram-post/README.md create mode 100644 frontend/src/toolui/components/instagram-post/_adapter.tsx create mode 100644 frontend/src/toolui/components/instagram-post/index.ts create mode 100644 frontend/src/toolui/components/instagram-post/instagram-post.tsx create mode 100644 frontend/src/toolui/components/instagram-post/schema.ts create mode 100644 frontend/src/toolui/components/item-carousel/README.md create mode 100644 frontend/src/toolui/components/item-carousel/_adapter.tsx create mode 100644 frontend/src/toolui/components/item-carousel/index.tsx create mode 100644 frontend/src/toolui/components/item-carousel/item-card.tsx create mode 100644 frontend/src/toolui/components/item-carousel/item-carousel.tsx create mode 100644 frontend/src/toolui/components/item-carousel/schema.ts create mode 100644 frontend/src/toolui/components/link-preview/README.md create mode 100644 frontend/src/toolui/components/link-preview/_adapter.tsx create mode 100644 frontend/src/toolui/components/link-preview/index.ts create mode 100644 frontend/src/toolui/components/link-preview/link-preview.tsx create mode 100644 frontend/src/toolui/components/link-preview/schema.ts create mode 100644 frontend/src/toolui/components/linkedin-post/README.md create mode 100644 frontend/src/toolui/components/linkedin-post/_adapter.tsx create mode 100644 frontend/src/toolui/components/linkedin-post/index.ts create mode 100644 frontend/src/toolui/components/linkedin-post/linkedin-post.tsx create mode 100644 frontend/src/toolui/components/linkedin-post/schema.ts create mode 100644 frontend/src/toolui/components/message-draft/README.md create mode 100644 frontend/src/toolui/components/message-draft/_adapter.tsx create mode 100644 frontend/src/toolui/components/message-draft/index.tsx create mode 100644 frontend/src/toolui/components/message-draft/message-draft.tsx create mode 100644 frontend/src/toolui/components/message-draft/schema.ts create mode 100644 frontend/src/toolui/components/option-list/README.md create mode 100644 frontend/src/toolui/components/option-list/_adapter.tsx create mode 100644 frontend/src/toolui/components/option-list/index.tsx create mode 100644 frontend/src/toolui/components/option-list/option-list.tsx create mode 100644 frontend/src/toolui/components/option-list/schema.ts create mode 100644 frontend/src/toolui/components/option-list/selection.ts create mode 100644 frontend/src/toolui/components/order-summary/README.md create mode 100644 frontend/src/toolui/components/order-summary/_adapter.tsx create mode 100644 frontend/src/toolui/components/order-summary/index.tsx create mode 100644 frontend/src/toolui/components/order-summary/order-summary.tsx create mode 100644 frontend/src/toolui/components/order-summary/schema.ts create mode 100644 frontend/src/toolui/components/parameter-slider/README.md create mode 100644 frontend/src/toolui/components/parameter-slider/_adapter.tsx create mode 100644 frontend/src/toolui/components/parameter-slider/index.tsx create mode 100644 frontend/src/toolui/components/parameter-slider/math.ts create mode 100644 frontend/src/toolui/components/parameter-slider/parameter-slider.tsx create mode 100644 frontend/src/toolui/components/parameter-slider/schema.ts create mode 100644 frontend/src/toolui/components/plan/README.md create mode 100644 frontend/src/toolui/components/plan/_adapter.tsx create mode 100644 frontend/src/toolui/components/plan/index.tsx create mode 100644 frontend/src/toolui/components/plan/plan.tsx create mode 100644 frontend/src/toolui/components/plan/progress.ts create mode 100644 frontend/src/toolui/components/plan/schema.ts create mode 100644 frontend/src/toolui/components/preferences-panel/README.md create mode 100644 frontend/src/toolui/components/preferences-panel/_adapter.tsx create mode 100644 frontend/src/toolui/components/preferences-panel/index.tsx create mode 100644 frontend/src/toolui/components/preferences-panel/preferences-panel.tsx create mode 100644 frontend/src/toolui/components/preferences-panel/schema.ts create mode 100644 frontend/src/toolui/components/preferences-panel/signature.ts create mode 100644 frontend/src/toolui/components/progress-tracker/README.md create mode 100644 frontend/src/toolui/components/progress-tracker/_adapter.tsx create mode 100644 frontend/src/toolui/components/progress-tracker/index.tsx create mode 100644 frontend/src/toolui/components/progress-tracker/progress-tracker.tsx create mode 100644 frontend/src/toolui/components/progress-tracker/schema.ts create mode 100644 frontend/src/toolui/components/question-flow/README.md create mode 100644 frontend/src/toolui/components/question-flow/_adapter.tsx create mode 100644 frontend/src/toolui/components/question-flow/index.tsx create mode 100644 frontend/src/toolui/components/question-flow/question-flow.tsx create mode 100644 frontend/src/toolui/components/question-flow/schema.ts create mode 100644 frontend/src/toolui/components/shared/README.md create mode 100644 frontend/src/toolui/components/shared/_adapter.tsx create mode 100644 frontend/src/toolui/components/shared/action-buttons.tsx create mode 100644 frontend/src/toolui/components/shared/actions-config.ts create mode 100644 frontend/src/toolui/components/shared/contract.ts create mode 100644 frontend/src/toolui/components/shared/decision-actions.tsx create mode 100644 frontend/src/toolui/components/shared/embedded-actions.ts create mode 100644 frontend/src/toolui/components/shared/index.ts create mode 100644 frontend/src/toolui/components/shared/local-actions.tsx create mode 100644 frontend/src/toolui/components/shared/media/aspect-ratio.ts create mode 100644 frontend/src/toolui/components/shared/media/format-utils.ts create mode 100644 frontend/src/toolui/components/shared/media/index.ts create mode 100644 frontend/src/toolui/components/shared/media/overlay-gradient.ts create mode 100644 frontend/src/toolui/components/shared/media/safe-navigation.ts create mode 100644 frontend/src/toolui/components/shared/media/sanitize-href.ts create mode 100644 frontend/src/toolui/components/shared/parse.ts create mode 100644 frontend/src/toolui/components/shared/pierre-dark-theme.js create mode 100644 frontend/src/toolui/components/shared/pierre-light-theme.js create mode 100644 frontend/src/toolui/components/shared/schema.ts create mode 100644 frontend/src/toolui/components/shared/tool-ui-context.tsx create mode 100644 frontend/src/toolui/components/shared/tool-ui.tsx create mode 100644 frontend/src/toolui/components/shared/toolkit.tsx create mode 100644 frontend/src/toolui/components/shared/use-action-buttons.tsx create mode 100644 frontend/src/toolui/components/shared/use-controllable-state.ts create mode 100644 frontend/src/toolui/components/shared/use-copy-to-clipboard.ts create mode 100644 frontend/src/toolui/components/shared/use-signature-reset.ts create mode 100644 frontend/src/toolui/components/shared/utils.ts create mode 100644 frontend/src/toolui/components/stats-display/README.md create mode 100644 frontend/src/toolui/components/stats-display/_adapter.tsx create mode 100644 frontend/src/toolui/components/stats-display/index.tsx create mode 100644 frontend/src/toolui/components/stats-display/schema.ts create mode 100644 frontend/src/toolui/components/stats-display/sparkline.tsx create mode 100644 frontend/src/toolui/components/stats-display/stats-display.tsx create mode 100644 frontend/src/toolui/components/terminal/README.md create mode 100644 frontend/src/toolui/components/terminal/_adapter.tsx create mode 100644 frontend/src/toolui/components/terminal/index.tsx create mode 100644 frontend/src/toolui/components/terminal/schema.ts create mode 100644 frontend/src/toolui/components/terminal/terminal.tsx create mode 100644 frontend/src/toolui/components/video/README.md create mode 100644 frontend/src/toolui/components/video/_adapter.tsx create mode 100644 frontend/src/toolui/components/video/context.tsx create mode 100644 frontend/src/toolui/components/video/index.ts create mode 100644 frontend/src/toolui/components/video/schema.ts create mode 100644 frontend/src/toolui/components/video/video-helpers.ts create mode 100644 frontend/src/toolui/components/video/video.tsx create mode 100644 frontend/src/toolui/components/weather-widget/generated/weather-runtime-core.generated.ts create mode 100644 frontend/src/toolui/components/weather-widget/runtime.ts create mode 100644 frontend/src/toolui/components/weather-widget/schema-runtime.ts create mode 100644 frontend/src/toolui/components/weather-widget/weather-data-overlay.tsx create mode 100644 frontend/src/toolui/components/weather-widget/weather-widget-container.tsx create mode 100644 frontend/src/toolui/components/x-post/README.md create mode 100644 frontend/src/toolui/components/x-post/_adapter.tsx create mode 100644 frontend/src/toolui/components/x-post/index.ts create mode 100644 frontend/src/toolui/components/x-post/schema.ts create mode 100644 frontend/src/toolui/components/x-post/x-post.tsx create mode 100644 frontend/src/toolui/lib/utils.ts create mode 100644 frontend/src/toolui/registry.tsx create mode 100644 frontend/src/toolui/toolui.css create mode 100644 frontend/src/toolui/ui/accordion.tsx create mode 100644 frontend/src/toolui/ui/alert.tsx create mode 100644 frontend/src/toolui/ui/avatar.tsx create mode 100644 frontend/src/toolui/ui/badge.tsx create mode 100644 frontend/src/toolui/ui/button-group.tsx create mode 100644 frontend/src/toolui/ui/button.tsx create mode 100644 frontend/src/toolui/ui/card.tsx create mode 100644 frontend/src/toolui/ui/collapsible.tsx create mode 100644 frontend/src/toolui/ui/dialog.tsx create mode 100644 frontend/src/toolui/ui/dropdown-menu.tsx create mode 100644 frontend/src/toolui/ui/input-group.tsx create mode 100644 frontend/src/toolui/ui/input.tsx create mode 100644 frontend/src/toolui/ui/item.tsx create mode 100644 frontend/src/toolui/ui/label.tsx create mode 100644 frontend/src/toolui/ui/popover.tsx create mode 100644 frontend/src/toolui/ui/radio-group.tsx create mode 100644 frontend/src/toolui/ui/select.tsx create mode 100644 frontend/src/toolui/ui/separator.tsx create mode 100644 frontend/src/toolui/ui/sheet.tsx create mode 100644 frontend/src/toolui/ui/skeleton.tsx create mode 100644 frontend/src/toolui/ui/slider.tsx create mode 100644 frontend/src/toolui/ui/switch.tsx create mode 100644 frontend/src/toolui/ui/table.tsx create mode 100644 frontend/src/toolui/ui/tabs.tsx create mode 100644 frontend/src/toolui/ui/textarea.tsx create mode 100644 frontend/src/toolui/ui/toggle-group.tsx create mode 100644 frontend/src/toolui/ui/toggle.tsx create mode 100644 frontend/src/toolui/ui/tooltip.tsx diff --git a/backend/apps/agents/show_ui_mcp_server.py b/backend/apps/agents/show_ui_mcp_server.py index dfc52719..e00504d3 100644 --- a/backend/apps/agents/show_ui_mcp_server.py +++ b/backend/apps/agents/show_ui_mcp_server.py @@ -16,6 +16,28 @@ COMPONENT_SPECS = { "plan": "props: {title?: str, steps: [{label: str, status: 'pending'|'in_progress'|'completed'}] (max 20)}", "stats": "props: {title?: str, stats: [{label: str, value: str, delta?: str, direction?: 'up'|'down'}] (max 8)}", "links": "props: {links: [{title: str, url: str, description?: str}] (max 10)}", + # tool-ui vendored set: props follow the upstream Serializable contracts (https://tool-ui.com); + # the client validates strictly and shows a validation note instead of rendering on mismatch. + "data-table": "tabular results. props: {id: str, columns: [{key: str, label: str}], data: [{: str|number|bool}]}", + "citation": "sourced claims. props: {id: str, citations: [{id: str, title: str, url?: str, snippet?: str}]}", + "item-carousel": "browsable items. props: {id: str, items: [{id: str, title: str, description?: str, imageUrl?: str, badge?: str}]}", + "link-preview": "one rich link card. props: {id: str, url: str, title: str, description?: str, imageUrl?: str, siteName?: str}", + "progress-tracker": "multi-stage progress. props: {id: str, stages: [{id: str, label: str, status: 'pending'|'active'|'complete'|'error'}]}", + "order-summary": "purchase/receipt breakdown. props: {id: str, items: [{id: str, label: str, amount: number}], total?: number, currency?: str}", + "terminal": "command output. props: {id: str, command?: str, output: str}", + "image": "single image. props: {id: str, src: str, alt?: str, caption?: str}", + "image-gallery": "several images. props: {id: str, images: [{src: str, alt?: str}]}", + "video": "video embed. props: {id: str, src: str, poster?: str, title?: str}", + "message-draft": "email/message draft for review. props: {id: str, to?: [str], subject?: str, body: str}", + "x-post": "an X/Twitter post preview. props: {id: str, author: {name: str, handle: str}, text: str}", + "linkedin-post": "a LinkedIn post preview. props: {id: str, author: {name: str, headline?: str}, text: str}", + "instagram-post": "an Instagram post preview. props: {id: str, username: str, imageUrl: str, caption?: str}", + "option-list": "choices for the user (display for now). props: {id: str, options: [{id: str, label: str, description?: str}], selectionMode?: 'single'|'multi'}", + "question-flow": "step-by-step question sequence (display for now). props follow the upstream question-flow contract", + "parameter-slider": "adjustable parameters (display for now). props: {id: str, parameters: [{id: str, label: str, min: number, max: number, value: number, step?: number}]}", + "preferences-panel": "grouped preference toggles (display for now). props follow the upstream preferences-panel contract", + "approval-card": "an approve/reject summary card. props follow the upstream approval-card contract", + "stats-display": "upstream stats-display contract (prefer 'stats' unless you need its exact shape)", } TOOLS = [ @@ -74,6 +96,8 @@ def validate(component: str, props: dict) -> str: return f"stats needs a non-empty stats list. {COMPONENT_SPECS['stats']}" if component == "links" and not (isinstance(props.get("links"), list) and props["links"]): return f"links needs a non-empty links list. {COMPONENT_SPECS['links']}" + # Vendored tool-ui components validate deeply client-side against their zod contracts; here we + # only shape-check so a wrong payload comes back as a teaching error instead of a dead render. return "" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 92d511d7..33a69a28 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,23 +20,31 @@ "@mui/material": "^7.3.9", "@reduxjs/toolkit": "^2.8.2", "@types/react-syntax-highlighter": "^15.5.13", + "ansi-to-react": "^6.2.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "codemirror": "^6.0.2", "framer-motion": "^12.35.2", "html-to-image": "^1.11.13", "lucide-react": "^1.17.0", + "radix-ui": "^1.6.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.13.1", "react-syntax-highlighter": "^16.1.1", - "remark-gfm": "^4.0.1" + "recharts": "^3.9.2", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.6.0", + "zod": "^4.4.3" }, "devDependencies": { "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", + "@tailwindcss/postcss": "^4.3.3", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/react-redux": "^7.1.34", @@ -45,15 +53,32 @@ "css-loader": "^6.8.0", "css-modules-types-loader": "^0.6.10", "html-webpack-plugin": "^5.5.0", + "postcss": "^8.5.20", + "postcss-loader": "^8.2.1", "sass": "^1.89.2", "sass-loader": "^16.0.5", "style-loader": "^3.3.0", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", "typescript": "^5.0.0", "webpack": "^5.88.0", "webpack-cli": "^5.1.0", "webpack-dev-server": "^4.15.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -2052,6 +2077,44 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3077,6 +3140,1502 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.6.tgz", + "integrity": "sha512-w9hl+724uYEgCGR3bhuRepjBtrNB/6gkhCnAf58Ke+SLbHPPQqVZZB59z60roB+5H+nh3nWTcdJhQdFMEydWmw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.12.tgz", + "integrity": "sha512-Y0zhCQ/XUdTom5hAxvE8RlXqR4hZmKGK6g2//LfgHmb88PJFOpXSh9B/7FlfYXezVY5FKGjRYWCYz5FXxZ9WZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.17.tgz", + "integrity": "sha512-l3Dmp+qPPc3SqT8+SPnxIgoWBEU2MMBxcQ7BsoRgak2UT75xY83SFvFcrUkUAWukOV3LFF+BQ9aBIFtZsIG8yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collapsible": "1.1.17", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.20.tgz", + "integrity": "sha512-Ft1W+jPqSh5BKfSTe4dpq6UYQKKQJ5Tvq3wfux+WVlg7nPwFK/3pIlHTb3Rbe+b/tNurx8YGXD9em91ujmgwuQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-dialog": "1.1.20", + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.12.tgz", + "integrity": "sha512-ltXCE0glRomMZ9+u10d9o1Go+edqa1aLxufH59JRNNM3Yz1uvaeNWSaS1HeVh1X64agtdBG5JA1W1I6ySqWiwA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.12.tgz", + "integrity": "sha512-Sok2IBJxA1XO4pU3ldzZMwUBMumIt64EY8zOUlVq5CdS+i0FrEbajVslfDB+YGWLMsrjY2kZQB0DgkrZXLZvcg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.3.tgz", + "integrity": "sha512-peavtnApRB1tABx42tHw+rPU83GSg5tXicMYO/Xi1/lqNcRsF6jkr6L7Njo7gj4q/xtDRDKBkqJvbMtoOMYWtA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.8.tgz", + "integrity": "sha512-wfN60IGuxynWK7rP4Ks2p7u9G7gqirzkAiFptuzVbsR1ot2/K+PavNUAtxiKxyRfLOvSbVfvvm9m3rFqLEXz7A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.17.tgz", + "integrity": "sha512-DJgqGsNXa0df3ifz9PFNgvgj/bzIu5QTVWCt5nQWaUkM6y0EarUv4QG4s6mCoeQdOIyVOT/Q1osFuEGub2TDXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", + "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", + "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.4.tgz", + "integrity": "sha512-eO9tkvHvo4dNwb+lytEcKWjy8c8To+ttLwNt0f9XzzsVFIaspqt3i1/c0JaaksxBB5G//zPo9CCgn39huWQyBA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-menu": "2.1.21", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.20.tgz", + "integrity": "sha512-cngVJcvK0yMvR7wICJpv+1uW3Qw4T7QM5sdbb+oE/lxOdTdvF00oaRpWUjVgmjyXe3J+xh7eZyXZlVF3g2g59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.16.tgz", + "integrity": "sha512-t45h68IjFx0ccBnPJqk0X6ecv69LkCFWd6DNCFQX56mUnVEXZbNOLCH/u9fHlAjFZ1RrFdl8/m4zev7B7NyhXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-effect-event": "0.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.21.tgz", + "integrity": "sha512-gavFM1iWLmWdxWNdGJHVeWeSQul5WE/0pxfvWWt1QnD71hyyujyMCDVacqBomaSOjdxwDzYB+Ng4+MxOvrFB1A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.21", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.13.tgz", + "integrity": "sha512-dE04aPEuP9rvKKT0d0KjSOtTEYNg6bmCYFsoSJpfC+y91Hic28ZfDCGgv6aJ+2Kw/LBXYipMZpyqVj/OD3Z8Gg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.13.tgz", + "integrity": "sha512-PopvWqiutoZh5TJXk9EV9Wh+khbp+LQ+A0H4uHocIjVcKIi6gMlBy4sAaW15thwUSc6PrR8J62nB2uM+htqrcg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-label": "2.1.12", + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.20.tgz", + "integrity": "sha512-UPmdiR8NsngWjG/y9mClzFg+Rbbpy8u0p0SKM+t7mfH4V07TiLsuylqR0RhJiRibopsawoTtMQudm/TxwHWa9w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-popper": "1.3.4", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.12.tgz", + "integrity": "sha512-dxioNQ7VOrYKKWJIxMRmJPDSWQN0gNCUy3zaqUSBwsuFAiFzI0yLGJr2q3ml07k/HlOk55N8KEfwa1ZgfprJ3w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.21.tgz", + "integrity": "sha512-2BHtaJHvvoWTECyrja1mOjN6z2dWdpeHL6b8PxqZYgex8J8xakT2KAchpZIaMwNPauIRHH/VlPJYhSSKe8lz2g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.4", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.21.tgz", + "integrity": "sha512-uQONG1qM4D8FSEt0xRs5yDpzeSWggf8lOKqHa84NvqoVoc1qJ6XN+gdkrJuQCyEY55793gLlQI3wjgWO5A/Oqg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.21", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.19.tgz", + "integrity": "sha512-58OVQUrpWx/zGVV3lxGUyAtjX4n0305Z8xIdUAq2QlFO2m2hd1eBS4x1yIVtV8bzCQJja0TJttWcwiPI6y6tmw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.13.tgz", + "integrity": "sha512-reLtbZtEBsMcqXkjd/wOga4e8t9uxzFHdX9W/j/ZfGznTNJxLGjRrDNGnGOOWcBazMH1BI/b7Cx+hblSWSD7aw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.8.tgz", + "integrity": "sha512-NH9puF7Es5Loh8vFELm+SyayzV27nyBw8kiP/uD9wbkwgq359FfbkKEvccrNk75z0LiSqC4REWk1iL9xdeWJkQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.20.tgz", + "integrity": "sha512-/PYqbsyuDkNj+IxMcRx71qNt6GelnuNulMwdCV7AtFEhUyK6XkbwreEN6CCLydMeTiDozBV4uv5aF5d12dDH7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.4", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.4.tgz", + "integrity": "sha512-PXnCa3XgTQk0FegMctxgqJXtFLZe4IFJdbUkB7jKSCKEpb6utEO4S9Vog/pkyCfEPdzM331gvE4xpztmBAfMng==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.14.tgz", + "integrity": "sha512-REwjAGPMa3J9oyDE4cuWkZbwnCbbyky66NurquQklXMSDn67cl6oGFx2gO7KZhPtFNbNw9xTWNrti3VIhgluYw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.8.tgz", + "integrity": "sha512-0hhyrQdXMaATgq4ammLG9+iPqsXxzZkgTSIxdrJHdfLnXO4Uo5L7BoO3/Xf0AEaettadGZWGGJMw6ujzQvIpGA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", + "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.13.tgz", + "integrity": "sha512-1dUdKDd63Tz9FfbTw20MVr28ohG4v7HOJ1dsavGBBPBS3KGzLOyLKiMJAC1OdgiY18nTSHpD4fULGK5gsLY/ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.4.tgz", + "integrity": "sha512-OpbUmp/korY+tjEQmHwGyQ+QQ3LBlCPC70z03Q/NSqGaHf2EijuwpjQPnswrH6cZLWyT2J6FmB+kzRoMUtPBig==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.16.tgz", + "integrity": "sha512-w7lLsTSd3940vFYEshKkHw+NGf7H0QDJPHYsy8NRjDCVbO6ZdKW1X/xoJSYHZtttnrdZiYqbN2O/2uHGB0zasw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.15.tgz", + "integrity": "sha512-JVBHNfTBbGd9hhq/xZZOgmVnBCXhLs8PJJ8vMzgwI0pLZNsKckW9pkoqHyxokUCt1hoxbwDNvF9DItEeZsG68g==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.4.tgz", + "integrity": "sha512-E2JxqAvaTUEhWtBptWo02g8FnLYPymv9ahEvW/cZQPPV4ySeyo0M8n3sXccsLUAIfMbexnfXt91qF7UjTbTMMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.4", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.8", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.12.tgz", + "integrity": "sha512-2hezgFBBR5jU3S9L9bIZ9Uag6LnvxuFBNsLCfTR8qx+NshuvFmpL4C72+5zMS3Z6UgHNSU1thOw2UaBBPEDpsQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.4.tgz", + "integrity": "sha512-8dUytW34KoJaB22ctfP7hqUCuyYa8xn2w7H8kCneeOtS5oM7UBivcnZtR8P4kPYMgdoAZlkMhE9/qkYZ5MlRzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.4.tgz", + "integrity": "sha512-7iGMj1SfZBAc6xRiy0Y3Wr/v52viQeDhOmaM3fNRyNf2nbYooZA3kKoEDGLPtrDv8JitpzcueqdZLusVMojLdQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.18.tgz", + "integrity": "sha512-1zq2XkQkK/KfbZn84edytYpOLquhNalra5LXc3NAMKhNRSGtyXqjMv6OyC9jlSuNKpqvQtsb57WKoICNk1v/sQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.20.tgz", + "integrity": "sha512-S28OtO1IvYSpWfaUBtiYCTTwRLF8doafj+a+uQw8rc8dLINS52uuG3CIPCeZc3Jfdb/S7o7HhlQxLoXlIYRu6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.15.tgz", + "integrity": "sha512-tyCejFjhJ51UKFVIG8jh9nTdRIsFPxrgrI4IdlxuJeP+AKTfTko+0gBueyBFLHqsyE71Aj9PKHjMnG+YRPyKhA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.16.tgz", + "integrity": "sha512-uil+A0Um3LaZQJkMap4nIg0VgqWc0j3iNU4AXf9a/zHOgPHNYWfVk5WVsG2296Y8HLv1bxiN7uQJblHc1+00tw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-toggle": "1.1.15", + "@radix-ui/react-use-controllable-state": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.16.tgz", + "integrity": "sha512-ZnvUAH+ftoRYzUzFQ8gqKnQ1lUFYb3amguGu+BXpfjvLIkjmXCcHCJlQeBLBlJCOtGNVtP+wHrZaUCC/zYKQMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-separator": "1.1.12", + "@radix-ui/react-toggle-group": "1.1.16" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.13.tgz", + "integrity": "sha512-56XPNYGMnGBcPyiBTaEXB7IGPybbsdNkFgSv90SCrHkXnu2Av1HhsyZMegzXlTu/QHA3V6/l22GZCv9iEoiqmQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.4", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.4.tgz", + "integrity": "sha512-cx2DixxmSfjCcEoRvDvy1NLd6SWK94XFcEEOZUcharUlXbmahFQGKCfwdKZL2ub34iIwOPOEFVF80xb+yfLYiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.3.tgz", + "integrity": "sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", + "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", + "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.8.tgz", + "integrity": "sha512-FjsQEpkNBJJYiPSat6jh2LGKLPX2jAoDVS3AZSBNX3cOUoEGhw/f+z2FCY8Cf1NkoYIbytJ1f4mlWPQpR+MjVg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", + "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "license": "MIT" + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -3115,6 +4674,277 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -3157,6 +4987,69 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -3383,7 +5276,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3851,6 +5744,12 @@ "ajv": "^8.8.2" } }, + "node_modules/anser": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", + "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", + "license": "MIT" + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -3874,6 +5773,21 @@ "node": ">=8" } }, + "node_modules/ansi-to-react": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-6.2.6.tgz", + "integrity": "sha512-Eqi0iaMK5OZ3jsVFxWvU2B74UZBnGuHlkflKMX6wTOeH+luy9KE2O0gUkc2PxhIP1R4IO0xohv62UMFInQOSeg==", + "license": "BSD-3-Clause", + "dependencies": { + "anser": "^2.3.2", + "escape-carriage": "^1.3.1", + "linkify-it": "^3.0.3" + }, + "peerDependencies": { + "react": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3901,6 +5815,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -4316,6 +6249,18 @@ "node": ">=6.0" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -4733,6 +6678,127 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4750,6 +6816,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", @@ -4822,7 +6894,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -4834,6 +6905,12 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -4990,14 +7067,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -5013,6 +7090,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/envinfo": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", @@ -5074,6 +7161,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5084,6 +7181,12 @@ "node": ">=6" } }, + "node_modules/escape-carriage": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz", + "integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==", + "license": "MIT" + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -5627,6 +7730,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6147,9 +8259,9 @@ } }, "node_modules/immer": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", - "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -6303,6 +8415,15 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -6540,12 +8661,45 @@ "node": ">= 10.13.0" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6605,12 +8759,282 @@ "shell-quote": "^1.8.3" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -6720,6 +9144,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -7774,9 +10208,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -8221,9 +10655,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", "dev": true, "funding": [ { @@ -8241,7 +10675,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -8249,6 +10683,78 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-loader": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.1.tgz", + "integrity": "sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^2.5.1", + "semver": "^7.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", @@ -8427,6 +10933,83 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/radix-ui": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.3.tgz", + "integrity": "sha512-KmhSq0NfxIwN9q6ZpEaZ+J0hiVFQcGyrPYYhbxg34q9B8CIrQoccLJ3mJ9znLRslLoaogsP2ml8JKOVoKMXgvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-accessible-icon": "1.1.12", + "@radix-ui/react-accordion": "1.2.17", + "@radix-ui/react-alert-dialog": "1.1.20", + "@radix-ui/react-arrow": "1.1.12", + "@radix-ui/react-aspect-ratio": "1.1.12", + "@radix-ui/react-avatar": "1.2.3", + "@radix-ui/react-checkbox": "1.3.8", + "@radix-ui/react-collapsible": "1.1.17", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-context-menu": "2.3.4", + "@radix-ui/react-dialog": "1.1.20", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.16", + "@radix-ui/react-dropdown-menu": "2.1.21", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.13", + "@radix-ui/react-form": "0.1.13", + "@radix-ui/react-hover-card": "1.1.20", + "@radix-ui/react-label": "2.1.12", + "@radix-ui/react-menu": "2.1.21", + "@radix-ui/react-menubar": "1.1.21", + "@radix-ui/react-navigation-menu": "1.2.19", + "@radix-ui/react-one-time-password-field": "0.1.13", + "@radix-ui/react-password-toggle-field": "0.1.8", + "@radix-ui/react-popover": "1.1.20", + "@radix-ui/react-popper": "1.3.4", + "@radix-ui/react-portal": "1.1.14", + "@radix-ui/react-presence": "1.1.8", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-progress": "1.1.13", + "@radix-ui/react-radio-group": "1.4.4", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-scroll-area": "1.2.15", + "@radix-ui/react-select": "2.3.4", + "@radix-ui/react-separator": "1.1.12", + "@radix-ui/react-slider": "1.4.4", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-switch": "1.3.4", + "@radix-ui/react-tabs": "1.1.18", + "@radix-ui/react-toast": "1.2.20", + "@radix-ui/react-toggle": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.16", + "@radix-ui/react-toolbar": "1.1.16", + "@radix-ui/react-tooltip": "1.2.13", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-escape-keydown": "1.1.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8534,6 +11117,53 @@ } } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.14.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", @@ -8572,6 +11202,28 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", @@ -8637,6 +11289,42 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recharts": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.2.tgz", + "integrity": "sha512-G4fy+Pk46RaXgwWMh+Nzhyo/lbFAVqXo9gtetlyehe6Ehge9CsgDuOTwQDD+i1+llaLktNBiNq4bhnGlDRXFtw==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -8847,9 +11535,9 @@ "license": "MIT" }, "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, "node_modules/resolve": { @@ -9598,6 +12286,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -9679,6 +12384,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -9745,6 +12456,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -9773,6 +12494,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -9952,6 +12679,49 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -10033,6 +12803,28 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -10446,6 +13238,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7eacee24..b9b9ad14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,23 +21,31 @@ "@mui/material": "^7.3.9", "@reduxjs/toolkit": "^2.8.2", "@types/react-syntax-highlighter": "^15.5.13", + "ansi-to-react": "^6.2.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "codemirror": "^6.0.2", "framer-motion": "^12.35.2", "html-to-image": "^1.11.13", "lucide-react": "^1.17.0", + "radix-ui": "^1.6.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.13.1", "react-syntax-highlighter": "^16.1.1", - "remark-gfm": "^4.0.1" + "recharts": "^3.9.2", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.6.0", + "zod": "^4.4.3" }, "devDependencies": { "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", + "@tailwindcss/postcss": "^4.3.3", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/react-redux": "^7.1.34", @@ -46,9 +54,13 @@ "css-loader": "^6.8.0", "css-modules-types-loader": "^0.6.10", "html-webpack-plugin": "^5.5.0", + "postcss": "^8.5.20", + "postcss-loader": "^8.2.1", "sass": "^1.89.2", "sass-loader": "^16.0.5", "style-loader": "^3.3.0", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", "typescript": "^5.0.0", "webpack": "^5.88.0", "webpack-cli": "^5.1.0", diff --git a/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx b/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx index e62f0b02..583a8ac3 100644 --- a/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx +++ b/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx @@ -3,6 +3,7 @@ import WeatherWidget from './WeatherWidget'; import PlanWidget from './PlanWidget'; import StatsWidget from './StatsWidget'; import LinksWidget from './LinksWidget'; +import VendoredToolUi from '@toolui/VendoredToolUi'; import type { ShowUiPayload } from './showUiPayload'; /** One switch for every surface that renders a ShowUI payload (chat bubble, pill artifact). */ @@ -11,6 +12,7 @@ function ShowUiWidgetView({ payload }: { payload: ShowUiPayload }): React.ReactE if (payload.component === 'plan') return ; if (payload.component === 'stats') return ; if (payload.component === 'links') return ; + if (payload.component === 'vendored') return ; return null; } diff --git a/frontend/src/app/pages/AgentChat/tool-ui/showUiPayload.ts b/frontend/src/app/pages/AgentChat/tool-ui/showUiPayload.ts index 21f962ee..09537faa 100644 --- a/frontend/src/app/pages/AgentChat/tool-ui/showUiPayload.ts +++ b/frontend/src/app/pages/AgentChat/tool-ui/showUiPayload.ts @@ -1,4 +1,5 @@ import type { ToolPair } from '../tool-bubbles/ToolCallBubble'; +import { isToolUiComponent } from '@toolui/registry'; export interface WeatherForecastDay { day: string; @@ -53,7 +54,8 @@ export type ShowUiPayload = | { component: 'weather'; props: WeatherProps } | { component: 'plan'; props: PlanProps } | { component: 'stats'; props: StatsProps } - | { component: 'links'; props: LinksProps }; + | { component: 'links'; props: LinksProps } + | { component: 'vendored'; name: string; props: Record }; function num(v: unknown): v is number { return typeof v === 'number' && Number.isFinite(v); @@ -89,6 +91,14 @@ export function parseShowUiPayload(pair: ToolPair): ShowUiPayload | null { function parseShowUiInput(input: unknown): ShowUiPayload | null { if (!input || typeof input !== 'object') return null; + { + const name = String((input as { component?: unknown }).component || ''); + const rawProps = (input as { props?: unknown }).props; + if (isToolUiComponent(name) && rawProps && typeof rawProps === 'object') { + // Vendored components carry their own zod contract; deep validation happens at render. + return { component: 'vendored', name, props: rawProps as Record }; + } + } const component = String((input as { component?: unknown }).component || ''); const props = (input as { props?: unknown }).props; if (!props || typeof props !== 'object') return null; diff --git a/frontend/src/toolui/LICENSE.md b/frontend/src/toolui/LICENSE.md new file mode 100644 index 00000000..1be0da05 --- /dev/null +++ b/frontend/src/toolui/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentbaseAI Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/frontend/src/toolui/VendoredToolUi.tsx b/frontend/src/toolui/VendoredToolUi.tsx new file mode 100644 index 00000000..09a33234 --- /dev/null +++ b/frontend/src/toolui/VendoredToolUi.tsx @@ -0,0 +1,59 @@ +import React, { Suspense, useEffect, useState } from 'react'; +import { useThemeMode } from '@/shared/styles/ThemeContext'; +import { TOOL_UI_REGISTRY } from './registry'; + +interface VendoredToolUiProps { + name: string; + props: Record; +} + +type Gate = 'pending' | 'ok' | 'bad'; + +/** Validates against the upstream zod contract, then renders the vendored component inside the scoped theme. */ +function VendoredToolUi({ name, props }: VendoredToolUiProps): React.ReactElement | null { + const { mode } = useThemeMode(); + const entry = TOOL_UI_REGISTRY[name]; + const [gate, setGate] = useState('pending'); + const [problem, setProblem] = useState(''); + + useEffect(() => { + let cancelled = false; + if (!entry) return undefined; + entry + .loadSchema() + .then((schema) => { + if (cancelled) return; + const result = schema.safeParse(props); + if (result.success) { + setGate('ok'); + } else { + setGate('bad'); + setProblem(result.error.issues.slice(0, 2).map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')); + } + }) + .catch(() => { if (!cancelled) { setGate('bad'); setProblem('component failed to load'); } }); + return () => { cancelled = true; }; + }, [entry, props]); + + if (!entry) return null; + if (gate === 'bad') { + return ( +
+ {name} payload didn't validate ({problem}) +
+ ); + } + if (gate === 'pending') { + return
; + } + const Component = entry.Component; + return ( +
+ }> + + +
+ ); +} + +export default VendoredToolUi; diff --git a/frontend/src/toolui/components/approval-card/README.md b/frontend/src/toolui/components/approval-card/README.md new file mode 100644 index 00000000..070bdded --- /dev/null +++ b/frontend/src/toolui/components/approval-card/README.md @@ -0,0 +1,19 @@ +# Approval Card + +Implementation for the "approval-card" Tool UI surface. + +## Files + +- public exports: components/tool-ui/approval-card/index.tsx +- serializable schema + parse helpers: components/tool-ui/approval-card/schema.ts + +## Companion assets + +- Docs page: app/docs/approval-card/content.mdx +- Preset payload: lib/presets/approval-card.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/approval-card/_adapter.tsx b/frontend/src/toolui/components/approval-card/_adapter.tsx new file mode 100644 index 00000000..770d1920 --- /dev/null +++ b/frontend/src/toolui/components/approval-card/_adapter.tsx @@ -0,0 +1,11 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + */ + +export { cn } from "@toolui/lib/utils"; +export { Separator } from "@toolui/ui/separator"; diff --git a/frontend/src/toolui/components/approval-card/approval-card.tsx b/frontend/src/toolui/components/approval-card/approval-card.tsx new file mode 100644 index 00000000..f0ea1ed9 --- /dev/null +++ b/frontend/src/toolui/components/approval-card/approval-card.tsx @@ -0,0 +1,212 @@ +"use client"; + +import * as React from "react"; +import { cn, Separator } from "./_adapter"; +import type { ApprovalCardProps, ApprovalDecision } from "./schema"; +import { ActionButtons } from "../shared/action-buttons"; +import { type Action } from "../shared/schema"; + +import { icons, Check, X } from "lucide-react"; + +type LucideIcon = React.ComponentType<{ className?: string }>; + +function getLucideIcon(name: string): LucideIcon | null { + const pascalName = name + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(""); + + const Icon = icons[pascalName as keyof typeof icons]; + return Icon ?? null; +} + +interface ApprovalCardReceiptProps { + id: string; + title: string; + choice: ApprovalDecision; + actionLabel?: string; + className?: string; +} + +function ApprovalCardReceipt({ + id, + title, + choice, + actionLabel, + className, +}: ApprovalCardReceiptProps) { + const isApproved = choice === "approved"; + const displayLabel = actionLabel ?? (isApproved ? "Approved" : "Denied"); + + return ( +
+
+ + {isApproved ? : } + +
+ {displayLabel} + {title} +
+
+
+ ); +} + +export function ApprovalCard({ + id, + title, + description, + icon, + metadata, + variant, + confirmLabel, + cancelLabel, + className, + choice, + onConfirm, + onCancel, +}: ApprovalCardProps) { + const resolvedVariant = variant ?? "default"; + const resolvedConfirmLabel = confirmLabel ?? "Approve"; + const resolvedCancelLabel = cancelLabel ?? "Deny"; + const Icon = icon ? getLucideIcon(icon) : null; + + const handleAction = React.useCallback( + async (actionId: string) => { + if (actionId === "confirm") { + await onConfirm?.(); + } else if (actionId === "cancel") { + await onCancel?.(); + } + }, + [onConfirm, onCancel], + ); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + onCancel?.(); + } + }, + [onCancel], + ); + + const isDestructive = resolvedVariant === "destructive"; + + const actions: Action[] = [ + { + id: "cancel", + label: resolvedCancelLabel, + variant: "ghost", + }, + { + id: "confirm", + label: resolvedConfirmLabel, + variant: isDestructive ? "destructive" : "default", + }, + ]; + + const viewKey = choice ? `receipt-${choice}` : "interactive"; + + return ( +
+ {choice ? ( + + ) : ( +
+
+
+ {Icon && ( + + + + )} +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+
+ + {metadata && metadata.length > 0 && ( + <> + +
+ {metadata.map((item, index) => ( +
+
+ {item.key} +
+
{item.value}
+
+ ))} +
+ + )} +
+
+ +
+
+ )} +
+ ); +} diff --git a/frontend/src/toolui/components/approval-card/index.tsx b/frontend/src/toolui/components/approval-card/index.tsx new file mode 100644 index 00000000..1bbfb6a6 --- /dev/null +++ b/frontend/src/toolui/components/approval-card/index.tsx @@ -0,0 +1,7 @@ +export { ApprovalCard } from "./approval-card"; +export { + type SerializableApprovalCard, + type ApprovalCardProps, + type ApprovalDecision, + type MetadataItem, +} from "./schema"; diff --git a/frontend/src/toolui/components/approval-card/schema.ts b/frontend/src/toolui/components/approval-card/schema.ts new file mode 100644 index 00000000..9371e726 --- /dev/null +++ b/frontend/src/toolui/components/approval-card/schema.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import { ToolUIIdSchema, ToolUIRoleSchema } from "../shared/schema"; +import { defineToolUiContract } from "../shared/contract"; + +export const MetadataItemSchema = z.object({ + key: z.string().min(1), + value: z.string(), +}); + +export type MetadataItem = z.infer; + +export const ApprovalDecisionSchema = z.enum(["approved", "denied"]); + +export type ApprovalDecision = z.infer; + +export const SerializableApprovalCardSchema = z.object({ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + + title: z.string().min(1), + description: z.string().optional(), + icon: z.string().optional(), + metadata: z.array(MetadataItemSchema).optional(), + + variant: z.enum(["default", "destructive"]).optional(), + + confirmLabel: z.string().optional(), + cancelLabel: z.string().optional(), + + choice: ApprovalDecisionSchema.optional(), +}); + +export type SerializableApprovalCard = z.infer< + typeof SerializableApprovalCardSchema +>; + +const SerializableApprovalCardSchemaContract = defineToolUiContract( + "ApprovalCard", + SerializableApprovalCardSchema, +); + +export const parseSerializableApprovalCard: ( + input: unknown, +) => SerializableApprovalCard = SerializableApprovalCardSchemaContract.parse; + +export const safeParseSerializableApprovalCard: ( + input: unknown, +) => SerializableApprovalCard | null = + SerializableApprovalCardSchemaContract.safeParse; +export interface ApprovalCardProps extends SerializableApprovalCard { + className?: string; + onConfirm?: () => void | Promise; + onCancel?: () => void | Promise; +} diff --git a/frontend/src/toolui/components/citation/README.md b/frontend/src/toolui/components/citation/README.md new file mode 100644 index 00000000..e248e34d --- /dev/null +++ b/frontend/src/toolui/components/citation/README.md @@ -0,0 +1,19 @@ +# Citation + +Implementation for the "citation" Tool UI surface. + +## Files + +- public exports: components/tool-ui/citation/index.ts +- serializable schema + parse helpers: components/tool-ui/citation/schema.ts + +## Companion assets + +- Docs page: app/docs/citation/content.mdx +- Preset payload: lib/presets/citation.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/citation/_adapter.tsx b/frontend/src/toolui/components/citation/_adapter.tsx new file mode 100644 index 00000000..33f4cdc4 --- /dev/null +++ b/frontend/src/toolui/components/citation/_adapter.tsx @@ -0,0 +1,18 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Tooltip → shadcn/ui Tooltip (only needed for variant="inline") + * Popover → shadcn/ui Popover (only needed for CitationList) + */ +"use client"; + +export { cn } from "@toolui/lib/utils"; +export { + Popover, + PopoverContent, + PopoverTrigger, +} from "@toolui/ui/popover"; diff --git a/frontend/src/toolui/components/citation/citation-list.tsx b/frontend/src/toolui/components/citation/citation-list.tsx new file mode 100644 index 00000000..db586c7e --- /dev/null +++ b/frontend/src/toolui/components/citation/citation-list.tsx @@ -0,0 +1,460 @@ +"use client"; + +import * as React from "react"; +import type { LucideIcon } from "lucide-react"; +import { + FileText, + Globe, + Code2, + Newspaper, + Database, + File, + ExternalLink, +} from "lucide-react"; +import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter"; +import { Citation } from "./citation"; +import type { + SerializableCitation, + CitationType, + CitationVariant, +} from "./schema"; +import { + openSafeNavigationHref, + resolveSafeNavigationHref, +} from "../shared/media"; + +const TYPE_ICONS: Record = { + webpage: Globe, + document: FileText, + article: Newspaper, + api: Database, + code: Code2, + other: File, +}; + +function useHoverPopover(delay = 100) { + const [open, setOpen] = React.useState(false); + const timeoutRef = React.useRef | null>(null); + const containerRef = React.useRef(null); + + const handleMouseEnter = React.useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setOpen(true), delay); + }, [delay]); + + const handleMouseLeave = React.useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setOpen(false), delay); + }, [delay]); + + const handleFocus = React.useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setOpen(true); + }, []); + + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + const relatedTarget = e.relatedTarget as HTMLElement | null; + if (containerRef.current?.contains(relatedTarget)) { + return; + } + if (relatedTarget?.closest("[data-radix-popper-content-wrapper]")) { + return; + } + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setOpen(false), delay); + }, + [delay], + ); + + React.useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + return { + open, + setOpen, + containerRef, + handleMouseEnter, + handleMouseLeave, + handleFocus, + handleBlur, + }; +} + +export interface CitationListProps { + id: string; + citations: SerializableCitation[]; + variant?: CitationVariant; + maxVisible?: number; + className?: string; + onNavigate?: (href: string, citation: SerializableCitation) => void; +} + +export function CitationList(props: CitationListProps) { + const { + id, + citations, + variant = "default", + maxVisible, + className, + onNavigate, + } = props; + + const shouldTruncate = + maxVisible !== undefined && citations.length > maxVisible; + const visibleCitations = shouldTruncate + ? citations.slice(0, maxVisible) + : citations; + const overflowCitations = shouldTruncate ? citations.slice(maxVisible) : []; + const overflowCount = overflowCitations.length; + + const wrapperClass = + variant === "inline" + ? "flex flex-wrap items-center gap-1.5" + : "flex flex-col gap-2"; + + // Stacked variant: overlapping favicons with popover + if (variant === "stacked") { + return ( + + ); + } + + if (variant === "default") { + return ( +
+ {visibleCitations.map((citation) => ( + + ))} + {shouldTruncate && ( + + )} +
+ ); + } + + return ( +
+ {visibleCitations.map((citation) => ( + + ))} + {shouldTruncate && ( + + )} +
+ ); +} + +interface OverflowIndicatorProps { + citations: SerializableCitation[]; + count: number; + variant: CitationVariant; + onNavigate?: (href: string, citation: SerializableCitation) => void; +} + +function OverflowIndicator({ + citations, + count, + variant, + onNavigate, +}: OverflowIndicatorProps) { + const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover(); + + const handleClick = (citation: SerializableCitation) => { + const href = resolveSafeNavigationHref(citation.href); + if (!href) return; + if (onNavigate) { + onNavigate(href, citation); + } else { + openSafeNavigationHref(href); + } + }; + + const popoverContent = ( +
+ {citations.map((citation) => ( + handleClick(citation)} + /> + ))} +
+ ); + + if (variant === "inline") { + return ( + + + + + e.preventDefault()} + > + {popoverContent} + + + ); + } + + // Default variant + return ( + + + + + e.preventDefault()} + > + {popoverContent} + + + ); +} + +interface OverflowItemProps { + citation: SerializableCitation; + onClick: () => void; +} + +function OverflowItem({ citation, onClick }: OverflowItemProps) { + const TypeIcon = TYPE_ICONS[citation.type ?? "webpage"] ?? Globe; + + return ( + + ); +} + +interface StackedCitationsProps { + id: string; + citations: SerializableCitation[]; + className?: string; + onNavigate?: (href: string, citation: SerializableCitation) => void; +} + +function StackedCitations({ + id, + citations, + className, + onNavigate, +}: StackedCitationsProps) { + const { + open, + setOpen, + containerRef, + handleMouseEnter, + handleMouseLeave, + handleBlur, + } = useHoverPopover(); + const maxIcons = 4; + const visibleCitations = citations.slice(0, maxIcons); + const remainingCount = Math.max(0, citations.length - maxIcons); + + const handleClick = (citation: SerializableCitation) => { + const href = resolveSafeNavigationHref(citation.href); + if (!href) return; + if (onNavigate) { + onNavigate(href, citation); + } else { + openSafeNavigationHref(href); + } + }; + + return ( +
+ + + + + setOpen(false)} + > +
+ {citations.map((citation) => ( + handleClick(citation)} + /> + ))} +
+
+
+
+ ); +} diff --git a/frontend/src/toolui/components/citation/citation.tsx b/frontend/src/toolui/components/citation/citation.tsx new file mode 100644 index 00000000..551f82a7 --- /dev/null +++ b/frontend/src/toolui/components/citation/citation.tsx @@ -0,0 +1,259 @@ +"use client"; + +import * as React from "react"; +import type { LucideIcon } from "lucide-react"; +import { + FileText, + Globe, + Code2, + Newspaper, + Database, + File, + ExternalLink, +} from "lucide-react"; +import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter"; + +import { openSafeNavigationHref, sanitizeHref } from "../shared/media"; +import type { + SerializableCitation, + CitationType, + CitationVariant, +} from "./schema"; + +const FALLBACK_LOCALE = "en-US"; + +const TYPE_ICONS: Record = { + webpage: Globe, + document: FileText, + article: Newspaper, + api: Database, + code: Code2, + other: File, +}; + +function extractDomain(url: string): string | undefined { + try { + const urlObj = new URL(url); + return urlObj.hostname.replace(/^www\./, ""); + } catch { + return undefined; + } +} + +function formatDate(isoString: string, locale: string): string { + try { + const date = new Date(isoString); + return date.toLocaleDateString(locale, { + year: "numeric", + month: "short", + }); + } catch { + return isoString; + } +} + +function useHoverPopover(delay = 100) { + const [open, setOpen] = React.useState(false); + const timeoutRef = React.useRef | null>(null); + + const handleMouseEnter = React.useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setOpen(true), delay); + }, [delay]); + + const handleMouseLeave = React.useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setOpen(false), delay); + }, [delay]); + + React.useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + return { open, setOpen, handleMouseEnter, handleMouseLeave }; +} + +export interface CitationProps extends SerializableCitation { + variant?: CitationVariant; + className?: string; + onNavigate?: (href: string, citation: SerializableCitation) => void; +} + +export function Citation(props: CitationProps) { + const { variant = "default", className, onNavigate, ...serializable } = props; + + const { + id, + href: rawHref, + title, + snippet, + domain: providedDomain, + favicon, + author, + publishedAt, + type = "webpage", + locale: providedLocale, + } = serializable; + + const locale = providedLocale ?? FALLBACK_LOCALE; + const sanitizedHref = sanitizeHref(rawHref); + const domain = providedDomain ?? extractDomain(rawHref); + + const citationData: SerializableCitation = { + ...serializable, + href: sanitizedHref ?? rawHref, + domain, + locale, + }; + + const TypeIcon = TYPE_ICONS[type] ?? Globe; + + const handleClick = () => { + if (!sanitizedHref) return; + if (onNavigate) { + onNavigate(sanitizedHref, citationData); + } else { + openSafeNavigationHref(sanitizedHref); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (sanitizedHref && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + handleClick(); + } + }; + + const iconElement = favicon ? ( + + ) : ( +
+ )} + + + ); +} diff --git a/frontend/src/toolui/components/item-carousel/item-carousel.tsx b/frontend/src/toolui/components/item-carousel/item-carousel.tsx new file mode 100644 index 00000000..6c0b601b --- /dev/null +++ b/frontend/src/toolui/components/item-carousel/item-carousel.tsx @@ -0,0 +1,404 @@ +"use client"; + +import { useRef, useState, useEffect, useCallback } from "react"; +import { cn, Button, Card, ChevronLeft, ChevronRight } from "./_adapter"; +import { ItemCard } from "./item-card"; +import { prefersReducedMotion } from "../shared/utils"; +import type { ItemCarouselProps } from "./schema"; + +const SCROLL_PADDING_STYLE = { scrollPaddingInline: "1rem" }; + +const SCROLL_EDGE_THRESHOLD_PX = 8; +const SNAP_EPSILON_PX = 5; +const SCROLL_ANIMATION_DURATION_MS = 300; +const PAGE_SCROLL_RATIO = 0.8; +const PAGE_SCROLL_BREAKPOINT_PX = 640; + +type ScrollDirection = "left" | "right"; + +interface ScrollAnimationState { + target: number; + start: number; + startTime: number; + duration: number; + onComplete?: () => void; +} + +function useSmoothScroll() { + const animationRef = useRef(null); + const frameRef = useRef(null); + + const cancelAnimation = useCallback(() => { + if (frameRef.current !== null) { + cancelAnimationFrame(frameRef.current); + frameRef.current = null; + } + animationRef.current = null; + }, []); + + useEffect(() => cancelAnimation, [cancelAnimation]); + + const scrollTo = useCallback( + ( + element: HTMLElement, + target: number, + duration = SCROLL_ANIMATION_DURATION_MS, + onComplete?: () => void, + ) => { + if (prefersReducedMotion() || duration <= 0) { + element.scrollLeft = target; + onComplete?.(); + return; + } + + cancelAnimation(); + + animationRef.current = { + target, + start: element.scrollLeft, + startTime: performance.now(), + duration, + onComplete, + }; + + element.style.scrollSnapType = "none"; + + const step = () => { + const anim = animationRef.current; + if (!anim) return; + + const elapsed = performance.now() - anim.startTime; + const progress = Math.min(elapsed / anim.duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + + element.scrollLeft = anim.start + (anim.target - anim.start) * eased; + + if (progress < 1) { + frameRef.current = requestAnimationFrame(step); + return; + } + + element.scrollLeft = anim.target; + const callback = anim.onComplete; + cancelAnimation(); + + requestAnimationFrame(() => { + element.style.scrollSnapType = ""; + callback?.(); + }); + }; + + frameRef.current = requestAnimationFrame(step); + }, + [cancelAnimation], + ); + + const isAnimating = useCallback( + () => animationRef.current !== null && frameRef.current !== null, + [], + ); + + return { scrollTo, isAnimating, cancelAnimation }; +} + +function useScrollEdgeState( + scrollRef: React.RefObject, + itemCount: number, +) { + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + const updateState = useCallback(() => { + const container = scrollRef.current; + if (!container) return; + + const scrollLeft = Math.round(container.scrollLeft); + const maxScroll = Math.max( + 0, + Math.round(container.scrollWidth - container.clientWidth), + ); + + setCanScrollLeft(scrollLeft > SCROLL_EDGE_THRESHOLD_PX); + setCanScrollRight(scrollLeft < maxScroll - SCROLL_EDGE_THRESHOLD_PX); + }, [scrollRef]); + + useEffect(() => { + const container = scrollRef.current; + if (!container) return; + + let rafId: number | null = null; + + const scheduleUpdate = () => { + if (rafId !== null) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(() => { + rafId = null; + updateState(); + }); + }; + + scheduleUpdate(); + + container.addEventListener("scroll", scheduleUpdate, { passive: true }); + const resizeObserver = new ResizeObserver(scheduleUpdate); + resizeObserver.observe(container); + + return () => { + container.removeEventListener("scroll", scheduleUpdate); + resizeObserver.disconnect(); + if (rafId !== null) cancelAnimationFrame(rafId); + }; + }, [scrollRef, updateState, itemCount]); + + return { canScrollLeft, canScrollRight }; +} + +function CarouselNavButton({ + direction, + visible, + onClick, +}: { + direction: ScrollDirection; + visible: boolean; + onClick: () => void; +}) { + const isLeft = direction === "left"; + const Icon = isLeft ? ChevronLeft : ChevronRight; + + return ( + + ); +} + +interface ItemCarouselHeaderProps { + title?: string; + description?: string; +} + +function ItemCarouselHeader({ title, description }: ItemCarouselHeaderProps) { + if (!title && !description) return null; + + return ( +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ ); +} + +interface EmptyStateProps { + id: string; + className?: string; +} + +function EmptyState({ id, className }: EmptyStateProps) { + return ( + +

No items to display

+
+ ); +} + +function ItemCarouselRoot({ + id, + title, + description, + items, + className, + onItemClick, + onItemAction, +}: ItemCarouselProps) { + const scrollRef = useRef(null); + const targetIndexRef = useRef(null); + + const { scrollTo, isAnimating } = useSmoothScroll(); + const { canScrollLeft, canScrollRight } = useScrollEdgeState( + scrollRef, + items.length, + ); + + const scroll = useCallback( + (direction: ScrollDirection) => { + const container = scrollRef.current; + if (!container) return; + + const paddingValue = window.getComputedStyle(container).scrollPaddingLeft; + const scrollPaddingLeft = Number.isFinite(Number.parseFloat(paddingValue)) + ? Number.parseFloat(paddingValue) + : 0; + + const itemElements = Array.from( + container.querySelectorAll("[data-carousel-item]"), + ); + if (itemElements.length === 0) return; + + const snapPositions = itemElements.map((el) => + Math.max(0, el.offsetLeft - scrollPaddingLeft), + ); + + const scrollLeft = Math.round(container.scrollLeft); + let currentIndex: number; + if (isAnimating()) { + currentIndex = Math.min( + targetIndexRef.current ?? 0, + snapPositions.length - 1, + ); + } else { + currentIndex = snapPositions.length - 1; + for (let i = 0; i < snapPositions.length; i++) { + const snap = snapPositions[i]; + if (Math.abs(snap - scrollLeft) < SNAP_EPSILON_PX) { + currentIndex = i; + break; + } + if (snap > scrollLeft) { + currentIndex = Math.max(0, i - 1); + break; + } + } + } + + const itemStep = + itemElements.length > 1 + ? itemElements[1].offsetLeft - itemElements[0].offsetLeft + : 0; + const safeStep = + itemStep > 0 ? itemStep : itemElements[0].offsetWidth || 1; + + const pageIndexStep = + container.clientWidth >= PAGE_SCROLL_BREAKPOINT_PX + ? Math.max( + 1, + Math.floor( + (container.clientWidth * PAGE_SCROLL_RATIO) / safeStep, + ), + ) + : 1; + + const targetIndex = + direction === "right" + ? Math.min(currentIndex + pageIndexStep, itemElements.length - 1) + : Math.max(currentIndex - pageIndexStep, 0); + + targetIndexRef.current = targetIndex; + const targetScrollLeft = snapPositions[targetIndex]; + + if (Math.abs(targetScrollLeft - container.scrollLeft) > 1) { + scrollTo( + container, + targetScrollLeft, + SCROLL_ANIMATION_DURATION_MS, + () => { + targetIndexRef.current = null; + }, + ); + } + }, + [scrollTo, isAnimating], + ); + + const handleScrollLeft = useCallback(() => scroll("left"), [scroll]); + const handleScrollRight = useCallback(() => scroll("right"), [scroll]); + + if (items.length === 0) { + return ; + } + + return ( +
+ + +
+ + + +
+ {items.map((item) => ( +
+ +
+ ))} +
+
+
+ ); +} + +type ItemCarouselComponent = typeof ItemCarouselRoot & { + Root: typeof ItemCarouselRoot; + Header: typeof ItemCarouselHeader; + EmptyState: typeof EmptyState; + NavButton: typeof CarouselNavButton; + Card: typeof ItemCard; +}; + +export const ItemCarousel = Object.assign(ItemCarouselRoot, { + Root: ItemCarouselRoot, + Header: ItemCarouselHeader, + EmptyState, + NavButton: CarouselNavButton, + Card: ItemCard, +}) as ItemCarouselComponent; diff --git a/frontend/src/toolui/components/item-carousel/schema.ts b/frontend/src/toolui/components/item-carousel/schema.ts new file mode 100644 index 00000000..d20a72b5 --- /dev/null +++ b/frontend/src/toolui/components/item-carousel/schema.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; +import { defineToolUiContract } from "../shared/contract"; +import { + ActionSchema, + SerializableActionSchema, + ToolUIIdSchema, +} from "../shared/schema"; + +export const ItemSchema = z.object({ + id: z.string().min(1), + name: z.string().min(1), + subtitle: z.string().optional(), + image: z.url().optional(), + color: z.string().optional(), + actions: z.array(ActionSchema).optional(), +}); + +export const ItemCarouselPropsSchema = z.object({ + id: ToolUIIdSchema, + title: z.string().optional(), + description: z.string().optional(), + items: z.array(ItemSchema), + className: z.string().optional(), +}); + +export type Item = z.infer; + +export type ItemCarouselProps = z.infer & { + onItemClick?: (itemId: string) => void; + onItemAction?: (itemId: string, actionId: string) => void; +}; + +export const SerializableItemSchema = ItemSchema.extend({ + actions: z.array(SerializableActionSchema).optional(), +}); + +export const SerializableItemCarouselSchema = ItemCarouselPropsSchema.omit({ + className: true, +}) + .extend({ + items: z.array(SerializableItemSchema), + }) + .superRefine((payload, ctx) => { + const seenItemIds = new Map(); + + payload.items.forEach((item, index) => { + const firstSeenAt = seenItemIds.get(item.id); + if (firstSeenAt !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["items", index, "id"], + message: `duplicate item id '${item.id}' (first seen at index ${firstSeenAt})`, + }); + return; + } + seenItemIds.set(item.id, index); + }); + }); + +export type SerializableItem = z.infer; +export type SerializableItemCarousel = z.infer< + typeof SerializableItemCarouselSchema +>; + +const SerializableItemCarouselSchemaContract = defineToolUiContract( + "ItemCarousel", + SerializableItemCarouselSchema, +); + +export const parseSerializableItemCarousel: ( + input: unknown, +) => SerializableItemCarousel = SerializableItemCarouselSchemaContract.parse; + +export const safeParseSerializableItemCarousel: ( + input: unknown, +) => SerializableItemCarousel | null = + SerializableItemCarouselSchemaContract.safeParse; diff --git a/frontend/src/toolui/components/link-preview/README.md b/frontend/src/toolui/components/link-preview/README.md new file mode 100644 index 00000000..0fab3dbb --- /dev/null +++ b/frontend/src/toolui/components/link-preview/README.md @@ -0,0 +1,19 @@ +# Link Preview + +Implementation for the "link-preview" Tool UI surface. + +## Files + +- public exports: components/tool-ui/link-preview/index.ts +- serializable schema + parse helpers: components/tool-ui/link-preview/schema.ts + +## Companion assets + +- Docs page: app/docs/link-preview/content.mdx +- Preset payload: lib/presets/link-preview.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/link-preview/_adapter.tsx b/frontend/src/toolui/components/link-preview/_adapter.tsx new file mode 100644 index 00000000..ac928498 --- /dev/null +++ b/frontend/src/toolui/components/link-preview/_adapter.tsx @@ -0,0 +1,6 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + */ +"use client"; + +export { cn } from "@toolui/lib/utils"; diff --git a/frontend/src/toolui/components/link-preview/index.ts b/frontend/src/toolui/components/link-preview/index.ts new file mode 100644 index 00000000..cea9fe77 --- /dev/null +++ b/frontend/src/toolui/components/link-preview/index.ts @@ -0,0 +1,3 @@ +export { LinkPreview } from "./link-preview"; +export type { LinkPreviewProps } from "./link-preview"; +export type { SerializableLinkPreview } from "./schema"; diff --git a/frontend/src/toolui/components/link-preview/link-preview.tsx b/frontend/src/toolui/components/link-preview/link-preview.tsx new file mode 100644 index 00000000..9d2ae161 --- /dev/null +++ b/frontend/src/toolui/components/link-preview/link-preview.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { Globe } from "lucide-react"; +import { cn } from "./_adapter"; + +import { + RATIO_CLASS_MAP, + getFitClass, + openSafeNavigationHref, + sanitizeHref, +} from "../shared/media"; +import type { SerializableLinkPreview } from "./schema"; + +const FALLBACK_LOCALE = "en-US"; +const CONTENT_SPACING = "px-5 py-4 gap-2"; + +export interface LinkPreviewProps extends SerializableLinkPreview { + className?: string; + onNavigate?: (href: string, preview: SerializableLinkPreview) => void; +} + +export function LinkPreview(props: LinkPreviewProps) { + const { className, onNavigate, ...serializable } = props; + + const { + id, + href: rawHref, + title, + description, + image, + domain, + favicon, + ratio = "16:9", + fit = "cover", + locale: providedLocale, + } = serializable; + + const locale = providedLocale ?? FALLBACK_LOCALE; + const sanitizedHref = sanitizeHref(rawHref); + + const previewData: SerializableLinkPreview = { + ...serializable, + href: sanitizedHref ?? rawHref, + locale, + }; + + const handleClick = () => { + if (!sanitizedHref) return; + if (onNavigate) { + onNavigate(sanitizedHref, previewData); + } else { + openSafeNavigationHref(sanitizedHref); + } + }; + + return ( +
+
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleClick(); + } + } + : undefined + } + > +
+ {image && ( +
+ +
+ )} +
+ {domain && ( +
+ {favicon ? ( + + ) : ( +
+
+ )} + {domain} +
+ )} + {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/toolui/components/link-preview/schema.ts b/frontend/src/toolui/components/link-preview/schema.ts new file mode 100644 index 00000000..3bde91b5 --- /dev/null +++ b/frontend/src/toolui/components/link-preview/schema.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import { defineToolUiContract } from "../shared/contract"; +import { + ToolUIIdSchema, + ToolUIReceiptSchema, + ToolUIRoleSchema, +} from "../shared/schema"; + +import { AspectRatioSchema, MediaFitSchema } from "../shared/media"; + +export const SerializableLinkPreviewSchema = z.object({ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + receipt: ToolUIReceiptSchema.optional(), + href: z.url(), + title: z.string().optional(), + description: z.string().optional(), + image: z.url().optional(), + domain: z.string().optional(), + favicon: z.url().optional(), + ratio: AspectRatioSchema.optional(), + fit: MediaFitSchema.optional(), + createdAt: z.string().datetime().optional(), + locale: z.string().optional(), +}); + +export type SerializableLinkPreview = z.infer< + typeof SerializableLinkPreviewSchema +>; + +const SerializableLinkPreviewSchemaContract = defineToolUiContract( + "LinkPreview", + SerializableLinkPreviewSchema, +); + +export const parseSerializableLinkPreview: ( + input: unknown, +) => SerializableLinkPreview = SerializableLinkPreviewSchemaContract.parse; + +export const safeParseSerializableLinkPreview: ( + input: unknown, +) => SerializableLinkPreview | null = + SerializableLinkPreviewSchemaContract.safeParse; diff --git a/frontend/src/toolui/components/linkedin-post/README.md b/frontend/src/toolui/components/linkedin-post/README.md new file mode 100644 index 00000000..1869eff8 --- /dev/null +++ b/frontend/src/toolui/components/linkedin-post/README.md @@ -0,0 +1,19 @@ +# Linkedin Post + +Implementation for the "linkedin-post" Tool UI surface. + +## Files + +- public exports: components/tool-ui/linkedin-post/index.ts +- serializable schema + parse helpers: components/tool-ui/linkedin-post/schema.ts + +## Companion assets + +- Docs page: app/docs/social-post/content.mdx +- Preset payload: lib/presets/linkedin-post.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/linkedin-post/_adapter.tsx b/frontend/src/toolui/components/linkedin-post/_adapter.tsx new file mode 100644 index 00000000..c314b96c --- /dev/null +++ b/frontend/src/toolui/components/linkedin-post/_adapter.tsx @@ -0,0 +1,19 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Button → shadcn/ui Button + * Tooltip → shadcn/ui Tooltip + */ + +export { cn } from "@toolui/lib/utils"; +export { Button } from "@toolui/ui/button"; +export { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@toolui/ui/tooltip"; diff --git a/frontend/src/toolui/components/linkedin-post/index.ts b/frontend/src/toolui/components/linkedin-post/index.ts new file mode 100644 index 00000000..2fd27172 --- /dev/null +++ b/frontend/src/toolui/components/linkedin-post/index.ts @@ -0,0 +1,9 @@ +export { LinkedInPost } from "./linkedin-post"; +export type { LinkedInPostProps } from "./linkedin-post"; +export type { + LinkedInPostData, + LinkedInPostAuthor, + LinkedInPostMedia, + LinkedInPostLinkPreview, + LinkedInPostStats, +} from "./schema"; diff --git a/frontend/src/toolui/components/linkedin-post/linkedin-post.tsx b/frontend/src/toolui/components/linkedin-post/linkedin-post.tsx new file mode 100644 index 00000000..51fcd221 --- /dev/null +++ b/frontend/src/toolui/components/linkedin-post/linkedin-post.tsx @@ -0,0 +1,283 @@ +"use client"; + +import * as React from "react"; +import { ThumbsUp, Share } from "lucide-react"; +import { + cn, + Button, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./_adapter"; +import { formatCount, formatRelativeTime, getDomain } from "../shared/utils"; + +import { resolveSafeNavigationHref } from "../shared/media"; +import type { + LinkedInPostData, + LinkedInPostMedia, + LinkedInPostLinkPreview, +} from "./schema"; + +const TEXT_PREVIEW_LENGTH = 280; + +export interface LinkedInPostProps { + post: LinkedInPostData; + className?: string; + onAction?: (action: string, post: LinkedInPostData) => void; +} + +function LinkedInLogo({ className }: { className?: string }) { + return ( + + + + + + + ); +} + +function Header({ + author, + createdAt, +}: { + author: LinkedInPostData["author"]; + createdAt?: string; +}) { + return ( +
+ {`${author.name} +
+ {author.name} + {author.headline && ( + + {author.headline} + + )} + {createdAt && ( +
+ {formatRelativeTime(createdAt)} + · + Edited +
+ )} +
+ +
+ ); +} + +function PostBody({ text }: { text?: string }) { + const [isExpanded, setIsExpanded] = React.useState(false); + const shouldTruncate = text && text.length > TEXT_PREVIEW_LENGTH; + + if (!text) return null; + + return ( +
+ {shouldTruncate && !isExpanded ? ( + <> + {text.slice(0, TEXT_PREVIEW_LENGTH)} + ... + + + ) : ( + text + )} +
+ ); +} + +function PostMedia({ media }: { media: LinkedInPostMedia }) { + return ( +
+ {media.type === "image" ? ( + {media.alt} + ) : ( +
+ ); +} + +function PostLinkPreview({ preview }: { preview: LinkedInPostLinkPreview }) { + const href = resolveSafeNavigationHref(preview.url); + const domain = preview.domain ?? getDomain(preview.url); + const content = ( + <> + {preview.imageUrl && ( + + )} +
+ {preview.title && ( +
+ {preview.title} +
+ )} + {domain && ( +
{domain}
+ )} +
+ + ); + + if (!href) { + return ( +
{content}
+ ); + } + + return ( + + {content} + + ); +} + +function ActionButton({ + icon: Icon, + label, + count, + active, + hoverColor, + activeColor, + onClick, +}: { + icon: React.ComponentType<{ className?: string }>; + label: string; + count?: number; + active?: boolean; + hoverColor: string; + activeColor?: string; + onClick: () => void; +}) { + return ( + + + + + {label} + + ); +} + +function PostActions({ + stats, + onAction, +}: { + stats?: LinkedInPostData["stats"]; + onAction: (action: string) => void; +}) { + return ( + +
+ onAction("like")} + /> + onAction("share")} + /> +
+
+ ); +} + +export function LinkedInPost({ post, className, onAction }: LinkedInPostProps) { + return ( +
+
+
+ + + {post.media && } + + {post.linkPreview && !post.media && ( + + )} + + onAction?.(action, post)} + /> +
+
+ ); +} diff --git a/frontend/src/toolui/components/linkedin-post/schema.ts b/frontend/src/toolui/components/linkedin-post/schema.ts new file mode 100644 index 00000000..177f5b4c --- /dev/null +++ b/frontend/src/toolui/components/linkedin-post/schema.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; +import { defineToolUiContract } from "../shared/contract"; + +export const LinkedInPostAuthorSchema = z.object({ + name: z.string(), + avatarUrl: z.string(), + headline: z.string().optional(), +}); + +export const LinkedInPostMediaSchema = z.object({ + type: z.enum(["image", "video"]), + url: z.string(), + alt: z.string(), +}); + +export const LinkedInPostLinkPreviewSchema = z.object({ + url: z.string(), + title: z.string().optional(), + description: z.string().optional(), + imageUrl: z.string().optional(), + domain: z.string().optional(), +}); + +export const LinkedInPostStatsSchema = z.object({ + likes: z.number().optional(), + isLiked: z.boolean().optional(), +}); + +export const SerializableLinkedInPostSchema = z.object({ + id: z.string(), + author: LinkedInPostAuthorSchema, + text: z.string().optional(), + media: LinkedInPostMediaSchema.optional(), + linkPreview: LinkedInPostLinkPreviewSchema.optional(), + stats: LinkedInPostStatsSchema.optional(), + createdAt: z.string().optional(), +}); + +export type LinkedInPostData = z.infer; + +export type LinkedInPostAuthor = z.infer; +export type LinkedInPostMedia = z.infer; +export type LinkedInPostLinkPreview = z.infer< + typeof LinkedInPostLinkPreviewSchema +>; +export type LinkedInPostStats = z.infer; + +const SerializableLinkedInPostSchemaContract = defineToolUiContract( + "LinkedInPost", + SerializableLinkedInPostSchema, +); + +export const parseSerializableLinkedInPost: ( + input: unknown, +) => LinkedInPostData = SerializableLinkedInPostSchemaContract.parse; + +export const safeParseSerializableLinkedInPost: ( + input: unknown, +) => LinkedInPostData | null = SerializableLinkedInPostSchemaContract.safeParse; diff --git a/frontend/src/toolui/components/message-draft/README.md b/frontend/src/toolui/components/message-draft/README.md new file mode 100644 index 00000000..48ed63e2 --- /dev/null +++ b/frontend/src/toolui/components/message-draft/README.md @@ -0,0 +1,19 @@ +# Message Draft + +Implementation for the "message-draft" Tool UI surface. + +## Files + +- public exports: components/tool-ui/message-draft/index.tsx +- serializable schema + parse helpers: components/tool-ui/message-draft/schema.ts + +## Companion assets + +- Docs page: app/docs/message-draft/content.mdx +- Preset payload: lib/presets/message-draft.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/message-draft/_adapter.tsx b/frontend/src/toolui/components/message-draft/_adapter.tsx new file mode 100644 index 00000000..4d2303fd --- /dev/null +++ b/frontend/src/toolui/components/message-draft/_adapter.tsx @@ -0,0 +1,12 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Button → shadcn/ui Button + */ + +export { cn } from "@toolui/lib/utils"; +export { Button } from "@toolui/ui/button"; diff --git a/frontend/src/toolui/components/message-draft/index.tsx b/frontend/src/toolui/components/message-draft/index.tsx new file mode 100644 index 00000000..9970ea34 --- /dev/null +++ b/frontend/src/toolui/components/message-draft/index.tsx @@ -0,0 +1,10 @@ +export { MessageDraft } from "./message-draft"; +export { + type SerializableMessageDraft, + type SerializableEmailDraft, + type SerializableSlackDraft, + type MessageDraftChannel, + type MessageDraftOutcome, + type SlackTarget, + type MessageDraftProps, +} from "./schema"; diff --git a/frontend/src/toolui/components/message-draft/message-draft.tsx b/frontend/src/toolui/components/message-draft/message-draft.tsx new file mode 100644 index 00000000..1753b795 --- /dev/null +++ b/frontend/src/toolui/components/message-draft/message-draft.tsx @@ -0,0 +1,511 @@ +"use client"; + +import * as React from "react"; +import { cn, Button } from "./_adapter"; +import type { + MessageDraftProps, + SerializableEmailDraft, + SerializableSlackDraft, +} from "./schema"; +import { ActionButtons } from "../shared/action-buttons"; +import type { Action } from "../shared/schema"; +import { Check, ChevronDown } from "lucide-react"; + +type DraftState = "review" | "sending" | "sent" | "cancelled"; +type DraftOutcome = MessageDraftProps["outcome"]; + +const DEFAULT_GRACE_PERIOD = 5000; +const COLLAPSED_BODY_HEIGHT = 280; + +interface RecipientRowProps { + label: string; + recipients: string[]; + maxVisible?: number; + muted?: boolean; +} + +function RecipientRow({ + label, + recipients, + maxVisible = 3, + muted = false, +}: RecipientRowProps) { + const visibleRecipients = recipients.slice(0, maxVisible); + const overflowCount = recipients.length - maxVisible; + + return ( + + + {label} + + + {visibleRecipients.join(", ")} + {overflowCount > 0 && ( + +{overflowCount} more + )} + + + ); +} + +interface SingleFieldRowProps { + label: string; + value: string; +} + +function SingleFieldRow({ label, value }: SingleFieldRowProps) { + return ( + + + {label} + + {value} + + ); +} + +interface ExpandableBodyProps { + body: string; + isExpanded: boolean; + onNeedsExpansionChange?: (needsExpansion: boolean) => void; +} + +function ExpandableBody({ + body, + isExpanded, + onNeedsExpansionChange, +}: ExpandableBodyProps) { + const [needsExpansion, setNeedsExpansion] = React.useState( + null, + ); + const contentRef = React.useRef(null); + + React.useLayoutEffect(() => { + if (contentRef.current) { + const needs = contentRef.current.scrollHeight > COLLAPSED_BODY_HEIGHT; + setNeedsExpansion(needs); + onNeedsExpansionChange?.(needs); + } + }, [body, onNeedsExpansionChange]); + + return ( +
+
+

{body}

+
+ {needsExpansion && ( +
+ )} +
+ ); +} + +interface EmailDraftContentProps { + draft: SerializableEmailDraft; + titleId: string; + isExpanded: boolean; + onNeedsExpansionChange?: (needsExpansion: boolean) => void; +} + +function EmailDraftContent({ + draft, + titleId, + isExpanded, + onNeedsExpansionChange, +}: EmailDraftContentProps) { + return ( + <> +

+ {draft.subject} +

+ + + + {draft.from && } + + {draft.cc && draft.cc.length > 0 && ( + + )} + {draft.bcc && draft.bcc.length > 0 && ( + + )} + +
+ +
+ + + + ); +} + +interface SlackDraftContentProps { + draft: SerializableSlackDraft; + titleId: string; + isExpanded: boolean; + onNeedsExpansionChange?: (needsExpansion: boolean) => void; +} + +function SlackLogo({ className }: { className?: string }) { + return ( + + ); +} + +function SlackDraftContent({ + draft, + titleId, + isExpanded, + onNeedsExpansionChange, +}: SlackDraftContentProps) { + const { target } = draft; + const isChannel = target.type === "channel"; + const targetDisplay = isChannel + ? `#${target.name}` + : `Message to @${target.name}`; + const memberCount = isChannel ? target.memberCount : undefined; + + return ( + <> +
+ + {targetDisplay} + {memberCount !== undefined && ( + + {memberCount.toLocaleString()} members + + )} +
+ +
+ + + + ); +} + +function formatSentTime(date: Date): string { + return date.toLocaleTimeString(undefined, { + hour: "numeric", + minute: "2-digit", + }); +} + +export function resolveStateFromOutcome(outcome: DraftOutcome): DraftState { + if (outcome === "sent") return "sent"; + if (outcome === "cancelled") return "cancelled"; + return "review"; +} + +export function resolveOutcomeTransition( + previousOutcome: DraftOutcome, + nextOutcome: DraftOutcome, +): DraftState | null { + if (previousOutcome === nextOutcome) { + return null; + } + + return resolveStateFromOutcome(nextOutcome); +} + +interface SentConfirmationProps { + sentAt: Date; +} + +function SentConfirmation({ sentAt }: SentConfirmationProps) { + return ( +
+ + Sent at {formatSentTime(sentAt)} + + + + +
+ ); +} + +export function MessageDraft(props: MessageDraftProps) { + const { + id, + className, + outcome, + undoGracePeriod = DEFAULT_GRACE_PERIOD, + onSend, + onUndo, + onCancel, + } = props; + + const [state, setState] = React.useState(() => + resolveStateFromOutcome(outcome), + ); + const [countdown, setCountdown] = React.useState( + Math.ceil(undoGracePeriod / 1000), + ); + const [sentAt, setSentAt] = React.useState(() => + outcome === "sent" ? new Date() : null, + ); + const [isExpanded, setIsExpanded] = React.useState(false); + const [needsExpansion, setNeedsExpansion] = React.useState(false); + const undoButtonRef = React.useRef(null); + const timerRef = React.useRef | null>(null); + const countdownRef = React.useRef | null>( + null, + ); + const previousOutcomeRef = React.useRef(outcome); + + const clearTimers = React.useCallback(() => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + if (countdownRef.current) { + clearInterval(countdownRef.current); + countdownRef.current = null; + } + }, []); + + React.useEffect(() => { + return clearTimers; + }, [clearTimers]); + + React.useEffect(() => { + const nextState = resolveOutcomeTransition( + previousOutcomeRef.current, + outcome, + ); + + previousOutcomeRef.current = outcome; + + if (nextState === null) { + return; + } + + clearTimers(); + setState(nextState); + setCountdown(Math.ceil(undoGracePeriod / 1000)); + setSentAt(nextState === "sent" ? new Date() : null); + }, [outcome, undoGracePeriod, clearTimers]); + + React.useEffect(() => { + if (state === "sending") { + undoButtonRef.current?.focus(); + + setCountdown(Math.ceil(undoGracePeriod / 1000)); + + countdownRef.current = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + if (countdownRef.current) { + clearInterval(countdownRef.current); + countdownRef.current = null; + } + return 0; + } + return prev - 1; + }); + }, 1000); + + timerRef.current = setTimeout(async () => { + clearTimers(); + await onSend?.(); + setSentAt(new Date()); + setState("sent"); + }, undoGracePeriod); + } + }, [state, undoGracePeriod, onSend, clearTimers]); + + const handleSend = React.useCallback(() => { + setState("sending"); + }, []); + + const handleUndo = React.useCallback(() => { + clearTimers(); + setState("review"); + onUndo?.(); + }, [clearTimers, onUndo]); + + const handleCancel = React.useCallback(() => { + clearTimers(); + setState("cancelled"); + onCancel?.(); + }, [clearTimers, onCancel]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Escape" && state === "review") { + event.preventDefault(); + handleCancel(); + } + }, + [state, handleCancel], + ); + + const handleNeedsExpansionChange = React.useCallback((needs: boolean) => { + setNeedsExpansion(needs); + }, []); + + const handleToggleExpand = React.useCallback(() => { + setIsExpanded((prev) => !prev); + }, []); + + const handleAction = React.useCallback( + async (actionId: string) => { + if (actionId === "send") { + handleSend(); + } else if (actionId === "cancel") { + handleCancel(); + } + }, + [handleSend, handleCancel], + ); + + const actions: Action[] = [ + { + id: "cancel", + label: "Cancel", + variant: "ghost", + }, + { + id: "send", + label: "Send", + variant: "default", + }, + ]; + + const expandButton = needsExpansion ? ( + + ) : null; + + const renderActions = () => { + switch (state) { + case "sending": + return ( +
+ + Sending in {countdown}s + + +
+ ); + case "sent": + return ; + case "cancelled": + return null; + default: + return ; + } + }; + + if (state === "cancelled") { + return null; + } + + return ( +
+
+ {props.channel === "email" ? ( + + ) : ( + + )} + + {expandButton} +
+ +
{renderActions()}
+
+ ); +} diff --git a/frontend/src/toolui/components/message-draft/schema.ts b/frontend/src/toolui/components/message-draft/schema.ts new file mode 100644 index 00000000..8cf14d5c --- /dev/null +++ b/frontend/src/toolui/components/message-draft/schema.ts @@ -0,0 +1,83 @@ +import { z } from "zod"; +import { ToolUIIdSchema, ToolUIRoleSchema } from "../shared/schema"; +import { defineToolUiContract } from "../shared/contract"; + +export const MessageDraftChannelSchema = z.enum(["email", "slack"]); + +export type MessageDraftChannel = z.infer; + +export const MessageDraftOutcomeSchema = z.enum(["sent", "cancelled"]); + +export type MessageDraftOutcome = z.infer; + +const SlackTargetSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("channel"), + name: z.string().min(1), + memberCount: z.number().optional(), + }), + z.object({ type: z.literal("dm"), name: z.string().min(1) }), +]); + +export type SlackTarget = z.infer; + +export const SerializableEmailDraftSchema = z.object({ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + body: z.string().min(1), + outcome: MessageDraftOutcomeSchema.optional(), + channel: z.literal("email"), + subject: z.string().min(1), + from: z.string().optional(), + to: z.array(z.string()).min(1), + cc: z.array(z.string()).optional(), + bcc: z.array(z.string()).optional(), +}); + +export const SerializableSlackDraftSchema = z.object({ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + body: z.string().min(1), + outcome: MessageDraftOutcomeSchema.optional(), + channel: z.literal("slack"), + target: SlackTargetSchema, +}); + +export const SerializableMessageDraftSchema = z.discriminatedUnion("channel", [ + SerializableEmailDraftSchema, + SerializableSlackDraftSchema, +]); + +export type SerializableMessageDraft = z.infer< + typeof SerializableMessageDraftSchema +>; + +export type SerializableEmailDraft = z.infer< + typeof SerializableEmailDraftSchema +>; + +export type SerializableSlackDraft = z.infer< + typeof SerializableSlackDraftSchema +>; + +const SerializableMessageDraftSchemaContract = defineToolUiContract( + "MessageDraft", + SerializableMessageDraftSchema, +); + +export const parseSerializableMessageDraft: ( + input: unknown, +) => SerializableMessageDraft = SerializableMessageDraftSchemaContract.parse; + +export const safeParseSerializableMessageDraft: ( + input: unknown, +) => SerializableMessageDraft | null = + SerializableMessageDraftSchemaContract.safeParse; + +export type MessageDraftProps = SerializableMessageDraft & { + className?: string; + undoGracePeriod?: number; + onSend?: () => void | Promise; + onUndo?: () => void; + onCancel?: () => void; +}; diff --git a/frontend/src/toolui/components/option-list/README.md b/frontend/src/toolui/components/option-list/README.md new file mode 100644 index 00000000..6d8d4574 --- /dev/null +++ b/frontend/src/toolui/components/option-list/README.md @@ -0,0 +1,19 @@ +# Option List + +Implementation for the "option-list" Tool UI surface. + +## Files + +- public exports: components/tool-ui/option-list/index.tsx +- serializable schema + parse helpers: components/tool-ui/option-list/schema.ts + +## Companion assets + +- Docs page: app/docs/option-list/content.mdx +- Preset payload: lib/presets/option-list.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/option-list/_adapter.tsx b/frontend/src/toolui/components/option-list/_adapter.tsx new file mode 100644 index 00000000..a7873bcc --- /dev/null +++ b/frontend/src/toolui/components/option-list/_adapter.tsx @@ -0,0 +1,14 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Button → shadcn/ui Button + * Separator → shadcn/ui Separator + */ + +export { cn } from "@toolui/lib/utils"; +export { Button } from "@toolui/ui/button"; +export { Separator } from "@toolui/ui/separator"; diff --git a/frontend/src/toolui/components/option-list/index.tsx b/frontend/src/toolui/components/option-list/index.tsx new file mode 100644 index 00000000..7ae9d2cb --- /dev/null +++ b/frontend/src/toolui/components/option-list/index.tsx @@ -0,0 +1,7 @@ +export { OptionList } from "./option-list"; +export type { + OptionListProps, + OptionListOption, + OptionListSelection, + SerializableOptionList, +} from "./schema"; diff --git a/frontend/src/toolui/components/option-list/option-list.tsx b/frontend/src/toolui/components/option-list/option-list.tsx new file mode 100644 index 00000000..34df143b --- /dev/null +++ b/frontend/src/toolui/components/option-list/option-list.tsx @@ -0,0 +1,625 @@ +"use client"; + +import { + useMemo, + useState, + useCallback, + useEffect, + useRef, + Fragment, +} from "react"; +import type { KeyboardEvent } from "react"; +import type { + OptionListProps, + OptionListSelection, + OptionListOption, +} from "./schema"; +import { + normalizeSelectionForOptions, + parseSelectionToIdSet, +} from "./selection"; +import { ActionButtons } from "../shared/action-buttons"; +import { normalizeActionsConfig } from "../shared/actions-config"; +import type { Action } from "../shared/schema"; +import { cn, Button, Separator } from "./_adapter"; +import { Check } from "lucide-react"; + +function convertIdSetToSelection( + selected: Set, + mode: "multi" | "single", +): OptionListSelection { + if (mode === "single") { + const [first] = selected; + return first ?? null; + } + return Array.from(selected); +} + +function areSetsEqual(a: Set, b: Set) { + if (a.size !== b.size) return false; + for (const val of a) { + if (!b.has(val)) return false; + } + return true; +} + +interface SelectionIndicatorProps { + mode: "multi" | "single"; + isSelected: boolean; + disabled?: boolean; +} + +function SelectionIndicator({ + mode, + isSelected, + disabled, +}: SelectionIndicatorProps) { + const shape = mode === "single" ? "rounded-full" : "rounded"; + + return ( +
+ {mode === "multi" && isSelected && } + {mode === "single" && isSelected && ( + + )} +
+ ); +} + +interface OptionItemProps { + option: OptionListOption; + isSelected: boolean; + isDisabled: boolean; + selectionMode: "multi" | "single"; + isFirst: boolean; + isLast: boolean; + onToggle: () => void; + tabIndex?: number; + onFocus?: () => void; + buttonRef?: (el: HTMLButtonElement | null) => void; +} + +function OptionItem({ + option, + isSelected, + isDisabled, + selectionMode, + isFirst, + isLast, + onToggle, + tabIndex, + onFocus, + buttonRef, +}: OptionItemProps) { + const hasAdjacentOptions = !isFirst && !isLast; + + return ( + + ); +} + +interface OptionListConfirmationProps { + id: string; + options: OptionListOption[]; + selectedIds: Set; + className?: string; +} + +function OptionListConfirmation({ + id, + options, + selectedIds, + className, +}: OptionListConfirmationProps) { + const confirmedOptions = options.filter((opt) => selectedIds.has(opt.id)); + + return ( +
+
+ {confirmedOptions.map((option, index) => ( + + {index > 0 && ( + + )} +
+ + + + {option.icon && ( + {option.icon} + )} +
+ + {option.label} + + {option.description && ( + + {option.description} + + )} +
+
+
+ ))} +
+
+ ); +} + +export function OptionList({ + id, + options, + selectionMode = "multi", + minSelections = 1, + maxSelections, + value, + defaultValue, + choice, + onChange, + actions, + onAction, + onBeforeAction, + className, +}: OptionListProps) { + if (process.env["NODE_ENV"] !== "production") { + if (value !== undefined && defaultValue !== undefined) { + console.warn( + "[OptionList] Both `value` (controlled) and `defaultValue` (uncontrolled) were provided. `defaultValue` is ignored when `value` is set.", + ); + } + if (value !== undefined && !onChange) { + console.warn( + "[OptionList] `value` was provided without `onChange`. This makes OptionList controlled; selection will not update unless the parent updates `value`.", + ); + } + } + + const effectiveMaxSelections = selectionMode === "single" ? 1 : maxSelections; + const optionIds = useMemo( + () => new Set(options.map((option) => option.id)), + [options], + ); + + const [uncontrolledSelected, setUncontrolledSelected] = useState>( + () => + normalizeSelectionForOptions( + parseSelectionToIdSet( + defaultValue, + selectionMode, + effectiveMaxSelections, + ), + optionIds, + ), + ); + + const selectedIds = useMemo(() => { + const parsed = + value !== undefined + ? parseSelectionToIdSet(value, selectionMode, effectiveMaxSelections) + : uncontrolledSelected; + return normalizeSelectionForOptions(parsed, optionIds); + }, [ + value, + uncontrolledSelected, + selectionMode, + effectiveMaxSelections, + optionIds, + ]); + + const selectedCount = selectedIds.size; + + const optionStates = useMemo(() => { + return options.map((option) => { + const isSelected = selectedIds.has(option.id); + const isSelectionLocked = + selectionMode === "multi" && + effectiveMaxSelections !== undefined && + selectedCount >= effectiveMaxSelections && + !isSelected; + const isDisabled = option.disabled || isSelectionLocked; + + return { option, isSelected, isDisabled }; + }); + }, [ + options, + selectedIds, + selectionMode, + effectiveMaxSelections, + selectedCount, + ]); + + const optionRefs = useRef>([]); + const [activeIndex, setActiveIndex] = useState(() => { + const firstSelected = optionStates.findIndex( + (s) => s.isSelected && !s.isDisabled, + ); + if (firstSelected >= 0) return firstSelected; + const firstEnabled = optionStates.findIndex((s) => !s.isDisabled); + return firstEnabled >= 0 ? firstEnabled : 0; + }); + + useEffect(() => { + if (optionStates.length === 0) return; + setActiveIndex((prev) => { + if ( + prev < 0 || + prev >= optionStates.length || + optionStates[prev].isDisabled + ) { + const firstEnabled = optionStates.findIndex((s) => !s.isDisabled); + return firstEnabled >= 0 ? firstEnabled : 0; + } + return prev; + }); + }, [optionStates]); + + const updateSelection = useCallback( + (next: Set) => { + const normalizedNext = normalizeSelectionForOptions( + parseSelectionToIdSet( + Array.from(next), + selectionMode, + effectiveMaxSelections, + ), + optionIds, + ); + + if (value === undefined) { + if (!areSetsEqual(uncontrolledSelected, normalizedNext)) { + setUncontrolledSelected(normalizedNext); + } + } + + onChange?.(convertIdSetToSelection(normalizedNext, selectionMode)); + }, + [ + effectiveMaxSelections, + selectionMode, + uncontrolledSelected, + value, + onChange, + optionIds, + ], + ); + + const toggleSelection = useCallback( + (optionId: string) => { + const next = new Set(selectedIds); + const isSelected = next.has(optionId); + + if (selectionMode === "single") { + if (isSelected) { + next.delete(optionId); + } else { + next.clear(); + next.add(optionId); + } + } else { + if (isSelected) { + next.delete(optionId); + } else { + if (effectiveMaxSelections && next.size >= effectiveMaxSelections) { + return; + } + next.add(optionId); + } + } + + updateSelection(next); + }, + [effectiveMaxSelections, selectedIds, selectionMode, updateSelection], + ); + + const toSelectionState = useCallback( + (selected: Set): OptionListSelection => + convertIdSetToSelection(selected, selectionMode), + [selectionMode], + ); + + const handleCancel = useCallback((): OptionListSelection => { + const empty = new Set(); + updateSelection(empty); + return toSelectionState(empty); + }, [toSelectionState, updateSelection]); + + const customActions = useMemo( + () => normalizeActionsConfig(actions), + [actions], + ); + + const handleFooterAction = useCallback( + async (actionId: string) => { + let nextState = toSelectionState(selectedIds); + + if (actionId === "cancel") { + nextState = handleCancel(); + } + + await onAction?.(actionId, nextState); + }, + [handleCancel, onAction, selectedIds, toSelectionState], + ); + + const normalizedFooterActions = useMemo(() => { + if (customActions) return customActions; + return { + items: [ + { id: "cancel", label: "Clear", variant: "ghost" as const }, + { id: "confirm", label: "Confirm", variant: "default" as const }, + ], + align: "right" as const, + } satisfies ReturnType; + }, [customActions]); + + const isConfirmDisabled = + selectedCount < minSelections || selectedCount === 0; + const hasNothingToClear = selectedCount === 0; + + const focusOptionAt = useCallback((index: number) => { + const el = optionRefs.current[index]; + if (el) el.focus(); + setActiveIndex(index); + }, []); + + const findFirstEnabledIndex = useCallback(() => { + const idx = optionStates.findIndex((s) => !s.isDisabled); + return idx >= 0 ? idx : 0; + }, [optionStates]); + + const findLastEnabledIndex = useCallback(() => { + for (let i = optionStates.length - 1; i >= 0; i--) { + if (!optionStates[i].isDisabled) return i; + } + return 0; + }, [optionStates]); + + const findNextEnabledIndex = useCallback( + (start: number, direction: 1 | -1) => { + const len = optionStates.length; + if (len === 0) return 0; + for (let step = 1; step <= len; step++) { + const idx = (start + direction * step + len) % len; + if (!optionStates[idx].isDisabled) return idx; + } + return start; + }, + [optionStates], + ); + + const handleListboxKeyDown = useCallback( + (e: KeyboardEvent) => { + if (optionStates.length === 0) return; + + const key = e.key; + + if (key === "ArrowDown") { + e.preventDefault(); + e.stopPropagation(); + focusOptionAt(findNextEnabledIndex(activeIndex, 1)); + return; + } + + if (key === "ArrowUp") { + e.preventDefault(); + e.stopPropagation(); + focusOptionAt(findNextEnabledIndex(activeIndex, -1)); + return; + } + + if (key === "Home") { + e.preventDefault(); + e.stopPropagation(); + focusOptionAt(findFirstEnabledIndex()); + return; + } + + if (key === "End") { + e.preventDefault(); + e.stopPropagation(); + focusOptionAt(findLastEnabledIndex()); + return; + } + + if (key === "Enter" || key === " ") { + e.preventDefault(); + e.stopPropagation(); + const current = optionStates[activeIndex]; + if (!current || current.isDisabled) return; + toggleSelection(current.option.id); + return; + } + + if (key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + if (!hasNothingToClear) { + handleCancel(); + } + } + }, + [ + activeIndex, + findFirstEnabledIndex, + findLastEnabledIndex, + findNextEnabledIndex, + focusOptionAt, + handleCancel, + hasNothingToClear, + optionStates, + toggleSelection, + ], + ); + + const actionsWithDisabledState = useMemo((): Action[] => { + return normalizedFooterActions.items.map((action) => { + const isDisabledByValidation = + (action.id === "confirm" && isConfirmDisabled) || + (action.id === "cancel" && hasNothingToClear); + return { + ...action, + disabled: action.disabled || isDisabledByValidation, + label: + action.id === "confirm" && + selectionMode === "multi" && + selectedCount > 0 + ? `${action.label} (${selectedCount})` + : action.label, + }; + }); + }, [ + normalizedFooterActions.items, + isConfirmDisabled, + hasNothingToClear, + selectionMode, + selectedCount, + ]); + + const isReceipt = choice !== undefined && choice !== null; + const viewKey = isReceipt ? `receipt-${String(choice)}` : "interactive"; + + return ( +
+ {isReceipt ? ( + + ) : ( +
+
+ {optionStates.map(({ option, isSelected, isDisabled }, index) => { + return ( + + {index > 0 && ( + + )} + setActiveIndex(index)} + buttonRef={(el) => { + optionRefs.current[index] = el; + }} + onToggle={() => toggleSelection(option.id)} + /> + + ); + })} +
+ +
+ + onBeforeAction(actionId, toSelectionState(selectedIds)) + : undefined + } + /> +
+
+ )} +
+ ); +} diff --git a/frontend/src/toolui/components/option-list/schema.ts b/frontend/src/toolui/components/option-list/schema.ts new file mode 100644 index 00000000..76a9745c --- /dev/null +++ b/frontend/src/toolui/components/option-list/schema.ts @@ -0,0 +1,210 @@ +import { z } from "zod"; +import type { ReactNode } from "react"; +import type { ActionsProp } from "../shared/actions-config"; +import type { EmbeddedActionsProps } from "../shared/embedded-actions"; +import { + ActionSchema, + SerializableActionSchema, + SerializableActionsConfigSchema, + ToolUIIdSchema, + ToolUIReceiptSchema, + ToolUIRoleSchema, +} from "../shared/schema"; +import { defineToolUiContract } from "../shared/contract"; + +export const OptionListOptionSchema = z.object({ + id: z.string().min(1), + label: z.string().min(1), + description: z.string().optional(), + icon: z.custom().optional(), + disabled: z.boolean().optional(), +}); + +export type OptionListSelection = string[] | string | null; + +const OptionListSelectionSchema = z + .union([z.array(z.string()), z.string(), z.null()]) + .optional(); + +type OptionListSchemaInvariantInput = { + options: Array<{ id: string }>; + minSelections?: number; + maxSelections?: number; + value?: OptionListSelection; + defaultValue?: OptionListSelection; + choice?: OptionListSelection; +}; + +function selectionToIds(selection: OptionListSelection | undefined): string[] { + if (selection == null) return []; + if (typeof selection === "string") return [selection]; + return Array.isArray(selection) ? selection : []; +} + +function validateOptionListInvariants( + data: OptionListSchemaInvariantInput, + ctx: z.RefinementCtx, +) { + if ( + data.minSelections !== undefined && + data.maxSelections !== undefined && + data.minSelections > data.maxSelections + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["minSelections"], + message: "`minSelections` cannot be greater than `maxSelections`.", + }); + } + + const optionIds = new Set(); + for (let index = 0; index < data.options.length; index++) { + const optionId = data.options[index]?.id; + if (!optionId) continue; + + if (optionIds.has(optionId)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["options", index, "id"], + message: `Duplicate option id "${optionId}" is not allowed.`, + }); + } else { + optionIds.add(optionId); + } + } + + const selectionFields: Array< + ["value" | "defaultValue" | "choice", OptionListSelection | undefined] + > = [ + ["value", data.value], + ["defaultValue", data.defaultValue], + ["choice", data.choice], + ]; + + for (const [fieldName, selection] of selectionFields) { + if (selection == null) continue; + + const ids = selectionToIds(selection); + ids.forEach((selectionId, index) => { + if (!optionIds.has(selectionId)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: + typeof selection === "string" ? [fieldName] : [fieldName, index], + message: `Selection id "${selectionId}" must exist in options.`, + }); + } + }); + } +} + +const OptionListPropsSchemaBase = z.object({ + /** + * Unique identifier for this tool UI instance in the conversation. + * + * Used for: + * - Assistant referencing ("the options above") + * - Receipt generation (linking selections to their source) + * - Narration context + * + * Should be stable across re-renders, meaningful, and unique within the conversation. + * + * @example "option-list-deploy-target", "format-selection" + */ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + receipt: ToolUIReceiptSchema.optional(), + options: z.array(OptionListOptionSchema).min(1), + selectionMode: z.enum(["multi", "single"]).optional(), + /** + * Controlled selection value (advanced / runtime only). + * + * For Tool UI tool payloads, prefer `defaultValue` (initial selection) and + * `choice` (receipt state). Controlled `value` is intentionally excluded + * from `SerializableOptionListSchema` to avoid accidental "controlled but + * non-interactive" states when an LLM includes `value` in args. + */ + value: OptionListSelectionSchema, + defaultValue: OptionListSelectionSchema, + /** + * When set, renders the component in receipt state showing the user's choice. + * + * In receipt state: + * - Only the chosen option(s) are shown + * - Actions are hidden + * - The component is read-only + * + * Use this with assistant-ui's `addResult` to show the outcome of a decision. + * + * @example + * ```tsx + * // In a toolkit render function: + * if (result) { + * return ; + * } + * ``` + */ + choice: OptionListSelectionSchema, + actions: z + .union([z.array(ActionSchema), SerializableActionsConfigSchema]) + .optional(), + minSelections: z.number().min(0).optional(), + maxSelections: z.number().min(1).optional(), +}); + +export const OptionListPropsSchema = OptionListPropsSchemaBase.superRefine( + validateOptionListInvariants, +); + +export type OptionListOption = z.infer; + +export type OptionListProps = Omit< + z.infer, + "value" | "defaultValue" | "choice" | "actions" +> & { + /** @see OptionListPropsSchema.id */ + id: string; + value?: OptionListSelection; + defaultValue?: OptionListSelection; + /** @see OptionListPropsSchema.choice */ + choice?: OptionListSelection; + onChange?: (value: OptionListSelection) => void; + actions?: ActionsProp; + onAction?: EmbeddedActionsProps["onAction"]; + onBeforeAction?: EmbeddedActionsProps["onBeforeAction"]; + className?: string; +}; + +export const SerializableOptionListSchema = OptionListPropsSchemaBase.omit({ + // Exclude controlled selection from tool/LLM payloads. + value: true, +}) + .extend({ + options: z.array(OptionListOptionSchema.omit({ icon: true })), + actions: z + .union([ + z.array(SerializableActionSchema), + SerializableActionsConfigSchema, + ]) + .optional(), + }) + .strict() + .superRefine(validateOptionListInvariants); + +export type SerializableOptionList = z.infer< + typeof SerializableOptionListSchema +>; + +const SerializableOptionListSchemaContract = defineToolUiContract( + "OptionList", + SerializableOptionListSchema, +); + +export const parseSerializableOptionList: ( + input: unknown, +) => SerializableOptionList = SerializableOptionListSchemaContract.parse; + +export const safeParseSerializableOptionList: ( + input: unknown, +) => SerializableOptionList | null = + SerializableOptionListSchemaContract.safeParse; diff --git a/frontend/src/toolui/components/option-list/selection.ts b/frontend/src/toolui/components/option-list/selection.ts new file mode 100644 index 00000000..48ff8ece --- /dev/null +++ b/frontend/src/toolui/components/option-list/selection.ts @@ -0,0 +1,35 @@ +import type { OptionListSelection } from "./schema"; + +export function parseSelectionToIdSet( + value: OptionListSelection | undefined, + mode: "multi" | "single", + maxSelections?: number, +): Set { + if (mode === "single") { + const single = + typeof value === "string" + ? value + : Array.isArray(value) + ? value[0] + : null; + return single ? new Set([single]) : new Set(); + } + + const arr = + typeof value === "string" ? [value] : Array.isArray(value) ? value : []; + + return new Set(maxSelections ? arr.slice(0, maxSelections) : arr); +} + +export function normalizeSelectionForOptions( + selection: Set, + optionIds: Set, +): Set { + const normalized = new Set(); + for (const id of selection) { + if (optionIds.has(id)) { + normalized.add(id); + } + } + return normalized; +} diff --git a/frontend/src/toolui/components/order-summary/README.md b/frontend/src/toolui/components/order-summary/README.md new file mode 100644 index 00000000..6f80a24d --- /dev/null +++ b/frontend/src/toolui/components/order-summary/README.md @@ -0,0 +1,19 @@ +# Order Summary + +Implementation for the "order-summary" Tool UI surface. + +## Files + +- public exports: components/tool-ui/order-summary/index.tsx +- serializable schema + parse helpers: components/tool-ui/order-summary/schema.ts + +## Companion assets + +- Docs page: app/docs/order-summary/content.mdx +- Preset payload: lib/presets/order-summary.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/order-summary/_adapter.tsx b/frontend/src/toolui/components/order-summary/_adapter.tsx new file mode 100644 index 00000000..b111ed52 --- /dev/null +++ b/frontend/src/toolui/components/order-summary/_adapter.tsx @@ -0,0 +1,16 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Button → shadcn/ui Button + * Separator → shadcn/ui Separator + * Skeleton → shadcn/ui Skeleton + */ + +export { cn } from "@toolui/lib/utils"; +export { Button } from "@toolui/ui/button"; +export { Separator } from "@toolui/ui/separator"; +export { Skeleton } from "@toolui/ui/skeleton"; diff --git a/frontend/src/toolui/components/order-summary/index.tsx b/frontend/src/toolui/components/order-summary/index.tsx new file mode 100644 index 00000000..d8024df9 --- /dev/null +++ b/frontend/src/toolui/components/order-summary/index.tsx @@ -0,0 +1,14 @@ +export { OrderSummary } from "./order-summary"; +export type { + OrderSummaryDisplayProps, + OrderSummaryReceiptProps, + OrderSummaryCompoundComponent, +} from "./order-summary"; +export { + type SerializableOrderSummary, + type OrderSummaryProps, + type OrderSummaryVariant, + type OrderItem, + type Pricing, + type OrderDecision, +} from "./schema"; diff --git a/frontend/src/toolui/components/order-summary/order-summary.tsx b/frontend/src/toolui/components/order-summary/order-summary.tsx new file mode 100644 index 00000000..5583dd55 --- /dev/null +++ b/frontend/src/toolui/components/order-summary/order-summary.tsx @@ -0,0 +1,296 @@ +import { CheckCircle, Package } from "lucide-react"; +import type { ReactElement } from "react"; +import { cn, Separator } from "./_adapter"; +import type { + OrderSummaryProps, + OrderItem, + Pricing, + OrderDecision, + OrderSummaryVariant, +} from "./schema"; + +function formatCurrency(amount: number, currency: string): string { + try { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency, + }).format(amount); + } catch { + return `${currency} ${amount.toFixed(2)}`; + } +} + +function formatQuantity(quantity: number): string { + return quantity === 1 ? "" : `Qty: ${quantity}`; +} + +function ItemImage({ src, alt }: { src?: string; alt: string }) { + if (!src) { + return ( +
+
+ ); + } + + return ( + {alt} + ); +} + +function OrderItemRow({ + item, + currency, +}: { + item: OrderItem; + currency: string; +}) { + const quantity = item.quantity ?? 1; + const quantityText = formatQuantity(quantity); + const hasDescription = item.description || quantityText; + const lineTotal = item.unitPrice * quantity; + + return ( +
+ +
+
+
+ {item.name} + + {formatCurrency(lineTotal, currency)} + +
+ {hasDescription && ( +
+ {[item.description, quantityText].filter(Boolean).join(" · ")} +
+ )} +
+
+
+ ); +} + +function PricingBreakdown({ + pricing, + className, +}: { + pricing: Pricing; + className?: string; +}) { + const currency = pricing.currency ?? "USD"; + + return ( +
+
+
Subtotal
+
+ {formatCurrency(pricing.subtotal, currency)} +
+
+ + {pricing.discount !== undefined && pricing.discount > 0 && ( +
+
{pricing.discountLabel || "Discount"}
+
+ -{formatCurrency(pricing.discount, currency)} +
+
+ )} + + {pricing.shipping !== undefined && ( +
+
Shipping
+
+ {pricing.shipping === 0 + ? "Free" + : formatCurrency(pricing.shipping, currency)} +
+
+ )} + + {pricing.tax !== undefined && ( +
+
{pricing.taxLabel || "Tax"}
+
+ {formatCurrency(pricing.tax, currency)} +
+
+ )} + +
+
Total
+
+ {formatCurrency(pricing.total, currency)} +
+
+
+ ); +} + +function formatDate(isoString: string): string | undefined { + try { + const date = new Date(isoString); + if (isNaN(date.getTime())) return undefined; + return date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }); + } catch { + return undefined; + } +} + +function ReceiptBadge({ + orderId, + confirmedAt, +}: { + orderId?: string; + confirmedAt?: string; +}) { + const formattedDate = confirmedAt ? formatDate(confirmedAt) : undefined; + + const parts = [orderId && `#${orderId}`, formattedDate].filter(Boolean); + if (parts.length === 0) return null; + + return ( +

{parts.join(" · ")}

+ ); +} + +function OrderSummaryRoot({ + id, + title = "Order Summary", + variant, + items, + pricing, + choice, + className, +}: OrderSummaryProps) { + const titleId = `${id}-title`; + const resolvedVariant: OrderSummaryVariant = + variant ?? (choice === undefined ? "summary" : "receipt"); + const isReceipt = resolvedVariant === "receipt"; + const isMalformedPayload = + !Array.isArray(items) || + items.length === 0 || + pricing == null || + (isReceipt && choice === undefined); + + if (isMalformedPayload) { + return ( +
+
+

+ {title} +

+

+ Unable to render order summary +

+
+
+ ); + } + + return ( +
+
+
+
+

+ {isReceipt && ( +

+ {isReceipt && choice && ( + + )} +
+ +
+ {items.map((item) => ( + + ))} +
+ + + + +
+
+
+ ); +} + +export type OrderSummaryDisplayProps = OrderSummaryProps; + +function OrderSummaryDisplay(props: OrderSummaryDisplayProps) { + return ; +} + +export interface OrderSummaryReceiptProps extends Omit< + OrderSummaryProps, + "choice" +> { + choice: OrderDecision; +} + +function OrderSummaryReceipt(props: OrderSummaryReceiptProps) { + return ; +} + +export interface OrderSummaryCompoundComponent { + (props: OrderSummaryProps): ReactElement; + Display: (props: OrderSummaryDisplayProps) => ReactElement; + Receipt: (props: OrderSummaryReceiptProps) => ReactElement; +} + +export const OrderSummary: OrderSummaryCompoundComponent = Object.assign( + OrderSummaryRoot, + { + Display: OrderSummaryDisplay, + Receipt: OrderSummaryReceipt, + }, +); diff --git a/frontend/src/toolui/components/order-summary/schema.ts b/frontend/src/toolui/components/order-summary/schema.ts new file mode 100644 index 00000000..9e9c1d0a --- /dev/null +++ b/frontend/src/toolui/components/order-summary/schema.ts @@ -0,0 +1,108 @@ +import { z } from "zod"; +import { defineToolUiContract } from "../shared/contract"; +import { ToolUIIdSchema, ToolUIRoleSchema } from "../shared/schema"; + +export const OrderItemSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + imageUrl: z.string().url().optional(), + quantity: z.number().int().positive().optional(), + unitPrice: z.number(), +}); + +export type OrderItem = z.infer; + +const OrderItemsSchema = z + .array(OrderItemSchema) + .min(1) + .superRefine((items, ctx) => { + const seenIds = new Set(); + + for (const [index, item] of items.entries()) { + if (seenIds.has(item.id)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Duplicate item id: "${item.id}"`, + path: [index, "id"], + }); + } + + seenIds.add(item.id); + } + }); + +export const PricingSchema = z.object({ + subtotal: z.number(), + tax: z.number().optional(), + taxLabel: z.string().optional(), + shipping: z.number().optional(), + discount: z.number().nonnegative().optional(), + discountLabel: z.string().optional(), + total: z.number(), + currency: z.string().optional(), +}); + +export type Pricing = z.infer; + +export const OrderSummaryVariantSchema = z.enum(["summary", "receipt"]); +export type OrderSummaryVariant = z.infer; + +export const OrderDecisionSchema = z.object({ + action: z.literal("confirm"), + orderId: z.string().optional(), + confirmedAt: z.string().datetime().optional(), +}); + +export type OrderDecision = z.infer; + +export const SerializableOrderSummarySchema = z + .object({ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + title: z.string().optional(), + variant: OrderSummaryVariantSchema.optional(), + items: OrderItemsSchema, + pricing: PricingSchema, + choice: OrderDecisionSchema.optional(), + }) + .strict() + .superRefine((value, ctx) => { + if (value.variant === "receipt" && value.choice === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Receipt variant requires "choice".', + path: ["choice"], + }); + } + + if (value.variant === "summary" && value.choice !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Summary variant cannot include "choice".', + path: ["choice"], + }); + } + }); + +export type SerializableOrderSummary = z.infer< + typeof SerializableOrderSummarySchema +>; + +const SerializableOrderSummarySchemaContract = defineToolUiContract( + "OrderSummary", + SerializableOrderSummarySchema, +); + +export const parseSerializableOrderSummary: ( + input: unknown, +) => SerializableOrderSummary = SerializableOrderSummarySchemaContract.parse; + +export const safeParseSerializableOrderSummary: ( + input: unknown, +) => SerializableOrderSummary | null = + SerializableOrderSummarySchemaContract.safeParse; + +export interface OrderSummaryProps extends SerializableOrderSummary { + className?: string; +} diff --git a/frontend/src/toolui/components/parameter-slider/README.md b/frontend/src/toolui/components/parameter-slider/README.md new file mode 100644 index 00000000..04d1be58 --- /dev/null +++ b/frontend/src/toolui/components/parameter-slider/README.md @@ -0,0 +1,19 @@ +# Parameter Slider + +Implementation for the "parameter-slider" Tool UI surface. + +## Files + +- public exports: components/tool-ui/parameter-slider/index.tsx +- serializable schema + parse helpers: components/tool-ui/parameter-slider/schema.ts + +## Companion assets + +- Docs page: app/docs/parameter-slider/content.mdx +- Preset payload: lib/presets/parameter-slider.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/parameter-slider/_adapter.tsx b/frontend/src/toolui/components/parameter-slider/_adapter.tsx new file mode 100644 index 00000000..52a1b4d7 --- /dev/null +++ b/frontend/src/toolui/components/parameter-slider/_adapter.tsx @@ -0,0 +1,16 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Button → shadcn/ui Button + * Separator → shadcn/ui Separator + * Slider → shadcn/ui Slider + */ + +export { cn } from "@toolui/lib/utils"; +export { Button } from "@toolui/ui/button"; +export { Separator } from "@toolui/ui/separator"; +export { Slider } from "@toolui/ui/slider"; diff --git a/frontend/src/toolui/components/parameter-slider/index.tsx b/frontend/src/toolui/components/parameter-slider/index.tsx new file mode 100644 index 00000000..8f2ecf3e --- /dev/null +++ b/frontend/src/toolui/components/parameter-slider/index.tsx @@ -0,0 +1,7 @@ +export { ParameterSlider } from "./parameter-slider"; +export type { + ParameterSliderProps, + SliderConfig, + SliderValue, + SerializableParameterSlider, +} from "./schema"; diff --git a/frontend/src/toolui/components/parameter-slider/math.ts b/frontend/src/toolui/components/parameter-slider/math.ts new file mode 100644 index 00000000..80df94e3 --- /dev/null +++ b/frontend/src/toolui/components/parameter-slider/math.ts @@ -0,0 +1,42 @@ +import type { SliderConfig, SliderValue } from "./schema"; + +type SliderPercentInput = { + value: number; + min: number; + max: number; +}; + +function clampPercent(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(100, value)); +} + +export function sliderRangeToPercent({ + value, + min, + max, +}: SliderPercentInput): number { + const range = max - min; + if (!Number.isFinite(range) || range <= 0) return 0; + return clampPercent(((value - min) / range) * 100); +} + +export function createSliderValueSnapshot( + sliders: SliderConfig[], +): SliderValue[] { + return sliders.map((slider) => ({ id: slider.id, value: slider.value })); +} + +export function createSliderSignature(sliders: SliderConfig[]): string { + return JSON.stringify( + sliders.map(({ id, min, max, step, value, unit, precision }) => ({ + id, + min, + max, + step: step ?? 1, + value, + unit: unit ?? "", + precision: precision ?? null, + })), + ); +} diff --git a/frontend/src/toolui/components/parameter-slider/parameter-slider.tsx b/frontend/src/toolui/components/parameter-slider/parameter-slider.tsx new file mode 100644 index 00000000..4bbaa0df --- /dev/null +++ b/frontend/src/toolui/components/parameter-slider/parameter-slider.tsx @@ -0,0 +1,821 @@ +"use client"; + +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; +import type { ParameterSliderProps, SliderConfig, SliderValue } from "./schema"; +import { ActionButtons } from "../shared/action-buttons"; +import { normalizeActionsConfig } from "../shared/actions-config"; +import { useControllableState } from "../shared/use-controllable-state"; +import { useSignatureReset } from "../shared/use-signature-reset"; + +import { cn } from "./_adapter"; +import { + createSliderSignature, + createSliderValueSnapshot, + sliderRangeToPercent, +} from "./math"; + +function formatSignedValue( + value: number, + min: number, + max: number, + precision?: number, + unit?: string, +): string { + const crossesZero = min < 0 && max > 0; + const fixed = + precision !== undefined ? value.toFixed(precision) : String(value); + const numericPart = crossesZero && value >= 0 ? `+${fixed}` : fixed; + return unit ? `${numericPart} ${unit}` : numericPart; +} + +function getAriaValueText( + value: number, + min: number, + max: number, + unit?: string, +): string { + const crossesZero = min < 0 && max > 0; + if (crossesZero) { + if (value > 0) { + return unit ? `plus ${value} ${unit}` : `plus ${value}`; + } else if (value < 0) { + return unit + ? `minus ${Math.abs(value)} ${unit}` + : `minus ${Math.abs(value)}`; + } + } + return unit ? `${value} ${unit}` : String(value); +} + +const TICK_COUNT = 16; +const TEXT_PADDING_X = 4; +const TEXT_PADDING_X_OUTER = 0; // Less inset on outer-facing side (near edges) +const TEXT_PADDING_Y = 2; +const DETECTION_MARGIN_X = 12; +const DETECTION_MARGIN_X_OUTER = 4; // Small margin at edges for steep falloff - segments fully close at terminal positions +const DETECTION_MARGIN_Y = 12; +const TRACK_HEIGHT = 48; +const TEXT_RELEASE_INSET = 8; +const TRACK_EDGE_INSET = 4; // px from track edge - keeps elements visible at extremes +const THUMB_WIDTH = 12; // w-3 +// Text vertical offset: raised slightly from center +// Positive = raised, negative = lowered +const TEXT_VERTICAL_OFFSET = 0.5; + +function clampPercent(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(100, value)); +} + +// Convert a percentage (0-100) to an inset position string +// At 0%: 4px from left edge; at 100%: 4px from right edge +function toInsetPosition(percent: number): string { + const safePercent = clampPercent(percent); + return `calc(${TRACK_EDGE_INSET}px + (100% - ${TRACK_EDGE_INSET * 2}px) * ${safePercent / 100})`; +} + +// Radix keeps the thumb in bounds by applying a percent-dependent px offset. +// Matching this for fill clipping prevents handle/fill drift near extremes. +function getRadixThumbInBoundsOffsetPx(percent: number): number { + const safePercent = clampPercent(percent); + const halfWidth = THUMB_WIDTH / 2; + return halfWidth - (safePercent * halfWidth) / 50; +} + +function toRadixThumbPosition(percent: number): string { + const safePercent = clampPercent(percent); + const offsetPx = getRadixThumbInBoundsOffsetPx(safePercent); + return `calc(${safePercent}% + ${offsetPx}px)`; +} + +function signedDistanceToRoundedRect( + px: number, + py: number, + left: number, + right: number, + top: number, + bottom: number, + radiusLeft: number, + radiusRight: number, +): number { + const innerLeft = left + radiusLeft; + const innerRight = right - radiusRight; + const innerTop = top + Math.max(radiusLeft, radiusRight); + const innerBottom = bottom - Math.max(radiusLeft, radiusRight); + + const inLeftCorner = px < innerLeft; + const inRightCorner = px > innerRight; + const inCornerY = py < innerTop || py > innerBottom; + + if ((inLeftCorner || inRightCorner) && inCornerY) { + const radius = inLeftCorner ? radiusLeft : radiusRight; + const cornerX = inLeftCorner ? innerLeft : innerRight; + const cornerY = py < innerTop ? top + radius : bottom - radius; + const distToCornerCenter = Math.hypot(px - cornerX, py - cornerY); + return distToCornerCenter - radius; + } + + const dx = Math.max(left - px, px - right, 0); + const dy = Math.max(top - py, py - bottom, 0); + + if (dx === 0 && dy === 0) { + return -Math.min(px - left, right - px, py - top, bottom - py); + } + + return Math.max(dx, dy); +} + +const OUTER_EDGE_RADIUS_FACTOR = 0.3; // Reduced radius on outer-facing sides for steeper falloff + +function calculateGap( + thumbCenterX: number, + textRect: { left: number; right: number; height: number; centerY: number }, + isLeftAligned: boolean, +): number { + const { left, right, height, centerY } = textRect; + // Asymmetric padding/margin: outer-facing side has less padding, more margin + const paddingLeft = isLeftAligned ? TEXT_PADDING_X_OUTER : TEXT_PADDING_X; + const paddingRight = isLeftAligned ? TEXT_PADDING_X : TEXT_PADDING_X_OUTER; + const marginLeft = isLeftAligned + ? DETECTION_MARGIN_X_OUTER + : DETECTION_MARGIN_X; + const marginRight = isLeftAligned + ? DETECTION_MARGIN_X + : DETECTION_MARGIN_X_OUTER; + const paddingY = TEXT_PADDING_Y; + const marginY = DETECTION_MARGIN_Y; + const thumbCenterY = centerY; + + // Inner boundary (where max gap occurs) + const innerLeft = left - paddingLeft; + const innerRight = right + paddingRight; + const innerTop = centerY - height / 2 - paddingY; + const innerBottom = centerY + height / 2 + paddingY; + const innerHeight = height + paddingY * 2; + const innerRadius = innerHeight / 2; + // Smaller radius on outer-facing side (left for label, right for value) + const innerRadiusLeft = isLeftAligned + ? innerRadius * OUTER_EDGE_RADIUS_FACTOR + : innerRadius; + const innerRadiusRight = isLeftAligned + ? innerRadius + : innerRadius * OUTER_EDGE_RADIUS_FACTOR; + + // Outer boundary (where effect starts) - proportionally larger + const outerLeft = left - paddingLeft - marginLeft; + const outerRight = right + paddingRight + marginRight; + const outerTop = centerY - height / 2 - paddingY - marginY; + const outerBottom = centerY + height / 2 + paddingY + marginY; + const outerHeight = height + paddingY * 2 + marginY * 2; + const outerRadius = outerHeight / 2; + const outerRadiusLeft = isLeftAligned + ? outerRadius * OUTER_EDGE_RADIUS_FACTOR + : outerRadius; + const outerRadiusRight = isLeftAligned + ? outerRadius + : outerRadius * OUTER_EDGE_RADIUS_FACTOR; + + const outerDist = signedDistanceToRoundedRect( + thumbCenterX, + thumbCenterY, + outerLeft, + outerRight, + outerTop, + outerBottom, + outerRadiusLeft, + outerRadiusRight, + ); + + // Outside outer boundary - no gap + if (outerDist > 0) return 0; + + const innerDist = signedDistanceToRoundedRect( + thumbCenterX, + thumbCenterY, + innerLeft, + innerRight, + innerTop, + innerBottom, + innerRadiusLeft, + innerRadiusRight, + ); + + // Inside inner boundary - max gap + const maxGap = height + paddingY * 2; + if (innerDist <= 0) return maxGap; + + // Between boundaries - linear interpolation + // outerDist is negative (inside outer), innerDist is positive (outside inner) + const totalDist = Math.abs(outerDist) + innerDist; + const t = Math.abs(outerDist) / totalDist; + + return maxGap * t; +} + +interface SliderRowProps { + config: SliderConfig; + value: number; + onChange: (value: number) => void; + trackClassName?: string; + fillClassName?: string; + handleClassName?: string; +} + +function SliderRow({ + config, + value, + onChange, + trackClassName, + fillClassName, + handleClassName, +}: SliderRowProps) { + const { id, label, min, max, step = 1, unit, precision, disabled } = config; + // Per-slider theming overrides component-level theming + const resolvedTrackClassName = config.trackClassName ?? trackClassName; + const resolvedFillClassName = config.fillClassName ?? fillClassName; + const resolvedHandleClassName = config.handleClassName ?? handleClassName; + const crossesZero = min < 0 && max > 0; + const [isDragging, setIsDragging] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + const trackRef = useRef(null); + const labelRef = useRef(null); + const valueRef = useRef(null); + + const [dragGap, setDragGap] = useState(0); + const [fullGap, setFullGap] = useState(0); + const [intersectsText, setIntersectsText] = useState(false); + const [layoutVersion, setLayoutVersion] = useState(0); + + useEffect(() => { + if (!isDragging) return; + const handlePointerUp = () => setIsDragging(false); + document.addEventListener("pointerup", handlePointerUp); + return () => document.removeEventListener("pointerup", handlePointerUp); + }, [isDragging]); + + useEffect(() => { + const track = trackRef.current; + const labelEl = labelRef.current; + const valueEl = valueRef.current; + if (!track || !labelEl || !valueEl) return; + + const bumpLayoutVersion = () => setLayoutVersion((v) => v + 1); + + if (typeof ResizeObserver !== "undefined") { + const observer = new ResizeObserver(() => { + bumpLayoutVersion(); + }); + observer.observe(track); + observer.observe(labelEl); + observer.observe(valueEl); + return () => observer.disconnect(); + } + + window.addEventListener("resize", bumpLayoutVersion); + return () => window.removeEventListener("resize", bumpLayoutVersion); + }, []); + + useLayoutEffect(() => { + const track = trackRef.current; + const labelEl = labelRef.current; + const valueEl = valueRef.current; + + if (!track || !labelEl || !valueEl) return; + + const trackRect = track.getBoundingClientRect(); + const labelRect = labelEl.getBoundingClientRect(); + const valueRect = valueEl.getBoundingClientRect(); + + const trackWidth = trackRect.width; + const valuePercent = sliderRangeToPercent({ value, min, max }); + // Use same inset coordinate system as visual elements + const thumbCenterPx = + (trackWidth * clampPercent(valuePercent)) / 100 + + getRadixThumbInBoundsOffsetPx(valuePercent); + const thumbHalfWidth = THUMB_WIDTH / 2; + + // Text is raised by TEXT_VERTICAL_OFFSET from center + const trackCenterY = TRACK_HEIGHT / 2 - TEXT_VERTICAL_OFFSET; + + const labelGap = calculateGap( + thumbCenterPx, + { + left: labelRect.left - trackRect.left, + right: labelRect.right - trackRect.left, + height: labelRect.height, + centerY: trackCenterY, + }, + true, + ); // label is left-aligned + + const valueGap = calculateGap( + thumbCenterPx, + { + left: valueRect.left - trackRect.left, + right: valueRect.right - trackRect.left, + height: valueRect.height, + centerY: trackCenterY, + }, + false, + ); // value is right-aligned + + setDragGap(Math.max(labelGap, valueGap)); + + // Tight intersection check for release state + // Inset by px-2 (8px) padding to check against actual text, not padded container + const labelLeft = labelRect.left - trackRect.left + TEXT_RELEASE_INSET; + const labelRight = labelRect.right - trackRect.left - TEXT_RELEASE_INSET; + const valueLeft = valueRect.left - trackRect.left + TEXT_RELEASE_INSET; + const valueRight = valueRect.right - trackRect.left - TEXT_RELEASE_INSET; + + const thumbLeft = thumbCenterPx - thumbHalfWidth; + const thumbRight = thumbCenterPx + thumbHalfWidth; + + const hitsLabel = thumbRight > labelLeft && thumbLeft < labelRight; + const hitsValue = thumbRight > valueLeft && thumbLeft < valueRight; + + setIntersectsText(hitsLabel || hitsValue); + + // Calculate full separation gap for release state + // Use the max gap of whichever text element(s) the handle intersects + const labelFullGap = labelRect.height + TEXT_PADDING_Y * 2; + const valueFullGap = valueRect.height + TEXT_PADDING_Y * 2; + const releaseGap = + hitsLabel && hitsValue + ? Math.max(labelFullGap, valueFullGap) + : hitsLabel + ? labelFullGap + : hitsValue + ? valueFullGap + : 0; + setFullGap(releaseGap); + }, [value, min, max, layoutVersion]); + + // While dragging: use distance-based separation, but never collapse below + // the release split when the thumb still intersects text. + const gap = isDragging + ? Math.max(dragGap, intersectsText ? fullGap : 0) + : intersectsText + ? fullGap + : 0; + + const ticks = useMemo(() => { + // Generate equidistant ticks regardless of step value + const majorTickCount = TICK_COUNT; + const result: { percent: number; isCenter: boolean; isSubtick: boolean }[] = + []; + + for (let i = 0; i <= majorTickCount; i++) { + const percent = (i / majorTickCount) * 100; + const isCenter = !crossesZero && percent === 50; + + // Skip the center tick (50%) for crossesZero sliders + if (crossesZero && percent === 50) continue; + + // Add subtick at midpoint before this tick (except for first) + if (i > 0) { + const prevPercent = ((i - 1) / majorTickCount) * 100; + // Don't add subtick if it would be at 50% for crossesZero + const midPercent = (prevPercent + percent) / 2; + if (!(crossesZero && midPercent === 50)) { + result.push({ + percent: midPercent, + isCenter: false, + isSubtick: true, + }); + } + } + + result.push({ percent, isCenter, isSubtick: false }); + } + + return result; + }, [crossesZero]); + + const zeroPercent = crossesZero + ? sliderRangeToPercent({ value: 0, min, max }) + : 0; + const valuePercent = sliderRangeToPercent({ value, min, max }); + + // Fill clip-path uses the same inset coordinate system as the handle. + // This keeps the collapsed stroke aligned with the fill edge near extremes. + const fillClipPath = useMemo(() => { + const toClipFromRightInset = (percent: number) => + `calc(100% - ${toRadixThumbPosition(percent)})`; + const toClipFromLeftInset = (percent: number) => + toRadixThumbPosition(percent); + const TERMINAL_EPSILON = 1e-6; + const snapLeftInset = (percent: number) => { + if (percent <= TERMINAL_EPSILON) return "0"; + if (percent >= 100 - TERMINAL_EPSILON) return "100%"; + return toClipFromLeftInset(percent); + }; + const snapRightInset = (percent: number) => { + if (percent <= TERMINAL_EPSILON) return "100%"; + if (percent >= 100 - TERMINAL_EPSILON) return "0"; + return toClipFromRightInset(percent); + }; + + if (crossesZero) { + // Keep center anchor stable by always clipping the low/high pair, + // independent of sign branch, then snapping at terminal edges. + const lowPercent = Math.min(valuePercent, zeroPercent); + const highPercent = Math.max(valuePercent, zeroPercent); + return `inset(0 ${snapRightInset(highPercent)} 0 ${snapLeftInset(lowPercent)})`; + } + // Non-crossing: fill starts at left edge; snap right inset at terminals. + return `inset(0 ${snapRightInset(valuePercent)} 0 0)`; + }, [crossesZero, zeroPercent, valuePercent]); + + const fillMaskImage = crossesZero + ? "linear-gradient(to right, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.35) 50%, rgba(0,0,0,0.7) 100%)" + : "linear-gradient(to right, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%)"; + + // Metallic reflection gradient that follows the handle position + // Visible while dragging OR when resting at edges (0%/100%) + const reflectionStyle = useMemo(() => { + const edgeThreshold = 3; + const nearEdge = + valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold; + + // Narrower spread when stationary at edges (~35% narrower) + const spreadPercent = nearEdge && !isDragging ? 6.5 : 10; + const handlePos = toRadixThumbPosition(valuePercent); + const start = `clamp(0%, calc(${handlePos} - ${spreadPercent}%), 100%)`; + const end = `clamp(0%, calc(${handlePos} + ${spreadPercent}%), 100%)`; + + const gradient = `linear-gradient(to right, + transparent ${start}, + white ${handlePos}, + transparent ${end})`; + + return { + background: gradient, + WebkitMask: + "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)", + WebkitMaskComposite: "xor", + maskComposite: "exclude", + padding: "1px", + }; + }, [valuePercent, isDragging]); + + // Opacity scales with handle size: rest → hover → drag + const reflectionOpacity = useMemo(() => { + const edgeThreshold = 3; + const atEdge = + valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold; + + if (isDragging || atEdge) { + return 1; + } + if (isHovered) { + return 0.6; + } + return 0; + }, [valuePercent, isDragging, isHovered]); + + const handleValueChange = useCallback( + (values: number[]) => { + if (values[0] !== undefined) { + onChange(values[0]); + } + }, + [onChange], + ); + + return ( +
+ span]:transition-[left,transform] [&>span]:duration-45 [&>span]:ease-linear" + : "[&>span]:transition-[left,transform] [&>span]:duration-90 [&>span]:ease-[cubic-bezier(0.22,1,0.36,1)]", + "[&>span]:will-change-[left,transform]", + "motion-reduce:[&>span]:transition-none", + disabled && "pointer-events-none opacity-50", + )} + value={[value]} + onValueChange={handleValueChange} + onPointerDown={() => setIsDragging(true)} + onPointerUp={() => setIsDragging(false)} + onPointerEnter={() => setIsHovered(true)} + onPointerLeave={() => setIsHovered(false)} + min={min} + max={max} + step={step} + disabled={disabled} + aria-valuetext={getAriaValueText(value, min, max, unit)} + > + +
+ + {ticks.map((tick, i) => { + const isEdge = + !tick.isSubtick && (tick.percent === 0 || tick.percent === 100); + return ( + + ); + })} + + + {/* Metallic reflection overlay - follows handle, brightness scales with interaction */} +
+ + + {(() => { + // Calculate morph state + const isActive = isHovered || isDragging; + + // Indicator stays centered on the real thumb while CSS transitions + // smooth thumb wrapper and fill movement together. + const fillEdgeOffset = 0; + + // Hide rest-state indicator at edges (0% or 100%) - the reflection gradient handles this + const edgeThreshold = 3; + const atEdge = + valuePercent <= edgeThreshold || + valuePercent >= 100 - edgeThreshold; + const restOpacity = atEdge ? 0 : 0.25; + + // Asymmetric segment heights: gap is shifted up to match raised text position + // Top segment is shorter, bottom segment is taller + const topHeight = + isActive && gap > 0 + ? `calc(50% - ${gap / 2 + TEXT_VERTICAL_OFFSET}px)` + : "50%"; + const bottomHeight = + isActive && gap > 0 + ? `calc(50% - ${gap / 2 - TEXT_VERTICAL_OFFSET}px)` + : "50%"; + + return ( + <> + 0 + ? "rounded-full" + : "rounded-t-full" + : "rounded-t-sm", + isDragging ? "w-2" : isActive ? "w-1.5" : "w-px", + resolvedHandleClassName ?? "bg-primary", + )} + style={{ + transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`, + height: topHeight, + opacity: isActive ? 1 : restOpacity, + }} + /> + 0 + ? "rounded-full" + : "rounded-b-full" + : "rounded-b-sm", + isDragging ? "w-2" : isActive ? "w-1.5" : "w-px", + resolvedHandleClassName ?? "bg-primary", + )} + style={{ + transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`, + height: bottomHeight, + opacity: isActive ? 1 : restOpacity, + }} + /> + + ); + })()} + + +
+ + {label} + + + {formatSignedValue(value, min, max, precision, unit)} + +
+ +
+ ); +} + +export function ParameterSlider({ + id, + sliders, + values: controlledValues, + onChange, + actions, + onAction, + onBeforeAction, + className, + trackClassName, + fillClassName, + handleClassName, +}: ParameterSliderProps) { + const slidersSignature = useMemo( + () => createSliderSignature(sliders), + [sliders], + ); + const sliderSnapshot = useMemo( + () => createSliderValueSnapshot(sliders), + [sliders], + ); + const { + value: currentValues, + isControlled, + setValue, + setUncontrolledValue, + } = useControllableState({ + value: controlledValues, + defaultValue: sliderSnapshot, + onChange, + }); + + useSignatureReset(slidersSignature, () => { + if (!isControlled) { + setUncontrolledValue(sliderSnapshot); + } + }); + + const valueMap = useMemo(() => { + const map = new Map(); + for (const v of currentValues) { + map.set(v.id, v.value); + } + return map; + }, [currentValues]); + + const updateValue = useCallback( + (sliderId: string, newValue: number) => { + setValue((prev) => + prev.map((v) => (v.id === sliderId ? { ...v, value: newValue } : v)), + ); + }, + [setValue], + ); + + const handleReset = useCallback(() => { + setValue(sliderSnapshot); + }, [setValue, sliderSnapshot]); + + const handleAction = useCallback( + async (actionId: string) => { + let nextValues = currentValues; + if (actionId === "reset") { + handleReset(); + nextValues = sliderSnapshot; + } + + await onAction?.(actionId, nextValues); + }, + [currentValues, handleReset, onAction, sliderSnapshot], + ); + + const normalizedActions = useMemo(() => { + const normalized = normalizeActionsConfig(actions); + if (normalized) return normalized; + return { + items: [ + { id: "reset", label: "Reset", variant: "ghost" as const }, + { id: "apply", label: "Apply", variant: "default" as const }, + ], + align: "right" as const, + }; + }, [actions]); + + return ( +
+
+ {sliders.map((slider) => ( + updateValue(slider.id, v)} + trackClassName={trackClassName} + fillClassName={fillClassName} + handleClassName={handleClassName} + /> + ))} +
+ +
+ onBeforeAction(actionId, currentValues) + : undefined + } + /> +
+
+ ); +} diff --git a/frontend/src/toolui/components/parameter-slider/schema.ts b/frontend/src/toolui/components/parameter-slider/schema.ts new file mode 100644 index 00000000..86673967 --- /dev/null +++ b/frontend/src/toolui/components/parameter-slider/schema.ts @@ -0,0 +1,114 @@ +import { z } from "zod"; +import { type ActionsProp } from "../shared/actions-config"; +import type { EmbeddedActionsProps } from "../shared/embedded-actions"; +import { defineToolUiContract } from "../shared/contract"; +import { + SerializableActionSchema, + SerializableActionsConfigSchema, + ToolUIIdSchema, + ToolUIRoleSchema, +} from "../shared/schema"; + +export const SliderConfigSchema = z + .object({ + id: z.string().min(1), + label: z.string().min(1), + min: z.number().finite(), + max: z.number().finite(), + step: z.number().finite().positive().optional(), + value: z.number().finite(), + unit: z.string().optional(), + precision: z.number().int().min(0).optional(), + disabled: z.boolean().optional(), + trackClassName: z.string().optional(), + fillClassName: z.string().optional(), + handleClassName: z.string().optional(), + }) + .superRefine((slider, ctx) => { + if (slider.max <= slider.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["max"], + message: "max must be greater than min", + }); + } + + if (slider.value < slider.min || slider.value > slider.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["value"], + message: "value must be between min and max", + }); + } + }); + +export type SliderConfig = z.infer; + +export const SerializableParameterSliderSchema = z + .object({ + id: ToolUIIdSchema, + role: ToolUIRoleSchema.optional(), + sliders: z.array(SliderConfigSchema).min(1), + actions: z + .union([ + z.array(SerializableActionSchema), + SerializableActionsConfigSchema, + ]) + .optional(), + }) + .strict() + .superRefine((payload, ctx) => { + const seenIds = new Map(); + + payload.sliders.forEach((slider, index) => { + const firstSeenAt = seenIds.get(slider.id); + if (firstSeenAt !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["sliders", index, "id"], + message: `duplicate slider id '${slider.id}' (first seen at index ${firstSeenAt})`, + }); + return; + } + seenIds.set(slider.id, index); + }); + }); + +export type SerializableParameterSlider = z.infer< + typeof SerializableParameterSliderSchema +>; + +const SerializableParameterSliderSchemaContract = defineToolUiContract( + "ParameterSlider", + SerializableParameterSliderSchema, +); + +export const parseSerializableParameterSlider: ( + input: unknown, +) => SerializableParameterSlider = + SerializableParameterSliderSchemaContract.parse; + +export const safeParseSerializableParameterSlider: ( + input: unknown, +) => SerializableParameterSlider | null = + SerializableParameterSliderSchemaContract.safeParse; + +export interface SliderValue { + id: string; + value: number; +} + +export interface ParameterSliderProps extends Omit< + SerializableParameterSlider, + "actions" +> { + className?: string; + values?: SliderValue[]; + onChange?: (values: SliderValue[]) => void; + actions?: ActionsProp; + onAction?: EmbeddedActionsProps["onAction"]; + onBeforeAction?: EmbeddedActionsProps["onBeforeAction"]; + trackClassName?: string; + fillClassName?: string; + handleClassName?: string; +} diff --git a/frontend/src/toolui/components/plan/README.md b/frontend/src/toolui/components/plan/README.md new file mode 100644 index 00000000..2e7b7db7 --- /dev/null +++ b/frontend/src/toolui/components/plan/README.md @@ -0,0 +1,19 @@ +# Plan + +Implementation for the "plan" Tool UI surface. + +## Files + +- public exports: components/tool-ui/plan/index.tsx +- serializable schema + parse helpers: components/tool-ui/plan/schema.ts + +## Companion assets + +- Docs page: app/docs/plan/content.mdx +- Preset payload: lib/presets/plan.ts + +## Quick check + +Run this after edits: + +pnpm test diff --git a/frontend/src/toolui/components/plan/_adapter.tsx b/frontend/src/toolui/components/plan/_adapter.tsx new file mode 100644 index 00000000..48ea8dc9 --- /dev/null +++ b/frontend/src/toolui/components/plan/_adapter.tsx @@ -0,0 +1,32 @@ +/** + * Adapter: UI and utility re-exports for copy-standalone portability. + * + * When copying this component to another project, update these imports + * to match your project's paths: + * + * cn → Your Tailwind merge utility (e.g., "@toolui/lib/utils", "~/lib/cn") + * Accordion → shadcn/ui Accordion + * Card → shadcn/ui Card + * Collapsible → shadcn/ui Collapsible + */ + +export { cn } from "@toolui/lib/utils"; +export { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from "@toolui/ui/accordion"; +export { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "@toolui/ui/card"; +export { + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "@toolui/ui/collapsible"; diff --git a/frontend/src/toolui/components/plan/index.tsx b/frontend/src/toolui/components/plan/index.tsx new file mode 100644 index 00000000..de5021c2 --- /dev/null +++ b/frontend/src/toolui/components/plan/index.tsx @@ -0,0 +1,7 @@ +export { Plan, PlanCompact } from "./plan"; +export type { + PlanProps, + PlanTodo, + PlanTodoStatus, + SerializablePlan, +} from "./schema"; diff --git a/frontend/src/toolui/components/plan/plan.tsx b/frontend/src/toolui/components/plan/plan.tsx new file mode 100644 index 00000000..a62032bb --- /dev/null +++ b/frontend/src/toolui/components/plan/plan.tsx @@ -0,0 +1,428 @@ +"use client"; + +import * as React from "react"; +import { useMemo, useState, useEffect, useRef, memo } from "react"; +import { Loader2, Check, X, MoreHorizontal, ChevronRight } from "lucide-react"; +import type { PlanProps, PlanTodo, PlanTodoStatus } from "./schema"; +import { + cn, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "./_adapter"; +import { calculatePlanProgress, shouldCelebrateProgress } from "./progress"; + +const INITIAL_VISIBLE_TODO_COUNT = 4; + +const TodoIcon = memo(function TodoIcon({ + status, +}: { + status: PlanTodoStatus; +}) { + if (status === "pending") { + return ( +