@ankhorage/zora 2.10.0 → 2.12.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 (180) 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 +23 -4
  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/text/Text.d.ts +0 -1
  51. package/dist/components/text/Text.d.ts.map +1 -1
  52. package/dist/components/text/Text.js +14 -16
  53. package/dist/components/text/Text.js.map +1 -1
  54. package/dist/components/text/meta.d.ts +6 -5
  55. package/dist/components/text/meta.d.ts.map +1 -1
  56. package/dist/components/text/meta.js +13 -25
  57. package/dist/components/text/meta.js.map +1 -1
  58. package/dist/components/text/resolveTextThemeRecipe.d.ts +11 -0
  59. package/dist/components/text/resolveTextThemeRecipe.d.ts.map +1 -0
  60. package/dist/components/text/resolveTextThemeRecipe.js +46 -0
  61. package/dist/components/text/resolveTextThemeRecipe.js.map +1 -0
  62. package/dist/components/text/themeRecipeMeta.d.ts +15 -7
  63. package/dist/components/text/themeRecipeMeta.d.ts.map +1 -1
  64. package/dist/components/text/themeRecipeMeta.js +12 -11
  65. package/dist/components/text/themeRecipeMeta.js.map +1 -1
  66. package/dist/foundation/meta.d.ts +4 -4
  67. package/dist/layout/screen/meta.d.ts +1 -1
  68. package/dist/layout/screen-section/meta.d.ts +1 -1
  69. package/dist/metadata/allowedChildren.d.ts +3 -3
  70. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  71. package/dist/metadata/allowedChildren.js +1 -0
  72. package/dist/metadata/allowedChildren.js.map +1 -1
  73. package/dist/metadata/componentMeta.d.ts.map +1 -1
  74. package/dist/metadata/componentMeta.js +2 -0
  75. package/dist/metadata/componentMeta.js.map +1 -1
  76. package/dist/metadata/themeRecipeMeta.d.ts.map +1 -1
  77. package/dist/metadata/themeRecipeMeta.js +2 -0
  78. package/dist/metadata/themeRecipeMeta.js.map +1 -1
  79. package/dist/metadata/themeRecipeTypes.d.ts +3 -3
  80. package/dist/metadata/themeRecipeTypes.d.ts.map +1 -1
  81. package/dist/metadata/themeRecipeTypes.js.map +1 -1
  82. package/dist/metadata/types.d.ts +1 -0
  83. package/dist/metadata/types.d.ts.map +1 -1
  84. package/dist/metadata/types.js.map +1 -1
  85. package/dist/patterns/message-bubble/meta.d.ts +1 -1
  86. package/dist/patterns/notice/meta.d.ts +1 -1
  87. package/dist/patterns/panel/Panel.d.ts +0 -2
  88. package/dist/patterns/panel/Panel.d.ts.map +1 -1
  89. package/dist/patterns/panel/Panel.js +6 -4
  90. package/dist/patterns/panel/Panel.js.map +1 -1
  91. package/dist/patterns/panel/meta.d.ts +23 -4
  92. package/dist/patterns/panel/meta.d.ts.map +1 -1
  93. package/dist/patterns/panel/meta.js +14 -15
  94. package/dist/patterns/panel/meta.js.map +1 -1
  95. package/dist/patterns/panel/themeRecipeMeta.d.ts +1 -1
  96. package/dist/patterns/panel/themeRecipeMeta.js +1 -1
  97. package/dist/patterns/panel/themeRecipeMeta.js.map +1 -1
  98. package/dist/patterns/panel/types.d.ts +2 -0
  99. package/dist/patterns/panel/types.d.ts.map +1 -1
  100. package/dist/patterns/panel/types.js.map +1 -1
  101. package/dist/patterns/post-card/meta.d.ts +1 -1
  102. package/dist/patterns/scanner/meta.d.ts +2 -2
  103. package/dist/registry.d.ts.map +1 -1
  104. package/dist/registry.js +2 -0
  105. package/dist/registry.js.map +1 -1
  106. package/dist/theme/ThemeModeToggle.d.ts +1 -1
  107. package/dist/theme/ThemeModeToggle.d.ts.map +1 -1
  108. package/dist/theme/ThemeModeToggle.js +2 -2
  109. package/dist/theme/ThemeModeToggle.js.map +1 -1
  110. package/dist/theme/ThemeModeToggle.meta.d.ts +28 -0
  111. package/dist/theme/ThemeModeToggle.meta.d.ts.map +1 -0
  112. package/dist/theme/ThemeModeToggle.meta.js +28 -0
  113. package/dist/theme/ThemeModeToggle.meta.js.map +1 -0
  114. package/dist/theme/ThemeModeToggleProps.d.ts +1 -1
  115. package/dist/theme/ThemeModeToggleProps.d.ts.map +1 -1
  116. package/dist/theme/ThemeModeToggleProps.js.map +1 -1
  117. package/dist/theme/ZoraProvider.d.ts +4 -12
  118. package/dist/theme/ZoraProvider.d.ts.map +1 -1
  119. package/dist/theme/ZoraProvider.js +5 -14
  120. package/dist/theme/ZoraProvider.js.map +1 -1
  121. package/dist/theme/ZoraThemeRuntimeContext.d.ts +1 -2
  122. package/dist/theme/ZoraThemeRuntimeContext.d.ts.map +1 -1
  123. package/dist/theme/ZoraThemeRuntimeContext.js +0 -1
  124. package/dist/theme/ZoraThemeRuntimeContext.js.map +1 -1
  125. package/dist/theme/ZoraThemeScope.d.ts.map +1 -1
  126. package/dist/theme/ZoraThemeScope.js +4 -10
  127. package/dist/theme/ZoraThemeScope.js.map +1 -1
  128. package/dist/theme/resolveZoraThemeRecipe.d.ts +16 -0
  129. package/dist/theme/resolveZoraThemeRecipe.d.ts.map +1 -0
  130. package/dist/theme/resolveZoraThemeRecipe.js +56 -0
  131. package/dist/theme/resolveZoraThemeRecipe.js.map +1 -0
  132. package/dist/theme/useZoraThemeRecipe.d.ts +2 -0
  133. package/dist/theme/useZoraThemeRecipe.d.ts.map +1 -0
  134. package/dist/theme/useZoraThemeRecipe.js +7 -0
  135. package/dist/theme/useZoraThemeRecipe.js.map +1 -0
  136. package/package.json +4 -4
  137. package/src/components/button/Button.tsx +4 -2
  138. package/src/components/button/meta.ts +10 -15
  139. package/src/components/button/resolveButtonThemeRecipe.test.ts +25 -0
  140. package/src/components/button/resolveButtonThemeRecipe.ts +41 -0
  141. package/src/components/button/themeRecipeMeta.ts +5 -10
  142. package/src/components/card/Card.tsx +13 -7
  143. package/src/components/card/meta.ts +15 -15
  144. package/src/components/card/resolveCardThemeRecipe.test.ts +28 -0
  145. package/src/components/card/resolveCardThemeRecipe.ts +33 -0
  146. package/src/components/card/themeRecipeMeta.ts +1 -1
  147. package/src/components/card/types.ts +2 -0
  148. package/src/components/contentFallback.test.ts +4 -4
  149. package/src/components/heading/Heading.tsx +18 -28
  150. package/src/components/heading/meta.ts +14 -22
  151. package/src/components/heading/resolveHeadingThemeRecipe.ts +55 -0
  152. package/src/components/heading/themeRecipeMeta.ts +33 -0
  153. package/src/components/text/Text.tsx +16 -25
  154. package/src/components/text/meta.ts +14 -25
  155. package/src/components/text/resolveTextThemeRecipe.ts +48 -0
  156. package/src/components/text/themeRecipeMeta.ts +12 -11
  157. package/src/metadata/allowedChildren.ts +1 -0
  158. package/src/metadata/authoringMeta.test.ts +18 -44
  159. package/src/metadata/componentMeta.test.ts +8 -0
  160. package/src/metadata/componentMeta.ts +2 -0
  161. package/src/metadata/themeRecipeMeta.test.ts +41 -39
  162. package/src/metadata/themeRecipeMeta.ts +2 -0
  163. package/src/metadata/themeRecipeTypes.ts +3 -3
  164. package/src/metadata/types.ts +3 -9
  165. package/src/patterns/panel/Panel.tsx +14 -4
  166. package/src/patterns/panel/meta.ts +15 -15
  167. package/src/patterns/panel/themeRecipeMeta.ts +1 -1
  168. package/src/patterns/panel/types.ts +2 -0
  169. package/src/registry.test.ts +5 -3
  170. package/src/registry.ts +2 -0
  171. package/src/theme/ThemeModeToggle.meta.ts +29 -0
  172. package/src/theme/ThemeModeToggle.tsx +7 -1
  173. package/src/theme/ThemeModeToggleProps.ts +4 -1
  174. package/src/theme/ZoraProvider.tsx +10 -13
  175. package/src/theme/ZoraThemeRuntimeContext.tsx +1 -3
  176. package/src/theme/ZoraThemeScope.tsx +5 -25
  177. package/src/theme/resolveZoraThemeRecipe.test.ts +72 -0
  178. package/src/theme/resolveZoraThemeRecipe.ts +91 -0
  179. package/src/theme/themeConfigScope.test.ts +25 -0
  180. package/src/theme/useZoraThemeRecipe.ts +7 -0
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveCardThemeRecipe } from './resolveCardThemeRecipe';
4
+
5
+ describe('resolveCardThemeRecipe', () => {
6
+ test('preserves compact-aware padding when theme padding is unset', () => {
7
+ expect(
8
+ resolveCardThemeRecipe({ themeFields: { tone: 'default', compact: true, radius: 'l' } }),
9
+ ).toEqual({
10
+ tone: 'default',
11
+ compact: true,
12
+ padding: 'm',
13
+ radius: 'l',
14
+ });
15
+ });
16
+
17
+ test('lets explicit instance props win over theme values', () => {
18
+ expect(
19
+ resolveCardThemeRecipe({
20
+ tone: 'outline',
21
+ compact: false,
22
+ padding: 'hero',
23
+ radius: 'pill',
24
+ themeFields: { tone: 'subtle', compact: true, padding: 'm', radius: 'l' },
25
+ }),
26
+ ).toEqual({ tone: 'outline', compact: false, padding: 'hero', radius: 'pill' });
27
+ });
28
+ });
@@ -0,0 +1,33 @@
1
+ import type { ThemeRecipeOverrideValue } from '@ankhorage/contracts';
2
+
3
+ import type { ZoraCardTone } from '../../internal/recipes';
4
+
5
+ export function resolveCardThemeRecipe(input: {
6
+ readonly tone?: ZoraCardTone;
7
+ readonly compact?: boolean;
8
+ readonly padding?: string;
9
+ readonly radius?: string;
10
+ readonly themeFields: Readonly<Record<string, ThemeRecipeOverrideValue>>;
11
+ }) {
12
+ const tone = input.tone ?? readTone(input.themeFields.tone) ?? 'default';
13
+ const compact = input.compact ?? readBoolean(input.themeFields.compact) ?? false;
14
+ return {
15
+ tone,
16
+ compact,
17
+ padding: input.padding ?? readString(input.themeFields.padding) ?? (compact ? 'm' : 'l'),
18
+ radius: input.radius ?? readString(input.themeFields.radius) ?? 'l',
19
+ };
20
+ }
21
+
22
+ function readTone(value: ThemeRecipeOverrideValue | undefined): ZoraCardTone | undefined {
23
+ if (value === 'default' || value === 'subtle' || value === 'outline') return value;
24
+ return undefined;
25
+ }
26
+
27
+ function readBoolean(value: ThemeRecipeOverrideValue | undefined): boolean | undefined {
28
+ return typeof value === 'boolean' ? value : undefined;
29
+ }
30
+
31
+ function readString(value: ThemeRecipeOverrideValue | undefined): string | undefined {
32
+ return typeof value === 'string' ? value : undefined;
33
+ }
@@ -15,7 +15,7 @@ export const cardThemeRecipeMeta = {
15
15
  type: 'token',
16
16
  tokenFamily: 'spacing',
17
17
  label: 'Padding',
18
- default: 'l',
18
+ description: 'When unset, Card keeps its compact-aware spacing default.',
19
19
  },
