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,88 @@
1
+ <script setup lang="ts">
2
+ import { useHasSlot } from '@/composables/useHasSlots'
3
+ import { computed } from 'vue'
4
+ import type { FdsRadioProps } from './types'
5
+
6
+ // Support v-model for selected value in a radio group (string|number)
7
+ const modelValue = defineModel<string | number>({ default: undefined, required: false })
8
+
9
+ const props = withDefaults(defineProps<FdsRadioProps>(), {
10
+ label: undefined,
11
+ checked: false,
12
+ disabled: false,
13
+ value: undefined,
14
+ name: undefined,
15
+ id: undefined,
16
+ required: false,
17
+ })
18
+
19
+ const emit = defineEmits<{
20
+ (e: 'update:modelValue', value: string | number): void
21
+ (e: 'change', value: string | number): void
22
+ (e: 'input', value: string | number): void
23
+ }>()
24
+
25
+ const autoId = `fds-radio-${Math.random().toString(36).slice(2, 9)}`
26
+ const inputId = computed(() => props.id ?? autoId)
27
+
28
+ const hasLabelSlot = useHasSlot()
29
+
30
+ // Proxy for v-model on radio groups: binds the selected value (string|number)
31
+ const radioModel = computed({
32
+ get() {
33
+ return modelValue.value
34
+ },
35
+ set(newChecked: string | number) {
36
+ emit('update:modelValue', newChecked)
37
+ emit('change', newChecked)
38
+ emit('input', newChecked)
39
+ },
40
+ })
41
+
42
+ const wrapperClasses = computed(() => ['block relative flex items-center mb-2 last:mb-0'])
43
+ const innerWrapperClasses = computed(() => [
44
+ 'flex p-0.5 items-start rounded-l-xl rounded-r-md',
45
+ 'hover:bg-blue_t-100 active:bg-blue_t-200',
46
+ '[&:has(:focus-visible)]:outline-2 [&:has(:focus-visible)]:outline-dashed [&:has(:focus-visible)]:-outline-offset-2 [&:has(:focus-visible)]:outline-blue-500',
47
+ props.disabled && 'hover:bg-transparent active:bg-transparent',
48
+ ])
49
+
50
+ const inputClasses = computed(() => [
51
+ 'peer z-2 bg-white min-w-[20px] min-h-[20px] focus-visible:outline-none rounded-full accent-blue-500',
52
+ props.disabled && 'cursor-not-allowed',
53
+ ])
54
+ </script>
55
+
56
+ <template>
57
+ <div :class="wrapperClasses">
58
+ <label
59
+ :for="inputId"
60
+ :class="[innerWrapperClasses, { 'cursor-not-allowed': disabled }]"
61
+ v-bind="$attrs"
62
+ >
63
+ <input
64
+ :id="inputId"
65
+ :name="name"
66
+ :value="value"
67
+ v-model="radioModel"
68
+ :disabled="disabled"
69
+ :required="required"
70
+ type="radio"
71
+ :class="inputClasses"
72
+ class="m-[2px]"
73
+ />
74
+ <span
75
+ v-if="hasLabelSlot || label"
76
+ class="relative inline-block leading-6 pl-1 select-none"
77
+ :class="{ 'cursor-not-allowed': disabled }"
78
+ >
79
+ <template v-if="hasLabelSlot">
80
+ <slot></slot>
81
+ </template>
82
+ <template v-else-if="label">
83
+ {{ label }}
84
+ </template>
85
+ </span>
86
+ </label>
87
+ </div>
88
+ </template>
@@ -0,0 +1,12 @@
1
+ export interface FdsRadioProps {
2
+ label?: string
3
+ checked?: boolean
4
+ disabled?: boolean
5
+ value?: string | number
6
+ name?: string
7
+ id?: string
8
+ required?: boolean
9
+ onKeydown?: ((event: KeyboardEvent) => void) | Array<(event: KeyboardEvent) => void>
10
+ onBlur?: ((event: FocusEvent) => void) | Array<(event: FocusEvent) => void>
11
+ onChange?: ((event: Event) => void) | Array<(event: Event) => void>
12
+ }
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import FdsSelect from './FdsSelect.vue'
3
+
4
+ const meta: Meta<typeof FdsSelect> = {
5
+ title: 'FDS/Form/FdsSelect',
6
+ component: FdsSelect,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ modelValue: { control: { type: 'text' } },
10
+ label: { control: { type: 'text' } },
11
+ meta: { control: { type: 'text' } },
12
+ disabled: { control: { type: 'boolean' } },
13
+ optional: { control: { type: 'boolean' } },
14
+ valid: { control: { type: 'select' }, options: ['true', 'false', 'null'] },
15
+ invalidMessage: { control: { type: 'text' } },
16
+ placeholder: { control: { type: 'text' } },
17
+ },
18
+ args: {
19
+ modelValue: '',
20
+ label: 'Välj alternativ',
21
+ meta: undefined,
22
+ disabled: false,
23
+ optional: false,
24
+ valid: 'null',
25
+ invalidMessage: undefined,
26
+ placeholder: undefined,
27
+ options: [
28
+ { value: 'option1', label: 'Alternativ 1' },
29
+ { value: 'option2', label: 'Alternativ 2' },
30
+ { value: 'option3', label: 'Alternativ 3', disabled: true },
31
+ { value: 'option4', label: 'Alternativ 4' },
32
+ ],
33
+ },
34
+ }
35
+
36
+ export default meta
37
+ type Story = StoryObj<typeof meta>
38
+
39
+ export const Default: Story = {
40
+ render: (args) => ({
41
+ components: { FdsSelect },
42
+ setup: () => ({ args }),
43
+ template: `<FdsSelect v-bind="args" />`,
44
+ }),
45
+ }
46
+
47
+ export const Disabled: Story = {
48
+ args: { disabled: true },
49
+ render: (args) => ({
50
+ components: { FdsSelect },
51
+ setup: () => ({ args }),
52
+ template: `<FdsSelect v-bind="args" />`,
53
+ }),
54
+ }
55
+
56
+ export const Invalid: Story = {
57
+ args: {
58
+ valid: 'false',
59
+ invalidMessage: 'Välj ett giltigt alternativ',
60
+ },
61
+ render: (args) => ({
62
+ components: { FdsSelect },
63
+ setup: () => ({ args }),
64
+ template: `<FdsSelect v-bind="args" />`,
65
+ }),
66
+ }
67
+
68
+ export const Valid: Story = {
69
+ args: {
70
+ valid: 'true',
71
+ modelValue: 'option1',
72
+ },
73
+ render: (args) => ({
74
+ components: { FdsSelect },
75
+ setup: () => ({ args }),
76
+ template: `<FdsSelect v-bind="args" />`,
77
+ }),
78
+ }
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ import { computed, useSlots } from 'vue'
3
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
4
+ import type { FdsSelectProps } from './types'
5
+
6
+ // Support both v-model (modelValue) and :value prop for backward compatibility
7
+ const modelValue = defineModel<string>({ default: undefined, required: false })
8
+
9
+ const props = withDefaults(defineProps<FdsSelectProps>(), {
10
+ value: undefined,
11
+ label: undefined,
12
+ meta: undefined,
13
+ disabled: false,
14
+ optional: false,
15
+ valid: undefined,
16
+ invalidMessage: undefined,
17
+ name: undefined,
18
+ id: undefined,
19
+ placeholder: undefined,
20
+ options: undefined,
21
+ })
22
+
23
+ const emit = defineEmits<{
24
+ (e: 'input', ev: Event): void
25
+ (e: 'change', ev: Event): void
26
+ (e: 'update:value', value: string): void
27
+ }>()
28
+
29
+ const autoId = `fds-select-${Math.random().toString(36).slice(2, 9)}`
30
+ const selectId = computed(() => props.id ?? autoId)
31
+
32
+ const slots = useSlots()
33
+ const hasDefaultSlot = computed(() => !!slots.default)
34
+
35
+ const showInvalidMessage = computed(
36
+ () => props.valid === 'false' && !props.optional && props.invalidMessage && !props.disabled,
37
+ )
38
+ const isInvalid = computed(() => props.valid === 'false' && !props.optional && !props.disabled)
39
+
40
+ const selectClasses = computed(() => [
41
+ 'block w-full rounded-md border border-gray-500 px-3 py-2 pr-8 h-12 mb-4',
42
+ 'focus:outline-2 focus:outline-blue-500 focus:border-transparent',
43
+ 'appearance-none',
44
+ props.disabled
45
+ ? 'outline-dashed text-gray-600 outline-2 -outline-offset-2 outline-gray-400 cursor-not-allowed border-transparent'
46
+ : 'bg-white cursor-pointer',
47
+ isInvalid.value && !props.disabled && 'outline-2 -outline-offset-2 outline-red-600 text-red-600',
48
+ ])
49
+
50
+ // Use modelValue if bound via v-model, otherwise use value prop
51
+ const internalValue = computed({
52
+ get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
53
+ set: (newValue: string) => {
54
+ // Update modelValue if it's being used
55
+ if (modelValue.value !== undefined) {
56
+ modelValue.value = newValue
57
+ }
58
+ // Always emit update:value for backward compatibility
59
+ emit('update:value', newValue)
60
+ },
61
+ })
62
+
63
+ function handleChange(ev: Event) {
64
+ emit('change', ev)
65
+ emit('input', ev)
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <div class="w-full mb-4">
71
+ <label
72
+ v-if="label"
73
+ :for="selectId"
74
+ class="block font-bold text-gray-900 cursor-pointer"
75
+ :class="{ 'mb-0': meta, 'mb-1': !meta }"
76
+ >{{ label }}</label
77
+ >
78
+ <div
79
+ v-if="meta"
80
+ class="font-thin mb-1"
81
+ >
82
+ {{ meta }}
83
+ </div>
84
+ <div class="relative">
85
+ <select
86
+ :id="selectId"
87
+ :name="name || undefined"
88
+ :disabled="disabled"
89
+ v-model="internalValue"
90
+ :aria-invalid="valid === 'false' ? 'true' : undefined"
91
+ :class="selectClasses"
92
+ v-bind="$attrs"
93
+ @change="handleChange"
94
+ @input="(e) => emit('input', e)"
95
+ >
96
+ <option
97
+ v-if="placeholder && !hasDefaultSlot"
98
+ value=""
99
+ disabled
100
+ >
101
+ {{ placeholder }}
102
+ </option>
103
+ <!-- Render options from prop if no slot is provided -->
104
+ <template v-if="!hasDefaultSlot && options">
105
+ <option
106
+ v-for="option in options"
107
+ :key="option.value"
108
+ :value="option.value"
109
+ :disabled="option.disabled"
110
+ >
111
+ {{ option.label }}
112
+ </option>
113
+ </template>
114
+ <!-- Render slot content (supports optgroups and custom options) -->
115
+ <slot v-else />
116
+ </select>
117
+ <div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
118
+ <FdsIcon
119
+ name="arrowDown"
120
+ :size="24"
121
+ :class="{
122
+ 'fill-gray-500': disabled,
123
+ 'fill-red-500': isInvalid && !disabled,
124
+ 'fill-blue-500': !disabled && !isInvalid,
125
+ }"
126
+ />
127
+ </div>
128
+ </div>
129
+ <div
130
+ v-if="showInvalidMessage"
131
+ class="text-red-600 font-bold mt-1"
132
+ >
133
+ {{ invalidMessage }}
134
+ </div>
135
+ </div>
136
+ </template>
@@ -0,0 +1,13 @@
1
+ export interface FdsSelectProps {
2
+ value?: string
3
+ label?: string
4
+ meta?: string
5
+ disabled?: boolean
6
+ optional?: boolean
7
+ valid?: 'true' | 'false' | 'null'
8
+ invalidMessage?: string
9
+ name?: string
10
+ id?: string
11
+ placeholder?: string
12
+ options?: { value: string; label: string; disabled?: boolean }[]
13
+ }
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import FdsTextarea from './FdsTextarea.vue'
3
+
4
+ const meta: Meta<typeof FdsTextarea> = {
5
+ title: 'FDS/Form/FdsTextarea',
6
+ component: FdsTextarea,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ modelValue: { control: { type: 'text' } },
10
+ label: { control: { type: 'text' } },
11
+ meta: { control: { type: 'text' } },
12
+ disabled: { control: { type: 'boolean' } },
13
+ optional: { control: { type: 'boolean' } },
14
+ valid: { control: { type: 'select' }, options: ['true', 'false', 'null'] },
15
+ invalidMessage: { control: { type: 'text' } },
16
+ rows: { control: { type: 'number', min: 1, step: 1 } },
17
+ id: { control: { type: 'text' } },
18
+ name: { control: { type: 'text' } },
19
+ },
20
+ args: {
21
+ modelValue: '',
22
+ label: 'Beskrivning',
23
+ meta: '',
24
+ disabled: false,
25
+ optional: false,
26
+ valid: 'null',
27
+ invalidMessage: undefined,
28
+ rows: 4,
29
+ id: '',
30
+ name: '',
31
+ },
32
+ }
33
+
34
+ export default meta
35
+ type Story = StoryObj<typeof meta>
36
+
37
+ export const Default: Story = {
38
+ render: (args) => ({
39
+ components: { FdsTextarea },
40
+ setup: () => ({ args }),
41
+ template: `<FdsTextarea v-bind="args" />`,
42
+ }),
43
+ }
44
+
45
+ export const Disabled: Story = {
46
+ args: { disabled: true },
47
+ render: (args) => ({
48
+ components: { FdsTextarea },
49
+ setup: () => ({ args }),
50
+ template: `<FdsTextarea v-bind="args" />`,
51
+ }),
52
+ }
@@ -0,0 +1,110 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
4
+ import type { FdsTextareaProps } from './types'
5
+
6
+ // Support both v-model (modelValue) and :value prop for backward compatibility
7
+ const modelValue = defineModel<string>({ default: undefined, required: false })
8
+
9
+ const props = withDefaults(defineProps<FdsTextareaProps>(), {
10
+ value: undefined,
11
+ label: undefined,
12
+ meta: undefined,
13
+ disabled: false,
14
+ optional: false,
15
+ valid: undefined,
16
+ rows: 4,
17
+ name: undefined,
18
+ id: undefined,
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ (e: 'input', ev: Event): void
23
+ (e: 'update:value', value: string): void
24
+ }>()
25
+
26
+ const autoId = `fds-textarea-${Math.random().toString(36).slice(2, 9)}`
27
+ const textareaId = computed(() => props.id ?? autoId)
28
+
29
+ const showInvalidMessage = computed(() => props.valid === 'false' && !props.optional && props.invalidMessage)
30
+ const isInvalid = computed(() => props.valid === 'false' && !props.optional && !props.disabled)
31
+ const isValid = computed(() => props.valid === 'true')
32
+
33
+ const inputClasses = computed(() => [
34
+ 'block w-full rounded-md border border-gray-500 px-3 py-2',
35
+ 'focus:outline-2 focus:outline-blue-500 focus:border-transparent',
36
+ props.disabled
37
+ ? 'outline-dashed outline-2 -outline-offset-2 outline-gray-400 cursor-not-allowed border-transparent bg-gray-50'
38
+ : 'bg-white',
39
+ isInvalid.value && 'outline-2 -outline-offset-2 outline-red-600',
40
+ ])
41
+
42
+ const validationIconClasses = computed(() => ['absolute right-3 top-3 flex items-center gap-2 pointer-events-none'])
43
+
44
+ // Use modelValue if bound via v-model, otherwise use value prop
45
+ const internalValue = computed({
46
+ get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
47
+ set: (newValue: string) => {
48
+ // Update modelValue if it's being used
49
+ if (modelValue.value !== undefined) {
50
+ modelValue.value = newValue
51
+ }
52
+ // Always emit update:value for backward compatibility
53
+ emit('update:value', newValue)
54
+ },
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <div class="w-full mb-4">
60
+ <label
61
+ v-if="label"
62
+ :for="textareaId"
63
+ class="block font-bold text-gray-900 cursor-pointer"
64
+ :class="{ 'mb-0': meta, 'mb-1': !meta }"
65
+ >{{ label }}</label
66
+ >
67
+ <div
68
+ v-if="meta"
69
+ class="font-thin mb-1"
70
+ >
71
+ {{ meta }}
72
+ </div>
73
+ <div class="relative">
74
+ <textarea
75
+ :id="textareaId"
76
+ :name="name || undefined"
77
+ :disabled="disabled"
78
+ :rows="rows"
79
+ v-model="internalValue"
80
+ :aria-invalid="valid === 'false' ? 'true' : undefined"
81
+ :class="inputClasses"
82
+ v-bind="$attrs"
83
+ @input="(e) => emit('input', e)"
84
+ />
85
+ <div :class="validationIconClasses">
86
+ <FdsIcon
87
+ v-if="isInvalid"
88
+ name="alert"
89
+ class="fill-red-600"
90
+ />
91
+ <FdsIcon
92
+ v-if="isValid"
93
+ name="bigSuccess"
94
+ />
95
+ </div>
96
+ </div>
97
+ <div
98
+ v-if="showInvalidMessage"
99
+ class="text-red-600 font-bold mt-1"
100
+ >
101
+ {{ invalidMessage }}
102
+ </div>
103
+ </div>
104
+ <div
105
+ v-if="isValid"
106
+ class="sr-only"
107
+ >
108
+ OK
109
+ </div>
110
+ </template>
@@ -0,0 +1,12 @@
1
+ export type FdsTextareaProps = {
2
+ value?: string
3
+ label?: string
4
+ meta?: string
5
+ disabled?: boolean
6
+ optional?: boolean
7
+ valid?: 'true' | 'false' | 'null'
8
+ invalidMessage?: string
9
+ rows?: number
10
+ name?: string
11
+ id?: string
12
+ }