@ankhorage/zora 4.3.0 → 4.4.1

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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/card/meta.d.ts +1 -1
  3. package/dist/components/radio/RadioGroup.d.ts.map +1 -1
  4. package/dist/components/radio/RadioGroup.js +27 -7
  5. package/dist/components/radio/RadioGroup.js.map +1 -1
  6. package/dist/components/radio/meta.d.ts +203 -4
  7. package/dist/components/radio/meta.d.ts.map +1 -1
  8. package/dist/components/radio/meta.js +168 -4
  9. package/dist/components/radio/meta.js.map +1 -1
  10. package/dist/components/radio/resolveRadioGroupThemeRecipe.d.ts +20 -0
  11. package/dist/components/radio/resolveRadioGroupThemeRecipe.d.ts.map +1 -0
  12. package/dist/components/radio/resolveRadioGroupThemeRecipe.js +34 -0
  13. package/dist/components/radio/resolveRadioGroupThemeRecipe.js.map +1 -0
  14. package/dist/components/radio/themeRecipeMeta.d.ts +26 -0
  15. package/dist/components/radio/themeRecipeMeta.d.ts.map +1 -0
  16. package/dist/components/radio/themeRecipeMeta.js +27 -0
  17. package/dist/components/radio/themeRecipeMeta.js.map +1 -0
  18. package/dist/components/radio/types.d.ts +1 -0
  19. package/dist/components/radio/types.d.ts.map +1 -1
  20. package/dist/components/radio/types.js.map +1 -1
  21. package/dist/foundation/meta.d.ts +4 -4
  22. package/dist/layout/screen/meta.d.ts +1 -1
  23. package/dist/layout/screen-section/meta.d.ts +1 -1
  24. package/dist/metadata/allowedChildren.d.ts +3 -3
  25. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  26. package/dist/metadata/allowedChildren.js +1 -0
  27. package/dist/metadata/allowedChildren.js.map +1 -1
  28. package/dist/metadata/bindableComponentMeta.d.ts +2 -2
  29. package/dist/metadata/themeRecipeMeta.d.ts.map +1 -1
  30. package/dist/metadata/themeRecipeMeta.js +2 -0
  31. package/dist/metadata/themeRecipeMeta.js.map +1 -1
  32. package/dist/patterns/message-bubble/meta.d.ts +1 -1
  33. package/dist/patterns/notice/meta.d.ts +1 -1
  34. package/dist/patterns/panel/meta.d.ts +1 -1
  35. package/dist/patterns/post-card/meta.d.ts +1 -1
  36. package/dist/patterns/reader/meta.d.ts +2 -2
  37. package/dist/patterns/scanner/meta.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/src/components/radio/RadioGroup.test.ts +31 -0
  40. package/src/components/radio/RadioGroup.tsx +45 -8
  41. package/src/components/radio/meta.ts +170 -4
  42. package/src/components/radio/resolveRadioGroupThemeRecipe.ts +45 -0
  43. package/src/components/radio/themeRecipeMeta.ts +28 -0
  44. package/src/components/radio/types.ts +1 -0
  45. package/src/metadata/allowedChildren.ts +1 -0
  46. package/src/metadata/componentMeta.test.ts +21 -0
  47. package/src/metadata/radioGroupReachability.test.ts +20 -0
  48. package/src/metadata/themeRecipeMeta.test.ts +4 -0
  49. package/src/metadata/themeRecipeMeta.ts +2 -0
@@ -1,19 +1,185 @@
1
+ import { ZORA_COLORS } from '../../internal/colorModel';
1
2
  import type { ZoraComponentMeta } from '../../metadata';
2
3
 
4
+ const themeAuthoring = {
5
+ authority: 'theme',
6
+ scope: 'component',
7
+ allowInstanceOverride: true,
8
+ } as const;
9
+
3
10
  export const radioMeta = {
4
11
  name: 'Radio',
5
12
  category: 'component',
6
13
  directManifestNode: false,
7
14
  allowedChildren: [],
8
- note: 'Form control component; not represented as a manifest node in v1.',
15
+ note: 'Composition-level radio control; author repeated single-choice UI through RadioGroup.',
9
16
  props: {},
10
17
  } as const satisfies ZoraComponentMeta;
11
18
 
