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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.28.1",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
6
6
  "main": "vite.config.js",
@@ -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
- /* Femtocrank 2.7 compacts section toolbars; keep pre-upgrade default button padding. */
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
- // When using v-model, the page prop will be reactive and change
116
- // When using regular props, currentPage will be reactive
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="label || showCopyButton" class="readonly-textarea-header">
3
+ <div v-if="showHeader" class="readonly-textarea-header">
4
4
  <label v-if="label" :for="textareaId">{{ label }}</label>
5
- <div v-else class="fg1" />
6
- <button
7
- v-if="showCopyButton"
8
- type="button"
9
- class="readonly-textarea-copy"
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
- <HugeiconsIcon :icon="copied ? CopyCheckIcon : Copy01Icon" width="1em" height="1em" />
15
- <span>{{ copied ? copiedLabel : copyLabel }}</span>
16
- </button>
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 STORAGE_KEY = 'picocrank-nav-sections-collapsed'
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(false)
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(STORAGE_KEY)
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(STORAGE_KEY, JSON.stringify([...collapsedSections.value]))
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"