@talelk/newui-organisms 0.0.0-stage → 2.0.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 (64) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +12 -2
  3. package/package.json +38 -3
  4. package/src/exports.test.ts +27 -0
  5. package/src/index.ts +1 -0
  6. package/src/organisms/AppSidebar/AppSidebar.test.tsx +136 -0
  7. package/src/organisms/AppSidebar/AppSidebar.tsx +259 -0
  8. package/src/organisms/AppSidebar/index.ts +1 -0
  9. package/src/organisms/AuthCard/AuthCard.a11y.test.tsx +36 -0
  10. package/src/organisms/AuthCard/AuthCard.test.tsx +56 -0
  11. package/src/organisms/AuthCard/AuthCard.tsx +89 -0
  12. package/src/organisms/AuthCard/index.ts +1 -0
  13. package/src/organisms/CommandPalette/CommandPalette.test.tsx +196 -0
  14. package/src/organisms/CommandPalette/CommandPalette.tsx +468 -0
  15. package/src/organisms/CommandPalette/index.ts +1 -0
  16. package/src/organisms/DataState/DataState.test.tsx +45 -0
  17. package/src/organisms/DataState/DataState.tsx +111 -0
  18. package/src/organisms/DataState/index.ts +6 -0
  19. package/src/organisms/DataTable/DataTable.test.tsx +161 -0
  20. package/src/organisms/DataTable/DataTable.tsx +612 -0
  21. package/src/organisms/DataTable/index.ts +1 -0
  22. package/src/organisms/DetailHeader/DetailHeader.test.tsx +55 -0
  23. package/src/organisms/DetailHeader/DetailHeader.tsx +95 -0
  24. package/src/organisms/DetailHeader/index.ts +1 -0
  25. package/src/organisms/FilterBar/FilterBar.test.tsx +152 -0
  26. package/src/organisms/FilterBar/FilterBar.tsx +251 -0
  27. package/src/organisms/FilterBar/index.ts +1 -0
  28. package/src/organisms/KanbanBoard/KanbanBoard.test.tsx +255 -0
  29. package/src/organisms/KanbanBoard/KanbanBoard.tsx +605 -0
  30. package/src/organisms/KanbanBoard/drag.test.ts +448 -0
  31. package/src/organisms/KanbanBoard/drag.ts +407 -0
  32. package/src/organisms/KanbanBoard/index.ts +17 -0
  33. package/src/organisms/MobileNav/MobileNav.browser.test.tsx +39 -0
  34. package/src/organisms/MobileNav/MobileNav.test.tsx +108 -0
  35. package/src/organisms/MobileNav/MobileNav.tsx +120 -0
  36. package/src/organisms/MobileNav/index.ts +6 -0
  37. package/src/organisms/PageToolbar/PageToolbar.a11y.test.tsx +35 -0
  38. package/src/organisms/PageToolbar/PageToolbar.test.tsx +42 -0
  39. package/src/organisms/PageToolbar/PageToolbar.tsx +126 -0
  40. package/src/organisms/PageToolbar/index.ts +1 -0
  41. package/src/organisms/Section/Section.test.tsx +41 -0
  42. package/src/organisms/Section/Section.tsx +108 -0
  43. package/src/organisms/Section/index.ts +1 -0
  44. package/src/organisms/SelectionBar/SelectionBar.test.tsx +35 -0
  45. package/src/organisms/SelectionBar/SelectionBar.tsx +84 -0
  46. package/src/organisms/SelectionBar/index.ts +5 -0
  47. package/src/organisms/SettingsDialog/SettingsDialog.a11y.test.tsx +58 -0
  48. package/src/organisms/SettingsDialog/SettingsDialog.browser.test.tsx +43 -0
  49. package/src/organisms/SettingsDialog/SettingsDialog.ssr.test.tsx +27 -0
  50. package/src/organisms/SettingsDialog/SettingsDialog.test.tsx +202 -0
  51. package/src/organisms/SettingsDialog/SettingsDialog.tsx +548 -0
  52. package/src/organisms/SettingsDialog/index.ts +20 -0
  53. package/src/organisms/Sidebar/Sidebar.browser.test.tsx +52 -0
  54. package/src/organisms/Sidebar/Sidebar.test.tsx +308 -0
  55. package/src/organisms/Sidebar/Sidebar.tsx +732 -0
  56. package/src/organisms/Sidebar/index.ts +22 -0
  57. package/src/organisms/StatGrid/StatGrid.a11y.test.tsx +40 -0
  58. package/src/organisms/StatGrid/StatGrid.test.tsx +95 -0
  59. package/src/organisms/StatGrid/StatGrid.tsx +151 -0
  60. package/src/organisms/StatGrid/index.ts +1 -0
  61. package/src/organisms/VirtualList/VirtualList.test.tsx +80 -0
  62. package/src/organisms/VirtualList/VirtualList.tsx +113 -0
  63. package/src/organisms/VirtualList/index.ts +1 -0
  64. package/src/organisms/index.ts +16 -0
