picocrank 1.25.2 → 1.27.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 (51) hide show
  1. package/eslint.config.js +75 -0
  2. package/package.json +17 -4
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +20 -11
  5. package/styles/picocrank-extensions.css +123 -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/vite.config.js +5 -0
  11. package/vue/components/Breadcrumbs.vue +192 -45
  12. package/vue/components/Calendar.vue +162 -145
  13. package/vue/components/DangerZone.vue +186 -0
  14. package/vue/components/FormField.spec.js +350 -0
  15. package/vue/components/FormField.vue +236 -32
  16. package/vue/components/FormLayout.spec.js +56 -0
  17. package/vue/components/Header.vue +22 -6
  18. package/vue/components/Login.vue +152 -41
  19. package/vue/components/NavigationGrid.vue +26 -180
  20. package/vue/components/NotificationBlock.vue +120 -0
  21. package/vue/components/NotificationPopup.vue +44 -41
  22. package/vue/components/Pagination.vue +5 -4
  23. package/vue/components/ReadOnlyTextArea.vue +1 -1
  24. package/vue/components/SidebarNavLink.vue +3 -3
  25. package/vue/components/Table.vue +107 -79
  26. package/vue/components/TableColumnOptionsPopover.vue +9 -0
  27. package/vue/components/Tabs.spec.js +101 -0
  28. package/vue/components/Tabs.vue +455 -81
  29. package/vue/components/TopBar.vue +2 -2
  30. package/vue/components/form-validation.spec.js +126 -0
  31. package/vue/composables/tableColumnPriorities.js +33 -0
  32. package/vue/composables/tableFilters.js +1 -1
  33. package/vue/data/navSectionHubs.js +196 -0
  34. package/vue/data/routeBreadcrumbs.js +90 -0
  35. package/vue/examples/Admin.vue +12 -12
  36. package/vue/examples/App.vue +27 -36
  37. package/vue/examples/CalendarExample.vue +32 -22
  38. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  39. package/vue/examples/FormExample.vue +76 -48
  40. package/vue/examples/Login.vue +5 -3
  41. package/vue/examples/LoginExample.vue +99 -15
  42. package/vue/examples/NavSectionHub.vue +36 -0
  43. package/vue/examples/PageStructure.vue +15 -12
  44. package/vue/examples/StatusExample.vue +248 -129
  45. package/vue/examples/TableExample.vue +72 -71
  46. package/vue/examples/TabsExample.vue +236 -257
  47. package/vue/examples/ThemePreview.vue +21 -23
  48. package/vue/examples/ViewItem.vue +32 -5
  49. package/vue/examples/Welcome.vue +3 -40
  50. package/vue/test/formTestUtils.js +279 -0
  51. package/vue/test/setup.js +5 -0
@@ -1,70 +1,128 @@
1
1
  <template>
2
2
  <Section
3
3
  title="Notifications"
4
- subtitle="Block-level status messages using Femtocrank .notification with karma background classes"
4
+ subtitle="Block-level status messages using Femtocrank .notification-key and .notification-body"
5
5
  >
6
- <template #toolbar>
7
- <button type="button" @click="resetNotifications">Reset notifications</button>
8
- <button type="button" class="good" @click="showSampleNotification">Show sample</button>
9
- </template>
6
+ <p>
7
+ Each block uses a karma type class for the key tint
8
+ (<code>.notification.info</code>, <code>.notification.warning</code>, …).
9
+ Add <code>clickable</code> when the whole notification is interactive.
10
+ </p>
10
11
 
11
- <p>Click a notification to dismiss it.</p>
12
+ <FormLayout class="notification-builder" @submit.prevent="addBuiltNotification">
13
+ <FormField label="Type" for="notification-type">
14
+ <select id="notification-type" v-model="builder.type">
15
+ <option
16
+ v-for="option in notificationTypeOptions"
17
+ :key="option.value"
18
+ :value="option.value"
19
+ >
20
+ {{ option.label }}
21
+ </option>
22
+ </select>
23
+ </FormField>
12
24
 
