1+ <script >
2+ import { createEventDispatcher } from ' svelte' ;
3+
4+ // schema: one entry from formSchema.ts, or null/undefined if this
5+ // model has no schema yet (see the 'vehicle_ride_sharing' gap note
6+ // there) — Details renders a small fallback instead of guessing.
7+ export let schema = null ;
8+
9+ // values: the field state, lifted and owned by the parent (HexMenu),
10+ // same pattern as Anypay's `selected` — Details never holds its own
11+ // copy, it only dispatches what changed and lets the parent decide
12+ // what to do with it.
13+ export let values = {};
14+
15+ const dispatch = createEventDispatcher ();
16+
17+ const MODE_OPTIONS = [
18+ { id: ' in_person' , label: ' In-Person' },
19+ { id: ' online' , label: ' Online' },
20+ { id: ' both' , label: ' Both' },
21+ ];
22+
23+ function set (key , value ) {
24+ dispatch (' update' , { key, value });
25+ }
26+
27+ function pickCategory (id ) {
28+ if (schema .category .multi ) {
29+ const cur = values .categoryIds || [];
30+ const next = cur .includes (id) ? cur .filter (x => x !== id) : [... cur, id];
31+ set (' categoryIds' , next);
32+ } else {
33+ set (' categoryId' , id);
34+ }
35+ }
36+
37+ function isCategorySelected (id ) {
38+ return schema .category .multi
39+ ? (values .categoryIds || []).includes (id)
40+ : values .categoryId === id;
41+ }
42+
43+ function onBackdrop (e ) {
44+ if (e .target === e .currentTarget ) dispatch (' close' );
45+ }
46+ </script >
47+
48+ <div class ="backdrop" on:click ={onBackdrop }>
49+ <div class =" modal" role =" dialog" aria-modal =" true" aria-label =" Details" >
50+ <button class ="close" on:click ={() => dispatch (' close' )} aria-label =" Close" >✕</button >
51+
52+ {#if ! schema }
53+ <h2 >DETAILS</h2 >
54+ <p class =" hint" >No fields defined for this model yet.</p >
55+ {:else }
56+ <h2 >DETAILS</h2 >
57+ <p class =" hint" >Fill in what applies, then close with ✕.</p >
58+
59+ {#if schema .modeSelector }
60+ <div class =" mode-row" >
61+ {#each MODE_OPTIONS as m }
62+ <button
63+ class =" mode-opt"
64+ class:selected ={(values .mode || ' in_person' ) === m .id }
65+ on:click ={() => set (' mode' , m .id )}
66+ >{m .label }</button >
67+ {/each }
68+ </div >
69+ {/if }
70+
71+ <label class ="field-label" for ="details-title" >{schema .titleLabel } *</label >
72+ <input
73+ id =" details-title"
74+ class =" text-input"
75+ placeholder ={schema .titlePlaceholder }
76+ value ={values .title || ' ' }
77+ on:input ={(e ) => set (' title' , e .currentTarget .value )}
78+ />
79+
80+ <span class =" field-label" >Category *</span >
81+ <div class =" options" >
82+ {#each schema .category .options as opt }
83+ <button
84+ class =" option"
85+ class:selected ={isCategorySelected (opt .id )}
86+ on:click ={() => pickCategory (opt .id )}
87+ >
88+ <span class ="opt-name" >{opt .name }</span >
89+ {#if opt .description }<span class ="opt-desc" >{opt .description }</span >{/if }
90+ </button >
91+ {/each }
92+ </div >
93+
94+ {#if schema .date }
95+ <label class =" field-label" for =" details-date" >
96+ {schema .date .label || ' Date & Time' }{schema .date .required ? ' *' : ' ' }
97+ </label >
98+ <input
99+ id =" details-date"
100+ class =" text-input"
101+ type =" datetime-local"
102+ value ={values .date || ' ' }
103+ on:input ={(e ) => set (' date' , e .currentTarget .value )}
104+ />
105+ {#if ! schema .date .required }
106+ <p class =" subtle" >Optional — leave empty for recurring or open-ended.</p >
107+ {/if }
108+ {/if }
109+
110+ <label class =" field-label" for =" details-description" >Description *</label >
111+ <textarea
112+ id =" details-description"
113+ class =" textarea"
114+ placeholder ={schema .descriptionPlaceholder }
115+ value ={values .description || ' ' }
116+ on:input ={(e ) => set (' description' , e .currentTarget .value )}
117+ ></textarea >
118+
119+ <label class =" field-label" for =" details-contact" >Contact Link *</label >
120+ <input
121+ id =" details-contact"
122+ class =" text-input"
123+ placeholder =" https://t.me/you or https://wa.me/123..."
124+ value ={values .contact || ' ' }
125+ on:input ={(e ) => set (' contact' , e .currentTarget .value )}
126+ />
127+ <p class ="subtle" >{schema .contactHint }</p >
128+ {/if }
129+ </div >
130+ </div >
131+
132+ <style >
133+ .backdrop {
134+ position : fixed ;
135+ inset : 0 ;
136+ background : rgba (0 , 0 , 0 , 0.6 );
137+ display : flex ;
138+ align-items : center ;
139+ justify-content : center ;
140+ z-index : 50 ;
141+ padding : 24px 0 ;
142+ }
143+
144+ .modal {
145+ position : relative ;
146+ background : #161616 ;
147+ border : 1px solid #333 ;
148+ border-radius : 14px ;
149+ padding : 28px 24px 24px ;
150+ width : min (360px , calc (100vw - 40px ));
151+ max-height : calc (100vh - 48px );
152+ overflow-y : auto ;
153+ box-shadow : 0 20px 60px rgba (0 , 0 , 0 , 0.5 );
154+ font-family : -apple-system , BlinkMacSystemFont, ' Segoe UI' , sans-serif ;
155+ }
156+
157+ h2 {
158+ margin : 0 0 6px ;
159+ color : #fff ;
160+ font-size : 1em ;
161+ letter-spacing : 1.5px ;
162+ text-align : center ;
163+ }
164+
165+ .hint {
166+ margin : 0 0 18px ;
167+ color : #888 ;
168+ font-size : 0.78em ;
169+ text-align : center ;
170+ }
171+
172+ .close {
173+ position : absolute ;
174+ top : 10px ;
175+ right : 10px ;
176+ background : none ;
177+ border : none ;
178+ color : #888 ;
179+ font-size : 1em ;
180+ cursor : pointer ;
181+ line-height : 1 ;
182+ padding : 6px ;
183+ }
184+ .close :hover { color : #fff ; }
185+
186+ .mode-row {
187+ display : flex ;
188+ gap : 6px ;
189+ margin-bottom : 18px ;
190+ }
191+ .mode-opt {
192+ flex : 1 ;
193+ border : 1.5px solid #333 ;
194+ background : #111 ;
195+ color : #aaa ;
196+ border-radius : 8px ;
197+ padding : 8px 4px ;
198+ font-size : 0.78em ;
199+ font-weight : 600 ;
200+ cursor : pointer ;
201+ }
202+ .mode-opt.selected {
203+ background : rgba (51 , 91 , 244 , 0.38 );
204+ border-color : #8fb0ff ;
205+ color : #fff ;
206+ }
207+
208+ .field-label {
209+ display : block ;
210+ margin : 14px 0 6px ;
211+ color : #ccc ;
212+ font-size : 0.8em ;
213+ font-weight : 600 ;
214+ }
215+
216+ .text-input , .textarea {
217+ width : 100% ;
218+ box-sizing : border-box ;
219+ background : #0d0d0d ;
220+ border : 1px solid #333 ;
221+ border-radius : 8px ;
222+ padding : 10px 12px ;
223+ color : #fff ;
224+ font-size : 0.9em ;
225+ font-family : inherit ;
226+ }
227+ .text-input ::placeholder , .textarea ::placeholder { color : #666 ; }
228+ .textarea { min-height : 80px ; resize : vertical ; }
229+
230+ .subtle {
231+ margin : 6px 0 0 ;
232+ color : #777 ;
233+ font-size : 0.72em ;
234+ }
235+
236+ .options {
237+ display : flex ;
238+ flex-direction : column ;
239+ gap : 8px ;
240+ }
241+
242+ .option {
243+ display : flex ;
244+ flex-direction : column ;
245+ align-items : flex-start ;
246+ gap : 2px ;
247+ border : 1.5px solid ;
248+ border-image : linear-gradient (90deg , #335bf4 , #2ae9c9 ) 1 ;
249+ background : #111 ;
250+ color : #fff ;
251+ border-radius : 10px ;
252+ padding : 10px 12px ;
253+ text-align : left ;
254+ cursor : pointer ;
255+ transition : background 0.2s ;
256+ }
257+ .option.selected {
258+ background : rgba (51 , 91 , 244 , 0.38 );
259+ border-image : none ;
260+ border-color : #8fb0ff ;
261+ }
262+ .opt-name {
263+ font-size : 0.9em ;
264+ font-weight : 600 ;
265+ letter-spacing : 0.3px ;
266+ }
267+ .opt-desc {
268+ font-size : 0.74em ;
269+ color : #9aa4b2 ;
270+ }
271+ </style >
0 commit comments