picocrank 1.31.0 → 1.32.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/package.json +1 -1
- package/router.js +11 -0
- package/styles/picocrank-extensions.css +96 -6
- package/vue/components/Calendar.vue +41 -63
- package/vue/components/Header.vue +5 -1
- package/vue/components/LoadingArea.spec.js +72 -0
- package/vue/components/LoadingArea.vue +176 -0
- package/vue/components/Sidebar.vue +28 -0
- package/vue/components/StatusCard.vue +211 -0
- package/vue/components/Table.spec.js +253 -0
- package/vue/components/Table.vue +379 -2
- package/vue/components/TableColumnFilterPopover.vue +56 -8
- package/vue/components/TableRowContextMenu.vue +267 -0
- package/vue/data/navSectionHubs.js +5 -0
- package/vue/examples/Admin.vue +148 -53
- package/vue/examples/LoadingAreaExample.vue +175 -0
- package/vue/examples/TableExample.vue +156 -4
package/package.json
CHANGED
package/router.js
CHANGED
|
@@ -15,6 +15,7 @@ import { StatusIcon } from '@hugeicons/core-free-icons';
|
|
|
15
15
|
import { PaintBoardIcon } from '@hugeicons/core-free-icons';
|
|
16
16
|
import { UserIcon } from '@hugeicons/core-free-icons';
|
|
17
17
|
import { InformationCircleIcon } from '@hugeicons/core-free-icons';
|
|
18
|
+
import { Loading03Icon } from '@hugeicons/core-free-icons';
|
|
18
19
|
|
|
19
20
|
import {
|
|
20
21
|
hubChildBreadcrumbs,
|
|
@@ -176,6 +177,16 @@ const routes = [
|
|
|
176
177
|
breadcrumbs: hubChildBreadcrumbs('NavFeedbackStatusHub', 'DialogExample', 'Dialog'),
|
|
177
178
|
}
|
|
178
179
|
},
|
|
180
|
+
{
|
|
181
|
+
name: 'LoadingAreaExample',
|
|
182
|
+
path: '/loading-area-example',
|
|
183
|
+
component: () => import('./vue/examples/LoadingAreaExample.vue'),
|
|
184
|
+
meta: {
|
|
185
|
+
title: 'Loading area',
|
|
186
|
+
icon: Loading03Icon,
|
|
187
|
+
breadcrumbs: hubChildBreadcrumbs('NavFeedbackStatusHub', 'LoadingAreaExample', 'Loading area'),
|
|
188
|
+
}
|
|
189
|
+
},
|
|
179
190
|
{
|
|
180
191
|
name: 'TabsExample',
|
|
181
192
|
path: '/tabs-example',
|
|
@@ -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
|
|
@@ -227,3 +252,68 @@ select.field-invalid:focus-visible {
|
|
|
227
252
|
@container responsive-cols (max-width: 22rem) {
|
|
228
253
|
:is(th, td).col-priority-5 { display: none; }
|
|
229
254
|
}
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* Floating context menu: reuse Femtocrank nav.menu dropdown chrome, forced open
|
|
258
|
+
* for cursor-positioned / teleported menus (table rows, calendar days, etc.).
|
|
259
|
+
*/
|
|
260
|
+
nav.menu.context-menu {
|
|
261
|
+
position: fixed;
|
|
262
|
+
z-index: 1000;
|
|
263
|
+
margin: 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
nav.menu.context-menu:focus,
|
|
267
|
+
nav.menu.context-menu:focus-visible {
|
|
268
|
+
outline: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
nav.menu.context-menu > ul {
|
|
272
|
+
margin: 0;
|
|
273
|
+
padding: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
nav.menu.context-menu > ul > li {
|
|
277
|
+
display: block;
|
|
278
|
+
margin: 0;
|
|
279
|
+
padding: 0;
|
|
280
|
+
border-radius: 0;
|
|
281
|
+
background: transparent;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
nav.menu.context-menu > ul > li:hover,
|
|
285
|
+
nav.menu.context-menu > ul > li:focus-within {
|
|
286
|
+
background: transparent;
|
|
287
|
+
color: inherit;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
nav.menu.context-menu > ul > li > div {
|
|
291
|
+
display: block;
|
|
292
|
+
position: static;
|
|
293
|
+
top: auto;
|
|
294
|
+
left: auto;
|
|
295
|
+
min-width: 12em;
|
|
296
|
+
padding: var(--menu-dropdown-padding);
|
|
297
|
+
border-radius: var(--menu-dropdown-radius);
|
|
298
|
+
background-color: var(--menu-dropdown-background-color);
|
|
299
|
+
box-shadow: var(--menu-dropdown-shadow);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
nav.menu.context-menu > ul > li > div::before {
|
|
303
|
+
content: none;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
nav.menu.context-menu > ul div ul button.inline-icon {
|
|
307
|
+
display: inline-flex;
|
|
308
|
+
align-items: center;
|
|
309
|
+
gap: 0.45em;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
nav.menu.context-menu > ul div ul li[role="separator"] {
|
|
313
|
+
height: 1px;
|
|
314
|
+
margin: 0.25em 0.35em;
|
|
315
|
+
padding: 0;
|
|
316
|
+
background: var(--menu-dropdown-border-color, var(--border-color));
|
|
317
|
+
border-radius: 0;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
}
|
|
@@ -641,7 +641,7 @@ function onDayContextMenu(date, mouseEvent) {
|
|
|
641
641
|
|
|
642
642
|
nextTick(() => {
|
|
643
643
|
positionDayContextMenu(mouseEvent.clientX, mouseEvent.clientY)
|
|
644
|
-
dayContextMenuRef.value?.
|
|
644
|
+
dayContextMenuRef.value?.focus({ preventScroll: true })
|
|
645
645
|
})
|
|
646
646
|
}
|
|
647
647
|
|
|
@@ -1067,43 +1067,53 @@ watch([viewMonth, viewYear], () => {
|
|
|
1067
1067
|
</div>
|
|
1068
1068
|
|
|
1069
1069
|
<Teleport to="body">
|
|
1070
|
-
<
|
|
1070
|
+
<nav
|
|
1071
1071
|
v-if="dayContextMenuOpen"
|
|
1072
1072
|
ref="dayContextMenuRef"
|
|
1073
|
-
class="calendar-day-context-menu"
|
|
1073
|
+
class="menu context-menu calendar-day-context-menu"
|
|
1074
1074
|
role="menu"
|
|
1075
|
+
tabindex="-1"
|
|
1075
1076
|
:style="dayContextMenuStyle"
|
|
1077
|
+
aria-label="Day view options"
|
|
1076
1078
|
@click.stop
|
|
1077
1079
|
@contextmenu.prevent
|
|
1078
1080
|
>
|
|
1079
|
-
<
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1081
|
+
<ul class="noListStyle">
|
|
1082
|
+
<li>
|
|
1083
|
+
<div>
|
|
1084
|
+
<ul>
|
|
1085
|
+
<li v-if="internalViewMode !== 'day'">
|
|
1086
|
+
<button
|
|
1087
|
+
type="button"
|
|
1088
|
+
role="menuitem"
|
|
1089
|
+
@click="showOnlyThisDay"
|
|
1090
|
+
>
|
|
1091
|
+
Show only this day
|
|
1092
|
+
</button>
|
|
1093
|
+
</li>
|
|
1094
|
+
<li v-if="internalViewMode !== 'week'">
|
|
1095
|
+
<button
|
|
1096
|
+
type="button"
|
|
1097
|
+
role="menuitem"
|
|
1098
|
+
@click="showOnlyThisWeek"
|
|
1099
|
+
>
|
|
1100
|
+
Show only this week
|
|
1101
|
+
</button>
|
|
1102
|
+
</li>
|
|
1103
|
+
<li v-if="internalViewMode !== 'month'">
|
|
1104
|
+
<button
|
|
1105
|
+
type="button"
|
|
1106
|
+
role="menuitem"
|
|
1107
|
+
@click="showFullMonth"
|
|
1108
|
+
>
|
|
1109
|
+
Show full month
|
|
1110
|
+
</button>
|
|
1111
|
+
</li>
|
|
1112
|
+
</ul>
|
|
1113
|
+
</div>
|
|
1114
|
+
</li>
|
|
1115
|
+
</ul>
|
|
1116
|
+
</nav>
|
|
1107
1117
|
</Teleport>
|
|
1108
1118
|
</div>
|
|
1109
1119
|
</template>
|
|
@@ -1522,35 +1532,3 @@ watch([viewMonth, viewYear], () => {
|
|
|
1522
1532
|
}
|
|
1523
1533
|
}
|
|
1524
1534
|
</style>
|
|
1525
|
-
|
|
1526
|
-
<style>
|
|
1527
|
-
.calendar-day-context-menu {
|
|
1528
|
-
position: fixed;
|
|
1529
|
-
z-index: 1000;
|
|
1530
|
-
min-width: 12rem;
|
|
1531
|
-
padding: 0.35rem;
|
|
1532
|
-
border: 1px solid var(--table-popover-border);
|
|
1533
|
-
border-radius: 8px;
|
|
1534
|
-
background: var(--table-popover-bg);
|
|
1535
|
-
box-shadow: var(--table-popover-shadow);
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
.calendar-day-context-menu-item {
|
|
1539
|
-
display: block;
|
|
1540
|
-
width: 100%;
|
|
1541
|
-
padding: 0.5rem 0.75rem;
|
|
1542
|
-
border: none;
|
|
1543
|
-
border-radius: 4px;
|
|
1544
|
-
background: transparent;
|
|
1545
|
-
color: var(--table-popover-fg);
|
|
1546
|
-
font: inherit;
|
|
1547
|
-
text-align: left;
|
|
1548
|
-
cursor: pointer;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
.calendar-day-context-menu-item:hover,
|
|
1552
|
-
.calendar-day-context-menu-item:focus-visible {
|
|
1553
|
-
background: var(--table-popover-item-bg);
|
|
1554
|
-
outline: none;
|
|
1555
|
-
}
|
|
1556
|
-
</style>
|
|
@@ -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,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import LoadingArea from './LoadingArea.vue'
|
|
5
|
+
|
|
6
|
+
const globalStubs = {
|
|
7
|
+
HugeiconsIcon: true,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('LoadingArea', () => {
|
|
11
|
+
it('shows the placeholder by default', () => {
|
|
12
|
+
const wrapper = mount(LoadingArea, {
|
|
13
|
+
global: { stubs: globalStubs },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(wrapper.find('[role="status"]').exists()).toBe(true)
|
|
17
|
+
expect(wrapper.text()).toContain('Loading…')
|
|
18
|
+
expect(wrapper.attributes('aria-busy')).toBe('true')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('renders default-slot content when not loading', async () => {
|
|
22
|
+
const wrapper = mount(LoadingArea, {
|
|
23
|
+
props: {
|
|
24
|
+
loading: false,
|
|
25
|
+
},
|
|
26
|
+
slots: {
|
|
27
|
+
default: '<p class="ready">Ready</p>',
|
|
28
|
+
},
|
|
29
|
+
global: { stubs: globalStubs },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
await nextTick()
|
|
33
|
+
|
|
34
|
+
expect(wrapper.find('[role="status"]').exists()).toBe(false)
|
|
35
|
+
expect(wrapper.find('.ready').text()).toBe('Ready')
|
|
36
|
+
expect(wrapper.attributes('aria-busy')).toBe('false')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('keeps content mounted in overlay mode while loading', async () => {
|
|
40
|
+
const wrapper = mount(LoadingArea, {
|
|
41
|
+
props: {
|
|
42
|
+
loading: true,
|
|
43
|
+
overlay: true,
|
|
44
|
+
},
|
|
45
|
+
slots: {
|
|
46
|
+
default: '<p class="ready">Ready</p>',
|
|
47
|
+
},
|
|
48
|
+
global: { stubs: globalStubs },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await nextTick()
|
|
52
|
+
|
|
53
|
+
expect(wrapper.find('[role="status"]').exists()).toBe(true)
|
|
54
|
+
expect(wrapper.find('.ready').exists()).toBe(true)
|
|
55
|
+
expect(wrapper.classes()).toContain('is-overlay')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('supports a custom placeholder slot', () => {
|
|
59
|
+
const wrapper = mount(LoadingArea, {
|
|
60
|
+
props: {
|
|
61
|
+
loading: true,
|
|
62
|
+
},
|
|
63
|
+
slots: {
|
|
64
|
+
placeholder: '<span class="custom">Please wait</span>',
|
|
65
|
+
},
|
|
66
|
+
global: { stubs: globalStubs },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(wrapper.find('.custom').text()).toBe('Please wait')
|
|
70
|
+
expect(wrapper.text()).not.toContain('Loading…')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="loading-area"
|
|
4
|
+
:class="{
|
|
5
|
+
'is-loading': loading,
|
|
6
|
+
'is-overlay': loading && overlay && hasContent,
|
|
7
|
+
}"
|
|
8
|
+
:style="rootStyle"
|
|
9
|
+
:aria-busy="loading ? 'true' : 'false'"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
v-if="loading"
|
|
13
|
+
class="loading-area-placeholder"
|
|
14
|
+
role="status"
|
|
15
|
+
aria-live="polite"
|
|
16
|
+
>
|
|
17
|
+
<slot name="placeholder">
|
|
18
|
+
<HugeiconsIcon
|
|
19
|
+
:icon="resolvedIcon"
|
|
20
|
+
class="loading-area-spinner"
|
|
21
|
+
:size="resolvedIconSize"
|
|
22
|
+
:strokeWidth="2"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
/>
|
|
25
|
+
<p v-if="message" class="loading-area-message">{{ message }}</p>
|
|
26
|
+
</slot>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
v-if="hasContent"
|
|
31
|
+
v-show="!loading || overlay"
|
|
32
|
+
class="loading-area-content"
|
|
33
|
+
:aria-hidden="loading && overlay ? 'true' : undefined"
|
|
34
|
+
>
|
|
35
|
+
<slot />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup>
|
|
41
|
+
import { computed, useSlots } from 'vue'
|
|
42
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
43
|
+
import { Loading03Icon } from '@hugeicons/core-free-icons'
|
|
44
|
+
|
|
45
|
+
const props = defineProps({
|
|
46
|
+
/**
|
|
47
|
+
* When true, shows the loading placeholder (and optionally overlays content).
|
|
48
|
+
*/
|
|
49
|
+
loading: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: true,
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Status text announced to assistive tech and shown under the spinner.
|
|
55
|
+
* Pass an empty string to hide the message.
|
|
56
|
+
*/
|
|
57
|
+
message: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: 'Loading…',
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Keep default-slot content in the layout and cover it with the placeholder.
|
|
63
|
+
*/
|
|
64
|
+
overlay: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Hugeicons icon definition. Defaults to Loading03Icon.
|
|
70
|
+
*/
|
|
71
|
+
icon: {
|
|
72
|
+
type: [Object, Array, Function],
|
|
73
|
+
default: null,
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Spinner size in px, or a CSS length string (e.g. "1.5em").
|
|
77
|
+
*/
|
|
78
|
+
iconSize: {
|
|
79
|
+
type: [Number, String],
|
|
80
|
+
default: 28,
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Minimum height of the loading area (number = px, or any CSS length).
|
|
84
|
+
*/
|
|
85
|
+
minHeight: {
|
|
86
|
+
type: [Number, String],
|
|
87
|
+
default: null,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const slots = useSlots()
|
|
92
|
+
|
|
93
|
+
const hasContent = computed(() => typeof slots.default === 'function')
|
|
94
|
+
|
|
95
|
+
const resolvedIcon = computed(() => props.icon || Loading03Icon)
|
|
96
|
+
|
|
97
|
+
const resolvedIconSize = computed(() => {
|
|
98
|
+
if (typeof props.iconSize === 'number') {
|
|
99
|
+
return props.iconSize
|
|
100
|
+
}
|
|
101
|
+
const parsed = Number.parseFloat(props.iconSize)
|
|
102
|
+
return Number.isFinite(parsed) ? parsed : 28
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const rootStyle = computed(() => {
|
|
106
|
+
if (props.minHeight == null || props.minHeight === '') {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
const value = typeof props.minHeight === 'number'
|
|
110
|
+
? `${props.minHeight}px`
|
|
111
|
+
: props.minHeight
|
|
112
|
+
return { minHeight: value }
|
|
113
|
+
})
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<style scoped>
|
|
117
|
+
.loading-area {
|
|
118
|
+
position: relative;
|
|
119
|
+
display: block;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.loading-area-placeholder {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
gap: 0.75rem;
|
|
129
|
+
padding: 2rem 1rem;
|
|
130
|
+
text-align: center;
|
|
131
|
+
color: var(--table-muted-fg, var(--muted-text-color));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.loading-area.is-overlay > .loading-area-placeholder {
|
|
135
|
+
position: absolute;
|
|
136
|
+
inset: 0;
|
|
137
|
+
z-index: 1;
|
|
138
|
+
box-sizing: border-box;
|
|
139
|
+
background-color: color-mix(in srgb, var(--section-bg-color, #fff) 78%, transparent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.loading-area-spinner {
|
|
143
|
+
display: block;
|
|
144
|
+
flex: 0 0 auto;
|
|
145
|
+
animation: loading-area-spin 0.8s linear infinite;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.loading-area-message {
|
|
149
|
+
margin: 0;
|
|
150
|
+
font-size: 0.95em;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.loading-area-content {
|
|
154
|
+
min-width: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.loading-area.is-overlay > .loading-area-content {
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
user-select: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@keyframes loading-area-spin {
|
|
163
|
+
from {
|
|
164
|
+
transform: rotate(0deg);
|
|
165
|
+
}
|
|
166
|
+
to {
|
|
167
|
+
transform: rotate(360deg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@media (prefers-reduced-motion: reduce) {
|
|
172
|
+
.loading-area-spinner {
|
|
173
|
+
animation: none;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -401,6 +401,34 @@ defineExpose({
|
|
|
401
401
|
</script>
|
|
402
402
|
|
|
403
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
|
+
|
|
404
432
|
h2 {
|
|
405
433
|
padding: .75em;
|
|
406
434
|
}
|