This repository has no description
0

Configure Feed

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

web/components: give Select an optional leading icon

Puts a glyph on the field's leading edge, mirroring the chevron on the
other side. Wired into both renderings, so flipping `rich` still doesn't
change how the field looks.

Signed-off-by: eti <eti@eti.tf>

+29 -6
+10 -4
web/src/lib/components/ui/Combobox.svelte
··· 7 7 slots: { 8 8 root: "relative inline-block w-full", 9 9 trigger: "flex min-h-9 cursor-pointer items-center gap-2 px-2.5 py-1", 10 - value: "min-w-0 flex-1 truncate typography-paragraph-regular text-left", 10 + value: "min-w-0 flex-1 truncate text-left typography-paragraph-regular", 11 11 panel: 12 12 "combobox-panel fixed z-50 m-0 hidden max-h-[min(24rem,calc(100dvh-1rem))] w-72 max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-sm border border-border-default bg-background-default p-0 text-foreground-default shadow-regular open:flex dark:shadow-none", 13 13 search: "shrink-0 border-b border-border-default p-2", 14 14 list: "min-h-0 flex-1 overflow-y-auto p-1", 15 - groupHeading: 16 - "px-2 pt-2 pb-1 font-semibold text-foreground-muted first:pt-1", 15 + groupHeading: "px-2 pt-2 pb-1 font-semibold text-foreground-muted first:pt-1", 17 16 option: 18 17 "flex w-full cursor-pointer items-center gap-2 rounded-sm bg-transparent px-2 py-1.5 text-left aria-disabled:cursor-not-allowed aria-disabled:text-foreground-disabled data-active:bg-background-subtle dark:data-active:bg-background-subtle", 19 18 check: "size-4 shrink-0 text-foreground-default", ··· 36 35 import Check from "$icon/check"; 37 36 import ChevronsUpDown from "$icon/chevrons-up-down"; 38 37 import Search from "$icon/search"; 39 - import type { Snippet } from "svelte"; 38 + import type { Component, Snippet } from "svelte"; 39 + import type { SvelteHTMLElements } from "svelte/elements"; 40 40 import { tick } from "svelte"; 41 41 import Input from "./Input.svelte"; 42 42 import { selectField, type SelectAlign, type SelectOption } from "./selectField"; ··· 53 53 emptyLabel?: string; 54 54 /** names the trigger and the listbox for screen readers */ 55 55 label?: string; 56 + /** decorative glyph on the trigger's leading edge, standing for the current selection */ 57 + icon?: Component<SvelteHTMLElements["svg"]>; 56 58 /** take filtering over, e.g. to query a server: `options` is then rendered as given */ 57 59 onSearch?: (query: string) => void; 58 60 loading?: boolean; ··· 75 77 searchPlaceholder = "Filter…", 76 78 emptyLabel = "No matches", 77 79 label, 80 + icon: Icon, 78 81 onSearch, 79 82 loading = false, 80 83 error = false, ··· 259 262 aria-label={label} 260 263 style="anchor-name: {anchorName}" 261 264 > 265 + {#if Icon} 266 + <Icon class="size-4 shrink-0 text-foreground-default" aria-hidden="true" /> 267 + {/if} 262 268 <span class={classes.value()}>{triggerLabel === "" ? placeholder : triggerLabel}</span> 263 269 <ChevronsUpDown class={field.chevron({ class: "size-3.5" })} aria-hidden="true" /> 264 270 </button>
+19 -2
web/src/lib/components/ui/Select.svelte
··· 2 2 // SelectOption and SelectAlign live in ./selectField, which owns the shape both renderings 3 3 // read; re-exporting them from here trips eslint's no-import-assign on module scripts 4 4 import ChevronDown from "$icon/chevron-down"; 5 - import type { Snippet } from "svelte"; 5 + import type { Component, Snippet } from "svelte"; 6 + import type { SvelteHTMLElements } from "svelte/elements"; 6 7 import Combobox from "./Combobox.svelte"; 7 8 import { selectField, type SelectAlign, type SelectOption } from "./selectField"; 8 9 ··· 19 20 placeholder?: string; 20 21 /** names the control for screen readers */ 21 22 label?: string; 23 + /** glyph pinned to the leading edge, mirroring the chevron. Decorative — it stands for 24 + * the current selection, so keep it in step with `value` rather than the list. */ 25 + icon?: Component<SvelteHTMLElements["svg"]>; 22 26 error?: boolean; 23 27 disabled?: boolean; 24 28 /** goes on the control itself, so a Field's label can point at it */ ··· 43 47 onSelect, 44 48 placeholder, 45 49 label, 50 + icon: Icon, 46 51 error = false, 47 52 disabled = false, 48 53 id, ··· 69 74 70 75 const field = $derived(selectField({ error, disabled })); 71 76 77 + // `pl-7` has to be able to beat this `px-2`, so it is merged after rather than interpolated 78 + // into the same string — prettier's class sorter would hoist an inline conditional to the 79 + // front, where tailwind-merge resolves it the other way and the label lands under the icon 80 + const nativeBox = "min-h-9 appearance-none px-2 py-1 pr-8"; 81 + 72 82 const onchange = (event: Event) => { 73 83 onSelect?.((event.currentTarget as HTMLSelectElement).value); 74 84 }; ··· 87 97 {onSelect} 88 98 {placeholder} 89 99 {label} 100 + icon={Icon} 90 101 {error} 91 102 {disabled} 92 103 {id} ··· 101 112 /> 102 113 {:else} 103 114 <div class="relative {className ?? ''}"> 115 + {#if Icon} 116 + <Icon 117 + class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-foreground-default" 118 + aria-hidden="true" 119 + /> 120 + {/if} 104 121 <select 105 122 bind:value 106 123 {id} ··· 108 125 {onchange} 109 126 aria-label={label} 110 127 aria-invalid={error} 111 - class={field.box({ class: "min-h-9 appearance-none px-2 py-1 pr-8" })} 128 + class={field.box({ class: [nativeBox, Icon && "pl-7"] })} 112 129 > 113 130 {#if placeholder} 114 131 <option value="">{placeholder}</option>