From 7f2794a015f24202be104e20e3ca34021150394c Mon Sep 17 00:00:00 2001 From: Brendan Dash Date: Thu, 30 Apr 2026 15:18:05 +0800 Subject: [PATCH 1/7] feat: wip --- .../app/api/registry/[component]/route.ts | 492 +-- .../app/api/registry/_lib/create-registry.ts | 497 +++ .../api/registry/base/[component]/route.ts | 13 + .../custom/elements-installer-tabs.tsx | 68 + .../components/custom/elements-installer.tsx | 100 +- .../custom/preview-library-switch.tsx | 18 + apps/docs/components/custom/preview.tsx | 126 +- apps/docs/components/geistdocs/navbar.tsx | 2 + .../geistdocs/ui-library-switcher.tsx | 42 + .../components/(chatbot)/confirmation.mdx | 3 +- .../content/components/(chatbot)/message.mdx | 3 +- apps/docs/hooks/geistdocs/use-ui-library.ts | 14 + apps/docs/package.json | 5 +- packages/base-ui/components.json | 21 + packages/base-ui/components/ui/accordion.tsx | 77 + .../base-ui/components/ui/alert-dialog.tsx | 186 + packages/base-ui/components/ui/alert.tsx | 75 + .../base-ui/components/ui/aspect-ratio.tsx | 22 + packages/base-ui/components/ui/avatar.tsx | 108 + packages/base-ui/components/ui/badge.tsx | 51 + packages/base-ui/components/ui/breadcrumb.tsx | 121 + .../base-ui/components/ui/button-group.tsx | 86 + packages/base-ui/components/ui/button.tsx | 57 + packages/base-ui/components/ui/calendar.tsx | 227 ++ packages/base-ui/components/ui/card.tsx | 102 + packages/base-ui/components/ui/carousel.tsx | 241 ++ packages/base-ui/components/ui/chart.tsx | 372 ++ packages/base-ui/components/ui/checkbox.tsx | 27 + .../base-ui/components/ui/collapsible.tsx | 21 + packages/base-ui/components/ui/combobox.tsx | 299 ++ packages/base-ui/components/ui/command.tsx | 195 + .../base-ui/components/ui/context-menu.tsx | 271 ++ packages/base-ui/components/ui/dialog.tsx | 155 + packages/base-ui/components/ui/direction.tsx | 6 + packages/base-ui/components/ui/drawer.tsx | 130 + .../base-ui/components/ui/dropdown-menu.tsx | 271 ++ packages/base-ui/components/ui/empty.tsx | 100 + packages/base-ui/components/ui/field.tsx | 237 ++ packages/base-ui/components/ui/hover-card.tsx | 50 + .../base-ui/components/ui/input-group.tsx | 157 + packages/base-ui/components/ui/input-otp.tsx | 85 + packages/base-ui/components/ui/input.tsx | 19 + packages/base-ui/components/ui/item.tsx | 200 + packages/base-ui/components/ui/kbd.tsx | 26 + packages/base-ui/components/ui/label.tsx | 19 + packages/base-ui/components/ui/menubar.tsx | 283 ++ .../base-ui/components/ui/native-select.tsx | 64 + .../base-ui/components/ui/navigation-menu.tsx | 170 + packages/base-ui/components/ui/pagination.tsx | 132 + packages/base-ui/components/ui/popover.tsx | 89 + packages/base-ui/components/ui/progress.tsx | 82 + .../base-ui/components/ui/radio-group.tsx | 37 + packages/base-ui/components/ui/resizable.tsx | 49 + .../base-ui/components/ui/scroll-area.tsx | 54 + packages/base-ui/components/ui/select.tsx | 201 + packages/base-ui/components/ui/separator.tsx | 24 + packages/base-ui/components/ui/sheet.tsx | 133 + packages/base-ui/components/ui/sidebar.tsx | 722 ++++ packages/base-ui/components/ui/skeleton.tsx | 13 + packages/base-ui/components/ui/slider.tsx | 51 + packages/base-ui/components/ui/sonner.tsx | 45 + packages/base-ui/components/ui/spinner.tsx | 15 + packages/base-ui/components/ui/switch.tsx | 31 + packages/base-ui/components/ui/table.tsx | 115 + packages/base-ui/components/ui/tabs.tsx | 81 + packages/base-ui/components/ui/textarea.tsx | 17 + .../base-ui/components/ui/toggle-group.tsx | 88 + packages/base-ui/components/ui/toggle.tsx | 44 + packages/base-ui/components/ui/tooltip.tsx | 65 + packages/base-ui/hooks/use-mobile.ts | 21 + packages/base-ui/lib/utils.ts | 6 + packages/base-ui/package.json | 32 + packages/base-ui/tsconfig.json | 18 + packages/cli/index.js | 24 +- packages/elements-base/__tests__/README.md | 110 + .../elements-base/__tests__/agent.test.tsx | 264 ++ .../elements-base/__tests__/artifact.test.tsx | 103 + .../__tests__/audio-player.test.tsx | 445 +++ .../elements-base/__tests__/canvas.test.tsx | 48 + .../__tests__/chain-of-thought.test.tsx | 208 ++ .../__tests__/code-block.test.tsx | 150 + .../elements-base/__tests__/commit.test.tsx | 205 + .../__tests__/confirmation.test.tsx | 271 ++ .../__tests__/connection.test.tsx | 89 + .../elements-base/__tests__/context.test.tsx | 371 ++ .../elements-base/__tests__/controls.test.tsx | 48 + .../__tests__/conversation.test.tsx | 409 ++ .../elements-base/__tests__/edge.test.tsx | 397 ++ .../__tests__/environment-variables.test.tsx | 177 + .../__tests__/file-tree.test.tsx | 191 + .../elements-base/__tests__/image.test.tsx | 48 + .../__tests__/inline-citation.test.tsx | 352 ++ .../__tests__/jsx-preview.test.tsx | 343 ++ .../elements-base/__tests__/message.test.tsx | 338 ++ .../__tests__/mic-selector.test.tsx | 816 ++++ .../__tests__/model-selector.test.tsx | 568 +++ .../elements-base/__tests__/node.test.tsx | 95 + .../__tests__/open-in-chat.test.tsx | 215 ++ .../__tests__/package-info.test.tsx | 139 + .../elements-base/__tests__/panel.test.tsx | 45 + .../elements-base/__tests__/persona.test.tsx | 846 +++++ .../elements-base/__tests__/plan.test.tsx | 565 +++ .../__tests__/prompt-input.test.tsx | 3296 +++++++++++++++++ .../elements-base/__tests__/queue.test.tsx | 350 ++ .../__tests__/reasoning.test.tsx | 251 ++ .../elements-base/__tests__/sandbox.test.tsx | 414 +++ .../__tests__/schema-display.test.tsx | 228 ++ packages/elements-base/__tests__/setup.ts | 52 + .../elements-base/__tests__/shimmer.test.tsx | 111 + .../elements-base/__tests__/snippet.test.tsx | 102 + .../elements-base/__tests__/sources.test.tsx | 115 + .../__tests__/speech-input.test.tsx | 716 ++++ .../__tests__/stack-trace.test.tsx | 604 +++ .../elements-base/__tests__/style-mock.js | 3 + .../__tests__/suggestion.test.tsx | 67 + .../elements-base/__tests__/task.test.tsx | 133 + .../elements-base/__tests__/terminal.test.tsx | 196 + .../__tests__/test-results.test.tsx | 278 ++ .../elements-base/__tests__/tool.test.tsx | 253 ++ .../elements-base/__tests__/toolbar.test.tsx | 79 + .../__tests__/transcription.test.tsx | 794 ++++ .../__tests__/voice-selector.test.tsx | 907 +++++ .../__tests__/web-preview.test.tsx | 209 ++ packages/elements-base/package.json | 55 + packages/elements-base/src/agent.tsx | 141 + packages/elements-base/src/artifact.tsx | 148 + packages/elements-base/src/attachments.tsx | 422 +++ packages/elements-base/src/audio-player.tsx | 258 ++ packages/elements-base/src/canvas.tsx | 26 + .../elements-base/src/chain-of-thought.tsx | 223 ++ packages/elements-base/src/checkpoint.tsx | 73 + packages/elements-base/src/code-block.tsx | 562 +++ packages/elements-base/src/commit.tsx | 461 +++ packages/elements-base/src/confirmation.tsx | 174 + packages/elements-base/src/connection.tsx | 28 + packages/elements-base/src/context.tsx | 417 +++ packages/elements-base/src/controls.tsx | 18 + packages/elements-base/src/conversation.tsx | 168 + packages/elements-base/src/edge.tsx | 143 + .../src/environment-variables.tsx | 324 ++ packages/elements-base/src/file-tree.tsx | 306 ++ .../src/hooks/use-controllable-state.ts | 39 + packages/elements-base/src/image.tsx | 24 + .../elements-base/src/inline-citation.tsx | 298 ++ packages/elements-base/src/jsx-preview.tsx | 310 ++ packages/elements-base/src/message.tsx | 360 ++ packages/elements-base/src/mic-selector.tsx | 374 ++ packages/elements-base/src/model-selector.tsx | 213 ++ packages/elements-base/src/node.tsx | 71 + packages/elements-base/src/open-in-chat.tsx | 390 ++ packages/elements-base/src/package-info.tsx | 239 ++ packages/elements-base/src/panel.tsx | 15 + packages/elements-base/src/persona.tsx | 306 ++ packages/elements-base/src/plan.tsx | 155 + packages/elements-base/src/prompt-input.tsx | 1459 ++++++++ packages/elements-base/src/queue.tsx | 276 ++ packages/elements-base/src/reasoning.tsx | 226 ++ packages/elements-base/src/sandbox.tsx | 132 + packages/elements-base/src/schema-display.tsx | 471 +++ packages/elements-base/src/shimmer.tsx | 77 + packages/elements-base/src/snippet.tsx | 145 + packages/elements-base/src/sources.tsx | 77 + packages/elements-base/src/speech-input.tsx | 323 ++ packages/elements-base/src/stack-trace.tsx | 532 +++ packages/elements-base/src/suggestion.tsx | 54 + packages/elements-base/src/task.tsx | 100 + packages/elements-base/src/terminal.tsx | 273 ++ packages/elements-base/src/test-results.tsx | 496 +++ packages/elements-base/src/tool.tsx | 173 + packages/elements-base/src/toolbar.tsx | 16 + packages/elements-base/src/transcription.tsx | 126 + packages/elements-base/src/voice-selector.tsx | 529 +++ packages/elements-base/src/web-preview.tsx | 285 ++ packages/elements-base/tsconfig.json | 16 + packages/elements-base/vitest.config.mts | 54 + packages/examples-base/package.json | 27 + packages/examples-base/src/agent.tsx | 61 + packages/examples-base/src/artifact.tsx | 127 + .../examples-base/src/attachments-inline.tsx | 128 + .../examples-base/src/attachments-list.tsx | 95 + packages/examples-base/src/attachments.tsx | 85 + .../examples-base/src/audio-player-remote.tsx | 35 + packages/examples-base/src/audio-player.tsx | 68 + .../examples-base/src/chain-of-thought.tsx | 82 + packages/examples-base/src/chatbot.tsx | 725 ++++ packages/examples-base/src/checkpoint.tsx | 113 + .../examples-base/src/code-block-dark.tsx | 46 + packages/examples-base/src/code-block.tsx | 120 + packages/examples-base/src/commit.tsx | 94 + .../src/confirmation-accepted.tsx | 40 + .../src/confirmation-rejected.tsx | 40 + .../src/confirmation-request.tsx | 54 + packages/examples-base/src/confirmation.tsx | 55 + packages/examples-base/src/context.tsx | 45 + packages/examples-base/src/conversation.tsx | 177 + packages/examples-base/src/demo-chatgpt.tsx | 789 ++++ packages/examples-base/src/demo-claude.tsx | 827 +++++ packages/examples-base/src/demo-cursor.tsx | 707 ++++ packages/examples-base/src/demo-grok.tsx | 853 +++++ packages/examples-base/src/demo-workflow.tsx | 183 + .../src/environment-variables.tsx | 62 + .../examples-base/src/file-tree-basic.tsx | 18 + .../examples-base/src/file-tree-expanded.tsx | 21 + .../examples-base/src/file-tree-selection.tsx | 24 + packages/examples-base/src/file-tree.tsx | 42 + packages/examples-base/src/image.tsx | 20 + .../examples-base/src/inline-citation.tsx | 97 + packages/examples-base/src/jsx-preview.tsx | 104 + packages/examples-base/src/message.tsx | 365 ++ packages/examples-base/src/mic-selector.tsx | 49 + packages/examples-base/src/model-selector.tsx | 367 ++ packages/examples-base/src/open-in-chat.tsx | 33 + packages/examples-base/src/package-info.tsx | 51 + .../examples-base/src/persona-command.tsx | 112 + packages/examples-base/src/persona-glint.tsx | 112 + packages/examples-base/src/persona-halo.tsx | 112 + packages/examples-base/src/persona-mana.tsx | 112 + .../examples-base/src/persona-obsidian.tsx | 112 + packages/examples-base/src/persona-opal.tsx | 112 + packages/examples-base/src/plan.tsx | 65 + .../examples-base/src/prompt-input-cursor.tsx | 498 +++ .../src/prompt-input-tooltip.tsx | 44 + packages/examples-base/src/prompt-input.tsx | 263 ++ .../examples-base/src/queue-prompt-input.tsx | 392 ++ packages/examples-base/src/queue.tsx | 295 ++ packages/examples-base/src/reasoning.tsx | 71 + packages/examples-base/src/sandbox.tsx | 178 + .../src/schema-display-basic.tsx | 9 + .../examples-base/src/schema-display-body.tsx | 20 + .../src/schema-display-nested.tsx | 23 + .../src/schema-display-params.tsx | 16 + packages/examples-base/src/schema-display.tsx | 110 + .../examples-base/src/shimmer-duration.tsx | 31 + .../examples-base/src/shimmer-elements.tsx | 43 + packages/examples-base/src/shimmer.tsx | 17 + packages/examples-base/src/snippet-plain.tsx | 19 + packages/examples-base/src/snippet.tsx | 25 + packages/examples-base/src/sources-custom.tsx | 39 + packages/examples-base/src/sources.tsx | 32 + packages/examples-base/src/speech-input.tsx | 84 + .../src/stack-trace-collapsed.tsx | 39 + .../src/stack-trace-no-internal.tsx | 41 + packages/examples-base/src/stack-trace.tsx | 58 + .../examples-base/src/suggestion-input.tsx | 144 + packages/examples-base/src/suggestion.tsx | 32 + packages/examples-base/src/task.tsx | 59 + packages/examples-base/src/terminal-basic.tsx | 7 + packages/examples-base/src/terminal-clear.tsx | 19 + .../examples-base/src/terminal-streaming.tsx | 38 + packages/examples-base/src/terminal.tsx | 82 + .../examples-base/src/test-results-basic.tsx | 27 + .../examples-base/src/test-results-errors.tsx | 51 + .../examples-base/src/test-results-suites.tsx | 40 + packages/examples-base/src/test-results.tsx | 102 + .../src/tool-input-available.tsx | 36 + .../src/tool-input-streaming.tsx | 35 + .../src/tool-output-available.tsx | 80 + .../examples-base/src/tool-output-error.tsx | 44 + packages/examples-base/src/tool.tsx | 222 ++ packages/examples-base/src/transcription.tsx | 290 ++ packages/examples-base/src/v0-clone.tsx | 259 ++ packages/examples-base/src/voice-selector.tsx | 243 ++ packages/examples-base/src/web-preview.tsx | 115 + packages/examples-base/src/workflow.tsx | 166 + packages/examples-base/tsconfig.json | 14 + packages/shadcn-ui/components.json | 2 +- .../shadcn-ui/components/ui/accordion.tsx | 29 +- .../shadcn-ui/components/ui/alert-dialog.tsx | 92 +- packages/shadcn-ui/components/ui/alert.tsx | 20 +- packages/shadcn-ui/components/ui/avatar.tsx | 69 +- packages/shadcn-ui/components/ui/badge.tsx | 21 +- .../shadcn-ui/components/ui/breadcrumb.tsx | 35 +- .../shadcn-ui/components/ui/button-group.tsx | 14 +- packages/shadcn-ui/components/ui/button.tsx | 37 +- packages/shadcn-ui/components/ui/calendar.tsx | 78 +- packages/shadcn-ui/components/ui/card.tsx | 25 +- packages/shadcn-ui/components/ui/carousel.tsx | 15 +- packages/shadcn-ui/components/ui/chart.tsx | 82 +- packages/shadcn-ui/components/ui/checkbox.tsx | 9 +- packages/shadcn-ui/components/ui/combobox.tsx | 299 ++ packages/shadcn-ui/components/ui/command.tsx | 65 +- .../shadcn-ui/components/ui/context-menu.tsx | 129 +- packages/shadcn-ui/components/ui/dialog.tsx | 52 +- .../shadcn-ui/components/ui/direction.tsx | 22 + packages/shadcn-ui/components/ui/drawer.tsx | 14 +- .../shadcn-ui/components/ui/dropdown-menu.tsx | 58 +- packages/shadcn-ui/components/ui/empty.tsx | 18 +- packages/shadcn-ui/components/ui/field.tsx | 44 +- .../shadcn-ui/components/ui/hover-card.tsx | 2 +- .../shadcn-ui/components/ui/input-group.tsx | 38 +- .../shadcn-ui/components/ui/input-otp.tsx | 24 +- packages/shadcn-ui/components/ui/input.tsx | 4 +- packages/shadcn-ui/components/ui/item.tsx | 35 +- packages/shadcn-ui/components/ui/kbd.tsx | 4 +- packages/shadcn-ui/components/ui/menubar.tsx | 54 +- .../shadcn-ui/components/ui/native-select.tsx | 61 + .../components/ui/navigation-menu.tsx | 24 +- .../shadcn-ui/components/ui/pagination.tsx | 68 +- packages/shadcn-ui/components/ui/popover.tsx | 45 +- packages/shadcn-ui/components/ui/progress.tsx | 4 +- .../shadcn-ui/components/ui/radio-group.tsx | 9 +- .../shadcn-ui/components/ui/resizable.tsx | 26 +- .../shadcn-ui/components/ui/scroll-area.tsx | 11 +- packages/shadcn-ui/components/ui/select.tsx | 51 +- .../shadcn-ui/components/ui/separator.tsx | 2 +- packages/shadcn-ui/components/ui/sheet.tsx | 39 +- packages/shadcn-ui/components/ui/sidebar.tsx | 124 +- packages/shadcn-ui/components/ui/skeleton.tsx | 2 +- packages/shadcn-ui/components/ui/slider.tsx | 12 +- packages/shadcn-ui/components/ui/sonner.tsx | 33 +- packages/shadcn-ui/components/ui/spinner.tsx | 10 +- packages/shadcn-ui/components/ui/switch.tsx | 12 +- packages/shadcn-ui/components/ui/table.tsx | 10 +- packages/shadcn-ui/components/ui/tabs.tsx | 42 +- packages/shadcn-ui/components/ui/textarea.tsx | 2 +- .../shadcn-ui/components/ui/toggle-group.tsx | 20 +- packages/shadcn-ui/components/ui/toggle.tsx | 18 +- packages/shadcn-ui/components/ui/tooltip.tsx | 12 +- packages/shadcn-ui/package.json | 7 +- pnpm-lock.yaml | 555 ++- 320 files changed, 50946 insertions(+), 1359 deletions(-) create mode 100644 apps/docs/app/api/registry/_lib/create-registry.ts create mode 100644 apps/docs/app/api/registry/base/[component]/route.ts create mode 100644 apps/docs/components/custom/elements-installer-tabs.tsx create mode 100644 apps/docs/components/custom/preview-library-switch.tsx create mode 100644 apps/docs/components/geistdocs/ui-library-switcher.tsx create mode 100644 apps/docs/hooks/geistdocs/use-ui-library.ts create mode 100644 packages/base-ui/components.json create mode 100644 packages/base-ui/components/ui/accordion.tsx create mode 100644 packages/base-ui/components/ui/alert-dialog.tsx create mode 100644 packages/base-ui/components/ui/alert.tsx create mode 100644 packages/base-ui/components/ui/aspect-ratio.tsx create mode 100644 packages/base-ui/components/ui/avatar.tsx create mode 100644 packages/base-ui/components/ui/badge.tsx create mode 100644 packages/base-ui/components/ui/breadcrumb.tsx create mode 100644 packages/base-ui/components/ui/button-group.tsx create mode 100644 packages/base-ui/components/ui/button.tsx create mode 100644 packages/base-ui/components/ui/calendar.tsx create mode 100644 packages/base-ui/components/ui/card.tsx create mode 100644 packages/base-ui/components/ui/carousel.tsx create mode 100644 packages/base-ui/components/ui/chart.tsx create mode 100644 packages/base-ui/components/ui/checkbox.tsx create mode 100644 packages/base-ui/components/ui/collapsible.tsx create mode 100644 packages/base-ui/components/ui/combobox.tsx create mode 100644 packages/base-ui/components/ui/command.tsx create mode 100644 packages/base-ui/components/ui/context-menu.tsx create mode 100644 packages/base-ui/components/ui/dialog.tsx create mode 100644 packages/base-ui/components/ui/direction.tsx create mode 100644 packages/base-ui/components/ui/drawer.tsx create mode 100644 packages/base-ui/components/ui/dropdown-menu.tsx create mode 100644 packages/base-ui/components/ui/empty.tsx create mode 100644 packages/base-ui/components/ui/field.tsx create mode 100644 packages/base-ui/components/ui/hover-card.tsx create mode 100644 packages/base-ui/components/ui/input-group.tsx create mode 100644 packages/base-ui/components/ui/input-otp.tsx create mode 100644 packages/base-ui/components/ui/input.tsx create mode 100644 packages/base-ui/components/ui/item.tsx create mode 100644 packages/base-ui/components/ui/kbd.tsx create mode 100644 packages/base-ui/components/ui/label.tsx create mode 100644 packages/base-ui/components/ui/menubar.tsx create mode 100644 packages/base-ui/components/ui/native-select.tsx create mode 100644 packages/base-ui/components/ui/navigation-menu.tsx create mode 100644 packages/base-ui/components/ui/pagination.tsx create mode 100644 packages/base-ui/components/ui/popover.tsx create mode 100644 packages/base-ui/components/ui/progress.tsx create mode 100644 packages/base-ui/components/ui/radio-group.tsx create mode 100644 packages/base-ui/components/ui/resizable.tsx create mode 100644 packages/base-ui/components/ui/scroll-area.tsx create mode 100644 packages/base-ui/components/ui/select.tsx create mode 100644 packages/base-ui/components/ui/separator.tsx create mode 100644 packages/base-ui/components/ui/sheet.tsx create mode 100644 packages/base-ui/components/ui/sidebar.tsx create mode 100644 packages/base-ui/components/ui/skeleton.tsx create mode 100644 packages/base-ui/components/ui/slider.tsx create mode 100644 packages/base-ui/components/ui/sonner.tsx create mode 100644 packages/base-ui/components/ui/spinner.tsx create mode 100644 packages/base-ui/components/ui/switch.tsx create mode 100644 packages/base-ui/components/ui/table.tsx create mode 100644 packages/base-ui/components/ui/tabs.tsx create mode 100644 packages/base-ui/components/ui/textarea.tsx create mode 100644 packages/base-ui/components/ui/toggle-group.tsx create mode 100644 packages/base-ui/components/ui/toggle.tsx create mode 100644 packages/base-ui/components/ui/tooltip.tsx create mode 100644 packages/base-ui/hooks/use-mobile.ts create mode 100644 packages/base-ui/lib/utils.ts create mode 100644 packages/base-ui/package.json create mode 100644 packages/base-ui/tsconfig.json create mode 100644 packages/elements-base/__tests__/README.md create mode 100644 packages/elements-base/__tests__/agent.test.tsx create mode 100644 packages/elements-base/__tests__/artifact.test.tsx create mode 100644 packages/elements-base/__tests__/audio-player.test.tsx create mode 100644 packages/elements-base/__tests__/canvas.test.tsx create mode 100644 packages/elements-base/__tests__/chain-of-thought.test.tsx create mode 100644 packages/elements-base/__tests__/code-block.test.tsx create mode 100644 packages/elements-base/__tests__/commit.test.tsx create mode 100644 packages/elements-base/__tests__/confirmation.test.tsx create mode 100644 packages/elements-base/__tests__/connection.test.tsx create mode 100644 packages/elements-base/__tests__/context.test.tsx create mode 100644 packages/elements-base/__tests__/controls.test.tsx create mode 100644 packages/elements-base/__tests__/conversation.test.tsx create mode 100644 packages/elements-base/__tests__/edge.test.tsx create mode 100644 packages/elements-base/__tests__/environment-variables.test.tsx create mode 100644 packages/elements-base/__tests__/file-tree.test.tsx create mode 100644 packages/elements-base/__tests__/image.test.tsx create mode 100644 packages/elements-base/__tests__/inline-citation.test.tsx create mode 100644 packages/elements-base/__tests__/jsx-preview.test.tsx create mode 100644 packages/elements-base/__tests__/message.test.tsx create mode 100644 packages/elements-base/__tests__/mic-selector.test.tsx create mode 100644 packages/elements-base/__tests__/model-selector.test.tsx create mode 100644 packages/elements-base/__tests__/node.test.tsx create mode 100644 packages/elements-base/__tests__/open-in-chat.test.tsx create mode 100644 packages/elements-base/__tests__/package-info.test.tsx create mode 100644 packages/elements-base/__tests__/panel.test.tsx create mode 100644 packages/elements-base/__tests__/persona.test.tsx create mode 100644 packages/elements-base/__tests__/plan.test.tsx create mode 100644 packages/elements-base/__tests__/prompt-input.test.tsx create mode 100644 packages/elements-base/__tests__/queue.test.tsx create mode 100644 packages/elements-base/__tests__/reasoning.test.tsx create mode 100644 packages/elements-base/__tests__/sandbox.test.tsx create mode 100644 packages/elements-base/__tests__/schema-display.test.tsx create mode 100644 packages/elements-base/__tests__/setup.ts create mode 100644 packages/elements-base/__tests__/shimmer.test.tsx create mode 100644 packages/elements-base/__tests__/snippet.test.tsx create mode 100644 packages/elements-base/__tests__/sources.test.tsx create mode 100644 packages/elements-base/__tests__/speech-input.test.tsx create mode 100644 packages/elements-base/__tests__/stack-trace.test.tsx create mode 100644 packages/elements-base/__tests__/style-mock.js create mode 100644 packages/elements-base/__tests__/suggestion.test.tsx create mode 100644 packages/elements-base/__tests__/task.test.tsx create mode 100644 packages/elements-base/__tests__/terminal.test.tsx create mode 100644 packages/elements-base/__tests__/test-results.test.tsx create mode 100644 packages/elements-base/__tests__/tool.test.tsx create mode 100644 packages/elements-base/__tests__/toolbar.test.tsx create mode 100644 packages/elements-base/__tests__/transcription.test.tsx create mode 100644 packages/elements-base/__tests__/voice-selector.test.tsx create mode 100644 packages/elements-base/__tests__/web-preview.test.tsx create mode 100644 packages/elements-base/package.json create mode 100644 packages/elements-base/src/agent.tsx create mode 100644 packages/elements-base/src/artifact.tsx create mode 100644 packages/elements-base/src/attachments.tsx create mode 100644 packages/elements-base/src/audio-player.tsx create mode 100644 packages/elements-base/src/canvas.tsx create mode 100644 packages/elements-base/src/chain-of-thought.tsx create mode 100644 packages/elements-base/src/checkpoint.tsx create mode 100644 packages/elements-base/src/code-block.tsx create mode 100644 packages/elements-base/src/commit.tsx create mode 100644 packages/elements-base/src/confirmation.tsx create mode 100644 packages/elements-base/src/connection.tsx create mode 100644 packages/elements-base/src/context.tsx create mode 100644 packages/elements-base/src/controls.tsx create mode 100644 packages/elements-base/src/conversation.tsx create mode 100644 packages/elements-base/src/edge.tsx create mode 100644 packages/elements-base/src/environment-variables.tsx create mode 100644 packages/elements-base/src/file-tree.tsx create mode 100644 packages/elements-base/src/hooks/use-controllable-state.ts create mode 100644 packages/elements-base/src/image.tsx create mode 100644 packages/elements-base/src/inline-citation.tsx create mode 100644 packages/elements-base/src/jsx-preview.tsx create mode 100644 packages/elements-base/src/message.tsx create mode 100644 packages/elements-base/src/mic-selector.tsx create mode 100644 packages/elements-base/src/model-selector.tsx create mode 100644 packages/elements-base/src/node.tsx create mode 100644 packages/elements-base/src/open-in-chat.tsx create mode 100644 packages/elements-base/src/package-info.tsx create mode 100644 packages/elements-base/src/panel.tsx create mode 100644 packages/elements-base/src/persona.tsx create mode 100644 packages/elements-base/src/plan.tsx create mode 100644 packages/elements-base/src/prompt-input.tsx create mode 100644 packages/elements-base/src/queue.tsx create mode 100644 packages/elements-base/src/reasoning.tsx create mode 100644 packages/elements-base/src/sandbox.tsx create mode 100644 packages/elements-base/src/schema-display.tsx create mode 100644 packages/elements-base/src/shimmer.tsx create mode 100644 packages/elements-base/src/snippet.tsx create mode 100644 packages/elements-base/src/sources.tsx create mode 100644 packages/elements-base/src/speech-input.tsx create mode 100644 packages/elements-base/src/stack-trace.tsx create mode 100644 packages/elements-base/src/suggestion.tsx create mode 100644 packages/elements-base/src/task.tsx create mode 100644 packages/elements-base/src/terminal.tsx create mode 100644 packages/elements-base/src/test-results.tsx create mode 100644 packages/elements-base/src/tool.tsx create mode 100644 packages/elements-base/src/toolbar.tsx create mode 100644 packages/elements-base/src/transcription.tsx create mode 100644 packages/elements-base/src/voice-selector.tsx create mode 100644 packages/elements-base/src/web-preview.tsx create mode 100644 packages/elements-base/tsconfig.json create mode 100644 packages/elements-base/vitest.config.mts create mode 100644 packages/examples-base/package.json create mode 100644 packages/examples-base/src/agent.tsx create mode 100644 packages/examples-base/src/artifact.tsx create mode 100644 packages/examples-base/src/attachments-inline.tsx create mode 100644 packages/examples-base/src/attachments-list.tsx create mode 100644 packages/examples-base/src/attachments.tsx create mode 100644 packages/examples-base/src/audio-player-remote.tsx create mode 100644 packages/examples-base/src/audio-player.tsx create mode 100644 packages/examples-base/src/chain-of-thought.tsx create mode 100644 packages/examples-base/src/chatbot.tsx create mode 100644 packages/examples-base/src/checkpoint.tsx create mode 100644 packages/examples-base/src/code-block-dark.tsx create mode 100644 packages/examples-base/src/code-block.tsx create mode 100644 packages/examples-base/src/commit.tsx create mode 100644 packages/examples-base/src/confirmation-accepted.tsx create mode 100644 packages/examples-base/src/confirmation-rejected.tsx create mode 100644 packages/examples-base/src/confirmation-request.tsx create mode 100644 packages/examples-base/src/confirmation.tsx create mode 100644 packages/examples-base/src/context.tsx create mode 100644 packages/examples-base/src/conversation.tsx create mode 100644 packages/examples-base/src/demo-chatgpt.tsx create mode 100644 packages/examples-base/src/demo-claude.tsx create mode 100644 packages/examples-base/src/demo-cursor.tsx create mode 100644 packages/examples-base/src/demo-grok.tsx create mode 100644 packages/examples-base/src/demo-workflow.tsx create mode 100644 packages/examples-base/src/environment-variables.tsx create mode 100644 packages/examples-base/src/file-tree-basic.tsx create mode 100644 packages/examples-base/src/file-tree-expanded.tsx create mode 100644 packages/examples-base/src/file-tree-selection.tsx create mode 100644 packages/examples-base/src/file-tree.tsx create mode 100644 packages/examples-base/src/image.tsx create mode 100644 packages/examples-base/src/inline-citation.tsx create mode 100644 packages/examples-base/src/jsx-preview.tsx create mode 100644 packages/examples-base/src/message.tsx create mode 100644 packages/examples-base/src/mic-selector.tsx create mode 100644 packages/examples-base/src/model-selector.tsx create mode 100644 packages/examples-base/src/open-in-chat.tsx create mode 100644 packages/examples-base/src/package-info.tsx create mode 100644 packages/examples-base/src/persona-command.tsx create mode 100644 packages/examples-base/src/persona-glint.tsx create mode 100644 packages/examples-base/src/persona-halo.tsx create mode 100644 packages/examples-base/src/persona-mana.tsx create mode 100644 packages/examples-base/src/persona-obsidian.tsx create mode 100644 packages/examples-base/src/persona-opal.tsx create mode 100644 packages/examples-base/src/plan.tsx create mode 100644 packages/examples-base/src/prompt-input-cursor.tsx create mode 100644 packages/examples-base/src/prompt-input-tooltip.tsx create mode 100644 packages/examples-base/src/prompt-input.tsx create mode 100644 packages/examples-base/src/queue-prompt-input.tsx create mode 100644 packages/examples-base/src/queue.tsx create mode 100644 packages/examples-base/src/reasoning.tsx create mode 100644 packages/examples-base/src/sandbox.tsx create mode 100644 packages/examples-base/src/schema-display-basic.tsx create mode 100644 packages/examples-base/src/schema-display-body.tsx create mode 100644 packages/examples-base/src/schema-display-nested.tsx create mode 100644 packages/examples-base/src/schema-display-params.tsx create mode 100644 packages/examples-base/src/schema-display.tsx create mode 100644 packages/examples-base/src/shimmer-duration.tsx create mode 100644 packages/examples-base/src/shimmer-elements.tsx create mode 100644 packages/examples-base/src/shimmer.tsx create mode 100644 packages/examples-base/src/snippet-plain.tsx create mode 100644 packages/examples-base/src/snippet.tsx create mode 100644 packages/examples-base/src/sources-custom.tsx create mode 100644 packages/examples-base/src/sources.tsx create mode 100644 packages/examples-base/src/speech-input.tsx create mode 100644 packages/examples-base/src/stack-trace-collapsed.tsx create mode 100644 packages/examples-base/src/stack-trace-no-internal.tsx create mode 100644 packages/examples-base/src/stack-trace.tsx create mode 100644 packages/examples-base/src/suggestion-input.tsx create mode 100644 packages/examples-base/src/suggestion.tsx create mode 100644 packages/examples-base/src/task.tsx create mode 100644 packages/examples-base/src/terminal-basic.tsx create mode 100644 packages/examples-base/src/terminal-clear.tsx create mode 100644 packages/examples-base/src/terminal-streaming.tsx create mode 100644 packages/examples-base/src/terminal.tsx create mode 100644 packages/examples-base/src/test-results-basic.tsx create mode 100644 packages/examples-base/src/test-results-errors.tsx create mode 100644 packages/examples-base/src/test-results-suites.tsx create mode 100644 packages/examples-base/src/test-results.tsx create mode 100644 packages/examples-base/src/tool-input-available.tsx create mode 100644 packages/examples-base/src/tool-input-streaming.tsx create mode 100644 packages/examples-base/src/tool-output-available.tsx create mode 100644 packages/examples-base/src/tool-output-error.tsx create mode 100644 packages/examples-base/src/tool.tsx create mode 100644 packages/examples-base/src/transcription.tsx create mode 100644 packages/examples-base/src/v0-clone.tsx create mode 100644 packages/examples-base/src/voice-selector.tsx create mode 100644 packages/examples-base/src/web-preview.tsx create mode 100644 packages/examples-base/src/workflow.tsx create mode 100644 packages/examples-base/tsconfig.json create mode 100644 packages/shadcn-ui/components/ui/combobox.tsx create mode 100644 packages/shadcn-ui/components/ui/direction.tsx create mode 100644 packages/shadcn-ui/components/ui/native-select.tsx diff --git a/apps/docs/app/api/registry/[component]/route.ts b/apps/docs/app/api/registry/[component]/route.ts index 713a03bf..eccd8c61 100644 --- a/apps/docs/app/api/registry/[component]/route.ts +++ b/apps/docs/app/api/registry/[component]/route.ts @@ -1,483 +1,13 @@ -/** biome-ignore-all lint/suspicious/noConsole: "server only" */ - -import type { NextRequest } from "next/server"; -import type { Registry, RegistryItem } from "shadcn/schema"; - -import { track } from "@vercel/analytics/server"; -import { NextResponse } from "next/server"; -import { promises as fs, readdirSync } from "node:fs"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import { Project } from "ts-morph"; - -const getRegistryUrl = (request: NextRequest) => { - const host = request.headers.get("host"); - const protocol = host?.includes("localhost") ? "http" : "https"; - return `${protocol}://${host}`; -}; - -const packageDir = join(process.cwd(), "..", "..", "packages", "elements"); -const packagePath = join(packageDir, "package.json"); -const packageJson = JSON.parse(await readFile(packagePath, "utf8")); - -const examplesDir = join( - process.cwd(), - "..", - "..", - "packages", - "examples", - "src" -); - -const internalDependencies = Object.keys(packageJson.dependencies || {}).filter( - (dep) => dep.startsWith("@repo") && dep !== "@repo/shadcn-ui" -); - -const dependenciesSet = new Set( - Object.keys(packageJson.dependencies || {}).filter( - (dep) => - ![ - "react", - "react-dom", - "@repo/shadcn-ui", - ...internalDependencies, - ].includes(dep) - ) -); - -const devDependenciesSet = new Set( - Object.keys(packageJson.devDependencies || {}).filter( - (dep) => - ![ - "@repo/typescript-config", - "@types/react", - "@types/react-dom", - "typescript", - ].includes(dep) - ) -); - -// Registry should auto-add ai sdk v5 as a dependency -dependenciesSet.add("ai"); -dependenciesSet.add("@ai-sdk/react"); -dependenciesSet.add("zod"); - -const dependencies = new Set(dependenciesSet); -const devDependencies = [...devDependenciesSet]; - -// Normalize to package root (supports scoped and deep subpath imports) -const getBasePackageName = (specifier: string) => { - if (specifier.startsWith("@")) { - const parts = specifier.split("/"); - return parts.slice(0, 2).join("/"); - } - return specifier.split("/")[0]; -}; - -// Build a mapping from runtime package -> its @types devDependency package(s) -const TYPES_PREFIX = "@types/"; -const typesDevDepsMap = new Map(); -for (const devDep of devDependencies) { - if (devDep.startsWith(TYPES_PREFIX)) { - const name = devDep.slice(TYPES_PREFIX.length); - // Scoped packages in DefinitelyTyped use __ separator, e.g. @types/babel__core for @babel/core - const runtime = name.includes("__") ? name.replace("__", "/") : name; - const list = typesDevDepsMap.get(runtime) ?? []; - list.push(devDep); - typesDevDepsMap.set(runtime, list); - } -} - -const srcDir = join(packageDir, "src"); - -const packageFiles = readdirSync(srcDir, { withFileTypes: true }); -const tsxFiles = packageFiles.filter( - (file) => file.isFile() && file.name.endsWith(".tsx") -); - -const exampleFiles = readdirSync(examplesDir, { withFileTypes: true }); -const exampleTsxFiles = exampleFiles.filter( - (file) => file.isFile() && file.name.endsWith(".tsx") -); - -const files: { - type: string; - path: string; - content: string; -}[] = []; - -const fileContents = await Promise.all( - tsxFiles.map(async (tsxFile) => { - const filePath = join(srcDir, tsxFile.name); - const content = await fs.readFile(filePath, "utf8"); - const parsedContent = content - .replaceAll("@repo/shadcn-ui/components/ui/", "@/registry/default/ui/") - .replaceAll("@repo/shadcn-ui/lib/", "@/lib/"); - - return { - content: parsedContent, - path: `registry/default/ai-elements/${tsxFile.name}`, - type: "registry:component", - }; - }) -); - -const exampleContents = await Promise.all( - exampleTsxFiles.map(async (exampleFile) => { - const filePath = join(examplesDir, exampleFile.name); - const content = await fs.readFile(filePath, "utf8"); - const parsedContent = content - .replaceAll("@repo/shadcn-ui/components/ui/", "@/registry/default/ui/") - .replaceAll("@repo/shadcn-ui/lib/", "@/lib/") - .replaceAll("@repo/elements/", "@/components/ai-elements/"); - - return { - content: parsedContent, - path: `registry/default/examples/${exampleFile.name}`, - type: "registry:block", - }; - }) -); - -files.push(...fileContents, ...exampleContents); - -const registryDependenciesSet = new Set(); - -// Extract shadcn/ui components from file content -const shadcnComponents = - files - .map((f) => f.content) - .join("\n") - .match(/@\/registry\/default\/ui\/([a-z-]+)/g) - ?.map((path) => path.split("/").pop()) - .filter((name): name is string => Boolean(name)) || []; - -// Extract AI element components from file content -const aiElementComponents = - files - .map((f) => f.content) - .join("\n") - .match(/@\/components\/ai-elements\/([a-z-]+)/g) - ?.map((path) => path.split("/").pop()) - .filter((name): name is string => Boolean(name)) || []; - -// Add shadcn/ui components to set -for (const component of shadcnComponents) { - registryDependenciesSet.add(component); -} - -// Add AI element components to set (these become registry dependencies) -for (const component of aiElementComponents) { - registryDependenciesSet.add(component); -} - -// Create items for the root registry response -const componentItems: RegistryItem[] = tsxFiles.map((componentFile) => { - const componentName = componentFile.name.replace(".tsx", ""); - - const item: RegistryItem = { - description: `AI-powered ${componentName.replace("-", " ")} component.`, - files: [ - { - path: `registry/default/ai-elements/${componentFile.name}`, - target: `components/ai-elements/${componentFile.name}.tsx`, - type: "registry:component", - }, - ], - name: componentName, - title: componentName - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "), - type: "registry:component", - }; - - return item; -}); - -const exampleItems: RegistryItem[] = exampleTsxFiles.map((exampleFile) => { - const exampleName = exampleFile.name.replace(".tsx", ""); - - const item: RegistryItem = { - description: `Example implementation of ${exampleName.replace("-", " ")}.`, - files: [ - { - path: `registry/default/examples/${exampleFile.name}`, - target: `components/ai-elements/examples/${exampleFile.name}.tsx`, - type: "registry:block", - }, - ], - name: `example-${exampleName}`, - title: `${exampleName - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ")} Example`, - type: "registry:block", - }; - - return item; +import { createRegistryGET, createRegistryHandler } from "../_lib/create-registry"; + +const registryPromise = createRegistryHandler({ + elementsPackage: "elements", + uiPackageName: "@repo/shadcn-ui", + registryStyle: "default", + uiImportPrefix: "@/registry/default/ui/", + utilsImportPrefix: "@/lib/", + apiBasePath: "/api/registry", + examplesPackage: "examples", }); -const items: RegistryItem[] = [...componentItems, ...exampleItems]; - -const getResponse = (registryUrl: string): Registry => ({ - homepage: new URL("/elements", registryUrl).toString(), - items, - name: "ai-elements", -}); - -interface RequestProps { - params: Promise<{ component: string }>; -} - -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Registry route requires handling many component cases -export const GET = async (request: NextRequest, { params }: RequestProps) => { - const { component } = await params; - const parsedComponent = component.replace(".json", ""); - const registryUrl = getRegistryUrl(request); - const response = getResponse(registryUrl); - - if (parsedComponent === "registry") { - try { - track("registry:registry"); - } catch (error) { - console.warn("Failed to track registry:registry:", error); - } - return NextResponse.json(response); - } - - // Handle "all.json" - bundle all components into a single RegistryItem - if (parsedComponent === "all") { - try { - track("registry:all"); - } catch (error) { - console.warn("Failed to track registry:all:", error); - } - - // Collect all dependencies and registry dependencies from all components - const allDependencies = new Set(); - const allDevDependencies = new Set(); - const allRegistryDependencies = new Set(); - const allFiles: RegistryItem["files"] = []; - - // Process each component file - for (const componentFile of tsxFiles) { - const file = files.find( - (f) => f.path === `registry/default/ai-elements/${componentFile.name}` - ); - - if (file) { - allFiles.push({ - content: file.content, - path: file.path, - target: `components/ai-elements/${componentFile.name}`, - type: file.type as RegistryItem["type"], - }); - - // Parse imports for dependencies - const project = new Project({ useInMemoryFileSystem: true }); - try { - const sourceFile = project.createSourceFile(file.path, file.content); - const imports = sourceFile - .getImportDeclarations() - .map((d) => d.getModuleSpecifierValue()); - - for (const moduleName of imports) { - if (!moduleName) { - continue; - } - - const pkg = getBasePackageName(moduleName); - // Check if it's a regular dependency - if (dependencies.has(pkg)) { - allDependencies.add(pkg); - // Check if it has a corresponding @types/ package - const typePkgs = typesDevDepsMap.get(pkg); - if (typePkgs) { - for (const t of typePkgs) { - allDevDependencies.add(t); - } - } - } - - // Check if it's a dev dependency - if (devDependencies.includes(moduleName)) { - allDevDependencies.add(moduleName); - } - - // Check if it's a registry dependency (shadcn/ui components) - if (moduleName.startsWith("@/registry/default/ui/")) { - const componentName = moduleName.split("/").pop(); - if (componentName) { - allRegistryDependencies.add(componentName); - } - } - } - } catch (error) { - console.warn(`Failed to parse imports for ${file.path}:`, error); - } - } - } - - const allComponentsItem: RegistryItem = { - $schema: "https://ui.shadcn.com/schema/registry-item.json", - dependencies: [...allDependencies], - description: "Bundle containing all AI-powered components.", - devDependencies: [...allDevDependencies], - files: allFiles, - name: "all", - registryDependencies: [...allRegistryDependencies], - title: "All AI Elements", - type: "registry:component", - }; - - return NextResponse.json(allComponentsItem); - } - - try { - track(`registry:${parsedComponent}`); - } catch (error) { - console.warn(`Failed to track ${parsedComponent}:`, error); - } - - // Only process the parsedComponent, not an array - - // Find the item for the requested component or example - const item = response.items.find((i) => i.name === parsedComponent); - - if (!item) { - return NextResponse.json( - { error: `Component "${parsedComponent}" not found.` }, - { status: 404 } - ); - } - - // Find the corresponding file content - let file: { type: string; path: string; content: string } | undefined; - if (item.type === "registry:component") { - file = files.find( - (f) => f.path === `registry/default/ai-elements/${parsedComponent}.tsx` - ); - } else if ( - item.type === "registry:block" && - parsedComponent.startsWith("example-") - ) { - const exampleFileName = `${parsedComponent.replace("example-", "")}.tsx`; - file = files.find( - (f) => f.path === `registry/default/examples/${exampleFileName}` - ); - } - - if (!file) { - return NextResponse.json( - { error: `File for "${parsedComponent}" not found.` }, - { status: 404 } - ); - } - - // Parse imports for the single component to determine actual dependencies - const usedDependencies = new Set(); - const usedDevDependencies = new Set(); - const usedRegistryDependencies = new Set(); - - const project = new Project({ useInMemoryFileSystem: true }); - - try { - const sourceFile = project.createSourceFile(file.path, file.content); - const imports = sourceFile - .getImportDeclarations() - .map((d) => d.getModuleSpecifierValue()); - - for (const moduleName of imports) { - if (!moduleName) { - continue; - } - - // Check if it's a relative dependency - if (moduleName.startsWith("./")) { - const relativePath = moduleName.split("/").pop(); - if (relativePath) { - usedRegistryDependencies.add( - new URL( - `/api/registry/${relativePath}.json`, - registryUrl - ).toString() - ); - } - } - - const pkg = getBasePackageName(moduleName); - // Check if it's a regular dependency - if (dependencies.has(pkg)) { - usedDependencies.add(pkg); - // Check if it has a corresponding @types/ package - const typePkgs = typesDevDepsMap.get(pkg); - if (typePkgs) { - for (const t of typePkgs) { - usedDevDependencies.add(t); - } - } - } - - // Check if it's a dev dependency (though less common in component files) - if (devDependencies.includes(moduleName)) { - usedDevDependencies.add(moduleName); - } - - // Check if it's a registry dependency (shadcn/ui components) - if (moduleName.startsWith("@/registry/default/ui/")) { - const componentName = moduleName.split("/").pop(); - if (componentName) { - usedRegistryDependencies.add(componentName); - } - } - - // Check if it's an AI element dependency - if (moduleName.startsWith("@/components/ai-elements/")) { - const componentName = moduleName.split("/").pop(); - if (componentName) { - usedRegistryDependencies.add( - new URL(`/${componentName}.json`, registryUrl).toString() - ); - } - } - } - } catch (error) { - console.warn(`Failed to parse imports for ${file.path}:`, error); - } - - // Add internal dependencies for the requested component - for (const dep of internalDependencies) { - const packageName = dep.replace("@repo/", ""); - usedRegistryDependencies.add( - new URL(`/elements/${packageName}.json`, registryUrl).toString() - ); - } - - const itemResponse: RegistryItem = { - $schema: "https://ui.shadcn.com/schema/registry-item.json", - dependencies: [...usedDependencies], - description: item.description, - devDependencies: [...usedDevDependencies], - files: [ - { - content: file.content, - path: file.path, - target: `components/ai-elements/${item.name}.tsx`, - type: file.type as Exclude< - RegistryItem["type"], - "registry:base" | "registry:font" - >, - }, - ], - name: item.name, - registryDependencies: [...usedRegistryDependencies], - title: item.title, - type: item.type as Exclude< - RegistryItem["type"], - "registry:base" | "registry:font" - >, - }; - - return NextResponse.json(itemResponse); -}; +export const GET = createRegistryGET(registryPromise); diff --git a/apps/docs/app/api/registry/_lib/create-registry.ts b/apps/docs/app/api/registry/_lib/create-registry.ts new file mode 100644 index 00000000..1c8160ae --- /dev/null +++ b/apps/docs/app/api/registry/_lib/create-registry.ts @@ -0,0 +1,497 @@ +/** biome-ignore-all lint/suspicious/noConsole: "server only" */ + +import type { NextRequest } from "next/server"; +import type { Registry, RegistryItem } from "shadcn/schema"; + +import { track } from "@vercel/analytics/server"; +import { NextResponse } from "next/server"; +import { promises as fs, readdirSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { Project } from "ts-morph"; + +const getRegistryUrl = (request: NextRequest) => { + const host = request.headers.get("host"); + const protocol = host?.includes("localhost") ? "http" : "https"; + return `${protocol}://${host}`; +}; + +// Normalize to package root (supports scoped and deep subpath imports) +const getBasePackageName = (specifier: string) => { + if (specifier.startsWith("@")) { + const parts = specifier.split("/"); + return parts.slice(0, 2).join("/"); + } + return specifier.split("/")[0]; +}; + +interface RegistryConfig { + /** Name of the elements package directory, e.g. "elements" or "elements-base" */ + elementsPackage: string; + /** Name of the shadcn-ui package to exclude from deps, e.g. "@repo/shadcn-ui" or "@repo/base-ui" */ + uiPackageName: string; + /** Registry path style prefix used in file paths, e.g. "default" or "base" */ + registryStyle: string; + /** Import path prefix for UI components in output, e.g. "@/registry/default/ui/" */ + uiImportPrefix: string; + /** Import path prefix for utils in output, e.g. "@/lib/" */ + utilsImportPrefix: string; + /** Base path for the registry API, e.g. "/api/registry" or "/api/registry/base" */ + apiBasePath: string; + /** Name of the examples package directory, e.g. "examples" or "examples-base" */ + examplesPackage: string; +} + +export async function createRegistryHandler(config: RegistryConfig) { + const { + elementsPackage, + uiPackageName, + registryStyle, + uiImportPrefix, + utilsImportPrefix, + apiBasePath, + examplesPackage, + } = config; + + const packageDir = join(process.cwd(), "..", "..", "packages", elementsPackage); + const packagePath = join(packageDir, "package.json"); + const packageJson = JSON.parse(await readFile(packagePath, "utf8")); + + const examplesDir = join( + process.cwd(), + "..", + "..", + "packages", + examplesPackage, + "src" + ); + + const internalDependencies = Object.keys(packageJson.dependencies || {}).filter( + (dep) => dep.startsWith("@repo") && dep !== uiPackageName + ); + + const dependenciesSet = new Set( + Object.keys(packageJson.dependencies || {}).filter( + (dep) => + ![ + "react", + "react-dom", + uiPackageName, + ...internalDependencies, + ].includes(dep) + ) + ); + + const devDependenciesSet = new Set( + Object.keys(packageJson.devDependencies || {}).filter( + (dep) => + ![ + "@repo/typescript-config", + "@types/react", + "@types/react-dom", + "typescript", + ].includes(dep) + ) + ); + + // Registry should auto-add ai sdk v5 as a dependency + dependenciesSet.add("ai"); + dependenciesSet.add("@ai-sdk/react"); + dependenciesSet.add("zod"); + + const dependencies = new Set(dependenciesSet); + const devDependencies = [...devDependenciesSet]; + + // Build a mapping from runtime package -> its @types devDependency package(s) + const TYPES_PREFIX = "@types/"; + const typesDevDepsMap = new Map(); + for (const devDep of devDependencies) { + if (devDep.startsWith(TYPES_PREFIX)) { + const name = devDep.slice(TYPES_PREFIX.length); + const runtime = name.includes("__") ? name.replace("__", "/") : name; + const list = typesDevDepsMap.get(runtime) ?? []; + list.push(devDep); + typesDevDepsMap.set(runtime, list); + } + } + + const srcDir = join(packageDir, "src"); + + const packageFiles = readdirSync(srcDir, { withFileTypes: true }); + const tsxFiles = packageFiles.filter( + (file) => file.isFile() && file.name.endsWith(".tsx") + ); + + const exampleFiles = readdirSync(examplesDir, { withFileTypes: true }); + const exampleTsxFiles = exampleFiles.filter( + (file) => file.isFile() && file.name.endsWith(".tsx") + ); + + const uiSourceImport = `${uiPackageName}/components/ui/`; + const libSourceImport = `${uiPackageName}/lib/`; + const elementsSourceImport = elementsPackage === "elements" ? "@repo/elements/" : "@repo/elements-base/"; + + const files: { + type: string; + path: string; + content: string; + }[] = []; + + const fileContents = await Promise.all( + tsxFiles.map(async (tsxFile) => { + const filePath = join(srcDir, tsxFile.name); + const content = await fs.readFile(filePath, "utf8"); + const parsedContent = content + .replaceAll(uiSourceImport, uiImportPrefix) + .replaceAll(libSourceImport, utilsImportPrefix); + + return { + content: parsedContent, + path: `registry/${registryStyle}/ai-elements/${tsxFile.name}`, + type: "registry:component", + }; + }) + ); + + const exampleContents = await Promise.all( + exampleTsxFiles.map(async (exampleFile) => { + const filePath = join(examplesDir, exampleFile.name); + const content = await fs.readFile(filePath, "utf8"); + const parsedContent = content + .replaceAll(uiSourceImport, uiImportPrefix) + .replaceAll(libSourceImport, utilsImportPrefix) + .replaceAll(elementsSourceImport, "@/components/ai-elements/"); + + return { + content: parsedContent, + path: `registry/${registryStyle}/examples/${exampleFile.name}`, + type: "registry:block", + }; + }) + ); + + files.push(...fileContents, ...exampleContents); + + // Create items for the root registry response + const componentItems: RegistryItem[] = tsxFiles.map((componentFile) => { + const componentName = componentFile.name.replace(".tsx", ""); + + const item: RegistryItem = { + description: `AI-powered ${componentName.replace("-", " ")} component.`, + files: [ + { + path: `registry/${registryStyle}/ai-elements/${componentFile.name}`, + target: `components/ai-elements/${componentFile.name}.tsx`, + type: "registry:component", + }, + ], + name: componentName, + title: componentName + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + type: "registry:component", + }; + + return item; + }); + + const exampleItems: RegistryItem[] = exampleTsxFiles.map((exampleFile) => { + const exampleName = exampleFile.name.replace(".tsx", ""); + + const item: RegistryItem = { + description: `Example implementation of ${exampleName.replace("-", " ")}.`, + files: [ + { + path: `registry/${registryStyle}/examples/${exampleFile.name}`, + target: `components/ai-elements/examples/${exampleFile.name}.tsx`, + type: "registry:block", + }, + ], + name: `example-${exampleName}`, + title: `${exampleName + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ")} Example`, + type: "registry:block", + }; + + return item; + }); + + const items: RegistryItem[] = [...componentItems, ...exampleItems]; + + return { + items, + tsxFiles, + files, + dependencies, + devDependencies, + typesDevDepsMap, + internalDependencies, + getRegistryUrl, + getBasePackageName, + registryStyle, + apiBasePath, + }; +} + +interface RequestProps { + params: Promise<{ component: string }>; +} + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Registry route requires handling many component cases +export function createRegistryGET(registryPromise: ReturnType) { + return async (request: NextRequest, { params }: RequestProps) => { + const registry = await registryPromise; + const { + items, + tsxFiles, + files, + dependencies, + devDependencies, + typesDevDepsMap, + internalDependencies, + getRegistryUrl: getUrl, + getBasePackageName: getBasePkg, + registryStyle, + apiBasePath, + } = registry; + + const { component } = await params; + const parsedComponent = component.replace(".json", ""); + const registryUrl = getUrl(request); + + const getResponse = (): Registry => ({ + homepage: new URL("/elements", registryUrl).toString(), + items, + name: "ai-elements", + }); + + if (parsedComponent === "registry") { + try { + track(`registry:${registryStyle}:registry`); + } catch (error) { + console.warn("Failed to track registry:", error); + } + return NextResponse.json(getResponse()); + } + + // Handle "all.json" - bundle all components into a single RegistryItem + if (parsedComponent === "all") { + try { + track(`registry:${registryStyle}:all`); + } catch (error) { + console.warn("Failed to track registry:all:", error); + } + + const allDependencies = new Set(); + const allDevDependencies = new Set(); + const allRegistryDependencies = new Set(); + const allFiles: RegistryItem["files"] = []; + + for (const componentFile of tsxFiles) { + const file = files.find( + (f) => f.path === `registry/${registryStyle}/ai-elements/${componentFile.name}` + ); + + if (file) { + allFiles.push({ + content: file.content, + path: file.path, + target: `components/ai-elements/${componentFile.name}`, + type: file.type as RegistryItem["type"], + }); + + const project = new Project({ useInMemoryFileSystem: true }); + try { + const sourceFile = project.createSourceFile(file.path, file.content); + const imports = sourceFile + .getImportDeclarations() + .map((d) => d.getModuleSpecifierValue()); + + for (const moduleName of imports) { + if (!moduleName) { + continue; + } + + const pkg = getBasePkg(moduleName); + if (dependencies.has(pkg)) { + allDependencies.add(pkg); + const typePkgs = typesDevDepsMap.get(pkg); + if (typePkgs) { + for (const t of typePkgs) { + allDevDependencies.add(t); + } + } + } + + if (devDependencies.includes(moduleName)) { + allDevDependencies.add(moduleName); + } + + if (moduleName.startsWith(`@/registry/${registryStyle}/ui/`)) { + const componentName = moduleName.split("/").pop(); + if (componentName) { + allRegistryDependencies.add(componentName); + } + } + } + } catch (error) { + console.warn(`Failed to parse imports for ${file.path}:`, error); + } + } + } + + const allComponentsItem: RegistryItem = { + $schema: "https://ui.shadcn.com/schema/registry-item.json", + dependencies: [...allDependencies], + description: "Bundle containing all AI-powered components.", + devDependencies: [...allDevDependencies], + files: allFiles, + name: "all", + registryDependencies: [...allRegistryDependencies], + title: "All AI Elements", + type: "registry:component", + }; + + return NextResponse.json(allComponentsItem); + } + + try { + track(`registry:${registryStyle}:${parsedComponent}`); + } catch (error) { + console.warn(`Failed to track ${parsedComponent}:`, error); + } + + const response = getResponse(); + const item = response.items.find((i) => i.name === parsedComponent); + + if (!item) { + return NextResponse.json( + { error: `Component "${parsedComponent}" not found.` }, + { status: 404 } + ); + } + + let file: { type: string; path: string; content: string } | undefined; + if (item.type === "registry:component") { + file = files.find( + (f) => f.path === `registry/${registryStyle}/ai-elements/${parsedComponent}.tsx` + ); + } else if ( + item.type === "registry:block" && + parsedComponent.startsWith("example-") + ) { + const exampleFileName = `${parsedComponent.replace("example-", "")}.tsx`; + file = files.find( + (f) => f.path === `registry/${registryStyle}/examples/${exampleFileName}` + ); + } + + if (!file) { + return NextResponse.json( + { error: `File for "${parsedComponent}" not found.` }, + { status: 404 } + ); + } + + const usedDependencies = new Set(); + const usedDevDependencies = new Set(); + const usedRegistryDependencies = new Set(); + + const project = new Project({ useInMemoryFileSystem: true }); + + try { + const sourceFile = project.createSourceFile(file.path, file.content); + const imports = sourceFile + .getImportDeclarations() + .map((d) => d.getModuleSpecifierValue()); + + for (const moduleName of imports) { + if (!moduleName) { + continue; + } + + if (moduleName.startsWith("./")) { + const relativePath = moduleName.split("/").pop(); + if (relativePath) { + usedRegistryDependencies.add( + new URL( + `${apiBasePath}/${relativePath}.json`, + registryUrl + ).toString() + ); + } + } + + const pkg = getBasePkg(moduleName); + if (dependencies.has(pkg)) { + usedDependencies.add(pkg); + const typePkgs = typesDevDepsMap.get(pkg); + if (typePkgs) { + for (const t of typePkgs) { + usedDevDependencies.add(t); + } + } + } + + if (devDependencies.includes(moduleName)) { + usedDevDependencies.add(moduleName); + } + + if (moduleName.startsWith(`@/registry/${registryStyle}/ui/`)) { + const componentName = moduleName.split("/").pop(); + if (componentName) { + usedRegistryDependencies.add(componentName); + } + } + + if (moduleName.startsWith("@/components/ai-elements/")) { + const componentName = moduleName.split("/").pop(); + if (componentName) { + usedRegistryDependencies.add( + new URL(`${apiBasePath}/${componentName}.json`, registryUrl).toString() + ); + } + } + } + } catch (error) { + console.warn(`Failed to parse imports for ${file.path}:`, error); + } + + for (const dep of internalDependencies) { + const packageName = dep.replace("@repo/", ""); + usedRegistryDependencies.add( + new URL(`/elements/${packageName}.json`, registryUrl).toString() + ); + } + + const itemResponse: RegistryItem = { + $schema: "https://ui.shadcn.com/schema/registry-item.json", + dependencies: [...usedDependencies], + description: item.description, + devDependencies: [...usedDevDependencies], + files: [ + { + content: file.content, + path: file.path, + target: `components/ai-elements/${item.name}.tsx`, + type: file.type as Exclude< + RegistryItem["type"], + "registry:base" | "registry:font" + >, + }, + ], + name: item.name, + registryDependencies: [...usedRegistryDependencies], + title: item.title, + type: item.type as Exclude< + RegistryItem["type"], + "registry:base" | "registry:font" + >, + }; + + return NextResponse.json(itemResponse); + }; +} diff --git a/apps/docs/app/api/registry/base/[component]/route.ts b/apps/docs/app/api/registry/base/[component]/route.ts new file mode 100644 index 00000000..7d4d7425 --- /dev/null +++ b/apps/docs/app/api/registry/base/[component]/route.ts @@ -0,0 +1,13 @@ +import { createRegistryGET, createRegistryHandler } from "../../_lib/create-registry"; + +const registryPromise = createRegistryHandler({ + elementsPackage: "elements-base", + uiPackageName: "@repo/base-ui", + registryStyle: "base", + uiImportPrefix: "@/registry/base/ui/", + utilsImportPrefix: "@/lib/", + apiBasePath: "/api/registry/base", + examplesPackage: "examples-base", +}); + +export const GET = createRegistryGET(registryPromise); diff --git a/apps/docs/components/custom/elements-installer-tabs.tsx b/apps/docs/components/custom/elements-installer-tabs.tsx new file mode 100644 index 00000000..c1b70ca4 --- /dev/null +++ b/apps/docs/components/custom/elements-installer-tabs.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { CodeBlock } from "@/components/geistdocs/code-block"; +import { + CodeBlockTab, + CodeBlockTabs, + CodeBlockTabsList, + CodeBlockTabsTrigger, +} from "@/components/geistdocs/code-block-tabs"; +import { useUILibrary } from "@/hooks/geistdocs/use-ui-library"; + +interface VariantData { + elementsCommand: string; + shadcnCommand: string; + highlightedCode: string; + hasSource: boolean; +} + +interface ElementsInstallerTabsProps { + radix: VariantData; + base: VariantData; +} + +const HighlightedCodePreview = ({ + highlightedCode, +}: { + highlightedCode: string; +}) => ( + // oxlint-disable-next-line eslint-plugin-react(no-danger) +
+);
+
+export const ElementsInstallerTabs = ({
+  radix,
+  base,
+}: ElementsInstallerTabsProps) => {
+  const { library } = useUILibrary();
+  const data = library === "base" ? base : radix;
+
+  return (
+    
+ + + + AI Elements + + shadcn CLI + {data.hasSource && ( + Manual + )} + + + {data.elementsCommand} + + + {data.shadcnCommand} + + {data.hasSource && ( + + + + + + )} + +
+ ); +}; diff --git a/apps/docs/components/custom/elements-installer.tsx b/apps/docs/components/custom/elements-installer.tsx index 74ad2b83..818ed1a1 100644 --- a/apps/docs/components/custom/elements-installer.tsx +++ b/apps/docs/components/custom/elements-installer.tsx @@ -6,19 +6,16 @@ import { join } from "node:path"; import { codeToHtml } from "shiki"; -import { CodeBlock } from "@/components/geistdocs/code-block"; -import { - CodeBlockTab, - CodeBlockTabs, - CodeBlockTabsList, - CodeBlockTabsTrigger, -} from "@/components/geistdocs/code-block-tabs"; +import { ElementsInstallerTabs } from "./elements-installer-tabs"; interface ElementsInstallerProps { path?: string; } -const loadSourceCode = async (componentPath: string): Promise => { +const loadSourceCode = async ( + packageName: string, + componentPath: string +): Promise => { try { const code = await readFile( join( @@ -26,7 +23,7 @@ const loadSourceCode = async (componentPath: string): Promise => { "..", "..", "packages", - "elements", + packageName, "src", `${componentPath}.tsx` ), @@ -44,61 +41,56 @@ const loadSourceCode = async (componentPath: string): Promise => { } }; -const getCommands = (path?: string) => { +const getCommands = (path?: string, variant?: "radix" | "base") => { + const registryPrefix = + variant === "base" ? "@ai-elements/base/" : "@ai-elements/"; const elementsCommand = path ? `npx ai-elements@latest add ${path}` : "npx ai-elements@latest"; const shadcnCommand = path - ? `npx shadcn@latest add @ai-elements/${path}` - : "npx shadcn@latest add @ai-elements/all"; + ? `npx shadcn@latest add ${registryPrefix}${path}` + : `npx shadcn@latest add ${registryPrefix}all`; return { elementsCommand, shadcnCommand }; }; -interface HighlightedCodePreviewProps { - highlightedCode: string; -} +export const ElementsInstaller = async ({ path }: ElementsInstallerProps) => { + const [radixSource, baseSource] = await Promise.all([ + path ? loadSourceCode("elements", path) : Promise.resolve(""), + path ? loadSourceCode("elements-base", path) : Promise.resolve(""), + ]); -const HighlightedCodePreview = ({ - highlightedCode, -}: HighlightedCodePreviewProps) => ( - // oxlint-disable-next-line eslint-plugin-react(no-danger) -
-);
+  const radixCommands = getCommands(path, "radix");
+  const baseCommands = getCommands(path, "base");
 
-export const ElementsInstaller = async ({ path }: ElementsInstallerProps) => {
-  const sourceCode = path ? await loadSourceCode(path) : "";
-  const { elementsCommand, shadcnCommand } = getCommands(path);
-  const highlightedCode = await codeToHtml(sourceCode, {
-    lang: "tsx",
-    themes: { dark: "github-dark", light: "github-light" },
-  });
+  const [radixHighlighted, baseHighlighted] = await Promise.all([
+    radixSource
+      ? codeToHtml(radixSource, {
+          lang: "tsx",
+          themes: { dark: "github-dark", light: "github-light" },
+        })
+      : Promise.resolve(""),
+    baseSource
+      ? codeToHtml(baseSource, {
+          lang: "tsx",
+          themes: { dark: "github-dark", light: "github-light" },
+        })
+      : Promise.resolve(""),
+  ]);
 
   return (
-    
- - - - AI Elements - - shadcn CLI - {sourceCode && ( - Manual - )} - - - {elementsCommand} - - - {shadcnCommand} - - {sourceCode && ( - - - - - - )} - -
+ ); }; diff --git a/apps/docs/components/custom/preview-library-switch.tsx b/apps/docs/components/custom/preview-library-switch.tsx new file mode 100644 index 00000000..cabe716c --- /dev/null +++ b/apps/docs/components/custom/preview-library-switch.tsx @@ -0,0 +1,18 @@ +"use client"; + +import type { ReactNode } from "react"; + +import { useUILibrary } from "@/hooks/geistdocs/use-ui-library"; + +interface PreviewLibrarySwitchProps { + radix: ReactNode; + base: ReactNode; +} + +export const PreviewLibrarySwitch = ({ + radix, + base, +}: PreviewLibrarySwitchProps) => { + const { library } = useUILibrary(); + return <>{library === "base" ? base : radix}; +}; diff --git a/apps/docs/components/custom/preview.tsx b/apps/docs/components/custom/preview.tsx index 6646b0c8..5770ec8d 100644 --- a/apps/docs/components/custom/preview.tsx +++ b/apps/docs/components/custom/preview.tsx @@ -19,39 +19,90 @@ import { CodeBlockTabsTrigger, } from "@/components/geistdocs/code-block-tabs"; +import { PreviewLibrarySwitch } from "./preview-library-switch"; + interface ComponentPreviewProps { path: string; className?: string; } +const loadCode = async (pkg: string, path: string) => { + try { + return await readFile( + join(process.cwd(), "..", "..", "packages", pkg, "src", `${path}.tsx`), + "utf8" + ); + } catch { + return ""; + } +}; + export const Preview = async ({ path, className }: ComponentPreviewProps) => { - const code = await readFile( - join( - process.cwd(), - "..", - "..", - "packages", - "examples", - "src", - `${path}.tsx` - ), - "utf8" - ); + const [radixCode, baseCode] = await Promise.all([ + loadCode("examples", path), + loadCode("examples-base", path), + ]); - const Component = await import(`@repo/examples/src/${path}.tsx`).then( - (module) => module.default + // Dynamic imports must use inline template literals so the bundler + // can resolve the glob pattern for each package. + const RadixComponent = await import(`@repo/examples/src/${path}.tsx`).then( + (m) => m.default ); - if (!Component) { + const BaseComponent = await import( + `@repo/examples-base/src/${path}.tsx` + ).then((m) => m.default); + + if (!RadixComponent) { throw new Error( `No default export found for example: ${path}. Check that @repo/examples/src/${path}.tsx exports a default component.` ); } - const parsedCode = code + if (!BaseComponent) { + throw new Error( + `No default export found for example: ${path}. Check that @repo/examples-base/src/${path}.tsx exports a default component.` + ); + } + + const radixParsedCode = radixCode .replaceAll("@repo/shadcn-ui/", "@/") .replaceAll("@repo/elements/", "@/components/ai-elements/"); + const baseParsedCode = baseCode + .replaceAll("@repo/base-ui/", "@/") + .replaceAll("@repo/elements-base/", "@/components/ai-elements/"); + + const radixPreview = ( + + +
+ +
+
+ + +
+ ); + + const basePreview = BaseComponent ? ( + + +
+ +
+
+ + +
+ ) : null; + return ( @@ -59,27 +110,32 @@ export const Preview = async ({ path, className }: ComponentPreviewProps) => { Code - - -
- -
-
- - -
+
-
- -
+ + + + } + base={ +
+ +
+ } + />
); diff --git a/apps/docs/components/geistdocs/navbar.tsx b/apps/docs/components/geistdocs/navbar.tsx index e62aeed7..64da6dc1 100644 --- a/apps/docs/components/geistdocs/navbar.tsx +++ b/apps/docs/components/geistdocs/navbar.tsx @@ -8,6 +8,7 @@ import { DesktopMenu } from "./desktop-menu"; import { SlashIcon } from "./icons"; import { MobileMenu } from "./mobile-menu"; import { SearchButton } from "./search"; +import { UILibrarySwitcher } from "./ui-library-switcher"; export const Navbar = () => (
@@ -23,6 +24,7 @@ export const Navbar = () => (
+ diff --git a/apps/docs/components/geistdocs/ui-library-switcher.tsx b/apps/docs/components/geistdocs/ui-library-switcher.tsx new file mode 100644 index 00000000..d131c0cd --- /dev/null +++ b/apps/docs/components/geistdocs/ui-library-switcher.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { cn } from "@repo/shadcn-ui/lib/utils"; + +import { + type UILibrary, + useUILibrary, +} from "@/hooks/geistdocs/use-ui-library"; + +const options: { value: UILibrary; label: string }[] = [ + { value: "radix", label: "Radix UI" }, + { value: "base", label: "Base UI" }, +]; + +export const UILibrarySwitcher = ({ className }: { className?: string }) => { + const { library, setLibrary } = useUILibrary(); + + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +}; diff --git a/apps/docs/content/components/(chatbot)/confirmation.mdx b/apps/docs/content/components/(chatbot)/confirmation.mdx index 1ddcb700..af6e2696 100644 --- a/apps/docs/content/components/(chatbot)/confirmation.mdx +++ b/apps/docs/content/components/(chatbot)/confirmation.mdx @@ -245,7 +245,8 @@ A styled description element for displaying a title or label within the confirma ", }, }} diff --git a/apps/docs/content/components/(chatbot)/message.mdx b/apps/docs/content/components/(chatbot)/message.mdx index ed189847..668a021c 100644 --- a/apps/docs/content/components/(chatbot)/message.mdx +++ b/apps/docs/content/components/(chatbot)/message.mdx @@ -314,7 +314,8 @@ export default ActionsDemo; ", }, }} diff --git a/apps/docs/hooks/geistdocs/use-ui-library.ts b/apps/docs/hooks/geistdocs/use-ui-library.ts new file mode 100644 index 00000000..5e4844ee --- /dev/null +++ b/apps/docs/hooks/geistdocs/use-ui-library.ts @@ -0,0 +1,14 @@ +import { useAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; + +export type UILibrary = "radix" | "base"; + +export const uiLibraryAtom = atomWithStorage( + "geistdocs:ui-library", + "radix" +); + +export const useUILibrary = () => { + const [library, setLibrary] = useAtom(uiLibraryAtom); + return { library, setLibrary }; +}; diff --git a/apps/docs/package.json b/apps/docs/package.json index 866ade8d..476311d0 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -12,8 +12,11 @@ "@ai-sdk/react": "^3.0.41", "@icons-pack/react-simple-icons": "^13.8.0", "@orama/tokenizers": "^3.1.16", + "@repo/base-ui": "workspace:*", "@repo/elements": "workspace:*", + "@repo/elements-base": "workspace:*", "@repo/examples": "workspace:*", + "@repo/examples-base": "workspace:*", "@repo/shadcn-ui": "workspace:*", "@streamdown/cjk": "^1.0.1", "@streamdown/code": "^1.0.1", @@ -64,4 +67,4 @@ "tw-animate-css": "^1.4.0", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/packages/base-ui/components.json b/packages/base-ui/components.json new file mode 100644 index 00000000..ce3cc801 --- /dev/null +++ b/packages/base-ui/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-vega", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "../../apps/docs/app/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@repo/base-ui/components", + "utils": "@repo/base-ui/lib/utils", + "hooks": "@repo/base-ui/hooks", + "lib": "@repo/base-ui/lib", + "ui": "@repo/base-ui/components/ui" + }, + "iconLibrary": "lucide" +} diff --git a/packages/base-ui/components/ui/accordion.tsx b/packages/base-ui/components/ui/accordion.tsx new file mode 100644 index 00000000..cd272ae0 --- /dev/null +++ b/packages/base-ui/components/ui/accordion.tsx @@ -0,0 +1,77 @@ +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; +import { cn } from "@repo/base-ui/lib/utils"; +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + + ); +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + + + {children} + + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + +
+ {children} +
+
+ ); +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/packages/base-ui/components/ui/alert-dialog.tsx b/packages/base-ui/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..06b8bc70 --- /dev/null +++ b/packages/base-ui/components/ui/alert-dialog.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; +import { Button } from "@repo/base-ui/components/ui/button"; +import { cn } from "@repo/base-ui/lib/utils"; +import * as React from "react"; + +function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { + return ; +} + +function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { + return ( + + ); +} + +function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ); +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: AlertDialogPrimitive.Popup.Props & { + size?: "default" | "sm"; +}) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CarouselNext({ + className, + variant = "outline", + size = "icon-sm", + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, + useCarousel, +}; diff --git a/packages/base-ui/components/ui/chart.tsx b/packages/base-ui/components/ui/chart.tsx new file mode 100644 index 00000000..88b636b6 --- /dev/null +++ b/packages/base-ui/components/ui/chart.tsx @@ -0,0 +1,372 @@ +"use client"; + +import { cn } from "@repo/base-ui/lib/utils"; +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; +import type { TooltipValueType } from "recharts"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const; +type TooltipNameType = number | string; + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +>; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + initialDimension?: { + width: number; + height: number; + }; +}) { + const uniqueId = React.useId(); + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color + ); + + if (!colorConfig.length) { + return null; + } + + return ( +