66'use client' ;
77
88import { Fragment , useCallback , useEffect , useRef , useState } from 'react' ;
9- import { Dialog , DialogPanel , DialogTitle , Transition , TransitionChild } from '@headlessui/react' ;
10- import { XMarkIcon , PhotoIcon , CheckIcon } from '@heroicons/react/24/outline' ;
9+ import { Dialog , DialogPanel , DialogTitle , Transition , TransitionChild , Listbox , ListboxButton , ListboxOption , ListboxOptions } from '@headlessui/react' ;
10+ import { XMarkIcon , PhotoIcon , CheckIcon , ChevronDownIcon } from '@heroicons/react/24/outline' ;
11+ import { createPortal } from 'react-dom' ;
1112import { cn } from '@/lib/utils' ;
1213import { processImage , validateImageFile } from '@/lib/utils/imageUtils' ;
1314import { processDocFile , validateDocFile , formatFileSize , getFileIcon } from '@/lib/utils/fileUtils' ;
@@ -22,6 +23,109 @@ interface CreateCardModalProps {
2223 preselectedCategoryId ?: string ;
2324}
2425
26+ interface DropdownSelectProps {
27+ id : string ;
28+ value : string ;
29+ onChange : ( val : string ) => void ;
30+ options : { id : string ; name : string } [ ] ;
31+ }
32+
33+ function DropdownSelect ( { id, value, onChange, options } : DropdownSelectProps ) {
34+ const selectedOption = options . find ( ( o ) => o . id === value ) || options [ 0 ] ;
35+
36+ return (
37+ < Listbox value = { value } onChange = { onChange } >
38+ { ( { open } ) => (
39+ < >
40+ < ListboxButton
41+ data-select-id = { id }
42+ className = { cn (
43+ "flex w-full items-center justify-between gap-2 px-3 py-2 rounded-xl text-sm font-medium" ,
44+ "transition-all duration-200 border" ,
45+ "surface-card surface-card--subtle hover:border-[color-mix(in_srgb,var(--accent-border)_40%,transparent)] bg-[rgba(5,6,11,0.9)] text-(--text-strong)"
46+ ) }
47+ >
48+ < span className = "truncate" > { selectedOption ?. name || 'Select option' } </ span >
49+ < ChevronDownIcon className = "w-4 h-4 opacity-60 shrink-0" />
50+ </ ListboxButton >
51+
52+ { globalThis . window !== undefined && open && createPortal (
53+ < Transition
54+ show = { open }
55+ as = { Fragment }
56+ enter = "transition ease-out duration-100"
57+ enterFrom = "transform opacity-0 scale-95"
58+ enterTo = "transform opacity-100 scale-100"
59+ leave = "transition ease-in duration-75"
60+ leaveFrom = "transform opacity-100 scale-100"
61+ leaveTo = "transform opacity-0 scale-95"
62+ >
63+ < ListboxOptions
64+ static
65+ className = "fixed rounded-2xl focus:outline-none z-9999 overflow-hidden shadow-[0_26px_70px_rgba(0,0,0,0.75)]"
66+ style = { {
67+ top : `${ ( document . querySelector ( `[data-select-id="${ id } "]` ) as HTMLElement ) ?. getBoundingClientRect ( ) . bottom + 8 || 0 } px` ,
68+ left : `${ ( document . querySelector ( `[data-select-id="${ id } "]` ) as HTMLElement ) ?. getBoundingClientRect ( ) . left || 0 } px` ,
69+ width : `${ ( document . querySelector ( `[data-select-id="${ id } "]` ) as HTMLElement ) ?. getBoundingClientRect ( ) . width || 256 } px` ,
70+ backgroundColor : 'color-mix(in srgb, var(--background-muted) 50%, transparent)' ,
71+ backdropFilter : 'blur(24px)' ,
72+ WebkitBackdropFilter : 'blur(24px)' ,
73+ border : '1px solid color-mix(in srgb, var(--glass-border) 10%, transparent)' ,
74+ maxHeight : '250px' ,
75+ overflowY : 'auto'
76+ } }
77+ >
78+ < div className = "p-2 relative flex flex-col gap-0.5" style = { { color : '#f8fafc' } } >
79+ { options . map ( ( option ) => (
80+ < ListboxOption key = { option . id } value = { option . id } >
81+ { ( { focus, selected } ) => (
82+ < div
83+ className = { cn (
84+ "group relative w-full flex items-center gap-3 px-3 py-2.5 rounded-[20px] text-left transition-all duration-300 ease-out overflow-hidden cursor-pointer" ,
85+ "bg-[color-mix(in_srgb,var(--background)_90%,transparent)]" ,
86+ selected && "border border-transparent" ,
87+ ! selected && focus && "scale-[1.02]"
88+ ) }
89+ >
90+ { ( selected || focus ) && (
91+ < >
92+ < div className = "absolute inset-0 opacity-70 transition-opacity duration-500 backdrop-blur-md" >
93+ < div
94+ className = "absolute inset-0 mix-blend-screen transition-opacity duration-300"
95+ style = { {
96+ backgroundImage : `repeating-linear-gradient(125deg, transparent 0%, transparent 15%, color-mix(in srgb, var(--accent-primary) 25%, transparent) 25%, transparent 35%, transparent 50%)` ,
97+ backgroundSize : '200%' ,
98+ } }
99+ />
100+ </ div >
101+ < div className = "absolute inset-[1.5px] rounded-[18.5px] bg-[color-mix(in_srgb,var(--background-muted)_95%,transparent)] transition-colors duration-300 group-hover:bg-[color-mix(in_srgb,var(--background-muted)_80%,transparent)] pointer-events-none" />
102+ </ >
103+ ) }
104+ < div className = "relative flex-1 min-w-0" >
105+ < p
106+ className = "text-sm font-medium transition-colors duration-300"
107+ style = { {
108+ color : selected ? 'var(--accent-primary)' : '#f8fafc' ,
109+ } }
110+ >
111+ { option . name }
112+ </ p >
113+ </ div >
114+ </ div >
115+ ) }
116+ </ ListboxOption >
117+ ) ) }
118+ </ div >
119+ </ ListboxOptions >
120+ </ Transition > ,
121+ document . body
122+ ) }
123+ </ >
124+ ) }
125+ </ Listbox >
126+ ) ;
127+ }
128+
25129export default function CreateCardModal ( {
26130 isOpen,
27131 onClose,
@@ -337,38 +441,28 @@ export default function CreateCardModal({
337441 < div className = "grid grid-cols-2 gap-4" >
338442 < div >
339443 < label htmlFor = "card-type" className = "block text-xs font-medium mb-1" > Type</ label >
340- < select
444+ < DropdownSelect
341445 id = "card-type"
342446 value = { type }
343- onChange = { ( e ) => {
344- const next = e . target . value as typeof type ;
447+ onChange = { ( val ) => {
448+ const next = val as typeof type ;
345449 setType ( next ) ;
346450 setContent ( '' ) ;
347451 resetFileState ( ) ;
348452 } }
349- className = "w-full rounded-lg border border-[color-mix(in_srgb,var(--card-border)_80%,transparent)] px-3 py-2 bg-[color-mix(in_srgb,var(--surface-card)_90%,transparent)] text-(--text-strong) focus:outline-none focus:ring-2 focus:ring-[color-mix(in_srgb,var(--accent-primary)_50%,transparent)] [&>option]:bg-(--surface-card) [&>option]:text-(--text-strong)"
350- >
351- { [ 'bookmark' , 'snippet' , 'command' , 'doc' , 'image' , 'note' ] . map ( ( t ) => (
352- < option key = { t } value = { t } >
353- { t }
354- </ option >
355- ) ) }
356- </ select >
453+ options = { [ 'bookmark' , 'snippet' , 'command' , 'doc' , 'image' , 'note' ] . map ( t => ( { id : t , name : t . charAt ( 0 ) . toUpperCase ( ) + t . slice ( 1 ) } ) ) }
454+ />
357455 </ div >
358456 < div >
359457 < div className = "flex items-center justify-between mb-1" >
360458 < label htmlFor = "card-category" className = "text-xs font-medium" > Category</ label >
361459 </ div >
362- < select
460+ < DropdownSelect
363461 id = "card-category"
364462 value = { categoryId }
365- onChange = { ( e ) => setCategoryId ( e . target . value ) }
366- className = "w-full rounded-lg border border-[color-mix(in_srgb,var(--card-border)_80%,transparent)] px-3 py-2 bg-[color-mix(in_srgb,var(--surface-card)_90%,transparent)] text-(--text-strong) focus:outline-none focus:ring-2 focus:ring-[color-mix(in_srgb,var(--accent-primary)_50%,transparent)] [&>option]:bg-(--surface-card) [&>option]:text-(--text-strong)"
367- >
368- { categories . map ( ( c ) => (
369- < option key = { c . id } value = { c . id } > { c . name } </ option >
370- ) ) }
371- </ select >
463+ onChange = { setCategoryId }
464+ options = { categories }
465+ />
372466 </ div >
373467 </ div >
374468
@@ -397,16 +491,12 @@ export default function CreateCardModal({
397491 { ( type === 'snippet' || type === 'command' ) && (
398492 < div >
399493 < label htmlFor = "card-language" className = "block text-xs font-medium mb-1" > Language</ label >
400- < select
494+ < DropdownSelect
401495 id = "card-language"
402496 value = { language }
403- onChange = { ( e ) => setLanguage ( e . target . value ) }
404- className = "w-full rounded-lg border border-[color-mix(in_srgb,var(--card-border)_80%,transparent)] px-3 py-2 bg-[color-mix(in_srgb,var(--surface-card)_90%,transparent)] text-(--text-strong) focus:outline-none focus:ring-2 focus:ring-[color-mix(in_srgb,var(--accent-primary)_50%,transparent)] [&>option]:bg-(--surface-card) [&>option]:text-(--text-strong)"
405- >
406- { LANGUAGE_OPTIONS . map ( ( lang ) => (
407- < option key = { lang . value } value = { lang . value } > { lang . label } </ option >
408- ) ) }
409- </ select >
497+ onChange = { setLanguage }
498+ options = { LANGUAGE_OPTIONS . map ( lang => ( { id : lang . value , name : lang . label } ) ) }
499+ />
410500 </ div >
411501 ) }
412502
0 commit comments