aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 16:41:28 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 16:41:28 +0200
commit751af663f38e174203c10438e5ef36277e93cd68 (patch)
tree6a5504ef58e43e79d85aca0c9df5257b9914049b
parent4f2f6d757380e80f8e2937ffe99f4390865cb7fa (diff)
Use radio inputs and collapsible curation on ride detail
Replace dropdowns with vertical radio groups for bike and ride type. Make curation block collapsible (details/summary) collapsed by default and expanded when both categories are empty.
-rw-r--r--web/static/style.css23
-rw-r--r--web/templates.templ55
2 files changed, 39 insertions, 39 deletions
diff --git a/web/static/style.css b/web/static/style.css
index 3c9c05b..755a5cb 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -157,15 +157,28 @@ select { border: 1px solid var(--color-input-border); border-radius: .55rem; pad
.section-heading .eyebrow { margin-bottom: .2rem; }
.section-heading h2 { margin: 0; font-size: 1.2rem; }
.section-count { display: grid; place-items: center; min-width: 2rem; height: 2rem; border-radius: 999px; background: var(--color-secondary-surface); color: var(--color-forest); font-weight: 800; }
-.categories-panel { padding: 1rem 1.25rem; }
-.categories-layout { display: flex; align-items: center; justify-content: space-between; gap: 1.25rem; flex-wrap: wrap; }
+.categories-panel { padding: 0; overflow: hidden; }
+.categories-details { display: block; }
+.categories-summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; cursor: pointer; list-style: none; }
+.categories-summary::-webkit-details-marker { display: none; }
+.categories-summary::marker { display: none; }
+.categories-summary:hover { background: var(--color-secondary-surface); }
+.categories-summary:focus-visible { outline: .2rem solid var(--color-focus); outline-offset: -.2rem; }
.categories-heading { display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap; }
.categories-heading .eyebrow { margin: 0; }
.categories-heading h2 { margin: 0; font-size: 1.1rem; }
+.categories-chevron { color: var(--color-accent); font-size: 1.5rem; font-weight: 500; line-height: 1; transition: transform .2s; }
+.categories-details[open] .categories-chevron { transform: rotate(180deg); }
+.categories-details[open] .categories-summary { border-bottom: 1px solid var(--color-border-subtle); }
.category-badge { display: inline-flex; align-items: center; border-radius: 999px; padding: .35rem .65rem; background: var(--color-notice-surface); color: var(--color-notice-text); font-size: .75rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
-.category-form { display: flex; align-items: end; gap: .65rem; flex-wrap: wrap; margin-left: auto; }
-.category-form label { min-width: 8.5rem; }
-.category-form select { min-width: 8.5rem; }
+.category-form { display: flex; align-items: end; gap: 1rem 1.25rem; flex-wrap: wrap; padding: 1rem 1.25rem; }
+.category-fieldset { display: grid; gap: .35rem; margin: 0; padding: 0; border: 0; min-width: 0; }
+.category-fieldset legend { padding: 0; color: var(--color-muted); font-size: .85rem; font-weight: 650; }
+.category-options { display: grid; gap: .4rem; }
+.category-option { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid var(--color-input-border); border-radius: 999px; padding: .4rem .65rem; background: var(--color-surface); color: var(--color-foreground); font-size: .85rem; font-weight: 650; cursor: pointer; }
+.category-option:has(input:checked) { border-color: var(--color-leaf); background: var(--color-secondary-surface); color: var(--color-forest); }
+.category-option input { accent-color: var(--color-accent); }
+.category-option:focus-within { outline: .2rem solid var(--color-focus); outline-offset: .1rem; }
.category-form .button { align-self: end; padding: .55rem .9rem; }
.data-quality-bulk { display: flex; align-items: end; gap: .75rem; flex-wrap: wrap; margin-bottom: 1rem; }
.data-quality-bulk label { min-width: 8.5rem; }
diff --git a/web/templates.templ b/web/templates.templ
index 8fbed0c..10008d2 100644
--- a/web/templates.templ
+++ b/web/templates.templ
@@ -228,47 +228,34 @@ templ RideDetailContent(data RideDetailView) {
</dl>
</section>
<section class="panel categories-panel">
- <div class="categories-layout">
- <div class="categories-heading">
- <p class="eyebrow">Curation</p>
- <h2>Categories</h2>
- </div>
+ <details class="categories-details" open?={ data.Ride.BikeType == "" && data.Ride.RideType == "" }>
+ <summary class="categories-summary">
+ <div class="categories-heading">
+ <p class="eyebrow">Curation</p>
+ <h2>Categories</h2>
+ </div>
+ <span class="categories-chevron" aria-hidden="true">⌄</span>
+ </summary>
<form method="post" action={ templ.URL(rideCategoriesUpdateURL(data.Ride.ID)) } class="category-form">
- <label for="ride-bike-type">Bike type
- <select id="ride-bike-type" name="bike_type" required>
- if data.Ride.BikeType == "" {
- <option value="" selected>—</option>
- } else {
- <option value="">—</option>
- }
+ <fieldset class="category-fieldset">
+ <legend>Bike type</legend>
+ <div class="category-options">
for _, opt := range bikeTypeOptions {
- if data.Ride.BikeType == opt {
- <option value={ string(opt) } selected>{ bikeTypeLabels[opt] }</option>
- } else {
- <option value={ string(opt) }>{ bikeTypeLabels[opt] }</option>
- }
- }
- </select>
- </label>
- <label for="ride-ride-type">Ride type
- <select id="ride-ride-type" name="ride_type" required>
- if data.Ride.RideType == "" {
- <option value="" selected>—</option>
- } else {
- <option value="">—</option>
+ <label class="category-option"><input type="radio" name="bike_type" value={ string(opt) } required checked?={ data.Ride.BikeType == opt }/><span>{ bikeTypeLabels[opt] }</span></label>
}
+ </div>
+ </fieldset>
+ <fieldset class="category-fieldset">
+ <legend>Ride type</legend>
+ <div class="category-options">
for _, opt := range rideTypeOptions {
- if data.Ride.RideType == opt {
- <option value={ string(opt) } selected>{ rideTypeLabels[opt] }</option>
- } else {
- <option value={ string(opt) }>{ rideTypeLabels[opt] }</option>
- }
+ <label class="category-option"><input type="radio" name="ride_type" value={ string(opt) } required checked?={ data.Ride.RideType == opt }/><span>{ rideTypeLabels[opt] }</span></label>
}
- </select>
- </label>
+ </div>
+ </fieldset>
<button class="button" type="submit">Save</button>
</form>
- </div>
+ </details>
</section>
if data.HasRoute {
if len(data.Profile.OfficialClimbs()) > 0 {