@warkypublic/svelix 0.1.43 → 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.
Files changed (27) hide show
  1. package/dist/components/CardGrid/CardGrid.svelte +1312 -0
  2. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  3. package/dist/components/CardGrid/CardGridFilterPanel.svelte +299 -0
  4. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  5. package/dist/components/CardGrid/DefaultCard.svelte +303 -0
  6. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  7. package/dist/components/CardGrid/ImageCardStory.svelte +257 -0
  8. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  9. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  10. package/dist/components/CardGrid/cardGrid.js +21 -0
  11. package/dist/components/CardGrid/index.d.ts +4 -0
  12. package/dist/components/CardGrid/index.js +4 -0
  13. package/dist/components/Gridler/components/GridlerCanvas.svelte +1 -0
  14. package/dist/components/Gridler/components/GridlerFull.svelte +4 -1
  15. package/dist/components/Gridler/components/GridlerSearch.svelte +3 -3
  16. package/dist/components/Gridler/components/GridlerSearch.svelte.d.ts +3 -1
  17. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -0
  18. package/dist/components/Gridler/types.d.ts +2 -12
  19. package/dist/components/Gridler/utils/cellContent.js +2 -1
  20. package/dist/components/Gridler/utils/filters.js +2 -2
  21. package/dist/components/Gridler/utils/sort.js +2 -1
  22. package/dist/components/Types/generic_grid.d.ts +73 -5
  23. package/dist/components/index.d.ts +1 -0
  24. package/dist/components/index.js +1 -0
  25. package/llm/COMPONENT_GUIDE.md +240 -0
  26. package/llm/plans/card_grid.plan.md +305 -0
  27. package/package.json +19 -18
@@ -767,6 +767,246 @@ Story behavior worth preserving:
767
767
  - markdown, Monaco/text, media, and PDF flows are all real supported paths
768
768
  - office-document integrations expose `insertText` and `insertHtml` hooks for external automation
769
769
 
