aboutsummaryrefslogtreecommitdiff
path: root/src/lib/components/Button.svelte
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-09-12 22:22:33 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-09-12 22:22:33 +0200
commit1cef356be95a67f95d40f5b0b131f98d11f13c13 (patch)
treea0d93976998b0395e373a7a9f0127cc35e77a6f2 /src/lib/components/Button.svelte
parentb11a40840cfe0e9978f78cacd68d68764eabec58 (diff)
Extract Button primitive, migrate all call sites
Variants primary/accent/ghost/done with scoped styles. Disabled handling unified; block/size/style props cover login stretch, done-lg, save-check positioning. Delete btn-* globals and dead page overrides.
Diffstat (limited to 'src/lib/components/Button.svelte')
-rw-r--r--src/lib/components/Button.svelte150
1 files changed, 150 insertions, 0 deletions
diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte
new file mode 100644
index 0000000..30e9fc2
--- /dev/null
+++ b/src/lib/components/Button.svelte
@@ -0,0 +1,150 @@
+<script lang="ts">
+ import type { Snippet } from 'svelte';
+
+ let {
+ variant = 'primary',
+ type = 'submit',
+ disabled = false,
+ name,
+ value,
+ ariaLabel,
+ block = false,
+ size = 'md',
+ style,
+ class: klass = '',
+ onclick,
+ children,
+ }: {
+ variant?: 'primary' | 'accent' | 'ghost' | 'done';
+ type?: 'button' | 'submit' | 'reset';
+ disabled?: boolean;
+ name?: string;
+ value?: string | number;
+ ariaLabel?: string;
+ block?: boolean;
+ size?: 'md' | 'lg';
+ style?: string;
+ class?: string;
+ onclick?: (e: MouseEvent) => void;
+ children: Snippet;
+ } = $props();
+
+ const cls = $derived(
+ [variant, size === 'lg' ? 'lg' : '', block ? 'block' : '', klass]
+ .filter(Boolean)
+ .join(' '),
+ );
+</script>
+
+<button
+ {type}
+ {disabled}
+ {name}
+ {value}
+ aria-label={ariaLabel}
+ class={cls}
+ {style}
+ {onclick}
+>
+ {@render children()}
+</button>
+
+<style>
+ button {
+ cursor: pointer;
+ font-family: inherit;
+ transition: all 0.2s ease;
+ border: none;
+ }
+
+ button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ }
+
+ .primary {
+ padding: 0.75rem 1.5rem;
+ border-radius: 0.75rem;
+ background: var(--accent);
+ color: var(--white);
+ font-size: 0.9375rem;
+ font-weight: 500;
+ align-self: flex-start;
+ box-shadow: 0 1px 2px var(--shadow);
+ }
+
+ .primary:hover:not(:disabled) {
+ background: var(--accent-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px var(--shadow-accent-lg);
+ }
+
+ .accent {
+ width: 3rem;
+ height: 3rem;
+ flex-shrink: 0;
+ padding: 0;
+ border-radius: 0.75rem;
+ background: var(--accent);
+ color: var(--white);
+ font-size: 1.25rem;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 1px 2px var(--shadow);
+ }
+
+ .accent:hover:not(:disabled) {
+ background: var(--accent-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px var(--shadow-accent-lg);
+ }
+
+ .ghost {
+ flex-shrink: 0;
+ width: 2rem;
+ aspect-ratio: 1;
+ padding: 0;
+ border-radius: 0.625rem;
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .ghost:hover:not(:disabled) {
+ background: var(--danger-hover-bg);
+ color: var(--danger);
+ }
+
+ .done {
+ flex-shrink: 0;
+ padding: 0.5rem 0.875rem;
+ border-radius: 0.625rem;
+ background: var(--success);
+ color: var(--white);
+ font-size: 0.875rem;
+ font-weight: 500;
+ }
+
+ .done:hover:not(:disabled) {
+ filter: brightness(1.05);
+ transform: translateY(-1px);
+ }
+
+ .done.lg {
+ padding: 0.75rem 1.25rem;
+ border-radius: 0.75rem;
+ font-size: 0.9375rem;
+ }
+
+ .block {
+ align-self: stretch;
+ width: 100%;
+ }
+</style>