Add workshop generate-link type, Email DNS for domains, and editor polish.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
6be867ea7a
commit
3cbadb4001
@@ -69,7 +69,7 @@
|
||||
background-color: transparent;
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: #0a1628;
|
||||
border-color: var(--primary);
|
||||
border-radius: 12px;
|
||||
color: var(--primary);
|
||||
font-family: var(--font-ui);
|
||||
@@ -89,7 +89,7 @@ button.addBox {
|
||||
background-color: transparent;
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: #0a1628;
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ a.addBox:hover,
|
||||
button.addBox:hover {
|
||||
background-color: rgba(var(--primary-rgb) / 0.08);
|
||||
border-style: dashed;
|
||||
border-color: #0a1628;
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(var(--primary-rgb) / 0.18);
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toolbar button.toolbarBtnActive {
|
||||
background: rgba(var(--primary-rgb) / 0.1);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.dirBtn {
|
||||
width: auto;
|
||||
min-width: 34px;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useRef, useEffect, useState, useCallback, useContext } from 'react'
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
IndentDecrease,
|
||||
IndentIncrease,
|
||||
@@ -17,6 +21,15 @@ import styles from './RichTextEditor.module.css'
|
||||
const INDENT_STEP_PX = 24
|
||||
const INDENT_BLOCK_TAGS = new Set(['P', 'DIV', 'LI', 'BLOCKQUOTE', 'H1', 'H2', 'H3', 'H4'])
|
||||
const DEFAULT_TEXT_COLOR = '#0f172a'
|
||||
|
||||
type TextAlign = 'left' | 'center' | 'right' | 'justify'
|
||||
|
||||
const ALIGN_COMMAND: Record<TextAlign, string> = {
|
||||
left: 'justifyLeft',
|
||||
center: 'justifyCenter',
|
||||
right: 'justifyRight',
|
||||
justify: 'justifyFull',
|
||||
}
|
||||
const SELECTED_IMAGE_CLASS = 'richTextImageSelected'
|
||||
const HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/
|
||||
|
||||
@@ -77,6 +90,7 @@ export function RichTextEditor({
|
||||
const [textColor, setTextColor] = useState(DEFAULT_TEXT_COLOR)
|
||||
const [colorOpen, setColorOpen] = useState(false)
|
||||
const [hexInput, setHexInput] = useState(DEFAULT_TEXT_COLOR)
|
||||
const [currentAlign, setCurrentAlign] = useState<TextAlign | null>(null)
|
||||
|
||||
const syncChange = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
@@ -204,6 +218,48 @@ export function RichTextEditor({
|
||||
return null
|
||||
}
|
||||
|
||||
function readBlockAlign(editor: HTMLElement): TextAlign | null {
|
||||
const block = findIndentBlock(editor)
|
||||
if (!block) return null
|
||||
|
||||
const inline = block.style.textAlign.trim().toLowerCase()
|
||||
if (inline === 'left' || inline === 'center' || inline === 'right' || inline === 'justify') {
|
||||
return inline
|
||||
}
|
||||
|
||||
const computed = getComputedStyle(block).textAlign.toLowerCase()
|
||||
if (computed === 'center') return 'center'
|
||||
if (computed === 'justify') return 'justify'
|
||||
if (computed === 'right') return 'right'
|
||||
if (computed === 'left') return 'left'
|
||||
|
||||
const blockDir = (block.getAttribute('dir') || editor.getAttribute('dir') || 'ltr').toLowerCase()
|
||||
if (computed === 'start') return blockDir === 'rtl' ? 'right' : 'left'
|
||||
if (computed === 'end') return blockDir === 'rtl' ? 'left' : 'right'
|
||||
return null
|
||||
}
|
||||
|
||||
function refreshAlignFromSelection() {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
const selection = window.getSelection()
|
||||
if (!selection || !selection.anchorNode || !editor.contains(selection.anchorNode)) return
|
||||
setCurrentAlign(readBlockAlign(editor))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onSelectionChange() {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
const selection = window.getSelection()
|
||||
if (!selection || !selection.anchorNode || !editor.contains(selection.anchorNode)) return
|
||||
setCurrentAlign(readBlockAlign(editor))
|
||||
}
|
||||
|
||||
document.addEventListener('selectionchange', onSelectionChange)
|
||||
return () => document.removeEventListener('selectionchange', onSelectionChange)
|
||||
}, [])
|
||||
|
||||
function changeIndent(direction: 1 | -1) {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
@@ -245,6 +301,23 @@ export function RichTextEditor({
|
||||
syncChange()
|
||||
}
|
||||
|
||||
function setBlockAlign(align: TextAlign) {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
|
||||
editor.focus()
|
||||
document.execCommand(ALIGN_COMMAND[align], false)
|
||||
|
||||
const block = findIndentBlock(editor)
|
||||
if (block) {
|
||||
block.style.textAlign = align
|
||||
block.removeAttribute('align')
|
||||
}
|
||||
|
||||
setCurrentAlign(align)
|
||||
syncChange()
|
||||
}
|
||||
|
||||
function applyTextColor(color: string) {
|
||||
const normalized = normalizeHexColor(color) ?? DEFAULT_TEXT_COLOR
|
||||
setTextColor(normalized)
|
||||
@@ -435,6 +508,51 @@ export function RichTextEditor({
|
||||
LTR
|
||||
</button>
|
||||
<span className={styles.divider} />
|
||||
<button
|
||||
type="button"
|
||||
className={currentAlign === 'left' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setBlockAlign('left')}
|
||||
title="Align left"
|
||||
aria-label="Align left"
|
||||
aria-pressed={currentAlign === 'left'}
|
||||
>
|
||||
<AlignLeft size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={currentAlign === 'center' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setBlockAlign('center')}
|
||||
title="Align center"
|
||||
aria-label="Align center"
|
||||
aria-pressed={currentAlign === 'center'}
|
||||
>
|
||||
<AlignCenter size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={currentAlign === 'right' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setBlockAlign('right')}
|
||||
title="Align right"
|
||||
aria-label="Align right"
|
||||
aria-pressed={currentAlign === 'right'}
|
||||
>
|
||||
<AlignRight size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={currentAlign === 'justify' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setBlockAlign('justify')}
|
||||
title="Justify"
|
||||
aria-label="Justify"
|
||||
aria-pressed={currentAlign === 'justify'}
|
||||
>
|
||||
<AlignJustify size={16} />
|
||||
</button>
|
||||
<span className={styles.divider} />
|
||||
<button type="button" onClick={() => exec('insertUnorderedList')} title="Bullet list" aria-label="Bullet list">
|
||||
<List size={16} />
|
||||
</button>
|
||||
@@ -498,6 +616,8 @@ export function RichTextEditor({
|
||||
data-placeholder={placeholder}
|
||||
onInput={syncChange}
|
||||
onKeyDown={onEditorKeyDown}
|
||||
onKeyUp={refreshAlignFromSelection}
|
||||
onMouseUp={refreshAlignFromSelection}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
{allowImages && selectedImageEl && handlePos && (
|
||||
|
||||
Reference in New Issue
Block a user