aboutsummaryrefslogtreecommitdiff
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
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.
-rw-r--r--src/app.css86
-rw-r--r--src/lib/components/Button.svelte150
-rw-r--r--src/routes/+page.svelte3
-rw-r--r--src/routes/groceries/catalog/+page.svelte22
-rw-r--r--src/routes/groceries/categories/+page.svelte13
-rw-r--r--src/routes/grocery/[id]/+page.svelte13
-rw-r--r--src/routes/login/+page.svelte7
-rw-r--r--src/routes/projects/[id]/+page.svelte14
-rw-r--r--src/routes/projects/[projectId]/routine/[routineId]/+page.svelte29
9 files changed, 181 insertions, 156 deletions
diff --git a/src/app.css b/src/app.css
index b219650..87817bd 100644
--- a/src/app.css
+++ b/src/app.css
@@ -206,49 +206,6 @@
box-shadow: 0 0 0 3px var(--shadow-accent-focus);
}
-/* Buttons */
-.btn-primary {
- padding: 0.75rem 1.5rem;
- border: none;
- border-radius: 0.75rem;
- background: var(--accent);
- color: var(--white);
- font-size: 0.9375rem;
- font-weight: 500;
- align-self: flex-start;
- cursor: pointer;
- transition: all 0.2s ease;
- box-shadow: 0 1px 2px var(--shadow);
-}
-
-.btn-primary:hover {
- background: var(--accent-hover);
- transform: translateY(-1px);
- box-shadow: 0 4px 8px var(--shadow-accent-lg);
-}
-
-.btn-ghost {
- flex-shrink: 0;
- width: 2rem;
- aspect-ratio: 1;
- padding: 0;
- border: none;
- border-radius: 0.625rem;
- background: transparent;
- color: var(--text-secondary);
- font-size: 1.25rem;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.btn-ghost:hover {
- background: var(--danger-hover-bg);
- color: var(--danger);
-}
-
.score {
flex-shrink: 0;
min-width: 2.75rem;
@@ -296,32 +253,6 @@
min-width: 0;
}
-.btn-accent {
- width: 3rem;
- height: 3rem;
- flex-shrink: 0;
- padding: 0;
- border: none;
- border-radius: 0.75rem;
- background: var(--accent);
- color: var(--white);
- font-size: 1.25rem;
- font-weight: 500;
- font-family: inherit;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
- box-shadow: 0 1px 2px var(--shadow);
-}
-
-.btn-accent:hover {
- background: var(--accent-hover);
- transform: translateY(-1px);
- box-shadow: 0 4px 8px var(--shadow-accent-lg);
-}
-
.status {
flex-shrink: 0;
display: inline-flex;
@@ -378,20 +309,3 @@
visibility: visible;
opacity: 1;
}
-
-.btn-done {
- flex-shrink: 0;
- padding: 0.5rem 0.875rem;
- border: none;
- border-radius: 0.625rem;
- background: var(--success);
- color: var(--white);
- font-size: 0.875rem;
- font-weight: 500;
- transition: all 0.2s ease;
-}
-
-.btn-done:hover {
- filter: brightness(1.05);
- transform: translateY(-1px);
-}
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>
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 5214bd6..1adfb6b 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import { enhance } from '$app/forms';
import ScoreBadge from '$lib/components/ScoreBadge.svelte';
+ import Button from '$lib/components/Button.svelte';
let { data, form } = $props();
@@ -22,7 +23,7 @@
placeholder="Nouveau projet..."
autocomplete="off"
/>
- <button class="btn-accent" type="submit" aria-label="Ajouter">+</button>
+ <Button variant="accent" ariaLabel="Ajouter">+</Button>
</form>
{#if form?.error}
diff --git a/src/routes/groceries/catalog/+page.svelte b/src/routes/groceries/catalog/+page.svelte
index 2b92a35..a7f3c0e 100644
--- a/src/routes/groceries/catalog/+page.svelte
+++ b/src/routes/groceries/catalog/+page.svelte
@@ -2,6 +2,7 @@
import { enhance } from '$app/forms';
import { SvelteSet } from 'svelte/reactivity';
import GroceryRow from '$lib/components/GroceryRow.svelte';
+ import Button from '$lib/components/Button.svelte';
import { groupByCategory } from '$lib/shared/group-by-category';
let { data, form } = $props();
@@ -45,13 +46,9 @@
<option value={cat.id}>{cat.name}</option>
{/each}
</select>
- <button
- class="btn-primary"
- type="submit"
- disabled={selectedIds.size === 0}
+ <Button disabled={selectedIds.size === 0}
+ >Mettre à jour ({selectedIds.size})</Button
>
- Mettre à jour ({selectedIds.size})
- </button>
</form>
</div>
{/if}
@@ -118,19 +115,6 @@
min-width: 0;
}
- .bulk-bar .btn-primary {
- padding: 0.75rem 1rem;
- align-self: center;
- white-space: nowrap;
- }
-
- .bulk-bar .btn-primary:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- transform: none;
- box-shadow: none;
- }
-
.error {
margin-bottom: 1rem;
}
diff --git a/src/routes/groceries/categories/+page.svelte b/src/routes/groceries/categories/+page.svelte
index af85adf..3d6d5b3 100644
--- a/src/routes/groceries/categories/+page.svelte
+++ b/src/routes/groceries/categories/+page.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { Category } from '$lib/server/services/types';
+ import Button from '$lib/components/Button.svelte';
let { data, form } = $props();
let name = $state('');
@@ -46,7 +47,7 @@
placeholder="Nouvelle catégorie..."
autocomplete="off"
/>
- <button class="btn-accent" type="submit" aria-label="Ajouter">+</button>
+ <Button variant="accent" ariaLabel="Ajouter">+</Button>
</form>
{#if form?.error}
@@ -81,15 +82,9 @@
>
<span class="drag-handle">⋮⋮</span>
<span class="cat-name">{cat.name}</span>
- <button
- class="btn-ghost"
- name="id"
- value={cat.id}
- type="submit"
- aria-label="Supprimer"
+ <Button variant="ghost" name="id" value={cat.id} ariaLabel="Supprimer"
+ >&times;</Button
>
- &times;
- </button>
</li>
{/each}
</ul>
diff --git a/src/routes/grocery/[id]/+page.svelte b/src/routes/grocery/[id]/+page.svelte
index 4a96f32..8a0a479 100644
--- a/src/routes/grocery/[id]/+page.svelte
+++ b/src/routes/grocery/[id]/+page.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import { enhance } from '$app/forms';
+ import Button from '$lib/components/Button.svelte';
let { data, form } = $props();
let name = $state('');
@@ -132,7 +133,7 @@
/>
</label>
</div>
- <button class="btn-primary" type="submit">Ajouter une référence</button>
+ <Button>Ajouter une référence</Button>
</form>
{#if form?.error}
@@ -154,15 +155,9 @@
<span class="ref-detail">Note : {ref.qualityScore}/100</span>
{/if}
</div>
- <button
- class="btn-ghost"
- name="id"
- value={ref.id}
- type="submit"
- aria-label="Supprimer"
+ <Button variant="ghost" name="id" value={ref.id} ariaLabel="Supprimer"
+ >&times;</Button
>
- &times;
- </button>
</li>
{/each}
</ul>
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte
index 303043f..3da1529 100644
--- a/src/routes/login/+page.svelte
+++ b/src/routes/login/+page.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
+ import Button from '$lib/components/Button.svelte';
let { data, form } = $props();
</script>
@@ -23,7 +24,7 @@
/>
</label>
<input type="hidden" name="next" value={data.next} />
- <button class="btn-primary" type="submit">Entrer</button>
+ <Button block>Entrer</Button>
</form>
{#if form?.error}
@@ -73,10 +74,6 @@
gap: 1rem;
}
- .btn-primary {
- align-self: stretch;
- }
-
.error {
margin-top: 1rem;
}
diff --git a/src/routes/projects/[id]/+page.svelte b/src/routes/projects/[id]/+page.svelte
index 8a3561b..2efe05b 100644
--- a/src/routes/projects/[id]/+page.svelte
+++ b/src/routes/projects/[id]/+page.svelte
@@ -4,6 +4,7 @@
import HealthChart from '$lib/components/HealthChart.svelte';
import ScoreBadge from '$lib/components/ScoreBadge.svelte';
import StatusBadge from '$lib/components/StatusBadge.svelte';
+ import Button from '$lib/components/Button.svelte';
import {
formatFrequency,
parseIntervalToDays,
@@ -82,14 +83,13 @@
autocomplete="off"
aria-label="Intervalle (ex: 1d, 2w, 3m)"
/>
- <button
- class="btn-accent"
- type="submit"
+ <Button
+ variant="accent"
disabled={!name.trim() || frequencyDays === null}
- aria-label="Ajouter"
+ ariaLabel="Ajouter"
>
+
- </button>
+ </Button>
</form>
{#if frequency !== '' && frequencyDays === null}
@@ -105,12 +105,12 @@
<li class="item-row item-row--card routine-row">
<form method="POST" action="?/complete" use:enhance>
<input type="hidden" name="id" value={routine.id} />
- <button type="submit" class="btn-ghost">
+ <Button variant="ghost">
<StatusBadge
status={routine.status}
overdueDays={routine.overdueDays}
/>
- </button>
+ </Button>
</form>
<div class="routine-info">
<a
diff --git a/src/routes/projects/[projectId]/routine/[routineId]/+page.svelte b/src/routes/projects/[projectId]/routine/[routineId]/+page.svelte
index 74880cb..867719a 100644
--- a/src/routes/projects/[projectId]/routine/[routineId]/+page.svelte
+++ b/src/routes/projects/[projectId]/routine/[routineId]/+page.svelte
@@ -3,6 +3,7 @@
import HealthChart from '$lib/components/HealthChart.svelte';
import ScoreBadge from '$lib/components/ScoreBadge.svelte';
import StatusBadge from '$lib/components/StatusBadge.svelte';
+ import Button from '$lib/components/Button.svelte';
import {
formatFrequency,
parseIntervalToDays,
@@ -111,16 +112,15 @@
{#if frequency !== '' && frequencyDays === null}
<p class="error">Intervalle invalide — ex : 1d, 2w, 3m, 1y</p>
{/if}
- <button
- class="btn-primary btn-save"
- type="submit"
+ <Button
disabled={!name.trim() || frequencyDays === null}
+ style="position: relative; padding-right: 2.75rem;"
>
- <span class="btn-save-label">Enregistrer</span>
- <span class="btn-save-check" class:visible={saved} aria-hidden="true"
+ <span>Enregistrer</span>
+ <span class="save-check" class:visible={saved} aria-hidden="true"
>✓</span
>
- </button>
+ </Button>
</form>
{/if}
@@ -170,7 +170,7 @@
placeholder="Note (optionnel)"
autocomplete="off"
/>
- <button class="btn-done btn-done-lg" type="submit">Fait</button>
+ <Button variant="done" size="lg">Fait</Button>
</form>
{/if}
@@ -259,12 +259,6 @@
flex: 1;
}
- .btn-done-lg {
- padding: 0.75rem 1.25rem;
- border-radius: 0.75rem;
- font-size: 0.9375rem;
- }
-
.completion-row {
flex-wrap: wrap;
gap: 0.25rem 0.75rem;
@@ -298,12 +292,7 @@
color: var(--danger);
}
- .btn-save {
- position: relative;
- padding-right: 2.75rem;
- }
-
- .btn-save-check {
+ .save-check {
position: absolute;
right: 1rem;
top: 50%;
@@ -312,7 +301,7 @@
transition: opacity 0.2s ease;
}
- .btn-save-check.visible {
+ .save-check.visible {
opacity: 1;
}
</style>