@dillingerstaffing/strand-vue 0.4.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.
Files changed (233) hide show
  1. package/dist/components/Alert/Alert.vue.d.ts +31 -0
  2. package/dist/components/Alert/Alert.vue.d.ts.map +1 -0
  3. package/dist/components/Alert/index.d.ts +3 -0
  4. package/dist/components/Alert/index.d.ts.map +1 -0
  5. package/dist/components/Avatar/Avatar.vue.d.ts +20 -0
  6. package/dist/components/Avatar/Avatar.vue.d.ts.map +1 -0
  7. package/dist/components/Avatar/index.d.ts +2 -0
  8. package/dist/components/Avatar/index.d.ts.map +1 -0
  9. package/dist/components/Badge/Badge.vue.d.ts +35 -0
  10. package/dist/components/Badge/Badge.vue.d.ts.map +1 -0
  11. package/dist/components/Badge/index.d.ts +2 -0
  12. package/dist/components/Badge/index.d.ts.map +1 -0
  13. package/dist/components/Breadcrumb/Breadcrumb.vue.d.ts +15 -0
  14. package/dist/components/Breadcrumb/Breadcrumb.vue.d.ts.map +1 -0
  15. package/dist/components/Breadcrumb/index.d.ts +3 -0
  16. package/dist/components/Breadcrumb/index.d.ts.map +1 -0
  17. package/dist/components/Button/Button.vue.d.ts +46 -0
  18. package/dist/components/Button/Button.vue.d.ts.map +1 -0
  19. package/dist/components/Button/index.d.ts +3 -0
  20. package/dist/components/Button/index.d.ts.map +1 -0
  21. package/dist/components/Card/Card.vue.d.ts +30 -0
  22. package/dist/components/Card/Card.vue.d.ts.map +1 -0
  23. package/dist/components/Card/index.d.ts +2 -0
  24. package/dist/components/Card/index.d.ts.map +1 -0
  25. package/dist/components/Checkbox/Checkbox.vue.d.ts +23 -0
  26. package/dist/components/Checkbox/Checkbox.vue.d.ts.map +1 -0
  27. package/dist/components/Checkbox/index.d.ts +3 -0
  28. package/dist/components/Checkbox/index.d.ts.map +1 -0
  29. package/dist/components/Container/Container.vue.d.ts +27 -0
  30. package/dist/components/Container/Container.vue.d.ts.map +1 -0
  31. package/dist/components/Container/index.d.ts +2 -0
  32. package/dist/components/Container/index.d.ts.map +1 -0
  33. package/dist/components/DataReadout/DataReadout.vue.d.ts +15 -0
  34. package/dist/components/DataReadout/DataReadout.vue.d.ts.map +1 -0
  35. package/dist/components/DataReadout/index.d.ts +2 -0
  36. package/dist/components/DataReadout/index.d.ts.map +1 -0
  37. package/dist/components/Dialog/Dialog.vue.d.ts +39 -0
  38. package/dist/components/Dialog/Dialog.vue.d.ts.map +1 -0
  39. package/dist/components/Dialog/index.d.ts +3 -0
  40. package/dist/components/Dialog/index.d.ts.map +1 -0
  41. package/dist/components/Divider/Divider.vue.d.ts +14 -0
  42. package/dist/components/Divider/Divider.vue.d.ts.map +1 -0
  43. package/dist/components/Divider/index.d.ts +2 -0
  44. package/dist/components/Divider/index.d.ts.map +1 -0
  45. package/dist/components/FormField/FormField.vue.d.ts +32 -0
  46. package/dist/components/FormField/FormField.vue.d.ts.map +1 -0
  47. package/dist/components/FormField/index.d.ts +3 -0
  48. package/dist/components/FormField/index.d.ts.map +1 -0
  49. package/dist/components/Grid/Grid.vue.d.ts +30 -0
  50. package/dist/components/Grid/Grid.vue.d.ts.map +1 -0
  51. package/dist/components/Grid/index.d.ts +2 -0
  52. package/dist/components/Grid/index.d.ts.map +1 -0
  53. package/dist/components/Input/Input.vue.d.ts +37 -0
  54. package/dist/components/Input/Input.vue.d.ts.map +1 -0
  55. package/dist/components/Input/index.d.ts +3 -0
  56. package/dist/components/Input/index.d.ts.map +1 -0
  57. package/dist/components/Link/Link.vue.d.ts +29 -0
  58. package/dist/components/Link/Link.vue.d.ts.map +1 -0
  59. package/dist/components/Link/index.d.ts +2 -0
  60. package/dist/components/Link/index.d.ts.map +1 -0
  61. package/dist/components/Nav/Nav.vue.d.ts +30 -0
  62. package/dist/components/Nav/Nav.vue.d.ts.map +1 -0
  63. package/dist/components/Nav/index.d.ts +3 -0
  64. package/dist/components/Nav/index.d.ts.map +1 -0
  65. package/dist/components/Progress/Progress.vue.d.ts +17 -0
  66. package/dist/components/Progress/Progress.vue.d.ts.map +1 -0
  67. package/dist/components/Progress/index.d.ts +2 -0
  68. package/dist/components/Progress/index.d.ts.map +1 -0
  69. package/dist/components/Radio/Radio.vue.d.ts +22 -0
  70. package/dist/components/Radio/Radio.vue.d.ts.map +1 -0
  71. package/dist/components/Radio/index.d.ts +3 -0
  72. package/dist/components/Radio/index.d.ts.map +1 -0
  73. package/dist/components/Section/Section.vue.d.ts +30 -0
  74. package/dist/components/Section/Section.vue.d.ts.map +1 -0
  75. package/dist/components/Section/index.d.ts +2 -0
  76. package/dist/components/Section/index.d.ts.map +1 -0
  77. package/dist/components/Select/Select.vue.d.ts +26 -0
  78. package/dist/components/Select/Select.vue.d.ts.map +1 -0
  79. package/dist/components/Select/index.d.ts +3 -0
  80. package/dist/components/Select/index.d.ts.map +1 -0
  81. package/dist/components/Skeleton/Skeleton.vue.d.ts +16 -0
  82. package/dist/components/Skeleton/Skeleton.vue.d.ts.map +1 -0
  83. package/dist/components/Skeleton/index.d.ts +2 -0
  84. package/dist/components/Skeleton/index.d.ts.map +1 -0
  85. package/dist/components/Slider/Slider.vue.d.ts +24 -0
  86. package/dist/components/Slider/Slider.vue.d.ts.map +1 -0
  87. package/dist/components/Slider/index.d.ts +3 -0
  88. package/dist/components/Slider/index.d.ts.map +1 -0
  89. package/dist/components/Spinner/Spinner.vue.d.ts +12 -0
  90. package/dist/components/Spinner/Spinner.vue.d.ts.map +1 -0
  91. package/dist/components/Spinner/index.d.ts +2 -0
  92. package/dist/components/Spinner/index.d.ts.map +1 -0
  93. package/dist/components/Stack/Stack.vue.d.ts +38 -0
  94. package/dist/components/Stack/Stack.vue.d.ts.map +1 -0
  95. package/dist/components/Stack/index.d.ts +2 -0
  96. package/dist/components/Stack/index.d.ts.map +1 -0
  97. package/dist/components/Switch/Switch.vue.d.ts +18 -0
  98. package/dist/components/Switch/Switch.vue.d.ts.map +1 -0
  99. package/dist/components/Switch/index.d.ts +3 -0
  100. package/dist/components/Switch/index.d.ts.map +1 -0
  101. package/dist/components/Table/Table.vue.d.ts +23 -0
  102. package/dist/components/Table/Table.vue.d.ts.map +1 -0
  103. package/dist/components/Table/index.d.ts +3 -0
  104. package/dist/components/Table/index.d.ts.map +1 -0
  105. package/dist/components/Tabs/Tabs.vue.d.ts +34 -0
  106. package/dist/components/Tabs/Tabs.vue.d.ts.map +1 -0
  107. package/dist/components/Tabs/index.d.ts +3 -0
  108. package/dist/components/Tabs/index.d.ts.map +1 -0
  109. package/dist/components/Tag/Tag.vue.d.ts +37 -0
  110. package/dist/components/Tag/Tag.vue.d.ts.map +1 -0
  111. package/dist/components/Tag/index.d.ts +2 -0
  112. package/dist/components/Tag/index.d.ts.map +1 -0
  113. package/dist/components/Textarea/Textarea.vue.d.ts +29 -0
  114. package/dist/components/Textarea/Textarea.vue.d.ts.map +1 -0
  115. package/dist/components/Textarea/index.d.ts +3 -0
  116. package/dist/components/Textarea/index.d.ts.map +1 -0
  117. package/dist/components/Toast/Toast.vue.d.ts +16 -0
  118. package/dist/components/Toast/Toast.vue.d.ts.map +1 -0
  119. package/dist/components/Toast/ToastProvider.vue.d.ts +18 -0
  120. package/dist/components/Toast/ToastProvider.vue.d.ts.map +1 -0
  121. package/dist/components/Toast/index.d.ts +6 -0
  122. package/dist/components/Toast/index.d.ts.map +1 -0
  123. package/dist/components/Toast/useToast.d.ts +13 -0
  124. package/dist/components/Toast/useToast.d.ts.map +1 -0
  125. package/dist/components/Tooltip/Tooltip.vue.d.ts +29 -0
  126. package/dist/components/Tooltip/Tooltip.vue.d.ts.map +1 -0
  127. package/dist/components/Tooltip/index.d.ts +3 -0
  128. package/dist/components/Tooltip/index.d.ts.map +1 -0
  129. package/dist/css/strand-ui.css +2534 -0
  130. package/dist/index.d.ts +35 -0
  131. package/dist/index.d.ts.map +1 -0
  132. package/dist/index.js +1413 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/test-setup.d.ts +1 -0
  135. package/dist/test-setup.d.ts.map +1 -0
  136. package/package.json +51 -0
  137. package/src/components/Alert/Alert.test.ts +100 -0
  138. package/src/components/Alert/Alert.vue +54 -0
  139. package/src/components/Alert/index.ts +2 -0
  140. package/src/components/Avatar/Avatar.test.ts +105 -0
  141. package/src/components/Avatar/Avatar.vue +56 -0
  142. package/src/components/Avatar/index.ts +1 -0
  143. package/src/components/Badge/Badge.test.ts +114 -0
  144. package/src/components/Badge/Badge.vue +66 -0
  145. package/src/components/Badge/index.ts +1 -0
  146. package/src/components/Breadcrumb/Breadcrumb.test.ts +119 -0
  147. package/src/components/Breadcrumb/Breadcrumb.vue +58 -0
  148. package/src/components/Breadcrumb/index.ts +2 -0
  149. package/src/components/Button/Button.test.ts +148 -0
  150. package/src/components/Button/Button.vue +75 -0
  151. package/src/components/Button/index.ts +2 -0
  152. package/src/components/Card/Card.test.ts +93 -0
  153. package/src/components/Card/Card.vue +36 -0
  154. package/src/components/Card/index.ts +1 -0
  155. package/src/components/Checkbox/Checkbox.test.ts +118 -0
  156. package/src/components/Checkbox/Checkbox.vue +117 -0
  157. package/src/components/Checkbox/index.ts +2 -0
  158. package/src/components/Container/Container.test.ts +70 -0
  159. package/src/components/Container/Container.vue +32 -0
  160. package/src/components/Container/index.ts +1 -0
  161. package/src/components/DataReadout/DataReadout.test.ts +99 -0
  162. package/src/components/DataReadout/DataReadout.vue +36 -0
  163. package/src/components/DataReadout/index.ts +1 -0
  164. package/src/components/Dialog/Dialog.test.ts +224 -0
  165. package/src/components/Dialog/Dialog.vue +146 -0
  166. package/src/components/Dialog/index.ts +2 -0
  167. package/src/components/Divider/Divider.test.ts +95 -0
  168. package/src/components/Divider/Divider.vue +63 -0
  169. package/src/components/Divider/index.ts +1 -0
  170. package/src/components/FormField/FormField.test.ts +98 -0
  171. package/src/components/FormField/FormField.vue +59 -0
  172. package/src/components/FormField/index.ts +2 -0
  173. package/src/components/Grid/Grid.test.ts +73 -0
  174. package/src/components/Grid/Grid.vue +34 -0
  175. package/src/components/Grid/index.ts +1 -0
  176. package/src/components/Input/Input.test.ts +102 -0
  177. package/src/components/Input/Input.vue +63 -0
  178. package/src/components/Input/index.ts +2 -0
  179. package/src/components/Link/Link.test.ts +92 -0
  180. package/src/components/Link/Link.vue +35 -0
  181. package/src/components/Link/index.ts +1 -0
  182. package/src/components/Nav/Nav.test.ts +171 -0
  183. package/src/components/Nav/Nav.vue +81 -0
  184. package/src/components/Nav/index.ts +2 -0
  185. package/src/components/Progress/Progress.test.ts +103 -0
  186. package/src/components/Progress/Progress.vue +96 -0
  187. package/src/components/Progress/index.ts +1 -0
  188. package/src/components/Radio/Radio.test.ts +92 -0
  189. package/src/components/Radio/Radio.vue +60 -0
  190. package/src/components/Radio/index.ts +2 -0
  191. package/src/components/Section/Section.test.ts +77 -0
  192. package/src/components/Section/Section.vue +36 -0
  193. package/src/components/Section/index.ts +1 -0
  194. package/src/components/Select/Select.test.ts +102 -0
  195. package/src/components/Select/Select.vue +70 -0
  196. package/src/components/Select/index.ts +2 -0
  197. package/src/components/Skeleton/Skeleton.test.ts +77 -0
  198. package/src/components/Skeleton/Skeleton.vue +48 -0
  199. package/src/components/Skeleton/index.ts +1 -0
  200. package/src/components/Slider/Slider.test.ts +73 -0
  201. package/src/components/Slider/Slider.vue +60 -0
  202. package/src/components/Slider/index.ts +2 -0
  203. package/src/components/Spinner/Spinner.test.ts +66 -0
  204. package/src/components/Spinner/Spinner.vue +33 -0
  205. package/src/components/Spinner/index.ts +1 -0
  206. package/src/components/Stack/Stack.test.ts +140 -0
  207. package/src/components/Stack/Stack.vue +50 -0
  208. package/src/components/Stack/index.ts +1 -0
  209. package/src/components/Switch/Switch.test.ts +116 -0
  210. package/src/components/Switch/Switch.vue +62 -0
  211. package/src/components/Switch/index.ts +2 -0
  212. package/src/components/Table/Table.test.ts +152 -0
  213. package/src/components/Table/Table.vue +98 -0
  214. package/src/components/Table/index.ts +2 -0
  215. package/src/components/Tabs/Tabs.test.ts +138 -0
  216. package/src/components/Tabs/Tabs.vue +96 -0
  217. package/src/components/Tabs/index.ts +2 -0
  218. package/src/components/Tag/Tag.test.ts +128 -0
  219. package/src/components/Tag/Tag.vue +65 -0
  220. package/src/components/Tag/index.ts +1 -0
  221. package/src/components/Textarea/Textarea.test.ts +107 -0
  222. package/src/components/Textarea/Textarea.vue +90 -0
  223. package/src/components/Textarea/index.ts +2 -0
  224. package/src/components/Toast/Toast.test.ts +204 -0
  225. package/src/components/Toast/Toast.vue +48 -0
  226. package/src/components/Toast/ToastProvider.vue +80 -0
  227. package/src/components/Toast/index.ts +5 -0
  228. package/src/components/Toast/useToast.ts +26 -0
  229. package/src/components/Tooltip/Tooltip.test.ts +145 -0
  230. package/src/components/Tooltip/Tooltip.vue +79 -0
  231. package/src/components/Tooltip/index.ts +2 -0
  232. package/src/index.ts +44 -0
  233. package/src/test-setup.ts +7 -0
