Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: button styling improvements, icons, and dark mode

authored by

Patrick Dewey and committed by tangled.org 677d4ac8 b1c91fad

+636 -147
+8 -8
internal/web/components/brew_list_table.templ
··· 33 33 <thead class="table-header"> 34 34 <tr> 35 35 <th class="table-th px-4 whitespace-nowrap">📅 Date</th> 36 - <th class="table-th px-4 whitespace-nowrap">☕ Bean</th> 37 - <th class="table-th px-4 whitespace-nowrap">🫖 Brewer</th> 36 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@IconCoffee() Bean</span></th> 37 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@IconBrewer() Brewer</span></th> 38 38 <th class="table-th px-4 whitespace-nowrap">🔧 Variables</th> 39 - <th class="table-th px-4 whitespace-nowrap">📝 Notes</th> 40 - <th class="table-th px-4 whitespace-nowrap">⭐ Rating</th> 39 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@IconFileText() Notes</span></th> 40 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@IconStar() Rating</span></th> 41 41 <th class="table-th px-4 whitespace-nowrap">⚙️ Actions</th> 42 42 </tr> 43 43 </thead> ··· 78 78 } 79 79 <div class="text-meta mt-0.5 flex flex-wrap gap-x-2 gap-y-0.5"> 80 80 if brew.Bean.Origin != "" { 81 - <span class="inline-flex items-center gap-0.5">📍 { brew.Bean.Origin }</span> 81 + <span class="inline-flex items-center gap-0.5">@IconMapPin() { brew.Bean.Origin }</span> 82 82 } 83 83 if brew.Bean.RoastLevel != "" { 84 - <span class="inline-flex items-center gap-0.5">🔥 { brew.Bean.RoastLevel }</span> 84 + <span class="inline-flex items-center gap-0.5">@IconFlame() { brew.Bean.RoastLevel }</span> 85 85 } 86 86 if brew.CoffeeAmount > 0 { 87 - <span class="inline-flex items-center gap-0.5">⚖️ { fmt.Sprintf("%dg", brew.CoffeeAmount) }</span> 87 + <span class="inline-flex items-center gap-0.5">@IconScale() { fmt.Sprintf("%dg", brew.CoffeeAmount) }</span> 88 88 } 89 89 </div> 90 90 } else { ··· 143 143 <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top"> 144 144 if brew.Rating > 0 { 145 145 <span class="badge-rating-sm"> 146 - ⭐ { fmt.Sprintf("%d/10", brew.Rating) } 146 + @IconStar() { fmt.Sprintf("%d/10", brew.Rating) } 147 147 </span> 148 148 } else { 149 149 <span class="text-brown-400">-</span>
+33 -18
internal/web/components/entity_tables.templ
··· 40 40 </div> 41 41 if bean.Roaster != nil && bean.Roaster.Name != "" { 42 42 <div class="text-sm text-brown-700 mt-0.5"> 43 - <span class="font-medium">🏭 { bean.Roaster.Name }</span> 43 + <span class="font-medium inline-flex items-center gap-0.5"> 44 + @IconStore() 45 + { bean.Roaster.Name } 46 + </span> 44 47 </div> 45 48 } 46 49 </div> ··· 64 67 </div> 65 68 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 66 69 if bean.Origin != "" { 67 - <span class="inline-flex items-center gap-0.5">📍 { bean.Origin }</span> 70 + <span class="inline-flex items-center gap-0.5"> 71 + @IconMapPin() 72 + { bean.Origin } 73 + </span> 68 74 } 69 75 if bean.Variety != "" { 70 - <span class="inline-flex items-center gap-0.5">🌿 { bean.Variety }</span> 76 + <span class="inline-flex items-center gap-0.5"> 77 + @IconLeaf() 78 + { bean.Variety } 79 + </span> 71 80 } 72 81 if bean.RoastLevel != "" { 73 - <span class="inline-flex items-center gap-0.5">🔥 { bean.RoastLevel }</span> 82 + <span class="inline-flex items-center gap-0.5"> 83 + @IconFlame() 84 + { bean.RoastLevel } 85 + </span> 74 86 } 75 87 if bean.Process != "" { 76 - <span class="inline-flex items-center gap-0.5">🌱 { bean.Process }</span> 88 + <span class="inline-flex items-center gap-0.5"> 89 + @IconSprout() 90 + { bean.Process } 91 + </span> 77 92 } 78 93 </div> 79 94 if bean.Description != "" { ··· 99 114 <table class="table"> 100 115 <thead class="table-header"> 101 116 <tr> 102 - <th class="table-th whitespace-nowrap">🏷️ Name</th> 103 - <th class="table-th whitespace-nowrap">📍 Location</th> 104 - <th class="table-th whitespace-nowrap">🌐 Website</th> 117 + <th class="table-th whitespace-nowrap">Name</th> 118 + <th class="table-th whitespace-nowrap">Location</th> 119 + <th class="table-th whitespace-nowrap">Website</th> 105 120 if props.ShowActions { 106 - <th class="table-th whitespace-nowrap">⚙️ Actions</th> 121 + <th class="table-th whitespace-nowrap">Actions</th> 107 122 } 108 123 </tr> 109 124 </thead> ··· 178 193 <table class="table"> 179 194 <thead class="table-header"> 180 195 <tr> 181 - <th class="table-th whitespace-nowrap">🏷️ Name</th> 182 - <th class="table-th whitespace-nowrap">🎛️ Type</th> 183 - <th class="table-th whitespace-nowrap">🔩 Burr Type</th> 184 - <th class="table-th whitespace-nowrap">📄 Notes</th> 196 + <th class="table-th whitespace-nowrap">Name</th> 197 + <th class="table-th whitespace-nowrap">Type</th> 198 + <th class="table-th whitespace-nowrap">Burr Type</th> 199 + <th class="table-th whitespace-nowrap">Notes</th> 185 200 if props.ShowActions { 186 - <th class="table-th whitespace-nowrap">⚙️ Actions</th> 201 + <th class="table-th whitespace-nowrap">Actions</th> 187 202 } 188 203 </tr> 189 204 </thead> ··· 259 274 <table class="table"> 260 275 <thead class="table-header"> 261 276 <tr> 262 - <th class="table-th whitespace-nowrap">🏷️ Name</th> 263 - <th class="table-th whitespace-nowrap">🎛️ Type</th> 264 - <th class="table-th whitespace-nowrap">📄 Description</th> 277 + <th class="table-th whitespace-nowrap">Name</th> 278 + <th class="table-th whitespace-nowrap">Type</th> 279 + <th class="table-th whitespace-nowrap">Description</th> 265 280 if props.ShowActions { 266 - <th class="table-th whitespace-nowrap">⚙️ Actions</th> 281 + <th class="table-th whitespace-nowrap">Actions</th> 267 282 } 268 283 </tr> 269 284 </thead>
+124
internal/web/components/icons.templ
··· 1 + package components 2 + 3 + // IconCoffee renders a coffee cup icon (replaces coffee emoji) 4 + templ IconCoffee() { 5 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 6 + <path d="M17 8h1a4 4 0 1 1 0 8h-1"></path> 7 + <path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"></path> 8 + <line x1="6" y1="2" x2="6" y2="4"></line> 9 + <line x1="10" y1="2" x2="10" y2="4"></line> 10 + <line x1="14" y1="2" x2="14" y2="4"></line> 11 + </svg> 12 + } 13 + 14 + // IconBrewer renders a dripper/funnel icon (replaces teapot emoji) 15 + templ IconBrewer() { 16 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 17 + <path d="M6 4h12l-3 16H9L6 4z"></path> 18 + <path d="M4 4h16"></path> 19 + </svg> 20 + } 21 + 22 + // IconMapPin renders a location pin icon (replaces pin emoji) 23 + templ IconMapPin() { 24 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-red-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 25 + <path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"></path> 26 + <circle cx="12" cy="10" r="3"></circle> 27 + </svg> 28 + } 29 + 30 + // IconFlame renders a flame icon (replaces fire emoji) 31 + templ IconFlame() { 32 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 33 + <path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path> 34 + </svg> 35 + } 36 + 37 + // IconSprout renders a sprout/seedling icon (replaces seedling emoji) 38 + templ IconSprout() { 39 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-green-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 40 + <path d="M7 20h10"></path> 41 + <path d="M10 20c5.5-2.5.8-6.4 3-10"></path> 42 + <path d="M9.5 9.4c1.1.8 1.8 2.2 2.3 3.7-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2 2.8-.5 4.4 0 5.5.8z"></path> 43 + <path d="M14.1 6a7 7 0 0 0-1.1 4c1.9-.1 3.3-.6 4.3-1.4 1-1 1.6-2.3 1.7-4.6-2.7.1-4 1-4.9 2z"></path> 44 + </svg> 45 + } 46 + 47 + // IconLeaf renders a leaf icon (replaces herb emoji) 48 + templ IconLeaf() { 49 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-green-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 50 + <path d="M11 20A7 7 0 0 1 9.8 6.9C15.5 4.9 20 7.4 20 7.4S17.5 12 11.8 14"></path> 51 + <path d="M5.7 18.3C3 15.6 3 11 5.7 8.3"></path> 52 + </svg> 53 + } 54 + 55 + // IconScale renders a scale/weight icon (replaces balance emoji) 56 + templ IconScale() { 57 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 58 + <path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path> 59 + <path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path> 60 + <path d="M7 21h10"></path> 61 + <path d="M12 3v18"></path> 62 + <path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"></path> 63 + </svg> 64 + } 65 + 66 + // IconStore renders a storefront icon (replaces factory emoji) 67 + templ IconStore() { 68 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 69 + <path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path> 70 + <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path> 71 + <path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path> 72 + <path d="M2 7h20"></path> 73 + <path d="M22 7v3a2 2 0 0 1-2 2v0a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12v0a2 2 0 0 1-2-2V7"></path> 74 + </svg> 75 + } 76 + 77 + // IconFileText renders a document/notes icon (replaces memo emoji) 78 + templ IconFileText() { 79 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 80 + <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path> 81 + <polyline points="14 2 14 8 20 8"></polyline> 82 + <line x1="16" y1="13" x2="8" y2="13"></line> 83 + <line x1="16" y1="17" x2="8" y2="17"></line> 84 + <line x1="10" y1="9" x2="8" y2="9"></line> 85 + </svg> 86 + } 87 + 88 + // IconDroplet renders a water droplet icon (replaces droplet emoji) 89 + templ IconDroplet() { 90 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-blue-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 91 + <path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z"></path> 92 + </svg> 93 + } 94 + 95 + // IconStar renders a star icon (replaces star emoji) 96 + templ IconStar() { 97 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-amber-500" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 98 + <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon> 99 + </svg> 100 + } 101 + 102 + // IconBarChart renders a bar chart icon (replaces chart emoji) 103 + templ IconBarChart() { 104 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 105 + <line x1="12" y1="20" x2="12" y2="10"></line> 106 + <line x1="18" y1="20" x2="18" y2="4"></line> 107 + <line x1="6" y1="20" x2="6" y2="16"></line> 108 + </svg> 109 + } 110 + 111 + // IconThermometer renders a thermometer icon (replaces thermometer emoji) 112 + templ IconThermometer() { 113 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-red-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 114 + <path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"></path> 115 + </svg> 116 + } 117 + 118 + // IconClock renders a clock icon (replaces timer emoji) 119 + templ IconClock() { 120 + <svg class="inline-block w-4 h-4 flex-shrink-0 text-brown-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 121 + <circle cx="12" cy="12" r="10"></circle> 122 + <polyline points="12 6 12 12 16 14"></polyline> 123 + </svg> 124 + }
+4 -3
internal/web/components/layout.templ
··· 46 46 47 47 templ Layout(data *LayoutData, content templ.Component) { 48 48 <!DOCTYPE html> 49 - <html lang="en" class="h-full" style="background-color: #FAF7F5;"> 49 + <html lang="en" class="h-full" style="background-color: var(--page-bg);"> 50 50 <head> 51 51 <meta charset="UTF-8"/> 52 52 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> ··· 70 70 if data.OGImage != "" { 71 71 <meta name="twitter:image" content={ data.OGImage }/> 72 72 } 73 - <meta name="theme-color" content="#4a2c2a"/> 73 + <meta name="theme-color" content="#4a2c2a" media="(prefers-color-scheme: light)"/> 74 + <meta name="theme-color" content="#0F0A08" media="(prefers-color-scheme: dark)"/> 74 75 <title>{ data.Title } - Arabica</title> 75 76 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/> 76 77 <link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32"/> 77 78 <link rel="apple-touch-icon" href="/static/icon-192.svg"/> 78 - <link rel="stylesheet" href="/static/css/output.css?v=0.7.0"/> 79 + <link rel="stylesheet" href="/static/css/output.css?v=0.8.0"/> 79 80 <style> 80 81 [x-cloak] { display: none !important; } 81 82 </style>
+2 -1
internal/web/components/record_brew.templ
··· 19 19 </div> 20 20 if brew.Rating > 0 { 21 21 <span class="badge-rating"> 22 - ⭐ { fmt.Sprintf("%d/10", brew.Rating) } 22 + @IconStar() 23 + { fmt.Sprintf("%d/10", brew.Rating) } 23 24 </span> 24 25 } 25 26 </div>
+4 -1
internal/web/components/record_brewer.templ
··· 12 12 </div> 13 13 if brewer.BrewerType != "" { 14 14 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 15 - <span class="inline-flex items-center gap-0.5">🫖 { brewer.BrewerType }</span> 15 + <span class="inline-flex items-center gap-0.5"> 16 + @IconBrewer() 17 + { brewer.BrewerType } 18 + </span> 16 19 </div> 17 20 } 18 21 if brewer.Description != "" {
+4 -1
internal/web/components/record_roaster.templ
··· 13 13 </div> 14 14 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 15 15 if roaster.Location != "" { 16 - <span class="inline-flex items-center gap-0.5">📍 { roaster.Location }</span> 16 + <span class="inline-flex items-center gap-0.5"> 17 + @IconMapPin() 18 + { roaster.Location } 19 + </span> 17 20 } 18 21 if roaster.Website != "" { 19 22 if safeWebsite := bff.SafeWebsiteURL(roaster.Website); safeWebsite != "" {
+51 -23
internal/web/components/shared.templ
··· 10 10 // Used in both bean feed cards and brew feed cards. 11 11 type BeanSummaryProps struct { 12 12 Bean *models.Bean 13 - CoffeeAmount int // if > 0, shows "⚖️ Xg" (brew context) 13 + CoffeeAmount int // if > 0, shows scale icon + "Xg" (brew context) 14 14 } 15 15 16 16 // BeanSummary renders the bean name, roaster, and attribute tags. ··· 25 25 </div> 26 26 if props.Bean.Roaster != nil && props.Bean.Roaster.Name != "" { 27 27 <div class="text-sm text-brown-700 mt-0.5"> 28 - <span class="font-medium">🏭 { props.Bean.Roaster.Name }</span> 28 + <span class="inline-flex items-center gap-0.5 font-medium"> 29 + @IconStore() 30 + { props.Bean.Roaster.Name } 31 + </span> 29 32 </div> 30 33 } 31 34 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 32 35 if props.Bean.Origin != "" { 33 - <span class="inline-flex items-center gap-0.5">📍 { props.Bean.Origin }</span> 36 + <span class="inline-flex items-center gap-0.5"> 37 + @IconMapPin() 38 + { props.Bean.Origin } 39 + </span> 34 40 } 35 41 if props.Bean.RoastLevel != "" { 36 - <span class="inline-flex items-center gap-0.5">🔥 { props.Bean.RoastLevel }</span> 42 + <span class="inline-flex items-center gap-0.5"> 43 + @IconFlame() 44 + { props.Bean.RoastLevel } 45 + </span> 37 46 } 38 47 if props.Bean.Variety != "" { 39 - <span class="inline-flex items-center gap-0.5">🌿 { props.Bean.Variety }</span> 48 + <span class="inline-flex items-center gap-0.5"> 49 + @IconLeaf() 50 + { props.Bean.Variety } 51 + </span> 40 52 } 41 53 if props.Bean.Process != "" { 42 - <span class="inline-flex items-center gap-0.5">🌱 { props.Bean.Process }</span> 54 + <span class="inline-flex items-center gap-0.5"> 55 + @IconSprout() 56 + { props.Bean.Process } 57 + </span> 43 58 } 44 59 if props.CoffeeAmount > 0 { 45 - <span class="inline-flex items-center gap-0.5">⚖️ { fmt.Sprintf("%dg", props.CoffeeAmount) }</span> 60 + <span class="inline-flex items-center gap-0.5"> 61 + @IconScale() 62 + { fmt.Sprintf("%dg", props.CoffeeAmount) } 63 + </span> 46 64 } 47 65 </div> 48 66 } ··· 80 98 type DetailFieldProps struct { 81 99 Label string 82 100 Value string 83 - LinkHref string // Optional: wraps value in a link 101 + LinkHref string // Optional: wraps value in a link 102 + Icon templ.Component // Optional: icon rendered before the label 84 103 } 85 104 86 105 // DetailField renders a labeled value with a "Not specified" fallback 87 106 templ DetailField(props DetailFieldProps) { 88 107 <div class="section-box"> 89 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">{ props.Label }</h3> 108 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"> 109 + if props.Icon != nil { 110 + <span class="inline-flex items-center gap-1"> 111 + @props.Icon 112 + { props.Label } 113 + </span> 114 + } else { 115 + { props.Label } 116 + } 117 + </h3> 90 118 if props.Value != "" && props.LinkHref != "" { 91 119 <a href={ templ.SafeURL(props.LinkHref) } class="font-semibold text-brown-900 hover:underline">{ props.Value }</a> 92 120 } else if props.Value != "" { ··· 192 220 <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> 193 221 <a 194 222 href="/brews/new" 195 - class="btn-primary block text-center py-4 px-6 rounded-xl" 223 + class="home-action-primary block text-center py-4 px-6 rounded-xl" 196 224 hx-get="/brews/new" 197 225 hx-target="main" 198 226 hx-swap="innerHTML show:top" 199 227 hx-select="main > *" 200 228 hx-push-url="true" 201 229 > 202 - <span class="text-xl font-semibold">☕ Add New Brew</span> 230 + <span class="text-lg font-semibold">Add New Brew</span> 203 231 </a> 204 232 <a 205 233 href="/brews" 206 - class="btn-tertiary block text-center py-4 px-6 rounded-xl" 234 + class="home-action-secondary block text-center py-4 px-6 rounded-xl" 207 235 hx-get="/brews" 208 236 hx-target="main" 209 237 hx-swap="innerHTML show:top" 210 238 hx-select="main > *" 211 239 hx-push-url="true" 212 240 > 213 - <span class="text-xl font-semibold">📋 View All Brews</span> 241 + <span class="text-lg font-semibold">View All Brews</span> 214 242 </a> 215 243 </div> 216 244 } ··· 253 281 templ AboutInfoCard() { 254 282 // TODO: only show this at the bottom of the page when authenticated already? 255 283 <div class="card p-6 mb-6"> 256 - <h3 class="text-lg font-bold text-brown-900 mb-3">✨ About Arabica</h3> 284 + <h3 class="text-lg font-bold text-brown-900 mb-3">About Arabica</h3> 257 285 <ul class="text-brown-800 space-y-2 leading-relaxed mb-3"> 258 - <li class="flex items-start"><span class="mr-2">📝</span><span>Add tasting notes and ratings to each brew</span></li> 259 - <li class="flex items-start"><span class="mr-2">📊</span><span>Track brewing variables like temperature, time, and grind size</span></li> 260 - <li class="flex items-start"><span class="mr-2">🌍</span><span>Organize beans by origin and roaster</span></li> 261 - <li class="flex items-start"><span class="mr-2">🚀</span><span><strong>Portable:</strong> Own your coffee brewing history</span></li> 262 - <li class="flex items-start"><span class="mr-2">🔒</span><span><strong>Decentralized:</strong> Your data lives in your Personal Data Server (PDS)</span></li> 286 + <li>Add tasting notes and ratings to each brew</li> 287 + <li>Track brewing variables like temperature, time, and grind size</li> 288 + <li>Organize beans by origin and roaster</li> 289 + <li><strong>Portable:</strong> Own your coffee brewing history</li> 290 + <li><strong>Decentralized:</strong> Your data lives in your Personal Data Server (PDS)</li> 263 291 </ul> 264 292 // TODO: include a link to the about page here somewhere 265 293 // <div class="text-large text-brown-900"><a href="/about" class="text-brown-700 hover:text-brown-900 transition-colors">Learn more</a></div> ··· 394 422 // RecipeAlphaWarning displays a warning banner about recipes being in early alpha. 395 423 // TODO: remove this later once recipes are stable 396 424 templ RecipeAlphaWarning() { 397 - <div class="rounded-lg border border-amber-400 bg-amber-50 p-4 mb-6"> 425 + <div class="alert-warning mb-6"> 398 426 <div class="flex items-start gap-3"> 399 427 <span class="text-xl leading-none mt-0.5">&#9888;&#65039;</span> 400 - <div class="text-sm text-amber-900"> 428 + <div class="text-sm"> 401 429 <p class="font-bold text-base mb-1">Recipes are in early alpha</p> 402 430 <p class="mb-2"> 403 - The recipe format may change significantly as we figure out what works best (e.g. it 431 + The recipe format may change significantly as we figure out what works best (e.g. it 404 432 may be completely redesigned from the ground up with a more powerful recipe creator system). 405 433 If that happens, your brews won't break &ndash; brew fields are 406 434 filled in from the recipe at creation time, so they stand on their own. Only the ··· 410 438 Feedback is very welcome! Arabica has had a pourover bias so far (due to its developer's preferences), 411 439 so input from espresso folks is especially appreciated. 412 440 </p> 413 - <p class="text-xs text-amber-700"> 441 + <p class="text-xs alert-warning-muted"> 414 442 If you have feedback or suggestions (or comments) reach out on Bluesky or open an issue on Tangled or GitHub. 415 443 </p> 416 444 </div>
+7 -7
internal/web/pages/bean_view.templ
··· 43 43 <div class="space-y-6"> 44 44 if props.Bean.Roaster != nil && props.Bean.Roaster.Name != "" { 45 45 <div class="section-box"> 46 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">🏭 Roaster</h3> 46 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconStore() Roaster</span></h3> 47 47 <div class="font-semibold text-brown-900"> 48 48 <a 49 49 href={ templ.SafeURL(fmt.Sprintf("/roasters/%s?owner=%s", props.Bean.Roaster.RKey, getOwnerFromShareURL(props.ShareURL))) } ··· 53 53 </a> 54 54 </div> 55 55 if props.Bean.Roaster.Location != "" { 56 - <div class="text-sm text-brown-600 mt-1">📍 { props.Bean.Roaster.Location }</div> 56 + <div class="text-sm text-brown-600 mt-1"><span class="inline-flex items-center gap-1">@components.IconMapPin() { props.Bean.Roaster.Location }</span></div> 57 57 } 58 58 </div> 59 59 } 60 60 <div class="grid grid-cols-2 gap-4"> 61 - @components.DetailField(components.DetailFieldProps{Label: "📍 Origin", Value: props.Bean.Origin}) 62 - @components.DetailField(components.DetailFieldProps{Label: "🌿 Variety", Value: props.Bean.Variety}) 63 - @components.DetailField(components.DetailFieldProps{Label: "🔥 Roast Level", Value: props.Bean.RoastLevel}) 64 - @components.DetailField(components.DetailFieldProps{Label: "🌱 Process", Value: props.Bean.Process}) 61 + @components.DetailField(components.DetailFieldProps{Icon: components.IconMapPin(), Label: "Origin", Value: props.Bean.Origin}) 62 + @components.DetailField(components.DetailFieldProps{Icon: components.IconLeaf(), Label: "Variety", Value: props.Bean.Variety}) 63 + @components.DetailField(components.DetailFieldProps{Icon: components.IconFlame(), Label: "Roast Level", Value: props.Bean.RoastLevel}) 64 + @components.DetailField(components.DetailFieldProps{Icon: components.IconSprout(), Label: "Process", Value: props.Bean.Process}) 65 65 if props.Bean.Closed { 66 66 <div class="section-box"> 67 67 <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Status</h3> ··· 71 71 </div> 72 72 if props.Bean.Description != "" { 73 73 <div class="section-box"> 74 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">📝 Description</h3> 74 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconFileText() Description</span></h3> 75 75 <div class="text-brown-900 whitespace-pre-wrap">{ props.Bean.Description }</div> 76 76 </div> 77 77 }
+1 -1
internal/web/pages/brew_form.templ
··· 196 196 <div> 197 197 <label class="form-label">Recipe (Optional)</label> 198 198 <p class="text-sm text-brown-600 mb-2">Select a recipe to autofill brew parameters</p> 199 - <div class="rounded-md border border-amber-300 bg-amber-50 px-3 py-2 mb-2 text-xs text-amber-800"> 199 + <div class="alert-warning px-3 py-2 mb-2 text-xs"> 200 200 Recipes are in early alpha &mdash; the format may change. Your brew data won't be affected. 201 201 </div> 202 202 <!-- Category filter chips -->
+4 -4
internal/web/pages/brew_list.templ
··· 36 36 <thead class="table-header"> 37 37 <tr> 38 38 <th class="table-th px-4 whitespace-nowrap">📅 Date</th> 39 - <th class="table-th px-4 whitespace-nowrap">☕ Bean</th> 40 - <th class="table-th px-4 whitespace-nowrap">🫖 Brewer</th> 39 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconCoffee() Bean</span></th> 40 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconBrewer() Brewer</span></th> 41 41 <th class="table-th px-4 whitespace-nowrap">🔧 Variables</th> 42 - <th class="table-th px-4 whitespace-nowrap">📝 Notes</th> 43 - <th class="table-th px-4 whitespace-nowrap">⭐ Rating</th> 42 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconFileText() Notes</span></th> 43 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconStar() Rating</span></th> 44 44 <th class="table-th px-4 whitespace-nowrap">⚙️ Actions</th> 45 45 </tr> 46 46 </thead>
+26 -21
internal/web/pages/brew_view.templ
··· 116 116 templ BrewRating(rating int) { 117 117 <div class="section-box text-center py-4"> 118 118 <span class="badge-rating text-2xl !font-bold px-5 py-2"> 119 - ⭐ { fmt.Sprintf("%d/10", rating) } 119 + <span class="inline-flex items-center gap-1"> 120 + @components.IconStar() 121 + { fmt.Sprintf("%d/10", rating) } 122 + </span> 120 123 </span> 121 124 <div class="text-sm text-brown-600 mt-2">Rating</div> 122 125 </div> ··· 125 128 // BrewBeanSection renders the coffee bean information 126 129 templ BrewBeanSection(brew *models.Brew, owner string) { 127 130 <div class="section-box"> 128 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">☕ Coffee Bean</h3> 131 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconCoffee() Coffee Bean</span></h3> 129 132 if brew.Bean != nil { 130 133 <div class="font-bold text-lg text-brown-900"> 131 134 <a href={ templ.SafeURL(fmt.Sprintf("/beans/%s?owner=%s", brew.Bean.RKey, owner)) } class="hover:underline"> ··· 138 141 </div> 139 142 if brew.Bean.Roaster != nil && brew.Bean.Roaster.Name != "" { 140 143 <div class="text-sm text-brown-700 mt-1"> 141 - 🏭 142 - <a href={ templ.SafeURL(fmt.Sprintf("/roasters/%s?owner=%s", brew.Bean.Roaster.RKey, owner)) } class="hover:underline"> 143 - { brew.Bean.Roaster.Name } 144 - </a> 144 + <span class="inline-flex items-center gap-1"> 145 + @components.IconStore() 146 + <a href={ templ.SafeURL(fmt.Sprintf("/roasters/%s?owner=%s", brew.Bean.Roaster.RKey, owner)) } class="hover:underline"> 147 + { brew.Bean.Roaster.Name } 148 + </a> 149 + </span> 145 150 </div> 146 151 } 147 152 <div class="flex flex-wrap gap-3 mt-2 text-sm text-brown-600"> 148 153 if brew.Bean.Origin != "" { 149 - <span>📍 { brew.Bean.Origin }</span> 154 + <span class="inline-flex items-center gap-1">@components.IconMapPin() { brew.Bean.Origin }</span> 150 155 } 151 156 if brew.Bean.RoastLevel != "" { 152 - <span>🔥 { brew.Bean.RoastLevel }</span> 157 + <span class="inline-flex items-center gap-1">@components.IconFlame() { brew.Bean.RoastLevel }</span> 153 158 } 154 159 </div> 155 160 } else { ··· 161 166 // BrewParametersGrid renders the brew parameters in a grid 162 167 templ BrewParametersGrid(brew *models.Brew, owner string) { 163 168 <div class="grid grid-cols-2 gap-4"> 164 - @components.DetailField(components.DetailFieldProps{Label: "⚖️ Coffee", Value: getCoffeeAmountDisplay(brew)}) 165 - @components.DetailField(components.DetailFieldProps{Label: "☕ Brew Method", Value: getBrewerName(brew), LinkHref: getBrewerViewURL(brew, owner)}) 169 + @components.DetailField(components.DetailFieldProps{Icon: components.IconScale(), Label: "Coffee", Value: getCoffeeAmountDisplay(brew)}) 170 + @components.DetailField(components.DetailFieldProps{Icon: components.IconCoffee(), Label: "Brew Method", Value: getBrewerName(brew), LinkHref: getBrewerViewURL(brew, owner)}) 166 171 @components.DetailField(components.DetailFieldProps{Label: "⚙️ Grinder", Value: getGrinderName(brew), LinkHref: getGrinderViewURL(brew, owner)}) 167 172 @components.DetailField(components.DetailFieldProps{Label: "🔩 Grind Size", Value: getGrindSizeDisplay(brew)}) 168 - @components.DetailField(components.DetailFieldProps{Label: "💧 Water", Value: getWaterAmountDisplay(brew)}) 169 - @components.DetailField(components.DetailFieldProps{Label: "🌡️ Temperature", Value: getTemperatureDisplay(brew)}) 173 + @components.DetailField(components.DetailFieldProps{Icon: components.IconDroplet(), Label: "Water", Value: getWaterAmountDisplay(brew)}) 174 + @components.DetailField(components.DetailFieldProps{Icon: components.IconThermometer(), Label: "Temperature", Value: getTemperatureDisplay(brew)}) 170 175 <div class="col-span-2"> 171 - @components.DetailField(components.DetailFieldProps{Label: "⏱️ Brew Time", Value: getBrewTimeDisplay(brew)}) 176 + @components.DetailField(components.DetailFieldProps{Icon: components.IconClock(), Label: "Brew Time", Value: getBrewTimeDisplay(brew)}) 172 177 </div> 173 178 </div> 174 179 } ··· 271 276 @click="showForm = true" 272 277 class="w-full btn-secondary text-sm" 273 278 > 274 - 📋 Save as Recipe 279 + Save as Recipe 275 280 </button> 276 281 </template> 277 282 <template x-if="showForm && !success"> ··· 319 324 // BrewRecipeSection renders the linked recipe info 320 325 templ BrewRecipeSection(recipe *models.Recipe, owner string) { 321 326 <div class="section-box"> 322 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">📋 Recipe</h3> 327 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Recipe</h3> 323 328 <a href={ templ.SafeURL(fmt.Sprintf("/recipes/%s?owner=%s", recipe.RKey, owner)) } class="font-bold text-lg text-brown-900 hover:text-brown-700 underline decoration-brown-300 hover:decoration-brown-500 transition-colors">{ recipe.Name }</a> 324 329 <div class="flex flex-wrap gap-3 mt-2 text-sm text-brown-600"> 325 330 if recipe.CoffeeAmount > 0 { 326 - <span>☕ { fmt.Sprintf("%.1fg coffee", recipe.CoffeeAmount) }</span> 331 + <span class="inline-flex items-center gap-1">@components.IconCoffee() { fmt.Sprintf("%.1fg coffee", recipe.CoffeeAmount) }</span> 327 332 } 328 333 if recipe.WaterAmount > 0 { 329 - <span>💧 { fmt.Sprintf("%.1fg water", recipe.WaterAmount) }</span> 334 + <span class="inline-flex items-center gap-1">@components.IconDroplet() { fmt.Sprintf("%.1fg water", recipe.WaterAmount) }</span> 330 335 } 331 336 if recipe.BrewerObj != nil { 332 - <span>🫖 { recipe.BrewerObj.Name }</span> 337 + <span class="inline-flex items-center gap-1">@components.IconBrewer() { recipe.BrewerObj.Name }</span> 333 338 } else if recipe.BrewerType != "" { 334 - <span>🫖 { recipe.BrewerType }</span> 339 + <span class="inline-flex items-center gap-1">@components.IconBrewer() { recipe.BrewerType }</span> 335 340 } 336 341 </div> 337 342 if recipe.Notes != "" { ··· 343 348 // BrewPoursSection renders the pours section 344 349 templ BrewPoursSection(pours []*models.Pour) { 345 350 <div class="section-box"> 346 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-3">💧 Pours</h3> 351 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-3"><span class="inline-flex items-center gap-1">@components.IconDroplet() Pours</span></h3> 347 352 <div class="space-y-2"> 348 353 for _, pour := range pours { 349 354 <div class="flex justify-between items-center bg-white p-3 rounded-lg border border-brown-200"> ··· 361 366 // BrewTastingNotes renders the tasting notes section 362 367 templ BrewTastingNotes(notes string) { 363 368 <div class="section-box"> 364 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">📝 Tasting Notes</h3> 369 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconFileText() Tasting Notes</span></h3> 365 370 <div class="text-brown-900 whitespace-pre-wrap">{ notes }</div> 366 371 </div> 367 372 }
+2 -2
internal/web/pages/brewer_view.templ
··· 39 39 templ BrewerViewCard(props BrewerViewProps) { 40 40 @BrewerViewHeader(props) 41 41 <div class="space-y-6"> 42 - @components.DetailField(components.DetailFieldProps{Label: "☕ Type", Value: props.Brewer.BrewerType}) 42 + @components.DetailField(components.DetailFieldProps{Icon: components.IconCoffee(), Label: "Type", Value: props.Brewer.BrewerType}) 43 43 if props.Brewer.Description != "" { 44 44 <div class="section-box"> 45 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">📝 Description</h3> 45 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconFileText() Description</span></h3> 46 46 <div class="text-brown-900 whitespace-pre-wrap">{ props.Brewer.Description }</div> 47 47 </div> 48 48 }
+46 -18
internal/web/pages/feed.templ
··· 105 105 class="flex flex-wrap items-center justify-between gap-2 mb-4" 106 106 data-type-filter={ qs.TypeFilter } 107 107 data-sort={ qs.Sort } 108 - x-data="{ typeFilter: $el.dataset.typeFilter, sort: $el.dataset.sort, feedURL(t, s) { let u = '/api/feed', sep = '?'; if (t) { u += sep + 'type=' + t; sep = '&'; } if (s) { if (s !== 'recent') { u += sep + 'sort=' + s; } } return u; }, changeFilter(t) { this.typeFilter = t; htmx.ajax('GET', this.feedURL(t, this.sort), {target: '#feed-items', swap: 'outerHTML', select: '#feed-items'}); }, changeSort(s) { this.sort = s; htmx.ajax('GET', this.feedURL(this.typeFilter, s), {target: '#feed-items', swap: 'outerHTML', select: '#feed-items'}); } }" 108 + x-data="{ typeFilter: $el.dataset.typeFilter, sort: $el.dataset.sort, pillClass(tab) { if (this.typeFilter !== tab) return 'filter-pill'; if (!tab) return 'filter-pill-active'; return 'filter-pill-' + tab; }, feedURL(t, s) { let u = '/api/feed', sep = '?'; if (t) { u += sep + 'type=' + t; sep = '&'; } if (s) { if (s !== 'recent') { u += sep + 'sort=' + s; } } return u; }, changeFilter(t) { this.typeFilter = t; htmx.ajax('GET', this.feedURL(t, this.sort), {target: '#feed-items', swap: 'outerHTML', select: '#feed-items'}); }, changeSort(s) { this.sort = s; htmx.ajax('GET', this.feedURL(this.typeFilter, s), {target: '#feed-items', swap: 'outerHTML', select: '#feed-items'}); } }" 109 109 > 110 110 <!-- Type filter tabs --> 111 111 <div class="flex flex-wrap gap-1"> 112 112 for _, tab := range feedFilterTabs() { 113 113 <button 114 - class="px-3 py-1.5 text-sm rounded-full transition-colors" 115 - :class="typeFilter === $el.dataset.tab ? 'bg-brown-800 text-brown-50 font-medium' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 114 + class="filter-pill" 115 + :class="pillClass($el.dataset.tab)" 116 116 data-tab={ tab.Value } 117 117 @click="changeFilter($el.dataset.tab)" 118 118 > ··· 123 123 <!-- Sort selector --> 124 124 <div class="flex items-center gap-1"> 125 125 <button 126 - class="px-3 py-1.5 text-sm rounded-full transition-colors" 127 - :class="(sort === '' || sort === 'recent') ? 'bg-brown-800 text-brown-50 font-medium' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 126 + class="filter-pill" 127 + :class="(sort === '' || sort === 'recent') ? 'filter-pill-active' : 'filter-pill'" 128 128 @click="changeSort('recent')" 129 129 > 130 130 New 131 131 </button> 132 132 <button 133 - class="px-3 py-1.5 text-sm rounded-full transition-colors" 134 - :class="sort === 'popular' ? 'bg-brown-800 text-brown-50 font-medium' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 133 + class="filter-pill" 134 + :class="sort === 'popular' ? 'filter-pill-active' : 'filter-pill'" 135 135 @click="changeSort('popular')" 136 136 > 137 137 Popular ··· 144 144 templ FeedLoadMoreButton(qs FeedQueryState) { 145 145 <div class="text-center pt-2" x-data="{ loading: false }"> 146 146 <button 147 - class="px-4 py-2 text-sm text-brown-700 bg-brown-100 hover:bg-brown-200 rounded-lg transition-colors disabled:opacity-50" 147 + class="btn-secondary text-sm disabled:opacity-50" 148 148 hx-get={ buildFeedURLWithCursor(qs.TypeFilter, qs.Sort, qs.NextCursor) } 149 149 hx-target="closest div" 150 150 hx-swap="outerHTML" ··· 354 354 </div> 355 355 if item.Brew.Bean.Roaster != nil && item.Brew.Bean.Roaster.Name != "" { 356 356 <div class="text-sm text-brown-700 mt-0.5"> 357 - <span class="font-medium">🏭 { item.Brew.Bean.Roaster.Name }</span> 357 + <span class="inline-flex items-center gap-0.5 font-medium"> 358 + @components.IconStore() 359 + { item.Brew.Bean.Roaster.Name } 360 + </span> 358 361 </div> 359 362 } 360 363 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 361 364 if item.Brew.Bean.Origin != "" { 362 - <span class="inline-flex items-center gap-0.5">📍 { item.Brew.Bean.Origin }</span> 365 + <span class="inline-flex items-center gap-0.5"> 366 + @components.IconMapPin() 367 + { item.Brew.Bean.Origin } 368 + </span> 363 369 } 364 370 if item.Brew.Bean.RoastLevel != "" { 365 - <span class="inline-flex items-center gap-0.5">🔥 { item.Brew.Bean.RoastLevel }</span> 371 + <span class="inline-flex items-center gap-0.5"> 372 + @components.IconFlame() 373 + { item.Brew.Bean.RoastLevel } 374 + </span> 366 375 } 367 376 if item.Brew.Bean.Process != "" { 368 - <span class="inline-flex items-center gap-0.5">🌱 { item.Brew.Bean.Process }</span> 377 + <span class="inline-flex items-center gap-0.5"> 378 + @components.IconSprout() 379 + { item.Brew.Bean.Process } 380 + </span> 369 381 } 370 382 if item.Brew.CoffeeAmount > 0 { 371 - <span class="inline-flex items-center gap-0.5">⚖️ { fmt.Sprintf("%dg", item.Brew.CoffeeAmount) }</span> 383 + <span class="inline-flex items-center gap-0.5"> 384 + @components.IconScale() 385 + { fmt.Sprintf("%dg", item.Brew.CoffeeAmount) } 386 + </span> 372 387 } 373 388 </div> 374 389 } 375 390 </div> 376 391 if item.Brew.Rating > 0 { 377 392 <span class="badge-rating"> 378 - ⭐ { fmt.Sprintf("%d/10", item.Brew.Rating) } 393 + @components.IconStar() 394 + { fmt.Sprintf("%d/10", item.Brew.Rating) } 379 395 </span> 380 396 } 381 397 </div> ··· 448 464 </div> 449 465 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 450 466 if item.Recipe.CoffeeAmount > 0 { 451 - <span class="inline-flex items-center gap-0.5">☕ { fmt.Sprintf("%.1fg", item.Recipe.CoffeeAmount) }</span> 467 + <span class="inline-flex items-center gap-0.5"> 468 + @components.IconCoffee() 469 + { fmt.Sprintf("%.1fg", item.Recipe.CoffeeAmount) } 470 + </span> 452 471 } 453 472 if item.Recipe.WaterAmount > 0 { 454 - <span class="inline-flex items-center gap-0.5">💧 { fmt.Sprintf("%.1fg", item.Recipe.WaterAmount) }</span> 473 + <span class="inline-flex items-center gap-0.5"> 474 + @components.IconDroplet() 475 + { fmt.Sprintf("%.1fg", item.Recipe.WaterAmount) } 476 + </span> 455 477 } 456 478 if item.Recipe.BrewerObj != nil { 457 - <span class="inline-flex items-center gap-0.5">🫖 { item.Recipe.BrewerObj.Name }</span> 479 + <span class="inline-flex items-center gap-0.5"> 480 + @components.IconBrewer() 481 + { item.Recipe.BrewerObj.Name } 482 + </span> 458 483 } else if item.Recipe.BrewerType != "" { 459 - <span class="inline-flex items-center gap-0.5">🫖 { item.Recipe.BrewerType }</span> 484 + <span class="inline-flex items-center gap-0.5"> 485 + @components.IconBrewer() 486 + { item.Recipe.BrewerType } 487 + </span> 460 488 } 461 489 </div> 462 490 if item.Recipe.Notes != "" {
+1 -1
internal/web/pages/grinder_view.templ
··· 45 45 </div> 46 46 if props.Grinder.Notes != "" { 47 47 <div class="section-box"> 48 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">📝 Notes</h3> 48 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconFileText() Notes</span></h3> 49 49 <div class="text-brown-900 whitespace-pre-wrap">{ props.Grinder.Notes }</div> 50 50 </div> 51 51 }
+1 -1
internal/web/pages/home.templ
··· 32 32 33 33 templ CommunityFeedSection() { 34 34 <div class="card p-2 sm:p-6 mb-8"> 35 - <h3 class="text-xl font-bold text-brown-900 mb-4">☕ Community Feed</h3> 35 + <h3 class="text-xl font-bold text-brown-900 mb-4">Community Feed</h3> 36 36 <div hx-get="/api/feed" hx-trigger="load" hx-swap="innerHTML"> 37 37 @FeedLoadingSkeleton() 38 38 </div>
+7 -7
internal/web/pages/manage.templ
··· 88 88 <table class="table"> 89 89 <thead class="table-header"> 90 90 <tr> 91 - <th class="table-th whitespace-nowrap">📊 Status</th> 91 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconBarChart() Status</span></th> 92 92 <th class="table-th whitespace-nowrap">🏷️ Name</th> 93 - <th class="table-th whitespace-nowrap">📍 Origin</th> 94 - <th class="table-th whitespace-nowrap">☕ Roaster</th> 95 - <th class="table-th whitespace-nowrap">🔥 Roast Level</th> 96 - <th class="table-th whitespace-nowrap">🌱 Process</th> 97 - <th class="table-th whitespace-nowrap">📝 Description</th> 93 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconMapPin() Origin</span></th> 94 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconCoffee() Roaster</span></th> 95 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconFlame() Roast Level</span></th> 96 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconSprout() Process</span></th> 97 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconFileText() Description</span></th> 98 98 <th class="table-th whitespace-nowrap">⚙️ Actions</th> 99 99 </tr> 100 100 </thead> ··· 131 131 <thead class="table-header"> 132 132 <tr> 133 133 <th class="table-th whitespace-nowrap">🏷️ Name</th> 134 - <th class="table-th whitespace-nowrap">📍 Location</th> 134 + <th class="table-th whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconMapPin() Location</span></th> 135 135 <th class="table-th whitespace-nowrap">🌐 Website</th> 136 136 <th class="table-th whitespace-nowrap">⚙️ Actions</th> 137 137 </tr>
+4 -4
internal/web/pages/profile.templ
··· 133 133 <thead class="table-header"> 134 134 <tr> 135 135 <th class="table-th px-4 whitespace-nowrap">📅 Date</th> 136 - <th class="table-th px-4 whitespace-nowrap">☕ Bean</th> 137 - <th class="table-th px-4 whitespace-nowrap">🫖 Brewer</th> 136 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconCoffee() Bean</span></th> 137 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconBrewer() Brewer</span></th> 138 138 <th class="table-th px-4 whitespace-nowrap">🔧 Variables</th> 139 - <th class="table-th px-4 whitespace-nowrap">📝 Notes</th> 140 - <th class="table-th px-4 whitespace-nowrap">⭐ Rating</th> 139 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconFileText() Notes</span></th> 140 + <th class="table-th px-4 whitespace-nowrap"><span class="inline-flex items-center gap-1">@components.IconStar() Rating</span></th> 141 141 <th class="table-th px-4 whitespace-nowrap">⚙️ Actions</th> 142 142 </tr> 143 143 </thead>
+10 -10
internal/web/pages/recipe_explore.templ
··· 46 46 <div class="flex flex-wrap gap-2"> 47 47 <button 48 48 type="button" 49 + class="filter-pill" 49 50 @click="setCategory('')" 50 - :class="category === '' ? 'bg-brown-700 text-white' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 51 - class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors" 51 + :class="category === '' ? 'filter-pill-active' : 'filter-pill'" 52 52 > 53 53 All 54 54 </button> 55 55 <button 56 56 type="button" 57 + class="filter-pill" 57 58 @click="setCategory('small')" 58 - :class="category === 'small' ? 'bg-brown-700 text-white' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 59 - class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors" 59 + :class="category === 'small' ? 'filter-pill-active' : 'filter-pill'" 60 60 > 61 61 Small (&le;12g) 62 62 </button> 63 63 <button 64 64 type="button" 65 + class="filter-pill" 65 66 @click="setCategory('single')" 66 - :class="category === 'single' ? 'bg-brown-700 text-white' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 67 - class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors" 67 + :class="category === 'single' ? 'filter-pill-active' : 'filter-pill'" 68 68 > 69 69 Single cup (12-22g) 70 70 </button> 71 71 <button 72 72 type="button" 73 + class="filter-pill" 73 74 @click="setCategory('large')" 74 - :class="category === 'large' ? 'bg-brown-700 text-white' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 75 - class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors" 75 + :class="category === 'large' ? 'filter-pill-active' : 'filter-pill'" 76 76 > 77 77 Large (22g+) 78 78 </button> 79 79 <button 80 80 type="button" 81 + class="filter-pill" 81 82 @click="setCategory('batch')" 82 - :class="category === 'batch' ? 'bg-brown-700 text-white' : 'bg-brown-100 text-brown-700 hover:bg-brown-200'" 83 - class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors" 83 + :class="category === 'batch' ? 'filter-pill-active' : 'filter-pill'" 84 84 > 85 85 Batch brew (500g+ water) 86 86 </button>
+8 -7
internal/web/pages/recipe_view.templ
··· 46 46 <!-- Main details grid --> 47 47 <div class="grid grid-cols-2 gap-4"> 48 48 if props.Recipe.CoffeeAmount > 0 { 49 - @components.DetailField(components.DetailFieldProps{Label: "☕ Coffee", Value: fmt.Sprintf("%.1fg", props.Recipe.CoffeeAmount)}) 49 + @components.DetailField(components.DetailFieldProps{Icon: components.IconCoffee(), Label: "Coffee", Value: fmt.Sprintf("%.1fg", props.Recipe.CoffeeAmount)}) 50 50 } 51 51 if props.Recipe.WaterAmount > 0 { 52 - @components.DetailField(components.DetailFieldProps{Label: "💧 Water", Value: fmt.Sprintf("%.1fg", props.Recipe.WaterAmount)}) 52 + @components.DetailField(components.DetailFieldProps{Icon: components.IconDroplet(), Label: "Water", Value: fmt.Sprintf("%.1fg", props.Recipe.WaterAmount)}) 53 53 } 54 54 if props.Recipe.Ratio > 0 { 55 - @components.DetailField(components.DetailFieldProps{Label: "⚖️ Ratio", Value: fmt.Sprintf("1:%.1f", props.Recipe.Ratio)}) 55 + @components.DetailField(components.DetailFieldProps{Icon: components.IconScale(), Label: "Ratio", Value: fmt.Sprintf("1:%.1f", props.Recipe.Ratio)}) 56 56 } 57 57 if props.Recipe.BrewerObj != nil { 58 58 @components.DetailField(components.DetailFieldProps{ 59 - Label: "🫖 Brewer", 59 + Icon: components.IconBrewer(), 60 + Label: "Brewer", 60 61 Value: props.Recipe.BrewerObj.Name, 61 62 LinkHref: recipeBrewerLink(props), 62 63 }) 63 64 } else if props.Recipe.BrewerType != "" { 64 - @components.DetailField(components.DetailFieldProps{Label: "🫖 Brewer Type", Value: props.Recipe.BrewerType}) 65 + @components.DetailField(components.DetailFieldProps{Icon: components.IconBrewer(), Label: "Brewer Type", Value: props.Recipe.BrewerType}) 65 66 } 66 67 </div> 67 68 <!-- Pours --> 68 69 if len(props.Recipe.Pours) > 0 { 69 70 <div class="section-box"> 70 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">💧 Pours</h3> 71 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconDroplet() Pours</span></h3> 71 72 <div class="space-y-2"> 72 73 for i, pour := range props.Recipe.Pours { 73 74 <div class="flex items-center gap-4 bg-brown-50 rounded-lg px-3 py-2 border border-brown-200"> ··· 84 85 <!-- Notes --> 85 86 if props.Recipe.Notes != "" { 86 87 <div class="section-box"> 87 - <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">📝 Notes</h3> 88 + <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2"><span class="inline-flex items-center gap-1">@components.IconFileText() Notes</span></h3> 88 89 <div class="text-brown-900 whitespace-pre-wrap">{ props.Recipe.Notes }</div> 89 90 </div> 90 91 }
+1 -1
internal/web/pages/roaster_view.templ
··· 40 40 @RoasterViewHeader(props) 41 41 <div class="space-y-6"> 42 42 <div class="grid grid-cols-2 gap-4"> 43 - @components.DetailField(components.DetailFieldProps{Label: "📍 Location", Value: props.Roaster.Location}) 43 + @components.DetailField(components.DetailFieldProps{Icon: components.IconMapPin(), Label: "Location", Value: props.Roaster.Location}) 44 44 if props.Roaster.Website != "" { 45 45 <div class="section-box"> 46 46 <h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">🔗 Website</h3>
+287 -8
static/css/app.css
··· 82 82 --modal-border: #eaddd7; 83 83 --modal-backdrop: rgba(0, 0, 0, 0.4); 84 84 85 - /* Feed type indicators (left border) */ 85 + /* Feed type indicators (left border + filter pills) */ 86 86 --type-brew: #6b4423; 87 - --type-bean: #d97706; 88 - --type-recipe: #bfa094; 89 - --type-roaster: #d2bab0; 90 - --type-grinder: #d2bab0; 91 - --type-brewer: #d2bab0; 87 + --type-bean: #b45309; 88 + --type-recipe: #7f5539; 89 + --type-roaster: #92400e; 90 + --type-grinder: #6b4423; 91 + --type-brewer: #6b4423; 92 + 93 + /* Rating badges */ 94 + --rating-bg: #fef3c7; 95 + --rating-text: #78350f; 96 + 97 + /* Alerts/warnings */ 98 + --alert-warning-bg: #fffbeb; 99 + --alert-warning-border: #fbbf24; 100 + --alert-warning-text: #78350f; 101 + --alert-warning-text-muted: #92400e; 92 102 93 103 /* Shadows */ 94 104 --shadow-sm: 0 1px 3px var(--card-shadow); ··· 100 110 --footer-border: #eaddd7; 101 111 } 102 112 113 + /* ======================================== 114 + Dark theme (auto via prefers-color-scheme) 115 + ======================================== */ 116 + @media (prefers-color-scheme: dark) { 117 + :root { 118 + /* Page */ 119 + --page-bg: #0F0A08; 120 + --page-text: #FAF7F5; 121 + 122 + /* Cards */ 123 + --card-bg: #1C1210; 124 + --card-border: #2E211B; 125 + --card-shadow: rgba(0, 0, 0, 0.3); 126 + --card-shadow-hover: rgba(0, 0, 0, 0.4); 127 + 128 + /* Surfaces (inset areas inside cards) */ 129 + --surface-bg: rgba(36, 26, 22, 0.6); 130 + --surface-border: #2E211B; 131 + 132 + /* Header */ 133 + --header-bg-from: #0F0A08; 134 + --header-bg-to: #0F0A08; 135 + --header-border: #2E211B; 136 + --header-text: #FAF7F5; 137 + 138 + /* Text hierarchy */ 139 + --text-primary: #FAF7F5; 140 + --text-secondary: #E0CEC4; 141 + --text-muted: #C4A898; 142 + --text-faint: #8B7265; 143 + --text-placeholder: #5A4A40; 144 + 145 + /* Interactive */ 146 + --btn-primary-bg: #7f5539; 147 + --btn-primary-bg-hover: #6b4423; 148 + --btn-primary-text: #FAF7F5; 149 + --btn-secondary-bg: #241A16; 150 + --btn-secondary-border: #3D2D24; 151 + --btn-secondary-text: #E0CEC4; 152 + --btn-secondary-bg-hover: #2E211B; 153 + 154 + /* Forms */ 155 + --input-bg: #241A16; 156 + --input-border: #3D2D24; 157 + --input-border-focus: #fbbf24; 158 + --input-ring-focus: rgba(251, 191, 36, 0.15); 159 + --input-bg-focus: rgba(36, 26, 22, 0.8); 160 + 161 + /* Tables */ 162 + --table-bg: #1C1210; 163 + --table-header-bg: #241A16; 164 + --table-border: #2E211B; 165 + --table-row-hover: #241A16; 166 + --table-divider: #241A16; 167 + 168 + /* Modals */ 169 + --modal-bg: #1C1210; 170 + --modal-border: #2E211B; 171 + --modal-backdrop: rgba(0, 0, 0, 0.6); 172 + 173 + /* Feed type indicators (left border) - brighter on dark */ 174 + --type-brew: #6b4423; 175 + --type-bean: #b45309; 176 + --type-recipe: #7f5539; 177 + --type-roaster: #92400e; 178 + --type-grinder: #6b4423; 179 + --type-brewer: #6b4423; 180 + 181 + /* Rating badges */ 182 + --rating-bg: rgba(251, 191, 36, 0.15); 183 + --rating-text: #fbbf24; 184 + 185 + /* Alerts/warnings */ 186 + --alert-warning-bg: rgba(251, 191, 36, 0.08); 187 + --alert-warning-border: rgba(251, 191, 36, 0.3); 188 + --alert-warning-text: #fde68a; 189 + --alert-warning-text-muted: #fcd34d; 190 + 191 + /* Shadows - darker, less visible on dark bg */ 192 + --shadow-sm: 0 1px 3px var(--card-shadow); 193 + --shadow-md: 0 4px 12px var(--card-shadow-hover); 194 + --shadow-lg: 0 10px 25px var(--card-shadow-hover); 195 + 196 + /* Footer */ 197 + --footer-bg: #0F0A08; 198 + --footer-border: #2E211B; 199 + } 200 + 201 + /* Override common hardcoded Tailwind classes for dark mode */ 202 + .text-brown-900 { color: var(--text-primary) !important; } 203 + .text-brown-800 { color: var(--text-primary) !important; } 204 + .text-brown-700 { color: var(--text-secondary) !important; } 205 + .text-brown-600 { color: var(--text-muted) !important; } 206 + .text-brown-500 { color: var(--text-faint) !important; } 207 + .text-brown-400 { color: var(--text-placeholder) !important; } 208 + 209 + .bg-brown-50 { background-color: var(--page-bg) !important; } 210 + .bg-brown-100 { background-color: var(--surface-bg) !important; } 211 + .bg-brown-200 { background-color: var(--card-border) !important; } 212 + 213 + .border-brown-200 { border-color: var(--card-border) !important; } 214 + .border-brown-300 { border-color: var(--card-border) !important; } 215 + 216 + .bg-white { background-color: var(--card-bg) !important; } 217 + .bg-white\/60 { background-color: var(--surface-bg) !important; } 218 + 219 + /* Amber badges stay visible on dark */ 220 + .bg-amber-50 { background-color: rgba(251, 191, 36, 0.1) !important; } 221 + .bg-amber-100 { background-color: rgba(251, 191, 36, 0.15) !important; } 222 + .bg-amber-400 { background-color: #fbbf24 !important; } 223 + .text-amber-900 { color: #fde68a !important; } 224 + .text-amber-700 { color: #fcd34d !important; } 225 + .text-amber-600 { color: #fbbf24 !important; } 226 + .border-amber-400 { border-color: rgba(251, 191, 36, 0.4) !important; } 227 + 228 + /* Warning/alert backgrounds */ 229 + .bg-green-50 { background-color: rgba(251, 191, 36, 0.08) !important; } 230 + 231 + /* Hover states */ 232 + .hover\:bg-brown-200:hover { background-color: var(--surface-bg) !important; } 233 + .hover\:bg-brown-100:hover { background-color: var(--surface-bg) !important; } 234 + .hover\:bg-brown-50:hover { background-color: var(--surface-bg) !important; } 235 + .hover\:text-brown-900:hover { color: var(--text-primary) !important; } 236 + .hover\:text-brown-800:hover { color: var(--text-primary) !important; } 237 + .hover\:text-brown-700:hover { color: var(--text-secondary) !important; } 238 + 239 + /* Dividers */ 240 + .divide-brown-200 > :not([hidden]) ~ :not([hidden]) { border-color: var(--card-border) !important; } 241 + .divide-brown-300 > :not([hidden]) ~ :not([hidden]) { border-color: var(--card-border) !important; } 242 + 243 + /* Ring colors for avatars */ 244 + .ring-brown-500 { --tw-ring-color: #3D2D24 !important; } 245 + .hover\:ring-brown-400:hover { --tw-ring-color: #5A4A40 !important; } 246 + 247 + /* Opacity-variant backgrounds (used in recipe stats grid, etc.) */ 248 + .bg-brown-50\/60 { background-color: var(--surface-bg) !important; } 249 + .bg-brown-200\/60 { background-color: var(--card-border) !important; } 250 + .border-brown-200\/60 { border-color: var(--card-border) !important; } 251 + 252 + /* Group hover text (used in recipe author links) */ 253 + .group-hover\/author\:text-brown-900:hover, 254 + .group\/author:hover .group-hover\/author\:text-brown-900 { color: var(--text-primary) !important; } 255 + .group-hover\/author\:text-brown-800:hover, 256 + .group\/author:hover .group-hover\/author\:text-brown-800 { color: var(--text-primary) !important; } 257 + 258 + /* Forker avatar borders */ 259 + .border-white { border-color: var(--card-bg) !important; } 260 + 261 + /* Decoration colors */ 262 + .decoration-brown-300 { text-decoration-color: var(--card-border) !important; } 263 + .hover\:decoration-brown-500:hover { text-decoration-color: var(--text-faint) !important; } 264 + } 265 + 103 266 @tailwind base; 104 267 @tailwind components; 105 268 @tailwind utilities; ··· 115 278 input[type="button"] { 116 279 min-height: 44px; 117 280 min-width: 44px; 281 + } 282 + 283 + /* Filter pills opt out of the 44px touch target */ 284 + [class*="filter-pill"] { 285 + min-height: auto; 286 + min-width: auto; 118 287 } 119 288 120 289 /* Prevent iOS zoom on input focus */ ··· 491 660 492 661 /* Badges */ 493 662 .badge-rating { 494 - @apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0; 663 + @apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium flex-shrink-0; 664 + background: var(--rating-bg); 665 + color: var(--rating-text); 495 666 } 496 667 497 668 .badge-rating-sm { 498 - @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-900; 669 + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; 670 + background: var(--rating-bg); 671 + color: var(--rating-text); 672 + } 673 + 674 + /* Filter Pills */ 675 + .filter-pill { 676 + @apply inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-full transition-all cursor-pointer; 677 + color: var(--text-muted); 678 + background: transparent; 679 + border: 1px solid var(--card-border); 680 + } 681 + 682 + .filter-pill:hover { 683 + color: var(--text-secondary); 684 + border-color: var(--input-border-focus); 685 + background: var(--surface-bg); 686 + } 687 + 688 + .filter-pill-active { 689 + @apply inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-full transition-all cursor-pointer; 690 + color: var(--btn-primary-text); 691 + background: var(--btn-primary-bg); 692 + border: 1px solid var(--btn-primary-bg); 693 + } 694 + 695 + .filter-pill-active:hover { 696 + background: var(--btn-primary-bg-hover); 697 + border-color: var(--btn-primary-bg-hover); 698 + } 699 + 700 + /* Type-colored filter pills (active state matches feed card left border) */ 701 + .filter-pill-brew, 702 + .filter-pill-bean, 703 + .filter-pill-recipe, 704 + .filter-pill-roaster, 705 + .filter-pill-grinder, 706 + .filter-pill-brewer { 707 + @apply inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-full transition-all cursor-pointer; 708 + color: var(--btn-primary-text); 709 + } 710 + 711 + .filter-pill-brew { 712 + background: var(--type-brew); 713 + border: 1px solid var(--type-brew); 714 + } 715 + 716 + .filter-pill-bean { 717 + background: var(--type-bean); 718 + border: 1px solid var(--type-bean); 719 + } 720 + 721 + .filter-pill-recipe { 722 + background: var(--type-recipe); 723 + border: 1px solid var(--type-recipe); 724 + } 725 + 726 + .filter-pill-roaster { 727 + background: var(--type-roaster); 728 + border: 1px solid var(--type-roaster); 729 + } 730 + 731 + .filter-pill-grinder { 732 + background: var(--type-grinder); 733 + border: 1px solid var(--type-grinder); 734 + } 735 + 736 + .filter-pill-brewer { 737 + background: var(--type-brewer); 738 + border: 1px solid var(--type-brewer); 499 739 } 500 740 501 741 /* Links */ ··· 791 1031 /* Hidden record indicator badge */ 792 1032 .hidden-badge { 793 1033 @apply inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-700 rounded; 1034 + } 1035 + 1036 + /* Alert banners */ 1037 + .alert-warning { 1038 + @apply rounded-lg p-4; 1039 + background: var(--alert-warning-bg); 1040 + border: 1px solid var(--alert-warning-border); 1041 + color: var(--alert-warning-text); 1042 + } 1043 + 1044 + .alert-warning-muted { 1045 + color: var(--alert-warning-text-muted); 1046 + } 1047 + 1048 + /* Home page action buttons */ 1049 + .home-action-primary { 1050 + @apply inline-flex items-center justify-center font-medium transition-all cursor-pointer; 1051 + background: linear-gradient(135deg, #6b4423, #4a2c2a); 1052 + color: #FAF7F5; 1053 + border: 1px solid #6b4423; 1054 + box-shadow: var(--shadow-sm); 1055 + } 1056 + 1057 + .home-action-primary:hover { 1058 + box-shadow: var(--shadow-md); 1059 + filter: brightness(1.1); 1060 + } 1061 + 1062 + .home-action-secondary { 1063 + @apply inline-flex items-center justify-center font-medium transition-all cursor-pointer; 1064 + background: var(--card-bg); 1065 + color: var(--text-primary); 1066 + border: 1.5px solid var(--card-border); 1067 + box-shadow: var(--shadow-sm); 1068 + } 1069 + 1070 + .home-action-secondary:hover { 1071 + border-color: var(--input-border-focus); 1072 + box-shadow: var(--shadow-md); 794 1073 } 795 1074 } 796 1075
+1
tailwind.config.js
··· 1 1 /** @type {import('tailwindcss').Config} */ 2 2 module.exports = { 3 + darkMode: 'media', 3 4 content: [ 4 5 "./internal/**/*.templ", 5 6 "./web/**/*.{html,js}",