@liwe3/webcomponents 1.0.2 → 1.1.0

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.
Files changed (52) hide show
  1. package/dist/AITextEditor.d.ts +173 -0
  2. package/dist/AITextEditor.d.ts.map +1 -0
  3. package/dist/ChunkUploader.d.ts +103 -0
  4. package/dist/ChunkUploader.d.ts.map +1 -0
  5. package/dist/ChunkUploader.js +614 -0
  6. package/dist/ChunkUploader.js.map +1 -0
  7. package/dist/ContainerBox.d.ts +112 -0
  8. package/dist/ContainerBox.d.ts.map +1 -0
  9. package/dist/ContainerBox.js +359 -0
  10. package/dist/ContainerBox.js.map +1 -0
  11. package/dist/DateSelector.d.ts +103 -0
  12. package/dist/DateSelector.d.ts.map +1 -0
  13. package/dist/DateSelector.js +372 -0
  14. package/dist/DateSelector.js.map +1 -0
  15. package/dist/Drawer.d.ts +63 -0
  16. package/dist/Drawer.d.ts.map +1 -0
  17. package/dist/Drawer.js +340 -0
  18. package/dist/Drawer.js.map +1 -0
  19. package/dist/ImageView.d.ts +42 -0
  20. package/dist/ImageView.d.ts.map +1 -0
  21. package/dist/ImageView.js +209 -0
  22. package/dist/ImageView.js.map +1 -0
  23. package/dist/PopoverMenu.d.ts +103 -0
  24. package/dist/PopoverMenu.d.ts.map +1 -0
  25. package/dist/PopoverMenu.js +312 -0
  26. package/dist/PopoverMenu.js.map +1 -0
  27. package/dist/SmartSelect.d.ts +99 -0
  28. package/dist/SmartSelect.d.ts.map +1 -0
  29. package/dist/SmartSelect.js.map +1 -1
  30. package/dist/Toast.d.ts +127 -0
  31. package/dist/Toast.d.ts.map +1 -0
  32. package/dist/Toast.js +507 -0
  33. package/dist/Toast.js.map +1 -0
  34. package/dist/TreeView.d.ts +84 -0
  35. package/dist/TreeView.d.ts.map +1 -0
  36. package/dist/TreeView.js +478 -0
  37. package/dist/TreeView.js.map +1 -0
  38. package/dist/index.d.ts +16 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +30 -6
  41. package/dist/index.js.map +1 -1
  42. package/package.json +43 -3
  43. package/src/ChunkUploader.ts +921 -0
  44. package/src/ContainerBox.ts +570 -0
  45. package/src/DateSelector.ts +550 -0
  46. package/src/Drawer.ts +435 -0
  47. package/src/ImageView.ts +265 -0
  48. package/src/PopoverMenu.ts +595 -0
  49. package/src/SmartSelect.ts +231 -231
  50. package/src/Toast.ts +834 -0
  51. package/src/TreeView.ts +673 -0
  52. package/src/index.ts +70 -3
