Coves frontend - a photon fork
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>