@ankhorage/zora 0.9.0 → 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 (98) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +59 -34
  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.map +1 -1
  54. package/dist/theme/ZoraProvider.js +8 -4
  55. package/dist/theme/ZoraProvider.js.map +1 -1
  56. package/dist/theme/ZoraThemeRuntimeContext.d.ts +9 -0
  57. package/dist/theme/ZoraThemeRuntimeContext.d.ts.map +1 -0
  58. package/dist/theme/ZoraThemeRuntimeContext.js +10 -0
  59. package/dist/theme/ZoraThemeRuntimeContext.js.map +1 -0
  60. package/dist/theme/ZoraThemeScope.d.ts +9 -0
  61. package/dist/theme/ZoraThemeScope.d.ts.map +1 -0
  62. package/dist/theme/ZoraThemeScope.js +41 -0
  63. package/dist/theme/ZoraThemeScope.js.map +1 -0
  64. package/dist/theme/index.d.ts +4 -0
  65. package/dist/theme/index.d.ts.map +1 -1
  66. package/dist/theme/index.js +2 -0
  67. package/dist/theme/index.js.map +1 -1
  68. package/dist/theme/resolveZoraScopedThemeId.d.ts +6 -0
  69. package/dist/theme/resolveZoraScopedThemeId.d.ts.map +1 -0
  70. package/dist/theme/resolveZoraScopedThemeId.js +15 -0
  71. package/dist/theme/resolveZoraScopedThemeId.js.map +1 -0
  72. package/dist/theme/withZoraThemeScope.d.ts +4 -0
  73. package/dist/theme/withZoraThemeScope.d.ts.map +1 -0
  74. package/dist/theme/withZoraThemeScope.js +16 -0
  75. package/dist/theme/withZoraThemeScope.js.map +1 -0
  76. package/package.json +1 -1
  77. package/src/components/button/Button.tsx +11 -1
  78. package/src/components/button/types.ts +3 -4
  79. package/src/components/card/Card.tsx +6 -1
  80. package/src/components/card/types.ts +3 -1
  81. package/src/components/heading/Heading.tsx +6 -1
  82. package/src/components/heading/types.ts +3 -2
  83. package/src/components/icon/Icon.tsx +7 -2
  84. package/src/components/icon-button/IconButton.tsx +6 -1
  85. package/src/components/icon-button/types.ts +2 -1
  86. package/src/components/text/Text.tsx +6 -1
  87. package/src/components/text/types.ts +3 -2
  88. package/src/patterns/panel/Panel.tsx +4 -1
  89. package/src/patterns/panel/types.ts +2 -2
  90. package/src/theme/ZoraBaseProps.ts +20 -0
  91. package/src/theme/ZoraProvider.tsx +9 -4
  92. package/src/theme/ZoraThemeRuntimeContext.tsx +18 -0
  93. package/src/theme/ZoraThemeScope.tsx +74 -0
  94. package/src/theme/index.ts +4 -0
  95. package/src/theme/resolveZoraScopedThemeId.test.ts +47 -0
  96. package/src/theme/resolveZoraScopedThemeId.ts +25 -0
  97. package/src/theme/themeScopeStructure.test.ts +99 -0
  98. package/src/theme/withZoraThemeScope.tsx +25 -0
@@ -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,9 +1,10 @@
1
- import { ResponsiveProvider, ThemeProvider } from '@ankhorage/surface';
1
+ import { ThemeProvider } from '@ankhorage/surface';
2
2
  import React from 'react';
3
3
 
4
4
  import { createZoraThemeConfig } from './createZoraThemeConfig';
5
5
  import type { ZoraTheme, ZoraThemeMode } from './types';
6
6
  import { zoraDefaultTheme } from './zoraDefaultTheme';
7
+ import { ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';
7
8
 
8
9
  export interface ZoraProviderProps {
9
10
  children: React.ReactNode;
@@ -16,9 +17,13 @@ export function ZoraProvider({
16
17
  theme = zoraDefaultTheme,
17
18
  initialMode = 'light',
18
19
  }: ZoraProviderProps) {
20
+ const runtimeValue = React.useMemo(() => ({ sourceTheme: theme, themeId: theme.id }), [theme]);
21
+
19
22
  return (
20
- <ThemeProvider initialConfig={createZoraThemeConfig(theme)} initialMode={initialMode}>
21
- <ResponsiveProvider>{children}</ResponsiveProvider>
22
- </ThemeProvider>
23
+ <ZoraThemeRuntimeContext.Provider value={runtimeValue}>
24
+ <ThemeProvider initialConfig={createZoraThemeConfig(theme)} initialMode={initialMode}>
25
+ {children}
26
+ </ThemeProvider>
27
+ </ZoraThemeRuntimeContext.Provider>
23
28
  );
24
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
+ }
@@ -9,6 +9,10 @@ export type {
9
9
  ZoraThemeMode,
10
10
  } from './types';
11
11
  export { useZoraTheme } from './useZoraTheme';
12
+ export { withZoraThemeScope } from './withZoraThemeScope';
13
+ export type { ZoraBaseProps } from './ZoraBaseProps';
12
14
  export { zoraDefaultTheme } from './zoraDefaultTheme';
13
15
  export type { ZoraProviderProps } from './ZoraProvider';
14
16
  export { ZoraProvider } from './ZoraProvider';
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
+ });
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ import type { ZoraBaseProps } from './ZoraBaseProps';
4
+ import { ZoraThemeScope } from './ZoraThemeScope';
5
+
6
+ export function withZoraThemeScope<P extends ZoraBaseProps>(
7
+ Component: React.ComponentType<P>,
8
+ ): React.FC<P> {
9
+ const Wrapped: React.FC<P> = (props) => {
10
+ if (props.mode === undefined && props.themeId === undefined) {
11
+ return <Component {...props} />;
12
+ }
13
+
14
+ return (
15
+ <ZoraThemeScope mode={props.mode} themeId={props.themeId}>
16
+ <Component {...props} />
17
+ </ZoraThemeScope>
18
+ );
19
+ };
20
+
21
+ const name = Component.displayName ?? (Component.name || 'Component');
22
+ Wrapped.displayName = `withZoraThemeScope(${name})`;
23
+
24
+ return Wrapped;
25
+ }