Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ node_modules
dist
.next
.DS_Store
next-env.d.ts
*/**/next-env.d.ts


1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"@types/node": "^22.10.7",
"@types/react": "^19.0.7",
"bunchee": "^6.5.2",
"html-to-image": "^1.11.13",
"next": "^16.0.7",
"postcss": "^8.5.4",
"prettier": "^3.6.2",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 15 additions & 15 deletions site/app/live-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ export async function copyImageDataUrl(dataUrl: string) {
}

const blob = await (await fetch(dataUrl)).blob()

// Safari compatibility: Check if the format is supported
const mimeType = 'image/png'

// Use ClipboardItem.supports() if available (modern browsers)
if (typeof ClipboardItem.supports === 'function') {
if (!ClipboardItem.supports(mimeType)) {
Expand All @@ -122,19 +122,19 @@ export async function copyImageDataUrl(dataUrl: string) {
}

// Safari-specific: Create ClipboardItem with promise-based blob for better compatibility
const clipboardItem = new ClipboardItem({
const clipboardItem = new ClipboardItem({
[mimeType]: Promise.resolve(blob)
})

// Safari requires user gesture - this should be called within a user interaction
await navigator.clipboard.write([clipboardItem])
return Promise.resolve(dataUrl)

} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('Clipboard error:', error)
}

// Safari-specific error handling with better messages
if (error instanceof Error) {
if (error.name === 'NotAllowedError') {
Expand All @@ -148,7 +148,7 @@ export async function copyImageDataUrl(dataUrl: string) {
return Promise.reject('Security policy prevented clipboard access.')
}
}

// Generic error fallback
return Promise.reject('Failed to copy image to clipboard. Please try again.')
}
Expand Down Expand Up @@ -204,7 +204,7 @@ function ScreenshotButton({ editorElementRef }: { editorElementRef: React.RefObj
if (!editorElementRef.current) {
return Promise.resolve(null)
}

try {
// Safari fix: Create clipboard promise immediately to preserve user gesture
if (!navigator.clipboard || !window.ClipboardItem) {
Expand All @@ -222,7 +222,7 @@ function ScreenshotButton({ editorElementRef }: { editorElementRef: React.RefObj

// Start clipboard write immediately (synchronously from user event)
await navigator.clipboard.write([clipboardItem])

// Generate dataUrl for return (can be done after clipboard operation)
const dataUrl = await toPng(editorElementRef.current)
return dataUrl
Expand All @@ -235,15 +235,15 @@ function ScreenshotButton({ editorElementRef }: { editorElementRef: React.RefObj
}

const [actionState, dispatch, isPending] = useActionState<
{ state: 'idle' | 'succeed' | 'error'; dataUrl?: string },
{ state: 'idle' | 'succeed' | 'error'; dataUrl?: string },
{ type: 'reset' } | { type: 'copy'; dataUrl: string | null }
>(
(state, action) => {
if (action.type === 'reset') {
return { state: 'idle' }
} else if (action.type === 'copy') {
const imageDataUrl = action.dataUrl

if (imageDataUrl) {
const id = Date.now().toString()

Expand Down Expand Up @@ -453,7 +453,7 @@ function DropdownMenu({
if (Date.now() - openTimeRef.current < 100) {
return
}

const dropdown = nodeRef.current
if (dropdown && !dropdown.contains(event.target as Node)) {
setIsOpen(false)
Expand Down Expand Up @@ -481,7 +481,7 @@ function DropdownMenu({
const arrowRect = arrowElement.getBoundingClientRect()
const arrowLeft = arrowRect.left
const clientX = e.clientX

// Check if pointer is on the arrow (right side) - use a wider touch target
const touchPadding = e.pointerType === 'touch' ? 12 : 8 // Extra padding for touch
if (clientX >= arrowLeft - touchPadding) {
Expand All @@ -502,9 +502,9 @@ function DropdownMenu({

return (
<div {...props} ref={nodeRef} className={cx('dropdown-menu relative', props.className)}>
<button
ref={buttonRef}
className="dropdown-menu-button relative w-full"
<button
ref={buttonRef}
className="dropdown-menu-button relative w-full"
onPointerDown={handlePointerDown}
>
<span className="flex items-center gap-1 min-w-0 w-full relative">
Expand Down
11 changes: 8 additions & 3 deletions site/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ export default async function Page(props: { searchParams: Promise<SearchParams>

return (
<div>
<a className="absolute top-4 right-4" href="https://github.com/huozhi/codice">
Source Code ↗
</a>
<div className="absolute top-4 right-4 flex gap-4">
<a href="/studio" className="text-[#f47067] hover:underline">
Studio →
</a>
<a href="https://github.com/huozhi/codice">
Source Code ↗
</a>
</div>
<div className="titles">
<h1>
<span className="huge-title">Codice<span className="cursor-blink">_</span></span>
Expand Down
Loading