@constela/ui 0.2.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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/components/alert/alert.constela.json +22 -0
  4. package/components/alert/alert.styles.json +14 -0
  5. package/components/alert/alert.test.ts +173 -0
  6. package/components/avatar/avatar.constela.json +32 -0
  7. package/components/avatar/avatar.styles.json +15 -0
  8. package/components/avatar/avatar.test.ts +197 -0
  9. package/components/badge/badge.constela.json +19 -0
  10. package/components/badge/badge.styles.json +16 -0
  11. package/components/badge/badge.test.ts +135 -0
  12. package/components/breadcrumb/breadcrumb.constela.json +17 -0
  13. package/components/breadcrumb/breadcrumb.styles.json +5 -0
  14. package/components/breadcrumb/breadcrumb.test.ts +149 -0
  15. package/components/button/README.md +164 -0
  16. package/components/button/button.constela.json +27 -0
  17. package/components/button/button.styles.json +25 -0
  18. package/components/button/button.test.ts +233 -0
  19. package/components/card/card.constela.json +21 -0
  20. package/components/card/card.styles.json +14 -0
  21. package/components/card/card.test.ts +154 -0
  22. package/components/checkbox/checkbox.constela.json +33 -0
  23. package/components/checkbox/checkbox.styles.json +15 -0
  24. package/components/checkbox/checkbox.test.ts +275 -0
  25. package/components/container/container.constela.json +21 -0
  26. package/components/container/container.styles.json +18 -0
  27. package/components/container/container.test.ts +164 -0
  28. package/components/dialog/dialog.constela.json +19 -0
  29. package/components/dialog/dialog.styles.json +5 -0
  30. package/components/dialog/dialog.test.ts +139 -0
  31. package/components/grid/grid.constela.json +23 -0
  32. package/components/grid/grid.styles.json +25 -0
  33. package/components/grid/grid.test.ts +193 -0
  34. package/components/input/input.constela.json +34 -0
  35. package/components/input/input.styles.json +19 -0
  36. package/components/input/input.test.ts +301 -0
  37. package/components/pagination/pagination.constela.json +22 -0
  38. package/components/pagination/pagination.styles.json +15 -0
  39. package/components/pagination/pagination.test.ts +170 -0
  40. package/components/popover/popover.constela.json +20 -0
  41. package/components/popover/popover.styles.json +16 -0
  42. package/components/popover/popover.test.ts +165 -0
  43. package/components/radio/radio.constela.json +31 -0
  44. package/components/radio/radio.styles.json +15 -0
  45. package/components/radio/radio.test.ts +253 -0
  46. package/components/select/select.constela.json +32 -0
  47. package/components/select/select.styles.json +15 -0
  48. package/components/select/select.test.ts +257 -0
  49. package/components/skeleton/skeleton.constela.json +24 -0
  50. package/components/skeleton/skeleton.styles.json +5 -0
  51. package/components/skeleton/skeleton.test.ts +164 -0
  52. package/components/stack/stack.constela.json +27 -0
  53. package/components/stack/stack.styles.json +33 -0
  54. package/components/stack/stack.test.ts +261 -0
  55. package/components/switch/switch.constela.json +29 -0
  56. package/components/switch/switch.styles.json +15 -0
  57. package/components/switch/switch.test.ts +224 -0
  58. package/components/tabs/tabs.constela.json +21 -0
  59. package/components/tabs/tabs.styles.json +14 -0
  60. package/components/tabs/tabs.test.ts +163 -0
  61. package/components/textarea/textarea.constela.json +34 -0
  62. package/components/textarea/textarea.styles.json +15 -0
  63. package/components/textarea/textarea.test.ts +290 -0
  64. package/components/toast/toast.constela.json +23 -0
  65. package/components/toast/toast.styles.json +17 -0
  66. package/components/toast/toast.test.ts +183 -0
  67. package/components/tooltip/tooltip.constela.json +21 -0
  68. package/components/tooltip/tooltip.styles.json +16 -0
  69. package/components/tooltip/tooltip.test.ts +164 -0
  70. package/dist/index.d.ts +54 -0
  71. package/dist/index.js +83 -0
  72. package/package.json +39 -0
