Move all three skills into skills/; add root README; fix paths
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
312742cfef
commit
d95e5d452d
84 files changed
+97
-31
No files matched your search
@@ -0,0 +1,147 @@
|
||||
/* EXAMPLE ONLY: an extract from the Superbill Review prototype, kept as reference for porting. ECW/eClinicalWorks, Tebra, 'Example Client', SPB_* tables and patient/ticket wording are that project's examples. Don't copy them into a new project. */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
const KEY = 'superbill.split'
|
||||
|
||||
/**
|
||||
* Draggable two-pane split, PDF on the left and values on the right.
|
||||
*
|
||||
* The ratio is remembered: how much page a reviewer needs beside the values
|
||||
* depends on their screen and their document, and re-dragging it every session
|
||||
* would be a small tax paid hundreds of times.
|
||||
*
|
||||
* The drag listeners live on window, not the handle, so the pointer can outrun
|
||||
* the 6px handle mid-drag without dropping it.
|
||||
*
|
||||
* Below `md`, side-by-side stops being usable at all — squeezing both panes
|
||||
* into a 375px screen leaves the PDF at ~165px wide, unreadable. Below that
|
||||
* breakpoint this becomes two full-width tabs instead of a shrinking split;
|
||||
* both panes stay mounted (toggling `hidden`/`flex`, not unmounting) so
|
||||
* switching tabs never resets the PDF viewer's scroll or zoom.
|
||||
*/
|
||||
export function SplitPane({
|
||||
left,
|
||||
right,
|
||||
min = 25,
|
||||
max = 75,
|
||||
ratio,
|
||||
}: {
|
||||
left: React.ReactNode
|
||||
right: React.ReactNode
|
||||
min?: number
|
||||
max?: number
|
||||
/**
|
||||
* Force a split for as long as it is set, without overwriting the reviewer's
|
||||
* remembered one — fullscreen widens the page and docks the values to the
|
||||
* right, and leaving it must restore the ratio they chose, not this one.
|
||||
*/
|
||||
ratio?: number
|
||||
}) {
|
||||
const [saved, setSaved] = useState(() => {
|
||||
const stored = Number(localStorage.getItem(KEY))
|
||||
// The scan is the thing being read; the values are short. The page gets
|
||||
// the larger share by default.
|
||||
return stored >= min && stored <= max ? stored : 60
|
||||
})
|
||||
const pct = ratio ?? saved
|
||||
const setPct = setSaved
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const [mobileTab, setMobileTab] = useState<'left' | 'right'>('left')
|
||||
const box = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(KEY, String(saved))
|
||||
}, [saved])
|
||||
|
||||
const onMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
const rect = box.current?.getBoundingClientRect()
|
||||
if (!rect) return
|
||||
const next = ((e.clientX - rect.left) / rect.width) * 100
|
||||
setPct(Math.min(max, Math.max(min, next)))
|
||||
},
|
||||
[min, max],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!dragging) return
|
||||
const stop = () => setDragging(false)
|
||||
window.addEventListener('mousemove', onMove)
|
||||
window.addEventListener('mouseup', stop)
|
||||
// The PDF pane has its own drag-to-pan handler; without this it would
|
||||
// interpret the divider drag as a pan and text would select across both
|
||||
// panes on the way.
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.cursor = 'col-resize'
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMove)
|
||||
window.removeEventListener('mouseup', stop)
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.cursor = ''
|
||||
}
|
||||
}, [dragging, onMove])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{/* Tab bar: mobile only. The drag handle is the equivalent affordance on
|
||||
desktop, so this disappears at md instead of coexisting with it. */}
|
||||
<div className="flex shrink-0 border-b border-line-soft bg-rail md:hidden">
|
||||
<button
|
||||
onClick={() => setMobileTab('left')}
|
||||
aria-current={mobileTab === 'left'}
|
||||
className="flex-1 border-b-2 py-2.5 font-sora text-[13px] font-semibold transition-colors"
|
||||
style={{
|
||||
borderColor: mobileTab === 'left' ? 'var(--accent-1)' : 'transparent',
|
||||
color: mobileTab === 'left' ? 'var(--ink)' : 'var(--muted)',
|
||||
}}
|
||||
>
|
||||
Source page
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileTab('right')}
|
||||
aria-current={mobileTab === 'right'}
|
||||
className="flex-1 border-b-2 py-2.5 font-sora text-[13px] font-semibold transition-colors"
|
||||
style={{
|
||||
borderColor: mobileTab === 'right' ? 'var(--accent-1)' : 'transparent',
|
||||
color: mobileTab === 'right' ? 'var(--ink)' : 'var(--muted)',
|
||||
}}
|
||||
>
|
||||
Fields to review
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref={box} className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||
<div
|
||||
style={{ '--split-w': `${pct}%` } as React.CSSProperties}
|
||||
className={`min-h-0 min-w-0 flex-1 border-b border-line md:w-[var(--split-w)] md:flex-none md:border-b-0 md:border-r md:border-line ${
|
||||
mobileTab === 'left' ? 'flex' : 'hidden'
|
||||
} md:flex`}
|
||||
>
|
||||
{left}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseDown={() => setDragging(true)}
|
||||
onDoubleClick={() => setPct(52)}
|
||||
title="Drag to resize — double-click to reset"
|
||||
className={`hidden w-1.5 shrink-0 cursor-col-resize transition-colors md:block ${
|
||||
dragging ? 'bg-accent-1' : 'bg-line hover:bg-muted'
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Pointer events off during the drag, so neither pane steals the move. */}
|
||||
<div
|
||||
style={{
|
||||
'--split-w': `${100 - pct}%`,
|
||||
pointerEvents: dragging ? 'none' : undefined,
|
||||
} as React.CSSProperties}
|
||||
className={`min-h-0 min-w-0 flex-1 md:w-[var(--split-w)] md:flex-none ${
|
||||
mobileTab === 'right' ? 'flex' : 'hidden'
|
||||
} md:flex`}
|
||||
>
|
||||
{right}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user