picocrank 1.30.0 → 1.31.1
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 +1 -1
- package/styles/picocrank-extensions.css +31 -6
- package/vue/components/Header.vue +5 -1
- package/vue/components/ReadOnlyTextArea.vue +44 -15
- package/vue/components/Section.vue +0 -3
- package/vue/components/Sidebar.vue +59 -5
- package/vue/components/StatusCard.vue +211 -0
- package/vue/examples/Admin.vue +148 -53
- package/vue/examples/App.vue +0 -5
- package/vue/examples/ReadOnlyTextAreaExample.vue +171 -4
package/package.json
CHANGED
|
@@ -91,7 +91,13 @@
|
|
|
91
91
|
--danger-zone-warning-fg: var(--muted-text-color);
|
|
92
92
|
--danger-zone-focus-outline: var(--karma-bad-border);
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
/* Matches Femtocrank header min-height (logo 2em + .5em padding). */
|
|
95
|
+
--picocrank-fixed-header-offset: 3em;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* No fixed header → no sidebar/main offset. */
|
|
99
|
+
html:not(:has(header.fixed)) {
|
|
100
|
+
--picocrank-fixed-header-offset: 0px;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
table thead th {
|
|
@@ -172,9 +178,23 @@ select.field-invalid:focus-visible {
|
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
/*
|
|
175
|
-
* Fixed header layout: pin the app header and offset
|
|
176
|
-
*
|
|
181
|
+
* Fixed header layout: pin the app header and offset main content.
|
|
182
|
+
* Prefer Header's `fixed` prop (adds Femtocrank `.fixed` on <header>).
|
|
183
|
+
* Sidebar top/height use --picocrank-fixed-header-offset in Sidebar.vue
|
|
184
|
+
* (unlayered) so they are not overridden by stuck-mode defaults.
|
|
185
|
+
* Alternatively wrap with .layout-fixed-header when header and #layout are direct children.
|
|
177
186
|
*/
|
|
187
|
+
header.fixed {
|
|
188
|
+
top: 0;
|
|
189
|
+
left: 0;
|
|
190
|
+
z-index: 5;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
header.fixed ~ aside,
|
|
194
|
+
header.fixed ~ #layout aside {
|
|
195
|
+
z-index: 3;
|
|
196
|
+
}
|
|
197
|
+
|
|
178
198
|
.layout-fixed-header > header {
|
|
179
199
|
position: fixed;
|
|
180
200
|
top: 0;
|
|
@@ -183,15 +203,20 @@ select.field-invalid:focus-visible {
|
|
|
183
203
|
z-index: 5;
|
|
184
204
|
}
|
|
185
205
|
|
|
206
|
+
.layout-fixed-header > aside,
|
|
186
207
|
.layout-fixed-header > #layout aside {
|
|
187
208
|
z-index: 3;
|
|
188
|
-
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
|
-
|
|
212
|
+
/*
|
|
213
|
+
* Unlayered so this wins over @layer components (same issue as Sidebar offsets).
|
|
214
|
+
* Pads #content below a fixed header — clears the chrome and leaves a readable gap.
|
|
215
|
+
*/
|
|
216
|
+
header.fixed ~ #layout > #content,
|
|
217
|
+
.layout-fixed-header > #layout > #content {
|
|
192
218
|
padding-top: var(--picocrank-fixed-header-offset);
|
|
193
219
|
}
|
|
194
|
-
}
|
|
195
220
|
|
|
196
221
|
/*
|
|
197
222
|
* Responsive column priorities: classes col-priority-1 … col-priority-5 on th/td only
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<header>
|
|
2
|
+
<header :class="{ fixed }">
|
|
3
3
|
<div
|
|
4
4
|
:id = "showSidebarChrome ? 'sidebar-button' : null"
|
|
5
5
|
:class="['image-and-title', 'flex-row', { 'disabled-branding': !showBranding, 'logo-home-link': !showSidebarChrome && showBranding }]"
|
|
@@ -146,6 +146,10 @@
|
|
|
146
146
|
type: Boolean,
|
|
147
147
|
default: false,
|
|
148
148
|
},
|
|
149
|
+
fixed: {
|
|
150
|
+
type: Boolean,
|
|
151
|
+
default: true,
|
|
152
|
+
},
|
|
149
153
|
navigation: {
|
|
150
154
|
type: Object,
|
|
151
155
|
default: null,
|
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
<div
|
|
7
7
|
v-if="showActions"
|
|
8
8
|
class="readonly-textarea-actions"
|
|
9
|
-
:class="{ 'readonly-textarea-actions--leading': hasActions }"
|
|
10
9
|
>
|
|
11
10
|
<div v-if="hasActions" class="readonly-textarea-actions-slot">
|
|
12
11
|
<slot name="actions" />
|
|
13
12
|
</div>
|
|
13
|
+
<button
|
|
14
|
+
v-if="showClearButton"
|
|
15
|
+
type="button"
|
|
16
|
+
class="readonly-textarea-clear neutral"
|
|
17
|
+
title="Clear contents"
|
|
18
|
+
:disabled="!modelValue"
|
|
19
|
+
@click="clear"
|
|
20
|
+
>
|
|
21
|
+
<HugeiconsIcon :icon="Delete02Icon" width="1em" height="1em" />
|
|
22
|
+
<span>{{ clearLabel }}</span>
|
|
23
|
+
</button>
|
|
14
24
|
<button
|
|
15
25
|
v-if="showCopyButton"
|
|
16
26
|
type="button"
|
|
@@ -39,7 +49,7 @@
|
|
|
39
49
|
<script setup>
|
|
40
50
|
import { ref, computed, useId, useSlots, watch, Comment } from 'vue'
|
|
41
51
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
42
|
-
import { Copy01Icon, CopyCheckIcon } from '@hugeicons/core-free-icons'
|
|
52
|
+
import { Copy01Icon, CopyCheckIcon, Delete02Icon } from '@hugeicons/core-free-icons'
|
|
43
53
|
|
|
44
54
|
const props = defineProps({
|
|
45
55
|
modelValue: {
|
|
@@ -70,6 +80,10 @@ const props = defineProps({
|
|
|
70
80
|
type: Boolean,
|
|
71
81
|
default: true,
|
|
72
82
|
},
|
|
83
|
+
showClearButton: {
|
|
84
|
+
type: Boolean,
|
|
85
|
+
default: false,
|
|
86
|
+
},
|
|
73
87
|
copyLabel: {
|
|
74
88
|
type: String,
|
|
75
89
|
default: 'Copy',
|
|
@@ -78,6 +92,10 @@ const props = defineProps({
|
|
|
78
92
|
type: String,
|
|
79
93
|
default: 'Copied',
|
|
80
94
|
},
|
|
95
|
+
clearLabel: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: 'Clear',
|
|
98
|
+
},
|
|
81
99
|
copiedFeedbackMs: {
|
|
82
100
|
type: Number,
|
|
83
101
|
default: 2000,
|
|
@@ -92,27 +110,40 @@ const props = defineProps({
|
|
|
92
110
|
},
|
|
93
111
|
})
|
|
94
112
|
|
|
95
|
-
const emit = defineEmits(['copy', 'copy-error', 'update:modelValue'])
|
|
113
|
+
const emit = defineEmits(['copy', 'copy-error', 'clear', 'update:modelValue'])
|
|
96
114
|
|
|
97
115
|
const slots = useSlots()
|
|
98
116
|
|
|
99
|
-
const
|
|
117
|
+
const slotActionCount = computed(() => {
|
|
100
118
|
const render = slots.actions
|
|
101
119
|
if (!render) {
|
|
102
|
-
return
|
|
120
|
+
return 0
|
|
103
121
|
}
|
|
104
|
-
return render().
|
|
122
|
+
return render().filter((vnode) => vnode.type !== Comment).length
|
|
105
123
|
})
|
|
106
124
|
|
|
107
|
-
const
|
|
125
|
+
const hasActions = computed(() => slotActionCount.value > 0)
|
|
126
|
+
|
|
127
|
+
const builtInActionCount = computed(() => {
|
|
128
|
+
let count = 0
|
|
129
|
+
if (props.showClearButton) count++
|
|
130
|
+
if (props.showCopyButton) count++
|
|
131
|
+
return count
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const actionButtonCount = computed(
|
|
135
|
+
() => slotActionCount.value + builtInActionCount.value,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const showActions = computed(
|
|
139
|
+
() => actionButtonCount.value > 0,
|
|
140
|
+
)
|
|
108
141
|
|
|
109
142
|
const showHeader = computed(
|
|
110
143
|
() => Boolean(props.label) || showActions.value,
|
|
111
144
|
)
|
|
112
145
|
|
|
113
|
-
const pushActionsEnd = computed(
|
|
114
|
-
() => showActions.value && !hasActions.value,
|
|
115
|
-
)
|
|
146
|
+
const pushActionsEnd = computed(() => showActions.value)
|
|
116
147
|
|
|
117
148
|
const generatedId = useId()
|
|
118
149
|
const textareaId = props.id || `readonly-textarea-${generatedId}`
|
|
@@ -156,6 +187,7 @@ function appendSection(sectionName) {
|
|
|
156
187
|
function clear() {
|
|
157
188
|
content.value = ''
|
|
158
189
|
emit('update:modelValue', content.value)
|
|
190
|
+
emit('clear')
|
|
159
191
|
}
|
|
160
192
|
|
|
161
193
|
function getContentAsString() {
|
|
@@ -236,10 +268,6 @@ defineExpose({
|
|
|
236
268
|
flex-shrink: 0;
|
|
237
269
|
}
|
|
238
270
|
|
|
239
|
-
.readonly-textarea-actions--leading {
|
|
240
|
-
margin-left: 0;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
271
|
.readonly-textarea-actions-slot {
|
|
244
272
|
display: inline-flex;
|
|
245
273
|
align-items: center;
|
|
@@ -247,7 +275,8 @@ defineExpose({
|
|
|
247
275
|
flex-wrap: wrap;
|
|
248
276
|
}
|
|
249
277
|
|
|
250
|
-
.readonly-textarea-copy
|
|
278
|
+
.readonly-textarea-copy,
|
|
279
|
+
.readonly-textarea-clear {
|
|
251
280
|
display: inline-flex;
|
|
252
281
|
align-items: center;
|
|
253
282
|
gap: 0.35em;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="SIDEBAR_PANEL_ID"
|
|
4
4
|
ref="sidebarRef"
|
|
5
5
|
class="sidebar"
|
|
6
|
-
:class="{ 'shown':
|
|
6
|
+
:class="{ 'shown': isVisible, 'stuck': isStuck }"
|
|
7
7
|
aria-label="Sidebar"
|
|
8
8
|
:aria-hidden="isVisible ? undefined : 'true'"
|
|
9
9
|
:inert="isVisible ? undefined : true"
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
:icon="Pin02Icon"
|
|
26
26
|
width="1em"
|
|
27
27
|
height="1em"
|
|
28
|
-
:strokeWidth="
|
|
28
|
+
:strokeWidth="1.5"
|
|
29
29
|
/>
|
|
30
30
|
<HugeiconsIcon
|
|
31
31
|
v-else
|
|
32
32
|
:icon="PinIcon"
|
|
33
33
|
width="1em"
|
|
34
34
|
height="1em"
|
|
35
|
-
:strokeWidth="
|
|
35
|
+
:strokeWidth="1.5"
|
|
36
36
|
/>
|
|
37
37
|
</span>
|
|
38
38
|
</button>
|
|
@@ -274,8 +274,13 @@ function unstick() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
function toggle() {
|
|
277
|
+
if (isStuck.value) {
|
|
278
|
+
isStuck.value = false
|
|
279
|
+
isOpen.value = false
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
277
283
|
isOpen.value = !isOpen.value
|
|
278
|
-
isStuck.value = false
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
function open() {
|
|
@@ -284,7 +289,6 @@ function open() {
|
|
|
284
289
|
|
|
285
290
|
function close() {
|
|
286
291
|
isOpen.value = false
|
|
287
|
-
isStuck.value = false
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
function isLinkActive(link) {
|
|
@@ -397,6 +401,34 @@ defineExpose({
|
|
|
397
401
|
</script>
|
|
398
402
|
|
|
399
403
|
<style scoped>
|
|
404
|
+
.sidebar {
|
|
405
|
+
display: flex;
|
|
406
|
+
flex-direction: column;
|
|
407
|
+
box-sizing: border-box;
|
|
408
|
+
overflow-x: hidden;
|
|
409
|
+
overflow-y: auto;
|
|
410
|
+
overscroll-behavior: contain;
|
|
411
|
+
/*
|
|
412
|
+
* Sit fully below a fixed Header (Navigation, pin, and scrollbar).
|
|
413
|
+
* Offset is 0 when no header.fixed is present (see picocrank-extensions).
|
|
414
|
+
*/
|
|
415
|
+
top: var(--picocrank-fixed-header-offset);
|
|
416
|
+
height: calc(100vh - var(--picocrank-fixed-header-offset));
|
|
417
|
+
max-height: calc(100vh - var(--picocrank-fixed-header-offset));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/*
|
|
421
|
+
* Stuck mode: same viewport band as overlay mode; scroll the whole contents
|
|
422
|
+
* instead of growing with content (Femtocrank defaults to height: auto).
|
|
423
|
+
*/
|
|
424
|
+
.sidebar.stuck {
|
|
425
|
+
align-self: flex-start;
|
|
426
|
+
position: sticky;
|
|
427
|
+
top: var(--picocrank-fixed-header-offset);
|
|
428
|
+
height: calc(100vh - var(--picocrank-fixed-header-offset));
|
|
429
|
+
max-height: calc(100vh - var(--picocrank-fixed-header-offset));
|
|
430
|
+
}
|
|
431
|
+
|
|
400
432
|
h2 {
|
|
401
433
|
padding: .75em;
|
|
402
434
|
}
|
|
@@ -410,6 +442,28 @@ button {
|
|
|
410
442
|
border: 0;
|
|
411
443
|
}
|
|
412
444
|
|
|
445
|
+
.stick-toggle {
|
|
446
|
+
display: inline-flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
justify-content: center;
|
|
449
|
+
align-self: stretch;
|
|
450
|
+
flex-shrink: 0;
|
|
451
|
+
padding-top: 0;
|
|
452
|
+
padding-bottom: 0;
|
|
453
|
+
padding-left: 1em;
|
|
454
|
+
padding-right: 1em;
|
|
455
|
+
border-radius: 0;
|
|
456
|
+
color: var(--text-color);
|
|
457
|
+
background-color: transparent;
|
|
458
|
+
cursor: pointer;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.stick-toggle:hover,
|
|
462
|
+
.stick-toggle:focus-visible {
|
|
463
|
+
background-color: var(--sidebar-hover-bg-color);
|
|
464
|
+
color: var(--hover-text-color);
|
|
465
|
+
}
|
|
466
|
+
|
|
413
467
|
.navigation-links,
|
|
414
468
|
.nav-section-links {
|
|
415
469
|
list-style: none;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="rootComponent"
|
|
4
|
+
v-bind="rootBindings"
|
|
5
|
+
:class="rootClasses"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
v-if="karma"
|
|
9
|
+
class="status-card-bar"
|
|
10
|
+
:class="{ 'status-card-bar--labeled': Boolean(barLabel) }"
|
|
11
|
+
>
|
|
12
|
+
<span
|
|
13
|
+
v-if="barLabel"
|
|
14
|
+
class="status-card-bar-label"
|
|
15
|
+
>{{ barLabel }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="status-card-body">
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
</component>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
import { computed } from 'vue'
|
|
25
|
+
import { RouterLink } from 'vue-router'
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
karma: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: '',
|
|
31
|
+
validator: (value) => (
|
|
32
|
+
value === ''
|
|
33
|
+
|| ['info', 'note', 'success', 'good', 'warning', 'severe', 'critical', 'error', 'bad', 'old'].includes(value)
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
barLabel: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: '',
|
|
39
|
+
},
|
|
40
|
+
align: {
|
|
41
|
+
type: String,
|
|
42
|
+
default: 'start',
|
|
43
|
+
validator: (value) => ['start', 'center'].includes(value),
|
|
44
|
+
},
|
|
45
|
+
compact: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
to: {
|
|
50
|
+
type: [String, Object],
|
|
51
|
+
default: null,
|
|
52
|
+
},
|
|
53
|
+
href: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: '',
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const isLink = computed(() => Boolean(props.to || props.href))
|
|
60
|
+
|
|
61
|
+
const rootComponent = computed(() => {
|
|
62
|
+
if (props.to) {
|
|
63
|
+
return RouterLink
|
|
64
|
+
}
|
|
65
|
+
if (props.href) {
|
|
66
|
+
return 'a'
|
|
67
|
+
}
|
|
68
|
+
return 'div'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const rootBindings = computed(() => {
|
|
72
|
+
if (props.to) {
|
|
73
|
+
return { to: props.to }
|
|
74
|
+
}
|
|
75
|
+
if (props.href) {
|
|
76
|
+
return { href: props.href }
|
|
77
|
+
}
|
|
78
|
+
return {}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const rootClasses = computed(() => ({
|
|
82
|
+
'status-card': true,
|
|
83
|
+
'status-card--link': isLink.value,
|
|
84
|
+
[`status-card--${props.karma}`]: Boolean(props.karma),
|
|
85
|
+
[`status-card--align-${props.align}`]: true,
|
|
86
|
+
'status-card--compact': props.compact,
|
|
87
|
+
}))
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style scoped>
|
|
91
|
+
.status-card {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: stretch;
|
|
94
|
+
position: relative;
|
|
95
|
+
padding: 0;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
background: transparent;
|
|
98
|
+
border: 1px solid var(--nav-grid-border);
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
color: inherit;
|
|
101
|
+
text-align: left;
|
|
102
|
+
transition: all 0.2s ease;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
a.status-card {
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.status-card:hover {
|
|
111
|
+
border-color: var(--nav-grid-accent-fg);
|
|
112
|
+
background: var(--nav-grid-hover-bg);
|
|
113
|
+
color: var(--text-color);
|
|
114
|
+
transform: translateY(-2px);
|
|
115
|
+
box-shadow: 0 4px 8px var(--nav-grid-hover-shadow);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.status-card-bar {
|
|
119
|
+
position: relative;
|
|
120
|
+
flex: 0 0 0.35em;
|
|
121
|
+
width: 0.35em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.status-card-bar--labeled {
|
|
125
|
+
flex-basis: 1.35em;
|
|
126
|
+
width: 1.35em;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.status-card-bar-label {
|
|
130
|
+
position: absolute;
|
|
131
|
+
top: 50%;
|
|
132
|
+
left: 50%;
|
|
133
|
+
transform: translate(-50%, -50%) rotate(270deg);
|
|
134
|
+
font-size: 0.65em;
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
letter-spacing: 0.06em;
|
|
137
|
+
line-height: 1;
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
color: var(--text-color);
|
|
141
|
+
user-select: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.status-card-body {
|
|
145
|
+
display: flex;
|
|
146
|
+
flex: 1;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
justify-content: flex-start;
|
|
149
|
+
gap: 0.25rem;
|
|
150
|
+
min-width: 0;
|
|
151
|
+
padding: 1.25rem 0.75rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.status-card--compact .status-card-body {
|
|
155
|
+
padding: 0.75rem 0.5rem;
|
|
156
|
+
gap: 0.15rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.status-card--align-start .status-card-body {
|
|
160
|
+
align-items: flex-start;
|
|
161
|
+
text-align: left;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.status-card--align-center .status-card-body {
|
|
165
|
+
align-items: center;
|
|
166
|
+
text-align: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.status-card-body :deep(h4) {
|
|
170
|
+
margin: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.status-card-body :deep(.subtle) {
|
|
174
|
+
margin-top: 0.35rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.status-card--compact .status-card-body :deep(.subtle) {
|
|
178
|
+
margin-top: 0.2rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.status-card--good .status-card-bar,
|
|
182
|
+
.status-card--success .status-card-bar {
|
|
183
|
+
background-color: var(--karma-good-tint);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.status-card--bad .status-card-bar,
|
|
187
|
+
.status-card--critical .status-card-bar,
|
|
188
|
+
.status-card--error .status-card-bar {
|
|
189
|
+
background-color: var(--karma-bad-tint);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.status-card--warning .status-card-bar {
|
|
193
|
+
background-color: var(--karma-warning-tint);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.status-card--severe .status-card-bar {
|
|
197
|
+
background-color: var(--karma-severe-tint);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.status-card--note .status-card-bar {
|
|
201
|
+
background-color: var(--karma-note-tint);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.status-card--info .status-card-bar {
|
|
205
|
+
background-color: var(--karma-info-tint);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.status-card--old .status-card-bar {
|
|
209
|
+
background-color: var(--karma-old-tint);
|
|
210
|
+
}
|
|
211
|
+
</style>
|
package/vue/examples/Admin.vue
CHANGED
|
@@ -8,67 +8,99 @@
|
|
|
8
8
|
<div class="admin-section">
|
|
9
9
|
<h3>System Overview</h3>
|
|
10
10
|
<p>Welcome to the administrative panel. This is a placeholder for administrative functionality.</p>
|
|
11
|
+
|
|
12
|
+
<FormLayout @submit.prevent>
|
|
13
|
+
<FormField
|
|
14
|
+
label="Grid"
|
|
15
|
+
component-has-label
|
|
16
|
+
description="How many StatusCard columns to show."
|
|
17
|
+
>
|
|
18
|
+
<RadioGroup
|
|
19
|
+
v-model="gridLayout"
|
|
20
|
+
name="status-card-grid"
|
|
21
|
+
variant="boolean"
|
|
22
|
+
:options="gridLayoutOptions"
|
|
23
|
+
/>
|
|
24
|
+
</FormField>
|
|
25
|
+
|
|
26
|
+
<FormField label="Content align" component-has-label>
|
|
27
|
+
<RadioGroup
|
|
28
|
+
v-model="cardAlign"
|
|
29
|
+
name="status-card-align"
|
|
30
|
+
variant="boolean"
|
|
31
|
+
:options="alignOptions"
|
|
32
|
+
/>
|
|
33
|
+
</FormField>
|
|
34
|
+
|
|
35
|
+
<FormField label="Bar labels" component-has-label>
|
|
36
|
+
<RadioGroup
|
|
37
|
+
v-model="showBarLabels"
|
|
38
|
+
name="status-card-bar-labels"
|
|
39
|
+
variant="boolean"
|
|
40
|
+
:options="showHideOptions"
|
|
41
|
+
/>
|
|
42
|
+
</FormField>
|
|
43
|
+
|
|
44
|
+
<FormField label="Compact" component-has-label>
|
|
45
|
+
<RadioGroup
|
|
46
|
+
v-model="cardCompact"
|
|
47
|
+
name="status-card-compact"
|
|
48
|
+
variant="boolean"
|
|
49
|
+
:options="onOffOptions"
|
|
50
|
+
/>
|
|
51
|
+
</FormField>
|
|
52
|
+
</FormLayout>
|
|
11
53
|
|
|
12
|
-
<div class="
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<h4>System Load</h4>
|
|
27
|
-
<span class="stat">67%</span>
|
|
28
|
-
<span class="subtle">Moderate</span>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="stat-display good">
|
|
32
|
-
<h4>Storage Used</h4>
|
|
33
|
-
<span class="stat">2.4 GB</span>
|
|
34
|
-
<span class="subtle">45% of total</span>
|
|
35
|
-
</div>
|
|
54
|
+
<div :class="gridClass">
|
|
55
|
+
<StatusCard
|
|
56
|
+
v-for="card in overviewCards"
|
|
57
|
+
:key="card.title"
|
|
58
|
+
:karma="card.karma"
|
|
59
|
+
:bar-label="showBarLabels ? card.barLabel : ''"
|
|
60
|
+
:align="cardAlign"
|
|
61
|
+
:compact="cardCompact"
|
|
62
|
+
:to="card.to"
|
|
63
|
+
>
|
|
64
|
+
<h4>{{ card.title }}</h4>
|
|
65
|
+
<span class="stat">{{ card.stat }}</span>
|
|
66
|
+
<span class="subtle">{{ card.subtle }}</span>
|
|
67
|
+
</StatusCard>
|
|
36
68
|
</div>
|
|
37
69
|
</div>
|
|
38
70
|
|
|
39
71
|
<div class="admin-section">
|
|
40
72
|
<h3>Recent Activity</h3>
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<span class="
|
|
44
|
-
|
|
45
|
-
</
|
|
46
|
-
<
|
|
47
|
-
<span class="
|
|
48
|
-
|
|
49
|
-
</
|
|
50
|
-
<
|
|
51
|
-
<span class="
|
|
52
|
-
|
|
53
|
-
</
|
|
54
|
-
<
|
|
55
|
-
<span class="
|
|
56
|
-
|
|
57
|
-
</
|
|
58
|
-
<
|
|
59
|
-
<span class="
|
|
60
|
-
|
|
61
|
-
</
|
|
62
|
-
</
|
|
73
|
+
<ul>
|
|
74
|
+
<li>
|
|
75
|
+
<span class="subtle">2 minutes ago</span>
|
|
76
|
+
— User "john.doe" logged in
|
|
77
|
+
</li>
|
|
78
|
+
<li>
|
|
79
|
+
<span class="subtle">5 minutes ago</span>
|
|
80
|
+
— New user account created
|
|
81
|
+
</li>
|
|
82
|
+
<li>
|
|
83
|
+
<span class="subtle">12 minutes ago</span>
|
|
84
|
+
— System backup completed
|
|
85
|
+
</li>
|
|
86
|
+
<li>
|
|
87
|
+
<span class="subtle">1 hour ago</span>
|
|
88
|
+
— Database maintenance performed
|
|
89
|
+
</li>
|
|
90
|
+
<li>
|
|
91
|
+
<span class="subtle">2 hours ago</span>
|
|
92
|
+
— User "admin" updated settings
|
|
93
|
+
</li>
|
|
94
|
+
</ul>
|
|
63
95
|
</div>
|
|
64
96
|
|
|
65
97
|
<div class="admin-section">
|
|
66
98
|
<h3>Quick Actions</h3>
|
|
67
|
-
<div role
|
|
68
|
-
<button
|
|
69
|
-
<button
|
|
70
|
-
<button
|
|
71
|
-
<button
|
|
99
|
+
<div role="toolbar">
|
|
100
|
+
<button type="button">Create User</button>
|
|
101
|
+
<button type="button">System Backup</button>
|
|
102
|
+
<button type="button">View Logs</button>
|
|
103
|
+
<button type="button">Settings</button>
|
|
72
104
|
</div>
|
|
73
105
|
</div>
|
|
74
106
|
</div>
|
|
@@ -76,6 +108,69 @@
|
|
|
76
108
|
</template>
|
|
77
109
|
|
|
78
110
|
<script setup>
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
import { computed, ref } from 'vue'
|
|
112
|
+
|
|
113
|
+
const showBarLabels = ref(true)
|
|
114
|
+
const cardCompact = ref(false)
|
|
115
|
+
const cardAlign = ref('start')
|
|
116
|
+
const gridLayout = ref('3')
|
|
117
|
+
|
|
118
|
+
const showHideOptions = [
|
|
119
|
+
{ label: 'Shown', value: true },
|
|
120
|
+
{ label: 'Hidden', value: false },
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
const onOffOptions = [
|
|
124
|
+
{ label: 'On', value: true },
|
|
125
|
+
{ label: 'Off', value: false },
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
const alignOptions = [
|
|
129
|
+
{ value: 'start', label: 'Start' },
|
|
130
|
+
{ value: 'center', label: 'Center' },
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
const gridLayoutOptions = [
|
|
134
|
+
{ value: '3', label: '3 columns' },
|
|
135
|
+
{ value: '2', label: '2 columns' },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
const gridClass = computed(() => (
|
|
139
|
+
gridLayout.value === '2' ? 'grid-boxed-2' : 'grid-boxed'
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
const overviewCards = [
|
|
143
|
+
{
|
|
144
|
+
karma: 'good',
|
|
145
|
+
barLabel: 'Users',
|
|
146
|
+
title: 'Total Users',
|
|
147
|
+
stat: '1,247',
|
|
148
|
+
subtle: '+12% from last month',
|
|
149
|
+
to: { name: 'UserDetails' },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
karma: 'bad',
|
|
153
|
+
barLabel: 'Live',
|
|
154
|
+
title: 'Active Sessions',
|
|
155
|
+
stat: '89',
|
|
156
|
+
subtle: '-3% from last hour',
|
|
157
|
+
to: { name: 'TableExample' },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
karma: 'warning',
|
|
161
|
+
barLabel: 'Load',
|
|
162
|
+
title: 'System Load',
|
|
163
|
+
stat: '67%',
|
|
164
|
+
subtle: 'Moderate',
|
|
165
|
+
to: { name: 'StatusExample' },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
karma: 'good',
|
|
169
|
+
barLabel: 'Disk',
|
|
170
|
+
title: 'Storage Used',
|
|
171
|
+
stat: '2.4 GB',
|
|
172
|
+
subtle: '45% of total',
|
|
173
|
+
to: { name: 'ViewItem', params: { id: '1' } },
|
|
174
|
+
},
|
|
175
|
+
]
|
|
81
176
|
</script>
|
package/vue/examples/App.vue
CHANGED
|
@@ -222,11 +222,6 @@
|
|
|
222
222
|
registerSidebarNavigation(navigation.value, { onHelloWorld: helloWorld })
|
|
223
223
|
registerTopBarNavigation(topBarNavigation.value)
|
|
224
224
|
|
|
225
|
-
if (sidebar.value) {
|
|
226
|
-
sidebar.value.open();
|
|
227
|
-
sidebar.value.stick();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
225
|
registerQuickSearchItems()
|
|
231
226
|
});
|
|
232
227
|
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
v-model="diagnosticLog"
|
|
18
18
|
label="Application log"
|
|
19
19
|
:rows="10"
|
|
20
|
+
show-clear-button
|
|
20
21
|
@copy="onCopy"
|
|
22
|
+
@clear="onClear"
|
|
21
23
|
>
|
|
22
24
|
<template #actions>
|
|
23
25
|
<button type="button" @click="refreshDiagnostics">Refresh log</button>
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
v-model="environmentInfo"
|
|
29
31
|
label="Environment (JSON)"
|
|
30
32
|
:rows="12"
|
|
33
|
+
show-clear-button
|
|
31
34
|
markdown-ticks
|
|
32
35
|
markdown-lang="json"
|
|
33
36
|
/>
|
|
@@ -37,14 +40,99 @@
|
|
|
37
40
|
v-model="yamlConfig"
|
|
38
41
|
label="Configuration (YAML)"
|
|
39
42
|
:rows="12"
|
|
43
|
+
show-clear-button
|
|
40
44
|
markdown-ticks
|
|
41
45
|
markdown-lang="yaml"
|
|
42
46
|
/>
|
|
43
47
|
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
<div class="readonly-playground">
|
|
49
|
+
<p class="readonly-playground-intro">
|
|
50
|
+
Toggle options on the playground instance below to explore every
|
|
51
|
+
<code>ReadOnlyTextArea</code> prop and the actions slot.
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<div
|
|
55
|
+
class="readonly-playground-controls"
|
|
56
|
+
role="group"
|
|
57
|
+
aria-label="ReadOnlyTextArea playground options"
|
|
58
|
+
>
|
|
59
|
+
<label class="readonly-example-toggle">
|
|
60
|
+
<input v-model="playground.labelEnabled" type="checkbox" />
|
|
61
|
+
Label
|
|
62
|
+
</label>
|
|
63
|
+
<label class="readonly-example-toggle">
|
|
64
|
+
<input v-model="playground.showCopyButton" type="checkbox" />
|
|
65
|
+
Copy button
|
|
66
|
+
</label>
|
|
67
|
+
<label class="readonly-example-toggle">
|
|
68
|
+
<input v-model="playground.showClearButton" type="checkbox" />
|
|
69
|
+
Clear button
|
|
70
|
+
</label>
|
|
71
|
+
<label class="readonly-example-toggle">
|
|
72
|
+
<input v-model="playground.showActions" type="checkbox" />
|
|
73
|
+
Actions slot
|
|
74
|
+
</label>
|
|
75
|
+
<label class="readonly-example-toggle">
|
|
76
|
+
<input v-model="playground.monospace" type="checkbox" />
|
|
77
|
+
Monospace
|
|
78
|
+
</label>
|
|
79
|
+
<label class="readonly-example-toggle">
|
|
80
|
+
<input v-model="playground.markdownTicks" type="checkbox" />
|
|
81
|
+
Markdown ticks
|
|
82
|
+
</label>
|
|
83
|
+
<label class="readonly-example-toggle">
|
|
84
|
+
<input
|
|
85
|
+
v-model="playground.markdownLangJson"
|
|
86
|
+
type="checkbox"
|
|
87
|
+
:disabled="!playground.markdownTicks"
|
|
88
|
+
/>
|
|
89
|
+
Markdown lang: json
|
|
90
|
+
</label>
|
|
91
|
+
<label class="readonly-example-toggle">
|
|
92
|
+
<input v-model="playground.usePlaceholder" type="checkbox" />
|
|
93
|
+
Placeholder
|
|
94
|
+
</label>
|
|
95
|
+
<label class="readonly-example-toggle">
|
|
96
|
+
<input v-model="playground.customButtonLabels" type="checkbox" />
|
|
97
|
+
Custom button labels
|
|
98
|
+
</label>
|
|
99
|
+
<label class="readonly-example-toggle readonly-playground-rows">
|
|
100
|
+
<span>Rows</span>
|
|
101
|
+
<input
|
|
102
|
+
v-model.number="playground.rows"
|
|
103
|
+
type="number"
|
|
104
|
+
min="2"
|
|
105
|
+
max="24"
|
|
106
|
+
/>
|
|
107
|
+
</label>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<ReadOnlyTextArea
|
|
111
|
+
v-model="playgroundContent"
|
|
112
|
+
:label="playground.labelEnabled ? 'Playground output' : ''"
|
|
113
|
+
:rows="playground.rows"
|
|
114
|
+
:placeholder="playground.usePlaceholder ? 'No diagnostics available.' : ''"
|
|
115
|
+
:monospace="playground.monospace"
|
|
116
|
+
:show-copy-button="playground.showCopyButton"
|
|
117
|
+
:show-clear-button="playground.showClearButton"
|
|
118
|
+
:copy-label="playground.customButtonLabels ? 'Copy all' : undefined"
|
|
119
|
+
:copied-label="playground.customButtonLabels ? 'Copied!' : undefined"
|
|
120
|
+
:clear-label="playground.customButtonLabels ? 'Reset' : undefined"
|
|
121
|
+
:markdown-ticks="playground.markdownTicks"
|
|
122
|
+
:markdown-lang="playground.markdownTicks && playground.markdownLangJson ? 'json' : ''"
|
|
123
|
+
@copy="onPlaygroundCopy"
|
|
124
|
+
@clear="onPlaygroundClear"
|
|
125
|
+
@copy-error="onPlaygroundCopyError"
|
|
126
|
+
>
|
|
127
|
+
<template v-if="playground.showActions" #actions>
|
|
128
|
+
<button type="button" @click="fillPlaygroundContent">Fill sample</button>
|
|
129
|
+
</template>
|
|
130
|
+
</ReadOnlyTextArea>
|
|
131
|
+
|
|
132
|
+
<p v-if="playgroundFeedback" class="copy-feedback subtle">
|
|
133
|
+
{{ playgroundFeedback }}
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
48
136
|
|
|
49
137
|
<p v-if="lastCopied" class="copy-feedback subtle">
|
|
50
138
|
Copied {{ lastCopied.length > 60 ? `${lastCopied.slice(0, 60)}…` : lastCopied }}
|
|
@@ -62,8 +150,40 @@ const diagnosticLog = ref('')
|
|
|
62
150
|
const environmentInfo = ref('')
|
|
63
151
|
const yamlConfig = ref('')
|
|
64
152
|
const lastCopied = ref('')
|
|
153
|
+
const playgroundFeedback = ref('')
|
|
65
154
|
const yamlConfigRef = ref(null)
|
|
66
155
|
|
|
156
|
+
const playground = ref({
|
|
157
|
+
labelEnabled: false,
|
|
158
|
+
showCopyButton: true,
|
|
159
|
+
showClearButton: false,
|
|
160
|
+
showActions: false,
|
|
161
|
+
monospace: true,
|
|
162
|
+
markdownTicks: false,
|
|
163
|
+
markdownLangJson: false,
|
|
164
|
+
usePlaceholder: false,
|
|
165
|
+
customButtonLabels: false,
|
|
166
|
+
rows: 4,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
function buildPlaygroundSample() {
|
|
170
|
+
return [
|
|
171
|
+
'Playground sample output',
|
|
172
|
+
'Toggle the checkboxes above to try label, buttons, markdown fences, and monospace.',
|
|
173
|
+
'',
|
|
174
|
+
'status: ok',
|
|
175
|
+
'items: 3',
|
|
176
|
+
].join('\n')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const playgroundContent = ref(buildPlaygroundSample())
|
|
180
|
+
|
|
181
|
+
function fillPlaygroundContent() {
|
|
182
|
+
playground.value.usePlaceholder = false
|
|
183
|
+
playgroundContent.value = buildPlaygroundSample()
|
|
184
|
+
playgroundFeedback.value = 'Filled playground with sample content.'
|
|
185
|
+
}
|
|
186
|
+
|
|
67
187
|
function buildDiagnosticLog() {
|
|
68
188
|
const now = new Date().toISOString()
|
|
69
189
|
return [
|
|
@@ -122,6 +242,23 @@ function onCopy(value) {
|
|
|
122
242
|
lastCopied.value = value.split('\n')[0]
|
|
123
243
|
}
|
|
124
244
|
|
|
245
|
+
function onClear() {
|
|
246
|
+
lastCopied.value = ''
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onPlaygroundCopy(value) {
|
|
250
|
+
playgroundFeedback.value = `Copied ${value.split('\n').length} line(s).`
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function onPlaygroundClear() {
|
|
254
|
+
playgroundContent.value = ''
|
|
255
|
+
playgroundFeedback.value = 'Playground cleared.'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function onPlaygroundCopyError(error) {
|
|
259
|
+
playgroundFeedback.value = `Copy failed: ${error?.message ?? error}`
|
|
260
|
+
}
|
|
261
|
+
|
|
125
262
|
refreshDiagnostics()
|
|
126
263
|
</script>
|
|
127
264
|
|
|
@@ -140,4 +277,34 @@ refreshDiagnostics()
|
|
|
140
277
|
margin: 0;
|
|
141
278
|
font-size: 0.875rem;
|
|
142
279
|
}
|
|
280
|
+
|
|
281
|
+
.readonly-playground {
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
gap: 0.75rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.readonly-playground-intro {
|
|
288
|
+
margin: 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.readonly-example-toggle {
|
|
292
|
+
display: inline-flex;
|
|
293
|
+
align-items: center;
|
|
294
|
+
gap: 0.35rem;
|
|
295
|
+
font-size: 0.875rem;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
user-select: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.readonly-playground-controls {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-wrap: wrap;
|
|
303
|
+
gap: 0.75rem 1rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.readonly-playground-rows input[type='number'] {
|
|
307
|
+
width: 4rem;
|
|
308
|
+
padding: 0.15rem 0.35rem;
|
|
309
|
+
}
|
|
143
310
|
</style>
|