@nikala-ui/docs 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +27 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +7 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +32 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +308 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/preview.d.ts +7 -0
- package/dist/cli/commands/preview.d.ts.map +1 -0
- package/dist/cli/commands/preview.js +24 -0
- package/dist/cli/commands/preview.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +7 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +33 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +53 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/App.d.ts +5 -0
- package/dist/client/App.d.ts.map +1 -0
- package/dist/client/App.jsx +31 -0
- package/dist/client/App.jsx.map +1 -0
- package/dist/client/app-types.d.ts +14 -0
- package/dist/client/app-types.d.ts.map +1 -0
- package/dist/client/app-types.js +2 -0
- package/dist/client/app-types.js.map +1 -0
- package/dist/client/components/DocsLayoutShell.d.ts +21 -0
- package/dist/client/components/DocsLayoutShell.d.ts.map +1 -0
- package/dist/client/components/DocsLayoutShell.jsx +14 -0
- package/dist/client/components/DocsLayoutShell.jsx.map +1 -0
- package/dist/client/components/DocsPageContent.d.ts +9 -0
- package/dist/client/components/DocsPageContent.d.ts.map +1 -0
- package/dist/client/components/DocsPageContent.jsx +10 -0
- package/dist/client/components/DocsPageContent.jsx.map +1 -0
- package/dist/client/entry.d.ts +2 -0
- package/dist/client/entry.d.ts.map +1 -0
- package/dist/client/entry.jsx +28 -0
- package/dist/client/entry.jsx.map +1 -0
- package/dist/client/index.html +21 -0
- package/dist/client/navigation/page-navigation.d.ts +9 -0
- package/dist/client/navigation/page-navigation.d.ts.map +1 -0
- package/dist/client/navigation/page-navigation.js +51 -0
- package/dist/client/navigation/page-navigation.js.map +1 -0
- package/dist/client/routing/use-docs-router.d.ts +17 -0
- package/dist/client/routing/use-docs-router.d.ts.map +1 -0
- package/dist/client/routing/use-docs-router.js +71 -0
- package/dist/client/routing/use-docs-router.js.map +1 -0
- package/dist/client/ssr-entry.d.ts +3 -0
- package/dist/client/ssr-entry.d.ts.map +1 -0
- package/dist/client/ssr-entry.jsx +13 -0
- package/dist/client/ssr-entry.jsx.map +1 -0
- package/dist/client/style.css +183 -0
- package/dist/client/utils/sidebar.d.ts +2 -0
- package/dist/client/utils/sidebar.d.ts.map +1 -0
- package/dist/client/utils/sidebar.js +2 -0
- package/dist/client/utils/sidebar.js.map +1 -0
- package/dist/components/mdx-components.d.ts +34 -0
- package/dist/components/mdx-components.d.ts.map +1 -0
- package/dist/components/mdx-components.jsx +131 -0
- package/dist/components/mdx-components.jsx.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +105 -0
- package/dist/config.js.map +1 -0
- package/dist/core/content-scanner.d.ts +10 -0
- package/dist/core/content-scanner.d.ts.map +1 -0
- package/dist/core/content-scanner.js +153 -0
- package/dist/core/content-scanner.js.map +1 -0
- package/dist/core/route-tree.d.ts +19 -0
- package/dist/core/route-tree.d.ts.map +1 -0
- package/dist/core/route-tree.js +176 -0
- package/dist/core/route-tree.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +9 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/mdx/compiler.d.ts +13 -0
- package/dist/mdx/compiler.d.ts.map +1 -0
- package/dist/mdx/compiler.js +38 -0
- package/dist/mdx/compiler.js.map +1 -0
- package/dist/mdx/highlighter.d.ts +22 -0
- package/dist/mdx/highlighter.d.ts.map +1 -0
- package/dist/mdx/highlighter.js +139 -0
- package/dist/mdx/highlighter.js.map +1 -0
- package/dist/mdx/rehype-shiki.d.ts +3 -0
- package/dist/mdx/rehype-shiki.d.ts.map +1 -0
- package/dist/mdx/rehype-shiki.js +83 -0
- package/dist/mdx/rehype-shiki.js.map +1 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/sidebar-state.d.ts +6 -0
- package/dist/navigation/sidebar-state.d.ts.map +1 -0
- package/dist/navigation/sidebar-state.js +20 -0
- package/dist/navigation/sidebar-state.js.map +1 -0
- package/dist/navigation/sidebar-tree.d.ts +7 -0
- package/dist/navigation/sidebar-tree.d.ts.map +1 -0
- package/dist/navigation/sidebar-tree.js +11 -0
- package/dist/navigation/sidebar-tree.js.map +1 -0
- package/dist/registry/accordion.json +18 -0
- package/dist/registry/alert.json +18 -0
- package/dist/registry/api-table.json +21 -0
- package/dist/registry/aspect-ratio.json +17 -0
- package/dist/registry/avatar.json +17 -0
- package/dist/registry/badge.json +18 -0
- package/dist/registry/banner.json +19 -0
- package/dist/registry/breadcrumb.json +17 -0
- package/dist/registry/bubble.json +18 -0
- package/dist/registry/button-group.json +20 -0
- package/dist/registry/button.json +21 -0
- package/dist/registry/callout.json +19 -0
- package/dist/registry/card.json +17 -0
- package/dist/registry/checkbox.json +20 -0
- package/dist/registry/code-block.json +26 -0
- package/dist/registry/code-group.json +20 -0
- package/dist/registry/collapsible.json +18 -0
- package/dist/registry/combobox.json +21 -0
- package/dist/registry/command.json +27 -0
- package/dist/registry/component-viewer.json +25 -0
- package/dist/registry/container.json +18 -0
- package/dist/registry/context-menu.json +24 -0
- package/dist/registry/create-active-element.json +13 -0
- package/dist/registry/create-app-updater.json +13 -0
- package/dist/registry/create-audio.json +7 -0
- package/dist/registry/create-battery.json +13 -0
- package/dist/registry/create-chat-scroll.json +13 -0
- package/dist/registry/create-click-outside.json +13 -0
- package/dist/registry/create-clipboard.json +13 -0
- package/dist/registry/create-color-mode.json +13 -0
- package/dist/registry/create-controllable-signal.json +13 -0
- package/dist/registry/create-debounce.json +13 -0
- package/dist/registry/create-disclosure.json +13 -0
- package/dist/registry/create-document-tabs.json +13 -0
- package/dist/registry/create-document-title.json +13 -0
- package/dist/registry/create-drop-zone.json +13 -0
- package/dist/registry/create-event-source.json +13 -0
- package/dist/registry/create-favicon.json +13 -0
- package/dist/registry/create-fetch.json +13 -0
- package/dist/registry/create-focus-trap.json +13 -0
- package/dist/registry/create-form.json +13 -0
- package/dist/registry/create-fullscreen.json +13 -0
- package/dist/registry/create-geolocation.json +13 -0
- package/dist/registry/create-global-shortcut.json +13 -0
- package/dist/registry/create-hover.json +13 -0
- package/dist/registry/create-idle.json +13 -0
- package/dist/registry/create-infinite-scroll.json +13 -0
- package/dist/registry/create-input-mask.json +13 -0
- package/dist/registry/create-intersection-observer.json +13 -0
- package/dist/registry/create-keybindings.json +13 -0
- package/dist/registry/create-lock-scroll.json +13 -0
- package/dist/registry/create-long-press.json +13 -0
- package/dist/registry/create-media-query.json +13 -0
- package/dist/registry/create-mouse-position.json +13 -0
- package/dist/registry/create-network-status.json +13 -0
- package/dist/registry/create-orientation.json +13 -0
- package/dist/registry/create-pagination.json +13 -0
- package/dist/registry/create-permission.json +13 -0
- package/dist/registry/create-previous.json +13 -0
- package/dist/registry/create-resize-observer.json +13 -0
- package/dist/registry/create-scroll-into-view.json +13 -0
- package/dist/registry/create-scroll-position.json +13 -0
- package/dist/registry/create-storage.json +13 -0
- package/dist/registry/create-tauri-window.json +13 -0
- package/dist/registry/create-timer.json +13 -0
- package/dist/registry/create-tiptap-editor.json +34 -0
- package/dist/registry/create-undo-redo.json +13 -0
- package/dist/registry/create-web-notification.json +13 -0
- package/dist/registry/create-websocket.json +13 -0
- package/dist/registry/create-window-size.json +13 -0
- package/dist/registry/dialog.json +21 -0
- package/dist/registry/dropdown-menu.json +22 -0
- package/dist/registry/dropzone.json +21 -0
- package/dist/registry/empty.json +17 -0
- package/dist/registry/field.json +20 -0
- package/dist/registry/file-tree.json +21 -0
- package/dist/registry/footer.json +18 -0
- package/dist/registry/forgot-password-01.json +28 -0
- package/dist/registry/form-message.json +17 -0
- package/dist/registry/form.json +17 -0
- package/dist/registry/hero-01.json +22 -0
- package/dist/registry/hover-card.json +18 -0
- package/dist/registry/icon-button.json +20 -0
- package/dist/registry/index.json +1489 -0
- package/dist/registry/input-group.json +21 -0
- package/dist/registry/input.json +17 -0
- package/dist/registry/kbd.json +18 -0
- package/dist/registry/label.json +18 -0
- package/dist/registry/list.json +20 -0
- package/dist/registry/login-01.json +28 -0
- package/dist/registry/logo.json +17 -0
- package/dist/registry/marker.json +17 -0
- package/dist/registry/marquee.json +17 -0
- package/dist/registry/message.json +17 -0
- package/dist/registry/navbar.json +19 -0
- package/dist/registry/navigation-menu.json +22 -0
- package/dist/registry/number-input.json +24 -0
- package/dist/registry/otp-verification-01.json +26 -0
- package/dist/registry/package-manager-tabs.json +24 -0
- package/dist/registry/pager.json +21 -0
- package/dist/registry/pagination.json +22 -0
- package/dist/registry/pin-input.json +17 -0
- package/dist/registry/popover.json +22 -0
- package/dist/registry/progress.json +18 -0
- package/dist/registry/radio-group.json +18 -0
- package/dist/registry/rating.json +19 -0
- package/dist/registry/register-01.json +31 -0
- package/dist/registry/resizable.json +21 -0
- package/dist/registry/review-card.json +23 -0
- package/dist/registry/rich-text-editor.json +95 -0
- package/dist/registry/scroll-area.json +21 -0
- package/dist/registry/section-heading.json +21 -0
- package/dist/registry/select.json +22 -0
- package/dist/registry/separator.json +17 -0
- package/dist/registry/sheet.json +22 -0
- package/dist/registry/sidebar.json +24 -0
- package/dist/registry/skeleton.json +17 -0
- package/dist/registry/slider.json +18 -0
- package/dist/registry/spinner.json +18 -0
- package/dist/registry/stat.json +19 -0
- package/dist/registry/status.json +18 -0
- package/dist/registry/steps.json +20 -0
- package/dist/registry/switch.json +20 -0
- package/dist/registry/table-of-contents.json +23 -0
- package/dist/registry/table.json +17 -0
- package/dist/registry/tabs.json +20 -0
- package/dist/registry/textarea.json +17 -0
- package/dist/registry/theme-manager.json +39 -0
- package/dist/registry/timeline.json +18 -0
- package/dist/registry/titlebar-tabs.json +24 -0
- package/dist/registry/titlebar.json +26 -0
- package/dist/registry/toast.json +20 -0
- package/dist/registry/toggle-group.json +21 -0
- package/dist/registry/toggle.json +18 -0
- package/dist/registry/tooltip.json +18 -0
- package/dist/registry/updater-modal.json +26 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +526 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/plugin.d.ts +10 -0
- package/dist/server/plugin.d.ts.map +1 -0
- package/dist/server/plugin.js +271 -0
- package/dist/server/plugin.js.map +1 -0
- package/dist/themes/default/content/breadcrumbs.d.ts +4 -0
- package/dist/themes/default/content/breadcrumbs.d.ts.map +1 -0
- package/dist/themes/default/content/breadcrumbs.jsx +31 -0
- package/dist/themes/default/content/breadcrumbs.jsx.map +1 -0
- package/dist/themes/default/content/pager.d.ts +5 -0
- package/dist/themes/default/content/pager.d.ts.map +1 -0
- package/dist/themes/default/content/pager.jsx +9 -0
- package/dist/themes/default/content/pager.jsx.map +1 -0
- package/dist/themes/default/content/table-of-contents.d.ts +4 -0
- package/dist/themes/default/content/table-of-contents.d.ts.map +1 -0
- package/dist/themes/default/content/table-of-contents.jsx +19 -0
- package/dist/themes/default/content/table-of-contents.jsx.map +1 -0
- package/dist/themes/default/index.d.ts +11 -0
- package/dist/themes/default/index.d.ts.map +1 -0
- package/dist/themes/default/index.js +30 -0
- package/dist/themes/default/index.js.map +1 -0
- package/dist/themes/default/layout.d.ts +4 -0
- package/dist/themes/default/layout.d.ts.map +1 -0
- package/dist/themes/default/layout.jsx +75 -0
- package/dist/themes/default/layout.jsx.map +1 -0
- package/dist/themes/default/navbar.d.ts +4 -0
- package/dist/themes/default/navbar.d.ts.map +1 -0
- package/dist/themes/default/navbar.jsx +74 -0
- package/dist/themes/default/navbar.jsx.map +1 -0
- package/dist/themes/default/navigation/mobile-table-of-contents.d.ts +4 -0
- package/dist/themes/default/navigation/mobile-table-of-contents.d.ts.map +1 -0
- package/dist/themes/default/navigation/mobile-table-of-contents.jsx +22 -0
- package/dist/themes/default/navigation/mobile-table-of-contents.jsx.map +1 -0
- package/dist/themes/default/navigation/sidebar-tree.d.ts +8 -0
- package/dist/themes/default/navigation/sidebar-tree.d.ts.map +1 -0
- package/dist/themes/default/navigation/sidebar-tree.jsx +100 -0
- package/dist/themes/default/navigation/sidebar-tree.jsx.map +1 -0
- package/dist/themes/default/overlays/search-dialog.d.ts +4 -0
- package/dist/themes/default/overlays/search-dialog.d.ts.map +1 -0
- package/dist/themes/default/overlays/search-dialog.jsx +34 -0
- package/dist/themes/default/overlays/search-dialog.jsx.map +1 -0
- package/dist/themes/default/sidebar.d.ts +4 -0
- package/dist/themes/default/sidebar.d.ts.map +1 -0
- package/dist/themes/default/sidebar.jsx +60 -0
- package/dist/themes/default/sidebar.jsx.map +1 -0
- package/dist/themes/index.d.ts +4 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +5 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/types.d.ts +83 -0
- package/dist/themes/types.d.ts.map +1 -0
- package/dist/themes/types.js +2 -0
- package/dist/themes/types.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/core-src/index.css +11 -0
- package/dist/vendor/core-src/index.ts +48 -0
- package/dist/vendor/core-src/lib/cn.ts +9 -0
- package/dist/vendor/core-src/registry/blocks/forgot-password-01.tsx +141 -0
- package/dist/vendor/core-src/registry/blocks/hero-01.tsx +28 -0
- package/dist/vendor/core-src/registry/blocks/login-01.tsx +231 -0
- package/dist/vendor/core-src/registry/blocks/otp-verification-01.tsx +170 -0
- package/dist/vendor/core-src/registry/blocks/register-01.tsx +318 -0
- package/dist/vendor/core-src/registry/components/ui/accordion.tsx +128 -0
- package/dist/vendor/core-src/registry/components/ui/alert.tsx +155 -0
- package/dist/vendor/core-src/registry/components/ui/api-table.tsx +117 -0
- package/dist/vendor/core-src/registry/components/ui/aspect-ratio.tsx +31 -0
- package/dist/vendor/core-src/registry/components/ui/avatar.tsx +114 -0
- package/dist/vendor/core-src/registry/components/ui/badge.tsx +56 -0
- package/dist/vendor/core-src/registry/components/ui/banner.tsx +187 -0
- package/dist/vendor/core-src/registry/components/ui/breadcrumb.tsx +136 -0
- package/dist/vendor/core-src/registry/components/ui/bubble.tsx +142 -0
- package/dist/vendor/core-src/registry/components/ui/button-group.tsx +42 -0
- package/dist/vendor/core-src/registry/components/ui/button.tsx +82 -0
- package/dist/vendor/core-src/registry/components/ui/callout.tsx +137 -0
- package/dist/vendor/core-src/registry/components/ui/card.tsx +113 -0
- package/dist/vendor/core-src/registry/components/ui/checkbox.tsx +74 -0
- package/dist/vendor/core-src/registry/components/ui/code-block.tsx +326 -0
- package/dist/vendor/core-src/registry/components/ui/code-group.tsx +105 -0
- package/dist/vendor/core-src/registry/components/ui/collapsible.tsx +73 -0
- package/dist/vendor/core-src/registry/components/ui/combobox.tsx +299 -0
- package/dist/vendor/core-src/registry/components/ui/command.tsx +378 -0
- package/dist/vendor/core-src/registry/components/ui/component-viewer.tsx +363 -0
- package/dist/vendor/core-src/registry/components/ui/container.tsx +45 -0
- package/dist/vendor/core-src/registry/components/ui/context-menu.tsx +237 -0
- package/dist/vendor/core-src/registry/components/ui/dialog.tsx +200 -0
- package/dist/vendor/core-src/registry/components/ui/dropdown-menu.tsx +298 -0
- package/dist/vendor/core-src/registry/components/ui/dropzone.tsx +189 -0
- package/dist/vendor/core-src/registry/components/ui/empty.tsx +83 -0
- package/dist/vendor/core-src/registry/components/ui/field.tsx +71 -0
- package/dist/vendor/core-src/registry/components/ui/file-tree.tsx +181 -0
- package/dist/vendor/core-src/registry/components/ui/footer.tsx +247 -0
- package/dist/vendor/core-src/registry/components/ui/form-message.tsx +36 -0
- package/dist/vendor/core-src/registry/components/ui/form.tsx +21 -0
- package/dist/vendor/core-src/registry/components/ui/hover-card.tsx +105 -0
- package/dist/vendor/core-src/registry/components/ui/icon-button.tsx +34 -0
- package/dist/vendor/core-src/registry/components/ui/input-group.tsx +80 -0
- package/dist/vendor/core-src/registry/components/ui/input.tsx +28 -0
- package/dist/vendor/core-src/registry/components/ui/kbd.tsx +73 -0
- package/dist/vendor/core-src/registry/components/ui/label.tsx +30 -0
- package/dist/vendor/core-src/registry/components/ui/list.tsx +222 -0
- package/dist/vendor/core-src/registry/components/ui/logo.tsx +68 -0
- package/dist/vendor/core-src/registry/components/ui/marker.tsx +102 -0
- package/dist/vendor/core-src/registry/components/ui/marquee.tsx +118 -0
- package/dist/vendor/core-src/registry/components/ui/message.tsx +168 -0
- package/dist/vendor/core-src/registry/components/ui/navbar.tsx +375 -0
- package/dist/vendor/core-src/registry/components/ui/navigation-menu.tsx +358 -0
- package/dist/vendor/core-src/registry/components/ui/number-input.tsx +150 -0
- package/dist/vendor/core-src/registry/components/ui/package-manager-tabs.tsx +226 -0
- package/dist/vendor/core-src/registry/components/ui/pager.tsx +102 -0
- package/dist/vendor/core-src/registry/components/ui/pagination.tsx +258 -0
- package/dist/vendor/core-src/registry/components/ui/pin-input.tsx +198 -0
- package/dist/vendor/core-src/registry/components/ui/popover.tsx +90 -0
- package/dist/vendor/core-src/registry/components/ui/progress.tsx +66 -0
- package/dist/vendor/core-src/registry/components/ui/radio-group.tsx +95 -0
- package/dist/vendor/core-src/registry/components/ui/rating.tsx +185 -0
- package/dist/vendor/core-src/registry/components/ui/resizable.tsx +195 -0
- package/dist/vendor/core-src/registry/components/ui/review-card.tsx +195 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/index.ts +11 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
- package/dist/vendor/core-src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
- package/dist/vendor/core-src/registry/components/ui/scroll-area.tsx +228 -0
- package/dist/vendor/core-src/registry/components/ui/section-heading.tsx +108 -0
- package/dist/vendor/core-src/registry/components/ui/select.tsx +156 -0
- package/dist/vendor/core-src/registry/components/ui/separator.tsx +37 -0
- package/dist/vendor/core-src/registry/components/ui/sheet.tsx +220 -0
- package/dist/vendor/core-src/registry/components/ui/sidebar.tsx +692 -0
- package/dist/vendor/core-src/registry/components/ui/skeleton.tsx +24 -0
- package/dist/vendor/core-src/registry/components/ui/slider.tsx +153 -0
- package/dist/vendor/core-src/registry/components/ui/spinner.tsx +41 -0
- package/dist/vendor/core-src/registry/components/ui/stat.tsx +245 -0
- package/dist/vendor/core-src/registry/components/ui/status.tsx +121 -0
- package/dist/vendor/core-src/registry/components/ui/steps.tsx +198 -0
- package/dist/vendor/core-src/registry/components/ui/switch.tsx +68 -0
- package/dist/vendor/core-src/registry/components/ui/table-of-contents.tsx +131 -0
- package/dist/vendor/core-src/registry/components/ui/table.tsx +160 -0
- package/dist/vendor/core-src/registry/components/ui/tabs.tsx +204 -0
- package/dist/vendor/core-src/registry/components/ui/textarea.tsx +82 -0
- package/dist/vendor/core-src/registry/components/ui/theme-toggle.tsx +238 -0
- package/dist/vendor/core-src/registry/components/ui/timeline.tsx +350 -0
- package/dist/vendor/core-src/registry/components/ui/titlebar-tabs.tsx +773 -0
- package/dist/vendor/core-src/registry/components/ui/titlebar.tsx +468 -0
- package/dist/vendor/core-src/registry/components/ui/toast.tsx +124 -0
- package/dist/vendor/core-src/registry/components/ui/toggle-group.tsx +203 -0
- package/dist/vendor/core-src/registry/components/ui/toggle.tsx +101 -0
- package/dist/vendor/core-src/registry/components/ui/tooltip.tsx +41 -0
- package/dist/vendor/core-src/registry/components/ui/updater-modal.tsx +254 -0
- package/dist/vendor/core-src/registry/index.ts +50 -0
- package/dist/vendor/core-src/registry/metadata.ts +742 -0
- package/dist/vendor/core-src/registry/providers/theme-provider.tsx +216 -0
- package/dist/vendor/core-src/registry/providers/theme-script.tsx +78 -0
- package/dist/vendor/core-src/registry/providers/theme-transitions.ts +139 -0
- package/dist/vendor/hooks-src/create-active-element.ts +51 -0
- package/dist/vendor/hooks-src/create-app-updater.ts +272 -0
- package/dist/vendor/hooks-src/create-audio-video.ts +444 -0
- package/dist/vendor/hooks-src/create-battery.ts +78 -0
- package/dist/vendor/hooks-src/create-chat-scroll.ts +113 -0
- package/dist/vendor/hooks-src/create-click-outside.ts +82 -0
- package/dist/vendor/hooks-src/create-clipboard.ts +69 -0
- package/dist/vendor/hooks-src/create-color-mode.ts +115 -0
- package/dist/vendor/hooks-src/create-controllable-signal.ts +56 -0
- package/dist/vendor/hooks-src/create-debounce.ts +90 -0
- package/dist/vendor/hooks-src/create-disclosure.ts +82 -0
- package/dist/vendor/hooks-src/create-document-tabs.ts +267 -0
- package/dist/vendor/hooks-src/create-document-title.ts +33 -0
- package/dist/vendor/hooks-src/create-drop-zone.ts +317 -0
- package/dist/vendor/hooks-src/create-event-source.ts +126 -0
- package/dist/vendor/hooks-src/create-favicon.ts +52 -0
- package/dist/vendor/hooks-src/create-fetch.ts +93 -0
- package/dist/vendor/hooks-src/create-focus-trap.ts +94 -0
- package/dist/vendor/hooks-src/create-form.ts +200 -0
- package/dist/vendor/hooks-src/create-fullscreen.ts +105 -0
- package/dist/vendor/hooks-src/create-geolocation.ts +140 -0
- package/dist/vendor/hooks-src/create-global-shortcut.ts +140 -0
- package/dist/vendor/hooks-src/create-hover.ts +88 -0
- package/dist/vendor/hooks-src/create-idle.ts +109 -0
- package/dist/vendor/hooks-src/create-infinite-scroll.ts +90 -0
- package/dist/vendor/hooks-src/create-input-mask.ts +109 -0
- package/dist/vendor/hooks-src/create-intersection-observer.ts +82 -0
- package/dist/vendor/hooks-src/create-keybindings.ts +148 -0
- package/dist/vendor/hooks-src/create-lock-scroll.ts +97 -0
- package/dist/vendor/hooks-src/create-long-press.ts +85 -0
- package/dist/vendor/hooks-src/create-media-query.ts +94 -0
- package/dist/vendor/hooks-src/create-mouse-position.ts +87 -0
- package/dist/vendor/hooks-src/create-network-status.ts +100 -0
- package/dist/vendor/hooks-src/create-orientation.ts +129 -0
- package/dist/vendor/hooks-src/create-pagination.ts +208 -0
- package/dist/vendor/hooks-src/create-permission.ts +91 -0
- package/dist/vendor/hooks-src/create-previous.ts +21 -0
- package/dist/vendor/hooks-src/create-resize-observer.ts +89 -0
- package/dist/vendor/hooks-src/create-scroll-into-view.ts +57 -0
- package/dist/vendor/hooks-src/create-scroll-position.ts +129 -0
- package/dist/vendor/hooks-src/create-storage.ts +106 -0
- package/dist/vendor/hooks-src/create-tauri-window.ts +628 -0
- package/dist/vendor/hooks-src/create-timer.ts +150 -0
- package/dist/vendor/hooks-src/create-tiptap-editor.ts +326 -0
- package/dist/vendor/hooks-src/create-undo-redo.ts +100 -0
- package/dist/vendor/hooks-src/create-web-notification.ts +111 -0
- package/dist/vendor/hooks-src/create-websocket.ts +179 -0
- package/dist/vendor/hooks-src/create-window-size.ts +41 -0
- package/dist/vendor/hooks-src/index.ts +48 -0
- package/package.json +87 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pin-input",
|
|
3
|
+
"title": "Pin Input",
|
|
4
|
+
"description": "Interactive multi-slot PIN/OTP input component for SMS and 2FA authentication verification codes.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "ui/pin-input.tsx",
|
|
13
|
+
"content": "import {\n createSignal,\n createMemo,\n createContext,\n useContext,\n type JSX,\n} from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport type PinInputType = \"numeric\" | \"alphanumeric\";\n\nexport interface PinInputContextValue {\n value: () => string;\n setValue: (val: string) => void;\n mask: () => boolean;\n disabled: () => boolean;\n length: () => number;\n type: () => PinInputType;\n containerRef: HTMLDivElement | undefined;\n}\n\nconst PinInputContext = createContext<PinInputContextValue>();\n\nexport interface PinInputProps {\n value?: string;\n onValueChange?: (value: string) => void;\n length?: number;\n type?: PinInputType;\n mask?: boolean;\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root PinInput component for 4 or 6-digit OTP / verification PIN codes.\n */\nexport const PinInput = (props: PinInputProps) => {\n let containerRef: HTMLDivElement | undefined;\n const [internalValue, setInternalValue] = createSignal(props.value ?? \"\");\n\n const value = () => props.value ?? internalValue();\n const mask = () => props.mask ?? false;\n const disabled = () => props.disabled ?? false;\n const length = () => props.length ?? 6;\n\n const inputType = (): PinInputType => {\n const t = props.type ?? \"numeric\";\n if (t !== \"numeric\" && t !== \"alphanumeric\") {\n return \"numeric\";\n }\n return t;\n };\n\n const handleValueChange = (val: string) => {\n setInternalValue(val);\n props.onValueChange?.(val);\n };\n\n const contextValue: PinInputContextValue = {\n value,\n setValue: handleValueChange,\n mask,\n disabled,\n length,\n type: inputType,\n get containerRef() {\n return containerRef;\n },\n };\n\n return (\n <PinInputContext.Provider value={contextValue}>\n <div ref={containerRef} class={cn(\"flex items-center gap-2\", props.class)}>\n {props.children}\n </div>\n </PinInputContext.Provider>\n );\n};\n\nexport interface PinInputLabelProps {\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Accessible label for PinInput.\n */\nexport const PinInputLabel = (props: PinInputLabelProps) => {\n return (\n <label class={cn(\"text-sm font-medium leading-none text-foreground\", props.class)}>\n {props.children}\n </label>\n );\n};\n\nconst focusIndex = (containerRef: HTMLDivElement | undefined, index: number) => {\n const el = containerRef?.querySelector<HTMLInputElement>(\n `[data-pin-input-index=\"${index}\"]`\n );\n el?.focus();\n el?.select();\n};\n\nexport interface PinInputInputProps {\n index: number;\n class?: string;\n}\n\n/**\n * Individual input slot for a single OTP digit/character.\n */\nexport const PinInputInput = (props: PinInputInputProps) => {\n const ctx = useContext(PinInputContext);\n const digit = createMemo(() => ctx?.value()[props.index] ?? \"\");\n\n const isCharAllowed = (char: string) => {\n if (!ctx || char === \"\") return false;\n return ctx.type() === \"numeric\"\n ? /^[0-9]$/.test(char)\n : /^[a-zA-Z0-9]$/.test(char);\n };\n\n const commitChar = (char: string) => {\n if (!ctx) return;\n const currentArray = ctx.value().split(\"\");\n while (currentArray.length < props.index) currentArray.push(\"\");\n currentArray[props.index] = char;\n ctx.setValue(currentArray.join(\"\"));\n };\n\n const handleInput = (e: InputEvent & { currentTarget: HTMLInputElement }) => {\n if (!ctx || ctx.disabled()) return;\n const val = e.currentTarget.value;\n const rawChar = val ? val[val.length - 1] : \"\";\n\n // Clear digit if input is empty\n if (rawChar === \"\") {\n commitChar(\"\");\n return;\n }\n\n if (!isCharAllowed(rawChar)) {\n e.currentTarget.value = digit();\n return;\n }\n\n commitChar(rawChar);\n\n if (props.index < ctx.length() - 1) {\n focusIndex(ctx.containerRef, props.index + 1);\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!ctx || ctx.disabled()) return;\n if (e.key === \"Backspace\" && digit() === \"\" && props.index > 0) {\n focusIndex(ctx.containerRef, props.index - 1);\n }\n };\n\n const handlePaste = (e: ClipboardEvent & { currentTarget: HTMLInputElement }) => {\n if (!ctx || ctx.disabled()) return;\n e.preventDefault();\n const pasted = e.clipboardData?.getData(\"text\") ?? \"\";\n const chars = pasted.split(\"\").filter(isCharAllowed);\n if (chars.length === 0) return;\n\n const currentArray = ctx.value().split(\"\");\n let lastFilled = props.index;\n for (let i = 0; i < chars.length && props.index + i < ctx.length(); i++) {\n while (currentArray.length < props.index + i) currentArray.push(\"\");\n currentArray[props.index + i] = chars[i];\n lastFilled = props.index + i;\n }\n ctx.setValue(currentArray.join(\"\"));\n focusIndex(ctx.containerRef, Math.min(lastFilled + 1, ctx.length() - 1));\n };\n\n return (\n <input\n type={ctx?.mask() ? \"password\" : \"text\"}\n inputMode={ctx?.type() === \"numeric\" ? \"numeric\" : \"text\"}\n autocomplete=\"one-time-code\"\n maxLength={1}\n disabled={ctx?.disabled()}\n data-pin-input-index={props.index}\n value={digit()}\n onInput={handleInput}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n class={cn(\n \"relative flex h-10 w-10 text-center text-sm font-semibold rounded-md border border-input bg-background shadow-xs transition-all focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50\",\n props.class\n )}\n />\n );\n};\n",
|
|
14
|
+
"type": "registry:ui"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "popover",
|
|
3
|
+
"title": "Popover",
|
|
4
|
+
"description": "Displays rich content in a portal layer triggered by a button, built on Kobalte primitives.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"@kobalte/core",
|
|
10
|
+
"lucide-solid"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"create-click-outside"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "ui/popover.tsx",
|
|
18
|
+
"content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Popover as KobaltePopover } from \"@kobalte/core/popover\";\nimport { createClickOutside } from \"@/hooks/create-click-outside\";\nimport { X } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Popover = KobaltePopover;\n\nexport const PopoverTrigger = KobaltePopover.Trigger;\n\nexport const PopoverArrow: Component<ComponentProps<typeof KobaltePopover.Arrow>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Arrow\n class={cn(\"fill-popover stroke-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverContent: Component<ComponentProps<typeof KobaltePopover.Content>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\", \"onInteractOutside\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n },\n });\n\n return (\n <KobaltePopover.Portal>\n <KobaltePopover.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"z-50 w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </KobaltePopover.Content>\n </KobaltePopover.Portal>\n );\n};\n\nexport const PopoverTitle: Component<ComponentProps<typeof KobaltePopover.Title>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Title\n class={cn(\"text-sm font-semibold text-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverDescription: Component<ComponentProps<typeof KobaltePopover.Description>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverCloseButton: Component<ComponentProps<typeof KobaltePopover.CloseButton>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <KobaltePopover.CloseButton\n class={cn(\n \"absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || <X class=\"h-4 w-4 text-muted-foreground\" />}\n </KobaltePopover.CloseButton>\n );\n};",
|
|
19
|
+
"type": "registry:ui"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "progress",
|
|
3
|
+
"title": "Progress",
|
|
4
|
+
"description": "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"@kobalte/core"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/progress.tsx",
|
|
14
|
+
"content": "import { splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { Progress as KobalteProgress } from \"@kobalte/core/progress\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ProgressProps\n extends Omit<\n JSX.HTMLAttributes<HTMLDivElement>,\n \"value\" | \"aria-valuenow\" | \"aria-valuemin\" | \"aria-valuemax\"\n > {\n /** The current numeric progress value. */\n value?: number;\n /** Minimum progress value. Defaults to 0. */\n minValue?: number;\n /** Maximum progress value. Defaults to 100. */\n maxValue?: number;\n /** Label text for accessibility and screen readers. */\n getValueLabel?: (params: { value: number; max: number }) => string;\n /** Custom label element to display above or beside progress bar. */\n label?: JSX.Element;\n /** Optional custom class for root container. */\n class?: string;\n /** Optional custom class for indicator fill bar. */\n indicatorClass?: string;\n}\n\n/**\n * Nikala UI Progress component for showing task completion status or media timeline positions.\n */\nexport const Progress: Component<ProgressProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"minValue\",\n \"maxValue\",\n \"getValueLabel\",\n \"label\",\n \"class\",\n \"indicatorClass\",\n ]);\n\n return (\n <KobalteProgress\n value={local.value ?? 0}\n minValue={local.minValue ?? 0}\n maxValue={local.maxValue ?? 100}\n getValueLabel={local.getValueLabel}\n class={cn(\"flex w-full flex-col gap-1.5\", local.class)}\n {...rest}\n >\n <Show when={local.label}>\n <div class=\"flex justify-between text-xs font-medium text-muted-foreground\">\n <KobalteProgress.Label>{local.label}</KobalteProgress.Label>\n <KobalteProgress.ValueLabel class=\"font-mono text-foreground\" />\n </div>\n </Show>\n\n <KobalteProgress.Track class=\"relative h-2 w-full overflow-hidden rounded-lg bg-primary/20\">\n <KobalteProgress.Fill\n class={cn(\n \"h-full w-(--kb-progress-fill-width) bg-primary transition-all duration-300 ease-in-out\",\n local.indicatorClass\n )}\n />\n </KobalteProgress.Track>\n </KobalteProgress>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "radio-group",
|
|
3
|
+
"title": "Radio Group",
|
|
4
|
+
"description": "A set of checkable buttons built on Kobalte primitives where only one button can be checked at a time.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"@kobalte/core"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/radio-group.tsx",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as RadioGroupPrimitive from \"@kobalte/core/radio-group\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cn } from \"@/lib/cn\";\n\nexport type RadioGroupRootProps<T extends ValidComponent = \"div\"> =\n RadioGroupPrimitive.RadioGroupRootProps<T> & {\n class?: string;\n };\n\n/**\n * Root RadioGroup component built on top of Kobalte headless primitives.\n * Supports both \"vertical\" (default) and \"horizontal\" layout orientations.\n */\nexport const RadioGroup = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, RadioGroupRootProps<T>>\n) => {\n const [local, rest] = splitProps(props as RadioGroupRootProps, [\"class\", \"orientation\"]);\n\n const isHorizontal = () => local.orientation === \"horizontal\";\n\n return (\n <RadioGroupPrimitive.Root\n orientation={local.orientation || \"vertical\"}\n class={cn(\n isHorizontal()\n ? \"flex flex-row items-center gap-4\"\n : \"grid gap-2\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type RadioGroupItemProps<T extends ValidComponent = \"div\"> =\n RadioGroupPrimitive.RadioGroupItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n id?: string;\n value: string;\n };\n\n/**\n * Accessible RadioGroup item choice wrapping Kobalte primitives.\n */\nexport const RadioGroupItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, RadioGroupItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as RadioGroupItemProps, [\"class\", \"children\"]);\n\n return (\n <RadioGroupPrimitive.Item\n class={cn(\"flex items-center space-x-2 cursor-pointer\", local.class)}\n {...rest}\n >\n <RadioGroupPrimitive.ItemInput />\n <RadioGroupPrimitive.ItemControl\n class={cn(\n \"aspect-square h-4 w-4 rounded-lg border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 flex items-center justify-center transition-colors data-[checked]:bg-primary data-[checked]:text-primary-foreground\"\n )}\n >\n <RadioGroupPrimitive.ItemIndicator class=\"flex items-center justify-center\">\n <span class=\"h-2 w-2 rounded-lg bg-primary-foreground\" />\n </RadioGroupPrimitive.ItemIndicator>\n </RadioGroupPrimitive.ItemControl>\n {local.children}\n </RadioGroupPrimitive.Item>\n );\n};\n\nexport type RadioGroupItemLabelProps<T extends ValidComponent = \"label\"> =\n RadioGroupPrimitive.RadioGroupItemLabelProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Accessible text label for a RadioGroup item.\n */\nexport const RadioGroupItemLabel = <T extends ValidComponent = \"label\">(\n props: PolymorphicProps<T, RadioGroupItemLabelProps<T>>\n) => {\n const [local, rest] = splitProps(props as RadioGroupItemLabelProps, [\"class\"]);\n\n return (\n <RadioGroupPrimitive.ItemLabel\n class={cn(\n \"text-sm font-medium leading-none select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-foreground\",\n local.class\n )}\n {...rest}\n />\n );\n};",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rating",
|
|
3
|
+
"title": "Rating",
|
|
4
|
+
"description": "An accessible star rating component supporting interactive inputs, hover preview states, and read-only score badges.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority",
|
|
10
|
+
"lucide-solid"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "ui/rating.tsx",
|
|
15
|
+
"content": "import {\n createSignal,\n splitProps,\n For,\n type JSX,\n type Component,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Star } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const ratingVariants = cva(\n \"inline-flex items-center select-none transition-colors\",\n {\n variants: {\n size: {\n sm: \"gap-0.5\",\n default: \"gap-1\",\n lg: \"gap-1.5\",\n },\n variant: {\n yellow: \"text-amber-500 dark:text-amber-400\",\n primary: \"text-primary\",\n destructive: \"text-destructive\",\n },\n },\n defaultVariants: {\n size: \"default\",\n variant: \"yellow\",\n },\n }\n);\n\nexport const ratingStarVariants = cva(\n \"transition-all duration-150 shrink-0\",\n {\n variants: {\n size: {\n sm: \"size-3.5\",\n default: \"size-4.5\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface RatingProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n VariantProps<typeof ratingVariants> {\n value?: number;\n defaultValue?: number;\n max?: number;\n readOnly?: boolean;\n disabled?: boolean;\n class?: string;\n starClass?: string;\n onChange?: (value: number) => void;\n onHover?: (value: number | null) => void;\n}\n\nexport const Rating: Component<RatingProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"max\",\n \"readOnly\",\n \"disabled\",\n \"size\",\n \"variant\",\n \"class\",\n \"starClass\",\n \"onChange\",\n \"onHover\",\n ]);\n\n const [internalValue, setInternalValue] = createSignal(\n local.defaultValue ?? 0\n );\n const [hoverValue, setHoverValue] = createSignal<number | null>(null);\n\n const currentValue = () =>\n local.value !== undefined ? local.value : internalValue();\n\n const maxStars = () => local.max ?? 5;\n const isInteractive = () => !local.readOnly && !local.disabled;\n\n const stars = () => Array.from({ length: maxStars() }, (_, i) => i + 1);\n\n const handleSelect = (starValue: number) => {\n if (!isInteractive()) return;\n if (local.value === undefined) {\n setInternalValue(starValue);\n }\n local.onChange?.(starValue);\n };\n\n const handleMouseEnter = (starValue: number) => {\n if (!isInteractive()) return;\n setHoverValue(starValue);\n local.onHover?.(starValue);\n };\n\n const handleMouseLeave = () => {\n if (!isInteractive()) return;\n setHoverValue(null);\n local.onHover?.(null);\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!isInteractive()) return;\n\n const val = currentValue();\n if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n const next = Math.min(maxStars(), val + 1);\n handleSelect(next);\n } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n e.preventDefault();\n const prev = Math.max(1, val - 1);\n handleSelect(prev);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n handleSelect(1);\n } else if (e.key === \"End\") {\n e.preventDefault();\n handleSelect(maxStars());\n }\n };\n\n return (\n <div\n role={isInteractive() ? \"radiogroup\" : \"img\"}\n aria-label={`Rating: ${currentValue()} of ${maxStars()} stars`}\n tabIndex={isInteractive() ? 0 : undefined}\n onKeyDown={handleKeyDown}\n onMouseLeave={handleMouseLeave}\n class={cn(\n ratingVariants({ size: local.size, variant: local.variant }),\n local.disabled && \"opacity-50 cursor-not-allowed\",\n isInteractive() && \"cursor-pointer focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md\",\n local.class\n )}\n {...rest}\n >\n <For each={stars()}>\n {(star) => {\n const isFilled = () => {\n const active = hoverValue() !== null ? hoverValue()! : currentValue();\n return star <= active;\n };\n\n return (\n <button\n type=\"button\"\n disabled={local.disabled || local.readOnly}\n tabIndex={-1}\n aria-label={`${star} star${star > 1 ? \"s\" : \"\"}`}\n onClick={() => handleSelect(star)}\n onMouseEnter={() => handleMouseEnter(star)}\n class={cn(\n \"p-0.5 border-0 bg-transparent transition-transform focus:outline-hidden\",\n isInteractive() && \"hover:scale-115 active:scale-95 cursor-pointer\",\n local.readOnly && \"cursor-default\",\n local.disabled && \"cursor-not-allowed\"\n )}\n >\n <Star\n class={cn(\n ratingStarVariants({ size: local.size }),\n isFilled()\n ? \"fill-current\"\n : \"text-muted-foreground/30 fill-transparent\",\n local.starClass\n )}\n />\n </button>\n );\n }}\n </For>\n </div>\n );\n};\n",
|
|
16
|
+
"type": "registry:ui"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "register-01",
|
|
3
|
+
"title": "Register 01 — Sign-Up Card with Password Strength",
|
|
4
|
+
"description": "A comprehensive sign-up card featuring social logins, live password strength meter with validation checklist, and terms agreement.",
|
|
5
|
+
"type": "registry:block",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"card",
|
|
13
|
+
"input",
|
|
14
|
+
"label",
|
|
15
|
+
"button",
|
|
16
|
+
"checkbox",
|
|
17
|
+
"progress",
|
|
18
|
+
"badge",
|
|
19
|
+
"separator",
|
|
20
|
+
"form",
|
|
21
|
+
"field",
|
|
22
|
+
"form-message"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "blocks/register-01.tsx",
|
|
27
|
+
"content": "import { createSignal, Show, type Component } from \"solid-js\";\nimport { createForm } from \"@nikala-ui/hooks\";\nimport {\n Card,\n CardHeader,\n CardTitle,\n CardDescription,\n CardContent,\n CardFooter,\n} from \"../components/ui/card\";\nimport { Form } from \"../components/ui/form\";\nimport { Field, FieldLabel } from \"../components/ui/field\";\nimport { FormMessage } from \"../components/ui/form-message\";\nimport { Button } from \"../components/ui/button\";\nimport { Input } from \"../components/ui/input\";\nimport { Checkbox } from \"../components/ui/checkbox\";\nimport { Label } from \"../components/ui/label\";\nimport { Progress } from \"../components/ui/progress\";\nimport { Badge } from \"../components/ui/badge\";\nimport { Separator } from \"../components/ui/separator\";\nimport { Eye, EyeOff, Check, X, ArrowRight, ShieldCheck } from \"lucide-solid\";\n\nexport const Register01: Component = () => {\n const [showPassword, setShowPassword] = createSignal(false);\n\n const form = createForm({\n initialValues: {\n fullName: \"\",\n email: \"\",\n password: \"\",\n agreeTerms: false,\n },\n validate: (values) => {\n const errors: Record<string, string> = {};\n if (!values.fullName.trim()) {\n errors.fullName = \"Full name is required\";\n }\n if (!values.email.trim()) {\n errors.email = \"Email is required\";\n } else if (!values.email.includes(\"@\")) {\n errors.email = \"Please enter a valid email address\";\n }\n if (!values.password) {\n errors.password = \"Password is required\";\n } else if (values.password.length < 8) {\n errors.password = \"Password must be at least 8 characters\";\n }\n if (!values.agreeTerms) {\n errors.agreeTerms = \"You must agree to the terms and privacy policy\";\n }\n return errors;\n },\n onSubmit: async (values) => {\n // Simulate API registration request\n await new Promise((resolve) => setTimeout(resolve, 1200));\n },\n });\n\n const password = () => form.values().password;\n\n // Reactive password criteria calculations\n const hasMinLength = () => password().length >= 8;\n const hasNumber = () => /\\d/.test(password());\n const hasSpecial = () => /[^A-Za-z0-9]/.test(password());\n const hasUpperLower = () => /[a-z]/.test(password()) && /[A-Z]/.test(password());\n\n const strengthScore = () => {\n let score = 0;\n if (password().length === 0) return 0;\n if (hasMinLength()) score += 25;\n if (hasNumber()) score += 25;\n if (hasSpecial()) score += 25;\n if (hasUpperLower()) score += 25;\n return score;\n };\n\n const strengthLabel = () => {\n const score = strengthScore();\n if (score === 0) return \"None\";\n if (score <= 25) return \"Weak\";\n if (score <= 50) return \"Fair\";\n if (score <= 75) return \"Good\";\n return \"Strong\";\n };\n\n const strengthColorClass = () => {\n const score = strengthScore();\n if (score <= 25) return \"bg-destructive\";\n if (score <= 50) return \"bg-amber-500\";\n if (score <= 75) return \"bg-blue-500\";\n return \"bg-emerald-500\";\n };\n\n const strengthBadgeVariant = () => {\n const score = strengthScore();\n if (score <= 25) return \"destructive\";\n if (score <= 50) return \"outline\";\n return \"secondary\";\n };\n\n const isFormValid = () =>\n form.values().fullName.trim() !== \"\" &&\n form.values().email.trim() !== \"\" &&\n form.values().agreeTerms &&\n strengthScore() >= 50;\n\n return (\n <div class=\"@container w-full min-h-[600px] flex items-center justify-center p-4 sm:p-6 md:p-10\">\n <Card class=\"w-full max-w-lg border-border shadow-md bg-card\">\n {/* Card Header */}\n <CardHeader class=\"space-y-2 text-center pb-6\">\n <div class=\"mx-auto size-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-1\">\n <ShieldCheck class=\"size-5\" />\n </div>\n <CardTitle class=\"text-2xl font-bold tracking-tight\">Create your account</CardTitle>\n <CardDescription class=\"text-xs sm:text-sm\">\n Join thousands of developers building fast SolidJS interfaces\n </CardDescription>\n </CardHeader>\n\n <CardContent class=\"space-y-6\">\n {/* Social OAuth Buttons */}\n <div class=\"grid grid-cols-1 @xs:grid-cols-2 gap-2.5\">\n <Button variant=\"outline\" class=\"w-full justify-center text-xs h-9\">\n <svg class=\"size-4 mr-2 shrink-0\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z\" />\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" class=\"w-full justify-center text-xs h-9\">\n <svg class=\"size-4 mr-2 shrink-0\" viewBox=\"0 0 24 24\">\n <path\n fill=\"#4285F4\"\n d=\"M23.745 12.27c0-.7-.06-1.4-.19-2.07H12v4.51h6.6c-.29 1.52-1.14 2.82-2.4 3.68v3.05h3.88c2.27-2.09 3.66-5.17 3.66-9.17z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 24c3.24 0 5.95-1.08 7.93-2.91l-3.88-3.05c-1.08.72-2.45 1.16-4.05 1.16-3.12 0-5.77-2.1-6.72-4.93H1.25v3.15C3.26 21.36 7.33 24 12 24z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.28 14.27c-.25-.72-.38-1.49-.38-2.27s.13-1.55.38-2.27V6.58H1.25C.45 8.18 0 10.03 0 12s.45 3.82 1.25 5.42l4.03-3.15z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 4.75c1.77 0 3.35.61 4.6 1.8l3.42-3.42C17.95 1.19 15.24 0 12 0 7.33 0 3.26 2.64 1.25 6.58l4.03 3.15c.95-2.83 3.6-4.98 6.72-4.98z\"\n />\n </svg>\n Google\n </Button>\n </div>\n\n <div class=\"relative\">\n <div class=\"absolute inset-0 flex items-center\">\n <Separator />\n </div>\n <div class=\"relative flex justify-center text-xs uppercase\">\n <span class=\"bg-card px-3 text-muted-foreground font-medium text-[11px]\">\n Or register with email\n </span>\n </div>\n </div>\n\n {/* Registration Form with Nikala UI Form & Field ecosystem */}\n <Form onSubmit={form.handleSubmit} loading={form.isSubmitting()} class=\"space-y-4\">\n <Field>\n <FieldLabel for=\"reg-name\" class=\"text-xs sm:text-sm\">Full Name</FieldLabel>\n <Input\n id=\"reg-name\"\n type=\"text\"\n placeholder=\"Niko Pirosmani\"\n value={form.values().fullName}\n onInput={form.handleChange(\"fullName\")}\n onBlur={form.handleBlur(\"fullName\")}\n autocomplete=\"name\"\n />\n <FormMessage form={form} name=\"fullName\" />\n </Field>\n\n <Field>\n <FieldLabel for=\"reg-email\" class=\"text-xs sm:text-sm\">Email Address</FieldLabel>\n <Input\n id=\"reg-email\"\n type=\"email\"\n placeholder=\"niko@nikala.dev\"\n value={form.values().email}\n onInput={form.handleChange(\"email\")}\n onBlur={form.handleBlur(\"email\")}\n autocomplete=\"email\"\n />\n <FormMessage form={form} name=\"email\" />\n </Field>\n\n {/* Password with Strength Meter */}\n <Field class=\"space-y-1.5\">\n <div class=\"flex items-center justify-between\">\n <FieldLabel for=\"reg-password\" class=\"text-xs sm:text-sm\">Password</FieldLabel>\n <Show when={password().length > 0}>\n <Badge variant={strengthBadgeVariant()} class=\"text-[10px] px-1.5 py-0 font-mono\">\n {strengthLabel()}\n </Badge>\n </Show>\n </div>\n\n <div class=\"relative\">\n <Input\n id=\"reg-password\"\n type={showPassword() ? \"text\" : \"password\"}\n placeholder=\"Create a strong password\"\n value={form.values().password}\n onInput={form.handleChange(\"password\")}\n onBlur={form.handleBlur(\"password\")}\n autocomplete=\"new-password\"\n class=\"pr-10\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword())}\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors p-1 cursor-pointer\"\n aria-label={showPassword() ? \"Hide password\" : \"Show password\"}\n >\n <Show when={showPassword()} fallback={<Eye class=\"size-4\" />}>\n <EyeOff class=\"size-4\" />\n </Show>\n </button>\n </div>\n <FormMessage form={form} name=\"password\" />\n\n {/* Reactive Password Strength Progress Bar */}\n <Show when={password().length > 0}>\n <Progress\n value={strengthScore()}\n class=\"h-1.5 mt-2\"\n indicatorClass={strengthColorClass()}\n />\n\n {/* Validation Checklist */}\n <div class=\"grid grid-cols-2 gap-1.5 pt-1 text-[11px] text-muted-foreground\">\n <div class={`flex items-center gap-1.5 ${hasMinLength() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasMinLength()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>8+ characters</span>\n </div>\n <div class={`flex items-center gap-1.5 ${hasNumber() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasNumber()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>At least 1 number</span>\n </div>\n <div class={`flex items-center gap-1.5 ${hasUpperLower() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasUpperLower()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>Uppercase & lowercase</span>\n </div>\n <div class={`flex items-center gap-1.5 ${hasSpecial() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasSpecial()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>1 special symbol</span>\n </div>\n </div>\n </Show>\n </Field>\n\n {/* Terms of Service Agreement */}\n <div class=\"space-y-1 pt-1\">\n <div class=\"flex items-start space-x-2.5\">\n <Checkbox\n id=\"reg-terms\"\n checked={form.values().agreeTerms}\n onChange={(checked) => form.setFieldValue(\"agreeTerms\", checked)}\n class=\"mt-0.5\"\n />\n <Label\n for=\"reg-terms\"\n class=\"text-xs font-normal text-muted-foreground cursor-pointer select-none leading-tight\"\n >\n I agree to the{\" \"}\n <a href=\"#terms\" class=\"text-primary font-medium underline hover:text-primary/80\">\n Terms of Service\n </a>{\" \"}\n and{\" \"}\n <a href=\"#privacy\" class=\"text-primary font-medium underline hover:text-primary/80\">\n Privacy Policy\n </a>.\n </Label>\n </div>\n <FormMessage form={form} name=\"agreeTerms\" />\n </div>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n class=\"w-full mt-3 font-medium\"\n disabled={form.isSubmitting() || !isFormValid()}\n >\n <Show when={form.isSubmitting()} fallback={<>Create Account <ArrowRight class=\"ml-2 size-4\" /></>}>\n Creating account...\n </Show>\n </Button>\n </Form>\n </CardContent>\n\n {/* Card Footer */}\n <CardFooter class=\"justify-center border-t border-border/50 py-4 text-xs text-muted-foreground\">\n Already have an account?{\" \"}\n <a href=\"/blocks/login-01\" class=\"text-primary font-semibold hover:underline ml-1\">\n Sign in\n </a>\n </CardFooter>\n </Card>\n </div>\n );\n};\n\nexport default Register01;\n",
|
|
28
|
+
"type": "registry:block"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resizable",
|
|
3
|
+
"title": "Resizable",
|
|
4
|
+
"description": "Accessible resizable panel layout component supporting drag-to-resize handles.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"create-resize-observer"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/resizable.tsx",
|
|
17
|
+
"content": "import {\n createSignal,\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createElementSize } from \"@/hooks/create-resize-observer\";\nimport { GripVertical, GripHorizontal } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ResizableContextValue {\n orientation: Accessor<\"horizontal\" | \"vertical\">;\n registerPanel: (id: string, initialSizes: number) => void;\n sizes: Accessor<Record<string, number>>;\n startDragging: (handleIndex: number, event: PointerEvent) => void;\n containerRef: () => HTMLDivElement | undefined;\n}\n\nconst ResizableContext = createContext<ResizableContextValue>();\n\nexport interface ResizableGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ResizableGroup: Component<ResizableGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"orientation\", \"class\", \"children\"]);\n const orientation = () => local.orientation || \"horizontal\";\n let containerEl: HTMLDivElement | undefined;\n\n const [panelOrder, setPanelOrder] = createSignal<string[]>([]);\n const [sizes, setSizes] = createSignal<Record<string, number>>({});\n\n const registerPanel = (id: string, initialSize: number) => {\n setPanelOrder((prev) => (prev.includes(id) ? prev : [...prev, id]));\n setSizes((prev) => (prev[id] !== undefined ? prev : { ...prev, [id]: initialSize }));\n };\n\n const startDragging = (handleIndex: number, event: PointerEvent) => {\n if (!containerEl) return;\n event.preventDefault();\n\n const order = panelOrder();\n if (handleIndex < 0 || handleIndex >= order.length - 1) return;\n\n const leftId = order[handleIndex];\n const rightId = order[handleIndex + 1];\n\n const isHoriz = orientation() === \"horizontal\";\n const startPos = isHoriz ? event.clientX : event.clientY;\n const rect = containerEl.getBoundingClientRect();\n const totalPx = isHoriz ? rect.width : rect.height;\n\n const startLeftPct = sizes()[leftId] ?? 50;\n const startRightPct = sizes()[rightId] ?? 50;\n\n const onPointerMove = (e: PointerEvent) => {\n const currentPos = isHoriz ? e.clientX : e.clientY;\n const deltaPx = currentPos - startPos;\n const deltaPct = (deltaPx / totalPx) * 100;\n\n let newLeft = startLeftPct + deltaPct;\n let newRight = startRightPct - deltaPct;\n\n // Min size limits (10% minimum)\n if (newLeft < 10) {\n newLeft = 10;\n newRight = startLeftPct + startRightPct - 10;\n } else if (newRight < 10) {\n newRight = 10;\n newLeft = startLeftPct + startRightPct - 10;\n }\n\n setSizes((prev) => ({\n ...prev,\n [leftId]: newLeft,\n [rightId]: newRight,\n }));\n };\n\n const onPointerUp = () => {\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n return (\n <ResizableContext.Provider\n value={{\n orientation,\n registerPanel,\n sizes,\n startDragging,\n containerRef: () => containerEl,\n }}\n >\n <div\n ref={containerEl}\n class={cn(\n \"flex h-full w-full overflow-hidden rounded-lg border border-border bg-background\",\n orientation() === \"vertical\" ? \"flex-col\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </ResizableContext.Provider>\n );\n};\n\nexport interface ResizablePanelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n id: string;\n initialSize?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ResizablePanel: Component<ResizablePanelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"id\", \"initialSize\", \"class\", \"children\"]);\n const ctx = useContext(ResizableContext);\n\n if (!ctx) {\n throw new Error(\"ResizablePanel must be used within a ResizableGroup\");\n }\n\n ctx.registerPanel(local.id, local.initialSize ?? 50);\n\n const currentPct = () => ctx.sizes()[local.id] ?? local.initialSize ?? 50;\n\n // Utilize Nikala UI createElementSize hook for reactive size inspection\n const containerSize = createElementSize(() => ctx.containerRef());\n\n return (\n <div\n class={cn(\"overflow-auto transition-[flex-basis] duration-75\", local.class)}\n style={{\n \"flex-basis\": `${currentPct()}%`,\n \"flex-grow\": 0,\n \"flex-shrink\": 0,\n }}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface ResizableHandleProps extends JSX.HTMLAttributes<HTMLDivElement> {\n handleIndex: number;\n withHandle?: boolean;\n class?: string;\n}\n\nexport const ResizableHandle: Component<ResizableHandleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"handleIndex\", \"withHandle\", \"class\"]);\n const ctx = useContext(ResizableContext);\n\n if (!ctx) {\n throw new Error(\"ResizableHandle must be used within a ResizableGroup\");\n }\n\n const isHoriz = () => ctx.orientation() === \"horizontal\";\n\n return (\n <div\n role=\"separator\"\n tabIndex={0}\n class={cn(\n \"relative flex select-none items-center justify-center bg-border transition-colors hover:bg-primary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-col-resize\",\n isHoriz() ? \"h-full w-1.5 cursor-col-resize\" : \"h-1.5 w-full cursor-row-resize\",\n local.class\n )}\n onPointerDown={(e) => ctx.startDragging(local.handleIndex, e)}\n {...rest}\n >\n {local.withHandle && (\n <div class=\"z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-border bg-muted shadow-2xs\">\n {isHoriz() ? (\n <GripVertical class=\"h-2.5 w-2.5 text-muted-foreground\" />\n ) : (\n <GripHorizontal class=\"h-2.5 w-2.5 text-muted-foreground\" />\n )}\n </div>\n )}\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "review-card",
|
|
3
|
+
"title": "Review Card",
|
|
4
|
+
"description": "A versatile, structured card component for customer testimonials, product ratings, verified buyer badges, and social proof.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority",
|
|
10
|
+
"lucide-solid"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"avatar",
|
|
14
|
+
"rating"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "ui/review-card.tsx",
|
|
19
|
+
"content": "import {\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n type Component,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { CheckCircle2 } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"./avatar\";\nimport { Rating, type RatingProps } from \"./rating\";\n\n/* --- 1. ReviewCard Root --- */\nexport const reviewCardVariants = cva(\n \"relative flex flex-col justify-between rounded-lg transition-all duration-200 overflow-hidden\",\n {\n variants: {\n variant: {\n default: \"border border-border bg-card text-card-foreground p-4 sm:p-5 shadow-2xs hover:shadow-xs\",\n bordered: \"border-2 border-border bg-background text-foreground p-4 sm:p-5\",\n flat: \"bg-muted/40 text-foreground p-4 sm:p-5\",\n glass: \"backdrop-blur-md bg-card/70 border border-border/80 text-card-foreground p-4 sm:p-5 shadow-2xs\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nexport interface ReviewCardProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof reviewCardVariants> {\n class?: string;\n}\n\nexport const ReviewCard: ParentComponent<ReviewCardProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"children\"]);\n\n return (\n <div\n class={cn(reviewCardVariants({ variant: local.variant }), local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 2. ReviewHeader --- */\nexport interface ReviewHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const ReviewHeader: ParentComponent<ReviewHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex items-start justify-between gap-3 mb-3 min-w-0 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 3. ReviewProfile (Avatar + Names container) --- */\nexport interface ReviewProfileProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const ReviewProfile: ParentComponent<ReviewProfileProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex items-start gap-2.5 min-w-0 flex-1 overflow-hidden\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\n/* --- 4. ReviewAvatar --- */\nexport interface ReviewAvatarProps {\n src?: string;\n alt?: string;\n fallback?: string;\n class?: string;\n}\n\nexport const ReviewAvatar: Component<ReviewAvatarProps> = (props) => {\n const [local] = splitProps(props, [\"src\", \"alt\", \"fallback\", \"class\"]);\n\n return (\n <Avatar class={cn(\"size-9 shrink-0 mt-0.5\", local.class)}>\n <Show when={local.src}>\n <AvatarImage src={local.src!} alt={local.alt || \"Reviewer Avatar\"} />\n </Show>\n <AvatarFallback>\n {local.fallback || (local.alt ? local.alt.slice(0, 2).toUpperCase() : \"U\")}\n </AvatarFallback>\n </Avatar>\n );\n};\n\n/* --- 5. ReviewAuthor (Name & Subtitle/Role/Handle) --- */\nexport interface ReviewAuthorProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"role\"> {\n name: string;\n username?: string;\n role?: string;\n verified?: boolean;\n class?: string;\n}\n\nexport const ReviewAuthor: Component<ReviewAuthorProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"name\",\n \"username\",\n \"role\",\n \"verified\",\n \"class\",\n ]);\n\n return (\n <div class={cn(\"flex flex-col min-w-0 flex-1 overflow-hidden\", local.class)} {...rest}>\n <div class=\"flex items-center gap-1 font-semibold text-sm leading-tight text-foreground truncate\">\n <span class=\"truncate\">{local.name}</span>\n <Show when={local.verified}>\n <CheckCircle2 class=\"size-3.5 text-primary shrink-0\" />\n </Show>\n </div>\n <Show when={local.username || local.role}>\n <p class=\"text-xs text-muted-foreground truncate mt-0.5\">\n {local.username || local.role}\n </p>\n </Show>\n </div>\n );\n};\n\n/* --- 6. ReviewRating (Composing standalone Rating) --- */\nexport interface ReviewRatingProps extends RatingProps {}\n\nexport const ReviewRating: Component<ReviewRatingProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"size\", \"readOnly\"]);\n\n return (\n <Rating\n size={local.size || \"sm\"}\n readOnly={local.readOnly !== undefined ? local.readOnly : true}\n class={cn(\"shrink-0 select-none ml-auto pt-0.5\", local.class)}\n {...rest}\n />\n );\n};\n\n/* --- 7. ReviewBody (Quote / Content) --- */\nexport interface ReviewBodyProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const ReviewBody: ParentComponent<ReviewBodyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n class={cn(\"text-xs sm:text-sm text-muted-foreground leading-relaxed\", local.class)}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n\n/* --- 8. ReviewFooter --- */\nexport interface ReviewFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const ReviewFooter: ParentComponent<ReviewFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"mt-3 pt-3 border-t border-border/40 flex items-center justify-between text-xs text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
|
|
20
|
+
"type": "registry:ui"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rich-text-editor",
|
|
3
|
+
"title": "Rich Text Editor",
|
|
4
|
+
"description": "A full-featured WYSIWYG rich text editor with toolbar, bubble formatting, tables, task lists, and image uploads built on Tiptap.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@tiptap/core",
|
|
8
|
+
"@tiptap/starter-kit",
|
|
9
|
+
"@tiptap/extension-link",
|
|
10
|
+
"@tiptap/extension-image",
|
|
11
|
+
"@tiptap/extension-placeholder",
|
|
12
|
+
"@tiptap/extension-task-list",
|
|
13
|
+
"@tiptap/extension-task-item",
|
|
14
|
+
"@tiptap/extension-table",
|
|
15
|
+
"@tiptap/extension-table-row",
|
|
16
|
+
"@tiptap/extension-table-cell",
|
|
17
|
+
"@tiptap/extension-table-header",
|
|
18
|
+
"@tiptap/extension-underline",
|
|
19
|
+
"@tiptap/extension-text-align",
|
|
20
|
+
"@tiptap/extension-highlight",
|
|
21
|
+
"@tiptap/extension-character-count",
|
|
22
|
+
"@tiptap/extension-subscript",
|
|
23
|
+
"@tiptap/extension-superscript",
|
|
24
|
+
"@tiptap/extension-typography",
|
|
25
|
+
"tiptap-markdown",
|
|
26
|
+
"clsx",
|
|
27
|
+
"tailwind-merge",
|
|
28
|
+
"lucide-solid"
|
|
29
|
+
],
|
|
30
|
+
"registryDependencies": [
|
|
31
|
+
"create-tiptap-editor"
|
|
32
|
+
],
|
|
33
|
+
"files": [
|
|
34
|
+
{
|
|
35
|
+
"path": "ui/rich-text-editor/bubble-menu.tsx",
|
|
36
|
+
"content": "import {\n createSignal,\n createEffect,\n onCleanup,\n Show,\n For,\n type Component,\n} from \"solid-js\";\nimport { Portal } from \"solid-js/web\";\nimport { useRichTextEditor } from \"./editor.js\";\nimport { ToolbarButton, HIGHLIGHT_PALETTE } from \"./toolbar.js\";\nimport {\n Bold,\n Italic,\n Underline as UnderlineIcon,\n Strikethrough,\n Code,\n Link as LinkIcon,\n Highlighter,\n Heading1,\n Heading2,\n List,\n ListTodo,\n} from \"lucide-solid\";\n\nexport const EditorBubbleMenu: Component = () => {\n const { actions, container, isFullscreen, setShowLinkDialog } = useRichTextEditor();\n const [isVisible, setIsVisible] = createSignal(false);\n const [position, setPosition] = createSignal({ top: 0, left: 0 });\n const [showHighlightPalette, setShowHighlightPalette] = createSignal(false);\n\n createEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleSelectionChange = () => {\n const ed = actions.editor();\n const containerEl = container();\n if (!ed || !ed.isFocused || !ed.isEditable) {\n setIsVisible(false);\n setShowHighlightPalette(false);\n return;\n }\n\n const { from, to } = ed.state.selection;\n if (from === to || ed.state.selection.empty) {\n setIsVisible(false);\n setShowHighlightPalette(false);\n return;\n }\n\n const selection = window.getSelection();\n if (!selection || selection.rangeCount === 0) {\n setIsVisible(false);\n setShowHighlightPalette(false);\n return;\n }\n\n const range = selection.getRangeAt(0);\n const rect = range.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n setIsVisible(false);\n setShowHighlightPalette(false);\n return;\n }\n\n // Check that selection is inside the editor view element\n const editorDom = ed.view.dom;\n if (!editorDom.contains(range.commonAncestorContainer)) {\n setIsVisible(false);\n setShowHighlightPalette(false);\n return;\n }\n\n if (isFullscreen() && containerEl) {\n const containerRect = containerEl.getBoundingClientRect();\n const top = rect.top - containerRect.top - 46;\n const left = rect.left - containerRect.left + rect.width / 2;\n\n setPosition({\n top: Math.max(8, top),\n left: Math.max(140, Math.min(containerRect.width - 140, left)),\n });\n } else {\n // Normal mode: viewport coordinates\n setPosition({\n top: Math.max(12, rect.top - 46),\n left: Math.max(140, Math.min(window.innerWidth - 140, rect.left + rect.width / 2)),\n });\n }\n\n setIsVisible(true);\n };\n\n const handleMouseDown = (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n // If clicking toolbar or any open popovers, hide bubble menu\n if (\n target.closest(\".editor-popover-trigger\") ||\n target.closest(\".editor-popover-content\") ||\n target.closest(\".editor-toolbar\")\n ) {\n setIsVisible(false);\n setShowHighlightPalette(false);\n }\n };\n\n document.addEventListener(\"selectionchange\", handleSelectionChange);\n document.addEventListener(\"mousedown\", handleMouseDown);\n\n onCleanup(() => {\n document.removeEventListener(\"selectionchange\", handleSelectionChange);\n document.removeEventListener(\"mousedown\", handleMouseDown);\n });\n });\n\n const portalTarget = () => {\n if (typeof document === \"undefined\") return undefined;\n return isFullscreen() ? container() || document.body : document.body;\n };\n\n return (\n <Show when={isVisible()}>\n <Portal mount={portalTarget()}>\n <div\n class={`${\n isFullscreen() ? \"absolute\" : \"fixed\"\n } z-[999999] flex items-center gap-0.5 p-1 rounded-md border border-border bg-popover text-popover-foreground shadow-xl backdrop-blur-md -translate-x-1/2 transition-all duration-75 animate-in fade-in zoom-in-95 pointer-events-auto select-none`}\n style={{\n top: `${position().top}px`,\n left: `${position().left}px`,\n }}\n >\n <ToolbarButton onClick={actions.toggleBold} isActive={actions.isActive(\"bold\")} tooltip=\"Bold\">\n <Bold class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleItalic} isActive={actions.isActive(\"italic\")} tooltip=\"Italic\">\n <Italic class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleUnderline} isActive={actions.isActive(\"underline\")} tooltip=\"Underline\">\n <UnderlineIcon class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleStrike} isActive={actions.isActive(\"strike\")} tooltip=\"Strikethrough\">\n <Strikethrough class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleCode} isActive={actions.isActive(\"code\")} tooltip=\"Code\">\n <Code class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n\n {/* Marker Highlight with Tailwind Palette */}\n <div class=\"relative\">\n <ToolbarButton\n onClick={() => setShowHighlightPalette((p) => !p)}\n isActive={actions.isActive(\"highlight\")}\n tooltip=\"Highlight Color\"\n >\n <Highlighter class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n\n <Show when={showHighlightPalette()}>\n <div class=\"absolute top-full left-1/2 -translate-x-1/2 mt-1.5 z-30 flex flex-col gap-1.5 p-2 bg-popover text-popover-foreground border border-border rounded-lg shadow-2xl min-w-[200px]\">\n <div class=\"text-[10px] font-semibold text-muted-foreground uppercase tracking-wider\">\n Marker Palette\n </div>\n <div class=\"grid grid-cols-5 gap-1.5\">\n <For each={HIGHLIGHT_PALETTE}>\n {(color) => (\n <button\n type=\"button\"\n title={color.name}\n class=\"h-6 w-6 rounded-md border border-border/60 hover:scale-110 transition-transform cursor-pointer shadow-2xs\"\n style={{ \"background-color\": color.value }}\n onClick={() => {\n actions.toggleHighlight(color.value);\n setShowHighlightPalette(false);\n }}\n />\n )}\n </For>\n </div>\n <button\n type=\"button\"\n class=\"w-full text-center py-1 mt-1 text-xs rounded-md bg-muted hover:bg-muted/80 text-foreground transition-colors cursor-pointer font-medium\"\n onClick={() => {\n actions.toggleHighlight();\n setShowHighlightPalette(false);\n }}\n >\n Clear Highlight\n </button>\n </div>\n </Show>\n </div>\n\n <ToolbarButton\n onClick={() => setShowLinkDialog(true)}\n isActive={actions.isActive(\"link\")}\n tooltip=\"Insert Link\"\n >\n <LinkIcon class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n\n <div class=\"h-4 w-px bg-border mx-0.5\" />\n\n <ToolbarButton\n onClick={() => actions.setHeading(1)}\n isActive={actions.isActive(\"heading\", { level: 1 })}\n tooltip=\"H1\"\n >\n <Heading1 class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => actions.setHeading(2)}\n isActive={actions.isActive(\"heading\", { level: 2 })}\n tooltip=\"H2\"\n >\n <Heading2 class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={actions.toggleBulletList}\n isActive={actions.isActive(\"bulletList\")}\n tooltip=\"List\"\n >\n <List class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={actions.toggleTaskList}\n isActive={actions.isActive(\"taskList\")}\n tooltip=\"Tasks\"\n >\n <ListTodo class=\"h-3.5 w-3.5\" />\n </ToolbarButton>\n </div>\n </Portal>\n </Show>\n );\n};\n",
|
|
37
|
+
"type": "registry:ui"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"path": "ui/rich-text-editor/editor.tsx",
|
|
41
|
+
"content": "import {\n createContext,\n createSignal,\n createEffect,\n useContext,\n splitProps,\n Show,\n onMount,\n onCleanup,\n type Component,\n type ComponentProps,\n type Accessor,\n} from \"solid-js\";\nimport { useEditor, type UseEditorReturn, type UseEditorOptions } from \"./use-editor.js\";\nimport { EditorToolbar } from \"./toolbar.js\";\nimport { EditorFooter } from \"./footer.js\";\nimport { EditorLinkDialog } from \"./link-dialog.js\";\nimport { EditorImageDialog } from \"./image-dialog.js\";\nimport { EditorBubbleMenu } from \"./bubble-menu.js\";\nimport { EditorSlashCommand } from \"./slash-command.js\";\nimport { cn } from \"@/lib/cn\";\n\nexport type EditorViewMode = \"visual\" | \"markdown\";\n\nexport interface RichTextEditorContextValue {\n actions: UseEditorReturn;\n container: Accessor<HTMLDivElement | undefined>;\n isFullscreen: Accessor<boolean>;\n toggleFullscreen: () => void;\n viewMode: Accessor<EditorViewMode>;\n toggleViewMode: () => void;\n showLinkDialog: Accessor<boolean>;\n setShowLinkDialog: (show: boolean) => void;\n showImageDialog: Accessor<boolean>;\n setShowImageDialog: (show: boolean) => void;\n showTableMenu: Accessor<boolean>;\n setShowTableMenu: (show: boolean | ((prev: boolean) => boolean)) => void;\n}\n\nconst RichTextEditorContext = createContext<RichTextEditorContextValue>();\n\nexport function useRichTextEditor(): RichTextEditorContextValue {\n const context = useContext(RichTextEditorContext);\n if (!context) {\n throw new Error(\"useRichTextEditor must be used within a <RichTextEditor /> component\");\n }\n return context;\n}\n\nexport interface RichTextEditorProps\n extends Omit<ComponentProps<\"div\">, \"autofocus\" | \"onChange\"> {\n value?: string;\n onChange?: (html: string) => void;\n placeholder?: string;\n editable?: boolean;\n autofocus?: boolean | \"start\" | \"end\" | \"all\" | number;\n characterLimit?: number;\n hideToolbar?: boolean;\n hideFooter?: boolean;\n hideBubbleMenu?: boolean;\n editorOptions?: Omit<UseEditorOptions, \"content\" | \"placeholder\" | \"editable\">;\n}\n\nexport const RichTextEditor: Component<RichTextEditorProps> = (props) => {\n const [local, others] = splitProps(props, [\n \"class\",\n \"value\",\n \"onChange\",\n \"placeholder\",\n \"editable\",\n \"autofocus\",\n \"characterLimit\",\n \"hideToolbar\",\n \"hideFooter\",\n \"hideBubbleMenu\",\n \"editorOptions\",\n \"children\",\n ]);\n\n let containerRef: HTMLDivElement | undefined;\n let editorElementRef: HTMLDivElement | undefined;\n const [container, setContainer] = createSignal<HTMLDivElement | undefined>();\n const [isFullscreen, setIsFullscreen] = createSignal(false);\n const [viewMode, setViewMode] = createSignal<EditorViewMode>(\"visual\");\n const [markdownSource, setMarkdownSource] = createSignal(\"\");\n const [showLinkDialog, setShowLinkDialog] = createSignal(false);\n const [showImageDialog, setShowImageDialog] = createSignal(false);\n const [showTableMenu, setShowTableMenu] = createSignal(false);\n\n const actions = useEditor({\n content: local.value,\n placeholder: local.placeholder || \"Write something rich or type '/' for commands...\",\n editable: local.editable ?? true,\n autofocus: local.autofocus ?? false,\n characterLimit: local.characterLimit,\n onUpdate: ({ editor: ed }) => {\n local.onChange?.(ed.getHTML());\n },\n ...local.editorOptions,\n });\n\n onMount(() => {\n if (editorElementRef) {\n actions.mount(editorElementRef);\n }\n });\n\n createEffect(() => {\n const ed = actions.editor();\n if (ed && local.value !== undefined && ed.getHTML() !== local.value) {\n ed.commands.setContent(local.value, { emitUpdate: false });\n }\n });\n\n createEffect(() => {\n const ed = actions.editor();\n if (ed && local.editable !== undefined) {\n actions.setEditable(local.editable);\n }\n });\n\n const toggleFullscreen = () => {\n if (typeof document === \"undefined\") return;\n if (!document.fullscreenElement) {\n if (containerRef?.requestFullscreen) {\n containerRef.requestFullscreen().catch(() => {\n setIsFullscreen(true);\n });\n } else {\n setIsFullscreen(true);\n }\n } else {\n if (document.exitFullscreen) {\n document.exitFullscreen().catch(() => {\n setIsFullscreen(false);\n });\n } else {\n setIsFullscreen(false);\n }\n }\n };\n\n createEffect(() => {\n if (typeof document === \"undefined\") return;\n const handleFullscreenChange = () => {\n const isNativeFs = document.fullscreenElement === containerRef;\n setIsFullscreen(isNativeFs);\n };\n document.addEventListener(\"fullscreenchange\", handleFullscreenChange);\n document.addEventListener(\"webkitfullscreenchange\", handleFullscreenChange);\n onCleanup(() => {\n document.removeEventListener(\"fullscreenchange\", handleFullscreenChange);\n document.removeEventListener(\"webkitfullscreenchange\", handleFullscreenChange);\n });\n });\n\n const toggleViewMode = () => {\n const ed = actions.editor();\n if (!ed) return;\n\n if (viewMode() === \"visual\") {\n // Switch to Markdown mode\n const currentMd = (ed.storage as any).markdown?.getMarkdown?.() || \"\";\n setMarkdownSource(currentMd);\n setViewMode(\"markdown\");\n } else {\n // Switch back to Visual mode\n ed.commands.setContent(markdownSource(), { emitUpdate: true });\n local.onChange?.(ed.getHTML());\n setViewMode(\"visual\");\n }\n };\n\n const handleMarkdownInput = (val: string) => {\n setMarkdownSource(val);\n const ed = actions.editor();\n if (ed) {\n ed.commands.setContent(val, { emitUpdate: false });\n local.onChange?.(ed.getHTML());\n }\n };\n\n createEffect(() => {\n if (typeof window === \"undefined\") return;\n const handleEsc = (e: KeyboardEvent) => {\n if (e.key === \"Escape\" && isFullscreen()) {\n if (typeof document !== \"undefined\" && document.fullscreenElement) {\n document.exitFullscreen?.().catch(() => {});\n } else {\n setIsFullscreen(false);\n }\n }\n };\n window.addEventListener(\"keydown\", handleEsc);\n onCleanup(() => window.removeEventListener(\"keydown\", handleEsc));\n });\n\n createEffect(() => {\n if (typeof document === \"undefined\") return;\n if (isFullscreen()) {\n const original = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n onCleanup(() => {\n document.body.style.overflow = original;\n });\n }\n });\n\n const contextValue: RichTextEditorContextValue = {\n actions,\n container,\n isFullscreen,\n toggleFullscreen,\n viewMode,\n toggleViewMode,\n showLinkDialog,\n setShowLinkDialog,\n showImageDialog,\n setShowImageDialog,\n showTableMenu,\n setShowTableMenu: (val) => {\n if (typeof val === \"function\") {\n setShowTableMenu(val);\n } else {\n setShowTableMenu(val);\n }\n },\n };\n\n return (\n <RichTextEditorContext.Provider value={contextValue}>\n <div\n ref={(el) => {\n containerRef = el;\n setContainer(el);\n }}\n class={cn(\n \"relative flex flex-col w-full rounded-lg border border-border bg-card text-card-foreground shadow-xs transition-all\",\n isFullscreen() &&\n \"fixed inset-0 z-[99999] rounded-none border-0 h-screen w-screen bg-background flex flex-col overflow-hidden m-0 p-0\",\n local.class\n )}\n {...others}\n >\n <Show when={!local.hideToolbar}>\n <EditorToolbar />\n </Show>\n\n {/* Visual WYSIWYG View */}\n <div\n class={cn(\n \"relative flex-1 w-full overflow-y-auto min-h-[220px]\",\n viewMode() === \"markdown\" && \"hidden\"\n )}\n >\n <div\n class={cn(\n \"w-full px-4 py-4 min-h-full\",\n isFullscreen() && \"max-w-4xl mx-auto px-8 py-8\"\n )}\n >\n <div ref={editorElementRef} class=\"tiptap-wrapper w-full h-full\" />\n <Show when={local.children}>{local.children}</Show>\n </div>\n </div>\n\n {/* Raw Markdown Source View */}\n <Show when={viewMode() === \"markdown\"}>\n <div class=\"relative flex-1 w-full overflow-y-auto min-h-[220px] bg-muted/20\">\n <div\n class={cn(\n \"w-full p-4 min-h-full\",\n isFullscreen() && \"max-w-4xl mx-auto px-8 py-8\"\n )}\n >\n <textarea\n class=\"w-full h-full min-h-[220px] bg-transparent text-foreground focus:outline-hidden resize-none leading-relaxed font-mono text-xs\"\n placeholder=\"Write or paste raw markdown here...\"\n value={markdownSource()}\n onInput={(e) => handleMarkdownInput(e.currentTarget.value)}\n />\n </div>\n </div>\n </Show>\n\n <Show when={!local.hideFooter}>\n <EditorFooter />\n </Show>\n\n <Show when={!local.hideBubbleMenu && viewMode() === \"visual\"}>\n <EditorBubbleMenu />\n </Show>\n\n <Show when={viewMode() === \"visual\"}>\n <EditorSlashCommand />\n </Show>\n\n <Show when={showLinkDialog()}>\n <EditorLinkDialog />\n </Show>\n\n <Show when={showImageDialog()}>\n <EditorImageDialog />\n </Show>\n </div>\n </RichTextEditorContext.Provider>\n );\n};\n",
|
|
42
|
+
"type": "registry:ui"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "ui/rich-text-editor/extensions.ts",
|
|
46
|
+
"content": "import StarterKit from \"@tiptap/starter-kit\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport Link from \"@tiptap/extension-link\";\nimport Image from \"@tiptap/extension-image\";\nimport TaskList from \"@tiptap/extension-task-list\";\nimport TaskItem from \"@tiptap/extension-task-item\";\nimport { Table, TableRow, TableCell, TableHeader } from \"@tiptap/extension-table\";\nimport Underline from \"@tiptap/extension-underline\";\nimport TextAlign from \"@tiptap/extension-text-align\";\nimport Highlight from \"@tiptap/extension-highlight\";\nimport CharacterCount from \"@tiptap/extension-character-count\";\nimport Subscript from \"@tiptap/extension-subscript\";\nimport Superscript from \"@tiptap/extension-superscript\";\nimport Typography from \"@tiptap/extension-typography\";\nimport { Markdown } from \"tiptap-markdown\";\nimport type { Extension } from \"@tiptap/core\";\n\nexport interface DefaultExtensionsOptions {\n placeholder?: string;\n characterLimit?: number;\n}\n\nexport function getDefaultExtensions(options?: DefaultExtensionsOptions): Extension[] {\n return [\n StarterKit.configure({\n heading: {\n levels: [1, 2, 3, 4, 5, 6],\n HTMLAttributes: { class: \"font-heading tracking-tight\" },\n },\n bulletList: {\n HTMLAttributes: { class: \"list-disc pl-6 my-3 space-y-1\" },\n },\n orderedList: {\n HTMLAttributes: { class: \"list-decimal pl-6 my-3 space-y-1\" },\n },\n listItem: {\n HTMLAttributes: { class: \"leading-normal my-0.5\" },\n },\n codeBlock: {\n HTMLAttributes: {\n class: \"rounded-lg bg-muted p-4 font-mono text-xs border border-border my-3 overflow-x-auto text-foreground\",\n },\n },\n code: {\n HTMLAttributes: {\n class: \"rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground font-semibold border border-border/50\",\n },\n },\n blockquote: {\n HTMLAttributes: {\n class: \"border-l-4 border-primary pl-4 italic text-muted-foreground my-3\",\n },\n },\n horizontalRule: {\n HTMLAttributes: {\n class: \"border-border my-6\",\n },\n },\n }) as Extension,\n Placeholder.configure({\n placeholder: options?.placeholder || \"Write something rich or type '/' for commands...\",\n emptyEditorClass: \"is-editor-empty\",\n }) as Extension,\n Underline as Extension,\n Subscript as Extension,\n Superscript as Extension,\n Typography as Extension,\n Highlight.configure({\n multicolor: true,\n HTMLAttributes: { class: \"rounded px-1 py-0.5\" },\n }) as Extension,\n TextAlign.configure({ types: [\"heading\", \"paragraph\"] }) as Extension,\n Link.configure({\n openOnClick: false,\n HTMLAttributes: {\n class: \"text-primary font-medium underline underline-offset-4 hover:opacity-80 transition-opacity cursor-pointer\",\n },\n }) as Extension,\n Image.configure({\n HTMLAttributes: {\n class: \"rounded-lg max-w-full h-auto my-4 border border-border shadow-xs\",\n },\n }) as Extension,\n TaskList.configure({\n HTMLAttributes: { class: \"list-none pl-0 my-3 space-y-2\" },\n }) as Extension,\n TaskItem.configure({\n nested: true,\n HTMLAttributes: { class: \"flex items-start gap-2.5 my-1\" },\n }) as Extension,\n Table.configure({\n resizable: true,\n HTMLAttributes: {\n class: \"border-collapse table-auto w-full my-4 border border-border rounded-lg overflow-hidden text-sm\",\n },\n }) as Extension,\n TableRow as Extension,\n TableHeader.configure({\n HTMLAttributes: { class: \"border border-border bg-muted/70 px-3 py-2 font-semibold text-left text-xs\" },\n }) as Extension,\n TableCell.configure({\n HTMLAttributes: { class: \"border border-border px-3 py-2 text-xs\" },\n }) as Extension,\n CharacterCount.configure({ limit: options?.characterLimit }) as Extension,\n Markdown.configure({\n html: true,\n tightLists: true,\n tightListClass: \"tight\",\n bulletListMarker: \"-\",\n linkify: true,\n breaks: false,\n transformPastedText: true,\n transformCopiedText: true,\n }) as Extension,\n ];\n}\n",
|
|
47
|
+
"type": "registry:ui"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"path": "ui/rich-text-editor/footer.tsx",
|
|
51
|
+
"content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { useRichTextEditor } from \"./editor.js\";\nimport { cn } from \"@/lib/cn\";\n\nexport const EditorFooter: Component<ComponentProps<\"div\">> = (props) => {\n const [local, others] = splitProps(props, [\"class\"]);\n const { actions } = useRichTextEditor();\n\n const readingTime = () => {\n const words = actions.wordCount();\n const minutes = Math.max(1, Math.ceil(words / 200));\n return `${minutes} min read`;\n };\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border px-3 py-1.5 text-[11px] text-muted-foreground bg-muted/30 rounded-b-lg select-none\",\n local.class\n )}\n {...others}\n >\n <div class=\"flex items-center gap-3\">\n <span>\n <strong class=\"text-foreground\">{actions.wordCount()}</strong> words\n </span>\n <span>\n <strong class=\"text-foreground\">{actions.characterCount()}</strong> characters\n </span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span>{readingTime()}</span>\n </div>\n </div>\n );\n};\n",
|
|
52
|
+
"type": "registry:ui"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"path": "ui/rich-text-editor/image-dialog.tsx",
|
|
56
|
+
"content": "import { createSignal, type Component } from \"solid-js\";\nimport { useRichTextEditor } from \"./editor.js\";\n\nexport const EditorImageDialog: Component = () => {\n const { actions, setShowImageDialog } = useRichTextEditor();\n const [url, setUrl] = createSignal(\"\");\n const [alt, setAlt] = createSignal(\"\");\n\n const handleApply = (e: Event) => {\n e.preventDefault();\n if (url().trim()) {\n actions.setImage({ src: url().trim(), alt: alt().trim() });\n setShowImageDialog(false);\n }\n };\n\n const handleFileUpload = (e: Event & { currentTarget: HTMLInputElement }) => {\n const file = e.currentTarget.files?.[0];\n if (!file) return;\n\n const reader = new FileReader();\n reader.onload = () => {\n if (typeof reader.result === \"string\") {\n actions.setImage({ src: reader.result, alt: file.name });\n setShowImageDialog(false);\n }\n };\n reader.readAsDataURL(file);\n };\n\n return (\n <div class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-xs p-4\">\n <div class=\"w-full max-w-sm rounded-lg border border-border bg-card p-4 shadow-lg text-card-foreground\">\n <h3 class=\"text-sm font-semibold mb-3\">Insert Image</h3>\n <form onSubmit={handleApply} class=\"flex flex-col gap-3\">\n <div>\n <label class=\"text-[11px] font-medium text-muted-foreground block mb-1\">Image URL</label>\n <input\n type=\"url\"\n placeholder=\"https://images.unsplash.com/...\"\n value={url()}\n onInput={(e) => setUrl(e.currentTarget.value)}\n class=\"w-full h-8 px-2.5 text-xs rounded-md border border-border bg-background text-foreground focus:outline-hidden focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label class=\"text-[11px] font-medium text-muted-foreground block mb-1\">Or Upload Local File</label>\n <input\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileUpload}\n class=\"w-full text-xs text-muted-foreground file:mr-2 file:py-1 file:px-2.5 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-muted file:text-foreground hover:file:bg-muted/80 cursor-pointer\"\n />\n </div>\n <div class=\"flex items-center justify-end gap-2 pt-1\">\n <button\n type=\"button\"\n class=\"px-2.5 py-1 text-xs rounded-md border border-border hover:bg-muted cursor-pointer\"\n onClick={() => setShowImageDialog(false)}\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={!url().trim()}\n class=\"px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground font-medium hover:bg-primary/90 disabled:opacity-50 cursor-pointer\"\n >\n Insert Image\n </button>\n </div>\n </form>\n </div>\n </div>\n );\n};\n",
|
|
57
|
+
"type": "registry:ui"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"path": "ui/rich-text-editor/index.ts",
|
|
61
|
+
"content": "export * from \"./editor.js\";\nexport * from \"./use-editor.js\";\nexport * from \"./extensions.js\";\nexport * from \"./toolbar.js\";\nexport * from \"./bubble-menu.js\";\nexport * from \"./slash-command.js\";\nexport * from \"./link-dialog.js\";\nexport * from \"./image-dialog.js\";\nexport * from \"./table-controls.js\";\nexport * from \"./footer.js\";\nexport * from \"./markdown.js\";\n",
|
|
62
|
+
"type": "registry:ui"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"path": "ui/rich-text-editor/link-dialog.tsx",
|
|
66
|
+
"content": "import { createSignal, type Component } from \"solid-js\";\nimport { useRichTextEditor } from \"./editor.js\";\n\nexport const EditorLinkDialog: Component = () => {\n const { actions, setShowLinkDialog } = useRichTextEditor();\n const [url, setUrl] = createSignal(\"\");\n\n const handleApply = (e: Event) => {\n e.preventDefault();\n if (url().trim()) {\n actions.setLink({ href: url().trim() });\n setShowLinkDialog(false);\n }\n };\n\n return (\n <div class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-xs p-4\">\n <div class=\"w-full max-w-sm rounded-lg border border-border bg-card p-4 shadow-lg text-card-foreground\">\n <h3 class=\"text-sm font-semibold mb-3\">Insert Hyperlink</h3>\n <form onSubmit={handleApply} class=\"flex flex-col gap-3\">\n <input\n type=\"url\"\n placeholder=\"https://example.com\"\n value={url()}\n onInput={(e) => setUrl(e.currentTarget.value)}\n autofocus\n class=\"w-full h-8 px-2.5 text-xs rounded-md border border-border bg-background text-foreground focus:outline-hidden focus:ring-1 focus:ring-primary\"\n />\n <div class=\"flex items-center justify-end gap-2\">\n <button\n type=\"button\"\n class=\"px-2.5 py-1 text-xs rounded-md border border-border hover:bg-muted cursor-pointer\"\n onClick={() => setShowLinkDialog(false)}\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n class=\"px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground font-medium hover:bg-primary/90 cursor-pointer\"\n >\n Apply Link\n </button>\n </div>\n </form>\n </div>\n </div>\n );\n};\n",
|
|
67
|
+
"type": "registry:ui"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"path": "ui/rich-text-editor/markdown.ts",
|
|
71
|
+
"content": "/**\n * Clean bidirectional HTML <-> Markdown serializer for Nikala Rich Text Editor.\n */\n\nexport function htmlToMarkdown(html: string): string {\n if (!html) return \"\";\n\n let md = html;\n\n // Replace headings\n md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, \"# $1\\n\\n\");\n md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, \"## $1\\n\\n\");\n md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, \"### $1\\n\\n\");\n md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, \"#### $1\\n\\n\");\n\n // Replace blockquotes\n md = md.replace(/<blockquote[^>]*>[\\s\\S]*?<p>(.*?)<\\/p>[\\s\\S]*?<\\/blockquote>/gi, \"> $1\\n\\n\");\n md = md.replace(/<blockquote[^>]*>(.*?)<\\/blockquote>/gi, \"> $1\\n\\n\");\n\n // Replace code blocks\n md = md.replace(/<pre[^>]*><code[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi, \"```\\n$1\\n```\\n\\n\");\n\n // Replace inline formatting\n md = md.replace(/<strong>(.*?)<\\/strong>/gi, \"**$1**\");\n md = md.replace(/<b>(.*?)<\\/b>/gi, \"**$1**\");\n md = md.replace(/<em>(.*?)<\\/em>/gi, \"*$1*\");\n md = md.replace(/<i>(.*?)<\\/i>/gi, \"*$1*\");\n md = md.replace(/<s>(.*?)<\\/s>/gi, \"~~$1~~\");\n md = md.replace(/<del>(.*?)<\\/del>/gi, \"~~$1~~\");\n md = md.replace(/<code>(.*?)<\\/code>/gi, \"`$1`\");\n md = md.replace(/<mark[^>]*>(.*?)<\\/mark>/gi, \"==$1==\");\n\n // Replace task list items\n md = md.replace(/<li[^>]*data-checked=\"true\"[^>]*>[\\s\\S]*?<label>[\\s\\S]*?<\\/label>[\\s\\S]*?<div>[\\s\\S]*?<p>(.*?)<\\/p>[\\s\\S]*?<\\/div><\\/li>/gi, \"- [x] $1\\n\");\n md = md.replace(/<li[^>]*data-checked=\"false\"[^>]*>[\\s\\S]*?<label>[\\s\\S]*?<\\/label>[\\s\\S]*?<div>[\\s\\S]*?<p>(.*?)<\\/p>[\\s\\S]*?<\\/div><\\/li>/gi, \"- [ ] $1\\n\");\n md = md.replace(/<li[^>]*data-checked=\"true\"[^>]*>[\\s\\S]*?<label>[\\s\\S]*?<\\/label>[\\s\\S]*?<div>(.*?)<\\/div><\\/li>/gi, \"- [x] $1\\n\");\n md = md.replace(/<li[^>]*data-checked=\"false\"[^>]*>[\\s\\S]*?<label>[\\s\\S]*?<\\/label>[\\s\\S]*?<div>(.*?)<\\/div><\\/li>/gi, \"- [ ] $1\\n\");\n md = md.replace(/<li[^>]*data-checked=\"true\"[^>]*>([\\s\\S]*?)<\\/li>/gi, \"- [x] $1\\n\");\n md = md.replace(/<li[^>]*data-checked=\"false\"[^>]*>([\\s\\S]*?)<\\/li>/gi, \"- [ ] $1\\n\");\n\n // Replace ordered list items inside <ol>\n md = md.replace(/<ol[^>]*>([\\s\\S]*?)<\\/ol>/gi, (match, listContent) => {\n let index = 1;\n return listContent.replace(/<li[^>]*>[\\s\\S]*?<p>(.*?)<\\/p>[\\s\\S]*?<\\/li>/gi, () => `${index++}. $1\\n`)\n .replace(/<li[^>]*>(.*?)<\\/li>/gi, () => `${index++}. $1\\n`) + \"\\n\";\n });\n\n // Replace unordered list items inside <ul>\n md = md.replace(/<ul(?:(?!data-type=\"taskList\")[^>])*>([\\s\\S]*?)<\\/ul>/gi, (match, listContent) => {\n return listContent.replace(/<li[^>]*>[\\s\\S]*?<p>(.*?)<\\/p>[\\s\\S]*?<\\/li>/gi, \"- $1\\n\")\n .replace(/<li[^>]*>(.*?)<\\/li>/gi, \"- $1\\n\") + \"\\n\";\n });\n\n // Replace links\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi, \"[$2]($1)\");\n\n // Replace images\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*alt=\"([^\"]*)\"[^>]*>/gi, \"\");\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*>/gi, \"\");\n\n // Replace paragraphs and line breaks\n md = md.replace(/<p[^>]*>(.*?)<\\/p>/gi, \"$1\\n\\n\");\n md = md.replace(/<br\\s*[\\/]?>/gi, \"\\n\");\n md = md.replace(/<hr\\s*[\\/]?>/gi, \"---\\n\\n\");\n\n // Strip remaining HTML tags securely (iterative loop prevents bypasses)\n let prev: string;\n do {\n prev = md;\n md = md.replace(/<[^>]*>/g, \"\");\n } while (md !== prev);\n\n // Clean multiple extra line breaks\n md = md.replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n return md;\n}\n\nexport function markdownToHtml(md: string): string {\n if (!md) return \"\";\n\n let processed = md;\n\n // Headings\n processed = processed.replace(/^### (.*$)/gim, \"<h3>$1</h3>\");\n processed = processed.replace(/^## (.*$)/gim, \"<h2>$1</h2>\");\n processed = processed.replace(/^# (.*$)/gim, \"<h1>$1</h1>\");\n\n // Code blocks\n processed = processed.replace(/```([\\s\\S]*?)```/gim, \"<pre><code>$1</code></pre>\");\n\n // Blockquotes\n processed = processed.replace(/^\\> (.*$)/gim, \"<blockquote><p>$1</p></blockquote>\");\n\n // Bold, Italic, Strikethrough, Inline Code, Highlight\n processed = processed.replace(/\\*\\*(.*?)\\*\\*/gim, \"<strong>$1</strong>\");\n processed = processed.replace(/\\*(.*?)\\*/gim, \"<em>$1</em>\");\n processed = processed.replace(/~~(.*?)~~/gim, \"<s>$1</s>\");\n processed = processed.replace(/`([^`]+)`/gim, \"<code>$1</code>\");\n processed = processed.replace(/==([^=]+)==/gim, \"<mark>$1</mark>\");\n\n // Links & Images\n processed = processed.replace(/!\\[(.*?)\\]\\((.*?)\\)/gim, '<img src=\"$2\" alt=\"$1\" />');\n processed = processed.replace(/\\[(.*?)\\]\\((.*?)\\)/gim, '<a href=\"$2\" target=\"_blank\">$1</a>');\n\n // Group consecutive Task Checklist items (- [x] or - [ ])\n processed = processed.replace(/(?:^|\\n)((?:- \\[[ xX]\\] .*(?:\\n|$))+)/g, (match, block) => {\n const items = block\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((line: string) => {\n const checked = /^- \\[[xX]\\]/.test(line.trim());\n const text = line.trim().replace(/^- \\[[ xX]\\]\\s*/, \"\");\n return `<li data-checked=\"${checked}\" data-type=\"taskItem\"><label><input type=\"checkbox\"${checked ? ' checked=\"checked\"' : \"\"}><span></span></label><div><p>${text}</p></div></li>`;\n })\n .join(\"\");\n return `\\n<ul data-type=\"taskList\">${items}</ul>\\n`;\n });\n\n // Group consecutive Bullet List items (- or *)\n processed = processed.replace(/(?:^|\\n)((?:(?:-|\\*) (?!\\[[ xX]\\]).*(?:\\n|$))+)/g, (match, block) => {\n const items = block\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((line: string) => {\n const text = line.trim().replace(/^(?:-|\\*)\\s*/, \"\");\n return `<li><p>${text}</p></li>`;\n })\n .join(\"\");\n return `\\n<ul>${items}</ul>\\n`;\n });\n\n // Group consecutive Numbered List items (1. 2. 3.)\n processed = processed.replace(/(?:^|\\n)((?:^\\d+\\. .*(?:\\n|$))+)/gm, (match, block) => {\n const items = block\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((line: string) => {\n const text = line.trim().replace(/^\\d+\\.\\s*/, \"\");\n return `<li><p>${text}</p></li>`;\n })\n .join(\"\");\n return `\\n<ol>${items}</ol>\\n`;\n });\n\n // Paragraphs for remaining text blocks\n const finalHtml = processed\n .split(/\\n\\n+/)\n .map((block) => {\n const trimmed = block.trim();\n if (!trimmed) return \"\";\n if (\n trimmed.startsWith(\"<h\") ||\n trimmed.startsWith(\"<pre\") ||\n trimmed.startsWith(\"<blockquote\") ||\n trimmed.startsWith(\"<ul\") ||\n trimmed.startsWith(\"<ol\") ||\n trimmed.startsWith(\"<table\")\n ) {\n return trimmed;\n }\n return `<p>${trimmed.replace(/\\n/g, \"<br />\")}</p>`;\n })\n .join(\"\\n\");\n\n return finalHtml;\n}\n",
|
|
72
|
+
"type": "registry:ui"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"path": "ui/rich-text-editor/slash-command.tsx",
|
|
76
|
+
"content": "import {\n createSignal,\n createEffect,\n onCleanup,\n Show,\n For,\n type Component,\n} from \"solid-js\";\nimport { Portal } from \"solid-js/web\";\nimport { useRichTextEditor } from \"./editor.js\";\nimport {\n Heading1,\n Heading2,\n Heading3,\n List,\n ListOrdered,\n ListTodo,\n Quote,\n Table as TableIcon,\n Sparkles,\n Minus,\n} from \"lucide-solid\";\n\nexport interface SlashCommandItem {\n title: string;\n description: string;\n icon: Component<{ class?: string }>;\n action: () => void;\n}\n\nexport const EditorSlashCommand: Component = () => {\n const { actions, container, isFullscreen } = useRichTextEditor();\n const [isOpen, setIsOpen] = createSignal(false);\n const [selectedIndex, setSelectedIndex] = createSignal(0);\n const [query, setQuery] = createSignal(\"\");\n const [position, setPosition] = createSignal({ top: 0, left: 0 });\n\n const itemRefs: HTMLElement[] = [];\n\n const items: SlashCommandItem[] = [\n {\n title: \"Heading 1\",\n description: \"Big section heading\",\n icon: Heading1,\n action: () => actions.setHeading(1),\n },\n {\n title: \"Heading 2\",\n description: \"Medium section heading\",\n icon: Heading2,\n action: () => actions.setHeading(2),\n },\n {\n title: \"Heading 3\",\n description: \"Small section heading\",\n icon: Heading3,\n action: () => actions.setHeading(3),\n },\n {\n title: \"Bullet List\",\n description: \"Create a simple bulleted list\",\n icon: List,\n action: actions.toggleBulletList,\n },\n {\n title: \"Numbered List\",\n description: \"Create a numbered sequence\",\n icon: ListOrdered,\n action: actions.toggleOrderedList,\n },\n {\n title: \"Task Checklist\",\n description: \"Track tasks with interactive checkboxes\",\n icon: ListTodo,\n action: actions.toggleTaskList,\n },\n {\n title: \"Blockquote\",\n description: \"Capture a notable quote or callout\",\n icon: Quote,\n action: actions.toggleBlockquote,\n },\n {\n title: \"Code Block\",\n description: \"Display code with monospace font\",\n icon: Sparkles,\n action: actions.toggleCodeBlock,\n },\n {\n title: \"Table\",\n description: \"Insert a 3x3 editable data table\",\n icon: TableIcon,\n action: () => actions.insertTable({ rows: 3, cols: 3, withHeaderRow: true }),\n },\n {\n title: \"Divider\",\n description: \"Separate content with a horizontal line\",\n icon: Minus,\n action: actions.insertHorizontalRule,\n },\n ];\n\n const filteredItems = () =>\n items.filter(\n (item) =>\n item.title.toLowerCase().includes(query().toLowerCase()) ||\n item.description.toLowerCase().includes(query().toLowerCase())\n );\n\n // Auto-scroll active item into view\n createEffect(() => {\n if (!isOpen()) return;\n const idx = selectedIndex();\n const el = itemRefs[idx];\n if (el) {\n el.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n }\n });\n\n const executeItem = (item: SlashCommandItem) => {\n const ed = actions.editor();\n if (ed) {\n const { from } = ed.state.selection;\n const textBefore = ed.state.doc.textBetween(Math.max(0, from - 20), from, \"\\n\");\n const slashIndex = textBefore.lastIndexOf(\"/\");\n if (slashIndex !== -1) {\n const deleteCount = textBefore.length - slashIndex;\n ed.commands.deleteRange({ from: from - deleteCount, to: from });\n }\n }\n item.action();\n setIsOpen(false);\n setQuery(\"\");\n };\n\n createEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleEditorUpdate = () => {\n const ed = actions.editor();\n const containerEl = container();\n if (!ed || !ed.isFocused || !ed.isEditable) {\n setIsOpen(false);\n return;\n }\n\n const { from, empty } = ed.state.selection;\n if (!empty) {\n setIsOpen(false);\n return;\n }\n\n const textBefore = ed.state.doc.textBetween(Math.max(0, from - 25), from, \"\\n\");\n const match = textBefore.match(/(?:^|\\s)\\/([a-zA-Z0-9]*)$/);\n\n if (match) {\n const matchedQuery = match[1] || \"\";\n setQuery(matchedQuery);\n setSelectedIndex(0);\n\n try {\n const coords = ed.view.coordsAtPos(from);\n const menuHeight = 280;\n const menuWidth = 288;\n\n if (isFullscreen() && containerEl) {\n // Fullscreen mode: position relative to container\n const containerRect = containerEl.getBoundingClientRect();\n const spaceBelow = containerRect.bottom - coords.bottom;\n let top: number;\n\n if (spaceBelow < menuHeight && coords.top - containerRect.top > menuHeight) {\n // Flip upwards if not enough room below\n top = coords.top - containerRect.top - menuHeight - 6;\n } else {\n // Open downwards\n top = coords.bottom - containerRect.top + 6;\n }\n\n const left = Math.max(12, Math.min(containerRect.width - menuWidth - 12, coords.left - containerRect.left));\n\n setPosition({ top, left });\n setIsOpen(true);\n } else {\n // Normal mode: viewport fixed coordinates to prevent any container overflow clipping\n const spaceBelow = window.innerHeight - coords.bottom;\n let top: number;\n\n if (spaceBelow < menuHeight && coords.top > menuHeight) {\n // Flip upwards\n top = coords.top - menuHeight - 6;\n } else {\n // Open downwards\n top = coords.bottom + 6;\n }\n\n const left = Math.max(12, Math.min(window.innerWidth - menuWidth - 12, coords.left));\n\n setPosition({ top, left });\n setIsOpen(true);\n }\n } catch {\n setIsOpen(false);\n }\n } else {\n setIsOpen(false);\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!isOpen()) return;\n\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setSelectedIndex((prev) => (prev + 1) % filteredItems().length);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setSelectedIndex((prev) => (prev - 1 + filteredItems().length) % filteredItems().length);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const current = filteredItems()[selectedIndex()];\n if (current) executeItem(current);\n } else if (e.key === \"Escape\") {\n setIsOpen(false);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown, true);\n document.addEventListener(\"selectionchange\", handleEditorUpdate);\n document.addEventListener(\"input\", handleEditorUpdate);\n\n onCleanup(() => {\n window.removeEventListener(\"keydown\", handleKeyDown, true);\n document.removeEventListener(\"selectionchange\", handleEditorUpdate);\n document.removeEventListener(\"input\", handleEditorUpdate);\n });\n });\n\n const portalTarget = () => {\n if (typeof document === \"undefined\") return undefined;\n return isFullscreen() ? container() || document.body : document.body;\n };\n\n return (\n <Show when={isOpen() && filteredItems().length > 0}>\n <Portal mount={portalTarget()}>\n <div\n class={`${\n isFullscreen() ? \"absolute\" : \"fixed\"\n } z-[999999] w-72 rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl p-1.5 max-h-72 overflow-y-auto animate-in fade-in zoom-in-95 pointer-events-auto`}\n style={{\n top: `${position().top}px`,\n left: `${position().left}px`,\n }}\n >\n <div class=\"px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider border-b border-border/60 mb-1\">\n Slash Commands\n </div>\n <For each={filteredItems()}>\n {(item, index) => {\n const Icon = item.icon;\n const isSelected = () => index() === selectedIndex();\n return (\n <button\n ref={(el) => (itemRefs[index()] = el)}\n type=\"button\"\n class={`flex items-center justify-between w-full px-2 py-2 rounded-md text-left text-xs transition-colors cursor-pointer ${\n isSelected()\n ? \"bg-accent/80 text-accent-foreground font-semibold ring-1 ring-border shadow-xs\"\n : \"text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n }`}\n onClick={() => executeItem(item)}\n onMouseEnter={() => setSelectedIndex(index())}\n >\n <div class=\"flex items-center gap-2.5 truncate\">\n <div\n class={`flex items-center justify-center h-6 w-6 rounded-md shrink-0 border ${\n isSelected()\n ? \"bg-background text-foreground border-border shadow-2xs\"\n : \"bg-muted text-muted-foreground border-transparent\"\n }`}\n >\n <Icon class=\"h-3.5 w-3.5\" />\n </div>\n <div class=\"flex flex-col truncate\">\n <span class=\"text-foreground text-xs\">{item.title}</span>\n <span class=\"text-[10px] text-muted-foreground truncate\">{item.description}</span>\n </div>\n </div>\n <Show when={isSelected()}>\n <span class=\"text-[10px] text-muted-foreground font-mono ml-1\">↵</span>\n </Show>\n </button>\n );\n }}\n </For>\n </div>\n </Portal>\n </Show>\n );\n};\n",
|
|
77
|
+
"type": "registry:ui"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"path": "ui/rich-text-editor/table-controls.tsx",
|
|
81
|
+
"content": "import { Show, type Component } from \"solid-js\";\nimport { useRichTextEditor } from \"./editor.js\";\nimport { Plus, Minus, Trash2 } from \"lucide-solid\";\n\nexport const EditorTableControls: Component = () => {\n const { actions, showTableMenu, setShowTableMenu } = useRichTextEditor();\n\n return (\n <Show when={showTableMenu()}>\n <div class=\"absolute top-full left-0 mt-1 z-20 flex flex-col gap-1 p-2 bg-popover text-popover-foreground border border-border rounded-md shadow-md min-w-[160px]\">\n <Show\n when={actions.isActive(\"table\")}\n fallback={\n <button\n type=\"button\"\n class=\"flex items-center gap-2 px-2 py-1.5 text-xs text-left rounded hover:bg-muted cursor-pointer transition-colors\"\n onClick={() => {\n actions.insertTable({ rows: 3, cols: 3, withHeaderRow: true });\n setShowTableMenu(false);\n }}\n >\n <Plus class=\"h-3.5 w-3.5\" /> Insert 3x3 Table\n </button>\n }\n >\n <button\n type=\"button\"\n class=\"flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer transition-colors\"\n onClick={() => actions.editor()?.chain().focus().addRowAfter().run()}\n >\n <Plus class=\"h-3.5 w-3.5\" /> Add Row Below\n </button>\n <button\n type=\"button\"\n class=\"flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer transition-colors\"\n onClick={() => actions.editor()?.chain().focus().addColumnAfter().run()}\n >\n <Plus class=\"h-3.5 w-3.5\" /> Add Column Right\n </button>\n <button\n type=\"button\"\n class=\"flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer text-destructive transition-colors\"\n onClick={() => actions.editor()?.chain().focus().deleteRow().run()}\n >\n <Minus class=\"h-3.5 w-3.5\" /> Delete Row\n </button>\n <button\n type=\"button\"\n class=\"flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer text-destructive transition-colors\"\n onClick={() => actions.editor()?.chain().focus().deleteColumn().run()}\n >\n <Minus class=\"h-3.5 w-3.5\" /> Delete Column\n </button>\n <button\n type=\"button\"\n class=\"flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer text-destructive font-medium border-t border-border mt-1 pt-1.5 transition-colors\"\n onClick={() => {\n actions.editor()?.chain().focus().deleteTable().run();\n setShowTableMenu(false);\n }}\n >\n <Trash2 class=\"h-3.5 w-3.5\" /> Delete Table\n </button>\n </Show>\n </div>\n </Show>\n );\n};\n",
|
|
82
|
+
"type": "registry:ui"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"path": "ui/rich-text-editor/toolbar.tsx",
|
|
86
|
+
"content": "import {\n createSignal,\n splitProps,\n Show,\n For,\n onCleanup,\n createEffect,\n type Component,\n type ComponentProps,\n} from \"solid-js\";\nimport { useRichTextEditor } from \"./editor.js\";\nimport { EditorTableControls } from \"./table-controls.js\";\nimport { cn } from \"@/lib/cn\";\nimport {\n Bold,\n Italic,\n Underline as UnderlineIcon,\n Strikethrough,\n Code,\n Type,\n Heading1,\n Heading2,\n Heading3,\n List,\n ListOrdered,\n ListTodo,\n Quote,\n Table as TableIcon,\n Link as LinkIcon,\n Unlink,\n Image as ImageIcon,\n Undo,\n Redo,\n Maximize2,\n Minimize2,\n AlignLeft,\n AlignCenter,\n AlignRight,\n Highlighter,\n Sparkles,\n RemoveFormatting,\n FileCode2,\n Eye,\n ChevronDown,\n} from \"lucide-solid\";\n\nexport const HIGHLIGHT_PALETTE = [\n { name: \"Yellow\", value: \"#eab308\" },\n { name: \"Amber\", value: \"#f59e0b\" },\n { name: \"Orange\", value: \"#f97316\" },\n { name: \"Red\", value: \"#ef4444\" },\n { name: \"Rose\", value: \"#f43f5e\" },\n { name: \"Pink\", value: \"#ec4899\" },\n { name: \"Purple\", value: \"#a855f7\" },\n { name: \"Violet\", value: \"#8b5cf6\" },\n { name: \"Indigo\", value: \"#6366f1\" },\n { name: \"Blue\", value: \"#3b82f6\" },\n { name: \"Cyan\", value: \"#06b6d4\" },\n { name: \"Teal\", value: \"#14b8a6\" },\n { name: \"Emerald\", value: \"#10b981\" },\n { name: \"Green\", value: \"#22c55e\" },\n { name: \"Lime\", value: \"#84cc16\" },\n];\n\nexport interface ToolbarButtonProps extends ComponentProps<\"button\"> {\n isActive?: boolean;\n tooltip?: string;\n}\n\nexport const ToolbarButton: Component<ToolbarButtonProps> = (props) => {\n const [local, others] = splitProps(props, [\"class\", \"isActive\", \"tooltip\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n title={local.tooltip}\n class={cn(\n \"inline-flex items-center justify-center h-8 w-8 rounded-md text-xs font-medium transition-colors cursor-pointer shrink-0 select-none\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring\",\n local.isActive &&\n \"bg-primary text-primary-foreground font-semibold shadow-xs hover:bg-primary/90 hover:text-primary-foreground\",\n local.class\n )}\n {...others}\n >\n {local.children}\n </button>\n );\n};\n\nexport const EditorToolbar: Component<ComponentProps<\"div\">> = (props) => {\n const [local, others] = splitProps(props, [\"class\"]);\n const {\n actions,\n isFullscreen,\n toggleFullscreen,\n viewMode,\n toggleViewMode,\n setShowLinkDialog,\n setShowImageDialog,\n setShowTableMenu,\n } = useRichTextEditor();\n\n const [showTypography, setShowTypography] = createSignal(false);\n const [showAlign, setShowAlign] = createSignal(false);\n const [showHighlightPalette, setShowHighlightPalette] = createSignal(false);\n\n // Close menus on outside click\n createEffect(() => {\n if (typeof window === \"undefined\") return;\n const handleClickOutside = (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n if (!target.closest(\".editor-popover-trigger\") && !target.closest(\".editor-popover-content\")) {\n setShowTypography(false);\n setShowAlign(false);\n setShowHighlightPalette(false);\n }\n };\n document.addEventListener(\"click\", handleClickOutside);\n onCleanup(() => document.removeEventListener(\"click\", handleClickOutside));\n });\n\n const currentTypographyLabel = () => {\n if (actions.isActive(\"heading\", { level: 1 })) return \"H1\";\n if (actions.isActive(\"heading\", { level: 2 })) return \"H2\";\n if (actions.isActive(\"heading\", { level: 3 })) return \"H3\";\n return \"Normal\";\n };\n\n const CurrentAlignIcon = () => {\n if (actions.isActive({ textAlign: \"center\" })) return AlignCenter;\n if (actions.isActive({ textAlign: \"right\" })) return AlignRight;\n return AlignLeft;\n };\n\n const isEditable = () => actions.editor()?.isEditable ?? true;\n\n return (\n <div\n class={cn(\n \"sticky top-0 z-10 flex flex-wrap items-center gap-1 border-b border-border bg-card/95 backdrop-blur-xs p-1.5 rounded-t-lg\",\n local.class\n )}\n {...others}\n >\n {/* Editing Controls Group (Disabled in Read-Only Mode) */}\n <div\n class=\"flex flex-wrap items-center gap-1\"\n classList={{\n \"opacity-40 pointer-events-none select-none\": !isEditable(),\n }}\n >\n {/* 1. History (Undo / Redo) */}\n <div class=\"flex items-center gap-0.5 pr-1 border-r border-border shrink-0\">\n <ToolbarButton onClick={actions.undo} disabled={!isEditable() || !actions.canUndo()} tooltip=\"Undo (Ctrl+Z)\">\n <Undo class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.redo} disabled={!isEditable() || !actions.canRedo()} tooltip=\"Redo (Ctrl+Y)\">\n <Redo class=\"h-4 w-4\" />\n </ToolbarButton>\n </div>\n\n {/* 2. Compact Typography Dropdown */}\n <div class=\"relative shrink-0 editor-popover-trigger\">\n <button\n type=\"button\"\n onClick={() => setShowTypography((p) => !p)}\n class=\"inline-flex items-center gap-1 h-8 px-2 rounded-md text-xs font-medium text-foreground bg-muted/50 hover:bg-muted transition-colors cursor-pointer border border-border/40\"\n title=\"Typography\"\n >\n <Type class=\"h-3.5 w-3.5 text-muted-foreground\" />\n <span class=\"font-semibold text-xs min-w-[44px] text-left\">{currentTypographyLabel()}</span>\n <ChevronDown class=\"h-3 w-3 text-muted-foreground\" />\n </button>\n\n <Show when={showTypography()}>\n <div class=\"absolute top-full left-0 mt-1 z-30 flex flex-col gap-0.5 p-1 bg-popover text-popover-foreground border border-border rounded-lg shadow-xl min-w-[140px] editor-popover-content animate-in fade-in zoom-in-95\">\n <button\n type=\"button\"\n class={cn(\n \"flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors\",\n !actions.isActive(\"heading\") && \"bg-primary text-primary-foreground font-semibold hover:bg-primary\"\n )}\n onClick={() => {\n actions.setParagraph();\n setShowTypography(false);\n }}\n >\n <Type class=\"h-3.5 w-3.5\" />\n <span>Normal text</span>\n </button>\n <button\n type=\"button\"\n class={cn(\n \"flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors\",\n actions.isActive(\"heading\", { level: 1 }) && \"bg-primary text-primary-foreground font-semibold hover:bg-primary\"\n )}\n onClick={() => {\n actions.setHeading(1);\n setShowTypography(false);\n }}\n >\n <Heading1 class=\"h-3.5 w-3.5\" />\n <span class=\"font-bold\">Heading 1</span>\n </button>\n <button\n type=\"button\"\n class={cn(\n \"flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors\",\n actions.isActive(\"heading\", { level: 2 }) && \"bg-primary text-primary-foreground font-semibold hover:bg-primary\"\n )}\n onClick={() => {\n actions.setHeading(2);\n setShowTypography(false);\n }}\n >\n <Heading2 class=\"h-3.5 w-3.5\" />\n <span class=\"font-semibold\">Heading 2</span>\n </button>\n <button\n type=\"button\"\n class={cn(\n \"flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors\",\n actions.isActive(\"heading\", { level: 3 }) && \"bg-primary text-primary-foreground font-semibold hover:bg-primary\"\n )}\n onClick={() => {\n actions.setHeading(3);\n setShowTypography(false);\n }}\n >\n <Heading3 class=\"h-3.5 w-3.5\" />\n <span class=\"font-medium\">Heading 3</span>\n </button>\n </div>\n </Show>\n </div>\n\n {/* 3. Inline Marks (Bold, Italic, Underline, Strike, Code, Highlight) */}\n <div class=\"flex items-center gap-0.5 px-1 border-r border-border shrink-0\">\n <ToolbarButton onClick={actions.toggleBold} isActive={actions.isActive(\"bold\")} tooltip=\"Bold (Ctrl+B)\">\n <Bold class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleItalic} isActive={actions.isActive(\"italic\")} tooltip=\"Italic (Ctrl+I)\">\n <Italic class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={actions.toggleUnderline}\n isActive={actions.isActive(\"underline\")}\n tooltip=\"Underline (Ctrl+U)\"\n >\n <UnderlineIcon class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleStrike} isActive={actions.isActive(\"strike\")} tooltip=\"Strikethrough\">\n <Strikethrough class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleCode} isActive={actions.isActive(\"code\")} tooltip=\"Inline Code\">\n <Code class=\"h-4 w-4\" />\n </ToolbarButton>\n\n {/* Highlighter Color Palette */}\n <div class=\"relative shrink-0 editor-popover-trigger\">\n <ToolbarButton\n onClick={() => setShowHighlightPalette((p) => !p)}\n isActive={actions.isActive(\"highlight\")}\n tooltip=\"Highlight Color\"\n >\n <Highlighter class=\"h-4 w-4\" />\n </ToolbarButton>\n\n <Show when={showHighlightPalette()}>\n <div class=\"absolute top-full left-0 mt-1 z-30 flex flex-col gap-1.5 p-2 bg-popover text-popover-foreground border border-border rounded-lg shadow-xl min-w-[200px] editor-popover-content animate-in fade-in zoom-in-95\">\n <div class=\"text-[10px] font-semibold text-muted-foreground uppercase tracking-wider\">\n Tailwind Highlight Colors\n </div>\n <div class=\"grid grid-cols-5 gap-1.5\">\n <For each={HIGHLIGHT_PALETTE}>\n {(color) => (\n <button\n type=\"button\"\n title={color.name}\n class=\"h-6 w-6 rounded-md border border-border/60 hover:scale-110 transition-transform cursor-pointer shadow-2xs\"\n style={{ \"background-color\": color.value }}\n onClick={() => {\n actions.toggleHighlight(color.value);\n setShowHighlightPalette(false);\n }}\n />\n )}\n </For>\n </div>\n <button\n type=\"button\"\n class=\"w-full text-center py-1 mt-1 text-xs rounded-md bg-muted hover:bg-muted/80 text-foreground transition-colors cursor-pointer font-medium\"\n onClick={() => {\n actions.toggleHighlight();\n setShowHighlightPalette(false);\n }}\n >\n Clear Highlight\n </button>\n </div>\n </Show>\n </div>\n </div>\n\n {/* 4. Compact Alignment Dropdown */}\n <div class=\"relative shrink-0 editor-popover-trigger\">\n <button\n type=\"button\"\n onClick={() => setShowAlign((p) => !p)}\n class=\"inline-flex items-center justify-center h-8 w-8 rounded-md text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors cursor-pointer\"\n title=\"Text Alignment\"\n >\n {(() => {\n const Icon = CurrentAlignIcon();\n return <Icon class=\"h-4 w-4\" />;\n })()}\n </button>\n\n <Show when={showAlign()}>\n <div class=\"absolute top-full left-0 mt-1 z-30 flex items-center gap-0.5 p-1 bg-popover text-popover-foreground border border-border rounded-lg shadow-xl editor-popover-content animate-in fade-in zoom-in-95\">\n <ToolbarButton\n onClick={() => {\n actions.setTextAlign(\"left\");\n setShowAlign(false);\n }}\n isActive={actions.isActive({ textAlign: \"left\" })}\n tooltip=\"Align Left\"\n >\n <AlignLeft class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n actions.setTextAlign(\"center\");\n setShowAlign(false);\n }}\n isActive={actions.isActive({ textAlign: \"center\" })}\n tooltip=\"Align Center\"\n >\n <AlignCenter class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n actions.setTextAlign(\"right\");\n setShowAlign(false);\n }}\n isActive={actions.isActive({ textAlign: \"right\" })}\n tooltip=\"Align Right\"\n >\n <AlignRight class=\"h-4 w-4\" />\n </ToolbarButton>\n </div>\n </Show>\n </div>\n\n {/* 5. Lists & Blocks */}\n <div class=\"flex items-center gap-0.5 px-1 border-r border-border shrink-0\">\n <ToolbarButton\n onClick={actions.toggleBulletList}\n isActive={actions.isActive(\"bulletList\")}\n tooltip=\"Bullet List\"\n >\n <List class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={actions.toggleOrderedList}\n isActive={actions.isActive(\"orderedList\")}\n tooltip=\"Numbered List\"\n >\n <ListOrdered class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton onClick={actions.toggleTaskList} isActive={actions.isActive(\"taskList\")} tooltip=\"Task Checklist\">\n <ListTodo class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={actions.toggleBlockquote}\n isActive={actions.isActive(\"blockquote\")}\n tooltip=\"Blockquote\"\n >\n <Quote class=\"h-4 w-4\" />\n </ToolbarButton>\n <ToolbarButton\n onClick={actions.toggleCodeBlock}\n isActive={actions.isActive(\"codeBlock\")}\n tooltip=\"Code Block\"\n >\n <Sparkles class=\"h-4 w-4\" />\n </ToolbarButton>\n </div>\n\n {/* 6. Inserts: Link, Image, Table */}\n <div class=\"flex items-center gap-0.5 px-1 shrink-0\">\n <ToolbarButton\n onClick={() => {\n if (actions.isActive(\"link\")) {\n actions.unsetLink();\n } else {\n setShowLinkDialog(true);\n }\n }}\n isActive={actions.isActive(\"link\")}\n tooltip={actions.isActive(\"link\") ? \"Remove Link\" : \"Insert Link\"}\n >\n <Show when={actions.isActive(\"link\")} fallback={<LinkIcon class=\"h-4 w-4\" />}>\n <Unlink class=\"h-4 w-4\" />\n </Show>\n </ToolbarButton>\n\n <ToolbarButton onClick={() => setShowImageDialog(true)} tooltip=\"Insert Image\">\n <ImageIcon class=\"h-4 w-4\" />\n </ToolbarButton>\n\n <div class=\"relative shrink-0\">\n <ToolbarButton\n onClick={() => setShowTableMenu((prev) => !prev)}\n isActive={actions.isActive(\"table\")}\n tooltip=\"Table Controls\"\n >\n <TableIcon class=\"h-4 w-4\" />\n </ToolbarButton>\n\n <EditorTableControls />\n </div>\n </div>\n </div>\n\n {/* 7. Markdown Toggle, Clear Formatting, Fullscreen */}\n <div class=\"flex items-center gap-1 border-l border-border pl-1.5 shrink-0 ml-auto\">\n <ToolbarButton\n onClick={toggleViewMode}\n isActive={viewMode() === \"markdown\"}\n tooltip={viewMode() === \"markdown\" ? \"Switch to Visual Editor\" : \"Switch to Markdown Mode\"}\n >\n <Show when={viewMode() === \"markdown\"} fallback={<FileCode2 class=\"h-4 w-4\" />}>\n <Eye class=\"h-4 w-4\" />\n </Show>\n </ToolbarButton>\n\n <ToolbarButton onClick={actions.clearContent} disabled={!isEditable()} tooltip=\"Clear All Content\">\n <RemoveFormatting class=\"h-4 w-4\" />\n </ToolbarButton>\n\n <Show\n when={isFullscreen()}\n fallback={\n <ToolbarButton onClick={toggleFullscreen} tooltip=\"Fullscreen Mode\">\n <Maximize2 class=\"h-4 w-4\" />\n </ToolbarButton>\n }\n >\n <button\n type=\"button\"\n onClick={toggleFullscreen}\n class=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-semibold bg-primary text-primary-foreground hover:bg-primary/90 transition-colors cursor-pointer shadow-xs ml-0.5 shrink-0\"\n title=\"Exit Fullscreen (Esc)\"\n >\n <Minimize2 class=\"h-3.5 w-3.5\" />\n <span>Exit Fullscreen</span>\n </button>\n </Show>\n </div>\n </div>\n );\n};\n",
|
|
87
|
+
"type": "registry:ui"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"path": "ui/rich-text-editor/use-editor.ts",
|
|
91
|
+
"content": "import {\n createSignal,\n onMount,\n onCleanup,\n type Accessor,\n} from \"solid-js\";\nimport { Editor, type EditorOptions, type Extension } from \"@tiptap/core\";\nimport { getDefaultExtensions } from \"./extensions.js\";\n\nexport interface UseEditorOptions extends Partial<Omit<EditorOptions, \"element\">> {\n placeholder?: string;\n characterLimit?: number;\n}\n\nexport interface UseEditorReturn {\n editor: Accessor<Editor | null>;\n html: Accessor<string>;\n text: Accessor<string>;\n markdown: Accessor<string>;\n isEmpty: Accessor<boolean>;\n characterCount: Accessor<number>;\n wordCount: Accessor<number>;\n canUndo: Accessor<boolean>;\n canRedo: Accessor<boolean>;\n mount: (element: HTMLElement) => void;\n destroy: () => void;\n isActive: (name: string | Record<string, any>, attributes?: Record<string, any>) => boolean;\n toggleBold: () => void;\n toggleItalic: () => void;\n toggleUnderline: () => void;\n toggleStrike: () => void;\n toggleCode: () => void;\n toggleHighlight: (color?: string) => void;\n setHeading: (level: 1 | 2 | 3 | 4 | 5 | 6) => void;\n setParagraph: () => void;\n toggleBulletList: () => void;\n toggleOrderedList: () => void;\n toggleTaskList: () => void;\n toggleBlockquote: () => void;\n toggleCodeBlock: () => void;\n setTextAlign: (alignment: \"left\" | \"center\" | \"right\" | \"justify\") => void;\n setLink: (options: string | { href: string; target?: string }) => void;\n unsetLink: () => void;\n setImage: (options: string | { src: string; alt?: string; title?: string }) => void;\n insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => void;\n insertHorizontalRule: () => void;\n clearContent: () => void;\n setContent: (content: string) => void;\n setEditable: (editable: boolean) => void;\n undo: () => void;\n redo: () => void;\n}\n\nexport function useEditor(options: UseEditorOptions = {}): UseEditorReturn {\n const [editorInstance, setEditorInstance] = createSignal<Editor | null>(null);\n const [html, setHtml] = createSignal<string>(\"\");\n const [text, setText] = createSignal<string>(\"\");\n const [markdown, setMarkdown] = createSignal<string>(\"\");\n const [isEmpty, setIsEmpty] = createSignal<boolean>(true);\n const [characterCount, setCharacterCount] = createSignal<number>(0);\n const [wordCount, setWordCount] = createSignal<number>(0);\n const [canUndo, setCanUndo] = createSignal<boolean>(false);\n const [canRedo, setCanRedo] = createSignal<boolean>(false);\n const [transactionVersion, setTransactionVersion] = createSignal<number>(0);\n\n let targetElement: HTMLElement | null = null;\n\n const updateStats = (ed: Editor) => {\n const currentHtml = ed.getHTML();\n const currentText = ed.getText();\n const currentMd = (ed.storage as any).markdown?.getMarkdown?.() || \"\";\n setHtml(currentHtml);\n setText(currentText);\n setMarkdown(currentMd);\n setIsEmpty(ed.isEmpty);\n setCharacterCount(ed.storage.characterCount?.characters?.() ?? currentText.length);\n setWordCount(ed.storage.characterCount?.words?.() ?? currentText.split(/\\s+/).filter(Boolean).length);\n setCanUndo(ed.can().undo());\n setCanRedo(ed.can().redo());\n setTransactionVersion((v) => v + 1);\n };\n\n const initEditor = (el: HTMLElement) => {\n if (typeof window === \"undefined\") return;\n\n const extensions = [\n ...getDefaultExtensions({\n placeholder: options.placeholder,\n characterLimit: options.characterLimit,\n }),\n ...(options.extensions || []),\n ];\n\n const editor = new Editor({\n element: el,\n content: options.content,\n editable: options.editable ?? true,\n autofocus: options.autofocus ?? false,\n extensions,\n editorProps: {\n attributes: {\n class:\n \"focus:outline-hidden min-h-[220px] w-full max-w-none text-foreground leading-relaxed text-sm \" +\n \"[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:tracking-tight [&_h1]:mt-6 [&_h1]:mb-3 \" +\n \"[&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:mt-5 [&_h2]:mb-2.5 \" +\n \"[&_h3]:text-xl [&_h3]:font-semibold [&_h3]:mt-4 [&_h3]:mb-2 \" +\n \"[&_h4]:text-lg [&_h4]:font-semibold [&_h4]:mt-3 [&_h4]:mb-1.5 \" +\n \"[&_p]:my-2 [&_p]:leading-7 \" +\n \"[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:my-3 [&_ul]:space-y-1 \" +\n \"[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:my-3 [&_ol]:space-y-1 \" +\n \"[&_li]:my-0.5 \" +\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0 [&_ul[data-type=taskList]]:my-3 [&_ul[data-type=taskList]]:space-y-2 \" +\n \"[&_ul[data-type=taskList]_li]:flex [&_ul[data-type=taskList]_li]:items-start [&_ul[data-type=taskList]_li]:gap-2.5 \" +\n \"[&_ul[data-type=taskList]_input[type=checkbox]]:mt-1 [&_ul[data-type=taskList]_input[type=checkbox]]:h-4 [&_ul[data-type=taskList]_input[type=checkbox]]:w-4 [&_ul[data-type=taskList]_input[type=checkbox]]:rounded [&_ul[data-type=taskList]_input[type=checkbox]]:border-border [&_ul[data-type=taskList]_input[type=checkbox]]:accent-primary [&_ul[data-type=taskList]_input[type=checkbox]]:cursor-pointer \" +\n \"[&_mark]:text-foreground [&_mark]:dark:text-background [&_mark]:font-medium [&_mark]:rounded-xs [&_mark]:px-1 [&_mark]:py-0.5 \" +\n \"[&_.is-editor-empty:first-child::before]:content-[attr(data-placeholder)] [&_.is-editor-empty:first-child::before]:text-muted-foreground [&_.is-editor-empty:first-child::before]:float-left [&_.is-editor-empty:first-child::before]:pointer-events-none [&_.is-editor-empty:first-child::before]:h-0\",\n },\n ...options.editorProps,\n },\n onUpdate: (props) => {\n updateStats(props.editor);\n options.onUpdate?.(props);\n },\n onSelectionUpdate: (props) => {\n updateStats(props.editor);\n options.onSelectionUpdate?.(props);\n },\n onTransaction: (props) => {\n updateStats(props.editor);\n options.onTransaction?.(props);\n },\n onFocus: (args) => options.onFocus?.(args),\n onBlur: (args) => options.onBlur?.(args),\n });\n\n setEditorInstance(editor);\n updateStats(editor);\n };\n\n const mount = (element: HTMLElement) => {\n targetElement = element;\n initEditor(element);\n };\n\n const destroy = () => {\n editorInstance()?.destroy();\n setEditorInstance(null);\n };\n\n onCleanup(() => {\n destroy();\n });\n\n const isActive = (name: string | Record<string, any>, attributes?: Record<string, any>) => {\n transactionVersion();\n const ed = editorInstance();\n if (!ed) return false;\n if (typeof name === \"string\") {\n return ed.isActive(name, attributes);\n }\n return ed.isActive(name);\n };\n\n return {\n editor: editorInstance,\n html,\n text,\n markdown,\n isEmpty,\n characterCount,\n wordCount,\n canUndo,\n canRedo,\n mount,\n destroy,\n isActive,\n toggleBold: () => editorInstance()?.chain().focus().toggleBold().run(),\n toggleItalic: () => editorInstance()?.chain().focus().toggleItalic().run(),\n toggleUnderline: () => editorInstance()?.chain().focus().toggleUnderline().run(),\n toggleStrike: () => editorInstance()?.chain().focus().toggleStrike().run(),\n toggleCode: () => editorInstance()?.chain().focus().toggleCode().run(),\n toggleHighlight: (color) => {\n const ed = editorInstance();\n if (!ed) return;\n if (color) {\n ed.chain().focus().toggleHighlight({ color }).run();\n } else {\n ed.chain().focus().toggleHighlight().run();\n }\n },\n setHeading: (level) => editorInstance()?.chain().focus().toggleHeading({ level }).run(),\n setParagraph: () => editorInstance()?.chain().focus().setParagraph().run(),\n toggleBulletList: () => editorInstance()?.chain().focus().toggleBulletList().run(),\n toggleOrderedList: () => editorInstance()?.chain().focus().toggleOrderedList().run(),\n toggleTaskList: () => editorInstance()?.chain().focus().toggleTaskList().run(),\n toggleBlockquote: () => editorInstance()?.chain().focus().toggleBlockquote().run(),\n toggleCodeBlock: () => editorInstance()?.chain().focus().toggleCodeBlock().run(),\n setTextAlign: (alignment) => editorInstance()?.chain().focus().setTextAlign(alignment).run(),\n setLink: (opts) => {\n const ed = editorInstance();\n if (!ed) return;\n if (typeof opts === \"string\") {\n ed.chain().focus().setLink({ href: opts, target: \"_blank\" }).run();\n } else {\n ed.chain().focus().setLink({ href: opts.href, target: opts.target ?? \"_blank\" }).run();\n }\n },\n unsetLink: () => editorInstance()?.chain().focus().unsetLink().run(),\n setImage: (opts) => {\n const ed = editorInstance();\n if (!ed) return;\n if (typeof opts === \"string\") {\n ed.chain().focus().setImage({ src: opts }).run();\n } else {\n ed.chain().focus().setImage(opts).run();\n }\n },\n insertTable: (opts) =>\n editorInstance()?.chain().focus().insertTable({\n rows: opts?.rows ?? 3,\n cols: opts?.cols ?? 3,\n withHeaderRow: opts?.withHeaderRow ?? true,\n }).run(),\n insertHorizontalRule: () => editorInstance()?.chain().focus().setHorizontalRule().run(),\n clearContent: () => editorInstance()?.chain().focus().clearContent().run(),\n setContent: (c) => editorInstance()?.commands.setContent(c),\n setEditable: (editable: boolean) => editorInstance()?.setEditable(editable),\n undo: () => editorInstance()?.chain().focus().undo().run(),\n redo: () => editorInstance()?.chain().focus().redo().run(),\n };\n}\n",
|
|
92
|
+
"type": "registry:ui"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|