@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
@@ -0,0 +1,56 @@
1
+ import { ZORA_THEME_RECIPE_META } from '../metadata/themeRecipeMeta';
2
+ const recipeMetaByName = new Map(Object.entries(ZORA_THEME_RECIPE_META));
3
+ export function resolveZoraThemeRecipe(theme, recipeName) {
4
+ const meta = recipeMetaByName.get(recipeName);
5
+ if (!meta)
6
+ throw new RangeError(`Unknown ZORA theme recipe: ${recipeName}.`);
7
+ const recipes = meta.kind === 'component' ? theme.config.recipes?.components : theme.config.recipes?.patterns;
8
+ const overrides = new Map(Object.entries(recipes ?? {})).get(recipeName);
9
+ const overrideByField = new Map(Object.entries(overrides ?? {}));
10
+ const resolved = [];
11
+ for (const [fieldName, fieldMeta] of Object.entries(meta.fields)) {
12
+ const override = overrideByField.get(fieldName);
13
+ if (override !== undefined) {
14
+ validateFieldValue(theme, recipeName, fieldName, fieldMeta, override);
15
+ resolved.push([fieldName, override]);
16
+ }
17
+ else if (fieldMeta.default !== undefined) {
18
+ resolved.push([fieldName, fieldMeta.default]);
19
+ }
20
+ }
21
+ return Object.fromEntries(resolved);
22
+ }
23
+ function validateFieldValue(theme, recipeName, fieldName, meta, value) {
24
+ if (meta.type === 'boolean') {
25
+ if (typeof value !== 'boolean') {
26
+ throw new TypeError(`Theme recipe ${recipeName}.${fieldName} must be boolean.`);
27
+ }
28
+ return;
29
+ }
30
+ if (typeof value !== 'string') {
31
+ throw new TypeError(`Theme recipe ${recipeName}.${fieldName} must be a string.`);
32
+ }
33
+ if (meta.type === 'choice' && !meta.options.includes(value)) {
34
+ throw new RangeError(`Invalid theme recipe choice for ${recipeName}.${fieldName}: ${value}.`);
35
+ }
36
+ if (meta.type === 'token' && !hasThemeToken(theme, meta.tokenFamily, value)) {
37
+ throw new RangeError(`Unknown ${meta.tokenFamily} token for ${recipeName}.${fieldName}: ${value}.`);
38
+ }
39
+ }
40
+ function hasThemeToken(theme, family, token) {
41
+ if (family === 'colors')
42
+ return hasOwn(theme.colors, token);
43
+ if (family === 'spacing')
44
+ return hasOwn(theme.spacing, token);
45
+ if (family === 'radii')
46
+ return hasOwn(theme.radii, token);
47
+ if (family === 'shadows')
48
+ return hasOwn(theme.shadows, token);
49
+ return (hasOwn(theme.typography.sizes, token) ||
50
+ hasOwn(theme.typography.weights, token) ||
51
+ hasOwn(theme.typography.headings, token));
52
+ }
53
+ function hasOwn(value, key) {
54
+ return Object.prototype.hasOwnProperty.call(value, key);
55
+ }
56
+ //# sourceMappingURL=resolveZoraThemeRecipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveZoraThemeRecipe.js","sourceRoot":"","sources":["../../src/theme/resolveZoraThemeRecipe.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThemeConfig, ThemeRecipeOverrideValue } from '@ankhorage/contracts';\n\nimport { ZORA_THEME_RECIPE_META } from '../metadata/themeRecipeMeta';\nimport type { ZoraThemeRecipeFieldMeta, ZoraThemeTokenFamily } from '../metadata/themeRecipeTypes';\n\ninterface ZoraThemeRecipeRuntimeTheme {\n readonly config: ThemeConfig;\n readonly colors: object;\n readonly spacing: object;\n readonly radii: object;\n readonly shadows: object;\n readonly typography: {\n readonly sizes: object;\n readonly weights: object;\n readonly headings: object;\n };\n}\n\nconst recipeMetaByName = new Map(Object.entries(ZORA_THEME_RECIPE_META));\n\nexport function resolveZoraThemeRecipe(\n theme: ZoraThemeRecipeRuntimeTheme,\n recipeName: string,\n): Readonly<Record<string, ThemeRecipeOverrideValue>> {\n const meta = recipeMetaByName.get(recipeName);\n if (!meta) throw new RangeError(`Unknown ZORA theme recipe: ${recipeName}.`);\n\n const recipes =\n meta.kind === 'component' ? theme.config.recipes?.components : theme.config.recipes?.patterns;\n const overrides = new Map(Object.entries(recipes ?? {})).get(recipeName);\n const overrideByField = new Map(Object.entries(overrides ?? {}));\n const resolved: [string, ThemeRecipeOverrideValue][] = [];\n\n for (const [fieldName, fieldMeta] of Object.entries(meta.fields)) {\n const override = overrideByField.get(fieldName);\n if (override !== undefined) {\n validateFieldValue(theme, recipeName, fieldName, fieldMeta, override);\n resolved.push([fieldName, override]);\n } else if (fieldMeta.default !== undefined) {\n resolved.push([fieldName, fieldMeta.default]);\n }\n }\n\n return Object.fromEntries(resolved);\n}\n\nfunction validateFieldValue(\n theme: ZoraThemeRecipeRuntimeTheme,\n recipeName: string,\n fieldName: string,\n meta: ZoraThemeRecipeFieldMeta,\n value: ThemeRecipeOverrideValue,\n): void {\n if (meta.type === 'boolean') {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Theme recipe ${recipeName}.${fieldName} must be boolean.`);\n }\n return;\n }\n if (typeof value !== 'string') {\n throw new TypeError(`Theme recipe ${recipeName}.${fieldName} must be a string.`);\n }\n if (meta.type === 'choice' && !meta.options.includes(value)) {\n throw new RangeError(`Invalid theme recipe choice for ${recipeName}.${fieldName}: ${value}.`);\n }\n if (meta.type === 'token' && !hasThemeToken(theme, meta.tokenFamily, value)) {\n throw new RangeError(\n `Unknown ${meta.tokenFamily} token for ${recipeName}.${fieldName}: ${value}.`,\n );\n }\n}\n\nfunction hasThemeToken(\n theme: ZoraThemeRecipeRuntimeTheme,\n family: ZoraThemeTokenFamily,\n token: string,\n): boolean {\n if (family === 'colors') return hasOwn(theme.colors, token);\n if (family === 'spacing') return hasOwn(theme.spacing, token);\n if (family === 'radii') return hasOwn(theme.radii, token);\n if (family === 'shadows') return hasOwn(theme.shadows, token);\n return (\n hasOwn(theme.typography.sizes, token) ||\n hasOwn(theme.typography.weights, token) ||\n hasOwn(theme.typography.headings, token)\n );\n}\n\nfunction hasOwn(value: object, key: PropertyKey): boolean {\n return Object.prototype.hasOwnProperty.call(value, key);\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  export declare function useZoraTheme(): {
2
2
  theme: import("@ankhorage/surface").SurfaceTheme;
