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.
@@ -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: 'Sortable columns and pagination',
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 pagination',
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