12
19
  export const radioGroupMeta = {
13
20
  name: 'RadioGroup',
14
21
  category: 'component',
15
- directManifestNode: false,
22
+ description:
23
+ 'Single-choice group with inline or card presentation while preserving radio semantics.',
24
+ directManifestNode: true,
16
25
  allowedChildren: [],
17
- note: 'Form control component; not represented as a manifest node in v1.',
18
- props: {},
26
+ blueprint: {
27
+ label: 'Radio group',
28
+ defaultProps: {
29
+ value: 'option-b',
30
+ options: [
31
+ { value: 'option-a', label: 'First option', description: 'Supporting detail' },
32
+ { value: 'option-b', label: 'Second option', description: 'Supporting detail' },
33
+ { value: 'option-c', label: 'Third option', description: 'Supporting detail' },
34
+ ],
35
+ orientation: 'vertical',
36
+ presentation: 'inline',
37
+ invalid: false,
38
+ readOnly: false,
39
+ disabled: false,
40
+ },
41
+ },
42
+ bindings: {
43
+ props: {
44
+ value: {
45
+ label: 'Value',
46
+ description: 'Currently selected option value.',
47
+ value: { type: 'string' },
48
+ acceptsFallback: true,
49
+ acceptsTransforms: true,
50
+ },
51
+ disabled: {
52
+ label: 'Disabled',
53
+ description: 'Whether the complete radio group is disabled.',
54
+ value: { type: 'boolean' },
55
+ acceptsFallback: true,
56
+ acceptsTransforms: true,
57
+ },
58
+ },
59
+ events: {
60
+ valueChange: {
61
+ label: 'Value change',
62
+ description: 'Runs when the selected radio value changes.',
63
+ payload: {
64
+ eventType: 'radioGroup.valueChange',
65
+ fields: [{ path: 'value', type: 'string', label: 'Value' }],
66
+ },
67
+ },
68
+ },
69
+ },
70
+ events: {
71
+ valueChange: {
72
+ label: 'Value change',
73
+ eventType: 'radioGroup.valueChange',
74
+ description: 'Emitted when the selected radio value changes.',
75
+ payloadFields: [{ path: 'value', type: 'string', label: 'Value' }],
76
+ },
77
+ },
78
+ props: {
79
+ value: {
80
+ type: 'string',
81
+ category: 'State',
82
+ label: 'Value',
83
+ authoring: { authority: 'instance' },
84
+ },
85
+ options: {
86
+ type: 'array',
87
+ category: 'Content',
88
+ label: 'Options',
89
+ itemSchema: [
90
+ {
91
+ key: 'value',
92
+ schema: {
93
+ type: 'string',
94
+ category: 'Content',
95
+ label: 'Value',
96
+ },
97
+ },
98
+ {
99
+ key: 'label',
100
+ schema: {
101
+ type: 'string',
102
+ category: 'Content',
103
+ label: 'Label',
104
+ },
105
+ },
106
+ {
107
+ key: 'description',
108
+ schema: {
109
+ type: 'string',
110
+ category: 'Content',
111
+ label: 'Description',
112
+ },
113
+ },
114
+ {
115
+ key: 'disabled',
116
+ schema: {
117
+ type: 'boolean',
118
+ category: 'State',
119
+ label: 'Disabled',
120
+ default: false,
121
+ },
122
+ },
123
+ ],
124
+ authoring: { authority: 'instance' },
125
+ },
126
+ orientation: {
127
+ type: 'enum',
128
+ category: 'Layout',
129
+ label: 'Orientation',
130
+ enum: ['horizontal', 'vertical'],
131
+ default: 'vertical',
132
+ authoring: { authority: 'instance' },
133
+ },
134
+ gap: {
135
+ type: 'enum',
136
+ category: 'Layout',
137
+ label: 'Gap',
138
+ enum: ['xs', 's', 'm', 'l'],
139
+ authoring: themeAuthoring,
140
+ },
141
+ presentation: {
142
+ type: 'enum',
143
+ category: 'Style',
144
+ label: 'Presentation',
145
+ enum: ['inline', 'card'],
146
+ default: 'inline',
147
+ authoring: { authority: 'instance' },
148
+ },
149
+ color: {
150
+ type: 'enum',
151
+ category: 'Style',
152
+ label: 'Color',
153
+ enum: [...ZORA_COLORS],
154
+ authoring: themeAuthoring,
155
+ },
156
+ size: {
157
+ type: 'enum',
158
+ category: 'Style',
159
+ label: 'Size',
160
+ enum: ['s', 'm', 'l'],
161
+ authoring: themeAuthoring,
162
+ },
163
+ invalid: {
164
+ type: 'boolean',
165
+ category: 'State',
166
+ label: 'Invalid',
167
+ default: false,
168
+ authoring: { authority: 'instance' },
169
+ },
170
+ readOnly: {
171
+ type: 'boolean',
172
+ category: 'State',
173
+ label: 'Read only',
174
+ default: false,
175
+ authoring: { authority: 'instance' },
176
+ },
177
+ disabled: {
178
+ type: 'boolean',
179
+ category: 'State',
180
+ label: 'Disabled',
181
+ default: false,
182
+ authoring: { authority: 'instance' },
183
+ },
184
+ },
19
185
  } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,45 @@