@@ -0,0 +1,36 @@
1
+ <!--! Strand Vue | MIT License | dillingerstaffing.com -->
2
+ <script setup lang="ts">
3
+ import { computed } from 'vue'
4
+
5
+ interface Props {
6
+ /** Visual style variant */
7
+ variant?: 'elevated' | 'outlined' | 'interactive'
8
+ /** Inner padding */
9
+ padding?: 'none' | 'sm' | 'md' | 'lg'
10
+ /** Additional CSS class */
11
+ className?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'elevated',
16
+ padding: 'md',
17
+ className: '',
18
+ })
19
+
20
+ const classes = computed(() =>
21
+ [
22
+ 'strand-card',
23
+ `strand-card--${props.variant}`,
24
+ `strand-card--pad-${props.padding}`,
25
+ props.className,
26
+ ]
27
+ .filter(Boolean)
28
+ .join(' '),
29
+ )
30
+ </script>
31
+
32
+ <template>
33
+ <div :class="classes" v-bind="$attrs">
34
+ <slot />
35
+ </div>
36
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Card } from './Card.vue'
@@ -0,0 +1,118 @@
1
+ /*! Strand Vue | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render, fireEvent } from '@testing-library/vue'
5
+ import Checkbox from './Checkbox.vue'
6
+
7
+ describe('Checkbox', () => {
8
+ it('renders with default props', () => {
9
+ const { container } = render(Checkbox)
10
+ const label = container.querySelector('.strand-checkbox')
11
+ expect(label).toBeInTheDocument()
12
+ const input = container.querySelector('.strand-checkbox__native') as HTMLInputElement
13
+ expect(input).toBeInTheDocument()
14
+ expect(input).toHaveAttribute('type', 'checkbox')
15
+ expect(input).toHaveAttribute('role', 'checkbox')
16
+ expect(input).toHaveAttribute('aria-checked', 'false')
17
+ expect(input).not.toBeChecked()
18
+ })
19
+
20
+ it('applies checked state and class', () => {
21
+ const { container } = render(Checkbox, {
22
+ props: { checked: true },
23
+ })
24
+ const wrapper = container.querySelector('.strand-checkbox')
25
+ expect(wrapper).toHaveClass('strand-checkbox--checked')
26
+ const input = container.querySelector('.strand-checkbox__native') as HTMLInputElement
27
+ expect(input).toBeChecked()
28
+ expect(input).toHaveAttribute('aria-checked', 'true')
29
+ })
30
+
31
+ it('shows checkmark SVG when checked', () => {
32
+ const { container } = render(Checkbox, {
33
+ props: { checked: true },
34
+ })
35
+ const svg = container.querySelector('.strand-checkbox__icon')
36
+ expect(svg).toBeInTheDocument()
37
+ expect(container.querySelector('path')).toBeInTheDocument()
38
+ })
39
+
40
+ it('shows indeterminate state with mixed aria-checked', () => {
41
+ const { container } = render(Checkbox, {
42
+ props: { indeterminate: true },
43
+ })
44
+ const wrapper = container.querySelector('.strand-checkbox')
45
+ expect(wrapper).toHaveClass('strand-checkbox--indeterminate')
46
+ const input = container.querySelector('.strand-checkbox__native') as HTMLInputElement
47
+ expect(input).toHaveAttribute('aria-checked', 'mixed')
48
+ })
49
+
50
+ it('shows dash SVG when indeterminate', () => {
51
+ const { container } = render(Checkbox, {
52
+ props: { indeterminate: true },
53
+ })
54
+ const svg = container.querySelector('.strand-checkbox__icon')
55
+ expect(svg).toBeInTheDocument()
56
+ expect(container.querySelector('line')).toBeInTheDocument()
57
+ })
58
+
59
+ it('does not show SVG when unchecked and not indeterminate', () => {
60
+ const { container } = render(Checkbox)
61
+ expect(container.querySelector('.strand-checkbox__icon')).not.toBeInTheDocument()
62
+ })
63
+
64
+ it('applies disabled state', () => {
65
+ const { container } = render(Checkbox, {
66
+ props: { disabled: true },
67
+ })
68
+ const wrapper = container.querySelector('.strand-checkbox')
69
+ expect(wrapper).toHaveClass('strand-checkbox--disabled')
70
+ const input = container.querySelector('.strand-checkbox__native')
71
+ expect(input).toBeDisabled()
72
+ })
73
+
74
+ it('renders label text', () => {
75
+ const { container } = render(Checkbox, {
76
+ props: { label: 'Accept terms' },
77
+ })
78
+ const labelSpan = container.querySelector('.strand-checkbox__label')
79
+ expect(labelSpan).toBeInTheDocument()
80
+ expect(labelSpan).toHaveTextContent('Accept terms')
81
+ })
82
+
83
+ it('does not render label span without label prop', () => {
84
+ const { container } = render(Checkbox)
85
+ expect(container.querySelector('.strand-checkbox__label')).not.toBeInTheDocument()
86
+ })
87
+
88
+ it('emits change event when clicked', async () => {
89
+ const { container, emitted } = render(Checkbox)
90
+ const input = container.querySelector('.strand-checkbox__native') as HTMLInputElement
91
+ await fireEvent.click(input)
92
+ expect(emitted().change).toHaveLength(1)
93
+ })
94
+
95
+ it('does not emit change when disabled', async () => {
96
+ const { container, emitted } = render(Checkbox, {
97
+ props: { disabled: true },
98
+ })
99
+ const input = container.querySelector('.strand-checkbox__native') as HTMLInputElement
100
+ await fireEvent.click(input)
101
+ expect(emitted().change).toBeUndefined()
102
+ })
103
+
104
+ it('renders custom control with aria-hidden', () => {
105
+ const { container } = render(Checkbox)
106
+ const control = container.querySelector('.strand-checkbox__control')
107
+ expect(control).toBeInTheDocument()
108
+ expect(control).toHaveAttribute('aria-hidden', 'true')
109
+ })
110
+
111
+ it('sets indeterminate property on the native input', () => {
112
+ const { container } = render(Checkbox, {
113
+ props: { indeterminate: true },
114
+ })
115
+ const input = container.querySelector('.strand-checkbox__native') as HTMLInputElement
116
+ expect(input.indeterminate).toBe(true)
117
+ })
118
+ })
@@ -0,0 +1,117 @@
1
+ <!--! Strand Vue | MIT License | dillingerstaffing.com -->
2
+ <script setup lang="ts">
3
+ import { computed, ref, watch, onMounted } from 'vue'
4
+
5
+ export interface CheckboxProps {
6
+ /** Controlled checked state */
7
+ checked?: boolean
8
+ /** Indeterminate visual state */
9
+ indeterminate?: boolean
10
+ /** Disabled state */
11
+ disabled?: boolean
12
+ /** Label text */
13
+ label?: string
14
+ }
15
+
16
+ const props = withDefaults(defineProps<CheckboxProps>(), {
17
+ checked: false,
18
+ indeterminate: false,
19
+ disabled: false,
20
+ })
21
+
22
+ const emit = defineEmits<{
23
+ (e: 'change', event: Event): void
24
+ }>()
25
+
26
+ const inputRef = ref<HTMLInputElement | null>(null)
27
+
28
+ onMounted(() => {
29
+ if (inputRef.value) {
30
+ inputRef.value.indeterminate = props.indeterminate
31
+ }
32
+ })
33
+
34
+ watch(() => props.indeterminate, (val) => {
35
+ if (inputRef.value) {
36
+ inputRef.value.indeterminate = val
37
+ }
38
+ })
39
+
40
+ const classes = computed(() =>
41
+ [
42
+ 'strand-checkbox',
43
+ props.checked && 'strand-checkbox--checked',
44
+ props.indeterminate && 'strand-checkbox--indeterminate',
45
+ props.disabled && 'strand-checkbox--disabled',
46
+ ]
47
+ .filter(Boolean)
48
+ .join(' '),
49
+ )
50
+
51
+ const ariaChecked = computed(() =>
52
+ props.indeterminate ? 'mixed' : props.checked ? 'true' : 'false',
53
+ )
54
+
55
+ function handleChange(event: Event) {
56
+ if (!props.disabled) {
57
+ emit('change', event)
58
+ }
59
+ }
60
+
61
+ function handleKeyDown(event: KeyboardEvent) {
62
+ if (event.key === ' ' && !props.disabled) {
63
+ event.preventDefault()
64
+ if (inputRef.value) {
65
+ inputRef.value.click()
66
+ }
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <template>
72
+ <label :class="classes" @keydown="handleKeyDown">
73
+ <input
74
+ ref="inputRef"
75
+ type="checkbox"
76
+ class="strand-checkbox__native"
77
+ :checked="checked"
78
+ :disabled="disabled"
79
+ :aria-checked="ariaChecked"
80
+ role="checkbox"
81
+ @change="handleChange"
82
+ />
83
+ <span class="strand-checkbox__control" aria-hidden="true">
84
+ <svg
85
+ v-if="indeterminate"
86
+ class="strand-checkbox__icon"
87
+ viewBox="0 0 16 16"
88
+ fill="none"
89
+ >
90
+ <line
91
+ x1="4"
92
+ y1="8"
93
+ x2="12"
94
+ y2="8"
95
+ stroke="currentColor"
96
+ stroke-width="2"
97
+ stroke-linecap="round"
98
+ />
99
+ </svg>
100
+ <svg
101
+ v-else-if="checked"
102
+ class="strand-checkbox__icon"
103
+ viewBox="0 0 16 16"
104
+ fill="none"
105
+ >
106
+ <path
107
+ d="M3.5 8L6.5 11L12.5 5"
108
+ stroke="currentColor"
109
+ stroke-width="2"
110
+ stroke-linecap="round"
111
+ stroke-linejoin="round"
112
+ />
113
+ </svg>
114
+ </span>
115
+ <span v-if="label" class="strand-checkbox__label">{{ label }}</span>
116
+ </label>
117
+ </template>
@@ -0,0 +1,2 @@
1
+ /*! Strand Vue | MIT License | dillingerstaffing.com */
2
+ export { default as Checkbox } from './Checkbox.vue'
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render } from '@testing-library/vue'
3
+ import Container from './Container.vue'
4
+
5
+ describe('Container', () => {
6
+ // ── Rendering ──
7
+
8
+ it('renders a div element', () => {
9
+ const { container } = render(Container, { slots: { default: 'Content' } })
10
+ expect(container.firstElementChild?.tagName).toBe('DIV')
11
+ })
12
+
13
+ it('renders slot content', () => {
14
+ const { getByText } = render(Container, { slots: { default: 'Hello' } })
15
+ expect(getByText('Hello')).toBeTruthy()
16
+ })
17
+
18
+ // ── Sizes ──
19
+
20
+ it('applies default size class by default', () => {
21
+ const { container } = render(Container, { slots: { default: 'Test' } })
22
+ expect(container.firstElementChild?.className).toContain('strand-container--default')
23
+ })
24
+
25
+ it('applies narrow size class', () => {
26
+ const { container } = render(Container, {
27
+ props: { size: 'narrow' },
28
+ slots: { default: 'Test' },
29
+ })
30
+ expect(container.firstElementChild?.className).toContain('strand-container--narrow')
31
+ })
32
+
33
+ it('applies wide size class', () => {
34
+ const { container } = render(Container, {
35
+ props: { size: 'wide' },
36
+ slots: { default: 'Test' },
37
+ })
38
+ expect(container.firstElementChild?.className).toContain('strand-container--wide')
39
+ })
40
+
41
+ it('applies full size class', () => {
42
+ const { container } = render(Container, {
43
+ props: { size: 'full' },
44
+ slots: { default: 'Test' },
45
+ })
46
+ expect(container.firstElementChild?.className).toContain('strand-container--full')
47
+ })
48
+
49
+ // ── Custom className ──
50
+
51
+ it('merges custom className', () => {
52
+ const { container } = render(Container, {
53
+ props: { className: 'custom' },
54
+ slots: { default: 'Test' },
55
+ })
56
+ const el = container.firstElementChild
57
+ expect(el?.className).toContain('strand-container')
58
+ expect(el?.className).toContain('custom')
59
+ })
60
+
61
+ // ── Props forwarding ──
62
+
63
+ it('forwards additional attributes', () => {
64
+ const { container } = render(Container, {
65
+ attrs: { id: 'main-container' },
66
+ slots: { default: 'Test' },
67
+ })
68
+ expect(container.firstElementChild?.getAttribute('id')).toBe('main-container')
69
+ })
70
+ })
@@ -0,0 +1,32 @@
1
+ <!--! Strand Vue | MIT License | dillingerstaffing.com -->
2
+ <script setup lang="ts">
3
+ import { computed } from 'vue'
4
+
5
+ interface Props {
6
+ /** Max-width constraint */
7
+ size?: 'narrow' | 'default' | 'wide' | 'full'
8
+ /** Additional CSS class */
9
+ className?: string
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ size: 'default',
14
+ className: '',
15
+ })
16
+
17
+ const classes = computed(() =>
18
+ [
19
+ 'strand-container',
20
+ `strand-container--${props.size}`,
21
+ props.className,
22
+ ]
23
+ .filter(Boolean)
24
+ .join(' '),
25
+ )
26
+ </script>
27
+
28
+ <template>
29
+ <div :class="classes" v-bind="$attrs">
30
+ <slot />
31
+ </div>
32
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Container } from './Container.vue'
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render } from '@testing-library/vue'
3
+ import DataReadout from './DataReadout.vue'
4
+
5
+ describe('DataReadout', () => {
6
+ // ── Rendering ──
7
+
8
+ it('renders a div element', () => {
9
+ const { container } = render(DataReadout, {
10
+ props: { label: 'Revenue', value: '$1.2M' },
11
+ })
12
+ expect(container.firstElementChild?.tagName).toBe('DIV')
13
+ })
14
+
15
+ it('applies strand-data-readout base class', () => {
16
+ const { container } = render(DataReadout, {
17
+ props: { label: 'Revenue', value: '$1.2M' },
18
+ })
19
+ expect(container.firstElementChild?.className).toContain('strand-data-readout')
20
+ })
21
+
22
+ // ── Label and value ──
23
+
24
+ it('renders label text', () => {
25
+ const { container } = render(DataReadout, {
26
+ props: { label: 'Revenue', value: '$1.2M' },
27
+ })
28
+ const label = container.querySelector('.strand-data-readout__label')
29
+ expect(label?.textContent).toBe('Revenue')
30
+ })
31
+
32
+ it('renders string value', () => {
33
+ const { container } = render(DataReadout, {
34
+ props: { label: 'Revenue', value: '$1.2M' },
35
+ })
36
+ const val = container.querySelector('.strand-data-readout__value')
37
+ expect(val?.textContent).toBe('$1.2M')
38
+ })
39
+
40
+ it('renders numeric value', () => {
41
+ const { container } = render(DataReadout, {
42
+ props: { label: 'Count', value: 42 },
43
+ })
44
+ const val = container.querySelector('.strand-data-readout__value')
45
+ expect(val?.textContent).toBe('42')
46
+ })
47
+
48
+ // ── Sizes ──
49
+
50
+ it('does not apply size class for md (default)', () => {
51
+ const { container } = render(DataReadout, {
52
+ props: { label: 'Test', value: '0', size: 'md' },
53
+ })
54
+ expect(container.firstElementChild?.className).not.toContain('strand-data-readout--md')
55
+ })
56
+
57
+ it('does not apply size class when size is omitted', () => {
58
+ const { container } = render(DataReadout, {
59
+ props: { label: 'Test', value: '0' },
60
+ })
61
+ expect(container.firstElementChild?.className).not.toContain('strand-data-readout--sm')
62
+ expect(container.firstElementChild?.className).not.toContain('strand-data-readout--lg')
63
+ })
64
+
65
+ it('applies sm size class', () => {
66
+ const { container } = render(DataReadout, {
67
+ props: { label: 'Test', value: '0', size: 'sm' },
68
+ })
69
+ expect(container.firstElementChild?.className).toContain('strand-data-readout--sm')
70
+ })
71
+
72
+ it('applies lg size class', () => {
73
+ const { container } = render(DataReadout, {
74
+ props: { label: 'Test', value: '0', size: 'lg' },
75
+ })
76
+ expect(container.firstElementChild?.className).toContain('strand-data-readout--lg')
77
+ })
78
+
79
+ // ── Custom className ──
80
+
81
+ it('merges custom className', () => {
82
+ const { container } = render(DataReadout, {
83
+ props: { label: 'Test', value: '0', className: 'custom' },
84
+ })
85
+ const el = container.firstElementChild
86
+ expect(el?.className).toContain('strand-data-readout')
87
+ expect(el?.className).toContain('custom')
88
+ })
89
+
90
+ // ── Props forwarding ──
91
+
92
+ it('forwards additional attributes', () => {
93
+ const { container } = render(DataReadout, {
94
+ props: { label: 'Test', value: '0' },
95
+ attrs: { id: 'my-readout' },
96
+ })
97
+ expect(container.firstElementChild?.getAttribute('id')).toBe('my-readout')
98
+ })
99
+ })
@@ -0,0 +1,36 @@
1
+ <!--! Strand Vue | MIT License | dillingerstaffing.com -->
2
+ <script setup lang="ts">
3
+ import { computed } from 'vue'
4
+
5
+ interface Props {
6
+ /** Overline label text */
7
+ label: string
8
+ /** The large displayed value */
9
+ value: string | number
10
+ /** Size variant: sm (compact), md (default), lg (hero) */
11
+ size?: 'sm' | 'md' | 'lg'
12
+ /** Additional CSS class */
13
+ className?: string
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ className: '',
18
+ })
19
+
20
+ const classes = computed(() =>
21
+ [
22
+ 'strand-data-readout',
23
+ props.size && props.size !== 'md' ? `strand-data-readout--${props.size}` : '',
24
+ props.className,
25
+ ]
26
+ .filter(Boolean)
27
+ .join(' '),
28
+ )
29
+ </script>
30
+
31
+ <template>
32
+ <div :class="classes" v-bind="$attrs">
33
+ <span class="strand-data-readout__label">{{ label }}</span>
34
+ <span class="strand-data-readout__value">{{ value }}</span>
35
+ </div>
36
+ </template>
@@ -0,0 +1 @@
1
+ export { default as DataReadout } from './DataReadout.vue'