fds-vue-core 2.1.4 → 2.1.6

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.
Files changed (121) hide show
  1. package/components.d.ts +8 -0
  2. package/configs/tsconfig.base.json +2 -1
  3. package/dist/fds-vue-core.cjs.js +35 -15
  4. package/dist/fds-vue-core.cjs.js.map +1 -1
  5. package/dist/fds-vue-core.es.js +35 -15
  6. package/dist/fds-vue-core.es.js.map +1 -1
  7. package/dist/global-components.d.ts +35 -33
  8. package/package.json +23 -21
  9. package/src/.DS_Store +0 -0
  10. package/src/App.vue +133 -0
  11. package/src/apply.css +60 -0
  12. package/src/assets/icons.ts +517 -0
  13. package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.stories.ts +94 -0
  14. package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.vue +112 -0
  15. package/src/components/Blocks/FdsBlockAlert/types.ts +12 -0
  16. package/src/components/Blocks/FdsBlockContent/FdsBlockContent.stories.ts +110 -0
  17. package/src/components/Blocks/FdsBlockContent/FdsBlockContent.vue +66 -0
  18. package/src/components/Blocks/FdsBlockContent/types.ts +6 -0
  19. package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.stories.ts +123 -0
  20. package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.vue +87 -0
  21. package/src/components/Blocks/FdsBlockExpander/types.ts +8 -0
  22. package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.stories.ts +110 -0
  23. package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.vue +75 -0
  24. package/src/components/Blocks/FdsBlockInfo/types.ts +9 -0
  25. package/src/components/Blocks/FdsBlockLink/FdsBlockLink.css +9 -0
  26. package/src/components/Blocks/FdsBlockLink/FdsBlockLink.stories.ts +179 -0
  27. package/src/components/Blocks/FdsBlockLink/FdsBlockLink.vue +149 -0
  28. package/src/components/Blocks/FdsBlockLink/types.ts +14 -0
  29. package/src/components/Buttons/ButtonBaseProps.ts +18 -0
  30. package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.stories.ts +53 -0
  31. package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.vue +87 -0
  32. package/src/components/Buttons/FdsButtonCopy/types.ts +8 -0
  33. package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.stories.ts +111 -0
  34. package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.vue +187 -0
  35. package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.stories.ts +55 -0
  36. package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue +57 -0
  37. package/src/components/Buttons/FdsButtonIcon/types.ts +12 -0
  38. package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.stories.ts +68 -0
  39. package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.vue +126 -0
  40. package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.stories.ts +86 -0
  41. package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.vue +107 -0
  42. package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.stories.ts +68 -0
  43. package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.vue +107 -0
  44. package/src/components/FdsIcon/FdsIcon.stories.ts +69 -0
  45. package/src/components/FdsIcon/FdsIcon.vue +34 -0
  46. package/src/components/FdsIcon/types.ts +9 -0
  47. package/src/components/FdsModal/FdsModal.stories.ts +241 -0
  48. package/src/components/FdsModal/FdsModal.vue +269 -0
  49. package/src/components/FdsModal/types.ts +12 -0
  50. package/src/components/FdsPagination/FdsPagination.stories.ts +109 -0
  51. package/src/components/FdsPagination/FdsPagination.vue +193 -0
  52. package/src/components/FdsPagination/types.ts +6 -0
  53. package/src/components/FdsSearchSelect/FdsSearchSelect.stories.ts +428 -0
  54. package/src/components/FdsSearchSelect/FdsSearchSelect.vue +621 -0
  55. package/src/components/FdsSearchSelect/types.ts +25 -0
  56. package/src/components/FdsSpinner/FdsSpinner.stories.ts +31 -0
  57. package/src/components/FdsSpinner/FdsSpinner.vue +90 -0
  58. package/src/components/FdsSpinner/types.ts +6 -0
  59. package/src/components/FdsSticker/FdsSticker.stories.ts +148 -0
  60. package/src/components/FdsSticker/FdsSticker.vue +44 -0
  61. package/src/components/FdsSticker/types.ts +4 -0
  62. package/src/components/FdsTreeView/FdsTreeView.stories.ts +136 -0
  63. package/src/components/FdsTreeView/FdsTreeView.vue +162 -0
  64. package/src/components/FdsTreeView/TreeNode.vue +383 -0
  65. package/src/components/FdsTreeView/types.ts +141 -0
  66. package/src/components/FdsTreeView/useTreeState.ts +607 -0
  67. package/src/components/FdsTreeView/utils.ts +69 -0
  68. package/src/components/FdsTruncatedText/FdsTruncatedText.stories.ts +78 -0
  69. package/src/components/FdsTruncatedText/FdsTruncatedText.vue +85 -0
  70. package/src/components/FdsTruncatedText/types.ts +6 -0
  71. package/src/components/Form/FdsCheckbox/FdsCheckbox.stories.ts +275 -0
  72. package/src/components/Form/FdsCheckbox/FdsCheckbox.vue +155 -0
  73. package/src/components/Form/FdsCheckbox/types.ts +10 -0
  74. package/src/components/Form/FdsInput/FdsInput.stories.ts +319 -0
  75. package/src/components/Form/FdsInput/FdsInput.vue +233 -0
  76. package/src/components/Form/FdsInput/types.ts +25 -0
  77. package/src/components/Form/FdsRadio/FdsRadio.stories.ts +63 -0
  78. package/src/components/Form/FdsRadio/FdsRadio.vue +88 -0
  79. package/src/components/Form/FdsRadio/types.ts +12 -0
  80. package/src/components/Form/FdsSelect/FdsSelect.stories.ts +78 -0
  81. package/src/components/Form/FdsSelect/FdsSelect.vue +136 -0
  82. package/src/components/Form/FdsSelect/types.ts +13 -0
  83. package/src/components/Form/FdsTextarea/FdsTextarea.stories.ts +52 -0
  84. package/src/components/Form/FdsTextarea/FdsTextarea.vue +110 -0
  85. package/src/components/Form/FdsTextarea/types.ts +12 -0
  86. package/src/components/Table/FdsTable/FdsTable.stories.ts +221 -0
  87. package/src/components/Table/FdsTable/FdsTable.vue +25 -0
  88. package/src/components/Table/FdsTable/types.ts +4 -0
  89. package/src/components/Table/FdsTableHead/FdsTableHead.stories.ts +151 -0
  90. package/src/components/Table/FdsTableHead/FdsTableHead.vue +54 -0
  91. package/src/components/Table/FdsTableHead/types.ts +5 -0
  92. package/src/components/Tabs/FdsTabs/FdsTabs.stories.ts +247 -0
  93. package/src/components/Tabs/FdsTabs/FdsTabs.vue +27 -0
  94. package/src/components/Tabs/FdsTabs/types.ts +4 -0
  95. package/src/components/Tabs/FdsTabsItem/FdsTabsItem.vue +125 -0
  96. package/src/components/Tabs/FdsTabsItem/types.ts +16 -0
  97. package/src/components/Typography/FdsHeading/FdsHeading.stories.ts +93 -0
  98. package/src/components/Typography/FdsHeading/FdsHeading.vue +51 -0
  99. package/src/components/Typography/FdsHeading/types.ts +5 -0
  100. package/src/components/Typography/FdsListHeading/FdsListHeading.stories.ts +58 -0
  101. package/src/components/Typography/FdsListHeading/FdsListHeading.vue +62 -0
  102. package/src/components/Typography/FdsListHeading/types.ts +8 -0
  103. package/src/components/Typography/FdsSeparator/FdsSeparator.stories.ts +31 -0
  104. package/src/components/Typography/FdsSeparator/FdsSeparator.vue +5 -0
  105. package/src/components/Typography/FdsText/FdsText.stories.ts +66 -0
  106. package/src/components/Typography/FdsText/FdsText.vue +28 -0
  107. package/src/components/Typography/FdsText/types.ts +3 -0
  108. package/src/composables/useBoldQuery.ts +29 -0
  109. package/src/composables/useElementFinalSize.ts +24 -0
  110. package/src/composables/useHasSlots.ts +17 -0
  111. package/src/composables/useIsPid.ts +48 -0
  112. package/src/docs/Start/Start.mdx +12 -0
  113. package/src/docs/Usage.md +117 -0
  114. package/src/fonts.css +28 -0
  115. package/src/global-components.ts +75 -0
  116. package/src/index.ts +180 -0
  117. package/src/main.ts +7 -0
  118. package/src/slot-styles.css +93 -0
  119. package/src/style.css +89 -0
  120. package/src/tokens.css +252 -0
  121. package/dist/index.d.ts +0 -2
