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
787 changes: 80 additions & 707 deletions README.md

Large diffs are not rendered by default.

Binary file modified demo/public/og/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/public/og/zoom-lens.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/public/zoom-lens-demo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 118 additions & 1 deletion demo/scripts/gen-og.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,117 @@ const illustrations = {
);
},

zoomlens: () => {
// Geometric illustration in the same flat style as the other OG cards:
// a rainbow grid of small tiles in the background, with a circular
// magnifier lens on top showing a patch of visibly larger tiles so the
// "zoom" effect reads at a glance.
const BG_TILE = 32;
const BG_GAP = 6;
const BG_PAD = 16;
const BG_COLS = 11;
const BG_ROWS = 7;

// Shades of the card-set accent (indigo ≈ hsl(235, ~70%, ~74%)) instead
// of a rainbow — keeps the ZoomLens card visually coherent with the rest.
const tileColor = (r, c) => {
const i = r * BG_COLS + c;
const l = 26 + (i * 7) % 26; // 26–52 — subtle, low-contrast grid
return `hsl(235 42% ${l}%)`;
};
const magColor = (r, c) => {
const i = r * 4 + c;
const l = 46 + (i * 9) % 28; // 46–74 — brighter inside the lens
return `hsl(235 58% ${l}%)`;
};

const bgTiles = [];
for (let r = 0; r < BG_ROWS; r++) {
for (let c = 0; c < BG_COLS; c++) {
bgTiles.push(
box({
position: 'absolute',
top: BG_PAD + r * (BG_TILE + BG_GAP),
left: BG_PAD + c * (BG_TILE + BG_GAP),
width: BG_TILE,
height: BG_TILE,
borderRadius: 5,
background: tileColor(r, c),
}),
);
}
}

const LENS_D = 185;
const LENS_CX = 290;
const LENS_CY = 150;
const LENS_X = LENS_CX - LENS_D / 2;
const LENS_Y = LENS_CY - LENS_D / 2;

// Larger tiles inside the lens so the magnification reads clearly. We
// render a 4×4 patch wider than the lens so the tiles appear to flow
// out of its edges, like a real magnified window.
const ZOOM = 2.4;
const MAG_TILE = BG_TILE * ZOOM;
const MAG_GAP = BG_GAP * ZOOM;
const magTiles = [];
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
magTiles.push(
box({
position: 'absolute',
top: -28 + r * (MAG_TILE + MAG_GAP),
left: -24 + c * (MAG_TILE + MAG_GAP),
width: MAG_TILE,
height: MAG_TILE,
borderRadius: 6 * ZOOM,
background: magColor(r, c),
}),
);
}
}

return viewport(
box(
{ width: '100%', height: '100%', position: 'relative', display: 'flex' },
...bgTiles,
// Lens — circular clipped container holding the magnified tiles.
box(
{
position: 'absolute',
left: LENS_X,
top: LENS_Y,
width: LENS_D,
height: LENS_D,
borderRadius: 999,
overflow: 'hidden',
boxShadow:
'inset 0 0 0 3px rgba(255,255,255,0.95), 0 18px 50px rgba(0,0,0,0.45)',
display: 'flex',
},
...magTiles,
),
// Zoom badge in the lens corner — matches the real component UI.
box(
{
position: 'absolute',
left: LENS_CX + LENS_D / 2 - 54,
top: LENS_CY + LENS_D / 2 - 26,
padding: '3px 7px',
borderRadius: 5,
background: 'rgba(20,20,20,0.78)',
color: '#fff',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.04em',
display: 'flex',
},
'2.40×',
),
),
);
},

