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