buttons: one definition of the shape, worn by a <button> and by an <a>
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Successful in 40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m1s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m14s
Desktop (Tauri) / Update manifest (push) Successful in 6s
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Successful in 40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m1s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m14s
Desktop (Tauri) / Update manifest (push) Successful in 6s
The download links added in fd1e4ae carried their own copy of BaseButton's
class list, because BaseButton is a <button> and cannot hold an href — and a
download must be an anchor, so the browser's own download manager gets the
3-95 MB transfer instead of a blob this app would have to hold in memory.
A copy is not a solution to that; it is two primary buttons that look alike
until someone changes one. So the shape moves to `.btn` + `.btn-primary` /
`.btn-ghost` in the components layer, where both elements can wear it, and
neither owns it.
The `disabled:` variants stay on BaseButton. An anchor has no :disabled, so
they were never shared and pretending otherwise would put a rule in the
shared definition that only one of its two users can ever match.
Verified there is exactly one shape to unify and no third copy: `px-4 py-2.5`
appears in three other files and all three are something else (a toast, a
dashed quick-add affordance, a retention notice). The smaller brand buttons in
AppShell and NoteEditor are a different size, which is a size-variant question
and not this one. And exactly one call site passes a class to BaseButton —
`shrink-0` — which cannot conflict with anything the shape declares.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
This commit is contained in:
+18
-10
@@ -253,25 +253,33 @@ body {
|
||||
@apply inline-flex min-h-[2.25rem] items-center justify-center px-3;
|
||||
}
|
||||
}
|
||||
/* A DOWNLOAD is an anchor, never a button. These are 3-95 MB files, and the
|
||||
* browser's own download manager handles the transfer better than anything this
|
||||
* app would do with a blob - but only an <a> can carry an href, and BaseButton
|
||||
* is a <button>. So the button SHAPE lives here where an anchor can wear it.
|
||||
/* THE button shape — the ONE definition of it in the app.
|
||||
*
|
||||
* Declarations mirror BaseButton.vue exactly, minus its disabled: variants (an
|
||||
* anchor has no :disabled). The two are a pair: changing the look of one without
|
||||
* the other is how a page ends up with two kinds of primary button.
|
||||
* It lives here, in the components layer, rather than inside BaseButton.vue,
|
||||
* because not every button in this app is a <button>. A DOWNLOAD has to be an
|
||||
* anchor: these are 3-95 MB installers, only an <a> can carry an href, and the
|
||||
* browser's own download manager handles that transfer better than anything the
|
||||
* app would do by fetching to a blob. BaseButton cannot serve that case, and a
|
||||
* second copy of its class list for anchors is how a page ends up with two
|
||||
* kinds of primary button that drift apart.
|
||||
*
|
||||
* So: BaseButton.vue wears these, and so does any anchor that must read as a
|
||||
* button. Neither owns the look.
|
||||
*
|
||||
* The `disabled:` variants are NOT here on purpose — an anchor has no
|
||||
* :disabled. BaseButton adds them itself, which is exactly the split: shared
|
||||
* where it is shared, local where the element differs.
|
||||
*/
|
||||
.btn-link {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm
|
||||
font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
|
||||
focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50
|
||||
dark:focus-visible:ring-offset-neutral-950;
|
||||
}
|
||||
.btn-link-primary {
|
||||
.btn-primary {
|
||||
@apply bg-brand text-neutral-900 shadow-sm hover:bg-brand-600 active:bg-brand-700;
|
||||
}
|
||||
.btn-link-ghost {
|
||||
.btn-ghost {
|
||||
@apply text-neutral-700 hover:bg-neutral-200/70 dark:text-neutral-200 dark:hover:bg-neutral-800;
|
||||
}
|
||||
.nav-link {
|
||||
|
||||
Reference in New Issue
Block a user