// Home: a 3x2 grid of mini-widget previews. Each tile contains a small
// illustration of the component it represents, plus a short label.
home: () =>
Expand Down Expand Up @@ -503,7 +614,7 @@ const pages = [
slug: 'home',
kind: 'home',
title: 'Floating UI primitives for React',
tagline: 'Draggable launchers, docks, sheets, split panes, and a DevTools-style inspector. Tree-shakable and unstyled.',
tagline: 'Draggable launchers, docks, sheets, split panes, a DevTools-style inspector, and a zoom lens. Tree-shakable and unstyled.',
},
{
slug: 'movable-launcher',
Expand Down Expand Up @@ -535,6 +646,12 @@ const pages = [
title: 'InspectorBubble',
tagline: 'A Chrome-DevTools-style element picker. Tag, selector, font, WCAG contrast, ARIA role and accessible name.',
},
{
slug: 'zoom-lens',
kind: 'zoomlens',
title: 'ZoomLens',
tagline: 'A draggable magnifier circle that zooms into whatever it hovers. Drag to move, scroll to zoom, hotkey or Escape to dismiss.',
},
];

/* ── render pipeline ─────────────────────────────────────────────────── */
Expand Down
12 changes: 9 additions & 3 deletions demo/src/components/ApiTable.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,27 @@ const { rows, footnoteHtml } = Astro.props;

<div class="table-wrap">
<table class="api-table">
<colgroup>
<col class="api-table__col-prop" />
<col class="api-table__col-type" />
<col class="api-table__col-default" />
</colgroup>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr>
<td><code>{row.prop}</code></td>
<td>
<code>{row.prop}</code>
<div class="api-table__desc" set:html={row.descriptionHtml} />
</td>
<td set:html={row.typeHtml} />
<td set:html={row.defaultHtml} />
<td set:html={row.descriptionHtml} />
</tr>
))}
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion demo/src/components/Nav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import ComponentDropdown from './islands/ComponentDropdown';

export interface Props {
activeComponent?: 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector' | null;
activeComponent?: 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector' | 'zoomlens' | null;
}

const { activeComponent = null } = Astro.props;
Expand Down
5 changes: 3 additions & 2 deletions demo/src/components/islands/ComponentDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { MoveIcon, DockGlyphIcon, SheetIcon, SplitterIcon, InspectorIcon, ChevronDownIcon } from './Icons';
import { MoveIcon, DockGlyphIcon, SheetIcon, SplitterIcon, InspectorIcon, ZoomLensIcon, ChevronDownIcon } from './Icons';

export type ComponentKey = 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector';
export type ComponentKey = 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector' | 'zoomlens';

const items: { key: ComponentKey; label: string; href: string; icon: ReactNode }[] = [
{ key: 'launcher', label: 'MovableLauncher', href: '/movable-launcher', icon: <MoveIcon size={16} strokeWidth={2.2} /> },
{ key: 'dock', label: 'SnapDock', href: '/snap-dock', icon: <DockGlyphIcon size={16} /> },
{ key: 'sheet', label: 'DraggableSheet', href: '/draggable-sheet', icon: <SheetIcon size={16} /> },
{ key: 'splitter', label: 'ResizableSplitPane', href: '/resizable-split-pane', icon: <SplitterIcon size={16} /> },
{ key: 'inspector', label: 'InspectorBubble', href: '/inspector-bubble', icon: <InspectorIcon size={16} /> },
{ key: 'zoomlens', label: 'ZoomLens', href: '/zoom-lens', icon: <ZoomLensIcon size={16} /> },
];

export default function ComponentDropdown({ active }: { active: ComponentKey | null }) {
Expand Down
12 changes: 12 additions & 0 deletions demo/src/components/islands/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ export function InspectorIcon({ size = 16, strokeWidth = 2 }: IconProps) {
);
}

export function ZoomLensIcon({ size = 16, strokeWidth = 2 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 40 40" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="6" y="6" width="28" height="28" rx="3" opacity="0.35" />
<circle cx="18" cy="18" r="8" />
<path d="M14 18h8" />
<path d="M18 14v8" />
<path d="M24 24l6 6" strokeWidth={strokeWidth + 0.4} />
</svg>
);
}