13
- <div
14
- v-for="notification in visibleNotifications"
15
- :key="notification.id"
16
- class="notification show"
17
- :class="notification.class"
18
- role="status"
19
- @click="dismissNotification(notification.id)"
20
- >
21
- <strong>{{ notification.label }}:</strong>
22
- {{ notification.message }}
23
- </div>
24
-
25
- <p v-if="visibleNotifications.length === 0" class="subtle">
26
- All notifications dismissed. Use the toolbar to restore them.
27
- </p>
28
- </Section>
25
+ <FormField label="Key label" for="notification-label">
26
+ <input
27
+ id="notification-label"
28
+ v-model="builder.label"
29
+ type="text"
30
+ placeholder="Defaults from type, e.g. INFO:"
31
+ />
32
+ </FormField>
29
33
 
30
- <Section
31
- title="Inline notifications"
32
- subtitle="Status embedded in content flow using .inline-notification with karma foreground or background classes"
33
- >
34
- <p class="fg-info show inline-notification">
35
- <strong>INFO:</strong>
36
- This is an informational message.
37
- </p>
34
+ <FormField
35
+ :label="builderFeatures.includes('link') ? 'Text before link' : 'Body message'"
36
+ for="notification-message"
37
+ >
38
+ <textarea
39
+ id="notification-message"
40
+ v-model="builder.message"
41
+ rows="2"
42
+ placeholder="Notification body text"
43
+ required
44
+ />
45
+ </FormField>
38
46
 
39
- <p class="fg-note show inline-notification">
40
- <strong>NOTE:</strong>
41
- This is a note.
42
- </p>
47
+ <CheckGroup
48
+ v-model="builderFeatures"
49
+ :options="builderFeatureOptions"
50
+ name="notification-builder-features"
51
+ />
52
+
53
+ <template v-if="builderFeatures.includes('link')">
54
+ <FormField label="Link text" for="notification-link-label">
55
+ <input
56
+ id="notification-link-label"
57
+ v-model="builder.linkLabel"
58
+ type="text"
59
+ placeholder="e.g. New version available"
60
+ />
61
+ </FormField>
62
+
63
+ <FormField label="Text after link" for="notification-link-suffix">
64
+ <input
65
+ id="notification-link-suffix"
66
+ v-model="builder.messageSuffix"
67
+ type="text"
68
+ placeholder="e.g. — click to view release notes."
69
+ />
70
+ </FormField>
71
+ </template>
43
72
 
44
- <p class="fg-success show inline-notification">
45
- <strong>SUCCESS:</strong>
46
- This is a success message.
47
- </p>
73
+ <template #actions>
74
+ <button type="submit" class="good">Add notification</button>
75
+ <button type="button" @click="resetNotifications">Reset</button>
76
+ <button type="button" class="bad" @click="clearNotifications">Clear all</button>
77
+ </template>
78
+ </FormLayout>
48
79
 
49
- <p class="fg-important show inline-notification">
50
- <strong>IMPORTANT:</strong>
51
- This is an important message.
80
+ <p v-if="dismissibleNotifications" class="subtle">
81
+ Click a dismissible notification to remove it.
52
82
  </p>
53
83
 
54
- <p class="fg-warning show inline-notification">
55
- <strong>WARNING:</strong>
56
- This is a warning message.
57
- </p>
84
+ <NotificationBlock
85
+ v-for="notification in notifications"
86
+ :key="notification.id"
87
+ v-bind="notificationProps(notification)"
88
+ :dismissible="notification.dismissible"
89
+ @dismiss="dismissNotification(notification.id)"
90
+ />
91
+ </Section>
58
92
 
