diff --git a/packages/ui/src/components.tsx b/packages/ui/src/components.tsx index 38e5f2e..f146e20 100644 --- a/packages/ui/src/components.tsx +++ b/packages/ui/src/components.tsx @@ -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 ( + +
{props.children}
+ {props.footer !== undefined &&
{props.footer}
} + + + ) +} + +/** Small confirm variant — kills window.confirm. Busy state while onConfirm resolves. */ +export function ConfirmDialog(props: { + open: boolean + onClose: () => void + onConfirm: () => void | Promise + 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 ( + + + + + } + > + {props.body !== undefined &&
{props.body}
} +
+ ) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 9845ff8..2c1e39c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,5 +1,6 @@ export * from './avatar' export * from './components' +export * from './dialog' export * from './theme' export * from './ThemeSwitcher' export * from './states' diff --git a/packages/ui/src/tokens.css b/packages/ui/src/tokens.css index 685d626..1597060 100644 --- a/packages/ui/src/tokens.css +++ b/packages/ui/src/tokens.css @@ -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; }