picocrank 1.22.2 → 1.23.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 +11 -0
- package/docs/buttons-with-icons.md +97 -0
- package/index.html +1 -1
- package/package.json +2 -2
- package/router.js +26 -17
- package/vite.config.js +1 -1
- package/vue/components/Table.vue +448 -104
- package/vue/components/TableColumnFilterPopover.vue +421 -0
- package/vue/composables/tableFilters.js +251 -0
- package/vue/composables/useLongPress.js +76 -0
- package/vue/{views → examples}/App.vue +1 -0
- package/vue/{views → examples}/ButtonsExample.vue +54 -1
- package/vue/examples/TableExample.vue +46 -0
- package/vue/examples/TableRemoteExample.vue +79 -0
- package/vue/{views → examples}/Welcome.vue +5 -2
- package/vue/views/TableExample.vue +0 -46
- /package/vue/{views → examples}/Admin.vue +0 -0
- /package/vue/{views → examples}/CalendarExample.vue +0 -0
- /package/vue/{views → examples}/DialogExample.vue +0 -0
- /package/vue/{views → examples}/FormExample.vue +0 -0
- /package/vue/{views → examples}/Login.vue +0 -0
- /package/vue/{views → examples}/LoginExample.vue +0 -0
- /package/vue/{views → examples}/NavigationGridExample.vue +0 -0
- /package/vue/{views → examples}/PageStructure.vue +0 -0
- /package/vue/{views → examples}/ReadOnlyTextAreaExample.vue +0 -0
- /package/vue/{views → examples}/StatusExample.vue +0 -0
- /package/vue/{views → examples}/TabsExample.vue +0 -0
- /package/vue/{views → examples}/ThemePreview.vue +0 -0
- /package/vue/{views → examples}/UserDetails.vue +0 -0
- /package/vue/{views → examples}/ViewItem.vue +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DELAY_MS = 500
|
|
4
|
+
const DEFAULT_MOVE_TOLERANCE_PX = 10
|
|
5
|
+
|
|
6
|
+
export function useLongPress(callback, options = {}) {
|
|
7
|
+
const delayMs = options.delayMs ?? DEFAULT_DELAY_MS
|
|
8
|
+
const moveTolerancePx = options.moveTolerancePx ?? DEFAULT_MOVE_TOLERANCE_PX
|
|
9
|
+
|
|
10
|
+
const suppressClick = ref(false)
|
|
11
|
+
let timer = null
|
|
12
|
+
let startX = 0
|
|
13
|
+
let startY = 0
|
|
14
|
+
let pointerId = null
|
|
15
|
+
|
|
16
|
+
function clearTimer() {
|
|
17
|
+
if (timer !== null) {
|
|
18
|
+
clearTimeout(timer)
|
|
19
|
+
timer = null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function onPointerDown(event) {
|
|
24
|
+
if (typeof callback !== 'function') {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clearTimer()
|
|
29
|
+
pointerId = event.pointerId
|
|
30
|
+
startX = event.clientX
|
|
31
|
+
startY = event.clientY
|
|
32
|
+
|
|
33
|
+
timer = window.setTimeout(() => {
|
|
34
|
+
timer = null
|
|
35
|
+
suppressClick.value = true
|
|
36
|
+
callback(event)
|
|
37
|
+
}, delayMs)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onPointerMove(event) {
|
|
41
|
+
if (timer === null || event.pointerId !== pointerId) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dx = Math.abs(event.clientX - startX)
|
|
46
|
+
const dy = Math.abs(event.clientY - startY)
|
|
47
|
+
if (dx > moveTolerancePx || dy > moveTolerancePx) {
|
|
48
|
+
clearTimer()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onPointerUp() {
|
|
53
|
+
clearTimer()
|
|
54
|
+
pointerId = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onClick(event) {
|
|
58
|
+
if (!suppressClick.value) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
event.stopPropagation()
|
|
64
|
+
suppressClick.value = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
suppressClick,
|
|
69
|
+
onPointerDown,
|
|
70
|
+
onPointerMove,
|
|
71
|
+
onPointerUp,
|
|
72
|
+
onPointerCancel: onPointerUp,
|
|
73
|
+
onPointerLeave: onPointerUp,
|
|
74
|
+
onClick,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -188,6 +188,7 @@
|
|
|
188
188
|
|
|
189
189
|
navigation.value.addSection('Data display', { name: 'nav-data' })
|
|
190
190
|
navigation.value.addRouterLink('TableExample', 'Table')
|
|
191
|
+
navigation.value.addRouterLink('TableRemoteExample', 'Remote table')
|
|
191
192
|
navigation.value.addRouterLink('CalendarExample', 'Calendar')
|
|
192
193
|
navigation.value.addRouterLink('ReadOnlyTextAreaExample', 'Read-only output')
|
|
193
194
|
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
>
|
|
6
6
|
<p>
|
|
7
7
|
Buttons use the global Femtocrank theme. Apply karma classes for intent, or
|
|
8
|
-
rely on <code>type</code> attributes for form semantics.
|
|
8
|
+
rely on <code>type</code> attributes for form semantics. Use
|
|
9
|
+
<code>.inline-icon</code> on buttons with icons, and
|
|
10
|
+
<code>aria-label</code> when there is no visible text.
|
|
9
11
|
</p>
|
|
10
12
|
</Section>
|
|
11
13
|
|
|
@@ -19,6 +21,47 @@
|
|
|
19
21
|
</div>
|
|
20
22
|
</Section>
|
|
21
23
|
|
|
24
|
+
<Section title="Icon and text" subtitle="Hugeicons with a visible label">
|
|
25
|
+
<div role="toolbar" class="buttons-row">
|
|
26
|
+
<button type="button" class="inline-icon">
|
|
27
|
+
<HugeiconsIcon :icon="Add01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
28
|
+
<span>Add item</span>
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" class="inline-icon neutral">
|
|
31
|
+
<HugeiconsIcon :icon="EditIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
32
|
+
<span>Edit</span>
|
|
33
|
+
</button>
|
|
34
|
+
<button type="button" class="inline-icon good">
|
|
35
|
+
<HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
36
|
+
<span>Search</span>
|
|
37
|
+
</button>
|
|
38
|
+
<button type="button" class="inline-icon">
|
|
39
|
+
<HugeiconsIcon :icon="Copy01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
40
|
+
<span>Copy</span>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</Section>
|
|
44
|
+
|
|
45
|
+
<Section title="Icon only" subtitle="Hugeicons without visible text">
|
|
46
|
+
<div role="toolbar" class="buttons-row">
|
|
47
|
+
<button type="button" class="inline-icon" aria-label="Add item">
|
|
48
|
+
<HugeiconsIcon :icon="Add01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
49
|
+
</button>
|
|
50
|
+
<button type="button" class="inline-icon neutral" aria-label="Edit">
|
|
51
|
+
<HugeiconsIcon :icon="EditIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
52
|
+
</button>
|
|
53
|
+
<button type="button" class="inline-icon good" aria-label="Search">
|
|
54
|
+
<HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
55
|
+
</button>
|
|
56
|
+
<button type="button" class="inline-icon bad" aria-label="Delete">
|
|
57
|
+
<HugeiconsIcon :icon="Delete02Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
58
|
+
</button>
|
|
59
|
+
<button type="button" class="inline-icon neutral" aria-label="Settings">
|
|
60
|
+
<HugeiconsIcon :icon="Settings01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</Section>
|
|
64
|
+
|
|
22
65
|
<Section title="Form actions" subtitle="Semantic button types in a fieldset">
|
|
23
66
|
<FormLayout class="buttons-demo-form" @submit.prevent="onSubmit">
|
|
24
67
|
<template #actions>
|
|
@@ -67,10 +110,20 @@
|
|
|
67
110
|
|
|
68
111
|
<script setup>
|
|
69
112
|
import { ref } from 'vue'
|
|
113
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
114
|
+
import {
|
|
115
|
+
Add01Icon,
|
|
116
|
+
Copy01Icon,
|
|
117
|
+
Delete02Icon,
|
|
118
|
+
EditIcon,
|
|
119
|
+
SearchIcon,
|
|
120
|
+
Settings01Icon,
|
|
121
|
+
} from '@hugeicons/core-free-icons'
|
|
70
122
|
import Section from '../components/Section.vue'
|
|
71
123
|
import FormLayout from '../components/FormLayout.vue'
|
|
72
124
|
|
|
73
125
|
const lastAction = ref('')
|
|
126
|
+
const iconStrokeWidth = 2.5
|
|
74
127
|
|
|
75
128
|
function onSubmit() {
|
|
76
129
|
lastAction.value = 'Form submitted.'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Table Example"
|
|
4
|
+
subtitle="Sortable columns, pagination, and column filters. Right-click or long-press a column header to filter."
|
|
5
|
+
:padding="false"
|
|
6
|
+
>
|
|
7
|
+
<template #toolbar>
|
|
8
|
+
<button>Add Item</button>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<Table
|
|
12
|
+
:data="data"
|
|
13
|
+
:headers="headers"
|
|
14
|
+
:show-pagination="true"
|
|
15
|
+
>
|
|
16
|
+
<template #cell-name="{ row, value }">
|
|
17
|
+
<router-link :to="{ name: 'ViewItem', params: { id: row.name } }">
|
|
18
|
+
{{ value }}
|
|
19
|
+
</router-link>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<template #cell-city="{ row, value }">
|
|
23
|
+
<span class="subtle">
|
|
24
|
+
{{ value }}
|
|
25
|
+
</span>
|
|
26
|
+
</template>
|
|
27
|
+
</Table>
|
|
28
|
+
</Section>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup>
|
|
32
|
+
import { ref, onMounted } from 'vue'
|
|
33
|
+
import { getExampleTableRows } from '../data/examplePeople.js'
|
|
34
|
+
|
|
35
|
+
const headers = ref([
|
|
36
|
+
{ key: 'name', label: 'Name', sortable: true, filterable: true, filterType: 'text', hidden: false, width: '20%' },
|
|
37
|
+
{ key: 'age', label: 'Age', sortable: true, filterable: true, filterType: 'number' },
|
|
38
|
+
{ key: 'city', label: 'City', sortable: true, filterable: true, filterType: 'select', width: '200px' },
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
const data = ref([])
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
data.value = getExampleTableRows()
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Remote table"
|
|
4
|
+
subtitle="Server-style pagination with fetchRows. Filters, sort, and page changes are sent to the callback. Right-click or long-press a column header to filter."
|
|
5
|
+
:padding="false"
|
|
6
|
+
>
|
|
7
|
+
<p v-if="lastQuerySummary" class="subtle query-summary">{{ lastQuerySummary }}</p>
|
|
8
|
+
|
|
9
|
+
<Table
|
|
10
|
+
:headers="headers"
|
|
11
|
+
:fetch-rows="fetchRows"
|
|
12
|
+
:show-pagination="true"
|
|
13
|
+
:remote-debounce-ms="250"
|
|
14
|
+
/>
|
|
15
|
+
</Section>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import { ref } from 'vue'
|
|
20
|
+
import { getExampleTableRows } from '../data/examplePeople.js'
|
|
21
|
+
import { applyFilters, sortRows } from '../composables/tableFilters.js'
|
|
22
|
+
|
|
23
|
+
const allRows = getExampleTableRows()
|
|
24
|
+
|
|
25
|
+
const headers = ref([
|
|
26
|
+
{ key: 'name', label: 'Name', sortable: true, filterable: true, filterType: 'text', width: '20%' },
|
|
27
|
+
{ key: 'age', label: 'Age', sortable: true, filterable: true, filterType: 'number' },
|
|
28
|
+
{
|
|
29
|
+
key: 'city',
|
|
30
|
+
label: 'City',
|
|
31
|
+
sortable: true,
|
|
32
|
+
filterable: true,
|
|
33
|
+
filterType: 'select',
|
|
34
|
+
filterOptions: [...new Set(allRows.map((row) => row.city))].sort().map((city) => ({
|
|
35
|
+
value: city,
|
|
36
|
+
label: city,
|
|
37
|
+
})),
|
|
38
|
+
width: '200px',
|
|
39
|
+
},
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
const lastQuerySummary = ref('')
|
|
43
|
+
|
|
44
|
+
async function fetchRows(query, { signal } = {}) {
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
const timer = window.setTimeout(resolve, 200)
|
|
47
|
+
if (!signal) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const onAbort = () => {
|
|
51
|
+
window.clearTimeout(timer)
|
|
52
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
53
|
+
}
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
onAbort()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const filtered = applyFilters(allRows, headers.value, query.filters)
|
|
62
|
+
const sorted = sortRows(filtered, query.sortBy, query.sortDir)
|
|
63
|
+
const start = (query.page - 1) * query.pageSize
|
|
64
|
+
const rows = sorted.slice(start, start + query.pageSize)
|
|
65
|
+
|
|
66
|
+
lastQuerySummary.value = `Last query: page ${query.page}, ${Object.keys(query.filters ?? {}).length} active filter(s), ${sorted.length} matching row(s).`
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
rows,
|
|
70
|
+
total: sorted.length,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.query-summary {
|
|
77
|
+
margin: 0 0 0.75rem 1rem;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -120,7 +120,7 @@ const popularExamples = [
|
|
|
120
120
|
name: 'TableExample',
|
|
121
121
|
category: 'Data display',
|
|
122
122
|
label: 'Table',
|
|
123
|
-
description: '
|
|
123
|
+
description: 'Sorting, pagination, and column filters',
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
name: 'CalendarExample',
|
|
@@ -160,7 +160,10 @@ onMounted(() => {
|
|
|
160
160
|
description: 'Drop-in themes on Femtocrank',
|
|
161
161
|
})
|
|
162
162
|
nav.addRouterLink('TableExample', 'Table', {
|
|
163
|
-
description: 'Lists with sorting and
|
|
163
|
+
description: 'Lists with sorting, pagination, and filters',
|
|
164
|
+
})
|
|
165
|
+
nav.addRouterLink('TableRemoteExample', 'Remote table', {
|
|
166
|
+
description: 'Server-style fetchRows pagination and filters',
|
|
164
167
|
})
|
|
165
168
|
nav.addRouterLink('CalendarExample', 'Calendar', {
|
|
166
169
|
description: 'Month view with events',
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Section
|
|
3
|
-
title="Table Example"
|
|
4
|
-
subtitle="A simple table with sortable columns and pagination"
|
|
5
|
-
:padding = "false"
|
|
6
|
-
>
|
|
7
|
-
<template #toolbar>
|
|
8
|
-
<button>Add Item</button>
|
|
9
|
-
</template>
|
|
10
|
-
|
|
11
|
-
<Table
|
|
12
|
-
:data = "data"
|
|
13
|
-
:headers = "headers"
|
|
14
|
-
:show-pagination = "true"
|
|
15
|
-
>
|
|
16
|
-
<template #cell-name="{ row, value }">
|
|
17
|
-
<router-link :to="{ name: 'ViewItem', params: { id: row.name } }">
|
|
18
|
-
{{ value }}
|
|
19
|
-
</router-link>
|
|
20
|
-
</template>
|
|
21
|
-
|
|
22
|
-
<template #cell-city="{ row, value }">
|
|
23
|
-
<span class = "subtle">
|
|
24
|
-
{{ value }}
|
|
25
|
-
</span>
|
|
26
|
-
</template>
|
|
27
|
-
</Table>
|
|
28
|
-
</Section>
|
|
29
|
-
</template>
|
|
30
|
-
|
|
31
|
-
<script setup>
|
|
32
|
-
import { ref, onMounted } from 'vue'
|
|
33
|
-
import { getExampleTableRows } from '../data/examplePeople.js'
|
|
34
|
-
|
|
35
|
-
const headers = ref([
|
|
36
|
-
{ key: "name", label: "Name", sortable: true, hidden: false, width: "20%" },
|
|
37
|
-
{ key: "age", label: "Age", sortable: false },
|
|
38
|
-
{ key: "city", label: "City", sortable: true, width: "200px" },
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
const data = ref([])
|
|
42
|
-
|
|
43
|
-
onMounted(() => {
|
|
44
|
-
data.value = getExampleTableRows()
|
|
45
|
-
})
|
|
46
|
-
</script>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|