feat(web): buttons on shadcn Button — house variants (primary/danger/subtle/toolbar) keep the exact look

cva variants map to the existing .pbtn/.danger-btn/.ai-btn/.btn classes, so
style.css owns appearance while shadcn adds focus-ring/disabled plumbing.
Migrated: modals, ShareDialog, EmptyState, Browser topbar. 53/53.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
This commit is contained in:
Snow Lee
2026-07-19 13:05:13 -07:00
co-authored by Claude Fable 5
parent b7ce76b72e
commit 2993049bf4
8 changed files with 93 additions and 81 deletions
@@ -6,6 +6,7 @@ import {
type ReactNode,
} from "react";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import type { Project, ServerConfig } from "../api/types";
import { useHeat, useTree } from "../hooks/useBrowse";
import { urlForPath, urlForView, type Route } from "../router";
@@ -355,14 +356,14 @@ export default function Browser(props: {
actions={
<>
{canShare && (
<button id="share-btn" className="btn icon-only" title="Share" aria-label="Share" onClick={shareNow}>
<Button id="share-btn" variant="toolbar" className="icon-only" title="Share" aria-label="Share" onClick={shareNow}>
<Icon name="share" />
</button>
</Button>
)}
{canHistory && !path && !route.view && (
<button id="history-btn" className="btn" onClick={historyNow}>
<Button id="history-btn" variant="toolbar" onClick={historyNow}>
<Icon name="hist" /> <span className="lbl">History</span>
</button>
</Button>
)}
{canDownload && (
<a id="download" hidden download href={downloadURL} ref={downloadRef}>
@@ -370,9 +371,10 @@ export default function Browser(props: {
</a>
)}
{canMore && (
<button
<Button
id="more-btn"
className="btn icon-only"
variant="toolbar"
className="icon-only"
title="More actions"
aria-label="More actions"
onClick={(e) => {
@@ -381,7 +383,7 @@ export default function Browser(props: {
}}
>
<Icon name="dots" />
</button>
</Button>
)}
{moreOpen && (
<div id="more-menu" role="menu">
@@ -1,4 +1,5 @@
import { useRef } from "react";
import { Button } from "@/components/ui/button";
import { toast } from "../toast";
// Onboarding: a signed-in account with no projects shouldn't hit a blank
@@ -34,9 +35,9 @@ export function EmptyState({
<p>A teammate can send you a join link. Paste it here:</p>
<div className="ob-row">
<input id="ob-invite" type="text" placeholder="https://…/join/…" autoComplete="off" ref={invite} />
<button id="ob-join" className="pbtn" onClick={join}>
<Button id="ob-join" variant="primary" onClick={join}>
Join
</button>
</Button>
</div>
</div>
)}
@@ -45,9 +46,9 @@ export function EmptyState({
<p>Create a shared space for your team's files.</p>
<div className="ob-row">
<input id="ob-name" type="text" placeholder="Project name, e.g. wiki" autoComplete="off" ref={name} />
<button id="ob-create" className="pbtn" onClick={() => onCreate(name.current!.value.trim())}>
<Button id="ob-create" variant="primary" onClick={() => onCreate(name.current!.value.trim())}>
Create
</button>
</Button>
</div>
</div>
</div>
@@ -1,4 +1,5 @@
import { api } from "../api/http";
import { Button } from "@/components/ui/button";
import { copyText } from "../util";
import { toast } from "../toast";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
@@ -27,19 +28,19 @@ export function ShareDialog({
</p>
<div className="modal-url">{url}</div>
<div className="modal-actions">
<button
className="pbtn"
<Button
variant="primary"
onClick={() =>
copyText(url).then((ok) => toast(ok ? "Copied." : "Select and copy the link above."))
}
>
{copied ? "Copied ✓" : "Copy link"}
</button>
<button className="ai-btn" onClick={() => window.open(url, "_blank")}>
</Button>
<Button variant="subtle" onClick={() => window.open(url, "_blank")}>
Open
</button>
<button
className="ai-del"
</Button>
<Button
variant="subtle" className="ai-del"
onClick={async () => {
try {
await api("DELETE", "/api/shares/" + token);
@@ -51,10 +52,10 @@ export function ShareDialog({
}}
>
Revoke
</button>
<button className="ai-btn" onClick={onClose}>
</Button>
<Button variant="subtle" onClick={onClose}>
Done
</button>
</Button>
</div>
</DialogContent>
</Dialog>
@@ -10,6 +10,13 @@ const buttonVariants = cva(
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// House variants: class names style.css already owns, so migrated
// call sites keep the exact BearDrive look (+ shadcn focus/disabled
// plumbing from the base classes).
primary: "pbtn",
danger: "danger-btn",
subtle: "ai-btn",
toolbar: "btn",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
+9 -8
View File
@@ -1,4 +1,5 @@
import { useRef, useSyncExternalStore } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -108,12 +109,12 @@ function PromptBody({ m }: { m: Prompt }) {
onKeyDown={(e) => e.key === "Enter" && ok()}
/>
<div className="modal-actions">
<button className="ai-btn" onClick={() => done(null)}>
<Button variant="subtle" onClick={() => done(null)}>
Cancel
</button>
<button className="pbtn" onClick={ok}>
</Button>
<Button variant="primary" onClick={ok}>
{m.okLabel}
</button>
</Button>
</div>
</>
);
@@ -131,12 +132,12 @@ function ConfirmBody({ m }: { m: Confirm }) {
</DialogTitle>
<p className="modal-msg">{m.message}</p>
<div className="modal-actions">
<button className="ai-btn" onClick={() => done(false)}>
<Button variant="subtle" onClick={() => done(false)}>
Cancel
</button>
<button className={m.danger ? "danger-btn" : "pbtn"} onClick={() => done(true)} autoFocus>
</Button>
<Button variant={m.danger ? "danger" : "primary"} onClick={() => done(true)} autoFocus>
{m.confirmLabel}
</button>
</Button>
</div>
</>
);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#128059;</text></svg>">
<script type="module" crossorigin src="/assets/index-CZBsmcK-.js"></script>
<script type="module" crossorigin src="/assets/index-Ky-4qqmy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DagN_NN8.css">
</head>
<body>