@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.
Files changed (53) hide show
  1. package/AGENTS.md +36 -12
  2. package/BREAKING.json +46 -0
  3. package/MIGRATION.md +35 -0
  4. package/dist/_chunks/emoji-suggestion.js +124 -0
  5. package/dist/_chunks/emoji-suggestion.js.map +1 -0
  6. package/dist/_chunks/emoji.js +1115 -0
  7. package/dist/_chunks/emoji.js.map +1 -0
  8. package/dist/_chunks/tiptap.js.map +1 -1
  9. package/dist/composed/emoji-picker.d.ts +19 -9
  10. package/dist/composed/emoji-picker.d.ts.map +1 -1
  11. package/dist/composed/emoji-picker.js +87 -68
  12. package/dist/composed/emoji-picker.js.map +1 -1
  13. package/dist/composed/extensions/emoji-data.d.ts +1 -14
  14. package/dist/composed/extensions/emoji-data.d.ts.map +1 -1
  15. package/dist/composed/extensions/emoji-node.d.ts +0 -3
  16. package/dist/composed/extensions/emoji-node.d.ts.map +1 -1
  17. package/dist/composed/extensions/emoji-node.js +9 -34
  18. package/dist/composed/extensions/emoji-node.js.map +1 -1
  19. package/dist/composed/extensions/emoji-suggestion.d.ts +2 -2
  20. package/dist/composed/extensions/emoji-suggestion.d.ts.map +1 -1
  21. package/dist/composed/extensions/emoji-suggestion.js +2 -116
  22. package/dist/composed/rich-chat-input.d.ts.map +1 -1
  23. package/dist/composed/rich-chat-input.js +554 -575
  24. package/dist/composed/rich-chat-input.js.map +1 -1
  25. package/dist/composed/rich-text-editor.d.ts.map +1 -1
  26. package/dist/composed/rich-text-editor.js +167 -190
  27. package/dist/composed/rich-text-editor.js.map +1 -1
  28. package/docs/components/composed/emoji-picker.md +19 -17
  29. package/docs/components/composed/rich-text-editor.md +1 -1
  30. package/docs/components/ui/table-row-link.md +1 -1
  31. package/docs/recipes/index.md +1 -1
  32. package/docs/recipes/install-astro.md +15 -12
  33. package/docs/recipes/install-next-app-router.md +20 -12
  34. package/docs/recipes/install-next-pages.md +3 -1
  35. package/docs/recipes/install-remix.md +16 -13
  36. package/docs/recipes/install-tanstack-start.md +19 -12
  37. package/docs/recipes/install-vite.md +18 -15
  38. package/docs/recipes/troubleshoot.md +7 -8
  39. package/llms.txt +2 -2
  40. package/mcp-manifest.json +29 -66
  41. package/package.json +6 -9
  42. package/skill/SKILL.md +1 -1
  43. package/skill/references/components.md +2 -2
  44. package/skill/references/setup-astro.md +15 -12
  45. package/skill/references/setup-next-app-router.md +20 -12
  46. package/skill/references/setup-next-pages.md +3 -1
  47. package/skill/references/setup-remix.md +16 -13
  48. package/skill/references/setup-tanstack-start.md +19 -12
  49. package/skill/references/setup-vite.md +18 -15
  50. package/skill/references/troubleshoot.md +7 -8
  51. package/dist/_chunks/emoji-data.js +0 -45
  52. package/dist/_chunks/emoji-data.js.map +0 -1
  53. 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
