+80
-33
Diff
round #0
+2
-2
internal/web/components/action_bar.templ
+2
-2
internal/web/components/action_bar.templ
···
288
288
289
289
// ReportModal renders an inline report modal for the action bar
290
290
templ ReportModal(props ReportModalProps) {
291
-
<dialog id={ props.ID } class="modal-dialog" x-data="{ reason: '', charCount: 0, submitting: false, error: '', success: false }">
291
+
<dialog id={ props.ID } class="modal-dialog" aria-labelledby={ props.ID + "-title" } x-data="{ reason: '', charCount: 0, submitting: false, error: '', success: false }">
292
292
<div class="modal-content">
293
-
<h3 class="modal-title">Report Content</h3>
293
+
<h3 id={ props.ID + "-title" } class="modal-title">Report Content</h3>
294
294
<template x-if="!success">
295
295
<form
296
296
@submit.prevent={ fmt.Sprintf(`
+1
-1
internal/web/components/layout.templ
+1
-1
internal/web/components/layout.templ
···
115
115
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/>
116
116
<link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32"/>
117
117
<link rel="apple-touch-icon" href="/static/icon-192.svg"/>
118
-
<link rel="stylesheet" href="/static/css/output.css?v=0.11.0"/>
118
+
<link rel="stylesheet" href="/static/css/output.css?v=0.12.0"/>
119
119
<style>
120
120
[x-cloak] { display: none !important; }
121
121
</style>
+10
-7
internal/web/pages/bean_view.templ
+10
-7
internal/web/pages/bean_view.templ
···
44
44
{ props.Bean.Roaster.Name }
45
45
</a>
46
46
if props.Bean.Roaster.Location != "" {
47
-
<span style="color: var(--text-faint)">·</span>
47
+
<span class="text-faint">·</span>
48
48
<span class="inline-flex items-center gap-1">
49
49
@components.IconMapPin()
50
50
{ props.Bean.Roaster.Location }
···
194
194
// BeanCloseBagConfirm renders a styled confirmation dialog for closing a bag
195
195
templ BeanCloseBagConfirm(props BeanViewProps) {
196
196
<dialog
197
-
id="close-bag-confirm"
197
+
id={ "close-bag-confirm-" + props.Bean.RKey }
198
198
class="modal-dialog"
199
199
x-ref="closeDialog"
200
200
x-init="$watch('showConfirm', v => { if (v) $refs.closeDialog.showModal() })"
201
201
@close="showConfirm = false"
202
+
aria-labelledby={ "close-bag-title-" + props.Bean.RKey }
202
203
>
203
204
<div class="modal-content">
204
-
<h3 class="modal-title">Close Bag</h3>
205
+
<h3 id={ "close-bag-title-" + props.Bean.RKey } class="modal-title">Close Bag</h3>
205
206
<p class="text-brown-700 text-sm mb-4">
206
207
Mark this bag as finished? You can reopen it later from the edit menu.
207
208
</p>
···
230
231
// BeanRateModal renders a dialog for rating or editing a bean's rating
231
232
templ BeanRateModal(props BeanViewProps) {
232
233
<dialog
233
-
id="rate-bag-modal"
234
+
id={ "rate-bag-modal-" + props.Bean.RKey }
234
235
class="modal-dialog"
235
236
x-ref="rateDialog"
237
+
aria-labelledby={ "rate-bag-title-" + props.Bean.RKey }
236
238
>
237
239
<div class="modal-content">
238
-
<h3 class="modal-title">
240
+
<h3 id={ "rate-bag-title-" + props.Bean.RKey } class="modal-title">
239
241
if props.Bean.Rating != nil {
240
242
Edit Rating
241
243
} else {
···
249
251
max="10"
250
252
x-model.number="rating"
251
253
class="w-full accent-brown-700"
254
+
aria-label="Bag rating, 1 to 10"
252
255
/>
253
-
<div class="text-center text-3xl font-bold text-brown-800">
256
+
<div class="text-center text-3xl font-bold text-brown-800" aria-hidden="true">
254
257
<span x-text="rating"></span>/10
255
258
</div>
256
259
</div>
···
268
271
<button
269
272
type="button"
270
273
@click={ beanPatchAction(props.Bean, `{ "rating": null }`, "Failed to remove rating") }
271
-
class="flex-1 btn-secondary text-red-700"
274
+
class="flex-1 btn-secondary text-danger"
272
275
:disabled="_saving"
273
276
>
274
277
<span x-show="!_saving">Remove</span>
+14
-10
internal/web/pages/brew_view.templ
+14
-10
internal/web/pages/brew_view.templ
···
221
221
}
222
222
</a>
223
223
if brew.Bean.Roaster != nil && brew.Bean.Roaster.Name != "" {
224
-
<div class="text-sm mt-1" style="color: var(--text-secondary)">
224
+
<div class="text-sm mt-1 text-secondary">
225
225
<span class="inline-flex items-center gap-1">
226
226
@components.IconStore()
227
227
<a href={ templ.SafeURL(fmt.Sprintf("/roasters/%s?owner=%s", brew.Bean.Roaster.RKey, owner)) } class="hover:underline">
···
230
230
</span>
231
231
</div>
232
232
}
233
-
<div class="flex flex-wrap gap-3 mt-2 text-sm" style="color: var(--text-muted)">
233
+
<div class="flex flex-wrap gap-3 mt-2 text-sm text-muted">
234
234
if brew.Bean.Origin != "" {
235
235
<span class="inline-flex items-center gap-1">
236
236
@components.IconMapPin()
···
364
364
</template>
365
365
<template x-if="showForm && !success">
366
366
<div class="space-y-3">
367
-
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider">Save as Recipe</h3>
367
+
<h3 class="text-sm font-medium text-muted uppercase tracking-wider">Save as Recipe</h3>
368
+
<label for={ "save-recipe-name-" + brewRKey } class="sr-only">Recipe name</label>
368
369
<input
370
+
id={ "save-recipe-name-" + brewRKey }
369
371
type="text"
370
372
x-model="name"
371
-
placeholder="Recipe name *"
373
+
placeholder="Recipe name"
374
+
required
375
+
aria-required="true"
372
376
class="w-full form-input"
373
377
/>
374
378
<template x-if="error">
375
-
<div class="text-red-600 text-sm" x-text="error"></div>
379
+
<div class="text-danger text-sm" x-text="error"></div>
376
380
</template>
377
381
<div class="flex gap-2">
378
382
<button
···
393
397
</div>
394
398
</template>
395
399
<template x-if="success">
396
-
<div class="text-center text-green-700 text-sm font-medium py-2">
400
+
<div class="text-center text-success text-sm font-medium py-2">
397
401
Recipe saved!
398
402
</div>
399
403
</template>
···
409
413
<div>
410
414
<span class="detail-label mb-2 block">Recipe</span>
411
415
<a href={ templ.SafeURL(fmt.Sprintf("/recipes/%s?owner=%s", recipe.RKey, owner)) } class="detail-value-lg hover:underline">{ recipe.Name }</a>
412
-
<div class="flex flex-wrap gap-3 mt-2 text-sm" style="color: var(--text-muted)">
416
+
<div class="flex flex-wrap gap-3 mt-2 text-sm text-muted">
413
417
if recipe.CoffeeAmount > 0 {
414
418
<span class="inline-flex items-center gap-1">
415
419
@components.IconCoffee()
···
435
439
}
436
440
</div>
437
441
if recipe.Notes != "" {
438
-
<div class="mt-2 text-sm italic" style="color: var(--text-secondary)">"{ recipe.Notes }"</div>
442
+
<div class="mt-2 text-sm italic text-secondary">"{ recipe.Notes }"</div>
439
443
}
440
444
</div>
441
445
}
···
451
455
</span>
452
456
<div class="space-y-2">
453
457
for _, pour := range pours {
454
-
<div class="flex gap-4 text-sm py-2" style="border-bottom: 1px solid var(--surface-border)">
458
+
<div class="pour-row">
455
459
<span class="detail-value">{ fmt.Sprintf("%dg", pour.WaterAmount) }</span>
456
460
// TODO: add a setting to allow users to configure "at" vs "for" in pours display here
457
-
<span style="color: var(--text-muted)">{ "for " + bff.FormatTime(pour.TimeSeconds) }</span>
461
+
<span class="text-muted">{ "for " + bff.FormatTime(pour.TimeSeconds) }</span>
458
462
</div>
459
463
}
460
464
</div>
+2
-2
internal/web/pages/recipe_view.templ
+2
-2
internal/web/pages/recipe_view.templ
···
99
99
</span>
100
100
<div>
101
101
for _, pour := range props.Recipe.Pours {
102
-
<div class="flex gap-4 text-sm py-2" style="border-bottom: 1px solid var(--surface-border)">
102
+
<div class="pour-row">
103
103
<span class="detail-value">{ fmt.Sprintf("%dg", pour.WaterAmount) }</span>
104
-
<span style="color: var(--text-muted)">{ "for " + bff.FormatTime(pour.TimeSeconds) }</span>
104
+
<span class="text-muted">{ "for " + bff.FormatTime(pour.TimeSeconds) }</span>
105
105
</div>
106
106
}
107
107
</div>
+2
-2
internal/web/pages/roaster_view.templ
+2
-2
internal/web/pages/roaster_view.templ
···
44
44
</span>
45
45
</span>
46
46
if props.Roaster.Location != "" {
47
-
<span class="label-origin-hero" style="font-size: 1.5rem">{ props.Roaster.Location }</span>
47
+
<span class="detail-value-lg">{ props.Roaster.Location }</span>
48
48
} else {
49
-
<span class="text-sm" style="color: var(--text-faint)">—</span>
49
+
<span class="text-sm text-faint">—</span>
50
50
}
51
51
</div>
52
52
if props.Roaster.Website != "" {
+48
-9
static/css/app.css
+48
-9
static/css/app.css
···
34
34
--feed-board-border: #B09470;
35
35
/* Sticky note base — warm off-white */
36
36
--feed-card-bg: #FFFDF5;
37
-
/* Dot-grid — faint dots on warm paper, for journal pages */
38
-
--texture-dotgrid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%23d2bab0' fill-opacity='0.35'/%3E%3C/svg%3E");
39
-
/* Kraft grain — subtle fiber texture for label pages */
40
-
--texture-kraft: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.15 0 0 0 0.1 0.1 0 0 0 0.06 0.05 0 0 0 0.02 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E");
37
+
/* Dot-grid + kraft — defined once per theme, theme blocks just swap the active alias */
38
+
--texture-dotgrid-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%23d2bab0' fill-opacity='0.35'/%3E%3C/svg%3E");
39
+
--texture-dotgrid-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%235A4A40' fill-opacity='0.4'/%3E%3C/svg%3E");
40
+
--texture-kraft-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.15 0 0 0 0.1 0.1 0 0 0 0.06 0.05 0 0 0 0.02 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E");
41
+
--texture-kraft-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.08 0 0 0 0.04 0.06 0 0 0 0.03 0.03 0 0 0 0.01 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E");
42
+
--texture-dotgrid: var(--texture-dotgrid-light);
43
+
--texture-kraft: var(--texture-kraft-light);
41
44
/* Journal paper — warm cream */
42
45
--journal-bg: #FFFDF5;
43
46
/* Kraft — slightly warmer/darker than card bg */
···
124
127
--rating-bg: #fef3c7;
125
128
--rating-text: #78350f;
126
129
130
+
/* Status text — danger / success */
131
+
--text-danger: #b91c1c;
132
+
--text-success: #15803d;
133
+
127
134
/* Alerts/warnings */
128
135
--alert-warning-bg: #fffbeb;
129
136
--alert-warning-border: #fbbf24;
···
162
169
--surface-border: #2E211B;
163
170
164
171
/* Journal/label page backgrounds */
165
-
--texture-dotgrid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%235A4A40' fill-opacity='0.4'/%3E%3C/svg%3E");
166
-
--texture-kraft: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.08 0 0 0 0.04 0.06 0 0 0 0.03 0.03 0 0 0 0.01 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E");
172
+
--texture-dotgrid: var(--texture-dotgrid-dark);
173
+
--texture-kraft: var(--texture-kraft-dark);
167
174
--journal-bg: #1A1210;
168
175
--kraft-bg: #201814;
169
176
···
234
241
--rating-bg: rgba(251, 191, 36, 0.15);
235
242
--rating-text: #fbbf24;
236
243
244
+
/* Status text — danger / success */
245
+
--text-danger: #fca5a5;
246
+
--text-success: #86efac;
247
+
237
248
/* Alerts/warnings */
238
249
--alert-warning-bg: rgba(251, 191, 36, 0.08);
239
250
--alert-warning-border: rgba(251, 191, 36, 0.3);
···
329
340
--feed-board-bg: #3D2D22;
330
341
--feed-board-border: #4A3828;
331
342
--feed-card-bg: #1A1210;
332
-
--texture-dotgrid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%235A4A40' fill-opacity='0.4'/%3E%3C/svg%3E");
333
-
--texture-kraft: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.08 0 0 0 0.04 0.06 0 0 0 0.03 0.03 0 0 0 0.01 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E");
343
+
--texture-dotgrid: var(--texture-dotgrid-dark);
344
+
--texture-kraft: var(--texture-kraft-dark);
334
345
--journal-bg: #1A1210;
335
346
--kraft-bg: #201814;
336
347
--header-bg-from: #0F0A08;
···
376
387
--type-brewer-tint: rgba(139, 163, 122, 0.15);
377
388
--rating-bg: rgba(251, 191, 36, 0.15);
378
389
--rating-text: #fbbf24;
390
+
--text-danger: #fca5a5;
391
+
--text-success: #86efac;
379
392
--alert-warning-bg: rgba(251, 191, 36, 0.08);
380
393
--alert-warning-border: rgba(251, 191, 36, 0.3);
381
394
--alert-warning-text: #fde68a;
···
449
462
min-width: auto;
450
463
}
451
464
465
+
/* Global keyboard focus ring — visible only for keyboard users (not click) */
466
+
:focus-visible {
467
+
outline: 2px solid var(--input-border-focus);
468
+
outline-offset: 2px;
469
+
border-radius: 2px;
470
+
}
471
+
/* Clear native outline when focus-visible is in play (we draw our own above) */
472
+
:focus:not(:focus-visible) {
473
+
outline: none;
474
+
}
475
+
452
476
/* Prevent iOS zoom on input focus */
453
477
@media (max-width: 768px) {
454
478
input,
···
606
630
}
607
631
608
632
.record-view-footer {
609
-
@apply flex justify-between items-center pt-4 mt-6;
633
+
@apply flex flex-wrap justify-between items-center gap-3 pt-4 mt-6;
610
634
}
611
635
612
636
.record-stat-line {
···
698
722
}
699
723
}
700
724
725
+
/* Pour rows: shared by brew and recipe views */
726
+
.pour-row {
727
+
@apply flex gap-4 text-sm py-2;
728
+
border-bottom: 1px solid var(--surface-border);
729
+
}
730
+
.pour-row:last-child {
731
+
border-bottom: none;
732
+
}
733
+
701
734
/* Prose section in journal context */
702
735
.journal-prose {
703
736
@apply whitespace-pre-wrap leading-relaxed;
···
1335
1368
color: var(--text-muted);
1336
1369
}
1337
1370
1371
+
.text-secondary { color: var(--text-secondary); }
1372
+
.text-muted { color: var(--text-muted); }
1373
+
.text-faint { color: var(--text-faint); }
1374
+
.text-danger { color: var(--text-danger); }
1375
+
.text-success { color: var(--text-success); }
1376
+
1338
1377
/* Badges */
1339
1378
.badge-rating {
1340
1379
@apply inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium flex-shrink-0;
History
1 round
0 comments
pdewey.com
submitted
#0
1 commit
expand
collapse
fix: design audit
merge conflicts detected
expand
collapse
expand
collapse
- internal/web/components/layout.templ:115
- internal/web/components/shared.templ:218
- internal/web/pages/bean_view.templ:53
- internal/web/pages/brew_view.templ:56
- internal/web/pages/brewer_view.templ:53
- internal/web/pages/grinder_view.templ:53
- internal/web/pages/recipe_view.templ:66
- internal/web/pages/roaster_view.templ:53
- static/css/app.css:561