feat(ui): Dialog + ConfirmDialog (Pinterest chrome), pill buttons; Modal removed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent 66ec742523
commit 2fbb4c6442

@ -32,8 +32,9 @@ export function Button(props: {
onClick?: () => void
tone?: 'default' | 'primary' | 'danger'
hotkey?: string
pill?: boolean
}) {
const cls = `wf${props.tone === 'primary' ? ' primary' : props.tone === 'danger' ? ' danger' : ''}`
const cls = `wf${props.tone === 'primary' ? ' primary' : props.tone === 'danger' ? ' danger' : ''}${props.pill === true ? ' pill' : ''}`
return (
<button type="button" className={cls} onClick={props.onClick}>
{props.children}
@ -120,17 +121,6 @@ export function Field(props: { label: string; children: ReactNode }) {
)
}
export function Modal(props: { title: string; onClose: () => void; children: ReactNode }) {
return (
<div className="wf-modal-back" onClick={props.onClose}>
<div className="wf-modal" onClick={(e) => e.stopPropagation()}>
<h2>{props.title}</h2>
{props.children}
</div>
</div>
)
}
export function Notice(props: { tone?: 'ok' | 'warn' | 'err'; children: ReactNode }) {
return <div className={`wf-notice${props.tone !== undefined ? ` ${props.tone}` : ''}`}>{props.children}</div>
}

@ -0,0 +1,112 @@
import { useEffect, useRef, useState, type ReactNode } from 'react'
const WIDTH: Record<'sm' | 'md' | 'lg', number> = { sm: 420, md: 560, lg: 720 }
const FOCUSABLE = 'input, select, textarea, button, a[href], [tabindex]:not([tabindex="-1"])'
/** Pinterest-chrome modal: centered rounded card, scrim, focus trap + restore. */
export function Dialog(props: {
open: boolean
onClose: () => void
title: string
size?: 'sm' | 'md' | 'lg'
footer?: ReactNode
children: ReactNode
}) {
const prevFocus = useRef<HTMLElement | null>(null)
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (props.open) {
prevFocus.current = document.activeElement as HTMLElement | null
const first = cardRef.current?.querySelector<HTMLElement>(
'input, select, textarea, button:not(.wf-dialog-x)',
)
;(first ?? cardRef.current ?? undefined)?.focus()
}
}, [props.open])
useEffect(() => {
if (!props.open) prevFocus.current?.focus()
}, [props.open])
if (!props.open) return null
const onKey = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') { e.preventDefault(); props.onClose(); return }
if (e.key !== 'Tab') return
const nodes = cardRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE)
const list = nodes === undefined ? [] : [...nodes].filter((n) => !n.hasAttribute('disabled'))
const first = list[0]
const last = list[list.length - 1]
if (first === undefined || last === undefined) { e.preventDefault(); return }
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
}
return (
<div className="wf-dialog-back" onClick={props.onClose}>
<div
ref={cardRef}
className="wf-dialog"
style={{ maxWidth: WIDTH[props.size ?? 'md'] }}
role="dialog"
aria-modal="true"
aria-label={props.title}
tabIndex={-1}
onClick={(e) => e.stopPropagation()}
onKeyDown={onKey}
>
<div className="wf-dialog-head">
<h2>{props.title}</h2>
<button type="button" className="wf-dialog-x" aria-label="Close" onClick={props.onClose}></button>
</div>
<div className="wf-dialog-body">{props.children}</div>
{props.footer !== undefined && <div className="wf-dialog-foot">{props.footer}</div>}
</div>
</div>
)
}
/** Small confirm variant — kills window.confirm. Busy state while onConfirm resolves. */
export function ConfirmDialog(props: {
open: boolean
onClose: () => void
onConfirm: () => void | Promise<void>
title: string
body?: ReactNode
confirmLabel?: string
tone?: 'danger'
}) {
const [busy, setBusy] = useState(false)
const confirm = () => {
const out = props.onConfirm()
if (out instanceof Promise) {
setBusy(true)
out.finally(() => { setBusy(false); props.onClose() })
} else {
props.onClose()
}
}
return (
<Dialog
open={props.open}
onClose={props.onClose}
title={props.title}
size="sm"
footer={
<>
<button type="button" className="wf pill" onClick={props.onClose}>Cancel</button>
<button
type="button"
className={`wf pill ${props.tone === 'danger' ? 'danger' : 'primary'}`}
onClick={confirm}
disabled={busy}
>
{busy ? '…' : props.confirmLabel ?? 'Confirm'}
</button>
</>
}
>
{props.body !== undefined && <div style={{ fontSize: 13.5, color: 'var(--text-dim)' }}>{props.body}</div>}
</Dialog>
)
}

@ -1,5 +1,6 @@
export * from './avatar'
export * from './components'
export * from './dialog'
export * from './theme'
export * from './ThemeSwitcher'
export * from './states'

@ -291,19 +291,6 @@ table.wf tbody tr.tone-ok:hover td { filter: brightness(0.97); }
text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 5px;
}
.wf-modal-back {
position: fixed; inset: 0; background: rgba(15, 14, 11, 0.45);
backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center; z-index: 50;
}
.wf-modal {
background: var(--bg-raised); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: 22px;
min-width: 430px; max-width: 740px; max-height: 85vh; overflow: auto;
box-shadow: var(--shadow-lg);
}
.wf-modal h2 { margin: 0 0 14px; font-size: 17px; }
.wf-notice {
border-radius: var(--radius); padding: 9px 13px; margin: 10px 0;
border: 1px solid var(--border); background: var(--bg-raised);
@ -365,3 +352,27 @@ table.wf tbody tr.tone-ok:hover td { filter: brightness(0.97); }
font: 500 12px var(--font); color: var(--text-dim);
}
.wf-themer .mode:hover { color: var(--text); border-color: var(--border-strong); }
/* ================= dialogs (Pinterest chrome) ================= */
.wf-dialog-back {
position: fixed; inset: 0; background: rgba(15, 14, 11, 0.45);
backdrop-filter: blur(4px); z-index: 50;
display: flex; align-items: center; justify-content: center; padding: 20px;
}
.wf-dialog {
background: var(--bg-raised); border: 1px solid var(--border);
border-radius: 16px; box-shadow: var(--shadow-lg);
width: 100%; max-height: 86vh; display: flex; flex-direction: column;
}
.wf-dialog-head { display: flex; align-items: center; gap: 10px; padding: 18px 20px 0; }
.wf-dialog-head h2 { margin: 0; font-size: 17px; flex: 1; }
.wf-dialog-x {
border: none; background: var(--bg-inset); color: var(--text-dim);
width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 13px;
}
.wf-dialog-x:hover { background: var(--bg-hover); color: var(--text); }
.wf-dialog-body { padding: 14px 20px; overflow-y: auto; }
.wf-dialog-foot {
display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px 18px;
}
button.wf.pill { border-radius: 999px; padding: 8px 18px; }

Loading…
Cancel
Save