picocrank 1.24.0 → 1.25.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.
- package/AGENTS.md +2 -0
- package/docs/filter-query-v1.md +180 -0
- package/logo.svg +7 -7
- package/package.json +7 -5
- package/public/themes/README.md +34 -2
- package/public/themes/index.json +1 -5
- package/styles/dark-theme.css +42 -186
- package/styles/picocrank-extensions.css +15 -0
- package/styles.css +1 -0
- package/supplemental-themes/README.md +9 -0
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +211 -0
- package/supplemental-themes/dracula-alucard/theme.css +193 -0
- package/supplemental-themes/gruvbox-dark-light/theme.css +199 -0
- package/supplemental-themes/index.json +17 -0
- package/{public/themes → supplemental-themes}/waffles/theme.css +69 -46
- package/vite.config.js +91 -0
- package/vue/components/Header.vue +3 -3
- package/vue/components/Login.vue +1 -3
- package/vue/components/Section.vue +45 -14
- package/vue/components/Sidebar.vue +6 -11
- package/vue/components/SidebarNavLink.vue +6 -9
- package/vue/components/Table.vue +1115 -75
- package/vue/components/TableColumnFilterPopover.vue +263 -44
- package/vue/components/TableColumnOptionsPopover.vue +1116 -0
- package/vue/components/Tabs.vue +4 -25
- package/vue/components/ThemeSwitcher.vue +158 -0
- package/vue/components/TopBar.vue +2 -2
- package/vue/composables/tableColumnPresets.js +296 -0
- package/vue/composables/tableFilterQuery.js +202 -0
- package/vue/composables/tableFilters.js +167 -3
- package/vue/composables/useCustomTheme.js +167 -24
- package/vue/data/examplePeople.js +2 -6
- package/vue/examples/App.vue +14 -1
- package/vue/examples/ExampleHeaderThemeSwitcher.vue +78 -0
- package/vue/examples/LoginExample.vue +1 -3
- package/vue/examples/PageStructure.vue +17 -0
- package/vue/examples/TableExample.vue +329 -11
- package/vue/examples/TableRemoteExample.vue +112 -4
- package/vue/examples/ThemePreview.vue +45 -38
- package/public/themes/catppuccin/theme.css +0 -195
- package/public/themes/dracula/theme.css +0 -181
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</label>
|
|
27
27
|
<label v-if="draft.operator !== 'empty'">
|
|
28
28
|
<span class="field-label">Value</span>
|
|
29
|
-
<input v-model="draft.value" type="text" @keydown.enter.prevent="
|
|
29
|
+
<input v-model="draft.value" type="text" @keydown.enter.prevent="addFilter" />
|
|
30
30
|
</label>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</label>
|
|
44
44
|
<label>
|
|
45
45
|
<span class="field-label">Value</span>
|
|
46
|
-
<input v-model="draft.value" type="number" @keydown.enter.prevent="
|
|
46
|
+
<input v-model="draft.value" type="number" @keydown.enter.prevent="addFilter" />
|
|
47
47
|
</label>
|
|
48
48
|
</div>
|
|
49
49
|
|
|
@@ -70,12 +70,76 @@
|
|
|
70
70
|
</div>
|
|
71
71
|
<p v-if="selectOptions.length === 0" class="subtle">No options available.</p>
|
|
72
72
|
</div>
|
|
73
|
+
|
|
74
|
+
<label class="filter-name-field">
|
|
75
|
+
<span class="field-label">Name (optional)</span>
|
|
76
|
+
<input
|
|
77
|
+
v-model="draftName"
|
|
78
|
+
type="text"
|
|
79
|
+
placeholder="Label for this filter"
|
|
80
|
+
@keydown.enter.prevent="addFilter"
|
|
81
|
+
/>
|
|
82
|
+
</label>
|
|
73
83
|
</div>
|
|
74
84
|
|
|
75
85
|
<div ref="actionsRef" role="toolbar" class="popover-actions">
|
|
76
86
|
<button type="button" class="neutral" @click="cancel">Cancel</button>
|
|
77
87
|
<button type="button" class="neutral" @click="clear">Clear</button>
|
|
78
|
-
<button type="button" @click="
|
|
88
|
+
<button type="button" :disabled="!canAddFilter" @click="addFilter">Add filter</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div v-if="savedFilters.length > 0" class="saved-filters">
|
|
92
|
+
<p class="saved-filters-heading">Filters</p>
|
|
93
|
+
<ul class="saved-filters-list">
|
|
94
|
+
<li
|
|
95
|
+
v-for="(entry, index) in savedFilters"
|
|
96
|
+
:key="entry.id"
|
|
97
|
+
class="saved-filter-item"
|
|
98
|
+
>
|
|
99
|
+
<label class="saved-filter-label">
|
|
100
|
+
<input
|
|
101
|
+
type="checkbox"
|
|
102
|
+
:checked="entry.enabled"
|
|
103
|
+
@change="toggleFilterEnabled(entry.id, $event.target.checked)"
|
|
104
|
+
/>
|
|
105
|
+
<span>{{ filterLabel(entry) }}</span>
|
|
106
|
+
</label>
|
|
107
|
+
<div class="saved-filter-actions" role="group" :aria-label="`Actions for ${filterLabel(entry)}`">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="neutral saved-filter-action-button"
|
|
111
|
+
:disabled="index === 0"
|
|
112
|
+
aria-label="Move filter up"
|
|
113
|
+
@click="moveFilter(index, -1)"
|
|
114
|
+
>
|
|
115
|
+
↑
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
class="neutral saved-filter-action-button"
|
|
120
|
+
:disabled="index === savedFilters.length - 1"
|
|
121
|
+
aria-label="Move filter down"
|
|
122
|
+
@click="moveFilter(index, 1)"
|
|
123
|
+
>
|
|
124
|
+
↓
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="neutral saved-filter-action-button saved-filter-delete-button"
|
|
129
|
+
aria-label="Delete filter"
|
|
130
|
+
@click="deleteFilter(entry.id)"
|
|
131
|
+
>
|
|
132
|
+
<HugeiconsIcon
|
|
133
|
+
:icon="Delete02Icon"
|
|
134
|
+
width="0.9em"
|
|
135
|
+
height="0.9em"
|
|
136
|
+
:strokeWidth="2"
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
/>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</li>
|
|
142
|
+
</ul>
|
|
79
143
|
</div>
|
|
80
144
|
</div>
|
|
81
145
|
</Teleport>
|
|
@@ -83,11 +147,17 @@
|
|
|
83
147
|
|
|
84
148
|
<script setup>
|
|
85
149
|
import { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'
|
|
150
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
151
|
+
import { Delete02Icon } from '@hugeicons/core-free-icons'
|
|
86
152
|
import CheckGroup from './CheckGroup.vue'
|
|
87
153
|
import RadioGroup from './RadioGroup.vue'
|
|
88
154
|
import {
|
|
89
155
|
createEmptyFilter,
|
|
156
|
+
createFilterEntry,
|
|
157
|
+
describeFilter,
|
|
90
158
|
inferFilterType,
|
|
159
|
+
isFilterEntryActive,
|
|
160
|
+
normalizeColumnFilterEntries,
|
|
91
161
|
normalizeFilter,
|
|
92
162
|
} from '../composables/tableFilters.js'
|
|
93
163
|
|
|
@@ -105,8 +175,8 @@ const props = defineProps({
|
|
|
105
175
|
default: null,
|
|
106
176
|
},
|
|
107
177
|
value: {
|
|
108
|
-
type:
|
|
109
|
-
default:
|
|
178
|
+
type: Array,
|
|
179
|
+
default: () => [],
|
|
110
180
|
},
|
|
111
181
|
rows: {
|
|
112
182
|
type: Array,
|
|
@@ -132,6 +202,8 @@ const popoverStyle = ref({ top: '0px', left: '0px' })
|
|
|
132
202
|
const checkGroupScrollStyle = ref({})
|
|
133
203
|
const titleId = useId()
|
|
134
204
|
const draft = ref(createEmptyFilter({ key: '' }))
|
|
205
|
+
const draftName = ref('')
|
|
206
|
+
const savedFilters = ref([])
|
|
135
207
|
|
|
136
208
|
const filterType = computed(() => {
|
|
137
209
|
if (!props.header) {
|
|
@@ -142,18 +214,16 @@ const filterType = computed(() => {
|
|
|
142
214
|
|
|
143
215
|
const columnLabel = computed(() => props.header?.label || props.header?.key || '')
|
|
144
216
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
return draft.value.values.length
|
|
150
|
-
})
|
|
217
|
+
const activeSavedCount = computed(() =>
|
|
218
|
+
savedFilters.value.filter((entry) => isFilterEntryActive(entry)).length,
|
|
219
|
+
)
|
|
151
220
|
|
|
152
221
|
const popoverTitle = computed(() => {
|
|
153
|
-
|
|
154
|
-
|
|
222
|
+
const base = columnLabel.value
|
|
223
|
+
if (activeSavedCount.value > 0) {
|
|
224
|
+
return `Filter ${base} (${activeSavedCount.value})`
|
|
155
225
|
}
|
|
156
|
-
return `Filter ${
|
|
226
|
+
return `Filter ${base}`
|
|
157
227
|
})
|
|
158
228
|
|
|
159
229
|
const booleanOptions = [
|
|
@@ -177,6 +247,15 @@ const booleanValue = computed({
|
|
|
177
247
|
},
|
|
178
248
|
})
|
|
179
249
|
|
|
250
|
+
const canAddFilter = computed(() =>
|
|
251
|
+
Boolean(normalizeFilter(props.header, draft.value, props.rows)),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
function filterLabel(entry) {
|
|
255
|
+
const name = entry.name?.trim()
|
|
256
|
+
return name || describeFilter(props.header, entry)
|
|
257
|
+
}
|
|
258
|
+
|
|
180
259
|
function positionPopover() {
|
|
181
260
|
const anchor = props.anchorEl
|
|
182
261
|
if (!anchor || !popoverRef.value) {
|
|
@@ -254,22 +333,28 @@ function updateCheckGroupScrollLimit() {
|
|
|
254
333
|
}
|
|
255
334
|
}
|
|
256
335
|
|
|
257
|
-
function
|
|
336
|
+
function resetDraftForm() {
|
|
258
337
|
if (!props.header) {
|
|
259
338
|
draft.value = createEmptyFilter({ key: '' })
|
|
339
|
+
draftName.value = ''
|
|
260
340
|
return
|
|
261
341
|
}
|
|
262
342
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
343
|
+
draft.value = createEmptyFilter(props.header, props.rows)
|
|
344
|
+
draftName.value = ''
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function resetSavedFilters() {
|
|
348
|
+
if (!props.header) {
|
|
349
|
+
savedFilters.value = []
|
|
269
350
|
return
|
|
270
351
|
}
|
|
271
352
|
|
|
272
|
-
|
|
353
|
+
savedFilters.value = normalizeColumnFilterEntries(props.value, props.header, props.rows)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function emitFilters() {
|
|
357
|
+
emit('apply', savedFilters.value.map((entry) => ({ ...entry })))
|
|
273
358
|
}
|
|
274
359
|
|
|
275
360
|
function onDocumentPointerDown(event) {
|
|
@@ -295,48 +380,110 @@ function onDocumentKeyDown(event) {
|
|
|
295
380
|
}
|
|
296
381
|
}
|
|
297
382
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
383
|
+
function addFilter() {
|
|
384
|
+
if (!canAddFilter.value) {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const entry = createFilterEntry(
|
|
389
|
+
props.header,
|
|
390
|
+
draft.value,
|
|
391
|
+
props.rows,
|
|
392
|
+
{ name: draftName.value },
|
|
393
|
+
)
|
|
394
|
+
if (!entry) {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
savedFilters.value = [...savedFilters.value, entry]
|
|
399
|
+
resetDraftForm()
|
|
400
|
+
emitFilters()
|
|
401
|
+
nextTick(() => {
|
|
402
|
+
updateCheckGroupScrollLimit()
|
|
403
|
+
positionPopover()
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function toggleFilterEnabled(id, enabled) {
|
|
408
|
+
savedFilters.value = savedFilters.value.map((entry) => (
|
|
409
|
+
entry.id === id ? { ...entry, enabled } : entry
|
|
410
|
+
))
|
|
411
|
+
emitFilters()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function moveFilter(index, direction) {
|
|
415
|
+
const targetIndex = index + direction
|
|
416
|
+
if (targetIndex < 0 || targetIndex >= savedFilters.value.length) {
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const next = [...savedFilters.value]
|
|
421
|
+
const [entry] = next.splice(index, 1)
|
|
422
|
+
next.splice(targetIndex, 0, entry)
|
|
423
|
+
savedFilters.value = next
|
|
424
|
+
emitFilters()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function deleteFilter(id) {
|
|
428
|
+
savedFilters.value = savedFilters.value.filter((entry) => entry.id !== id)
|
|
429
|
+
emitFilters()
|
|
302
430
|
}
|
|
303
431
|
|
|
304
432
|
function clear() {
|
|
433
|
+
savedFilters.value = []
|
|
434
|
+
resetDraftForm()
|
|
305
435
|
emit('clear')
|
|
306
436
|
emit('update:open', false)
|
|
307
437
|
}
|
|
308
438
|
|
|
309
439
|
function cancel() {
|
|
310
|
-
emit('cancel')
|
|
311
440
|
emit('update:open', false)
|
|
441
|
+
emit('cancel')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function detachDocumentListeners() {
|
|
445
|
+
document.removeEventListener('pointerdown', onDocumentPointerDown)
|
|
446
|
+
document.removeEventListener('keydown', onDocumentKeyDown)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function attachDocumentListeners() {
|
|
450
|
+
detachDocumentListeners()
|
|
451
|
+
document.addEventListener('pointerdown', onDocumentPointerDown)
|
|
452
|
+
document.addEventListener('keydown', onDocumentKeyDown)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function initializePopover() {
|
|
456
|
+
resetSavedFilters()
|
|
457
|
+
resetDraftForm()
|
|
458
|
+
await nextTick()
|
|
459
|
+
updateCheckGroupScrollLimit()
|
|
460
|
+
await nextTick()
|
|
461
|
+
positionPopover()
|
|
462
|
+
popoverRef.value?.querySelector('input, select, button')?.focus()
|
|
463
|
+
attachDocumentListeners()
|
|
312
464
|
}
|
|
313
465
|
|
|
314
466
|
watch(
|
|
315
|
-
() => [props.open, props.header
|
|
316
|
-
async ([isOpen]) => {
|
|
467
|
+
() => [props.open, props.header?.key],
|
|
468
|
+
async ([isOpen, headerKey], previous) => {
|
|
469
|
+
const wasOpen = previous?.[0] ?? false
|
|
470
|
+
const previousHeaderKey = previous?.[1]
|
|
471
|
+
|
|
317
472
|
if (!isOpen) {
|
|
318
473
|
checkGroupScrollStyle.value = {}
|
|
319
|
-
|
|
320
|
-
document.removeEventListener('keydown', onDocumentKeyDown)
|
|
474
|
+
detachDocumentListeners()
|
|
321
475
|
return
|
|
322
476
|
}
|
|
323
477
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
await nextTick()
|
|
328
|
-
positionPopover()
|
|
329
|
-
popoverRef.value?.querySelector('input, select, button')?.focus()
|
|
330
|
-
|
|
331
|
-
document.addEventListener('pointerdown', onDocumentPointerDown)
|
|
332
|
-
document.addEventListener('keydown', onDocumentKeyDown)
|
|
478
|
+
if (!wasOpen || headerKey !== previousHeaderKey) {
|
|
479
|
+
await initializePopover()
|
|
480
|
+
}
|
|
333
481
|
},
|
|
334
482
|
{ immediate: true },
|
|
335
483
|
)
|
|
336
484
|
|
|
337
485
|
onBeforeUnmount(() => {
|
|
338
|
-
|
|
339
|
-
document.removeEventListener('keydown', onDocumentKeyDown)
|
|
486
|
+
detachDocumentListeners()
|
|
340
487
|
})
|
|
341
488
|
</script>
|
|
342
489
|
|
|
@@ -382,13 +529,22 @@ onBeforeUnmount(() => {
|
|
|
382
529
|
min-height: 0;
|
|
383
530
|
}
|
|
384
531
|
|
|
532
|
+
.filter-name-field {
|
|
533
|
+
display: flex;
|
|
534
|
+
flex-direction: column;
|
|
535
|
+
gap: 0.25rem;
|
|
536
|
+
margin-top: 0.75rem;
|
|
537
|
+
flex-shrink: 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
385
540
|
.checkgroup-scroll {
|
|
386
541
|
overflow-y: auto;
|
|
387
542
|
min-height: 0;
|
|
388
543
|
overscroll-behavior: contain;
|
|
389
544
|
}
|
|
390
545
|
|
|
391
|
-
.filter-fields label
|
|
546
|
+
.filter-fields label,
|
|
547
|
+
.filter-name-field {
|
|
392
548
|
display: flex;
|
|
393
549
|
flex-direction: column;
|
|
394
550
|
gap: 0.25rem;
|
|
@@ -410,12 +566,75 @@ onBeforeUnmount(() => {
|
|
|
410
566
|
border-top: 1px solid var(--border-color, #e1e5e9);
|
|
411
567
|
}
|
|
412
568
|
|
|
569
|
+
.saved-filters {
|
|
570
|
+
flex-shrink: 0;
|
|
571
|
+
margin-top: 0.75rem;
|
|
572
|
+
padding-top: 0.75rem;
|
|
573
|
+
border-top: 1px solid var(--border-color, #e1e5e9);
|
|
574
|
+
min-height: 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.saved-filters-heading {
|
|
578
|
+
margin: 0 0 0.5rem;
|
|
579
|
+
font-size: 0.85rem;
|
|
580
|
+
color: var(--text-muted, #666);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.saved-filters-list {
|
|
584
|
+
list-style: none;
|
|
585
|
+
margin: 0;
|
|
586
|
+
padding: 0;
|
|
587
|
+
display: flex;
|
|
588
|
+
flex-direction: column;
|
|
589
|
+
gap: 0.35rem;
|
|
590
|
+
max-height: 12rem;
|
|
591
|
+
overflow-y: auto;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.saved-filter-item {
|
|
595
|
+
display: grid;
|
|
596
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
597
|
+
align-items: center;
|
|
598
|
+
gap: 0.35rem;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.saved-filter-label {
|
|
602
|
+
display: flex;
|
|
603
|
+
align-items: flex-start;
|
|
604
|
+
gap: 0.45rem;
|
|
605
|
+
min-width: 0;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.saved-filter-label span {
|
|
609
|
+
overflow: hidden;
|
|
610
|
+
text-overflow: ellipsis;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.saved-filter-actions {
|
|
614
|
+
display: inline-flex;
|
|
615
|
+
gap: 0.15rem;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.saved-filter-action-button {
|
|
619
|
+
min-width: 1.6rem;
|
|
620
|
+
padding-inline: 0.25rem;
|
|
621
|
+
font-size: 0.85rem;
|
|
622
|
+
line-height: 1.2;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.saved-filter-delete-button {
|
|
626
|
+
display: inline-flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
justify-content: center;
|
|
629
|
+
}
|
|
630
|
+
|
|
413
631
|
html[data-theme="dark"] .table-column-filter-popover {
|
|
414
632
|
background: var(--background-color, #1e1e1e);
|
|
415
633
|
border-color: var(--border-color, #444);
|
|
416
634
|
}
|
|
417
635
|
|
|
418
|
-
html[data-theme="dark"] .popover-actions
|
|
636
|
+
html[data-theme="dark"] .popover-actions,
|
|
637
|
+
html[data-theme="dark"] .saved-filters {
|
|
419
638
|
border-top-color: var(--border-color, #444);
|
|
420
639
|
}
|
|
421
640
|
</style>
|