@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,135 @@
1
+ /**
2
+ * Test suite for Badge component
3
+ *
4
+ * @constela/ui Badge component tests following TDD methodology.
5
+ * These tests verify the Badge 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
+ hasVariants,
23
+ hasVariantOptions,
24
+ hasDefaultVariants,
25
+ hasSlot,
26
+ findPropInView,
27
+ type ComponentTestContext,
28
+ } from '../../tests/helpers/test-utils.js';
29
+
30
+ describe('Badge Component', () => {
31
+ let ctx: ComponentTestContext;
32
+
33
+ beforeAll(() => {
34
+ ctx = loadComponentForTesting('badge');
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 div as root element', () => {
45
+ const rootTag = getRootTag(ctx.component);
46
+ expect(rootTag).toBe('div');
47
+ });
48
+
49
+ it('should contain a slot for badge content', () => {
50
+ expect(hasSlot(ctx.component.view)).toBe(true);
51
+ });
52
+
53
+ it('should have className using StyleExpr with preset badgeStyles', () => {
54
+ const className = findPropInView(ctx.component.view, 'className');
55
+ expect(className).not.toBeNull();
56
+ // StyleExpr should have expr: 'style' and preset reference
57
+ expect(className).toMatchObject({
58
+ expr: 'style',
59
+ preset: 'badgeStyles',
60
+ });
61
+ });
62
+ });
63
+
64
+ // ==================== Params Validation Tests ====================
65
+
66
+ describe('Params Validation', () => {
67
+ const expectedParams = ['variant'];
68
+
69
+ it('should have all expected params', () => {
70
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
71
+ });
72
+
73
+ describe('param: variant', () => {
74
+ it('should be optional', () => {
75
+ expect(isOptionalParam(ctx.component, 'variant')).toBe(true);
76
+ });
77
+
78
+ it('should have type string', () => {
79
+ expect(hasParamType(ctx.component, 'variant', 'string')).toBe(true);
80
+ });
81
+ });
82
+ });
83
+
84
+ // ==================== Style Preset Tests ====================
85
+
86
+ describe('Style Preset', () => {
87
+ it('should have valid style preset structure', () => {
88
+ const badgeStyles = ctx.styles['badgeStyles'];
89
+ expect(badgeStyles).toBeDefined();
90
+ assertValidStylePreset(badgeStyles);
91
+ });
92
+
93
+ it('should have base classes for common badge styles', () => {
94
+ const badgeStyles = ctx.styles['badgeStyles'];
95
+ expect(badgeStyles.base).toBeDefined();
96
+ expect(typeof badgeStyles.base).toBe('string');
97
+ expect(badgeStyles.base.length).toBeGreaterThan(0);
98
+ });
99
+
100
+ describe('variant options', () => {
101
+ const variantOptions = ['default', 'secondary', 'destructive', 'outline'];
102
+
103
+ it('should have variant variants', () => {
104
+ const badgeStyles = ctx.styles['badgeStyles'];
105
+ expect(hasVariants(badgeStyles, ['variant'])).toBe(true);
106
+ });
107
+
108
+ it.each(variantOptions)('should have %s variant option', (option) => {
109
+ const badgeStyles = ctx.styles['badgeStyles'];
110
+ expect(hasVariantOptions(badgeStyles, 'variant', [option])).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe('default variants', () => {
115
+ it('should have default variant set to default', () => {
116
+ const badgeStyles = ctx.styles['badgeStyles'];
117
+ expect(hasDefaultVariants(badgeStyles, { variant: 'default' })).toBe(true);
118
+ });
119
+ });
120
+ });
121
+
122
+ // ==================== View Props Tests ====================
123
+
124
+ describe('View Props', () => {
125
+ it('should pass variant to StyleExpr', () => {
126
+ const className = findPropInView(ctx.component.view, 'className');
127
+ expect(className).toMatchObject({
128
+ expr: 'style',
129
+ props: expect.objectContaining({
130
+ variant: expect.objectContaining({ expr: 'param', name: 'variant' }),
131
+ }),
132
+ });
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "params": {
3
+ "separator": { "type": "string", "required": false, "default": "/" }
4
+ },
5
+ "view": {
6
+ "kind": "element",
7
+ "tag": "nav",
8
+ "props": {
9
+ "aria-label": { "expr": "lit", "value": "Breadcrumb" },
10
+ "className": {
11
+ "expr": "style",
12
+ "preset": "breadcrumbStyles"
13
+ }
14
+ },
15
+ "children": [{ "kind": "slot" }]
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "breadcrumbStyles": {
3
+ "base": "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5"
4
+ }
5
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Test suite for Breadcrumb component
3
+ *
4
+ * @constela/ui Breadcrumb component tests following TDD methodology.
5
+ * These tests verify the Breadcrumb 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
+ hasSlot,
24
+ findPropInView,
25
+ hasAriaAttribute,
26
+ type ComponentTestContext,
27
+ } from '../../tests/helpers/test-utils.js';
28
+
29
+ describe('Breadcrumb Component', () => {
30
+ let ctx: ComponentTestContext;
31
+
32
+ beforeAll(() => {
33
+ ctx = loadComponentForTesting('breadcrumb');
34
+ });
35
+
36
+ // ==================== Component Structure Tests ====================
37
+
38
+ describe('Component Structure', () => {
39
+ it('should have valid component structure', () => {
40
+ assertValidComponent(ctx.component);
41
+ });
42
+
43
+ it('should have nav as root element', () => {
44
+ const rootTag = getRootTag(ctx.component);
45
+ expect(rootTag).toBe('nav');
46
+ });
47
+
48
+ it('should have aria-label="Breadcrumb" attribute', () => {
49
+ const ariaLabel = findPropInView(ctx.component.view, 'aria-label');
50
+ expect(ariaLabel).not.toBeNull();
51
+ expect(ariaLabel).toMatchObject({
52
+ expr: 'lit',
53
+ value: 'Breadcrumb',
54
+ });
55
+ });
56
+
57
+ it('should contain a slot for breadcrumb items', () => {
58
+ expect(hasSlot(ctx.component.view)).toBe(true);
59
+ });
60
+
61
+ it('should have className using StyleExpr with preset breadcrumbStyles', () => {
62
+ const className = findPropInView(ctx.component.view, 'className');
63
+ expect(className).not.toBeNull();
64
+ expect(className).toMatchObject({
65
+ expr: 'style',
66
+ preset: 'breadcrumbStyles',
67
+ });
68
+ });
69
+ });
70
+
71
+ // ==================== Params Validation Tests ====================
72
+
73
+ describe('Params Validation', () => {
74
+ const expectedParams = ['separator'];
75
+
76
+ it('should have all expected params', () => {
77
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
78
+ });
79
+
80
+ describe('param: separator', () => {
81
+ it('should be optional', () => {
82
+ expect(isOptionalParam(ctx.component, 'separator')).toBe(true);
83
+ });
84
+
85
+ it('should have type string', () => {
86
+ expect(hasParamType(ctx.component, 'separator', 'string')).toBe(true);
87
+ });
88
+
89
+ it('should have default value of "/"', () => {
90
+ const separatorParam = ctx.component.params?.['separator'];
91
+ expect(separatorParam).toBeDefined();
92
+ expect(separatorParam?.default).toBe('/');
93
+ });
94
+ });
95
+ });
96
+
97
+ // ==================== Style Preset Tests ====================
98
+
99
+ describe('Style Preset', () => {
100
+ it('should have valid style preset structure', () => {
101
+ const breadcrumbStyles = ctx.styles['breadcrumbStyles'];
102
+ expect(breadcrumbStyles).toBeDefined();
103
+ assertValidStylePreset(breadcrumbStyles);
104
+ });
105
+
106
+ it('should have base classes for common breadcrumb styles', () => {
107
+ const breadcrumbStyles = ctx.styles['breadcrumbStyles'];
108
+ expect(breadcrumbStyles.base).toBeDefined();
109
+ expect(typeof breadcrumbStyles.base).toBe('string');
110
+ expect(breadcrumbStyles.base.length).toBeGreaterThan(0);
111
+ });
112
+
113
+ it('should include flex layout classes in base', () => {
114
+ const breadcrumbStyles = ctx.styles['breadcrumbStyles'];
115
+ expect(breadcrumbStyles.base).toMatch(/flex/);
116
+ });
117
+
118
+ it('should include items-center class in base', () => {
119
+ const breadcrumbStyles = ctx.styles['breadcrumbStyles'];
120
+ expect(breadcrumbStyles.base).toMatch(/items-center/);
121
+ });
122
+
123
+ it('should include gap class in base', () => {
124
+ const breadcrumbStyles = ctx.styles['breadcrumbStyles'];
125
+ expect(breadcrumbStyles.base).toMatch(/gap/);
126
+ });
127
+ });
128
+
129
+ // ==================== Accessibility Tests ====================
130
+
131
+ describe('Accessibility', () => {
132
+ it('should have aria-label attribute for screen readers', () => {
133
+ expect(hasAriaAttribute(ctx.component.view, 'aria-label')).toBe(true);
134
+ });
135
+
136
+ it('should have aria-label value of "Breadcrumb"', () => {
137
+ const ariaLabel = findPropInView(ctx.component.view, 'aria-label');
138
+ expect(ariaLabel).toMatchObject({
139
+ expr: 'lit',
140
+ value: 'Breadcrumb',
141
+ });
142
+ });
143
+
144
+ it('should use nav element for semantic navigation', () => {
145
+ const rootTag = getRootTag(ctx.component);
146
+ expect(rootTag).toBe('nav');
147
+ });
148
+ });
149
+ });
@@ -0,0 +1,164 @@
1
+ # Button
2
+
3
+ A customizable button component with multiple variants and sizes.
4
+
5
+ ## Usage
6
+
7
+ Copy `button.constela.json` and `button.styles.json` to your project's components directory.
8
+
9
+ ### Basic Usage
10
+
11
+ ```json
12
+ {
13
+ "kind": "component",
14
+ "name": "Button",
15
+ "children": [
16
+ { "kind": "text", "value": { "expr": "lit", "value": "Click me" } }
17
+ ]
18
+ }
19
+ ```
20
+
21
+ ### With Variants
22
+
23
+ ```json
24
+ {
25
+ "kind": "component",
26
+ "name": "Button",
27
+ "props": {
28
+ "variant": { "expr": "lit", "value": "destructive" }
29
+ },
30
+ "children": [
31
+ { "kind": "text", "value": { "expr": "lit", "value": "Delete" } }
32
+ ]
33
+ }
34
+ ```
35
+
36
+ ### With Size
37
+
38
+ ```json
39
+ {
40
+ "kind": "component",
41
+ "name": "Button",
42
+ "props": {
43
+ "size": { "expr": "lit", "value": "lg" }
44
+ },
45
+ "children": [
46
+ { "kind": "text", "value": { "expr": "lit", "value": "Large Button" } }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ ### Disabled State
52
+
53
+ ```json
54
+ {
55
+ "kind": "component",
56
+ "name": "Button",
57
+ "props": {
58
+ "disabled": { "expr": "lit", "value": true }
59
+ },
60
+ "children": [
61
+ { "kind": "text", "value": { "expr": "lit", "value": "Disabled" } }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ ### With ARIA Label
67
+
68
+ ```json
69
+ {
70
+ "kind": "component",
71
+ "name": "Button",
72
+ "props": {
73
+ "ariaLabel": { "expr": "lit", "value": "Close dialog" },
74
+ "variant": { "expr": "lit", "value": "ghost" },
75
+ "size": { "expr": "lit", "value": "icon" }
76
+ },
77
+ "children": [
78
+ { "kind": "text", "value": { "expr": "lit", "value": "X" } }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ ## Props
84
+
85
+ | Prop | Type | Required | Default | Description |
86
+ |------|------|----------|---------|-------------|
87
+ | `variant` | string | No | `"default"` | Button style variant |
88
+ | `size` | string | No | `"default"` | Button size |
89
+ | `disabled` | boolean | No | `false` | Whether the button is disabled |
90
+ | `type` | string | No | - | HTML button type attribute (browser default: `"submit"`) |
91
+ | `ariaLabel` | string | No | - | ARIA label for accessibility |
92
+
93
+ ## Variants
94
+
95
+ | Variant | Description |
96
+ |---------|-------------|
97
+ | `default` | Primary button style with solid background |
98
+ | `destructive` | Red/danger button for destructive actions |
99
+ | `outline` | Button with border and transparent background |
100
+ | `secondary` | Secondary button with muted background |
101
+ | `ghost` | Minimal button with no background until hover |
102
+ | `link` | Text-only button styled as a link |
103
+
104
+ ## Sizes
105
+
106
+ | Size | Description |
107
+ |------|-------------|
108
+ | `default` | Standard button size (h-10) |
109
+ | `sm` | Small button (h-9) |
110
+ | `lg` | Large button (h-11) |
111
+ | `icon` | Square icon button (h-10 w-10) |
112
+
113
+ ## Accessibility
114
+
115
+ - Uses semantic `<button>` element
116
+ - Supports `aria-label` for screen readers
117
+ - Disabled state is properly communicated via `disabled` attribute
118
+ - Focus ring visible on keyboard navigation
119
+ - Disabled buttons have reduced opacity
120
+
121
+ ## Customization
122
+
123
+ ### Modifying Styles
124
+
125
+ Edit `button.styles.json` to customize the appearance:
126
+
127
+ ```json
128
+ {
129
+ "buttonStyles": {
130
+ "base": "your-base-classes",
131
+ "variants": {
132
+ "variant": {
133
+ "custom": "your-custom-variant-classes"
134
+ }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Adding New Variants
141
+
142
+ 1. Add the variant to `button.styles.json`:
143
+
144
+ ```json
145
+ {
146
+ "variants": {
147
+ "variant": {
148
+ "success": "bg-green-500 text-white hover:bg-green-600"
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ 2. Use it in your component:
155
+
156
+ ```json
157
+ {
158
+ "kind": "component",
159
+ "name": "Button",
160
+ "props": {
161
+ "variant": { "expr": "lit", "value": "success" }
162
+ }
163
+ }
164
+ ```
@@ -0,0 +1,27 @@
1
+ {
2
+ "params": {
3
+ "variant": { "type": "string", "required": false },
4
+ "size": { "type": "string", "required": false },
5
+ "disabled": { "type": "boolean", "required": false },
6
+ "type": { "type": "string", "required": false },
7
+ "ariaLabel": { "type": "string", "required": false }
8
+ },
9
+ "view": {
10
+ "kind": "element",
11
+ "tag": "button",
12
+ "props": {
13
+ "className": {
14
+ "expr": "style",
15
+ "preset": "buttonStyles",
16
+ "props": {
17
+ "variant": { "expr": "param", "name": "variant" },
18
+ "size": { "expr": "param", "name": "size" }
19
+ }
20
+ },
21
+ "disabled": { "expr": "param", "name": "disabled" },
22
+ "type": { "expr": "param", "name": "type" },
23
+ "aria-label": { "expr": "param", "name": "ariaLabel" }
24
+ },
25
+ "children": [{ "kind": "slot" }]
26
+ }
27
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "buttonStyles": {
3
+ "base": "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
4
+ "variants": {
5
+ "variant": {
6
+ "default": "bg-primary text-primary-foreground hover:bg-primary/90",
7
+ "destructive": "bg-destructive text-destructive-foreground hover:bg-destructive/90",
8
+ "outline": "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
9
+ "secondary": "bg-secondary text-secondary-foreground hover:bg-secondary/80",
10
+ "ghost": "hover:bg-accent hover:text-accent-foreground",
11
+ "link": "text-primary underline-offset-4 hover:underline"
12
+ },
13
+ "size": {
14
+ "default": "h-10 px-4 py-2",
15
+ "sm": "h-9 rounded-md px-3",
16
+ "lg": "h-11 rounded-md px-8",
17
+ "icon": "h-10 w-10"
18
+ }
19
+ },
20
+ "defaultVariants": {
21
+ "variant": "default",
22
+ "size": "default"
23
+ }
24
+ }
25
+ }