This repository has no description
0

Configure Feed

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

web/components: improve Dropdown

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

+43 -43
+19
web/src/lib/components/ui/Dropdown.stories.svelte
··· 54 54 </Dropdown> 55 55 </Story> 56 56 57 + <!-- `size` goes straight to the trigger Button, so it drives the trigger's height, 58 + padding and label size the same way it does on a standalone Button. --> 59 + <Story name="Button trigger sizes" asChild> 60 + <div class="flex items-center gap-3"> 61 + <Dropdown variant="button" size="sm" trigger={actionsTrigger} label="Actions"> 62 + <DropdownItem icon={Pencil}>Edit</DropdownItem> 63 + <DropdownItem icon={Settings}>Settings</DropdownItem> 64 + </Dropdown> 65 + <Dropdown variant="button" size="md" trigger={actionsTrigger} label="Actions"> 66 + <DropdownItem icon={Pencil}>Edit</DropdownItem> 67 + <DropdownItem icon={Settings}>Settings</DropdownItem> 68 + </Dropdown> 69 + <Dropdown variant="button" size="lg" trigger={actionsTrigger} label="Actions"> 70 + <DropdownItem icon={Pencil}>Edit</DropdownItem> 71 + <DropdownItem icon={Settings}>Settings</DropdownItem> 72 + </Dropdown> 73 + </div> 74 + </Story> 75 + 57 76 <!-- `align="right"` anchors the menu's right edge to the trigger, for triggers near 58 77 the right edge of the viewport (e.g. the topbar profile menu). Default is left. --> 59 78 <Story name="Right aligned" asChild>
+24 -43
web/src/lib/components/ui/Dropdown.svelte
··· 5 5 export const dropdown = tv({ 6 6 slots: { 7 7 root: "relative inline-block", 8 - plainTrigger: 9 - "flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-inherit", 8 + plainTrigger: "min-h-0 gap-1 border-0 bg-transparent p-0 text-inherit hover:bg-transparent", 10 9 menu: "dropdown-menu fixed z-50 m-0 max-h-[calc(100dvh-1rem)] w-56 max-w-[calc(100vw-1rem)] divide-y divide-border-default overflow-auto rounded-sm border border-border-default bg-background-default text-foreground-default shadow-regular" 11 10 }, 12 11 variants: { ··· 57 56 group?: string; 58 57 variant?: "plain" | "button"; 59 58 buttonVariant?: ButtonVariants["variant"]; 59 + size?: ButtonVariants["size"]; 60 + insetShadow?: ButtonVariants["insetShadow"]; 60 61 align?: DropdownVariants["align"]; 61 62 label?: string; 62 63 menuClass?: string; ··· 69 70 group, 70 71 variant = "plain", 71 72 buttonVariant = "default", 73 + insetShadow = false, 74 + size = "md", 72 75 align = "left", 73 76 label, 74 77 menuClass, ··· 78 81 79 82 let open = $state(false); 80 83 const classes = $derived(dropdown({ align })); 81 - let triggerWrapper = $state<HTMLElement>(); 82 - let triggerButtonEl = $state<HTMLElement>(); 84 + let triggerElement = $state<HTMLAnchorElement | HTMLButtonElement>(); 83 85 let menuElement = $state<HTMLElement>(); 84 86 const fallbackId = $props.id(); 85 87 const menuId = $derived(id ?? fallbackId); 86 88 87 - const triggerElement = () => 88 - variant === "button" 89 - ? triggerWrapper?.querySelector<HTMLElement>("button, a") 90 - : triggerButtonEl; 91 - 92 89 const focusTrigger = () => { 93 - triggerElement()?.focus(); 90 + triggerElement?.focus(); 94 91 }; 95 92 // intentionally not $state: only used for imperative focus, and reactive 96 93 // reads inside DropdownItem's register $effect would loop it forever ··· 117 114 const onTriggerKeydown = (event: KeyboardEvent) => { 118 115 if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) { 119 116 event.preventDefault(); 120 - triggerElement()?.click(); 117 + triggerElement?.click(); 121 118 } 122 119 }; 123 120 ··· 181 178 </script> 182 179 183 180 <div class={classes.root()}> 184 - {#if variant === "button"} 185 - <span bind:this={triggerWrapper} class="contents"> 186 - <Button 187 - variant={buttonVariant} 188 - class="px-3" 189 - popovertarget={menuId} 190 - popovertargetaction="toggle" 191 - onkeydown={onTriggerKeydown} 192 - aria-haspopup="menu" 193 - aria-expanded={open} 194 - aria-controls={menuId} 195 - aria-label={label} 196 - > 197 - {@render trigger()} 198 - </Button> 199 - </span> 200 - {:else} 201 - <button 202 - bind:this={triggerButtonEl} 203 - type="button" 204 - class={classes.plainTrigger()} 205 - popovertarget={menuId} 206 - popovertargetaction="toggle" 207 - onkeydown={onTriggerKeydown} 208 - aria-haspopup="menu" 209 - aria-expanded={open} 210 - aria-controls={menuId} 211 - aria-label={label} 212 - > 213 - {@render trigger()} 214 - </button> 215 - {/if} 181 + <Button 182 + bind:ref={triggerElement} 183 + variant={variant === "button" ? buttonVariant : "ghost"} 184 + {size} 185 + class={variant === "button" ? undefined : classes.plainTrigger()} 186 + popovertarget={menuId} 187 + popovertargetaction="toggle" 188 + onkeydown={onTriggerKeydown} 189 + aria-haspopup="menu" 190 + aria-expanded={open} 191 + aria-controls={menuId} 192 + aria-label={label} 193 + insetShadow={insetShadow} 194 + > 195 + {@render trigger()} 196 + </Button> 216 197 217 198 <div 218 199 bind:this={menuElement}