@@ -0,0 +1,32 @@
1
+ {
2
+ "params": {
3
+ "value": { "type": "string", "required": false },
4
+ "disabled": { "type": "boolean", "required": false },
5
+ "required": { "type": "boolean", "required": false },
6
+ "placeholder": { "type": "string", "required": false },
7
+ "name": { "type": "string", "required": false },
8
+ "id": { "type": "string", "required": false },
9
+ "ariaLabel": { "type": "string", "required": false },
10
+ "size": { "type": "string", "required": false }
11
+ },
12
+ "view": {
13
+ "kind": "element",
14
+ "tag": "select",
15
+ "props": {
16
+ "className": {
17
+ "expr": "style",
18
+ "preset": "selectStyles",
19
+ "props": {
20
+ "size": { "expr": "param", "name": "size" }
21
+ }
22
+ },
23
+ "value": { "expr": "param", "name": "value" },
24
+ "disabled": { "expr": "param", "name": "disabled" },
25
+ "required": { "expr": "param", "name": "required" },
26
+ "name": { "expr": "param", "name": "name" },
27
+ "id": { "expr": "param", "name": "id" },
28
+ "aria-label": { "expr": "param", "name": "ariaLabel" }
29
+ },
30
+ "children": [{ "kind": "slot" }]
31
+ }
32
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "selectStyles": {
3
+ "base": "flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
4
+ "variants": {
5
+ "size": {
6
+ "default": "h-10",
7
+ "sm": "h-9 text-xs",
8
+ "lg": "h-11 text-base"
9
+ }
10
+ },
11
+ "defaultVariants": {
12
+ "size": "default"
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Test suite for Select component
3
+ *
4
+ * @constela/ui Select component tests following TDD methodology.
5
+ * These tests verify the Select component structure, params, styles, and accessibility.
6
+ *
7
+ * Coverage:
8
+ * - Component structure validation
9
+ * - Params definition validation
10
+ * - Style preset validation
11
+ * - Accessibility attributes
12
+ */
13
+
14
+ import { describe, it, expect, beforeAll } from 'vitest';
15
+ import {
16
+ loadComponentForTesting,
17
+ assertValidComponent,
18
+ assertValidStylePreset,
19
+ hasParams,
20
+ isOptionalParam,
21
+ hasParamType,
22
+ getRootTag,
23
+ hasVariants,
24
+ hasVariantOptions,
25
+ hasDefaultVariants,
26
+ hasSlot,
27
+ findPropInView,
28
+ type ComponentTestContext,
29
+ } from '../../tests/helpers/test-utils.js';
30
+
31
+ describe('Select Component', () => {
32
+ let ctx: ComponentTestContext;
33
+
34
+ beforeAll(() => {
35
+ ctx = loadComponentForTesting('select');
36
+ });
37
+
38
+ // ==================== Component Structure Tests ====================
39
+
40
+ describe('Component Structure', () => {
41
+ it('should have valid component structure', () => {
42
+ assertValidComponent(ctx.component);
43
+ });
44
+
45
+ it('should have select as root element', () => {
46
+ const rootTag = getRootTag(ctx.component);
47
+ expect(rootTag).toBe('select');
48
+ });
49
+
50
+ it('should contain a slot for option children', () => {
51
+ expect(hasSlot(ctx.component.view)).toBe(true);
52
+ });
53
+
54
+ it('should have className using StyleExpr', () => {
55
+ const className = findPropInView(ctx.component.view, 'className');
56
+ expect(className).not.toBeNull();
57
+ // StyleExpr should have expr: 'style' and preset reference
58
+ expect(className).toMatchObject({
59
+ expr: 'style',
60
+ preset: 'selectStyles',
61
+ });
62
+ });
63
+ });
64
+
65
+ // ==================== Params Validation Tests ====================
66
+
67
+ describe('Params Validation', () => {
68
+ const expectedParams = ['value', 'disabled', 'required', 'placeholder', 'name', 'id', 'ariaLabel'];
69
+
70
+ it('should have all expected params', () => {
71
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
72
+ });
73
+
74
+ describe('param: value', () => {
75
+ it('should be optional', () => {
76
+ expect(isOptionalParam(ctx.component, 'value')).toBe(true);
77
+ });
78
+
79
+ it('should have type string', () => {
80
+ expect(hasParamType(ctx.component, 'value', 'string')).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe('param: disabled', () => {
85
+ it('should be optional', () => {
86
+ expect(isOptionalParam(ctx.component, 'disabled')).toBe(true);
87
+ });
88
+
89
+ it('should have type boolean', () => {
90
+ expect(hasParamType(ctx.component, 'disabled', 'boolean')).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('param: required', () => {
95
+ it('should be optional', () => {
96
+ expect(isOptionalParam(ctx.component, 'required')).toBe(true);
97
+ });
98
+
99
+ it('should have type boolean', () => {
100
+ expect(hasParamType(ctx.component, 'required', 'boolean')).toBe(true);
101
+ });
102
+ });
103
+
104
+ describe('param: placeholder', () => {
105
+ it('should be optional', () => {
106
+ expect(isOptionalParam(ctx.component, 'placeholder')).toBe(true);
107
+ });
108
+
109
+ it('should have type string', () => {
110
+ expect(hasParamType(ctx.component, 'placeholder', 'string')).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe('param: name', () => {
115
+ it('should be optional', () => {
116
+ expect(isOptionalParam(ctx.component, 'name')).toBe(true);
117
+ });
118
+
119
+ it('should have type string', () => {
120
+ expect(hasParamType(ctx.component, 'name', 'string')).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe('param: id', () => {
125
+ it('should be optional', () => {
126
+ expect(isOptionalParam(ctx.component, 'id')).toBe(true);
127
+ });
128
+
129
+ it('should have type string', () => {
130
+ expect(hasParamType(ctx.component, 'id', 'string')).toBe(true);
131
+ });
132
+ });
133
+
134
+ describe('param: ariaLabel', () => {
135
+ it('should be optional', () => {
136
+ expect(isOptionalParam(ctx.component, 'ariaLabel')).toBe(true);
137
+ });
138
+
139
+ it('should have type string', () => {
140
+ expect(hasParamType(ctx.component, 'ariaLabel', 'string')).toBe(true);
141
+ });
142
+ });
143
+ });
144
+
145
+ // ==================== Style Preset Tests ====================
146
+
147
+ describe('Style Preset', () => {
148
+ it('should have valid style preset structure', () => {
149
+ const selectStyles = ctx.styles['selectStyles'];
150
+ expect(selectStyles).toBeDefined();
151
+ assertValidStylePreset(selectStyles);
152
+ });
153
+
154
+ it('should have base classes', () => {
155
+ const selectStyles = ctx.styles['selectStyles'];
156
+ expect(selectStyles.base).toBeDefined();
157
+ expect(typeof selectStyles.base).toBe('string');
158
+ expect(selectStyles.base.length).toBeGreaterThan(0);
159
+ });
160
+
161
+ describe('size variants', () => {
162
+ const sizeOptions = ['default', 'sm', 'lg'];
163
+
164
+ it('should have size variants', () => {
165
+ const selectStyles = ctx.styles['selectStyles'];
166
+ expect(hasVariants(selectStyles, ['size'])).toBe(true);
167
+ });
168
+
169
+ it.each(sizeOptions)('should have %s size option', (option) => {
170
+ const selectStyles = ctx.styles['selectStyles'];
171
+ expect(hasVariantOptions(selectStyles, 'size', [option])).toBe(true);
172
+ });
173
+ });
174
+
175
+ describe('default variants', () => {
176
+ it('should have default size set to default', () => {
177
+ const selectStyles = ctx.styles['selectStyles'];
178
+ expect(hasDefaultVariants(selectStyles, { size: 'default' })).toBe(true);
179
+ });
180
+ });
181
+ });
182
+
183
+ // ==================== Accessibility Tests ====================
184
+
185
+ describe('Accessibility', () => {
186
+ it('should support aria-label attribute', () => {
187
+ const ariaLabel = findPropInView(ctx.component.view, 'aria-label');
188
+ expect(ariaLabel).not.toBeNull();
189
+ // Should reference the ariaLabel param
190
+ expect(ariaLabel).toMatchObject({
191
+ expr: 'param',
192
+ name: 'ariaLabel',
193
+ });
194
+ });
195
+
196
+ it('should support disabled attribute', () => {
197
+ const disabled = findPropInView(ctx.component.view, 'disabled');
198
+ expect(disabled).not.toBeNull();
199
+ // Should reference the disabled param
200
+ expect(disabled).toMatchObject({
201
+ expr: 'param',
202
+ name: 'disabled',
203
+ });
204
+ });
205
+
206
+ it('should support required attribute', () => {
207
+ const required = findPropInView(ctx.component.view, 'required');
208
+ expect(required).not.toBeNull();
209
+ // Should reference the required param
210
+ expect(required).toMatchObject({
211
+ expr: 'param',
212
+ name: 'required',
213
+ });
214
+ });
215
+ });
216
+
217
+ // ==================== View Props Tests ====================
218
+
219
+ describe('View Props', () => {
220
+ it('should pass value to select element', () => {
221
+ const value = findPropInView(ctx.component.view, 'value');
222
+ expect(value).not.toBeNull();
223
+ expect(value).toMatchObject({
224
+ expr: 'param',
225
+ name: 'value',
226
+ });
227
+ });
228
+
229
+ it('should pass name to select element', () => {
230
+ const name = findPropInView(ctx.component.view, 'name');
231
+ expect(name).not.toBeNull();
232
+ expect(name).toMatchObject({
233
+ expr: 'param',
234
+ name: 'name',
235
+ });
236
+ });
237
+
238
+ it('should pass id to select element', () => {
239
+ const id = findPropInView(ctx.component.view, 'id');
240
+ expect(id).not.toBeNull();
241
+ expect(id).toMatchObject({
242
+ expr: 'param',
243
+ name: 'id',
244
+ });
245
+ });
246
+
247
+ it('should pass size to StyleExpr', () => {
248
+ const className = findPropInView(ctx.component.view, 'className');
249
+ expect(className).toMatchObject({
250
+ expr: 'style',
251
+ props: expect.objectContaining({
252
+ size: expect.objectContaining({ expr: 'param', name: 'size' }),
253
+ }),
254
+ });
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "params": {
3
+ "width": { "type": "string", "required": false },
4
+ "height": { "type": "string", "required": false },
5
+ "className": { "type": "string", "required": false }
6
+ },
7
+ "view": {
8
+ "kind": "element",
9
+ "tag": "div",
10
+ "props": {
11
+ "className": {
12
+ "expr": "style",
13
+ "preset": "skeletonStyles",
14
+ "props": {
15
+ "className": { "expr": "param", "name": "className" }
16
+ }
17
+ },
18
+ "style": {
19
+ "width": { "expr": "param", "name": "width" },
20
+ "height": { "expr": "param", "name": "height" }
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "skeletonStyles": {
3
+ "base": "animate-pulse rounded-md bg-muted"
4
+ }
5
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Test suite for Skeleton component
3
+ *
4
+ * @constela/ui Skeleton component tests following TDD methodology.
5
+ * These tests verify the Skeleton component structure, params, and styles.
6
+ *
7
+ * Coverage:
8
+ * - Component structure validation
9
+ * - Params definition validation
10
+ * - Style preset validation
11
+ */
12
+
13
+ import { describe, it, expect, beforeAll } from 'vitest';
14
+ import {
15
+ loadComponentForTesting,
16
+ assertValidComponent,
17
+ assertValidStylePreset,
18
+ hasParams,
19
+ isOptionalParam,
20
+ hasParamType,
21
+ getRootTag,
22
+ hasSlot,
23
+ findPropInView,
24
+ type ComponentTestContext,
25
+ } from '../../tests/helpers/test-utils.js';
26
+
27
+ describe('Skeleton Component', () => {
28
+ let ctx: ComponentTestContext;
29
+
30
+ beforeAll(() => {
31
+ ctx = loadComponentForTesting('skeleton');
32
+ });
33
+
34
+ // ==================== Component Structure Tests ====================
35
+
36
+ describe('Component Structure', () => {
37
+ it('should have valid component structure', () => {
38
+ assertValidComponent(ctx.component);
39
+ });
40
+
41
+ it('should have div as root element', () => {
42
+ const rootTag = getRootTag(ctx.component);
43
+ expect(rootTag).toBe('div');
44
+ });
45
+
46
+ it('should NOT contain a slot (skeleton is self-contained)', () => {
47
+ expect(hasSlot(ctx.component.view)).toBe(false);
48
+ });
49
+
50
+ it('should have className using StyleExpr with preset "skeletonStyles"', () => {
51
+ const className = findPropInView(ctx.component.view, 'className');
52
+ expect(className).not.toBeNull();
53
+ // StyleExpr should have expr: 'style' and preset reference
54
+ expect(className).toMatchObject({
55
+ expr: 'style',
56
+ preset: 'skeletonStyles',
57
+ });
58
+ });
59
+ });
60
+
61
+ // ==================== Params Validation Tests ====================
62
+
63
+ describe('Params Validation', () => {
64
+ const expectedParams = ['width', 'height', 'className'];
65
+
66
+ it('should have all expected params', () => {
67
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
68
+ });
69
+
70
+ describe('param: width', () => {
71
+ it('should be optional', () => {
72
+ expect(isOptionalParam(ctx.component, 'width')).toBe(true);
73
+ });
74
+
75
+ it('should have type string', () => {
76
+ expect(hasParamType(ctx.component, 'width', 'string')).toBe(true);
77
+ });
78
+ });
79
+
80
+ describe('param: height', () => {
81
+ it('should be optional', () => {
82
+ expect(isOptionalParam(ctx.component, 'height')).toBe(true);
83
+ });
84
+
85
+ it('should have type string', () => {
86
+ expect(hasParamType(ctx.component, 'height', 'string')).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe('param: className', () => {
91
+ it('should be optional', () => {
92
+ expect(isOptionalParam(ctx.component, 'className')).toBe(true);
93
+ });
94
+
95
+ it('should have type string', () => {
96
+ expect(hasParamType(ctx.component, 'className', 'string')).toBe(true);
97
+ });
98
+ });
99
+ });
100
+
101
+ // ==================== Style Preset Tests ====================
102
+
103
+ describe('Style Preset', () => {
104
+ it('should have valid style preset structure', () => {
105
+ const skeletonStyles = ctx.styles['skeletonStyles'];
106
+ expect(skeletonStyles).toBeDefined();
107
+ assertValidStylePreset(skeletonStyles);
108
+ });
109
+
110
+ it('should have base classes with common skeleton styles', () => {
111
+ const skeletonStyles = ctx.styles['skeletonStyles'];
112
+ expect(skeletonStyles.base).toBeDefined();
113
+ expect(typeof skeletonStyles.base).toBe('string');
114
+ expect(skeletonStyles.base.length).toBeGreaterThan(0);
115
+ });
116
+
117
+ it('should include animate-pulse in base classes', () => {
118
+ const skeletonStyles = ctx.styles['skeletonStyles'];
119
+ expect(skeletonStyles.base).toContain('animate-pulse');
120
+ });
121
+
122
+ it('should include bg-muted in base classes', () => {
123
+ const skeletonStyles = ctx.styles['skeletonStyles'];
124
+ expect(skeletonStyles.base).toContain('bg-muted');
125
+ });
126
+
127
+ it('should include rounded in base classes', () => {
128
+ const skeletonStyles = ctx.styles['skeletonStyles'];
129
+ expect(skeletonStyles.base).toContain('rounded');
130
+ });
131
+ });
132
+
133
+ // ==================== View Props Tests ====================
134
+
135
+ describe('View Props', () => {
136
+ it('should pass width to style attribute', () => {
137
+ const style = findPropInView(ctx.component.view, 'style');
138
+ expect(style).not.toBeNull();
139
+ // Style object should contain width
140
+ expect(style).toMatchObject({
141
+ width: expect.objectContaining({ expr: 'param', name: 'width' }),
142
+ });
143
+ });
144
+
145
+ it('should pass height to style attribute', () => {
146
+ const style = findPropInView(ctx.component.view, 'style');
147
+ expect(style).not.toBeNull();
148
+ // Style object should contain height
149
+ expect(style).toMatchObject({
150
+ height: expect.objectContaining({ expr: 'param', name: 'height' }),
151
+ });
152
+ });
153
+
154
+ it('should pass className to StyleExpr props', () => {
155
+ const className = findPropInView(ctx.component.view, 'className');
156
+ expect(className).toMatchObject({
157
+ expr: 'style',
158
+ props: expect.objectContaining({
159
+ className: expect.objectContaining({ expr: 'param', name: 'className' }),
160
+ }),
161
+ });
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "params": {
3
+ "direction": { "type": "string", "required": false },
4
+ "gap": { "type": "string", "required": false },
5
+ "align": { "type": "string", "required": false },
6
+ "justify": { "type": "string", "required": false },
7
+ "className": { "type": "string", "required": false }
8
+ },
9
+ "view": {
10
+ "kind": "element",
11
+ "tag": "div",
12
+ "props": {
13
+ "className": {
14
+ "expr": "style",
15
+ "preset": "stackStyles",
16
+ "props": {
17
+ "direction": { "expr": "param", "name": "direction" },
18
+ "gap": { "expr": "param", "name": "gap" },
19
+ "align": { "expr": "param", "name": "align" },
20
+ "justify": { "expr": "param", "name": "justify" },
21
+ "className": { "expr": "param", "name": "className" }
22
+ }
23
+ }
24
+ },
25
+ "children": [{ "kind": "slot" }]
26
+ }
27
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "stackStyles": {
3
+ "base": "flex",
4
+ "variants": {
5
+ "direction": {
6
+ "row": "flex-row",
7
+ "column": "flex-col"
8
+ },
9
+ "gap": {
10
+ "none": "gap-0",
11
+ "sm": "gap-2",
12
+ "md": "gap-4",
13
+ "lg": "gap-8"
14
+ },
15
+ "align": {
16
+ "start": "items-start",
17
+ "center": "items-center",
18
+ "end": "items-end",
19
+ "stretch": "items-stretch"
20
+ },
21
+ "justify": {
22
+ "start": "justify-start",
23
+ "center": "justify-center",
24
+ "end": "justify-end",
25
+ "between": "justify-between"
26
+ }
27
+ },
28
+ "defaultVariants": {
29
+ "direction": "column",
30
+ "gap": "md"
31
+ }
32
+ }
33
+ }