@mikestools/usetable 0.0.1 → 0.0.2

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.
@@ -0,0 +1,474 @@
1
+ <template>
2
+ <section id="basicTable">
3
+ <h2 class="h4 mb-3">
4
+ <IconTable class="me-2" />
5
+ Product Inventory Dashboard
6
+ </h2>
7
+ <p class="text-muted mb-4">
8
+ A comprehensive example demonstrating useTable's core features: sorting, filtering, selection,
9
+ inline editing, aggregation, and undo/redo — all reactive and type-safe.
10
+ </p>
11
+
12
+ <!-- Toolbar -->
13
+ <div class="card shadow-sm mb-3">
14
+ <div class="card-body py-2">
15
+ <div class="row g-2 align-items-center">
16
+ <!-- Search -->
17
+ <div class="col-lg-3">
18
+ <div class="input-group input-group-sm">
19
+ <span class="input-group-text">
20
+ <IconSearch />
21
+ </span>
22
+ <input
23
+ v-model="searchQuery"
24
+ type="text"
25
+ class="form-control"
26
+ placeholder="Search products..."
27
+ @input="handleSearch"
28
+ >
29
+ <button
30
+ v-if="searchQuery"
31
+ class="btn btn-outline-secondary"
32
+ type="button"
33
+ @click="clearSearch"
34
+ >
35
+ <IconX />
36
+ </button>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Category Filter -->
41
+ <div class="col-lg-2">
42
+ <select
43
+ v-model="categoryFilter"
44
+ class="form-select form-select-sm"
45
+ @change="handleCategoryFilter"
46
+ >
47
+ <option value="">
48
+ All Categories
49
+ </option>
50
+ <option
51
+ v-for="cat in categories"
52
+ :key="cat"
53
+ :value="cat"
54
+ >
55
+ {{ cat }}
56
+ </option>
57
+ </select>
58
+ </div>
59
+
60
+ <!-- Actions -->
61
+ <div class="col-lg-7">
62
+ <div class="d-flex gap-2 justify-content-end flex-wrap">
63
+ <button
64
+ class="btn btn-primary btn-sm"
65
+ @click="addProduct"
66
+ >
67
+ <IconPlus class="me-1" />
68
+ Add Product
69
+ </button>
70
+ <button
71
+ class="btn btn-outline-danger btn-sm"
72
+ :disabled="selectedCount === 0"
73
+ @click="deleteSelected"
74
+ >
75
+ <IconTrash class="me-1" />
76
+ Delete ({{ selectedCount }})
77
+ </button>
78
+ <div class="btn-group">
79
+ <button
80
+ class="btn btn-outline-secondary btn-sm"
81
+ :disabled="!canUndo"
82
+ title="Undo (Ctrl+Z)"
83
+ @click="handleUndo"
84
+ >
85
+ <IconArrowCounterclockwise />
86
+ </button>
87
+ <button
88
+ class="btn btn-outline-secondary btn-sm"
89
+ :disabled="!canRedo"
90
+ title="Redo (Ctrl+Y)"
91
+ @click="handleRedo"
92
+ >
93
+ <IconArrowClockwise />
94
+ </button>
95
+ </div>
96
+ <button
97
+ class="btn btn-outline-secondary btn-sm"
98
+ @click="resetTable"
99
+ >
100
+ <IconArrowRepeat class="me-1" />
101
+ Reset
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Table -->
110
+ <div class="card shadow-sm mb-3">
111
+ <div class="table-responsive">
112
+ <table
113
+ ref="tableRef"
114
+ class="table table-hover table-striped mb-0 align-middle"
115
+ >
116
+ <!-- Table structure built via useTable -->
117
+ </table>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- Footer Stats -->
122
+ <div class="card shadow-sm mb-4">
123
+ <div class="card-body py-2">
124
+ <div class="row text-center">
125
+ <div class="col">
126
+ <div class="small text-muted">
127
+ Products
128
+ </div>
129
+ <div class="fw-bold">
130
+ {{ rowCount }}
131
+ </div>
132
+ </div>
133
+ <div class="col">
134
+ <div class="small text-muted">
135
+ Total Stock
136
+ </div>
137
+ <div class="fw-bold">
138
+ {{ totalStock.toLocaleString() }}
139
+ </div>
140
+ </div>
141
+ <div class="col">
142
+ <div class="small text-muted">
143
+ Total Value
144
+ </div>
145
+ <div class="fw-bold text-success">
146
+ ${{ totalValue.toLocaleString() }}
147
+ </div>
148
+ </div>
149
+ <div class="col">
150
+ <div class="small text-muted">
151
+ Avg Price
152
+ </div>
153
+ <div class="fw-bold">
154
+ ${{ avgPrice.toFixed(2) }}
155
+ </div>
156
+ </div>
157
+ <div class="col">
158
+ <div class="small text-muted">
159
+ Low Stock
160
+ </div>
161
+ <div
162
+ class="fw-bold"
163
+ :class="lowStockCount > 0 ? 'text-warning' : 'text-success'"
164
+ >
165
+ {{ lowStockCount }}
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- Instructions -->
173
+ <div class="alert alert-info mb-4">
174
+ <strong>Try it:</strong>
175
+ <ul class="mb-0 small">
176
+ <li><strong>Sort:</strong> Click column headers</li>
177
+ <li><strong>Select:</strong> Click rows (Shift+click for range, Ctrl+click for multi)</li>
178
+ <li><strong>Edit:</strong> Double-click any cell to edit inline</li>
179
+ <li><strong>Search:</strong> Filter products in real-time</li>
180
+ <li><strong>Undo/Redo:</strong> Ctrl+Z / Ctrl+Y or toolbar buttons</li>
181
+ </ul>
182
+ </div>
183
+
184
+ <!-- Code Example -->
185
+ <CodeBlock :code="codeExample" />
186
+ </section>
187
+ </template>
188
+
189
+ <script setup lang="ts">
190
+ import { ref, onMounted, onUnmounted, watch } from 'vue'
191
+ import { useTable } from '@mikestools/usetable'
192
+
193
+ import CodeBlock from '@mikestools/usetools/components/CodeBlock.vue'
194
+ import IconTable from 'bootstrap-icons/icons/table.svg?component'
195
+ import IconSearch from 'bootstrap-icons/icons/search.svg?component'
196
+ import IconX from 'bootstrap-icons/icons/x-lg.svg?component'
197
+ import IconPlus from 'bootstrap-icons/icons/plus-lg.svg?component'
198
+ import IconTrash from 'bootstrap-icons/icons/trash.svg?component'
199
+ import IconArrowCounterclockwise from 'bootstrap-icons/icons/arrow-counterclockwise.svg?component'
200
+ import IconArrowClockwise from 'bootstrap-icons/icons/arrow-clockwise.svg?component'
201
+ import IconArrowRepeat from 'bootstrap-icons/icons/arrow-repeat.svg?component'
202
+
203
+ const tableRef = ref<HTMLTableElement>()
204
+
205
+ // Reactive state
206
+ const rowCount = ref(0)
207
+ const selectedCount = ref(0)
208
+ const canUndo = ref(false)
209
+ const canRedo = ref(false)
210
+ const searchQuery = ref('')
211
+ const categoryFilter = ref('')
212
+
213
+ // Aggregation computed values
214
+ const totalStock = ref(0)
215
+ const totalValue = ref(0)
216
+ const avgPrice = ref(0)
217
+ const lowStockCount = ref(0)
218
+
219
+ // Sample data
220
+ const categories = ['Electronics', 'Accessories', 'Audio', 'Storage', 'Display']
221
+
222
+ const sampleProducts = [
223
+ ['PRD-001', 'MacBook Pro 14"', 'Electronics', 1999.99, 25, 'Active'],
224
+ ['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150, 'Active'],
225
+ ['PRD-003', 'AirPods Pro', 'Audio', 249.99, 75, 'Active'],
226
+ ['PRD-004', 'USB-C Hub', 'Accessories', 79.99, 200, 'Active'],
227
+ ['PRD-005', 'Studio Display', 'Display', 1599.99, 12, 'Low Stock'],
228
+ ['PRD-006', 'Magic Mouse', 'Accessories', 79.99, 180, 'Active'],
229
+ ['PRD-007', 'HomePod Mini', 'Audio', 99.99, 90, 'Active'],
230
+ ['PRD-008', 'iPad Pro 12.9"', 'Electronics', 1099.99, 35, 'Active'],
231
+ ['PRD-009', 'External SSD 1TB', 'Storage', 149.99, 8, 'Low Stock'],
232
+ ['PRD-010', 'Thunderbolt Cable', 'Accessories', 49.99, 300, 'Active'],
233
+ ]
234
+
235
+ let productCounter = 10
236
+ let table: ReturnType<typeof useTable> | undefined
237
+ let cleanupEditing: (() => void) | undefined
238
+ let cleanupKeyboard: (() => void) | undefined
239
+
240
+ onMounted(() => {
241
+ const element = tableRef.value
242
+ if (!element) return
243
+
244
+ table = useTable(ref(element))
245
+
246
+ // Set up table structure
247
+ table.setCaption('Product Inventory')
248
+ table.setHeaders(['SKU', 'Product Name', 'Category', 'Price ($)', 'Stock', 'Status'])
249
+ table.setData(sampleProducts)
250
+
251
+ // Add footer with totals
252
+ updateFooter()
253
+
254
+ // Enable features
255
+ cleanupEditing = table.enableEditing()
256
+ cleanupKeyboard = table.enableKeyboardNavigation()
257
+
258
+ // Set row key function for identification (using SKU)
259
+ table.setRowKeyFunction((row) => String(row[0]))
260
+
261
+ // Watch for changes
262
+ table.onDataChange(() => {
263
+ updateStats()
264
+ updateFooter()
265
+ })
266
+
267
+ table.onRowSelectionChange((selected) => {
268
+ selectedCount.value = selected.size
269
+ })
270
+
271
+ table.onSortChange(() => {
272
+ updateStats()
273
+ })
274
+
275
+ // Initial stats
276
+ updateStats()
277
+
278
+ // Add keyboard shortcuts
279
+ document.addEventListener('keydown', handleKeyDown)
280
+ })
281
+
282
+ onUnmounted(() => {
283
+ cleanupEditing?.()
284
+ cleanupKeyboard?.()
285
+ document.removeEventListener('keydown', handleKeyDown)
286
+ })
287
+
288
+ function handleKeyDown(event: KeyboardEvent) {
289
+ if (event.ctrlKey || event.metaKey) {
290
+ if (event.key === 'z') {
291
+ event.preventDefault()
292
+ handleUndo()
293
+ } else if (event.key === 'y') {
294
+ event.preventDefault()
295
+ handleRedo()
296
+ }
297
+ }
298
+ }
299
+
300
+ function updateStats() {
301
+ if (!table) return
302
+
303
+ rowCount.value = table.rowCount.value
304
+ canUndo.value = table.canUndo()
305
+ canRedo.value = table.canRedo()
306
+
307
+ // Aggregations
308
+ totalStock.value = table.sum(4)
309
+ const priceSum = table.sum(3)
310
+ avgPrice.value = rowCount.value > 0 ? priceSum / rowCount.value : 0
311
+ totalValue.value = calculateTotalValue()
312
+ lowStockCount.value = table.count(5, (val) => val === 'Low Stock')
313
+ }
314
+
315
+ function calculateTotalValue(): number {
316
+ if (!table) return 0
317
+ const data = table.data.value
318
+ return data.reduce((sum, row) => {
319
+ const price = Number(row[3]) || 0
320
+ const stock = Number(row[4]) || 0
321
+ return sum + price * stock
322
+ }, 0)
323
+ }
324
+
325
+ function updateFooter() {
326
+ if (!table) return
327
+ const priceSum = table.sum(3)
328
+ const stockSum = table.sum(4)
329
+ table.setFooter(['', 'TOTALS', '', `$${priceSum.toFixed(2)}`, stockSum.toString(), ''])
330
+ }
331
+
332
+ function addProduct() {
333
+ if (!table) return
334
+ productCounter++
335
+ const newProduct = [
336
+ `PRD-${String(productCounter).padStart(3, '0')}`,
337
+ `New Product ${productCounter}`,
338
+ categories[Math.floor(Math.random() * categories.length)],
339
+ (Math.random() * 500 + 50).toFixed(2),
340
+ Math.floor(Math.random() * 100 + 10),
341
+ 'Active',
342
+ ]
343
+ table.addRow(newProduct)
344
+ updateStats()
345
+ }
346
+
347
+ function deleteSelected() {
348
+ if (!table) return
349
+ const selected = Array.from(table.selectedRows.value).sort((a, b) => b - a)
350
+ table.transaction(() => {
351
+ for (const index of selected) {
352
+ table!.removeRow(index)
353
+ }
354
+ })
355
+ table.clearSelection()
356
+ updateStats()
357
+ }
358
+
359
+ function handleUndo() {
360
+ if (!table) return
361
+ table.undo()
362
+ updateStats()
363
+ }
364
+
365
+ function handleRedo() {
366
+ if (!table) return
367
+ table.redo()
368
+ updateStats()
369
+ }
370
+
371
+ function handleSearch() {
372
+ if (!table) return
373
+ if (searchQuery.value) {
374
+ table.filterRows((row) => {
375
+ const query = searchQuery.value.toLowerCase()
376
+ return row.some((cell) => String(cell).toLowerCase().includes(query))
377
+ })
378
+ } else {
379
+ table.clearFilters()
380
+ }
381
+ updateStats()
382
+ }
383
+
384
+ function clearSearch() {
385
+ searchQuery.value = ''
386
+ handleSearch()
387
+ }
388
+
389
+ function handleCategoryFilter() {
390
+ if (!table) return
391
+ if (categoryFilter.value) {
392
+ table.filterColumnByValue(2, categoryFilter.value)
393
+ } else {
394
+ table.clearFilters()
395
+ }
396
+ updateStats()
397
+ }
398
+
399
+ function resetTable() {
400
+ if (!table) return
401
+ table.setData(sampleProducts)
402
+ table.clearSelection()
403
+ table.clearFilters()
404
+ searchQuery.value = ''
405
+ categoryFilter.value = ''
406
+ productCounter = 10
407
+ updateStats()
408
+ }
409
+
410
+ // Watch for filter changes to clear search when category changes
411
+ watch(categoryFilter, () => {
412
+ if (categoryFilter.value && searchQuery.value) {
413
+ searchQuery.value = ''
414
+ }
415
+ })
416
+
417
+ const codeExample = `import { ref, onMounted, onUnmounted } from 'vue'
418
+ import { useTable } from '@mikestools/usetable'
419
+
420
+ const tableRef = ref<HTMLTableElement>()
421
+ let table: ReturnType<typeof useTable>
422
+
423
+ onMounted(() => {
424
+ table = useTable(ref(tableRef.value!))
425
+
426
+ // Set up structure
427
+ table.setHeaders(['SKU', 'Product', 'Category', 'Price', 'Stock', 'Status'])
428
+ table.setData([
429
+ ['PRD-001', 'MacBook Pro', 'Electronics', 1999.99, 25, 'Active'],
430
+ ['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150, 'Active'],
431
+ ])
432
+
433
+ // Enable inline editing (double-click to edit)
434
+ const cleanupEdit = table.enableEditing()
435
+
436
+ // Enable keyboard navigation (arrow keys)
437
+ const cleanupNav = table.enableKeyboardNavigation()
438
+
439
+ // Set row key for identification
440
+ table.setRowKeyFunction((row) => String(row[0]))
441
+
442
+ // React to changes
443
+ table.onDataChange((data) => {
444
+ console.log('Data changed:', data)
445
+ // Save to database, localStorage, etc.
446
+ })
447
+
448
+ // Aggregations
449
+ const totalStock = table.sum(4) // Sum of Stock column
450
+ const avgPrice = table.average(3) // Average of Price column
451
+ const lowStock = table.count(5, (v) => v === 'Low Stock')
452
+
453
+ // Sorting
454
+ table.sortColumnAscending(1) // Sort by Product name
455
+
456
+ // Filtering
457
+ table.filterColumnByValue(2, 'Electronics') // Show only Electronics
458
+
459
+ // Selection
460
+ table.selectRow(0)
461
+ table.selectRowRange(0, 2) // Select rows 0-2
462
+
463
+ // Undo/Redo
464
+ table.setCell(0, 4, 30) // Change stock
465
+ table.undo() // Revert change
466
+
467
+ // Transactions (atomic operations)
468
+ table.transaction(() => {
469
+ table.addRow(['PRD-003', 'New', 'Audio', 199.99, 50, 'Active'])
470
+ table.setCell(0, 4, 20)
471
+ // If any fails, all changes roll back
472
+ })
473
+ })`
474
+ </script>