picocrank 1.18.1 → 1.18.3
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/index.html +10 -0
- package/package.json +1 -1
- package/router.js +43 -12
- package/styles/dark-theme.css +189 -0
- package/styles.css +1 -1
- package/vue/components/Calendar.vue +36 -2
- package/vue/components/Header.vue +18 -1
- package/vue/components/Navigation.vue +19 -2
- package/vue/components/NavigationGrid.vue +71 -4
- package/vue/components/NotificationPopup.vue +118 -0
- package/vue/components/NotificationPopups.vue +73 -0
- package/vue/components/Pagination.vue +1 -1
- package/vue/components/QuickSearch.vue +1 -1
- package/vue/components/Sidebar.vue +185 -31
- package/vue/components/Tabs.vue +1 -1
- package/vue/components/TopBar.vue +4 -2
- package/vue/composables/useNotificationPopups.js +78 -0
- package/vue/composables/useTheme.js +60 -0
- package/vue/views/App.vue +27 -20
- package/vue/views/ButtonsExample.vue +93 -0
- package/vue/views/CalendarExample.vue +26 -7
- package/vue/views/NavigationGridExample.vue +89 -0
- package/vue/views/PageStructure.vue +111 -0
- package/vue/views/StatusExample.vue +273 -0
- package/vue/views/TabsExample.vue +1 -1
- package/vue/views/ViewItem.vue +189 -3
- package/vue/views/Welcome.vue +218 -96
- package/vue/views/UserControlPanel.vue +0 -79
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Notifications"
|
|
4
|
+
subtitle="Block-level status messages using Femtocrank .notification with karma background classes"
|
|
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>
|
|
10
|
+
|
|
11
|
+
<p>Click a notification to dismiss it.</p>
|
|
12
|
+
|
|
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>
|
|
29
|
+
|
|
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>
|
|
38
|
+
|
|
39
|
+
<p class="fg-note show inline-notification">
|
|
40
|
+
<strong>NOTE:</strong>
|
|
41
|
+
This is a note.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<p class="fg-success show inline-notification">
|
|
45
|
+
<strong>SUCCESS:</strong>
|
|
46
|
+
This is a success message.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p class="fg-important show inline-notification">
|
|
50
|
+
<strong>IMPORTANT:</strong>
|
|
51
|
+
This is an important message.
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<p class="fg-warning show inline-notification">
|
|
55
|
+
<strong>WARNING:</strong>
|
|
56
|
+
This is a warning message.
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<p class="fg-severe show inline-notification">
|
|
60
|
+
<strong>SEVERE:</strong>
|
|
61
|
+
This is a severe message.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<p class="critical show inline-notification">
|
|
65
|
+
<strong>CRITICAL:</strong>
|
|
66
|
+
This is a critical message using a background karma class.
|
|
67
|
+
</p>
|
|
68
|
+
</Section>
|
|
69
|
+
|
|
70
|
+
<Section
|
|
71
|
+
title="Notification popups"
|
|
72
|
+
subtitle="Transient corner toasts via useNotificationPopups()"
|
|
73
|
+
>
|
|
74
|
+
<template #toolbar>
|
|
75
|
+
<button type="button" class="good" @click="showSuccessPopup">Success</button>
|
|
76
|
+
<button type="button" class="warning" @click="showWarningPopup">Warning</button>
|
|
77
|
+
<button type="button" @click="showLinkedPopup">With link</button>
|
|
78
|
+
<button type="button" @click="showStackedPopups">Stack three</button>
|
|
79
|
+
<button type="button" class="bad" @click="dismissAllPopups">Dismiss all</button>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<p>
|
|
83
|
+
Popups stack in the bottom-right, fade away after 5 seconds, and can be dismissed
|
|
84
|
+
manually or link to another page.
|
|
85
|
+
</p>
|
|
86
|
+
</Section>
|
|
87
|
+
|
|
88
|
+
<Section
|
|
89
|
+
title="In context"
|
|
90
|
+
subtitle="Inline notifications beside or below form fields"
|
|
91
|
+
>
|
|
92
|
+
<form class="status-form" @submit.prevent="submitForm">
|
|
93
|
+
<label for="status-email">Email</label>
|
|
94
|
+
<input
|
|
95
|
+
id="status-email"
|
|
96
|
+
v-model="formEmail"
|
|
97
|
+
type="email"
|
|
98
|
+
placeholder="you@example.com"
|
|
99
|
+
required
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<p
|
|
103
|
+
v-if="formMessage"
|
|
104
|
+
class="show inline-notification"
|
|
105
|
+
:class="formMessageClass"
|
|
106
|
+
role="status"
|
|
107
|
+
>
|
|
108
|
+
<strong>{{ formMessageLabel }}:</strong>
|
|
109
|
+
{{ formMessage }}
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
<fieldset>
|
|
113
|
+
<button type="submit" class="good">Save</button>
|
|
114
|
+
<button type="button" @click="clearForm">Clear</button>
|
|
115
|
+
</fieldset>
|
|
116
|
+
</form>
|
|
117
|
+
</Section>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<script setup>
|
|
121
|
+
import { ref, computed } from 'vue'
|
|
122
|
+
import Section from '../components/Section.vue'
|
|
123
|
+
import { useNotificationPopups } from '../composables/useNotificationPopups.js'
|
|
124
|
+
|
|
125
|
+
const { show: showPopup, dismissAll: dismissAllPopups } = useNotificationPopups()
|
|
126
|
+
|
|
127
|
+
const blockNotifications = ref([
|
|
128
|
+
{
|
|
129
|
+
id: 'info',
|
|
130
|
+
class: 'info',
|
|
131
|
+
label: 'INFO',
|
|
132
|
+
message: 'This is an informational message.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'note',
|
|
136
|
+
class: 'note',
|
|
137
|
+
label: 'NOTE',
|
|
138
|
+
message: 'This is a note.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'success',
|
|
142
|
+
class: 'success',
|
|
143
|
+
label: 'SUCCESS',
|
|
144
|
+
message: 'The operation completed successfully.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'important',
|
|
148
|
+
class: 'important',
|
|
149
|
+
label: 'IMPORTANT',
|
|
150
|
+
message: 'Please review this before continuing.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'warning',
|
|
154
|
+
class: 'warning',
|
|
155
|
+
label: 'WARNING',
|
|
156
|
+
message: 'This action may have side effects.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'severe',
|
|
160
|
+
class: 'severe',
|
|
161
|
+
label: 'SEVERE',
|
|
162
|
+
message: 'A severe problem was detected.',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'critical',
|
|
166
|
+
class: 'critical',
|
|
167
|
+
label: 'CRITICAL',
|
|
168
|
+
message: 'Immediate attention is required.',
|
|
169
|
+
},
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
const initialNotificationIds = blockNotifications.value.map((notification) => notification.id)
|
|
173
|
+
|
|
174
|
+
const visibleNotificationIds = ref([...initialNotificationIds])
|
|
175
|
+
const sampleCounter = ref(0)
|
|
176
|
+
|
|
177
|
+
const visibleNotifications = computed(() =>
|
|
178
|
+
blockNotifications.value.filter((notification) => visibleNotificationIds.value.includes(notification.id))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const formEmail = ref('')
|
|
182
|
+
const formMessage = ref('')
|
|
183
|
+
const formMessageClass = ref('fg-note')
|
|
184
|
+
const formMessageLabel = ref('NOTE')
|
|
185
|
+
|
|
186
|
+
function dismissNotification(id) {
|
|
187
|
+
visibleNotificationIds.value = visibleNotificationIds.value.filter((visibleId) => visibleId !== id)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resetNotifications() {
|
|
191
|
+
blockNotifications.value = blockNotifications.value.filter((notification) =>
|
|
192
|
+
initialNotificationIds.includes(notification.id)
|
|
193
|
+
)
|
|
194
|
+
visibleNotificationIds.value = [...initialNotificationIds]
|
|
195
|
+
sampleCounter.value = 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function showSampleNotification() {
|
|
199
|
+
sampleCounter.value += 1
|
|
200
|
+
const sample = {
|
|
201
|
+
id: `sample-${sampleCounter.value}`,
|
|
202
|
+
class: 'info',
|
|
203
|
+
label: 'INFO',
|
|
204
|
+
message: `Sample notification #${sampleCounter.value}.`,
|
|
205
|
+
}
|
|
206
|
+
blockNotifications.value.push(sample)
|
|
207
|
+
visibleNotificationIds.value = [...visibleNotificationIds.value, sample.id]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function submitForm() {
|
|
211
|
+
if (!formEmail.value.trim()) {
|
|
212
|
+
formMessageClass.value = 'fg-warning'
|
|
213
|
+
formMessageLabel.value = 'WARNING'
|
|
214
|
+
formMessage.value = 'Please enter an email address.'
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
formMessageClass.value = 'fg-success'
|
|
219
|
+
formMessageLabel.value = 'SUCCESS'
|
|
220
|
+
formMessage.value = `Saved preferences for ${formEmail.value.trim()}.`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function clearForm() {
|
|
224
|
+
formEmail.value = ''
|
|
225
|
+
formMessage.value = ''
|
|
226
|
+
formMessageClass.value = 'fg-note'
|
|
227
|
+
formMessageLabel.value = 'NOTE'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function showSuccessPopup() {
|
|
231
|
+
showPopup({
|
|
232
|
+
label: 'SUCCESS',
|
|
233
|
+
class: 'success',
|
|
234
|
+
message: 'Your changes were saved.',
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function showWarningPopup() {
|
|
239
|
+
showPopup({
|
|
240
|
+
label: 'WARNING',
|
|
241
|
+
class: 'warning',
|
|
242
|
+
message: 'Disk space is running low.',
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function showLinkedPopup() {
|
|
247
|
+
showPopup({
|
|
248
|
+
label: 'INFO',
|
|
249
|
+
class: 'info',
|
|
250
|
+
message: 'A new table example is available.',
|
|
251
|
+
linkTo: { name: 'TableExample' },
|
|
252
|
+
linkLabel: 'Open table example',
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function showStackedPopups() {
|
|
257
|
+
const items = [
|
|
258
|
+
{ label: 'NOTE', class: 'note', message: 'First popup in the stack.' },
|
|
259
|
+
{ label: 'INFO', class: 'info', message: 'Second popup in the stack.' },
|
|
260
|
+
{ label: 'SUCCESS', class: 'success', message: 'Third popup in the stack.' },
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
for (const [index, item] of items.entries()) {
|
|
264
|
+
window.setTimeout(() => showPopup(item), index * 250)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
</script>
|
|
268
|
+
|
|
269
|
+
<style scoped>
|
|
270
|
+
.status-form {
|
|
271
|
+
max-width: 32rem;
|
|
272
|
+
}
|
|
273
|
+
</style>
|
package/vue/views/ViewItem.vue
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<section class="with-header-and-content">
|
|
3
|
+
<div class="section-header flex-row">
|
|
4
|
+
<div class="fg1">
|
|
5
|
+
<h2 :title="item.subtitle">{{ item.title }}</h2>
|
|
6
|
+
</div>
|
|
7
|
+
<div role="toolbar">
|
|
8
|
+
<router-link :to="{ name: 'TableExample' }" class="button neutral">Back to table</router-link>
|
|
9
|
+
<button type="button" class="good">Edit</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="section-content padding">
|
|
14
|
+
<p>{{ item.summary }}</p>
|
|
15
|
+
|
|
16
|
+
<dl>
|
|
17
|
+
<dt>ID</dt>
|
|
18
|
+
<dd>{{ id }}</dd>
|
|
19
|
+
|
|
20
|
+
<dt>Name</dt>
|
|
21
|
+
<dd>{{ item.name }}</dd>
|
|
22
|
+
|
|
23
|
+
<dt>Status</dt>
|
|
24
|
+
<dd>
|
|
25
|
+
<span class="tag" :class="item.statusClass">{{ item.status }}</span>
|
|
26
|
+
</dd>
|
|
27
|
+
|
|
28
|
+
<dt>Role</dt>
|
|
29
|
+
<dd>{{ item.role }}</dd>
|
|
30
|
+
|
|
31
|
+
<dt>Location</dt>
|
|
32
|
+
<dd>{{ item.location }}</dd>
|
|
33
|
+
</dl>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="section-subheader">
|
|
37
|
+
<h3>Contact</h3>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="section-content padding">
|
|
41
|
+
<dl>
|
|
42
|
+
<dt>Email</dt>
|
|
43
|
+
<dd><a :href="`mailto:${item.email}`">{{ item.email }}</a></dd>
|
|
44
|
+
|
|
45
|
+
<dt>Phone</dt>
|
|
46
|
+
<dd>{{ item.phone }}</dd>
|
|
47
|
+
|
|
48
|
+
<dt>Timezone</dt>
|
|
49
|
+
<dd>{{ item.timezone }}</dd>
|
|
50
|
+
</dl>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="section-subheader">
|
|
54
|
+
<h3>Details</h3>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="section-content padding">
|
|
58
|
+
<dl>
|
|
59
|
+
<dt>Department</dt>
|
|
60
|
+
<dd>{{ item.department }}</dd>
|
|
61
|
+
|
|
62
|
+
<dt>Manager</dt>
|
|
63
|
+
<dd>{{ item.manager }}</dd>
|
|
64
|
+
|
|
65
|
+
<dt>Start date</dt>
|
|
66
|
+
<dd>{{ item.startDate }}</dd>
|
|
67
|
+
|
|
68
|
+
<dt>Last updated</dt>
|
|
69
|
+
<dd>{{ item.lastUpdated }}</dd>
|
|
70
|
+
</dl>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="section-subheader">
|
|
74
|
+
<h3>Recent activity</h3>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="section-content padding">
|
|
78
|
+
<ul>
|
|
79
|
+
<li v-for="(entry, index) in item.activity" :key="index">
|
|
80
|
+
<span class="subtle">{{ entry.when }}</span>
|
|
81
|
+
— {{ entry.text }}
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
5
86
|
</template>
|
|
87
|
+
|
|
88
|
+
<script setup>
|
|
89
|
+
import { computed } from 'vue'
|
|
90
|
+
|
|
91
|
+
const props = defineProps({
|
|
92
|
+
id: {
|
|
93
|
+
type: [String, Number],
|
|
94
|
+
required: true,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const itemsById = {
|
|
99
|
+
1: {
|
|
100
|
+
title: 'Acme deployment',
|
|
101
|
+
subtitle: 'Production release #1842',
|
|
102
|
+
summary: 'Read-only detail view for a single record, using Femtocrank definition lists and section subheaders.',
|
|
103
|
+
name: 'Acme deployment',
|
|
104
|
+
status: 'Active',
|
|
105
|
+
statusClass: 'fg-good',
|
|
106
|
+
role: 'Release',
|
|
107
|
+
location: 'us-east-1',
|
|
108
|
+
email: 'releases@example.com',
|
|
109
|
+
phone: '+1 555 0100',
|
|
110
|
+
timezone: 'UTC',
|
|
111
|
+
department: 'Platform engineering',
|
|
112
|
+
manager: 'Alex Morgan',
|
|
113
|
+
startDate: '12 March 2026',
|
|
114
|
+
lastUpdated: '9 July 2026, 09:14',
|
|
115
|
+
activity: [
|
|
116
|
+
{ when: '2 hours ago', text: 'Health check passed for all regions.' },
|
|
117
|
+
{ when: 'Yesterday', text: 'Configuration reviewed by on-call engineer.' },
|
|
118
|
+
{ when: '3 days ago', text: 'Deployed from build 1842.' },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
Alice: {
|
|
122
|
+
title: 'Alice',
|
|
123
|
+
subtitle: 'Senior analyst',
|
|
124
|
+
summary: 'Example person record linked from the table example.',
|
|
125
|
+
name: 'Alice',
|
|
126
|
+
status: 'Active',
|
|
127
|
+
statusClass: 'fg-good',
|
|
128
|
+
role: 'Senior analyst',
|
|
129
|
+
location: 'New York',
|
|
130
|
+
email: 'alice@example.com',
|
|
131
|
+
phone: '+1 555 0142',
|
|
132
|
+
timezone: 'America/New_York',
|
|
133
|
+
department: 'Operations',
|
|
134
|
+
manager: 'Jordan Lee',
|
|
135
|
+
startDate: '4 June 2022',
|
|
136
|
+
lastUpdated: '8 July 2026, 16:30',
|
|
137
|
+
activity: [
|
|
138
|
+
{ when: 'Today', text: 'Approved quarterly forecast submission.' },
|
|
139
|
+
{ when: '2 days ago', text: 'Updated territory assignment.' },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
Bob: {
|
|
143
|
+
title: 'Bob',
|
|
144
|
+
subtitle: 'Support specialist',
|
|
145
|
+
summary: 'Example person record linked from the table example.',
|
|
146
|
+
name: 'Bob',
|
|
147
|
+
status: 'Away',
|
|
148
|
+
statusClass: 'fg-warning',
|
|
149
|
+
role: 'Support specialist',
|
|
150
|
+
location: 'Los Angeles',
|
|
151
|
+
email: 'bob@example.com',
|
|
152
|
+
phone: '+1 555 0198',
|
|
153
|
+
timezone: 'America/Los_Angeles',
|
|
154
|
+
department: 'Customer success',
|
|
155
|
+
manager: 'Sam Patel',
|
|
156
|
+
startDate: '19 January 2024',
|
|
157
|
+
lastUpdated: '5 July 2026, 11:02',
|
|
158
|
+
activity: [
|
|
159
|
+
{ when: '4 hours ago', text: 'Set status to away.' },
|
|
160
|
+
{ when: 'Yesterday', text: 'Closed ticket #8821.' },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const item = computed(() => {
|
|
166
|
+
const key = String(props.id)
|
|
167
|
+
if (itemsById[key]) {
|
|
168
|
+
return itemsById[key]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const label = key.charAt(0).toUpperCase() + key.slice(1)
|
|
172
|
+
return {
|
|
173
|
+
title: label,
|
|
174
|
+
subtitle: `Record ${key}`,
|
|
175
|
+
summary: 'Generic detail view for records opened from the table or navigation.',
|
|
176
|
+
name: label,
|
|
177
|
+
status: 'Unknown',
|
|
178
|
+
statusClass: 'fg-note',
|
|
179
|
+
role: '—',
|
|
180
|
+
location: '—',
|
|
181
|
+
email: `${key.toLowerCase()}@example.com`,
|
|
182
|
+
phone: '—',
|
|
183
|
+
timezone: 'UTC',
|
|
184
|
+
department: '—',
|
|
185
|
+
manager: '—',
|
|
186
|
+
startDate: '—',
|
|
187
|
+
lastUpdated: '—',
|
|
188
|
+
activity: [{ when: '—', text: 'No activity recorded for this item.' }],
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
</script>
|