@skewedaspect/sleekspace-ui 0.9.0 → 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 (210) 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 +15 -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 +836 -280
  39. package/dist/sleekspace-ui.es.js +3759 -2545
  40. package/dist/sleekspace-ui.umd.js +3765 -2543
  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/dist/utils/slots.d.ts +6 -0
  85. package/llms-full.txt +17 -9
  86. package/package.json +9 -3
  87. package/src/components/Accordion/SkAccordion.vue +5 -2
  88. package/src/components/Accordion/SkAccordionItem.vue +7 -4
  89. package/src/components/Accordion/context.ts +23 -0
  90. package/src/components/Alert/SkAlert.vue +4 -2
  91. package/src/components/Autocomplete/SkAutocomplete.test.ts +83 -0
  92. package/src/components/Autocomplete/SkAutocomplete.vue +305 -0
  93. package/src/components/Autocomplete/SkAutocompleteEmpty.vue +39 -0
  94. package/src/components/Autocomplete/SkAutocompleteGroup.vue +46 -0
  95. package/src/components/Autocomplete/SkAutocompleteGroupLabel.vue +39 -0
  96. package/src/components/Autocomplete/SkAutocompleteItem.vue +85 -0
  97. package/src/components/Autocomplete/SkAutocompleteSeparator.vue +39 -0
  98. package/src/components/Autocomplete/index.ts +13 -0
  99. package/src/components/Autocomplete/types.ts +10 -0
  100. package/src/components/Breadcrumbs/SkBreadcrumbItem.vue +8 -3
  101. package/src/components/Breadcrumbs/SkBreadcrumbSeparator.vue +8 -2
  102. package/src/components/Breadcrumbs/SkBreadcrumbs.vue +11 -14
  103. package/src/components/Breadcrumbs/context.ts +20 -0
  104. package/src/components/Button/SkButton.vue +54 -11
  105. package/src/components/Button/types.ts +6 -0
  106. package/src/components/Card/SkCard.vue +12 -5
  107. package/src/components/Checkbox/SkCheckbox.vue +9 -2
  108. package/src/components/ColorPicker/SkColorPicker.vue +27 -5
  109. package/src/components/ContextMenu/SkContextMenu.vue +4 -1
  110. package/src/components/ContextMenu/SkContextMenuSubmenu.vue +5 -2
  111. package/src/components/ContextMenu/context.ts +17 -0
  112. package/src/components/Dropdown/SkDropdown.vue +2 -1
  113. package/src/components/Dropdown/SkDropdownSubmenu.vue +4 -3
  114. package/src/components/Dropdown/context.ts +16 -0
  115. package/src/components/Field/SkField.test.ts +88 -0
  116. package/src/components/Field/SkField.vue +15 -7
  117. package/src/components/Input/SkInput.test.ts +61 -0
  118. package/src/components/Input/SkInput.vue +42 -7
  119. package/src/components/Input/types.ts +2 -0
  120. package/src/components/InputGroup/SkInputGroup.test.ts +171 -0
  121. package/src/components/InputGroup/SkInputGroup.vue +131 -0
  122. package/src/components/InputGroup/SkInputGroupAddon.test.ts +104 -0
  123. package/src/components/InputGroup/SkInputGroupAddon.vue +107 -0
  124. package/src/components/InputGroup/types.ts +27 -0
  125. package/src/components/Listbox/SkListbox.vue +27 -6
  126. package/src/components/Modal/SkModal.vue +11 -4
  127. package/src/components/NavBar/SkNavBar.vue +5 -4
  128. package/src/components/NumberInput/SkNumberInput.vue +49 -8
  129. package/src/components/NumberInput/types.ts +2 -0
  130. package/src/components/Page/SkPage.vue +18 -15
  131. package/src/components/Pagination/SkPagination.vue +6 -3
  132. package/src/components/Pagination/SkPaginationItem.vue +8 -5
  133. package/src/components/Pagination/context.ts +19 -0
  134. package/src/components/Panel/types.ts +3 -2
  135. package/src/components/Popover/SkPopover.vue +11 -4
  136. package/src/components/Radio/SkRadio.vue +14 -4
  137. package/src/components/Radio/SkRadioGroup.vue +4 -2
  138. package/src/components/Radio/context.ts +17 -0
  139. package/src/components/Select/SkSelect.vue +39 -7
  140. package/src/components/Select/types.ts +2 -0
  141. package/src/components/Switch/SkSwitch.vue +14 -13
  142. package/src/components/Tabs/SkTab.vue +10 -3
  143. package/src/components/Tabs/SkTabList.vue +4 -2
  144. package/src/components/Tabs/SkTabs.vue +5 -3
  145. package/src/components/Tabs/context.ts +19 -0
  146. package/src/components/TagsInput/SkTagsInput.vue +28 -7
  147. package/src/components/Textarea/SkTextarea.vue +27 -6
  148. package/src/components/TreeView/SkTreeItem.vue +10 -2
  149. package/src/composables/injectionKeys.ts +52 -0
  150. package/src/index.ts +28 -0
  151. package/src/static/__tests__/parity.test.ts +2 -1
  152. package/src/static/__tests__/parityHarness.ts +5 -2
  153. package/src/static/components/__tests__/helpers.test.ts +191 -99
  154. package/src/static/components/alert.ts +12 -11
  155. package/src/static/components/avatar.ts +15 -16
  156. package/src/static/components/breadcrumbs.ts +3 -2
  157. package/src/static/components/button.ts +23 -27
  158. package/src/static/components/card.ts +3 -2
  159. package/src/static/components/checkbox.ts +11 -14
  160. package/src/static/components/colorPicker.ts +7 -9
  161. package/src/static/components/divider.ts +4 -3
  162. package/src/static/components/dropdown.ts +15 -6
  163. package/src/static/components/field.ts +32 -15
  164. package/src/static/components/group.ts +3 -2
  165. package/src/static/components/input.ts +20 -15
  166. package/src/static/components/inputGroup.ts +30 -0
  167. package/src/static/components/inputGroupAddon.ts +29 -0
  168. package/src/static/components/navBar.ts +30 -17
  169. package/src/static/components/numberInput.ts +17 -17
  170. package/src/static/components/page.ts +3 -2
  171. package/src/static/components/pagination.ts +3 -2
  172. package/src/static/components/panel.ts +3 -2
  173. package/src/static/components/progress.ts +3 -2
  174. package/src/static/components/radio.ts +14 -20
  175. package/src/static/components/select.ts +18 -15
  176. package/src/static/components/sidebar.ts +9 -13
  177. package/src/static/components/skeleton.ts +7 -10
  178. package/src/static/components/slider.ts +7 -9
  179. package/src/static/components/spinner.ts +22 -22
  180. package/src/static/components/switchInput.ts +12 -14
  181. package/src/static/components/table.ts +8 -10
  182. package/src/static/components/tag.ts +17 -11
  183. package/src/static/components/tagsInput.ts +3 -3
  184. package/src/static/components/textarea.ts +8 -13
  185. package/src/static/components/toolbar.ts +7 -10
  186. package/src/static/components/tooltip.ts +3 -2
  187. package/src/static/generated/defaults.ts +25 -9
  188. package/src/static/generated/propTypes.ts +19 -2
  189. package/src/static/h.ts +16 -0
  190. package/src/static/index.ts +8 -0
  191. package/src/static/render.test.ts +14 -10
  192. package/src/static/render.ts +33 -18
  193. package/src/static/specs.test.ts +1 -0
  194. package/src/static/specs.ts +22 -2
  195. package/src/static/stringH.ts +104 -0
  196. package/src/static/types.ts +25 -0
  197. package/src/styles/components/_autocomplete.scss +498 -0
  198. package/src/styles/components/_button.scss +55 -6
  199. package/src/styles/components/_index.scss +2 -0
  200. package/src/styles/components/_input-group.scss +292 -0
  201. package/src/styles/components/_input.scss +57 -9
  202. package/src/styles/components/_number-input.scss +88 -14
  203. package/src/styles/components/_select.scss +56 -9
  204. package/src/styles/mixins/_cut-border.scss +83 -0
  205. package/src/styles/tailwind.scss +262 -0
  206. package/src/styles/tokens.scss +8 -255
  207. package/src/types/corners.ts +10 -0
  208. package/src/utils/slots.test.ts +89 -0
  209. package/src/utils/slots.ts +80 -0
  210. package/web-types.json +392 -12
