picocrank 1.17.0 → 1.18.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.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
5
  "main": "vite.config.js",
6
6
  "scripts": {
package/router.js CHANGED
@@ -7,6 +7,7 @@ import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
7
7
  import { CalendarIcon } from '@hugeicons/core-free-icons';
8
8
  import { EditIcon } from '@hugeicons/core-free-icons';
9
9
  import { Menu01Icon } from '@hugeicons/core-free-icons';
10
+ import { ClipboardCopyIcon } from '@hugeicons/core-free-icons';
10
11
 
11
12
  const routes = [
12
13
  {
@@ -75,6 +76,15 @@ const routes = [
75
76
  icon: EditIcon,
76
77
  }
77
78
  },
79
+ {
80
+ name: 'ReadOnlyTextAreaExample',
81
+ path: '/readonly-textarea-example',
82
+ component: () => import('./vue/views/ReadOnlyTextAreaExample.vue'),
83
+ meta: {
84
+ title: 'ReadOnlyTextArea Example',
85
+ icon: ClipboardCopyIcon,
86
+ }
87
+ },
78
88
  {
79
89
  name: 'TabsExample',
80
90
  path: '/tabs-example',
@@ -0,0 +1,178 @@
1
+ <template>
2
+ <div class="readonly-textarea">
3
+ <div v-if="label || showCopyButton" class="readonly-textarea-header">
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"
13
+ >
14
+ <HugeiconsIcon :icon="copied ? CopyCheckIcon : Copy01Icon" width="1em" height="1em" />
15
+ <span>{{ copied ? copiedLabel : copyLabel }}</span>
16
+ </button>
17
+ </div>
18
+ <textarea
19
+ :id="textareaId"
20
+ ref="textareaRef"
21
+ :value="displayValue"
22
+ readonly
23
+ :rows="rows"
24
+ :placeholder="placeholder"
25
+ :class="{ monospace }"
26
+ />
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { ref, computed, useId } from 'vue'
32
+ import { HugeiconsIcon } from '@hugeicons/vue'
33
+ import { Copy01Icon, CopyCheckIcon } from '@hugeicons/core-free-icons'
34
+
35
+ const props = defineProps({
36
+ modelValue: {
37
+ type: String,
38
+ default: '',
39
+ },
40
+ label: {
41
+ type: String,
42
+ default: '',
43
+ },
44
+ id: {
45
+ type: String,
46
+ default: '',
47
+ },
48
+ rows: {
49
+ type: Number,
50
+ default: 8,
51
+ },
52
+ placeholder: {
53
+ type: String,
54
+ default: '',
55
+ },
56
+ monospace: {
57
+ type: Boolean,
58
+ default: true,
59
+ },
60
+ showCopyButton: {
61
+ type: Boolean,
62
+ default: true,
63
+ },
64
+ copyLabel: {
65
+ type: String,
66
+ default: 'Copy',
67
+ },
68
+ copiedLabel: {
69
+ type: String,
70
+ default: 'Copied',
71
+ },
72
+ copiedFeedbackMs: {
73
+ type: Number,
74
+ default: 2000,
75
+ },
76
+ markdownTicks: {
77
+ type: Boolean,
78
+ default: false,
79
+ },
80
+ markdownLang: {
81
+ type: String,
82
+ default: '',
83
+ },
84
+ })
85
+
86
+ const emit = defineEmits(['copy', 'copy-error'])
87
+
88
+ const generatedId = useId()
89
+ const textareaId = props.id || `readonly-textarea-${generatedId}`
90
+ const textareaRef = ref(null)
91
+ const copied = ref(false)
92
+ let copiedTimer = null
93
+
94
+ const displayValue = computed(() => {
95
+ if (!props.markdownTicks || !props.modelValue) {
96
+ return props.modelValue
97
+ }
98
+
99
+ const openingFence = props.markdownLang ? `\`\`\`${props.markdownLang}` : '```'
100
+ return `${openingFence}\n${props.modelValue}\n\`\`\``
101
+ })
102
+
103
+ async function copy() {
104
+ if (!props.modelValue) {
105
+ return
106
+ }
107
+
108
+ const text = displayValue.value
109
+
110
+ try {
111
+ await navigator.clipboard.writeText(text)
112
+ markCopied()
113
+ emit('copy', text)
114
+ } catch (error) {
115
+ try {
116
+ textareaRef.value?.select()
117
+ document.execCommand('copy')
118
+ markCopied()
119
+ emit('copy', text)
120
+ } catch (fallbackError) {
121
+ emit('copy-error', fallbackError)
122
+ }
123
+ }
124
+ }
125
+
126
+ function markCopied() {
127
+ copied.value = true
128
+ clearTimeout(copiedTimer)
129
+ copiedTimer = setTimeout(() => {
130
+ copied.value = false
131
+ }, props.copiedFeedbackMs)
132
+ }
133
+
134
+ defineExpose({
135
+ copy,
136
+ })
137
+ </script>
138
+
139
+ <style scoped>
140
+ .readonly-textarea {
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: 0.5rem;
144
+ width: 100%;
145
+ }
146
+
147
+ .readonly-textarea-header {
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: space-between;
151
+ gap: 0.75rem;
152
+ }
153
+
154
+ .readonly-textarea-header label {
155
+ margin: 0;
156
+ }
157
+
158
+ .fg1 {
159
+ flex: 1;
160
+ }
161
+
162
+ .readonly-textarea-copy {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ gap: 0.35em;
166
+ }
167
+
168
+ .readonly-textarea textarea {
169
+ width: 100%;
170
+ resize: vertical;
171
+ margin: 0;
172
+ }
173
+
174
+ .readonly-textarea textarea.monospace {
175
+ font-family: monospace;
176
+ white-space: pre;
177
+ }
178
+ </style>
package/vue/views/App.vue CHANGED
@@ -93,6 +93,7 @@
93
93
  navigation.value.addRouterLink('TableExample')
94
94
  navigation.value.addRouterLink('CalendarExample')
95
95
  navigation.value.addRouterLink('FormExample')
96
+ navigation.value.addRouterLink('ReadOnlyTextAreaExample')
96
97
  navigation.value.addRouterLink('TabsExample')
97
98
  navigation.value.addSeparator('separator-1');
98
99
  navigation.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <Section
3
+ title="ReadOnlyTextArea Example"
4
+ subtitle="Read-only text areas with copy-to-clipboard, for diagnostics and similar output"
5
+ >
6
+ <template #toolbar>
7
+ <button type="button" @click="refreshDiagnostics">Refresh diagnostics</button>
8
+ </template>
9
+
10
+ <div class="readonly-textarea-examples">
11
+ <p>
12
+ Use <code>ReadOnlyTextArea</code> when you need to show log output, API responses,
13
+ or other text that users should read and copy but not edit.
14
+ </p>
15
+
16
+ <ReadOnlyTextArea
17
+ v-model="diagnosticLog"
18
+ label="Application log"
19
+ :rows="10"
20
+ @copy="onCopy"
21
+ />
22
+
23
+ <ReadOnlyTextArea
24
+ v-model="environmentInfo"
25
+ label="Environment (JSON)"
26
+ :rows="12"
27
+ markdown-ticks
28
+ markdown-lang="json"
29
+ />
30
+
31
+ <ReadOnlyTextArea
32
+ label="Empty output"
33
+ :rows="4"
34
+ placeholder="No diagnostics available."
35
+ />
36
+
37
+ <p v-if="lastCopied" class="copy-feedback subtle">
38
+ Copied {{ lastCopied.length > 60 ? `${lastCopied.slice(0, 60)}…` : lastCopied }}
39
+ </p>
40
+ </div>
41
+ </Section>
42
+ </template>
43
+
44
+ <script setup>
45
+ import { ref } from 'vue'
46
+ import Section from '../components/Section.vue'
47
+ import ReadOnlyTextArea from '../components/ReadOnlyTextArea.vue'
48
+
49
+ const diagnosticLog = ref('')
50
+ const environmentInfo = ref('')
51
+ const lastCopied = ref('')
52
+
53
+ function buildDiagnosticLog() {
54
+ const now = new Date().toISOString()
55
+ return [
56
+ `[${now}] INFO Service started`,
57
+ `[${now}] INFO Listening on http://localhost:8080`,
58
+ `[${now}] DEBUG Database connection pool ready (size=5)`,
59
+ `[${now}] WARN Cache miss for key: user:session:abc123`,
60
+ `[${now}] INFO GET /api/health 200 12ms`,
61
+ `[${now}] ERROR Failed to reach upstream metrics collector`,
62
+ `[${now}] INFO Retrying metrics collector in 30s`,
63
+ ].join('\n')
64
+ }
65
+
66
+ function buildEnvironmentInfo() {
67
+ return JSON.stringify({
68
+ app: 'picocrank',
69
+ version: '1.0.0',
70
+ environment: 'development',
71
+ timestamp: new Date().toISOString(),
72
+ runtime: {
73
+ userAgent: navigator.userAgent,
74
+ language: navigator.language,
75
+ online: navigator.onLine,
76
+ },
77
+ features: {
78
+ clipboard: Boolean(navigator.clipboard),
79
+ serviceWorker: 'serviceWorker' in navigator,
80
+ },
81
+ }, null, 2)
82
+ }
83
+
84
+ function refreshDiagnostics() {
85
+ diagnosticLog.value = buildDiagnosticLog()
86
+ environmentInfo.value = buildEnvironmentInfo()
87
+ lastCopied.value = ''
88
+ }
89
+
90
+ function onCopy(value) {
91
+ lastCopied.value = value.split('\n')[0]
92
+ }
93
+
94
+ refreshDiagnostics()
95
+ </script>
96
+
97
+ <style scoped>
98
+ .readonly-textarea-examples {
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 1.5rem;
102
+ }
103
+
104
+ .readonly-textarea-examples p {
105
+ margin: 0;
106
+ }
107
+
108
+ .copy-feedback {
109
+ margin: 0;
110
+ font-size: 0.875rem;
111
+ }
112
+ </style>