3
3
  mode: "light" | "dark";
4
- setThemeConfig: (config: Partial<import("@ankhorage/contracts").ThemeConfig>) => void;
4
+ setThemeConfig: (config: Partial<import("@ankhorage/surface").ThemeConfig>) => void;
5
5
  setMode: (mode: "light" | "dark") => void;
6
6
  _hasProvider?: boolean;
7
7
  };
@@ -0,0 +1,2 @@
1
+ export declare function useZoraThemeRecipe(recipeName: string): Readonly<Record<string, import("@ankhorage/contracts").ThemeRecipeOverrideValue>>;
2
+ //# sourceMappingURL=useZoraThemeRecipe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useZoraThemeRecipe.d.ts","sourceRoot":"","sources":["../../src/theme/useZoraThemeRecipe.ts"],"names":[],"mappings":"AAGA,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,qFAGpD"}
@@ -0,0 +1,7 @@
1
+ import { resolveZoraThemeRecipe } from './resolveZoraThemeRecipe';
2
+ import { useZoraTheme } from './useZoraTheme';
3
+ export function useZoraThemeRecipe(recipeName) {
4
+ const { theme } = useZoraTheme();
5
+ return resolveZoraThemeRecipe(theme, recipeName);
6
+ }
7
+ //# sourceMappingURL=useZoraThemeRecipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useZoraThemeRecipe.js","sourceRoot":"","sources":["../../src/theme/useZoraThemeRecipe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,UAAU,kBAAkB,CAAC,UAAkB;IACnD,MAAM,EAAE,KAAK,EAAE,GAAG,YAAY,EAAE,CAAC;IACjC,OAAO,sBAAsB,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AACnD,CAAC","sourcesContent":["import { resolveZoraThemeRecipe } from './resolveZoraThemeRecipe';\nimport { useZoraTheme } from './useZoraTheme';\n\nexport function useZoraThemeRecipe(recipeName: string) {\n const { theme } = useZoraTheme();\n return resolveZoraThemeRecipe(theme, recipeName);\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora",
3
3
  "type": "module",
