picocrank 1.25.2 → 1.26.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.
Files changed (40) hide show
  1. package/eslint.config.js +68 -0
  2. package/package.json +13 -3
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +20 -11
  5. package/styles/picocrank-extensions.css +77 -30
  6. package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
  7. package/supplemental-themes/dracula-alucard/theme.css +10 -24
  8. package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
  9. package/supplemental-themes/waffles/theme.css +2 -16
  10. package/vue/components/Breadcrumbs.vue +192 -45
  11. package/vue/components/Calendar.vue +45 -25
  12. package/vue/components/DangerZone.vue +186 -0
  13. package/vue/components/Header.vue +22 -6
  14. package/vue/components/NavigationGrid.vue +26 -180
  15. package/vue/components/NotificationBlock.vue +120 -0
  16. package/vue/components/NotificationPopup.vue +44 -41
  17. package/vue/components/ReadOnlyTextArea.vue +1 -1
  18. package/vue/components/SidebarNavLink.vue +3 -3
  19. package/vue/components/Table.vue +107 -79
  20. package/vue/components/TableColumnOptionsPopover.vue +9 -0
  21. package/vue/components/Tabs.vue +363 -79
  22. package/vue/components/TopBar.vue +2 -2
  23. package/vue/composables/tableColumnPriorities.js +33 -0
  24. package/vue/composables/tableFilters.js +1 -1
  25. package/vue/data/navSectionHubs.js +196 -0
  26. package/vue/data/routeBreadcrumbs.js +90 -0
  27. package/vue/examples/Admin.vue +12 -12
  28. package/vue/examples/App.vue +27 -36
  29. package/vue/examples/CalendarExample.vue +32 -22
  30. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  31. package/vue/examples/Login.vue +0 -3
  32. package/vue/examples/LoginExample.vue +2 -14
  33. package/vue/examples/NavSectionHub.vue +36 -0
  34. package/vue/examples/PageStructure.vue +6 -3
  35. package/vue/examples/StatusExample.vue +248 -129
  36. package/vue/examples/TableExample.vue +68 -67
  37. package/vue/examples/TabsExample.vue +200 -255
  38. package/vue/examples/ThemePreview.vue +21 -23
  39. package/vue/examples/ViewItem.vue +32 -5
  40. package/vue/examples/Welcome.vue +3 -40
