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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.12.4",
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.0",
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.4.12",
24
+ "femtocrank": "^2.5.0",
25
25
  "unplugin-vue-components": "^30.0.0",
26
- "vite": "^7.3.0",
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(220px, 1fr));
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
- <button
36
- v-if="showFirstPage"
37
- class="button"
38
- :class="{ active: currentPageValue === 1 }"
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: #6c757d;
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
- // Provide sidebar state and toggle function for child components
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.addItem({
88
- id: 'hello-world',
89
- title: 'Hello World',
90
- name: 'hello-world',
91
- description: 'This is a test item',
92
- category: 'Actions',
93
- type: 'callback',
94
- callback: helloWorld
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() {
@@ -1,10 +1,20 @@
1
1
  <template>
2
2
  <Section title = "Welcome" classes = "">
3
3
  <template #toolbar>
4
- <button @click="toggleSidebarEnabled" :class="sidebarEnabled ? 'neutral' : 'good'">
5
- <HugeiconsIcon :icon="Menu01Icon" width="1em" height="1em" />
6
- {{ sidebarEnabled ? 'Disable Sidebar' : 'Enable Sidebar' }}
7
- </button>
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 function from App.vue
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>