1
+ import type { ThemeRecipeOverrideValue } from '@ankhorage/contracts';
2
+
3
+ import type { ZoraColor } from '../../internal/colorModel';
4
+ import type { RadioGroupProps } from './types';
5
+
6
+ type RadioGroupGap = NonNullable<RadioGroupProps<string>['gap']>;
7
+ type RadioGroupSize = NonNullable<RadioGroupProps<string>['size']>;
8
+
9
+ /***
10
+ * Resolves RadioGroup presentation props from instance overrides and the active theme recipe.
11
+ */
12
+ export function resolveRadioGroupThemeRecipe(input: {
13
+ readonly gap?: RadioGroupGap;
14
+ readonly color?: ZoraColor;
15
+ readonly size?: RadioGroupSize;
16
+ readonly themeFields: Readonly<Record<string, ThemeRecipeOverrideValue>>;
17
+ }) {
18
+ return {
19
+ gap: input.gap ?? readGap(input.themeFields.gap) ?? 's',
20
+ color: input.color ?? readColor(input.themeFields.color) ?? 'primary',
21
+ size: input.size ?? readSize(input.themeFields.size) ?? 'm',
22
+ } as const;
23
+ }
24
+
25
+ /*** Reads a valid RadioGroup gap from a theme recipe value. */
26
+ function readGap(value: ThemeRecipeOverrideValue | undefined): RadioGroupGap | undefined {
27
+ if (value === 'xs' || value === 's' || value === 'm' || value === 'l') return value;
28
+ return undefined;
29
+ }
30
+
31
+ /*** Reads a valid ZORA color from a theme recipe value. */
32
+ function readColor(value: ThemeRecipeOverrideValue | undefined): ZoraColor | undefined {
33
+ if (value === 'primary' || value === 'secondary' || value === 'tertiary') return value;
34
+ if (value === 'quaternary' || value === 'neutral' || value === 'danger') return value;
35
+ if (value === 'success' || value === 'warning' || value === 'error' || value === 'info') {
36
+ return value;
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ /*** Reads a valid RadioGroup control size from a theme recipe value. */
42
+ function readSize(value: ThemeRecipeOverrideValue | undefined): RadioGroupSize | undefined {
43
+ if (value === 's' || value === 'm' || value === 'l') return value;
44
+ return undefined;
45
+ }
@@ -0,0 +1,28 @@
1
+ import { ZORA_COLORS } from '../../internal/colorModel';
2
+ import type { ZoraThemeRecipeMeta } from '../../metadata/themeRecipeTypes';
3
+
4
+ export const radioGroupThemeRecipeMeta = {
5
+ name: 'RadioGroup',
6
+ kind: 'component',
7
+ description: 'Maps shared selection-control presentation choices into RadioGroup defaults.',
8
+ fields: {
9
+ gap: {
10
+ type: 'choice',
11
+ label: 'Gap',
12
+ options: ['xs', 's', 'm', 'l'],
13
+ default: 's',
14
+ },
15
+ color: {
16
+ type: 'choice',
17
+ label: 'Color',
18
+ options: ZORA_COLORS,
19
+ default: 'primary',
20
+ },
21
+ size: {
22
+ type: 'choice',
23
+ label: 'Size',
24
+ options: ['s', 'm', 'l'],
25
+ default: 'm',
26
+ },
27
+ },
28
+ } as const satisfies ZoraThemeRecipeMeta;
@@ -25,4 +25,5 @@ export interface RadioGroupProps<TValue extends string>
25
25
  options: readonly RadioGroupOption<TValue>[];
26
26
  orientation?: 'horizontal' | 'vertical';
27
27
  gap?: 'xs' | 's' | 'm' | 'l';
28
+ presentation?: 'inline' | 'card';
28
29
  }
@@ -10,6 +10,7 @@ export const CONTAINER_ALLOWED_CHILDREN = [
10
10
  'Image',
11
11
  'Button',
12
12
  'ButtonGroup',
13
+ 'RadioGroup',
13
14
  'ThemeModeToggle',
14
15
  'Input',
15
16
  'Textarea',
@@ -126,6 +126,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
126
126
  'EmptyState',
127
127
  'Hero',
128
128
  'Button',
129
+ 'RadioGroup',
129
130
  'ThemeModeToggle',
130
131
  'Input',
131
132
  'Textarea',
@@ -198,6 +199,26 @@ describe('ZORA_COMPONENT_META invariants', () => {
198
199
  expect(ZORA_COMPONENT_META.Image.props.alt?.authoring).toEqual({ authority: 'instance' });
199
200
  });
200
201
 
202
+ test('RadioGroup is a direct manifest leaf with card presentation and normalized value events', () => {
203
+ const radioGroup = ZORA_COMPONENT_META.RadioGroup;
204
+
205
+ expect(radioGroup.directManifestNode).toBe(true);
206
+ expect(radioGroup.allowedChildren).toEqual([]);
207
+ expect(radioGroup.props.presentation?.enum).toEqual(['inline', 'card']);
208
+ expect(radioGroup.props.options?.type).toBe('array');
209
+ expect(radioGroup.props.options?.itemSchema?.map((item) => item.key)).toEqual([
210
+ 'value',
211
+ 'label',
212
+ 'description',
213
+ 'disabled',
214
+ ]);
215
+ expect(radioGroup.bindings?.props?.value?.value).toEqual({ type: 'string' });
216
+ expect(radioGroup.events?.valueChange?.eventType).toBe('radioGroup.valueChange');
217
+ expect(radioGroup.events?.valueChange?.payloadFields).toEqual([
218
+ { path: 'value', type: 'string', label: 'Value' },
219
+ ]);
220
+ });
221
+
201
222
  test('ThemeModeToggle is a direct manifest leaf with serializable defaults', () => {
202
223
  expect(ZORA_COMPONENT_META.ThemeModeToggle.directManifestNode).toBe(true);
203
224
  expect(ZORA_COMPONENT_META.ThemeModeToggle.allowedChildren).toEqual([]);
@@ -0,0 +1,20 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { ZORA_COMPONENT_META } from './index';
4
+
5
+ const containers = [
6
+ ['Screen', ZORA_COMPONENT_META.Screen],
7
+ ['ScreenSection', ZORA_COMPONENT_META.ScreenSection],
8
+ ['Box', ZORA_COMPONENT_META.Box],
9
+ ['Stack', ZORA_COMPONENT_META.Stack],
10
+ ['Card', ZORA_COMPONENT_META.Card],
11
+ ['Panel', ZORA_COMPONENT_META.Panel],
12
+ ] as const;
13
+
14
+ test('keeps manifest RadioGroup reachable through canonical container composition', () => {
15
+ expect(ZORA_COMPONENT_META.RadioGroup.directManifestNode).toBe(true);
16
+
17
+ for (const [name, metadata] of containers) {
18
+ expect(metadata.allowedChildren, `${name} should allow RadioGroup`).toContain('RadioGroup');
19
+ }
20
+ });
@@ -11,6 +11,7 @@ describe('ZORA_THEME_RECIPE_META', () => {
11
11
  'Button',
12
12
  'Card',
13
13
  'Heading',
14
+ 'RadioGroup',
14
15
  'Text',
15
16
  'Panel',
16
17
  ]);
@@ -50,6 +51,9 @@ describe('ZORA_THEME_RECIPE_META', () => {
50
51
  test('keeps recipe defaults aligned with existing runtime behavior', () => {
51
52
  expect(ZORA_THEME_RECIPE_META.Button?.fields.size?.default).toBe('l');
52
53
  expect(ZORA_THEME_RECIPE_META.Card?.fields.padding?.default).toBeUndefined();
54
+ expect(ZORA_THEME_RECIPE_META.RadioGroup?.fields.gap?.default).toBe('s');
55
+ expect(ZORA_THEME_RECIPE_META.RadioGroup?.fields.color?.default).toBe('primary');
56
+ expect(ZORA_THEME_RECIPE_META.RadioGroup?.fields.size?.default).toBe('m');
53
57
  expect(ZORA_THEME_RECIPE_META.Text?.fields.weight?.default).toBeUndefined();
54
58
  expect(ZORA_THEME_RECIPE_META.Heading?.fields.size?.default).toBeUndefined();
55
59
  });
@@ -1,6 +1,7 @@
1
1
  import { buttonThemeRecipeMeta } from '../components/button/themeRecipeMeta';
2
2
  import { cardThemeRecipeMeta } from '../components/card/themeRecipeMeta';
3
3
  import { headingThemeRecipeMeta } from '../components/heading/themeRecipeMeta';
4
+ import { radioGroupThemeRecipeMeta } from '../components/radio/themeRecipeMeta';
4
5
  import { textThemeRecipeMeta } from '../components/text/themeRecipeMeta';
5
6
  import { panelThemeRecipeMeta } from '../patterns/panel/themeRecipeMeta';
6
7
  import type { ZoraThemeRecipeMetaRegistry } from './themeRecipeTypes';
@@ -9,6 +10,7 @@ export const ZORA_THEME_RECIPE_META: ZoraThemeRecipeMetaRegistry = {
9
10
  Button: buttonThemeRecipeMeta,
10
11
  Card: cardThemeRecipeMeta,
11
12
  Heading: headingThemeRecipeMeta,
13
+ RadioGroup: radioGroupThemeRecipeMeta,
12
14
  Text: textThemeRecipeMeta,
13
15
  Panel: panelThemeRecipeMeta,
14
16
  };