4
- "version": "2.11.0",
4
+ "version": "2.13.0",
5
5
  "description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
6
6
  "homepage": "https://github.com/ankhorage/zora#readme",
7
7
  "bugs": {
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@ankhorage/color-theory": "^0.0.8",
45
- "@ankhorage/contracts": "^4.0.1",
46
- "@ankhorage/surface": "^2.1.1"
45
+ "@ankhorage/contracts": "^7.6.0",
46
+ "@ankhorage/surface": "^2.2.0"
47
47
  },
48
48
  "files": [
49
49
  "dist",
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "devDependencies": {
92
92
  "@ankhorage/devtools": "^1.3.4",
93
- "@ankhorage/paradox": "^0.1.10",
93
+ "@ankhorage/paradox": "^0.1.21",
94
94
  "@changesets/cli": "^2.31.0",
95
95
  "@expo/vector-icons": "^15.1.1",
96
96
  "@react-native-picker/picker": "^2.11.4",
@@ -1,8 +1,9 @@
1
1
  import { Button as SurfaceButton } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
- import { resolveButtonRecipe } from '../../internal/recipes';
4
+ import { useZoraThemeRecipe } from '../../theme/useZoraThemeRecipe';
5
5
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
6
+ import { resolveButtonThemeRecipe } from './resolveButtonThemeRecipe';
6
7
  import type { ButtonProps } from './types';
7
8
 
8
9
  function ButtonInner({
@@ -14,7 +15,8 @@ function ButtonInner({
14
15
  interactionPolicy,
15
16
  ...props
16
17
  }: ButtonProps) {
17
- const recipe = resolveButtonRecipe({ color, variant, size });
18
+ const themeFields = useZoraThemeRecipe('Button');
19
+ const recipe = resolveButtonThemeRecipe({ color, variant, size, themeFields });
18
20
 
19
21
  return (
20
22
  <SurfaceButton
@@ -1,20 +1,18 @@
1
1
  import { ZORA_COLORS } 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 buttonMeta = {
5
11
  name: 'Button',
6
12
  category: 'component',
7
13
  directManifestNode: true,
8
14
  allowedChildren: [],
9
- blueprint: {
10
- label: 'Button',
11
- defaultProps: {
12
- children: 'Continue',
13
- color: 'primary',
14
- variant: 'solid',
15
- size: 'm',
16
- },
17
- },
15
+ blueprint: { label: 'Button', defaultProps: { children: 'Continue' } },
18
16
  events: {
19
17
  press: {
20
18
  label: 'Press',
@@ -36,24 +34,21 @@ export const buttonMeta = {
36
34
  category: 'Style',
37
35
  label: 'Color',
38
36
  enum: [...ZORA_COLORS],
39
- default: 'primary',
40
- authoring: { authority: 'theme', scope: 'component' },
37
+ authoring: themeAuthoring,
41
38
  },
42
39
  variant: {
43
40
  type: 'enum',
44
41
  category: 'Style',
45
42
  label: 'Variant',
46
43
  enum: ['solid', 'outline', 'ghost', 'soft'],
47
- default: 'solid',
48
- authoring: { authority: 'theme', scope: 'component' },
44
+ authoring: themeAuthoring,
49
45
  },
50
46
  size: {
51
47
  type: 'enum',
52
48
  category: 'Style',
53
49
  label: 'Size',
54
50
  enum: ['s', 'm', 'l'],
55
- default: 'm',
56
- authoring: { authority: 'theme', scope: 'component' },
51
+ authoring: themeAuthoring,
57
52
  },
58
53
  },
59
54
  } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveButtonThemeRecipe } from './resolveButtonThemeRecipe';
4
+
5
+ describe('resolveButtonThemeRecipe', () => {
6
+ test('uses theme defaults without changing the existing hard default', () => {
7
+ expect(
8
+ resolveButtonThemeRecipe({ themeFields: { color: 'primary', variant: 'solid', size: 'l' } }),
9
+ ).toEqual({
10
+ color: 'primary',
11
+ variant: 'solid',
12
+ size: 'l',
13
+ });
14
+ });
15
+
16
+ test('lets explicit instance props override theme recipe values', () => {
17
+ expect(
18
+ resolveButtonThemeRecipe({
19
+ color: 'danger',
20
+ size: 's',
21
+ themeFields: { color: 'primary', variant: 'soft', size: 'l' },
22
+ }),
23
+ ).toEqual({ color: 'danger', variant: 'soft', size: 's' });
24
+ });
25
+ });
@@ -0,0 +1,41 @@
1
+ import type { ThemeRecipeOverrideValue } from '@ankhorage/contracts';
2
+
3
+ import {
4
+ resolveButtonRecipe,
5
+ type ZoraButtonVariant,
6
+ type ZoraColor,
7
+ type ZoraControlSize,
8
+ } from '../../internal/recipes';
9
+
10
+ export function resolveButtonThemeRecipe(input: {
11
+ readonly color?: ZoraColor;
12
+ readonly variant?: ZoraButtonVariant;
13
+ readonly size?: ZoraControlSize;
14
+ readonly themeFields: Readonly<Record<string, ThemeRecipeOverrideValue>>;
15
+ }) {
16
+ return resolveButtonRecipe({
17
+ color: input.color ?? readColor(input.themeFields.color),
18
+ variant: input.variant ?? readVariant(input.themeFields.variant),
19
+ size: input.size ?? readSize(input.themeFields.size),
20
+ });
21
+ }
22
+
23
+ function readColor(value: ThemeRecipeOverrideValue | undefined): ZoraColor | 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 readVariant(value: ThemeRecipeOverrideValue | undefined): ZoraButtonVariant | undefined {
33
+ if (value === 'solid' || value === 'outline' || value === 'ghost' || value === 'soft')
34
+ return value;
35
+ return undefined;
36
+ }
37
+
38
+ function readSize(value: ThemeRecipeOverrideValue | undefined): ZoraControlSize | undefined {
39
+ if (value === 's' || value === 'm' || value === 'l') return value;
40
+ return undefined;
41
+ }
@@ -1,14 +1,15 @@
1
+ import { ZORA_COLORS } from '../../internal/colorModel';
1
2
  import type { ZoraThemeRecipeMeta } from '../../metadata/themeRecipeTypes';
2
3
 
3
4
  export const buttonThemeRecipeMeta = {
4
5
  name: 'Button',
5
6
  kind: 'component',
6
- description: 'Maps shared theme tokens and semantic choices into Button presentation.',
7
+ description: 'Maps shared semantic choices into Button presentation defaults.',
7
8
  fields: {
8
9
  color: {
9
- type: 'token',
10
- tokenFamily: 'colors',
10
+ type: 'choice',
11
11
  label: 'Color',
12
+ options: ZORA_COLORS,
12
13
  default: 'primary',
13
14
  },
14
15
  variant: {
@@ -22,13 +23,7 @@ export const buttonThemeRecipeMeta = {
22
23
  label: 'Size',
23
24
  description: 'A Button-specific semantic scale, not a universal pixel size.',
24
25
  options: ['s', 'm', 'l'],
25
- default: 'm',
26
- },
27
- radius: {
28
- type: 'token',
29
- tokenFamily: 'radii',
30
- label: 'Corner radius',
31
- default: 'm',
26
+ default: 'l',
32
27
  },
33
28
  },
34
29
  } as const satisfies ZoraThemeRecipeMeta;
@@ -3,9 +3,11 @@ import React from 'react';
3
3
 
4
4
  import { Box, Stack } from '../../foundation';
5
5
  import { resolveCardVariant } from '../../internal/recipes';
6
+ import { useZoraThemeRecipe } from '../../theme/useZoraThemeRecipe';
6
7
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
8
  import { Heading } from '../heading';
8
9
  import { Text } from '../text';
10
+ import { resolveCardThemeRecipe } from './resolveCardThemeRecipe';
9
11
  import type { CardProps } from './types';
10
12
 
11
13
  function CardInner({
@@ -17,15 +19,19 @@ function CardInner({
17
19
  eyebrow,
18
20
  actions,
19
21
  footer,
20
- tone = 'default',
21
- compact = false,
22
+ tone,
23
+ compact,
24
+ padding,
25
+ radius,
22
26
  onPress,
23
27
  interactionPolicy,
24
28
  ...props
25
29
  }: CardProps) {
30
+ const themeFields = useZoraThemeRecipe('Card');
31
+ const recipe = resolveCardThemeRecipe({ tone, compact, padding, radius, themeFields });
26
32
  const hasHeader = [eyebrow, title, description, actions].some((item) => item != null);
27
33
  const hasFooter = footer !== undefined;
28
- const gap = compact ? 's' : 'm';
34
+ const gap = recipe.compact ? 's' : 'm';
29
35
  const isInteractive = Boolean(onPress) && !actions;
30
36
  const passive = interactionPolicy === 'passive';
31
37
 
@@ -33,9 +39,9 @@ function CardInner({
33
39
  <SurfaceCard
34
40
  {...props}
35
41
  onPress={isInteractive && !passive ? onPress : undefined}
36
- p={compact ? 'm' : 'l'}
37
- radius="l"
38
- variant={resolveCardVariant(tone)}
42
+ p={recipe.padding}
43
+ radius={recipe.radius}
44
+ variant={resolveCardVariant(recipe.tone)}
39
45
  >
40
46
  <Stack gap={gap}>
41
47
  {hasHeader ? (
@@ -52,7 +58,7 @@ function CardInner({
52
58
  {eyebrow}
53
59
  </Text>
54
60
  ) : null}
55
- {title ? <Heading level={compact ? 4 : 3}>{title}</Heading> : null}
61
+ {title ? <Heading level={recipe.compact ? 4 : 3}>{title}</Heading> : null}
56
62
  {description ? (
57
63
  <Text emphasis="muted" variant="bodySmall">
58
64
  {description}
@@ -1,18 +1,18 @@
1
1
  import type { ZoraComponentMeta } from '../../metadata';
2
2
  import { CONTAINER_ALLOWED_CHILDREN } from '../../metadata/allowedChildren';
3
3
 
4
+ const themeAuthoring = {
5
+ authority: 'theme',
6
+ scope: 'component',
7
+ allowInstanceOverride: true,
8
+ } as const;
9
+
4
10
  export const cardMeta = {
5
11
  name: 'Card',
6
12
  category: 'component',
7
13
  directManifestNode: true,
8
14
  allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
9
- blueprint: {
10
- label: 'Card',
11
- defaultProps: {
12
- title: 'Card',
13
- tone: 'default',
14
- },
15
- },
15
+ blueprint: { label: 'Card', defaultProps: { title: 'Card' } },
16
16
  props: {
17
17
  title: {
18
18
  type: 'string',
@@ -37,15 +37,15 @@ export const cardMeta = {
37
37
  category: 'Style',
38
38
  label: 'Tone',
39
39
  enum: ['default', 'subtle', 'outline'],
40
- default: 'default',
41
- authoring: { authority: 'theme', scope: 'component' },
40
+ authoring: themeAuthoring,
42
41
  },
43
- compact: {
44
- type: 'boolean',
45
- category: 'Layout',
46
- label: 'Compact',
47
- default: false,
48
- authoring: { authority: 'theme', scope: 'component' },
42
+ compact: { type: 'boolean', category: 'Layout', label: 'Compact', authoring: themeAuthoring },
43
+ padding: { type: 'spacing', category: 'Layout', label: 'Padding', authoring: themeAuthoring },
44
+ radius: {
45
+ type: 'radius',
46
+ category: 'Style',
47
+ label: 'Corner radius',
48
+ authoring: themeAuthoring,
49
49
  },
50
50
  },
51
51
  } as const satisfies ZoraComponentMeta;
@@ -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,