770
+ ### CardGrid: virtualized card-based data grid
771
+
772
+ Based on:
773
+ - [src/lib/components/CardGrid/CardGrid.svelte](/home/hein/dev/svelix/src/lib/components/CardGrid/CardGrid.svelte)
774
+ - [src/lib/components/CardGrid/CardGrid.stories.ts](/home/hein/dev/svelix/src/lib/components/CardGrid/CardGrid.stories.ts)
775
+
776
+ A responsive, virtualized card grid with search, sort, filter, multi-select, context menus, and optional flip mode. Renders a toolbar above a scrollable grid of cards. Supports local data, async local data, and server-side paging via ResolveSpec or HeaderSpec adapters — the same adapter layer used by `GridlerFull`.
777
+
778
+ The grid requires a bounded height on its scroll container. Always place it inside an element that has an explicit height, or use the `height: 100%` + flex approach shown below.
779
+
780
+ #### Minimal local-data example
781
+
782
+ ```svelte
783
+ <script lang="ts">
784
+ import { CardGrid } from '@warkypublic/svelix';
785
+ import type { CardGridColumn } from '@warkypublic/svelix';
786
+
787
+ const columns: CardGridColumn[] = [
788
+ { id: 'id', title: 'ID', disableSearch: true },
789
+ { id: 'name', title: 'Name' },
790
+ { id: 'role', title: 'Role' },
791
+ ];
792
+
793
+ const data = [
794
+ { id: 1, name: 'Alice Smith', role: 'Admin' },
795
+ { id: 2, name: 'Bob Jones', role: 'Viewer' },
796
+ ];
797
+ </script>
798
+
799
+ <div style="height: 600px; display: flex; flex-direction: column;">
800
+ <CardGrid {columns} {data} pageSize={20} searchPlaceholder="Search…" />
801
+ </div>
802
+ ```
803
+
804
+ #### Props reference
805
+
806
+ | Prop | Type | Default | Description |
807
+ |---|---|---|---|
808
+ | `columns` | `CardGridColumn[]` | — | Column definitions. **Required.** |
809
+ | `displayColumns` | `string[]` | — | Subset of column IDs to show; omit to show all. |
810
+ | `cardMinWidth` | `number` | `280` | Minimum card width in px. Grid fills available columns using `auto-fill`. |
811
+ | `cardGap` | `number` | `16` | Gap between cards in px. |
812
+ | `cardMaxFrontColumns` | `number` | — | Enables flip mode: first N columns on the front face, the rest on the back. |
813
+ | `card` | `Snippet` | — | Custom front-face card snippet. Receives `(record, index, selected, focused)`. |
814
+ | `cardBack` | `Snippet` | — | Custom back-face snippet for flip mode. Same signature as `card`. |
815
+ | `empty` | `Snippet` | — | Rendered when the data set is empty. |
816
+ | `skeleton` | `Snippet` | — | Rendered while the initial load is in flight. |
817
+ | `data` | `Record<string,unknown>[] \| (() => Promise<Record<string,unknown>[]>)` | — | Local array or async factory. Used when `dataSource` is `'local'`. |
818
+ | `dataSource` | `'local' \| 'resolvespec' \| 'headerspec'` | `'local'` | Data source adapter. |
819
+ | `dataSourceOptions` | `object` | — | Adapter config — see table below. |
820
+ | `pageSize` | `number` | `20` | Rows per page / server fetch page size. |
821
+ | `uniqueID` | `string` | `'id'` | Field used for identity in selection and cursor paging. Overridden by `dataSourceOptions.uniqueID`. |
822
+ | `searchValue` | `string` | — | Controlled search string. |
823
+ | `onSearchValueChange` | `(v: string) => void` | — | Fires on every search input change. |
824
+ | `serverSideSearch` | `boolean` | `true` | When true, search sends `ilike` filters to the server instead of filtering locally. |
825
+ | `searchColumns` | `string[]` | — | Column IDs to search. Defaults to all searchable columns. |
826
+ | `searchPlaceholder` | `string` | `'Search…'` | Placeholder text in the search input. |
827
+ | `sortOptions` | `CardSortOption[]` | — | Sort options shown in the toolbar sort menu. |
828
+ | `sortOrder` | `GridColumnSortOrder` | — | Controlled sort state. |
829
+ | `onSortOrderChange` | `(order: GridColumnSortOrder) => void` | — | Fires when sort changes. |
830
+ | `filters` | `GridColumnFilters` | — | Controlled filter state. |
831
+ | `onFilterChange` | `(filters: GridColumnFilters) => void` | — | Fires when filters are applied or cleared. |
832
+ | `selectedItems` | `Record<string,unknown>[]` | — | Controlled selection. |
833
+ | `onSelectedItemsChange` | `(items: Record<string,unknown>[]) => void` | — | Fires when selection changes. |
834
+ | `multiSelect` | `boolean` | `true` | `false` = single-select mode. Arrow keys move the selection. |
835
+ | `menuItems` | `CardGridContextMenuItem[]` | — | Custom context menu items appended to the built-in ones. |
836
+ | `onCardClick` | `(record, index) => void` | — | Fires on single click. |
837
+ | `onCardDblClick` | `(record, index) => void` | — | Fires on double click. |
838
+ | `onCardContextMenu` | `(record, index, x, y) => void` | — | Fires on right-click. `x`/`y` are viewport coordinates. |
839
+ | `total` | `number` | bindable | Server-reported total count. |
840
+ | `loading` | `boolean` | bindable | `true` while any fetch is in flight. |
841
+ | `onLoadError` | `(error: string) => void` | — | Fires on adapter fetch failure. |
842
+ | `toolbarEnd` | `Snippet` | — | Extra content rendered at the right end of the toolbar. |
843
+ | `class` | `string` | — | Extra CSS class on the root element. |
844
+
845
+ #### `CardGridColumn` fields
846
+
847
+ | Field | Type | Description |
848
+ |---|---|---|
849
+ | `id` | `string` | Unique column identifier. Also used as the field key unless `dataKey` is set. |
850
+ | `title` | `string` | Display label. |
851
+ | `dataKey` | `string` | Dot-notation path into the record (e.g. `'login.contact.email'`). Defaults to `id`. |
852
+ | `format` | `'currency' \| 'number' \| 'percentage' \| 'date' \| 'datetime'` | Value formatter applied in the default card renderer. |
853
+ | `disableSort` | `boolean` | Excludes this column from sort options. |
854
+ | `disableFilter` | `boolean` | Excludes this column from the filter panel. |
855
+ | `disableSearch` | `boolean` | Excludes this column from local/server search. |
856
+
857
+ #### `dataSourceOptions` fields
858
+
859
+ | Field | Description |
860
+ |---|---|
861
+ | `url` | API base URL. |
862
+ | `authToken` | Bearer token sent in `Authorization` header. |
863
+ | `schema` | Database schema (ResolveSpec). |
864
+ | `entity` | Table / entity name (ResolveSpec). |
865
+ | `uniqueID` | Unique field for cursor paging. |
866
+ | `hotfields` | Extra fields fetched but not rendered. |
867
+ | `extraOptions` | Passed through to the adapter as `Partial<Options>` — same shape as `GridlerFull`. |
868
+
869
+ #### Server-side paging
870
+
871
+ ```svelte
872
+ <CardGrid
873
+ {columns}
874
+ {sortOptions}
875
+ dataSource="resolvespec"
876
+ dataSourceOptions={{
877
+ url: 'https://api.example.com',
878
+ schema: 'public',
879
+ entity: 'employees',
880
+ uniqueID: 'id',
881
+ }}
882
+ pageSize={20}
883
+ serverSideSearch={true}
884
+ searchColumns={['name', 'email']}
885
+ />
886
+ ```
887
+
888
+ Cards are loaded a page at a time. Scrolling to the bottom appends the next page automatically. `total` and `loading` are bindable and update after each fetch.
889
+
890
+ #### Flip mode (default card renderer)
891
+
892
+ Set `cardMaxFrontColumns` to split columns across front and back faces. A `+N` badge appears on cards that have overflow columns; clicking it flips the card to show the remaining fields.
893
+
894
+ ```svelte
895
+ <CardGrid
896
+ {columns}
897
+ {data}
898
+ cardMaxFrontColumns={3}
899
+ />
900
+ ```
901
+
902
+ The flip button and the back button are always in the same bottom-right position on their respective faces.
903
+
904
+ #### Custom card snippets
905
+
906
+ Use `card` and `cardBack` snippets to replace the default renderer with fully custom markup. Both receive `(record, index, selected, focused)`.
907
+
908
+ ```svelte
909
+ <CardGrid {columns} {data} cardMaxFrontColumns={3} pageSize={20}>
910
+ {#snippet card(record, _i, selected)}
911
+ <article class:selected>
912
+ <img src={record.imageUrl as string} alt={record.title as string} />
913
+ <p>{record.title as string}</p>
914
+ </article>
915
+ {/snippet}
916
+
917
+ {#snippet cardBack(record, _i, selected)}
918
+ <article class:selected>
919
+ <p>Views: {(record.views as number).toLocaleString()}</p>
920
+ <p>Likes: {(record.likes as number).toLocaleString()}</p>
921
+ </article>
922
+ {/snippet}
923
+ </CardGrid>
924
+ ```
925
+
926
+ When `cardBack` is provided without `card`, the default renderer is used for the front face and the custom snippet for the back.
927
+
928
+ #### Custom card snippets from a `.ts` story file
929
+
930
+ Svelte snippets cannot be written directly in a `.ts` file. Wrap them in a `.svelte` wrapper component and render that component from the story:
931
+
932
+ ```typescript
933
+ // MyStory.stories.ts
934
+ import WrapperComponent from './WrapperComponent.svelte';
935
+
936
+ export const Custom: Story = {
937
+ render: () => ({ Component: WrapperComponent }),
938
+ };
939
+ ```
940
+
941
+ ```svelte
942
+ <!-- WrapperComponent.svelte -->
943
+ <script lang="ts">
944
+ import CardGrid from './CardGrid.svelte';
945
+ const data = [...];
946
+ const columns = [...];
947
+ </script>
948
+
949
+ <CardGrid {columns} {data} cardMaxFrontColumns={3}>
950
+ {#snippet card(record, _i, selected, focused)}
951
+ ...
952
+ {/snippet}
953
+ </CardGrid>
954
+ ```
955
+
956
+ #### Multi-select keyboard shortcuts
957
+
958
+ | Key | Behaviour |
959
+ |---|---|
960
+ | `Space` / `Enter` | Toggle selection on the focused card. |
961
+ | `Arrow keys` | Move focus. In `multiSelect: false` mode, also moves selection. |
962
+ | `Ctrl+A` / `Cmd+A` | Select all loaded cards (multi-select only). |
963
+ | `Shift+Click` | Range-select from the last anchor to the clicked card. |
964
+ | `Ctrl+Click` | Toggle a single card without clearing others. |
965
+
966
+ #### Storybook height decorator
967
+
968
+ The virtualizer requires a bounded scroll container. In Storybook, apply this decorator to give the root element a height:
969
+
970
+ ```typescript
971
+ const heightDecorator = (Story) => {
972
+ const root = document.getElementById('storybook-root');
973
+ if (root) root.style.cssText = 'height:calc(100vh - 40px);display:flex;flex-direction:column;';
974
+ document.body.style.margin = '0';
975
+ return Story();
976
+ };
977
+
978
+ const meta = {
979
+ decorators: [heightDecorator],
980
+ ...
981
+ };
982
+ ```
983
+
984
+ #### Large local datasets in stories
985
+
986
+ Generate large datasets lazily to avoid crashing Storybook at module-load time:
987
+
988
+ ```typescript
989
+ let rows: ReturnType<typeof makeRow>[] | null = null;
990
+ function getRows() {
991
+ if (!rows) rows = Array.from({ length: 100_000 }, (_, i) => makeRow(i));
992
+ return rows;
993
+ }
994
+
995
+ export const HundredThousandRows: Story = {
996
+ render: (args) => ({ Component: CardGrid, props: { ...args, data: getRows() } }),
997
+ };
998
+ ```
999
+
1000
+ Story behaviors worth preserving:
1001
+ - `StaticData`: local array, verifies rendering, context menu, and search
1002
+ - `ServerSideCursorPaging`: cursor-forward paging, verifies infinite scroll
1003
+ - `ServerSideSearch`: `ilike` filters sent on every keystroke
1004
+ - `ServerError`: `onLoadError` fires on non-2xx response
1005
+ - `MultiSelect` / `SingleSelect`: selection modes and keyboard shortcuts
1006
+ - `FlipMode`: `cardMaxFrontColumns` splits columns, flip/back buttons match position
1007
+ - `ImageCards`: fully custom `card` + `cardBack` snippets via a wrapper `.svelte` component
1008
+ - `HundredThousandRows`: virtualizer handles 100 000 rows; only visible rows are in the DOM
1009
+
770
1010
  ### BetterMenu: interaction-first menu behavior
771
1011
 
772
1012
  Based on:
@@ -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.43",
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
  }