@talelk/newui-organisms 1.0.0 → 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.
- package/package.json +3 -3
- package/src/organisms/AppSidebar/AppSidebar.test.tsx +36 -0
- package/src/organisms/AppSidebar/AppSidebar.tsx +57 -12
- package/src/organisms/AuthCard/AuthCard.tsx +41 -32
- package/src/organisms/CommandPalette/CommandPalette.tsx +13 -2
- package/src/organisms/DataState/DataState.test.tsx +45 -0
- package/src/organisms/DataState/DataState.tsx +111 -0
- package/src/organisms/DataState/index.ts +6 -0
- package/src/organisms/DataTable/DataTable.test.tsx +73 -1
- package/src/organisms/DataTable/DataTable.tsx +426 -148
- package/src/organisms/DetailHeader/DetailHeader.tsx +9 -1
- package/src/organisms/FilterBar/FilterBar.test.tsx +2 -1
- package/src/organisms/FilterBar/FilterBar.tsx +24 -5
- package/src/organisms/KanbanBoard/KanbanBoard.tsx +220 -165
- package/src/organisms/MobileNav/MobileNav.tsx +1 -1
- package/src/organisms/PageToolbar/PageToolbar.tsx +63 -14
- package/src/organisms/Section/Section.test.tsx +41 -0
- package/src/organisms/Section/Section.tsx +108 -0
- package/src/organisms/Section/index.ts +1 -0
- package/src/organisms/SelectionBar/SelectionBar.test.tsx +35 -0
- package/src/organisms/SelectionBar/SelectionBar.tsx +84 -0
- package/src/organisms/SelectionBar/index.ts +5 -0
- package/src/organisms/SettingsDialog/SettingsDialog.tsx +13 -4
- package/src/organisms/Sidebar/Sidebar.tsx +17 -10
- package/src/organisms/StatGrid/StatGrid.test.tsx +13 -1
- package/src/organisms/StatGrid/StatGrid.tsx +98 -71
- package/src/organisms/VirtualList/VirtualList.tsx +56 -22
- package/src/organisms/index.ts +3 -0
- package/src/organisms/Sidebar/__screenshots__/Sidebar.browser.test.tsx/Sidebar--browser--real-CSS--reserves-its-declared-width-1.png +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cn, Icon, type IconProp } from '@talelk/newui-atoms'
|
|
1
|
+
import { cn, Icon, type IconProp, Skeleton } from '@talelk/newui-atoms'
|
|
2
2
|
import { For, type JSX, Show } from 'solid-js'
|
|
3
3
|
import { Dynamic } from 'solid-js/web'
|
|
4
4
|
|
|
@@ -17,6 +17,10 @@ export interface StatGridProps {
|
|
|
17
17
|
stats: Stat[]
|
|
18
18
|
/** Columns at the widest breakpoint. Defaults to the number of stats, capped. */
|
|
19
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
|
|
20
24
|
class?: string
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -34,9 +38,16 @@ export interface StatGridProps {
|
|
|
34
38
|
* to fail it.
|
|
35
39
|
*/
|
|
36
40
|
const COLUMNS: Record<2 | 3 | 4, string> = {
|
|
37
|
-
2: '
|
|
38
|
-
3: '
|
|
39
|
-
4: '
|
|
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',
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export function StatGrid(props: StatGridProps): JSX.Element {
|
|
@@ -44,76 +55,92 @@ export function StatGrid(props: StatGridProps): JSX.Element {
|
|
|
44
55
|
props.columns ?? (Math.min(4, Math.max(2, props.stats.length)) as 2 | 3 | 4)
|
|
45
56
|
|
|
46
57
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
71
|
>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
stat
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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}
|
|
76
133
|
</span>
|
|
77
|
-
<Show when={stat.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
classList={{
|
|
82
|
-
'bg-surface-green-2 text-ink-green-8':
|
|
83
|
-
delta().direction !== 'flat' && isGood(delta()),
|
|
84
|
-
'bg-surface-red-2 text-ink-red-8':
|
|
85
|
-
delta().direction !== 'flat' && !isGood(delta()),
|
|
86
|
-
'bg-surface-gray-2 text-ink-gray-7':
|
|
87
|
-
delta().direction === 'flat',
|
|
88
|
-
}}
|
|
89
|
-
>
|
|
90
|
-
<Icon
|
|
91
|
-
icon={
|
|
92
|
-
delta().direction === 'up'
|
|
93
|
-
? 'lucide-arrow-up'
|
|
94
|
-
: delta().direction === 'down'
|
|
95
|
-
? 'lucide-arrow-down'
|
|
96
|
-
: 'lucide-minus'
|
|
97
|
-
}
|
|
98
|
-
class="size-3"
|
|
99
|
-
/>
|
|
100
|
-
{delta().value}
|
|
101
|
-
</span>
|
|
102
|
-
)}
|
|
134
|
+
<Show when={stat.hint}>
|
|
135
|
+
<span class="text-xs [overflow-wrap:anywhere] text-ink-gray-7">
|
|
136
|
+
{stat.hint}
|
|
137
|
+
</span>
|
|
103
138
|
</Show>
|
|
104
|
-
</
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<span class="text-xs [overflow-wrap:anywhere] text-ink-gray-7">
|
|
110
|
-
{stat.hint}
|
|
111
|
-
</span>
|
|
112
|
-
</Show>
|
|
113
|
-
</Dynamic>
|
|
114
|
-
)}
|
|
115
|
-
</For>
|
|
116
|
-
</div>
|
|
139
|
+
</Dynamic>
|
|
140
|
+
)}
|
|
141
|
+
</For>
|
|
142
|
+
</div>
|
|
143
|
+
</Show>
|
|
117
144
|
)
|
|
118
145
|
}
|
|
119
146
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createVirtualizer } from '@tanstack/solid-virtual'
|
|
2
|
-
import { For, type JSX, untrack } from 'solid-js'
|
|
2
|
+
import { For, type JSX, Show, untrack } from 'solid-js'
|
|
3
|
+
import { DataState } from '../DataState'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A bounded DOM for a long list.
|
|
@@ -26,6 +27,14 @@ export interface VirtualListProps<T> {
|
|
|
26
27
|
height?: string
|
|
27
28
|
/** Rows rendered beyond the viewport. Default `6`. */
|
|
28
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
|
|
29
38
|
/** One row's content; the `li` and its position belong to the list. */
|
|
30
39
|
children: (item: T, index: number) => JSX.Element
|
|
31
40
|
}
|
|
@@ -43,6 +52,15 @@ export function VirtualList<T>(props: VirtualListProps<T>): JSX.Element {
|
|
|
43
52
|
overscan: untrack(() => props.overscan) ?? 6,
|
|
44
53
|
})
|
|
45
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
|
+
|
|
46
64
|
return (
|
|
47
65
|
<section
|
|
48
66
|
ref={viewport}
|
|
@@ -52,28 +70,44 @@ export function VirtualList<T>(props: VirtualListProps<T>): JSX.Element {
|
|
|
52
70
|
class="relative overflow-auto rounded-3 focus-visible:focus-ring"
|
|
53
71
|
data-slot="virtual-list"
|
|
54
72
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|
|
58
87
|
>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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>
|
|
77
111
|
</section>
|
|
78
112
|
)
|
|
79
113
|
}
|
package/src/organisms/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export * from './AppSidebar'
|
|
2
2
|
export * from './AuthCard'
|
|
3
3
|
export * from './CommandPalette'
|
|
4
|
+
export * from './DataState'
|
|
4
5
|
export * from './DataTable'
|
|
5
6
|
export * from './DetailHeader'
|
|
6
7
|
export * from './FilterBar'
|
|
7
8
|
export * from './KanbanBoard'
|
|
8
9
|
export * from './MobileNav'
|
|
9
10
|
export * from './PageToolbar'
|
|
11
|
+
export * from './Section'
|
|
12
|
+
export * from './SelectionBar'
|
|
10
13
|
export * from './SettingsDialog'
|
|
11
14
|
export * from './Sidebar'
|
|
12
15
|
export * from './StatGrid'
|