@@ -94,6 +94,7 @@
94
94
 
95
95
  // Context
96
96
  import { NAVBAR_SIZE_KEY } from '../NavBar/context';
97
+ import { DROPDOWN_KIND_KEY } from './context';
97
98
 
98
99
  //------------------------------------------------------------------------------------------------------------------
99
100
 
@@ -170,7 +171,7 @@
170
171
  const effectiveSize = computed<SkDropdownSize>(() => props.size ?? navbarSize?.value ?? 'md');
171
172
 
172
173
  // Provide kind for submenus (reactive computed so changes propagate)
173
- provide('dropdown-kind', computed(() => props.kind));
174
+ provide(DROPDOWN_KIND_KEY, computed(() => props.kind));
174
175
 
175
176
  //------------------------------------------------------------------------------------------------------------------
176
177
 
@@ -75,6 +75,8 @@
75
75
  // Types
76
76
  import type { SkDropdownKind } from './types';
77
77
 
78
+ import { DROPDOWN_KIND_KEY } from './context';
79
+
78
80
  //------------------------------------------------------------------------------------------------------------------
79
81
 
80
82
  export interface SkDropdownSubmenuComponentProps
@@ -105,12 +107,11 @@
105
107
  // Handle portal context (theme injection/re-provision for nested portal components)
