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
4 changes: 4 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"type": "stdio",
"command": "node",
"args": ["${workspaceFolder}/packages/mcp/dist/server.js"]
},
"ark-ui": {
"command": "npx",
"args": ["-y", "@ark-ui/mcp"]
}
// "stripe": {
// "type": "http",
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface DialogProps {
/** Dialog content */
children?: JSX.Element;
/** Dialog size */
size?: 'sm' | 'md' | 'lg' | 'xl';
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
}

/**
Expand All @@ -47,6 +47,8 @@ const DialogComponent: Component<DialogProps> = props => {
return 'max-w-lg';
case 'xl':
return 'max-w-xl';
case '2xl':
return 'max-w-6xl';
default:
return 'max-w-md';
}
Expand Down
20 changes: 8 additions & 12 deletions packages/web/src/components/project-ui/PdfPreviewPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
/**
* PdfPreviewPanel - Slide-in drawer for previewing PDFs
* PdfPreviewPanel - Slide-in panel for previewing PDFs
*
* Reads from pdfPreviewStore to show/hide and display PDF content.
* Used across project views to preview PDFs without leaving context.
*/

import { Show } from 'solid-js';
import { Drawer } from '@corates/ui';
import SlidingPanel from './SlidingPanel.jsx';
import PdfViewer from '@/components/checklist-ui/pdf/PdfViewer.jsx';
import pdfPreviewStore from '@/stores/pdfPreviewStore.js';

export default function PdfPreviewPanel() {
const handleOpenChange = open => {
if (!open) {
pdfPreviewStore.closePreview();
}
const handleClose = () => {
pdfPreviewStore.closePreview();
};

// Format title with filename
Expand All @@ -25,13 +23,11 @@ export default function PdfPreviewPanel() {
};

return (
<Drawer
<SlidingPanel
open={pdfPreviewStore.isOpen()}
onOpenChange={handleOpenChange}
onClose={handleClose}
title={title()}
side='right'
size='lg'
showBackdrop={false}
size='2xl'
closeOnOutsideClick={true}
>
<div class='flex h-full min-h-0 flex-col'>
Expand Down Expand Up @@ -70,6 +66,6 @@ export default function PdfPreviewPanel() {
/>
</Show>
</div>
</Drawer>
</SlidingPanel>
);
}
141 changes: 141 additions & 0 deletions packages/web/src/components/project-ui/SlidingPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* SlidingPanel - A lightweight, GPU-accelerated sliding panel
*
* Optimized for smooth animations without the overhead of Portal/Dialog.
* Uses CSS transforms and will-change for 60fps animations.
*/

import { Show, createSignal, createEffect, onCleanup } from 'solid-js';
import { FiX } from 'solid-icons/fi';

/**
* @param {Object} props
* @param {boolean} props.open - Whether panel is open
* @param {(open: boolean) => void} props.onClose - Close handler
* @param {string} [props.title] - Panel title
* @param {'sm' | 'md' | 'lg' | 'xl' | '2xl'} [props.size='xl'] - Panel width
* @param {boolean} [props.closeOnOutsideClick=true] - Close when clicking outside
* @param {import('solid-js').JSX.Element} props.children - Panel content
*/
export default function SlidingPanel(props) {
const [mounted, setMounted] = createSignal(false);
const [visible, setVisible] = createSignal(false);

let panelRef = null;

const size = () => props.size ?? 'xl';
const closeOnOutsideClick = () => props.closeOnOutsideClick ?? true;

const getSizeClass = () => {
switch (size()) {
case 'sm':
return 'w-80';
case 'md':
return 'w-96';
case 'lg':
return 'w-[32rem]';
case 'xl':
return 'w-[40rem]';
case '2xl':
return 'w-[48rem]';
default:
return 'w-[40rem]';
}
};

// Handle open/close with proper mount/unmount timing
createEffect(() => {
if (props.open) {
// Mount first, then animate in
setMounted(true);
// Use RAF to ensure DOM is ready before animating
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setVisible(true);
});
});
} else {
// Animate out first, then unmount
setVisible(false);
}
});

// Handle unmount after close animation
const handleTransitionEnd = e => {
if (e.propertyName === 'transform' && !props.open) {
setMounted(false);
}
};

// Handle escape key
createEffect(() => {
if (!props.open) return;

const handleKeyDown = e => {
if (e.key === 'Escape') {
props.onClose?.(false);
}
};

document.addEventListener('keydown', handleKeyDown);
onCleanup(() => document.removeEventListener('keydown', handleKeyDown));
});

// Handle click outside
createEffect(() => {
if (!props.open || !closeOnOutsideClick()) return;

const handleClickOutside = e => {
if (panelRef && !panelRef.contains(e.target)) {
props.onClose?.(false);
}
};

// Delay adding listener to avoid catching the opening click
const timer = setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside);
}, 10);

onCleanup(() => {
clearTimeout(timer);
document.removeEventListener('mousedown', handleClickOutside);
});
});

return (
<Show when={mounted()}>
{/* Panel container - fixed position, no portal needed */}
<div
ref={panelRef}
class={`fixed inset-y-0 right-0 z-50 ${getSizeClass()} flex flex-col bg-white shadow-2xl`}
style={{
transform: visible() ? 'translateX(0) translateZ(0)' : 'translateX(100%) translateZ(0)',
transition: 'transform 250ms cubic-bezier(0.32, 0.72, 0, 1)',
'will-change': 'transform',
'backface-visibility': 'hidden',
}}
onTransitionEnd={handleTransitionEnd}
role='complementary'
aria-label={props.title || 'Side panel'}
>
{/* Header */}
<div class='flex shrink-0 items-center justify-between border-b border-gray-200 px-4 py-3'>
<Show when={props.title}>
<h2 class='truncate pr-4 text-lg font-semibold text-gray-900'>{props.title}</h2>
</Show>
<button
type='button'
onClick={() => props.onClose?.(false)}
class='ml-auto rounded-md p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600'
aria-label='Close panel'
>
<FiX class='h-5 w-5' />
</button>
</div>

{/* Content */}
<div class='min-h-0 flex-1 overflow-hidden'>{props.children}</div>
</div>
</Show>
);
}
Loading