feat: add element-inspector

- Implemented Rulers component for zoom/pan-aware rulers on canvas.
- Created Stage component to serve as a zoomable and pannable viewport for the device frame.
- Developed Toolbar component for responsive controls, including device presets and zoom functionalities.
- Introduced useFrame composable to manage iframe interactions and inspections.
- Established a reactive store to manage application state, including guides and viewport dimensions.
- Added utility functions for color parsing and box model calculations.
- Integrated Tailwind CSS for styling and improved scrollbar aesthetics.
- Implemented unit tests for color utilities and rectangle calculations.
- Configured TypeScript and Vite for the project setup.
This commit is contained in:
2026-06-05 02:45:54 +07:00
parent ee14101fc1
commit 32ed0b45f0
30 changed files with 4658 additions and 0 deletions
@@ -0,0 +1,53 @@
import { setFrameSize, state } from '../store';
type Mode = 'r' | 'b' | 'rb';
// Drag handles on the right / bottom / corner of the frame to resize it (top-left anchored),
// which re-fires the page's media queries inside the iframe. Positioned in viewport space.
export default function ResizeHandles() {
const startDrag = (e: PointerEvent, mode: Mode): void => {
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startY = e.clientY;
const startW = state.frameWidth;
const startH = state.frameHeight;
const move = (ev: PointerEvent): void => {
const dw = (ev.clientX - startX) / state.zoom;
const dh = (ev.clientY - startY) / state.zoom;
setFrameSize(mode === 'b' ? startW : startW + dw, mode === 'r' ? startH : startH + dh);
};
const up = (): void => {
window.removeEventListener('pointermove', move, true);
window.removeEventListener('pointerup', up, true);
};
window.addEventListener('pointermove', move, true);
window.addEventListener('pointerup', up, true);
};
const right = state.panX + state.frameWidth * state.zoom;
const bottom = state.panY + state.frameHeight * state.zoom;
const midX = state.panX + (state.frameWidth * state.zoom) / 2;
const midY = state.panY + (state.frameHeight * state.zoom) / 2;
return (
<div class="pointer-events-none absolute inset-0">
<div
class="pointer-events-auto absolute h-7 w-1.5 -translate-x-1/2 cursor-ew-resize rounded-full bg-sky-500/80 hover:bg-sky-400"
style={{ left: `${right}px`, top: `${midY - 14}px` }}
onPointerdown={(e) => startDrag(e, 'r')}
/>
<div
class="pointer-events-auto absolute h-1.5 w-7 -translate-y-1/2 cursor-ns-resize rounded-full bg-sky-500/80 hover:bg-sky-400"
style={{ left: `${midX - 14}px`, top: `${bottom}px` }}
onPointerdown={(e) => startDrag(e, 'b')}
/>
<div
class="pointer-events-auto absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 cursor-nwse-resize rounded-sm border border-white/40 bg-sky-500 hover:bg-sky-400"
style={{ left: `${right}px`, top: `${bottom}px` }}
onPointerdown={(e) => startDrag(e, 'rb')}
/>
</div>
);
}