Add workshop generate-link type, Email DNS for domains, and editor polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-01 22:00:04 +03:30
co-authored by Cursor
parent 6be867ea7a
commit 3cbadb4001
25 changed files with 467 additions and 70 deletions
@@ -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 && (