picocrank 1.12.4 → 1.13.0
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/package.json +4 -4
- package/vue/components/Header.vue +11 -0
- package/vue/components/NavigationGrid.vue +1 -1
- package/vue/components/Pagination.vue +35 -68
- package/vue/components/TopBar.vue +132 -0
- package/vue/views/App.vue +28 -11
- package/vue/views/Welcome.vue +19 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocrank",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
|
|
5
5
|
"main": "vite.config.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"url": "https://github.com/jamesread/picocrank/issues"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@hugeicons/core-free-icons": "^3.1.
|
|
21
|
+
"@hugeicons/core-free-icons": "^3.1.1",
|
|
22
22
|
"@hugeicons/vue": "^1.0.4",
|
|
23
23
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
24
|
-
"femtocrank": "^2.
|
|
24
|
+
"femtocrank": "^2.5.0",
|
|
25
25
|
"unplugin-vue-components": "^30.0.0",
|
|
26
|
-
"vite": "^7.3.
|
|
26
|
+
"vite": "^7.3.1",
|
|
27
27
|
"vue": "^3.5.26",
|
|
28
28
|
"vue-router": "^4.6.4"
|
|
29
29
|
},
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
</button>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
+
<TopBar v-if="topBarEnabled" :navigation="navigation" />
|
|
15
|
+
|
|
14
16
|
<Breadcrumbs v-if="breadcrumbs" />
|
|
15
17
|
|
|
16
18
|
<slot name="toolbar" />
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
import { Menu01Icon } from "@hugeicons/core-free-icons";
|
|
31
33
|
|
|
32
34
|
import Breadcrumbs from "./Breadcrumbs.vue";
|
|
35
|
+
import TopBar from "./TopBar.vue";
|
|
33
36
|
|
|
34
37
|
const emit = defineEmits(["toggleSidebar"]);
|
|
35
38
|
|
|
@@ -54,6 +57,14 @@
|
|
|
54
57
|
type: Boolean,
|
|
55
58
|
default: true,
|
|
56
59
|
},
|
|
60
|
+
topBarEnabled: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: false,
|
|
63
|
+
},
|
|
64
|
+
navigation: {
|
|
65
|
+
type: Object,
|
|
66
|
+
default: null,
|
|
67
|
+
},
|
|
57
68
|
});
|
|
58
69
|
</script>
|
|
59
70
|
|
|
@@ -124,7 +124,7 @@ function handleLinkClick(link) {
|
|
|
124
124
|
|
|
125
125
|
.navigation-grid {
|
|
126
126
|
display: grid;
|
|
127
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
127
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
128
128
|
gap: 1rem;
|
|
129
129
|
padding: 1rem 0;
|
|
130
130
|
}
|
|
@@ -20,6 +20,17 @@
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div class="pagination-controls">
|
|
23
|
+
<button
|
|
24
|
+
class="button"
|
|
25
|
+
:disabled="currentPageValue === 1"
|
|
26
|
+
@click="goToPage(1)"
|
|
27
|
+
title="First page"
|
|
28
|
+
>
|
|
29
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
30
|
+
<path fill="currentColor" d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6l6 6zM6 6h2v12H6z"/>
|
|
31
|
+
</svg>
|
|
32
|
+
</button>
|
|
33
|
+
|
|
23
34
|
<button
|
|
24
35
|
class="button"
|
|
25
36
|
:disabled="currentPageValue === 1"
|
|
@@ -31,37 +42,11 @@
|
|
|
31
42
|
</svg>
|
|
32
43
|
</button>
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@click="goToPage(1)"
|
|
40
|
-
>
|
|
41
|
-
1
|
|
42
|
-
</button>
|
|
43
|
-
|
|
44
|
-
<span v-if="showFirstEllipsis" class="pagination-ellipsis">...</span>
|
|
45
|
-
|
|
46
|
-
<button
|
|
47
|
-
v-for="page in visiblePages"
|
|
48
|
-
:key="page"
|
|
49
|
-
class="button"
|
|
50
|
-
:class="{ active: currentPageValue === page }"
|
|
51
|
-
@click="goToPage(page)"
|
|
52
|
-
>
|
|
53
|
-
{{ page }}
|
|
54
|
-
</button>
|
|
55
|
-
|
|
56
|
-
<span v-if="showLastEllipsis" class="pagination-ellipsis">...</span>
|
|
57
|
-
|
|
58
|
-
<button
|
|
59
|
-
v-if="showLastPage"
|
|
60
|
-
class="button"
|
|
61
|
-
:class="{ active: currentPageValue === totalPages }"
|
|
62
|
-
@click="goToPage(totalPages)"
|
|
63
|
-
>
|
|
64
|
-
{{ totalPages }}
|
|
45
|
+
<button
|
|
46
|
+
class="button active"
|
|
47
|
+
disabled
|
|
48
|
+
>
|
|
49
|
+
{{ currentPageValue }}
|
|
65
50
|
</button>
|
|
66
51
|
|
|
67
52
|
<button
|
|
@@ -74,6 +59,17 @@
|
|
|
74
59
|
<path fill="currentColor" d="M8.59 16.59L10 18l6-6l-6-6L8.59 7.41L13.17 12z"/>
|
|
75
60
|
</svg>
|
|
76
61
|
</button>
|
|
62
|
+
|
|
63
|
+
<button
|
|
64
|
+
class="button"
|
|
65
|
+
:disabled="currentPageValue === totalPages"
|
|
66
|
+
@click="goToPage(totalPages)"
|
|
67
|
+
title="Last page"
|
|
68
|
+
>
|
|
69
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
70
|
+
<path fill="currentColor" d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6l-6-6zM16 6h2v12h-2z"/>
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
77
73
|
</div>
|
|
78
74
|
</div>
|
|
79
75
|
</template>
|
|
@@ -126,30 +122,6 @@ const totalPages = computed(() => Math.ceil(props.total / localPageSize.value))
|
|
|
126
122
|
const startItem = computed(() => (currentPageValue.value - 1) * localPageSize.value)
|
|
127
123
|
const endItem = computed(() => Math.min(currentPageValue.value * localPageSize.value, props.total))
|
|
128
124
|
|
|
129
|
-
const maxVisiblePages = 5
|
|
130
|
-
const visiblePages = computed(() => {
|
|
131
|
-
const pages = []
|
|
132
|
-
const halfVisible = Math.floor(maxVisiblePages / 2)
|
|
133
|
-
|
|
134
|
-
let start = Math.max(1, currentPageValue.value - halfVisible)
|
|
135
|
-
let end = Math.min(totalPages.value, start + maxVisiblePages - 1)
|
|
136
|
-
|
|
137
|
-
if (end - start < maxVisiblePages - 1) {
|
|
138
|
-
start = Math.max(1, end - maxVisiblePages + 1)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
for (let i = start; i <= end; i++) {
|
|
142
|
-
pages.push(i)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return pages
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const showFirstPage = computed(() => visiblePages.value[0] > 1)
|
|
149
|
-
const showLastPage = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value)
|
|
150
|
-
const showFirstEllipsis = computed(() => visiblePages.value[0] > 2)
|
|
151
|
-
const showLastEllipsis = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value - 1)
|
|
152
|
-
|
|
153
125
|
function goToPage(page) {
|
|
154
126
|
if (page >= 1 && page <= totalPages.value && page !== currentPageValue.value) {
|
|
155
127
|
localCurrentPage.value = page
|
|
@@ -193,7 +165,7 @@ watch(() => props.pageSize, (newSize) => {
|
|
|
193
165
|
|
|
194
166
|
.pagination-text {
|
|
195
167
|
font-size: 0.875rem;
|
|
196
|
-
color: #
|
|
168
|
+
color: #666;
|
|
197
169
|
}
|
|
198
170
|
|
|
199
171
|
.pagination-controls {
|
|
@@ -203,6 +175,7 @@ watch(() => props.pageSize, (newSize) => {
|
|
|
203
175
|
}
|
|
204
176
|
|
|
205
177
|
.button {
|
|
178
|
+
border: 0;
|
|
206
179
|
display: flex;
|
|
207
180
|
align-items: center;
|
|
208
181
|
justify-content: center;
|
|
@@ -211,22 +184,10 @@ watch(() => props.pageSize, (newSize) => {
|
|
|
211
184
|
}
|
|
212
185
|
|
|
213
186
|
.button:disabled {
|
|
214
|
-
opacity: 0.5;
|
|
215
187
|
background: transparent;
|
|
216
188
|
cursor: not-allowed;
|
|
217
189
|
}
|
|
218
190
|
|
|
219
|
-
.button.active {
|
|
220
|
-
background: #545f69;
|
|
221
|
-
color: #fff;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.pagination-ellipsis {
|
|
225
|
-
padding: 0.5rem;
|
|
226
|
-
color: #6c757d;
|
|
227
|
-
font-size: 0.875rem;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
191
|
#page-size {
|
|
231
192
|
background: transparent;
|
|
232
193
|
margin-left: 0.5rem;
|
|
@@ -253,4 +214,10 @@ option {
|
|
|
253
214
|
justify-content: center;
|
|
254
215
|
}
|
|
255
216
|
}
|
|
217
|
+
|
|
218
|
+
@media (prefers-color-scheme: dark) {
|
|
219
|
+
.pagination-text {
|
|
220
|
+
color: #adb5bd;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
256
223
|
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="topbar">
|
|
3
|
+
<div class="topbar-links flex-row">
|
|
4
|
+
<template v-for="link in navigationLinks" :key="link.name">
|
|
5
|
+
<!-- Render separator as a divider -->
|
|
6
|
+
<div v-if="link.type === 'separator'" class="separator"></div>
|
|
7
|
+
<!-- Render callback link -->
|
|
8
|
+
<a v-else-if="link.type === 'callback'" href="#" @click.prevent="handleLinkClick(() => link.callback())" class="button topbar-link">
|
|
9
|
+
<HugeiconsIcon :icon="link.icon" width="1em" height="1em" />
|
|
10
|
+
<span>{{ link.title }}</span>
|
|
11
|
+
</a>
|
|
12
|
+
<!-- Render HTML content -->
|
|
13
|
+
<div v-else-if="link.type === 'html'" v-html="link.html" class="topbar-html"></div>
|
|
14
|
+
<!-- Render router link -->
|
|
15
|
+
<router-link v-else v-bind="link.props || {}" :to="link.to || link.path" :class="['button', 'topbar-link', { active: isActive(link) }]">
|
|
16
|
+
<HugeiconsIcon :icon="link.icon" width="1em" height="1em" />
|
|
17
|
+
<span>{{ link.title }}</span>
|
|
18
|
+
</router-link>
|
|
19
|
+
</template>
|
|
20
|
+
</div>
|
|
21
|
+
</nav>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
import { computed, inject } from 'vue'
|
|
26
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
27
|
+
|
|
28
|
+
const props = defineProps({
|
|
29
|
+
navigation: {
|
|
30
|
+
type: Object,
|
|
31
|
+
default: null
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Try to get navigation from inject first, fall back to prop
|
|
36
|
+
const injectedNavigation = inject('navigation', null)
|
|
37
|
+
|
|
38
|
+
// Get navigation object - handle both component ref and direct object
|
|
39
|
+
const navigation = computed(() => {
|
|
40
|
+
const nav = props.navigation || injectedNavigation
|
|
41
|
+
if (!nav) return null
|
|
42
|
+
|
|
43
|
+
// If it's a component ref, access its exposed API
|
|
44
|
+
if (nav.value) {
|
|
45
|
+
return nav.value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Otherwise, assume it's the navigation object directly
|
|
49
|
+
return nav
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Get navigation links and isActive function from navigation
|
|
53
|
+
// navigationLinks is a ref, so we return it directly (Vue will unwrap it in template)
|
|
54
|
+
const navigationLinks = computed(() => {
|
|
55
|
+
const nav = navigation.value
|
|
56
|
+
if (!nav || !nav.navigationLinks) {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
// Return the ref itself - Vue will unwrap it in the template
|
|
60
|
+
return nav.navigationLinks
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Get isActive function - create a function that accesses the navigation computed
|
|
64
|
+
function isActive(link) {
|
|
65
|
+
const nav = navigation.value
|
|
66
|
+
if (!nav || !nav.isActive) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
return nav.isActive(link)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleLinkClick(callback = null) {
|
|
73
|
+
if (callback) {
|
|
74
|
+
callback()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<style scoped>
|
|
80
|
+
.topbar {
|
|
81
|
+
width: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.topbar-links {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.125rem;
|
|
88
|
+
flex-wrap: wrap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.topbar-link {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 0.375rem;
|
|
95
|
+
padding: 0.5em 0.75em;
|
|
96
|
+
border-radius: 0.25em;
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
color: inherit;
|
|
99
|
+
transition: background-color 0.15s ease;
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
border: 0;
|
|
102
|
+
font-weight: normal;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.topbar-link.active {
|
|
106
|
+
text-decoration: underline;
|
|
107
|
+
font-weight: 500;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.topbar-html {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.separator {
|
|
116
|
+
width: 1px;
|
|
117
|
+
height: 1.5em;
|
|
118
|
+
background-color: currentColor;
|
|
119
|
+
opacity: 0.3;
|
|
120
|
+
margin: 0 0.125rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (prefers-color-scheme: dark) {
|
|
124
|
+
.topbar-link {
|
|
125
|
+
color: #f8f9fa;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.separator {
|
|
129
|
+
opacity: 0.5;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</style>
|
package/vue/views/App.vue
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Header username = "Guest" @toggleSidebar="toggleSidebar" title = "PicoCrank" :logoUrl="logoUrl" :sidebarEnabled="sidebarEnabled">
|
|
2
|
+
<Header username = "Guest" @toggleSidebar="toggleSidebar" title = "PicoCrank" :logoUrl="logoUrl" :sidebarEnabled="sidebarEnabled" :navigation="navigation" :topBarEnabled="topBarEnabled">
|
|
3
3
|
<template #toolbar>
|
|
4
4
|
<QuickSearch
|
|
5
|
+
v-if="quickSearchEnabled"
|
|
5
6
|
ref="quickSearchRef"
|
|
6
7
|
placeholder="Search items..."
|
|
7
8
|
:search-fields="['title', 'name']"
|
|
@@ -42,6 +43,8 @@
|
|
|
42
43
|
const sidebar = ref(null);
|
|
43
44
|
const navigation = ref(null);
|
|
44
45
|
const sidebarEnabled = ref(true);
|
|
46
|
+
const quickSearchEnabled = ref(true);
|
|
47
|
+
const topBarEnabled = ref(false);
|
|
45
48
|
|
|
46
49
|
function toggleSidebar() {
|
|
47
50
|
if (sidebar.value) {
|
|
@@ -53,9 +56,21 @@
|
|
|
53
56
|
sidebarEnabled.value = !sidebarEnabled.value;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
function toggleQuickSearchEnabled() {
|
|
60
|
+
quickSearchEnabled.value = !quickSearchEnabled.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toggleTopBarEnabled() {
|
|
64
|
+
topBarEnabled.value = !topBarEnabled.value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Provide sidebar, QuickSearch, and TopBar state and toggle functions for child components
|
|
57
68
|
provide('sidebarEnabled', sidebarEnabled);
|
|
58
69
|
provide('toggleSidebarEnabled', toggleSidebarEnabled);
|
|
70
|
+
provide('quickSearchEnabled', quickSearchEnabled);
|
|
71
|
+
provide('toggleQuickSearchEnabled', toggleQuickSearchEnabled);
|
|
72
|
+
provide('topBarEnabled', topBarEnabled);
|
|
73
|
+
provide('toggleTopBarEnabled', toggleTopBarEnabled);
|
|
59
74
|
|
|
60
75
|
onMounted(() => {
|
|
61
76
|
if (navigation.value) {
|
|
@@ -84,15 +99,17 @@
|
|
|
84
99
|
sidebar.value.stick();
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
quickSearchRef.value
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
if (quickSearchRef.value) {
|
|
103
|
+
quickSearchRef.value.addItem({
|
|
104
|
+
id: 'hello-world',
|
|
105
|
+
title: 'Hello World',
|
|
106
|
+
name: 'hello-world',
|
|
107
|
+
description: 'This is a test item',
|
|
108
|
+
category: 'Actions',
|
|
109
|
+
type: 'callback',
|
|
110
|
+
callback: helloWorld
|
|
111
|
+
})
|
|
112
|
+
}
|
|
96
113
|
});
|
|
97
114
|
|
|
98
115
|
function helloWorld() {
|
package/vue/views/Welcome.vue
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Section title = "Welcome" classes = "">
|
|
3
3
|
<template #toolbar>
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
5
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
6
|
+
<input type="checkbox" :checked="sidebarEnabled" @change="toggleSidebarEnabled" />
|
|
7
|
+
<span>Enable Sidebar</span>
|
|
8
|
+
</label>
|
|
9
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
10
|
+
<input type="checkbox" :checked="quickSearchEnabled" @change="toggleQuickSearchEnabled" />
|
|
11
|
+
<span>Enable QuickSearch</span>
|
|
12
|
+
</label>
|
|
13
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
14
|
+
<input type="checkbox" :checked="topBarEnabled" @change="toggleTopBarEnabled" />
|
|
15
|
+
<span>Enable TopBar</span>
|
|
16
|
+
</label>
|
|
17
|
+
</div>
|
|
8
18
|
</template>
|
|
9
19
|
<p>
|
|
10
20
|
Welcome to Femtocrank, a web-based tool for analyzing and visualizing the performance of your code.
|
|
@@ -83,7 +93,11 @@
|
|
|
83
93
|
import { HighlighterIcon } from '@hugeicons/core-free-icons';
|
|
84
94
|
import { Menu01Icon } from '@hugeicons/core-free-icons';
|
|
85
95
|
|
|
86
|
-
// Inject sidebar state and toggle
|
|
96
|
+
// Inject sidebar, QuickSearch, and TopBar state and toggle functions from App.vue
|
|
87
97
|
const sidebarEnabled = inject('sidebarEnabled');
|
|
88
98
|
const toggleSidebarEnabled = inject('toggleSidebarEnabled');
|
|
99
|
+
const quickSearchEnabled = inject('quickSearchEnabled');
|
|
100
|
+
const toggleQuickSearchEnabled = inject('toggleQuickSearchEnabled');
|
|
101
|
+
const topBarEnabled = inject('topBarEnabled');
|
|
102
|
+
const toggleTopBarEnabled = inject('toggleTopBarEnabled');
|
|
89
103
|
</script>
|