@@ -0,0 +1,186 @@
1
+ <template>
2
+ <section class="with-header-and-content danger-zone">
3
+ <details
4
+ class="danger-zone-details"
5
+ :open="open"
6
+ @toggle="onToggle"
7
+ >
8
+ <summary class="danger-zone-summary section-header flex-row">
9
+ <div class="fg1 danger-zone-heading">
10
+ <h2 :title="subtitle || undefined" class="danger-zone-title">
11
+ <HugeiconsIcon
12
+ v-if="icon"
13
+ :icon="icon"
14
+ class="danger-zone-title-icon"
15
+ width="1.1em"
16
+ height="1.1em"
17
+ :strokeWidth="2"
18
+ aria-hidden="true"
19
+ />
20
+ <span>{{ title }}</span>
21
+ </h2>
22
+ <span v-if="subtitle" class="danger-zone-subtitle subtle">{{ subtitle }}</span>
23
+ <span v-if="description" class="danger-zone-description subtle">{{ description }}</span>
24
+ </div>
25
+ <span class="danger-zone-chevron" aria-hidden="true">▸</span>
26
+ </summary>
27
+ <Section
28
+ :title="title"
29
+ :subtitle="subtitle"
30
+ :padding="padding"
31
+ :icon="icon"
32
+ :id="sectionId"
33
+ classes="danger-zone-section"
34
+ >
35
+ <template v-if="$slots.toolbar" #toolbar>
36
+ <slot name="toolbar" />
37
+ </template>
38
+ <p v-if="warning" class="danger-zone-warning">{{ warning }}</p>
39
+ <slot />
40
+ </Section>
41
+ </details>
42
+ </section>
43
+ </template>
44
+
45
+ <script setup>
46
+ import { HugeiconsIcon } from '@hugeicons/vue'
47
+ import { Alert02Icon } from '@hugeicons/core-free-icons'
48
+ import Section from './Section.vue'
49
+
50
+ defineProps({
51
+ title: {
52
+ type: String,
53
+ default: 'Danger zone',
54
+ },
55
+ subtitle: {
56
+ type: String,
57
+ default: '',
58
+ },
59
+ description: {
60
+ type: String,
61
+ default: '',
62
+ },
63
+ warning: {
64
+ type: String,
65
+ default: 'These actions are permanent and cannot be undone.',
66
+ },
67
+ padding: {
68
+ type: Boolean,
69
+ default: true,
70
+ },
71
+ icon: {
72
+ type: [Object, Array, Function],
73
+ default: () => Alert02Icon,
74
+ },
75
+ sectionId: {
76
+ type: String,
77
+ default: '',
78
+ },
79
+ })
80
+
81
+ const open = defineModel('open', {
82
+ type: Boolean,
83
+ default: false,
84
+ })
85
+
86
+ function onToggle(event) {
87
+ open.value = event.target.open
88
+ }
89
+ </script>
90
+
91
+ <style scoped>
92
+ .danger-zone-details {
93
+ display: block;
94
+ }
95
+
96
+ .danger-zone-summary {
97
+ cursor: pointer;
98
+ list-style: none;
99
+ user-select: none;
100
+ transition: background-color 0.2s ease;
101
+ }
102
+
103
+ .danger-zone-heading {
104
+ display: flex;
105
+ flex-wrap: wrap;
106
+ align-items: center;
107
+ gap: 0.35em 0.75em;
108
+ min-width: 0;
109
+ }
110
+
111
+ .danger-zone-subtitle,
112
+ .danger-zone-description {
113
+ line-height: 1.4;
114
+ }
115
+
116
+ .danger-zone-chevron {
117
+ flex-shrink: 0;
118
+ margin-left: 0.75em;
119
+ transition: transform 0.15s ease;
120
+ }
121
+
122
+ .danger-zone-details[open] > .danger-zone-summary .danger-zone-chevron {
123
+ transform: rotate(90deg);
124
+ }
125
+
126
+ .danger-zone-details[open] > .danger-zone-summary {
127
+ background: var(--danger-zone-summary-hover-bg);
128
+ border-bottom-color: var(--danger-zone-border);
129
+ }
130
+
131
+ .danger-zone-details:not([open]) > .danger-zone-summary {
132
+ border-radius: 0.4em;
133
+ border-bottom: none;
134
+ }
135
+
136
+ .danger-zone-summary::-webkit-details-marker {
137
+ display: none;
138
+ }
139
+
140
+ .danger-zone-summary:hover {
141
+ background: var(--danger-zone-summary-hover-bg);
142
+ }
143
+
144
+ .danger-zone-summary:focus {
145
+ outline: none;
146
+ }
147
+
148
+ .danger-zone-summary:focus-visible {
149
+ outline: 2px solid var(--danger-zone-focus-outline);
150
+ outline-offset: -2px;
151
+ }
152
+
153
+ .danger-zone-title {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ gap: 0.45em;
157
+ margin: 0;
158
+ padding-top: 0.5em;
159
+ padding-bottom: 0.5em;
160
+ }
161
+
162
+ .danger-zone-title-icon {
163
+ flex: 0 0 auto;
164
+ }
165
+
166
+ .danger-zone :deep(.danger-zone-section.with-header-and-content) {
167
+ margin: 0;
168
+ border: none;
169
+ border-radius: 0;
170
+ box-shadow: none;
171
+ background: transparent;
172
+ }
173
+
174
+ .danger-zone :deep(.danger-zone-section .section-header) {
175
+ display: none;
176
+ }
177
+
178
+ .danger-zone :deep(.danger-zone-section .section-content) {
179
+ border-top: none;
180
+ }
181
+
182
+ .danger-zone-warning {
183
+ margin: 0 0 1rem;
184
+ color: var(--danger-zone-warning-fg);
185
+ }
186
+ </style>
@@ -24,9 +24,9 @@
24
24
  </button>
25
25
  </div>
26
26
 
27
- <TopBar v-if="showTopBar" :navigation="navigation" />
27
+ <Breadcrumbs v-if="showBreadcrumbsBar" />
28
28
 
29
- <Breadcrumbs v-if="breadcrumbs" />
29
+ <TopBar v-if="showTopBar" :navigation="topBarNavigation" />
30
30
 
31
31
  <div class = "fg1"></div>
32
32
 
@@ -138,15 +138,23 @@
138
138
  type: Object,
139
139
  default: null,
140
140
  },
141
+ topBarNavigation: {
142
+ type: Object,
143
+ default: null,
144
+ },
141
145
  });
142
146
 
143
147
  const fallbackSidebar = ref(null);
144
148
 