59
- <p class="fg-severe show inline-notification">
60
- <strong>SEVERE:</strong>
61
- This is a severe message.
93
+ <Section
94
+ title="Inline karma"
95
+ subtitle="Annotations and tags for inline status (Femtocrank 2.7)"
96
+ >
97
+ <p>
98
+ Inline status no longer uses <code>.inline-notification</code>. Use
99
+ <code>.annotation</code> key/value pairs or <code>.tag</code> chips with karma type classes.
62
100
  </p>
63
101
 
64
- <p class="critical show inline-notification">
65
- <strong>CRITICAL:</strong>
66
- This is a critical message using a background karma class.
67
- </p>
102
+ <span class="annotation">
103
+ <span class="annotation-key">fyi</span>
104
+ <span class="annotation-val">This is an annotation.</span>
105
+ </span>
106
+
107
+ <span class="annotation note">
108
+ <span class="annotation-key">scheduled</span>
109
+ <span class="annotation-val">next release</span>
110
+ </span>
111
+
112
+ <span class="annotation good">
113
+ <span class="annotation-key">status</span>
114
+ <span class="annotation-val">active</span>
115
+ </span>
116
+
117
+ <br /><br />
118
+
119
+ <span class="tag">Neutral tag</span>
120
+ <span class="tag info">info</span>
121
+ <span class="tag note">note</span>
122
+ <span class="tag good">good/success</span>
123
+ <span class="tag warning">warning</span>
124
+ <span class="tag severe">severe</span>
125
+ <span class="tag bad">bad/critical</span>
68
126
  </Section>
69
127
 
70
128
  <Section
@@ -87,7 +145,7 @@
87
145
 
88
146
  <Section
89
147
  title="In context"
90
- subtitle="Inline notifications beside or below form fields"
148
+ subtitle="Block notifications beside form fields"
91
149
  >
92
150
  <FormLayout class="status-form" @submit.prevent="submitForm">
93
151
  <FormField label="Email" for="status-email">
@@ -100,15 +158,10 @@
100
158
  />
101
159
  </FormField>
102
160
 
103
- <p
104
- v-if="formMessage"
105
- class="show inline-notification"
106
- :class="formMessageClass"
107
- role="status"
108
- >
109
- <strong>{{ formMessageLabel }}:</strong>
110
- {{ formMessage }}
111
- </p>
161
+ <NotificationBlock
162
+ v-if="formNotification"
163
+ v-bind="formNotification"
164
+ />
112
165
 
113
166
  <template #actions>
114
167
  <button type="submit" class="good">Save</button>
@@ -123,111 +176,176 @@ import { ref, computed } from 'vue'
123
176
  import Section from '../components/Section.vue'
124
177
  import FormLayout from '../components/FormLayout.vue'
125
178
  import FormField from '../components/FormField.vue'
179
+ import CheckGroup from '../components/CheckGroup.vue'
180
+ import NotificationBlock from '../components/NotificationBlock.vue'
126
181
  import { useNotificationPopups } from '../composables/useNotificationPopups.js'
127
182
 
128
183
  const { show: showPopup, dismissAll: dismissAllPopups } = useNotificationPopups()
129
184
 
