@warkypublic/svelix 0.1.44 → 0.1.46
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/CardGrid/CardGrid.svelte +1312 -0
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte +299 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
- package/dist/components/CardGrid/DefaultCard.svelte +303 -0
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
- package/dist/components/CardGrid/ImageCardStory.svelte +257 -0
- package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
- package/dist/components/CardGrid/cardGrid.d.ts +33 -0
- package/dist/components/CardGrid/cardGrid.js +21 -0
- package/dist/components/CardGrid/index.d.ts +4 -0
- package/dist/components/CardGrid/index.js +4 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/llm/COMPONENT_GUIDE.md +240 -0
- package/llm/plans/card_grid.plan.md +305 -0
- package/package.json +19 -18
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# CardGrid Component — Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The project has `GridlerFull` for tabular data but no card-based equivalent. We need a `CardGrid` component that mirrors most GridlerFull features (sorting, filtering, search, context menus, data fetching) but renders records as responsive cards with **selection + animation** and **keyboard navigation**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
All under `src/lib/components/CardGrid/`:
|
|
12
|
+
|
|
13
|
+
| File | Lines (est.) | Role |
|
|
14
|
+
|------|-------------|------|
|
|
15
|
+
| `cardGrid.ts` | ~80 | Types + pure helpers (selection, grid math) |
|
|
16
|
+
| `DefaultCard.svelte` | ~40 | Fallback card renderer |
|
|
17
|
+
| `CardGridFilterPanel.svelte` | ~120 | Filter UI (reuses GridlerFull operator set) |
|
|
18
|
+
| `CardGrid.svelte` | ~450 | Main component |
|
|
19
|
+
| `index.ts` | ~10 | Barrel exports |
|
|
20
|
+
|
|
21
|
+
Also update: `src/lib/components/index.ts` (add `export * from './CardGrid/index'`)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Reuse From Existing Code
|
|
26
|
+
|
|
27
|
+
| What | Source | How |
|
|
28
|
+
|------|--------|-----|
|
|
29
|
+
| `GridlerResolveSpecAdapter` | `src/lib/components/Gridler/adapters/GridlerResolveSpecAdapter.ts` | Import & instantiate directly |
|
|
30
|
+
| `GridlerRestHeaderSpecAdapter` | `src/lib/components/Gridler/adapters/GridlerRestHeaderSpecAdapter.ts` | Same |
|
|
31
|
+
| `GridlerAdapterConfig`, `GridlerPageResult` | `src/lib/components/Gridler/types.ts` | Re-export from cardGrid.ts |
|
|
32
|
+
| `gridColumnFiltersToFilterOptions()` | `src/lib/components/Gridler/utils/filters.ts` | Import for server-side filters |
|
|
33
|
+
| `buildSearchFilters()` | `src/lib/components/Gridler/utils/filters.ts` | Import — always pass `searchColumns` explicitly to avoid GridlerColumn type mismatch |
|
|
34
|
+
| `sortOrderToSortOptions()` | `src/lib/components/Gridler/utils/sort.ts` | Import for server-side sort |
|
|
35
|
+
| `GridColumnSortOrder`, `GridColumnFilters`, `FilterOperator` | `src/lib/components/Types/generic_grid.ts` | Re-export |
|
|
36
|
+
| `BetterMenu` | `src/lib/components/BetterMenu/` | Context menu + filter panel popup |
|
|
37
|
+
| Data fetch pattern | `GridlerFull.svelte` lines 550-700 | Port adapter creation, fetchPage, loadNextPage, cancellation |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 1. `cardGrid.ts` — Types & Helpers
|
|
42
|
+
|
|
43
|
+
### Types
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export interface CardGridColumn {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
dataKey?: string; // field path; defaults to id
|
|
50
|
+
format?: GridColumnFormat;
|
|
51
|
+
disableSort?: boolean;
|
|
52
|
+
disableFilter?: boolean;
|
|
53
|
+
disableSearch?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CardSortOption {
|
|
57
|
+
columnId: string;
|
|
58
|
+
label: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CardGridContextMenuItem {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
icon?: string;
|
|
66
|
+
kind?: 'item' | 'separator';
|
|
67
|
+
onselect?: (rowData?: Record<string, unknown>) => void;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Pure Helpers
|
|
72
|
+
|
|
73
|
+
- `toggleItemInSelection(items, item, uniqueID)` — add/remove by uniqueID match
|
|
74
|
+
- `rangeSelect(allItems, anchorIndex, targetIndex)` — slice between anchor and target
|
|
75
|
+
- `isItemSelected(selectedItems, item, uniqueID)` — boolean check
|
|
76
|
+
- `computeColumnsInRow(containerWidth, cardMinWidth, gap)` — `Math.max(1, Math.floor((w + gap) / (minW + gap)))`
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 2. `CardGrid.svelte` — Props
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
interface Props {
|
|
84
|
+
// Column/display
|
|
85
|
+
columns: CardGridColumn[];
|
|
86
|
+
displayColumns?: string[];
|
|
87
|
+
cardMinWidth?: number; // default 280
|
|
88
|
+
cardGap?: number; // default 16
|
|
89
|
+
|
|
90
|
+
// Custom rendering — note: card snippet gets selected+focused state
|
|
91
|
+
card?: Snippet<[Record<string, unknown>, number, boolean, boolean]>;
|
|
92
|
+
empty?: Snippet;
|
|
93
|
+
skeleton?: Snippet;
|
|
94
|
+
|
|
95
|
+
// Data source (mirrors GridlerFull pattern)
|
|
96
|
+
dataSource?: 'resolvespec' | 'headerspec' | 'local';
|
|
97
|
+
dataSourceOptions?: { url, authToken, schema, entity, uniqueID, hotfields, extraOptions };
|
|
98
|
+
data?: Record<string, unknown>[] | (() => Promise<Record<string, unknown>[]>);
|
|
99
|
+
pageSize?: number; // default 20
|
|
100
|
+
uniqueID?: string; // default 'id'
|
|
101
|
+
|
|
102
|
+
// Search
|
|
103
|
+
searchValue?: string;
|
|
104
|
+
onSearchValueChange?: (v: string) => void;
|
|
105
|
+
serverSideSearch?: boolean; // default true
|
|
106
|
+
searchColumns?: string[];
|
|
107
|
+
searchPlaceholder?: string;
|
|
108
|
+
|
|
109
|
+
// Sort
|
|
110
|
+
sortOrder?: GridColumnSortOrder;
|
|
111
|
+
onSortOrderChange?: (order: GridColumnSortOrder) => void;
|
|
112
|
+
sortOptions?: CardSortOption[];
|
|
113
|
+
|
|
114
|
+
// Filters
|
|
115
|
+
filters?: GridColumnFilters;
|
|
116
|
+
onFilterChange?: (filters: GridColumnFilters) => void;
|
|
117
|
+
|
|
118
|
+
// Selection
|
|
119
|
+
selectedItems?: Record<string, unknown>[];
|
|
120
|
+
onSelectedItemsChange?: (items: Record<string, unknown>[]) => void;
|
|
121
|
+
multiSelect?: boolean; // default true
|
|
122
|
+
|
|
123
|
+
// Card events
|
|
124
|
+
onCardClick?: (record, index) => void;
|
|
125
|
+
onCardDblClick?: (record, index) => void;
|
|
126
|
+
onCardContextMenu?: (record, index, x, y) => void;
|
|
127
|
+
|
|
128
|
+
// Context menu
|
|
129
|
+
menuItems?: CardGridContextMenuItem[];
|
|
130
|
+
|
|
131
|
+
// Status
|
|
132
|
+
total?: number; // bindable
|
|
133
|
+
loading?: boolean; // bindable
|
|
134
|
+
onLoadError?: (error: string) => void;
|
|
135
|
+
|
|
136
|
+
// Layout
|
|
137
|
+
class?: string;
|
|
138
|
+
toolbarEnd?: Snippet;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 3. State Management
|
|
145
|
+
|
|
146
|
+
**Controlled/uncontrolled pairs** (same pattern as GridlerFull):
|
|
147
|
+
- `searchValue` / `internalSearchValue` → `resolvedSearchValue`
|
|
148
|
+
- `filters` / `internalFilters` → `resolvedFilters`
|
|
149
|
+
- `sortOrder` / `internalSortOrder` → `resolvedSortOrder`
|
|
150
|
+
- `selectedItems` / `internalSelectedItems` → `resolvedSelectedItems`
|
|
151
|
+
|
|
152
|
+
**Server state** (port of GridlerFull lines 537-546):
|
|
153
|
+
- `serverData`, `serverTotal`, `serverCursor`, `serverFetchVersion`, `serverLoading`, `serverAllLoaded`
|
|
154
|
+
|
|
155
|
+
**CardGrid-specific state**:
|
|
156
|
+
- `focusedIndex: $state(-1)` — keyboard-focused card
|
|
157
|
+
- `anchorIndex: $state(-1)` — shift+click range anchor
|
|
158
|
+
- `columnsInRow: $state(1)` — via ResizeObserver
|
|
159
|
+
- `containerEl`, `sentinelEl`, `searchInputEl` — element refs
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 4. Data Fetching (Port of GridlerFull)
|
|
164
|
+
|
|
165
|
+
- **Adapter instantiation**: `$derived.by` — create `GridlerResolveSpecAdapter` or `GridlerRestHeaderSpecAdapter` from `dataSourceOptions`
|
|
166
|
+
- **Initial fetch `$effect`**: triggers on adapter/sort/search/filters/refreshTrigger changes → reset state → `fetchPage()`
|
|
167
|
+
- **`fetchPage()`**: calls `adapter.readPage(pageSize, cursor, sort, allFilters, fields)` with cancellation via `serverFetchVersion`
|
|
168
|
+
- **`loadNextPage()`**: appends results, updates cursor
|
|
169
|
+
- **Infinite scroll**: IntersectionObserver on sentinel div with `rootMargin: '200px'`
|
|
170
|
+
- **`refresh()` export**: increment `refreshTrigger`
|
|
171
|
+
|
|
172
|
+
Filter construction reuses `gridColumnFiltersToFilterOptions()` + `buildSearchFilters()`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 5. Selection
|
|
177
|
+
|
|
178
|
+
**Click logic**:
|
|
179
|
+
- **Plain click**: select single card, set anchor
|
|
180
|
+
- **Ctrl/Cmd+click**: toggle card in/out of selection
|
|
181
|
+
- **Shift+click**: range select from anchor to target
|
|
182
|
+
- **Background click** (`e.target === e.currentTarget`): clear selection
|
|
183
|
+
- **Escape**: clear selection
|
|
184
|
+
|
|
185
|
+
**Bindable output**: `selectedItems` array + `onSelectedItemsChange` callback
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 6. Selection Animation (CSS Transitions)
|
|
190
|
+
|
|
191
|
+
```css
|
|
192
|
+
.cg-card {
|
|
193
|
+
transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease;
|
|
194
|
+
border: 2px solid transparent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cg-card-selected {
|
|
198
|
+
border-color: var(--color-primary-500);
|
|
199
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-primary-500) 25%, transparent);
|
|
200
|
+
transform: scale(1.02);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.cg-card-focused {
|
|
204
|
+
outline: 2px solid var(--color-primary-400);
|
|
205
|
+
outline-offset: 2px;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Selected = animated ring + subtle scale lift. Focused = outline ring (distinct from selection).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 7. Keyboard Navigation
|
|
214
|
+
|
|
215
|
+
Container gets `tabindex="0"` + `onkeydown`. `columnsInRow` computed via ResizeObserver.
|
|
216
|
+
|
|
217
|
+
| Key | Action |
|
|
218
|
+
|-----|--------|
|
|
219
|
+
| ←/→ | Move focus left/right |
|
|
220
|
+
| ↑/↓ | Move focus up/down by `columnsInRow` |
|
|
221
|
+
| Enter | Trigger `onCardClick` on focused card |
|
|
222
|
+
| Space | Toggle selection on focused card |
|
|
223
|
+
| Escape | Clear selection, blur focus |
|
|
224
|
+
| Tab/Shift+Tab | Move focus forward/backward |
|
|
225
|
+
| Ctrl+A | Select all loaded cards |
|
|
226
|
+
| Ctrl+F | Focus search input |
|
|
227
|
+
|
|
228
|
+
After focus change: `scrollIntoView({ block: 'nearest', behavior: 'smooth' })`.
|
|
229
|
+
|
|
230
|
+
Search input stops keydown propagation for its own keys to avoid grid nav conflicts.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 8. Filter Panel (`CardGridFilterPanel.svelte`)
|
|
235
|
+
|
|
236
|
+
- Opened via toolbar "Filters" button → BetterMenu popup (snippet-renderer pattern from GridlerFull line 406)
|
|
237
|
+
- Shows filterable columns with operator dropdown (same 14 operators as GridlerFull)
|
|
238
|
+
- Add/remove/edit filter rows
|
|
239
|
+
- Apply/Cancel/Clear buttons
|
|
240
|
+
- Emits updated `GridColumnFilters` object
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 9. Context Menu
|
|
245
|
+
|
|
246
|
+
- Right-click on card → `BetterMenu.show()` with card data
|
|
247
|
+
- Built-in items: Refresh, Clear Search (if active), Clear Filters (if active), Clear Sort (if active)
|
|
248
|
+
- Custom items from `menuItems` prop prepended with separator
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 10. Template Structure
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
┌─ Toolbar ────────────────────────────────────────────────┐
|
|
256
|
+
│ [Search input] [Sort pills...] [Filters (n)] [↻] N items │
|
|
257
|
+
└──────────────────────────────────────────────────────────┘
|
|
258
|
+
┌─ Card Grid (role="listbox", tabindex="0") ──────────────┐
|
|
259
|
+
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
|
260
|
+
│ │ Card │ │ Card │ │ Card │ │ Card │ ← auto-fill grid │
|
|
261
|
+
│ └──────┘ └──────┘ └──────┘ └──────┘ │
|
|
262
|
+
│ ┌──────┐ ┌──────┐ ┌──────┐ │
|
|
263
|
+
│ │ Card │ │ Card │ │ Card │ │
|
|
264
|
+
│ └──────┘ └──────┘ └──────┘ │
|
|
265
|
+
│ [sentinel] │
|
|
266
|
+
└──────────────────────────────────────────────────────────┘
|
|
267
|
+
┌─ Page loading indicator ─────────────────────────────────┐
|
|
268
|
+
│ Loading more... │
|
|
269
|
+
└──────────────────────────────────────────────────────────┘
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
States: skeleton (first load), empty, cards, loading-more (subsequent pages).
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Implementation Order
|
|
277
|
+
|
|
278
|
+
1. `cardGrid.ts` — types + helpers (no Svelte deps)
|
|
279
|
+
2. `DefaultCard.svelte` — simple presentational
|
|
280
|
+
3. `CardGridFilterPanel.svelte` — self-contained filter UI
|
|
281
|
+
4. `CardGrid.svelte` — main component (depends on all above + Gridler adapters/utils)
|
|
282
|
+
5. `index.ts` — barrel exports
|
|
283
|
+
6. Update `src/lib/components/index.ts`
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Verification
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
cd packages/svelix && pnpm run check # TypeScript + Svelte type check
|
|
291
|
+
cd packages/svelix && pnpm run test # Vitest
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Manual: drop `<CardGrid>` into a page with a known schema/entity. Verify:
|
|
295
|
+
- Cards render with data
|
|
296
|
+
- Search filters results server-side
|
|
297
|
+
- Sort pills toggle direction, re-fetch
|
|
298
|
+
- Filter panel applies/clears filters
|
|
299
|
+
- Click selects card with animation
|
|
300
|
+
- Ctrl+click multi-selects
|
|
301
|
+
- Shift+click range selects
|
|
302
|
+
- Arrow keys navigate, Space toggles, Enter activates
|
|
303
|
+
- Scroll to bottom loads next page
|
|
304
|
+
- Right-click shows context menu
|
|
305
|
+
- Refresh button reloads data
|
package/package.json
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warkypublic/svelix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "Svelte 5 component library with Skeleton UI and Tailwind CSS",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev",
|
|
8
|
+
"build": "vite build && pnpm run package",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"package": "svelte-kit sync && svelte-package && mkdir -p dist/css && cp src/lib/css/tailwind-source.css dist/css/tailwind-source.css && publint",
|
|
11
|
+
"prepublishOnly": "npm run package",
|
|
12
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
13
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
14
|
+
"storybook": "storybook dev -p 6006",
|
|
15
|
+
"build-storybook": "storybook build",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"lint:fix": "eslint . --fix",
|
|
18
|
+
"test": "vitest run --project unit",
|
|
19
|
+
"test:unit": "vitest run --project unit",
|
|
20
|
+
"test:watch": "vitest --project unit",
|
|
21
|
+
"test:e2e": "playwright test",
|
|
22
|
+
"test:e2e:ui": "playwright test --ui"
|
|
23
|
+
},
|
|
6
24
|
"exports": {
|
|
7
25
|
".": {
|
|
8
26
|
"types": "./dist/index.d.ts",
|
|
@@ -84,22 +102,5 @@
|
|
|
84
102
|
"isomorphic-dompurify": "^3.13.0",
|
|
85
103
|
"katex": "^0.16.47",
|
|
86
104
|
"monaco-editor": "^0.55.1"
|
|
87
|
-
},
|
|
88
|
-
"scripts": {
|
|
89
|
-
"dev": "vite dev",
|
|
90
|
-
"build": "vite build && pnpm run package",
|
|
91
|
-
"preview": "vite preview",
|
|
92
|
-
"package": "svelte-kit sync && svelte-package && mkdir -p dist/css && cp src/lib/css/tailwind-source.css dist/css/tailwind-source.css && publint",
|
|
93
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
94
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
95
|
-
"storybook": "storybook dev -p 6006",
|
|
96
|
-
"build-storybook": "storybook build",
|
|
97
|
-
"lint": "eslint .",
|
|
98
|
-
"lint:fix": "eslint . --fix",
|
|
99
|
-
"test": "vitest run --project unit",
|
|
100
|
-
"test:unit": "vitest run --project unit",
|
|
101
|
-
"test:watch": "vitest --project unit",
|
|
102
|
-
"test:e2e": "playwright test",
|
|
103
|
-
"test:e2e:ui": "playwright test --ui"
|
|
104
105
|
}
|
|
105
106
|
}
|