@@ -0,0 +1,22 @@
1
+ export {
2
+ Sidebar,
3
+ SidebarCard,
4
+ type SidebarCardAction,
5
+ type SidebarCardProps,
6
+ SidebarCollapseToggle,
7
+ type SidebarCollapseToggleProps,
8
+ SidebarHeader,
9
+ type SidebarHeaderProps,
10
+ SidebarItem,
11
+ type SidebarItemProps,
12
+ SidebarLabel,
13
+ type SidebarLabelProps,
14
+ type SidebarProps,
15
+ SidebarSearch,
16
+ type SidebarSearchProps,
17
+ SidebarSection,
18
+ type SidebarSectionProps,
19
+ SidebarTrigger,
20
+ type SidebarTriggerProps,
21
+ useSidebar,
22
+ } from './Sidebar'
@@ -0,0 +1,40 @@
1
+ import { render } from '@solidjs/testing-library'
2
+ import axe, { type AxeResults } from 'axe-core'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { type Stat, StatGrid } from './StatGrid'
5
+
6
+ const AXE_OPTIONS = {
7
+ rules: {
8
+ 'color-contrast': { enabled: false },
9
+ region: { enabled: false },
10
+ },
11
+ }
12
+
13
+ function summarize(results: AxeResults) {
14
+ return results.violations.map((violation) => ({
15
+ id: violation.id,
16
+ impact: violation.impact,
17
+ targets: violation.nodes.map((node) => node.target),
18
+ }))
19
+ }
20
+
21
+ const STATS: Stat[] = [
22
+ {
23
+ label: 'Revenue',
24
+ value: '$4.2m',
25
+ delta: { value: '+5.1%', direction: 'up', good: true },
26
+ },
27
+ {
28
+ label: 'Open deals',
29
+ value: '38',
30
+ hint: 'vs 42 last quarter',
31
+ delta: { value: '-0.4 pts', direction: 'down', good: false },
32
+ },
33
+ ]
34
+
35
+ describe('StatGrid accessibility', () => {
36
+ it('has no axe violations for a KPI row', async () => {
37
+ const { container } = render(() => <StatGrid stats={STATS} columns={2} />)
38
+ expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
39
+ })
40
+ })
@@ -0,0 +1,95 @@
1
+ import { render, screen } from '@solidjs/testing-library'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { StatGrid } from './StatGrid'
5
+
6
+ describe('StatGrid', () => {
7
+ it('renders a label, a value and a hint per stat', () => {
8
+ render(() => (
9
+ <StatGrid
10
+ stats={[
11
+ {
12
+ label: 'Net revenue',
13
+ value: '$220.2K',
14
+ hint: 'vs last month',
15
+ delta: { value: '5.1%', direction: 'up' },
16
+ },
17
+ ]}
18
+ />
19
+ ))
20
+ expect(screen.getByText('Net revenue')).toBeTruthy()
21
+ expect(screen.getByText('$220.2K')).toBeTruthy()
22
+ expect(screen.getByText('vs last month')).toBeTruthy()
23
+ })
24
+
25
+ it('tones a fall by whether it is good, not by its direction', () => {
26
+ const down = render(() => (
27
+ <StatGrid
28
+ stats={[
29
+ {
30
+ label: 'Monthly churn',
31
+ value: '2.1%',
32
+ delta: { value: '0.4 pts', direction: 'down', good: true },
33
+ },
34
+ ]}
35
+ />
36
+ ))
37
+ // The pill pairing is the contrast contract's ink-*-8 on surface-*-2.
38
+ expect(screen.getByText('0.4 pts').className).toContain('text-ink-green-8')
39
+ expect(screen.getByText('0.4 pts').className).toContain(
40
+ 'bg-surface-green-2',
41
+ )
42
+ expect(screen.getByText('0.4 pts').className).toContain('rounded-full')
43
+ down.unmount()
44
+
45
+ render(() => (
46
+ <StatGrid
47
+ stats={[
48
+ {
49
+ label: 'Revenue',
50
+ value: '$1',
51
+ delta: { value: '2%', direction: 'down' },
52
+ },
53
+ ]}
54
+ />
55
+ ))
56
+ expect(screen.getByText('2%').className).toContain('text-ink-red-8')
57
+ expect(screen.getByText('2%').className).toContain('bg-surface-red-2')
58
+ })
59
+
60
+ it('is a button when it navigates, a card when it does not', async () => {
61
+ const onClick = vi.fn()
62
+ const plain = render(() => (
63
+ <StatGrid stats={[{ label: 'Static', value: '1' }]} />
64
+ ))
65
+ expect(plain.container.querySelector('button')).toBeNull()
66
+ plain.unmount()
67
+
68
+ render(() => <StatGrid stats={[{ label: 'Live', value: '2', onClick }]} />)
69
+ await userEvent.click(screen.getByRole('button', { name: /Live/ }))
70
+ expect(onClick).toHaveBeenCalled()
71
+ })
72
+
73
+ it('uses literal grid classes so Tailwind can see them', () => {
74
+ const { container } = render(() => (
75
+ <StatGrid stats={[{ label: 'One', value: '1' }]} columns={3} />
76
+ ))
77
+ const className = container.querySelector(
78
+ '[data-slot="stat-grid"]',
79
+ )?.className
80
+ expect(className).toContain('min-[360px]:grid-cols-2')
81
+ expect(className).toContain('xl:grid-cols-3')
82
+ })
83
+
84
+ it('compact cards use the tight padding and omit the icon tile', () => {
85
+ const { container } = render(() => (
86
+ <StatGrid
87
+ compact
88
+ stats={[{ label: 'One', value: '1', icon: 'lucide-house' }]}
89
+ />
90
+ ))
91
+ const card = container.querySelector('[data-slot="stat"]')
92
+ expect(card?.className).toContain('p-2.5')
93
+ expect(card?.querySelector('.bg-surface-gray-2')).toBeNull()
94
+ })
95
+ })
@@ -0,0 +1,151 @@
1
+ import { cn, Icon, type IconProp, Skeleton } from '@talelk/newui-atoms'
2
+ import { For, type JSX, Show } from 'solid-js'
3
+ import { Dynamic } from 'solid-js/web'
4
+
5
+ export interface Stat {
6
+ label: string
7
+ value: JSX.Element
8
+ /** One line of context: the comparison, the period, the owner. */
9
+ hint?: JSX.Element
10
+ /** Signed change, rendered in a tone. `+5.1%` / `-0.4 pts`. */
11
+ delta?: { value: string; direction: 'up' | 'down' | 'flat'; good?: boolean }
12
+ icon?: IconProp
13
+ onClick?: () => void
14
+ }
15
+
16
+ export interface StatGridProps {
17
+ stats: Stat[]
18
+ /** Columns at the widest breakpoint. Defaults to the number of stats, capped. */
19
+ columns?: 2 | 3 | 4
20
+ /** Tighter cards without the icon tile; for table-led screens. */
21
+ compact?: boolean
22
+ /** Replaces the cards with the shared skeleton geometry. */
23
+ loading?: boolean
24
+ class?: string
25
+ }
26
+
27
+ /**
28
+ * A row of KPI cards.
29
+ *
30
+ * The delta is deliberately not coloured by direction but by *whether it is
31
+ * good*: churn going down is good, revenue going down is not, and a grid that
32
+ * paints every minus red gets that wrong on half the dashboards it is used on.
33
+ * `good` defaults to the intuitive reading and can be overridden per stat.
34
+ */
35
+ /**
36
+ * Literal classes, not a template string: Tailwind scans for whole class names,
37
+ * so `sm:grid-cols-${n}` would compile to nothing and `css:check` would be right
38
+ * to fail it.
39
+ */
40
+ const COLUMNS: Record<2 | 3 | 4, string> = {
41
+ 2: 'min-[360px]:grid-cols-2',
42
+ 3: 'min-[360px]:grid-cols-2 xl:grid-cols-3',
43
+ 4: 'min-[360px]:grid-cols-2 xl:grid-cols-4',
44
+ }
45
+
46
+ /** Compact table-led screens earn a four-up row on tablets. */
47
+ const COMPACT_COLUMNS: Record<2 | 3 | 4, string> = {
48
+ 2: 'min-[360px]:grid-cols-2',
49
+ 3: 'min-[360px]:grid-cols-2 md:grid-cols-3',
50
+ 4: 'min-[360px]:grid-cols-2 md:grid-cols-4',
51
+ }
52
+
53
+ export function StatGrid(props: StatGridProps): JSX.Element {
54
+ const columns = (): 2 | 3 | 4 =>
55
+ props.columns ?? (Math.min(4, Math.max(2, props.stats.length)) as 2 | 3 | 4)
56
+
57
+ return (
58
+ <Show
59
+ when={!props.loading}
60
+ fallback={
61
+ <div
62
+ data-slot="stat-grid"
63
+ data-loading
64
+ class={`grid gap-card-gap ${props.compact ? COMPACT_COLUMNS[columns()] : COLUMNS[columns()]} ${props.class ?? ''}`}
65
+ >
66
+ <For each={Array.from({ length: columns() })}>
67
+ {() => <Skeleton lines={3} class="h-24" />}
68
+ </For>
69
+ </div>
70
+ }
71
+ >
72
+ <div
73
+ data-slot="stat-grid"
74
+ class={`grid gap-card-gap ${props.compact ? COMPACT_COLUMNS[columns()] : COLUMNS[columns()]} ${props.class ?? ''}`}
75
+ >
76
+ <For each={props.stats}>
77
+ {(stat) => (
78
+ // A stat that navigates is a button, not a div with a click handler:
79
+ // it has to be reachable by keyboard and announced as interactive.
80
+ <Dynamic
81
+ component={stat.onClick ? 'button' : 'div'}
82
+ {...(stat.onClick ? { type: 'button' as const } : {})}
83
+ data-slot="stat"
84
+ onClick={stat.onClick}
85
+ class={cn(
86
+ 'flex flex-col rounded-card border border-outline-gray-1 bg-surface-elevation-1 text-start',
87
+ props.compact ? 'gap-1 p-2.5' : 'gap-control-gap p-card-pad',
88
+ stat.onClick &&
89
+ 'cursor-pointer transition-[border-color,box-shadow] hover:border-outline-gray-3 hover:shadow-sm focus-visible:focus-ring motion-reduce:transition-none',
90
+ )}
91
+ >
92
+ <div class="flex items-start gap-2">
93
+ <Show when={props.compact ? undefined : stat.icon}>
94
+ {(icon) => (
95
+ <span class="grid size-8 shrink-0 place-items-center rounded-4 bg-surface-gray-2">
96
+ <Icon icon={icon()} class="size-4 text-ink-gray-7" />
97
+ </span>
98
+ )}
99
+ </Show>
100
+ <span class="min-w-0 flex-1 text-sm-medium [overflow-wrap:anywhere] text-ink-gray-7">
101
+ {stat.label}
102
+ </span>
103
+ <Show when={stat.delta}>
104
+ {(delta) => (
105
+ <span
106
+ class="inline-flex shrink-0 items-center gap-0.5 rounded-full px-1.5 py-0.5 text-xs-medium tabular-nums"
107
+ classList={{
108
+ 'bg-surface-green-2 text-ink-green-8':
109
+ delta().direction !== 'flat' && isGood(delta()),
110
+ 'bg-surface-red-2 text-ink-red-8':
111
+ delta().direction !== 'flat' && !isGood(delta()),
112
+ 'bg-surface-gray-2 text-ink-gray-7':
113
+ delta().direction === 'flat',
114
+ }}
115
+ >
116
+ <Icon
117
+ icon={
118
+ delta().direction === 'up'
119
+ ? 'lucide-arrow-up'
120
+ : delta().direction === 'down'
121
+ ? 'lucide-arrow-down'
122
+ : 'lucide-minus'
123
+ }
124
+ class="size-3"
125
+ />
126
+ {delta().value}
127
+ </span>
128
+ )}
129
+ </Show>
130
+ </div>
131
+ <span class="text-metric tabular-nums [overflow-wrap:anywhere] text-ink-gray-9">
132
+ {stat.value}
133
+ </span>
134
+ <Show when={stat.hint}>
135
+ <span class="text-xs [overflow-wrap:anywhere] text-ink-gray-7">
136
+ {stat.hint}
137
+ </span>
138
+ </Show>
139
+ </Dynamic>
140
+ )}
141
+ </For>
142
+ </div>
143
+ </Show>
144
+ )
145
+ }
146
+
147
+ /** A flat delta is neutral; otherwise the direction is good unless overridden. */
148
+ function isGood(delta: NonNullable<Stat['delta']>): boolean {
149
+ if (delta.good !== undefined) return delta.good
150
+ return delta.direction !== 'down'
151
+ }
@@ -0,0 +1 @@
1
+ export * from './StatGrid'
@@ -0,0 +1,80 @@
1
+ import { render, screen } from '@solidjs/testing-library'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { VirtualList } from './VirtualList'
4
+
5
+ const ROWS = Array.from({ length: 1000 }, (_, index) => ({
6
+ id: `ROW-${index + 1}`,
7
+ title: `Row ${index + 1}`,
8
+ }))
9
+
10
+ /**
11
+ * jsdom has no layout, so the viewport reports a zero rect and the virtualiser
12
+ * renders nothing. The tests give every element a 440px box, which is the only
13
+ * measurement the list needs.
14
+ */
15
+ class MeasuredObserver {
16
+ constructor(private readonly callback: ResizeObserverCallback) {}
17
+ observe(target: Element) {
18
+ const rect = target.getBoundingClientRect()
19
+ this.callback(
20
+ [
21
+ {
22
+ target,
23
+ contentRect: rect,
24
+ borderBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
25
+ contentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
26
+ } as unknown as ResizeObserverEntry,
27
+ ],
28
+ this as unknown as ResizeObserver,
29
+ )
30
+ }
31
+ unobserve() {}
32
+ disconnect() {}
33
+ }
34
+
35
+ function withLayout() {
36
+ const rect = {
37
+ width: 600,
38
+ height: 440,
39
+ top: 0,
40
+ left: 0,
41
+ right: 600,
42
+ bottom: 440,
43
+ x: 0,
44
+ y: 0,
45
+ toJSON: () => ({}),
46
+ } as DOMRect
47
+ const spy = vi
48
+ .spyOn(HTMLElement.prototype, 'getBoundingClientRect')
49
+ .mockReturnValue(rect)
50
+ // The jsdom shim observes nothing; the virtualiser needs one measurement.
51
+ vi.stubGlobal('ResizeObserver', MeasuredObserver)
52
+ return spy
53
+ }
54
+
55
+ afterEach(() => {
56
+ vi.unstubAllGlobals()
57
+ vi.restoreAllMocks()
58
+ })
59
+
60
+ describe('VirtualList', () => {
61
+ it('keeps a bounded DOM, with row positions declared for assistive tech', () => {
62
+ const spy = withLayout()
63
+ render(() => (
64
+ <VirtualList label="Rows" items={ROWS} itemSize={72} height="440px">
65
+ {(row) => <span>{row.title}</span>}
66
+ </VirtualList>
67
+ ))
68
+ const viewport = screen.getByRole('region', { name: 'Rows' })
69
+ expect(viewport.getAttribute('data-slot')).toBe('virtual-list')
70
+ expect(viewport.getAttribute('tabindex')).toBe('0')
71
+
72
+ const rows = viewport.querySelectorAll('li')
73
+ expect(rows.length).toBeGreaterThan(0)
74
+ expect(rows.length).toBeLessThan(30)
75
+ const first = rows[0] as HTMLLIElement
76
+ expect(first.getAttribute('aria-posinset')).toBe('1')
77
+ expect(first.getAttribute('aria-setsize')).toBe('1000')
78
+ spy.mockRestore()
79
+ })
80
+ })
@@ -0,0 +1,113 @@
1
+ import { createVirtualizer } from '@tanstack/solid-virtual'
2
+ import { For, type JSX, Show, untrack } from 'solid-js'
3
+ import { DataState } from '../DataState'
4
+
5
+ /**
6
+ * A bounded DOM for a long list.
7
+ *
8
+ * A feed of a thousand rows does not need a thousand nodes: this keeps about a
9
+ * viewport's worth mounted and positions them, which is what makes the task
10
+ * activity stream scroll to the end with fewer than thirty `li`s in the document.
11
+ *
12
+ * The scroll region is focusable on purpose — a region that scrolls and cannot
13
+ * be reached from the keyboard cannot be read past its first screen (WCAG
14
+ * 2.1.1), and that is the same contract `ScrollArea` documents. `Home`/`End`/
15
+ * arrows scroll it natively once it has focus.
16
+ *
17
+ * Rows are fixed-height by design: a virtualiser that has to measure every row
18
+ * to know where the next one starts defeats the point on the lists this is for.
19
+ */
20
+ export interface VirtualListProps<T> {
21
+ items: readonly T[]
22
+ /** Accessible name of the scroll region. */
23
+ label: string
24
+ /** Row height in pixels. Fixed, so the total height is known up front. */
25
+ itemSize: number
26
+ /** The viewport's height as a CSS length. Default `24rem`. */
27
+ height?: string
28
+ /** Rows rendered beyond the viewport. Default `6`. */
29
+ overscan?: number
30
+ /** Replaces the list with the shared loading skeleton. */
31
+ loading?: boolean
32
+ /** Replaces the list with the shared error state. */
33
+ error?: JSX.Element
34
+ onRetry?: () => void
35
+ emptyTitle?: JSX.Element
36
+ emptyDescription?: JSX.Element
37
+ emptyAction?: JSX.Element
38
+ /** One row's content; the `li` and its position belong to the list. */
39
+ children: (item: T, index: number) => JSX.Element
40
+ }
41
+
42
+ export function VirtualList<T>(props: VirtualListProps<T>): JSX.Element {
43
+ let viewport: HTMLElement | undefined
44
+ const virtualizer = createVirtualizer<HTMLElement, HTMLLIElement>({
45
+ get count() {
46
+ return props.items.length
47
+ },
48
+ getScrollElement: () => viewport ?? null,
49
+ estimateSize: () => props.itemSize,
50
+ // The virtualiser is created once; a changing overscan is not a supported
51
+ // dynamic input, and reading it untracked says so.
52
+ overscan: untrack(() => props.overscan) ?? 6,
53
+ })
54
+
55
+ const status = () =>
56
+ props.loading
57
+ ? ('loading' as const)
58
+ : props.error
59
+ ? ('error' as const)
60
+ : props.items.length === 0
61
+ ? ('empty' as const)
62
+ : ('ready' as const)
63
+
64
+ return (
65
+ <section
66
+ ref={viewport}
67
+ aria-label={props.label}
68
+ tabindex="0"
69
+ style={{ height: props.height ?? '24rem' }}
70
+ class="relative overflow-auto rounded-3 focus-visible:focus-ring"
71
+ data-slot="virtual-list"
72
+ >
73
+ <Show
74
+ when={status() === 'ready'}
75
+ fallback={
76
+ <DataState
77
+ status={status()}
78
+ skeleton="rows"
79
+ rows={5}
80
+ title={props.emptyTitle}
81
+ description={props.emptyDescription}
82
+ action={props.emptyAction}
83
+ error={props.error}
84
+ onRetry={props.onRetry}
85
+ />
86
+ }
87
+ >
88
+ <ul
89
+ class="relative m-0 list-none p-0"
90
+ style={{ height: `${virtualizer.getTotalSize()}px` }}
91
+ >
92
+ <For each={virtualizer.getVirtualItems()}>
93
+ {(row) => (
94
+ <li
95
+ class="absolute inset-x-0 top-0"
96
+ style={{
97
+ height: `${props.itemSize}px`,
98
+ transform: `translateY(${row.start}px)`,
99
+ }}
100
+ aria-posinset={row.index + 1}
101
+ aria-setsize={props.items.length}
102
+ >
103
+ {/* The virtualiser's count is the array's length, so the index
104
+ is a valid key for the mapped row. */}
105
+ {props.children(props.items[row.index] as T, row.index)}
106
+ </li>
107
+ )}
108
+ </For>
109
+ </ul>
110
+ </Show>
111
+ </section>
112
+ )
113
+ }
@@ -0,0 +1 @@
1
+ export * from './VirtualList'
@@ -0,0 +1,16 @@
1
+ export * from './AppSidebar'
2
+ export * from './AuthCard'
3
+ export * from './CommandPalette'
4
+ export * from './DataState'
5
+ export * from './DataTable'
6
+ export * from './DetailHeader'
7
+ export * from './FilterBar'
8
+ export * from './KanbanBoard'
9
+ export * from './MobileNav'
10
+ export * from './PageToolbar'
11
+ export * from './Section'
12
+ export * from './SelectionBar'
13
+ export * from './SettingsDialog'
14
+ export * from './Sidebar'
15
+ export * from './StatGrid'
16
+ export * from './VirtualList'