@design.estate/dees-catalog 3.55.6 → 3.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "3.55.6",
3
+ "version": "3.56.1",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '3.55.6',
6
+ version: '3.56.1',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -0,0 +1,374 @@
1
+ import {
2
+ customElement,
3
+ type TemplateResult,
4
+ property,
5
+ state,
6
+ html,
7
+ css,
8
+ cssManager,
9
+ DeesElement,
10
+ } from '@design.estate/dees-element';
11
+ import * as domtools from '@design.estate/dees-domtools';
12
+ import { zIndexRegistry } from '../../00zindex.js';
13
+ import { cssGeistFontFamily } from '../../00fonts.js';
14
+ import { themeDefaultStyles } from '../../00theme.js';
15
+ import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
16
+
17
+ declare global {
18
+ interface HTMLElementTagNameMap {
19
+ 'dees-input-dropdown-popup': DeesInputDropdownPopup;
20
+ }
21
+ }
22
+
23
+ @customElement('dees-input-dropdown-popup')
24
+ export class DeesInputDropdownPopup extends DeesElement {
25
+ @property({ type: Array })
26
+ accessor options: { option: string; key: string; payload?: any }[] = [];
27
+
28
+ @property({ type: Boolean })
29
+ accessor enableSearch: boolean = true;
30
+
31
+ @property({ type: Boolean })
32
+ accessor opensToTop: boolean = false;
33
+
34
+ @property({ attribute: false })
35
+ accessor triggerRect: DOMRect | null = null;
36
+
37
+ @property({ attribute: false })
38
+ accessor ownerComponent: HTMLElement | null = null;
39
+
40
+ @state()
41
+ accessor filteredOptions: { option: string; key: string; payload?: any }[] = [];
42
+
43
+ @state()
44
+ accessor highlightedIndex: number = 0;
45
+
46
+ @state()
47
+ accessor searchValue: string = '';
48
+
49
+ @state()
50
+ accessor menuZIndex: number = 1000;
51
+
52
+ @state()
53
+ accessor visible: boolean = false;
54
+
55
+ private windowLayer: DeesWindowLayer | null = null;
56
+ private isDestroying: boolean = false;
57
+
58
+ public static styles = [
59
+ themeDefaultStyles,
60
+ cssManager.defaultStyles,
61
+ css`
62
+ :host {
63
+ position: fixed;
64
+ top: 0;
65
+ left: 0;
66
+ width: 0;
67
+ height: 0;
68
+ pointer-events: none;
69
+ font-family: ${cssGeistFontFamily};
70
+ color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
71
+ }
72
+
73
+ * {
74
+ box-sizing: border-box;
75
+ }
76
+
77
+ .selectionBox {
78
+ position: fixed;
79
+ pointer-events: auto;
80
+ will-change: transform, opacity;
81
+ transition: all 0.15s ease;
82
+ opacity: 0;
83
+ transform: translateY(-8px) scale(0.98);
84
+ background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
85
+ border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
86
+ box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
87
+ min-height: 40px;
88
+ max-height: 300px;
89
+ overflow: hidden;
90
+ border-radius: 6px;
91
+ user-select: none;
92
+ }
93
+
94
+ .selectionBox.top {
95
+ transform: translateY(8px) scale(0.98);
96
+ }
97
+
98
+ .selectionBox.show {
99
+ pointer-events: auto;
100
+ transform: translateY(0) scale(1);
101
+ opacity: 1;
102
+ }
103
+
104
+ .options-container {
105
+ max-height: 250px;
106
+ overflow-y: auto;
107
+ padding: 4px;
108
+ }
109
+
110
+ .option {
111
+ transition: all 0.15s ease;
112
+ line-height: 32px;
113
+ padding: 0 8px;
114
+ border-radius: 4px;
115
+ margin: 2px 0;
116
+ cursor: pointer;
117
+ font-size: 14px;
118
+ color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
119
+ }
120
+
121
+ .option.highlighted {
122
+ background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
123
+ }
124
+
125
+ .option:hover {
126
+ background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
127
+ color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
128
+ }
129
+
130
+ .no-options {
131
+ padding: 8px;
132
+ text-align: center;
133
+ font-size: 14px;
134
+ color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
135
+ font-style: italic;
136
+ }
137
+
138
+ .search {
139
+ padding: 4px;
140
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
141
+ margin-bottom: 4px;
142
+ }
143
+
144
+ .search input {
145
+ display: block;
146
+ width: 100%;
147
+ height: 32px;
148
+ padding: 0 8px;
149
+ background: transparent;
150
+ border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
151
+ border-radius: 4px;
152
+ color: inherit;
153
+ font-size: 14px;
154
+ font-family: inherit;
155
+ outline: none;
156
+ transition: border-color 0.15s ease;
157
+ }
158
+
159
+ .search input::placeholder {
160
+ color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
161
+ }
162
+
163
+ .search input:focus {
164
+ border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
165
+ }
166
+
167
+ .options-container::-webkit-scrollbar {
168
+ width: 8px;
169
+ }
170
+
171
+ .options-container::-webkit-scrollbar-track {
172
+ background: transparent;
173
+ }
174
+
175
+ .options-container::-webkit-scrollbar-thumb {
176
+ background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
177
+ border-radius: 4px;
178
+ }
179
+
180
+ .options-container::-webkit-scrollbar-thumb:hover {
181
+ background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
182
+ }
183
+ `,
184
+ ];
185
+
186
+ public render(): TemplateResult {
187
+ if (!this.triggerRect) return html``;
188
+
189
+ const posStyle = this.computePositionStyle();
190
+
191
+ return html`
192
+ <div
193
+ class="selectionBox ${this.visible ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}"
194
+ style="${posStyle}; z-index: ${this.menuZIndex};"
195
+ >
196
+ ${this.enableSearch
197
+ ? html`
198
+ <div class="search">
199
+ <input
200
+ type="text"
201
+ placeholder="Search options..."
202
+ .value="${this.searchValue}"
203
+ @input="${this.handleSearch}"
204
+ @click="${(e: Event) => e.stopPropagation()}"
205
+ @keydown="${this.handleSearchKeydown}"
206
+ />
207
+ </div>
208
+ `
209
+ : null}
210
+ <div class="options-container">
211
+ ${this.filteredOptions.length === 0
212
+ ? html`<div class="no-options">No options found</div>`
213
+ : this.filteredOptions.map((option, index) => {
214
+ const isHighlighted = this.highlightedIndex === index;
215
+ return html`
216
+ <div
217
+ class="option ${isHighlighted ? 'highlighted' : ''}"
218
+ @click="${() => this.selectOption(option)}"
219
+ @mouseenter="${() => (this.highlightedIndex = index)}"
220
+ >
221
+ ${option.option}
222
+ </div>
223
+ `;
224
+ })}
225
+ </div>
226
+ </div>
227
+ `;
228
+ }
229
+
230
+ private computePositionStyle(): string {
231
+ const rect = this.triggerRect!;
232
+ const left = rect.left;
233
+ const width = rect.width;
234
+
235
+ if (this.opensToTop) {
236
+ const bottom = window.innerHeight - rect.top + 4;
237
+ return `left: ${left}px; width: ${width}px; bottom: ${bottom}px; top: auto`;
238
+ } else {
239
+ const top = rect.bottom + 4;
240
+ return `left: ${left}px; width: ${width}px; top: ${top}px`;
241
+ }
242
+ }
243
+
244
+ public async show(): Promise<void> {
245
+ this.filteredOptions = this.options;
246
+ this.highlightedIndex = 0;
247
+ this.searchValue = '';
248
+
249
+ // Create window layer (transparent, no blur)
250
+ this.windowLayer = await DeesWindowLayer.createAndShow();
251
+ this.windowLayer.addEventListener('click', () => {
252
+ this.dispatchEvent(new CustomEvent('close-request'));
253
+ });
254
+
255
+ // Set z-index above the window layer
256
+ this.menuZIndex = zIndexRegistry.getNextZIndex();
257
+ zIndexRegistry.register(this, this.menuZIndex);
258
+ this.style.zIndex = this.menuZIndex.toString();
259
+
260
+ document.body.appendChild(this);
261
+
262
+ // Animate in on next frame
263
+ await domtools.plugins.smartdelay.delayFor(0);
264
+ this.visible = true;
265
+
266
+ // Add scroll/resize listeners for repositioning
267
+ window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
268
+ window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
269
+ }
270
+
271
+ public async hide(): Promise<void> {
272
+ // Guard against double-destruction
273
+ if (this.isDestroying) {
274
+ return;
275
+ }
276
+ this.isDestroying = true;
277
+
278
+ // Remove scroll/resize listeners
279
+ window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
280
+ window.removeEventListener('resize', this.handleScrollOrResize);
281
+
282
+ zIndexRegistry.unregister(this);
283
+
284
+ this.searchValue = '';
285
+ this.filteredOptions = this.options;
286
+ this.highlightedIndex = 0;
287
+
288
+ // Don't await - let window layer cleanup happen in background for instant visual feedback
289
+ if (this.windowLayer) {
290
+ this.windowLayer.destroy();
291
+ this.windowLayer = null;
292
+ }
293
+
294
+ // Animate out via CSS transition
295
+ this.visible = false;
296
+ await domtools.plugins.smartdelay.delayFor(150);
297
+
298
+ if (this.parentElement) {
299
+ this.parentElement.removeChild(this);
300
+ }
301
+
302
+ this.isDestroying = false;
303
+ }
304
+
305
+ public async focusSearchInput(): Promise<void> {
306
+ await this.updateComplete;
307
+ const input = this.shadowRoot!.querySelector('.search input') as HTMLInputElement;
308
+ if (input) input.focus();
309
+ }
310
+
311
+ public updateOptions(options: { option: string; key: string; payload?: any }[]): void {
312
+ this.options = options;
313
+ // Re-filter with current search value
314
+ if (this.searchValue) {
315
+ const searchLower = this.searchValue.toLowerCase();
316
+ this.filteredOptions = this.options.filter((opt) =>
317
+ opt.option.toLowerCase().includes(searchLower)
318
+ );
319
+ } else {
320
+ this.filteredOptions = this.options;
321
+ }
322
+ this.highlightedIndex = 0;
323
+ }
324
+
325
+ private selectOption(option: { option: string; key: string; payload?: any }): void {
326
+ this.dispatchEvent(
327
+ new CustomEvent('option-selected', {
328
+ detail: option,
329
+ })
330
+ );
331
+ }
332
+
333
+ private handleSearch = (event: Event): void => {
334
+ const searchTerm = (event.target as HTMLInputElement).value;
335
+ this.searchValue = searchTerm;
336
+ const searchLower = searchTerm.toLowerCase();
337
+ this.filteredOptions = this.options.filter((option) =>
338
+ option.option.toLowerCase().includes(searchLower)
339
+ );
340
+ this.highlightedIndex = 0;
341
+ };
342
+
343
+ private handleSearchKeydown = (event: KeyboardEvent): void => {
344
+ const key = event.key;
345
+ const maxIndex = this.filteredOptions.length - 1;
346
+
347
+ if (key === 'ArrowDown') {
348
+ event.preventDefault();
349
+ this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
350
+ } else if (key === 'ArrowUp') {
351
+ event.preventDefault();
352
+ this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
353
+ } else if (key === 'Enter') {
354
+ event.preventDefault();
355
+ if (this.filteredOptions[this.highlightedIndex]) {
356
+ this.selectOption(this.filteredOptions[this.highlightedIndex]);
357
+ }
358
+ } else if (key === 'Escape') {
359
+ event.preventDefault();
360
+ this.dispatchEvent(new CustomEvent('close-request'));
361
+ }
362
+ };
363
+
364
+ private handleScrollOrResize = (): void => {
365
+ this.dispatchEvent(new CustomEvent('reposition-request'));
366
+ };
367
+
368
+ async disconnectedCallback() {
369
+ await super.disconnectedCallback();
370
+ window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
371
+ window.removeEventListener('resize', this.handleScrollOrResize);
372
+ zIndexRegistry.unregister(this);
373
+ }
374
+ }