picocrank 1.9.1 → 1.11.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.
@@ -0,0 +1,294 @@
1
+ <template>
2
+ <div class="navigation-grid-container">
3
+ <div class="navigation-grid" :class="{ 'compact': compact }">
4
+ <button
5
+ v-for="link in filteredLinks"
6
+ :key="link.name"
7
+ :class="['nav-button', { active: isLinkActive(link) }]"
8
+ :title="link.title"
9
+ @click="handleLinkClick(link)"
10
+ >
11
+ <div class="nav-button-icon">
12
+ <HugeiconsIcon
13
+ :icon="link.icon"
14
+ :width="iconSize"
15
+ :height="iconSize"
16
+ />
17
+ </div>
18
+ <div class="nav-button-label">{{ link.title }}</div>
19
+ <div v-if="link.description" class="nav-button-description">
20
+ {{ link.description }}
21
+ </div>
22
+ </button>
23
+ </div>
24
+ <div v-if="filteredLinks.length === 0" class="no-links">
25
+ <p>No navigation links available</p>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { computed, inject, ref } from 'vue';
32
+ import { useRouter, useRoute } from 'vue-router';
33
+ import { HugeiconsIcon } from '@hugeicons/vue';
34
+
35
+ const props = defineProps({
36
+ // Filter links by type: 'route', 'callback', or null for all
37
+ filterType: {
38
+ type: String,
39
+ default: null,
40
+ validator: (value) => value === null || ['route', 'callback'].includes(value)
41
+ },
42
+ // Custom filter function
43
+ filter: {
44
+ type: Function,
45
+ default: null
46
+ },
47
+ // Compact mode (smaller buttons)
48
+ compact: {
49
+ type: Boolean,
50
+ default: false
51
+ },
52
+ // Icon size
53
+ iconSize: {
54
+ type: String,
55
+ default: '2em'
56
+ },
57
+ // Exclude separator and html items
58
+ excludeNonButtons: {
59
+ type: Boolean,
60
+ default: true
61
+ }
62
+ });
63
+
64
+ const router = useRouter();
65
+ const route = useRoute();
66
+
67
+ // Inject navigation from Navigation component
68
+ const navigation = inject('navigation', null);
69
+
70
+ // Get navigation links (it's a ref from the Navigation component)
71
+ const navigationLinks = navigation ? navigation.navigationLinks : ref([]);
72
+
73
+ // Get isActive function from navigation
74
+ const isActive = navigation ? navigation.isActive : (() => false);
75
+
76
+ // Filter links based on props
77
+ const filteredLinks = computed(() => {
78
+ if (!navigationLinks.value || navigationLinks.value.length === 0) {
79
+ return [];
80
+ }
81
+
82
+ let links = [...navigationLinks.value];
83
+
84
+ // Exclude separators and HTML items by default
85
+ if (props.excludeNonButtons) {
86
+ links = links.filter(link =>
87
+ link.type !== 'separator' && link.type !== 'html'
88
+ );
89
+ }
90
+
91
+ // Filter by type if specified
92
+ if (props.filterType) {
93
+ links = links.filter(link => link.type === props.filterType);
94
+ }
95
+
96
+ // Apply custom filter if provided
97
+ if (props.filter && typeof props.filter === 'function') {
98
+ links = links.filter(props.filter);
99
+ }
100
+
101
+ return links;
102
+ });
103
+
104
+ function isLinkActive(link) {
105
+ return isActive(link);
106
+ }
107
+
108
+ function handleLinkClick(link) {
109
+ if (link.type === 'route') {
110
+ const to = link.to || link.path;
111
+ if (to) {
112
+ router.push(to);
113
+ }
114
+ } else if (link.type === 'callback' && link.callback) {
115
+ link.callback();
116
+ }
117
+ }
118
+ </script>
119
+
120
+ <style scoped>
121
+ .navigation-grid-container {
122
+ width: 100%;
123
+ }
124
+
125
+ .navigation-grid {
126
+ display: grid;
127
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
128
+ gap: 1rem;
129
+ padding: 1rem 0;
130
+ }
131
+
132
+ .navigation-grid.compact {
133
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
134
+ gap: 0.75rem;
135
+ }
136
+
137
+ .nav-button {
138
+ display: flex;
139
+ flex-direction: column;
140
+ align-items: top;
141
+ justify-content: flex-start;
142
+ gap: 0.75rem;
143
+ padding: 1.25rem 0.75rem;
144
+ background: transparent;
145
+ border: 1px solid var(--border-color, #e1e5e9);
146
+ border-radius: 8px;
147
+ cursor: pointer;
148
+ transition: all 0.2s ease;
149
+ text-align: left;
150
+ }
151
+
152
+ .navigation-grid.compact .nav-button {
153
+ padding: 0.75rem 0.5rem;
154
+ gap: 0.5rem;
155
+ }
156
+
157
+ .nav-button:hover {
158
+ border-color: var(--primary-color, #007bff);
159
+ transform: translateY(-2px);
160
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
161
+ }
162
+
163
+ .nav-button:active {
164
+ transform: translateY(0);
165
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
166
+ }
167
+
168
+ .nav-button.active {
169
+ background: var(--primary-color, #007bff);
170
+ border-color: var(--primary-color, #007bff);
171
+ color: #fff;
172
+ }
173
+
174
+ .nav-button.active .nav-button-icon {
175
+ color: #fff;
176
+ }
177
+
178
+ .nav-button-icon {
179
+ display: flex;
180
+ align-items: start;
181
+ justify-content: start;
182
+ color: var(--primary-color, #007bff);
183
+ transition: color 0.2s ease;
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .nav-button.active .nav-button-icon {
188
+ color: #fff;
189
+ }
190
+
191
+ .nav-button-label {
192
+ font-size: normal;
193
+ font-weight: bold;
194
+ word-break: break-word;
195
+ text-align: left;
196
+ line-height: 1.2;
197
+ }
198
+
199
+ .navigation-grid.compact .nav-button-label {
200
+ font-size: 0.85em;
201
+ }
202
+
203
+ .nav-button.active .nav-button-label {
204
+ color: #fff;
205
+ }
206
+
207
+ .nav-button-description {
208
+ font-size: small;
209
+ font-weight: normal;
210
+ color: var(--text-muted, #666);
211
+ word-break: break-word;
212
+ text-align: left;
213
+ line-height: 1.5;
214
+ opacity: 0.8;
215
+ }
216
+
217
+ .navigation-grid.compact .nav-button-description {
218
+ font-size: 0.7em;
219
+ }
220
+
221
+ .nav-button.active .nav-button-description {
222
+ color: rgba(255, 255, 255, 0.9);
223
+ }
224
+
225
+ .no-links {
226
+ padding: 2rem;
227
+ text-align: center;
228
+ color: var(--text-muted, #666);
229
+ }
230
+
231
+ /* Dark theme support */
232
+ @media (prefers-color-scheme: dark) {
233
+ .nav-button {
234
+ background: transparent;
235
+ }
236
+
237
+ .nav-button:hover {
238
+ background: var(--hover-background-color, rgba(96, 165, 250, 0.1));
239
+ border-color: var(--primary-color, #60a5fa);
240
+ }
241
+
242
+ .nav-button.active {
243
+ background: var(--primary-color, #60a5fa);
244
+ border-color: var(--primary-color, #60a5fa);
245
+ }
246
+
247
+ .nav-button-description {
248
+ color: var(--text-muted, #999);
249
+ }
250
+
251
+ .nav-button-icon {
252
+ color: var(--primary-color, #60a5fa);
253
+ }
254
+ }
255
+
256
+ /* Responsive */
257
+ @media (max-width: 768px) {
258
+ .navigation-grid {
259
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
260
+ gap: 0.75rem;
261
+ }
262
+
263
+ .nav-button {
264
+ padding: 1rem 0.5rem;
265
+ }
266
+
267
+ .nav-button-label {
268
+ font-size: normal;
269
+ }
270
+
271
+ .nav-button-description {
272
+ font-size: 0.7em;
273
+ }
274
+ }
275
+
276
+ @media (max-width: 480px) {
277
+ .navigation-grid {
278
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
279
+ gap: 0.5rem;
280
+ }
281
+
282
+ .nav-button {
283
+ padding: 0.75rem 0.25rem;
284
+ }
285
+
286
+ .nav-button-label {
287
+ font-size: 0.8em;
288
+ }
289
+
290
+ .nav-button-description {
291
+ font-size: 0.65em;
292
+ }
293
+ }
294
+ </style>
@@ -36,111 +36,21 @@
36
36
  </template>
37
37
 
38
38
  <script setup>
39
- import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
40
- import { useRoute, useRouter } from 'vue-router'
39
+ import { ref, inject, onUnmounted, watch, nextTick } from 'vue'
41
40
  import { HugeiconsIcon } from '@hugeicons/vue'
42
41
  import { Pin02Icon } from '@hugeicons/core-free-icons'
43
42
  import { PinIcon } from '@hugeicons/core-free-icons'
44
43
 
45
44
  const isOpen = ref(false)
46
45
  const isStuck = ref(false)
47
- const navigationLinks = ref([])
48
46
  const sidebarRef = ref(null)
49
47
 
50
- const route = useRoute()
51
- const router = useRouter()
48
+ // Get navigation from Navigation component
49
+ const navigation = inject('navigation', null)
52
50
 
53
- /**
54
- * Add a router link to the sidebar navigation
55
- * @param {string} link - Route name
56
- * @param {string|null} altTitle - Optional alternative title
57
- * @param {object} options - Additional options
58
- * @param {object} options.params - Route params to pass (e.g., { id: 1 })
59
- * @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
60
- * @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
61
- */
62
- function addRouterLink(link, altTitle = null, options = {}) {
63
- const foundRoute = router.getRoutes().find(r => r.name === link)
64
-
65
- if (!foundRoute) {
66
- console.warn(`Route "${link}" not found`)
67
- return
68
- }
69
-
70
- const title = altTitle || foundRoute.meta?.title || foundRoute.name
71
-
72
- // Determine the 'to' value: use options.to if provided, otherwise construct from params, or fall back to path
73
- let toValue = foundRoute.path
74
- if (options.to) {
75
- toValue = options.to
76
- } else if (options.params) {
77
- toValue = { name: link, params: options.params }
78
- }
79
-
80
- const routeLink = {
81
- name: link,
82
- title: title,
83
- path: foundRoute.path,
84
- to: toValue,
85
- icon: foundRoute.meta?.icon || PinIcon,
86
- type: 'route',
87
- props: options.props || {}
88
- }
89
-
90
- addNavigationLink(routeLink)
91
- }
92
-
93
- function addNavigationLink(link) {
94
- const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
95
- if (existingIndex >= 0) {
96
- navigationLinks.value[existingIndex] = { ...link }
97
- } else {
98
- navigationLinks.value.push({ ...link })
99
- }
100
- }
101
-
102
- function addCallback(title, callback, options = {}) {
103
- const callbackLink = {
104
- name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
105
- type: 'callback',
106
- icon: options.icon || PinIcon,
107
- callback: callback || (() => {}),
108
- title: title
109
- }
110
-
111
- addNavigationLink(callbackLink)
112
- }
113
-
114
- function addSeparator(id) {
115
- const separator = {
116
- name: id || `nav-separator-${Date.now()}`,
117
- type: 'separator',
118
- title: 'Separator'
119
- }
120
- addNavigationLink(separator)
121
- }
122
-
123
- function addHtml(html, options = {}) {
124
- const htmlLink = {
125
- name: options.name || `html-item-${Date.now()}`,
126
- type: 'html',
127
- html: html,
128
- title: options.title || 'HTML Item'
129
- }
130
- addNavigationLink(htmlLink)
131
- }
132
-
133
- function removeNavigationLink(linkId) {
134
- navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
135
- }
136
-
137
- function clearNavigationLinks() {
138
- navigationLinks.value = []
139
- }
140
-
141
- function getNavigationLinks() {
142
- return [...navigationLinks.value]
143
- }
51
+ // Get navigation links and isActive function from navigation
52
+ const navigationLinks = navigation ? navigation.navigationLinks : ref([])
53
+ const isActive = navigation ? navigation.isActive : (() => false)
144
54
 
145
55
  function toggleStick() {
146
56
  isStuck.value = !isStuck.value
@@ -168,36 +78,6 @@ function close() {
168
78
  isStuck.value = false
169
79
  }
170
80
 
171
- function isActive(link) {
172
- // If link has a 'to' object with a name, compare route name and params
173
- if (link.to && typeof link.to === 'object' && link.to.name) {
174
- // Compare route names first
175
- if (route.name !== link.to.name) {
176
- return false
177
- }
178
-
179
- // If params are specified in the link, all specified params must match
180
- if (link.to.params && Object.keys(link.to.params).length > 0) {
181
- const linkParams = link.to.params
182
- const routeParams = route.params
183
-
184
- // Check if all params in link.to.params match route.params
185
- for (const key in linkParams) {
186
- if (String(routeParams[key]) !== String(linkParams[key])) {
187
- return false
188
- }
189
- }
190
- }
191
-
192
- // Route name matches, and either no params specified or all params match
193
- return true
194
- }
195
-
196
- // Fall back to path comparison for string paths
197
- const linkPath = typeof link.to === 'string' ? link.to : link.path
198
- return route.path === linkPath
199
- }
200
-
201
81
  function handleLinkClick(callback = null) {
202
82
  if (callback) {
203
83
  callback()
@@ -255,22 +135,12 @@ onUnmounted(() => {
255
135
 
256
136
  defineExpose({
257
137
  isOpen,
258
- navigationLinks,
259
138
  stick,
260
139
  unstick,
261
140
  toggleStick,
262
141
  toggle,
263
142
  open,
264
143
  close,
265
- isActive,
266
- addNavigationLink,
267
- addRouterLink,
268
- addCallback,
269
- addSeparator,
270
- addHtml,
271
- removeNavigationLink,
272
- clearNavigationLinks,
273
- getNavigationLinks,
274
144
  })
275
145
  </script>
276
146
 
@@ -33,7 +33,7 @@
33
33
  </tr>
34
34
  </tbody>
35
35
  </table>
36
- <div class = "padding">
36
+ <div v-if="showPagination" class = "padding">
37
37
  <Pagination :total = "total" v-model:page="page" v-model:page-size="pageSize" />
38
38
  </div>
39
39
  </template>
@@ -56,6 +56,10 @@
56
56
  type: Array,
57
57
  default: () => [],
58
58
  },
59
+ showPagination: {
60
+ type: Boolean,
61
+ default: true,
62
+ },
59
63
  });
60
64
 
61
65
  const slots = useSlots()
@@ -0,0 +1,233 @@
1
+ <template>
2
+ <div class="tabs-container">
3
+ <div class="tabs-header" role="tablist">
4
+ <button
5
+ v-for="(tab, index) in tabs"
6
+ :key="tab.id || index"
7
+ :class="['tab-button', { active: activeTabId === (tab.id || index) }]"
8
+ :aria-selected="activeTabId === (tab.id || index)"
9
+ :aria-controls="`tab-panel-${tab.id || index}`"
10
+ :id="`tab-${tab.id || index}`"
11
+ role="tab"
12
+ @click="selectTab(tab.id || index)"
13
+ >
14
+ <HugeiconsIcon
15
+ v-if="tab.icon"
16
+ :icon="tab.icon"
17
+ width="1em"
18
+ height="1em"
19
+ />
20
+ <span>{{ tab.label }}</span>
21
+ <slot :name="`tab-${tab.id || index}-badge`" />
22
+ </button>
23
+ </div>
24
+ <div class="tabs-content">
25
+ <div
26
+ v-for="(tab, index) in tabs"
27
+ :key="tab.id || index"
28
+ :id="`tab-panel-${tab.id || index}`"
29
+ :class="['tab-panel', { active: activeTabId === (tab.id || index) }]"
30
+ role="tabpanel"
31
+ :aria-labelledby="`tab-${tab.id || index}`"
32
+ v-show="activeTabId === (tab.id || index)"
33
+ >
34
+ <slot :name="`tab-${tab.id || index}`" :tab="tab" />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup>
41
+ import { ref, onMounted, watch } from 'vue';
42
+ import { HugeiconsIcon } from '@hugeicons/vue';
43
+
44
+ const props = defineProps({
45
+ tabs: {
46
+ type: Array,
47
+ required: true,
48
+ validator: (tabs) => {
49
+ return tabs.every(tab =>
50
+ typeof tab === 'object' &&
51
+ (tab.id !== undefined || true) &&
52
+ typeof tab.label === 'string'
53
+ );
54
+ }
55
+ },
56
+ defaultTab: {
57
+ type: [String, Number],
58
+ default: null
59
+ }
60
+ });
61
+
62
+ const emit = defineEmits(['tab-change']);
63
+
64
+ // Initialize activeTabId with first tab by default
65
+ function getInitialTabId() {
66
+ if (props.defaultTab !== null) {
67
+ return props.defaultTab;
68
+ }
69
+ if (props.tabs.length > 0) {
70
+ return props.tabs[0].id ?? 0;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ const activeTabId = ref(getInitialTabId());
76
+
77
+ function selectTab(tabId) {
78
+ if (activeTabId.value !== tabId) {
79
+ activeTabId.value = tabId;
80
+ const activeTab = props.tabs.find(tab => (tab.id ?? props.tabs.indexOf(tab)) === tabId);
81
+ emit('tab-change', activeTab, tabId);
82
+ }
83
+ }
84
+
85
+ function setActiveTab(tabId) {
86
+ const tab = props.tabs.find(t => (t.id ?? props.tabs.indexOf(t)) === tabId);
87
+ if (tab) {
88
+ selectTab(tabId);
89
+ }
90
+ }
91
+
92
+ // Initialize active tab on mount if not already set
93
+ onMounted(() => {
94
+ if (activeTabId.value === null && props.tabs.length > 0) {
95
+ activeTabId.value = props.tabs[0].id ?? 0;
96
+ }
97
+ });
98
+
99
+ // Watch for tabs changes
100
+ watch(() => props.tabs, (newTabs) => {
101
+ if (newTabs.length > 0 && activeTabId.value === null) {
102
+ activeTabId.value = newTabs[0].id ?? 0;
103
+ }
104
+ }, { immediate: true });
105
+
106
+ defineExpose({
107
+ setActiveTab,
108
+ activeTabId
109
+ });
110
+ </script>
111
+
112
+ <style scoped>
113
+ .tabs-container {
114
+ width: 100%;
115
+ }
116
+
117
+ .tabs-header {
118
+ display: flex;
119
+ border-bottom: 2px solid var(--border-color, #e1e5e9);
120
+ gap: 0.25rem;
121
+ overflow-x: auto;
122
+ scrollbar-width: thin;
123
+ }
124
+
125
+ .tabs-header::-webkit-scrollbar {
126
+ height: 4px;
127
+ }
128
+
129
+ .tabs-header::-webkit-scrollbar-track {
130
+ background: transparent;
131
+ }
132
+
133
+ .tabs-header::-webkit-scrollbar-thumb {
134
+ background: var(--border-color, #ccc);
135
+ border-radius: 2px;
136
+ }
137
+
138
+ .tab-button {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 0.5rem;
142
+ padding: 0.75rem 1.25rem;
143
+ background: transparent;
144
+ border: none;
145
+ border-bottom: 2px solid transparent;
146
+ cursor: pointer;
147
+ font-size: 1em;
148
+ color: var(--text-muted, #666);
149
+ transition: all 0.2s ease;
150
+ white-space: nowrap;
151
+ position: relative;
152
+ }
153
+
154
+ .tab-button:hover {
155
+ color: var(--text-color, #000);
156
+ background-color: var(--hover-background-color, #f5f5f5);
157
+ }
158
+
159
+ .tab-button.active {
160
+ color: var(--primary-color, #007bff);
161
+ border-bottom-color: var(--primary-color, #007bff);
162
+ font-weight: 500;
163
+ }
164
+
165
+ .tab-button:focus {
166
+ outline: 2px solid var(--primary-color, #007bff);
167
+ outline-offset: -2px;
168
+ border-radius: 4px 4px 0 0;
169
+ }
170
+
171
+ .tabs-content {
172
+ width: 100%;
173
+ min-height: 200px;
174
+ }
175
+
176
+ .tab-panel {
177
+ padding: 1.5rem;
178
+ display: none;
179
+ }
180
+
181
+ .tab-panel.active {
182
+ display: block;
183
+ animation: fadeIn 0.2s ease-in;
184
+ }
185
+
186
+ @keyframes fadeIn {
187
+ from {
188
+ opacity: 0;
189
+ transform: translateY(4px);
190
+ }
191
+ to {
192
+ opacity: 1;
193
+ transform: translateY(0);
194
+ }
195
+ }
196
+
197
+ /* Dark theme support */
198
+ @media (prefers-color-scheme: dark) {
199
+ .tabs-header {
200
+ border-bottom-color: var(--border-color, #444);
201
+ }
202
+
203
+ .tab-button {
204
+ color: var(--text-muted, #999);
205
+ }
206
+
207
+ .tab-button:hover {
208
+ color: var(--text-color, #fff);
209
+ background-color: var(--hover-background-color, #333);
210
+ }
211
+
212
+ .tab-button.active {
213
+ color: var(--primary-color, #60a5fa);
214
+ border-bottom-color: var(--primary-color, #60a5fa);
215
+ }
216
+ }
217
+
218
+ /* Responsive */
219
+ @media (max-width: 768px) {
220
+ .tabs-header {
221
+ gap: 0;
222
+ }
223
+
224
+ .tab-button {
225
+ padding: 0.75rem 1rem;
226
+ font-size: 0.9em;
227
+ }
228
+
229
+ .tab-panel {
230
+ padding: 1rem;
231
+ }
232
+ }
233
+ </style>