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.
- package/eslint.config.js +75 -0
- package/package.json +17 -4
- package/router.js +74 -0
- package/styles/dark-theme.css +20 -11
- package/styles/picocrank-extensions.css +123 -30
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
- package/supplemental-themes/dracula-alucard/theme.css +10 -24
- package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
- package/supplemental-themes/waffles/theme.css +2 -16
- package/vite.config.js +5 -0
- package/vue/components/Breadcrumbs.vue +192 -45
- package/vue/components/Calendar.vue +162 -145
- package/vue/components/DangerZone.vue +186 -0
- package/vue/components/FormField.spec.js +350 -0
- package/vue/components/FormField.vue +236 -32
- package/vue/components/FormLayout.spec.js +56 -0
- package/vue/components/Header.vue +22 -6
- package/vue/components/Login.vue +152 -41
- package/vue/components/NavigationGrid.vue +26 -180
- package/vue/components/NotificationBlock.vue +120 -0
- package/vue/components/NotificationPopup.vue +44 -41
- package/vue/components/Pagination.vue +5 -4
- package/vue/components/ReadOnlyTextArea.vue +1 -1
- package/vue/components/SidebarNavLink.vue +3 -3
- package/vue/components/Table.vue +107 -79
- package/vue/components/TableColumnOptionsPopover.vue +9 -0
- package/vue/components/Tabs.spec.js +101 -0
- package/vue/components/Tabs.vue +455 -81
- package/vue/components/TopBar.vue +2 -2
- package/vue/components/form-validation.spec.js +126 -0
- package/vue/composables/tableColumnPriorities.js +33 -0
- package/vue/composables/tableFilters.js +1 -1
- package/vue/data/navSectionHubs.js +196 -0
- package/vue/data/routeBreadcrumbs.js +90 -0
- package/vue/examples/Admin.vue +12 -12
- package/vue/examples/App.vue +27 -36
- package/vue/examples/CalendarExample.vue +32 -22
- package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
- package/vue/examples/FormExample.vue +76 -48
- package/vue/examples/Login.vue +5 -3
- package/vue/examples/LoginExample.vue +99 -15
- package/vue/examples/NavSectionHub.vue +36 -0
- package/vue/examples/PageStructure.vue +15 -12
- package/vue/examples/StatusExample.vue +248 -129
- package/vue/examples/TableExample.vue +72 -71
- package/vue/examples/TabsExample.vue +236 -257
- package/vue/examples/ThemePreview.vue +21 -23
- package/vue/examples/ViewItem.vue +32 -5
- package/vue/examples/Welcome.vue +3 -40
- package/vue/test/formTestUtils.js +279 -0
- 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
|
|
4
|
+
subtitle="Block-level status messages using Femtocrank .notification-key and .notification-body"
|
|
5
5
|
>
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
50
|
-
|
|
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
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
<
|
|
65
|
-
<
|
|
66
|
-
This is
|
|
67
|
-
</
|
|
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="
|
|
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
|
-
<
|
|
104
|
-
v-if="
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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: '
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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: '
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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: '
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
258
|
+
const builderFeatures = ref([])
|
|
176
259
|
|
|
177
|
-
const
|
|
178
|
-
const sampleCounter = ref(0)
|
|
260
|
+
const notifications = ref(referenceNotifications.map(cloneNotification))
|
|
179
261
|
|
|
180
|
-
const
|
|
181
|
-
|
|
262
|
+
const dismissibleNotifications = computed(() =>
|
|
263
|
+
notifications.value.some((notification) => notification.dismissible)
|
|
182
264
|
)
|
|
183
265
|
|
|
184
266
|
const formEmail = ref('')
|
|
185
|
-
const
|
|
186
|
-
const formMessageClass = ref('fg-note')
|
|
187
|
-
const formMessageLabel = ref('NOTE')
|
|
267
|
+
const formNotification = ref(null)
|
|
188
268
|
|
|
189
|
-
function
|
|
190
|
-
|
|
269
|
+
function cloneNotification(notification) {
|
|
270
|
+
return { ...notification }
|
|
191
271
|
}
|
|
192
272
|
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
333
|
+
formNotification.value = {
|
|
334
|
+
type: 'warning',
|
|
335
|
+
message: 'Please enter an email address.',
|
|
336
|
+
}
|
|
218
337
|
return
|
|
219
338
|
}
|
|
220
339
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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:
|
|
393
|
+
max-width: 36rem;
|
|
275
394
|
}
|
|
276
395
|
</style>
|