picocrank 1.30.0 → 1.31.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.30.0",
3
+ "version": "1.31.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",
@@ -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 hasActions = computed(() => {
117
+ const slotActionCount = computed(() => {
100
118
  const render = slots.actions
101
119
  if (!render) {
102
- return false
120
+ return 0
103
121
  }
104
- return render().some((vnode) => vnode.type !== Comment)
122
+ return render().filter((vnode) => vnode.type !== Comment).length
105
123
  })
106
124
 
107
- const showActions = computed(() => hasActions.value || props.showCopyButton)
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;
@@ -7,9 +7,6 @@
7
7
  <HugeiconsIcon
8
8
  :icon="icon"
9
9
  class="section-title-icon"
10
- width="1.1em"
11
- height="1.1em"
12
- :strokeWidth="2"
13
10
  aria-hidden="true"
14
11
  />
15
12
  <span class="section-title-text">
@@ -3,7 +3,7 @@
3
3
  :id="SIDEBAR_PANEL_ID"
4
4
  ref="sidebarRef"
5
5
  class="sidebar"
6
- :class="{ 'shown': isOpen, 'stuck': isStuck }"
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="3"
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="3"
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) {
@@ -410,6 +414,28 @@ button {
410
414
  border: 0;
411
415
  }
412
416
 
417
+ .stick-toggle {
418
+ display: inline-flex;
419
+ align-items: center;
420
+ justify-content: center;
421
+ align-self: stretch;
422
+ flex-shrink: 0;
423
+ padding-top: 0;
424
+ padding-bottom: 0;
425
+ padding-left: 1em;
426
+ padding-right: 1em;
427
+ border-radius: 0;
428
+ color: var(--text-color);
429
+ background-color: transparent;
430
+ cursor: pointer;
431
+ }
432
+
433
+ .stick-toggle:hover,
434
+ .stick-toggle:focus-visible {
435
+ background-color: var(--sidebar-hover-bg-color);
436
+ color: var(--hover-text-color);
437
+ }
438
+
413
439
  .navigation-links,
414
440
  .nav-section-links {
415
441
  list-style: none;
@@ -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
- <ReadOnlyTextArea
45
- :rows="4"
46
- placeholder="No diagnostics available."
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>