diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-12 22:22:33 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-12 22:22:33 +0200 |
| commit | 1cef356be95a67f95d40f5b0b131f98d11f13c13 (patch) | |
| tree | a0d93976998b0395e373a7a9f0127cc35e77a6f2 | |
| parent | b11a40840cfe0e9978f78cacd68d68764eabec58 (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.css | 86 | ||||
| -rw-r--r-- | src/lib/components/Button.svelte | 150 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 3 | ||||
| -rw-r--r-- | src/routes/groceries/catalog/+page.svelte | 22 | ||||
| -rw-r--r-- | src/routes/groceries/categories/+page.svelte | 13 | ||||
| -rw-r--r-- | src/routes/grocery/[id]/+page.svelte | 13 | ||||
| -rw-r--r-- | src/routes/login/+page.svelte | 7 | ||||
| -rw-r--r-- | src/routes/projects/[id]/+page.svelte | 14 | ||||
| -rw-r--r-- | src/routes/projects/[projectId]/routine/[routineId]/+page.svelte | 29 |
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" + >×</Button > - × - </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" + >×</Button > - × - </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> |