20
20
  radius: {
21
21
  type: 'token',
@@ -14,4 +14,6 @@ export interface CardProps
14
14
  footer?: React.ReactNode;
15
15
  tone?: ZoraCardTone;
16
16
  compact?: boolean;
17
+ padding?: string;
18
+ radius?: string;
17
19
  }
@@ -9,15 +9,15 @@ const headingSource = readFileSync(join(import.meta.dir, 'heading', 'Heading.tsx
9
9
  describe('content fallback ownership', () => {
10
10
  it('keeps Text free of translation context coupling', () => {
11
11
  expect(textSource).not.toMatch(/useTranslationContext/);
12
- expect(textSource).toMatch(/if \(children !== undefined\) \{/);
13
- expect(textSource).toMatch(/if \(text !== undefined\) \{/);
12
+ expect(textSource).toMatch(/if \(children !== undefined\) return children;/);
13
+ expect(textSource).toMatch(/if \(text !== undefined\) return text;/);
14
14
  expect(textSource).toMatch(/return i18nKey;/);
15
15
  });
16
16
 
17
17
  it('keeps Heading free of translation context coupling', () => {
18
18
  expect(headingSource).not.toMatch(/useTranslationContext/);
19
- expect(headingSource).toMatch(/if \(children !== undefined\) \{/);
20
- expect(headingSource).toMatch(/if \(text !== undefined\) \{/);
19
+ expect(headingSource).toMatch(/if \(children !== undefined\) return children;/);
20
+ expect(headingSource).toMatch(/if \(text !== undefined\) return text;/);
21
21
  expect(headingSource).toMatch(/return i18nKey;/);
22
22
  });
23
23
  });
@@ -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 { resolveHeadingRecipe, resolveHeadingSizeFromLevel } from './resolveHeadingRecipe';
9
+ import { resolveHeadingThemeRecipe } from './resolveHeadingThemeRecipe';
8
10
  import type { HeadingProps } from './types';
9
11
 
10
12
  const headingLayoutStyle = {
@@ -24,23 +26,10 @@ function resolveHeadingContent({
24
26
  children,
25
27
  text,
26
28
  i18nKey,
27
- }: {
28
- children: HeadingProps['children'];
29
- text: HeadingProps['text'];
30
- i18nKey: HeadingProps['i18nKey'];
31
- }): 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
-
29
+ }: Pick<HeadingProps, 'children' | 'text' | 'i18nKey'>) {
30
+ if (children !== undefined) return children;
31
+ if (text !== undefined) return text;
32
+ if (i18nKey === undefined || i18nKey === '') return null;
44
33
  return i18nKey;
45
34
  }
46
35
 
@@ -53,10 +42,10 @@ function HeadingInner({
53
42
  level = 2,
54
43
  size,
55
44
  color,
56
- emphasis = 'default',
45
+ emphasis,
57
46
  align,
58
47
  weight,
59
- italic = false,
48
+ italic,
60
49
  numberOfLines,
61
50
  ellipsizeMode,
62
51
  selectable,
@@ -70,16 +59,17 @@ function HeadingInner({
70
59
  }: HeadingProps) {
71
60
  const { theme } = useZoraTheme();
72
61
  const { breakpoint } = useResponsiveRuntime();
62
+ const themeRecipe = resolveHeadingThemeRecipe(useZoraThemeRecipe('Heading'));
73
63
  const content = resolveHeadingContent({ children, text, i18nKey });
74
- const resolvedSize = resolveResponsive(size, breakpoint) ?? resolveHeadingSizeFromLevel(level);
75
- const resolvedColor = resolveResponsive(color, breakpoint);
76
- const resolvedEmphasis = resolveResponsive(emphasis, breakpoint) ?? 'default';
77
- const resolvedAlign = resolveResponsive(align, breakpoint);
78
- const resolvedWeight = resolveResponsive(weight, breakpoint);
64
+ const resolvedSize =
65
+ resolveResponsive(size ?? themeRecipe.size, breakpoint) ?? resolveHeadingSizeFromLevel(level);
66
+ const resolvedColor = resolveResponsive(color ?? themeRecipe.color, breakpoint);
67
+ const resolvedEmphasis =
68
+ resolveResponsive(emphasis ?? themeRecipe.emphasis, breakpoint) ?? 'default';
69
+ const resolvedAlign = resolveResponsive(align ?? themeRecipe.align, breakpoint);
70
+ const resolvedWeight = resolveResponsive(weight ?? themeRecipe.weight, breakpoint);
79
71
 
80
- if (content === null || content === undefined) {
81
- return null;
82
- }
72
+ if (content === null) return null;
83
73
 
84
74
  return (
85
75
  <ReactNativeText
@@ -95,7 +85,7 @@ function HeadingInner({
95
85
  headingLayoutStyle,
96
86
  resolveHeadingRecipe(theme, {
97
87
  align: resolvedAlign,
98
- italic,
88
+ italic: italic ?? themeRecipe.italic ?? false,
99
89
  level,
100
90
  size: resolvedSize,
101
91
  color: resolvedColor,
@@ -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,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;
@@ -8,6 +8,7 @@ export const CONTAINER_ALLOWED_CHILDREN = [
8
8
  'Heading',
9
9
  'Button',
10
10
  'ButtonGroup',
11
+ 'ThemeModeToggle',
11
12
  'Input',
12
13
  'Textarea',
13
14
  'FormField',