mvc-kit 2.12.0 → 2.12.1

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 (139) hide show
  1. package/agent-config/bin/postinstall.mjs +5 -3
  2. package/agent-config/bin/setup.mjs +3 -4
  3. package/agent-config/claude-code/agents/mvc-kit-architect.md +14 -0
  4. package/agent-config/claude-code/skills/guide/api-reference.md +24 -2
  5. package/agent-config/lib/install-claude.mjs +10 -33
  6. package/dist/Model.cjs +9 -1
  7. package/dist/Model.cjs.map +1 -1
  8. package/dist/Model.d.ts +1 -1
  9. package/dist/Model.d.ts.map +1 -1
  10. package/dist/Model.js +9 -1
  11. package/dist/Model.js.map +1 -1
  12. package/dist/ViewModel.cjs +9 -1
  13. package/dist/ViewModel.cjs.map +1 -1
  14. package/dist/ViewModel.d.ts +1 -1
  15. package/dist/ViewModel.d.ts.map +1 -1
  16. package/dist/ViewModel.js +9 -1
  17. package/dist/ViewModel.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/mvc-kit.cjs +3 -0
  21. package/dist/mvc-kit.cjs.map +1 -1
  22. package/dist/mvc-kit.js +3 -0
  23. package/dist/mvc-kit.js.map +1 -1
  24. package/dist/produceDraft.cjs +105 -0
  25. package/dist/produceDraft.cjs.map +1 -0
  26. package/dist/produceDraft.d.ts +19 -0
  27. package/dist/produceDraft.d.ts.map +1 -0
  28. package/dist/produceDraft.js +105 -0
  29. package/dist/produceDraft.js.map +1 -0
  30. package/package.json +4 -2
  31. package/src/Channel.md +408 -0
  32. package/src/Channel.test.ts +957 -0
  33. package/src/Channel.ts +429 -0
  34. package/src/Collection.md +533 -0
  35. package/src/Collection.test.ts +1559 -0
  36. package/src/Collection.ts +653 -0
  37. package/src/Controller.md +306 -0
  38. package/src/Controller.test.ts +380 -0
  39. package/src/Controller.ts +90 -0
  40. package/src/EventBus.md +308 -0
  41. package/src/EventBus.test.ts +295 -0
  42. package/src/EventBus.ts +110 -0
  43. package/src/Feed.md +218 -0
  44. package/src/Feed.test.ts +442 -0
  45. package/src/Feed.ts +101 -0
  46. package/src/Model.md +524 -0
  47. package/src/Model.test.ts +642 -0
  48. package/src/Model.ts +260 -0
  49. package/src/Pagination.md +168 -0
  50. package/src/Pagination.test.ts +244 -0
  51. package/src/Pagination.ts +92 -0
  52. package/src/Pending.md +380 -0
  53. package/src/Pending.test.ts +1719 -0
  54. package/src/Pending.ts +390 -0
  55. package/src/PersistentCollection.md +183 -0
  56. package/src/PersistentCollection.test.ts +649 -0
  57. package/src/PersistentCollection.ts +375 -0
  58. package/src/Resource.ViewModel.test.ts +503 -0
  59. package/src/Resource.md +239 -0
  60. package/src/Resource.test.ts +786 -0
  61. package/src/Resource.ts +231 -0
  62. package/src/Selection.md +155 -0
  63. package/src/Selection.test.ts +326 -0
  64. package/src/Selection.ts +117 -0
  65. package/src/Service.md +440 -0
  66. package/src/Service.test.ts +241 -0
  67. package/src/Service.ts +72 -0
  68. package/src/Sorting.md +170 -0
  69. package/src/Sorting.test.ts +334 -0
  70. package/src/Sorting.ts +135 -0
  71. package/src/Trackable.md +166 -0
  72. package/src/Trackable.test.ts +236 -0
  73. package/src/Trackable.ts +129 -0
  74. package/src/ViewModel.async.test.ts +813 -0
  75. package/src/ViewModel.derived.test.ts +1583 -0
  76. package/src/ViewModel.md +1111 -0
  77. package/src/ViewModel.test.ts +1236 -0
  78. package/src/ViewModel.ts +800 -0
  79. package/src/bindPublicMethods.test.ts +126 -0
  80. package/src/bindPublicMethods.ts +48 -0
  81. package/src/env.d.ts +5 -0
  82. package/src/errors.test.ts +155 -0
  83. package/src/errors.ts +133 -0
  84. package/src/index.ts +49 -0
  85. package/src/produceDraft.md +90 -0
  86. package/src/produceDraft.test.ts +394 -0
  87. package/src/produceDraft.ts +168 -0
  88. package/src/react/components/CardList.md +97 -0
  89. package/src/react/components/CardList.test.tsx +142 -0
  90. package/src/react/components/CardList.tsx +68 -0
  91. package/src/react/components/DataTable.md +179 -0
  92. package/src/react/components/DataTable.test.tsx +599 -0
  93. package/src/react/components/DataTable.tsx +267 -0
  94. package/src/react/components/InfiniteScroll.md +116 -0
  95. package/src/react/components/InfiniteScroll.test.tsx +218 -0
  96. package/src/react/components/InfiniteScroll.tsx +70 -0
  97. package/src/react/components/types.ts +90 -0
  98. package/src/react/derived.test.tsx +261 -0
  99. package/src/react/guards.ts +24 -0
  100. package/src/react/index.ts +40 -0
  101. package/src/react/provider.test.tsx +143 -0
  102. package/src/react/provider.tsx +55 -0
  103. package/src/react/strict-mode.test.tsx +266 -0
  104. package/src/react/types.ts +25 -0
  105. package/src/react/use-event-bus.md +214 -0
  106. package/src/react/use-event-bus.test.tsx +168 -0
  107. package/src/react/use-event-bus.ts +40 -0
  108. package/src/react/use-instance.md +204 -0
  109. package/src/react/use-instance.test.tsx +350 -0
  110. package/src/react/use-instance.ts +60 -0
  111. package/src/react/use-local.md +457 -0
  112. package/src/react/use-local.rapid-remount.test.tsx +503 -0
  113. package/src/react/use-local.test.tsx +692 -0
  114. package/src/react/use-local.ts +165 -0
  115. package/src/react/use-model.md +364 -0
  116. package/src/react/use-model.test.tsx +394 -0
  117. package/src/react/use-model.ts +161 -0
  118. package/src/react/use-singleton.md +415 -0
  119. package/src/react/use-singleton.test.tsx +296 -0
  120. package/src/react/use-singleton.ts +69 -0
  121. package/src/react/use-subscribe-only.ts +39 -0
  122. package/src/react/use-teardown.md +169 -0
  123. package/src/react/use-teardown.test.tsx +86 -0
  124. package/src/react/use-teardown.ts +27 -0
  125. package/src/react-native/NativeCollection.test.ts +250 -0
  126. package/src/react-native/NativeCollection.ts +138 -0
  127. package/src/react-native/index.ts +1 -0
  128. package/src/singleton.md +310 -0
  129. package/src/singleton.test.ts +204 -0
  130. package/src/singleton.ts +70 -0
  131. package/src/types.ts +70 -0
  132. package/src/walkPrototypeChain.ts +22 -0
  133. package/src/web/IndexedDBCollection.test.ts +235 -0
  134. package/src/web/IndexedDBCollection.ts +66 -0
  135. package/src/web/WebStorageCollection.test.ts +214 -0
  136. package/src/web/WebStorageCollection.ts +116 -0
  137. package/src/web/idb.ts +184 -0
  138. package/src/web/index.ts +2 -0
  139. package/src/wrapAsyncMethods.ts +249 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+ import { describe, it, expect } from 'vitest';
