From 8318a5f97099d9da8bd11bf5a485a6fecd9b3f89 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 1 Aug 2026 09:51:19 +0200 Subject: [PATCH 1/3] fix(library): make the group header's two destinations findable Clicking a filament group header opens the filament, but nothing said so: its only hover signal was the same background tint a spool row gets, and the one glyph on the row is the collapse chevron, which says "fold", not "open". Reaching a manufacturer was worse off -- the breadcrumb, the search box and the group menu were the only routes, and each one asks you to already know it is there (#989). The title now takes a link's underline and colour on hover, beside an arrow that stays faintly visible at rest: hover is the one affordance a touch user never sees, and they are a good share of who could not find this. The manufacturer already printed on every filament group is now a link to its inspector, so the shortest route to a vendor is one click from the main list. Inspector breadcrumbs underline on hover too, for the same reason. Anchors cannot nest, so the entity link became an overlay covering the whole row, with the manufacturer link and the disclosure stacked above it. The row keeps its full click target, and the overlay is a real element rather than a stretched pseudo-element so tap-target tooling measures what people actually press. The manufacturer takes its colour from --accent-soft: --accent-link is tuned for the lighter surfaces links normally sit on and manages only 4.2:1 against the sunken header in the light theme, where --accent-soft clears AA in both (4.9 / 7.3). Row height is unchanged at 52px and the mobile a11y audit reports the same zero axe violations it did before the change. The manufacturer link is a 28px tap target rather than the audit's preferred 44px: at 44 it would span most of the row and start swallowing clicks meant for the filament link underneath it. Fixes #989 --- client_v2/locales/en/common.json | 2 + client_v2/src/lib/api/map.ts | 12 +- client_v2/src/lib/api/types.ts | 4 + .../src/lib/components/Breadcrumbs.svelte | 5 + .../lib/components/library/GroupHeader.svelte | 230 ++++++++++++++---- .../lib/components/library/GroupRow.svelte | 14 +- client_v2/src/lib/utils/library.ts | 3 + 7 files changed, 214 insertions(+), 56 deletions(-) diff --git a/client_v2/locales/en/common.json b/client_v2/locales/en/common.json index 569c7c7c..e7386d44 100644 --- a/client_v2/locales/en/common.json +++ b/client_v2/locales/en/common.json @@ -265,6 +265,8 @@ "library.nUnused": "×{{count}} unused", "library.noLocation": "no location", "library.noValues": "No values", + "library.openGroup": "Open {{name}}", + "library.openManufacturer": "Open manufacturer {{name}}", "library.section.extra": "Extra fields", "library.section.filament": "Filament", "library.section.spool": "Spool", diff --git a/client_v2/src/lib/api/map.ts b/client_v2/src/lib/api/map.ts index b4b6c777..71b7d84a 100644 --- a/client_v2/src/lib/api/map.ts +++ b/client_v2/src/lib/api/map.ts @@ -106,11 +106,19 @@ export function mapGroup(g: Json): GroupSummary { let badge = ''; let colors: string[] = []; let direction: MultiColorDirection | undefined; + // Filament groups carry their manufacturer separately from the subtitle text + // so the header can render it as a link to the vendor (see GroupHeader). + let vendorId: string | undefined; + let vendorName: string | undefined; if (field === 'filament' && g.filament) { const f: Json = g.filament; title = f.name ?? '(unnamed filament)'; - subtitle = `${f.vendor?.name ?? 'No manufacturer'} · ${f.diameter} mm`; + subtitle = `${f.diameter} mm`; + if (f.vendor) { + vendorId = String(f.vendor.id); + vendorName = f.vendor.name ?? '(unnamed manufacturer)'; + } badge = f.material ?? ''; colors = colorsFromApi(f); direction = f.multi_color_direction ?? undefined; @@ -136,6 +144,8 @@ export function mapGroup(g: Json): GroupSummary { title, subtitle, badge, + vendorId, + vendorName, colors, direction, spoolCount: g.spool_count ?? 0, diff --git a/client_v2/src/lib/api/types.ts b/client_v2/src/lib/api/types.ts index ef443610..390ba7fb 100644 --- a/client_v2/src/lib/api/types.ts +++ b/client_v2/src/lib/api/types.ts @@ -29,6 +29,10 @@ export interface GroupSummary { title: string; subtitle: string; badge: string; + /** Filament groups only: the manufacturer, so the header can link to it. + * Both absent when the filament has no manufacturer set. */ + vendorId?: string; + vendorName?: string; colors: string[]; /** Multi-color layout, when grouping by a single filament; undefined otherwise. */ direction?: MultiColorDirection; diff --git a/client_v2/src/lib/components/Breadcrumbs.svelte b/client_v2/src/lib/components/Breadcrumbs.svelte index 9bbe276b..81cecd3b 100644 --- a/client_v2/src/lib/components/Breadcrumbs.svelte +++ b/client_v2/src/lib/components/Breadcrumbs.svelte @@ -51,8 +51,13 @@ font: inherit; text-decoration: none; } + /* Underlined on hover, not just recoloured: a crumb is a short piece of dim + text in a corner, and colour alone left people reading it as a caption rather + than as the way to the manufacturer (#989). */ .crumb:hover { color: var(--accent-link-hover); + text-decoration: underline; + text-underline-offset: 2px; } .current { color: var(--text-2); diff --git a/client_v2/src/lib/components/library/GroupHeader.svelte b/client_v2/src/lib/components/library/GroupHeader.svelte index 469445f9..a29dd8d0 100644 --- a/client_v2/src/lib/components/library/GroupHeader.svelte +++ b/client_v2/src/lib/components/library/GroupHeader.svelte @@ -1,7 +1,12 @@ - + you are scrolled deep inside a long group, which is exactly when you want it. + + The entity link is only the title, but its stretched ::after (see the styles) + extends its hit area over the whole row, so the row-wide click target survives + the two links being siblings. That is also what makes `.main:hover` mean + "anywhere on the row except the other two controls", which is the precision the + hover affordance below needs. -->
- - {#if group.colors.length} - - {/if} -
-
- {group.title} - {#if group.badge}{/if} + {#if href} + + + {/if} + {#if group.colors.length} + + {/if} +
+
+ + {group.title} + {#if href}{/if} + {#if group.badge}{/if} +
+ {#if group.vendorName || group.subtitle} +
+ + {#if group.vendorName && vendorHref} + + + {group.vendorName} + + {:else if group.vendorName} + {group.vendorName} + {/if} + {#if group.vendorName && group.subtitle}{/if} + {#if group.subtitle}{group.subtitle}{/if}
- {#if group.subtitle}
{group.subtitle}
{/if} -
- -
- {group.meta} - {group.count} -
- + {/if} +
+ +
+ {group.meta} + {group.count} +