@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,163 @@
1
+ /**
2
+ * Test suite for Tabs component
3
+ *
4
+ * @constela/ui Tabs component tests following TDD methodology.
5
+ * These tests verify the Tabs component structure, params, styles, and accessibility.
6
+ *
7
+ * Coverage:
8
+ * - Component structure validation
9
+ * - Params definition validation
10
+ * - Style preset validation
11
+ * - Accessibility attributes (role="tablist")
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
+ hasRole,
29
+ type ComponentTestContext,
30
+ } from '../../tests/helpers/test-utils.js';
31
+
32
+ describe('Tabs Component', () => {
33
+ let ctx: ComponentTestContext;
34
+
35
+ beforeAll(() => {
36
+ ctx = loadComponentForTesting('tabs');
37
+ });
38
+
39
+ // ==================== Component Structure Tests ====================
40
+
41
+ describe('Component Structure', () => {
42
+ it('should have valid component structure', () => {
43
+ assertValidComponent(ctx.component);
44
+ });
45
+
46
+ it('should have div as root element', () => {
47
+ const rootTag = getRootTag(ctx.component);
48
+ expect(rootTag).toBe('div');
49
+ });
50
+
51
+ it('should have role="tablist" attribute', () => {
52
+ expect(hasRole(ctx.component.view, 'tablist')).toBe(true);
53
+ });
54
+
55
+ it('should contain a slot for tab items', () => {
56
+ expect(hasSlot(ctx.component.view)).toBe(true);
57
+ });
58
+
59
+ it('should have className using StyleExpr with tabsStyles preset', () => {
60
+ const className = findPropInView(ctx.component.view, 'className');
61
+ expect(className).not.toBeNull();
62
+ expect(className).toMatchObject({
63
+ expr: 'style',
64
+ preset: 'tabsStyles',
65
+ });
66
+ });
67
+ });
68
+
69
+ // ==================== Params Validation Tests ====================
70
+
71
+ describe('Params Validation', () => {
72
+ const expectedParams = ['activeTab', 'variant'];
73
+
74
+ it('should have all expected params', () => {
75
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
76
+ });
77
+
78
+ describe('param: activeTab', () => {
79
+ it('should be optional', () => {
80
+ expect(isOptionalParam(ctx.component, 'activeTab')).toBe(true);
81
+ });
82
+
83
+ it('should have type string', () => {
84
+ expect(hasParamType(ctx.component, 'activeTab', 'string')).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('param: variant', () => {
89
+ it('should be optional', () => {
90
+ expect(isOptionalParam(ctx.component, 'variant')).toBe(true);
91
+ });
92
+
93
+ it('should have type string', () => {
94
+ expect(hasParamType(ctx.component, 'variant', 'string')).toBe(true);
95
+ });
96
+ });
97
+ });
98
+
99
+ // ==================== Style Preset Tests ====================
100
+
101
+ describe('Style Preset', () => {
102
+ it('should have valid style preset structure', () => {
103
+ const tabsStyles = ctx.styles['tabsStyles'];
104
+ expect(tabsStyles).toBeDefined();
105
+ assertValidStylePreset(tabsStyles);
106
+ });
107
+
108
+ it('should have base classes for common tabs styles', () => {
109
+ const tabsStyles = ctx.styles['tabsStyles'];
110
+ expect(tabsStyles.base).toBeDefined();
111
+ expect(typeof tabsStyles.base).toBe('string');
112
+ expect(tabsStyles.base.length).toBeGreaterThan(0);
113
+ });
114
+
115
+ describe('variant options', () => {
116
+ const variantOptions = ['default', 'outline'];
117
+
118
+ it('should have variant variants', () => {
119
+ const tabsStyles = ctx.styles['tabsStyles'];
120
+ expect(hasVariants(tabsStyles, ['variant'])).toBe(true);
121
+ });
122
+
123
+ it.each(variantOptions)('should have %s variant option', (option) => {
124
+ const tabsStyles = ctx.styles['tabsStyles'];
125
+ expect(hasVariantOptions(tabsStyles, 'variant', [option])).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe('default variants', () => {
130
+ it('should have default variant set to default', () => {
131
+ const tabsStyles = ctx.styles['tabsStyles'];
132
+ expect(hasDefaultVariants(tabsStyles, { variant: 'default' })).toBe(true);
133
+ });
134
+ });
135
+ });
136
+
137
+ // ==================== Accessibility Tests ====================
138
+
139
+ describe('Accessibility', () => {
140
+ it('should have role="tablist" for screen readers', () => {
141
+ const role = findPropInView(ctx.component.view, 'role');
142
+ expect(role).not.toBeNull();
143
+ expect(role).toMatchObject({
144
+ expr: 'lit',
145
+ value: 'tablist',
146
+ });
147
+ });
148
+ });
149
+
150
+ // ==================== View Props Tests ====================
151
+
152
+ describe('View Props', () => {
153
+ it('should pass variant to StyleExpr', () => {
154
+ const className = findPropInView(ctx.component.view, 'className');
155
+ expect(className).toMatchObject({
156
+ expr: 'style',
157
+ props: expect.objectContaining({
158
+ variant: expect.objectContaining({ expr: 'param', name: 'variant' }),
159
+ }),
160
+ });
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,34 @@
1
+ {
2
+ "params": {
3
+ "value": { "type": "string", "required": false },
4
+ "placeholder": { "type": "string", "required": false },
5
+ "disabled": { "type": "boolean", "required": false },
6
+ "required": { "type": "boolean", "required": false },
7
+ "rows": { "type": "number", "required": false },
8
+ "name": { "type": "string", "required": false },
9
+ "id": { "type": "string", "required": false },
10
+ "ariaLabel": { "type": "string", "required": false },
11
+ "size": { "type": "string", "required": false }
12
+ },
13
+ "view": {
14
+ "kind": "element",
15
+ "tag": "textarea",
16
+ "props": {
17
+ "className": {
18
+ "expr": "style",
19
+ "preset": "textareaStyles",
20
+ "props": {
21
+ "size": { "expr": "param", "name": "size" }
22
+ }
23
+ },
24
+ "value": { "expr": "param", "name": "value" },
25
+ "placeholder": { "expr": "param", "name": "placeholder" },
26
+ "disabled": { "expr": "param", "name": "disabled" },
27
+ "required": { "expr": "param", "name": "required" },
28
+ "rows": { "expr": "param", "name": "rows" },
29
+ "name": { "expr": "param", "name": "name" },
30
+ "id": { "expr": "param", "name": "id" },
31
+ "aria-label": { "expr": "param", "name": "ariaLabel" }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "textareaStyles": {
3
+ "base": "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
4
+ "variants": {
5
+ "size": {
6
+ "default": "",
7
+ "sm": "min-h-[60px] text-xs",
8
+ "lg": "min-h-[120px] text-base"
9
+ }
10
+ },
11
+ "defaultVariants": {
12
+ "size": "default"
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Test suite for Textarea component
3
+ *
4
+ * @constela/ui Textarea component tests following TDD methodology.
5
+ * These tests verify the Textarea 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
+ findPropInView,
27
+ type ComponentTestContext,
28
+ } from '../../tests/helpers/test-utils.js';
29
+
30
+ describe('Textarea Component', () => {
31
+ let ctx: ComponentTestContext;
32
+
33
+ beforeAll(() => {
34
+ ctx = loadComponentForTesting('textarea');
35
+ });
36
+
37
+ // ==================== Component Structure Tests ====================
38
+
39
+ describe('Component Structure', () => {
40
+ it('should have valid component structure', () => {
41
+ assertValidComponent(ctx.component);
42
+ });
43
+
44
+ it('should have textarea as root element', () => {
45
+ const rootTag = getRootTag(ctx.component);
46
+ expect(rootTag).toBe('textarea');
47
+ });
48
+
49
+ it('should have className using StyleExpr', () => {
50
+ const className = findPropInView(ctx.component.view, 'className');
51
+ expect(className).not.toBeNull();
52
+ // StyleExpr should have expr: 'style' and preset reference
53
+ expect(className).toMatchObject({
54
+ expr: 'style',
55
+ preset: 'textareaStyles',
56
+ });
57
+ });
58
+
59
+ it('should support rows attribute', () => {
60
+ const rows = findPropInView(ctx.component.view, 'rows');
61
+ expect(rows).not.toBeNull();
62
+ // Should reference the rows param
63
+ expect(rows).toMatchObject({
64
+ expr: 'param',
65
+ name: 'rows',
66
+ });
67
+ });
68
+ });
69
+
70
+ // ==================== Params Validation Tests ====================
71
+
72
+ describe('Params Validation', () => {
73
+ const expectedParams = [
74
+ 'value',
75
+ 'placeholder',
76
+ 'disabled',
77
+ 'required',
78
+ 'rows',
79
+ 'name',
80
+ 'id',
81
+ 'ariaLabel',
82
+ ];
83
+
84
+ it('should have all expected params', () => {
85
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
86
+ });
87
+
88
+ describe('param: value', () => {
89
+ it('should be optional', () => {
90
+ expect(isOptionalParam(ctx.component, 'value')).toBe(true);
91
+ });
92
+
93
+ it('should have type string', () => {
94
+ expect(hasParamType(ctx.component, 'value', 'string')).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe('param: placeholder', () => {
99
+ it('should be optional', () => {
100
+ expect(isOptionalParam(ctx.component, 'placeholder')).toBe(true);
101
+ });
102
+
103
+ it('should have type string', () => {
104
+ expect(hasParamType(ctx.component, 'placeholder', 'string')).toBe(true);
105
+ });
106
+ });
107
+
108
+ describe('param: disabled', () => {
109
+ it('should be optional', () => {
110
+ expect(isOptionalParam(ctx.component, 'disabled')).toBe(true);
111
+ });
112
+
113
+ it('should have type boolean', () => {
114
+ expect(hasParamType(ctx.component, 'disabled', 'boolean')).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('param: required', () => {
119
+ it('should be optional', () => {
120
+ expect(isOptionalParam(ctx.component, 'required')).toBe(true);
121
+ });
122
+
123
+ it('should have type boolean', () => {
124
+ expect(hasParamType(ctx.component, 'required', 'boolean')).toBe(true);
125
+ });
126
+ });
127
+
128
+ describe('param: rows', () => {
129
+ it('should be optional', () => {
130
+ expect(isOptionalParam(ctx.component, 'rows')).toBe(true);
131
+ });
132
+
133
+ it('should have type number', () => {
134
+ expect(hasParamType(ctx.component, 'rows', 'number')).toBe(true);
135
+ });
136
+ });
137
+
138
+ describe('param: name', () => {
139
+ it('should be optional', () => {
140
+ expect(isOptionalParam(ctx.component, 'name')).toBe(true);
141
+ });
142
+
143
+ it('should have type string', () => {
144
+ expect(hasParamType(ctx.component, 'name', 'string')).toBe(true);
145
+ });
146
+ });
147
+
148
+ describe('param: id', () => {
149
+ it('should be optional', () => {
150
+ expect(isOptionalParam(ctx.component, 'id')).toBe(true);
151
+ });
152
+
153
+ it('should have type string', () => {
154
+ expect(hasParamType(ctx.component, 'id', 'string')).toBe(true);
155
+ });
156
+ });
157
+
158
+ describe('param: ariaLabel', () => {
159
+ it('should be optional', () => {
160
+ expect(isOptionalParam(ctx.component, 'ariaLabel')).toBe(true);
161
+ });
162
+
163
+ it('should have type string', () => {
164
+ expect(hasParamType(ctx.component, 'ariaLabel', 'string')).toBe(true);
165
+ });
166
+ });
167
+ });
168
+
169
+ // ==================== Style Preset Tests ====================
170
+
171
+ describe('Style Preset', () => {
172
+ it('should have valid style preset structure', () => {
173
+ const textareaStyles = ctx.styles['textareaStyles'];
174
+ expect(textareaStyles).toBeDefined();
175
+ assertValidStylePreset(textareaStyles);
176
+ });
177
+
178
+ it('should have base classes', () => {
179
+ const textareaStyles = ctx.styles['textareaStyles'];
180
+ expect(textareaStyles.base).toBeDefined();
181
+ expect(typeof textareaStyles.base).toBe('string');
182
+ expect(textareaStyles.base.length).toBeGreaterThan(0);
183
+ });
184
+
185
+ describe('size options', () => {
186
+ const sizeOptions = ['default', 'sm', 'lg'];
187
+
188
+ it('should have size variants', () => {
189
+ const textareaStyles = ctx.styles['textareaStyles'];
190
+ expect(hasVariants(textareaStyles, ['size'])).toBe(true);
191
+ });
192
+
193
+ it.each(sizeOptions)('should have %s size option', (option) => {
194
+ const textareaStyles = ctx.styles['textareaStyles'];
195
+ expect(hasVariantOptions(textareaStyles, 'size', [option])).toBe(true);
196
+ });
197
+ });
198
+
199
+ describe('default variants', () => {
200
+ it('should have default size set to default', () => {
201
+ const textareaStyles = ctx.styles['textareaStyles'];
202
+ expect(hasDefaultVariants(textareaStyles, { size: 'default' })).toBe(true);
203
+ });
204
+ });
205
+ });
206
+
207
+ // ==================== Accessibility Tests ====================
208
+
209
+ describe('Accessibility', () => {
210
+ it('should support aria-label attribute', () => {
211
+ const ariaLabel = findPropInView(ctx.component.view, 'aria-label');
212
+ expect(ariaLabel).not.toBeNull();
213
+ // Should reference the ariaLabel param
214
+ expect(ariaLabel).toMatchObject({
215
+ expr: 'param',
216
+ name: 'ariaLabel',
217
+ });
218
+ });
219
+
220
+ it('should support disabled attribute', () => {
221
+ const disabled = findPropInView(ctx.component.view, 'disabled');
222
+ expect(disabled).not.toBeNull();
223
+ // Should reference the disabled param
224
+ expect(disabled).toMatchObject({
225
+ expr: 'param',
226
+ name: 'disabled',
227
+ });
228
+ });
229
+
230
+ it('should support required attribute', () => {
231
+ const required = findPropInView(ctx.component.view, 'required');
232
+ expect(required).not.toBeNull();
233
+ // Should reference the required param
234
+ expect(required).toMatchObject({
235
+ expr: 'param',
236
+ name: 'required',
237
+ });
238
+ });
239
+ });
240
+
241
+ // ==================== View Props Tests ====================
242
+
243
+ describe('View Props', () => {
244
+ it('should pass value to textarea', () => {
245
+ const value = findPropInView(ctx.component.view, 'value');
246
+ expect(value).not.toBeNull();
247
+ expect(value).toMatchObject({
248
+ expr: 'param',
249
+ name: 'value',
250
+ });
251
+ });
252
+
253
+ it('should pass placeholder to textarea', () => {
254
+ const placeholder = findPropInView(ctx.component.view, 'placeholder');
255
+ expect(placeholder).not.toBeNull();
256
+ expect(placeholder).toMatchObject({
257
+ expr: 'param',
258
+ name: 'placeholder',
259
+ });
260
+ });
261
+
262
+ it('should pass name to textarea', () => {
263
+ const name = findPropInView(ctx.component.view, 'name');
264
+ expect(name).not.toBeNull();
265
+ expect(name).toMatchObject({
266
+ expr: 'param',
267
+ name: 'name',
268
+ });
269
+ });
270
+
271
+ it('should pass id to textarea', () => {
272
+ const id = findPropInView(ctx.component.view, 'id');
273
+ expect(id).not.toBeNull();
274
+ expect(id).toMatchObject({
275
+ expr: 'param',
276
+ name: 'id',
277
+ });
278
+ });
279
+
280
+ it('should pass size to StyleExpr', () => {
281
+ const className = findPropInView(ctx.component.view, 'className');
282
+ expect(className).toMatchObject({
283
+ expr: 'style',
284
+ props: expect.objectContaining({
285
+ size: expect.objectContaining({ expr: 'param', name: 'size' }),
286
+ }),
287
+ });
288
+ });
289
+ });
290
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "params": {
3
+ "variant": { "type": "string", "required": false },
4
+ "title": { "type": "string", "required": false },
5
+ "description": { "type": "string", "required": false }
6
+ },
7
+ "view": {
8
+ "kind": "element",
9
+ "tag": "div",
10
+ "props": {
11
+ "role": { "expr": "lit", "value": "alert" },
12
+ "aria-live": { "expr": "lit", "value": "polite" },
13
+ "className": {
14
+ "expr": "style",
15
+ "preset": "toastStyles",
16
+ "props": {
17
+ "variant": { "expr": "param", "name": "variant" }
18
+ }
19
+ }
20
+ },
21
+ "children": [{ "kind": "slot" }]
22
+ }
23
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "toastStyles": {
3
+ "base": "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
4
+ "variants": {
5
+ "variant": {
6
+ "default": "border bg-background text-foreground",
7
+ "success": "border-green-500 bg-green-50 text-green-900 dark:bg-green-900 dark:text-green-50",
8
+ "error": "border-destructive bg-destructive text-destructive-foreground",
9
+ "warning": "border-yellow-500 bg-yellow-50 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-50",
10
+ "info": "border-blue-500 bg-blue-50 text-blue-900 dark:bg-blue-900 dark:text-blue-50"
11
+ }
12
+ },
13
+ "defaultVariants": {
14
+ "variant": "default"
15
+ }
16
+ }
17
+ }