export function ChevronDownIcon({ size = 14 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
Expand Down
113 changes: 113 additions & 0 deletions demo/src/components/islands/ZoomLensDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useRef, useState } from 'react';
import { ZoomLens } from 'react-driftkit';

export default function ZoomLensDemo() {
const [zoom, setZoom] = useState(3);
const [size, setSize] = useState(200);
const [showCrosshair, setShowCrosshair] = useState(true);
const [showBadge, setShowBadge] = useState(true);
const imageRef = useRef<HTMLDivElement>(null);

return (
<>
<div className="demo-row">
<span className="demo-row-label">Zoom</span>
<div className="demo-row-controls">
<input
data-zoom-lens-ignore
type="range"
min={1.25}
max={8}
step={0.25}
value={zoom}
onChange={(e) => setZoom(parseFloat(e.target.value))}
/>
<span className="toggle-label" style={{ fontFamily: 'var(--font-mono)' }}>
{zoom.toFixed(2)}×
</span>
</div>
</div>

<div className="demo-row">
<span className="demo-row-label">Lens size</span>
<div className="demo-row-controls">
<input
data-zoom-lens-ignore
type="range"
min={120}
max={320}
step={10}
value={size}
onChange={(e) => setSize(parseInt(e.target.value, 10))}
/>
<span className="toggle-label" style={{ fontFamily: 'var(--font-mono)' }}>
{size}px
</span>
</div>
</div>

<div className="demo-row">
<span className="demo-row-label">Crosshair</span>
<div className="demo-row-controls">
<button
type="button"
data-zoom-lens-ignore
className={`toggle ${showCrosshair ? 'toggle--on' : ''}`}
onClick={() => setShowCrosshair((v) => !v)}
/>
</div>
</div>

<div className="demo-row" style={{ borderBottom: 'none' }}>
<span className="demo-row-label">Zoom badge</span>
<div className="demo-row-controls">
<button
type="button"
data-zoom-lens-ignore
className={`toggle ${showBadge ? 'toggle--on' : ''}`}
onClick={() => setShowBadge((v) => !v)}
/>
</div>
</div>

<div
ref={imageRef}
style={{
marginTop: 16,
aspectRatio: '3001 / 1762',
borderRadius: 'var(--radius)',
border: '1px solid var(--border)',
overflow: 'hidden',
backgroundImage: 'url(/zoom-lens-demo.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'none',
}}
aria-label="Hover to inspect the sailboats and water taxi"
/>

<div
style={{
marginTop: 12,
fontSize: 13,
color: 'var(--text-muted)',
lineHeight: 1.6,
}}
>
Hover anywhere on the photo — the lens snaps to your cursor, stays
inside the image, and hides when you leave. No activation, no drag;
the <code>target</code> ref wires it to this one element. Zoom into the
sails, the hulls, the water taxi wake, or the ripple texture.
</div>

<ZoomLens
defaultActive
target={imageRef}
zoom={zoom}
size={size}
showCrosshair={showCrosshair}
showZoomBadge={showBadge}
/>
</>
);
}
5 changes: 3 additions & 2 deletions demo/src/data/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { dockMeta } from './dock';
import { sheetMeta } from './sheet';
import { splitterMeta } from './splitter';
import { inspectorMeta } from './inspector';
import { zoomLensMeta } from './zoomlens';

export const allComponents = [launcherMeta, dockMeta, sheetMeta, splitterMeta, inspectorMeta] as const;
export const allComponents = [launcherMeta, dockMeta, sheetMeta, splitterMeta, inspectorMeta, zoomLensMeta] as const;

export { launcherMeta, dockMeta, sheetMeta, splitterMeta, inspectorMeta };
export { launcherMeta, dockMeta, sheetMeta, splitterMeta, inspectorMeta, zoomLensMeta };
2 changes: 1 addition & 1 deletion demo/src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type CodeExample = {
};

export type ComponentMeta = {
key: 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector';
key: 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector' | 'zoomlens';
slug: string;
title: string;
tagline: string;
Expand Down
Loading
Loading