picocrank 1.20.0 → 1.21.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
package/styles/dark-theme.css
CHANGED
|
@@ -20,6 +20,11 @@ const router = useRouter()
|
|
|
20
20
|
* @param {object} options.params - Route params to pass (e.g., { id: 1 })
|
|
21
21
|
* @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
|
|
22
22
|
* @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
|
|
23
|
+
* @param {boolean} options.disabled - Disable the link
|
|
24
|
+
* @param {boolean} options.indicator - Show a notification indicator dot
|
|
25
|
+
* @param {number} options.count - Show a notification count badge (takes precedence over indicator)
|
|
26
|
+
* @param {string} options.iconColor - Optional icon color
|
|
27
|
+
* @param {string} options.description - Optional description (used by NavigationGrid)
|
|
23
28
|
*/
|
|
24
29
|
function addRouterLink(link, altTitle = null, options = {}) {
|
|
25
30
|
const foundRoute = router.getRoutes().find(r => r.name === link)
|
|
@@ -50,7 +55,8 @@ function addRouterLink(link, altTitle = null, options = {}) {
|
|
|
50
55
|
description: options.description || foundRoute.meta?.description || null,
|
|
51
56
|
disabled: options.disabled || false,
|
|
52
57
|
iconColor: options.iconColor || null,
|
|
53
|
-
indicator: options.indicator || false
|
|
58
|
+
indicator: options.indicator || false,
|
|
59
|
+
count: normalizeCount(options.count),
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
addNavigationLink(routeLink)
|
|
@@ -75,12 +81,24 @@ function addCallback(title, callback, options = {}) {
|
|
|
75
81
|
description: options.description || null,
|
|
76
82
|
disabled: options.disabled || false,
|
|
77
83
|
iconColor: options.iconColor || null,
|
|
78
|
-
indicator: options.indicator || false
|
|
84
|
+
indicator: options.indicator || false,
|
|
85
|
+
count: normalizeCount(options.count),
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
addNavigationLink(callbackLink)
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
function normalizeCount(count) {
|
|
92
|
+
if (count === null || count === undefined || count === false) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
const value = Number(count)
|
|
96
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
return Math.floor(value)
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
function addSeparator(id) {
|
|
85
103
|
const separator = {
|
|
86
104
|
name: id || `nav-separator-${Date.now()}`,
|
|
@@ -110,7 +128,7 @@ function addHtml(html, options = {}) {
|
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
function removeNavigationLink(linkId) {
|
|
113
|
-
navigationLinks.value = navigationLinks.value.filter(link => link.
|
|
131
|
+
navigationLinks.value = navigationLinks.value.filter(link => link.name !== linkId)
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
function clearNavigationLinks() {
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
@click="handleLinkClick(link)"
|
|
11
11
|
>
|
|
12
12
|
<span
|
|
13
|
-
v-if="
|
|
13
|
+
v-if="showCount(link)"
|
|
14
|
+
class="nav-button-count"
|
|
15
|
+
:aria-label="`${link.count} notifications`"
|
|
16
|
+
>{{ formatCount(link.count) }}</span>
|
|
17
|
+
<span
|
|
18
|
+
v-else-if="showIndicator(link)"
|
|
14
19
|
class="nav-button-indicator"
|
|
15
20
|
aria-label="Requires attention"
|
|
16
21
|
/>
|
|
@@ -124,8 +129,16 @@ function getIconStyle(link) {
|
|
|
124
129
|
return undefined;
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
function showCount(link) {
|
|
133
|
+
return !link.disabled && link.count != null && link.count > 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
function showIndicator(link) {
|
|
128
|
-
return link.indicator && !link.disabled;
|
|
137
|
+
return link.indicator && !link.disabled && !showCount(link);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatCount(count) {
|
|
141
|
+
return count > 99 ? '99+' : String(count);
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
function handleLinkClick(link) {
|
|
@@ -246,12 +259,35 @@ function handleLinkClick(link) {
|
|
|
246
259
|
flex-shrink: 0;
|
|
247
260
|
}
|
|
248
261
|
|
|
262
|
+
.nav-button-count {
|
|
263
|
+
position: absolute;
|
|
264
|
+
top: 0.5rem;
|
|
265
|
+
right: 0.5rem;
|
|
266
|
+
min-width: 1.25rem;
|
|
267
|
+
padding: 0.1rem 0.4rem;
|
|
268
|
+
border-radius: 999px;
|
|
269
|
+
background: var(--indicator-color, #dc3545);
|
|
270
|
+
color: #fff;
|
|
271
|
+
font-size: 0.75em;
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
line-height: 1.2;
|
|
274
|
+
text-align: center;
|
|
275
|
+
box-shadow: 0 0 0 2px var(--background-color, #fff);
|
|
276
|
+
flex-shrink: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
249
279
|
.navigation-grid.compact .nav-button-indicator {
|
|
250
280
|
top: 0.5rem;
|
|
251
281
|
right: 0.5rem;
|
|
252
282
|
}
|
|
253
283
|
|
|
254
|
-
.
|
|
284
|
+
.navigation-grid.compact .nav-button-count {
|
|
285
|
+
top: 0.35rem;
|
|
286
|
+
right: 0.35rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.nav-button.active .nav-button-indicator,
|
|
290
|
+
.nav-button.active .nav-button-count {
|
|
255
291
|
box-shadow: 0 0 0 2px var(--primary-color, #007bff);
|
|
256
292
|
}
|
|
257
293
|
|
|
@@ -37,24 +37,49 @@
|
|
|
37
37
|
<li
|
|
38
38
|
v-else
|
|
39
39
|
:title="item.link.title"
|
|
40
|
+
:class="{ disabled: item.link.disabled }"
|
|
40
41
|
>
|
|
41
42
|
<div v-if="item.link.type === 'separator'" class="separator"></div>
|
|
42
|
-
<div v-else-if="item.link.type === 'callback'">
|
|
43
|
-
<a href="#" @click.prevent="handleLinkClick(() => item.link.callback())">
|
|
44
|
-
<HugeiconsIcon :icon="item.link.icon" />
|
|
45
|
-
<span>{{ item.link.title }}</span>
|
|
46
|
-
</a>
|
|
47
|
-
</div>
|
|
48
43
|
<div v-else-if="item.link.type === 'html'" v-html="item.link.html"></div>
|
|
44
|
+
<a
|
|
45
|
+
v-else-if="item.link.type === 'callback' || item.link.disabled"
|
|
46
|
+
href="#"
|
|
47
|
+
:class="{ active: isLinkActive(item.link), disabled: item.link.disabled }"
|
|
48
|
+
:aria-disabled="item.link.disabled || undefined"
|
|
49
|
+
@click.prevent="handleNavItemClick(item.link)"
|
|
50
|
+
>
|
|
51
|
+
<HugeiconsIcon :icon="item.link.icon" />
|
|
52
|
+
<span class="nav-link-title">{{ item.link.title }}</span>
|
|
53
|
+
<span
|
|
54
|
+
v-if="showCount(item.link)"
|
|
55
|
+
class="nav-link-count"
|
|
56
|
+
:aria-label="`${item.link.count} notifications`"
|
|
57
|
+
>{{ formatCount(item.link.count) }}</span>
|
|
58
|
+
<span
|
|
59
|
+
v-else-if="showIndicator(item.link)"
|
|
60
|
+
class="nav-link-indicator"
|
|
61
|
+
aria-label="Requires attention"
|
|
62
|
+
/>
|
|
63
|
+
</a>
|
|
49
64
|
<router-link
|
|
50
65
|
v-else
|
|
51
66
|
v-bind="item.link.props || {}"
|
|
52
67
|
:to="item.link.to || item.link.path"
|
|
53
|
-
:class="{ active:
|
|
68
|
+
:class="{ active: isLinkActive(item.link) }"
|
|
54
69
|
@click="handleLinkClick()"
|
|
55
70
|
>
|
|
56
71
|
<HugeiconsIcon :icon="item.link.icon" />
|
|
57
|
-
<span>{{ item.link.title }}</span>
|
|
72
|
+
<span class="nav-link-title">{{ item.link.title }}</span>
|
|
73
|
+
<span
|
|
74
|
+
v-if="showCount(item.link)"
|
|
75
|
+
class="nav-link-count"
|
|
76
|
+
:aria-label="`${item.link.count} notifications`"
|
|
77
|
+
>{{ formatCount(item.link.count) }}</span>
|
|
78
|
+
<span
|
|
79
|
+
v-else-if="showIndicator(item.link)"
|
|
80
|
+
class="nav-link-indicator"
|
|
81
|
+
aria-label="Requires attention"
|
|
82
|
+
/>
|
|
58
83
|
</router-link>
|
|
59
84
|
</li>
|
|
60
85
|
</template>
|
|
@@ -212,6 +237,36 @@ function close() {
|
|
|
212
237
|
isStuck.value = false
|
|
213
238
|
}
|
|
214
239
|
|
|
240
|
+
function isLinkActive(link) {
|
|
241
|
+
if (link.disabled) {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
return isActive(link)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function showCount(link) {
|
|
248
|
+
return !link.disabled && link.count != null && link.count > 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function showIndicator(link) {
|
|
252
|
+
return link.indicator && !link.disabled && !showCount(link)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatCount(count) {
|
|
256
|
+
return count > 99 ? '99+' : String(count)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function handleNavItemClick(link) {
|
|
260
|
+
if (link.disabled) {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if (link.type === 'callback' && link.callback) {
|
|
264
|
+
handleLinkClick(() => link.callback())
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
handleLinkClick()
|
|
268
|
+
}
|
|
269
|
+
|
|
215
270
|
function handleLinkClick(callback = null) {
|
|
216
271
|
if (callback) {
|
|
217
272
|
callback()
|
|
@@ -294,6 +349,39 @@ button {
|
|
|
294
349
|
gap: 0.75rem;
|
|
295
350
|
padding: .75em;
|
|
296
351
|
border-radius: 0;
|
|
352
|
+
position: relative;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.nav-link-title {
|
|
356
|
+
flex: 1;
|
|
357
|
+
min-width: 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.nav-link-indicator {
|
|
361
|
+
width: 0.5rem;
|
|
362
|
+
height: 0.5rem;
|
|
363
|
+
border-radius: 50%;
|
|
364
|
+
background: var(--indicator-color, #dc3545);
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.nav-link-count {
|
|
369
|
+
min-width: 1.25rem;
|
|
370
|
+
padding: 0.1rem 0.4rem;
|
|
371
|
+
border-radius: 999px;
|
|
372
|
+
background: var(--indicator-color, #dc3545);
|
|
373
|
+
color: #fff;
|
|
374
|
+
font-size: 0.75em;
|
|
375
|
+
font-weight: 600;
|
|
376
|
+
line-height: 1.2;
|
|
377
|
+
text-align: center;
|
|
378
|
+
flex-shrink: 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.navigation-links a.disabled,
|
|
382
|
+
.navigation-links li.disabled a {
|
|
383
|
+
cursor: not-allowed;
|
|
384
|
+
opacity: 0.55;
|
|
297
385
|
}
|
|
298
386
|
|
|
299
387
|
.nav-section-header {
|
|
@@ -308,12 +396,13 @@ button {
|
|
|
308
396
|
font-size: 1em;
|
|
309
397
|
color: inherit;
|
|
310
398
|
background: transparent;
|
|
399
|
+
border-radius: 0;
|
|
311
400
|
cursor: pointer;
|
|
312
401
|
}
|
|
313
402
|
|
|
314
403
|
.nav-section-header:hover {
|
|
315
404
|
background-color: var(--hover-background-color);
|
|
316
|
-
color:
|
|
405
|
+
color: var(--hover-text-color);
|
|
317
406
|
}
|
|
318
407
|
|
|
319
408
|
.nav-section-chevron {
|
|
@@ -77,6 +77,16 @@ onMounted(() => {
|
|
|
77
77
|
indicator: true,
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
localNavigation.value.addCallback('Inbox', () => {
|
|
81
|
+
alert('Inbox action')
|
|
82
|
+
}, {
|
|
83
|
+
icon: ViewIcon,
|
|
84
|
+
name: 'inbox',
|
|
85
|
+
description: 'Shows a notification count on the tile',
|
|
86
|
+
iconColor: '#e67e22',
|
|
87
|
+
count: 12,
|
|
88
|
+
})
|
|
89
|
+
|
|
80
90
|
localNavigation.value.addCallback('Billing', () => {
|
|
81
91
|
alert('This item is disabled')
|
|
82
92
|
}, {
|
|
@@ -77,13 +77,42 @@
|
|
|
77
77
|
When enabled, use the menu button in the header to open and pin the sidebar.
|
|
78
78
|
</p>
|
|
79
79
|
</dd>
|
|
80
|
+
|
|
81
|
+
<dt>Example link states</dt>
|
|
82
|
+
<dd>
|
|
83
|
+
<label class="page-structure-control">
|
|
84
|
+
<input
|
|
85
|
+
type="checkbox"
|
|
86
|
+
:checked="exampleLinksEnabled"
|
|
87
|
+
@change="toggleExampleLinks"
|
|
88
|
+
/>
|
|
89
|
+
<span>Add example links that demonstrate indicator, count, and disabled states</span>
|
|
90
|
+
</label>
|
|
91
|
+
<p class="subtle">
|
|
92
|
+
Adds a “Link states” section to the sidebar with an attention indicator,
|
|
93
|
+
a notification count badge, and a disabled item.
|
|
94
|
+
</p>
|
|
95
|
+
</dd>
|
|
80
96
|
</dl>
|
|
81
97
|
</Section>
|
|
82
98
|
</template>
|
|
83
99
|
|
|
84
100
|
<script setup>
|
|
85
|
-
import { inject } from 'vue'
|
|
101
|
+
import { inject, ref, onBeforeUnmount } from 'vue'
|
|
86
102
|
import Section from '../components/Section.vue'
|
|
103
|
+
import {
|
|
104
|
+
Notification01Icon,
|
|
105
|
+
Alert02Icon,
|
|
106
|
+
UnavailableIcon,
|
|
107
|
+
} from '@hugeicons/core-free-icons'
|
|
108
|
+
|
|
109
|
+
const EXAMPLE_SECTION_ID = 'nav-link-states'
|
|
110
|
+
const EXAMPLE_LINK_NAMES = [
|
|
111
|
+
EXAMPLE_SECTION_ID,
|
|
112
|
+
'example-indicator',
|
|
113
|
+
'example-count',
|
|
114
|
+
'example-disabled',
|
|
115
|
+
]
|
|
87
116
|
|
|
88
117
|
const sidebarEnabled = inject('sidebarEnabled')
|
|
89
118
|
const toggleSidebarEnabled = inject('toggleSidebarEnabled')
|
|
@@ -95,6 +124,70 @@ const topBarEnabled = inject('topBarEnabled')
|
|
|
95
124
|
const toggleTopBarEnabled = inject('toggleTopBarEnabled')
|
|
96
125
|
const breadcrumbsEnabled = inject('breadcrumbsEnabled')
|
|
97
126
|
const toggleBreadcrumbsEnabled = inject('toggleBreadcrumbsEnabled')
|
|
127
|
+
const navigation = inject('navigation', null)
|
|
128
|
+
|
|
129
|
+
const exampleLinksEnabled = ref(false)
|
|
130
|
+
|
|
131
|
+
function removeExampleLinks() {
|
|
132
|
+
if (!navigation) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
for (const name of EXAMPLE_LINK_NAMES) {
|
|
136
|
+
navigation.removeNavigationLink(name)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function addExampleLinks() {
|
|
141
|
+
if (!navigation) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
removeExampleLinks()
|
|
146
|
+
|
|
147
|
+
navigation.addSection('Link states', { name: EXAMPLE_SECTION_ID })
|
|
148
|
+
|
|
149
|
+
navigation.addCallback('Needs attention', () => {
|
|
150
|
+
alert('Example link with a notification indicator')
|
|
151
|
+
}, {
|
|
152
|
+
name: 'example-indicator',
|
|
153
|
+
icon: Alert02Icon,
|
|
154
|
+
indicator: true,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
navigation.addCallback('Inbox', () => {
|
|
158
|
+
alert('Example link with a notification count')
|
|
159
|
+
}, {
|
|
160
|
+
name: 'example-count',
|
|
161
|
+
icon: Notification01Icon,
|
|
162
|
+
count: 3,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
navigation.addCallback('Unavailable', () => {
|
|
166
|
+
alert('This should not run while disabled')
|
|
167
|
+
}, {
|
|
168
|
+
name: 'example-disabled',
|
|
169
|
+
icon: UnavailableIcon,
|
|
170
|
+
disabled: true,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toggleExampleLinks() {
|
|
175
|
+
exampleLinksEnabled.value = !exampleLinksEnabled.value
|
|
176
|
+
if (exampleLinksEnabled.value) {
|
|
177
|
+
if (!sidebarEnabled.value) {
|
|
178
|
+
toggleSidebarEnabled()
|
|
179
|
+
}
|
|
180
|
+
addExampleLinks()
|
|
181
|
+
} else {
|
|
182
|
+
removeExampleLinks()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onBeforeUnmount(() => {
|
|
187
|
+
if (exampleLinksEnabled.value) {
|
|
188
|
+
removeExampleLinks()
|
|
189
|
+
}
|
|
190
|
+
})
|
|
98
191
|
</script>
|
|
99
192
|
|
|
100
193
|
<style scoped>
|