@ankhorage/zora 2.11.0 → 2.13.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 (157) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/dist/components/button/Button.d.ts.map +1 -1
  4. package/dist/components/button/Button.js +4 -2
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/button/meta.d.ts +3 -6
  7. package/dist/components/button/meta.d.ts.map +1 -1
  8. package/dist/components/button/meta.js +9 -15
  9. package/dist/components/button/meta.js.map +1 -1
  10. package/dist/components/button/resolveButtonThemeRecipe.d.ts +9 -0
  11. package/dist/components/button/resolveButtonThemeRecipe.d.ts.map +1 -0
  12. package/dist/components/button/resolveButtonThemeRecipe.js +30 -0
  13. package/dist/components/button/resolveButtonThemeRecipe.js.map +1 -0
  14. package/dist/components/button/themeRecipeMeta.d.ts +4 -10
  15. package/dist/components/button/themeRecipeMeta.d.ts.map +1 -1
  16. package/dist/components/button/themeRecipeMeta.js +5 -10
  17. package/dist/components/button/themeRecipeMeta.js.map +1 -1
  18. package/dist/components/card/Card.d.ts.map +1 -1
  19. package/dist/components/card/Card.js +8 -4
  20. package/dist/components/card/Card.js.map +1 -1
  21. package/dist/components/card/meta.d.ts +22 -3
  22. package/dist/components/card/meta.d.ts.map +1 -1
  23. package/dist/components/card/meta.js +14 -15
  24. package/dist/components/card/meta.js.map +1 -1
  25. package/dist/components/card/resolveCardThemeRecipe.d.ts +15 -0
  26. package/dist/components/card/resolveCardThemeRecipe.d.ts.map +1 -0
  27. package/dist/components/card/resolveCardThemeRecipe.js +22 -0
  28. package/dist/components/card/resolveCardThemeRecipe.js.map +1 -0
  29. package/dist/components/card/themeRecipeMeta.d.ts +1 -1
  30. package/dist/components/card/themeRecipeMeta.js +1 -1
  31. package/dist/components/card/themeRecipeMeta.js.map +1 -1
  32. package/dist/components/card/types.d.ts +2 -0
  33. package/dist/components/card/types.d.ts.map +1 -1
  34. package/dist/components/card/types.js.map +1 -1
  35. package/dist/components/heading/Heading.d.ts.map +1 -1
  36. package/dist/components/heading/Heading.js +14 -15
  37. package/dist/components/heading/Heading.js.map +1 -1
  38. package/dist/components/heading/meta.d.ts +6 -1
  39. package/dist/components/heading/meta.d.ts.map +1 -1
  40. package/dist/components/heading/meta.js +13 -22
  41. package/dist/components/heading/meta.js.map +1 -1
  42. package/dist/components/heading/resolveHeadingThemeRecipe.d.ts +11 -0
  43. package/dist/components/heading/resolveHeadingThemeRecipe.d.ts.map +1 -0
  44. package/dist/components/heading/resolveHeadingThemeRecipe.js +46 -0
  45. package/dist/components/heading/resolveHeadingThemeRecipe.js.map +1 -0
  46. package/dist/components/heading/themeRecipeMeta.d.ts +39 -0
  47. package/dist/components/heading/themeRecipeMeta.d.ts.map +1 -0
  48. package/dist/components/heading/themeRecipeMeta.js +32 -0
  49. package/dist/components/heading/themeRecipeMeta.js.map +1 -0
  50. package/dist/components/image/meta.d.ts +24 -3
  51. package/dist/components/image/meta.d.ts.map +1 -1
  52. package/dist/components/image/meta.js +18 -3
  53. package/dist/components/image/meta.js.map +1 -1
  54. package/dist/components/text/Text.d.ts +0 -1
  55. package/dist/components/text/Text.d.ts.map +1 -1
  56. package/dist/components/text/Text.js +14 -16
  57. package/dist/components/text/Text.js.map +1 -1
  58. package/dist/components/text/meta.d.ts +6 -5
  59. package/dist/components/text/meta.d.ts.map +1 -1
  60. package/dist/components/text/meta.js +13 -25
  61. package/dist/components/text/meta.js.map +1 -1
  62. package/dist/components/text/resolveTextThemeRecipe.d.ts +11 -0
  63. package/dist/components/text/resolveTextThemeRecipe.d.ts.map +1 -0
  64. package/dist/components/text/resolveTextThemeRecipe.js +46 -0
  65. package/dist/components/text/resolveTextThemeRecipe.js.map +1 -0
  66. package/dist/components/text/themeRecipeMeta.d.ts +15 -7
  67. package/dist/components/text/themeRecipeMeta.d.ts.map +1 -1
  68. package/dist/components/text/themeRecipeMeta.js +12 -11
  69. package/dist/components/text/themeRecipeMeta.js.map +1 -1
  70. package/dist/metadata/bindableComponentMeta.d.ts +22 -7
  71. package/dist/metadata/bindableComponentMeta.d.ts.map +1 -1
  72. package/dist/metadata/bindableComponentMeta.js +3 -13
  73. package/dist/metadata/bindableComponentMeta.js.map +1 -1
  74. package/dist/metadata/themeRecipeMeta.d.ts.map +1 -1
  75. package/dist/metadata/themeRecipeMeta.js +2 -0
  76. package/dist/metadata/themeRecipeMeta.js.map +1 -1
  77. package/dist/metadata/themeRecipeTypes.d.ts +3 -3
  78. package/dist/metadata/themeRecipeTypes.d.ts.map +1 -1
  79. package/dist/metadata/themeRecipeTypes.js.map +1 -1
  80. package/dist/metadata/types.d.ts +4 -2
  81. package/dist/metadata/types.d.ts.map +1 -1
  82. package/dist/metadata/types.js.map +1 -1
  83. package/dist/patterns/panel/Panel.d.ts +0 -2
  84. package/dist/patterns/panel/Panel.d.ts.map +1 -1
  85. package/dist/patterns/panel/Panel.js +6 -4
  86. package/dist/patterns/panel/Panel.js.map +1 -1
  87. package/dist/patterns/panel/meta.d.ts +22 -3
  88. package/dist/patterns/panel/meta.d.ts.map +1 -1
  89. package/dist/patterns/panel/meta.js +14 -15
  90. package/dist/patterns/panel/meta.js.map +1 -1
  91. package/dist/patterns/panel/themeRecipeMeta.d.ts +1 -1
  92. package/dist/patterns/panel/themeRecipeMeta.js +1 -1
  93. package/dist/patterns/panel/themeRecipeMeta.js.map +1 -1
  94. package/dist/patterns/panel/types.d.ts +2 -0
  95. package/dist/patterns/panel/types.d.ts.map +1 -1
  96. package/dist/patterns/panel/types.js.map +1 -1
  97. package/dist/theme/ZoraProvider.d.ts +4 -12
  98. package/dist/theme/ZoraProvider.d.ts.map +1 -1
  99. package/dist/theme/ZoraProvider.js +5 -14
  100. package/dist/theme/ZoraProvider.js.map +1 -1
  101. package/dist/theme/ZoraThemeRuntimeContext.d.ts +1 -2
  102. package/dist/theme/ZoraThemeRuntimeContext.d.ts.map +1 -1
  103. package/dist/theme/ZoraThemeRuntimeContext.js +0 -1
  104. package/dist/theme/ZoraThemeRuntimeContext.js.map +1 -1
  105. package/dist/theme/ZoraThemeScope.d.ts.map +1 -1
  106. package/dist/theme/ZoraThemeScope.js +4 -10
  107. package/dist/theme/ZoraThemeScope.js.map +1 -1
  108. package/dist/theme/resolveZoraThemeRecipe.d.ts +16 -0
  109. package/dist/theme/resolveZoraThemeRecipe.d.ts.map +1 -0
  110. package/dist/theme/resolveZoraThemeRecipe.js +56 -0
  111. package/dist/theme/resolveZoraThemeRecipe.js.map +1 -0
  112. package/dist/theme/useZoraTheme.d.ts +1 -1
  113. package/dist/theme/useZoraThemeRecipe.d.ts +2 -0
  114. package/dist/theme/useZoraThemeRecipe.d.ts.map +1 -0
  115. package/dist/theme/useZoraThemeRecipe.js +7 -0
  116. package/dist/theme/useZoraThemeRecipe.js.map +1 -0
  117. package/package.json +4 -4
  118. package/src/components/button/Button.tsx +4 -2
  119. package/src/components/button/meta.ts +10 -15
  120. package/src/components/button/resolveButtonThemeRecipe.test.ts +25 -0
  121. package/src/components/button/resolveButtonThemeRecipe.ts +41 -0
  122. package/src/components/button/themeRecipeMeta.ts +5 -10
  123. package/src/components/card/Card.tsx +13 -7
  124. package/src/components/card/meta.ts +15 -15
  125. package/src/components/card/resolveCardThemeRecipe.test.ts +28 -0
  126. package/src/components/card/resolveCardThemeRecipe.ts +33 -0
  127. package/src/components/card/themeRecipeMeta.ts +1 -1
  128. package/src/components/card/types.ts +2 -0
  129. package/src/components/contentFallback.test.ts +4 -4
  130. package/src/components/heading/Heading.tsx +18 -28
  131. package/src/components/heading/meta.ts +14 -22
  132. package/src/components/heading/resolveHeadingThemeRecipe.ts +55 -0
  133. package/src/components/heading/themeRecipeMeta.ts +33 -0
  134. package/src/components/image/meta.ts +18 -3
  135. package/src/components/text/Text.tsx +16 -25
  136. package/src/components/text/meta.ts +14 -25
  137. package/src/components/text/resolveTextThemeRecipe.ts +48 -0
  138. package/src/components/text/themeRecipeMeta.ts +12 -11
  139. package/src/metadata/authoringMeta.test.ts +18 -44
  140. package/src/metadata/bindableComponentMeta.test.ts +13 -0
  141. package/src/metadata/bindableComponentMeta.ts +4 -13
  142. package/src/metadata/componentMeta.test.ts +14 -0
  143. package/src/metadata/themeRecipeMeta.test.ts +41 -39
  144. package/src/metadata/themeRecipeMeta.ts +2 -0
  145. package/src/metadata/themeRecipeTypes.ts +3 -3
  146. package/src/metadata/types.ts +11 -24
  147. package/src/patterns/panel/Panel.tsx +14 -4
  148. package/src/patterns/panel/meta.ts +15 -15
  149. package/src/patterns/panel/themeRecipeMeta.ts +1 -1
  150. package/src/patterns/panel/types.ts +2 -0
  151. package/src/theme/ZoraProvider.tsx +10 -13
  152. package/src/theme/ZoraThemeRuntimeContext.tsx +1 -3
  153. package/src/theme/ZoraThemeScope.tsx +5 -25
  154. package/src/theme/resolveZoraThemeRecipe.test.ts +72 -0
  155. package/src/theme/resolveZoraThemeRecipe.ts +91 -0
  156. package/src/theme/themeConfigScope.test.ts +25 -0
  157. package/src/theme/useZoraThemeRecipe.ts +7 -0