@@ -0,0 +1,319 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import FdsInput from './FdsInput.vue'
4
+
5
+ const meta: Meta<typeof FdsInput> = {
6
+ title: 'FDS/Form/FdsInput',
7
+ component: FdsInput,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ value: {
11
+ control: { type: 'text' },
12
+ description: 'Input value (use v-model for two-way binding)',
13
+ },
14
+ label: {
15
+ control: { type: 'text' },
16
+ description: 'Label text displayed above the input',
17
+ },
18
+ disabled: {
19
+ control: { type: 'boolean' },
20
+ description: 'Disables the input field',
21
+ },
22
+ valid: {
23
+ control: { type: 'select' },
24
+ options: ['true', 'false', 'null'],
25
+ description: 'Validation state: "true" for valid, "false" for invalid, "null" for neutral',
26
+ },
27
+ optional: {
28
+ control: { type: 'boolean' },
29
+ description: 'Marks the field as optional (hides invalid message if true)',
30
+ },
31
+ invalidMessage: {
32
+ control: { type: 'text' },
33
+ description: 'Error message displayed when validation is false',
34
+ },
35
+ required: {
36
+ control: { type: 'boolean' },
37
+ description: 'Marks the field as required',
38
+ },
39
+ labelLeft: {
40
+ control: { type: 'boolean' },
41
+ description: 'If true, positions the label to the left of the input',
42
+ },
43
+ meta: {
44
+ control: { type: 'text' },
45
+ description: 'Meta text displayed below the label',
46
+ },
47
+ type: {
48
+ control: { type: 'select' },
49
+ options: ['text', 'search', 'email', 'password'],
50
+ description: 'Input type',
51
+ },
52
+ clearButton: {
53
+ control: { type: 'boolean' },
54
+ description: 'Shows a clear button when input has a value',
55
+ },
56
+ name: {
57
+ control: { type: 'text' },
58
+ description: 'HTML name attribute for the input',
59
+ },
60
+ id: {
61
+ control: { type: 'text' },
62
+ description: 'HTML id attribute for the input',
63
+ },
64
+ passwordLabels: {
65
+ control: { type: 'object' },
66
+ description: 'Array with [showLabel, hideLabel] for password toggle button',
67
+ },
68
+ locale: {
69
+ control: { type: 'select' },
70
+ options: ['sv', 'en'],
71
+ description: 'Locale for translations ("sv" for Swedish, "en" for English)',
72
+ },
73
+ size: {
74
+ control: { type: 'number' },
75
+ description: 'HTML size attribute for the input',
76
+ },
77
+ maxlength: {
78
+ control: { type: 'number' },
79
+ description: 'Maximum length of the input value',
80
+ },
81
+ mask: {
82
+ control: { type: 'text' },
83
+ description: 'IMask pattern string (e.g., "00000000-0000" for personnummer)',
84
+ },
85
+ maskOptions: {
86
+ control: { type: 'object' },
87
+ description: 'Additional options for IMask (e.g., { lazy: true, placeholderChar: "" })',
88
+ },
89
+ },
90
+ args: {
91
+ value: '',
92
+ label: 'Label',
93
+ labelLeft: false,
94
+ meta: '',
95
+ valid: 'null',
96
+ optional: false,
97
+ invalidMessage: '',
98
+ clearButton: false,
99
+ disabled: false,
100
+ required: false,
101
+ type: 'text',
102
+ passwordLabels: undefined,
103
+ locale: 'sv',
104
+ size: undefined,
105
+ maxlength: undefined,
106
+ mask: undefined,
107
+ maskOptions: undefined,
108
+ },
109
+ }
110
+
111
+ export default meta
112
+ type Story = StoryObj<typeof meta>
113
+
114
+ export const Basic: Story = {
115
+ render: (args) => ({
116
+ components: { FdsInput },
117
+ setup: () => ({ args }),
118
+ template: '<FdsInput v-bind="args" />',
119
+ }),
120
+ args: {
121
+ label: 'Namn',
122
+ meta: 'Ange ditt fullständiga namn',
123
+ },
124
+ }
125
+
126
+ export const WithVModel: Story = {
127
+ render: () => ({
128
+ components: { FdsInput },
129
+ setup() {
130
+ const text = ref('')
131
+ return { text }
132
+ },
133
+ template: `
134
+ <div>
135
+ <FdsInput v-model="text" label="Sök" placeholder="Sök..." clearButton />
136
+ <p class="mt-4">Värde: {{ text }}</p>
137
+ </div>
138
+ `,
139
+ }),
140
+ }
141
+
142
+ export const WithMeta: Story = {
143
+ render: (args) => ({
144
+ components: { FdsInput },
145
+ setup: () => ({ args }),
146
+ template: '<FdsInput v-bind="args" meta="Detta är hjälptext som visas under labeln" />',
147
+ }),
148
+ args: {
149
+ label: 'E-postadress',
150
+ },
151
+ }
152
+
153
+ export const WithValidation: Story = {
154
+ render: () => ({
155
+ components: { FdsInput },
156
+ setup() {
157
+ const email = ref('')
158
+ const valid = ref<string>('null')
159
+
160
+ const validateEmail = (value: string) => {
161
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
162
+ if (value.length === 0) {
163
+ valid.value = 'null'
164
+ } else if (emailRegex.test(value)) {
165
+ valid.value = 'true'
166
+ } else {
167
+ valid.value = 'false'
168
+ }
169
+ }
170
+
171
+ return { email, valid, validateEmail }
172
+ },
173
+ template: `
174
+ <FdsInput
175
+ v-model="email"
176
+ label="E-postadress"
177
+ type="email"
178
+ :valid="valid"
179
+ invalid-message="Ange en giltig e-postadress"
180
+ @input="validateEmail(email)"
181
+ />
182
+ `,
183
+ }),
184
+ }
185
+
186
+ export const Disabled: Story = {
187
+ render: (args) => ({
188
+ components: { FdsInput },
189
+ setup: () => ({ args }),
190
+ template: '<FdsInput v-bind="args" />',
191
+ }),
192
+ args: {
193
+ label: 'Inaktiverat fält',
194
+ value: 'Detta värde kan inte ändras',
195
+ disabled: true,
196
+ },
197
+ }
198
+
199
+ export const Required: Story = {
200
+ render: (args) => ({
201
+ components: { FdsInput },
202
+ setup: () => ({ args }),
203
+ template: '<FdsInput v-bind="args" />',
204
+ }),
205
+ args: {
206
+ label: 'Obligatoriskt fält',
207
+ required: true,
208
+ invalidMessage: 'Detta fält är obligatoriskt',
209
+ },
210
+ }
211
+
212
+ export const WithLabelLeft: Story = {
213
+ render: (args) => ({
214
+ components: { FdsInput },
215
+ setup: () => ({ args }),
216
+ template: '<FdsInput v-bind="args" />',
217
+ }),
218
+ args: {
219
+ label: 'Label till vänster',
220
+ labelLeft: true,
221
+ meta: 'Meta-text visas här',
222
+ },
223
+ }
224
+
225
+ export const Password: Story = {
226
+ render: () => ({
227
+ components: { FdsInput },
228
+ setup() {
229
+ const password = ref('')
230
+ return { password }
231
+ },
232
+ template: `
233
+ <FdsInput
234
+ v-model="password"
235
+ label="Lösenord"
236
+ type="password"
237
+ meta="Minst 8 tecken"
238
+ />
239
+ `,
240
+ }),
241
+ }
242
+
243
+ export const Search: Story = {
244
+ render: () => ({
245
+ components: { FdsInput },
246
+ setup() {
247
+ const searchTerm = ref('')
248
+ return { searchTerm }
249
+ },
250
+ template: `
251
+ <FdsInput
252
+ v-model="searchTerm"
253
+ label="Sök"
254
+ type="search"
255
+ clearButton
256
+ placeholder="Sök efter..."
257
+ />
258
+ `,
259
+ }),
260
+ }
261
+
262
+ export const WithMask: Story = {
263
+ render: () => ({
264
+ components: { FdsInput },
265
+ setup() {
266
+ const personnummer = ref('')
267
+ return { personnummer }
268
+ },
269
+ template: `
270
+ <div>
271
+ <FdsInput
272
+ v-model="personnummer"
273
+ label="Personnummer"
274
+ mask="00000000-0000"
275
+ :maskOptions="{ lazy: true }"
276
+ meta="Format: yyyymmdd-nnnn"
277
+ />
278
+ <p class="mt-4">Värde: {{ personnummer }}</p>
279
+ </div>
280
+ `,
281
+ }),
282
+ }
283
+
284
+ export const WithMaskPhone: Story = {
285
+ render: () => ({
286
+ components: { FdsInput },
287
+ setup() {
288
+ const phone = ref('')
289
+ return { phone }
290
+ },
291
+ template: `
292
+ <div>
293
+ <FdsInput
294
+ v-model="phone"
295
+ label="Telefonnummer"
296
+ mask="+46 (000) 000-00-00"
297
+ :maskOptions="{ lazy: true }"
298
+ />
299
+ <p class="mt-4">Värde: {{ phone }}</p>
300
+ </div>
301
+ `,
302
+ }),
303
+ }
304
+
305
+ export const AllStates: Story = {
306
+ render: () => ({
307
+ components: { FdsInput },
308
+ setup: () => ({}),
309
+ template: `
310
+ <div class="space-y-6">
311
+ <FdsInput label="Neutral" value="Neutralt tillstånd" />
312
+ <FdsInput label="Valid" value="Giltigt värde" valid="true" />
313
+ <FdsInput label="Invalid" value="Ogiltigt värde" valid="false" invalid-message="Detta är ett felmeddelande" />
314
+ <FdsInput label="Disabled" value="Inaktiverat" disabled />
315
+ <FdsInput label="Required" required invalid-message="Detta fält är obligatoriskt" />
316
+ </div>
317
+ `,
318
+ }),
319
+ }
@@ -0,0 +1,233 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ // @ts-ignore - IMask types may not be resolved in build
4
+ import IMask from 'imask'
5
+ import FdsButtonIcon from '@/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue'
6
+ import FdsButtonMinor from '@/components/Buttons/FdsButtonMinor/FdsButtonMinor.vue'
7
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
8
+ import type { FdsInputProps } from './types'
9
+
10
+ // Support both v-model (modelValue) and :value prop for backward compatibility
11
+ const modelValue = defineModel<string>({ default: undefined, required: false })
12
+
13
+ const props = withDefaults(defineProps<FdsInputProps>(), {
14
+ value: undefined,
15
+ label: undefined,
16
+ disabled: false,
17
+ valid: undefined,
18
+ optional: false,
19
+ required: false,
20
+ labelLeft: false,
21
+ meta: undefined,
22
+ type: 'text',
23
+ passwordLabels: undefined,
24
+ clearButton: false,
25
+ name: undefined,
26
+ id: undefined,
27
+ mask: undefined,
28
+ maskOptions: undefined,
29
+ })
30
+
31
+ const emit = defineEmits<{
32
+ (e: 'input', ev: Event): void
33
+ (e: 'clearInput'): void
34
+ (e: 'update:value', value: string): void
35
+ }>()
36
+
37
+ const autoId = `fds-input-${Math.random().toString(36).slice(2, 9)}`
38
+ const inputId = computed(() => props.id ?? autoId)
39
+
40
+ const clearButtonLabel = computed(() => (props.locale === 'sv' ? 'Rensa input' : 'Clear input'))
41
+
42
+ const showInvalidMessage = computed(() => props.valid === 'false' && !props.optional && props.invalidMessage)
43
+ const isInvalid = computed(() => props.valid === 'false' && !props.optional && !props.disabled)
44
+ const isValid = computed(() => props.valid === 'true')
45
+ const showPasswordToggle = computed(() => isPasswordType.value && internalValue.value.length > 0)
46
+
47
+ const inputClasses = computed(() => [
48
+ 'block w-full rounded-md border border-gray-500 px-3 py-[calc(0.75rem-1px)] mb-6',
49
+ 'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
50
+ props.disabled ? 'outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent' : 'bg-white',
51
+ isInvalid.value && 'outline-2 outline-red-600',
52
+ ])
53
+
54
+ const validationIconClasses = computed(() => [
55
+ 'absolute flex gap-2 right-4 top-1/2 -translate-y-1/2 flex items-center justify-end',
56
+ ])
57
+
58
+ // Use modelValue if bound via v-model, otherwise use value prop
59
+ const internalValue = computed({
60
+ get: () =>
61
+ // If modelValue is explicitly set (via v-model), use it
62
+ // Otherwise fall back to value prop
63
+ modelValue.value !== undefined ? modelValue.value : (props.value ?? ''),
64
+ set: (newValue: string) => {
65
+ // Update modelValue if it's being used
66
+ if (modelValue.value !== undefined) {
67
+ modelValue.value = newValue
68
+ }
69
+ // Always emit update:value for backward compatibility
70
+ emit('update:value', newValue)
71
+ },
72
+ })
73
+
74
+ function onClear() {
75
+ internalValue.value = ''
76
+ emit('clearInput')
77
+ }
78
+
79
+ const showPassword = ref(false)
80
+ const isPasswordType = computed(() => props.type === 'password')
81
+
82
+ function toggleShowPassword() {
83
+ showPassword.value = !showPassword.value
84
+ }
85
+
86
+ // IMask setup
87
+ const inputRef = ref<HTMLInputElement | null>(null)
88
+
89
+ let maskInstance: any = null
90
+
91
+ const createMask = () => {
92
+ // Destroy existing mask if any
93
+ if (maskInstance) {
94
+ maskInstance.destroy()
95
+ maskInstance = null
96
+ }
97
+
98
+ // Create new mask if mask prop is provided and input exists
99
+ if (props.mask && inputRef.value) {
100
+ const maskConfig = {
101
+ mask: props.mask,
102
+ ...props.maskOptions,
103
+ }
104
+
105
+ maskInstance = IMask(inputRef.value, maskConfig as any)
106
+
107
+ // Sync mask value with internalValue
108
+ if (internalValue.value) {
109
+ maskInstance.value = internalValue.value
110
+ }
111
+
112
+ // Update internalValue when mask value changes
113
+ maskInstance.on('accept', () => {
114
+ if (maskInstance) {
115
+ internalValue.value = maskInstance.value
116
+ }
117
+ })
118
+ }
119
+ }
120
+
121
+ onMounted(() => {
122
+ nextTick(() => {
123
+ createMask()
124
+ })
125
+ })
126
+
127
+ onBeforeUnmount(() => {
128
+ if (maskInstance) {
129
+ maskInstance.destroy()
130
+ maskInstance = null
131
+ }
132
+ })
133
+
134
+ // Watch for mask prop changes and recreate mask
135
+ watch(
136
+ () => props.mask,
137
+ () => {
138
+ nextTick(() => {
139
+ createMask()
140
+ })
141
+ },
142
+ )
143
+
144
+ // Watch for external value changes and update mask
145
+ watch(
146
+ () => props.value,
147
+ (newValue) => {
148
+ if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
149
+ maskInstance.value = newValue
150
+ }
151
+ },
152
+ )
153
+
154
+ watch(
155
+ () => modelValue.value,
156
+ (newValue) => {
157
+ if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
158
+ maskInstance.value = newValue
159
+ }
160
+ },
161
+ )
162
+ </script>
163
+
164
+ <template>
165
+ <div class="w-full mb-4">
166
+ <div :class="{ 'flex flex-row gap-4': labelLeft }">
167
+ <div>
168
+ <label
169
+ v-if="label"
170
+ :for="inputId"
171
+ class="block font-bold text-gray-900 cursor-pointer"
172
+ :class="{ 'mb-0': meta, 'mb-1': !meta }"
173
+ >{{ label }}</label
174
+ >
175
+ <div
176
+ v-if="meta"
177
+ class="font-thin"
178
+ :class="{ 'mb-1': !labelLeft }"
179
+ >
180
+ {{ meta }}
181
+ </div>
182
+ </div>
183
+ <div :class="{ 'flex-1': labelLeft }">
184
+ <div class="relative">
185
+ <input
186
+ ref="inputRef"
187
+ :id="inputId"
188
+ :name="name || undefined"
189
+ :type="isPasswordType ? (showPassword ? 'text' : 'password') : type"
190
+ :disabled="disabled"
191
+ :required="required"
192
+ :size="size as any"
193
+ :maxlength="maxlength as any"
194
+ v-model="internalValue"
195
+ :aria-invalid="valid === 'false' ? 'true' : undefined"
196
+ :class="inputClasses"
197
+ v-bind="$attrs"
198
+ @input="$emit('input', $event)"
199
+ />
200
+ <div :class="validationIconClasses">
201
+ <FdsIcon
202
+ v-if="isInvalid"
203
+ name="alert"
204
+ class="fill-red-600"
205
+ />
206
+ <FdsIcon
207
+ v-if="isValid"
208
+ name="bigSuccess"
209
+ />
210
+ <FdsButtonIcon
211
+ v-if="clearButton && !!internalValue && !disabled"
212
+ icon="cross"
213
+ :ariaLabel="clearButtonLabel"
214
+ @click="onClear"
215
+ />
216
+ <FdsButtonMinor
217
+ v-if="showPasswordToggle"
218
+ :icon="showPassword ? 'viewOff' : 'viewOn'"
219
+ text=""
220
+ @click="toggleShowPassword"
221
+ />
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ <div
227
+ v-if="showInvalidMessage"
228
+ class="text-red-600 font-bold mt-1"
229
+ >
230
+ {{ invalidMessage }}
231
+ </div>
232
+ </div>
233
+ </template>
@@ -0,0 +1,25 @@
1
+ export interface FdsInputProps {
2
+ value?: string
3
+ label?: string
4
+ disabled?: boolean
5
+ valid?: string
6
+ optional?: boolean
7
+ invalidMessage?: string
8
+ required?: boolean
9
+ labelLeft?: boolean
10
+ meta?: string
11
+ type?: 'text' | 'search' | 'email' | 'password'
12
+ clearButton?: boolean
13
+ name?: string
14
+ id?: string
15
+ passwordLabels?: string[]
16
+ locale?: 'sv' | 'en'
17
+ size?: number | string
18
+ maxlength?: number
19
+ mask?: string | RegExp | Array<string | RegExp>
20
+ maskOptions?: {
21
+ lazy?: boolean
22
+ placeholderChar?: string
23
+ [key: string]: unknown
24
+ }
25
+ }
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref, watch } from 'vue'
3
+ import FdsRadio from './FdsRadio.vue'
4
+
5
+ const meta: Meta<typeof FdsRadio> = {
6
+ title: 'FDS/Form/FdsRadio',
7
+ component: FdsRadio,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ label: { control: 'text' },
11
+ checked: { control: 'boolean' },
12
+ disabled: { control: 'boolean' },
13
+ name: { control: 'text' },
14
+ value: { control: 'text' },
15
+ id: { control: 'text' },
16
+ },
17
+ args: {
18
+ label: 'Radio label',
19
+ checked: false,
20
+ disabled: false,
21
+ name: 'group1',
22
+ value: 'optionA',
23
+ id: undefined,
24
+ },
25
+ }
26
+
27
+ export default meta
28
+ type Story = StoryObj<typeof meta>
29
+
30
+ export const Default: Story = {
31
+ render: (args) => ({
32
+ components: { FdsRadio },
33
+ setup() {
34
+ const checked = ref(args.checked)
35
+ watch(
36
+ () => args.checked,
37
+ (v) => (checked.value = v),
38
+ )
39
+ return { args, checked }
40
+ },
41
+ template: `
42
+ <FdsRadio v-bind="args" v-model:checked="checked" />
43
+ `,
44
+ }),
45
+ }
46
+
47
+ export const Group: Story = {
48
+ render: () => ({
49
+ components: { FdsRadio },
50
+ setup() {
51
+ const selected = ref('a')
52
+ return { selected }
53
+ },
54
+ template: `
55
+ <div class="space-y-2">
56
+ <FdsRadio name="g1" value="a" v-model:checked="selected === 'a'" @change="(v) => selected = v ? 'a' : selected">Alternative A</FdsRadio>
57
+ <FdsRadio name="g1" value="b" v-model:checked="selected === 'b'" @change="(v) => selected = v ? 'b' : selected">Alternative B</FdsRadio>
58
+ <FdsRadio name="g1" value="c" v-model:checked="selected === 'c'" @change="(v) => selected = v ? 'c' : selected">Alternative C</FdsRadio>
59
+ <div>Selected: {{ selected }}</div>
60
+ </div>
61
+ `,
62
+ }),
63
+ }