@devalok/shilp-sutra 0.47.0 → 0.48.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/AGENTS.md +36 -12
- package/BREAKING.json +46 -0
- package/MIGRATION.md +35 -0
- package/dist/_chunks/emoji-suggestion.js +124 -0
- package/dist/_chunks/emoji-suggestion.js.map +1 -0
- package/dist/_chunks/emoji.js +1115 -0
- package/dist/_chunks/emoji.js.map +1 -0
- package/dist/_chunks/tiptap.js.map +1 -1
- package/dist/composed/emoji-picker.d.ts +19 -9
- package/dist/composed/emoji-picker.d.ts.map +1 -1
- package/dist/composed/emoji-picker.js +87 -68
- package/dist/composed/emoji-picker.js.map +1 -1
- package/dist/composed/extensions/emoji-data.d.ts +1 -14
- package/dist/composed/extensions/emoji-data.d.ts.map +1 -1
- package/dist/composed/extensions/emoji-node.d.ts +0 -3
- package/dist/composed/extensions/emoji-node.d.ts.map +1 -1
- package/dist/composed/extensions/emoji-node.js +9 -34
- package/dist/composed/extensions/emoji-node.js.map +1 -1
- package/dist/composed/extensions/emoji-suggestion.d.ts +2 -2
- package/dist/composed/extensions/emoji-suggestion.d.ts.map +1 -1
- package/dist/composed/extensions/emoji-suggestion.js +2 -116
- package/dist/composed/rich-chat-input.d.ts.map +1 -1
- package/dist/composed/rich-chat-input.js +554 -575
- package/dist/composed/rich-chat-input.js.map +1 -1
- package/dist/composed/rich-text-editor.d.ts.map +1 -1
- package/dist/composed/rich-text-editor.js +167 -190
- package/dist/composed/rich-text-editor.js.map +1 -1
- package/docs/components/composed/emoji-picker.md +19 -17
- package/docs/components/composed/rich-text-editor.md +1 -1
- package/docs/components/ui/table-row-link.md +1 -1
- package/docs/recipes/index.md +1 -1
- package/docs/recipes/install-astro.md +15 -12
- package/docs/recipes/install-next-app-router.md +20 -12
- package/docs/recipes/install-next-pages.md +3 -1
- package/docs/recipes/install-remix.md +16 -13
- package/docs/recipes/install-tanstack-start.md +19 -12
- package/docs/recipes/install-vite.md +18 -15
- package/docs/recipes/troubleshoot.md +7 -8
- package/llms.txt +2 -2
- package/mcp-manifest.json +29 -66
- package/package.json +6 -9
- package/skill/SKILL.md +1 -1
- package/skill/references/components.md +2 -2
- package/skill/references/setup-astro.md +15 -12
- package/skill/references/setup-next-app-router.md +20 -12
- package/skill/references/setup-next-pages.md +3 -1
- package/skill/references/setup-remix.md +16 -13
- package/skill/references/setup-tanstack-start.md +19 -12
- package/skill/references/setup-vite.md +18 -15
- package/skill/references/troubleshoot.md +7 -8
- package/dist/_chunks/emoji-data.js +0 -45
- package/dist/_chunks/emoji-data.js.map +0 -1
- package/dist/composed/extensions/emoji-suggestion.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../src/composed/rich-text-editor.tsx"],"sourcesContent":["'use client'\n\nimport {\n IconAlignCenter,\n IconAlignLeft,\n IconAlignRight,\n IconArrowBackUp,\n IconArrowForwardUp,\n IconBlockquote,\n IconBold,\n IconCode,\n IconH2,\n IconH3,\n IconHighlight,\n IconItalic,\n IconLineDashed,\n IconLink,\n IconList,\n IconListCheck,\n IconListNumbers,\n IconMoodSmile,\n IconPaperclip,\n IconPhoto,\n IconStrikethrough,\n IconUnderline,\n} from '@tabler/icons-react'\nimport Highlight from '@tiptap/extension-highlight'\nimport Image from '@tiptap/extension-image'\nimport { ListKit } from '@tiptap/extension-list'\nimport Mention from '@tiptap/extension-mention'\nimport TextAlign from '@tiptap/extension-text-align'\nimport { Placeholder } from '@tiptap/extensions'\nimport { type Editor,EditorContent, useEditor, useEditorState } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport * as React from 'react'\n\nimport { useColorMode } from '../hooks/use-color-mode'\nimport { Button } from '../ui/button'\nimport { Icon } from '../ui/icon'\nimport { MENTION_TOKEN_CLASS } from '../ui/lib/mention'\nimport { cn } from '../ui/lib/utils'\nimport type { EmojiSet } from './emoji-picker'\nimport { loadEmojiData, lookupEmoji } from './extensions/emoji-data'\nimport { EmojiNode } from './extensions/emoji-node'\nimport { createEmojiSuggestion } from './extensions/emoji-suggestion'\nimport { FileAttachment } from './extensions/file-attachment'\nimport { createSuggestionRenderer } from './extensions/mention-suggestion'\n\nconst PROSE_CLASSES = [\n 'prose prose-sm max-w-none',\n 'font-body text-ds-md leading-relaxed text-surface-fg',\n '[&_h2]:text-ds-xl [&_h2]:mb-ds-03 [&_h2]:mt-ds-05 [&_h2]:text-surface-fg',\n '[&_h3]:text-ds-base [&_h3]:font-semibold [&_h3]:mb-ds-02b [&_h3]:mt-ds-04 [&_h3]:text-surface-fg',\n '[&_p]:mb-ds-02b [&_p]:text-surface-fg-muted',\n '[&_ul]:ml-ds-05 [&_ul]:list-disc [&_ol]:ml-ds-05 [&_ol]:list-decimal',\n '[&_li]:text-surface-fg-muted',\n '[&_code]:rounded [&_code]:bg-surface-raised [&_code]:px-ds-02b [&_code]:py-ds-01 [&_code]:text-ds-md [&_code]:text-accent-11',\n '[&_pre]:rounded-surface [&_pre]:bg-surface-raised [&_pre]:p-ds-04',\n '[&_strong]:font-semibold [&_strong]:text-surface-fg',\n '[&_blockquote]:border-l-[3px] [&_blockquote]:border-accent-6 [&_blockquote]:pl-ds-04 [&_blockquote]:italic [&_blockquote]:text-surface-fg-subtle',\n '[&_mark]:rounded-xs [&_mark]:bg-warning-3 [&_mark]:px-[2px]',\n '[&_ul[data-type=\"taskList\"]]:ml-0 [&_ul[data-type=\"taskList\"]]:list-none [&_li[data-type=\"taskItem\"]]:flex [&_li[data-type=\"taskItem\"]]:items-start [&_li[data-type=\"taskItem\"]]:gap-ds-02',\n '[&_hr]:my-ds-04 [&_hr]:border-surface-border-strong',\n '[&_a]:text-accent-11 [&_a]:underline [&_a]:decoration-accent-6 hover:[&_a]:decoration-accent-11',\n '[&_img]:max-w-full [&_img]:rounded-control [&_img]:my-ds-03',\n MENTION_TOKEN_CLASS,\n] as const\n\ninterface ToolbarButtonProps {\n onClick: () => void\n isActive?: boolean\n disabled?: boolean\n title: string\n children: React.ReactNode\n}\n\nfunction ToolbarButton({\n onClick,\n isActive = false,\n disabled = false,\n title,\n children,\n}: ToolbarButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n title={title}\n aria-label={title}\n aria-pressed={isActive}\n className={cn(\n 'inline-flex h-ds-xs-plus w-ds-xs-plus items-center justify-center rounded-control transition-colors duration-fast-01 ease-productive-standard',\n 'hover:bg-surface-raised-hover',\n 'disabled:pointer-events-none disabled:opacity-action-disabled',\n isActive\n ? 'bg-surface-raised-hover text-accent-11'\n : 'text-surface-fg-subtle',\n )}\n >\n {children}\n </button>\n )\n}\n\nfunction LinkButton({ editor }: { editor: Editor }) {\n const [showInput, setShowInput] = React.useState(false)\n const [url, setUrl] = React.useState('')\n const inputRef = React.useRef<HTMLInputElement>(null)\n const { isLink } = useEditorState({\n editor,\n selector: ({ editor: e }) => ({ isLink: e.isActive('link') }),\n })\n\n const handleToggle = () => {\n if (isLink) {\n editor.chain().focus().unsetLink().run()\n return\n }\n const previousUrl = editor.getAttributes('link').href || ''\n setUrl(previousUrl)\n setShowInput(true)\n setTimeout(() => inputRef.current?.focus(), 0)\n }\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault()\n if (url.trim()) {\n editor.chain().focus().setLink({ href: url.trim() }).run()\n }\n setShowInput(false)\n setUrl('')\n }\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Escape') {\n setShowInput(false)\n setUrl('')\n editor.commands.focus()\n }\n }\n\n return (\n <div className=\"relative\">\n <ToolbarButton onClick={handleToggle} isActive={isLink} title=\"Link\">\n <Icon icon={IconLink} size=\"sm\" />\n </ToolbarButton>\n {showInput && (\n <form\n onSubmit={handleSubmit}\n aria-label=\"Edit link URL\"\n className=\"absolute left-0 top-full z-popover mt-ds-01 flex items-center gap-ds-02 rounded-control bg-surface-overlay p-ds-02 shadow-raised-hover\"\n >\n <input\n ref={inputRef}\n type=\"url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"https://...\"\n className=\"h-ds-sm w-[240px] rounded-control-inner border border-surface-border-strong bg-surface-overlay px-ds-03 text-ds-sm text-surface-fg focus:border-accent-7 focus:outline-hidden\"\n />\n <Button type=\"submit\" variant=\"solid\" size=\"sm\">\n Apply\n </Button>\n </form>\n )}\n </div>\n )\n}\n\nfunction ToolbarDivider() {\n return <div className=\"mx-ds-02 h-[16px] w-px bg-surface-border\" />\n}\n\nfunction Toolbar({ editor, toolbar, onImageClick, onFileClick, onEmojiClick }: {\n editor: Editor\n toolbar?: ToolbarItem[]\n onImageClick?: () => void\n onFileClick?: () => void\n onEmojiClick?: () => void\n}) {\n // v3: useEditorState subscribes to specific state slices so the toolbar\n // re-renders only when active-state or can-undo/redo actually changes.\n const state = useEditorState({\n editor,\n selector: ({ editor: e }) => ({\n isBold: e.isActive('bold'),\n isItalic: e.isActive('italic'),\n isUnderline: e.isActive('underline'),\n isStrike: e.isActive('strike'),\n isHighlight: e.isActive('highlight'),\n isH2: e.isActive('heading', { level: 2 }),\n isH3: e.isActive('heading', { level: 3 }),\n isBlockquote: e.isActive('blockquote'),\n isBulletList: e.isActive('bulletList'),\n isOrderedList: e.isActive('orderedList'),\n isTaskList: e.isActive('taskList'),\n isCodeBlock: e.isActive('codeBlock'),\n isLink: e.isActive('link'),\n isAlignLeft: e.isActive({ textAlign: 'left' }),\n isAlignCenter: e.isActive({ textAlign: 'center' }),\n isAlignRight: e.isActive({ textAlign: 'right' }),\n canUndo: e.can().undo(),\n canRedo: e.can().redo(),\n }),\n })\n\n const show = (item: ToolbarItem) => !toolbar || toolbar.includes(item)\n\n // Each group is an array of ToolbarItem names that belong to it.\n // A divider renders between two adjacent groups only if both have at least one visible item.\n const hasInline = show('bold') || show('italic') || show('underline') || show('strike') || show('highlight')\n const hasBlock = show('h2') || show('h3') || show('blockquote')\n const hasList = show('bulletList') || show('orderedList') || show('taskList') || show('codeBlock')\n const hasMedia = show('link') || (!!onImageClick && show('image')) || (!!onFileClick && show('file')) || show('hr')\n const hasAlign = show('alignLeft') || show('alignCenter') || show('alignRight')\n const hasEmojiGroup = !!onEmojiClick && show('emoji')\n const hasHistory = show('undo') || show('redo')\n\n return (\n <div role=\"toolbar\" aria-label=\"Text formatting\" className=\"flex flex-wrap items-center gap-ds-01 border-b border-surface-border-strong px-ds-04 py-ds-02b\">\n {/* Inline formatting */}\n {show('bold') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleBold().run()} isActive={state.isBold} title=\"Bold\">\n <Icon icon={IconBold} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('italic') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleItalic().run()} isActive={state.isItalic} title=\"Italic\">\n <Icon icon={IconItalic} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('underline') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleUnderline().run()} isActive={state.isUnderline} title=\"Underline\">\n <Icon icon={IconUnderline} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('strike') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleStrike().run()} isActive={state.isStrike} title=\"Strikethrough\">\n <Icon icon={IconStrikethrough} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('highlight') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleHighlight().run()} isActive={state.isHighlight} title=\"Highlight\">\n <Icon icon={IconHighlight} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n\n {hasInline && hasBlock && <ToolbarDivider />}\n\n {/* Block formatting */}\n {show('h2') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} isActive={state.isH2} title=\"Heading 2\">\n <Icon icon={IconH2} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('h3') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} isActive={state.isH3} title=\"Heading 3\">\n <Icon icon={IconH3} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('blockquote') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleBlockquote().run()} isActive={state.isBlockquote} title=\"Blockquote\">\n <Icon icon={IconBlockquote} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n\n {hasBlock && hasList && <ToolbarDivider />}\n\n {/* Lists */}\n {show('bulletList') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleBulletList().run()} isActive={state.isBulletList} title=\"Bullet list\">\n <Icon icon={IconList} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('orderedList') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleOrderedList().run()} isActive={state.isOrderedList} title=\"Ordered list\">\n <Icon icon={IconListNumbers} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('taskList') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleTaskList().run()} isActive={state.isTaskList} title=\"Task list\">\n <Icon icon={IconListCheck} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('codeBlock') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleCodeBlock().run()} isActive={state.isCodeBlock} title=\"Code block\">\n <Icon icon={IconCode} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n\n {hasList && hasMedia && <ToolbarDivider />}\n\n {/* Media & Links */}\n {show('link') && <LinkButton editor={editor} />}\n {onImageClick && show('image') && (\n <ToolbarButton onClick={onImageClick} title=\"Insert image\">\n <Icon icon={IconPhoto} size=\"sm\" />\n </ToolbarButton>\n )}\n {onFileClick && show('file') && (\n <ToolbarButton onClick={onFileClick} title=\"Attach file\">\n <Icon icon={IconPaperclip} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('hr') && (\n <ToolbarButton onClick={() => editor.chain().focus().setHorizontalRule().run()} title=\"Horizontal rule\">\n <Icon icon={IconLineDashed} size=\"sm\" />\n </ToolbarButton>\n )}\n\n {hasMedia && hasAlign && <ToolbarDivider />}\n\n {/* Alignment */}\n {show('alignLeft') && (\n <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('left').run()} isActive={state.isAlignLeft} title=\"Align left\">\n <Icon icon={IconAlignLeft} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('alignCenter') && (\n <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('center').run()} isActive={state.isAlignCenter} title=\"Align center\">\n <Icon icon={IconAlignCenter} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('alignRight') && (\n <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('right').run()} isActive={state.isAlignRight} title=\"Align right\">\n <Icon icon={IconAlignRight} size=\"sm\" />\n </ToolbarButton>\n )}\n\n {hasAlign && (hasEmojiGroup || hasHistory) && <ToolbarDivider />}\n\n {/* Emoji */}\n {onEmojiClick && show('emoji') && (\n <ToolbarButton onClick={onEmojiClick} title=\"Emoji\">\n <Icon icon={IconMoodSmile} size=\"sm\" />\n </ToolbarButton>\n )}\n\n {/* History */}\n {show('undo') && (\n <ToolbarButton onClick={() => editor.chain().focus().undo().run()} disabled={!state.canUndo} title=\"Undo\">\n <Icon icon={IconArrowBackUp} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('redo') && (\n <ToolbarButton onClick={() => editor.chain().focus().redo().run()} disabled={!state.canRedo} title=\"Redo\">\n <Icon icon={IconArrowForwardUp} size=\"sm\" />\n </ToolbarButton>\n )}\n </div>\n )\n}\n\nconst LazyPicker = React.lazy(() => import('@emoji-mart/react'))\n\nfunction EmojiPickerLazy({ set = 'native', onSelect }: { set?: string; onSelect: (emoji: { id: string; native: string }) => void }) {\n const [data, setData] = React.useState<unknown>(null)\n const { colorMode } = useColorMode()\n\n React.useEffect(() => {\n loadEmojiData(set).then((d) => setData(d))\n }, [set])\n\n const fallback = <div className=\"flex h-[350px] w-[352px] items-center justify-center rounded-surface bg-surface-overlay shadow-raised-hover\"><span className=\"text-ds-sm text-surface-fg-subtle\">Loading...</span></div>\n\n if (!data) return fallback\n\n return (\n <React.Suspense fallback={fallback}>\n <LazyPicker\n data={data}\n set={set}\n onEmojiSelect={(emoji: { native: string; id: string }) => onSelect({ id: emoji.id, native: emoji.native })}\n theme={colorMode === 'dark' ? 'dark' : 'light'}\n previewPosition=\"none\"\n skinTonePosition=\"none\"\n />\n </React.Suspense>\n )\n}\n\nexport type ToolbarItem =\n | 'bold' | 'italic' | 'underline' | 'strike' | 'highlight'\n | 'h2' | 'h3' | 'blockquote'\n | 'bulletList' | 'orderedList' | 'taskList' | 'codeBlock'\n | 'link' | 'image' | 'file' | 'hr'\n | 'alignLeft' | 'alignCenter' | 'alignRight'\n | 'emoji' | 'undo' | 'redo'\n\nexport interface MentionItem {\n id: string\n label: string\n avatar?: string\n}\n\n/**\n * A Tiptap-powered rich text editor with a configurable toolbar, @mentions,\n * emoji suggestions, file/image uploads, and task lists.\n * Outputs sanitized HTML via `onChange`.\n */\nexport interface RichTextEditorProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'onChange' | 'content'> {\n /** Initial HTML content. Updates are NOT reactive — use `onChange` for controlled state. */\n content?: string\n placeholder?: string\n /** Called with the editor's HTML string whenever content changes. */\n onChange?: (html: string) => void\n className?: string\n /** Set to false to render in read-only mode (e.g. for previewing). */\n editable?: boolean\n /** Called when an image is pasted/dropped. Return a URL. If not provided, images inline as base64. */\n onImageUpload?: (file: File) => Promise<string>\n /** Called when a non-image file is dropped/pasted. If not provided, non-image files are ignored. */\n onFileUpload?: (file: File) => Promise<{ url: string; name: string; size: number }>\n /** Whitelist of toolbar items to display. Omit to show all. */\n toolbar?: ToolbarItem[]\n /** Static list of mentionable items (shown when user types @). */\n mentions?: MentionItem[]\n /** Async mention search. Takes precedence over static `mentions` list. */\n onMentionSearch?: (query: string) => Promise<MentionItem[]>\n /** Called when a mention is selected from the suggestion dropdown. */\n onMentionSelect?: (item: MentionItem) => void\n /** Emoji art style. @default 'native' */\n emojiSet?: EmojiSet\n}\n\nconst RichTextEditor = React.forwardRef<HTMLDivElement, RichTextEditorProps>(\n function RichTextEditor({\n content = '',\n placeholder = 'Start writing...',\n onChange,\n className,\n editable = true,\n toolbar,\n onImageUpload,\n onFileUpload,\n mentions,\n onMentionSearch,\n onMentionSelect,\n emojiSet = 'native',\n ...props\n}, ref) {\n const editorRef = React.useRef<ReturnType<typeof useEditor>>(null)\n const isInternalChangeRef = React.useRef(false)\n const [showEmojiPicker, setShowEmojiPicker] = React.useState(false)\n const emojiPickerRef = React.useRef<HTMLDivElement>(null)\n const imageInputRef = React.useRef<HTMLInputElement>(null)\n const fileInputRef = React.useRef<HTMLInputElement>(null)\n\n const handleImageInsert = async (file: File) => {\n const ed = editorRef.current\n if (!ed) return\n if (onImageUpload) {\n const url = await onImageUpload(file)\n if (url && /^https?:\\/\\//i.test(url)) {\n ed.chain().focus().setImage({ src: url }).run()\n }\n } else {\n const reader = new FileReader()\n reader.onload = () => {\n ed.chain().focus().setImage({ src: reader.result as string }).run()\n }\n reader.readAsDataURL(file)\n }\n }\n\n const handleFileInsert = async (file: File) => {\n const ed = editorRef.current\n if (!ed || !onFileUpload) return\n const result = await onFileUpload(file)\n ed.chain().focus().insertContent({\n type: 'fileAttachment',\n attrs: { url: result.url, name: result.name, size: result.size },\n }).run()\n }\n\n const editor = useEditor({\n immediatelyRender: false, // SSR-safe — prevents hydration mismatch in Next.js\n extensions: [\n StarterKit.configure({\n heading: { levels: [2, 3] },\n // v3: Underline + Link are now included in StarterKit\n link: {\n openOnClick: false,\n protocols: ['http', 'https', 'mailto'],\n validate: (href: string) => /^(https?:\\/\\/|mailto:)/i.test(href),\n HTMLAttributes: {\n rel: 'noopener noreferrer',\n target: '_blank',\n },\n },\n }),\n Placeholder.configure({\n placeholder,\n emptyEditorClass:\n 'before:content-[attr(data-placeholder)] before:text-surface-fg-subtle before:float-left before:h-0 before:pointer-events-none',\n }),\n Highlight.configure({ multicolor: false }),\n ListKit.configure({\n bulletList: false, // Already in StarterKit\n orderedList: false, // Already in StarterKit\n listItem: false, // Already in StarterKit\n listKeymap: false, // Already in StarterKit\n taskList: {},\n taskItem: { nested: true },\n }),\n TextAlign.configure({ types: ['heading', 'paragraph'] }),\n Image.configure({\n allowBase64: true,\n HTMLAttributes: {\n class: 'max-w-full rounded-control',\n },\n }),\n FileAttachment,\n ...(mentions || onMentionSearch ? [\n Mention.configure({\n HTMLAttributes: { class: 'mention' },\n suggestion: {\n items: async ({ query }: { query: string }) => {\n if (onMentionSearch) return await onMentionSearch(query)\n if (mentions) return mentions.filter(m => m.label.toLowerCase().includes(query.toLowerCase())).slice(0, 8)\n return []\n },\n render: createSuggestionRenderer(onMentionSelect),\n },\n }),\n ] : []),\n EmojiNode,\n createEmojiSuggestion(emojiSet),\n ],\n content,\n editable,\n editorProps: {\n handleDrop: (_view, event, _slice, moved) => {\n if (moved || !event.dataTransfer?.files.length) return false\n const file = event.dataTransfer.files[0]\n if (!file) return false\n if (file.type.startsWith('image/')) {\n handleImageInsert(file)\n return true\n }\n if (onFileUpload) {\n handleFileInsert(file)\n return true\n }\n return false\n },\n handlePaste: (_view, event) => {\n const file = event.clipboardData?.files[0]\n if (!file) return false\n if (file.type.startsWith('image/')) {\n handleImageInsert(file)\n return true\n }\n if (onFileUpload) {\n handleFileInsert(file)\n return true\n }\n return false\n },\n attributes: {\n class: cn(\n ...PROSE_CLASSES,\n 'focus:outline-hidden',\n 'min-h-[120px] px-ds-04 py-ds-04',\n ),\n },\n },\n onUpdate: ({ editor: ed }) => {\n isInternalChangeRef.current = true\n onChange?.(ed.getHTML())\n queueMicrotask(() => {\n isInternalChangeRef.current = false\n })\n },\n })\n\n React.useEffect(() => {\n editorRef.current = editor\n }, [editor])\n\n // Close emoji picker on click outside\n React.useEffect(() => {\n if (!showEmojiPicker) return\n const handleClickOutside = (e: MouseEvent) => {\n if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) {\n setShowEmojiPicker(false)\n }\n }\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [showEmojiPicker])\n\n React.useEffect(() => {\n if (isInternalChangeRef.current) return\n if (editor && content !== editor.getHTML()) {\n editor.commands.setContent(content, { emitUpdate: false })\n }\n }, [editor, content])\n\n if (!editor) return null\n\n return (\n <div ref={ref} {...props} className={cn('relative', className)}>\n {/* Emoji picker rendered outside the overflow-hidden box so it isn't clipped */}\n {showEmojiPicker && (\n <div\n ref={emojiPickerRef}\n className=\"absolute bottom-full right-0 z-popover mb-ds-02\"\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.stopPropagation()\n setShowEmojiPicker(false)\n }\n }}\n >\n <EmojiPickerLazy\n set={emojiSet}\n onSelect={async ({ id, native }) => {\n const data = await loadEmojiData(emojiSet)\n const resolved = lookupEmoji(data, id)\n if (resolved) {\n editor.chain().focus().insertContent({\n type: 'emojiNode',\n attrs: { id, native: resolved.native, set: emojiSet, x: resolved.x, y: resolved.y },\n }).run()\n } else {\n editor.chain().focus().insertContent(native).run()\n }\n setShowEmojiPicker(false)\n }}\n />\n </div>\n )}\n <div\n className={cn(\n 'overflow-hidden rounded-surface border border-surface-border-strong bg-surface-raised',\n 'transition-colors ease-productive-standard focus-within:border-surface-border-strong',\n )}\n >\n <input\n ref={imageInputRef}\n type=\"file\"\n accept=\"image/*\"\n aria-label=\"Upload image\"\n className=\"hidden\"\n onChange={(e) => {\n const file = e.target.files?.[0]\n if (file) handleImageInsert(file)\n e.target.value = ''\n }}\n />\n {onFileUpload && (\n <input\n ref={fileInputRef}\n type=\"file\"\n aria-label=\"Upload file\"\n className=\"hidden\"\n onChange={(e) => {\n const file = e.target.files?.[0]\n if (file) handleFileInsert(file)\n e.target.value = ''\n }}\n />\n )}\n {editable && (\n <Toolbar\n editor={editor}\n toolbar={toolbar}\n onImageClick={() => imageInputRef.current?.click()}\n onFileClick={onFileUpload ? () => fileInputRef.current?.click() : undefined}\n onEmojiClick={() => setShowEmojiPicker((prev) => !prev)}\n />\n )}\n <EditorContent editor={editor} />\n </div>\n </div>\n )\n},\n)\n\nRichTextEditor.displayName = 'RichTextEditor'\n\nexport interface RichTextViewerProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'content'> {\n content: string\n className?: string\n}\n\nconst RichTextViewer = React.forwardRef<HTMLDivElement, RichTextViewerProps>(\n function RichTextViewer({ content, className, ...props }, ref) {\n const editor = useEditor({\n immediatelyRender: false, // SSR-safe — prevents hydration mismatch in Next.js\n extensions: [\n StarterKit.configure({\n heading: { levels: [2, 3] },\n link: {\n openOnClick: true,\n protocols: ['http', 'https', 'mailto'],\n validate: (href: string) => /^(https?:\\/\\/|mailto:)/i.test(href),\n HTMLAttributes: {\n rel: 'noopener noreferrer',\n target: '_blank',\n },\n },\n }),\n Highlight.configure({ multicolor: false }),\n ListKit.configure({\n bulletList: false,\n orderedList: false,\n listItem: false,\n listKeymap: false,\n taskList: {},\n taskItem: { nested: true },\n }),\n TextAlign.configure({ types: ['heading', 'paragraph'] }),\n Image.configure({\n allowBase64: true,\n HTMLAttributes: {\n class: 'max-w-full rounded-control',\n },\n }),\n FileAttachment,\n Mention.configure({\n HTMLAttributes: { class: 'mention' },\n }),\n ],\n content,\n editable: false,\n editorProps: {\n attributes: {\n class: cn(...PROSE_CLASSES),\n },\n },\n })\n\n if (!editor) return null\n\n return (\n <div ref={ref} {...props} className={className}>\n <EditorContent editor={editor} />\n </div>\n )\n},\n)\n\nRichTextViewer.displayName = 'RichTextViewer'\n\nexport { RichTextEditor, RichTextViewer }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../src/composed/rich-text-editor.tsx"],"sourcesContent":["'use client'\n\nimport {\n IconAlignCenter,\n IconAlignLeft,\n IconAlignRight,\n IconArrowBackUp,\n IconArrowForwardUp,\n IconBlockquote,\n IconBold,\n IconCode,\n IconH2,\n IconH3,\n IconHighlight,\n IconItalic,\n IconLineDashed,\n IconLink,\n IconList,\n IconListCheck,\n IconListNumbers,\n IconMoodSmile,\n IconPaperclip,\n IconPhoto,\n IconStrikethrough,\n IconUnderline,\n} from '@tabler/icons-react'\nimport Highlight from '@tiptap/extension-highlight'\nimport Image from '@tiptap/extension-image'\nimport { ListKit } from '@tiptap/extension-list'\nimport Mention from '@tiptap/extension-mention'\nimport TextAlign from '@tiptap/extension-text-align'\nimport { Placeholder } from '@tiptap/extensions'\nimport { type Editor,EditorContent, useEditor, useEditorState } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport * as React from 'react'\n\nimport { Button } from '../ui/button'\nimport { Icon } from '../ui/icon'\nimport { MENTION_TOKEN_CLASS } from '../ui/lib/mention'\nimport { cn } from '../ui/lib/utils'\nimport type { EmojiData, EmojiSet } from './emoji-picker'\nimport { EmojiNode } from './extensions/emoji-node'\nimport { createEmojiSuggestion } from './extensions/emoji-suggestion'\nimport { FileAttachment } from './extensions/file-attachment'\nimport { createSuggestionRenderer } from './extensions/mention-suggestion'\n\nconst PROSE_CLASSES = [\n 'prose prose-sm max-w-none',\n 'font-body text-ds-md leading-relaxed text-surface-fg',\n '[&_h2]:text-ds-xl [&_h2]:mb-ds-03 [&_h2]:mt-ds-05 [&_h2]:text-surface-fg',\n '[&_h3]:text-ds-base [&_h3]:font-semibold [&_h3]:mb-ds-02b [&_h3]:mt-ds-04 [&_h3]:text-surface-fg',\n '[&_p]:mb-ds-02b [&_p]:text-surface-fg-muted',\n '[&_ul]:ml-ds-05 [&_ul]:list-disc [&_ol]:ml-ds-05 [&_ol]:list-decimal',\n '[&_li]:text-surface-fg-muted',\n '[&_code]:rounded [&_code]:bg-surface-raised [&_code]:px-ds-02b [&_code]:py-ds-01 [&_code]:text-ds-md [&_code]:text-accent-11',\n '[&_pre]:rounded-surface [&_pre]:bg-surface-raised [&_pre]:p-ds-04',\n '[&_strong]:font-semibold [&_strong]:text-surface-fg',\n '[&_blockquote]:border-l-[3px] [&_blockquote]:border-accent-6 [&_blockquote]:pl-ds-04 [&_blockquote]:italic [&_blockquote]:text-surface-fg-subtle',\n '[&_mark]:rounded-xs [&_mark]:bg-warning-3 [&_mark]:px-[2px]',\n '[&_ul[data-type=\"taskList\"]]:ml-0 [&_ul[data-type=\"taskList\"]]:list-none [&_li[data-type=\"taskItem\"]]:flex [&_li[data-type=\"taskItem\"]]:items-start [&_li[data-type=\"taskItem\"]]:gap-ds-02',\n '[&_hr]:my-ds-04 [&_hr]:border-surface-border-strong',\n '[&_a]:text-accent-11 [&_a]:underline [&_a]:decoration-accent-6 hover:[&_a]:decoration-accent-11',\n '[&_img]:max-w-full [&_img]:rounded-control [&_img]:my-ds-03',\n MENTION_TOKEN_CLASS,\n] as const\n\ninterface ToolbarButtonProps {\n onClick: () => void\n isActive?: boolean\n disabled?: boolean\n title: string\n children: React.ReactNode\n}\n\nfunction ToolbarButton({\n onClick,\n isActive = false,\n disabled = false,\n title,\n children,\n}: ToolbarButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n title={title}\n aria-label={title}\n aria-pressed={isActive}\n className={cn(\n 'inline-flex h-ds-xs-plus w-ds-xs-plus items-center justify-center rounded-control transition-colors duration-fast-01 ease-productive-standard',\n 'hover:bg-surface-raised-hover',\n 'disabled:pointer-events-none disabled:opacity-action-disabled',\n isActive\n ? 'bg-surface-raised-hover text-accent-11'\n : 'text-surface-fg-subtle',\n )}\n >\n {children}\n </button>\n )\n}\n\nfunction LinkButton({ editor }: { editor: Editor }) {\n const [showInput, setShowInput] = React.useState(false)\n const [url, setUrl] = React.useState('')\n const inputRef = React.useRef<HTMLInputElement>(null)\n const { isLink } = useEditorState({\n editor,\n selector: ({ editor: e }) => ({ isLink: e.isActive('link') }),\n })\n\n const handleToggle = () => {\n if (isLink) {\n editor.chain().focus().unsetLink().run()\n return\n }\n const previousUrl = editor.getAttributes('link').href || ''\n setUrl(previousUrl)\n setShowInput(true)\n setTimeout(() => inputRef.current?.focus(), 0)\n }\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault()\n if (url.trim()) {\n editor.chain().focus().setLink({ href: url.trim() }).run()\n }\n setShowInput(false)\n setUrl('')\n }\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Escape') {\n setShowInput(false)\n setUrl('')\n editor.commands.focus()\n }\n }\n\n return (\n <div className=\"relative\">\n <ToolbarButton onClick={handleToggle} isActive={isLink} title=\"Link\">\n <Icon icon={IconLink} size=\"sm\" />\n </ToolbarButton>\n {showInput && (\n <form\n onSubmit={handleSubmit}\n aria-label=\"Edit link URL\"\n className=\"absolute left-0 top-full z-popover mt-ds-01 flex items-center gap-ds-02 rounded-control bg-surface-overlay p-ds-02 shadow-raised-hover\"\n >\n <input\n ref={inputRef}\n type=\"url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"https://...\"\n className=\"h-ds-sm w-[240px] rounded-control-inner border border-surface-border-strong bg-surface-overlay px-ds-03 text-ds-sm text-surface-fg focus:border-accent-7 focus:outline-hidden\"\n />\n <Button type=\"submit\" variant=\"solid\" size=\"sm\">\n Apply\n </Button>\n </form>\n )}\n </div>\n )\n}\n\nfunction ToolbarDivider() {\n return <div className=\"mx-ds-02 h-[16px] w-px bg-surface-border\" />\n}\n\nfunction Toolbar({ editor, toolbar, onImageClick, onFileClick, onEmojiClick }: {\n editor: Editor\n toolbar?: ToolbarItem[]\n onImageClick?: () => void\n onFileClick?: () => void\n onEmojiClick?: () => void\n}) {\n // v3: useEditorState subscribes to specific state slices so the toolbar\n // re-renders only when active-state or can-undo/redo actually changes.\n const state = useEditorState({\n editor,\n selector: ({ editor: e }) => ({\n isBold: e.isActive('bold'),\n isItalic: e.isActive('italic'),\n isUnderline: e.isActive('underline'),\n isStrike: e.isActive('strike'),\n isHighlight: e.isActive('highlight'),\n isH2: e.isActive('heading', { level: 2 }),\n isH3: e.isActive('heading', { level: 3 }),\n isBlockquote: e.isActive('blockquote'),\n isBulletList: e.isActive('bulletList'),\n isOrderedList: e.isActive('orderedList'),\n isTaskList: e.isActive('taskList'),\n isCodeBlock: e.isActive('codeBlock'),\n isLink: e.isActive('link'),\n isAlignLeft: e.isActive({ textAlign: 'left' }),\n isAlignCenter: e.isActive({ textAlign: 'center' }),\n isAlignRight: e.isActive({ textAlign: 'right' }),\n canUndo: e.can().undo(),\n canRedo: e.can().redo(),\n }),\n })\n\n const show = (item: ToolbarItem) => !toolbar || toolbar.includes(item)\n\n // Each group is an array of ToolbarItem names that belong to it.\n // A divider renders between two adjacent groups only if both have at least one visible item.\n const hasInline = show('bold') || show('italic') || show('underline') || show('strike') || show('highlight')\n const hasBlock = show('h2') || show('h3') || show('blockquote')\n const hasList = show('bulletList') || show('orderedList') || show('taskList') || show('codeBlock')\n const hasMedia = show('link') || (!!onImageClick && show('image')) || (!!onFileClick && show('file')) || show('hr')\n const hasAlign = show('alignLeft') || show('alignCenter') || show('alignRight')\n const hasEmojiGroup = !!onEmojiClick && show('emoji')\n const hasHistory = show('undo') || show('redo')\n\n return (\n <div role=\"toolbar\" aria-label=\"Text formatting\" className=\"flex flex-wrap items-center gap-ds-01 border-b border-surface-border-strong px-ds-04 py-ds-02b\">\n {/* Inline formatting */}\n {show('bold') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleBold().run()} isActive={state.isBold} title=\"Bold\">\n <Icon icon={IconBold} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('italic') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleItalic().run()} isActive={state.isItalic} title=\"Italic\">\n <Icon icon={IconItalic} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('underline') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleUnderline().run()} isActive={state.isUnderline} title=\"Underline\">\n <Icon icon={IconUnderline} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('strike') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleStrike().run()} isActive={state.isStrike} title=\"Strikethrough\">\n <Icon icon={IconStrikethrough} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('highlight') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleHighlight().run()} isActive={state.isHighlight} title=\"Highlight\">\n <Icon icon={IconHighlight} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n\n {hasInline && hasBlock && <ToolbarDivider />}\n\n {/* Block formatting */}\n {show('h2') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} isActive={state.isH2} title=\"Heading 2\">\n <Icon icon={IconH2} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('h3') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} isActive={state.isH3} title=\"Heading 3\">\n <Icon icon={IconH3} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('blockquote') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleBlockquote().run()} isActive={state.isBlockquote} title=\"Blockquote\">\n <Icon icon={IconBlockquote} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n\n {hasBlock && hasList && <ToolbarDivider />}\n\n {/* Lists */}\n {show('bulletList') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleBulletList().run()} isActive={state.isBulletList} title=\"Bullet list\">\n <Icon icon={IconList} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('orderedList') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleOrderedList().run()} isActive={state.isOrderedList} title=\"Ordered list\">\n <Icon icon={IconListNumbers} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('taskList') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleTaskList().run()} isActive={state.isTaskList} title=\"Task list\">\n <Icon icon={IconListCheck} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n {show('codeBlock') && (\n <ToolbarButton onClick={() => editor.chain().focus().toggleCodeBlock().run()} isActive={state.isCodeBlock} title=\"Code block\">\n <Icon icon={IconCode} size=\"sm\" stroke=\"bold\" />\n </ToolbarButton>\n )}\n\n {hasList && hasMedia && <ToolbarDivider />}\n\n {/* Media & Links */}\n {show('link') && <LinkButton editor={editor} />}\n {onImageClick && show('image') && (\n <ToolbarButton onClick={onImageClick} title=\"Insert image\">\n <Icon icon={IconPhoto} size=\"sm\" />\n </ToolbarButton>\n )}\n {onFileClick && show('file') && (\n <ToolbarButton onClick={onFileClick} title=\"Attach file\">\n <Icon icon={IconPaperclip} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('hr') && (\n <ToolbarButton onClick={() => editor.chain().focus().setHorizontalRule().run()} title=\"Horizontal rule\">\n <Icon icon={IconLineDashed} size=\"sm\" />\n </ToolbarButton>\n )}\n\n {hasMedia && hasAlign && <ToolbarDivider />}\n\n {/* Alignment */}\n {show('alignLeft') && (\n <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('left').run()} isActive={state.isAlignLeft} title=\"Align left\">\n <Icon icon={IconAlignLeft} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('alignCenter') && (\n <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('center').run()} isActive={state.isAlignCenter} title=\"Align center\">\n <Icon icon={IconAlignCenter} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('alignRight') && (\n <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('right').run()} isActive={state.isAlignRight} title=\"Align right\">\n <Icon icon={IconAlignRight} size=\"sm\" />\n </ToolbarButton>\n )}\n\n {hasAlign && (hasEmojiGroup || hasHistory) && <ToolbarDivider />}\n\n {/* Emoji */}\n {onEmojiClick && show('emoji') && (\n <ToolbarButton onClick={onEmojiClick} title=\"Emoji\">\n <Icon icon={IconMoodSmile} size=\"sm\" />\n </ToolbarButton>\n )}\n\n {/* History */}\n {show('undo') && (\n <ToolbarButton onClick={() => editor.chain().focus().undo().run()} disabled={!state.canUndo} title=\"Undo\">\n <Icon icon={IconArrowBackUp} size=\"sm\" />\n </ToolbarButton>\n )}\n {show('redo') && (\n <ToolbarButton onClick={() => editor.chain().focus().redo().run()} disabled={!state.canRedo} title=\"Redo\">\n <Icon icon={IconArrowForwardUp} size=\"sm\" />\n </ToolbarButton>\n )}\n </div>\n )\n}\n\nconst LazyPicker = React.lazy(() =>\n import('./emoji-picker').then((m) => ({ default: m.EmojiPicker })),\n)\n\nfunction EmojiPickerLazy({ onSelect }: { onSelect: (emoji: EmojiData) => void }) {\n const fallback = <div className=\"flex h-[435px] w-[352px] items-center justify-center rounded-surface bg-surface-overlay shadow-raised-hover\"><span className=\"text-ds-sm text-surface-fg-subtle\">Loading…</span></div>\n\n return (\n <React.Suspense fallback={fallback}>\n <LazyPicker onSelect={onSelect} />\n </React.Suspense>\n )\n}\n\nexport type ToolbarItem =\n | 'bold' | 'italic' | 'underline' | 'strike' | 'highlight'\n | 'h2' | 'h3' | 'blockquote'\n | 'bulletList' | 'orderedList' | 'taskList' | 'codeBlock'\n | 'link' | 'image' | 'file' | 'hr'\n | 'alignLeft' | 'alignCenter' | 'alignRight'\n | 'emoji' | 'undo' | 'redo'\n\nexport interface MentionItem {\n id: string\n label: string\n avatar?: string\n}\n\n/**\n * A Tiptap-powered rich text editor with a configurable toolbar, @mentions,\n * emoji suggestions, file/image uploads, and task lists.\n * Outputs sanitized HTML via `onChange`.\n */\nexport interface RichTextEditorProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'onChange' | 'content'> {\n /** Initial HTML content. Updates are NOT reactive — use `onChange` for controlled state. */\n content?: string\n placeholder?: string\n /** Called with the editor's HTML string whenever content changes. */\n onChange?: (html: string) => void\n className?: string\n /** Set to false to render in read-only mode (e.g. for previewing). */\n editable?: boolean\n /** Called when an image is pasted/dropped. Return a URL. If not provided, images inline as base64. */\n onImageUpload?: (file: File) => Promise<string>\n /** Called when a non-image file is dropped/pasted. If not provided, non-image files are ignored. */\n onFileUpload?: (file: File) => Promise<{ url: string; name: string; size: number }>\n /** Whitelist of toolbar items to display. Omit to show all. */\n toolbar?: ToolbarItem[]\n /** Static list of mentionable items (shown when user types @). */\n mentions?: MentionItem[]\n /** Async mention search. Takes precedence over static `mentions` list. */\n onMentionSearch?: (query: string) => Promise<MentionItem[]>\n /** Called when a mention is selected from the suggestion dropdown. */\n onMentionSelect?: (item: MentionItem) => void\n /** Emoji art style. @default 'native' */\n emojiSet?: EmojiSet\n}\n\nconst RichTextEditor = React.forwardRef<HTMLDivElement, RichTextEditorProps>(\n function RichTextEditor({\n content = '',\n placeholder = 'Start writing...',\n onChange,\n className,\n editable = true,\n toolbar,\n onImageUpload,\n onFileUpload,\n mentions,\n onMentionSearch,\n onMentionSelect,\n // @deprecated no-op since the frimousse migration (native-only emoji). Kept\n // out of `...props` so it never leaks onto the DOM.\n emojiSet: _emojiSet = 'native',\n ...props\n}, ref) {\n const editorRef = React.useRef<ReturnType<typeof useEditor>>(null)\n const isInternalChangeRef = React.useRef(false)\n const [showEmojiPicker, setShowEmojiPicker] = React.useState(false)\n const emojiPickerRef = React.useRef<HTMLDivElement>(null)\n const imageInputRef = React.useRef<HTMLInputElement>(null)\n const fileInputRef = React.useRef<HTMLInputElement>(null)\n\n const handleImageInsert = async (file: File) => {\n const ed = editorRef.current\n if (!ed) return\n if (onImageUpload) {\n const url = await onImageUpload(file)\n if (url && /^https?:\\/\\//i.test(url)) {\n ed.chain().focus().setImage({ src: url }).run()\n }\n } else {\n const reader = new FileReader()\n reader.onload = () => {\n ed.chain().focus().setImage({ src: reader.result as string }).run()\n }\n reader.readAsDataURL(file)\n }\n }\n\n const handleFileInsert = async (file: File) => {\n const ed = editorRef.current\n if (!ed || !onFileUpload) return\n const result = await onFileUpload(file)\n ed.chain().focus().insertContent({\n type: 'fileAttachment',\n attrs: { url: result.url, name: result.name, size: result.size },\n }).run()\n }\n\n const editor = useEditor({\n immediatelyRender: false, // SSR-safe — prevents hydration mismatch in Next.js\n extensions: [\n StarterKit.configure({\n heading: { levels: [2, 3] },\n // v3: Underline + Link are now included in StarterKit\n link: {\n openOnClick: false,\n protocols: ['http', 'https', 'mailto'],\n validate: (href: string) => /^(https?:\\/\\/|mailto:)/i.test(href),\n HTMLAttributes: {\n rel: 'noopener noreferrer',\n target: '_blank',\n },\n },\n }),\n Placeholder.configure({\n placeholder,\n emptyEditorClass:\n 'before:content-[attr(data-placeholder)] before:text-surface-fg-subtle before:float-left before:h-0 before:pointer-events-none',\n }),\n Highlight.configure({ multicolor: false }),\n ListKit.configure({\n bulletList: false, // Already in StarterKit\n orderedList: false, // Already in StarterKit\n listItem: false, // Already in StarterKit\n listKeymap: false, // Already in StarterKit\n taskList: {},\n taskItem: { nested: true },\n }),\n TextAlign.configure({ types: ['heading', 'paragraph'] }),\n Image.configure({\n allowBase64: true,\n HTMLAttributes: {\n class: 'max-w-full rounded-control',\n },\n }),\n FileAttachment,\n ...(mentions || onMentionSearch ? [\n Mention.configure({\n HTMLAttributes: { class: 'mention' },\n suggestion: {\n items: async ({ query }: { query: string }) => {\n if (onMentionSearch) return await onMentionSearch(query)\n if (mentions) return mentions.filter(m => m.label.toLowerCase().includes(query.toLowerCase())).slice(0, 8)\n return []\n },\n render: createSuggestionRenderer(onMentionSelect),\n },\n }),\n ] : []),\n EmojiNode,\n createEmojiSuggestion(),\n ],\n content,\n editable,\n editorProps: {\n handleDrop: (_view, event, _slice, moved) => {\n if (moved || !event.dataTransfer?.files.length) return false\n const file = event.dataTransfer.files[0]\n if (!file) return false\n if (file.type.startsWith('image/')) {\n handleImageInsert(file)\n return true\n }\n if (onFileUpload) {\n handleFileInsert(file)\n return true\n }\n return false\n },\n handlePaste: (_view, event) => {\n const file = event.clipboardData?.files[0]\n if (!file) return false\n if (file.type.startsWith('image/')) {\n handleImageInsert(file)\n return true\n }\n if (onFileUpload) {\n handleFileInsert(file)\n return true\n }\n return false\n },\n attributes: {\n class: cn(\n ...PROSE_CLASSES,\n 'focus:outline-hidden',\n 'min-h-[120px] px-ds-04 py-ds-04',\n ),\n },\n },\n onUpdate: ({ editor: ed }) => {\n isInternalChangeRef.current = true\n onChange?.(ed.getHTML())\n queueMicrotask(() => {\n isInternalChangeRef.current = false\n })\n },\n })\n\n React.useEffect(() => {\n editorRef.current = editor\n }, [editor])\n\n // Close emoji picker on click outside\n React.useEffect(() => {\n if (!showEmojiPicker) return\n const handleClickOutside = (e: MouseEvent) => {\n if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) {\n setShowEmojiPicker(false)\n }\n }\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [showEmojiPicker])\n\n React.useEffect(() => {\n if (isInternalChangeRef.current) return\n if (editor && content !== editor.getHTML()) {\n editor.commands.setContent(content, { emitUpdate: false })\n }\n }, [editor, content])\n\n if (!editor) return null\n\n return (\n <div ref={ref} {...props} className={cn('relative', className)}>\n {/* Emoji picker rendered outside the overflow-hidden box so it isn't clipped */}\n {showEmojiPicker && (\n <div\n ref={emojiPickerRef}\n className=\"absolute bottom-full right-0 z-popover mb-ds-02\"\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.stopPropagation()\n setShowEmojiPicker(false)\n }\n }}\n >\n <EmojiPickerLazy\n onSelect={({ id, native }) => {\n editor.chain().focus().insertContent({\n type: 'emojiNode',\n attrs: { id, native },\n }).run()\n setShowEmojiPicker(false)\n }}\n />\n </div>\n )}\n <div\n className={cn(\n 'overflow-hidden rounded-surface border border-surface-border-strong bg-surface-raised',\n 'transition-colors ease-productive-standard focus-within:border-surface-border-strong',\n )}\n >\n <input\n ref={imageInputRef}\n type=\"file\"\n accept=\"image/*\"\n aria-label=\"Upload image\"\n className=\"hidden\"\n onChange={(e) => {\n const file = e.target.files?.[0]\n if (file) handleImageInsert(file)\n e.target.value = ''\n }}\n />\n {onFileUpload && (\n <input\n ref={fileInputRef}\n type=\"file\"\n aria-label=\"Upload file\"\n className=\"hidden\"\n onChange={(e) => {\n const file = e.target.files?.[0]\n if (file) handleFileInsert(file)\n e.target.value = ''\n }}\n />\n )}\n {editable && (\n <Toolbar\n editor={editor}\n toolbar={toolbar}\n onImageClick={() => imageInputRef.current?.click()}\n onFileClick={onFileUpload ? () => fileInputRef.current?.click() : undefined}\n onEmojiClick={() => setShowEmojiPicker((prev) => !prev)}\n />\n )}\n <EditorContent editor={editor} />\n </div>\n </div>\n )\n},\n)\n\nRichTextEditor.displayName = 'RichTextEditor'\n\nexport interface RichTextViewerProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'content'> {\n content: string\n className?: string\n}\n\nconst RichTextViewer = React.forwardRef<HTMLDivElement, RichTextViewerProps>(\n function RichTextViewer({ content, className, ...props }, ref) {\n const editor = useEditor({\n immediatelyRender: false, // SSR-safe — prevents hydration mismatch in Next.js\n extensions: [\n StarterKit.configure({\n heading: { levels: [2, 3] },\n link: {\n openOnClick: true,\n protocols: ['http', 'https', 'mailto'],\n validate: (href: string) => /^(https?:\\/\\/|mailto:)/i.test(href),\n HTMLAttributes: {\n rel: 'noopener noreferrer',\n target: '_blank',\n },\n },\n }),\n Highlight.configure({ multicolor: false }),\n ListKit.configure({\n bulletList: false,\n orderedList: false,\n listItem: false,\n listKeymap: false,\n taskList: {},\n taskItem: { nested: true },\n }),\n TextAlign.configure({ types: ['heading', 'paragraph'] }),\n Image.configure({\n allowBase64: true,\n HTMLAttributes: {\n class: 'max-w-full rounded-control',\n },\n }),\n FileAttachment,\n Mention.configure({\n HTMLAttributes: { class: 'mention' },\n }),\n ],\n content,\n editable: false,\n editorProps: {\n attributes: {\n class: cn(...PROSE_CLASSES),\n },\n },\n })\n\n if (!editor) return null\n\n return (\n <div ref={ref} {...props} className={className}>\n <EditorContent editor={editor} />\n </div>\n )\n},\n)\n\nRichTextViewer.displayName = 'RichTextViewer'\n\nexport { RichTextEditor, RichTextViewer }\n"],"mappings":"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"}
|
|
@@ -11,9 +11,7 @@ EmojiPicker, EmojiPickerPopover
|
|
|
11
11
|
|
|
12
12
|
### EmojiPicker
|
|
13
13
|
onSelect: (emoji: EmojiData) => void
|
|
14
|
-
|
|
15
|
-
previewPosition: "top" | "bottom" | "none"
|
|
16
|
-
skinTonePosition: "search" | "preview" | "none"
|
|
14
|
+
emojibaseUrl: string (base URL for the emoji dataset; default = frimousse's jsdelivr CDN)
|
|
17
15
|
className: string
|
|
18
16
|
|
|
19
17
|
### EmojiPickerPopover (extends EmojiPicker props)
|
|
@@ -26,7 +24,7 @@ EmojiPicker, EmojiPickerPopover
|
|
|
26
24
|
shortcodes: string
|
|
27
25
|
|
|
28
26
|
## Defaults
|
|
29
|
-
|
|
27
|
+
align="start"
|
|
30
28
|
|
|
31
29
|
## Example
|
|
32
30
|
```jsx
|
|
@@ -34,27 +32,31 @@ EmojiPicker, EmojiPickerPopover
|
|
|
34
32
|
<Button variant="ghost" size="icon-sm">😀</Button>
|
|
35
33
|
</EmojiPickerPopover>
|
|
36
34
|
|
|
37
|
-
<EmojiPicker onSelect={handleEmoji}
|
|
35
|
+
<EmojiPicker onSelect={handleEmoji} />
|
|
38
36
|
```
|
|
39
37
|
|
|
40
38
|
## Composability
|
|
41
39
|
- **Two exports:** `EmojiPicker` (inline grid, no trigger) and `EmojiPickerPopover` (trigger + popover wrapper). Use EmojiPickerPopover 95% of the time — trigger-on-click is the standard UX.
|
|
42
|
-
- **
|
|
40
|
+
- **Built on frimousse**, bundled and lazy-loaded (own `emoji` chunk, incl. the dataset). Needs no consumer peer install and no React-19 peer workaround. Don't pre-import unless you need it eagerly.
|
|
41
|
+
- **Native emoji only.** Each platform renders its own glyphs. Theme follows the surrounding `.dark` class via DS tokens automatically — no theme prop.
|
|
42
|
+
- **Built-in footer:** a live preview of the hovered/active emoji (frimousse `ActiveEmoji`) plus a skin-tone selector (`SkinToneSelector`) — no props needed.
|
|
43
|
+
- **Dataset source / CSP:** frimousse fetches the emoji dataset from jsdelivr at runtime. To remove the CDN dependency (strict CSP, offline, air-gapped), copy `node_modules/emojibase-data` into your app's `public/emojibase` and pass `emojibaseUrl="/emojibase"`.
|
|
43
44
|
- **Trigger composition:** EmojiPickerPopover's `children` is the trigger — wrap any Button/IconButton. Typical pairing is an icon-only IconButton with a 😀 label.
|
|
44
|
-
- **TipTap integration:** Use `createEmojiSuggestion(
|
|
45
|
-
- **Theme matching:** `theme="auto"` reads the `.dark` class on `<html>` — matches the DS dark mode toggle automatically. Override with explicit light/dark.
|
|
46
|
-
- **Emoji sets:** Pass `set="apple" | "google" | ...` for consistent cross-platform emoji art (defaults to native OS glyphs).
|
|
45
|
+
- **TipTap integration:** Use `createEmojiSuggestion()` (from `./extensions/emoji-suggestion`) to open a `:shortcode:` suggestion list. Works with RichChatInput and RichTextEditor.
|
|
47
46
|
|
|
48
47
|
## Gotchas
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
48
|
+
- Client-only (fetches its emoji dataset on first open) — not server-safe.
|
|
49
|
+
- EmojiPickerPopover auto-closes after selection.
|
|
50
|
+
- The `set` / `theme` / `previewPosition` / `skinTonePosition` props are deprecated no-ops kept for source compatibility; the picker is native-only.
|
|
52
51
|
|
|
53
52
|
## Changes
|
|
54
53
|
|
|
54
|
+
### v-next
|
|
55
|
+
- **Breaking** picker migrated from `@emoji-mart/react` to `frimousse` — native emoji only. Art-style sets (apple/google/twitter/facebook) removed; `set`/`theme`/`previewPosition`/`skinTonePosition` are now no-ops.
|
|
56
|
+
- **Breaking** `emojiDataLoaders` export removed; `EmojiNodeAttrs` narrowed to `{ id, native }`; `createEmojiSuggestion()` takes no argument; `EmojiSuggestionItem` no longer has `x`/`y`.
|
|
57
|
+
- **Changed** frimousse + `@emoji-mart/data` are now bundled (lazy `emoji` chunk) — the emoji feature needs zero consumer peer installs.
|
|
58
|
+
- **Added** built-in footer: active-emoji preview + skin-tone selector. New `emojibaseUrl` prop to self-host the dataset (remove the runtime CDN dependency).
|
|
59
|
+
|
|
55
60
|
### v0.33.0
|
|
56
|
-
- **Added** `
|
|
57
|
-
- **
|
|
58
|
-
- **Added** `createEmojiSuggestion(set?)` factory — replaces `EmojiSuggestion` named export
|
|
59
|
-
- **Added** `EmojiSet` type exported from barrel
|
|
60
|
-
- **Breaking** `EmojiSuggestion` named export removed — use `createEmojiSuggestion()` factory
|
|
61
|
+
- **Added** `EmojiNode` TipTap extension and `createEmojiSuggestion()` factory.
|
|
62
|
+
- **Breaking** `EmojiSuggestion` named export removed — use `createEmojiSuggestion()` factory.
|
|
@@ -58,7 +58,7 @@ MentionItem: { id: string; label: string; avatar?: string }
|
|
|
58
58
|
|
|
59
59
|
## Gotchas
|
|
60
60
|
- Tiptap is bundled — no need to install `@tiptap/*` packages separately
|
|
61
|
-
- Emoji picker
|
|
61
|
+
- Emoji picker (frimousse) + its dataset are bundled too — no emoji peers to install (native-only since the frimousse migration)
|
|
62
62
|
- Images without `onImageUpload` are stored as base64 in HTML — large images bloat content
|
|
63
63
|
- Mention rendering in viewer always works (no mention props needed, just the HTML)
|
|
64
64
|
- Features: bold, italic, underline, strikethrough, highlight, headings, blockquote, lists, task lists, code, links, images, file attachments, mentions, emoji, text alignment, horizontal rule
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<TableCell><Badge color="success">Active</Badge></TableCell>
|
|
19
19
|
<TableCell>
|
|
20
20
|
<TableRowActions>
|
|
21
|
-
<IconButton className="relative z-[1]" size="
|
|
21
|
+
<IconButton className="relative z-[1]" size="sm" variant="ghost" aria-label={`Actions for ${name}`} icon={<IconDots />} />
|
|
22
22
|
</TableRowActions>
|
|
23
23
|
</TableCell>
|
|
24
24
|
</TableRow>
|
package/docs/recipes/index.md
CHANGED
|
@@ -21,7 +21,7 @@ Pick the recipe that matches your framework. Detection criteria are listed at th
|
|
|
21
21
|
| Vite + React | [install-vite.md](./install-vite.md) | `vite.config.*` exists; SPA or MPA |
|
|
22
22
|
| Astro | [install-astro.md](./install-astro.md) | `astro.config.*` exists |
|
|
23
23
|
| Remix | [install-remix.md](./install-remix.md) | `remix.config.*` or Remix v2 with Vite |
|
|
24
|
-
| TanStack Start | [install-tanstack-start.md](./install-tanstack-start.md) | `app.config.*`
|
|
24
|
+
| TanStack Start | [install-tanstack-start.md](./install-tanstack-start.md) | `@tanstack/react-start` + `vite.config.*` (the current Vite-plugin setup; the retired Vinxi `app.config.*` + `@tanstack/start` must migrate first) |
|
|
25
25
|
|
|
26
26
|
If the framework is not listed, start with [install-vite.md](./install-vite.md) and adapt — most React+Tailwind setups follow the same shape.
|
|
27
27
|
|
|
@@ -31,18 +31,21 @@ pnpm add sonner
|
|
|
31
31
|
|
|
32
32
|
### 2a. Optional peer dependencies (install ONLY when importing the matching subpath)
|
|
33
33
|
|
|
34
|
-
Some components ship hard peers as optional. **Install BEFORE first import
|
|
35
|
-
|
|
36
|
-
| When you import…
|
|
37
|
-
|
|
38
|
-
| `@devalok/shilp-sutra/
|
|
39
|
-
| `@devalok/shilp-sutra/
|
|
40
|
-
| `@devalok/shilp-sutra/composed/
|
|
41
|
-
| `@devalok/shilp-sutra/composed/
|
|
42
|
-
| `@devalok/shilp-sutra/ui/
|
|
43
|
-
| `@devalok/shilp-sutra/
|
|
44
|
-
| `@devalok/shilp-sutra/
|
|
45
|
-
|
|
|
34
|
+
Some components ship hard peers as optional. **Install BEFORE first import.** ⚠ On Vite / Rolldown a missing peer may **not** fail the build — the bundler can silently replace the import with a stub that throws `Could not resolve "…"` in the browser at runtime, while the build still exits 0. A green build is therefore **not** proof the app works. Confirm coverage with the MCP `verify_setup` / `preflight` tools or the table below. Skip only if you use core components.
|
|
35
|
+
|
|
36
|
+
| When you import… | Install |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `@devalok/shilp-sutra/composed/date-picker` | `pnpm add date-fns` |
|
|
39
|
+
| `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
|
|
40
|
+
| `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter remark-gfm` |
|
|
41
|
+
| `@devalok/shilp-sutra/composed/schedule-view` | `pnpm add date-fns` |
|
|
42
|
+
| `@devalok/shilp-sutra/ui/charts` | `pnpm add d3-axis d3-scale d3-selection d3-shape` |
|
|
43
|
+
| `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
|
|
44
|
+
| `@devalok/shilp-sutra/ui/data-table-toolbar` | `pnpm add @tanstack/react-table` |
|
|
45
|
+
| `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
|
|
46
|
+
| `@devalok/shilp-sutra/ui/toast` | `pnpm add sonner` |
|
|
47
|
+
| `@devalok/shilp-sutra/ui/toaster` | `pnpm add sonner` |
|
|
48
|
+
| Any `Icon` / `IconButton` with Tabler icons (near-universal — most components use icons internally, so it is a base-install peer) | `pnpm add @tabler/icons-react` |
|
|
46
49
|
|
|
47
50
|
## 3. Wire Tailwind 4 in `astro.config`
|
|
48
51
|
|
|
@@ -46,16 +46,19 @@ pnpm add sonner
|
|
|
46
46
|
|
|
47
47
|
Some components depend on third-party libraries that ship as optional peers. **Install BEFORE first import** of the matching component, or `next build` will exit with `Module not found`. Skip entirely if you only use core components (`Button`, `Text`, `Stack`, `Dialog`, `Toast`, `Form*`, `Input`, `Card`, etc.).
|
|
48
48
|
|
|
49
|
-
| When you import…
|
|
50
|
-
|
|
51
|
-
| `@devalok/shilp-sutra/
|
|
52
|
-
| `@devalok/shilp-sutra/
|
|
53
|
-
| `@devalok/shilp-sutra/composed/
|
|
54
|
-
| `@devalok/shilp-sutra/composed/
|
|
55
|
-
| `@devalok/shilp-sutra/ui/
|
|
56
|
-
| `@devalok/shilp-sutra/
|
|
57
|
-
| `@devalok/shilp-sutra/
|
|
58
|
-
|
|
|
49
|
+
| When you import… | Install |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `@devalok/shilp-sutra/composed/date-picker` | `pnpm add date-fns` |
|
|
52
|
+
| `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
|
|
53
|
+
| `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter remark-gfm` |
|
|
54
|
+
| `@devalok/shilp-sutra/composed/schedule-view` | `pnpm add date-fns` |
|
|
55
|
+
| `@devalok/shilp-sutra/ui/charts` | `pnpm add d3-axis d3-scale d3-selection d3-shape` |
|
|
56
|
+
| `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
|
|
57
|
+
| `@devalok/shilp-sutra/ui/data-table-toolbar` | `pnpm add @tanstack/react-table` |
|
|
58
|
+
| `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
|
|
59
|
+
| `@devalok/shilp-sutra/ui/toast` | `pnpm add sonner` |
|
|
60
|
+
| `@devalok/shilp-sutra/ui/toaster` | `pnpm add sonner` |
|
|
61
|
+
| Any `Icon` / `IconButton` with Tabler icons (near-universal — most components use icons internally, so it is a base-install peer) | `pnpm add @tabler/icons-react` |
|
|
59
62
|
|
|
60
63
|
> These aren't in core deps so consumers who never render a chart, OTP input, or rich-text editor don't pay the install / bundle cost. One-time decision at install.
|
|
61
64
|
|
|
@@ -164,9 +167,14 @@ export function Providers({ children }: { children: ReactNode }) {
|
|
|
164
167
|
}
|
|
165
168
|
```
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
> ⚠ **This default `Providers` renders `<Toaster />`, which imports `sonner`.** Install it now or `next build` fails with `Module not found: Can't resolve 'sonner'`:
|
|
171
|
+
> ```bash
|
|
172
|
+
> pnpm add sonner
|
|
173
|
+
> ```
|
|
168
174
|
|
|
169
|
-
-
|
|
175
|
+
`next-themes` is already in the install list from § 2 — no extra install step needed here. If you do **not** want toasts:
|
|
176
|
+
|
|
177
|
+
- Drop the `Toaster` import and its JSX usage from `Providers`
|
|
170
178
|
- Skip installing `sonner`
|
|
171
179
|
|
|
172
180
|
Mount `<Providers>` from `app/layout.tsx` (or `src/app/layout.tsx`). **Replace the scaffold's layout** with the version below — the scaffold imports `next/font/google` (Geist) and applies font-variable classes to `<html>`, which you don't need when shilp-sutra ships its own fonts:
|
|
@@ -16,7 +16,7 @@ If both `app/` and `pages/` exist, prefer [install-next-app-router.md](./install
|
|
|
16
16
|
|
|
17
17
|
Same dependencies as the App Router recipe — see [install-next-app-router.md § 2](./install-next-app-router.md#2-install-dependencies). Replace `next-themes`'s `attribute="class"` setup with the same on Pages Router (it works identically).
|
|
18
18
|
|
|
19
|
-
**Optional peer dependencies** — if you'll render `<DataTable>`, any chart, `<DatePicker>`, `<
|
|
19
|
+
**Optional peer dependencies** — if you'll render `<DataTable>`, any chart, `<DatePicker>`, `<ScheduleView>`, `<InputOTP>`, `<FilePreview>`, `<MarkdownViewer>`, or `<Toaster>`, install the matching peers BEFORE first import. (The emoji picker and rich-text editors bundle their deps — no install needed; `@tabler/icons-react` auto-installs as a required peer.) Full table at [install-next-app-router.md § 2a](./install-next-app-router.md#2a-optional-peer-dependencies-install-only-when-importing-the-matching-subpath) — identical for Pages Router.
|
|
20
20
|
|
|
21
21
|
## 3. PostCSS
|
|
22
22
|
|
|
@@ -67,6 +67,8 @@ export default function App({ Component, pageProps }: AppProps) {
|
|
|
67
67
|
}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
> ⚠ **This renders `<Toaster />`, which imports `sonner`.** Install it or `next build` fails with `Module not found: Can't resolve 'sonner'`: `pnpm add sonner`. If you don't want toasts, drop the `Toaster` import + usage and skip `sonner`.
|
|
71
|
+
|
|
70
72
|
For `next-themes` to avoid hydration warnings, add a `_document.tsx` with `suppressHydrationWarning` on the `<html>`:
|
|
71
73
|
|
|
72
74
|
```tsx
|
|
@@ -10,7 +10,7 @@ You are in this recipe if:
|
|
|
10
10
|
- `vite.config.{ts,js}` exists with the `vitePlugin` from `@remix-run/dev`
|
|
11
11
|
- `app/root.tsx` exists with `<Outlet />` inside `<Document>` shell
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
> **New projects scaffold as React Router v7, not Remix.** `create-remix` is deprecated and redirects to `create-react-router`; Remix v2 was upstreamed into React Router (maintenance mode). If you ran `create-react-router`, you have an RR7 **framework-mode** app (`@react-router/dev` + `react-router build` + `app/root.tsx` + `ssr: true`) — verified cold with shilp-sutra (SSR render, HTTP 200). Its wiring is the same shape as this recipe: `@tailwindcss/vite` plugin, `@import "@devalok/shilp-sutra/css"` in `app/app.css`, and **no `transpilePackages`** (Vite resolves our ESM). Ignore the SPA-only steps in [install-vite.md](./install-vite.md) (`index.html` bootstrap, `main.tsx` `createRoot`) — RR7 framework mode has its own `root.tsx` shell, like this recipe.
|
|
14
14
|
|
|
15
15
|
## 2. Install
|
|
16
16
|
|
|
@@ -27,18 +27,21 @@ pnpm add sonner # only if rendering <Toaster />
|
|
|
27
27
|
|
|
28
28
|
### 2a. Optional peer dependencies (install ONLY when importing the matching subpath)
|
|
29
29
|
|
|
30
|
-
Some components ship hard peers as optional. **Install BEFORE first import
|
|
31
|
-
|
|
32
|
-
| When you import…
|
|
33
|
-
|
|
34
|
-
| `@devalok/shilp-sutra/
|
|
35
|
-
| `@devalok/shilp-sutra/
|
|
36
|
-
| `@devalok/shilp-sutra/composed/
|
|
37
|
-
| `@devalok/shilp-sutra/composed/
|
|
38
|
-
| `@devalok/shilp-sutra/ui/
|
|
39
|
-
| `@devalok/shilp-sutra/
|
|
40
|
-
| `@devalok/shilp-sutra/
|
|
41
|
-
|
|
|
30
|
+
Some components ship hard peers as optional. **Install BEFORE first import.** ⚠ On Vite 8 / Rolldown a missing peer does **not** fail the build — Rolldown silently replaces the import with a stub that throws `Could not resolve "…"` in the browser at runtime, while the build still exits 0. A green build is therefore **not** proof the app works. Confirm coverage with the MCP `verify_setup` / `preflight` tools or the table below. Skip only if you use core components.
|
|
31
|
+
|
|
32
|
+
| When you import… | Install |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `@devalok/shilp-sutra/composed/date-picker` | `pnpm add date-fns` |
|
|
35
|
+
| `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
|
|
36
|
+
| `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter remark-gfm` |
|
|
37
|
+
| `@devalok/shilp-sutra/composed/schedule-view` | `pnpm add date-fns` |
|
|
38
|
+
| `@devalok/shilp-sutra/ui/charts` | `pnpm add d3-axis d3-scale d3-selection d3-shape` |
|
|
39
|
+
| `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
|
|
40
|
+
| `@devalok/shilp-sutra/ui/data-table-toolbar` | `pnpm add @tanstack/react-table` |
|
|
41
|
+
| `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
|
|
42
|
+
| `@devalok/shilp-sutra/ui/toast` | `pnpm add sonner` |
|
|
43
|
+
| `@devalok/shilp-sutra/ui/toaster` | `pnpm add sonner` |
|
|
44
|
+
| Any `Icon` / `IconButton` with Tabler icons (near-universal — most components use icons internally, so it is a base-install peer) | `pnpm add @tabler/icons-react` |
|
|
42
45
|
|
|
43
46
|
## 3. Wire Tailwind 4 in `vite.config.ts`
|
|
44
47
|
|
|
@@ -14,6 +14,8 @@ You are in this recipe if:
|
|
|
14
14
|
|
|
15
15
|
If instead you see `app.config.ts` + `@tanstack/start`, that is the legacy Vinxi setup — this recipe does not apply until you migrate.
|
|
16
16
|
|
|
17
|
+
> **Scaffolded with `create-start` and got a Router SPA?** As of `@tanstack/create-start` 0.59, the default template is a TanStack **Router** SPA — Vite + `@tanstack/react-router` + an `index.html` + `src/main.tsx` (client `createRoot`), with **no** `@tanstack/react-start` and no SSR server entry. That is NOT the Start SSR setup this recipe covers — use [install-vite.md](./install-vite.md) instead (it is router-agnostic and covers TanStack Router SPAs cleanly). This recipe applies only when `@tanstack/react-start` is a dependency.
|
|
18
|
+
|
|
17
19
|
## 2. Install
|
|
18
20
|
|
|
19
21
|
```bash
|
|
@@ -34,18 +36,21 @@ pnpm add sonner # only if rendering <Toaster />
|
|
|
34
36
|
|
|
35
37
|
### 2a. Optional peer dependencies (install ONLY when importing the matching subpath)
|
|
36
38
|
|
|
37
|
-
Some components ship hard peers as optional. **Install BEFORE first import
|
|
38
|
-
|
|
39
|
-
| When you import…
|
|
40
|
-
|
|
41
|
-
| `@devalok/shilp-sutra/
|
|
42
|
-
| `@devalok/shilp-sutra/
|
|
43
|
-
| `@devalok/shilp-sutra/composed/
|
|
44
|
-
| `@devalok/shilp-sutra/composed/
|
|
45
|
-
| `@devalok/shilp-sutra/ui/
|
|
46
|
-
| `@devalok/shilp-sutra/
|
|
47
|
-
| `@devalok/shilp-sutra/
|
|
48
|
-
|
|
|
39
|
+
Some components ship hard peers as optional. **Install BEFORE first import.** ⚠ On Vite 8 / Rolldown a missing peer does **not** fail the build — Rolldown silently replaces the import with a stub that throws `Could not resolve "…"` in the browser at runtime, while the build still exits 0. A green build is therefore **not** proof the app works. Confirm coverage with the MCP `verify_setup` / `preflight` tools or the table below. Skip only if you use core components.
|
|
40
|
+
|
|
41
|
+
| When you import… | Install |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `@devalok/shilp-sutra/composed/date-picker` | `pnpm add date-fns` |
|
|
44
|
+
| `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
|
|
45
|
+
| `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter remark-gfm` |
|
|
46
|
+
| `@devalok/shilp-sutra/composed/schedule-view` | `pnpm add date-fns` |
|
|
47
|
+
| `@devalok/shilp-sutra/ui/charts` | `pnpm add d3-axis d3-scale d3-selection d3-shape` |
|
|
48
|
+
| `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
|
|
49
|
+
| `@devalok/shilp-sutra/ui/data-table-toolbar` | `pnpm add @tanstack/react-table` |
|
|
50
|
+
| `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
|
|
51
|
+
| `@devalok/shilp-sutra/ui/toast` | `pnpm add sonner` |
|
|
52
|
+
| `@devalok/shilp-sutra/ui/toaster` | `pnpm add sonner` |
|
|
53
|
+
| Any `Icon` / `IconButton` with Tabler icons (near-universal — most components use icons internally, so it is a base-install peer) | `pnpm add @tabler/icons-react` |
|
|
49
54
|
|
|
50
55
|
## 3. Wire Tailwind 4 in `vite.config.ts`
|
|
51
56
|
|
|
@@ -115,6 +120,8 @@ function RootComponent() {
|
|
|
115
120
|
}
|
|
116
121
|
```
|
|
117
122
|
|
|
123
|
+
> **Newer scaffolds use `shellComponent`.** As of `@tanstack/create-start` 0.59 the generated `__root.tsx` uses `shellComponent: RootDocument` (which receives `{ children }`) instead of `component: RootComponent` with `<Outlet />`. Both wire up the same way for shilp-sutra — put the `{ rel: "stylesheet", href: appCss }` link in `head()` and keep `<HeadContent />` + `<Scripts />`. If your `__root.tsx` already has a `shellComponent`, add the stylesheet link to its existing `head()` rather than replacing the component. (Verified cold: shilp-sutra components — Button, Text, MarkdownViewer, EmojiPickerPopover — SSR-render cleanly under TanStack Start, HTTP 200.)
|
|
124
|
+
|
|
118
125
|
## 5. Theme toggle
|
|
119
126
|
|
|
120
127
|
Add a pre-hydration bootstrap so there is no flash of the wrong theme. The cleanest place is a `scripts` entry on the root route (runs before hydration); a static `public/theme-bootstrap.js` referenced from `<head>` also works and is CSP-friendly.
|
|
@@ -42,18 +42,21 @@ pnpm add sonner
|
|
|
42
42
|
|
|
43
43
|
### 2a. Optional peer dependencies (install ONLY when importing the matching subpath)
|
|
44
44
|
|
|
45
|
-
Some components ship hard peers as optional. **Install BEFORE first import**
|
|
46
|
-
|
|
47
|
-
| When you import…
|
|
48
|
-
|
|
49
|
-
| `@devalok/shilp-sutra/
|
|
50
|
-
| `@devalok/shilp-sutra/
|
|
51
|
-
| `@devalok/shilp-sutra/composed/
|
|
52
|
-
| `@devalok/shilp-sutra/composed/
|
|
53
|
-
| `@devalok/shilp-sutra/ui/
|
|
54
|
-
| `@devalok/shilp-sutra/
|
|
55
|
-
| `@devalok/shilp-sutra/
|
|
56
|
-
|
|
|
45
|
+
Some components ship hard peers as optional. **Install BEFORE first import.** ⚠ On Vite 8 / Rolldown a missing peer does **not** fail the build — Rolldown silently replaces the import with a stub that throws `Could not resolve "…"` in the browser at runtime, while `vite build` still exits 0. A green build is therefore **not** proof the app works. Confirm coverage with the MCP `verify_setup` / `preflight` tools or the table below. Skip only if you use core components.
|
|
46
|
+
|
|
47
|
+
| When you import… | Install |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `@devalok/shilp-sutra/composed/date-picker` | `pnpm add date-fns` |
|
|
50
|
+
| `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
|
|
51
|
+
| `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter remark-gfm` |
|
|
52
|
+
| `@devalok/shilp-sutra/composed/schedule-view` | `pnpm add date-fns` |
|
|
53
|
+
| `@devalok/shilp-sutra/ui/charts` | `pnpm add d3-axis d3-scale d3-selection d3-shape` |
|
|
54
|
+
| `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
|
|
55
|
+
| `@devalok/shilp-sutra/ui/data-table-toolbar` | `pnpm add @tanstack/react-table` |
|
|
56
|
+
| `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
|
|
57
|
+
| `@devalok/shilp-sutra/ui/toast` | `pnpm add sonner` |
|
|
58
|
+
| `@devalok/shilp-sutra/ui/toaster` | `pnpm add sonner` |
|
|
59
|
+
| Any `Icon` / `IconButton` with Tabler icons (near-universal — most components use icons internally, so it is a base-install peer) | `pnpm add @tabler/icons-react` |
|
|
57
60
|
|
|
58
61
|
## 3. Wire Tailwind 4 in `vite.config.ts`
|
|
59
62
|
|
|
@@ -136,7 +139,7 @@ If you installed `sonner`, mount the Toaster once near the app root:
|
|
|
136
139
|
import { StrictMode } from "react";
|
|
137
140
|
import { createRoot } from "react-dom/client";
|
|
138
141
|
import { Toaster } from "@devalok/shilp-sutra/ui/toaster";
|
|
139
|
-
import
|
|
142
|
+
import App from "./App";
|
|
140
143
|
import "./index.css";
|
|
141
144
|
|
|
142
145
|
createRoot(document.getElementById("root")!).render(
|
|
@@ -149,14 +152,14 @@ createRoot(document.getElementById("root")!).render(
|
|
|
149
152
|
|
|
150
153
|
## 7. Verify
|
|
151
154
|
|
|
152
|
-
Replace `src/App.tsx
|
|
155
|
+
Replace `src/App.tsx` (keep the **default export** — the `create-vite` template's `main.tsx` imports it as `import App from "./App"`, so a named export would break the build with `TS2613: Module has no default export`):
|
|
153
156
|
|
|
154
157
|
```tsx
|
|
155
158
|
import { Button } from "@devalok/shilp-sutra/ui/button";
|
|
156
159
|
import { Stack } from "@devalok/shilp-sutra/ui/stack";
|
|
157
160
|
import { Text } from "@devalok/shilp-sutra/ui/text";
|
|
158
161
|
|
|
159
|
-
export function App() {
|
|
162
|
+
export default function App() {
|
|
160
163
|
return (
|
|
161
164
|
<Stack className="p-ds-08" gap="ds-04">
|
|
162
165
|
<Text variant="heading-2xl">Hello, Shilp Sutra</Text>
|