130
- const blockNotifications = ref([
185
+ const notificationTypeOptions = [
186
+ { value: 'info', label: 'Info' },
187
+ { value: 'note', label: 'Note' },
188
+ { value: 'success', label: 'Success' },
189
+ { value: 'warning', label: 'Warning' },
190
+ { value: 'severe', label: 'Severe' },
191
+ { value: 'critical', label: 'Critical' },
192
+ { value: 'old', label: 'Old' },
193
+ ]
194
+
195
+ const builderFeatureOptions = [
196
+ { value: 'link', label: 'Include inline link in body' },
197
+ { value: 'clickable', label: 'Whole notification is clickable' },
198
+ { value: 'dismissible', label: 'Dismiss on click' },
199
+ ]
200
+
201
+ const referenceNotifications = [
131
202
  {
132
- id: 'info',
133
- class: 'info',
134
- label: 'INFO',
135
- message: 'This is an informational message.',
203
+ id: 'ref-info',
204
+ type: 'info',
205
+ linkLabel: 'New version available',
206
+ linkHref: '#',
207
+ messageSuffix: ' — click to view release notes.',
136
208
  },
137
209
  {
138
- id: 'note',
139
- class: 'note',
140
- label: 'NOTE',
141
- message: 'This is a note.',
210
+ id: 'ref-note',
211
+ type: 'note',
212
+ messagePrefix: 'See the ',
213
+ linkLabel: 'release notes',
214
+ linkHref: '#',
215
+ messageSuffix: ' for what changed.',
142
216
  },
143
217
  {
144
- id: 'success',
145
- class: 'success',
146
- label: 'SUCCESS',
147
- message: 'The operation completed successfully.',
218
+ id: 'ref-success',
219
+ type: 'success',
220
+ message: 'This is a success message.',
148
221
  },
149
222
  {
150
- id: 'important',
151
- class: 'important',
152
- label: 'IMPORTANT',
153
- message: 'Please review this before continuing.',
223
+ id: 'ref-warning',
224
+ type: 'warning',
225
+ messagePrefix: 'Disk usage is high — ',
226
+ linkLabel: 'manage storage',
227
+ linkHref: '#',
228
+ messageSuffix: ' before continuing.',
154
229
  },
155
230
  {
156
- id: 'warning',
157
- class: 'warning',
158
- label: 'WARNING',
159
- message: 'This action may have side effects.',
231
+ id: 'ref-severe',
232
+ type: 'severe',
233
+ messagePrefix: 'Service is degraded. Check the ',
234
+ linkLabel: 'status page',
235
+ linkHref: '#',
236
+ messageSuffix: ' for updates.',
160
237
  },
161
238
  {
162
- id: 'severe',
163
- class: 'severe',
164
- label: 'SEVERE',
165
- message: 'A severe problem was detected.',
239
+ id: 'ref-critical',
240
+ type: 'critical',
241
+ clickable: true,
242
+ linkLabel: 'Database unreachable',
243
+ linkHref: '#',
244
+ messageSuffix: ' — follow the recovery runbook immediately.',
166
245
  },
167
- {
168
- id: 'critical',
169
- class: 'critical',
170
- label: 'CRITICAL',
171
- message: 'Immediate attention is required.',
172
- },
173
- ])
246
+ ]
247
+
248
+ let nextNotificationId = 1
249
+
250
+ const builder = ref({
251
+ type: 'info',
252
+ label: '',
253
+ message: '',
254
+ linkLabel: '',
255
+ messageSuffix: '',
256
+ })
174
257
 
175
- const initialNotificationIds = blockNotifications.value.map((notification) => notification.id)
258
+ const builderFeatures = ref([])
176
259
 
177
- const visibleNotificationIds = ref([...initialNotificationIds])
178
- const sampleCounter = ref(0)
260
+ const notifications = ref(referenceNotifications.map(cloneNotification))
179
261
 