106
108
  const { theme } = usePortalContext();
107
109
 
108
- // Inject parent dropdown kind (will be a ComputedRef)
109
- const parentKind = inject<any>('dropdown-kind', computed(() => 'neutral'));
110
+ const parentKind = inject(DROPDOWN_KIND_KEY, computed(() => undefined));
110
111
 
111
112
  //------------------------------------------------------------------------------------------------------------------
112
113
 
113
- const effectiveKind = computed(() => props.kind || parentKind.value || 'neutral');
114
+ const effectiveKind = computed<SkDropdownKind>(() => props.kind ?? parentKind.value ?? 'neutral');
114
115
 
115
116
  //------------------------------------------------------------------------------------------------------------------
116
117
 
@@ -0,0 +1,16 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // Dropdown Context
3
+ //
4
+ // Injection key for nested SkDropdownSubmenu descendants to inherit the surrounding SkDropdown's
5
+ // kind without manual wiring.
6
+ //----------------------------------------------------------------------------------------------------------------------
7
+
8
+ import type { ComputedRef, InjectionKey } from 'vue';
9
+
10
+ import type { SkDropdownKind } from './types';
11
+
12
+ //----------------------------------------------------------------------------------------------------------------------
13
+
14
+ export const DROPDOWN_KIND_KEY : InjectionKey<ComputedRef<SkDropdownKind | undefined>> = Symbol('sk-dropdown-kind');
15
+
16
+ //----------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,88 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // SkField Tests — focuses on the validation-kind provide contract.
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import { computed, defineComponent, h, inject } 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 SkField from './SkField.vue';
12
+
13
+ //----------------------------------------------------------------------------------------------------------------------
14
+
15
+ // Probe component that surfaces the validation-kind / inherited-kind it sees as data attributes.
16
+ // Lets us assert what SkField actually puts on each channel without relying on a leaf form
17
+ // component's class output.
18
+ const Probe = defineComponent({
19
+ setup()
20
+ {
21
+ const validation = inject(validationKindKey, computed(() => 'NONE'));
22
+ const inherited = inject(inheritedKindKey, computed(() => 'NONE'));
23
+ return () => h('div', {
24
+ 'data-validation': validation.value ?? 'undef',
25
+ 'data-inherited': inherited.value ?? 'undef',
26
+ });
27
+ },
28
+ });
29
+
30
+ //----------------------------------------------------------------------------------------------------------------------
31
+
32
+ function mountField(props : Record<string, unknown> = {}) : ReturnType<typeof mount>
33
+ {
34
+ return mount(SkField, {
35
+ props,
36
+ slots: { default: () => h(Probe) },
37
+ });
38
+ }
39
+
40
+ //----------------------------------------------------------------------------------------------------------------------
41
+
42
+ describe('SkField validation-kind contract', () =>
43
+ {
44
+ it('provides undefined on validation-kind when state is null', () =>
45
+ {
46
+ const wrapper = mountField({ state: null });
47
+ expect(wrapper.find('[data-validation]').attributes('data-validation')).toBe('undef');
48
+ });
49
+
50
+ it('provides undefined on validation-kind when state is omitted', () =>
51
+ {
52
+ const wrapper = mountField();
53
+ expect(wrapper.find('[data-validation]').attributes('data-validation')).toBe('undef');
54
+ });
55
+
56
+ it('provides validKind on validation-kind when state is true', () =>
57
+ {
58
+ const wrapper = mountField({ state: true });
59
+ expect(wrapper.find('[data-validation]').attributes('data-validation')).toBe('success');
60
+ });
61
+
62
+ it('provides invalidKind on validation-kind when state is false', () =>
63
+ {
64
+ const wrapper = mountField({ state: false });
65
+ expect(wrapper.find('[data-validation]').attributes('data-validation')).toBe('danger');
66
+ });
67
+
68
+ it('honors custom validKind / invalidKind', () =>
69
+ {
70
+ const valid = mountField({ state: true, validKind: 'accent' });
71
+ expect(valid.find('[data-validation]').attributes('data-validation')).toBe('accent');
72
+
73
+ const invalid = mountField({ state: false, invalidKind: 'warning' });
74
+ expect(invalid.find('[data-validation]').attributes('data-validation')).toBe('warning');
75
+ });
76
+
77
+ it('does NOT touch the inherited-kind channel', () =>
78
+ {
79
+ // Validation override and ambient default live on separate channels. SkField must not
80
+ // bleed into inherited-kind, or an InputGroup further down the tree would pick up the
81
+ // validation kind as if it were an ambient default. The probe falls back to 'NONE' when
82
+ // no provider exists for inherited-kind — that's the assertion.
83
+ const wrapper = mountField({ state: false });
84
+ expect(wrapper.find('[data-inherited]').attributes('data-inherited')).toBe('NONE');
85
+ });
86
+ });
87
+
88
+ //----------------------------------------------------------------------------------------------------------------------
@@ -58,8 +58,12 @@
58
58
  import { computed, provide } from 'vue';
