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: card ui design refactor

authored by

Patrick Dewey and committed by tangled.org 8809bb60 00240cd7

+151 -32
+18 -8
internal/web/components/entity_tables.templ
··· 8 8 "time" 9 9 ) 10 10 11 - // entityCardHeader renders the date + edit/delete actions header used by all entity cards. 12 - templ entityCardHeader(createdAt time.Time, showActions bool, modalPath string, deletePath string, entityName string) { 11 + // entityCardHeader renders the date + type dot + edit/delete actions header used by all entity cards. 12 + templ entityCardHeader(createdAt time.Time, showActions bool, modalPath string, deletePath string, entityName string, entityType string) { 13 13 <div class="flex items-center justify-between mb-2"> 14 - <div class="text-sm text-brown-600"> 14 + <div class="flex items-center gap-2 text-sm text-brown-600"> 15 + <span 16 + class={ templ.Classes( 17 + "entity-card-dot", 18 + templ.KV("entity-card-dot-bean", entityType == "bean"), 19 + templ.KV("entity-card-dot-roaster", entityType == "roaster"), 20 + templ.KV("entity-card-dot-grinder", entityType == "grinder"), 21 + templ.KV("entity-card-dot-brewer", entityType == "brewer"), 22 + templ.KV("entity-card-dot-recipe", entityType == "recipe"), 23 + ) } 24 + ></span> 15 25 if !createdAt.IsZero() { 16 26 <time datetime={ bff.FormatISO(createdAt) } data-local="date">{ createdAt.Format("Jan 2, 2006") }</time> 17 27 } ··· 78 88 // BeanCard renders a single bean as a compact card 79 89 templ BeanCard(bean *models.Bean, showActions bool, ownerHandle string, brewCount int, avgBrewRating float64) { 80 90 <div class="feed-card feed-card-bean"> 81 - @entityCardHeader(bean.CreatedAt, showActions, "/api/modals/bean/"+bean.RKey, "/api/beans/"+bean.RKey, "bean") 91 + @entityCardHeader(bean.CreatedAt, showActions, "/api/modals/bean/"+bean.RKey, "/api/beans/"+bean.RKey, "bean", "bean") 82 92 <div class="feed-content-box-sm"> 83 93 <div class="flex items-start justify-between gap-3 mb-2"> 84 94 <div class="min-w-0"> ··· 188 198 // RoasterCard renders a single roaster as a compact card 189 199 templ RoasterCard(roaster *models.Roaster, showActions bool, ownerHandle string, beanCount int, avgBrewRating float64) { 190 200 <div class="feed-card feed-card-roaster"> 191 - @entityCardHeader(roaster.CreatedAt, showActions, "/api/modals/roaster/"+roaster.RKey, "/api/roasters/"+roaster.RKey, "roaster") 201 + @entityCardHeader(roaster.CreatedAt, showActions, "/api/modals/roaster/"+roaster.RKey, "/api/roasters/"+roaster.RKey, "roaster", "roaster") 192 202 <div class="feed-content-box-sm"> 193 203 <div class="flex items-start justify-between gap-2 mb-2"> 194 204 <div class="min-w-0"> ··· 264 274 // GrinderCard renders a single grinder as a compact card 265 275 templ GrinderCard(grinder *models.Grinder, showActions bool, ownerHandle string, brewCount int) { 266 276 <div class="feed-card feed-card-grinder"> 267 - @entityCardHeader(grinder.CreatedAt, showActions, "/api/modals/grinder/"+grinder.RKey, "/api/grinders/"+grinder.RKey, "grinder") 277 + @entityCardHeader(grinder.CreatedAt, showActions, "/api/modals/grinder/"+grinder.RKey, "/api/grinders/"+grinder.RKey, "grinder", "grinder") 268 278 <div class="feed-content-box-sm"> 269 279 <div class="flex items-start justify-between gap-2 mb-2"> 270 280 <div class="min-w-0"> ··· 331 341 // BrewerCard renders a single brewer as a compact card 332 342 templ BrewerCard(brewer *models.Brewer, showActions bool, ownerHandle string, brewCount int) { 333 343 <div class="feed-card feed-card-brewer"> 334 - @entityCardHeader(brewer.CreatedAt, showActions, "/api/modals/brewer/"+brewer.RKey, "/api/brewers/"+brewer.RKey, "brewer") 344 + @entityCardHeader(brewer.CreatedAt, showActions, "/api/modals/brewer/"+brewer.RKey, "/api/brewers/"+brewer.RKey, "brewer", "brewer") 335 345 <div class="feed-content-box-sm"> 336 346 <div class="flex items-start justify-between gap-2 mb-2"> 337 347 <div class="min-w-0"> ··· 389 399 // RecipeCard renders a single recipe as a compact card 390 400 templ RecipeCard(recipe *models.Recipe, showActions bool) { 391 401 <div class="feed-card feed-card-recipe"> 392 - @entityCardHeader(recipe.CreatedAt, showActions, "/api/modals/recipe/"+recipe.RKey, "/api/recipes/"+recipe.RKey, "recipe") 402 + @entityCardHeader(recipe.CreatedAt, showActions, "/api/modals/recipe/"+recipe.RKey, "/api/recipes/"+recipe.RKey, "recipe", "recipe") 393 403 <div class="feed-content-box-sm"> 394 404 <div class="flex items-start justify-between gap-2 mb-2"> 395 405 <div class="min-w-0">
+8
internal/web/components/icons.templ
··· 11 11 </svg> 12 12 } 13 13 14 + // IconBean renders a coffee bean icon (oval with S-curved center crease) 15 + templ IconBean() { 16 + <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"> 17 + <ellipse cx="12" cy="12" rx="5.5" ry="9" transform="rotate(25 12 12)"></ellipse> 18 + <path d="M12 3c-1.5 3 1.5 6 0 9s1.5 6 0 9" transform="rotate(25 12 12)"></path> 19 + </svg> 20 + } 21 + 14 22 // IconBrewer renders a dripper/funnel icon (replaces teapot emoji) 15 23 templ IconBrewer() { 16 24 <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">
+36
internal/web/components/shared.templ
··· 72 72 } 73 73 } 74 74 75 + // TypeBadge renders a colored pill with an icon and type label for record type identification. 76 + templ TypeBadge(recordType string) { 77 + switch recordType { 78 + case "brew": 79 + <span class="type-badge type-badge-brew"> 80 + @IconCoffee() 81 + Brew 82 + </span> 83 + case "bean": 84 + <span class="type-badge type-badge-bean"> 85 + @IconBean() 86 + Bean 87 + </span> 88 + case "recipe": 89 + <span class="type-badge type-badge-recipe"> 90 + @IconFileText() 91 + Recipe 92 + </span> 93 + case "roaster": 94 + <span class="type-badge type-badge-roaster"> 95 + @IconStore() 96 + Roaster 97 + </span> 98 + case "grinder": 99 + <span class="type-badge type-badge-grinder"> 100 + @IconGear() 101 + Grinder 102 + </span> 103 + case "brewer": 104 + <span class="type-badge type-badge-brewer"> 105 + @IconBrewer() 106 + Brewer 107 + </span> 108 + } 109 + } 110 + 75 111 // EmptyStateProps defines properties for empty state messaging 76 112 type EmptyStateProps struct { 77 113 Message string
+12
internal/web/pages/feed.templ
··· 284 284 if item.Brew != nil { 285 285 added a 286 286 <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="underline hover:text-brown-900">new brew</a> 287 + { " " } 288 + @components.TypeBadge("brew") 287 289 } else { 288 290 { item.Action } 289 291 } ··· 291 293 if item.Bean != nil { 292 294 added a 293 295 <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="underline hover:text-brown-900">new bean</a> 296 + { " " } 297 + @components.TypeBadge("bean") 294 298 } else { 295 299 { item.Action } 296 300 } ··· 298 302 if item.Roaster != nil { 299 303 added a 300 304 <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="underline hover:text-brown-900">new roaster</a> 305 + { " " } 306 + @components.TypeBadge("roaster") 301 307 } else { 302 308 { item.Action } 303 309 } ··· 305 311 if item.Grinder != nil { 306 312 added a 307 313 <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="underline hover:text-brown-900">new grinder</a> 314 + { " " } 315 + @components.TypeBadge("grinder") 308 316 } else { 309 317 { item.Action } 310 318 } ··· 312 320 if item.Brewer != nil { 313 321 added a 314 322 <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="underline hover:text-brown-900">new brewer</a> 323 + { " " } 324 + @components.TypeBadge("brewer") 315 325 } else { 316 326 { item.Action } 317 327 } ··· 319 329 if item.Recipe != nil { 320 330 added a 321 331 <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="underline hover:text-brown-900">new recipe</a> 332 + { " " } 333 + @components.TypeBadge("recipe") 322 334 } else { 323 335 { item.Action } 324 336 }
+77 -24
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 + filter pills) */ 85 + /* Feed type indicators (filter pills + type badges) */ 86 86 --type-brew: #6b4423; 87 87 --type-bean: #b45309; 88 - --type-recipe: #7f5539; 88 + --type-recipe: #a0522d; 89 89 --type-roaster: #92400e; 90 - --type-grinder: #6b4423; 91 - --type-brewer: #6b4423; 90 + --type-grinder: #78716c; 91 + --type-brewer: #5b6e4e; 92 + 93 + /* Type background tints (subtle card wash) */ 94 + --type-brew-tint: rgba(107, 68, 35, 0.04); 95 + --type-bean-tint: rgba(180, 83, 9, 0.04); 96 + --type-recipe-tint: rgba(160, 82, 45, 0.04); 97 + --type-roaster-tint: rgba(146, 64, 14, 0.04); 98 + --type-grinder-tint: rgba(120, 113, 108, 0.04); 99 + --type-brewer-tint: rgba(91, 110, 78, 0.04); 92 100 93 101 /* Rating badges */ 94 102 --rating-bg: #fef3c7; ··· 172 180 --modal-border: #2E211B; 173 181 --modal-backdrop: rgba(0, 0, 0, 0.6); 174 182 175 - /* Feed type indicators (left border) - brighter on dark */ 176 - --type-brew: #6b4423; 177 - --type-bean: #b45309; 178 - --type-recipe: #7f5539; 179 - --type-roaster: #92400e; 180 - --type-grinder: #6b4423; 181 - --type-brewer: #6b4423; 183 + /* Feed type indicators - brighter on dark */ 184 + --type-brew: #a67c52; 185 + --type-bean: #d97706; 186 + --type-recipe: #c4724a; 187 + --type-roaster: #c2610e; 188 + --type-grinder: #a8a29e; 189 + --type-brewer: #8ba37a; 190 + 191 + /* Type background tints (slightly stronger on dark) */ 192 + --type-brew-tint: rgba(166, 124, 82, 0.06); 193 + --type-bean-tint: rgba(217, 119, 6, 0.06); 194 + --type-recipe-tint: rgba(196, 114, 74, 0.06); 195 + --type-roaster-tint: rgba(194, 97, 14, 0.06); 196 + --type-grinder-tint: rgba(168, 162, 158, 0.06); 197 + --type-brewer-tint: rgba(139, 163, 122, 0.06); 182 198 183 199 /* Rating badges */ 184 200 --rating-bg: rgba(251, 191, 36, 0.15); ··· 305 321 --modal-bg: #1C1210; 306 322 --modal-border: #2E211B; 307 323 --modal-backdrop: rgba(0, 0, 0, 0.6); 308 - --type-brew: #6b4423; 309 - --type-bean: #b45309; 310 - --type-recipe: #7f5539; 311 - --type-roaster: #92400e; 312 - --type-grinder: #6b4423; 313 - --type-brewer: #6b4423; 324 + --type-brew: #a67c52; 325 + --type-bean: #d97706; 326 + --type-recipe: #c4724a; 327 + --type-roaster: #c2610e; 328 + --type-grinder: #a8a29e; 329 + --type-brewer: #8ba37a; 330 + --type-brew-tint: rgba(166, 124, 82, 0.06); 331 + --type-bean-tint: rgba(217, 119, 6, 0.06); 332 + --type-recipe-tint: rgba(196, 114, 74, 0.06); 333 + --type-roaster-tint: rgba(194, 97, 14, 0.06); 334 + --type-grinder-tint: rgba(168, 162, 158, 0.06); 335 + --type-brewer-tint: rgba(139, 163, 122, 0.06); 314 336 --rating-bg: rgba(251, 191, 36, 0.15); 315 337 --rating-text: #fbbf24; 316 338 --alert-warning-bg: rgba(251, 191, 36, 0.08); ··· 759 781 box-shadow: var(--shadow-md); 760 782 } 761 783 762 - /* Feed card type indicators */ 784 + /* Feed card type tints — subtle background wash per record type */ 763 785 .feed-card-brew { 764 - border-left: 3px solid var(--type-brew); 786 + background: linear-gradient(var(--type-brew-tint), var(--type-brew-tint)), var(--card-bg); 765 787 } 766 788 767 789 .feed-card-bean { 768 - border-left: 3px solid var(--type-bean); 790 + background: linear-gradient(var(--type-bean-tint), var(--type-bean-tint)), var(--card-bg); 769 791 } 770 792 771 793 .feed-card-recipe { 772 - border-left: 3px solid var(--type-recipe); 794 + background: linear-gradient(var(--type-recipe-tint), var(--type-recipe-tint)), var(--card-bg); 773 795 } 774 796 775 797 .feed-card-roaster { 776 - border-left: 3px solid var(--type-roaster); 798 + background: linear-gradient(var(--type-roaster-tint), var(--type-roaster-tint)), var(--card-bg); 777 799 } 778 800 779 801 .feed-card-grinder { 780 - border-left: 3px solid var(--type-grinder); 802 + background: linear-gradient(var(--type-grinder-tint), var(--type-grinder-tint)), var(--card-bg); 781 803 } 782 804 783 805 .feed-card-brewer { 784 - border-left: 3px solid var(--type-brewer); 806 + background: linear-gradient(var(--type-brewer-tint), var(--type-brewer-tint)), var(--card-bg); 807 + } 808 + 809 + /* Type badges — inline pill with icon + label */ 810 + .type-badge { 811 + @apply inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium; 812 + border: 1px solid currentColor; 813 + opacity: 0.8; 814 + } 815 + 816 + .type-badge svg { 817 + color: inherit; 818 + width: 0.75rem; 819 + height: 0.75rem; 785 820 } 821 + 822 + .type-badge-brew { color: var(--type-brew); } 823 + .type-badge-bean { color: var(--type-bean); } 824 + .type-badge-recipe { color: var(--type-recipe); } 825 + .type-badge-roaster { color: var(--type-roaster); } 826 + .type-badge-grinder { color: var(--type-grinder); } 827 + .type-badge-brewer { color: var(--type-brewer); } 828 + 829 + /* Entity card type dot — small colored indicator in card headers */ 830 + .entity-card-dot { 831 + @apply inline-block w-2 h-2 rounded-full flex-shrink-0; 832 + } 833 + 834 + .entity-card-dot-bean { background: var(--type-bean); } 835 + .entity-card-dot-roaster { background: var(--type-roaster); } 836 + .entity-card-dot-grinder { background: var(--type-grinder); } 837 + .entity-card-dot-brewer { background: var(--type-brewer); } 838 + .entity-card-dot-recipe { background: var(--type-recipe); } 786 839 787 840 .feed-content-box { 788 841 background: var(--surface-bg);