Coves frontend - a photon fork
at main 176 lines 4.7 kB view raw
1<script lang="ts" module> 2 import { Label, Menu, MenuButton } from 'mono-svelte' 3 import { buttonSize } from 'mono-svelte/button/Button.svelte' 4 import { type Snippet, setContext, tick } from 'svelte' 5 import type { Placement } from 'svelte-floating-ui/dom' 6 import { 7 CheckCircle, 8 ChevronUpDown, 9 Icon, 10 type IconSource, 11 } from 'svelte-hero-icons/dist' 12 import type { Attachment } from 'svelte/attachments' 13 import type { ClassValue, HTMLSelectAttributes } from 'svelte/elements' 14 15 interface Props<T> extends Omit<HTMLSelectAttributes, 'size'> { 16 value?: T | string | undefined 17 placeholder?: string | undefined 18 label?: string | undefined 19 size?: 'md' | 'sm' 20 id?: string 21 class?: ClassValue 22 baseClass?: ClassValue 23 selectClass?: ClassValue 24 customLabel?: import('svelte').Snippet 25 children?: import('svelte').Snippet 26 customOption?: import('svelte').Snippet< 27 [ 28 { 29 option: { 30 value: string 31 label: string 32 icon?: IconSource 33 disabled?: boolean 34 } 35 selected: boolean 36 }, 37 ] 38 > 39 target?: Snippet<[Attachment]> 40 oncontextmenu?: HTMLSelectAttributes['oncontextmenu'] 41 onchange?: HTMLSelectAttributes['onchange'] 42 placement?: Placement 43 } 44 45 export type { Props as SelectProps } 46</script> 47 48<script lang="ts" generics="T"> 49 let open = $state(false) 50 let element: HTMLSelectElement | undefined = $state() 51 52 interface SelectContext { 53 options: { 54 value: string 55 label: string 56 icon?: IconSource 57 disabled?: boolean 58 isLabel?: boolean 59 }[] 60 } 61 62 const context = setContext<SelectContext>('select', { 63 options: [], 64 }) 65 66 let { 67 value = $bindable(undefined), 68 placeholder = undefined, 69 label = undefined, 70 size = 'md', 71 class: clazz = '', 72 baseClass = '', 73 selectClass = '', 74 customLabel, 75 children, 76 customOption, 77 oncontextmenu, 78 onchange, 79 placement = 'bottom', 80 target: passedTarget, 81 ...rest 82 }: Props<T> = $props() 83</script> 84 85{#snippet selectTarget(attachment: Attachment)} 86 <Label 87 text={label} 88 customText={customLabel} 89 class={['space-y-1 relative max-w-full w-max min-w-0', baseClass]} 90 > 91 <div class="relative max-w-full" role="presentation"> 92 <select 93 {@attach attachment} 94 {...rest} 95 bind:this={element} 96 class={[ 97 buttonSize[size], 98 'btn btn-secondary select rounded-xl appearance-none pr-6! w-full shadow-xs', 99 selectClass, 100 clazz, 101 ]} 102 bind:value 103 onmousedown={(e) => { 104 e.preventDefault() 105 }} 106 onkeypress={(e) => { 107 e.preventDefault() 108 open = !open 109 }} 110 {onchange} 111 {oncontextmenu} 112 {placeholder} 113 > 114 {#if placeholder} 115 <option disabled selected value="">{placeholder}</option> 116 {/if} 117 {@render children?.()} 118 </select> 119 <Icon 120 src={ChevronUpDown} 121 micro 122 size="16" 123 class="absolute bottom-1/2 translate-y-1/2 right-1 box-border pointer-events-none z-10 text-slate-600 dark:text-zinc-400" 124 /> 125 </div> 126 </Label> 127{/snippet} 128 129<Menu bind:open {placement}> 130 {#snippet target(attachment)} 131 {@const render = passedTarget ?? selectTarget} 132 {@render render?.(attachment)} 133 {/snippet} 134 {#each context.options as option (option)} 135 {#if customOption}{@render customOption({ 136 option, 137 selected: option.value == value, 138 })}{:else} 139 <MenuButton 140 onclick={async () => { 141 value = option.value 142 await tick() 143 element?.dispatchEvent(new Event('change', { bubbles: true })) 144 }} 145 size="custom" 146 disabled={option.disabled} 147 color="none" 148 class={[ 149 'min-h-0! py-1 hover:bg-slate-100 dark:hover:bg-zinc-800', 150 option.value == value && 151 'bg-slate-100 dark:bg-zinc-800 text-primary-900 dark:text-primary-100 font-medium', 152 option.disabled && 153 'pointer-events-none text-slate-600 dark:text-zinc-400', 154 option.isLabel && 'text-xs mt-2', 155 ]} 156 > 157 {#if option.value == value} 158 <Icon 159 src={CheckCircle} 160 size="16" 161 micro 162 class="text-primary-900 dark:text-primary-100" 163 /> 164 {:else if option.icon} 165 <Icon 166 src={option.icon} 167 size="16" 168 micro 169 class="text-slate-600 dark:text-zinc-400" 170 /> 171 {/if} 172 {@html option.label} 173 </MenuButton> 174 {/if} 175 {/each} 176</Menu>