picocrank 1.32.0 → 1.34.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/docs/filter-query-v1.md +5 -1
- package/docs/table.md +99 -0
- package/package.json +1 -1
- package/router.js +33 -0
- package/styles/picocrank-extensions.css +23 -2
- package/vue/components/Accordion.spec.js +48 -0
- package/vue/components/Accordion.vue +46 -0
- package/vue/components/AccordionItem.vue +103 -0
- package/vue/components/FieldValue.spec.js +88 -0
- package/vue/components/FieldValue.vue +222 -0
- package/vue/components/FormField.spec.js +1 -0
- package/vue/components/FormField.vue +38 -10
- package/vue/components/Header.vue +24 -2
- package/vue/components/NotificationBlock.vue +25 -0
- package/vue/components/ReorderList.spec.js +318 -0
- package/vue/components/ReorderList.vue +614 -0
- package/vue/components/Table.spec.js +490 -1
- package/vue/components/Table.vue +1038 -111
- package/vue/components/TableColumnFilterPopover.vue +63 -19
- package/vue/components/TableColumnOptionsPopover.vue +248 -273
- package/vue/components/TopBar.vue +6 -2
- package/vue/components/accordionContext.js +1 -0
- package/vue/composables/tableColumnPresets.js +33 -0
- package/vue/composables/tableFilters.js +6 -1
- package/vue/composables/tableGroupBy.js +173 -0
- package/vue/composables/tableGroupBy.spec.js +69 -0
- package/vue/composables/tableViewSettings.js +34 -0
- package/vue/composables/tableViewSettings.spec.js +21 -0
- package/vue/composables/useHeaderTopBarFit.js +91 -0
- package/vue/composables/useResponsiveNav.js +5 -3
- package/vue/data/navSectionHubs.js +16 -1
- package/vue/examples/AccordionExample.vue +33 -0
- package/vue/examples/FieldValueExample.vue +143 -0
- package/vue/examples/FormExample.vue +11 -6
- package/vue/examples/ReorderListExample.vue +236 -0
- package/vue/examples/TableExample.vue +5 -1
- package/vue/examples/TableRemoteExample.vue +28 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="field-value"
|
|
4
|
+
:class="{
|
|
5
|
+
'is-inline': hasInlineEdit,
|
|
6
|
+
'is-outline': hasOutlineEdit,
|
|
7
|
+
'is-readonly': readOnly,
|
|
8
|
+
}"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
v-if="editing"
|
|
12
|
+
ref="editRoot"
|
|
13
|
+
class="field-value-edit"
|
|
14
|
+
@click.stop
|
|
15
|
+
>
|
|
16
|
+
<slot
|
|
17
|
+
name="edit"
|
|
18
|
+
:value="draft"
|
|
19
|
+
:update="updateDraft"
|
|
20
|
+
:commit="commit"
|
|
21
|
+
:cancel="cancel"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div
|
|
25
|
+
v-else
|
|
26
|
+
class="field-value-display"
|
|
27
|
+
:class="{ editable: hasInlineEdit }"
|
|
28
|
+
@click="startEdit"
|
|
29
|
+
>
|
|
30
|
+
<span class="field-value-content">
|
|
31
|
+
<slot>{{ displayText }}</slot>
|
|
32
|
+
</span>
|
|
33
|
+
<HugeiconsIcon
|
|
34
|
+
v-if="hasInlineEdit"
|
|
35
|
+
:icon="Edit03Icon"
|
|
36
|
+
class="field-value-icon"
|
|
37
|
+
width="0.9em"
|
|
38
|
+
height="0.9em"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
/>
|
|
41
|
+
<router-link
|
|
42
|
+
v-else-if="to"
|
|
43
|
+
:to="to"
|
|
44
|
+
class="field-value-icon field-value-outline"
|
|
45
|
+
:title="editLabel"
|
|
46
|
+
:aria-label="editLabel"
|
|
47
|
+
@click.stop
|
|
48
|
+
>
|
|
49
|
+
<HugeiconsIcon
|
|
50
|
+
:icon="FileEditIcon"
|
|
51
|
+
width="0.9em"
|
|
52
|
+
height="0.9em"
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
/>
|
|
55
|
+
</router-link>
|
|
56
|
+
<a
|
|
57
|
+
v-else-if="href"
|
|
58
|
+
:href="href"
|
|
59
|
+
class="field-value-icon field-value-outline"
|
|
60
|
+
:title="editLabel"
|
|
61
|
+
:aria-label="editLabel"
|
|
62
|
+
@click.stop
|
|
63
|
+
>
|
|
64
|
+
<HugeiconsIcon
|
|
65
|
+
:icon="FileEditIcon"
|
|
66
|
+
width="0.9em"
|
|
67
|
+
height="0.9em"
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
/>
|
|
70
|
+
</a>
|
|
71
|
+
<span
|
|
72
|
+
v-else
|
|
73
|
+
class="field-value-icon field-value-readonly"
|
|
74
|
+
title="Read only"
|
|
75
|
+
aria-label="Read only"
|
|
76
|
+
>
|
|
77
|
+
<HugeiconsIcon
|
|
78
|
+
:icon="SquareLock01Icon"
|
|
79
|
+
width="0.9em"
|
|
80
|
+
height="0.9em"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
/>
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<script setup>
|
|
89
|
+
import { computed, nextTick, ref, useSlots } from 'vue'
|
|
90
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
91
|
+
import { Edit03Icon, FileEditIcon, SquareLock01Icon } from '@hugeicons/core-free-icons'
|
|
92
|
+
|
|
93
|
+
const props = defineProps({
|
|
94
|
+
modelValue: {
|
|
95
|
+
type: [String, Number, Boolean],
|
|
96
|
+
default: '',
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Route for outline edit. Shown when no edit template is provided.
|
|
100
|
+
*/
|
|
101
|
+
to: {
|
|
102
|
+
type: [String, Object],
|
|
103
|
+
default: null,
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* URL for outline edit when a router location is not used.
|
|
107
|
+
*/
|
|
108
|
+
href: {
|
|
109
|
+
type: String,
|
|
110
|
+
default: '',
|
|
111
|
+
},
|
|
112
|
+
editLabel: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: 'Edit',
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const emit = defineEmits(['update:modelValue', 'commit', 'cancel'])
|
|
119
|
+
|
|
120
|
+
const slots = useSlots()
|
|
121
|
+
const editing = ref(false)
|
|
122
|
+
const draft = ref('')
|
|
123
|
+
const editRoot = ref(null)
|
|
124
|
+
|
|
125
|
+
const hasInlineEdit = computed(() => typeof slots.edit === 'function')
|
|
126
|
+
const hasOutlineEdit = computed(() => !hasInlineEdit.value && Boolean(props.to || props.href))
|
|
127
|
+
const readOnly = computed(() => !hasInlineEdit.value && !hasOutlineEdit.value)
|
|
128
|
+
const displayText = computed(() => (props.modelValue == null ? '' : String(props.modelValue)))
|
|
129
|
+
|
|
130
|
+
function startEdit() {
|
|
131
|
+
if (!hasInlineEdit.value || editing.value) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
draft.value = displayText.value
|
|
135
|
+
editing.value = true
|
|
136
|
+
nextTick(() => {
|
|
137
|
+
const control = editRoot.value?.querySelector('input, select, textarea')
|
|
138
|
+
if (!control) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
control.focus()
|
|
142
|
+
if (control instanceof HTMLInputElement && control.type !== 'checkbox') {
|
|
143
|
+
control.select()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function updateDraft(value) {
|
|
149
|
+
draft.value = value == null ? '' : String(value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function commit() {
|
|
153
|
+
if (!editing.value) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
emit('update:modelValue', draft.value)
|
|
157
|
+
emit('commit', draft.value)
|
|
158
|
+
editing.value = false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cancel() {
|
|
162
|
+
if (!editing.value) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
editing.value = false
|
|
166
|
+
draft.value = displayText.value
|
|
167
|
+
emit('cancel')
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<style scoped>
|
|
172
|
+
.field-value-display,
|
|
173
|
+
.field-value-edit {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 0.35rem;
|
|
177
|
+
min-height: 1.5em;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.field-value-display.editable {
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
padding: 0.25rem;
|
|
183
|
+
border-radius: 3px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.field-value-display.editable:hover {
|
|
187
|
+
background-color: var(--hover-background-color, #f8f9fa);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.field-value-content {
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.field-value-icon {
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
color: var(--muted-text-color, #8a8a8a);
|
|
197
|
+
opacity: 0.55;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.field-value-outline,
|
|
201
|
+
.field-value-readonly {
|
|
202
|
+
display: inline-flex;
|
|
203
|
+
line-height: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.field-value-outline {
|
|
207
|
+
text-decoration: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.field-value-display.editable:hover .field-value-icon,
|
|
211
|
+
.field-value-outline:hover {
|
|
212
|
+
opacity: 0.9;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.field-value-edit :deep(input),
|
|
216
|
+
.field-value-edit :deep(select),
|
|
217
|
+
.field-value-edit :deep(textarea) {
|
|
218
|
+
width: 100%;
|
|
219
|
+
max-width: 28rem;
|
|
220
|
+
box-sizing: border-box;
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -124,6 +124,7 @@ describe('FormField', () => {
|
|
|
124
124
|
|
|
125
125
|
const errorBlock = wrapper.find('.form-field-error')
|
|
126
126
|
expect(errorBlock.exists()).toBe(true)
|
|
127
|
+
expect(errorBlock.classes()).toContain('attached-below')
|
|
127
128
|
expect(errorBlock.attributes('role')).toBe('alert')
|
|
128
129
|
expect(errorBlock.text()).toContain('This field is required.')
|
|
129
130
|
})
|
|
@@ -20,16 +20,19 @@
|
|
|
20
20
|
rel="noopener noreferrer"
|
|
21
21
|
>{{ docsLinkTitle }}</a>
|
|
22
22
|
</div>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
<div class="form-field-value-stack">
|
|
24
|
+
<slot />
|
|
25
|
+
<NotificationBlock
|
|
26
|
+
v-if="fieldError"
|
|
27
|
+
:id="errorId"
|
|
28
|
+
class="form-field-error"
|
|
29
|
+
type="bad"
|
|
30
|
+
label="Error"
|
|
31
|
+
:message="fieldError"
|
|
32
|
+
attached-below
|
|
33
|
+
role="alert"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
33
36
|
<div v-if="!descriptionAbove && hasFieldMeta" class="form-field-meta">
|
|
34
37
|
<p v-if="description" class="subtle form-field-description">{{ description }}</p>
|
|
35
38
|
<a
|
|
@@ -303,10 +306,35 @@ label.disabled,
|
|
|
303
306
|
align-self: flex-start;
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
.form-field-value-stack {
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
gap: 0;
|
|
313
|
+
width: 100%;
|
|
314
|
+
min-width: 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
306
317
|
.form-field-control :deep(.form-field-error) {
|
|
307
318
|
font-size: 0.9em;
|
|
308
319
|
}
|
|
309
320
|
|
|
321
|
+
.form-field-value-stack:has(> :is(input, textarea, select).field-invalid + :deep(.form-field-error.attached-below)) {
|
|
322
|
+
box-shadow: var(--field-invalid-outline);
|
|
323
|
+
border-radius: 0.4em;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.form-field-value-stack:has(> :is(input, textarea, select).field-invalid + :deep(.form-field-error.attached-below)) > :is(input, textarea, select).field-invalid {
|
|
328
|
+
border-bottom-left-radius: 0;
|
|
329
|
+
border-bottom-right-radius: 0;
|
|
330
|
+
box-shadow: none;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.form-field-value-stack:has(> :is(input, textarea, select).field-invalid + :deep(.form-field-error.attached-below)) :deep(.form-field-error.attached-below) {
|
|
334
|
+
margin: 0;
|
|
335
|
+
box-shadow: none;
|
|
336
|
+
}
|
|
337
|
+
|
|
310
338
|
.form-field-required {
|
|
311
339
|
color: var(--form-field-required-fg);
|
|
312
340
|
margin-left: 0.15em;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<header :class="{ fixed }">
|
|
2
|
+
<header ref="headerRoot" :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 }]"
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
</template>
|
|
72
72
|
|
|
73
73
|
<script setup>
|
|
74
|
-
import { computed, ref } from "vue";
|
|
74
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
75
75
|
import { RouterLink } from "vue-router";
|
|
76
76
|
import { HugeiconsIcon } from "@hugeicons/vue";
|
|
77
77
|
import { BulbIcon, Menu01Icon, Moon02Icon, Sun01Icon, UserIcon } from "@hugeicons/core-free-icons";
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
import TopBar from "./TopBar.vue";
|
|
81
81
|
import Sidebar from "./Sidebar.vue";
|
|
82
82
|
import { useTheme } from '../composables/useTheme.js';
|
|
83
|
+
import { useHeaderTopBarFit } from '../composables/useHeaderTopBarFit.js';
|
|
83
84
|
import { useResponsiveNav } from '../composables/useResponsiveNav.js';
|
|
84
85
|
|
|
85
86
|
const { theme, nextTheme, toggleTheme } = useTheme();
|
|
@@ -161,10 +162,31 @@
|
|
|
161
162
|
});
|
|
162
163
|
|
|
163
164
|
const fallbackSidebar = ref(null);
|
|
165
|
+
const headerRoot = ref(null);
|
|
166
|
+
const topBarSpaceLimited = ref(false);
|
|
164
167
|
|
|
165
168
|
const { showTopBar, showSidebarChrome, needsFallbackSidebar, isMobile } = useResponsiveNav(
|
|
166
169
|
() => props.sidebarEnabled,
|
|
167
170
|
() => props.topBarEnabled,
|
|
171
|
+
topBarSpaceLimited,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const topBarMeasureActive = computed(
|
|
175
|
+
() => props.topBarEnabled && !isMobile.value,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const { remeasure: remeasureTopBar } = useHeaderTopBarFit(
|
|
179
|
+
headerRoot,
|
|
180
|
+
topBarMeasureActive,
|
|
181
|
+
topBarSpaceLimited,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
watch(showTopBar, () => nextTick(() => remeasureTopBar()));
|
|
185
|
+
|
|
186
|
+
watch(
|
|
187
|
+
() => props.topBarNavigation,
|
|
188
|
+
() => remeasureTopBar(),
|
|
189
|
+
{ deep: true },
|
|
168
190
|
);
|
|
169
191
|
|
|
170
192
|
const showBreadcrumbsBar = computed(
|
|
@@ -89,6 +89,14 @@ const props = defineProps({
|
|
|
89
89
|
type: String,
|
|
90
90
|
default: 'status',
|
|
91
91
|
},
|
|
92
|
+
/**
|
|
93
|
+
* Nest the block directly under a control (flat top edge, shared width).
|
|
94
|
+
* Pair with an input, select, or textarea immediately above.
|
|
95
|
+
*/
|
|
96
|
+
attachedBelow: {
|
|
97
|
+
type: Boolean,
|
|
98
|
+
default: false,
|
|
99
|
+
},
|
|
92
100
|
})
|
|
93
101
|
|
|
94
102
|
const emit = defineEmits(['click', 'dismiss'])
|
|
@@ -108,6 +116,7 @@ const hasLink = computed(() => Boolean(
|
|
|
108
116
|
const rootClasses = computed(() => ({
|
|
109
117
|
notification: true,
|
|
110
118
|
[props.type]: true,
|
|
119
|
+
'attached-below': props.attachedBelow,
|
|
111
120
|
clickable: props.clickable || props.dismissible,
|
|
112
121
|
}))
|
|
113
122
|
|
|
@@ -118,3 +127,19 @@ function handleClick(event) {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
</script>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.notification.attached-below {
|
|
133
|
+
box-sizing: border-box;
|
|
134
|
+
width: 100%;
|
|
135
|
+
margin: 0;
|
|
136
|
+
border-top: none;
|
|
137
|
+
border-radius: 0 0 0.4em 0.4em;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.notification.attached-below.bad,
|
|
141
|
+
.notification.attached-below.error,
|
|
142
|
+
.notification.attached-below.critical {
|
|
143
|
+
border-color: var(--karma-bad-border);
|
|
144
|
+
}
|
|
145
|
+
</style>
|