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,126 @@
1
+ <script setup lang="ts">
2
+ import type { FdsButtonBaseProps } from '@/components/Buttons/ButtonBaseProps'
3
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
4
+ import FdsSpinner from '@/components/FdsSpinner/FdsSpinner.vue'
5
+ import { computed, useAttrs } from 'vue'
6
+
7
+ defineOptions({
8
+ inheritAttrs: false,
9
+ })
10
+
11
+ const attrs = useAttrs()
12
+
13
+ interface FdsButtonMinorProps extends FdsButtonBaseProps {
14
+ invert?: boolean
15
+ }
16
+
17
+ const props = withDefaults(defineProps<FdsButtonMinorProps>(), {
18
+ loading: false,
19
+ disabled: false,
20
+ block: false,
21
+ state: undefined,
22
+ icon: undefined,
23
+ iconPos: 'left',
24
+ textSelection: false,
25
+ as: 'button',
26
+ href: undefined,
27
+ target: undefined,
28
+ rel: undefined,
29
+ type: 'button',
30
+ invert: false,
31
+ })
32
+
33
+ const emit = defineEmits<{
34
+ (e: 'click', ev: MouseEvent): void
35
+ }>()
36
+
37
+ const rootClasses = computed(() => [
38
+ 'inline-block transition-opacity duration-200',
39
+ props.disabled && 'opacity-20 pointer-events-none',
40
+ props.block && 'block w-full',
41
+ ])
42
+
43
+ const elBase =
44
+ 'box-border appearance-none inline-flex items-start justify-center w-fit cursor-pointer shadow-none p-0.5 text-base select-none m-0 rounded-md text-left align-start no-underline transition-[box-shadow,border-color,background-color] duration-200 font-main font-bold text-base leading-5 tracking-normal focus-visible:outline-dashed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-500'
45
+
46
+ const variantClasses = computed(() =>
47
+ props.invert
48
+ ? 'border-0 font-normal text-white bg-transparent focus-visible:outline-dashed focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white hover:bg-white-100 active:bg-white-200'
49
+ : 'border-0 font-normal text-blue-600 bg-transparent focus-visible:outline-dashed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-500 hover:bg-blue_t-100 active:bg-blue_t-200',
50
+ )
51
+
52
+ const blockElClasses = computed(() => props.block && 'w-full justify-center')
53
+
54
+ const iconOrderClasses = computed(() =>
55
+ props.icon && props.iconPos === 'right' ? 'order-2 ml-2 mr-0' : props.icon && 'mr-2',
56
+ )
57
+
58
+ const buttonClasses = computed(() => [
59
+ elBase,
60
+ variantClasses.value,
61
+ blockElClasses.value,
62
+ props.textSelection && 'select-text',
63
+ ])
64
+
65
+ const iconFillClass = computed(() => (props.invert ? 'fill-white' : 'fill-blue-500'))
66
+
67
+ function onClick(ev: MouseEvent) {
68
+ if (props.disabled || props.loading) {
69
+ ev.preventDefault()
70
+ return
71
+ }
72
+ emit('click', ev)
73
+ }
74
+
75
+ const linkAttrs = computed(() => {
76
+ if (props.as === 'a') return { href: props.href }
77
+ if (props.as === 'router-link') return { to: props.href }
78
+ return {}
79
+ })
80
+
81
+ const componentAttrs = computed(() => ({
82
+ ...linkAttrs.value,
83
+ ...attrs,
84
+ }))
85
+ </script>
86
+
87
+ <template>
88
+ <div
89
+ :class="rootClasses"
90
+ :aria-disabled="disabled ? 'true' : undefined"
91
+ >
92
+ <component
93
+ :is="as"
94
+ v-bind="componentAttrs"
95
+ :type="as === 'button' ? type : undefined"
96
+ :disabled="as === 'button' ? disabled : undefined"
97
+ :target="as === 'a' ? target : undefined"
98
+ :rel="as === 'a' ? rel : undefined"
99
+ :class="buttonClasses"
100
+ @click="onClick"
101
+ >
102
+ <template v-if="loading">
103
+ <FdsSpinner
104
+ class="mr-2"
105
+ size="24px"
106
+ color="inherit"
107
+ />
108
+ </template>
109
+ <template v-else-if="icon">
110
+ <span
111
+ :class="iconOrderClasses"
112
+ aria-hidden="true"
113
+ >
114
+ <FdsIcon
115
+ :class="iconFillClass"
116
+ :name="icon"
117
+ :size="24"
118
+ />
119
+ </span>
120
+ </template>
121
+ <span class="pt-0.5">
122
+ {{ text }}
123
+ </span>
124
+ </component>
125
+ </div>
126
+ </template>
@@ -0,0 +1,86 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import FdsButtonPrimary from './FdsButtonPrimary.vue'
3
+ import icons from '@/assets/icons'
4
+
5
+ const meta: Meta<typeof FdsButtonPrimary> = {
6
+ title: 'FDS/Buttons/FdsButtonPrimary',
7
+ component: FdsButtonPrimary,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ docs: {
11
+ source: {
12
+ transform: (_code: string, ctx: { args?: Record<string, unknown> }) => {
13
+ const a = (ctx.args || {}) as Record<string, unknown>
14
+ const attrs: string[] = []
15
+ const push = (s: string) => attrs.push(s)
16
+ if (typeof a.text === 'string') push(`text="${a.text}"`)
17
+ if (typeof a.size === 'string' && a.size !== 'm') push(`size="${a.size}"`)
18
+ if (typeof a.icon === 'string') push(`icon="${a.icon}"`)
19
+ if (typeof a.iconPos === 'string' && a.iconPos !== 'left') push(`icon-pos="${a.iconPos}"`)
20
+ if (a.loading) push(':loading="true"')
21
+ if (a.disabled) push(':disabled="true"')
22
+ if (a.block) push(':block="true"')
23
+ if (typeof a.as === 'string' && a.as !== 'button') push(`as="${a.as}"`)
24
+ if (typeof a.type === 'string' && a.type !== 'button') push(`type="${a.type}"`)
25
+ if (typeof a.href === 'string' && a.href) push(`href="${a.href}"`)
26
+ const attrsStr = attrs.length ? ` ${attrs.join(' ')}` : ''
27
+ return `<FdsButtonPrimary${attrsStr} />`
28
+ },
29
+ },
30
+ },
31
+ },
32
+ argTypes: {
33
+ text: { control: { type: 'text' } },
34
+ size: { control: { type: 'select' }, options: ['sm', 'md', 'lg'] },
35
+ icon: { control: { type: 'select' }, options: Object.keys(icons) },
36
+ iconPos: { control: { type: 'inline-radio' }, options: ['left', 'right'] },
37
+ loading: { control: { type: 'boolean' } },
38
+ disabled: { control: { type: 'boolean' } },
39
+ block: { control: { type: 'boolean' } },
40
+ textSelection: { control: { type: 'boolean' } },
41
+ as: { control: { type: 'select' }, options: ['button', 'a'] },
42
+ type: { control: { type: 'select' }, options: ['button', 'submit', 'reset'] },
43
+ href: { control: { type: 'text' } },
44
+ },
45
+ args: {
46
+ text: 'Primary',
47
+ size: 'md',
48
+ loading: false,
49
+ disabled: false,
50
+ block: false,
51
+ textSelection: false,
52
+ icon: undefined,
53
+ iconPos: 'left',
54
+ as: 'button',
55
+ type: 'button',
56
+ },
57
+ }
58
+
59
+ export default meta
60
+ type Story = StoryObj<typeof meta>
61
+
62
+ export const Default: Story = {
63
+ render: (args) => ({
64
+ components: { FdsButtonPrimary },
65
+ setup: () => ({ args }),
66
+ template: '<FdsButtonPrimary v-bind="args" />',
67
+ }),
68
+ }
69
+
70
+ export const WithIcon: Story = {
71
+ args: { icon: 'arrowRight', text: 'With icon' },
72
+ render: (args) => ({
73
+ components: { FdsButtonPrimary },
74
+ setup: () => ({ args }),
75
+ template: '<FdsButtonPrimary v-bind="args" />',
76
+ }),
77
+ }
78
+
79
+ export const Sizes: Story = {
80
+ render: (args) => ({
81
+ components: { FdsButtonPrimary },
82
+ setup: () => ({ args }),
83
+ template:
84
+ '<div class="flex gap-3 flex-wrap">\n <FdsButtonPrimary v-bind="args" size="s" text="Small" />\n <FdsButtonPrimary v-bind="args" size="m" text="Medium" />\n <FdsButtonPrimary v-bind="args" size="l" text="Large" />\n</div>',
85
+ }),
86
+ }
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
4
+ import FdsSpinner from '@/components/FdsSpinner/FdsSpinner.vue'
5
+ import type { FdsButtonBaseProps } from '@/components/Buttons/ButtonBaseProps'
6
+
7
+ const props = withDefaults(defineProps<FdsButtonBaseProps>(), {
8
+ loading: false,
9
+ disabled: false,
10
+ block: false,
11
+ state: undefined,
12
+ icon: undefined,
13
+ iconPos: 'left',
14
+ size: 'md',
15
+ textSelection: false,
16
+ as: 'button',
17
+ href: undefined,
18
+ type: 'button',
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ (e: 'click', ev: MouseEvent): void
23
+ }>()
24
+
25
+ const rootClasses = computed(() => [
26
+ 'inline-block transition-opacity duration-200',
27
+ props.disabled && 'opacity-20 pointer-events-none',
28
+ props.block && 'block w-full',
29
+ ])
30
+
31
+ const elBase =
32
+ 'box-border appearance-none inline-flex items-center justify-center cursor-pointer select-none w-full min-h-0 min-w-12 m-0 rounded-lg text-center align-middle whitespace-nowrap no-underline shadow-[0_2px_4px_rgba(12,72,153,0.12)] transition-[box-shadow,border-color,background-color] duration-200 font-main font-bold text-base leading-5 tracking-normal focus-visible:outline-dashed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-500'
33
+
34
+ const sizeClasses: Record<string, string> = {
35
+ sm: 'text-sm h-7 px-3',
36
+ md: 'text-base h-12 px-4',
37
+ lg: 'text-lg h-[68px] px-6',
38
+ }
39
+
40
+ const variantClasses =
41
+ 'bg-red-600 border border-red-700 text-white hover:bg-red-700 active:bg-red-800 active:border-red-800'
42
+
43
+ const blockElClasses = computed(() => props.block && 'w-full justify-center')
44
+
45
+ const iconOrderClasses = computed(() => (props.icon && props.iconPos === 'right' ? 'order-2' : ''))
46
+
47
+ const buttonClasses = computed(() => [
48
+ elBase,
49
+ sizeClasses[props.size || 'm'],
50
+ variantClasses,
51
+ blockElClasses.value,
52
+ props.textSelection && 'select-text',
53
+ (props.icon || props.text) && 'gap-2',
54
+ ])
55
+
56
+ const iconFillClass = 'fill-white'
57
+
58
+ function onClick(ev: MouseEvent) {
59
+ if (props.disabled || props.loading) {
60
+ ev.preventDefault()
61
+ return
62
+ }
63
+ emit('click', ev)
64
+ }
65
+
66
+ const linkAttrs = computed(() => {
67
+ if (props.as === 'a') return { href: props.href }
68
+ if (props.as === 'router-link') return { to: props.href }
69
+ return {}
70
+ })
71
+ </script>
72
+
73
+ <template>
74
+ <div
75
+ :class="rootClasses"
76
+ :aria-disabled="disabled ? 'true' : undefined"
77
+ >
78
+ <component
79
+ :is="as"
80
+ v-bind="linkAttrs"
81
+ :type="as === 'button' ? type : undefined"
82
+ :disabled="as === 'button' ? disabled : undefined"
83
+ :class="buttonClasses"
84
+ @click="onClick"
85
+ >
86
+ <template v-if="loading">
87
+ <FdsSpinner
88
+ size="24px"
89
+ color="inherit"
90
+ />
91
+ </template>
92
+ <template v-else-if="icon">
93
+ <span
94
+ :class="iconOrderClasses"
95
+ aria-hidden="true"
96
+ >
97
+ <FdsIcon
98
+ :class="iconFillClass"
99
+ :name="icon"
100
+ :size="24"
101
+ />
102
+ </span>
103
+ </template>
104
+ <span v-if="text">{{ text }}</span>
105
+ </component>
106
+ </div>
107
+ </template>
@@ -0,0 +1,68 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import FdsButtonSecondary from './FdsButtonSecondary.vue'
3
+ import icons from '@/assets/icons'
4
+
5
+ const meta: Meta<typeof FdsButtonSecondary> = {
6
+ title: 'FDS/Buttons/FdsButtonSecondary',
7
+ component: FdsButtonSecondary,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ docs: {
11
+ source: {
12
+ transform: (_code: string, ctx: { args?: Record<string, unknown> }) => {
13
+ const a = (ctx.args || {}) as Record<string, unknown>
14
+ const attrs: string[] = []
15
+ const push = (s: string) => attrs.push(s)
16
+ if (typeof a.text === 'string') push(`text="${a.text}"`)
17
+ if (typeof a.size === 'string' && a.size !== 'm') push(`size="${a.size}"`)
18
+ if (typeof a.icon === 'string') push(`icon="${a.icon}"`)
19
+ if (typeof a.iconPos === 'string' && a.iconPos !== 'left') push(`icon-pos="${a.iconPos}"`)
20
+ if (a.loading) push(':loading="true"')
21
+ if (a.disabled) push(':disabled="true"')
22
+ if (a.block) push(':block="true"')
23
+ if (typeof a.as === 'string' && a.as !== 'button') push(`as="${a.as}"`)
24
+ if (typeof a.type === 'string' && a.type !== 'button') push(`type="${a.type}"`)
25
+ if (typeof a.href === 'string' && a.href) push(`href="${a.href}"`)
26
+ const attrsStr = attrs.length ? ` ${attrs.join(' ')}` : ''
27
+ return `<FdsButtonSecondary${attrsStr} />`
28
+ },
29
+ },
30
+ },
31
+ },
32
+ argTypes: {
33
+ text: { control: { type: 'text' } },
34
+ size: { control: { type: 'select' }, options: ['sm', 'md', 'lg'] },
35
+ icon: { control: { type: 'select' }, options: Object.keys(icons) },
36
+ iconPos: { control: { type: 'inline-radio' }, options: ['left', 'right'] },
37
+ loading: { control: { type: 'boolean' } },
38
+ disabled: { control: { type: 'boolean' } },
39
+ block: { control: { type: 'boolean' } },
40
+ textSelection: { control: { type: 'boolean' } },
41
+ as: { control: { type: 'select' }, options: ['button', 'a'] },
42
+ type: { control: { type: 'select' }, options: ['button', 'submit', 'reset'] },
43
+ href: { control: { type: 'text' } },
44
+ },
45
+ args: {
46
+ text: 'Secondary',
47
+ size: 'md',
48
+ loading: false,
49
+ disabled: false,
50
+ block: false,
51
+ textSelection: false,
52
+ icon: undefined,
53
+ iconPos: 'left',
54
+ as: 'button',
55
+ type: 'button',
56
+ },
57
+ }
58
+
59
+ export default meta
60
+ type Story = StoryObj<typeof meta>
61
+
62
+ export const Default: Story = {
63
+ render: (args) => ({
64
+ components: { FdsButtonSecondary },
65
+ setup: () => ({ args }),
66
+ template: '<FdsButtonSecondary v-bind="args" />',
67
+ }),
68
+ }
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
4
+ import FdsSpinner from '@/components/FdsSpinner/FdsSpinner.vue'
5
+ import type { FdsButtonBaseProps } from '@/components/Buttons/ButtonBaseProps'
6
+
7
+ const props = withDefaults(defineProps<FdsButtonBaseProps>(), {
8
+ loading: false,
9
+ disabled: false,
10
+ block: false,
11
+ state: undefined,
12
+ icon: undefined,
13
+ iconPos: 'left',
14
+ size: 'md',
15
+ textSelection: false,
16
+ as: 'button',
17
+ href: undefined,
18
+ type: 'button',
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ (e: 'click', ev: MouseEvent): void
23
+ }>()
24
+
25
+ const rootClasses = computed(() => [
26
+ 'inline-block transition-opacity duration-200',
27
+ props.disabled && 'opacity-20 pointer-events-none',
28
+ props.block && 'block w-full',
29
+ ])
30
+
31
+ const elBase =
32
+ 'box-border appearance-none inline-flex items-center justify-center cursor-pointer select-none w-full min-h-0 min-w-12 m-0 rounded-lg text-center align-middle whitespace-nowrap no-underline shadow-[0_2px_4px_rgba(12,72,153,0.12)] transition-[box-shadow,border-color,background-color] duration-200 font-main font-bold text-base leading-5 tracking-normal focus-visible:outline-dashed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-500'
33
+
34
+ const sizeClasses: Record<string, string> = {
35
+ sm: 'text-sm h-7 px-3',
36
+ md: 'text-base h-12 px-4',
37
+ lg: 'text-lg h-[68px] px-6',
38
+ }
39
+
40
+ const variantClasses =
41
+ 'bg-white border-2 border-blue-500 text-blue-600 hover:border-blue-600 active:bg-blue-600 active:border-blue-600 active:text-white'
42
+
43
+ const blockElClasses = computed(() => props.block && 'w-full justify-center')
44
+
45
+ const iconOrderClasses = computed(() => (props.icon && props.iconPos === 'right' ? 'order-2' : ''))
46
+
47
+ const buttonClasses = computed(() => [
48
+ elBase,
49
+ sizeClasses[props.size || 'm'],
50
+ variantClasses,
51
+ blockElClasses.value,
52
+ props.textSelection && 'select-text',
53
+ (props.icon || props.text) && 'gap-2',
54
+ ])
55
+
56
+ const iconFillClass = 'fill-blue-500'
57
+
58
+ function onClick(ev: MouseEvent) {
59
+ if (props.disabled || props.loading) {
60
+ ev.preventDefault()
61
+ return
62
+ }
63
+ emit('click', ev)
64
+ }
65
+
66
+ const linkAttrs = computed(() => {
67
+ if (props.as === 'a') return { href: props.href }
68
+ if (props.as === 'router-link') return { to: props.href }
69
+ return {}
70
+ })
71
+ </script>
72
+
73
+ <template>
74
+ <div
75
+ :class="rootClasses"
76
+ :aria-disabled="disabled ? 'true' : undefined"
77
+ >
78
+ <component
79
+ :is="as"
80
+ v-bind="linkAttrs"
81
+ :type="as === 'button' ? type : undefined"
82
+ :disabled="as === 'button' ? disabled : undefined"
83
+ :class="buttonClasses"
84
+ @click="onClick"
85
+ >
86
+ <template v-if="loading">
87
+ <FdsSpinner
88
+ size="24px"
89
+ color="inherit"
90
+ />
91
+ </template>
92
+ <template v-else-if="icon">
93
+ <span
94
+ :class="iconOrderClasses"
95
+ aria-hidden="true"
96
+ >
97
+ <FdsIcon
98
+ :class="iconFillClass"
99
+ :name="icon"
100
+ :size="24"
101
+ />
102
+ </span>
103
+ </template>
104
+ <span v-if="text">{{ text }}</span>
105
+ </component>
106
+ </div>
107
+ </template>
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { computed, ref } from 'vue'
3
+ import icons from '@/assets/icons'
4
+ import FdsIcon from './FdsIcon.vue'
5
+ import type { FdsIconName } from './types'
6
+
7
+ const meta: Meta<typeof FdsIcon> = {
8
+ title: 'FDS/FdsIcon',
9
+ component: FdsIcon,
10
+ tags: ['autodocs'],
11
+ parameters: {},
12
+ argTypes: {
13
+ name: { control: { type: 'select' }, options: Object.keys(icons) },
14
+ size: { control: { type: 'text' } },
15
+ title: { control: { type: 'text' } },
16
+ },
17
+ args: {
18
+ name: (Object.keys(icons)[0] || '') as FdsIconName,
19
+ size: 24,
20
+ },
21
+ }
22
+
23
+ export default meta
24
+ type Story = StoryObj<typeof meta>
25
+
26
+ export const Basic: Story = {
27
+ render: (args) => ({
28
+ components: { FdsIcon },
29
+ setup: () => ({ args }),
30
+ template: '<FdsIcon v-bind="args" />',
31
+ }),
32
+ }
33
+
34
+ export const IconGallery: Story = {
35
+ parameters: {},
36
+ render: (args: { name?: string; size?: number | string; title?: string }) => ({
37
+ components: { FdsIcon },
38
+ setup() {
39
+ const iconEntries = ref(Object.keys(icons))
40
+ const selectedName = ref(args.name || iconEntries.value[0] || '')
41
+ function onPick(n: string) {
42
+ selectedName.value = n
43
+ }
44
+ const codeSnippet = computed(() => `<FdsIcon name="${selectedName.value}" size="${args.size}" />`)
45
+ return { args, iconEntries, onPick, codeSnippet, selectedName }
46
+ },
47
+ template: `
48
+ <div>
49
+ <div class="grid grid-cols-5 sm:grid-cols-8 md:grid-cols-10 gap-4">
50
+ <button
51
+ v-for="n in iconEntries"
52
+ :key="n"
53
+ type="button"
54
+ class="flex flex-col items-center gap-2 p-3 rounded border hover:bg-blue-100 focus-visible:outline-dashed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-500"
55
+ @click="onPick(n)"
56
+ :aria-label="'Välj ikon ' + n"
57
+ >
58
+ <FdsIcon :name="n" :size="28" />
59
+ <span class="text-[11px] text-gray-700 truncate max-w-[90px]">{{ n }}</span>
60
+ </button>
61
+ </div>
62
+ <div class="mt-4 text-sm">Vald ikon: <code class="px-1 py-0.5 bg-gray-100 rounded">{{ selectedName }}</code></div>
63
+ <div class="mt-2">
64
+ <FdsIcon :name="selectedName" :size="args.size" :title="args.title" />
65
+ </div>
66
+ </div>
67
+ `,
68
+ }),
69
+ }
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import icons, { type IconMap } from '@/assets/icons'
3
+ import { computed } from 'vue'
4
+ import type { FdsIconProps } from './types'
5
+
6
+ const props = withDefaults(defineProps<FdsIconProps>(), {
7
+ size: 24,
8
+ title: undefined,
9
+ })
10
+
11
+ function normalizeSvg(svg: string): string {
12
+ // Replace explicit width/height with 100% to allow container sizing
13
+ return svg.replace(/width="(.*?)"/g, 'width="100%"').replace(/height="(.*?)"/g, 'height="100%"')
14
+ }
15
+
16
+ const svgHtml = computed(() => {
17
+ const map = icons as IconMap
18
+ const entry = map[props.name]
19
+ if (!entry) return ''
20
+ const raw = typeof entry === 'function' ? entry() : entry
21
+ const withTitle = props.title ? raw.replace('<svg', `<svg aria-label="${props.title}" role="img"`) : raw
22
+ return normalizeSvg(withTitle)
23
+ })
24
+
25
+ const pixelSize = computed(() => (typeof props.size === 'number' ? `${props.size}px` : props.size))
26
+ </script>
27
+
28
+ <template>
29
+ <span
30
+ class="inline-flex items-center justify-center align-middle"
31
+ :style="{ width: pixelSize, height: pixelSize }"
32
+ v-html="svgHtml"
33
+ ></span>
34
+ </template>
@@ -0,0 +1,9 @@
1
+ import type icons from '@/assets/icons'
2
+
3
+ export type FdsIconName = keyof typeof icons
4
+
5
+ export interface FdsIconProps {
6
+ name: FdsIconName
7
+ size?: number | string
8
+ title?: string | undefined
9
+ }