- theme: "auto" | "light" | "dark"
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
- theme="auto", previewPosition="none", skinTonePosition="search", align="start"
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} theme="dark" />
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
- - **Wraps @emoji-mart/react**, lazy-loaded with a Skeleton placeholder while the ~200KB bundle fetches. Don't pre-import unless you need it eagerly.
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(set?)` factory to create a TipTap suggestion plugin that opens the picker on typing `:emoji`. Works with RichChatInput and RichTextEditor.
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
- - Wraps `@emoji-mart/react` which is lazy-loaded shows a Skeleton placeholder while loading
50
- - `theme="auto"` reads the `.dark` class on `<html>` to pick light/dark
51
- - EmojiPickerPopover auto-closes after selection
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** `set` prop on EmojiPicker and EmojiPickerPopover — `EmojiSet` type: 'native' | 'apple' | 'google' | 'twitter' | 'facebook'
57
- - **Added** `EmojiNode` TipTap extensioninline atom node with spritesheet rendering for consistent emoji art styles
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 requires `@emoji-mart/react` + `@emoji-mart/data` peers
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="xs" variant="ghost" aria-label={`Actions for ${name}`} icon={<IconDots />} />
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>
@@ -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.*` with `@tanstack/start` |
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** or Astro's Vite build will fail with `Failed to resolve import`. Skip if you only use core components.
35
-
36
- | When you import… | Install |
37
- |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
38
- | `@devalok/shilp-sutra/ui/charts/*` | `pnpm add d3-array d3-axis d3-format d3-interpolate d3-scale d3-selection d3-shape d3-time-format d3-transition` |
39
- | `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
40
- | `@devalok/shilp-sutra/composed/date-picker` (+ DateRange, DateTime, Calendar) | `pnpm add date-fns` |
41
- | `@devalok/shilp-sutra/composed/rich-text-editor` (+ RichChatInput, RichTextViewer) | `pnpm add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder` |
42
- | `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
43
- | `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
44
- | `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter` |
45
- | Any `Icon` / `IconButton` with Tabler icons | `pnpm add @tabler/icons-react` |
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… | Install |
50
- |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
51
- | `@devalok/shilp-sutra/ui/charts/*` (BarChart, LineChart, …)| `pnpm add d3-array d3-axis d3-format d3-interpolate d3-scale d3-selection d3-shape d3-time-format d3-transition` |
52
- | `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
53
- | `@devalok/shilp-sutra/composed/date-picker` (+ DateRange, DateTime, Calendar) | `pnpm add date-fns` |
54
- | `@devalok/shilp-sutra/composed/rich-text-editor` (+ RichChatInput, RichTextViewer) | `pnpm add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder` |
55
- | `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
56
- | `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
57
- | `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter` |
58
- | Any `Icon` / `IconButton` use with Tabler icons | `pnpm add @tabler/icons-react` |
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
- `next-themes` is already in the install list from § 2 no extra install step needed here. If `<Toaster />` is not used:
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
- - Drop the `Toaster` import and its JSX usage
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>`, `<RichTextEditor>`, `<InputOTP>`, `<FilePreview>`, `<MarkdownViewer>`, or use Tabler icons, install the matching peers BEFORE first import. 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.
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
- For React Router v7 (the spiritual successor to Remix), use the [install-vite.md](./install-vite.md) recipeit works the same way.
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** or Remix's Vite build will fail with `Failed to resolve import`. Skip if you only use core components.
31
-
32
- | When you import… | Install |
33
- |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
34
- | `@devalok/shilp-sutra/ui/charts/*` | `pnpm add d3-array d3-axis d3-format d3-interpolate d3-scale d3-selection d3-shape d3-time-format d3-transition` |
35
- | `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
36
- | `@devalok/shilp-sutra/composed/date-picker` (+ DateRange, DateTime, Calendar) | `pnpm add date-fns` |
37
- | `@devalok/shilp-sutra/composed/rich-text-editor` (+ RichChatInput, RichTextViewer) | `pnpm add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder` |
38
- | `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
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` |
41
- | Any `Icon` / `IconButton` with Tabler icons | `pnpm add @tabler/icons-react` |
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** or the TanStack Start dev/build will fail with `Failed to resolve import`. Skip if you only use core components.
38
-
39
- | When you import… | Install |
40
- |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
41
- | `@devalok/shilp-sutra/ui/charts/*` | `pnpm add d3-array d3-axis d3-format d3-interpolate d3-scale d3-selection d3-shape d3-time-format d3-transition` |
42
- | `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
43
- | `@devalok/shilp-sutra/composed/date-picker` (+ DateRange, DateTime, Calendar) | `pnpm add date-fns` |
44
- | `@devalok/shilp-sutra/composed/rich-text-editor` (+ RichChatInput, RichTextViewer) | `pnpm add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder` |
45
- | `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
46
- | `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
47
- | `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter` |
48
- | Any `Icon` / `IconButton` with Tabler icons | `pnpm add @tabler/icons-react` |
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** or `vite dev` / `vite build` will fail with `Failed to resolve import`. Skip if you only use core components.
46
-
47
- | When you import… | Install |
48
- |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
49
- | `@devalok/shilp-sutra/ui/charts/*` | `pnpm add d3-array d3-axis d3-format d3-interpolate d3-scale d3-selection d3-shape d3-time-format d3-transition` |
50
- | `@devalok/shilp-sutra/ui/data-table` | `pnpm add @tanstack/react-table @tanstack/react-virtual` |
51
- | `@devalok/shilp-sutra/composed/date-picker` (+ DateRange, DateTime, Calendar) | `pnpm add date-fns` |
52
- | `@devalok/shilp-sutra/composed/rich-text-editor` (+ RichChatInput, RichTextViewer) | `pnpm add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder` |
53
- | `@devalok/shilp-sutra/ui/input-otp` | `pnpm add input-otp` |
54
- | `@devalok/shilp-sutra/composed/file-preview` | `pnpm add react-pdf react-zoom-pan-pinch` |
55
- | `@devalok/shilp-sutra/composed/markdown-viewer` | `pnpm add react-markdown react-syntax-highlighter` |
56
- | Any `Icon` / `IconButton` with Tabler icons | `pnpm add @tabler/icons-react` |
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 { App } from "./App";
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>