@constela/ui 0.2.4 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +174 -3
  2. package/components/accordion/accordion-content.constela.json +20 -0
  3. package/components/accordion/accordion-item.constela.json +20 -0
  4. package/components/accordion/accordion-trigger.constela.json +21 -0
  5. package/components/accordion/accordion.constela.json +18 -0
  6. package/components/accordion/accordion.styles.json +54 -0
  7. package/components/accordion/accordion.test.ts +608 -0
  8. package/components/calendar/calendar.constela.json +195 -0
  9. package/components/calendar/calendar.styles.json +33 -0
  10. package/components/calendar/calendar.test.ts +458 -0
  11. package/components/chart/area-chart.constela.json +482 -0
  12. package/components/chart/bar-chart.constela.json +342 -0
  13. package/components/chart/chart-axis.constela.json +224 -0
  14. package/components/chart/chart-legend.constela.json +82 -0
  15. package/components/chart/chart-tooltip.constela.json +61 -0
  16. package/components/chart/chart.styles.json +183 -0
  17. package/components/chart/chart.test.ts +3260 -0
  18. package/components/chart/donut-chart.constela.json +369 -0
  19. package/components/chart/line-chart.constela.json +380 -0
  20. package/components/chart/pie-chart.constela.json +259 -0
  21. package/components/chart/radar-chart.constela.json +297 -0
  22. package/components/chart/scatter-chart.constela.json +300 -0
  23. package/components/data-table/data-table-cell.constela.json +22 -0
  24. package/components/data-table/data-table-header.constela.json +30 -0
  25. package/components/data-table/data-table-pagination.constela.json +19 -0
  26. package/components/data-table/data-table-row.constela.json +30 -0
  27. package/components/data-table/data-table.constela.json +32 -0
  28. package/components/data-table/data-table.styles.json +84 -0
  29. package/components/data-table/data-table.test.ts +873 -0
  30. package/components/datepicker/datepicker.constela.json +128 -0
  31. package/components/datepicker/datepicker.styles.json +47 -0
  32. package/components/datepicker/datepicker.test.ts +540 -0
  33. package/components/tree/tree-node.constela.json +26 -0
  34. package/components/tree/tree.constela.json +24 -0
  35. package/components/tree/tree.styles.json +50 -0
  36. package/components/tree/tree.test.ts +542 -0
  37. package/components/virtual-scroll/virtual-scroll.constela.json +27 -0
  38. package/components/virtual-scroll/virtual-scroll.styles.json +17 -0
  39. package/components/virtual-scroll/virtual-scroll.test.ts +345 -0
  40. package/package.json +2 -2
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Test suite for Tree Component Suite
3
+ *
4
+ * @constela/ui Tree component tests following TDD methodology.
5
+ * These tests verify the Tree (container) and TreeNode (recursive) components
6
+ * structure, params, styles, and accessibility.
7
+ *
8
+ * Coverage:
9
+ * - Component structure validation
10
+ * - Params definition validation (including required params)
11
+ * - Local state validation
12
+ * - Style preset validation
13
+ * - Accessibility attributes (role="tree", role="treeitem", aria-expanded, aria-selected, aria-level)
14
+ */
15
+
16
+ import { describe, it, expect, beforeAll } from 'vitest';
17
+ import { readFileSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import type { ComponentDef, StylePreset } from '@constela/core';
21
+ import {
22
+ assertValidComponent,
23
+ assertValidStylePreset,
24
+ hasParams,
25
+ isOptionalParam,
26
+ hasParamType,
27
+ getRootTag,
28
+ hasVariants,
29
+ hasVariantOptions,
30
+ hasDefaultVariants,
31
+ findPropInView,
32
+ hasRole,
33
+ hasAriaAttribute,
34
+ } from '../../tests/helpers/test-utils.js';
35
+
36
+ // ==================== Test Utilities ====================
37
+
38
+ /**
39
+ * Get the path to a component file in the tree directory
40
+ */
41
+ function getTreeComponentPath(fileName: string): string {
42
+ const __dirname = dirname(fileURLToPath(import.meta.url));
43
+ return join(__dirname, fileName);
44
+ }
45
+
46
+ /**
47
+ * Load a specific tree sub-component
48
+ */
49
+ function loadTreeComponent(componentName: string): ComponentDef {
50
+ const path = getTreeComponentPath(`${componentName}.constela.json`);
51
+ const content = readFileSync(path, 'utf-8');
52
+ return JSON.parse(content) as ComponentDef;
53
+ }
54
+
55
+ /**
56
+ * Load tree styles
57
+ */
58
+ function loadTreeStyles(): Record<string, StylePreset> {
59
+ const path = getTreeComponentPath('tree.styles.json');
60
+ const content = readFileSync(path, 'utf-8');
61
+ return JSON.parse(content) as Record<string, StylePreset>;
62
+ }
63
+
64
+ /**
65
+ * Check if a param is required (required: true or required not specified)
66
+ */
67
+ function isRequiredParam(component: ComponentDef, paramName: string): boolean {
68
+ if (!component.params || !(paramName in component.params)) {
69
+ return false;
70
+ }
71
+ const param = component.params[paramName];
72
+ // In Constela, params are required by default unless explicitly set to false
73
+ return param.required !== false;
74
+ }
75
+
76
+ /**
77
+ * Check if a component has local state with a specific field
78
+ */
79
+ function hasLocalState(component: ComponentDef, fieldName: string): boolean {
80
+ if (!component.localState) {
81
+ return false;
82
+ }
83
+ return fieldName in component.localState;
84
+ }
85
+
86
+ /**
87
+ * Check if a local state field has a specific type
88
+ */
89
+ function hasLocalStateType(
90
+ component: ComponentDef,
91
+ fieldName: string,
92
+ expectedType: 'string' | 'number' | 'boolean' | 'list' | 'object'
93
+ ): boolean {
94
+ if (!component.localState || !(fieldName in component.localState)) {
95
+ return false;
96
+ }
97
+ return component.localState[fieldName].type === expectedType;
98
+ }
99
+
100
+ /**
101
+ * Check if a local state field has a specific initial value
102
+ */
103
+ function hasLocalStateInitial(
104
+ component: ComponentDef,
105
+ fieldName: string,
106
+ expectedInitial: unknown
107
+ ): boolean {
108
+ if (!component.localState || !(fieldName in component.localState)) {
109
+ return false;
110
+ }
111
+ const actual = component.localState[fieldName].initial;
112
+ // Handle array comparison
113
+ if (Array.isArray(expectedInitial) && Array.isArray(actual)) {
114
+ return JSON.stringify(actual) === JSON.stringify(expectedInitial);
115
+ }
116
+ return actual === expectedInitial;
117
+ }
118
+
119
+ // ==================== Test Contexts ====================
120
+
121
+ interface TreeTestContext {
122
+ tree: ComponentDef;
123
+ treeNode: ComponentDef;
124
+ styles: Record<string, StylePreset>;
125
+ }
126
+
127
+ describe('Tree Component Suite', () => {
128
+ let ctx: TreeTestContext;
129
+
130
+ beforeAll(() => {
131
+ ctx = {
132
+ tree: loadTreeComponent('tree'),
133
+ treeNode: loadTreeComponent('tree-node'),
134
+ styles: loadTreeStyles(),
135
+ };
136
+ });
137
+
138
+ // ==================== Tree (Container) Tests ====================
139
+
140
+ describe('Tree (Container)', () => {
141
+ // ==================== Component Structure Tests ====================
142
+
143
+ describe('Component Structure', () => {
144
+ it('should have valid component structure', () => {
145
+ assertValidComponent(ctx.tree);
146
+ });
147
+
148
+ it('should have ul as root element', () => {
149
+ const rootTag = getRootTag(ctx.tree);
150
+ expect(rootTag).toBe('ul');
151
+ });
152
+
153
+ it('should have role="tree" attribute', () => {
154
+ expect(hasRole(ctx.tree.view, 'tree')).toBe(true);
155
+ });
156
+
157
+ it('should have className using StyleExpr with treeStyles preset', () => {
158
+ const className = findPropInView(ctx.tree.view, 'className');
159
+ expect(className).not.toBeNull();
160
+ expect(className).toMatchObject({
161
+ expr: 'style',
162
+ preset: 'treeStyles',
163
+ });
164
+ });
165
+
166
+ it('should have aria-multiselectable attribute', () => {
167
+ expect(hasAriaAttribute(ctx.tree.view, 'aria-multiselectable')).toBe(true);
168
+ });
169
+ });
170
+
171
+ // ==================== Params Validation Tests ====================
172
+
173
+ describe('Params Validation', () => {
174
+ const expectedParams = ['items', 'selectable', 'multiSelect'];
175
+
176
+ it('should have all expected params', () => {
177
+ expect(hasParams(ctx.tree, expectedParams)).toBe(true);
178
+ });
179
+
180
+ describe('param: items', () => {
181
+ it('should be required', () => {
182
+ expect(isRequiredParam(ctx.tree, 'items')).toBe(true);
183
+ });
184
+
185
+ it('should have type list', () => {
186
+ expect(hasParamType(ctx.tree, 'items', 'list')).toBe(true);
187
+ });
188
+ });
189
+
190
+ describe('param: selectable', () => {
191
+ it('should be optional', () => {
192
+ expect(isOptionalParam(ctx.tree, 'selectable')).toBe(true);
193
+ });
194
+
195
+ it('should have type boolean', () => {
196
+ expect(hasParamType(ctx.tree, 'selectable', 'boolean')).toBe(true);
197
+ });
198
+ });
199
+
200
+ describe('param: multiSelect', () => {
201
+ it('should be optional', () => {
202
+ expect(isOptionalParam(ctx.tree, 'multiSelect')).toBe(true);
203
+ });
204
+
205
+ it('should have type boolean', () => {
206
+ expect(hasParamType(ctx.tree, 'multiSelect', 'boolean')).toBe(true);
207
+ });
208
+ });
209
+ });
210
+
211
+ // ==================== Local State Tests ====================
212
+
213
+ describe('Local State', () => {
214
+ it('should have expandedNodes local state', () => {
215
+ expect(hasLocalState(ctx.tree, 'expandedNodes')).toBe(true);
216
+ });
217
+
218
+ it('should have expandedNodes as list type', () => {
219
+ expect(hasLocalStateType(ctx.tree, 'expandedNodes', 'list')).toBe(true);
220
+ });
221
+
222
+ it('should have expandedNodes initial value as empty array', () => {
223
+ expect(hasLocalStateInitial(ctx.tree, 'expandedNodes', [])).toBe(true);
224
+ });
225
+
226
+ it('should have selectedNodes local state', () => {
227
+ expect(hasLocalState(ctx.tree, 'selectedNodes')).toBe(true);
228
+ });
229
+
230
+ it('should have selectedNodes as list type', () => {
231
+ expect(hasLocalStateType(ctx.tree, 'selectedNodes', 'list')).toBe(true);
232
+ });
233
+
234
+ it('should have selectedNodes initial value as empty array', () => {
235
+ expect(hasLocalStateInitial(ctx.tree, 'selectedNodes', [])).toBe(true);
236
+ });
237
+ });
238
+
239
+ // ==================== Style Preset Tests ====================
240
+
241
+ describe('Style Preset (treeStyles)', () => {
242
+ it('should have valid style preset structure', () => {
243
+ const treeStyles = ctx.styles['treeStyles'];
244
+ expect(treeStyles).toBeDefined();
245
+ assertValidStylePreset(treeStyles);
246
+ });
247
+
248
+ it('should have base classes for list-none and spacing', () => {
249
+ const treeStyles = ctx.styles['treeStyles'];
250
+ expect(treeStyles.base).toBeDefined();
251
+ expect(typeof treeStyles.base).toBe('string');
252
+ expect(treeStyles.base).toContain('list-none');
253
+ expect(treeStyles.base).toContain('space-y-1');
254
+ });
255
+ });
256
+ });
257
+
258
+ // ==================== TreeNode Tests ====================
259
+
260
+ describe('TreeNode', () => {
261
+ // ==================== Component Structure Tests ====================
262
+
263
+ describe('Component Structure', () => {
264
+ it('should have valid component structure', () => {
265
+ assertValidComponent(ctx.treeNode);
266
+ });
267
+
268
+ it('should have li as root element', () => {
269
+ const rootTag = getRootTag(ctx.treeNode);
270
+ expect(rootTag).toBe('li');
271
+ });
272
+
273
+ it('should have role="treeitem" attribute', () => {
274
+ expect(hasRole(ctx.treeNode.view, 'treeitem')).toBe(true);
275
+ });
276
+
277
+ it('should have className using StyleExpr with treeNodeStyles preset', () => {
278
+ const className = findPropInView(ctx.treeNode.view, 'className');
279
+ expect(className).not.toBeNull();
280
+ expect(className).toMatchObject({
281
+ expr: 'style',
282
+ preset: 'treeNodeStyles',
283
+ });
284
+ });
285
+ });
286
+
287
+ // ==================== Params Validation Tests ====================
288
+
289
+ describe('Params Validation', () => {
290
+ const expectedParams = ['node', 'level', 'expandedNodes', 'selectedNodes', 'expanded', 'selected', 'selectable'];
291
+
292
+ it('should have all expected params', () => {
293
+ expect(hasParams(ctx.treeNode, expectedParams)).toBe(true);
294
+ });
295
+
296
+ describe('param: node', () => {
297
+ it('should be required', () => {
298
+ expect(isRequiredParam(ctx.treeNode, 'node')).toBe(true);
299
+ });
300
+
301
+ it('should have type object', () => {
302
+ expect(hasParamType(ctx.treeNode, 'node', 'object')).toBe(true);
303
+ });
304
+ });
305
+
306
+ describe('param: level', () => {
307
+ it('should be required', () => {
308
+ expect(isRequiredParam(ctx.treeNode, 'level')).toBe(true);
309
+ });
310
+
311
+ it('should have type number', () => {
312
+ expect(hasParamType(ctx.treeNode, 'level', 'number')).toBe(true);
313
+ });
314
+ });
315
+
316
+ describe('param: expandedNodes', () => {
317
+ it('should be required', () => {
318
+ expect(isRequiredParam(ctx.treeNode, 'expandedNodes')).toBe(true);
319
+ });
320
+
321
+ it('should have type list', () => {
322
+ expect(hasParamType(ctx.treeNode, 'expandedNodes', 'list')).toBe(true);
323
+ });
324
+ });
325
+
326
+ describe('param: selectedNodes', () => {
327
+ it('should be required', () => {
328
+ expect(isRequiredParam(ctx.treeNode, 'selectedNodes')).toBe(true);
329
+ });
330
+
331
+ it('should have type list', () => {
332
+ expect(hasParamType(ctx.treeNode, 'selectedNodes', 'list')).toBe(true);
333
+ });
334
+ });
335
+
336
+ describe('param: expanded', () => {
337
+ it('should be optional', () => {
338
+ expect(isOptionalParam(ctx.treeNode, 'expanded')).toBe(true);
339
+ });
340
+
341
+ it('should have type boolean', () => {
342
+ expect(hasParamType(ctx.treeNode, 'expanded', 'boolean')).toBe(true);
343
+ });
344
+ });
345
+
346
+ describe('param: selected', () => {
347
+ it('should be optional', () => {
348
+ expect(isOptionalParam(ctx.treeNode, 'selected')).toBe(true);
349
+ });
350
+
351
+ it('should have type boolean', () => {
352
+ expect(hasParamType(ctx.treeNode, 'selected', 'boolean')).toBe(true);
353
+ });
354
+ });
355
+
356
+ describe('param: selectable', () => {
357
+ it('should be optional', () => {
358
+ expect(isOptionalParam(ctx.treeNode, 'selectable')).toBe(true);
359
+ });
360
+
361
+ it('should have type boolean', () => {
362
+ expect(hasParamType(ctx.treeNode, 'selectable', 'boolean')).toBe(true);
363
+ });
364
+ });
365
+ });
366
+
367
+ // ==================== Accessibility Tests ====================
368
+
369
+ describe('Accessibility', () => {
370
+ it('should have aria-expanded attribute', () => {
371
+ expect(hasAriaAttribute(ctx.treeNode.view, 'aria-expanded')).toBe(true);
372
+ });
373
+
374
+ it('should have aria-selected attribute', () => {
375
+ expect(hasAriaAttribute(ctx.treeNode.view, 'aria-selected')).toBe(true);
376
+ });
377
+
378
+ it('should have aria-level attribute', () => {
379
+ expect(hasAriaAttribute(ctx.treeNode.view, 'aria-level')).toBe(true);
380
+ });
381
+ });
382
+
383
+ // ==================== Style Preset Tests ====================
384
+
385
+ describe('Style Preset (treeNodeStyles)', () => {
386
+ it('should have valid style preset structure', () => {
387
+ const treeNodeStyles = ctx.styles['treeNodeStyles'];
388
+ expect(treeNodeStyles).toBeDefined();
389
+ assertValidStylePreset(treeNodeStyles);
390
+ });
391
+
392
+ it('should have base classes with relative positioning', () => {
393
+ const treeNodeStyles = ctx.styles['treeNodeStyles'];
394
+ expect(treeNodeStyles.base).toBeDefined();
395
+ expect(typeof treeNodeStyles.base).toBe('string');
396
+ expect(treeNodeStyles.base).toContain('relative');
397
+ });
398
+ });
399
+
400
+ describe('Style Preset (treeNodeContentStyles)', () => {
401
+ it('should have valid style preset structure', () => {
402
+ const treeNodeContentStyles = ctx.styles['treeNodeContentStyles'];
403
+ expect(treeNodeContentStyles).toBeDefined();
404
+ assertValidStylePreset(treeNodeContentStyles);
405
+ });
406
+
407
+ it('should have base classes for flex layout with gap and rounded corners', () => {
408
+ const treeNodeContentStyles = ctx.styles['treeNodeContentStyles'];
409
+ expect(treeNodeContentStyles.base).toBeDefined();
410
+ expect(typeof treeNodeContentStyles.base).toBe('string');
411
+ expect(treeNodeContentStyles.base).toContain('flex');
412
+ expect(treeNodeContentStyles.base).toContain('items-center');
413
+ expect(treeNodeContentStyles.base).toContain('gap-1');
414
+ expect(treeNodeContentStyles.base).toContain('rounded-md');
415
+ expect(treeNodeContentStyles.base).toContain('px-2');
416
+ expect(treeNodeContentStyles.base).toContain('py-1');
417
+ });
418
+
419
+ describe('variant options', () => {
420
+ const selectedOptions = ['true', 'false'];
421
+
422
+ it('should have selected variants', () => {
423
+ const treeNodeContentStyles = ctx.styles['treeNodeContentStyles'];
424
+ expect(hasVariants(treeNodeContentStyles, ['selected'])).toBe(true);
425
+ });
426
+
427
+ it.each(selectedOptions)('should have %s selected option', (option) => {
428
+ const treeNodeContentStyles = ctx.styles['treeNodeContentStyles'];
429
+ expect(hasVariantOptions(treeNodeContentStyles, 'selected', [option])).toBe(true);
430
+ });
431
+ });
432
+ });
433
+
434
+ describe('Style Preset (treeToggleStyles)', () => {
435
+ it('should have valid style preset structure', () => {
436
+ const treeToggleStyles = ctx.styles['treeToggleStyles'];
437
+ expect(treeToggleStyles).toBeDefined();
438
+ assertValidStylePreset(treeToggleStyles);
439
+ });
440
+
441
+ it('should have base classes for inline-flex layout with fixed dimensions', () => {
442
+ const treeToggleStyles = ctx.styles['treeToggleStyles'];
443
+ expect(treeToggleStyles.base).toBeDefined();
444
+ expect(typeof treeToggleStyles.base).toBe('string');
445
+ expect(treeToggleStyles.base).toContain('inline-flex');
446
+ expect(treeToggleStyles.base).toContain('h-4');
447
+ expect(treeToggleStyles.base).toContain('w-4');
448
+ expect(treeToggleStyles.base).toContain('items-center');
449
+ expect(treeToggleStyles.base).toContain('justify-center');
450
+ });
451
+ });
452
+
453
+ describe('Style Preset (treeChevronStyles)', () => {
454
+ it('should have valid style preset structure', () => {
455
+ const treeChevronStyles = ctx.styles['treeChevronStyles'];
456
+ expect(treeChevronStyles).toBeDefined();
457
+ assertValidStylePreset(treeChevronStyles);
458
+ });
459
+
460
+ it('should have base classes for icon sizing and transition', () => {
461
+ const treeChevronStyles = ctx.styles['treeChevronStyles'];
462
+ expect(treeChevronStyles.base).toBeDefined();
463
+ expect(typeof treeChevronStyles.base).toBe('string');
464
+ expect(treeChevronStyles.base).toContain('h-4');
465
+ expect(treeChevronStyles.base).toContain('w-4');
466
+ expect(treeChevronStyles.base).toContain('shrink-0');
467
+ expect(treeChevronStyles.base).toContain('transition-transform');
468
+ });
469
+
470
+ describe('variant options', () => {
471
+ it('should have expanded variants', () => {
472
+ const treeChevronStyles = ctx.styles['treeChevronStyles'];
473
+ expect(hasVariants(treeChevronStyles, ['expanded'])).toBe(true);
474
+ });
475
+
476
+ it('should have true expanded option with rotate-90', () => {
477
+ const treeChevronStyles = ctx.styles['treeChevronStyles'];
478
+ expect(hasVariantOptions(treeChevronStyles, 'expanded', ['true'])).toBe(true);
479
+ expect(treeChevronStyles.variants?.expanded?.['true']).toContain('rotate-90');
480
+ });
481
+
482
+ it('should have false expanded option with rotate-0', () => {
483
+ const treeChevronStyles = ctx.styles['treeChevronStyles'];
484
+ expect(hasVariantOptions(treeChevronStyles, 'expanded', ['false'])).toBe(true);
485
+ expect(treeChevronStyles.variants?.expanded?.['false']).toContain('rotate-0');
486
+ });
487
+ });
488
+
489
+ describe('default variants', () => {
490
+ it('should have default expanded set to false', () => {
491
+ const treeChevronStyles = ctx.styles['treeChevronStyles'];
492
+ expect(hasDefaultVariants(treeChevronStyles, { expanded: 'false' })).toBe(true);
493
+ });
494
+ });
495
+ });
496
+
497
+ describe('Style Preset (treeLabelStyles)', () => {
498
+ it('should have valid style preset structure', () => {
499
+ const treeLabelStyles = ctx.styles['treeLabelStyles'];
500
+ expect(treeLabelStyles).toBeDefined();
501
+ assertValidStylePreset(treeLabelStyles);
502
+ });
503
+
504
+ it('should have base classes for flex-1 and truncate', () => {
505
+ const treeLabelStyles = ctx.styles['treeLabelStyles'];
506
+ expect(treeLabelStyles.base).toBeDefined();
507
+ expect(typeof treeLabelStyles.base).toBe('string');
508
+ expect(treeLabelStyles.base).toContain('flex-1');
509
+ expect(treeLabelStyles.base).toContain('truncate');
510
+ });
511
+
512
+ describe('variant options', () => {
513
+ const selectedOptions = ['true', 'false'];
514
+
515
+ it('should have selected variants', () => {
516
+ const treeLabelStyles = ctx.styles['treeLabelStyles'];
517
+ expect(hasVariants(treeLabelStyles, ['selected'])).toBe(true);
518
+ });
519
+
520
+ it.each(selectedOptions)('should have %s selected option', (option) => {
521
+ const treeLabelStyles = ctx.styles['treeLabelStyles'];
522
+ expect(hasVariantOptions(treeLabelStyles, 'selected', [option])).toBe(true);
523
+ });
524
+ });
525
+ });
526
+
527
+ describe('Style Preset (treeGroupStyles)', () => {
528
+ it('should have valid style preset structure', () => {
529
+ const treeGroupStyles = ctx.styles['treeGroupStyles'];
530
+ expect(treeGroupStyles).toBeDefined();
531
+ assertValidStylePreset(treeGroupStyles);
532
+ });
533
+
534
+ it('should have base classes with pl-4 for indentation', () => {
535
+ const treeGroupStyles = ctx.styles['treeGroupStyles'];
536
+ expect(treeGroupStyles.base).toBeDefined();
537
+ expect(typeof treeGroupStyles.base).toBe('string');
538
+ expect(treeGroupStyles.base).toContain('pl-4');
539
+ });
540
+ });
541
+ });
542
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "params": {
3
+ "items": { "type": "list", "required": true },
4
+ "itemHeight": { "type": "number", "required": true },
5
+ "containerHeight": { "type": "number", "required": true },
6
+ "overscan": { "type": "number", "required": false },
7
+ "keyField": { "type": "string", "required": false }
8
+ },
9
+ "localState": {
10
+ "scrollTop": { "type": "number", "initial": 0 }
11
+ },
12
+ "view": {
13
+ "kind": "element",
14
+ "tag": "div",
15
+ "props": {
16
+ "role": { "expr": "lit", "value": "listbox" },
17
+ "aria-setsize": { "expr": "call", "target": { "expr": "param", "name": "items" }, "method": "length" },
18
+ "aria-live": { "expr": "lit", "value": "polite" },
19
+ "tabindex": { "expr": "lit", "value": "0" },
20
+ "className": {
21
+ "expr": "style",
22
+ "preset": "virtualScrollContainer"
23
+ }
24
+ },
25
+ "children": [{ "kind": "slot" }]
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "virtualScrollContainer": {
3
+ "base": "relative overflow-hidden"
4
+ },
5
+ "virtualScrollViewport": {
6
+ "base": "overflow-auto w-full h-full"
7
+ },
8
+ "virtualScrollSpacer": {
9
+ "base": "w-full pointer-events-none"
10
+ },
11
+ "virtualScrollContent": {
12
+ "base": "absolute top-0 left-0 w-full"
13
+ },
14
+ "virtualScrollItem": {
15
+ "base": "w-full"
16
+ }
17
+ }