@ankhorage/zora 0.8.1 → 0.10.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 (131) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +91 -59
  3. package/dist/components/button/Button.d.ts +1 -1
  4. package/dist/components/button/Button.d.ts.map +1 -1
  5. package/dist/components/button/Button.js +3 -1
  6. package/dist/components/button/Button.js.map +1 -1
  7. package/dist/components/button/types.d.ts +2 -1
  8. package/dist/components/button/types.d.ts.map +1 -1
  9. package/dist/components/button/types.js.map +1 -1
  10. package/dist/components/card/Card.d.ts +1 -1
  11. package/dist/components/card/Card.d.ts.map +1 -1
  12. package/dist/components/card/Card.js +3 -1
  13. package/dist/components/card/Card.js.map +1 -1
  14. package/dist/components/card/types.d.ts +2 -1
  15. package/dist/components/card/types.d.ts.map +1 -1
  16. package/dist/components/card/types.js.map +1 -1
  17. package/dist/components/heading/Heading.d.ts +1 -1
  18. package/dist/components/heading/Heading.d.ts.map +1 -1
  19. package/dist/components/heading/Heading.js +3 -1
  20. package/dist/components/heading/Heading.js.map +1 -1
  21. package/dist/components/heading/types.d.ts +2 -2
  22. package/dist/components/heading/types.d.ts.map +1 -1
  23. package/dist/components/heading/types.js.map +1 -1
  24. package/dist/components/icon/Icon.d.ts +4 -2
  25. package/dist/components/icon/Icon.d.ts.map +1 -1
  26. package/dist/components/icon/Icon.js +3 -1
  27. package/dist/components/icon/Icon.js.map +1 -1
  28. package/dist/components/icon-button/IconButton.d.ts +1 -1
  29. package/dist/components/icon-button/IconButton.d.ts.map +1 -1
  30. package/dist/components/icon-button/IconButton.js +3 -1
  31. package/dist/components/icon-button/IconButton.js.map +1 -1
  32. package/dist/components/icon-button/types.d.ts +2 -1
  33. package/dist/components/icon-button/types.d.ts.map +1 -1
  34. package/dist/components/icon-button/types.js.map +1 -1
  35. package/dist/components/text/Text.d.ts +1 -1
  36. package/dist/components/text/Text.d.ts.map +1 -1
  37. package/dist/components/text/Text.js +3 -1
  38. package/dist/components/text/Text.js.map +1 -1
  39. package/dist/components/text/types.d.ts +2 -2
  40. package/dist/components/text/types.d.ts.map +1 -1
  41. package/dist/components/text/types.js.map +1 -1
  42. package/dist/patterns/panel/Panel.d.ts +1 -1
  43. package/dist/patterns/panel/Panel.d.ts.map +1 -1
  44. package/dist/patterns/panel/Panel.js +3 -1
  45. package/dist/patterns/panel/Panel.js.map +1 -1
  46. package/dist/patterns/panel/types.d.ts +2 -2
  47. package/dist/patterns/panel/types.d.ts.map +1 -1
  48. package/dist/patterns/panel/types.js.map +1 -1
  49. package/dist/theme/ZoraBaseProps.d.ts +18 -0
  50. package/dist/theme/ZoraBaseProps.d.ts.map +1 -0
  51. package/dist/theme/ZoraBaseProps.js +2 -0
  52. package/dist/theme/ZoraBaseProps.js.map +1 -0
  53. package/dist/theme/ZoraProvider.d.ts +4 -4
  54. package/dist/theme/ZoraProvider.d.ts.map +1 -1
  55. package/dist/theme/ZoraProvider.js +11 -6
  56. package/dist/theme/ZoraProvider.js.map +1 -1
  57. package/dist/theme/ZoraThemeRuntimeContext.d.ts +9 -0
  58. package/dist/theme/ZoraThemeRuntimeContext.d.ts.map +1 -0
  59. package/dist/theme/ZoraThemeRuntimeContext.js +10 -0
  60. package/dist/theme/ZoraThemeRuntimeContext.js.map +1 -0
  61. package/dist/theme/ZoraThemeScope.d.ts +9 -0
  62. package/dist/theme/ZoraThemeScope.d.ts.map +1 -0
  63. package/dist/theme/ZoraThemeScope.js +41 -0
  64. package/dist/theme/ZoraThemeScope.js.map +1 -0
  65. package/dist/theme/createZoraThemeConfig.d.ts +4 -0
  66. package/dist/theme/createZoraThemeConfig.d.ts.map +1 -0
  67. package/dist/theme/createZoraThemeConfig.js +23 -0
  68. package/dist/theme/createZoraThemeConfig.js.map +1 -0
  69. package/dist/theme/index.d.ts +7 -3
  70. package/dist/theme/index.d.ts.map +1 -1
  71. package/dist/theme/index.js +4 -2
  72. package/dist/theme/index.js.map +1 -1
  73. package/dist/theme/resolveZoraScopedThemeId.d.ts +6 -0
  74. package/dist/theme/resolveZoraScopedThemeId.d.ts.map +1 -0
  75. package/dist/theme/resolveZoraScopedThemeId.js +15 -0
  76. package/dist/theme/resolveZoraScopedThemeId.js.map +1 -0
  77. package/dist/theme/types.d.ts +21 -0
  78. package/dist/theme/types.d.ts.map +1 -0
  79. package/dist/theme/types.js +2 -0
  80. package/dist/theme/types.js.map +1 -0
  81. package/dist/theme/withZoraThemeScope.d.ts +4 -0
  82. package/dist/theme/withZoraThemeScope.d.ts.map +1 -0
  83. package/dist/theme/withZoraThemeScope.js +16 -0
  84. package/dist/theme/withZoraThemeScope.js.map +1 -0
  85. package/dist/theme/zoraDefaultTheme.d.ts +3 -0
  86. package/dist/theme/zoraDefaultTheme.d.ts.map +1 -0
  87. package/dist/theme/zoraDefaultTheme.js +8 -0
  88. package/dist/theme/zoraDefaultTheme.js.map +1 -0
  89. package/package.json +1 -1
  90. package/src/components/button/Button.tsx +11 -1
  91. package/src/components/button/types.ts +3 -4
  92. package/src/components/card/Card.tsx +6 -1
  93. package/src/components/card/types.ts +3 -1
  94. package/src/components/heading/Heading.tsx +6 -1
  95. package/src/components/heading/types.ts +3 -2
  96. package/src/components/icon/Icon.tsx +7 -2
  97. package/src/components/icon-button/IconButton.tsx +6 -1
  98. package/src/components/icon-button/types.ts +2 -1
  99. package/src/components/text/Text.tsx +6 -1
  100. package/src/components/text/types.ts +3 -2
  101. package/src/patterns/panel/Panel.tsx +4 -1
  102. package/src/patterns/panel/types.ts +2 -2
  103. package/src/theme/ZoraBaseProps.ts +20 -0
  104. package/src/theme/ZoraProvider.tsx +15 -8
  105. package/src/theme/ZoraThemeRuntimeContext.tsx +18 -0
  106. package/src/theme/ZoraThemeScope.tsx +74 -0
  107. package/src/theme/createZoraThemeConfig.test.ts +36 -0
  108. package/src/theme/createZoraThemeConfig.ts +27 -0
  109. package/src/theme/index.ts +15 -3
  110. package/src/theme/resolveZoraScopedThemeId.test.ts +47 -0
  111. package/src/theme/resolveZoraScopedThemeId.ts +25 -0
  112. package/src/theme/themeScopeStructure.test.ts +99 -0
  113. package/src/theme/types.ts +33 -0
  114. package/src/theme/withZoraThemeScope.tsx +25 -0
  115. package/src/theme/zoraDefaultTheme.ts +9 -0
  116. package/dist/internal/deepMerge.d.ts +0 -2
  117. package/dist/internal/deepMerge.d.ts.map +0 -1
  118. package/dist/internal/deepMerge.js +0 -20
  119. package/dist/internal/deepMerge.js.map +0 -1
  120. package/dist/theme/createZoraTheme.d.ts +0 -4
  121. package/dist/theme/createZoraTheme.d.ts.map +0 -1
  122. package/dist/theme/createZoraTheme.js +0 -6
  123. package/dist/theme/createZoraTheme.js.map +0 -1
  124. package/dist/theme/zoraTheme.d.ts +0 -3
  125. package/dist/theme/zoraTheme.d.ts.map +0 -1
  126. package/dist/theme/zoraTheme.js +0 -15
  127. package/dist/theme/zoraTheme.js.map +0 -1
  128. package/src/internal/deepMerge.ts +0 -23
  129. package/src/theme/createZoraTheme.test.ts +0 -25
  130. package/src/theme/createZoraTheme.ts +0 -10
  131. package/src/theme/zoraTheme.ts +0 -16
