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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.20.0",
3
+ "version": "1.21.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": {
@@ -4,6 +4,7 @@ html[data-theme="dark"] {
4
4
 
5
5
  --border-color: #595959;
6
6
  --hover-background-color: #1d345c;
7
+ --hover-text-color: #fff;
7
8
  --header-hover-background-color: #1d345c;
8
9
  --standout-bg-color: #1f1f1f;
9
10
  --disabled-background-color: #333;
@@ -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.id !== linkId)
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="showIndicator(link)"
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
- .nav-button.active .nav-button-indicator {
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: isActive(item.link) }"
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: white;
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>