@nikala-ui/core 0.11.0 → 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/package.json +43 -2
- package/registry/api-table.json +21 -0
- package/registry/banner.json +1 -1
- package/registry/button.json +1 -1
- package/registry/callout.json +19 -0
- package/registry/checkbox.json +4 -1
- package/registry/code-block.json +26 -0
- package/registry/code-group.json +20 -0
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/component-viewer.json +25 -0
- package/registry/container.json +18 -0
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/create-tiptap-editor.json +34 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/field.json +1 -1
- package/registry/file-tree.json +21 -0
- package/registry/footer.json +1 -1
- package/registry/form-message.json +3 -2
- package/registry/icon-button.json +1 -1
- package/registry/index.json +336 -5
- package/registry/navbar.json +1 -1
- package/registry/navigation-menu.json +1 -1
- package/registry/number-input.json +1 -1
- package/registry/package-manager-tabs.json +24 -0
- package/registry/pager.json +21 -0
- package/registry/popover.json +4 -1
- package/registry/resizable.json +1 -1
- package/registry/rich-text-editor.json +95 -0
- package/registry/scroll-area.json +1 -1
- package/registry/section-heading.json +21 -0
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +1 -1
- package/registry/status.json +1 -1
- package/registry/steps.json +20 -0
- package/registry/switch.json +4 -1
- package/registry/table-of-contents.json +23 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +6 -5
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +1 -1
- package/registry/updater-modal.json +26 -0
- package/src/index.ts +48 -0
- package/src/registry/components/ui/api-table.tsx +117 -0
- package/src/registry/components/ui/banner.tsx +0 -2
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/callout.tsx +137 -0
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/code-block.tsx +326 -0
- package/src/registry/components/ui/code-group.tsx +105 -0
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +110 -106
- package/src/registry/components/ui/component-viewer.tsx +363 -0
- package/src/registry/components/ui/container.tsx +45 -0
- package/src/registry/components/ui/context-menu.tsx +67 -22
- package/src/registry/components/ui/dialog.tsx +42 -32
- package/src/registry/components/ui/dropdown-menu.tsx +69 -31
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/file-tree.tsx +181 -0
- package/src/registry/components/ui/footer.tsx +1 -1
- package/src/registry/components/ui/form-message.tsx +2 -2
- package/src/registry/components/ui/icon-button.tsx +1 -1
- package/src/registry/components/ui/navbar.tsx +18 -11
- package/src/registry/components/ui/navigation-menu.tsx +2 -2
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
- package/src/registry/components/ui/pager.tsx +102 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
- package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
- package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
- package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
- package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
- package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
- package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
- package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
- package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
- package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
- package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
- package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
- package/src/registry/components/ui/scroll-area.tsx +12 -2
- package/src/registry/components/ui/section-heading.tsx +108 -0
- package/src/registry/components/ui/select.tsx +16 -19
- package/src/registry/components/ui/sheet.tsx +58 -53
- package/src/registry/components/ui/sidebar.tsx +4 -4
- package/src/registry/components/ui/status.tsx +7 -5
- package/src/registry/components/ui/steps.tsx +198 -0
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table-of-contents.tsx +131 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +175 -139
- package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
- package/src/registry/components/ui/titlebar.tsx +468 -0
- package/src/registry/components/ui/toggle-group.tsx +1 -1
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/metadata.ts +167 -6
- package/src/registry/providers/theme-provider.tsx +16 -8
- package/src/registry/providers/theme-script.tsx +29 -9
- package/src/registry/providers/theme-transitions.ts +81 -50
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pager",
|
|
3
|
+
"title": "Pager",
|
|
4
|
+
"description": "Previous and next article navigation links with card previews for documentation and blog layouts.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"card"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/pager.tsx",
|
|
17
|
+
"content": "import { splitProps, Show, children, type Component, type JSX } from \"solid-js\";\nimport { ChevronLeft, ChevronRight } from \"lucide-solid\";\nimport {\n Card,\n CardHeader,\n CardTitle,\n CardDescription,\n} from \"@/components/ui/card\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface PagerItem {\n title: string;\n href: string;\n}\n\nexport interface PagerLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n title: string;\n href: string;\n type: \"prev\" | \"next\";\n class?: string;\n}\n\n/**\n * Individual Next or Previous page navigation card.\n */\nexport const PagerLink: Component<PagerLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"title\", \"href\", \"type\", \"class\"]);\n\n const isNext = () => local.type === \"next\";\n\n return (\n <a\n href={local.href}\n class={cn(\n \"group block rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n isNext() && \"sm:col-start-2\",\n local.class\n )}\n {...rest}\n >\n <Card class=\"h-full transition-all group-hover:bg-accent/40 group-hover:shadow-2xs\">\n <CardHeader class={cn(\"p-4 flex flex-col space-y-1.5\", isNext() && \"items-end text-right\")}>\n <CardDescription class=\"flex items-center gap-1 text-xs font-medium text-muted-foreground\">\n <Show when={!isNext()}>\n <ChevronLeft class=\"size-3.5 transition-transform group-hover:-translate-x-0.5\" />\n <span>Previous</span>\n </Show>\n <Show when={isNext()}>\n <span>Next</span>\n <ChevronRight class=\"size-3.5 transition-transform group-hover:translate-x-0.5\" />\n </Show>\n </CardDescription>\n <CardTitle class=\"text-sm font-semibold tracking-tight group-hover:text-primary transition-colors line-clamp-1\">\n {local.title}\n </CardTitle>\n </CardHeader>\n </Card>\n </a>\n );\n};\n\nexport interface PagerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Previous documentation page item */\n prev?: PagerItem | null;\n /** Next documentation page item */\n next?: PagerItem | null;\n class?: string;\n}\n\n/**\n * Documentation Next / Previous article navigation block composed of Nikala UI Card primitives.\n */\nexport const Pager: Component<PagerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"prev\", \"next\", \"class\", \"children\"]);\n const resolved = children(() => local.children);\n\n return (\n <div\n class={cn(\"grid grid-cols-1 gap-4 sm:grid-cols-2 mt-10 pt-6 border-t border-border\", local.class)}\n {...rest}\n >\n <Show\n when={resolved()}\n fallback={\n <>\n <Show\n when={local.prev}\n fallback={<span class=\"hidden sm:block\" />}\n >\n <PagerLink type=\"prev\" title={local.prev!.title} href={local.prev!.href} />\n </Show>\n <Show when={local.next}>\n <PagerLink type=\"next\" title={local.next!.title} href={local.next!.href} />\n </Show>\n </>\n }\n >\n {resolved()}\n </Show>\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/registry/popover.json
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
"@kobalte/core",
|
|
10
10
|
"lucide-solid"
|
|
11
11
|
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"create-click-outside"
|
|
14
|
+
],
|
|
12
15
|
"files": [
|
|
13
16
|
{
|
|
14
17
|
"path": "ui/popover.tsx",
|
|
15
|
-
"content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Popover as KobaltePopover } from \"@kobalte/core/popover\";\nimport { createClickOutside } from \"
|
|
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};",
|
|
16
19
|
"type": "registry:ui"
|
|
17
20
|
}
|
|
18
21
|
]
|
package/registry/resizable.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
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 \"
|
|
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
18
|
"type": "registry:ui"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -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
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "ui/scroll-area.tsx",
|
|
17
|
-
"content": "import {\n createSignal,\n createEffect,\n onCleanup,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createScrollPosition
|
|
17
|
+
"content": "import {\n createSignal,\n createEffect,\n onCleanup,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createScrollPosition } from \"@/hooks/create-scroll-position\";\nimport { createElementSize } from \"@/hooks/create-resize-observer\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ScrollAreaProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"vertical\" | \"horizontal\" | \"both\";\n scrollHideDelay?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ScrollArea: Component<ScrollAreaProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"scrollHideDelay\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation || \"vertical\";\n let viewportRef: HTMLDivElement | undefined;\n let verticalTrackRef: HTMLDivElement | undefined;\n let horizontalTrackRef: HTMLDivElement | undefined;\n\n const scrollPos = createScrollPosition({\n target: () => viewportRef,\n });\n\n const viewportSize = createElementSize(() => viewportRef);\n\n const [thumbHeight, setThumbHeight] = createSignal(0);\n const [thumbTop, setThumbTop] = createSignal(0);\n const [thumbWidth, setThumbWidth] = createSignal(0);\n const [thumbLeft, setThumbLeft] = createSignal(0);\n const [isDragging, setIsDragging] = createSignal(false);\n\n const updateThumbMetrics = () => {\n if (!viewportRef) return;\n\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n\n // Track available lengths (considering 4px track padding)\n const trackHeight = verticalTrackRef ? verticalTrackRef.clientHeight - 4 : clientHeight - 4;\n const trackWidth = horizontalTrackRef ? horizontalTrackRef.clientWidth - 4 : clientWidth - 4;\n\n // Vertical thumb metrics\n if (scrollHeight > clientHeight && clientHeight > 0) {\n const vRatio = clientHeight / scrollHeight;\n const calculatedHeight = Math.max(vRatio * trackHeight, 20);\n const maxTop = trackHeight - calculatedHeight;\n const topPct = scrollPos.y() / (scrollHeight - clientHeight);\n setThumbHeight(calculatedHeight);\n setThumbTop(topPct * maxTop);\n } else {\n setThumbHeight(0);\n }\n\n // Horizontal thumb metrics\n if (scrollWidth > clientWidth && clientWidth > 0) {\n const hRatio = clientWidth / scrollWidth;\n const calculatedWidth = Math.max(hRatio * trackWidth, 20);\n const maxLeft = trackWidth - calculatedWidth;\n const leftPct = scrollPos.x() / (scrollWidth - clientWidth);\n setThumbWidth(calculatedWidth);\n setThumbLeft(leftPct * maxLeft);\n } else {\n setThumbWidth(0);\n }\n };\n\n createEffect(() => {\n viewportSize.width();\n viewportSize.height();\n scrollPos.x();\n scrollPos.y();\n updateThumbMetrics();\n });\n\n // Enable mouse drag on Vertical Thumb\n const handleVerticalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !verticalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startY = e.clientY;\n const startScrollTop = viewportRef.scrollTop;\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const trackHeight = verticalTrackRef.clientHeight - 4;\n\n const maxScrollTop = scrollHeight - clientHeight;\n const maxThumbTop = trackHeight - thumbHeight();\n const ratio = maxThumbTop > 0 ? maxScrollTop / maxThumbTop : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaY = moveEvent.clientY - startY;\n viewportRef!.scrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + deltaY * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\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 // Enable mouse drag on Horizontal Thumb\n const handleHorizontalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !horizontalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startX = e.clientX;\n const startScrollLeft = viewportRef.scrollLeft;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n const trackWidth = horizontalTrackRef.clientWidth - 4;\n\n const maxScrollLeft = scrollWidth - clientWidth;\n const maxThumbLeft = trackWidth - thumbWidth();\n const ratio = maxThumbLeft > 0 ? maxScrollLeft / maxThumbLeft : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaX = moveEvent.clientX - startX;\n viewportRef!.scrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + deltaX * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\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 // Convert vertical mouse wheel into horizontal scroll proportionally\n const handleWheel = (e: WheelEvent) => {\n if (orientation() === \"horizontal\" && viewportRef) {\n if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {\n e.preventDefault();\n viewportRef.scrollLeft += e.deltaY;\n }\n }\n };\n\n return (\n <div\n class={cn(\"relative overflow-hidden group/scroll-area\", local.class)}\n onWheel={handleWheel}\n {...rest}\n >\n {/* Scroll Viewport Container */}\n <div\n ref={viewportRef}\n class=\"h-full w-full max-h-[inherit] overflow-auto scrollbar-none rounded-[inherit]\"\n style={{\n \"scrollbar-width\": \"none\",\n \"-ms-overflow-style\": \"none\",\n }}\n >\n {local.children}\n </div>\n\n {/* Vertical Scrollbar Track & Thumb */}\n {(orientation() === \"vertical\" || orientation() === \"both\") && thumbHeight() > 0 && (\n <div\n ref={verticalTrackRef}\n class={cn(\n \"absolute right-0 top-0 bottom-0 w-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"w-1.5 rounded-lg bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n height: `${thumbHeight()}px`,\n transform: `translateY(${thumbTop()}px)`,\n }}\n onPointerDown={handleVerticalThumbPointerDown}\n />\n </div>\n )}\n\n {/* Horizontal Scrollbar Track & Thumb */}\n {(orientation() === \"horizontal\" || orientation() === \"both\") && thumbWidth() > 0 && (\n <div\n ref={horizontalTrackRef}\n class={cn(\n \"absolute bottom-0 left-0 right-0 h-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"h-1.5 rounded-lg bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n width: `${thumbWidth()}px`,\n transform: `translateX(${thumbLeft()}px)`,\n }}\n onPointerDown={handleHorizontalThumbPointerDown}\n />\n </div>\n )}\n </div>\n );\n};\n",
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "section-heading",
|
|
3
|
+
"title": "Section Heading",
|
|
4
|
+
"description": "A reusable heading block with title, optional badge, and description. Supports page and section variants.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"badge"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/section-heading.tsx",
|
|
17
|
+
"content": "import {\n splitProps,\n Show,\n type JSX,\n type Component,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/cn\";\n\nconst headingVariants = cva(\"tracking-tight\", {\n variants: {\n variant: {\n page: \"text-3xl sm:text-4xl font-bold\",\n section: \"text-2xl font-semibold border-b border-border/50 pb-2\",\n compact: \"text-base font-semibold\",\n },\n },\n defaultVariants: {\n variant: \"section\",\n },\n});\n\nconst descriptionVariants = cva(\"text-muted-foreground leading-relaxed\", {\n variants: {\n variant: {\n page: \"text-lg\",\n section: \"text-sm\",\n compact: \"text-xs\",\n },\n },\n defaultVariants: {\n variant: \"section\",\n },\n});\n\nexport interface SectionHeadingProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"title\">,\n VariantProps<typeof headingVariants> {\n /** Heading text content */\n title: string;\n /** Optional badge label displayed next to the title */\n badge?: string;\n /** Badge style variant */\n badgeVariant?: \"default\" | \"secondary\" | \"outline\" | \"destructive\";\n /** Optional subtitle or explanatory note below the title */\n description?: string;\n class?: string;\n}\n\n/**\n * Reusable heading block with title, optional badge, and description.\n * Supports \"page\" (h1), \"section\" (h2 with divider), and \"compact\" (h3) variants.\n */\nexport const SectionHeading: Component<SectionHeadingProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"title\",\n \"badge\",\n \"badgeVariant\",\n \"description\",\n \"variant\",\n \"class\",\n ]);\n\n const v = () => local.variant || \"section\";\n\n return (\n <div class={cn(\"space-y-2\", local.class)} {...rest}>\n <div class={cn(\"flex items-center gap-2\", v() === \"section\" && \"w-full\")}>\n <Show when={v() === \"page\"}>\n <h1 class={headingVariants({ variant: v() })}>\n {local.title}\n </h1>\n </Show>\n\n <Show when={v() === \"section\"}>\n <h2 class={cn(\"w-full\", headingVariants({ variant: v() }))}>\n {local.title}\n </h2>\n </Show>\n\n <Show when={v() === \"compact\"}>\n <h3 class={headingVariants({ variant: v() })}>\n {local.title}\n </h3>\n </Show>\n\n <Show when={local.badge}>\n <Badge\n variant={local.badgeVariant || \"outline\"}\n class={cn(\n \"select-none\",\n v() === \"page\" ? \"text-xs\" : \"font-mono text-[10px] py-0 px-1.5 font-medium\"\n )}\n >\n {local.badge}\n </Badge>\n </Show>\n </div>\n\n <Show when={local.description}>\n <p class={descriptionVariants({ variant: v() })}>\n {local.description}\n </p>\n </Show>\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/registry/select.json
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"@kobalte/core"
|
|
10
10
|
],
|
|
11
11
|
"registryDependencies": [
|
|
12
|
-
"scroll-area"
|
|
12
|
+
"scroll-area",
|
|
13
|
+
"create-click-outside"
|
|
13
14
|
],
|
|
14
15
|
"files": [
|
|
15
16
|
{
|
|
16
17
|
"path": "ui/select.tsx",
|
|
17
|
-
"content": "import { splitProps, type
|
|
18
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@/hooks/create-click-outside\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectRootProps<Option, OptGroup, T> & {\n class?: string;\n };\n\n/**\n * Root Select component built on top of Kobalte headless primitives.\n */\nexport const Select = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectRootProps<Option, OptGroup, T>>\n) => {\n const [local, rest] = splitProps(props as SelectRootProps, [\"class\"]);\n\n return <SelectPrimitive.Root class={cn(\"relative w-full\", local.class)} {...(rest as any)} />;\n};\n\nexport type SelectTriggerProps<T extends ValidComponent = \"button\"> =\n SelectPrimitive.SelectTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Trigger button opening the Select options list.\n */\nexport const SelectTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, SelectTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectTriggerProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Trigger\n class={cn(\n \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-muted px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <SelectPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 opacity-50 transition-transform\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nexport type SelectValueProps<Option, T extends ValidComponent = \"span\"> =\n SelectPrimitive.SelectValueProps<Option, T> & {\n class?: string;\n };\n\n/**\n * Renders the currently selected option text or placeholder.\n */\nexport const SelectValue = <Option = any, T extends ValidComponent = \"span\">(\n props: PolymorphicProps<T, SelectValueProps<Option, T>>\n) => {\n const [local, rest] = splitProps(props as SelectValueProps<Option>, [\"class\"]);\n\n return (\n <SelectPrimitive.Value\n class={cn(\"block truncate\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SelectContentProps<T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled overlay container rendering the listbox options.\n */\nexport const SelectContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectContentProps, [\"class\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.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 \"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 max-h-60\",\n local.class\n )}\n {...rest}\n >\n <ScrollArea class=\"max-h-60 w-full\">\n <SelectPrimitive.Listbox class=\"p-1 outline-none\" />\n </ScrollArea>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nexport type SelectItemProps<T extends ValidComponent = \"li\"> =\n SelectPrimitive.SelectItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item choice inside SelectContent.\n */\nexport const SelectItem = <T extends ValidComponent = \"li\">(\n props: PolymorphicProps<T, SelectItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectItemProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground\",\n local.class\n )}\n {...rest}\n >\n <SelectPrimitive.ItemIndicator class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <svg class=\"h-4 w-4 fill-none stroke-current stroke-2\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </SelectPrimitive.ItemIndicator>\n <SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>\n </SelectPrimitive.Item>\n );\n};",
|
|
18
19
|
"type": "registry:ui"
|
|
19
20
|
}
|
|
20
21
|
]
|
package/registry/sheet.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
|
17
17
|
"path": "ui/sheet.tsx",
|
|
18
|
-
"content": "import { splitProps, type Component, type JSX, type ValidComponent, Show } from \"solid-js\";\nimport * as DialogPrimitive from \"@kobalte/core/dialog\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-solid\";\nimport { ScrollArea } from \"
|
|
18
|
+
"content": "import { splitProps, type Component, type JSX, type ValidComponent, Show } from \"solid-js\";\nimport * as DialogPrimitive from \"@kobalte/core/dialog\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-solid\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\n// Global CSS Keyframe Animations specifically designed for Kobalte animationend DOM events\nconst sheetStyles = `\n@keyframes sheet-slide-in-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-left { from { transform: translateX(0); } to { transform: translateX(-100%); } }\n@keyframes sheet-slide-in-right { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-right { from { transform: translateX(0); } to { transform: translateX(100%); } }\n@keyframes sheet-slide-in-top { from { transform: translateY(-100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-top { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-slide-in-bottom { from { transform: translateY(100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(100%); } }\n@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }\n`;\n\n/**\n * CVA variants for CSS keyframe slide animations across 4 edges.\n */\nexport const sheetVariants = cva(\n \"fixed z-50 gap-4 bg-card p-6 text-card-foreground shadow-lg\",\n {\n variants: {\n side: {\n left: \"fixed top-0 bottom-0 left-0 w-3/4 border-r border-border sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-left_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-left_300ms_ease-in-out]\",\n right: \"fixed top-0 bottom-0 right-0 w-3/4 border-l border-border sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-right_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-right_300ms_ease-in-out]\",\n top: \"fixed top-0 left-0 right-0 border-b border-border data-[expanded]:animate-[sheet-slide-in-top_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-top_300ms_ease-in-out]\",\n bottom: \"fixed bottom-0 left-0 right-0 border-t border-border data-[expanded]:animate-[sheet-slide-in-bottom_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-bottom_300ms_ease-in-out]\",\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n }\n);\n\nexport type SheetRootProps = DialogPrimitive.DialogRootProps;\n\nexport const Sheet: Component<SheetRootProps> = (props) => {\n return <DialogPrimitive.Root {...props} />;\n};\n\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.CloseButton;\n\nexport interface SheetOverlayProps<T extends ValidComponent = \"div\"> {\n /** Whether to apply background blur effect or keep transparent (default: true) */\n blur?: boolean;\n class?: string;\n}\n\n/**\n * Backdrop overlay wrapper with fade-in and fade-out animations.\n */\nexport const SheetOverlay = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetOverlayProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetOverlayProps, [\"class\", \"blur\"]);\n\n return (\n <DialogPrimitive.Overlay\n class={cn(\n \"fixed inset-0 z-50 data-[expanded]:animate-[sheet-fade-in_300ms_ease-in-out] data-[closed]:animate-[sheet-fade-out_300ms_ease-in-out]\",\n local.blur !== false ? \"bg-black/80 backdrop-blur-sm\" : \"bg-transparent\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type SheetContentProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogContentProps<T> &\n VariantProps<typeof sheetVariants> & {\n /** Direction from which the sheet slides out: top, bottom, left, right */\n side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n /** Whether to display the top-right close (X) button (default: true) */\n showCloseButton?: boolean;\n /** Whether clicking outside closes the sheet (default: true) */\n closeOnOutsideClick?: boolean;\n /** Whether to apply background backdrop blur (default: true) */\n blur?: boolean;\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Main sheet container with smooth CSS keyframe slide animations.\n */\nexport const SheetContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetContentProps, [\n \"side\",\n \"showCloseButton\",\n \"closeOnOutsideClick\",\n \"blur\",\n \"class\",\n \"children\",\n \"onPointerDownOutside\",\n \"onInteractOutside\",\n ]);\n\n const side = () => local.side || \"right\";\n\n const handlePointerDownOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onPointerDownOutside === \"function\") {\n local.onPointerDownOutside(e as any);\n }\n };\n\n const handleInteractOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n };\n\n return (\n <DialogPrimitive.Portal>\n <style>{sheetStyles}</style>\n <SheetOverlay blur={local.blur} />\n <DialogPrimitive.Content\n onPointerDownOutside={handlePointerDownOutside}\n onInteractOutside={handleInteractOutside}\n class={cn(sheetVariants({ side: side() }), \"p-0\", local.class)}\n {...(rest as any)}\n >\n <ScrollArea class=\"h-full w-full\">\n <div class=\"p-6 space-y-4\">\n {local.children}\n </div>\n </ScrollArea>\n <Show when={local.showCloseButton !== false}>\n <DialogPrimitive.CloseButton class=\"absolute right-4 top-4 rounded-sm 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 cursor-pointer z-50\">\n <X class=\"h-4 w-4\" />\n <span class=\"sr-only\">Close</span>\n </DialogPrimitive.CloseButton>\n </Show>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n};\n\nexport interface SheetHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetHeader: Component<SheetHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col space-y-2 text-center sm:text-left\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface SheetFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetFooter: Component<SheetFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", local.class)}\n {...rest}\n />\n );\n};\n\nexport type SheetTitleProps<T extends ValidComponent = \"h2\"> =\n DialogPrimitive.DialogTitleProps<T> & {\n class?: string;\n };\n\nexport const SheetTitle = <T extends ValidComponent = \"h2\">(\n props: PolymorphicProps<T, SheetTitleProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetTitleProps, [\"class\"]);\n\n return (\n <DialogPrimitive.Title\n class={cn(\"text-lg font-semibold text-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SheetDescriptionProps<T extends ValidComponent = \"p\"> =\n DialogPrimitive.DialogDescriptionProps<T> & {\n class?: string;\n };\n\nexport const SheetDescription = <T extends ValidComponent = \"p\">(\n props: PolymorphicProps<T, SheetDescriptionProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetDescriptionProps, [\"class\"]);\n\n return (\n <DialogPrimitive.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};",
|
|
19
19
|
"type": "registry:ui"
|
|
20
20
|
}
|
|
21
21
|
]
|