fds-vue-core 2.2.0 → 2.3.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": "fds-vue-core",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "FDS Vue Core Component Library",
5
5
  "type": "module",
6
6
  "main": "./dist/fds-vue-core.cjs.js",
@@ -2,14 +2,9 @@
2
2
  import { computed, ref } from 'vue'
3
3
  import { useDevMode } from '../../composables/useDevMode'
4
4
  import FdsButtonIcon from '../Buttons/FdsButtonIcon/FdsButtonIcon.vue'
5
+ import FdsDevModeStorage from './FdsDevModeStorage.vue'
5
6
 
6
- type EnvironmentKey =
7
- | 'localhost'
8
- | 'development'
9
- | 'development:feature-branch'
10
- | 'preprod'
11
- | 'demo'
12
- | 'production'
7
+ type EnvironmentKey = 'localhost' | 'development' | 'development:feature-branch' | 'preprod' | 'demo' | 'production'
13
8
 
14
9
  const props = withDefaults(
15
10
  defineProps<{
@@ -22,6 +17,7 @@ const props = withDefaults(
22
17
 
23
18
  const { isDevMode, toggleDevMode } = useDevMode()
24
19
  const hideEnvBanner = ref(false)
20
+ const isStorageModalOpen = ref(false)
25
21
 
26
22
  const environmentKey = computed<EnvironmentKey | null>(() => props.environment ?? null)
27
23
 
@@ -93,6 +89,10 @@ const envBannerFillColor = computed(() => {
93
89
  const handleEnvBanner = () => {
94
90
  hideEnvBanner.value = true
95
91
  }
92
+
93
+ const openStorageModal = () => {
94
+ isStorageModalOpen.value = true
95
+ }
96
96
  </script>
97
97
 
98
98
  <template>
@@ -110,6 +110,10 @@ const handleEnvBanner = () => {
110
110
  {{ isDevMode ? 'Dev mode: ON' : 'Dev mode: OFF' }}
111
111
  </button>
112
112
 
113
+ <FdsButtonIcon icon="settings" :size="24" class="mx-1" :class="envBannerFillColor" @click="openStorageModal" />
114
+
113
115
  <FdsButtonIcon @click="handleEnvBanner" icon="cross" :size="24" :class="envBannerFillColor" />
114
116
  </div>
117
+
118
+ <FdsDevModeStorage v-model:open="isStorageModalOpen" />
115
119
  </template>
@@ -0,0 +1,413 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref, watch } from 'vue'
3
+ import FdsBlockInfo from '../Blocks/FdsBlockInfo/FdsBlockInfo.vue'
4
+ import FdsButtonPrimary from '../Buttons/FdsButtonPrimary/FdsButtonPrimary.vue'
5
+ import FdsButtonSecondary from '../Buttons/FdsButtonSecondary/FdsButtonSecondary.vue'
6
+ import FdsModal from '../FdsModal/FdsModal.vue'
7
+ import FdsTabs from '../Tabs/FdsTabs/FdsTabs.vue'
8
+ import FdsTabsItem from '../Tabs/FdsTabsItem/FdsTabsItem.vue'
9
+
10
+ type StorageEntry = {
11
+ key: string
12
+ value: string
13
+ }
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ open?: boolean
18
+ }>(),
19
+ {
20
+ open: false,
21
+ },
22
+ )
23
+
24
+ const emit = defineEmits<{
25
+ (e: 'update:open', value: boolean): void
26
+ }>()
27
+
28
+ const isOpen = computed({
29
+ get: () => props.open,
30
+ set: (value: boolean) => emit('update:open', value),
31
+ })
32
+
33
+ const activeTab = ref<'local' | 'session' | 'cookies' | 'placeholder1' | 'placeholder2'>('local')
34
+
35
+ const localStorageEntries = ref<StorageEntry[]>([])
36
+ const sessionStorageEntries = ref<StorageEntry[]>([])
37
+ const cookieEntries = ref<StorageEntry[]>([])
38
+
39
+ const localEditKey = ref('')
40
+ const localEditValue = ref('')
41
+ const sessionEditKey = ref('')
42
+ const sessionEditValue = ref('')
43
+ const cookieEditKey = ref('')
44
+ const cookieEditValue = ref('')
45
+
46
+ const localTextareaRef = ref<HTMLTextAreaElement | null>(null)
47
+ const sessionTextareaRef = ref<HTMLTextAreaElement | null>(null)
48
+ const cookieTextareaRef = ref<HTMLTextAreaElement | null>(null)
49
+
50
+ const resizeTextarea = (el: HTMLTextAreaElement | null) => {
51
+ if (!el) return
52
+ // Reset to default when empty
53
+ if (!el.value) {
54
+ el.style.height = ''
55
+ return
56
+ }
57
+ el.style.height = 'auto'
58
+ el.style.height = `${el.scrollHeight}px`
59
+ }
60
+
61
+ const loadStorageEntries = () => {
62
+ if (typeof window === 'undefined') {
63
+ localStorageEntries.value = []
64
+ sessionStorageEntries.value = []
65
+ cookieEntries.value = []
66
+ return
67
+ }
68
+
69
+ const extractEntries = (storage: Storage): StorageEntry[] => {
70
+ const entries: StorageEntry[] = []
71
+ for (let i = 0; i < storage.length; i += 1) {
72
+ const key = storage.key(i)
73
+ if (!key) continue
74
+ let value: string
75
+ try {
76
+ value = storage.getItem(key) ?? ''
77
+ } catch {
78
+ value = '[unreadable value]'
79
+ }
80
+ entries.push({ key, value })
81
+ }
82
+ return entries
83
+ }
84
+
85
+ localStorageEntries.value = extractEntries(window.localStorage)
86
+ sessionStorageEntries.value = extractEntries(window.sessionStorage)
87
+
88
+ const cookiesSource = typeof document !== 'undefined' && document.cookie ? document.cookie.split('; ') : []
89
+ cookieEntries.value = cookiesSource.filter(Boolean).map((cookie) => {
90
+ const [rawKey, ...rest] = cookie.split('=')
91
+ const key = decodeURIComponent(rawKey || '')
92
+ const value = decodeURIComponent(rest.join('='))
93
+ return {
94
+ key,
95
+ value,
96
+ }
97
+ })
98
+ }
99
+
100
+ const handleOpenChange = (value: boolean) => {
101
+ if (value) {
102
+ loadStorageEntries()
103
+ }
104
+ isOpen.value = value
105
+ }
106
+
107
+ watch(
108
+ () => props.open,
109
+ (newVal) => {
110
+ if (newVal) {
111
+ loadStorageEntries()
112
+ }
113
+ },
114
+ )
115
+
116
+ const refreshStorageEntries = () => {
117
+ loadStorageEntries()
118
+ }
119
+
120
+ const selectLocalEntry = (entry: StorageEntry) => {
121
+ localEditKey.value = entry.key
122
+ localEditValue.value = entry.value
123
+ nextTick(() => resizeTextarea(localTextareaRef.value))
124
+ }
125
+
126
+ const selectSessionEntry = (entry: StorageEntry) => {
127
+ sessionEditKey.value = entry.key
128
+ sessionEditValue.value = entry.value
129
+ nextTick(() => resizeTextarea(sessionTextareaRef.value))
130
+ }
131
+
132
+ const selectCookieEntry = (entry: StorageEntry) => {
133
+ cookieEditKey.value = entry.key
134
+ cookieEditValue.value = entry.value
135
+ nextTick(() => resizeTextarea(cookieTextareaRef.value))
136
+ }
137
+
138
+ const saveLocalEntry = () => {
139
+ if (typeof window === 'undefined') return
140
+ if (!localEditKey.value) return
141
+ window.localStorage.setItem(localEditKey.value, localEditValue.value)
142
+ // Clear selection after save
143
+ localEditKey.value = ''
144
+ localEditValue.value = ''
145
+ nextTick(() => resizeTextarea(localTextareaRef.value))
146
+ loadStorageEntries()
147
+ }
148
+
149
+ const deleteLocalEntry = () => {
150
+ if (typeof window === 'undefined') return
151
+ if (!localEditKey.value) return
152
+ window.localStorage.removeItem(localEditKey.value)
153
+ localEditKey.value = ''
154
+ localEditValue.value = ''
155
+ nextTick(() => resizeTextarea(localTextareaRef.value))
156
+ loadStorageEntries()
157
+ }
158
+
159
+ const saveSessionEntry = () => {
160
+ if (typeof window === 'undefined') return
161
+ if (!sessionEditKey.value) return
162
+ window.sessionStorage.setItem(sessionEditKey.value, sessionEditValue.value)
163
+ sessionEditKey.value = ''
164
+ sessionEditValue.value = ''
165
+ nextTick(() => resizeTextarea(sessionTextareaRef.value))
166
+ loadStorageEntries()
167
+ }
168
+
169
+ const deleteSessionEntry = () => {
170
+ if (typeof window === 'undefined') return
171
+ if (!sessionEditKey.value) return
172
+ window.sessionStorage.removeItem(sessionEditKey.value)
173
+ sessionEditKey.value = ''
174
+ sessionEditValue.value = ''
175
+ nextTick(() => resizeTextarea(sessionTextareaRef.value))
176
+ loadStorageEntries()
177
+ }
178
+
179
+ const saveCookieEntry = () => {
180
+ if (typeof document === 'undefined') return
181
+ if (!cookieEditKey.value) return
182
+ // Basic cookie setter; consumers can override domain/path via browser devtools if needed
183
+ document.cookie = `${encodeURIComponent(cookieEditKey.value)}=${encodeURIComponent(cookieEditValue.value)}; path=/`
184
+ cookieEditKey.value = ''
185
+ cookieEditValue.value = ''
186
+ nextTick(() => resizeTextarea(cookieTextareaRef.value))
187
+ loadStorageEntries()
188
+ }
189
+
190
+ const deleteCookieEntry = () => {
191
+ if (typeof document === 'undefined') return
192
+ if (!cookieEditKey.value) return
193
+ // Expire cookie
194
+ document.cookie = `${encodeURIComponent(cookieEditKey.value)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
195
+ cookieEditKey.value = ''
196
+ cookieEditValue.value = ''
197
+ nextTick(() => resizeTextarea(cookieTextareaRef.value))
198
+ loadStorageEntries()
199
+ }
200
+ </script>
201
+
202
+ <template>
203
+ <FdsModal :open="isOpen" heading="DevTools" size="xl" @update:open="handleOpenChange">
204
+ <div class="space-y-6">
205
+ <FdsTabs variant="primary">
206
+ <FdsTabsItem label="localStorage" :active="activeTab === 'local'" as="button" @click="activeTab = 'local'" />
207
+ <FdsTabsItem
208
+ label="sessionStorage"
209
+ :active="activeTab === 'session'"
210
+ as="button"
211
+ @click="activeTab = 'session'"
212
+ />
213
+ <FdsTabsItem label="Cookies" :active="activeTab === 'cookies'" as="button" @click="activeTab = 'cookies'" />
214
+ <FdsTabsItem
215
+ label="Placeholder 1"
216
+ :active="activeTab === 'placeholder1'"
217
+ as="button"
218
+ @click="activeTab = 'placeholder1'"
219
+ />
220
+ <FdsTabsItem
221
+ label="Placeholder 2"
222
+ :active="activeTab === 'placeholder2'"
223
+ as="button"
224
+ @click="activeTab = 'placeholder2'"
225
+ />
226
+ </FdsTabs>
227
+
228
+ <!-- localStorage TAB -->
229
+ <FdsBlockInfo v-if="activeTab === 'local'" heading="localStorage" icon="information" size="small">
230
+ <div v-if="!localStorageEntries.length" class="text-sm text-gray-600">Inga värden i localStorage.</div>
231
+ <div v-else class="max-h-72 overflow-auto border border-gray-200 rounded-md">
232
+ <table class="w-full text-left text-xs border-collapse">
233
+ <thead class="bg-gray-50">
234
+ <tr>
235
+ <th class="py-2 px-3 font-semibold">Nyckel</th>
236
+ <th class="py-2 px-3 font-semibold">Värde</th>
237
+ </tr>
238
+ </thead>
239
+ <tbody>
240
+ <tr
241
+ v-for="entry in localStorageEntries"
242
+ :key="`local-${entry.key}`"
243
+ class="border-t border-gray-200 align-top cursor-pointer hover:bg-gray-50"
244
+ @click="selectLocalEntry(entry)"
245
+ >
246
+ <td class="py-1.5 px-3 font-mono break-all">
247
+ {{ entry.key }}
248
+ </td>
249
+ <td class="py-1.5 px-3 font-mono whitespace-pre-wrap break-all">
250
+ {{ entry.value }}
251
+ </td>
252
+ </tr>
253
+ </tbody>
254
+ </table>
255
+ </div>
256
+
257
+ <div class="mt-4 space-y-2">
258
+ <div class="flex flex-col gap-1">
259
+ <label class="text-xs font-semibold">Nyckel</label>
260
+ <input
261
+ v-model="localEditKey"
262
+ class="border border-gray-300 rounded px-2 py-1 text-xs font-mono bg-white"
263
+ placeholder="localStorage-nyckel"
264
+ />
265
+ </div>
266
+ <div class="flex flex-col gap-1">
267
+ <label class="text-xs font-semibold">Värde</label>
268
+ <textarea
269
+ v-model="localEditValue"
270
+ rows="3"
271
+ ref="localTextareaRef"
272
+ class="border border-gray-300 rounded px-2 py-1 text-xs font-mono bg-white overflow-hidden"
273
+ placeholder="Värde (lagras som sträng)"
274
+ />
275
+ </div>
276
+
277
+ <div v-if="localEditKey" class="mt-2 flex flex-wrap gap-2">
278
+ <FdsButtonSecondary text="Ta bort" size="sm" @click="deleteLocalEntry" />
279
+ <FdsButtonPrimary text="Spara" size="sm" @click="saveLocalEntry" />
280
+ </div>
281
+ </div>
282
+ </FdsBlockInfo>
283
+
284
+ <!-- sessionStorage TAB -->
285
+ <FdsBlockInfo v-else-if="activeTab === 'session'" heading="sessionStorage" icon="information" size="small">
286
+ <div v-if="!sessionStorageEntries.length" class="text-sm text-gray-600">Inga värden i sessionStorage.</div>
287
+ <div v-else class="max-h-72 overflow-auto border border-gray-200 rounded-md">
288
+ <table class="w-full text-left text-xs border-collapse">
289
+ <thead class="bg-gray-50">
290
+ <tr>
291
+ <th class="py-2 px-3 font-semibold">Nyckel</th>
292
+ <th class="py-2 px-3 font-semibold">Värde</th>
293
+ </tr>
294
+ </thead>
295
+ <tbody>
296
+ <tr
297
+ v-for="entry in sessionStorageEntries"
298
+ :key="`session-${entry.key}`"
299
+ class="border-t border-gray-200 align-top cursor-pointer hover:bg-gray-50"
300
+ @click="selectSessionEntry(entry)"
301
+ >
302
+ <td class="py-1.5 px-3 font-mono break-all">
303
+ {{ entry.key }}
304
+ </td>
305
+ <td class="py-1.5 px-3 font-mono whitespace-pre-wrap break-all">
306
+ {{ entry.value }}
307
+ </td>
308
+ </tr>
309
+ </tbody>
310
+ </table>
311
+ </div>
312
+
313
+ <div class="mt-4 space-y-2">
314
+ <div class="flex flex-col gap-1">
315
+ <label class="text-xs font-semibold">Nyckel</label>
316
+ <input
317
+ v-model="sessionEditKey"
318
+ class="border border-gray-300 rounded px-2 py-1 text-xs font-mono bg-white"
319
+ placeholder="sessionStorage-nyckel"
320
+ />
321
+ </div>
322
+ <div class="flex flex-col gap-1">
323
+ <label class="text-xs font-semibold">Värde</label>
324
+ <textarea
325
+ v-model="sessionEditValue"
326
+ rows="3"
327
+ ref="sessionTextareaRef"
328
+ class="border border-gray-300 rounded px-2 py-1 text-xs font-mono bg-white overflow-hidden"
329
+ placeholder="Värde (lagras som sträng)"
330
+ />
331
+ </div>
332
+
333
+ <div v-if="sessionEditKey" class="mt-2 flex flex-wrap gap-2">
334
+ <FdsButtonSecondary text="Ta bort" size="sm" @click="deleteSessionEntry" />
335
+ <FdsButtonPrimary text="Spara" size="sm" @click="saveSessionEntry" />
336
+ </div>
337
+ </div>
338
+ </FdsBlockInfo>
339
+
340
+ <!-- Cookies TAB -->
341
+ <FdsBlockInfo v-else-if="activeTab === 'cookies'" heading="Cookies" icon="information" size="small">
342
+ <div v-if="!cookieEntries.length" class="text-sm text-gray-600">Inga cookies satta för domänen.</div>
343
+ <div v-else class="max-h-72 overflow-auto border border-gray-200 rounded-md">
344
+ <table class="w-full text-left text-xs border-collapse">
345
+ <thead class="bg-gray-50">
346
+ <tr>
347
+ <th class="py-2 px-3 font-semibold">Namn</th>
348
+ <th class="py-2 px-3 font-semibold">Värde</th>
349
+ </tr>
350
+ </thead>
351
+ <tbody>
352
+ <tr
353
+ v-for="entry in cookieEntries"
354
+ :key="`cookie-${entry.key}`"
355
+ class="border-t border-gray-200 align-top cursor-pointer hover:bg-gray-50"
356
+ @click="selectCookieEntry(entry)"
357
+ >
358
+ <td class="py-1.5 px-3 font-mono break-all">
359
+ {{ entry.key }}
360
+ </td>
361
+ <td class="py-1.5 px-3 font-mono whitespace-pre-wrap break-all">
362
+ {{ entry.value }}
363
+ </td>
364
+ </tr>
365
+ </tbody>
366
+ </table>
367
+ </div>
368
+
369
+ <div class="mt-4 space-y-2">
370
+ <div class="flex flex-col gap-1">
371
+ <label class="text-xs font-semibold">Namn</label>
372
+ <input
373
+ v-model="cookieEditKey"
374
+ class="border border-gray-300 rounded px-2 py-1 text-xs font-mono bg-white"
375
+ placeholder="cookie-namn"
376
+ />
377
+ </div>
378
+ <div class="flex flex-col gap-1">
379
+ <label class="text-xs font-semibold">Värde</label>
380
+ <textarea
381
+ v-model="cookieEditValue"
382
+ rows="3"
383
+ ref="cookieTextareaRef"
384
+ class="border border-gray-300 rounded px-2 py-1 text-xs font-mono bg-white overflow-hidden"
385
+ placeholder="Värde (lagras som sträng)"
386
+ />
387
+ </div>
388
+
389
+ <div v-if="cookieEditKey" class="mt-2 flex flex-wrap gap-2">
390
+ <FdsButtonSecondary text="Ta bort" size="sm" @click="deleteCookieEntry" />
391
+ <FdsButtonPrimary text="Spara" size="sm" @click="saveCookieEntry" />
392
+ </div>
393
+ </div>
394
+ </FdsBlockInfo>
395
+
396
+ <!-- Placeholder tabs -->
397
+ <FdsBlockInfo
398
+ v-else
399
+ heading="Placeholder"
400
+ icon="information"
401
+ size="small"
402
+ class="min-h-[200px] flex items-center justify-center text-sm text-gray-500"
403
+ >
404
+ (Tom flik – reserverad för framtida DevMode-verktyg)
405
+ </FdsBlockInfo>
406
+
407
+ <div class="mt-4 flex justify-end gap-3">
408
+ <FdsButtonSecondary text="Uppdatera" @click="refreshStorageEntries" />
409
+ <FdsButtonPrimary text="Stäng" @click="handleOpenChange(false)" />
410
+ </div>
411
+ </div>
412
+ </FdsModal>
413
+ </template>
@@ -27,6 +27,7 @@ const props = withDefaults(defineProps<FdsInputProps>(), {
27
27
  mask: undefined,
28
28
  maskOptions: undefined,
29
29
  class: undefined,
30
+ pattern: undefined,
30
31
  modelModifiers: () => ({}),
31
32
  locale: 'sv',
32
33
  ariaLabel: undefined,
@@ -300,6 +301,7 @@ watch(
300
301
  :style="inputStyle"
301
302
  :aria-label="props.ariaLabel"
302
303
  :autocomplete="props.autocomplete"
304
+ :pattern="props.pattern"
303
305
  v-bind="inputAttrs"
304
306
  @input="handleInputChange"
305
307
  @change="handleInputChange"
@@ -21,6 +21,7 @@ export interface FdsInputProps {
21
21
  autofocus?: boolean
22
22
  readonly?: boolean
23
23
  ariaLabel?: string
24
+ pattern?: string
24
25
  onClearInput?: () => void
25
26
  maskOptions?: {
26
27
  lazy?: boolean