180
- const visibleNotifications = computed(() =>
181
- blockNotifications.value.filter((notification) => visibleNotificationIds.value.includes(notification.id))
262
+ const dismissibleNotifications = computed(() =>
263
+ notifications.value.some((notification) => notification.dismissible)
182
264
  )
183
265
 
184
266
  const formEmail = ref('')
185
- const formMessage = ref('')
186
- const formMessageClass = ref('fg-note')
187
- const formMessageLabel = ref('NOTE')
267
+ const formNotification = ref(null)
188
268
 
189
- function dismissNotification(id) {
190
- visibleNotificationIds.value = visibleNotificationIds.value.filter((visibleId) => visibleId !== id)
269
+ function cloneNotification(notification) {
270
+ return { ...notification }
191
271
  }
192
272
 
193
- function resetNotifications() {
194
- blockNotifications.value = blockNotifications.value.filter((notification) =>
195
- initialNotificationIds.includes(notification.id)
196
- )
197
- visibleNotificationIds.value = [...initialNotificationIds]
198
- sampleCounter.value = 0
273
+ function notificationProps(notification) {
274
+ return {
275
+ type: notification.type,
276
+ label: notification.label,
277
+ message: notification.message,
278
+ messagePrefix: notification.messagePrefix,
279
+ linkLabel: notification.linkLabel,
280
+ linkHref: notification.linkHref,
281
+ linkTo: notification.linkTo,
282
+ messageSuffix: notification.messageSuffix,
283
+ clickable: notification.clickable,
284
+ }
199
285
  }
200
286
 
201
- function showSampleNotification() {
202
- sampleCounter.value += 1
203
- const sample = {
204
- id: `sample-${sampleCounter.value}`,
205
- class: 'info',
206
- label: 'INFO',
207
- message: `Sample notification #${sampleCounter.value}.`,
287
+ function createNotificationFromBuilder() {
288
+ const includeLink = builderFeatures.value.includes('link')
289
+ const id = `custom-${nextNotificationId++}`
290
+ const trimmedMessage = builder.value.message.trim()
291
+
292
+ return {
293
+ id,
294
+ type: builder.value.type,
295
+ label: builder.value.label.trim(),
296
+ message: includeLink ? '' : trimmedMessage,
297
+ messagePrefix: includeLink ? trimmedMessage : '',
298
+ linkLabel: includeLink ? builder.value.linkLabel.trim() : '',
299
+ linkHref: includeLink ? '#' : '',
300
+ messageSuffix: includeLink ? builder.value.messageSuffix : '',
301
+ clickable: builderFeatures.value.includes('clickable'),
302
+ dismissible: builderFeatures.value.includes('dismissible'),
303
+ }
304
+ }
305
+
306
+ function addBuiltNotification() {
307
+ notifications.value.push(createNotificationFromBuilder())
308
+ }
309
+
310
+ function resetNotifications() {
311
+ notifications.value = referenceNotifications.map(cloneNotification)
312
+ builder.value = {
313
+ type: 'info',
314
+ label: '',
315
+ message: '',
316
+ linkLabel: '',
317
+ messageSuffix: '',
208
318
  }
209
- blockNotifications.value.push(sample)
210
- visibleNotificationIds.value = [...visibleNotificationIds.value, sample.id]
319
+ builderFeatures.value = []
320
+ nextNotificationId = 1
321
+ }
322
+
323
+ function clearNotifications() {
324
+ notifications.value = []
325
+ }
326
+
327
+ function dismissNotification(id) {
328
+ notifications.value = notifications.value.filter((notification) => notification.id !== id)
211
329
  }
212
330
 
213
331
  function submitForm() {
214
332
  if (!formEmail.value.trim()) {
215
- formMessageClass.value = 'fg-warning'
216
- formMessageLabel.value = 'WARNING'
217
- formMessage.value = 'Please enter an email address.'
333
+ formNotification.value = {
334
+ type: 'warning',
335
+ message: 'Please enter an email address.',
336
+ }
218
337
  return
219
338
  }
220
339
 
221
- formMessageClass.value = 'fg-success'
222
- formMessageLabel.value = 'SUCCESS'
223
- formMessage.value = `Saved preferences for ${formEmail.value.trim()}.`
340
+ formNotification.value = {
341
+ type: 'success',
342
+ message: `Saved preferences for ${formEmail.value.trim()}.`,
343
+ }
224
344
  }
225
345
 
226
346
  function clearForm() {
227
347
  formEmail.value = ''
228
- formMessage.value = ''
229
- formMessageClass.value = 'fg-note'
230
- formMessageLabel.value = 'NOTE'
348
+ formNotification.value = null
231
349
  }
232
350
 
233
351
  function showSuccessPopup() {
@@ -270,7 +388,8 @@ function showStackedPopups() {
270
388
  </script>
271
389
 
272
390
  <style scoped>
391
+ .notification-builder,
273
392
  .status-form {
274
- max-width: 32rem;
393
+ max-width: 36rem;
275
394
  }
276
395
  </style>