6
+ import { CardList } from './CardList';
7
+
8
+ interface Item {
9
+ id: string;
10
+ name: string;
11
+ }
12
+
13
+ const items: Item[] = [
14
+ { id: '1', name: 'Alice' },
15
+ { id: '2', name: 'Bob' },
16
+ { id: '3', name: 'Charlie' },
17
+ ];
18
+
19
+ describe('CardList', () => {
20
+ it('renders items', () => {
21
+ render(
22
+ <CardList
23
+ items={items}
24
+ renderItem={(item) => <span>{item.name}</span>}
25
+ />,
26
+ );
27
+ expect(screen.getByText('Alice')).toBeDefined();
28
+ expect(screen.getByText('Bob')).toBeDefined();
29
+ expect(screen.getByText('Charlie')).toBeDefined();
30
+ });
31
+
32
+ it('renders as list layout by default', () => {
33
+ const { container } = render(
34
+ <CardList
35
+ items={items}
36
+ renderItem={(item) => <span>{item.name}</span>}
37
+ />,
38
+ );
39
+ const ul = container.querySelector('[data-component="card-list"]')!;
40
+ expect(ul.getAttribute('data-layout')).toBe('list');
41
+ expect(ul.getAttribute('role')).toBe('list');
42
+ });
43
+
44
+ it('renders as grid layout with CSS custom properties', () => {
45
+ const { container } = render(
46
+ <CardList
47
+ items={items}
48
+ renderItem={(item) => <span>{item.name}</span>}
49
+ layout="grid"
50
+ columns={4}
51
+ gap="2rem"
52
+ />,
53
+ );
54
+ const ul = container.querySelector('[data-component="card-list"]')!;
55
+ expect(ul.getAttribute('data-layout')).toBe('grid');
56
+ expect((ul as HTMLElement).style.display).toBe('grid');
57
+ expect((ul as HTMLElement).style.gridTemplateColumns).toContain('4');
58
+ expect((ul as HTMLElement).style.gap).toContain('2rem');
59
+ });
60
+
61
+ it('uses custom keyOf', () => {
62
+ const { container } = render(
63
+ <CardList
64
+ items={[{ uid: 'x', label: 'X' }]}
65
+ keyOf={(item: any) => item.uid}
66
+ renderItem={(item: any) => <span>{item.label}</span>}
67
+ />,
68
+ );
69
+ expect(screen.getByText('X')).toBeDefined();
70
+ });
71
+
72
+ it('renders empty state', () => {
73
+ render(
74
+ <CardList
75
+ items={[]}
76
+ renderItem={() => <span>item</span>}
77
+ renderEmpty={() => <p>No items found</p>}
78
+ />,
79
+ );
80
+ expect(screen.getByText('No items found')).toBeDefined();
81
+ });
82
+
83
+ it('renders loading state', () => {
84
+ render(
85
+ <CardList
86
+ items={items}
87
+ loading={true}
88
+ renderItem={(item) => <span>{item.name}</span>}
89
+ renderLoading={() => <p>Loading data...</p>}
90
+ />,
91
+ );
92
+ expect(screen.getByText('Loading data...')).toBeDefined();
93
+ expect(screen.queryByText('Alice')).toBeNull();
94
+ });
95
+
96
+ it('renders error state', () => {
97
+ render(
98
+ <CardList
99
+ items={items}
100
+ error="Something went wrong"
101
+ renderItem={(item) => <span>{item.name}</span>}
102
+ renderError={(err) => <p>Error: {err}</p>}
103
+ />,
104
+ );
105
+ expect(screen.getByText('Error: Something went wrong')).toBeDefined();
106
+ expect(screen.queryByText('Alice')).toBeNull();
107
+ });
108
+
109
+ it('applies className', () => {
110
+ const { container } = render(
111
+ <CardList
112
+ items={items}
113
+ renderItem={(item) => <span>{item.name}</span>}
114
+ className="my-list"
115
+ />,
116
+ );
117
+ expect(container.querySelector('.my-list')).not.toBeNull();
118
+ });
119
+
120
+ it('applies aria-label', () => {
121
+ const { container } = render(
122
+ <CardList
123
+ items={items}
124
+ renderItem={(item) => <span>{item.name}</span>}
125
+ aria-label="User list"
126
+ />,
127
+ );
128
+ const ul = container.querySelector('[data-component="card-list"]')!;
129
+ expect(ul.getAttribute('aria-label')).toBe('User list');
130
+ });
131
+
132
+ it('passes index to renderItem', () => {
133
+ render(
134
+ <CardList
135
+ items={items}
136
+ renderItem={(item, index) => <span>{index}: {item.name}</span>}
137
+ />,
138
+ );
139
+ expect(screen.getByText('0: Alice')).toBeDefined();
140
+ expect(screen.getByText('2: Charlie')).toBeDefined();
141
+ });
142
+ });
@@ -0,0 +1,68 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AsyncStateProps } from './types';
3
+
4
+ /** Props for the CardList headless component. */
5
+ export interface CardListProps<T> extends AsyncStateProps {
6
+ items: T[];
7
+ renderItem: (item: T, index: number) => ReactNode;
8
+ keyOf?: (item: T) => string | number;
9
+ layout?: 'list' | 'grid';
10
+ columns?: number;
11
+ gap?: string;
12
+ renderEmpty?: () => ReactNode;
13
+ renderLoading?: () => ReactNode;
14
+ renderError?: (error: string) => ReactNode;
15
+ className?: string;
16
+ 'aria-label'?: string;
17
+ }
18
+
19
+ const defaultKeyOf = (item: any) => item.id;
20
+
21
+ /**
22
+ * Headless list/grid component with render-prop items.
23
+ * Renders a semantic `<ul>` with optional CSS grid layout.
24
+ */
25
+ export function CardList<T>({
26
+ items,
27
+ renderItem,
28
+ keyOf = defaultKeyOf,
29
+ layout = 'list',
30
+ columns = 3,
31
+ gap = '1rem',
32
+ loading,
33
+ error,
34
+ renderEmpty,
35
+ renderLoading,
36
+ renderError,
37
+ className,
38
+ 'aria-label': ariaLabel,
39
+ }: CardListProps<T>) {
40
+ if (loading && renderLoading) return <>{renderLoading()}</>;
41
+ if (error && renderError) return <>{renderError(error)}</>;
42
+ if (items.length === 0 && renderEmpty) return <>{renderEmpty()}</>;
43
+
44
+ const style = layout === 'grid'
45
+ ? {
46
+ display: 'grid',
47
+ gridTemplateColumns: `repeat(var(--card-list-columns, ${columns}), 1fr)`,
48
+ gap: `var(--card-list-gap, ${gap})`,
49
+ } as const
50
+ : undefined;
51
+
52
+ return (
53
+ <ul
54
+ role="list"
55
+ data-component="card-list"
56
+ data-layout={layout}
57
+ className={className}
58
+ aria-label={ariaLabel}
59
+ style={style}
60
+ >
61
+ {items.map((item, index) => (
62
+ <li key={keyOf(item)} data-index={index}>
63
+ {renderItem(item, index)}
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ );
68
+ }
@@ -0,0 +1,179 @@
1
+ # DataTable
2
+
3
+ Headless, unstyled table component with sort headers, row selection, and pagination support.
4
+
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Use DataTable for tabular data displays. It renders semantic HTML (`<table role="grid">`) with data attributes for styling hooks. No CSS included — bring your own styles.
10
+
11
+ ---
12
+
13
+ ## Basic Usage
14
+
15
+ Pass helpers directly — DataTable detects them via duck-typing:
16
+
17
+ ```tsx
18
+ import { DataTable } from 'mvc-kit/react';
19
+ import type { Column } from 'mvc-kit/react';
20
+
21
+ const columns: Column<User>[] = [
22
+ { key: 'name', header: 'Name', render: u => u.name, sortable: true },
23
+ { key: 'email', header: 'Email', render: u => u.email },
24
+ { key: 'role', header: 'Role', render: u => u.role, align: 'center' },
25
+ ];
26
+
27
+ function UsersTable() {
28
+ const [state, vm] = useLocal(UsersVM, { search: '' });
29
+ return (
30
+ <DataTable
31
+ items={vm.items}
32
+ columns={columns}
33
+ sort={vm.sorting}
34
+ selection={vm.selection}
35
+ pagination={vm.pagination}
36
+ paginationTotal={vm.filteredCount}
37
+ />
38
+ );
39
+ }
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Props
45
+
46
+ ### Required
47
+
48
+ | Prop | Type | Description |
49
+ |------|------|-------------|
50
+ | `items` | `T[]` | Array of data items to render |
51
+ | `columns` | `Column<T>[]` | Column definitions |
52
+
53
+ ### Column Definition
54
+
55
+ ```typescript
56
+ interface Column<T> {
57
+ key: string; // Unique identifier
58
+ header: ReactNode; // Header content
59
+ render: (item: T, index: number) => ReactNode; // Cell renderer
60
+ sortable?: boolean; // Enable sort header
61
+ width?: string; // Column width CSS
62
+ align?: 'left' | 'center' | 'right'; // Alignment
63
+ }
64
+ ```
65
+
66
+ ### Optional
67
+
68
+ | Prop | Type | Default | Description |
69
+ |------|------|---------|-------------|
70
+ | `keyOf` | `(item: T) => string \| number` | `item => item.id` | Key extractor |
71
+ | `pageSize` | `number` | — | Enables client-side pagination |
72
+ | `sort` | `SortDescriptor[] \| SortingHelper` | — | Sort descriptors or Sorting helper |
73
+ | `onSort` | `(key: string) => void` | — | Sort header click handler (not needed with SortingHelper) |
74
+ | `selection` | `SelectionState \| SelectionHelper` | — | Selection state or Selection helper |
75
+ | `pagination` | `PaginationState \| PaginationHelper` | — | Pagination state or Pagination helper |
76
+ | `paginationTotal` | `number` | — | Total item count (required with PaginationHelper) |
77
+ | `loading` | `boolean` | — | Loading state |
78
+ | `error` | `string \| null` | — | Error message |
79
+ | `className` | `string` | — | Container class |
80
+ | `aria-label` | `string` | — | Table accessibility label |
81
+
82
+ ### Render Slots
83
+
84
+ | Prop | Type | Description |
85
+ |------|------|-------------|
86
+ | `renderEmpty` | `() => ReactNode` | Empty state |
87
+ | `renderLoading` | `() => ReactNode` | Loading state |
88
+ | `renderError` | `(error: string) => ReactNode` | Error state |
89
+ | `renderSortIndicator` | `(props: SortHeaderProps) => ReactNode` | Custom sort indicator |
90
+ | `renderRow` | `(item, index, cells) => ReactNode` | Custom row wrapper |
91
+ | `renderPagination` | `(info: PaginationInfo) => ReactNode` | Pagination controls |
92
+
93
+ ---
94
+
95
+ ## Sort Headers
96
+
97
+ Pass a `Sorting` helper directly, or use the object-literal form with `sort` + `onSort`:
98
+
99
+ ```tsx
100
+ // Helper (recommended)
101
+ sort={vm.sorting}
102
+
103
+ // Object-literal (custom usage)
104
+ sort={vm.sorting.sorts}
105
+ onSort={key => vm.sorting.toggle(key)}
106
+ ```
107
+
108
+ When sort is provided, sortable columns render `<button>` elements with `aria-sort` attributes:
109
+
110
+ ```tsx
111
+ renderSortIndicator={({ active, direction }) => (
112
+ <span>{active ? (direction === 'asc' ? ' ↑' : ' ↓') : ''}</span>
113
+ )}
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Selection
119
+
120
+ Pass a `Selection` helper directly, or use the object-literal form:
121
+
122
+ ```tsx
123
+ // Helper (recommended) — DataTable passes allKeys to toggleAll automatically
124
+ selection={vm.selection}
125
+
126
+ // Object-literal (custom usage)
127
+ selection={{
128
+ selected: vm.selection.selected,
129
+ onToggle: id => vm.selection.toggle(id),
130
+ onToggleAll: allKeys => vm.selection.toggleAll(allKeys),
131
+ }}
132
+ ```
133
+
134
+ DataTable computes `allKeys` from the visible items and passes them to `onToggleAll()` in both paths — you never need to reconstruct the key set yourself.
135
+
136
+ ---
137
+
138
+ ## Pagination
139
+
140
+ Pass a `Pagination` helper directly (with `paginationTotal`), or use the object-literal form:
141
+
142
+ ```tsx
143
+ // Helper (recommended)
144
+ pagination={vm.pagination}
145
+ paginationTotal={vm.filteredCount}
146
+
147
+ // Object-literal (custom usage)
148
+ pagination={{
149
+ page: vm.pagination.page,
150
+ total: vm.filteredCount,
151
+ onPageChange: p => vm.pagination.setPage(p),
152
+ }}
153
+ pageSize={vm.pagination.pageSize}
154
+ ```
155
+
156
+ **Uncontrolled** (with `pageSize` only): Shows first page of items.
157
+
158
+ ```tsx
159
+ renderPagination={info => (
160
+ <nav>
161
+ <button disabled={!info.hasPrev} onClick={info.goPrev}>Prev</button>
162
+ <span>Page {info.page} of {info.pageCount}</span>
163
+ <button disabled={!info.hasNext} onClick={info.goNext}>Next</button>
164
+ </nav>
165
+ )}
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Data Attributes
171
+
172
+ | Attribute | Element | Description |
173
+ |-----------|---------|-------------|
174
+ | `data-component="data-table"` | Container `<div>` | Component identifier |
175
+ | `data-sortable` | `<th>` | Present on sortable columns |
176
+ | `data-sorted` | `<th>` | Present on actively sorted columns |
177
+ | `data-selected` | `<tr>` | Present on selected rows |
178
+ | `data-align` | `<th>`, `<td>` | Column alignment value |
179
+ | `data-column="select"` | `<th>`, `<td>` | Selection checkbox column |