@skewedaspect/sleekspace-ui 0.9.1 → 0.10.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 (200) hide show
  1. package/dist/components/Accordion/context.d.ts +4 -0
  2. package/dist/components/Autocomplete/SkAutocomplete.vue.d.ts +87 -0
  3. package/dist/components/Autocomplete/SkAutocompleteEmpty.vue.d.ts +17 -0
  4. package/dist/components/Autocomplete/SkAutocompleteGroup.vue.d.ts +17 -0
  5. package/dist/components/Autocomplete/SkAutocompleteGroupLabel.vue.d.ts +17 -0
  6. package/dist/components/Autocomplete/SkAutocompleteItem.vue.d.ts +39 -0
  7. package/dist/components/Autocomplete/SkAutocompleteSeparator.vue.d.ts +2 -0
  8. package/dist/components/Autocomplete/index.d.ts +7 -0
  9. package/dist/components/Autocomplete/types.d.ts +3 -0
  10. package/dist/components/Breadcrumbs/context.d.ts +4 -0
  11. package/dist/components/Button/SkButton.vue.d.ts +8 -1
  12. package/dist/components/Button/types.d.ts +2 -0
  13. package/dist/components/Card/SkCard.vue.d.ts +1 -1
  14. package/dist/components/ContextMenu/context.d.ts +3 -0
  15. package/dist/components/Dropdown/SkDropdown.vue.d.ts +1 -1
  16. package/dist/components/Dropdown/context.d.ts +3 -0
  17. package/dist/components/Field/SkField.vue.d.ts +7 -6
  18. package/dist/components/Input/SkInput.vue.d.ts +9 -2
  19. package/dist/components/Input/types.d.ts +2 -0
  20. package/dist/components/InputGroup/SkInputGroup.vue.d.ts +23 -0
  21. package/dist/components/InputGroup/SkInputGroupAddon.vue.d.ts +33 -0
  22. package/dist/components/InputGroup/types.d.ts +13 -0
  23. package/dist/components/NumberInput/SkNumberInput.vue.d.ts +7 -1
  24. package/dist/components/NumberInput/types.d.ts +2 -0
  25. package/dist/components/Pagination/context.d.ts +5 -0
  26. package/dist/components/Panel/SkPanel.vue.d.ts +1 -1
  27. package/dist/components/Panel/types.d.ts +2 -1
  28. package/dist/components/Radio/context.d.ts +4 -0
  29. package/dist/components/Select/SkSelect.vue.d.ts +7 -1
  30. package/dist/components/Select/types.d.ts +2 -0
  31. package/dist/components/Sidebar/SkSidebar.vue.d.ts +1 -1
  32. package/dist/components/Tabs/context.d.ts +6 -0
  33. package/dist/components/Textarea/SkTextarea.vue.d.ts +1 -1
  34. package/dist/components/Tooltip/SkTooltip.vue.d.ts +1 -1
  35. package/dist/composables/injectionKeys.d.ts +9 -0
  36. package/dist/global.d.ts +4 -0
  37. package/dist/index.d.ts +18 -0
  38. package/dist/sleekspace-ui.css +831 -277
  39. package/dist/sleekspace-ui.es.js +3693 -2514
  40. package/dist/sleekspace-ui.umd.js +3700 -2513
  41. package/dist/static/components/alert.d.ts +2 -1
  42. package/dist/static/components/avatar.d.ts +2 -1
  43. package/dist/static/components/breadcrumbs.d.ts +2 -1
  44. package/dist/static/components/button.d.ts +4 -2
  45. package/dist/static/components/card.d.ts +2 -1
  46. package/dist/static/components/checkbox.d.ts +2 -1
  47. package/dist/static/components/colorPicker.d.ts +2 -1
  48. package/dist/static/components/divider.d.ts +2 -1
  49. package/dist/static/components/dropdown.d.ts +2 -1
  50. package/dist/static/components/field.d.ts +2 -1
  51. package/dist/static/components/group.d.ts +2 -1
  52. package/dist/static/components/input.d.ts +4 -2
  53. package/dist/static/components/inputGroup.d.ts +8 -0
  54. package/dist/static/components/inputGroupAddon.d.ts +7 -0
  55. package/dist/static/components/navBar.d.ts +2 -1
  56. package/dist/static/components/numberInput.d.ts +4 -2
  57. package/dist/static/components/page.d.ts +2 -1
  58. package/dist/static/components/pagination.d.ts +2 -1
  59. package/dist/static/components/panel.d.ts +2 -1
  60. package/dist/static/components/progress.d.ts +2 -1
  61. package/dist/static/components/radio.d.ts +2 -1
  62. package/dist/static/components/select.d.ts +4 -2
  63. package/dist/static/components/sidebar.d.ts +2 -1
  64. package/dist/static/components/skeleton.d.ts +2 -1
  65. package/dist/static/components/slider.d.ts +2 -1
  66. package/dist/static/components/spinner.d.ts +2 -1
  67. package/dist/static/components/switchInput.d.ts +2 -1
  68. package/dist/static/components/table.d.ts +2 -1
  69. package/dist/static/components/tag.d.ts +2 -1
  70. package/dist/static/components/tagsInput.d.ts +2 -1
  71. package/dist/static/components/textarea.d.ts +2 -1
  72. package/dist/static/components/toolbar.d.ts +2 -1
  73. package/dist/static/components/tooltip.d.ts +2 -1
  74. package/dist/static/h.d.ts +2 -0
  75. package/dist/static/index.cjs.js +1 -1
  76. package/dist/static/index.d.ts +6 -0
  77. package/dist/static/index.es.js +366 -216
  78. package/dist/static/render.d.ts +2 -1
  79. package/dist/static/stringH.d.ts +2 -0
  80. package/dist/static/types.d.ts +5 -0
  81. package/dist/tailwind.css +222 -0
  82. package/dist/tokens.css +0 -223
  83. package/dist/types/corners.d.ts +1 -0
  84. package/llms-full.txt +14 -9
  85. package/package.json +6 -3
  86. package/src/components/Accordion/SkAccordion.vue +5 -2
  87. package/src/components/Accordion/SkAccordionItem.vue +7 -4
  88. package/src/components/Accordion/context.ts +23 -0
  89. package/src/components/Autocomplete/SkAutocomplete.test.ts +83 -0
  90. package/src/components/Autocomplete/SkAutocomplete.vue +305 -0
  91. package/src/components/Autocomplete/SkAutocompleteEmpty.vue +39 -0
  92. package/src/components/Autocomplete/SkAutocompleteGroup.vue +46 -0
  93. package/src/components/Autocomplete/SkAutocompleteGroupLabel.vue +39 -0
  94. package/src/components/Autocomplete/SkAutocompleteItem.vue +85 -0
  95. package/src/components/Autocomplete/SkAutocompleteSeparator.vue +39 -0
  96. package/src/components/Autocomplete/index.ts +13 -0
  97. package/src/components/Autocomplete/types.ts +10 -0
  98. package/src/components/Breadcrumbs/SkBreadcrumbItem.vue +8 -3
  99. package/src/components/Breadcrumbs/SkBreadcrumbSeparator.vue +8 -2
  100. package/src/components/Breadcrumbs/SkBreadcrumbs.vue +5 -2
  101. package/src/components/Breadcrumbs/context.ts +20 -0
  102. package/src/components/Button/SkButton.vue +46 -6
  103. package/src/components/Button/types.ts +6 -0
  104. package/src/components/ColorPicker/SkColorPicker.vue +27 -5
  105. package/src/components/ContextMenu/SkContextMenu.vue +4 -1
  106. package/src/components/ContextMenu/SkContextMenuSubmenu.vue +5 -2
  107. package/src/components/ContextMenu/context.ts +17 -0
  108. package/src/components/Dropdown/SkDropdown.vue +2 -1
  109. package/src/components/Dropdown/SkDropdownSubmenu.vue +4 -3
  110. package/src/components/Dropdown/context.ts +16 -0
  111. package/src/components/Field/SkField.test.ts +88 -0
  112. package/src/components/Field/SkField.vue +15 -7
  113. package/src/components/Input/SkInput.test.ts +61 -0
  114. package/src/components/Input/SkInput.vue +42 -7
  115. package/src/components/Input/types.ts +2 -0
  116. package/src/components/InputGroup/SkInputGroup.test.ts +171 -0
  117. package/src/components/InputGroup/SkInputGroup.vue +131 -0
  118. package/src/components/InputGroup/SkInputGroupAddon.test.ts +104 -0
  119. package/src/components/InputGroup/SkInputGroupAddon.vue +107 -0
  120. package/src/components/InputGroup/types.ts +27 -0
  121. package/src/components/Listbox/SkListbox.vue +27 -6
  122. package/src/components/NumberInput/SkNumberInput.vue +39 -7
  123. package/src/components/NumberInput/types.ts +2 -0
  124. package/src/components/Pagination/SkPagination.vue +6 -3
  125. package/src/components/Pagination/SkPaginationItem.vue +8 -5
  126. package/src/components/Pagination/context.ts +19 -0
  127. package/src/components/Panel/types.ts +3 -2
  128. package/src/components/Radio/SkRadio.vue +6 -3
  129. package/src/components/Radio/SkRadioGroup.vue +4 -2
  130. package/src/components/Radio/context.ts +17 -0
  131. package/src/components/Select/SkSelect.vue +39 -7
  132. package/src/components/Select/types.ts +2 -0
  133. package/src/components/Tabs/SkTab.vue +4 -2
  134. package/src/components/Tabs/SkTabList.vue +4 -2
  135. package/src/components/Tabs/SkTabs.vue +5 -3
  136. package/src/components/Tabs/context.ts +19 -0
  137. package/src/components/TagsInput/SkTagsInput.vue +28 -7
  138. package/src/components/Textarea/SkTextarea.vue +27 -6
  139. package/src/composables/injectionKeys.ts +52 -0
  140. package/src/index.ts +28 -0
  141. package/src/static/__tests__/parity.test.ts +2 -1
  142. package/src/static/__tests__/parityHarness.ts +5 -2
  143. package/src/static/components/__tests__/helpers.test.ts +191 -99
  144. package/src/static/components/alert.ts +12 -11
  145. package/src/static/components/avatar.ts +15 -16
  146. package/src/static/components/breadcrumbs.ts +3 -2
  147. package/src/static/components/button.ts +23 -27
  148. package/src/static/components/card.ts +3 -2
  149. package/src/static/components/checkbox.ts +11 -14
  150. package/src/static/components/colorPicker.ts +7 -9
  151. package/src/static/components/divider.ts +4 -3
  152. package/src/static/components/dropdown.ts +15 -6
  153. package/src/static/components/field.ts +32 -15
  154. package/src/static/components/group.ts +3 -2
  155. package/src/static/components/input.ts +20 -15
  156. package/src/static/components/inputGroup.ts +30 -0
  157. package/src/static/components/inputGroupAddon.ts +29 -0
  158. package/src/static/components/navBar.ts +30 -17
  159. package/src/static/components/numberInput.ts +17 -17
  160. package/src/static/components/page.ts +3 -2
  161. package/src/static/components/pagination.ts +3 -2
  162. package/src/static/components/panel.ts +3 -2
  163. package/src/static/components/progress.ts +3 -2
  164. package/src/static/components/radio.ts +14 -20
  165. package/src/static/components/select.ts +18 -15
  166. package/src/static/components/sidebar.ts +9 -13
  167. package/src/static/components/skeleton.ts +7 -10
  168. package/src/static/components/slider.ts +7 -9
  169. package/src/static/components/spinner.ts +22 -22
  170. package/src/static/components/switchInput.ts +12 -14
  171. package/src/static/components/table.ts +8 -10
  172. package/src/static/components/tag.ts +17 -11
  173. package/src/static/components/tagsInput.ts +3 -3
  174. package/src/static/components/textarea.ts +8 -13
  175. package/src/static/components/toolbar.ts +7 -10
  176. package/src/static/components/tooltip.ts +3 -2
  177. package/src/static/generated/defaults.ts +24 -9
  178. package/src/static/generated/propTypes.ts +18 -2
  179. package/src/static/h.ts +16 -0
  180. package/src/static/index.ts +8 -0
  181. package/src/static/render.test.ts +14 -10
  182. package/src/static/render.ts +33 -18
  183. package/src/static/specs.test.ts +1 -0
  184. package/src/static/specs.ts +22 -2
  185. package/src/static/stringH.ts +104 -0
  186. package/src/static/types.ts +25 -0
  187. package/src/styles/components/_autocomplete.scss +498 -0
  188. package/src/styles/components/_button.scss +55 -6
  189. package/src/styles/components/_index.scss +2 -0
  190. package/src/styles/components/_input-group.scss +292 -0
  191. package/src/styles/components/_input.scss +57 -9
  192. package/src/styles/components/_number-input.scss +84 -18
  193. package/src/styles/components/_select.scss +56 -9
  194. package/src/styles/mixins/_cut-border.scss +83 -0
  195. package/src/styles/tailwind.scss +262 -0
  196. package/src/styles/tokens.scss +8 -255
  197. package/src/types/corners.ts +10 -0
  198. package/src/utils/slots.test.ts +89 -0
  199. package/src/utils/slots.ts +6 -1
  200. package/web-types.json +382 -12
