@papernote/ui 1.3.1 → 1.6.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/dist/components/ActionBar.d.ts +112 -0
- package/dist/components/ActionBar.d.ts.map +1 -0
- package/dist/components/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +78 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PageHeader.d.ts +86 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +31 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1871 -51
- package/dist/index.esm.js +3025 -196
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3063 -194
- package/dist/index.js.map +1 -1
- package/dist/styles.css +434 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionBar.stories.tsx +246 -0
- package/src/components/ActionBar.tsx +242 -0
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +388 -0
- package/src/components/Modal.tsx +122 -4
- package/src/components/PageHeader.stories.tsx +198 -0
- package/src/components/PageHeader.tsx +217 -0
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +193 -10
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +69 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- package/tailwind.config.js +56 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import { Search, ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
|
+
import Checkbox from './Checkbox';
|
|
4
|
+
import Input from './Input';
|
|
5
|
+
|
|
6
|
+
export interface CheckboxListItem<T = unknown> {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
group?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
data?: T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CheckboxListProps<T = unknown> {
|
|
16
|
+
/** Array of items to display */
|
|
17
|
+
items: CheckboxListItem<T>[];
|
|
18
|
+
/** Currently selected item keys */
|
|
19
|
+
selectedKeys: string[];
|
|
20
|
+
/** Callback when selection changes */
|
|
21
|
+
onSelectionChange: (selectedKeys: string[]) => void;
|
|
22
|
+
|
|
23
|
+
// Grouping
|
|
24
|
+
/** Labels for groups (key -> display label) */
|
|
25
|
+
groupLabels?: Record<string, string>;
|
|
26
|
+
/** Keys of groups that should be expanded (controlled) */
|
|
27
|
+
expandedGroups?: string[];
|
|
28
|
+
/** Default expanded groups (uncontrolled) */
|
|
29
|
+
defaultExpandedGroups?: string[];
|
|
30
|
+
/** Callback when group expansion changes */
|
|
31
|
+
onGroupToggle?: (groupKey: string, expanded: boolean) => void;
|
|
32
|
+
|
|
33
|
+
// Search/Filter
|
|
34
|
+
/** Enable search functionality */
|
|
35
|
+
searchable?: boolean;
|
|
36
|
+
/** Search input placeholder */
|
|
37
|
+
searchPlaceholder?: string;
|
|
38
|
+
/** Custom filter function */
|
|
39
|
+
filterFn?: (item: CheckboxListItem<T>, searchTerm: string) => boolean;
|
|
40
|
+
/** Debounce delay for search in ms */
|
|
41
|
+
debounceMs?: number;
|
|
42
|
+
|
|
43
|
+
// Display options
|
|
44
|
+
/** Maximum height with overflow scroll */
|
|
45
|
+
maxHeight?: string | number;
|
|
46
|
+
/** Show select all checkbox */
|
|
47
|
+
showSelectAll?: boolean;
|
|
48
|
+
/** Select all label text */
|
|
49
|
+
selectAllLabel?: string;
|
|
50
|
+
/** Show count of selected items */
|
|
51
|
+
showSelectedCount?: boolean;
|
|
52
|
+
/** Message when no items available */
|
|
53
|
+
emptyMessage?: string;
|
|
54
|
+
/** Message when search has no results */
|
|
55
|
+
noResultsMessage?: string;
|
|
56
|
+
|
|
57
|
+
// Styling
|
|
58
|
+
/** Size variant */
|
|
59
|
+
size?: 'sm' | 'md' | 'lg';
|
|
60
|
+
/** Visual variant */
|
|
61
|
+
variant?: 'default' | 'bordered' | 'card';
|
|
62
|
+
/** Additional CSS classes */
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sizeClasses = {
|
|
67
|
+
sm: {
|
|
68
|
+
item: 'py-1.5 px-2',
|
|
69
|
+
text: 'text-sm',
|
|
70
|
+
description: 'text-xs',
|
|
71
|
+
groupHeader: 'py-1.5 px-2 text-xs',
|
|
72
|
+
},
|
|
73
|
+
md: {
|
|
74
|
+
item: 'py-2 px-3',
|
|
75
|
+
text: 'text-sm',
|
|
76
|
+
description: 'text-xs',
|
|
77
|
+
groupHeader: 'py-2 px-3 text-xs',
|
|
78
|
+
},
|
|
79
|
+
lg: {
|
|
80
|
+
item: 'py-3 px-4',
|
|
81
|
+
text: 'text-base',
|
|
82
|
+
description: 'text-sm',
|
|
83
|
+
groupHeader: 'py-2.5 px-4 text-sm',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const variantClasses = {
|
|
88
|
+
default: 'bg-white',
|
|
89
|
+
bordered: 'bg-white border border-paper-300 rounded-lg',
|
|
90
|
+
card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* CheckboxList - Multi-select list with checkboxes, grouping, and search
|
|
95
|
+
*
|
|
96
|
+
* @example Basic usage
|
|
97
|
+
* ```tsx
|
|
98
|
+
* <CheckboxList
|
|
99
|
+
* items={[
|
|
100
|
+
* { key: '1', label: 'Option 1' },
|
|
101
|
+
* { key: '2', label: 'Option 2' },
|
|
102
|
+
* ]}
|
|
103
|
+
* selectedKeys={selected}
|
|
104
|
+
* onSelectionChange={setSelected}
|
|
105
|
+
* />
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @example With grouping and search
|
|
109
|
+
* ```tsx
|
|
110
|
+
* <CheckboxList
|
|
111
|
+
* items={fields}
|
|
112
|
+
* selectedKeys={selectedFields}
|
|
113
|
+
* onSelectionChange={setSelectedFields}
|
|
114
|
+
* groupLabels={{ table1: 'Users', table2: 'Orders' }}
|
|
115
|
+
* searchable
|
|
116
|
+
* searchPlaceholder="Search fields..."
|
|
117
|
+
* showSelectAll
|
|
118
|
+
* maxHeight="300px"
|
|
119
|
+
* />
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export default function CheckboxList<T = unknown>({
|
|
123
|
+
items,
|
|
124
|
+
selectedKeys,
|
|
125
|
+
onSelectionChange,
|
|
126
|
+
groupLabels = {},
|
|
127
|
+
expandedGroups: controlledExpandedGroups,
|
|
128
|
+
defaultExpandedGroups,
|
|
129
|
+
onGroupToggle,
|
|
130
|
+
searchable = false,
|
|
131
|
+
searchPlaceholder = 'Search...',
|
|
132
|
+
filterFn,
|
|
133
|
+
debounceMs = 150,
|
|
134
|
+
maxHeight,
|
|
135
|
+
showSelectAll = false,
|
|
136
|
+
selectAllLabel = 'Select All',
|
|
137
|
+
showSelectedCount = false,
|
|
138
|
+
emptyMessage = 'No items available',
|
|
139
|
+
noResultsMessage = 'No items match your search',
|
|
140
|
+
size = 'md',
|
|
141
|
+
variant = 'default',
|
|
142
|
+
className = '',
|
|
143
|
+
}: CheckboxListProps<T>) {
|
|
144
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
145
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
|
146
|
+
const [internalExpandedGroups, setInternalExpandedGroups] = useState<Set<string>>(
|
|
147
|
+
new Set(defaultExpandedGroups || [])
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Debounce search
|
|
151
|
+
const handleSearchChange = useCallback((value: string) => {
|
|
152
|
+
setSearchTerm(value);
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
setDebouncedSearchTerm(value);
|
|
155
|
+
}, debounceMs);
|
|
156
|
+
return () => clearTimeout(timer);
|
|
157
|
+
}, [debounceMs]);
|
|
158
|
+
|
|
159
|
+
// Filter items based on search
|
|
160
|
+
const filteredItems = useMemo(() => {
|
|
161
|
+
if (!debouncedSearchTerm) return items;
|
|
162
|
+
|
|
163
|
+
const term = debouncedSearchTerm.toLowerCase();
|
|
164
|
+
return items.filter(item => {
|
|
165
|
+
if (filterFn) {
|
|
166
|
+
return filterFn(item, debouncedSearchTerm);
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
item.label.toLowerCase().includes(term) ||
|
|
170
|
+
item.description?.toLowerCase().includes(term) ||
|
|
171
|
+
item.key.toLowerCase().includes(term)
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
}, [items, debouncedSearchTerm, filterFn]);
|
|
175
|
+
|
|
176
|
+
// Group items
|
|
177
|
+
const groupedItems = useMemo(() => {
|
|
178
|
+
const groups = new Map<string | null, CheckboxListItem<T>[]>();
|
|
179
|
+
|
|
180
|
+
filteredItems.forEach(item => {
|
|
181
|
+
const groupKey = item.group || null;
|
|
182
|
+
if (!groups.has(groupKey)) {
|
|
183
|
+
groups.set(groupKey, []);
|
|
184
|
+
}
|
|
185
|
+
groups.get(groupKey)!.push(item);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return groups;
|
|
189
|
+
}, [filteredItems]);
|
|
190
|
+
|
|
191
|
+
// Determine expanded groups
|
|
192
|
+
const expandedGroups = controlledExpandedGroups
|
|
193
|
+
? new Set(controlledExpandedGroups)
|
|
194
|
+
: internalExpandedGroups;
|
|
195
|
+
|
|
196
|
+
const handleGroupToggle = (groupKey: string) => {
|
|
197
|
+
const newExpanded = !expandedGroups.has(groupKey);
|
|
198
|
+
|
|
199
|
+
if (!controlledExpandedGroups) {
|
|
200
|
+
setInternalExpandedGroups(prev => {
|
|
201
|
+
const next = new Set(prev);
|
|
202
|
+
if (newExpanded) {
|
|
203
|
+
next.add(groupKey);
|
|
204
|
+
} else {
|
|
205
|
+
next.delete(groupKey);
|
|
206
|
+
}
|
|
207
|
+
return next;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
onGroupToggle?.(groupKey, newExpanded);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Selection handlers
|
|
215
|
+
const handleItemToggle = (key: string) => {
|
|
216
|
+
const newSelected = selectedKeys.includes(key)
|
|
217
|
+
? selectedKeys.filter(k => k !== key)
|
|
218
|
+
: [...selectedKeys, key];
|
|
219
|
+
onSelectionChange(newSelected);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleSelectAll = () => {
|
|
223
|
+
const enabledItems = filteredItems.filter(item => !item.disabled);
|
|
224
|
+
const allSelected = enabledItems.every(item => selectedKeys.includes(item.key));
|
|
225
|
+
|
|
226
|
+
if (allSelected) {
|
|
227
|
+
// Deselect all filtered items
|
|
228
|
+
const filteredKeys = new Set(enabledItems.map(item => item.key));
|
|
229
|
+
onSelectionChange(selectedKeys.filter(key => !filteredKeys.has(key)));
|
|
230
|
+
} else {
|
|
231
|
+
// Select all filtered items
|
|
232
|
+
const newKeys = new Set([...selectedKeys, ...enabledItems.map(item => item.key)]);
|
|
233
|
+
onSelectionChange(Array.from(newKeys));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const sizeStyle = sizeClasses[size];
|
|
238
|
+
const enabledItems = filteredItems.filter(item => !item.disabled);
|
|
239
|
+
const allSelected = enabledItems.length > 0 && enabledItems.every(item => selectedKeys.includes(item.key));
|
|
240
|
+
const someSelected = enabledItems.some(item => selectedKeys.includes(item.key)) && !allSelected;
|
|
241
|
+
|
|
242
|
+
// Check if we have groups
|
|
243
|
+
const hasGroups = groupedItems.size > 1 || (groupedItems.size === 1 && !groupedItems.has(null));
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className={`${variantClasses[variant]} ${className}`}>
|
|
247
|
+
{/* Search Input */}
|
|
248
|
+
{searchable && (
|
|
249
|
+
<div className={`${sizeStyle.item} border-b border-paper-200`}>
|
|
250
|
+
<Input
|
|
251
|
+
value={searchTerm}
|
|
252
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
253
|
+
placeholder={searchPlaceholder}
|
|
254
|
+
prefixIcon={<Search className="h-4 w-4" />}
|
|
255
|
+
size={size === 'lg' ? 'md' : 'sm'}
|
|
256
|
+
clearable
|
|
257
|
+
onClear={() => {
|
|
258
|
+
setSearchTerm('');
|
|
259
|
+
setDebouncedSearchTerm('');
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Select All & Count */}
|
|
266
|
+
{(showSelectAll || showSelectedCount) && filteredItems.length > 0 && (
|
|
267
|
+
<div className={`${sizeStyle.item} border-b border-paper-200 flex items-center justify-between`}>
|
|
268
|
+
{showSelectAll && (
|
|
269
|
+
<Checkbox
|
|
270
|
+
checked={allSelected}
|
|
271
|
+
indeterminate={someSelected}
|
|
272
|
+
onChange={handleSelectAll}
|
|
273
|
+
label={selectAllLabel}
|
|
274
|
+
size={size}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
{showSelectedCount && (
|
|
278
|
+
<span className={`${sizeStyle.description} text-ink-500`}>
|
|
279
|
+
{selectedKeys.length} selected
|
|
280
|
+
</span>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* List Content */}
|
|
286
|
+
<div
|
|
287
|
+
className="overflow-y-auto"
|
|
288
|
+
style={{ maxHeight: maxHeight || undefined }}
|
|
289
|
+
>
|
|
290
|
+
{/* Empty State */}
|
|
291
|
+
{items.length === 0 && (
|
|
292
|
+
<div className={`${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`}>
|
|
293
|
+
{emptyMessage}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* No Results */}
|
|
298
|
+
{items.length > 0 && filteredItems.length === 0 && (
|
|
299
|
+
<div className={`${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`}>
|
|
300
|
+
{noResultsMessage}
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{/* Grouped Items */}
|
|
305
|
+
{hasGroups ? (
|
|
306
|
+
Array.from(groupedItems.entries()).map(([groupKey, groupItems]) => {
|
|
307
|
+
if (groupKey === null) {
|
|
308
|
+
// Ungrouped items
|
|
309
|
+
return groupItems.map(item => (
|
|
310
|
+
<CheckboxListItemRow
|
|
311
|
+
key={item.key}
|
|
312
|
+
item={item}
|
|
313
|
+
selected={selectedKeys.includes(item.key)}
|
|
314
|
+
onToggle={() => handleItemToggle(item.key)}
|
|
315
|
+
size={size}
|
|
316
|
+
sizeStyle={sizeStyle}
|
|
317
|
+
/>
|
|
318
|
+
));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const isExpanded = expandedGroups.has(groupKey);
|
|
322
|
+
const groupLabel = groupLabels[groupKey] || groupKey;
|
|
323
|
+
const groupSelectedCount = groupItems.filter(item => selectedKeys.includes(item.key)).length;
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div key={groupKey}>
|
|
327
|
+
{/* Group Header */}
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
onClick={() => handleGroupToggle(groupKey)}
|
|
331
|
+
className={`
|
|
332
|
+
w-full flex items-center justify-between
|
|
333
|
+
${sizeStyle.groupHeader}
|
|
334
|
+
font-medium text-ink-700 bg-paper-50
|
|
335
|
+
hover:bg-paper-100 transition-colors
|
|
336
|
+
border-b border-paper-200
|
|
337
|
+
`}
|
|
338
|
+
>
|
|
339
|
+
<div className="flex items-center gap-2">
|
|
340
|
+
{isExpanded ? (
|
|
341
|
+
<ChevronDown className="h-4 w-4 text-ink-400" />
|
|
342
|
+
) : (
|
|
343
|
+
<ChevronRight className="h-4 w-4 text-ink-400" />
|
|
344
|
+
)}
|
|
345
|
+
<span>{groupLabel}</span>
|
|
346
|
+
</div>
|
|
347
|
+
<span className="text-ink-400 font-normal">
|
|
348
|
+
{groupSelectedCount > 0 && `${groupSelectedCount}/`}{groupItems.length}
|
|
349
|
+
</span>
|
|
350
|
+
</button>
|
|
351
|
+
|
|
352
|
+
{/* Group Items */}
|
|
353
|
+
{isExpanded && (
|
|
354
|
+
<div>
|
|
355
|
+
{groupItems.map(item => (
|
|
356
|
+
<CheckboxListItemRow
|
|
357
|
+
key={item.key}
|
|
358
|
+
item={item}
|
|
359
|
+
selected={selectedKeys.includes(item.key)}
|
|
360
|
+
onToggle={() => handleItemToggle(item.key)}
|
|
361
|
+
size={size}
|
|
362
|
+
sizeStyle={sizeStyle}
|
|
363
|
+
indented
|
|
364
|
+
/>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
})
|
|
371
|
+
) : (
|
|
372
|
+
// Flat list (no groups)
|
|
373
|
+
filteredItems.map(item => (
|
|
374
|
+
<CheckboxListItemRow
|
|
375
|
+
key={item.key}
|
|
376
|
+
item={item}
|
|
377
|
+
selected={selectedKeys.includes(item.key)}
|
|
378
|
+
onToggle={() => handleItemToggle(item.key)}
|
|
379
|
+
size={size}
|
|
380
|
+
sizeStyle={sizeStyle}
|
|
381
|
+
/>
|
|
382
|
+
))
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Helper component for rendering individual items
|
|
390
|
+
function CheckboxListItemRow<T>({
|
|
391
|
+
item,
|
|
392
|
+
selected,
|
|
393
|
+
onToggle,
|
|
394
|
+
size,
|
|
395
|
+
sizeStyle,
|
|
396
|
+
indented = false,
|
|
397
|
+
}: {
|
|
398
|
+
item: CheckboxListItem<T>;
|
|
399
|
+
selected: boolean;
|
|
400
|
+
onToggle: () => void;
|
|
401
|
+
size: 'sm' | 'md' | 'lg';
|
|
402
|
+
sizeStyle: typeof sizeClasses['md'];
|
|
403
|
+
indented?: boolean;
|
|
404
|
+
}) {
|
|
405
|
+
return (
|
|
406
|
+
<div
|
|
407
|
+
className={`
|
|
408
|
+
${sizeStyle.item}
|
|
409
|
+
${indented ? 'pl-8' : ''}
|
|
410
|
+
hover:bg-paper-50 transition-colors
|
|
411
|
+
border-b border-paper-100 last:border-b-0
|
|
412
|
+
${item.disabled ? 'opacity-50' : ''}
|
|
413
|
+
`}
|
|
414
|
+
>
|
|
415
|
+
<div className="flex items-start gap-3">
|
|
416
|
+
<Checkbox
|
|
417
|
+
checked={selected}
|
|
418
|
+
onChange={onToggle}
|
|
419
|
+
disabled={item.disabled}
|
|
420
|
+
size={size}
|
|
421
|
+
/>
|
|
422
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
423
|
+
<span className={sizeStyle.text}>{item.label}</span>
|
|
424
|
+
{item.description && (
|
|
425
|
+
<span className={`${sizeStyle.description} text-ink-500`}>
|
|
426
|
+
{item.description}
|
|
427
|
+
</span>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|