@@ -0,0 +1,673 @@
1
+ /**
2
+ * TreeView Web Component
3
+ * A customizable tree view with infinite depth, checkboxes, and folder icons
4
+ */
5
+
6
+ export type TreeNode = {
7
+ id: string;
8
+ label: string;
9
+ children?: TreeNode[];
10
+ icon?: string;
11
+ customIcon?: string;
12
+ selected?: boolean;
13
+ expanded?: boolean;
14
+ };
15
+
16
+ export class TreeViewElement extends HTMLElement {
17
+ declare shadowRoot: ShadowRoot;
18
+ private treeData: TreeNode[] = [];
19
+ private selectedIds: Set<string> = new Set();
20
+ private expandedIds: Set<string> = new Set();
21
+ private indentWidth: number = 20;
22
+ private showBorder = true;
23
+
24
+ constructor () {
25
+ super();
26
+ this.attachShadow( { mode: 'open' } );
27
+ this.render();
28
+ this.bindEvents();
29
+ }
30
+
31
+ static get observedAttributes (): string[] {
32
+ return [ 'data', 'indent-width', 'selected-ids', 'show-border' ];
33
+ }
34
+
35
+ attributeChangedCallback ( name: string, oldValue: string | null, newValue: string | null ): void {
36
+ if ( oldValue !== newValue ) {
37
+ if ( name === 'indent-width' ) {
38
+ this.indentWidth = parseInt( newValue || '20', 10 );
39
+ }
40
+
41
+ if ( name === 'show-border' ) {
42
+ this.showBorder = newValue !== 'false';
43
+ }
44
+ this.render();
45
+ }
46
+ }
47
+
48
+ get data (): TreeNode[] {
49
+ const dataAttr = this.getAttribute( 'data' );
50
+ if ( dataAttr ) {
51
+ try {
52
+ return JSON.parse( dataAttr );
53
+ } catch ( e ) {
54
+ console.error( 'Invalid data format:', e );
55
+ return [];
56
+ }
57
+ }
58
+ return this.treeData;
59
+ }
60
+
61
+ set data ( value: TreeNode[] ) {
62
+ this.treeData = value;
63
+ this.initializeExpandedState( value );
64
+ this.setAttribute( 'data', JSON.stringify( value ) );
65
+ }
66
+
67
+ get selectedNodeIds (): string[] {
68
+ return Array.from( this.selectedIds );
69
+ }
70
+
71
+ set selectedNodeIds ( ids: string[] ) {
72
+ this.selectedIds = new Set( ids );
73
+ this.render();
74
+ }
75
+
76
+ get showBorderEnabled (): boolean {
77
+ return this.showBorder;
78
+ }
79
+
80
+ set showBorderEnabled ( value: boolean ) {
81
+ this.showBorder = value;
82
+ this.setAttribute( 'show-border', value.toString() );
83
+ this.render();
84
+ }
85
+
86
+ /**
87
+ * Initialize expanded state from the data
88
+ */
89
+ private initializeExpandedState ( nodes: TreeNode[] ): void {
90
+ const traverse = ( node: TreeNode ) => {
91
+ if ( node.expanded ) {
92
+ this.expandedIds.add( node.id );
93
+ }
94
+ if ( node.children ) {
95
+ node.children.forEach( traverse );
96
+ }
97
+ };
98
+
99
+ nodes.forEach( traverse );
100
+ }
101
+
102
+ /**
103
+ * Toggle node expansion
104
+ */
105
+ toggleExpansion ( nodeId: string ): void {
106
+ // Update DOM directly instead of full re-render
107
+ const nodeElement = this.shadowRoot.querySelector( `.tree-node[data-node-id="${ nodeId }"]` ) as HTMLElement;
108
+ if ( !nodeElement ) return;
109
+
110
+ // Find the direct child .node-children element (not nested ones)
111
+ const childrenContainer = Array.from( nodeElement.children ).find(
112
+ el => el.classList.contains( 'node-children' )
113
+ ) as HTMLElement | undefined;
114
+
115
+ // Determine current state from DOM, not from expandedIds
116
+ const hasChildrenInDom = !!childrenContainer;
117
+
118
+ if ( hasChildrenInDom ) {
119
+ // Currently expanded - collapse it
120
+ childrenContainer.remove();
121
+ this.expandedIds.delete( nodeId );
122
+
123
+ // Toggle expand icon rotation
124
+ const expandIcon = nodeElement.querySelector( '.expand-icon' ) as HTMLElement;
125
+ if ( expandIcon ) {
126
+ expandIcon.classList.remove( 'expanded' );
127
+ }
128
+
129
+ this.dispatchEvent( new CustomEvent( 'toggle', {
130
+ detail: { nodeId, expanded: false }
131
+ } ) );
132
+ } else {
133
+ // Currently collapsed - expand it
134
+ const node = this.findNode( nodeId );
135
+ if ( node && node.children ) {
136
+ const depth = parseInt( nodeElement.dataset.depth || '0', 10 );
137
+ const childrenHtml = `
138
+ <div class="node-children">
139
+ ${ node.children.map( child => this.renderNode( child, depth + 1 ) ).join( '' ) }
140
+ </div>
141
+ `;
142
+ nodeElement.insertAdjacentHTML( 'beforeend', childrenHtml );
143
+ this.expandedIds.add( nodeId );
144
+
145
+ // Toggle expand icon rotation
146
+ const expandIcon = nodeElement.querySelector( '.expand-icon' ) as HTMLElement;
147
+ if ( expandIcon ) {
148
+ expandIcon.classList.add( 'expanded' );
149
+ }
150
+
151
+ this.dispatchEvent( new CustomEvent( 'toggle', {
152
+ detail: { nodeId, expanded: true }
153
+ } ) );
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Toggle node selection
160
+ */
161
+ toggleSelection ( nodeId: string ): void {
162
+ const isSelected = this.selectedIds.has( nodeId );
163
+
164
+ if ( isSelected ) {
165
+ this.selectedIds.delete( nodeId );
166
+ } else {
167
+ this.selectedIds.add( nodeId );
168
+ }
169
+
170
+ // Update checkbox state directly instead of full re-render
171
+ const checkbox = this.shadowRoot.querySelector( `.node-checkbox[data-node-id="${ nodeId }"]` ) as HTMLInputElement;
172
+ if ( checkbox ) {
173
+ checkbox.checked = !isSelected;
174
+ }
175
+
176
+ this.dispatchEvent( new CustomEvent( 'selectionchange', {
177
+ detail: { selectedIds: this.selectedNodeIds }
178
+ } ) );
179
+ }
180
+
181
+ /**
182
+ * Select all nodes recursively
183
+ */
184
+ selectAll (): void {
185
+ const traverse = ( node: TreeNode ) => {
186
+ this.selectedIds.add( node.id );
187
+ if ( node.children ) {
188
+ node.children.forEach( traverse );
189
+ }
190
+ };
191
+
192
+ this.data.forEach( traverse );
193
+ this.render();
194
+ this.dispatchEvent( new CustomEvent( 'selectionchange', {
195
+ detail: { selectedIds: this.selectedNodeIds }
196
+ } ) );
197
+ }
198
+
199
+ /**
200
+ * Deselect all nodes
201
+ */
202
+ deselectAll (): void {
203
+ this.selectedIds.clear();
204
+ this.render();
205
+ this.dispatchEvent( new CustomEvent( 'selectionchange', {
206
+ detail: { selectedIds: this.selectedNodeIds }
207
+ } ) );
208
+ }
209
+
210
+ /**
211
+ * Expand all nodes
212
+ */
213
+ expandAll (): void {
214
+ const traverse = ( node: TreeNode ) => {
215
+ if ( node.children && node.children.length > 0 ) {
216
+ this.expandedIds.add( node.id );
217
+ node.children.forEach( traverse );
218
+ }
219
+ };
220
+
221
+ this.data.forEach( traverse );
222
+ this.render();
223
+ }
224
+
225
+ /**
226
+ * Collapse all nodes
227
+ */
228
+ collapseAll (): void {
229
+ this.expandedIds.clear();
230
+ this.render();
231
+ }
232
+
233
+ /**
234
+ * Get node by ID
235
+ */
236
+ private findNode ( nodeId: string, nodes: TreeNode[] = this.data ): TreeNode | null {
237
+ for ( const node of nodes ) {
238
+ if ( node.id === nodeId ) {
239
+ return node;
240
+ }
241
+ if ( node.children ) {
242
+ const found = this.findNode( nodeId, node.children );
243
+ if ( found ) return found;
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+
249
+ /**
250
+ * Get the default folder icon
251
+ */
252
+ private getDefaultIcon ( node: TreeNode, isExpanded: boolean ): string {
253
+ if ( node.children && node.children.length > 0 ) {
254
+ return isExpanded ? '📂' : '📁';
255
+ }
256
+ return '📄';
257
+ }
258
+
259
+ /**
260
+ * Render a single tree node
261
+ */
262
+ private renderNode ( node: TreeNode, depth: number = 0 ): string {
263
+ const isExpanded = this.expandedIds.has( node.id );
264
+ const isSelected = this.selectedIds.has( node.id );
265
+ const hasChildren = node.children && node.children.length > 0;
266
+ const paddingLeft = depth * this.indentWidth;
267
+
268
+ // Determine icon to display
269
+ let iconHtml = '';
270
+ if ( node.customIcon ) {
271
+ iconHtml = `<span class="node-icon custom-icon">${ node.customIcon }</span>`;
272
+ } else if ( node.icon !== undefined && node.icon !== '' ) {
273
+ iconHtml = `<span class="node-icon">${ node.icon }</span>`;
274
+ } else if ( node.icon !== '' ) {
275
+ iconHtml = `<span class="node-icon">${ this.getDefaultIcon( node, isExpanded ) }</span>`;
276
+ }
277
+
278
+ // Render children if expanded
279
+ let childrenHtml = '';
280
+ if ( hasChildren && isExpanded ) {
281
+ childrenHtml = `<div class="node-children">${ node.children!.map( child => this.renderNode( child, depth + 1 ) ).join( '' ) }</div>`;
282
+ }
283
+
284
+ const nodeHtml = `
285
+ <div class="tree-node" data-node-id="${ node.id }" data-depth="${ depth }">
286
+ <div class="node-content" style="padding-left: ${ paddingLeft }px">
287
+ <div class="node-controls">
288
+ ${ hasChildren
289
+ ? `<button class="expand-toggle" data-node-id="${ node.id }" aria-label="${ isExpanded ? 'Collapse' : 'Expand' }">
290
+ <svg class="expand-icon ${ isExpanded ? 'expanded' : '' }" viewBox="0 0 24 24" width="16" height="16">
291
+ <path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
292
+ </svg>
293
+ </button>`
294
+ : '<span class="expand-spacer"></span>'
295
+ }
296
+ <label class="checkbox-wrapper">
297
+ <input
298
+ type="checkbox"
299
+ class="node-checkbox"
300
+ data-node-id="${ node.id }"
301
+ ${ isSelected ? 'checked' : '' }
302
+ >
303
+ <span class="checkbox-custom"></span>
304
+ </label>
305
+ </div>
306
+ ${ iconHtml }
307
+ <span class="node-label" data-node-id="${ node.id }">${ node.label }</span>
308
+ </div>
309
+ ${ childrenHtml }
310
+ </div>
311
+ `;
312
+
313
+ return nodeHtml;
314
+ }
315
+
316
+ /**
317
+ * Bind event listeners
318
+ */
319
+ private bindEvents (): void {
320
+ // Use event delegation for all interactive elements
321
+ this.shadowRoot.addEventListener( 'click', ( e ) => {
322
+ const target = e.target as HTMLElement;
323
+
324
+ // Handle expand/collapse toggle button
325
+ if ( target.closest( '.expand-toggle' ) ) {
326
+ const button = target.closest( '.expand-toggle' ) as HTMLElement;
327
+ const nodeId = button.dataset.nodeId;
328
+ if ( nodeId ) {
329
+ this.toggleExpansion( nodeId );
330
+ }
331
+ return;
332
+ }
333
+ } );
334
+
335
+ // Handle checkbox changes
336
+ this.shadowRoot.addEventListener( 'change', ( e ) => {
337
+ const target = e.target as HTMLInputElement;
338
+ if ( target.classList.contains( 'node-checkbox' ) ) {
339
+ const nodeId = target.dataset.nodeId;
340
+ if ( nodeId ) {
341
+ // Toggle current node selection
342
+ this.toggleSelection( nodeId );
343
+
344
+ // If this node is a folder, cascade selection state to all descendants
345
+ const node = this.findNode( nodeId );
346
+ if ( node && node.children && node.children.length > 0 ) {
347
+ const checked = !!target.checked;
348
+
349
+ const applyToDescendants = ( n: TreeNode ) => {
350
+ if ( n.children ) n.children.forEach( applyToDescendants );
351
+ // Skip the root node itself because it's already handled above
352
+ if ( n.id !== nodeId ) {
353
+ if ( checked ) this.selectedIds.add( n.id );
354
+ else this.selectedIds.delete( n.id );
355
+ }
356
+ };
357
+
358
+ applyToDescendants( node );
359
+
360
+ // Update any rendered descendant checkboxes in the DOM
361
+ const rootEl = this.shadowRoot.querySelector( `.tree-node[data-node-id="${ nodeId }"]` );
362
+ if ( rootEl ) {
363
+ rootEl.querySelectorAll<HTMLInputElement>( '.node-checkbox' ).forEach( ( cb ) => {
364
+ cb.checked = checked;
365
+ } );
366
+ }
367
+
368
+ this.dispatchEvent( new CustomEvent( 'selectionchange', {
369
+ detail: { selectedIds: this.selectedNodeIds }
370
+ } ) );
371
+ }
372
+ }
373
+ }
374
+ } );
375
+
376
+ // Handle double-click events
377
+ this.shadowRoot.addEventListener( 'dblclick', ( e ) => {
378
+ const target = e.target as HTMLElement;
379
+
380
+ // Ignore double-clicks that originate from the checkbox area
381
+ if ( target.closest( '.checkbox-wrapper' ) || target.classList.contains( 'node-checkbox' ) ) {
382
+ e.stopPropagation();
383
+ e.preventDefault();
384
+ return;
385
+ }
386
+
387
+ // Check if clicking on a node label or content
388
+ if ( target.closest( '.node-label' ) || target.closest( '.node-content' ) ) {
389
+ const nodeContent = target.closest( '.node-content' ) as HTMLElement;
390
+ if ( !nodeContent ) return;
391
+
392
+ const nodeElement = nodeContent.closest( '.tree-node' ) as HTMLElement;
393
+ if ( !nodeElement ) return;
394
+
395
+ const nodeId = nodeElement.dataset.nodeId;
396
+ if ( !nodeId ) return;
397
+
398
+ const node = this.findNode( nodeId );
399
+ if ( !node ) return;
400
+
401
+ // If it's a folder (has children), toggle expansion
402
+ if ( node.children && node.children.length > 0 ) {
403
+ this.toggleExpansion( nodeId );
404
+ } else {
405
+ // If it's an item (no children), fire itemselected event
406
+ this.dispatchEvent( new CustomEvent( 'itemselected', {
407
+ detail: { node, nodeId }
408
+ } ) );
409
+ }
410
+ }
411
+ } );
412
+ }
413
+
414
+ /**
415
+ * Render the component
416
+ */
417
+ private render (): void {
418
+ const containerClass = this.showBorder ? 'tree-container bordered' : 'tree-container borderless';
419
+
420
+ this.shadowRoot.innerHTML = `
421
+ <style>
422
+ :host {
423
+ display: block;
424
+ font-family: var(--tree-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
425
+ font-size: var(--tree-font-size, 14px);
426
+ color: var(--tree-text-color, #333);
427
+ background: var(--tree-background, transparent);
428
+ user-select: none;
429
+ }
430
+
431
+ .tree-container {
432
+ overflow: auto;
433
+ padding: var(--tree-padding, 8px);
434
+ border-radius: var(--tree-border-radius, 10px);
435
+ background: var(--tree-container-background, transparent);
436
+ border: none;
437
+ }
438
+
439
+ .tree-container.bordered {
440
+ border: var(--tree-border, 1px solid rgba(15, 23, 42, 0.12));
441
+ box-shadow: var(--tree-border-shadow, none);
442
+ }
443
+
444
+ .tree-container.borderless {
445
+ border: none;
446
+ }
447
+
448
+ .tree-node {
449
+ position: relative;
450
+ }
451
+
452
+ .node-content {
453
+ display: flex;
454
+ align-items: center;
455
+ gap: 6px;
456
+ padding: 4px 8px;
457
+ border-radius: var(--tree-node-border-radius, 6px);
458
+ transition: background-color 0.15s ease;
459
+ cursor: pointer;
460
+ }
461
+
462
+ .node-content:hover {
463
+ background-color: var(--tree-hover-background, rgba(0, 123, 255, 0.08));
464
+ }
465
+
466
+ .node-controls {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 4px;
470
+ }
471
+
472
+ .expand-toggle {
473
+ display: flex;
474
+ align-items: center;
475
+ justify-content: center;
476
+ width: 20px;
477
+ height: 20px;
478
+ padding: 0;
479
+ border: none;
480
+ background: transparent;
481
+ cursor: pointer;
482
+ border-radius: 4px;
483
+ transition: background-color 0.15s ease;
484
+ }
485
+
486
+ .expand-toggle:hover {
487
+ background-color: var(--tree-expand-hover-background, rgba(0, 0, 0, 0.1));
488
+ }
489
+
490
+ .expand-toggle:focus {
491
+ outline: 2px solid var(--tree-focus-color, #007bff);
492
+ outline-offset: 2px;
493
+ }
494
+
495
+ .expand-icon {
496
+ fill: var(--tree-icon-color, #666);
497
+ transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
498
+ }
499
+
500
+ .expand-icon.expanded {
501
+ transform: rotate(90deg);
502
+ }
503
+
504
+ .expand-spacer {
505
+ width: 20px;
506
+ height: 20px;
507
+ display: inline-block;
508
+ }
509
+
510
+ .checkbox-wrapper {
511
+ display: flex;
512
+ align-items: center;
513
+ cursor: pointer;
514
+ margin: 0;
515
+ }
516
+
517
+ .node-checkbox {
518
+ position: absolute;
519
+ opacity: 0;
520
+ width: 0;
521
+ height: 0;
522
+ }
523
+
524
+ .checkbox-custom {
525
+ position: relative;
526
+ width: 18px;
527
+ height: 18px;
528
+ border: 2px solid var(--tree-checkbox-border, #ccc);
529
+ border-radius: var(--tree-checkbox-border-radius, 4px);
530
+ background: var(--tree-checkbox-background, white);
531
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
532
+ }
533
+
534
+ .checkbox-custom::after {
535
+ content: '';
536
+ position: absolute;
537
+ display: none;
538
+ left: 5px;
539
+ top: 2px;
540
+ width: 4px;
541
+ height: 8px;
542
+ border: solid white;
543
+ border-width: 0 2px 2px 0;
544
+ transform: rotate(45deg);
545
+ }
546
+
547
+ .node-checkbox:checked + .checkbox-custom {
548
+ background: var(--tree-checkbox-checked-background, #007bff);
549
+ border-color: var(--tree-checkbox-checked-border, #007bff);
550
+ }
551
+
552
+ .node-checkbox:checked + .checkbox-custom::after {
553
+ display: block;
554
+ }
555
+
556
+ .checkbox-wrapper:hover .checkbox-custom {
557
+ border-color: var(--tree-checkbox-hover-border, #999);
558
+ box-shadow: 0 0 0 3px var(--tree-checkbox-hover-shadow, rgba(0, 123, 255, 0.1));
559
+ }
560
+
561
+ .node-checkbox:focus + .checkbox-custom {
562
+ outline: 2px solid var(--tree-focus-color, #007bff);
563
+ outline-offset: 2px;
564
+ }
565
+
566
+ .node-icon {
567
+ display: inline-flex;
568
+ align-items: center;
569
+ justify-content: center;
570
+ font-size: 18px;
571
+ min-width: 20px;
572
+ }
573
+
574
+ .node-icon.custom-icon {
575
+ font-size: var(--tree-custom-icon-size, 20px);
576
+ }
577
+
578
+ .node-label {
579
+ flex: 1;
580
+ padding: 2px 4px;
581
+ color: var(--tree-label-color, #333);
582
+ font-weight: var(--tree-label-font-weight, 400);
583
+ line-height: 1.4;
584
+ cursor: pointer;
585
+ transition: color 0.15s ease;
586
+ }
587
+
588
+ .node-label:hover {
589
+ color: var(--tree-label-hover-color, #007bff);
590
+ }
591
+
592
+ .node-children {
593
+ position: relative;
594
+ }
595
+
596
+ .node-children::before {
597
+ content: '';
598
+ position: absolute;
599
+ left: calc(var(--tree-indent-width, 20px) / 2 + 8px);
600
+ top: 0;
601
+ bottom: 0;
602
+ width: 1px;
603
+ background: var(--tree-guide-line-color, rgba(0, 0, 0, 0.1));
604
+ }
605
+
606
+ /* Animations */
607
+ @keyframes slideDown {
608
+ from {
609
+ opacity: 0;
610
+ transform: translateY(-4px);
611
+ }
612
+ to {
613
+ opacity: 1;
614
+ transform: translateY(0);
615
+ }
616
+ }
617
+
618
+ .node-children {
619
+ animation: slideDown 0.2s ease-out;
620
+ }
621
+
622
+ /* Empty state */
623
+ .tree-empty {
624
+ padding: 32px;
625
+ text-align: center;
626
+ color: var(--tree-empty-color, #999);
627
+ font-style: italic;
628
+ }
629
+
630
+ /* Scrollbar styling */
631
+ .tree-container::-webkit-scrollbar {
632
+ width: 8px;
633
+ height: 8px;
634
+ }
635
+
636
+ .tree-container::-webkit-scrollbar-track {
637
+ background: var(--tree-scrollbar-track, #f1f1f1);
638
+ border-radius: 4px;
639
+ }
640
+
641
+ .tree-container::-webkit-scrollbar-thumb {
642
+ background: var(--tree-scrollbar-thumb, #888);
643
+ border-radius: 4px;
644
+ }
645
+
646
+ .tree-container::-webkit-scrollbar-thumb:hover {
647
+ background: var(--tree-scrollbar-thumb-hover, #555);
648
+ }
649
+ </style>
650
+
651
+ <div class="${ containerClass }">
652
+ ${ this.data.length > 0
653
+ ? this.data.map( node => this.renderNode( node ) ).join( '' )
654
+ : '<div class="tree-empty">No items to display</div>'
655
+ }
656
+ </div>
657
+ `;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Conditionally defines the custom element if in a browser environment.
663
+ */
664
+ const defineTreeView = ( tagName: string = 'liwe3-tree-view' ): void => {
665
+ if ( typeof window !== 'undefined' && !window.customElements.get( tagName ) ) {
666
+ customElements.define( tagName, TreeViewElement );
667
+ }
668
+ };
669
+
670
+ // Auto-register with default tag name
671
+ defineTreeView();
672
+
673
+ export { defineTreeView };