@@ -0,0 +1,61 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // SkInput — input-group-size inject tests
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import { mount } from '@vue/test-utils';
6
+ import { computed, defineComponent, h, provide } from 'vue';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ import { inputGroupSizeKey } from '@/composables/injectionKeys';
10
+
11
+ import SkInput from './SkInput.vue';
12
+
13
+ //----------------------------------------------------------------------------------------------------------------------
14
+
15
+ describe('SkInput input-group-size inject', () =>
16
+ {
17
+ it('uses injected size when no explicit size prop is set', () =>
18
+ {
19
+ const Provider = defineComponent({
20
+ setup(_, { slots })
21
+ {
22
+ provide(inputGroupSizeKey, computed(() => 'lg'));
23
+ return () => h('div', slots.default ? slots.default() : []);
24
+ },
25
+ });
26
+
27
+ const wrapper = mount(Provider, {
28
+ slots: { default: () => h(SkInput) },
29
+ });
30
+
31
+ const input = wrapper.find('input.sk-input');
32
+ expect(input.classes()).toContain('sk-lg');
33
+ });
34
+
35
+ it('explicit size prop overrides injected size', () =>
36
+ {
37
+ const Provider = defineComponent({
38
+ setup(_, { slots })
39
+ {
40
+ provide(inputGroupSizeKey, computed(() => 'lg'));
41
+ return () => h('div', slots.default ? slots.default() : []);
42
+ },
43
+ });
44
+
45
+ const wrapper = mount(Provider, {
46
+ slots: { default: () => h(SkInput, { size: 'sm' }) },
47
+ });
48
+
49
+ const input = wrapper.find('input.sk-input');
50
+ expect(input.classes()).toContain('sk-sm');
51
+ expect(input.classes()).not.toContain('sk-lg');
52
+ });
53
+
54
+ it('falls back to md when neither prop nor inject is set', () =>
55
+ {
56
+ const wrapper = mount(SkInput);
57
+ expect(wrapper.classes()).toContain('sk-md');
58
+ });
59
+ });
60
+
61
+ //----------------------------------------------------------------------------------------------------------------------
@@ -43,13 +43,20 @@
43
43
  * ```
44
44
  */
45
45
 
46
- import { type ComputedRef, computed, inject, toRef } from 'vue';
46
+ import { computed, inject, toRef } from 'vue';
47
47
 
48
48
  // Types
49
49
  import type { ComponentCustomColors } from '@/types';
50
- import type { SkInputKind, SkInputSize, SkInputType } from './types';
50
+ import type { SkInputCorner, SkInputKind, SkInputSize, SkInputType } from './types';
51
51
 
52
52
  // Composables
53
+ import {
54
+ NO_KIND,
55
+ NO_SIZE,
56
+ inheritedKindKey,
57
+ inputGroupSizeKey,
58
+ validationKindKey,
59
+ } from '@/composables/injectionKeys';
53
60
  import { useCustomColors } from '@/composables/useCustomColors';
54
61
 
55
62
  //------------------------------------------------------------------------------------------------------------------
@@ -79,6 +86,13 @@
79
86
  */
80
87
  size ?: SkInputSize;
81
88
 
89
+ /**
90
+ * Which corners receive the beveled cut. Pass an empty array for square corners.
91
+ * When omitted, defaults to the input's standalone visual (`top-right` only).
92
+ * @default undefined (renders as ['top-right'])
93
+ */
94
+ corners ?: SkInputCorner[];
95
+
82
96
  /**
83
97
  * Placeholder text displayed when the input is empty. Use to provide hints about expected
84
98
  * input format or example values. The placeholder disappears when the user begins typing.
@@ -131,7 +145,8 @@
131
145
  const props = withDefaults(defineProps<SkInputComponentProps>(), {
132
146
  type: 'text',
133
147
  kind: undefined,
134
- size: 'md',
148
+ size: undefined,
149
+ corners: undefined,
135
150
  placeholder: undefined,
136
151
  disabled: false,
137
152
  readonly: false,
@@ -151,20 +166,40 @@
151
166
 
152
167
  //------------------------------------------------------------------------------------------------------------------
153
168
 
154
- // Inject kind from parent SkField if available
155
- const fieldKind = inject<ComputedRef<SkInputKind | undefined>>('field-kind', computed(() => undefined));
169
+ // Validation override (SkField with `state` set) always wins. Inherited kind (an ambient
170
+ // provider like SkInputGroup) is the default when the dev didn't set `kind` themselves.
171
+ const validationKind = inject(validationKindKey, NO_KIND);
172
+ const inheritedKind = inject(inheritedKindKey, NO_KIND);
173
+ const inputGroupSize = inject(inputGroupSizeKey, NO_SIZE);
156
174
 
157
175
  //------------------------------------------------------------------------------------------------------------------
158
176
 
159
- const effectiveKind = computed(() => fieldKind.value || props.kind || 'neutral');
177
+ const effectiveKind = computed<SkInputKind>(() =>
178
+ {
179
+ if(validationKind.value !== undefined) { return validationKind.value; }
180
+ if(props.kind !== undefined) { return props.kind; }
181
+ if(inheritedKind.value !== undefined) { return inheritedKind.value; }
182
+ return 'neutral';
183
+ });
184
+
185
+ const effectiveSize = computed<SkInputSize>(() =>
186
+ {
187
+ if(props.size !== undefined) { return props.size; }
188
+ if(inputGroupSize.value !== undefined) { return inputGroupSize.value; }
189
+ return 'md';
190
+ });
160
191
 
161
192
  //------------------------------------------------------------------------------------------------------------------
162
193
 
163
194
  const classes = computed(() => ({
164
195
  'sk-input': true,
165
196
  [`sk-${ effectiveKind.value }`]: true,
166
- [`sk-${ props.size }`]: true,
197
+ [`sk-${ effectiveSize.value }`]: true,
167
198
  'sk-readonly': props.readonly,
199
+ 'sk-cut-top-left': props.corners?.includes('top-left') ?? false,
200
+ 'sk-cut-top-right': props.corners?.includes('top-right') ?? false,
201
+ 'sk-cut-bottom-right': props.corners?.includes('bottom-right') ?? false,
202
+ 'sk-cut-bottom-left': props.corners?.includes('bottom-left') ?? false,
168
203
  }));
169
204
 
170
205
  //------------------------------------------------------------------------------------------------------------------
@@ -3,9 +3,11 @@
3
3
  //----------------------------------------------------------------------------------------------------------------------
4
4
 
5
5
  import type { ComponentKind, ComponentSize } from '@/types';
6
+ import type { SkCorner } from '@/types/corners';
6
7
 
7
8
  export type SkInputKind = ComponentKind;
8
9
  export type SkInputSize = ComponentSize;
9
10
  export type SkInputType = 'text' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'number';
11
+ export type SkInputCorner = SkCorner;
10
12
 
11
13
  //----------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,171 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // SkInputGroup Tests
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import { computed, defineComponent, h, inject, provide } from 'vue';
6
+ import { mount } from '@vue/test-utils';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ import { inheritedKindKey, validationKindKey } from '@/composables/injectionKeys';
10
+
11
+ import SkInputGroup from './SkInputGroup.vue';
12
+ import SkInputGroupAddon from './SkInputGroupAddon.vue';
13
+
14
+ //----------------------------------------------------------------------------------------------------------------------
15
+
16
+ describe('SkInputGroup', () =>
17
+ {
18
+ it('renders a div with the group class and default classes', () =>
19
+ {
20
+ const wrapper = mount(SkInputGroup);
21
+ const div = wrapper.find('div.sk-input-group');
22
+ expect(div.exists()).toBe(true);
23
+ expect(div.classes()).toContain('sk-md');
24
+ expect(div.classes()).toContain('sk-neutral');
25
+ });
26
+
27
+ it('applies corner classes from corners prop', () =>
28
+ {
29
+ const wrapper = mount(SkInputGroup, {
30
+ props: { corners: [ 'top-left', 'bottom-right' ] },
31
+ });
32
+ const div = wrapper.find('div.sk-input-group');
33
+ expect(div.classes()).toContain('sk-cut-top-left');
34
+ expect(div.classes()).toContain('sk-cut-bottom-right');
35
+ expect(div.classes()).not.toContain('sk-cut-top-right');
36
+ expect(div.classes()).not.toContain('sk-cut-bottom-left');
37
+ });
38
+
39
+ it('provides input-group-size to children', () =>
40
+ {
41
+ const wrapper = mount(SkInputGroup, {
42
+ props: { size: 'lg' },
43
+ slots: { default: () => h(SkInputGroupAddon, null, { default: () => '$' }) },
44
+ });
45
+ const addon = wrapper.find('span.sk-input-group-addon');
46
+ expect(addon.classes()).toContain('sk-lg');
47
+ });
48
+
49
+ it('provides inherited-kind to children', () =>
50
+ {
51
+ const wrapper = mount(SkInputGroup, {
52
+ props: { kind: 'primary' },
53
+ slots: { default: () => h(SkInputGroupAddon, null, { default: () => '$' }) },
54
+ });
55
+ const addon = wrapper.find('span.sk-input-group-addon');
56
+ expect(addon.classes()).toContain('sk-primary');
57
+ });
58
+
59
+ it('inherits ambient kind from a parent inherited-kind provider', () =>
60
+ {
61
+ const AmbientStub = defineComponent({
62
+ setup(_, { slots })
63
+ {
64
+ provide(inheritedKindKey, computed(() => 'accent'));
65
+ return () => h('div', slots.default ? slots.default() : []);
66
+ },
67
+ });
68
+
69
+ const wrapper = mount(AmbientStub, {
70
+ slots: {
71
+ default: () => h(SkInputGroup, null, {
72
+ default: () => h(SkInputGroupAddon, null, { default: () => '$' }),
73
+ }),
74
+ },
75
+ });
76
+
77
+ const group = wrapper.find('div.sk-input-group');
78
+ const addon = wrapper.find('span.sk-input-group-addon');
79
+ expect(group.classes()).toContain('sk-accent');
80
+ expect(addon.classes()).toContain('sk-accent');
81
+ });
82
+
83
+ it('explicit kind prop wins over ambient inherited-kind', () =>
84
+ {
85
+ const AmbientStub = defineComponent({
86
+ setup(_, { slots })
87
+ {
88
+ provide(inheritedKindKey, computed(() => 'accent'));
89
+ return () => h('div', slots.default ? slots.default() : []);
90
+ },
91
+ });
92
+
93
+ const wrapper = mount(AmbientStub, {
94
+ slots: {
95
+ default: () => h(SkInputGroup, { kind: 'success' }, {
96
+ default: () => h(SkInputGroupAddon, null, { default: () => '$' }),
97
+ }),
98
+ },
99
+ });
100
+
101
+ const group = wrapper.find('div.sk-input-group');
102
+ const addon = wrapper.find('span.sk-input-group-addon');
103
+ expect(group.classes()).toContain('sk-success');
104
+ expect(addon.classes()).toContain('sk-success');
105
+ expect(addon.classes()).not.toContain('sk-accent');
106
+ });
107
+
108
+ it('parent validation-kind always wins over explicit kind', () =>
109
+ {
110
+ const FieldStub = defineComponent({
111
+ setup(_, { slots })
112
+ {
113
+ provide(validationKindKey, computed(() => 'danger'));
114
+ return () => h('div', slots.default ? slots.default() : []);
115
+ },
116
+ });
117
+
118
+ const wrapper = mount(FieldStub, {
119
+ slots: {
120
+ default: () => h(SkInputGroup, { kind: 'success' }, {
121
+ default: () => h(SkInputGroupAddon, null, { default: () => '$' }),
122
+ }),
123
+ },
124
+ });
125
+
126
+ const group = wrapper.find('div.sk-input-group');
127
+ const addon = wrapper.find('span.sk-input-group-addon');
128
+ expect(group.classes()).toContain('sk-danger');
129
+ expect(addon.classes()).toContain('sk-danger');
130
+ });
131
+
132
+ it('re-provides inherited-kind from props.kind, not the validation-resolved kind', () =>
133
+ {
134
+ // The property under test: SkInputGroup's `inherited-kind` re-provide must use props.kind
135
+ // (or the parent's inherited-kind), NOT the validation-overridden effectiveKind. Otherwise
136
+ // a parent SkField's validation override would leak onto the inherited-kind channel and
137
+ // clobber descendants that consult only inherited-kind.
138
+ //
139
+ // To observe what the InputGroup actually puts on inherited-kind, we mount a synthetic
140
+ // probe inside the group that injects ONLY inherited-kind (no validation-kind) and
141
+ // surfaces the resolved value as a data attribute.
142
+ const Probe = defineComponent({
143
+ setup()
144
+ {
145
+ const seen = inject(inheritedKindKey, computed(() => 'NONE'));
146
+ return () => h('div', { 'data-seen': seen.value });
147
+ },
148
+ });
149
+
150
+ const FieldStub = defineComponent({
151
+ setup(_, { slots })
152
+ {
153
+ provide(validationKindKey, computed(() => 'danger'));
154
+ return () => h('div', slots.default ? slots.default() : []);
155
+ },
156
+ });
157
+
158
+ const wrapper = mount(FieldStub, {
159
+ slots: {
160
+ default: () => h(SkInputGroup, { kind: 'primary' }, {
161
+ default: () => h(Probe),
162
+ }),
163
+ },
164
+ });
165
+
166
+ // The probe sees 'primary' (props.kind), not 'danger' (validation override).
167
+ expect(wrapper.find('[data-seen]').attributes('data-seen')).toBe('primary');
168
+ });
169
+ });
170
+
171
+ //----------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,131 @@
1
+ <!----------------------------------------------------------------------------------------------------------------------
2
+ - InputGroup Component
3
+ --------------------------------------------------------------------------------------------------------------------->
4
+
5
+ <template>
6
+ <div :class="classes" :style="customColorStyles">
7
+ <slot />
8
+ </div>
9
+ </template>
10
+
11
+ <!--------------------------------------------------------------------------------------------------------------------->
12
+
13
+ <style lang="scss" scoped>
14
+ // Component styles are implemented in /src/styles/components/_input-group.scss
15
+ </style>
16
+
17
+ <!--------------------------------------------------------------------------------------------------------------------->
18
+
19
+ <script setup lang="ts">
20
+ /**
21
+ * @component SkInputGroup
22
+ * @description Wraps form inputs, buttons, selects, and SkInputGroupAddon children into a
23
+ * single visually unified control. Bevels appear only on the group's outer corners; interior
24
+ * joins are clean. Cascades kind and size to Vue children via provide/inject — size on the
25
+ * `input-group-size` channel, kind on the `inherited-kind` ambient-default channel. A
26
+ * parent SkField's validation override (the `validation-kind` channel) takes precedence over
27
+ * both the group's own kind and the descendants' kinds.
28
+ *
29
+ * @example
30
+ * ```vue
31
+ * <SkInputGroup :corners="['top-left', 'bottom-right']" size="md" kind="primary">
32
+ * <SkInputGroupAddon>$</SkInputGroupAddon>
33
+ * <SkInput v-model="amount" />
34
+ * <SkButton>Submit</SkButton>
35
+ * </SkInputGroup>
36
+ * ```
37
+ *
38
+ * @slot default - Form components, SkButton, and SkInputGroupAddon children in left-to-right order.
39
+ */
40
+
41
+ import { computed, inject, provide, toRef } from 'vue';
42
+
43
+ // Types
44
+ import type {
45
+ SkInputGroupBaseProps,
46
+ SkInputGroupCorner,
47
+ SkInputGroupKind,
48
+ SkInputGroupSize,
49
+ } from './types';
50
+
51
+ export type { SkInputGroupComponentProps } from './types';
52
+
53
+ // Composables
54
+ import {
55
+ NO_KIND,
56
+ NO_SIZE,
57
+ inheritedKindKey,
58
+ inputGroupSizeKey,
59
+ validationKindKey,
60
+ } from '@/composables/injectionKeys';
61
+ import { useCustomColors } from '@/composables/useCustomColors';
62
+
63
+ //------------------------------------------------------------------------------------------------------------------
64
+
65
+ const props = withDefaults(defineProps<SkInputGroupBaseProps>(), {
66
+ kind: undefined,
67
+ size: 'md',
68
+ corners: () => [ 'top-left', 'bottom-right' ],
69
+ });
70
+
71
+ //------------------------------------------------------------------------------------------------------------------
72
+
73
+ // Same resolution rule as the leaf form components: validation override (from a parent
74
+ // SkField with `state` set) wins, then explicit prop, then ambient inherited-kind, then
75
+ // 'neutral'. Validation-kind passes through to descendants on its own channel — we never
76
+ // re-provide it.
77
+ const validationKind = inject(validationKindKey, NO_KIND);
78
+ const inheritedKind = inject(inheritedKindKey, NO_KIND);
79
+
80
+ const effectiveKind = computed<SkInputGroupKind>(() =>
81
+ {
82
+ if(validationKind.value !== undefined) { return validationKind.value; }
83
+ if(props.kind !== undefined) { return props.kind; }
84
+ if(inheritedKind.value !== undefined) { return inheritedKind.value; }
85
+ return 'neutral';
86
+ });
87
+
88
+ //------------------------------------------------------------------------------------------------------------------
89
+
90
+ // Re-provide on the inherited-kind channel so descendants pick up the group's prop-driven
91
+ // kind as their default. Use the prop directly (not the resolved value) so a parent SkField's
92
+ // validation override doesn't propagate as the group's "ambient default" and clobber children
93
+ // that have their own explicit kind.
94
+ const ambientKind = computed<SkInputGroupKind | undefined>(() =>
95
+ {
96
+ if(props.kind !== undefined) { return props.kind; }
97
+ return inheritedKind.value;
98
+ });
99
+ provide(inheritedKindKey, ambientKind);
100
+
101
+ // Size passes straight through to the input-group-size channel — no resolution to do.
102
+ const sizeRef = toRef(props, 'size');
103
+ provide(inputGroupSizeKey, sizeRef);
104
+
105
+ //------------------------------------------------------------------------------------------------------------------
106
+
107
+ function hasCorner(corner : SkInputGroupCorner) : boolean
108
+ {
109
+ return props.corners.includes(corner);
110
+ }
111
+
112
+ const classes = computed<Record<string, boolean>>(() => ({
113
+ 'sk-input-group': true,
114
+ [`sk-${ effectiveKind.value }`]: true,
115
+ [`sk-${ sizeRef.value }`]: true,
116
+ 'sk-cut-top-left': hasCorner('top-left'),
117
+ 'sk-cut-top-right': hasCorner('top-right'),
118
+ 'sk-cut-bottom-right': hasCorner('bottom-right'),
119
+ 'sk-cut-bottom-left': hasCorner('bottom-left'),
120
+ }));
121
+
122
+ //------------------------------------------------------------------------------------------------------------------
123
+
124
+ const customColorStyles = useCustomColors(
125
+ 'input-group',
126
+ toRef(() => props.baseColor),
127
+ toRef(() => props.textColor)
128
+ );
129
+ </script>
130
+
131
+ <!--------------------------------------------------------------------------------------------------------------------->
@@ -0,0 +1,104 @@
1
+ import { type InjectionKey, computed, defineComponent, h, provide } from 'vue';
2
+ import { mount } from '@vue/test-utils';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { inheritedKindKey, inputGroupSizeKey, validationKindKey } from '@/composables/injectionKeys';
6
+
7
+ import SkInputGroupAddon from './SkInputGroupAddon.vue';
8
+
9
+ //----------------------------------------------------------------------------------------------------------------------
10
+
11
+ function withProvides(
12
+ children : () => ReturnType<typeof h>,
13
+ provides : Map<InjectionKey<unknown>, unknown> = new Map<InjectionKey<unknown>, unknown>()
14
+ ) : ReturnType<typeof defineComponent>
15
+ {
16
+ return defineComponent({
17
+ setup()
18
+ {
19
+ for(const [ key, value ] of provides)
20
+ {
21
+ provide(key, value);
22
+ }
23
+ return () => h('div', children());
24
+ },
25
+ });
26
+ }
27
+
28
+ //----------------------------------------------------------------------------------------------------------------------
29
+
30
+ describe('SkInputGroupAddon', () =>
31
+ {
32
+ it('renders a span with the addon class', () =>
33
+ {
34
+ const wrapper = mount(SkInputGroupAddon, { slots: { default: '$' } });
35
+ const span = wrapper.find('span.sk-input-group-addon');
36
+ expect(span.exists()).toBe(true);
37
+ expect(span.text()).toBe('$');
38
+ });
39
+
40
+ it('uses injected input-group-size when no explicit size is set', () =>
41
+ {
42
+ const Provider = withProvides(
43
+ () => h(SkInputGroupAddon, null, { default: () => '$' }),
44
+ new Map<InjectionKey<unknown>, unknown>([ [ inputGroupSizeKey, computed(() => 'lg') ] ])
45
+ );
46
+ const wrapper = mount(Provider);
47
+ const span = wrapper.find('span.sk-input-group-addon');
48
+ expect(span.classes()).toContain('sk-lg');
49
+ });
50
+
51
+ it('uses injected inherited-kind when no explicit kind is set', () =>
52
+ {
53
+ const Provider = withProvides(
54
+ () => h(SkInputGroupAddon, null, { default: () => '$' }),
55
+ new Map<InjectionKey<unknown>, unknown>([ [ inheritedKindKey, computed(() => 'primary') ] ])
56
+ );
57
+ const wrapper = mount(Provider);
58
+ const span = wrapper.find('span.sk-input-group-addon');
59
+ expect(span.classes()).toContain('sk-primary');
60
+ });
61
+
62
+ it('validation-kind always wins over explicit kind and inherited-kind', () =>
63
+ {
64
+ const Provider = withProvides(
65
+ () => h(SkInputGroupAddon, { kind: 'success' }, { default: () => '$' }),
66
+ new Map<InjectionKey<unknown>, unknown>([
67
+ [ validationKindKey, computed(() => 'danger') ],
68
+ [ inheritedKindKey, computed(() => 'primary') ],
69
+ ])
70
+ );
71
+ const wrapper = mount(Provider);
72
+ const span = wrapper.find('span.sk-input-group-addon');
73
+ expect(span.classes()).toContain('sk-danger');
74
+ expect(span.classes()).not.toContain('sk-success');
75
+ expect(span.classes()).not.toContain('sk-primary');
76
+ });
77
+
78
+ it('explicit props override inherited-kind and input-group-size', () =>
79
+ {
80
+ const Provider = withProvides(
81
+ () => h(SkInputGroupAddon, { size: 'sm', kind: 'danger' }, { default: () => '$' }),
82
+ new Map<InjectionKey<unknown>, unknown>([
83
+ [ inputGroupSizeKey, computed(() => 'lg') ],
84
+ [ inheritedKindKey, computed(() => 'primary') ],
85
+ ])
86
+ );
87
+ const wrapper = mount(Provider);
88
+ const span = wrapper.find('span.sk-input-group-addon');
89
+ expect(span.classes()).toContain('sk-sm');
90
+ expect(span.classes()).toContain('sk-danger');
91
+ expect(span.classes()).not.toContain('sk-lg');
92
+ expect(span.classes()).not.toContain('sk-primary');
93
+ });
94
+
95
+ it('defaults to md/neutral when nothing is provided', () =>
96
+ {
97
+ const wrapper = mount(SkInputGroupAddon);
98
+ const span = wrapper.find('span.sk-input-group-addon');
99
+ expect(span.classes()).toContain('sk-md');
100
+ expect(span.classes()).toContain('sk-neutral');
101
+ });
102
+ });
103
+
104
+ //----------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,107 @@
1
+ <!----------------------------------------------------------------------------------------------------------------------
2
+ - InputGroup Addon Component
3
+ --------------------------------------------------------------------------------------------------------------------->
4
+
5
+ <template>
6
+ <span :class="classes">
7
+ <slot />
8
+ </span>
9
+ </template>
10
+
11
+ <!--------------------------------------------------------------------------------------------------------------------->
12
+
13
+ <style lang="scss" scoped>
14
+ // Component styles are implemented in /src/styles/components/_input-group.scss
15
+ </style>
16
+
17
+ <!--------------------------------------------------------------------------------------------------------------------->
18
+
19
+ <script setup lang="ts">
20
+ /**
21
+ * @component SkInputGroupAddon
22
+ * @description Styled wrapper for non-component content (text, icons, plain HTML) inside an
23
+ * SkInputGroup. Renders as a span with the addon background, border, and font sizing matching
24
+ * the surrounding group. Inherits size and kind from the parent SkInputGroup via inject.
25
+ *
26
+ * @example
27
+ * ```vue
28
+ * <SkInputGroup>
29
+ * <SkInputGroupAddon>$</SkInputGroupAddon>
30
+ * <SkInput v-model="amount" />
31
+ * </SkInputGroup>
32
+ * ```
33
+ *
34
+ * @slot default - Addon content. Text, icons, or any inline element.
35
+ */
36
+
37
+ import { computed, inject } from 'vue';
38
+
39
+ // Types
40
+ import type { SkInputGroupAddonKind, SkInputGroupAddonSize } from './types';
41
+
42
+ // Composables
43
+ import {
44
+ NO_KIND,
45
+ NO_SIZE,
46
+ inheritedKindKey,
47
+ inputGroupSizeKey,
48
+ validationKindKey,
49
+ } from '@/composables/injectionKeys';
50
+
51
+ //------------------------------------------------------------------------------------------------------------------
52
+
53
+ export interface SkInputGroupAddonComponentProps
54
+ {
55
+ /**
56
+ * Semantic color kind. Inherits from a parent SkInputGroup or SkField when not set.
57
+ * @default inherited or 'neutral'
58
+ */
59
+ kind ?: SkInputGroupAddonKind;
60
+
61
+ /**
62
+ * Size of the addon. Inherits from a parent SkInputGroup when not set.
63
+ * @default inherited or 'md'
64
+ */
65
+ size ?: SkInputGroupAddonSize;
66
+ }
67
+
68
+ //------------------------------------------------------------------------------------------------------------------
69
+
70
+ const props = withDefaults(defineProps<SkInputGroupAddonComponentProps>(), {
71
+ kind: undefined,
72
+ size: undefined,
73
+ });
74
+
75
+ //------------------------------------------------------------------------------------------------------------------
76
+
77
+ const validationKind = inject(validationKindKey, NO_KIND);
78
+ const inheritedKind = inject(inheritedKindKey, NO_KIND);
79
+ const inputGroupSize = inject(inputGroupSizeKey, NO_SIZE);
80
+
81
+ //------------------------------------------------------------------------------------------------------------------
82
+
83
+ const effectiveKind = computed<SkInputGroupAddonKind>(() =>
84
+ {
85
+ if(validationKind.value !== undefined) { return validationKind.value; }
86
+ if(props.kind !== undefined) { return props.kind; }
87
+ if(inheritedKind.value !== undefined) { return inheritedKind.value; }
88
+ return 'neutral';
89
+ });
90
+
91
+ const effectiveSize = computed<SkInputGroupAddonSize>(() =>
92
+ {
93
+ if(props.size !== undefined) { return props.size; }
94
+ if(inputGroupSize.value !== undefined) { return inputGroupSize.value; }
95
+ return 'md';
96
+ });
97
+
98
+ //------------------------------------------------------------------------------------------------------------------
99
+
100
+ const classes = computed<Record<string, boolean>>(() => ({
101
+ 'sk-input-group-addon': true,
102
+ [`sk-${ effectiveKind.value }`]: true,
103
+ [`sk-${ effectiveSize.value }`]: true,
104
+ }));
105
+ </script>
106
+
107
+ <!--------------------------------------------------------------------------------------------------------------------->