[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

select, rich text editor

Florian (May 10, 2025, 4:27 PM +0200) f7b99868 e9cb5375

+931 -340
+6
.changeset/late-foxes-mix.md
··· 1 + --- 2 + '@fuxui/base': patch 3 + '@fuxui/text': patch 4 + --- 5 + 6 + change select, improve rich text editor
+1 -1
apps/docs/src/app.html
··· 45 45 </head> 46 46 <body 47 47 data-sveltekit-preload-data="hover" 48 - class="bg-base-50 dark:bg-base-950 overflow-x-hidden motion-safe:transition-colors" 48 + class="bg-base-100 dark:bg-base-950 overflow-x-hidden motion-safe:transition-colors" 49 49 > 50 50 <div style="display: contents">%sveltekit.body%</div> 51 51 </body>
+1 -1
apps/docs/src/lib/site-components/Cards.svelte
··· 15 15 class="group relative flex flex-col items-start gap-3 transition-opacity duration-150 hover:opacity-90 md:gap-4" 16 16 > 17 17 <div 18 - class="bg-base-100 dark:bg-base-900/30 border-base-200 dark:border-base-900 pointer-events-none relative h-44 w-full overflow-hidden rounded-2xl border" 18 + class="bg-base-50 dark:bg-base-900/30 border-base-200 dark:border-base-900 pointer-events-none relative h-44 w-full overflow-hidden rounded-2xl border" 19 19 > 20 20 <div 21 21 class={cn(
+20 -21
apps/docs/src/lib/cards/base/CardSelect.svelte
··· 1 1 <div class={'flex min-w-max items-center justify-center gap-4 rounded-full'}> 2 - <div class="flex items-center gap-x-1 text-sm"> 2 + <div 3 + class="bg-base-100 dark:bg-base-900 text-base-900 dark:text-base-50 border-base-200 dark:border-base-800 divide-base-200 dark:divide-base-800 flex w-28 flex-col gap-x-1 divide-y overflow-hidden rounded-2xl border text-sm" 4 + > 3 5 <div 4 6 class={[ 5 - 'inline-flex cursor-pointer items-center justify-center rounded-2xl p-1 px-2 transition-all', 6 - 'hover:text-accent-600 dark:hover:text-accent-400 outline-accent-300', 7 - 'dark:outline-accent-500/30 text-accent-600 dark:text-accent-400', 8 - 'dark:bg-accent-700/10 bg-accent-500/10 outline' 7 + 'bg-accent-500/5 inline-flex cursor-pointer items-center p-1 px-3 transition-all', 8 + 'text-accent-700 dark:text-accent-300' 9 9 ]} 10 10 > 11 + <svg 12 + xmlns="http://www.w3.org/2000/svg" 13 + fill="none" 14 + viewBox="0 0 24 24" 15 + stroke-width="2" 16 + stroke="currentColor" 17 + class="mr-1.5 size-3.5" 18 + > 19 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 20 + </svg> 21 + 11 22 Apple 12 23 </div> 13 24 14 - <div 15 - class={[ 16 - 'inline-flex cursor-pointer items-center justify-center rounded-2xl p-1 px-2 transition-all', 17 - 'text-base-600 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 outline-accent-300', 18 - 'dark:outline-accent-500/30 data-[state=on]:text-accent-600 dark:data-[state=on]:text-accent-400', 19 - 'dark:data-[state=on]:bg-accent-700/10 data-[state=on]:bg-accent-500/10 data-[state=on]:outline' 20 - ]} 21 - > 25 + <div class={['inline-flex cursor-pointer items-center p-1 px-3 transition-all']}> 26 + <div class="mr-1.5 size-3.5"></div> 22 27 Orange 23 28 </div> 24 29 25 - <div 26 - class={[ 27 - 'inline-flex cursor-pointer items-center justify-center rounded-2xl p-1 px-2 transition-all', 28 - 'text-base-600 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 outline-accent-300', 29 - 'dark:outline-accent-500/30 data-[state=on]:text-accent-600 dark:data-[state=on]:text-accent-400', 30 - 'dark:data-[state=on]:bg-accent-700/10 data-[state=on]:bg-accent-500/10 data-[state=on]:outline' 31 - ]} 32 - > 30 + <div class={['inline-flex cursor-pointer items-center p-1 px-3 transition-all']}> 31 + <div class="mr-1.5 size-3.5"></div> 33 32 Banana 34 33 </div> 35 34 </div>
+1 -1
apps/docs/src/lib/cards/base/CardSidebar.svelte
··· 1 1 <div class="absolute inset-0 h-full w-full p-1"> 2 - <div class="bg-base-200 dark:bg-base-900 h-full w-24 rounded-2xl p-2 border border-base-300 dark:border-base-800"> 2 + <div class="bg-base-100 dark:bg-base-900 h-full w-24 rounded-2xl p-2 border border-base-200 dark:border-base-800"> 3 3 <div class="text-base-900 dark:text-base-50 text-sm font-semibold">Menu</div> 4 4 <div class="text-base-900 dark:text-base-50 text-xs font-medium mt-4">Home</div> 5 5 <div class="text-base-900 dark:text-base-50 text-xs font-medium mt-2">Settings</div>
+46
packages/base/src/lib/components/select-2/Select.svelte
··· 1 + <script lang="ts"> 2 + import { Toolbar, type WithoutChildrenOrChild } from 'bits-ui'; 3 + 4 + type Item = 5 + | { 6 + label: string; 7 + value: string; 8 + } 9 + | string; 10 + 11 + let { 12 + ref = $bindable(null), 13 + 14 + class: className, 15 + items = $bindable([]), 16 + selected = $bindable(), 17 + 18 + ...restProps 19 + }: WithoutChildrenOrChild<Toolbar.RootProps> & { 20 + selected?: string; 21 + items: Item[]; 22 + } = $props(); 23 + 24 + if (!selected) { 25 + selected = typeof items[0] === 'string' ? items[0] : items[0].value; 26 + } 27 + </script> 28 + 29 + <Toolbar.Root bind:ref class={className} {...restProps}> 30 + <Toolbar.Group bind:value={selected} type="single" class="flex items-center gap-x-1 text-sm"> 31 + {#each items as item} 32 + <Toolbar.GroupItem 33 + aria-label="align left" 34 + value={typeof item === 'string' ? item : item.value} 35 + class={[ 36 + 'inline-flex cursor-pointer items-center justify-center rounded-2xl p-1 px-2 transition-all', 37 + 'text-base-600 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 outline-accent-300', 38 + 'dark:outline-accent-500/30 data-[state=on]:text-accent-600 dark:data-[state=on]:text-accent-400', 39 + 'dark:data-[state=on]:bg-accent-700/10 data-[state=on]:bg-accent-500/10 data-[state=on]:outline' 40 + ]} 41 + > 42 + {typeof item === 'string' ? item : item.label} 43 + </Toolbar.GroupItem> 44 + {/each} 45 + </Toolbar.Group> 46 + </Toolbar.Root>
+1
packages/base/src/lib/components/select-2/index.ts
··· 1 + export { default as Select } from './Select.svelte';
+81 -39
packages/base/src/lib/components/select/Select.svelte
··· 1 1 <script lang="ts"> 2 - import { Toolbar, type WithoutChildrenOrChild } from 'bits-ui'; 2 + import { cn } from '../../utils'; 3 + import { Select, type WithoutChildren } from 'bits-ui'; 3 4 4 - type Item = 5 - | { 6 - label: string; 7 - value: string; 8 - } 9 - | string; 5 + type Props = WithoutChildren<Select.RootProps> & { 6 + placeholder?: string; 7 + items: { value: string; label: string; disabled?: boolean }[]; 8 + contentProps?: WithoutChildren<Select.ContentProps>; 9 + }; 10 10 11 - let { 12 - ref = $bindable(null), 11 + let { value = $bindable(), items, contentProps, placeholder, ...restProps }: Props = $props(); 13 12 14 - class: className, 15 - items = $bindable([]), 16 - selected = $bindable(), 17 - 18 - ...restProps 19 - }: WithoutChildrenOrChild<Toolbar.RootProps> & { 20 - selected?: string; 21 - items: Item[]; 22 - } = $props(); 23 - 24 - if (!selected) { 25 - selected = typeof items[0] === 'string' ? items[0] : items[0].value; 26 - } 13 + const selectedLabel = $derived(items.find((item) => item.value === value)?.label); 27 14 </script> 28 15 29 - <Toolbar.Root bind:ref class={className} {...restProps}> 30 - <Toolbar.Group bind:value={selected} type="single" class="flex items-center gap-x-1 text-sm"> 31 - {#each items as item} 32 - <Toolbar.GroupItem 33 - aria-label="align left" 34 - value={typeof item === 'string' ? item : item.value} 35 - class={[ 36 - 'inline-flex cursor-pointer items-center justify-center rounded-2xl p-1 px-2 transition-all', 37 - 'text-base-600 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 outline-accent-300', 38 - 'dark:outline-accent-500/30 data-[state=on]:text-accent-600 dark:data-[state=on]:text-accent-400', 39 - 'dark:data-[state=on]:bg-accent-700/10 data-[state=on]:bg-accent-500/10 data-[state=on]:outline' 40 - ]} 16 + <Select.Root bind:value={value as never} {...restProps}> 17 + <Select.Trigger> 18 + <div 19 + class="bg-accent-500/10 border-accent-700/20 text-accent-800 dark:text-accent-300 inline-flex min-w-28 items-center justify-between gap-1 rounded-2xl border px-3 py-1 font-medium" 20 + > 21 + {selectedLabel ? selectedLabel : placeholder} 22 + 23 + <svg 24 + xmlns="http://www.w3.org/2000/svg" 25 + fill="none" 26 + viewBox="0 0 24 24" 27 + stroke-width="2.5" 28 + stroke="currentColor" 29 + class="size-4" 41 30 > 42 - {typeof item === 'string' ? item : item.label} 43 - </Toolbar.GroupItem> 44 - {/each} 45 - </Toolbar.Group> 46 - </Toolbar.Root> 31 + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> 32 + </svg> 33 + </div> 34 + </Select.Trigger> 35 + <Select.Portal> 36 + <Select.Content 37 + {...contentProps} 38 + class={cn( 39 + 'bg-base-50/50 border-base-500/20 overflow-hidden rounded-2xl border shadow-lg backdrop-blur-xl', 40 + 'dark:bg-base-900/50 dark:border-base-500/10', 41 + 'motion-safe:animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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', 42 + contentProps?.class 43 + )} 44 + sideOffset={6} 45 + > 46 + <Select.ScrollUpButton>up</Select.ScrollUpButton> 47 + <Select.Viewport class="divide-base-300 dark:divide-base-800 divide-y text-sm"> 48 + {#each items as { value, label, disabled } (value)} 49 + <Select.Item {value} {label} {disabled}> 50 + {#snippet children({ selected })} 51 + <div 52 + class={cn( 53 + 'text-base-900 dark:text-base-200 group relative isolate flex min-w-28 cursor-pointer items-center gap-1 px-1 py-2 font-medium', 54 + selected ? 'text-accent-700' : '' 55 + )} 56 + > 57 + {#if selected} 58 + <svg 59 + xmlns="http://www.w3.org/2000/svg" 60 + fill="none" 61 + viewBox="0 0 24 24" 62 + stroke-width="2.5" 63 + stroke="currentColor" 64 + class="size-4" 65 + > 66 + <path 67 + stroke-linecap="round" 68 + stroke-linejoin="round" 69 + d="m4.5 12.75 6 6 9-13.5" 70 + /> 71 + </svg> 72 + {:else} 73 + <div class="size-4"></div> 74 + 75 + <div 76 + class="group-hover:bg-base-300/20 dark:group-hover:bg-base-700/20 absolute inset-0 -z-10" 77 + ></div> 78 + {/if} 79 + {label} 80 + </div> 81 + {/snippet} 82 + </Select.Item> 83 + {/each} 84 + </Select.Viewport> 85 + <Select.ScrollDownButton>down</Select.ScrollDownButton> 86 + </Select.Content> 87 + </Select.Portal> 88 + </Select.Root>
+29
packages/base/src/lib/components/select/SelectChildren.svelte
··· 1 + <script lang="ts"> 2 + import { Select, type WithoutChildren } from "bits-ui"; 3 + 4 + type Props = Select.RootProps & { 5 + placeholder?: string; 6 + contentProps?: WithoutChildren<Select.ContentProps>; 7 + }; 8 + 9 + let { 10 + value = $bindable(), 11 + placeholder, 12 + contentProps, 13 + children, 14 + ...props 15 + }: Props = $props(); 16 + </script> 17 + 18 + <Select.Root bind:value={value as never} {...props}> 19 + <Select.Trigger> 20 + {value} 21 + </Select.Trigger> 22 + <Select.Portal> 23 + <Select.Content {...contentProps}> 24 + <Select.Viewport> 25 + {children?.()} 26 + </Select.Viewport> 27 + </Select.Content> 28 + </Select.Portal> 29 + </Select.Root>
+31
packages/base/src/lib/components/select/SelectItem.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '../../utils'; 3 + import { Select } from 'bits-ui'; 4 + 5 + let { value = $bindable(), label, children: myChildren, ...props }: Select.ItemProps = $props(); 6 + 7 + console.log(value); 8 + </script> 9 + 10 + <Select.Item {value} {label} {...props}> 11 + {#snippet children(props)} 12 + {#if props.selected} 13 + <svg 14 + xmlns="http://www.w3.org/2000/svg" 15 + fill="none" 16 + viewBox="0 0 24 24" 17 + stroke-width="1.5" 18 + stroke="currentColor" 19 + class="size-6" 20 + > 21 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 22 + </svg> 23 + {/if} 24 + 25 + {#if myChildren} 26 + {@render myChildren?.(props)} 27 + {:else} 28 + {label} 29 + {/if} 30 + {/snippet} 31 + </Select.Item>
+2
packages/base/src/lib/components/select/index.ts
··· 1 + export { default as SelectChildren } from './SelectChildren.svelte'; 2 + export { default as SelectItem } from './SelectItem.svelte'; 1 3 export { default as Select } from './Select.svelte';
+1 -1
packages/base/src/lib/components/tooltip/tooltip.svelte
··· 35 35 </Tooltip.Trigger> 36 36 37 37 <Tooltip.Portal> 38 - <TooltipContent sideOffset={10}> 38 + <TooltipContent sideOffset={6} side="top"> 39 39 {#if content} 40 40 {@render content()} 41 41 {:else}
+107
packages/text/src/lib/components/rich-text-editor/Icon.svelte
··· 1 + <script lang="ts"> 2 + import type { RichTextTypes } from "."; 3 + 4 + let { 5 + name 6 + }: { 7 + name: RichTextTypes 8 + } = $props(); 9 + </script> 10 + 11 + {#if name === 'paragraph'} 12 + <svg 13 + xmlns="http://www.w3.org/2000/svg" 14 + viewBox="0 0 24 24" 15 + fill="none" 16 + stroke="currentColor" 17 + stroke-width="2" 18 + stroke-linecap="round" 19 + stroke-linejoin="round" 20 + ><path d="M13 4v16" /><path d="M17 4v16" /><path d="M19 4H9.5a4.5 4.5 0 0 0 0 9H13" /></svg 21 + > 22 + {:else if name === 'heading-1'} 23 + <svg 24 + xmlns="http://www.w3.org/2000/svg" 25 + viewBox="0 0 24 24" 26 + fill="none" 27 + stroke="currentColor" 28 + stroke-width="2" 29 + stroke-linecap="round" 30 + stroke-linejoin="round" 31 + ><path d="M4 12h8" /><path d="M4 18V6" /><path d="M12 18V6" /><path d="m17 12 3-2v8" /></svg 32 + > 33 + {:else if name === 'heading-2'} 34 + <svg 35 + xmlns="http://www.w3.org/2000/svg" 36 + viewBox="0 0 24 24" 37 + fill="none" 38 + stroke="currentColor" 39 + stroke-width="2" 40 + stroke-linecap="round" 41 + stroke-linejoin="round" 42 + ><path d="M4 12h8" /><path d="M4 18V6" /><path d="M12 18V6" /><path 43 + d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1" 44 + /></svg 45 + > 46 + {:else if name === 'heading-3'} 47 + <svg 48 + xmlns="http://www.w3.org/2000/svg" 49 + viewBox="0 0 24 24" 50 + fill="none" 51 + stroke="currentColor" 52 + stroke-width="2" 53 + stroke-linecap="round" 54 + stroke-linejoin="round" 55 + ><path d="M4 12h8" /><path d="M4 18V6" /><path d="M12 18V6" /><path 56 + d="M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2" 57 + /><path d="M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2" /></svg 58 + > 59 + {:else if name === 'blockquote'} 60 + <svg 61 + xmlns="http://www.w3.org/2000/svg" 62 + viewBox="0 0 24 24" 63 + fill="none" 64 + stroke="currentColor" 65 + stroke-width="2" 66 + stroke-linecap="round" 67 + stroke-linejoin="round" 68 + ><path d="M17 6H3" /><path d="M21 12H8" /><path d="M21 18H8" /><path d="M3 12v6" /></svg 69 + > 70 + {:else if name === 'code'} 71 + <svg 72 + xmlns="http://www.w3.org/2000/svg" 73 + viewBox="0 0 24 24" 74 + fill="none" 75 + stroke="currentColor" 76 + stroke-width="2" 77 + stroke-linecap="round" 78 + stroke-linejoin="round" 79 + ><polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" /></svg 80 + > 81 + {:else if name === 'bullet-list'} 82 + <svg 83 + xmlns="http://www.w3.org/2000/svg" 84 + viewBox="0 0 24 24" 85 + fill="none" 86 + stroke="currentColor" 87 + stroke-width="2" 88 + stroke-linecap="round" 89 + stroke-linejoin="round" 90 + ><path d="M3 12h.01" /><path d="M3 18h.01" /><path d="M3 6h.01" /><path d="M8 12h13" /><path 91 + d="M8 18h13" 92 + /><path d="M8 6h13" /></svg 93 + > 94 + {:else if name === 'ordered-list'} 95 + <svg 96 + xmlns="http://www.w3.org/2000/svg" 97 + viewBox="0 0 24 24" 98 + fill="none" 99 + stroke="currentColor" 100 + stroke-width="2" 101 + stroke-linecap="round" 102 + stroke-linejoin="round" 103 + ><path d="M10 12h11" /><path d="M10 18h11" /><path d="M10 6h11" /><path d="M4 10h2" /><path 104 + d="M4 6h1v4" 105 + /><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" /></svg 106 + > 107 + {/if}
+84 -212
packages/text/src/lib/components/rich-text-editor/RichTextEditor.svelte
··· 4 4 import StarterKit from '@tiptap/starter-kit'; 5 5 import Placeholder from '@tiptap/extension-placeholder'; 6 6 import Image from '@tiptap/extension-image'; 7 - import { common, createLowlight } from 'lowlight'; 7 + import { all, createLowlight } from 'lowlight'; 8 8 import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; 9 9 import BubbleMenu from '@tiptap/extension-bubble-menu'; 10 - import Strike from '@tiptap/extension-strike'; 11 10 import Underline from '@tiptap/extension-underline'; 12 11 import Link from '@tiptap/extension-link'; 12 + import RichTextEditorMenu from './RichTextEditorMenu.svelte'; 13 + import type { RichTextTypes } from '.'; 14 + import RichTextEditorLinkMenu from './RichTextEditorLinkMenu.svelte'; 13 15 14 16 import './code.css'; 15 - import { Button, Input, Toggle } from '@fuxui/base'; 16 17 17 18 let { 18 19 content = $bindable({}), ··· 26 27 ref?: HTMLDivElement | null; 27 28 } = $props(); 28 29 29 - const lowlight = createLowlight(common); 30 + const lowlight = createLowlight(all); 30 31 31 32 let hasFocus = true; 32 33 33 34 let menu: HTMLElement | null = $state(null); 35 + let menuLink: HTMLElement | null = $state(null); 36 + 37 + let selectedType: RichTextTypes = $state('paragraph'); 34 38 35 39 let isBold = $state(false); 36 40 let isItalic = $state(false); 37 41 let isUnderline = $state(false); 38 42 let isStrikethrough = $state(false); 39 43 let isLink = $state(false); 44 + let isImage = $state(false); 40 45 41 46 onMount(() => { 42 47 if (!ref) return; ··· 44 49 let extensions = [ 45 50 StarterKit.configure({ 46 51 dropcursor: { 47 - class: 'text-base-500' 52 + class: 'text-accent-500/30 rounded-2xl', 53 + width: 2 48 54 }, 49 - codeBlock: false 55 + codeBlock: false, 56 + heading: { 57 + levels: [1, 2, 3] 58 + } 50 59 }), 51 60 Placeholder.configure({ placeholder }), 52 61 // Only use CustomImage, not the standard Image extension ··· 66 75 return ( 67 76 !editor.isActive('image') && 68 77 !editor.view.state.selection.empty && 69 - !editor.isActive('codeBlock') 78 + !editor.isActive('codeBlock') && 79 + !editor.isActive('link') 70 80 ); 71 - } 81 + }, 82 + pluginKey: 'bubble-menu-marks' 72 83 }), 73 - Strike.configure({}), 84 + BubbleMenu.configure({ 85 + element: menuLink, 86 + shouldShow: ({ editor }) => { 87 + // only show if link is selected 88 + return editor.isActive('link') && !editor.view.state.selection.empty; 89 + }, 90 + pluginKey: 'bubble-menu-links' 91 + }), 74 92 Underline.configure({}), 75 93 Link.configure({ 76 94 openOnClick: false, ··· 102 120 isUnderline = ctx.editor.isActive('underline'); 103 121 isStrikethrough = ctx.editor.isActive('strike'); 104 122 isLink = ctx.editor.isActive('link'); 123 + isImage = ctx.editor.isActive('image'); 105 124 106 - if (!isLink) showLinkEdit = false; 125 + if (ctx.editor.isActive('heading', { level: 1 })) { 126 + selectedType = 'heading-1'; 127 + } else if (ctx.editor.isActive('heading', { level: 2 })) { 128 + selectedType = 'heading-2'; 129 + } else if (ctx.editor.isActive('heading', { level: 3 })) { 130 + selectedType = 'heading-3'; 131 + } else if (ctx.editor.isActive('blockquote')) { 132 + selectedType = 'blockquote'; 133 + } else if (ctx.editor.isActive('code')) { 134 + selectedType = 'code'; 135 + } else if (ctx.editor.isActive('bulletList')) { 136 + selectedType = 'bullet-list'; 137 + } else if (ctx.editor.isActive('orderedList')) { 138 + selectedType = 'ordered-list'; 139 + } else { 140 + selectedType = 'paragraph'; 141 + } 107 142 }, 108 143 content 109 144 }); 110 145 111 146 menu?.classList.remove('hidden'); 147 + menuLink?.classList.remove('hidden'); 112 148 }); 113 149 114 150 // Flag to track whether a file is being dragged over the drop area ··· 217 253 218 254 let link = $state(''); 219 255 220 - let showLinkEdit = $state(false); 221 - 222 256 let linkInput: HTMLInputElement | null = $state(null); 257 + 258 + function clickedLink() { 259 + if (isLink) { 260 + //tiptap?.chain().focus().unsetLink().run(); 261 + // get current link 262 + link = editor?.getAttributes('link').href; 263 + 264 + setTimeout(() => { 265 + linkInput?.focus(); 266 + }, 100); 267 + } else { 268 + link = ''; 269 + // set link 270 + editor?.chain().focus().setLink({ href: link }).run(); 271 + 272 + setTimeout(() => { 273 + linkInput?.focus(); 274 + }, 100); 275 + } 276 + } 223 277 </script> 224 278 225 279 <div ··· 232 286 role="region" 233 287 ></div> 234 288 235 - <div 236 - bind:this={menu} 237 - class="menu bg-base-50 dark:bg-base-900 relative hidden w-fit rounded-2xl px-1 py-1 shadow-lg backdrop-blur-sm" 238 - > 239 - {#if !showLinkEdit} 240 - <Toggle 241 - size="sm" 242 - onclick={() => editor?.chain().focus().toggleBold().run()} 243 - bind:pressed={isBold} 244 - > 245 - <svg 246 - xmlns="http://www.w3.org/2000/svg" 247 - viewBox="0 0 24 24" 248 - fill="currentColor" 249 - class="size-6" 250 - > 251 - <path 252 - fill-rule="evenodd" 253 - d="M5.246 3.744a.75.75 0 0 1 .75-.75h7.125a4.875 4.875 0 0 1 3.346 8.422 5.25 5.25 0 0 1-2.97 9.58h-7.5a.75.75 0 0 1-.75-.75V3.744Zm7.125 6.75a2.625 2.625 0 0 0 0-5.25H8.246v5.25h4.125Zm-4.125 2.251v6h4.5a3 3 0 0 0 0-6h-4.5Z" 254 - clip-rule="evenodd" 255 - /> 256 - </svg> 289 + <RichTextEditorMenu 290 + bind:ref={menu} 291 + {editor} 292 + {isBold} 293 + {isItalic} 294 + {isUnderline} 295 + {isStrikethrough} 296 + {isLink} 297 + {isImage} 298 + {clickedLink} 299 + {processImageFile} 300 + bind:selectedType 301 + /> 257 302 258 - <span class="sr-only">Bold</span> 259 - </Toggle> 303 + <RichTextEditorLinkMenu 304 + bind:ref={menuLink} 305 + {editor} 306 + bind:link 307 + /> 260 308 261 - <Toggle 262 - size="sm" 263 - onclick={() => editor?.chain().focus().toggleItalic().run()} 264 - bind:pressed={isItalic} 265 - > 266 - <svg 267 - xmlns="http://www.w3.org/2000/svg" 268 - viewBox="0 0 24 24" 269 - fill="currentColor" 270 - class="size-6" 271 - > 272 - <path 273 - fill-rule="evenodd" 274 - d="M10.497 3.744a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-3.275l-5.357 15.002h2.632a.75.75 0 1 1 0 1.5h-7.5a.75.75 0 1 1 0-1.5h3.275l5.357-15.002h-2.632a.75.75 0 0 1-.75-.75Z" 275 - clip-rule="evenodd" 276 - /> 277 - </svg> 278 - 279 - <span class="sr-only">Italic</span> 280 - </Toggle> 281 - 282 - <Toggle 283 - size="sm" 284 - onclick={() => editor?.chain().focus().toggleUnderline().run()} 285 - bind:pressed={isUnderline} 286 - > 287 - <svg 288 - xmlns="http://www.w3.org/2000/svg" 289 - viewBox="0 0 24 24" 290 - fill="currentColor" 291 - class="size-6" 292 - > 293 - <path 294 - fill-rule="evenodd" 295 - d="M5.995 2.994a.75.75 0 0 1 .75.75v7.5a5.25 5.25 0 1 0 10.5 0v-7.5a.75.75 0 0 1 1.5 0v7.5a6.75 6.75 0 1 1-13.5 0v-7.5a.75.75 0 0 1 .75-.75Zm-3 17.252a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5h-16.5a.75.75 0 0 1-.75-.75Z" 296 - clip-rule="evenodd" 297 - /> 298 - </svg> 299 - 300 - <span class="sr-only">Underline</span> 301 - </Toggle> 302 - 303 - <Toggle 304 - size="sm" 305 - onclick={() => editor?.chain().focus().toggleStrike().run()} 306 - bind:pressed={isStrikethrough} 307 - > 308 - <svg 309 - xmlns="http://www.w3.org/2000/svg" 310 - viewBox="0 0 24 24" 311 - fill="currentColor" 312 - class="size-6" 313 - > 314 - <path 315 - fill-rule="evenodd" 316 - d="M9.657 4.728c-1.086.385-1.766 1.057-1.979 1.85-.214.8.046 1.733.81 2.616.746.862 1.93 1.612 3.388 2.003.07.019.14.037.21.053h8.163a.75.75 0 0 1 0 1.5h-8.24a.66.66 0 0 1-.02 0H3.75a.75.75 0 0 1 0-1.5h4.78a7.108 7.108 0 0 1-1.175-1.074C6.372 9.042 5.849 7.61 6.229 6.19c.377-1.408 1.528-2.38 2.927-2.876 1.402-.497 3.127-.55 4.855-.086A8.937 8.937 0 0 1 16.94 4.6a.75.75 0 0 1-.881 1.215 7.437 7.437 0 0 0-2.436-1.14c-1.473-.394-2.885-.331-3.966.052Zm6.533 9.632a.75.75 0 0 1 1.03.25c.592.974.846 2.094.55 3.2-.378 1.408-1.529 2.38-2.927 2.876-1.402.497-3.127.55-4.855.087-1.712-.46-3.168-1.354-4.134-2.47a.75.75 0 0 1 1.134-.982c.746.862 1.93 1.612 3.388 2.003 1.473.394 2.884.331 3.966-.052 1.085-.384 1.766-1.056 1.978-1.85.169-.628.046-1.33-.381-2.032a.75.75 0 0 1 .25-1.03Z" 317 - clip-rule="evenodd" 318 - /> 319 - </svg> 320 - 321 - <span class="sr-only">Strikethrough</span> 322 - </Toggle> 323 - 324 - <Toggle 325 - size="sm" 326 - onclick={() => { 327 - if (isLink) { 328 - //tiptap?.chain().focus().unsetLink().run(); 329 - // get current link 330 - link = editor?.getAttributes('link').href; 331 - showLinkEdit = true; 332 - 333 - setTimeout(() => { 334 - linkInput?.focus(); 335 - }, 10); 336 - } else { 337 - link = ''; 338 - showLinkEdit = true; 339 - 340 - setTimeout(() => { 341 - linkInput?.focus(); 342 - }, 10); 343 - } 344 - }} 345 - bind:pressed={isLink} 346 - > 347 - <svg 348 - xmlns="http://www.w3.org/2000/svg" 349 - viewBox="0 0 24 24" 350 - fill="currentColor" 351 - class="size-6" 352 - > 353 - <path 354 - fill-rule="evenodd" 355 - d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z" 356 - clip-rule="evenodd" 357 - /> 358 - </svg> 359 - 360 - <span class="sr-only">Link</span> 361 - </Toggle> 362 - {:else} 363 - <div class="flex items-center gap-1"> 364 - <Input 365 - bind:ref={linkInput} 366 - sizeVariant="sm" 367 - bind:value={link} 368 - placeholder="Enter link" 369 - onblur={() => { 370 - if (link === '') { 371 - editor?.chain().focus().unsetLink().run(); 372 - } else { 373 - editor?.chain().focus().setLink({ href: link }).run(); 374 - } 375 - }} 376 - onkeydown={(e) => { 377 - if (e.key === 'Enter') { 378 - if (link === '') { 379 - editor?.chain().focus().unsetLink().run(); 380 - } else { 381 - editor?.chain().focus().setLink({ href: link }).run(); 382 - } 383 - showLinkEdit = false; 384 - } 385 - }} 386 - /> 387 - <Button 388 - size="iconSm" 389 - onclick={() => { 390 - if (link === '') { 391 - editor?.chain().focus().unsetLink().run(); 392 - } else { 393 - editor?.chain().focus().setLink({ href: link }).run(); 394 - } 395 - showLinkEdit = false; 396 - }} 397 - > 398 - <svg 399 - xmlns="http://www.w3.org/2000/svg" 400 - fill="none" 401 - viewBox="0 0 24 24" 402 - stroke-width="1.5" 403 - stroke="currentColor" 404 - class="size-6" 405 - > 406 - <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 407 - </svg> 408 - 409 - <span class="sr-only">save link</span> 410 - </Button> 411 - <Button 412 - size="iconSm" 413 - onclick={() => { 414 - editor?.chain().focus().unsetLink().run(); 415 - showLinkEdit = false; 416 - }} 417 - variant="ghost" 418 - > 419 - <svg 420 - xmlns="http://www.w3.org/2000/svg" 421 - fill="none" 422 - viewBox="0 0 24 24" 423 - stroke-width="1.5" 424 - stroke="currentColor" 425 - > 426 - <path 427 - stroke-linecap="round" 428 - stroke-linejoin="round" 429 - d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" 430 - /> 431 - </svg> 432 - <span class="sr-only">remove link</span> 433 - </Button> 434 - </div> 435 - {/if} 436 - </div> 437 309 438 310 <style> 439 311 :global(.tiptap) {
+91
packages/text/src/lib/components/rich-text-editor/RichTextEditorLinkMenu.svelte
··· 1 + <script lang="ts"> 2 + import { Button, Input } from "@fuxui/base"; 3 + import type { Editor } from "@tiptap/core"; 4 + 5 + let { 6 + editor, 7 + link = $bindable(''), 8 + ref = $bindable(null) 9 + }: { 10 + editor: Editor | null; 11 + link: string; 12 + ref: HTMLElement | null; 13 + } = $props(); 14 + 15 + let linkInput: HTMLInputElement | null = $state(null); 16 + </script> 17 + 18 + <div 19 + bind:this={ref} 20 + class="menu bg-base-50 dark:bg-base-900 relative hidden w-fit rounded-2xl px-1 py-1 shadow-lg backdrop-blur-sm" 21 + > 22 + <div class="flex items-center gap-1"> 23 + <Input 24 + bind:ref={linkInput} 25 + sizeVariant="sm" 26 + bind:value={link} 27 + placeholder="Enter link" 28 + onblur={() => { 29 + if (link === '') { 30 + editor?.chain().focus().unsetLink().run(); 31 + } else { 32 + editor?.chain().focus().setLink({ href: link }).run(); 33 + } 34 + }} 35 + onkeydown={(e: KeyboardEvent) => { 36 + if (e.key === 'Enter') { 37 + if (link === '') { 38 + editor?.chain().focus().unsetLink().run(); 39 + } else { 40 + editor?.chain().focus().setLink({ href: link }).run(); 41 + } 42 + } 43 + }} 44 + /> 45 + <Button 46 + size="iconSm" 47 + onclick={() => { 48 + if (link === '') { 49 + editor?.chain().focus().unsetLink().run(); 50 + } else { 51 + editor?.chain().focus().setLink({ href: link }).run(); 52 + } 53 + }} 54 + > 55 + <svg 56 + xmlns="http://www.w3.org/2000/svg" 57 + fill="none" 58 + viewBox="0 0 24 24" 59 + stroke-width="1.5" 60 + stroke="currentColor" 61 + class="size-6" 62 + > 63 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 64 + </svg> 65 + 66 + <span class="sr-only">save link</span> 67 + </Button> 68 + <Button 69 + size="iconSm" 70 + onclick={() => { 71 + editor?.chain().focus().unsetLink().run(); 72 + }} 73 + variant="ghost" 74 + > 75 + <svg 76 + xmlns="http://www.w3.org/2000/svg" 77 + fill="none" 78 + viewBox="0 0 24 24" 79 + stroke-width="1.5" 80 + stroke="currentColor" 81 + > 82 + <path 83 + stroke-linecap="round" 84 + stroke-linejoin="round" 85 + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" 86 + /> 87 + </svg> 88 + <span class="sr-only">remove link</span> 89 + </Button> 90 + </div> 91 + </div>
+199
packages/text/src/lib/components/rich-text-editor/RichTextEditorMenu.svelte
··· 1 + <script lang="ts"> 2 + import { cn, Toggle, toggleVariants, Tooltip } from '@fuxui/base'; 3 + import type { Editor } from '@tiptap/core'; 4 + import Select from './Select.svelte'; 5 + import type { RichTextTypes } from '.'; 6 + 7 + let { 8 + editor, 9 + isBold, 10 + isImage, 11 + isItalic, 12 + isUnderline, 13 + isStrikethrough, 14 + isLink, 15 + clickedLink, 16 + selectedType = $bindable('paragraph'), 17 + ref = $bindable(null), 18 + processImageFile 19 + }: { 20 + editor: Editor | null; 21 + isBold: boolean; 22 + isImage: boolean; 23 + isItalic: boolean; 24 + isUnderline: boolean; 25 + isStrikethrough: boolean; 26 + isLink: boolean; 27 + clickedLink: () => void; 28 + selectedType: RichTextTypes; 29 + ref: HTMLElement | null; 30 + processImageFile: (file: File, input: HTMLInputElement) => void; 31 + } = $props(); 32 + 33 + $inspect(isBold); 34 + 35 + function handleFileProcess(event: Event) { 36 + const input = event.target as HTMLInputElement; 37 + if (!input.files?.length) return; 38 + const file = input.files[0]; 39 + if (!file || !file.type.startsWith('image/')) return; 40 + processImageFile(file, input); 41 + } 42 + 43 + let fileInput = $state<HTMLInputElement | null>(null); 44 + </script> 45 + 46 + <div 47 + bind:this={ref} 48 + class="bg-base-50 dark:bg-base-900 border-base-500/20 dark:border-base-700/20 relative hidden w-fit rounded-2xl border px-1 py-1 shadow-lg backdrop-blur-sm" 49 + > 50 + <Select 51 + onValueChange={(value) => { 52 + editor?.chain().focus().unsetAllMarks().run(); 53 + 54 + if (value === 'heading-1') { 55 + editor?.chain().focus().setNode('heading', { level: 1 }).run(); 56 + } else if (value === 'heading-2') { 57 + editor?.chain().focus().setNode('heading', { level: 2 }).run(); 58 + } else if (value === 'heading-3') { 59 + editor?.chain().focus().setNode('heading', { level: 3 }).run(); 60 + } else if (value === 'blockquote') { 61 + editor?.chain().focus().setBlockquote().run(); 62 + } else if (value === 'code') { 63 + editor?.chain().focus().setCodeBlock().run(); 64 + } else if (value === 'bullet-list') { 65 + editor?.chain().focus().toggleBulletList().run(); 66 + } else if (value === 'ordered-list') { 67 + editor?.chain().focus().toggleOrderedList().run(); 68 + } else if (value === 'paragraph') { 69 + editor?.chain().focus().setParagraph().run(); 70 + } 71 + }} 72 + type="single" 73 + items={[ 74 + { value: 'paragraph', label: 'Text' }, 75 + { value: 'heading-1', label: 'Heading 1' }, 76 + { value: 'heading-2', label: 'Heading 2' }, 77 + { value: 'heading-3', label: 'Heading 3' }, 78 + { value: 'blockquote', label: 'Blockquote' }, 79 + { value: 'code', label: 'Code Block' }, 80 + { value: 'bullet-list', label: 'Bullet List' }, 81 + { value: 'ordered-list', label: 'Ordered List' } 82 + ]} 83 + bind:value={selectedType} 84 + /> 85 + <!-- <Tooltip withContext text="Bold" delayDuration={0}> 86 + {#snippet child({ props })} --> 87 + <Toggle 88 + size="sm" 89 + onclick={() => editor?.chain().focus().toggleBold().run()} 90 + bind:pressed={() => isBold, (bold) => {}} 91 + > 92 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 93 + <path 94 + fill-rule="evenodd" 95 + d="M5.246 3.744a.75.75 0 0 1 .75-.75h7.125a4.875 4.875 0 0 1 3.346 8.422 5.25 5.25 0 0 1-2.97 9.58h-7.5a.75.75 0 0 1-.75-.75V3.744Zm7.125 6.75a2.625 2.625 0 0 0 0-5.25H8.246v5.25h4.125Zm-4.125 2.251v6h4.5a3 3 0 0 0 0-6h-4.5Z" 96 + clip-rule="evenodd" 97 + /> 98 + </svg> 99 + 100 + <span class="sr-only">Bold</span> 101 + </Toggle> 102 + <!-- {/snippet} 103 + </Tooltip> --> 104 + <Toggle 105 + size="sm" 106 + onclick={() => editor?.chain().focus().toggleItalic().run()} 107 + bind:pressed={() => isItalic, (italic) => {}} 108 + > 109 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 110 + <path 111 + fill-rule="evenodd" 112 + d="M10.497 3.744a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-3.275l-5.357 15.002h2.632a.75.75 0 1 1 0 1.5h-7.5a.75.75 0 1 1 0-1.5h3.275l5.357-15.002h-2.632a.75.75 0 0 1-.75-.75Z" 113 + clip-rule="evenodd" 114 + /> 115 + </svg> 116 + 117 + <span class="sr-only">Italic</span> 118 + </Toggle> 119 + 120 + <Toggle 121 + size="sm" 122 + onclick={() => editor?.chain().focus().toggleUnderline().run()} 123 + bind:pressed={() => isUnderline, (underline) => {}} 124 + > 125 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 126 + <path 127 + fill-rule="evenodd" 128 + d="M5.995 2.994a.75.75 0 0 1 .75.75v7.5a5.25 5.25 0 1 0 10.5 0v-7.5a.75.75 0 0 1 1.5 0v7.5a6.75 6.75 0 1 1-13.5 0v-7.5a.75.75 0 0 1 .75-.75Zm-3 17.252a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5h-16.5a.75.75 0 0 1-.75-.75Z" 129 + clip-rule="evenodd" 130 + /> 131 + </svg> 132 + 133 + <span class="sr-only">Underline</span> 134 + </Toggle> 135 + 136 + <Toggle 137 + size="sm" 138 + onclick={() => editor?.chain().focus().toggleStrike().run()} 139 + bind:pressed={() => isStrikethrough, (strikethrough) => {}} 140 + > 141 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 142 + <path 143 + fill-rule="evenodd" 144 + d="M9.657 4.728c-1.086.385-1.766 1.057-1.979 1.85-.214.8.046 1.733.81 2.616.746.862 1.93 1.612 3.388 2.003.07.019.14.037.21.053h8.163a.75.75 0 0 1 0 1.5h-8.24a.66.66 0 0 1-.02 0H3.75a.75.75 0 0 1 0-1.5h4.78a7.108 7.108 0 0 1-1.175-1.074C6.372 9.042 5.849 7.61 6.229 6.19c.377-1.408 1.528-2.38 2.927-2.876 1.402-.497 3.127-.55 4.855-.086A8.937 8.937 0 0 1 16.94 4.6a.75.75 0 0 1-.881 1.215 7.437 7.437 0 0 0-2.436-1.14c-1.473-.394-2.885-.331-3.966.052Zm6.533 9.632a.75.75 0 0 1 1.03.25c.592.974.846 2.094.55 3.2-.378 1.408-1.529 2.38-2.927 2.876-1.402.497-3.127.55-4.855.087-1.712-.46-3.168-1.354-4.134-2.47a.75.75 0 0 1 1.134-.982c.746.862 1.93 1.612 3.388 2.003 1.473.394 2.884.331 3.966-.052 1.085-.384 1.766-1.056 1.978-1.85.169-.628.046-1.33-.381-2.032a.75.75 0 0 1 .25-1.03Z" 145 + clip-rule="evenodd" 146 + /> 147 + </svg> 148 + 149 + <span class="sr-only">Strikethrough</span> 150 + </Toggle> 151 + 152 + <Toggle 153 + size="sm" 154 + onclick={() => { 155 + clickedLink(); 156 + }} 157 + bind:pressed={() => isLink, (link) => {}} 158 + > 159 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 160 + <path 161 + fill-rule="evenodd" 162 + d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z" 163 + clip-rule="evenodd" 164 + /> 165 + </svg> 166 + 167 + <span class="sr-only">Link</span> 168 + </Toggle> 169 + 170 + <!-- <Toggle 171 + size="sm" 172 + onclick={() => { 173 + fileInput?.click(); 174 + }} 175 + bind:pressed={() => isImage, (image) => {}} 176 + > 177 + <svg 178 + xmlns="http://www.w3.org/2000/svg" 179 + viewBox="0 0 24 24" 180 + fill="none" 181 + stroke="currentColor" 182 + stroke-width="2" 183 + stroke-linecap="round" 184 + stroke-linejoin="round" 185 + ><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path 186 + d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" 187 + /></svg 188 + > 189 + </Toggle> --> 190 + 191 + <input 192 + type="file" 193 + accept="image/*" 194 + class="hidden" 195 + onchange={handleFileProcess} 196 + tabindex="-1" 197 + bind:this={fileInput} 198 + /> 199 + </div>
+72
packages/text/src/lib/components/rich-text-editor/Select.svelte
··· 1 + <script lang="ts"> 2 + import { Button, cn, toggleVariants } from '@fuxui/base'; 3 + import { Select, type WithoutChildren } from 'bits-ui'; 4 + import Icon from './Icon.svelte'; 5 + 6 + type Props = WithoutChildren<Select.RootProps> & { 7 + placeholder?: string; 8 + items: { value: string; label: string; disabled?: boolean }[]; 9 + contentProps?: WithoutChildren<Select.ContentProps>; 10 + }; 11 + 12 + let { value = $bindable(), items, contentProps, placeholder, ...restProps }: Props = $props(); 13 + </script> 14 + 15 + <Select.Root bind:value={value as never} {...restProps}> 16 + <Select.Trigger> 17 + {#snippet child({ props })} 18 + <button {...props} class={cn(toggleVariants({ size: 'sm' }), 'gap-1')}> 19 + {#if value} 20 + <Icon name={value as any} /> 21 + {:else} 22 + <span class="size-3.5">?</span> 23 + {/if} 24 + 25 + <svg 26 + xmlns="http://www.w3.org/2000/svg" 27 + fill="none" 28 + viewBox="0 0 24 24" 29 + stroke-width="1.5" 30 + stroke="currentColor" 31 + class="!size-2.5" 32 + > 33 + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> 34 + </svg> 35 + </button> 36 + {/snippet} 37 + </Select.Trigger> 38 + <Select.Portal> 39 + <Select.Content 40 + {...contentProps} 41 + class={cn( 42 + 'bg-base-50/50 border-base-500/20 overflow-hidden rounded-2xl border shadow-lg backdrop-blur-xl', 43 + 'dark:bg-base-900/50 dark:border-base-500/10', 44 + 'motion-safe:animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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', 45 + contentProps?.class 46 + )} 47 + sideOffset={6} 48 + > 49 + <Select.ScrollUpButton>up</Select.ScrollUpButton> 50 + <Select.Viewport class="divide-base-300/30 dark:divide-base-800 divide-y text-sm"> 51 + {#each items as { value, label, disabled } (value)} 52 + <Select.Item {value} {label} {disabled}> 53 + {#snippet children({ selected })} 54 + <div 55 + class={cn( 56 + 'text-base-900 dark:text-base-200 group relative isolate flex min-w-28 cursor-pointer items-center gap-3 px-3 py-2 font-medium [&_svg]:size-3.5', 57 + selected 58 + ? 'text-accent-700 dark:text-accent-400 bg-accent-500/10' 59 + : 'hover:bg-accent-500/10' 60 + )} 61 + > 62 + <Icon name={value as any} /> 63 + {label} 64 + </div> 65 + {/snippet} 66 + </Select.Item> 67 + {/each} 68 + </Select.Viewport> 69 + <Select.ScrollDownButton>down</Select.ScrollDownButton> 70 + </Select.Content> 71 + </Select.Portal> 72 + </Select.Root>
+130 -62
packages/text/src/lib/components/rich-text-editor/code.css
··· 1 1 /* Basic editor styles */ 2 2 .tiptap { 3 - :first-child { 4 - margin-top: 0; 5 - } 3 + :first-child { 4 + margin-top: 0; 5 + } 6 6 7 - pre { 8 - background: var(--black); 9 - border-radius: 0.5rem; 10 - color: var(--white); 11 - font-family: 'JetBrainsMono', monospace; 12 - margin: 1.5rem 0; 13 - padding: 0.75rem 1rem; 7 + pre { 8 + background: var(--color-base-200); 9 + border-radius: 1rem; 10 + color: var(--color-base-900); 11 + font-family: 'JetBrainsMono', monospace; 12 + border: 1px solid var(--color-base-300); 13 + margin: 1.5rem 0; 14 + padding: 0.75rem 1rem; 14 15 15 - code { 16 - background: none; 17 - color: inherit; 18 - font-size: 0.8rem; 19 - padding: 0; 20 - } 16 + code { 17 + background: none; 18 + color: inherit; 19 + font-size: 0.8rem; 20 + padding: 0; 21 + } 21 22 22 - /* Code styling */ 23 - .hljs-comment, 24 - .hljs-quote { 25 - color: #616161; 26 - } 23 + /* Code styling */ 24 + .hljs-comment, 25 + .hljs-quote { 26 + color: var(--color-base-500); 27 + } 27 28 28 - .hljs-variable, 29 - .hljs-template-variable, 30 - .hljs-attribute, 31 - .hljs-tag, 32 - .hljs-regexp, 33 - .hljs-link, 34 - .hljs-name, 35 - .hljs-selector-id, 36 - .hljs-selector-class { 37 - color: #f98181; 38 - } 29 + .hljs-variable, 30 + .hljs-template-variable, 31 + .hljs-attribute, 32 + .hljs-tag, 33 + .hljs-regexp, 34 + .hljs-link, 35 + .hljs-name, 36 + .hljs-selector-id, 37 + .hljs-selector-class { 38 + color: var(--color-accent-600); 39 + } 39 40 40 - .hljs-number, 41 - .hljs-meta, 42 - .hljs-built_in, 43 - .hljs-builtin-name, 44 - .hljs-literal, 45 - .hljs-type, 46 - .hljs-params { 47 - color: #fbbc88; 48 - } 41 + .hljs-number, 42 + .hljs-meta, 43 + .hljs-built_in, 44 + .hljs-builtin-name, 45 + .hljs-literal, 46 + .hljs-type, 47 + .hljs-params { 48 + color: oklch(from var(--color-accent-600) l c calc(h + 30)); 49 + } 49 50 50 - .hljs-string, 51 - .hljs-symbol, 52 - .hljs-bullet { 53 - color: #b9f18d; 54 - } 51 + .hljs-string, 52 + .hljs-symbol, 53 + .hljs-bullet { 54 + color: oklch(from var(--color-accent-600) l c calc(h + 60)); 55 + } 55 56 56 - .hljs-title, 57 - .hljs-section { 58 - color: #faf594; 59 - } 57 + .hljs-title, 58 + .hljs-section { 59 + color: oklch(from var(--color-accent-700) l c calc(h + 15)); 60 + } 60 61 61 - .hljs-keyword, 62 - .hljs-selector-tag { 63 - color: #70cff8; 64 - } 62 + .hljs-keyword, 63 + .hljs-selector-tag { 64 + color: oklch(from var(--color-accent-700) l c calc(h + 45)); 65 + } 65 66 66 - .hljs-emphasis { 67 - font-style: italic; 68 - } 67 + .hljs-emphasis { 68 + font-style: italic; 69 + } 69 70 70 - .hljs-strong { 71 - font-weight: 700; 72 - } 73 - } 74 - } 71 + .hljs-strong { 72 + font-weight: 700; 73 + } 74 + } 75 + } 76 + 77 + .dark .tiptap { 78 + pre { 79 + background: var(--color-base-900); 80 + color: var(--color-base-200); 81 + border: 1px solid var(--color-base-800); 82 + 83 + code { 84 + background: none; 85 + color: inherit; 86 + font-size: 0.8rem; 87 + padding: 0; 88 + } 89 + 90 + /* Code styling */ 91 + .hljs-comment, 92 + .hljs-quote { 93 + color: var(--color-base-400); 94 + } 95 + 96 + .hljs-variable, 97 + .hljs-template-variable, 98 + .hljs-attribute, 99 + .hljs-tag, 100 + .hljs-regexp, 101 + .hljs-link, 102 + .hljs-name, 103 + .hljs-selector-id, 104 + .hljs-selector-class { 105 + color: var(--color-accent-400); 106 + } 107 + 108 + .hljs-number, 109 + .hljs-meta, 110 + .hljs-built_in, 111 + .hljs-builtin-name, 112 + .hljs-literal, 113 + .hljs-type, 114 + .hljs-params { 115 + color: oklch(from var(--color-accent-400) l c calc(h + 30)); 116 + } 117 + 118 + .hljs-string, 119 + .hljs-symbol, 120 + .hljs-bullet { 121 + color: oklch(from var(--color-accent-400) l c calc(h + 60)); 122 + } 123 + 124 + .hljs-title, 125 + .hljs-section { 126 + color: oklch(from var(--color-accent-300) l c calc(h + 15)); 127 + } 128 + 129 + .hljs-keyword, 130 + .hljs-selector-tag { 131 + color: oklch(from var(--color-accent-300) l c calc(h + 45)); 132 + } 133 + 134 + .hljs-emphasis { 135 + font-style: italic; 136 + } 137 + 138 + .hljs-strong { 139 + font-weight: 700; 140 + } 141 + } 142 + }
+10
packages/text/src/lib/components/rich-text-editor/index.ts
··· 1 1 export { default as RichTextEditor } from './RichTextEditor.svelte'; 2 + 3 + export type RichTextTypes = 4 + | 'paragraph' 5 + | 'heading-1' 6 + | 'heading-2' 7 + | 'heading-3' 8 + | 'blockquote' 9 + | 'code' 10 + | 'bullet-list' 11 + | 'ordered-list';
+9 -1
apps/docs/src/routes/(main)/components/base/select/Example.svelte
··· 4 4 let selected = $state('Banana'); 5 5 </script> 6 6 7 - <Select bind:selected items={['Apple', 'Orange', 'Banana']} /> 7 + <Select 8 + bind:value={selected} 9 + type="single" 10 + items={[ 11 + { value: 'Apple', label: 'Apple' }, 12 + { value: 'Orange', label: 'Orange' }, 13 + { value: 'Banana', label: 'Banana' } 14 + ]} 15 + />
+9 -1
apps/docs/src/routes/(main)/components/base/select/Select.md
··· 17 17 let selected = $state('Apple'); 18 18 </script> 19 19 20 - <Select bind:selected items={['Apple', 'Orange', 'Banana']} /> 20 + <Select 21 + bind:value={selected} 22 + type="single" 23 + items={[ 24 + { value: 'Apple', label: 'Apple' }, 25 + { value: 'Orange', label: 'Orange' }, 26 + { value: 'Banana', label: 'Banana' } 27 + ]} 28 + /> 21 29 ```