@@ -1,21 +1,19 @@
1
1
  import { ZORA_COLORS, ZORA_EMPHASES } from '../../internal/colorModel';
2
2
  import type { ZoraComponentMeta } from '../../metadata';
3
3
 
4
+ const themeAuthoring = {
5
+ authority: 'theme',
6
+ scope: 'component',
7
+ allowInstanceOverride: true,
8
+ } as const;
9
+
4
10
  export const headingMeta = {
5
11
  name: 'Heading',
6
12
  category: 'component',
7
13
  directManifestNode: true,
8
14
  allowedChildren: [],
9
- blueprint: {
10
- label: 'Heading',
11
- defaultProps: {
12
- text: 'Heading',
13
- level: 2,
14
- },
15
- },
16
- i18n: {
17
- fields: [{ keyProp: 'i18nKey', defaultTextProp: 'text' }],
18
- },
15
+ blueprint: { label: 'Heading', defaultProps: { text: 'Heading', level: 2 } },
16
+ i18n: { fields: [{ keyProp: 'i18nKey', defaultTextProp: 'text' }] },
19
17
  props: {
20
18
  text: {
21
19
  type: 'string',
@@ -43,43 +41,37 @@ export const headingMeta = {
43
41
  category: 'Typography',
44
42
  label: 'Size',
45
43
  enum: ['display', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
46
- authoring: { authority: 'theme', scope: 'component' },
44
+ authoring: themeAuthoring,
47
45
  },
48
46
  color: {
49
47
  type: 'enum',
50
48
  category: 'Style',
51
49
  label: 'Color',
52
50
  enum: ZORA_COLORS,
53
- authoring: { authority: 'theme', scope: 'component' },
51
+ authoring: themeAuthoring,
54
52
  },
55
53
  emphasis: {
56
54
  type: 'enum',
57
55
  category: 'Style',
58
56
  label: 'Emphasis',
59
57
  enum: ZORA_EMPHASES,
60
- authoring: { authority: 'theme', scope: 'component' },
58
+ authoring: themeAuthoring,
61
59
  },
62
60
  align: {
63
61
  type: 'enum',
64
62
  category: 'Layout',
65
63
  label: 'Align',
66
64
  enum: ['auto', 'left', 'right', 'center', 'justify'],
67
- authoring: { authority: 'theme', scope: 'component' },
65
+ authoring: themeAuthoring,
68
66
  },
69
67
  weight: {
70
68
  type: 'enum',
71
69
  category: 'Typography',
72
70
  label: 'Weight',
73
71
  enum: ['regular', 'medium', 'semiBold', 'bold'],
74
- authoring: { authority: 'theme', scope: 'component' },
75
- },
76
- italic: {
77
- type: 'boolean',
78
- category: 'Typography',
79
- label: 'Italic',
80
- default: false,
81
- authoring: { authority: 'theme', scope: 'component' },
72
+ authoring: themeAuthoring,
82
73
  },
74
+ italic: { type: 'boolean', category: 'Typography', label: 'Italic', authoring: themeAuthoring },
83
75
  numberOfLines: {
84
76
  type: 'number',
85
77
  category: 'Layout',
@@ -0,0 +1,55 @@
1
+ import type { ThemeRecipeOverrideValue } from '@ankhorage/contracts';
2
+
3
+ import type {
4
+ HeadingAlign,
5
+ HeadingColor,
6
+ HeadingEmphasis,
7
+ HeadingSize,
8
+ HeadingWeight,
9
+ } from './types';
10
+
11
+ export function resolveHeadingThemeRecipe(
12
+ fields: Readonly<Record<string, ThemeRecipeOverrideValue>>,
13
+ ) {
14
+ return {
15
+ size: readSize(fields.size),
16
+ color: readColor(fields.color),
17
+ emphasis: readEmphasis(fields.emphasis),
18
+ align: readAlign(fields.align),
19
+ weight: readWeight(fields.weight),
20
+ italic: typeof fields.italic === 'boolean' ? fields.italic : undefined,
21
+ };
22
+ }
23
+
24
+ function readSize(value: ThemeRecipeOverrideValue | undefined): HeadingSize | undefined {
25
+ if (value === 'display' || value === 'h1' || value === 'h2' || value === 'h3') return value;
26
+ if (value === 'h4' || value === 'h5' || value === 'h6') return value;
27
+ return undefined;
28
+ }
29
+
30
+ function readColor(value: ThemeRecipeOverrideValue | undefined): HeadingColor | undefined {
31
+ if (typeof value !== 'string') return undefined;
32
+ if (value === 'primary' || value === 'secondary' || value === 'tertiary') return value;
33
+ if (value === 'quaternary' || value === 'neutral' || value === 'danger') return value;
34
+ if (value === 'success' || value === 'warning' || value === 'error' || value === 'info')
35
+ return value;
36
+ return undefined;
37
+ }
38
+
39
+ function readEmphasis(value: ThemeRecipeOverrideValue | undefined): HeadingEmphasis | undefined {
40
+ if (value === 'default' || value === 'muted' || value === 'subtle' || value === 'inverse')
41
+ return value;
42
+ return undefined;
43
+ }
44
+
45
+ function readAlign(value: ThemeRecipeOverrideValue | undefined): HeadingAlign | undefined {
46
+ if (value === 'auto' || value === 'left' || value === 'right') return value;
47
+ if (value === 'center' || value === 'justify') return value;
48
+ return undefined;
49
+ }
50
+
51
+ function readWeight(value: ThemeRecipeOverrideValue | undefined): HeadingWeight | undefined {
52
+ if (value === 'regular' || value === 'medium' || value === 'semiBold' || value === 'bold')
53
+ return value;
54
+ return undefined;
55
+ }
@@ -0,0 +1,33 @@
1
+ import { ZORA_COLORS, ZORA_EMPHASES } from '../../internal/colorModel';
2
+ import type { ZoraThemeRecipeMeta } from '../../metadata/themeRecipeTypes';
3
+
4
+ export const headingThemeRecipeMeta = {
5
+ name: 'Heading',
6
+ kind: 'component',
7
+ description: 'Maps Heading presentation defaults without replacing semantic heading levels.',
8
+ fields: {
9
+ size: {
10
+ type: 'choice',
11
+ label: 'Size',
12
+ options: ['display', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
13
+ },
14
+ color: { type: 'choice', label: 'Color', options: ZORA_COLORS },
15
+ emphasis: {
16
+ type: 'choice',
17
+ label: 'Emphasis',
18
+ options: ZORA_EMPHASES,
19
+ default: 'default',
20
+ },
21
+ align: {
22
+ type: 'choice',
23
+ label: 'Align',
24
+ options: ['auto', 'left', 'right', 'center', 'justify'],
25
+ },
26
+ weight: {
27
+ type: 'choice',
28
+ label: 'Weight',
29
+ options: ['regular', 'medium', 'semiBold', 'bold'],
30
+ },
31
+ italic: { type: 'boolean', label: 'Italic', default: false },
32
+ },
33
+ } as const satisfies ZoraThemeRecipeMeta;
@@ -3,8 +3,23 @@ import type { ZoraComponentMeta } from '../../metadata';
3
3
  export const imageMeta = {
4
4
  name: 'Image',
5
5
  category: 'component',
6
- directManifestNode: false,
6
+ description: 'Displays an image from the app media registry or a resolved runtime source.',
7
+ directManifestNode: true,
7
8
  allowedChildren: [],
8
- note: 'Media component; not represented as a manifest node in v1.',
9
- props: {},
9
+ blueprint: { label: 'Image' },
10
+ props: {
11
+ source: {
12
+ type: 'media',
13
+ category: 'Content',
14
+ label: 'Source',
15
+ mediaKinds: ['image'],
16
+ authoring: { authority: 'instance' },
17
+ },
18
+ alt: {
19
+ type: 'string',
20
+ category: 'Accessibility',
21
+ label: 'Alt text',
22
+ authoring: { authority: 'instance' },
23
+ },
24
+ },
10
25
  } as const satisfies ZoraComponentMeta;
@@ -3,8 +3,10 @@ import React from 'react';
3
3
  import { Platform, Text as ReactNativeText, type TextStyle } from 'react-native';
4
4
 
5
5
  import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { useZoraThemeRecipe } from '../../theme/useZoraThemeRecipe';
6
7
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
8
  import { resolveTextStyle } from './resolveTextRecipe';
9
+ import { resolveTextThemeRecipe } from './resolveTextThemeRecipe';
8
10
  import type { TextProps } from './types';
9
11
 
10
12
  const textLayoutStyle = {
@@ -29,18 +31,9 @@ function resolveTextContent({
29
31
  text: TextProps['text'];
30
32
  i18nKey: TextProps['i18nKey'];
31
33
  }): React.ReactNode {
32
- if (children !== undefined) {
33
- return children;
34
- }
35
-
36
- if (text !== undefined) {
37
- return text;
38
- }
39
-
40
- if (!i18nKey) {
41
- return null;
42
- }
43
-
34
+ if (children !== undefined) return children;
35
+ if (text !== undefined) return text;
36
+ if (i18nKey === undefined || i18nKey === '') return null;
44
37
  return i18nKey;
45
38
  }
46
39
 
@@ -50,12 +43,12 @@ function TextInner({
50
43
  children,
51
44
  text,
52
45
  i18nKey,
53
- variant = 'body',
46
+ variant,
54
47
  color,
55
- emphasis = 'default',
48
+ emphasis,
56
49
  align,
57
50
  weight,
58
- italic = false,
51
+ italic,
59
52
  numberOfLines,
60
53
  ellipsizeMode,
61
54
  selectable,
@@ -69,22 +62,21 @@ function TextInner({
69
62
  }: TextProps) {
70
63
  const { theme } = useZoraTheme();
71
64
  const { breakpoint } = useResponsiveRuntime();
65
+ const themeRecipe = resolveTextThemeRecipe(useZoraThemeRecipe('Text'));
72
66
  const content = resolveTextContent({ children, text, i18nKey });
73
- const resolvedVariant = resolveResponsive(variant, breakpoint) ?? 'body';
67
+ const resolvedVariant = resolveResponsive(variant ?? themeRecipe.variant, breakpoint) ?? 'body';
74
68
  const resolvedStyle = resolveTextStyle({
75
69
  theme,
76
70
  breakpoint,
77
71
  variant: resolvedVariant,
78
- color,
79
- emphasis,
80
- align,
81
- weight,
82
- italic,
72
+ color: color ?? themeRecipe.color,
73
+ emphasis: emphasis ?? themeRecipe.emphasis,
74
+ align: align ?? themeRecipe.align,
75
+ weight: weight ?? themeRecipe.weight,
76
+ italic: italic ?? themeRecipe.italic ?? false,
83
77
  });
84
78
 
85
- if (content === null || content === undefined) {
86
- return null;
87
- }
79
+ if (content === null || content === undefined) return null;
88
80
 
89
81
  return (
90
82
  <ReactNativeText
@@ -109,7 +101,6 @@ function TextInner({
109
101
  * `Text` owns normal body, caption, label, code, and supporting-copy variants so
110
102
  * consumers do not need to import lower-level Surface typography directly.
111
103
  *
112
-
113
104
  * @example Muted supporting copy
114
105
  * ```tsx
115
106
  * <Text variant="bodySmall" emphasis="muted">Updated just now</Text>
@@ -1,22 +1,19 @@
1
1
  import { ZORA_COLORS, ZORA_EMPHASES } from '../../internal/colorModel';
2
2
  import type { ZoraComponentMeta } from '../../metadata';
3
3
 
4
+ const themeAuthoring = {
5
+ authority: 'theme',
6
+ scope: 'component',
7
+ allowInstanceOverride: true,
8
+ } as const;
9
+
4
10
  export const textMeta = {
5
11
  name: 'Text',
6
12
  category: 'component',
7
13
  directManifestNode: true,
8
14
  allowedChildren: [],
9
- blueprint: {
10
- label: 'Text',
11
- defaultProps: {
12
- text: 'Text',
13
- emphasis: 'default',
14
- variant: 'body',
15
- },
16
- },
17
- i18n: {
18
- fields: [{ keyProp: 'i18nKey', defaultTextProp: 'text' }],
19
- },
15
+ blueprint: { label: 'Text', defaultProps: { text: 'Text' } },
16
+ i18n: { fields: [{ keyProp: 'i18nKey', defaultTextProp: 'text' }] },
20
17
  props: {
21
18
  text: {
22
19
  type: 'string',
@@ -36,45 +33,37 @@ export const textMeta = {
36
33
  category: 'Typography',
37
34
  label: 'Variant',
38
35
  enum: ['body', 'lead', 'bodySmall', 'caption', 'label', 'eyebrow', 'code'],
39
- default: 'body',
40
- authoring: { authority: 'theme', scope: 'component' },
36
+ authoring: themeAuthoring,
41
37
  },
42
38
  color: {
43
39
  type: 'enum',
44
40
  category: 'Style',
45
41
  label: 'Color',
46
42
  enum: ZORA_COLORS,
47
- authoring: { authority: 'theme', scope: 'component' },
43
+ authoring: themeAuthoring,
48
44
  },
49
45
  emphasis: {
50
46
  type: 'enum',
51
47
  category: 'Style',
52
48
  label: 'Emphasis',
53
49
  enum: ZORA_EMPHASES,
54
- default: 'default',
55
- authoring: { authority: 'theme', scope: 'component' },
50
+ authoring: themeAuthoring,
56
51
  },
57
52
  align: {
58
53
  type: 'enum',
59
54
  category: 'Layout',
60
55
  label: 'Align',
61
56
  enum: ['auto', 'left', 'right', 'center', 'justify'],
62
- authoring: { authority: 'theme', scope: 'component' },
57
+ authoring: themeAuthoring,
63
58
  },
64
59
  weight: {
65
60
  type: 'enum',
66
61
  category: 'Typography',
67
62
  label: 'Weight',
68
63
  enum: ['regular', 'medium', 'semiBold', 'bold'],
69
- authoring: { authority: 'theme', scope: 'component' },
70
- },
71
- italic: {
72
- type: 'boolean',
73
- category: 'Typography',
74
- label: 'Italic',
75
- default: false,
76
- authoring: { authority: 'theme', scope: 'component' },
64
+ authoring: themeAuthoring,
77
65
  },
66
+ italic: { type: 'boolean', category: 'Typography', label: 'Italic', authoring: themeAuthoring },
78
67
  numberOfLines: {
79
68
  type: 'number',
80
69
  category: 'Layout',
@@ -0,0 +1,48 @@
1
+ import type { ThemeRecipeOverrideValue } from '@ankhorage/contracts';
2
+
3
+ import type { TextAlign, TextColor, TextEmphasis, TextVariant, TextWeight } from './types';
4
+
5
+ export function resolveTextThemeRecipe(fields: Readonly<Record<string, ThemeRecipeOverrideValue>>) {
6
+ return {
7
+ variant: readVariant(fields.variant),
8
+ color: readColor(fields.color),
9
+ emphasis: readEmphasis(fields.emphasis),
10
+ align: readAlign(fields.align),
11
+ weight: readWeight(fields.weight),
12
+ italic: typeof fields.italic === 'boolean' ? fields.italic : undefined,
13
+ };
14
+ }
15
+
16
+ function readVariant(value: ThemeRecipeOverrideValue | undefined): TextVariant | undefined {
17
+ if (value === 'body' || value === 'lead' || value === 'bodySmall' || value === 'caption')
18
+ return value;
19
+ if (value === 'label' || value === 'eyebrow' || value === 'code') return value;
20
+ return undefined;
21
+ }
22
+
23
+ function readColor(value: ThemeRecipeOverrideValue | undefined): TextColor | undefined {
24
+ if (typeof value !== 'string') return undefined;
25
+ if (value === 'primary' || value === 'secondary' || value === 'tertiary') return value;
26
+ if (value === 'quaternary' || value === 'neutral' || value === 'danger') return value;
27
+ if (value === 'success' || value === 'warning' || value === 'error' || value === 'info')
28
+ return value;
29
+ return undefined;
30
+ }
31
+
32
+ function readEmphasis(value: ThemeRecipeOverrideValue | undefined): TextEmphasis | undefined {
33
+ if (value === 'default' || value === 'muted' || value === 'subtle' || value === 'inverse')
34
+ return value;
35
+ return undefined;
36
+ }
37
+
38
+ function readAlign(value: ThemeRecipeOverrideValue | undefined): TextAlign | undefined {
39
+ if (value === 'auto' || value === 'left' || value === 'right') return value;
40
+ if (value === 'center' || value === 'justify') return value;
41
+ return undefined;
42
+ }
43
+
44
+ function readWeight(value: ThemeRecipeOverrideValue | undefined): TextWeight | undefined {
45
+ if (value === 'regular' || value === 'medium' || value === 'semiBold' || value === 'bold')
46
+ return value;
47
+ return undefined;
48
+ }
@@ -1,33 +1,34 @@
1
+ import { ZORA_COLORS, ZORA_EMPHASES } from '../../internal/colorModel';
1
2
  import type { ZoraThemeRecipeMeta } from '../../metadata/themeRecipeTypes';
2
3
 
3
4
  export const textThemeRecipeMeta = {
4
5
  name: 'Text',
5
6
  kind: 'component',
6
- description: 'Maps shared typography and color tokens into Text presentation.',
7
+ description: 'Maps semantic Text presentation defaults into runtime props.',
7
8
  fields: {
8
9
  variant: {
9
- type: 'token',
10
- tokenFamily: 'typography',
10
+ type: 'choice',
11
11
  label: 'Typography variant',
12
+ options: ['body', 'lead', 'bodySmall', 'caption', 'label', 'eyebrow', 'code'],
12
13
  default: 'body',
13
14
  },
14
- color: {
15
- type: 'token',
16
- tokenFamily: 'colors',
17
- label: 'Color',
18
- default: 'text',
19
- },
15
+ color: { type: 'choice', label: 'Color', options: ZORA_COLORS },
20
16
  emphasis: {
21
17
  type: 'choice',
22
18
  label: 'Emphasis',
23
- options: ['default', 'muted', 'subtle', 'inverse'],
19
+ options: ZORA_EMPHASES,
24
20
  default: 'default',
25
21
  },
22
+ align: {
23
+ type: 'choice',
24
+ label: 'Align',
25
+ options: ['auto', 'left', 'right', 'center', 'justify'],
26
+ },
26
27
  weight: {
27
28
  type: 'choice',
28
29
  label: 'Weight',
29
30
  options: ['regular', 'medium', 'semiBold', 'bold'],
30
- default: 'regular',
31
31
  },
32
+ italic: { type: 'boolean', label: 'Italic', default: false },
32
33
  },
33
34
  } as const satisfies ZoraThemeRecipeMeta;
@@ -2,70 +2,44 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { ZORA_COMPONENT_META, type ZoraComponentPropAuthoring } from './index';
4
4
 
5
+ const componentMetaByName = new Map(Object.entries(ZORA_COMPONENT_META));
6
+
5
7
  describe('ZORA component prop authoring authority', () => {
6
- test('supports the canonical instance and theme scope combinations', () => {
8
+ test('supports instance and theme authority with explicit instance override capability', () => {
7
9
  const values: readonly ZoraComponentPropAuthoring[] = [
8
10
  { authority: 'instance' },
9
11
  { authority: 'theme', scope: 'global' },
10
- { authority: 'theme', scope: 'component' },
11
- { authority: 'theme', scope: 'pattern' },
12
+ { authority: 'theme', scope: 'component', allowInstanceOverride: true },
13
+ { authority: 'theme', scope: 'pattern', allowInstanceOverride: true },
12
14
  ];
13
-
14
- expect(values).toEqual([
15
- { authority: 'instance' },
16
- { authority: 'theme', scope: 'global' },
17
- { authority: 'theme', scope: 'component' },
18
- { authority: 'theme', scope: 'pattern' },
19
- ]);
15
+ expect(values).toHaveLength(4);
20
16
  });
21
17
 
22
- test('classifies representative instance content without exposing arbitrary styling', () => {
23
- expect(ZORA_COMPONENT_META.Button.props.children?.authoring).toEqual({
24
- authority: 'instance',
25
- });
18
+ test('keeps representative content and semantics instance-owned', () => {
19
+ expect(ZORA_COMPONENT_META.Button.props.children?.authoring).toEqual({ authority: 'instance' });
26
20
  expect(ZORA_COMPONENT_META.Card.props.title?.authoring).toEqual({ authority: 'instance' });
27
- expect(ZORA_COMPONENT_META.Heading.props.text?.authoring).toEqual({ authority: 'instance' });
28
21
  expect(ZORA_COMPONENT_META.Heading.props.level?.authoring).toEqual({ authority: 'instance' });
29
22
  expect(ZORA_COMPONENT_META.Text.props.text?.authoring).toEqual({ authority: 'instance' });
30
- expect(ZORA_COMPONENT_META.Panel.props.description?.authoring).toEqual({
31
- authority: 'instance',
32
- });
33
23
  });
34
24
 
35
- test('classifies component and pattern styling as theme authority', () => {
36
- expect(ZORA_COMPONENT_META.Button.props.size?.authoring).toEqual({
37
- authority: 'theme',
38
- scope: 'component',
39
- });
40
- expect(ZORA_COMPONENT_META.Card.props.tone?.authoring).toEqual({
41
- authority: 'theme',
42
- scope: 'component',
43
- });
44
- expect(ZORA_COMPONENT_META.Heading.props.size?.authoring).toEqual({
45
- authority: 'theme',
46
- scope: 'component',
47
- });
48
- expect(ZORA_COMPONENT_META.Text.props.variant?.authoring).toEqual({
49
- authority: 'theme',
50
- scope: 'component',
51
- });
52
- expect(ZORA_COMPONENT_META.Panel.props.compact?.authoring).toEqual({
53
- authority: 'theme',
54
- scope: 'pattern',
55
- });
25
+ test('marks recipe props theme-owned while allowing explicit instance overrides', () => {
26
+ for (const name of ['Button', 'Card', 'Heading', 'Text', 'Panel']) {
27
+ const meta = componentMetaByName.get(name);
28
+ expect(meta, name).toBeDefined();
29
+ for (const prop of Object.values(meta?.props ?? {})) {
30
+ if (prop.authoring?.authority !== 'theme') continue;
31
+ expect(prop.authoring.allowInstanceOverride, name).toBe(true);
32
+ }
33
+ }
56
34
  });
57
35
 
58
- test('keeps Heading blueprint content useful before instance editing', () => {
36
+ test('keeps Heading blueprint semantic content useful', () => {
59
37
  expect(ZORA_COMPONENT_META.Heading.blueprint?.defaultProps).toEqual({
60
38
  text: 'Heading',
61
39
  level: 2,
62
40
  });
63
41
  });
64
42
 
65
- test('treats absent authoring metadata as not authorable', () => {
66
- expect(ZORA_COMPONENT_META.Progress.props.value?.authoring).toBeUndefined();
67
- });
68
-
69
43
  test('does not introduce a system authority', () => {
70
44
  expect(JSON.stringify(ZORA_COMPONENT_META)).not.toContain('"authority":"system"');
71
45
  });
@@ -2,6 +2,7 @@ import type { UiComponentMetaRegistry } from '@ankhorage/contracts';
2
2
  import { describe, expect, it } from 'bun:test';
3
3
 
4
4
  import { ZORA_BINDABLE_COMPONENT_META } from './bindableComponentMeta';
5
+ import { ZORA_COMPONENT_META } from './componentMeta';
5
6
 
6
7
  describe('ZORA bindable component metadata', () => {
7
8
  it('matches the shared UI metadata registry shape', () => {
@@ -15,4 +16,16 @@ describe('ZORA bindable component metadata', () => {
15
16
  expect(registry.Image?.bindings?.props?.source?.value.type).toBe('imageAsset');
16
17
  expect(registry.DataTable?.bindings?.props?.rows?.value.type).toBe('array');
17
18
  });
19
+
20
+ it('shares canonical Image authoring metadata while preserving dynamic image bindings', () => {
21
+ const image = ZORA_BINDABLE_COMPONENT_META.Image;
22
+
23
+ expect(image.props.source).toMatchObject({
24
+ type: 'media',
25
+ mediaKinds: ['image'],
26
+ });
27
+ expect(image.props).toEqual(ZORA_COMPONENT_META.Image.props);
28
+ expect(image.directManifestNode).toBe(ZORA_COMPONENT_META.Image.directManifestNode);
29
+ expect(image.bindings.props.source.value.type).toBe('imageAsset');
30
+ });
18
31
  });
@@ -1,5 +1,7 @@
1
1
  import type { UiComponentMetaRegistry } from '@ankhorage/contracts';
2
2
 
3
+ import { imageMeta } from '../components/image/meta';
4
+
3
5
  export const ZORA_BINDABLE_COMPONENT_META = {
4
6
  Text: {
5
7
  name: 'Text',
@@ -107,16 +109,12 @@ export const ZORA_BINDABLE_COMPONENT_META = {
107
109
  },
108
110
  },
109
111
  Image: {
110
- name: 'Image',
111
- category: 'component',
112
- description: 'Displays an image asset or remote image source.',
113
- directManifestNode: true,
114
- allowedChildren: [],
112
+ ...imageMeta,
115
113
  bindings: {
116
114
  props: {
117
115
  source: {
118
116
  label: 'Source',
119
- description: 'Image source or asset descriptor.',
117
+ description: 'Dynamic image source or asset descriptor.',
120
118
  value: {
121
119
  type: 'imageAsset',
122
120
  fields: [
@@ -131,13 +129,6 @@ export const ZORA_BINDABLE_COMPONENT_META = {
131
129
  },
132
130
  },
133
131
  },
134
- props: {
135
- source: {
136
- type: 'imageAsset',
137
- category: 'Content',
138
- label: 'Source',
139
- },
140
- },
141
132
  },
142
133
  Input: {
143
134
  name: 'Input',
@@ -124,6 +124,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
124
124
  'Textarea',
125
125
  'Text',
126
126
  'Heading',
127
+ 'Image',
127
128
  'Divider',
128
129
  'ChatListItem',
129
130
  'CameraPermissionView',
@@ -172,6 +173,19 @@ describe('ZORA_COMPONENT_META invariants', () => {
172
173
  }
173
174
  });
174
175
 
176
+ test('Image is a direct manifest leaf with canonical media authoring metadata', () => {
177
+ expect(ZORA_COMPONENT_META.Image.directManifestNode).toBe(true);
178
+ expect(ZORA_COMPONENT_META.Image.allowedChildren).toEqual([]);
179
+ expect(ZORA_COMPONENT_META.Image.props.source).toEqual({
180
+ type: 'media',
181
+ category: 'Content',
182
+ label: 'Source',
183
+ mediaKinds: ['image'],
184
+ authoring: { authority: 'instance' },
185
+ });
186
+ expect(ZORA_COMPONENT_META.Image.props.alt?.authoring).toEqual({ authority: 'instance' });
187
+ });
188
+
175
189
  test('ThemeModeToggle is a direct manifest leaf with serializable defaults', () => {
176
190
  expect(ZORA_COMPONENT_META.ThemeModeToggle.directManifestNode).toBe(true);
177
191
  expect(ZORA_COMPONENT_META.ThemeModeToggle.allowedChildren).toEqual([]);