59
59
 
60
60
  // Types
61
+ import type { ComponentKind } from '@/types';
61
62
  import type { SkFieldLabelPosition } from './types';
62
63
 
64
+ // Composables
65
+ import { validationKindKey } from '@/composables/injectionKeys';
66
+
63
67
  //------------------------------------------------------------------------------------------------------------------
64
68
 
65
69
  export interface SkFieldComponentProps
@@ -127,7 +131,7 @@
127
131
  * inputs via Vue's provide/inject system.
128
132
  * @default 'success'
129
133
  */
130
- validKind ?: string;
134
+ validKind ?: ComponentKind;
131
135
 
132
136
  /**
133
137
  * Semantic kind to apply to the child input when `state` is `false` (invalid). Typically
@@ -135,7 +139,7 @@
135
139
  * inputs via Vue's provide/inject system.
136
140
  * @default 'danger'
137
141
  */
138
- invalidKind ?: string;
142
+ invalidKind ?: ComponentKind;
139
143
  }
140
144
 
141
145
  //------------------------------------------------------------------------------------------------------------------
@@ -189,8 +193,10 @@
189
193
 
190
194
  //------------------------------------------------------------------------------------------------------------------
191
195
 
192
- // Determine the kind to apply based on state prop
193
- const effectiveKind = computed(() =>
196
+ // Determine the kind to apply based on state prop. Only emits a non-undefined value when the
197
+ // dev opted into validation by setting `state`; otherwise children fall through to their own
198
+ // kind prop / inherited-kind / default.
199
+ const effectiveKind = computed<ComponentKind | undefined>(() =>
194
200
  {
195
201
  if(props.state === true)
196
202
  {
@@ -200,13 +206,15 @@
200
206
  {
201
207
  return props.invalidKind;
202
208
  }
203
- return undefined; // Let the input use its own kind prop
209
+ return undefined;
204
210
  });
205
211
 
206
212
  //------------------------------------------------------------------------------------------------------------------
207
213
 
208
- // Provide kind to child inputs so they can automatically pick it up
209
- provide('field-kind', effectiveKind);
214
+ // Validation channel: always wins over the child's prop and any inherited-kind ambient
215
+ // default. Only carries a value when `state` is set, so the absence of a state prop leaves
216
+ // children fully in control.
217
+ provide(validationKindKey, effectiveKind);
210
218
 
211
219
  //------------------------------------------------------------------------------------------------------------------
212
220
 
@@ -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
+ //----------------------------------------------------------------------------------------------------------------------