picocrank 1.28.1 → 1.29.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
CHANGED
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
--danger-zone-summary-hover-bg: var(--karma-bad-tint);
|
|
91
91
|
--danger-zone-warning-fg: var(--muted-text-color);
|
|
92
92
|
--danger-zone-focus-outline: var(--karma-bad-border);
|
|
93
|
+
|
|
94
|
+
--picocrank-fixed-header-offset: 4em;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
table thead th {
|
|
@@ -103,7 +105,17 @@ section .section-content {
|
|
|
103
105
|
min-width: 0;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
/*
|
|
108
|
+
/*
|
|
109
|
+
* Femtocrank 2.7 compacts section toolbars; keep pre-upgrade default button padding.
|
|
110
|
+
* Toolbar is a flex row beside the section title — buttons must not shrink when a
|
|
111
|
+
* search or filter control shares the row (avoid width: 100% on flex siblings).
|
|
112
|
+
*/
|
|
113
|
+
section .section-header [role="toolbar"] {
|
|
114
|
+
flex: 1 1 auto;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
justify-content: flex-end;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
section .section-header [role="toolbar"] button,
|
|
108
120
|
section .section-header [role="toolbar"] .button {
|
|
109
121
|
padding-top: .4em;
|
|
@@ -112,6 +124,12 @@ section .section-header [role="toolbar"] .button {
|
|
|
112
124
|
padding-right: 1em;
|
|
113
125
|
min-width: 0;
|
|
114
126
|
line-height: normal;
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
section .section-header [role="toolbar"] > :not(button):not(.button) {
|
|
131
|
+
flex: 1 1 auto;
|
|
132
|
+
min-width: 0;
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
section.narrow {
|
|
@@ -152,6 +170,27 @@ select.field-invalid:focus-visible {
|
|
|
152
170
|
outline: 2px solid var(--karma-bad-border);
|
|
153
171
|
outline-offset: 2px;
|
|
154
172
|
}
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* Fixed header layout: pin the app header and offset sidebar + main content.
|
|
176
|
+
* Apply .layout-fixed-header on a wrapper whose direct children are header and #layout.
|
|
177
|
+
*/
|
|
178
|
+
.layout-fixed-header > header {
|
|
179
|
+
position: fixed;
|
|
180
|
+
top: 0;
|
|
181
|
+
left: 0;
|
|
182
|
+
width: 100%;
|
|
183
|
+
z-index: 5;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.layout-fixed-header > #layout aside {
|
|
187
|
+
z-index: 3;
|
|
188
|
+
padding-top: var(--picocrank-fixed-header-offset);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.layout-fixed-header > #layout main {
|
|
192
|
+
padding-top: var(--picocrank-fixed-header-offset);
|
|
193
|
+
}
|
|
155
194
|
}
|
|
156
195
|
|
|
157
196
|
/*
|
|
@@ -108,12 +108,12 @@ const props = defineProps({
|
|
|
108
108
|
const emit = defineEmits(['page-change', 'page-size-change', 'update:page', 'update:pageSize'])
|
|
109
109
|
|
|
110
110
|
const localPageSize = ref(props.pageSize)
|
|
111
|
-
const localCurrentPage = ref(props.currentPage)
|
|
112
111
|
|
|
113
|
-
// Computed property to get the current page value (supports both v-model and regular props)
|
|
114
112
|
const currentPageValue = computed(() => {
|
|
115
|
-
//
|
|
116
|
-
|
|
113
|
+
// `page` supports v-model; `currentPage` is the plain prop. Both default to 1.
|
|
114
|
+
if (props.currentPage !== 1) {
|
|
115
|
+
return props.currentPage
|
|
116
|
+
}
|
|
117
117
|
return props.page
|
|
118
118
|
})
|
|
119
119
|
|
|
@@ -124,28 +124,18 @@ const endItem = computed(() => Math.min(currentPageValue.value * localPageSize.v
|
|
|
124
124
|
|
|
125
125
|
function goToPage(page) {
|
|
126
126
|
if (page >= 1 && page <= totalPages.value && page !== currentPageValue.value) {
|
|
127
|
-
localCurrentPage.value = page
|
|
128
127
|
emit('page-change', page)
|
|
129
128
|
emit('update:page', page)
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
function handlePageSizeChange() {
|
|
134
|
-
localCurrentPage.value = 1
|
|
135
133
|
emit('page-size-change', localPageSize.value)
|
|
136
134
|
emit('update:pageSize', localPageSize.value)
|
|
137
135
|
emit('page-change', 1)
|
|
138
136
|
emit('update:page', 1)
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
watch(() => props.currentPage, (newPage) => {
|
|
142
|
-
localCurrentPage.value = newPage
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
watch(() => props.page, (newPage) => {
|
|
146
|
-
localCurrentPage.value = newPage
|
|
147
|
-
})
|
|
148
|
-
|
|
149
139
|
watch(() => props.pageSize, (newSize) => {
|
|
150
140
|
localPageSize.value = newSize
|
|
151
141
|
})
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="readonly-textarea">
|
|
3
|
-
<div v-if="
|
|
3
|
+
<div v-if="showHeader" class="readonly-textarea-header">
|
|
4
4
|
<label v-if="label" :for="textareaId">{{ label }}</label>
|
|
5
|
-
<div v-
|
|
6
|
-
<
|
|
7
|
-
v-if="
|
|
8
|
-
|
|
9
|
-
class="readonly-textarea-
|
|
10
|
-
:title="copied ? 'Copied' : 'Copy to clipboard'"
|
|
11
|
-
:disabled="!modelValue"
|
|
12
|
-
@click="copy"
|
|
5
|
+
<div v-if="pushActionsEnd" class="fg1" />
|
|
6
|
+
<div
|
|
7
|
+
v-if="showActions"
|
|
8
|
+
class="readonly-textarea-actions"
|
|
9
|
+
:class="{ 'readonly-textarea-actions--leading': hasActions }"
|
|
13
10
|
>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
<div v-if="hasActions" class="readonly-textarea-actions-slot">
|
|
12
|
+
<slot name="actions" />
|
|
13
|
+
</div>
|
|
14
|
+
<button
|
|
15
|
+
v-if="showCopyButton"
|
|
16
|
+
type="button"
|
|
17
|
+
class="readonly-textarea-copy"
|
|
18
|
+
:title="copied ? 'Copied' : 'Copy to clipboard'"
|
|
19
|
+
:disabled="!modelValue"
|
|
20
|
+
@click="copy"
|
|
21
|
+
>
|
|
22
|
+
<HugeiconsIcon :icon="copied ? CopyCheckIcon : Copy01Icon" width="1em" height="1em" />
|
|
23
|
+
<span>{{ copied ? copiedLabel : copyLabel }}</span>
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
17
26
|
</div>
|
|
18
27
|
<textarea
|
|
19
28
|
:id="textareaId"
|
|
@@ -28,7 +37,7 @@
|
|
|
28
37
|
</template>
|
|
29
38
|
|
|
30
39
|
<script setup>
|
|
31
|
-
import { ref, computed, useId, watch } from 'vue'
|
|
40
|
+
import { ref, computed, useId, useSlots, watch, Comment } from 'vue'
|
|
32
41
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
33
42
|
import { Copy01Icon, CopyCheckIcon } from '@hugeicons/core-free-icons'
|
|
34
43
|
|
|
@@ -85,6 +94,26 @@ const props = defineProps({
|
|
|
85
94
|
|
|
86
95
|
const emit = defineEmits(['copy', 'copy-error', 'update:modelValue'])
|
|
87
96
|
|
|
97
|
+
const slots = useSlots()
|
|
98
|
+
|
|
99
|
+
const hasActions = computed(() => {
|
|
100
|
+
const render = slots.actions
|
|
101
|
+
if (!render) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
return render().some((vnode) => vnode.type !== Comment)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const showActions = computed(() => hasActions.value || props.showCopyButton)
|
|
108
|
+
|
|
109
|
+
const showHeader = computed(
|
|
110
|
+
() => Boolean(props.label) || showActions.value,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const pushActionsEnd = computed(
|
|
114
|
+
() => showActions.value && !hasActions.value,
|
|
115
|
+
)
|
|
116
|
+
|
|
88
117
|
const generatedId = useId()
|
|
89
118
|
const textareaId = props.id || `readonly-textarea-${generatedId}`
|
|
90
119
|
const textareaRef = ref(null)
|
|
@@ -180,12 +209,13 @@ defineExpose({
|
|
|
180
209
|
flex-direction: column;
|
|
181
210
|
gap: 0.5rem;
|
|
182
211
|
width: 100%;
|
|
212
|
+
min-width: 0;
|
|
213
|
+
max-width: 100%;
|
|
183
214
|
}
|
|
184
215
|
|
|
185
216
|
.readonly-textarea-header {
|
|
186
217
|
display: flex;
|
|
187
218
|
align-items: center;
|
|
188
|
-
justify-content: space-between;
|
|
189
219
|
gap: 0.75rem;
|
|
190
220
|
}
|
|
191
221
|
|
|
@@ -195,22 +225,46 @@ defineExpose({
|
|
|
195
225
|
|
|
196
226
|
.fg1 {
|
|
197
227
|
flex: 1;
|
|
228
|
+
min-width: 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.readonly-textarea-actions {
|
|
232
|
+
display: inline-flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 0.75rem;
|
|
235
|
+
margin-left: auto;
|
|
236
|
+
flex-shrink: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.readonly-textarea-actions--leading {
|
|
240
|
+
margin-left: 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.readonly-textarea-actions-slot {
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: 0.75rem;
|
|
247
|
+
flex-wrap: wrap;
|
|
198
248
|
}
|
|
199
249
|
|
|
200
250
|
.readonly-textarea-copy {
|
|
201
251
|
display: inline-flex;
|
|
202
252
|
align-items: center;
|
|
203
253
|
gap: 0.35em;
|
|
254
|
+
flex-shrink: 0;
|
|
204
255
|
}
|
|
205
256
|
|
|
206
257
|
.readonly-textarea textarea {
|
|
207
258
|
width: 100%;
|
|
259
|
+
max-width: 100%;
|
|
260
|
+
box-sizing: border-box;
|
|
208
261
|
resize: vertical;
|
|
209
262
|
margin: 0;
|
|
210
263
|
}
|
|
211
264
|
|
|
212
265
|
.readonly-textarea textarea.monospace {
|
|
213
266
|
font-family: monospace;
|
|
214
|
-
white-space: pre;
|
|
267
|
+
white-space: pre-wrap;
|
|
268
|
+
overflow-wrap: anywhere;
|
|
215
269
|
}
|
|
216
270
|
</style>
|
|
@@ -107,7 +107,8 @@ import { HugeiconsIcon } from '@hugeicons/vue'
|
|
|
107
107
|
import { Pin02Icon, PinIcon, ArrowDown01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons'
|
|
108
108
|
import SidebarNavLink from './SidebarNavLink.vue'
|
|
109
109
|
|
|
110
|
-
const
|
|
110
|
+
const COLLAPSED_SECTIONS_STORAGE_KEY = 'picocrank-nav-sections-collapsed'
|
|
111
|
+
const STUCK_STORAGE_KEY = 'picocrank-sidebar-stuck'
|
|
111
112
|
const SIDEBAR_PANEL_ID = 'picocrank-sidebar'
|
|
112
113
|
const TOGGLER_ID = 'sidebar-toggler-button'
|
|
113
114
|
|
|
@@ -118,8 +119,16 @@ const props = defineProps({
|
|
|
118
119
|
},
|
|
119
120
|
})
|
|
120
121
|
|
|
122
|
+
function loadStuckPreference() {
|
|
123
|
+
try {
|
|
124
|
+
return localStorage.getItem(STUCK_STORAGE_KEY) === 'true'
|
|
125
|
+
} catch {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
const isOpen = ref(false)
|
|
122
|
-
const isStuck = ref(
|
|
131
|
+
const isStuck = ref(loadStuckPreference())
|
|
123
132
|
const sidebarRef = ref(null)
|
|
124
133
|
const route = useRoute()
|
|
125
134
|
const headingId = useId()
|
|
@@ -156,7 +165,7 @@ function isActive(link) {
|
|
|
156
165
|
|
|
157
166
|
function loadCollapsedSections() {
|
|
158
167
|
try {
|
|
159
|
-
const raw = localStorage.getItem(
|
|
168
|
+
const raw = localStorage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY)
|
|
160
169
|
if (!raw) return new Set()
|
|
161
170
|
const parsed = JSON.parse(raw)
|
|
162
171
|
return new Set(Array.isArray(parsed) ? parsed : [])
|
|
@@ -168,7 +177,7 @@ function loadCollapsedSections() {
|
|
|
168
177
|
const collapsedSections = ref(loadCollapsedSections())
|
|
169
178
|
|
|
170
179
|
function saveCollapsedSections() {
|
|
171
|
-
localStorage.setItem(
|
|
180
|
+
localStorage.setItem(COLLAPSED_SECTIONS_STORAGE_KEY, JSON.stringify([...collapsedSections.value]))
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
function isSectionCollapsed(sectionId) {
|
|
@@ -244,6 +253,14 @@ watch(() => route.fullPath, () => {
|
|
|
244
253
|
expandSectionContainingActiveRoute()
|
|
245
254
|
}, { immediate: true })
|
|
246
255
|
|
|
256
|
+
watch(isStuck, (stuck) => {
|
|
257
|
+
try {
|
|
258
|
+
localStorage.setItem(STUCK_STORAGE_KEY, stuck ? 'true' : 'false')
|
|
259
|
+
} catch {
|
|
260
|
+
// ignore storage errors (private mode, quota, etc.)
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
247
264
|
function toggleStick() {
|
|
248
265
|
isStuck.value = !isStuck.value
|
|
249
266
|
}
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
label="Application log"
|
|
19
19
|
:rows="10"
|
|
20
20
|
@copy="onCopy"
|
|
21
|
-
|
|
21
|
+
>
|
|
22
|
+
<template #actions>
|
|
23
|
+
<button type="button" @click="refreshDiagnostics">Refresh log</button>
|
|
24
|
+
</template>
|
|
25
|
+
</ReadOnlyTextArea>
|
|
22
26
|
|
|
23
27
|
<ReadOnlyTextArea
|
|
24
28
|
v-model="environmentInfo"
|