From 6e86fd9dda9dee257afdc41a4547586fc4a08dd3 Mon Sep 17 00:00:00 2001 From: TeerapatChan Date: Tue, 21 Oct 2025 23:19:25 +0700 Subject: [PATCH 1/4] chore: packages --- examples/erp/package.json | 1 + examples/ui-playground/package.json | 5 ++++ packages/react/package.json | 1 + pnpm-lock.yaml | 44 +++++++++++++++++++++++++++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/examples/erp/package.json b/examples/erp/package.json index 5af1ec27..ea549008 100644 --- a/examples/erp/package.json +++ b/examples/erp/package.json @@ -30,6 +30,7 @@ "@tanstack/react-table": "^8.21.3", "@tiptap/extension-color": "^2.26.3", "@tiptap/extension-image": "^2.26.3", + "@tiptap/extension-link": "2.26.3", "@tiptap/extension-text-align": "^2.26.3", "@tiptap/extension-text-style": "^2.26.3", "@tiptap/extension-underline": "^2.26.3", diff --git a/examples/ui-playground/package.json b/examples/ui-playground/package.json index a79abb00..dcd8ef75 100644 --- a/examples/ui-playground/package.json +++ b/examples/ui-playground/package.json @@ -20,6 +20,11 @@ "@phosphor-icons/react": "^2.1.8", "@tailwindcss/postcss": "^4.1.7", "@tanstack/react-query": "^5.71.5", + "@tiptap/extension-color": "^2.26.3", + "@tiptap/extension-link": "2.26.3", + "@tiptap/extension-text-align": "^2.26.3", + "@tiptap/extension-text-style": "^2.26.3", + "@tiptap/extension-underline": "^2.26.3", "@tiptap/starter-kit": "^2.26.3", "date-fns": "^4.1.0", "next": "15.2.2", diff --git a/packages/react/package.json b/packages/react/package.json index 90ade73d..79532879 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -73,6 +73,7 @@ "@tiptap/core": "^2.26.3", "@tiptap/extension-color": "^2.26.3", "@tiptap/extension-image": "^2.26.3", + "@tiptap/extension-link": "2.26.3", "@tiptap/extension-text-align": "^2.26.3", "@tiptap/extension-text-style": "^2.26.3", "@tiptap/extension-typography": "^2.26.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c5c954f..eeaeecdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: '@tiptap/extension-image': specifier: ^2.26.3 version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-link': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-text-align': specifier: ^2.26.3 version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) @@ -168,6 +171,21 @@ importers: '@tanstack/react-query': specifier: ^5.71.5 version: 5.76.1(react@19.1.0) + '@tiptap/extension-color': + specifier: ^2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-text-style@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))) + '@tiptap/extension-link': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-text-align': + specifier: ^2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-text-style': + specifier: ^2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-underline': + specifier: ^2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/starter-kit': specifier: ^2.26.3 version: 2.26.3 @@ -294,7 +312,7 @@ importers: version: 15.2.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) nuqs: specifier: ^2.4.3 - version: 2.4.3(next@15.2.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 2.4.3(next@15.2.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) radix3: specifier: ^1.1.2 version: 1.1.2 @@ -493,6 +511,9 @@ importers: '@tiptap/extension-image': specifier: ^2.26.3 version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-link': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-text-align': specifier: ^2.26.3 version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) @@ -546,7 +567,7 @@ importers: version: 12.11.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) nuqs: specifier: ^2.4.3 - version: 2.4.3(next@15.2.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 2.4.3(next@15.2.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) radix3: specifier: ^1.1.2 version: 1.1.2 @@ -3072,6 +3093,12 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-link@2.26.3': + resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/extension-list-item@2.26.3': resolution: {integrity: sha512-9qU0SoC+tDSKYhfdWFS3dkioEk3ml1ycBeRmOxh7h+w0ezmTomiT5yvc9t3KM30ps8n1p78sIPo19GF65u1dFQ==} peerDependencies: @@ -4391,6 +4418,9 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8977,6 +9007,12 @@ snapshots: dependencies: '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + linkifyjs: 4.3.2 + '@tiptap/extension-list-item@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': dependencies: '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) @@ -10555,6 +10591,8 @@ snapshots: dependencies: uc.micro: 2.1.0 + linkifyjs@4.3.2: {} + load-tsconfig@0.2.5: {} locate-path@5.0.0: @@ -10712,7 +10750,7 @@ snapshots: node-releases@2.0.19: {} - nuqs@2.4.3(next@15.2.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0): + nuqs@2.4.3(next@15.2.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0): dependencies: mitt: 3.0.1 react: 19.1.0 From d1a667e62d4190e1342066d4e1416c03d3390982 Mon Sep 17 00:00:00 2001 From: TeerapatChan Date: Tue, 21 Oct 2025 23:22:15 +0700 Subject: [PATCH 2/4] feat: edit richtext toolbar designs --- examples/erp/genseki/editor/slot-before.tsx | 153 ++++++++++++++-- .../src/components/slot-before.tsx | 173 ++++++++++++++++++ .../compound/editor/components/editor-bar.tsx | 9 +- .../editor/components/editor-color-picker.tsx | 5 +- .../editor/components/mark-button.tsx | 29 ++- .../editor/components/redo-undo-buttons.tsx | 14 +- .../editor/components/select-text-style.tsx | 6 +- .../editor/components/text-align-buttons.tsx | 84 +++++---- .../editor/components/upload-image-button.tsx | 6 +- .../components/primitives/color-picker.tsx | 2 +- .../react/components/primitives/toolbar.tsx | 17 +- 11 files changed, 418 insertions(+), 80 deletions(-) create mode 100644 examples/ui-playground/src/components/slot-before.tsx diff --git a/examples/erp/genseki/editor/slot-before.tsx b/examples/erp/genseki/editor/slot-before.tsx index ab068c1f..0e40fc1b 100644 --- a/examples/erp/genseki/editor/slot-before.tsx +++ b/examples/erp/genseki/editor/slot-before.tsx @@ -1,56 +1,173 @@ 'use client' +import type React from 'react' + +import Color from '@tiptap/extension-color' +import Link from '@tiptap/extension-link' +import TextAlign from '@tiptap/extension-text-align' +import TextStyle from '@tiptap/extension-text-style' +import Underline from '@tiptap/extension-underline' +import StarterKit from '@tiptap/starter-kit' + import { EditorBar, - EditorBarGroup, - EditorBgColorPicker, EditorTextColorPicker, MarkButton, RedoButton, SelectTextStyle, TextAlignButton, TextAlignButtonsGroup, - ToggleGroup, ToolbarGroup, ToolbarSeparator, UndoButton, UploadImageButton, } from '@genseki/react' +import { + BackColorExtension, + CustomImageExtension, + ImageUploadNodeExtension, + SelectionExtension, +} from '@genseki/react' export const EditorSlotBefore = () => { return ( - - - - - - - - - - + - + - - - + - + + + ) } + +export const postEditorProviderProps = { + immediatelyRender: false, + shouldRerenderOnTransaction: true, + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: '', + }, + ], + }, + ], + }, + slotBefore: , + extensions: [ + Color, + BackColorExtension, + Underline.configure({ HTMLAttributes: { class: 'earth-underline' } }), + SelectionExtension, + TextStyle, + TextAlign.configure({ + types: ['heading', 'paragraph'], + alignments: ['left', 'center', 'right', 'justify'], + defaultAlignment: 'left', + }), + StarterKit.configure({ + bold: { HTMLAttributes: { class: 'bold large-black' } }, + paragraph: { HTMLAttributes: { class: 'paragraph-custom' } }, + heading: { HTMLAttributes: { class: 'heading-custom' } }, + bulletList: { HTMLAttributes: { class: 'list-custom' } }, + orderedList: { HTMLAttributes: { class: 'ordered-list' } }, + code: { HTMLAttributes: { class: 'code' } }, + codeBlock: { HTMLAttributes: { class: 'code-block' } }, + horizontalRule: { HTMLAttributes: { class: 'hr-custom' } }, + italic: { HTMLAttributes: { class: 'italic-text' } }, + strike: { HTMLAttributes: { class: 'strikethrough' } }, + blockquote: { HTMLAttributes: { class: 'blockquote-custom' } }, + }), + CustomImageExtension.configure({ HTMLAttributes: { className: 'image-displayer' } }), + ImageUploadNodeExtension.configure({ + showProgress: false, + accept: 'image/*', + maxSize: 1024 * 1024 * 10, // 10MB + limit: 3, + pathName: 'posts/rich-text', + }), + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: 'https', + protocols: ['http', 'https'], + isAllowedUri: (url, ctx) => { + try { + // construct URL + const parsedUrl = url.includes(':') + ? new URL(url) + : new URL(`${ctx.defaultProtocol}://${url}`) + + // use default validation + if (!ctx.defaultValidate(parsedUrl.href)) { + return false + } + + // disallowed protocols + const disallowedProtocols = ['ftp', 'file', 'mailto'] + const protocol = parsedUrl.protocol.replace(':', '') + + if (disallowedProtocols.includes(protocol)) { + return false + } + + // only allow protocols specified in ctx.protocols + const allowedProtocols = ctx.protocols.map((p) => (typeof p === 'string' ? p : p.scheme)) + + if (!allowedProtocols.includes(protocol)) { + return false + } + + // disallowed domains + const disallowedDomains = ['example-phishing.com', 'malicious-site.net'] + const domain = parsedUrl.hostname + + if (disallowedDomains.includes(domain)) { + return false + } + + // all checks have passed + return true + } catch { + return false + } + }, + shouldAutoLink: (url) => { + try { + // construct URL + const parsedUrl = url.includes(':') ? new URL(url) : new URL(`https://${url}`) + + // only auto-link if the domain is not in the disallowed list + const disallowedDomains = ['example-no-autolink.com', 'another-no-autolink.com'] + const domain = parsedUrl.hostname + + return !disallowedDomains.includes(domain) + } catch { + return false + } + }, + }), + ], +} diff --git a/examples/ui-playground/src/components/slot-before.tsx b/examples/ui-playground/src/components/slot-before.tsx new file mode 100644 index 00000000..bcbdd159 --- /dev/null +++ b/examples/ui-playground/src/components/slot-before.tsx @@ -0,0 +1,173 @@ +'use client' +import type React from 'react' + +import Color from '@tiptap/extension-color' +import Link from '@tiptap/extension-link' +import TextAlign from '@tiptap/extension-text-align' +import TextStyle from '@tiptap/extension-text-style' +import Underline from '@tiptap/extension-underline' +import StarterKit from '@tiptap/starter-kit' + +import { + EditorBar, + EditorTextColorPicker, + MarkButton, + RedoButton, + SelectTextStyle, + TextAlignButton, + TextAlignButtonsGroup, + ToolbarGroup, + ToolbarSeparator, + UndoButton, + UploadImageButton, +} from '@genseki/react' +import { + BackColorExtension, + CustomImageExtension, + ImageUploadNodeExtension, + SelectionExtension, +} from '@genseki/react' + +export const EditorSlotBefore = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const editorProviderProps = { + immediatelyRender: false, + shouldRerenderOnTransaction: true, + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: '', + }, + ], + }, + ], + }, + slotBefore: , + extensions: [ + Color, + BackColorExtension, + Underline.configure({ HTMLAttributes: { class: 'earth-underline' } }), + SelectionExtension, + TextStyle, + TextAlign.configure({ + types: ['heading', 'paragraph'], + alignments: ['left', 'center', 'right', 'justify'], + defaultAlignment: 'left', + }), + StarterKit.configure({ + bold: { HTMLAttributes: { class: 'bold large-black' } }, + paragraph: { HTMLAttributes: { class: 'paragraph-custom' } }, + heading: { HTMLAttributes: { class: 'heading-custom' } }, + bulletList: { HTMLAttributes: { class: 'list-custom' } }, + orderedList: { HTMLAttributes: { class: 'ordered-list' } }, + code: { HTMLAttributes: { class: 'code' } }, + codeBlock: { HTMLAttributes: { class: 'code-block' } }, + horizontalRule: { HTMLAttributes: { class: 'hr-custom' } }, + italic: { HTMLAttributes: { class: 'italic-text' } }, + strike: { HTMLAttributes: { class: 'strikethrough' } }, + blockquote: { HTMLAttributes: { class: 'blockquote-custom' } }, + }), + CustomImageExtension.configure({ HTMLAttributes: { className: 'image-displayer' } }), + ImageUploadNodeExtension.configure({ + showProgress: false, + accept: 'image/*', + maxSize: 1024 * 1024 * 10, // 10MB + limit: 3, + pathName: 'posts/rich-text', + }), + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: 'https', + protocols: ['http', 'https'], + isAllowedUri: (url, ctx) => { + try { + // construct URL + const parsedUrl = url.includes(':') + ? new URL(url) + : new URL(`${ctx.defaultProtocol}://${url}`) + + // use default validation + if (!ctx.defaultValidate(parsedUrl.href)) { + return false + } + + // disallowed protocols + const disallowedProtocols = ['ftp', 'file', 'mailto'] + const protocol = parsedUrl.protocol.replace(':', '') + + if (disallowedProtocols.includes(protocol)) { + return false + } + + // only allow protocols specified in ctx.protocols + const allowedProtocols = ctx.protocols.map((p) => (typeof p === 'string' ? p : p.scheme)) + + if (!allowedProtocols.includes(protocol)) { + return false + } + + // disallowed domains + const disallowedDomains = ['example-phishing.com', 'malicious-site.net'] + const domain = parsedUrl.hostname + + if (disallowedDomains.includes(domain)) { + return false + } + + // all checks have passed + return true + } catch { + return false + } + }, + shouldAutoLink: (url) => { + try { + // construct URL + const parsedUrl = url.includes(':') ? new URL(url) : new URL(`https://${url}`) + + // only auto-link if the domain is not in the disallowed list + const disallowedDomains = ['example-no-autolink.com', 'another-no-autolink.com'] + const domain = parsedUrl.hostname + + return !disallowedDomains.includes(domain) + } catch { + return false + } + }, + }), + ], +} diff --git a/packages/react/src/react/components/compound/editor/components/editor-bar.tsx b/packages/react/src/react/components/compound/editor/components/editor-bar.tsx index 8e4597b1..2166f8cc 100644 --- a/packages/react/src/react/components/compound/editor/components/editor-bar.tsx +++ b/packages/react/src/react/components/compound/editor/components/editor-bar.tsx @@ -17,8 +17,13 @@ export const EditorBar: React.FC<{ className?: string; children?: React.ReactNod if (!editor) throw new Error('Editor not found') return ( -
- {children} +
+ {children}
) } diff --git a/packages/react/src/react/components/compound/editor/components/editor-color-picker.tsx b/packages/react/src/react/components/compound/editor/components/editor-color-picker.tsx index 20f1bb92..e707d49f 100644 --- a/packages/react/src/react/components/compound/editor/components/editor-color-picker.tsx +++ b/packages/react/src/react/components/compound/editor/components/editor-color-picker.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { type Color as ReactAriaColor, parseColor } from 'react-aria-components' -import { SelectionBackgroundIcon, TextAaIcon } from '@phosphor-icons/react' +import { SelectionBackgroundIcon } from '@phosphor-icons/react' import { useCurrentEditor } from '@tiptap/react' import { BaseIcon } from '../../../primitives' @@ -55,8 +55,7 @@ export const EditorTextColorPicker = () => { onPopupClose={(color) => { editor.chain().setColor(color.toString('hex')).run() }} - label={} - buttonClassName="p-4 border border-border/50 bg-secondary/25" + buttonClassName="p-4 bg-secondary/25" /> ) } diff --git a/packages/react/src/react/components/compound/editor/components/mark-button.tsx b/packages/react/src/react/components/compound/editor/components/mark-button.tsx index a2c3629c..608ffb4d 100644 --- a/packages/react/src/react/components/compound/editor/components/mark-button.tsx +++ b/packages/react/src/react/components/compound/editor/components/mark-button.tsx @@ -2,6 +2,7 @@ import { type Icon, + LinkIcon, ListBulletsIcon, TextBolderIcon, TextItalicIcon, @@ -13,7 +14,7 @@ import { useCurrentEditor } from '@tiptap/react' import { BaseIcon } from '../../../primitives/base-icon' import { ToolbarItem } from '../../../primitives/toolbar' -type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList' +type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList' | 'link' type MarkOptions = Record< MarkType, @@ -66,6 +67,27 @@ const useMark = (type: MarkType) => { editor.chain().focus().toggleBulletList().run() }, }, + link: { + label: 'Link', + icon: LinkIcon, + isSelected: editor.isActive('link'), + onClick() { + if (!editor.isActive('link')) { + const { state } = editor + const { from, to } = state.selection + const currentText = state.doc.textBetween(from, to, '') + + editor.chain().focus().extendMarkRange('link').run() + editor + .chain() + .focus() + .setMark('link', { href: currentText || 'https://' }) + .run() + return + } + editor.chain().focus().unsetMark('link').run() + }, + }, } return options[type] @@ -80,8 +102,9 @@ export const MarkButton = (props: { type: MarkType }) => { return ( diff --git a/packages/react/src/react/components/compound/editor/components/redo-undo-buttons.tsx b/packages/react/src/react/components/compound/editor/components/redo-undo-buttons.tsx index 37697aa6..20aeafb0 100644 --- a/packages/react/src/react/components/compound/editor/components/redo-undo-buttons.tsx +++ b/packages/react/src/react/components/compound/editor/components/redo-undo-buttons.tsx @@ -2,8 +2,8 @@ import { ArrowClockwiseIcon, ArrowCounterClockwiseIcon } from '@phosphor-icons/react' import { useCurrentEditor } from '@tiptap/react' +import { Button } from '../../../../../../v2' import { BaseIcon } from '../../../primitives/base-icon' -import { Button } from '../../../primitives/button' export const UndoButton = () => { const { editor } = useCurrentEditor() @@ -12,14 +12,14 @@ export const UndoButton = () => { return ( @@ -33,14 +33,14 @@ export const RedoButton = () => { return ( diff --git a/packages/react/src/react/components/compound/editor/components/select-text-style.tsx b/packages/react/src/react/components/compound/editor/components/select-text-style.tsx index b76d41d1..2978b14c 100644 --- a/packages/react/src/react/components/compound/editor/components/select-text-style.tsx +++ b/packages/react/src/react/components/compound/editor/components/select-text-style.tsx @@ -3,12 +3,12 @@ import React from 'react' import type { Key } from 'react-aria-components' import { - ParagraphIcon, TextHFiveIcon, TextHFourIcon, TextHOneIcon, TextHThreeIcon, TextHTwoIcon, + TextTIcon, } from '@phosphor-icons/react' import type { Editor } from '@tiptap/core' import { useCurrentEditor } from '@tiptap/react' @@ -17,7 +17,7 @@ import { BaseIcon } from '../../../primitives/base-icon' import { Select, SelectList, SelectOption, SelectTrigger } from '../../../primitives/select' const textStylesList = [ - { icon: ParagraphIcon, label: 'Normal', value: 'p', type: 'paragraph' }, + { icon: TextTIcon, label: 'Normal', value: 'p', type: 'paragraph' }, { icon: TextHOneIcon, label: 'Heading 1', value: 'h1', type: 'heading', level: 1 }, { icon: TextHTwoIcon, label: 'Heading 2', value: 'h2', type: 'heading', level: 2 }, { icon: TextHThreeIcon, label: 'Heading 3', value: 'h3', type: 'heading', level: 3 }, @@ -79,7 +79,7 @@ export const SelectTextStyle = () => { aria-label="Select text style" onSelectionChange={selectChange} > - + {(item) => ( diff --git a/packages/react/src/react/components/compound/editor/components/text-align-buttons.tsx b/packages/react/src/react/components/compound/editor/components/text-align-buttons.tsx index d780c30a..773a3c8f 100644 --- a/packages/react/src/react/components/compound/editor/components/text-align-buttons.tsx +++ b/packages/react/src/react/components/compound/editor/components/text-align-buttons.tsx @@ -1,6 +1,6 @@ 'use client' -import React from 'react' -import type { Key } from 'react-aria-components' + +import React, { createContext, useContext } from 'react' import { TextAlignCenterIcon, @@ -10,8 +10,8 @@ import { } from '@phosphor-icons/react' import { useCurrentEditor } from '@tiptap/react' +import { ToggleGroup, ToggleGroupItem } from '../../../../../../v2' import { BaseIcon } from '../../../primitives/base-icon' -import { ToggleGroup } from '../../../primitives/toggle' import { ToolbarItem } from '../../../primitives/toolbar' type TextAlignType = 'left' | 'center' | 'right' | 'justify' @@ -23,54 +23,68 @@ const justifyOptions = { justify: { icon: TextAlignJustifyIcon }, } as const satisfies Record -const useCurrentTextAlign = (): TextAlignType => { - const { editor } = useCurrentEditor() +interface TextAlignGroupContextValue { + currentAlign: TextAlignType + setAlign: (value: TextAlignType) => void +} +const TextAlignGroupContext = createContext(null) + +const useTextAlignGroup = () => { + const ctx = useContext(TextAlignGroupContext) + if (!ctx) throw new Error('TextAlignButton must be used inside TextAlignButtonsGroup') + return ctx +} + +const useCurrentTextAlign = (editor: any): TextAlignType => { if (!editor) throw new Error('Editor provider is missing') - if (editor.isActive({ textAlign: 'left' })) return 'left' if (editor.isActive({ textAlign: 'right' })) return 'right' if (editor.isActive({ textAlign: 'center' })) return 'center' if (editor.isActive({ textAlign: 'justify' })) return 'justify' - return 'left' } -export const TextAlignButton = (props: { type: TextAlignType }) => { - const currentTextAlign = useCurrentTextAlign() +export const TextAlignButtonsGroup = ({ children }: { children: React.ReactNode }) => { + const { editor } = useCurrentEditor() + if (!editor) throw new Error('Editor provider is missing') + + const currentAlign = useCurrentTextAlign(editor) - const isSelected = currentTextAlign === props.type + const handleValueChange = (value: string) => { + if (!value) return + editor.chain().focus().setTextAlign(value).run() + } return ( - - - + + + {children} + + ) } -export const TextAlignButtonsGroup = ({ children }: { children: React.ReactNode }) => { - const { editor } = useCurrentEditor() - const currentTextAlign = useCurrentTextAlign() - - if (!editor) throw new Error('Editor provider is missing') - - const selectionChange = (value: Set) => { - editor.commands.setTextAlign(value.keys().next().value as string) - } +export const TextAlignButton = ({ type }: { type: TextAlignType }) => { + const { currentAlign } = useTextAlignGroup() + const Icon = justifyOptions[type].icon + const isSelected = currentAlign === type return ( - - {children} - + + + + + ) } diff --git a/packages/react/src/react/components/compound/editor/components/upload-image-button.tsx b/packages/react/src/react/components/compound/editor/components/upload-image-button.tsx index 47aa7344..209c3475 100644 --- a/packages/react/src/react/components/compound/editor/components/upload-image-button.tsx +++ b/packages/react/src/react/components/compound/editor/components/upload-image-button.tsx @@ -2,8 +2,8 @@ import { ImageIcon } from '@phosphor-icons/react' import { useCurrentEditor } from '@tiptap/react' +import { Button } from '../../../../../../v2' import { BaseIcon } from '../../../primitives/base-icon' -import { Button } from '../../../primitives/button' export const UploadImageButton = () => { const { editor } = useCurrentEditor() @@ -12,9 +12,9 @@ export const UploadImageButton = () => { return ( ({ orientation: 'horizontal', @@ -55,13 +58,17 @@ const ToolbarGroup = ({ isDisabled, className, ...props }: ToolbarGroupProps) => ) } -type ToggleItemProps = ToggleProps -const ToolbarItem = ({ isDisabled, ref, ...props }: ToggleItemProps) => { +interface ToggleItemProps + extends React.ComponentProps, + VariantProps {} + +const ToolbarItem = ({ disabled, ref, ...props }: ToggleItemProps) => { const context = useContext(ToolbarGroupContext) - const effectiveIsDisabled = isDisabled || context.isDisabled + const effectiveIsDisabled = disabled || context.isDisabled - return + return } + type ToolbarSeparatorProps = SeparatorProps const ToolbarSeparator = ({ className, ...props }: ToolbarSeparatorProps) => { const { orientation } = useContext(ToolbarContext) From 69b5718617fe577583f6953ad7141b9382614372 Mon Sep 17 00:00:00 2001 From: TeerapatChan Date: Tue, 21 Oct 2025 23:23:00 +0700 Subject: [PATCH 3/4] fix: richtext editor design, add link extension --- .../ui-playground/src/app/playground/page.tsx | 163 +----------------- .../ui-playground/src/styles/tailwind.css | 8 + packages/react/src/core/richtext/index.ts | 1 + .../editor/components/rich-text-editor.tsx | 30 +--- .../editor/components/rich-text-provider.tsx | 30 +++- 5 files changed, 48 insertions(+), 184 deletions(-) diff --git a/examples/ui-playground/src/app/playground/page.tsx b/examples/ui-playground/src/app/playground/page.tsx index 6718c229..0a7fd732 100644 --- a/examples/ui-playground/src/app/playground/page.tsx +++ b/examples/ui-playground/src/app/playground/page.tsx @@ -1,25 +1,14 @@ 'use client' -import { Suspense, useState } from 'react' +import { useState } from 'react' -import { IconGallery, IconGrid4, IconLink, IconRedo, IconUndo } from '@intentui/icons' import { - CaretDownIcon, DiscordLogoIcon, GithubLogoIcon, MagnifyingGlassIcon, PlusCircleIcon, - TextAlignCenterIcon, - TextAlignJustifyIcon, - TextAlignLeftIcon, - TextAlignRightIcon, - TextBIcon, - TextItalicIcon, - TextStrikethroughIcon, - TextUnderlineIcon, TrashIcon, } from '@phosphor-icons/react' -import { StarterKit } from '@tiptap/starter-kit' import { useTheme } from 'next-themes' import { @@ -55,6 +44,7 @@ import { Radio, RadioGroup, ReorderGroup, + RichTextEditor, Switch, Tab, Table, @@ -71,15 +61,9 @@ import { Textarea, TextField, TimeField, - ToggleGroup, - Toolbar, - ToolbarGroup, - ToolbarItem, - ToolbarSeparator, Tooltip, TooltipContent, } from '@genseki/react' -import { RichTextEditor } from '@genseki/react' import { BaseIcon } from '@genseki/react' import { Typography } from '@genseki/react' import { Badge } from '@genseki/react' @@ -90,7 +74,6 @@ import { ColorPicker } from '@genseki/react' import { DateField } from '@genseki/react' import { DatePicker } from '@genseki/react' import { ListBox, ListBoxItem, ListBoxItemDetails, ListBoxSection } from '@genseki/react' -import { Menu, MenuContent, MenuItem } from '@genseki/react' import { MultipleSelect, MultipleSelectItem } from '@genseki/react' import { RangeCalendar } from '@genseki/react' import { @@ -104,6 +87,7 @@ import { } from '@genseki/react' import { PlaygroundCard } from '../../components/card' +import { editorProviderProps } from '../../components/slot-before' import { Wrapper } from '../../components/wrapper' const MOCK_OPTIONS = [ @@ -361,145 +345,8 @@ export default function UIPlayground() {
- -
- - - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - - - - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - {({ isSelected }) => ( - <> - {isSelected ? ( - - ) : ( - - )} - - )} - - - - - Spell Check - - - - - - - Undo - - - - Redo - - - - Insert Link - - - - Insert Image - - - - Insert Grid - - - - - -
-
- - Loading...}> - - + +
diff --git a/examples/ui-playground/src/styles/tailwind.css b/examples/ui-playground/src/styles/tailwind.css index cd514763..77f3d1c2 100644 --- a/examples/ui-playground/src/styles/tailwind.css +++ b/examples/ui-playground/src/styles/tailwind.css @@ -2,3 +2,11 @@ @import './tiptap-style.css'; @source "../../node_modules/@genseki/react"; + +.tiptap.ProseMirror { + height: 100%; + min-height: 240px; + padding: 16px; + outline: none; + background-color: var(--color-bluegray-50); +} diff --git a/packages/react/src/core/richtext/index.ts b/packages/react/src/core/richtext/index.ts index 7caf7f50..a6341051 100644 --- a/packages/react/src/core/richtext/index.ts +++ b/packages/react/src/core/richtext/index.ts @@ -39,6 +39,7 @@ const getSanitizedExtensions = (unSanitizedExtensions: Extensions): SanitizedExt case 'color': case 'image': case 'imageUpload': + case 'link': return R.omit(extension, ['config']) default: throw new Error(`Unknown extension name: ${extension.name}`) diff --git a/packages/react/src/react/components/compound/editor/components/rich-text-editor.tsx b/packages/react/src/react/components/compound/editor/components/rich-text-editor.tsx index 8fdf2732..b9e100c7 100644 --- a/packages/react/src/react/components/compound/editor/components/rich-text-editor.tsx +++ b/packages/react/src/react/components/compound/editor/components/rich-text-editor.tsx @@ -7,10 +7,8 @@ import { isDeepEqual } from 'remeda' import { EditorProvider } from './rich-text-provider' -import { cn } from '../../../../utils/cn' -import { focusStyles } from '../../../primitives' import { CustomFieldError } from '../../../primitives/custom-field-error' -import { Description, FieldGroup } from '../../../primitives/field' +import { Description } from '../../../primitives/field' export interface RichTextEditorProps { editorProviderProps: EditorProviderProps value?: string | Content | Content[] @@ -24,8 +22,6 @@ export interface RichTextEditorProps { } export const RichTextEditor = (props: RichTextEditorProps) => { - const isInvalid = !!props.errorMessage - const editor = useEditor({ ...props.editorProviderProps, content: props.value, @@ -42,22 +38,14 @@ export const RichTextEditor = (props: RichTextEditorProps) => { return (
- -
- -
-
+ {props.description && {props.description}}
diff --git a/packages/react/src/react/components/compound/editor/components/rich-text-provider.tsx b/packages/react/src/react/components/compound/editor/components/rich-text-provider.tsx index c94de9bc..b840f9fa 100644 --- a/packages/react/src/react/components/compound/editor/components/rich-text-provider.tsx +++ b/packages/react/src/react/components/compound/editor/components/rich-text-provider.tsx @@ -8,9 +8,15 @@ import { } from '@tiptap/react' import { cn } from '../../../../utils/cn' +import { InputGroup, InputGroupControl } from '../../../primitives' interface EditorProviderPropsWithEditor extends EditorProviderProps { editor?: Editor | null + inputGroupProps?: { + isInvalid?: boolean + isDisabled?: boolean + isPending?: boolean + } } export function EditorProvider({ @@ -19,6 +25,7 @@ export function EditorProvider({ slotBefore, editorContainerProps = {}, editor, + inputGroupProps, ...editorOptions }: EditorProviderPropsWithEditor) { const editorInstance = editor ?? useEditor(editorOptions) @@ -32,11 +39,24 @@ export function EditorProvider({ {slotBefore} {() => ( - + + + + + )} {children} From a4812ace26992a7a95deb17d4e7e85bec008fcd3 Mon Sep 17 00:00:00 2001 From: Benz <105777142+TeerapatChan@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:41:30 +0700 Subject: [PATCH 4/4] Create swift-turtles-pump.md --- .changeset/swift-turtles-pump.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/swift-turtles-pump.md diff --git a/.changeset/swift-turtles-pump.md b/.changeset/swift-turtles-pump.md new file mode 100644 index 00000000..9883b4e4 --- /dev/null +++ b/.changeset/swift-turtles-pump.md @@ -0,0 +1,7 @@ +--- +"@example/erp": patch +"@example/ui-playground": patch +"@genseki/react": patch +--- + +feat: richtext-editor designs and add link-extension