@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,183 @@
1
+ /**
2
+ * Test suite for Toast component
3
+ *
4
+ * @constela/ui Toast component tests following TDD methodology.
5
+ * These tests verify the Toast 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
+ hasRole,
29
+ type ComponentTestContext,
30
+ } from '../../tests/helpers/test-utils.js';
31
+
32
+ describe('Toast Component', () => {
33
+ let ctx: ComponentTestContext;
34
+
35
+ beforeAll(() => {
36
+ ctx = loadComponentForTesting('toast');
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="alert" attribute', () => {
52
+ expect(hasRole(ctx.component.view, 'alert')).toBe(true);
53
+ });
54
+
55
+ it('should contain a slot for toast content', () => {
56
+ expect(hasSlot(ctx.component.view)).toBe(true);
57
+ });
58
+
59
+ it('should have className using StyleExpr with preset toastStyles', () => {
60
+ const className = findPropInView(ctx.component.view, 'className');
61
+ expect(className).not.toBeNull();
62
+ // StyleExpr should have expr: 'style' and preset reference
63
+ expect(className).toMatchObject({
64
+ expr: 'style',
65
+ preset: 'toastStyles',
66
+ });
67
+ });
68
+ });
69
+
70
+ // ==================== Params Validation Tests ====================
71
+
72
+ describe('Params Validation', () => {
73
+ const expectedParams = ['variant', 'title', 'description'];
74
+
75
+ it('should have all expected params', () => {
76
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
77
+ });
78
+
79
+ describe('param: variant', () => {
80
+ it('should be optional', () => {
81
+ expect(isOptionalParam(ctx.component, 'variant')).toBe(true);
82
+ });
83
+
84
+ it('should have type string', () => {
85
+ expect(hasParamType(ctx.component, 'variant', 'string')).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('param: title', () => {
90
+ it('should be optional', () => {
91
+ expect(isOptionalParam(ctx.component, 'title')).toBe(true);
92
+ });
93
+
94
+ it('should have type string', () => {
95
+ expect(hasParamType(ctx.component, 'title', 'string')).toBe(true);
96
+ });
97
+ });
98
+
99
+ describe('param: description', () => {
100
+ it('should be optional', () => {
101
+ expect(isOptionalParam(ctx.component, 'description')).toBe(true);
102
+ });
103
+
104
+ it('should have type string', () => {
105
+ expect(hasParamType(ctx.component, 'description', 'string')).toBe(true);
106
+ });
107
+ });
108
+ });
109
+
110
+ // ==================== Style Preset Tests ====================
111
+
112
+ describe('Style Preset', () => {
113
+ it('should have valid style preset structure', () => {
114
+ const toastStyles = ctx.styles['toastStyles'];
115
+ expect(toastStyles).toBeDefined();
116
+ assertValidStylePreset(toastStyles);
117
+ });
118
+
119
+ it('should have base classes for common toast styles', () => {
120
+ const toastStyles = ctx.styles['toastStyles'];
121
+ expect(toastStyles.base).toBeDefined();
122
+ expect(typeof toastStyles.base).toBe('string');
123
+ expect(toastStyles.base.length).toBeGreaterThan(0);
124
+ });
125
+
126
+ describe('variant options', () => {
127
+ const variantOptions = ['default', 'success', 'error', 'warning', 'info'];
128
+
129
+ it('should have variant variants', () => {
130
+ const toastStyles = ctx.styles['toastStyles'];
131
+ expect(hasVariants(toastStyles, ['variant'])).toBe(true);
132
+ });
133
+
134
+ it.each(variantOptions)('should have %s variant option', (option) => {
135
+ const toastStyles = ctx.styles['toastStyles'];
136
+ expect(hasVariantOptions(toastStyles, 'variant', [option])).toBe(true);
137
+ });
138
+ });
139
+
140
+ describe('default variants', () => {
141
+ it('should have default variant set to default', () => {
142
+ const toastStyles = ctx.styles['toastStyles'];
143
+ expect(hasDefaultVariants(toastStyles, { variant: 'default' })).toBe(true);
144
+ });
145
+ });
146
+ });
147
+
148
+ // ==================== Accessibility Tests ====================
149
+
150
+ describe('Accessibility', () => {
151
+ it('should have role="alert" for screen readers', () => {
152
+ const role = findPropInView(ctx.component.view, 'role');
153
+ expect(role).not.toBeNull();
154
+ expect(role).toMatchObject({
155
+ expr: 'lit',
156
+ value: 'alert',
157
+ });
158
+ });
159
+
160
+ it('should have aria-live="polite" for non-intrusive notifications', () => {
161
+ const ariaLive = findPropInView(ctx.component.view, 'aria-live');
162
+ expect(ariaLive).not.toBeNull();
163
+ expect(ariaLive).toMatchObject({
164
+ expr: 'lit',
165
+ value: 'polite',
166
+ });
167
+ });
168
+ });
169
+
170
+ // ==================== View Props Tests ====================
171
+
172
+ describe('View Props', () => {
173
+ it('should pass variant to StyleExpr', () => {
174
+ const className = findPropInView(ctx.component.view, 'className');
175
+ expect(className).toMatchObject({
176
+ expr: 'style',
177
+ props: expect.objectContaining({
178
+ variant: expect.objectContaining({ expr: 'param', name: 'variant' }),
179
+ }),
180
+ });
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "params": {
3
+ "content": { "type": "string", "required": false },
4
+ "position": { "type": "string", "required": false }
5
+ },
6
+ "view": {
7
+ "kind": "element",
8
+ "tag": "div",
9
+ "props": {
10
+ "role": { "expr": "lit", "value": "tooltip" },
11
+ "className": {
12
+ "expr": "style",
13
+ "preset": "tooltipStyles",
14
+ "props": {
15
+ "position": { "expr": "param", "name": "position" }
16
+ }
17
+ }
18
+ },
19
+ "children": [{ "kind": "slot" }]
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "tooltipStyles": {
3
+ "base": "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
4
+ "variants": {
5
+ "position": {
6
+ "top": "bottom-full mb-2",
7
+ "right": "left-full ml-2",
8
+ "bottom": "top-full mt-2",
9
+ "left": "right-full mr-2"
10
+ }
11
+ },
12
+ "defaultVariants": {
13
+ "position": "top"
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Test suite for Tooltip component
3
+ *
4
+ * @constela/ui Tooltip component tests following TDD methodology.
5
+ * These tests verify the Tooltip 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
+ hasRole,
29
+ type ComponentTestContext,
30
+ } from '../../tests/helpers/test-utils.js';
31
+
32
+ describe('Tooltip Component', () => {
33
+ let ctx: ComponentTestContext;
34
+
35
+ beforeAll(() => {
36
+ ctx = loadComponentForTesting('tooltip');
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="tooltip" attribute', () => {
52
+ expect(hasRole(ctx.component.view, 'tooltip')).toBe(true);
53
+ });
54
+
55
+ it('should contain a slot for trigger element', () => {
56
+ expect(hasSlot(ctx.component.view)).toBe(true);
57
+ });
58
+
59
+ it('should have className using StyleExpr with preset tooltipStyles', () => {
60
+ const className = findPropInView(ctx.component.view, 'className');
61
+ expect(className).not.toBeNull();
62
+ // StyleExpr should have expr: 'style' and preset reference
63
+ expect(className).toMatchObject({
64
+ expr: 'style',
65
+ preset: 'tooltipStyles',
66
+ });
67
+ });
68
+ });
69
+
70
+ // ==================== Params Validation Tests ====================
71
+
72
+ describe('Params Validation', () => {
73
+ const expectedParams = ['content', 'position'];
74
+
75
+ it('should have all expected params', () => {
76
+ expect(hasParams(ctx.component, expectedParams)).toBe(true);
77
+ });
78
+
79
+ describe('param: content', () => {
80
+ it('should be optional', () => {
81
+ expect(isOptionalParam(ctx.component, 'content')).toBe(true);
82
+ });
83
+
84
+ it('should have type string', () => {
85
+ expect(hasParamType(ctx.component, 'content', 'string')).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('param: position', () => {
90
+ it('should be optional', () => {
91
+ expect(isOptionalParam(ctx.component, 'position')).toBe(true);
92
+ });
93
+
94
+ it('should have type string', () => {
95
+ expect(hasParamType(ctx.component, 'position', 'string')).toBe(true);
96
+ });
97
+ });
98
+ });
99
+
100
+ // ==================== Style Preset Tests ====================
101
+
102
+ describe('Style Preset', () => {
103
+ it('should have valid style preset structure', () => {
104
+ const tooltipStyles = ctx.styles['tooltipStyles'];
105
+ expect(tooltipStyles).toBeDefined();
106
+ assertValidStylePreset(tooltipStyles);
107
+ });
108
+
109
+ it('should have base classes for common tooltip styles', () => {
110
+ const tooltipStyles = ctx.styles['tooltipStyles'];
111
+ expect(tooltipStyles.base).toBeDefined();
112
+ expect(typeof tooltipStyles.base).toBe('string');
113
+ expect(tooltipStyles.base.length).toBeGreaterThan(0);
114
+ });
115
+
116
+ describe('position variants', () => {
117
+ const positionOptions = ['top', 'right', 'bottom', 'left'];
118
+
119
+ it('should have position variants', () => {
120
+ const tooltipStyles = ctx.styles['tooltipStyles'];
121
+ expect(hasVariants(tooltipStyles, ['position'])).toBe(true);
122
+ });
123
+
124
+ it.each(positionOptions)('should have %s position option', (option) => {
125
+ const tooltipStyles = ctx.styles['tooltipStyles'];
126
+ expect(hasVariantOptions(tooltipStyles, 'position', [option])).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe('default variants', () => {
131
+ it('should have default position set to top', () => {
132
+ const tooltipStyles = ctx.styles['tooltipStyles'];
133
+ expect(hasDefaultVariants(tooltipStyles, { position: 'top' })).toBe(true);
134
+ });
135
+ });
136
+ });
137
+
138
+ // ==================== Accessibility Tests ====================
139
+
140
+ describe('Accessibility', () => {
141
+ it('should have role="tooltip" for screen readers', () => {
142
+ const role = findPropInView(ctx.component.view, 'role');
143
+ expect(role).not.toBeNull();
144
+ expect(role).toMatchObject({
145
+ expr: 'lit',
146
+ value: 'tooltip',
147
+ });
148
+ });
149
+ });
150
+
151
+ // ==================== View Props Tests ====================
152
+
153
+ describe('View Props', () => {
154
+ it('should pass position to StyleExpr', () => {
155
+ const className = findPropInView(ctx.component.view, 'className');
156
+ expect(className).toMatchObject({
157
+ expr: 'style',
158
+ props: expect.objectContaining({
159
+ position: expect.objectContaining({ expr: 'param', name: 'position' }),
160
+ }),
161
+ });
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,54 @@
1
+ export { ComponentDef, Expression, ParamDef, StylePreset, ViewNode, validateAst } from '@constela/core';
2
+
3
+ /**
4
+ * @constela/ui - Copy-paste UI components for Constela
5
+ *
6
+ * A shadcn/ui style component library providing pre-built,
7
+ * accessible UI components written in Constela JSON DSL.
8
+ */
9
+
10
+ /**
11
+ * Component validation result
12
+ */
13
+ interface ComponentValidationResult {
14
+ valid: boolean;
15
+ errors: string[];
16
+ }
17
+ /**
18
+ * Style preset validation result
19
+ */
20
+ interface StyleValidationResult {
21
+ valid: boolean;
22
+ errors: string[];
23
+ }
24
+ /**
25
+ * Validate a Constela component definition
26
+ */
27
+ declare function validateComponent(component: unknown): ComponentValidationResult;
28
+ /**
29
+ * Validate a style preset definition
30
+ */
31
+ declare function validateStylePreset(preset: unknown): StyleValidationResult;
32
+ /**
33
+ * Button component variant types
34
+ */
35
+ type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
36
+ type ButtonSize = 'default' | 'sm' | 'lg' | 'icon';
37
+ /**
38
+ * Input component type
39
+ */
40
+ type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search';
41
+ /**
42
+ * Alert component variant types
43
+ */
44
+ type AlertVariant = 'default' | 'destructive';
45
+ /**
46
+ * Badge component variant types
47
+ */
48
+ type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline';
49
+ /**
50
+ * Toast component variant types
51
+ */
52
+ type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info';
53
+
54
+ export { type AlertVariant, type BadgeVariant, type ButtonSize, type ButtonVariant, type ComponentValidationResult, type InputType, type StyleValidationResult, type ToastVariant, validateComponent, validateStylePreset };
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ // src/index.ts
2
+ import { validateAst } from "@constela/core";
3
+ function validateComponent(component) {
4
+ const errors = [];
5
+ if (!component || typeof component !== "object") {
6
+ return { valid: false, errors: ["Component must be an object"] };
7
+ }
8
+ const comp = component;
9
+ if (!comp["view"]) {
10
+ errors.push("Component must have a view property");
11
+ }
12
+ if (comp["params"] !== void 0) {
13
+ if (typeof comp["params"] !== "object" || comp["params"] === null) {
14
+ errors.push("params must be an object");
15
+ } else {
16
+ const params = comp["params"];
17
+ for (const [name, def] of Object.entries(params)) {
18
+ if (!def || typeof def !== "object") {
19
+ errors.push(`Param "${name}" must be an object`);
20
+ continue;
21
+ }
22
+ const paramDef = def;
23
+ if (!paramDef["type"] || typeof paramDef["type"] !== "string") {
24
+ errors.push(`Param "${name}" must have a string type property`);
25
+ }
26
+ }
27
+ }
28
+ }
29
+ return {
30
+ valid: errors.length === 0,
31
+ errors
32
+ };
33
+ }
34
+ function validateStylePreset(preset) {
35
+ const errors = [];
36
+ if (!preset || typeof preset !== "object") {
37
+ return { valid: false, errors: ["Style preset must be an object"] };
38
+ }
39
+ const style = preset;
40
+ if (typeof style["base"] !== "string") {
41
+ errors.push("Style preset must have a string base property");
42
+ }
43
+ if (style["variants"] !== void 0) {
44
+ if (typeof style["variants"] !== "object" || style["variants"] === null) {
45
+ errors.push("variants must be an object");
46
+ } else {
47
+ const variants = style["variants"];
48
+ for (const [variantName, variantOptions] of Object.entries(variants)) {
49
+ if (typeof variantOptions !== "object" || variantOptions === null) {
50
+ errors.push(`Variant "${variantName}" must be an object`);
51
+ continue;
52
+ }
53
+ const options = variantOptions;
54
+ for (const [optionName, optionValue] of Object.entries(options)) {
55
+ if (typeof optionValue !== "string") {
56
+ errors.push(`Variant "${variantName}.${optionName}" must be a string`);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ if (style["defaultVariants"] !== void 0) {
63
+ if (typeof style["defaultVariants"] !== "object" || style["defaultVariants"] === null) {
64
+ errors.push("defaultVariants must be an object");
65
+ } else {
66
+ const defaults = style["defaultVariants"];
67
+ for (const [name, value] of Object.entries(defaults)) {
68
+ if (typeof value !== "string") {
69
+ errors.push(`defaultVariants.${name} must be a string`);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ valid: errors.length === 0,
76
+ errors
77
+ };
78
+ }
79
+ export {
80
+ validateAst,
81
+ validateComponent,
82
+ validateStylePreset
83
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@constela/ui",
3
+ "version": "0.2.0",
4
+ "description": "Copy-paste UI components for Constela - shadcn/ui style component library",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "components"
17
+ ],
18
+ "dependencies": {
19
+ "@constela/core": "0.15.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.10.0",
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.3.0",
25
+ "vitest": "^2.0.0"
26
+ },
27
+ "peerDependencies": {},
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ },
31
+ "license": "MIT",
32
+ "scripts": {
33
+ "build": "tsup src/index.ts --format esm --dts --clean",
34
+ "type-check": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "clean": "rm -rf dist"
38
+ }
39
+ }