145
- const { showTopBar, showSidebarChrome, needsFallbackSidebar } = useResponsiveNav(
149
+ const { showTopBar, showSidebarChrome, needsFallbackSidebar, isMobile } = useResponsiveNav(
146
150
  () => props.sidebarEnabled,
147
151
  () => props.topBarEnabled,
148
152
  );
149
153
 
154
+ const showBreadcrumbsBar = computed(
155
+ () => props.breadcrumbs && !isMobile.value,
156
+ );
157
+
150
158
  function toggleNavSidebar() {
151
159
  if (needsFallbackSidebar.value) {
152
160
  fallbackSidebar.value?.toggle();
@@ -165,8 +173,15 @@
165
173
  </script>
166
174
 
167
175
  <style scoped>
176
+ #sidebar-button {
177
+ flex-shrink: 0;
178
+ width: 14em;
179
+ min-width: 14em;
180
+ }
181
+
168
182
  #sidebar-button.disabled-branding {
169
183
  width: auto;
184
+ min-width: 0;
170
185
  }
171
186
 
172
187
  .logo-home-link {
@@ -194,6 +209,7 @@
194
209
  .header-actions {
195
210
  align-self: stretch;
196
211
  gap: 0.25rem;
212
+ flex-shrink: 0;
197
213
  }
198
214
 
199
215
  .theme-toggle {
@@ -205,7 +221,7 @@
205
221
  .header-actions :deep(button) {
206
222
  border: 0;
207
223
  border-radius: 0;
208
- color: var(--header-fg-color, var(--header-text-color, #fff));
224
+ color: var(--header-fg-color);
209
225
  background-color: transparent;
210
226
  align-self: stretch;
211
227
  display: inline-flex;
@@ -227,7 +243,7 @@
227
243
  #sidebar-toggler-button {
228
244
  border: 0;
229
245
  border-radius: 0;
230
- color: var(--header-fg-color, var(--header-text-color, #fff));
246
+ color: var(--header-fg-color);
231
247
  background-color: transparent;
232
248
  align-self: stretch;
233
249
  display: inline-flex;
@@ -260,7 +276,7 @@ header .search-trigger,
260
276
  header .search-trigger.neutral {
261
277
  border: 0;
262
278
  border-radius: 0;
263
- color: var(--header-fg-color, var(--header-text-color, #fff));
279
+ color: var(--header-fg-color);
264
280
  background-color: transparent;
265
281
  align-self: stretch;
266
282
  height: 100%;
@@ -19,20 +19,22 @@
19
19
  class="nav-button-indicator"
20
20
  aria-label="Requires attention"
21
21
  />
22
- <div class="nav-button-icon" :style="getIconStyle(link)">
23
- <HugeiconsIcon
24
- :icon="link.icon"
25
- :width="iconSize"
26
- :height="iconSize"
27
- />
28
- </div>
29
- <div class="nav-button-label">{{ link.title }}</div>
30
- <div v-if="link.description" class="nav-button-description">
31
- {{ link.description }}
22
+ <div class="nav-button-heading">
23
+ <div class="nav-button-icon" :style="getIconStyle(link)">
24
+ <HugeiconsIcon
25
+ :icon="link.icon"
26
+ :width="iconSize"
27
+ :height="iconSize"
28
+ />
32
29
  </div>
30
+ <div class="nav-button-label">{{ link.title }}</div>
31
+ </div>
32
+ <div v-if="link.description" class="nav-button-description">
33
+ {{ link.description }}
34
+ </div>
33
35
  </button>
34
36
  </div>
35
- <div v-if="filteredLinks.length === 0" class="no-links">
37
+ <div v-if="filteredLinks.length === 0" class="navigation-grid-empty">
36
38
  <p>No navigation links available</p>
37
39
  </div>
38
40
  </div>
@@ -40,7 +42,7 @@
40
42
 
41
43
  <script setup>
42
44
  import { computed, inject, ref } from 'vue';
43
- import { useRouter, useRoute } from 'vue-router';
45
+ import { useRouter } from 'vue-router';
44
46
  import { HugeiconsIcon } from '@hugeicons/vue';
45
47
 
46
48
  const props = defineProps({
@@ -63,7 +65,7 @@ const props = defineProps({
63
65
  // Icon size
64
66
  iconSize: {
65
67
  type: String,
66
- default: '2em'
68
+ default: '1.5em'
67
69
  },
68
70
  // Exclude separator and html items
69
71
  excludeNonButtons: {
@@ -73,7 +75,6 @@ const props = defineProps({
73
75
  });
74
76
 
75
77
  const router = useRouter();
76
- const route = useRoute();
77
78
 
78
79
  // Inject navigation from Navigation component
79
80
  const navigation = inject('navigation', null);
@@ -157,179 +158,24 @@ function handleLinkClick(link) {
157
158
  </script>
158
159
 
159
160
  <style scoped>
160
- .navigation-grid-container {
161
- width: 100%;
162
- }
163
-
164
- .navigation-grid {
165
- display: grid;
166
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
167
- gap: 1rem;
168
- padding: 1rem 0;
169
- }
170
-
171
- .navigation-grid.compact {
172
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
173
- gap: 0.75rem;
174
- }
175
-
176
- .nav-button {
161
+ .nav-button-heading {
177
162
  display: flex;
178
- flex-direction: column;
179
- align-items: top;
180
- justify-content: flex-start;
181
- gap: 0.75rem;
182
- padding: 1.25rem 0.75rem;
183
- background: transparent;
184
- border: 1px solid var(--nav-grid-border);
185
- border-radius: 8px;
186
- cursor: pointer;
187
- transition: all 0.2s ease;
188
- text-align: left;
189
- position: relative;
190
- }
191
-
192
- .navigation-grid.compact .nav-button {
193
- padding: 0.75rem 0.5rem;
163
+ align-items: center;
194
164
  gap: 0.5rem;
165
+ width: 100%;
166
+ min-width: 0;
195
167
  }
196
168
 
197
- .nav-button:hover {
198
- border-color: var(--nav-grid-accent-fg);
199
- background: var(--nav-grid-hover-bg);
200
- transform: translateY(-2px);
201
- box-shadow: 0 4px 8px var(--nav-grid-hover-shadow);
202
- }
203
-
204
- .nav-button:active {
205
- transform: translateY(0);
206
- box-shadow: 0 2px 4px var(--nav-grid-shadow);
207
- }
208
-
209
- .nav-button.active {
210
- background: var(--nav-grid-accent-bg);
211
- border-color: var(--nav-grid-accent-bg);
212
- color: var(--nav-grid-active-fg);
213
- }
214
-
215
- .nav-button.active .nav-button-icon {
216
- color: var(--nav-grid-active-fg);
217
- }
218
-
219
- .nav-button-icon {
220
- display: flex;
221
- align-items: start;
222
- justify-content: start;
223
- color: var(--nav-grid-accent-fg);
224
- transition: color 0.2s ease;
225
- flex-shrink: 0;
226
- }
227
-
228
- .nav-button.active .nav-button-icon {
229
- color: var(--nav-grid-active-fg);
230
- }
231
-
232
- .nav-button:disabled,
233
- .nav-button.disabled {
234
- cursor: not-allowed;
235
- opacity: 0.6;
236
- }
237
-
238
- .nav-button:disabled:hover,
239
- .nav-button.disabled:hover {
240
- border-color: var(--nav-grid-border);
241
- transform: none;
242
- box-shadow: none;
243
- background: transparent;
244
- }
245
-
246
- .nav-button:disabled .nav-button-icon,
247
- .nav-button.disabled .nav-button-icon {
248
- color: var(--nav-grid-disabled-fg);
249
- }
250
-
251
- .nav-button-indicator {
252
- position: absolute;
253
- top: 0.75rem;
254
- right: 0.75rem;
255
- width: 0.625rem;
256
- height: 0.625rem;
257
- border-radius: 50%;
258
- background: var(--nav-grid-indicator-bg);
259
- box-shadow: 0 0 0 2px var(--nav-grid-ring-bg);
260
- flex-shrink: 0;
261
- }
262
-
263
- .nav-button-count {
264
- position: absolute;
265
- top: 0.5rem;
266
- right: 0.5rem;
267
- min-width: 1.25rem;
268
- padding: 0.1rem 0.4rem;
269
- border-radius: 999px;
270
- background: var(--nav-grid-indicator-bg);
271
- color: var(--nav-grid-indicator-fg);
272
- font-size: 0.75em;
273
- font-weight: 600;
274
- line-height: 1.2;
275
- text-align: center;
276
- box-shadow: 0 0 0 2px var(--nav-grid-ring-bg);
277
- flex-shrink: 0;
278
- }
279
-
280
- .navigation-grid.compact .nav-button-indicator {
281
- top: 0.5rem;
282
- right: 0.5rem;
283
- }
284
-
285
- .navigation-grid.compact .nav-button-count {
286
- top: 0.35rem;
287
- right: 0.35rem;
288
- }
289
-
290
- .nav-button.active .nav-button-indicator,
291
- .nav-button.active .nav-button-count {
292
- box-shadow: 0 0 0 2px var(--nav-grid-accent-bg);
293
- }
294
-
295
- .nav-button-label {
296
- font-weight: bold;
297
- font-size: 1.125rem;
298
- word-break: break-word;
299
- text-align: left;
300
- line-height: 1.2;
301
- }
302
-
303
- .nav-button.active .nav-button-label {
304
- color: var(--nav-grid-active-fg);
305
- }
306
-
307
- .nav-button-description {
308
- font-weight: normal;
309
- color: var(--nav-grid-muted-fg);
310
- word-break: break-word;
311
- text-align: left;
312
- line-height: 1.5;
313
- }
314
-
315
- .nav-button.active .nav-button-description {
316
- color: var(--nav-grid-active-fg);
169
+ .navigation-grid.compact .nav-button-heading {
170
+ gap: 0.4rem;
317
171
  }
318
172
 
319
- .no-links {
320
- padding: 2rem;
321
- text-align: center;
322
- color: var(--nav-grid-muted-fg);
173
+ .nav-button-heading .nav-button-icon {
174
+ align-items: center;
323
175
  }
324
176
 
325
- @media (max-width: 768px) {
326
- .navigation-grid {
327
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
328
- gap: 0.75rem;
329
- }
330
-
331
- .nav-button {
332
- padding: 1rem 0.5rem;
333
- }
177
+ .nav-button-heading .nav-button-label {
178
+ flex: 1;
179
+ min-width: 0;
334
180
  }
335
181
  </style>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <div
3
+ :class="rootClasses"
4
+ :role="role"
5
+ @click="handleClick"
6
+ >
7
+ <span class="notification-key">{{ displayLabel }}</span>
8
+ <span class="notification-body">
9
+ <slot>
10
+ <template v-if="hasLink">
11
+ <template v-if="messagePrefix">{{ messagePrefix }}</template><RouterLink
12
+ v-if="linkTo"
13
+ :to="linkTo"
14
+ @click.stop
15
+ >{{ linkLabel }}</RouterLink><a
16
+ v-else
17
+ :href="linkHref || '#'"
18
+ @click.stop
19
+ >{{ linkLabel }}</a><template v-if="messageSuffix">{{ messageSuffix }}</template>
20
+ </template>
21
+ <template v-else>{{ message }}</template>
22
+ </slot>
23
+ </span>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { computed } from 'vue'
29
+ import { RouterLink } from 'vue-router'
30
+
31
+ const TYPE_LABELS = {
32
+ info: 'INFO:',
33
+ note: 'NOTE:',
34
+ success: 'SUCCESS:',
35
+ good: 'SUCCESS:',
36
+ warning: 'WARNING:',
37
+ severe: 'SEVERE:',
38
+ critical: 'CRITICAL:',
39
+ error: 'CRITICAL:',
40
+ bad: 'CRITICAL:',
41
+ old: 'OLD:',
42
+ }
43
+
44
+ const props = defineProps({
45
+ type: {
46
+ type: String,
47
+ default: 'info',
48
+ validator: (value) => (
49
+ ['info', 'note', 'success', 'good', 'warning', 'severe', 'critical', 'error', 'bad', 'old'].includes(value)
50
+ ),
51
+ },
52
+ label: {
53
+ type: String,
54
+ default: '',
55
+ },
56
+ message: {
57
+ type: String,
58
+ default: '',
59
+ },
60
+ messagePrefix: {
61
+ type: String,
62
+ default: '',
63
+ },
64
+ linkLabel: {
65
+ type: String,
66
+ default: '',
67
+ },
68
+ linkHref: {
69
+ type: String,
70
+ default: '',
71
+ },
72
+ linkTo: {
73
+ type: [String, Object],
74
+ default: null,
75
+ },
76
+ messageSuffix: {
77
+ type: String,
78
+ default: '',
79
+ },
80
+ clickable: {
81
+ type: Boolean,
82
+ default: false,
83
+ },
84
+ dismissible: {
85
+ type: Boolean,
86
+ default: false,
87
+ },
88
+ role: {
89
+ type: String,
90
+ default: 'status',
91
+ },
92
+ })
93
+
94
+ const emit = defineEmits(['click', 'dismiss'])
95
+
96
+ const displayLabel = computed(() => {
97
+ const trimmed = props.label.trim()
98
+ if (trimmed) {
99
+ return trimmed.endsWith(':') ? trimmed : `${trimmed}:`
100
+ }
101
+ return TYPE_LABELS[props.type]
102
+ })
103
+
104
+ const hasLink = computed(() => Boolean(
105
+ props.linkLabel && (props.linkHref || props.linkTo)
106
+ ))
107
+
108
+ const rootClasses = computed(() => ({
109
+ notification: true,
110
+ [props.type]: true,
111
+ clickable: props.clickable || props.dismissible,
112
+ }))
113
+
114
+ function handleClick(event) {
115
+ emit('click', event)
116
+ if (props.dismissible) {
117
+ emit('dismiss', event)
118
+ }
119
+ }
120
+ </script>