@@ -2,11 +2,10 @@ import type { ButtonIconSpec, ButtonProps as SurfaceButtonProps } from '@ankhora
2
2
  import type React from 'react';
3
3
 
4
4
  import type { ZoraControlSize, ZoraEmphasis, ZoraTone } from '../../internal/recipes';
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
5
6
 
6
- export interface ButtonProps extends Omit<
7
- SurfaceButtonProps,
8
- 'children' | 'size' | 'tone' | 'variant'
9
- > {
7
+ export interface ButtonProps
8
+ extends ZoraBaseProps, Omit<SurfaceButtonProps, 'children' | 'size' | 'tone' | 'variant'> {
10
9
  children?: React.ReactNode;
11
10
  tone?: ZoraTone;
12
11
  emphasis?: ZoraEmphasis;
@@ -2,11 +2,14 @@ import { Box, Card as SurfaceCard, Stack } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
4
  import { resolveCardVariant } from '../../internal/recipes';
5
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
6
  import { Heading } from '../heading';
6
7
  import { Text } from '../text';
7
8
  import type { CardProps } from './types';
8
9
 
9
- export function Card({
10
+ function CardInner({
11
+ themeId: _themeId,
12
+ mode: _mode,
10
13
  children,
11
14
  title,
12
15
  description,
@@ -65,3 +68,5 @@ export function Card({
65
68
  </SurfaceCard>
66
69
  );
67
70
  }
71
+
72
+ export const Card = withZoraThemeScope(CardInner);
@@ -2,8 +2,10 @@ import type { CardProps as SurfaceCardProps } from '@ankhorage/surface';
2
2
  import type React from 'react';
3
3
 
4
4
  import type { ZoraCardTone } from '../../internal/recipes';
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
5
6
 
6
- export interface CardProps extends Omit<SurfaceCardProps, 'children' | 'p' | 'radius' | 'variant'> {
7
+ export interface CardProps
8
+ extends ZoraBaseProps, Omit<SurfaceCardProps, 'children' | 'p' | 'radius' | 'variant'> {
7
9
  children?: React.ReactNode;
8
10
  title?: React.ReactNode;
9
11
  description?: React.ReactNode;
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { Text as ReactNativeText } from 'react-native';
4
4
 
5
5
  import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
6
7
  import { resolveHeadingRecipe, resolveHeadingSizeFromLevel } from './resolveHeadingRecipe';
7
8
  import type { HeadingProps } from './types';
8
9
 
@@ -30,7 +31,9 @@ function resolveHeadingContent({
30
31
  return i18nKey;
31
32
  }
32
33
 
33
- export function Heading({
34
+ function HeadingInner({
35
+ themeId: _themeId,
36
+ mode: _mode,
34
37
  children,
35
38
  text,
36
39
  i18nKey,
@@ -84,3 +87,5 @@ export function Heading({
84
87
  </ReactNativeText>
85
88
  );
86
89
  }
90
+
91
+ export const Heading = withZoraThemeScope(HeadingInner);
@@ -2,6 +2,8 @@ import type { Responsive } from '@ankhorage/surface';
2
2
  import type React from 'react';
3
3
  import type { AccessibilityRole, TextStyle } from 'react-native';
4
4
 
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
6
+
5
7
  export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
6
8
 
7
9
  export type HeadingSize = 'display' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
@@ -20,7 +22,7 @@ export type HeadingAlign = NonNullable<TextStyle['textAlign']>;
20
22
 
21
23
  export type HeadingWeight = 'regular' | 'medium' | 'semiBold' | 'bold';
22
24
 
23
- export interface HeadingProps {
25
+ export interface HeadingProps extends ZoraBaseProps {
24
26
  children?: React.ReactNode;
25
27
  text?: string;
26
28
  i18nKey?: string;
@@ -37,5 +39,4 @@ export interface HeadingProps {
37
39
  accessibilityHint?: string;
38
40
  accessibilityRole?: AccessibilityRole;
39
41
  nativeID?: string;
40
- testID?: string;
41
42
  }
@@ -1,8 +1,13 @@
1
1
  import { Icon as SurfaceIcon, type IconProps as SurfaceIconProps } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
- export type IconProps = SurfaceIconProps;
4
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
5
6
 
6
- export function Icon(props: IconProps) {
7
+ export interface IconProps extends ZoraBaseProps, SurfaceIconProps {}
8
+
9
+ function IconInner({ themeId: _themeId, mode: _mode, ...props }: IconProps) {
7
10
  return <SurfaceIcon {...props} />;
8
11
  }
12
+
13
+ export const Icon = withZoraThemeScope(IconInner);
@@ -2,9 +2,12 @@ import { IconButton as SurfaceIconButton } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
4
  import { resolveButtonRecipe } from '../../internal/recipes';
5
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
6
  import type { IconButtonProps } from './types';
6
7
 
7
- export function IconButton({
8
+ function IconButtonInner({
9
+ themeId: _themeId,
10
+ mode: _mode,
8
11
  icon,
9
12
  label,
10
13
  emphasis = 'ghost',
@@ -25,3 +28,5 @@ export function IconButton({
25
28
  />
26
29
  );
27
30
  }
31
+
32
+ export const IconButton = withZoraThemeScope(IconButtonInner);
@@ -1,8 +1,9 @@
1
1
  import type { ButtonIconSpec } from '@ankhorage/surface';
2
2
 
3
3
  import type { ZoraControlSize, ZoraEmphasis, ZoraTone } from '../../internal/recipes';
4
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
4
5
 
5
- export interface IconButtonProps {
6
+ export interface IconButtonProps extends ZoraBaseProps {
6
7
  icon: ButtonIconSpec;
7
8
  label: string;
8
9
  emphasis?: ZoraEmphasis;
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { Text as ReactNativeText } from 'react-native';
4
4
 
5
5
  import { useZoraTheme } from '../../theme/useZoraTheme';
6
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
6
7
  import { resolveTextStyle } from './resolveTextRecipe';
7
8
  import type { TextProps } from './types';
8
9
 
@@ -30,7 +31,9 @@ function resolveTextContent({
30
31
  return i18nKey;
31
32
  }
32
33
 
33
- export function Text({
34
+ function TextInner({
35
+ themeId: _themeId,
36
+ mode: _mode,
34
37
  children,
35
38
  text,
36
39
  i18nKey,
@@ -82,3 +85,5 @@ export function Text({
82
85
  </ReactNativeText>
83
86
  );
84
87
  }
88
+
89
+ export const Text = withZoraThemeScope(TextInner);
@@ -2,6 +2,8 @@ import type { Responsive } from '@ankhorage/surface';
2
2
  import type React from 'react';
3
3
  import type { AccessibilityRole, TextStyle } from 'react-native';
4
4
 
5
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
6
+
5
7
  export type TextVariant = 'body' | 'lead' | 'bodySmall' | 'caption' | 'label' | 'eyebrow' | 'code';
6
8
 
7
9
  export type TextTone =
@@ -18,7 +20,7 @@ export type TextWeight = 'regular' | 'medium' | 'semiBold' | 'bold';
18
20
 
19
21
  export type TextAlign = NonNullable<TextStyle['textAlign']>;
20
22
 
21
- export interface TextProps {
23
+ export interface TextProps extends ZoraBaseProps {
22
24
  children?: React.ReactNode;
23
25
  text?: string;
24
26
  i18nKey?: string;
@@ -34,5 +36,4 @@ export interface TextProps {
34
36
  accessibilityHint?: string;
35
37
  accessibilityRole?: AccessibilityRole;
36
38
  nativeID?: string;
37
- testID?: string;
38
39
  }
@@ -1,8 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { Card } from '../../components/card';
4
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
4
5
  import type { PanelProps } from './types';
5
6
 
6
- export function Panel(props: PanelProps) {
7
+ function PanelInner({ themeId: _themeId, mode: _mode, ...props }: PanelProps) {
7
8
  return <Card {...props} />;
8
9
  }
10
+
11
+ export const Panel = withZoraThemeScope(PanelInner);
@@ -1,8 +1,9 @@
1
1
  import type React from 'react';
2
2
 
3
3
  import type { ZoraCardTone } from '../../internal/recipes';
4
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
4
5
 
5
- export interface PanelProps {
6
+ export interface PanelProps extends ZoraBaseProps {
6
7
  title?: React.ReactNode;
7
8
  description?: React.ReactNode;
8
9
  eyebrow?: React.ReactNode;
@@ -11,5 +12,4 @@ export interface PanelProps {
11
12
  children?: React.ReactNode;
12
13
  tone?: ZoraCardTone;
13
14
  compact?: boolean;
14
- testID?: string;
15
15
  }
@@ -0,0 +1,20 @@
1
+ import type { ZoraThemeId, ZoraThemeMode } from './types';
2
+
3
+ export interface ZoraBaseProps {
4
+ /**
5
+ * Overrides the active ZORA theme for this component and its subtree.
6
+ * If omitted, the nearest parent theme is inherited.
7
+ *
8
+ * Plan 2: theme registries are not available yet. Only the inherited theme id
9
+ * is valid; unknown ids throw in dev/test and warn+fallback in production.
10
+ */
11
+ themeId?: ZoraThemeId;
12
+
13
+ /**
14
+ * Overrides the light/dark mode for this component and its subtree.
15
+ * If omitted, the nearest parent mode is inherited.
16
+ */
17
+ mode?: ZoraThemeMode;
18
+
19
+ testID?: string;
20
+ }
@@ -1,22 +1,29 @@
1
- import { ResponsiveProvider, ThemeProvider } from '@ankhorage/surface';
1
+ import { ThemeProvider } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
- import { createZoraTheme, type ZoraThemeOverride } from './createZoraTheme';
4
+ import { createZoraThemeConfig } from './createZoraThemeConfig';
5
+ import type { ZoraTheme, ZoraThemeMode } from './types';
6
+ import { zoraDefaultTheme } from './zoraDefaultTheme';
7
+ import { ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';
5
8
 
6
9
  export interface ZoraProviderProps {
7
10
  children: React.ReactNode;
8
- initialConfig?: ZoraThemeOverride;
9
- initialMode?: 'light' | 'dark';
11
+ theme?: ZoraTheme;
12
+ initialMode?: ZoraThemeMode;
10
13
  }
11
14
 
12
15
  export function ZoraProvider({
13
16
  children,
14
- initialConfig,
17
+ theme = zoraDefaultTheme,
15
18
  initialMode = 'light',
16
19
  }: ZoraProviderProps) {
20
+ const runtimeValue = React.useMemo(() => ({ sourceTheme: theme, themeId: theme.id }), [theme]);
21
+
17
22
  return (
18
- <ThemeProvider initialConfig={createZoraTheme(initialConfig)} initialMode={initialMode}>
19
- <ResponsiveProvider>{children}</ResponsiveProvider>
20
- </ThemeProvider>
23
+ <ZoraThemeRuntimeContext.Provider value={runtimeValue}>
24
+ <ThemeProvider initialConfig={createZoraThemeConfig(theme)} initialMode={initialMode}>
25
+ {children}
26
+ </ThemeProvider>
27
+ </ZoraThemeRuntimeContext.Provider>
21
28
  );
22
29
  }
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ import type { ZoraTheme, ZoraThemeId } from './types';
4
+ import { zoraDefaultTheme } from './zoraDefaultTheme';
5
+
6
+ interface ZoraThemeRuntime {
7
+ sourceTheme: ZoraTheme;
8
+ themeId: ZoraThemeId;
9
+ }
10
+
11
+ export const ZoraThemeRuntimeContext = createContext<ZoraThemeRuntime>({
12
+ sourceTheme: zoraDefaultTheme,
13
+ themeId: zoraDefaultTheme.id,
14
+ });
15
+
16
+ export function useZoraThemeRuntime(): ZoraThemeRuntime {
17
+ return useContext(ZoraThemeRuntimeContext);
18
+ }
@@ -0,0 +1,74 @@
1
+ import { createTheme, ThemeContext, useFontContext, useTheme } from '@ankhorage/surface';
2
+ import React, { useMemo } from 'react';
3
+
4
+ import { createZoraThemeConfig } from './createZoraThemeConfig';
5
+ import { resolveZoraScopedThemeId } from './resolveZoraScopedThemeId';
6
+ import type { ZoraThemeId, ZoraThemeMode } from './types';
7
+ import { useZoraThemeRuntime, ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';
8
+
9
+ export interface ZoraThemeScopeProps {
10
+ children: React.ReactNode;
11
+ themeId?: ZoraThemeId;
12
+ mode?: ZoraThemeMode;
13
+ }
14
+
15
+ function ZoraThemeScopeInner({ children, themeId, mode }: ZoraThemeScopeProps) {
16
+ const parentSurface = useTheme();
17
+ const parentRuntime = useZoraThemeRuntime();
18
+ const { activeFontId } = useFontContext();
19
+
20
+ const scopedThemeId = resolveZoraScopedThemeId({
21
+ desiredThemeId: themeId,
22
+ inheritedThemeId: parentRuntime.themeId,
23
+ });
24
+
25
+ const scopedMode = mode ?? parentSurface.mode;
26
+
27
+ // Plan 2: there is no multi-theme registry yet. Keep the active theme seed inherited.
28
+ const surfaceConfig = useMemo(
29
+ () => createZoraThemeConfig(parentRuntime.sourceTheme),
30
+ [parentRuntime.sourceTheme],
31
+ );
32
+
33
+ const scopedTheme = useMemo(
34
+ () => createTheme(surfaceConfig, scopedMode, activeFontId),
35
+ [surfaceConfig, scopedMode, activeFontId],
36
+ );
37
+
38
+ const scopedSurfaceValue = useMemo(
39
+ () => ({
40
+ theme: scopedTheme,
41
+ mode: scopedMode,
42
+ setMode: parentSurface.setMode,
43
+ setThemeConfig: parentSurface.setThemeConfig,
44
+ _hasProvider: true,
45
+ }),
46
+ [parentSurface.setMode, parentSurface.setThemeConfig, scopedMode, scopedTheme],
47
+ );
48
+
49
+ const scopedRuntimeValue = useMemo(
50
+ () => ({
51
+ sourceTheme: parentRuntime.sourceTheme,
52
+ themeId: scopedThemeId,
53
+ }),
54
+ [parentRuntime.sourceTheme, scopedThemeId],
55
+ );
56
+
57
+ return (
58
+ <ZoraThemeRuntimeContext.Provider value={scopedRuntimeValue}>
59
+ <ThemeContext.Provider value={scopedSurfaceValue}>{children}</ThemeContext.Provider>
60
+ </ZoraThemeRuntimeContext.Provider>
61
+ );
62
+ }
63
+
64
+ export function ZoraThemeScope({ children, themeId, mode }: ZoraThemeScopeProps) {
65
+ if (mode === undefined && themeId === undefined) {
66
+ return children;
67
+ }
68
+
69
+ return (
70
+ <ZoraThemeScopeInner mode={mode} themeId={themeId}>
71
+ {children}
72
+ </ZoraThemeScopeInner>
73
+ );
74
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { createZoraThemeConfig } from './createZoraThemeConfig';
4
+
5
+ describe('createZoraThemeConfig', () => {
6
+ test('converts the default theme seed into a surface config', () => {
7
+ const themeConfig = createZoraThemeConfig();
8
+
9
+ expect(themeConfig.id).toBe('zora');
10
+ expect(themeConfig.name).toBe('ZORA');
11
+ expect(themeConfig.light.primaryColor).toBe('#0f766e');
12
+ expect(themeConfig.light.harmony).toBe('analogous');
13
+ expect(themeConfig.light.systemTone).toBe('jewel');
14
+ expect(themeConfig.dark.primaryColor).toBe('#0f766e');
15
+ expect(themeConfig.dark.harmony).toBe('analogous');
16
+ expect(themeConfig.dark.systemTone).toBe('jewel');
17
+ });
18
+
19
+ test('falls back to id when name is omitted', () => {
20
+ const themeConfig = createZoraThemeConfig({
21
+ id: 'studio',
22
+ primaryColor: '#0f766e',
23
+ harmony: 'analogous',
24
+ tone: 'jewel',
25
+ });
26
+
27
+ expect(themeConfig.id).toBe('studio');
28
+ expect(themeConfig.name).toBe('studio');
29
+ expect(themeConfig.light.primaryColor).toBe('#0f766e');
30
+ expect(themeConfig.light.harmony).toBe('analogous');
31
+ expect(themeConfig.light.systemTone).toBe('jewel');
32
+ expect(themeConfig.dark.primaryColor).toBe('#0f766e');
33
+ expect(themeConfig.dark.harmony).toBe('analogous');
34
+ expect(themeConfig.dark.systemTone).toBe('jewel');
35
+ });
36
+ });
@@ -0,0 +1,27 @@
1
+ import type { ThemeConfig } from '@ankhorage/surface';
2
+
3
+ import type { ZoraHexColor, ZoraTheme, ZoraThemeMode } from './types';
4
+ import { zoraDefaultTheme } from './zoraDefaultTheme';
5
+
6
+ function resolveModePrimaryColor(primaryColor: ZoraHexColor, _mode: ZoraThemeMode): ZoraHexColor {
7
+ // Intentionally conservative in Plan 1: mode-specific primary derivation
8
+ // (OKLCH/lightness scale, etc.) comes in later theme-engine work.
9
+ return primaryColor;
10
+ }
11
+
12
+ export function createZoraThemeConfig(theme: ZoraTheme = zoraDefaultTheme): ThemeConfig {
13
+ return {
14
+ id: theme.id,
15
+ name: theme.name ?? theme.id,
16
+ light: {
17
+ primaryColor: resolveModePrimaryColor(theme.primaryColor, 'light'),
18
+ harmony: theme.harmony,
19
+ systemTone: theme.tone,
20
+ },
21
+ dark: {
22
+ primaryColor: resolveModePrimaryColor(theme.primaryColor, 'dark'),
23
+ harmony: theme.harmony,
24
+ systemTone: theme.tone,
25
+ },
26
+ };
27
+ }
@@ -1,6 +1,18 @@
1
- export type { ZoraThemeOverride } from './createZoraTheme';
2
- export { createZoraTheme } from './createZoraTheme';
1
+ export { createZoraThemeConfig } from './createZoraThemeConfig';
2
+ export type {
3
+ ZoraColorHarmony,
4
+ ZoraColorTone,
5
+ ZoraComputedTheme,
6
+ ZoraHexColor,
7
+ ZoraTheme,
8
+ ZoraThemeId,
9
+ ZoraThemeMode,
10
+ } from './types';
3
11
  export { useZoraTheme } from './useZoraTheme';
12
+ export { withZoraThemeScope } from './withZoraThemeScope';
13
+ export type { ZoraBaseProps } from './ZoraBaseProps';
14
+ export { zoraDefaultTheme } from './zoraDefaultTheme';
4
15
  export type { ZoraProviderProps } from './ZoraProvider';
5
16
  export { ZoraProvider } from './ZoraProvider';
6
- export { zoraTheme } from './zoraTheme';
17
+ export type { ZoraThemeScopeProps } from './ZoraThemeScope';
18
+ export { ZoraThemeScope } from './ZoraThemeScope';
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveZoraScopedThemeId } from './resolveZoraScopedThemeId';
4
+
5
+ describe('resolveZoraScopedThemeId', () => {
6
+ it('inherits the theme id when omitted', () => {
7
+ expect(resolveZoraScopedThemeId({ desiredThemeId: undefined, inheritedThemeId: 'zora' })).toBe(
8
+ 'zora',
9
+ );
10
+ });
11
+
12
+ it('accepts the inherited theme id when explicitly provided', () => {
13
+ expect(resolveZoraScopedThemeId({ desiredThemeId: 'zora', inheritedThemeId: 'zora' })).toBe(
14
+ 'zora',
15
+ );
16
+ });
17
+
18
+ it('throws in non-production for unknown theme ids', () => {
19
+ const originalEnv = process.env.NODE_ENV;
20
+ process.env.NODE_ENV = 'test';
21
+
22
+ expect(() =>
23
+ resolveZoraScopedThemeId({ desiredThemeId: 'studio', inheritedThemeId: 'zora' }),
24
+ ).toThrow(/Unknown ZORA theme id 'studio'/);
25
+
26
+ process.env.NODE_ENV = originalEnv;
27
+ });
28
+
29
+ it('warns and falls back in production for unknown theme ids', () => {
30
+ const originalEnv = process.env.NODE_ENV;
31
+ process.env.NODE_ENV = 'production';
32
+
33
+ const originalWarn = console.warn;
34
+ const warnings: string[] = [];
35
+ console.warn = (message: string) => {
36
+ warnings.push(message);
37
+ };
38
+
39
+ expect(resolveZoraScopedThemeId({ desiredThemeId: 'studio', inheritedThemeId: 'zora' })).toBe(
40
+ 'zora',
41
+ );
42
+ expect(warnings.join('\n')).toMatch(/Unknown ZORA theme id 'studio'/);
43
+
44
+ console.warn = originalWarn;
45
+ process.env.NODE_ENV = originalEnv;
46
+ });
47
+ });
@@ -0,0 +1,25 @@
1
+ import type { ZoraThemeId } from './types';
2
+
3
+ export function resolveZoraScopedThemeId({
4
+ desiredThemeId,
5
+ inheritedThemeId,
6
+ }: {
7
+ desiredThemeId: ZoraThemeId | undefined;
8
+ inheritedThemeId: ZoraThemeId;
9
+ }): ZoraThemeId {
10
+ if (desiredThemeId === undefined || desiredThemeId === inheritedThemeId) {
11
+ return inheritedThemeId;
12
+ }
13
+
14
+ const message = [
15
+ `Unknown ZORA theme id '${desiredThemeId}'.`,
16
+ 'Theme registries are not available yet; register the theme before using themeId scopes.',
17
+ ].join(' ');
18
+
19
+ if (process.env.NODE_ENV === 'production') {
20
+ console.warn(message);
21
+ return inheritedThemeId;
22
+ }
23
+
24
+ throw new Error(message);
25
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Replace themeScopeStructure.test.ts with real behavior tests once the ZORA component test strategy is decided.
3
+ */
4
+ import { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ import { describe, expect, it } from 'bun:test';
8
+
9
+ const themeDir = import.meta.dir;
10
+
11
+ const zoraProviderSource = readFileSync(join(themeDir, 'ZoraProvider.tsx'), 'utf8');
12
+ const themeScopeSource = readFileSync(join(themeDir, 'ZoraThemeScope.tsx'), 'utf8');
13
+ const hocSource = readFileSync(join(themeDir, 'withZoraThemeScope.tsx'), 'utf8');
14
+
15
+ const textSource = readFileSync(join(themeDir, '..', 'components', 'text', 'Text.tsx'), 'utf8');
16
+ const headingSource = readFileSync(
17
+ join(themeDir, '..', 'components', 'heading', 'Heading.tsx'),
18
+ 'utf8',
19
+ );
20
+ const buttonSource = readFileSync(
21
+ join(themeDir, '..', 'components', 'button', 'Button.tsx'),
22
+ 'utf8',
23
+ );
24
+ const iconSource = readFileSync(join(themeDir, '..', 'components', 'icon', 'Icon.tsx'), 'utf8');
25
+ const iconButtonSource = readFileSync(
26
+ join(themeDir, '..', 'components', 'icon-button', 'IconButton.tsx'),
27
+ 'utf8',
28
+ );
29
+ const cardSource = readFileSync(join(themeDir, '..', 'components', 'card', 'Card.tsx'), 'utf8');
30
+ const panelSource = readFileSync(join(themeDir, '..', 'patterns', 'panel', 'Panel.tsx'), 'utf8');
31
+
32
+ const textTypesSource = readFileSync(
33
+ join(themeDir, '..', 'components', 'text', 'types.ts'),
34
+ 'utf8',
35
+ );
36
+ const headingTypesSource = readFileSync(
37
+ join(themeDir, '..', 'components', 'heading', 'types.ts'),
38
+ 'utf8',
39
+ );
40
+ const panelTypesSource = readFileSync(
41
+ join(themeDir, '..', 'patterns', 'panel', 'types.ts'),
42
+ 'utf8',
43
+ );
44
+
45
+ describe('theme scope structure', () => {
46
+ it('keeps ZoraProvider lightweight (no extra ResponsiveProvider nesting)', () => {
47
+ expect(zoraProviderSource).toMatch(/ThemeProvider/);
48
+ expect(zoraProviderSource).toMatch(/ZoraThemeRuntimeContext\.Provider/);
49
+ expect(zoraProviderSource).not.toMatch(/ResponsiveProvider/);
50
+ });
51
+
52
+ it('implements nested scopes without nesting Surface ThemeProvider', () => {
53
+ expect(themeScopeSource).toMatch(/ThemeContext\.Provider/);
54
+ expect(themeScopeSource).toMatch(/createTheme\(/);
55
+ expect(themeScopeSource).not.toMatch(/ThemeProvider/);
56
+ });
57
+
58
+ it('wraps components only when mode/themeId overrides are present', () => {
59
+ expect(hocSource).toMatch(/props\.mode === undefined/);
60
+ expect(hocSource).toMatch(/props\.themeId === undefined/);
61
+ expect(hocSource).toMatch(/<ZoraThemeScope/);
62
+ });
63
+
64
+ it('adopts the inner + HOC pattern for the first component set', () => {
65
+ expect(textSource).toMatch(/themeId: _themeId/);
66
+ expect(textSource).toMatch(/mode: _mode/);
67
+ expect(textSource).toMatch(/export const Text = withZoraThemeScope/);
68
+
69
+ expect(headingSource).toMatch(/themeId: _themeId/);
70
+ expect(headingSource).toMatch(/mode: _mode/);
71
+ expect(headingSource).toMatch(/export const Heading = withZoraThemeScope/);
72
+
73
+ expect(buttonSource).toMatch(/themeId: _themeId/);
74
+ expect(buttonSource).toMatch(/mode: _mode/);
75
+ expect(buttonSource).toMatch(/export const Button = withZoraThemeScope/);
76
+
77
+ expect(iconSource).toMatch(/themeId: _themeId/);
78
+ expect(iconSource).toMatch(/mode: _mode/);
79
+ expect(iconSource).toMatch(/export const Icon = withZoraThemeScope/);
80
+
81
+ expect(iconButtonSource).toMatch(/themeId: _themeId/);
82
+ expect(iconButtonSource).toMatch(/mode: _mode/);
83
+ expect(iconButtonSource).toMatch(/export const IconButton = withZoraThemeScope/);
84
+
85
+ expect(cardSource).toMatch(/themeId: _themeId/);
86
+ expect(cardSource).toMatch(/mode: _mode/);
87
+ expect(cardSource).toMatch(/export const Card = withZoraThemeScope/);
88
+
89
+ expect(panelSource).toMatch(/themeId: _themeId/);
90
+ expect(panelSource).toMatch(/mode: _mode/);
91
+ expect(panelSource).toMatch(/export const Panel = withZoraThemeScope/);
92
+ });
93
+
94
+ it('adds ZoraBaseProps to converted public prop types', () => {
95
+ expect(textTypesSource).toMatch(/export interface TextProps extends ZoraBaseProps/);
96
+ expect(headingTypesSource).toMatch(/export interface HeadingProps extends ZoraBaseProps/);
97
+ expect(panelTypesSource).toMatch(/export interface PanelProps extends ZoraBaseProps/);
98
+ });
99
+ });