refactor: move studio to frontends workspace
- Move studio from root to frontends/studio/ - Add owner-tools frontend for live blog admin UI - Add shared ui component library - Set up npm workspaces for frontends - Add enhanced code block extension for editor Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c662e41b97
commit
bef5dd4437
108 changed files with 8650 additions and 441 deletions
|
|
@ -0,0 +1,168 @@
|
|||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'
|
||||
import type { NodeViewProps } from '@tiptap/react'
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { getLanguageIconUrl, getLanguageDisplayName, SUPPORTED_LANGUAGES } from './icons'
|
||||
import { Icons } from '../../../shared/Icons'
|
||||
|
||||
export function CodeBlockView({ node, updateAttributes, extension }: NodeViewProps) {
|
||||
const { language, title } = node.attrs
|
||||
const [showLanguageMenu, setShowLanguageMenu] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
||||
const [titleValue, setTitleValue] = useState(title || '')
|
||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const iconUrl = language ? getLanguageIconUrl(language) : null
|
||||
const displayName = language ? getLanguageDisplayName(language) : 'Plain text'
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
const code = node.textContent
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [node.textContent])
|
||||
|
||||
const handleLanguageSelect = useCallback((lang: string) => {
|
||||
updateAttributes({ language: lang })
|
||||
setShowLanguageMenu(false)
|
||||
}, [updateAttributes])
|
||||
|
||||
const handleTitleSubmit = useCallback(() => {
|
||||
updateAttributes({ title: titleValue || null })
|
||||
setIsEditingTitle(false)
|
||||
}, [titleValue, updateAttributes])
|
||||
|
||||
const handleTitleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleTitleSubmit()
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setTitleValue(title || '')
|
||||
setIsEditingTitle(false)
|
||||
}
|
||||
}, [handleTitleSubmit, title])
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditingTitle && titleInputRef.current) {
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.select()
|
||||
}
|
||||
}, [isEditingTitle])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setShowLanguageMenu(false)
|
||||
}
|
||||
}
|
||||
if (showLanguageMenu) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [showLanguageMenu])
|
||||
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="code-block-wrapper relative my-6">
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-zinc-50 border border-zinc-200 border-b-0 rounded-t-md">
|
||||
{/* Language selector */}
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLanguageMenu(!showLanguageMenu)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 text-xs text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100 rounded transition-colors"
|
||||
contentEditable={false}
|
||||
>
|
||||
{iconUrl && (
|
||||
<img src={iconUrl} alt="" className="w-3.5 h-3.5" />
|
||||
)}
|
||||
<span>{displayName}</span>
|
||||
<Icons.ChevronDown className="w-3 h-3 opacity-50" />
|
||||
</button>
|
||||
|
||||
{showLanguageMenu && (
|
||||
<div className="absolute top-full left-0 mt-1 w-48 max-h-64 overflow-y-auto bg-white border border-zinc-200 rounded-md shadow-lg z-50">
|
||||
{SUPPORTED_LANGUAGES.map((lang) => {
|
||||
const langIcon = getLanguageIconUrl(lang)
|
||||
return (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => handleLanguageSelect(lang)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left hover:bg-zinc-50 transition-colors ${
|
||||
language === lang ? 'bg-zinc-100 text-zinc-900' : 'text-zinc-600'
|
||||
}`}
|
||||
>
|
||||
{langIcon && <img src={langIcon} alt="" className="w-3.5 h-3.5" />}
|
||||
<span>{getLanguageDisplayName(lang)}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-px h-4 bg-zinc-200" />
|
||||
|
||||
{/* Title (editable) */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
ref={titleInputRef}
|
||||
type="text"
|
||||
value={titleValue}
|
||||
onChange={(e) => setTitleValue(e.target.value)}
|
||||
onBlur={handleTitleSubmit}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
placeholder="filename.ext"
|
||||
className="w-full px-1 py-0.5 text-xs bg-white border border-zinc-300 rounded focus:outline-none focus:border-zinc-400"
|
||||
contentEditable={false}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTitleValue(title || '')
|
||||
setIsEditingTitle(true)
|
||||
}}
|
||||
className="text-xs text-zinc-500 hover:text-zinc-700 truncate max-w-full"
|
||||
contentEditable={false}
|
||||
>
|
||||
{title || 'Add title...'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Copy button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 rounded transition-colors"
|
||||
contentEditable={false}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Icons.Check className="w-3 h-3 text-emerald-500" />
|
||||
<span className="text-emerald-600">Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icons.Copy className="w-3 h-3" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Code content */}
|
||||
<pre className="!mt-0 !rounded-t-none">
|
||||
<NodeViewContent as="code" className={language ? `language-${language}` : ''} />
|
||||
</pre>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue