@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.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/dist/components/button/Button.d.ts.map +1 -1
- package/dist/components/button/Button.js +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/meta.d.ts +3 -6
- package/dist/components/button/meta.d.ts.map +1 -1
- package/dist/components/button/meta.js +9 -15
- package/dist/components/button/meta.js.map +1 -1
- package/dist/components/button/resolveButtonThemeRecipe.d.ts +9 -0
- package/dist/components/button/resolveButtonThemeRecipe.d.ts.map +1 -0
- package/dist/components/button/resolveButtonThemeRecipe.js +30 -0
- package/dist/components/button/resolveButtonThemeRecipe.js.map +1 -0
- package/dist/components/button/themeRecipeMeta.d.ts +4 -10
- package/dist/components/button/themeRecipeMeta.d.ts.map +1 -1
- package/dist/components/button/themeRecipeMeta.js +5 -10
- package/dist/components/button/themeRecipeMeta.js.map +1 -1
- package/dist/components/card/Card.d.ts.map +1 -1
- package/dist/components/card/Card.js +8 -4
- package/dist/components/card/Card.js.map +1 -1
- package/dist/components/card/meta.d.ts +22 -3
- package/dist/components/card/meta.d.ts.map +1 -1
- package/dist/components/card/meta.js +14 -15
- package/dist/components/card/meta.js.map +1 -1
- package/dist/components/card/resolveCardThemeRecipe.d.ts +15 -0
- package/dist/components/card/resolveCardThemeRecipe.d.ts.map +1 -0
- package/dist/components/card/resolveCardThemeRecipe.js +22 -0
- package/dist/components/card/resolveCardThemeRecipe.js.map +1 -0
- package/dist/components/card/themeRecipeMeta.d.ts +1 -1
- package/dist/components/card/themeRecipeMeta.js +1 -1
- package/dist/components/card/themeRecipeMeta.js.map +1 -1
- package/dist/components/card/types.d.ts +2 -0
- package/dist/components/card/types.d.ts.map +1 -1
- package/dist/components/card/types.js.map +1 -1
- package/dist/components/heading/Heading.d.ts.map +1 -1
- package/dist/components/heading/Heading.js +14 -15
- package/dist/components/heading/Heading.js.map +1 -1
- package/dist/components/heading/meta.d.ts +6 -1
- package/dist/components/heading/meta.d.ts.map +1 -1
- package/dist/components/heading/meta.js +13 -22
- package/dist/components/heading/meta.js.map +1 -1
- package/dist/components/heading/resolveHeadingThemeRecipe.d.ts +11 -0
- package/dist/components/heading/resolveHeadingThemeRecipe.d.ts.map +1 -0
- package/dist/components/heading/resolveHeadingThemeRecipe.js +46 -0
- package/dist/components/heading/resolveHeadingThemeRecipe.js.map +1 -0
- package/dist/components/heading/themeRecipeMeta.d.ts +39 -0
- package/dist/components/heading/themeRecipeMeta.d.ts.map +1 -0
- package/dist/components/heading/themeRecipeMeta.js +32 -0
- package/dist/components/heading/themeRecipeMeta.js.map +1 -0
- package/dist/components/image/meta.d.ts +24 -3
- package/dist/components/image/meta.d.ts.map +1 -1
- package/dist/components/image/meta.js +18 -3
- package/dist/components/image/meta.js.map +1 -1
- package/dist/components/text/Text.d.ts +0 -1
- package/dist/components/text/Text.d.ts.map +1 -1
- package/dist/components/text/Text.js +14 -16
- package/dist/components/text/Text.js.map +1 -1
- package/dist/components/text/meta.d.ts +6 -5
- package/dist/components/text/meta.d.ts.map +1 -1
- package/dist/components/text/meta.js +13 -25
- package/dist/components/text/meta.js.map +1 -1
- package/dist/components/text/resolveTextThemeRecipe.d.ts +11 -0
- package/dist/components/text/resolveTextThemeRecipe.d.ts.map +1 -0
- package/dist/components/text/resolveTextThemeRecipe.js +46 -0
- package/dist/components/text/resolveTextThemeRecipe.js.map +1 -0
- package/dist/components/text/themeRecipeMeta.d.ts +15 -7
- package/dist/components/text/themeRecipeMeta.d.ts.map +1 -1
- package/dist/components/text/themeRecipeMeta.js +12 -11
- package/dist/components/text/themeRecipeMeta.js.map +1 -1
- package/dist/metadata/bindableComponentMeta.d.ts +22 -7
- package/dist/metadata/bindableComponentMeta.d.ts.map +1 -1
- package/dist/metadata/bindableComponentMeta.js +3 -13
- package/dist/metadata/bindableComponentMeta.js.map +1 -1
- package/dist/metadata/themeRecipeMeta.d.ts.map +1 -1
- package/dist/metadata/themeRecipeMeta.js +2 -0
- package/dist/metadata/themeRecipeMeta.js.map +1 -1
- package/dist/metadata/themeRecipeTypes.d.ts +3 -3
- package/dist/metadata/themeRecipeTypes.d.ts.map +1 -1
- package/dist/metadata/themeRecipeTypes.js.map +1 -1
- package/dist/metadata/types.d.ts +4 -2
- package/dist/metadata/types.d.ts.map +1 -1
- package/dist/metadata/types.js.map +1 -1
- package/dist/patterns/panel/Panel.d.ts +0 -2
- package/dist/patterns/panel/Panel.d.ts.map +1 -1
- package/dist/patterns/panel/Panel.js +6 -4
- package/dist/patterns/panel/Panel.js.map +1 -1
- package/dist/patterns/panel/meta.d.ts +22 -3
- package/dist/patterns/panel/meta.d.ts.map +1 -1
- package/dist/patterns/panel/meta.js +14 -15
- package/dist/patterns/panel/meta.js.map +1 -1
- package/dist/patterns/panel/themeRecipeMeta.d.ts +1 -1
- package/dist/patterns/panel/themeRecipeMeta.js +1 -1
- package/dist/patterns/panel/themeRecipeMeta.js.map +1 -1
- package/dist/patterns/panel/types.d.ts +2 -0
- package/dist/patterns/panel/types.d.ts.map +1 -1
- package/dist/patterns/panel/types.js.map +1 -1
- package/dist/theme/ZoraProvider.d.ts +4 -12
- package/dist/theme/ZoraProvider.d.ts.map +1 -1
- package/dist/theme/ZoraProvider.js +5 -14
- package/dist/theme/ZoraProvider.js.map +1 -1
- package/dist/theme/ZoraThemeRuntimeContext.d.ts +1 -2
- package/dist/theme/ZoraThemeRuntimeContext.d.ts.map +1 -1
- package/dist/theme/ZoraThemeRuntimeContext.js +0 -1
- package/dist/theme/ZoraThemeRuntimeContext.js.map +1 -1
- package/dist/theme/ZoraThemeScope.d.ts.map +1 -1
- package/dist/theme/ZoraThemeScope.js +4 -10
- package/dist/theme/ZoraThemeScope.js.map +1 -1
- package/dist/theme/resolveZoraThemeRecipe.d.ts +16 -0
- package/dist/theme/resolveZoraThemeRecipe.d.ts.map +1 -0
- package/dist/theme/resolveZoraThemeRecipe.js +56 -0
- package/dist/theme/resolveZoraThemeRecipe.js.map +1 -0
- package/dist/theme/useZoraTheme.d.ts +1 -1
- package/dist/theme/useZoraThemeRecipe.d.ts +2 -0
- package/dist/theme/useZoraThemeRecipe.d.ts.map +1 -0
- package/dist/theme/useZoraThemeRecipe.js +7 -0
- package/dist/theme/useZoraThemeRecipe.js.map +1 -0
- package/package.json +4 -4
- package/src/components/button/Button.tsx +4 -2
- package/src/components/button/meta.ts +10 -15
- package/src/components/button/resolveButtonThemeRecipe.test.ts +25 -0
- package/src/components/button/resolveButtonThemeRecipe.ts +41 -0
- package/src/components/button/themeRecipeMeta.ts +5 -10
- package/src/components/card/Card.tsx +13 -7
- package/src/components/card/meta.ts +15 -15
- package/src/components/card/resolveCardThemeRecipe.test.ts +28 -0
- package/src/components/card/resolveCardThemeRecipe.ts +33 -0
- package/src/components/card/themeRecipeMeta.ts +1 -1
- package/src/components/card/types.ts +2 -0
- package/src/components/contentFallback.test.ts +4 -4
- package/src/components/heading/Heading.tsx +18 -28
- package/src/components/heading/meta.ts +14 -22
- package/src/components/heading/resolveHeadingThemeRecipe.ts +55 -0
- package/src/components/heading/themeRecipeMeta.ts +33 -0
- package/src/components/image/meta.ts +18 -3
- package/src/components/text/Text.tsx +16 -25
- package/src/components/text/meta.ts +14 -25
- package/src/components/text/resolveTextThemeRecipe.ts +48 -0
- package/src/components/text/themeRecipeMeta.ts +12 -11
- package/src/metadata/authoringMeta.test.ts +18 -44
- package/src/metadata/bindableComponentMeta.test.ts +13 -0
- package/src/metadata/bindableComponentMeta.ts +4 -13
- package/src/metadata/componentMeta.test.ts +14 -0
- package/src/metadata/themeRecipeMeta.test.ts +41 -39
- package/src/metadata/themeRecipeMeta.ts +2 -0
- package/src/metadata/themeRecipeTypes.ts +3 -3
- package/src/metadata/types.ts +11 -24
- package/src/patterns/panel/Panel.tsx +14 -4
- package/src/patterns/panel/meta.ts +15 -15
- package/src/patterns/panel/themeRecipeMeta.ts +1 -1
- package/src/patterns/panel/types.ts +2 -0
- package/src/theme/ZoraProvider.tsx +10 -13
- package/src/theme/ZoraThemeRuntimeContext.tsx +1 -3
- package/src/theme/ZoraThemeScope.tsx +5 -25
- package/src/theme/resolveZoraThemeRecipe.test.ts +72 -0
- package/src/theme/resolveZoraThemeRecipe.ts +91 -0
- package/src/theme/themeConfigScope.test.ts +25 -0
- package/src/theme/useZoraThemeRecipe.ts +7 -0
|
@@ -3,65 +3,67 @@ import { describe, expect, test } from 'bun:test';
|
|
|
3
3
|
import { ZORA_COMPONENT_META, ZORA_THEME_RECIPE_META, ZORA_THEME_TOKEN_FAMILIES } from './index';
|
|
4
4
|
|
|
5
5
|
const tokenFamilies = new Set<string>(ZORA_THEME_TOKEN_FAMILIES);
|
|
6
|
-
const
|
|
7
|
-
['Button', 'component'],
|
|
8
|
-
['Card', 'component'],
|
|
9
|
-
['Text', 'component'],
|
|
10
|
-
['Panel', 'pattern'],
|
|
11
|
-
]);
|
|
6
|
+
const recipeMetaByName = new Map(Object.entries(ZORA_THEME_RECIPE_META));
|
|
12
7
|
|
|
13
8
|
describe('ZORA_THEME_RECIPE_META', () => {
|
|
14
|
-
test('exports
|
|
15
|
-
expect(Object.keys(ZORA_THEME_RECIPE_META)).toEqual([
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
test('exports every current component/pattern theme-authority recipe', () => {
|
|
10
|
+
expect(Object.keys(ZORA_THEME_RECIPE_META)).toEqual([
|
|
11
|
+
'Button',
|
|
12
|
+
'Card',
|
|
13
|
+
'Heading',
|
|
14
|
+
'Text',
|
|
15
|
+
'Panel',
|
|
16
|
+
]);
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
for (const [name, componentMeta] of Object.entries(ZORA_COMPONENT_META)) {
|
|
19
|
+
const themeProps = Object.entries(componentMeta.props).filter(
|
|
20
|
+
([, prop]) => prop.authoring?.authority === 'theme' && prop.authoring.scope !== 'global',
|
|
21
|
+
);
|
|
22
|
+
if (themeProps.length === 0) continue;
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
expect(
|
|
26
|
-
expect(
|
|
24
|
+
const recipe = recipeMetaByName.get(name);
|
|
25
|
+
expect(recipe, `${name} has theme props but no theme recipe`).toBeDefined();
|
|
26
|
+
expect(recipe?.kind).toBe(componentMeta.category === 'pattern' ? 'pattern' : 'component');
|
|
27
|
+
const fields = new Map(Object.entries(recipe?.fields ?? {}));
|
|
28
|
+
for (const [propName] of themeProps) {
|
|
29
|
+
expect(
|
|
30
|
+
fields.get(propName),
|
|
31
|
+
`${name}.${propName} is missing from its recipe`,
|
|
32
|
+
).toBeDefined();
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
|
-
|
|
29
|
-
expect(ZORA_COMPONENT_META.Button.category).toBe('component');
|
|
30
|
-
expect(ZORA_COMPONENT_META.Card.category).toBe('component');
|
|
31
|
-
expect(ZORA_COMPONENT_META.Text.category).toBe('component');
|
|
32
|
-
expect(ZORA_COMPONENT_META.Panel.category).toBe('pattern');
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
test('references only canonical token families and valid
|
|
37
|
+
test('references only canonical token families and valid declared defaults', () => {
|
|
36
38
|
for (const meta of Object.values(ZORA_THEME_RECIPE_META)) {
|
|
37
39
|
for (const field of Object.values(meta.fields)) {
|
|
38
40
|
if (field.type === 'token') {
|
|
39
41
|
expect(tokenFamilies.has(field.tokenFamily), `${meta.name}.${field.label}`).toBe(true);
|
|
40
42
|
}
|
|
41
|
-
if (field.type === 'choice') {
|
|
43
|
+
if (field.type === 'choice' && field.default !== undefined) {
|
|
42
44
|
expect(field.options).toContain(field.default);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
});
|
|
47
49
|
|
|
48
|
-
test('
|
|
49
|
-
expect(ZORA_THEME_RECIPE_META.Button?.fields.size).
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
expect(ZORA_THEME_RECIPE_META.Card?.fields.padding).toMatchObject({
|
|
55
|
-
type: 'token',
|
|
56
|
-
tokenFamily: 'spacing',
|
|
57
|
-
default: 'l',
|
|
58
|
-
});
|
|
50
|
+
test('keeps recipe defaults aligned with existing runtime behavior', () => {
|
|
51
|
+
expect(ZORA_THEME_RECIPE_META.Button?.fields.size?.default).toBe('l');
|
|
52
|
+
expect(ZORA_THEME_RECIPE_META.Card?.fields.padding?.default).toBeUndefined();
|
|
53
|
+
expect(ZORA_THEME_RECIPE_META.Text?.fields.weight?.default).toBeUndefined();
|
|
54
|
+
expect(ZORA_THEME_RECIPE_META.Heading?.fields.size?.default).toBeUndefined();
|
|
59
55
|
});
|
|
60
56
|
|
|
61
|
-
test('
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
test('does not persist theme-owned defaults into new instance blueprints', () => {
|
|
58
|
+
for (const [name, meta] of Object.entries(ZORA_COMPONENT_META)) {
|
|
59
|
+
const props = new Map(Object.entries(meta.props));
|
|
60
|
+
for (const propName of Object.keys(meta.blueprint?.defaultProps ?? {})) {
|
|
61
|
+
expect(
|
|
62
|
+
props.get(propName)?.authoring?.authority,
|
|
63
|
+
`${name}.${propName} blueprint must not pin a theme-owned value`,
|
|
64
|
+
).not.toBe('theme');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
test('is serializable for downstream authoring consumers', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buttonThemeRecipeMeta } from '../components/button/themeRecipeMeta';
|
|
2
2
|
import { cardThemeRecipeMeta } from '../components/card/themeRecipeMeta';
|
|
3
|
+
import { headingThemeRecipeMeta } from '../components/heading/themeRecipeMeta';
|
|
3
4
|
import { textThemeRecipeMeta } from '../components/text/themeRecipeMeta';
|
|
4
5
|
import { panelThemeRecipeMeta } from '../patterns/panel/themeRecipeMeta';
|
|
5
6
|
import type { ZoraThemeRecipeMetaRegistry } from './themeRecipeTypes';
|
|
@@ -7,6 +8,7 @@ import type { ZoraThemeRecipeMetaRegistry } from './themeRecipeTypes';
|
|
|
7
8
|
export const ZORA_THEME_RECIPE_META: ZoraThemeRecipeMetaRegistry = {
|
|
8
9
|
Button: buttonThemeRecipeMeta,
|
|
9
10
|
Card: cardThemeRecipeMeta,
|
|
11
|
+
Heading: headingThemeRecipeMeta,
|
|
10
12
|
Text: textThemeRecipeMeta,
|
|
11
13
|
Panel: panelThemeRecipeMeta,
|
|
12
14
|
};
|
|
@@ -18,18 +18,18 @@ interface ZoraThemeRecipeFieldBase {
|
|
|
18
18
|
export interface ZoraThemeRecipeTokenFieldMeta extends ZoraThemeRecipeFieldBase {
|
|
19
19
|
readonly type: 'token';
|
|
20
20
|
readonly tokenFamily: ZoraThemeTokenFamily;
|
|
21
|
-
readonly default
|
|
21
|
+
readonly default?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface ZoraThemeRecipeChoiceFieldMeta extends ZoraThemeRecipeFieldBase {
|
|
25
25
|
readonly type: 'choice';
|
|
26
26
|
readonly options: readonly string[];
|
|
27
|
-
readonly default
|
|
27
|
+
readonly default?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface ZoraThemeRecipeBooleanFieldMeta extends ZoraThemeRecipeFieldBase {
|
|
31
31
|
readonly type: 'boolean';
|
|
32
|
-
readonly default
|
|
32
|
+
readonly default?: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export type ZoraThemeRecipeFieldMeta =
|
package/src/metadata/types.ts
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ComponentEventDtoKind,
|
|
3
|
+
ComponentRequirements,
|
|
4
|
+
MediaAssetKind,
|
|
5
|
+
UiComponentPropType,
|
|
6
|
+
} from '@ankhorage/contracts';
|
|
2
7
|
|
|
3
8
|
export type ZoraComponentCategory = 'foundation' | 'component' | 'pattern' | 'layout';
|
|
4
9
|
|
|
5
|
-
export type ZoraComponentPropType =
|
|
6
|
-
| 'string'
|
|
7
|
-
| 'number'
|
|
8
|
-
| 'boolean'
|
|
9
|
-
| 'enum'
|
|
10
|
-
| 'color'
|
|
11
|
-
| 'spacing'
|
|
12
|
-
| 'radius'
|
|
13
|
-
| 'shadow'
|
|
14
|
-
| 'typographySize'
|
|
15
|
-
| 'typographyWeight'
|
|
16
|
-
| 'action'
|
|
17
|
-
| 'imageAsset'
|
|
18
|
-
| 'array';
|
|
10
|
+
export type ZoraComponentPropType = UiComponentPropType;
|
|
19
11
|
|
|
20
12
|
export type ZoraComponentPropValue =
|
|
21
13
|
| string
|
|
@@ -30,6 +22,7 @@ export type ZoraComponentPropAuthoring =
|
|
|
30
22
|
| {
|
|
31
23
|
readonly authority: 'theme';
|
|
32
24
|
readonly scope: 'global' | 'component' | 'pattern';
|
|
25
|
+
readonly allowInstanceOverride?: boolean;
|
|
33
26
|
};
|
|
34
27
|
|
|
35
28
|
export interface ZoraComponentPropArrayItemSchema {
|
|
@@ -44,27 +37,21 @@ export interface ZoraComponentPropSchema {
|
|
|
44
37
|
enum?: readonly (string | number)[];
|
|
45
38
|
default?: ZoraComponentPropValue;
|
|
46
39
|
itemSchema?: readonly ZoraComponentPropArrayItemSchema[];
|
|
40
|
+
mediaKinds?: readonly MediaAssetKind[];
|
|
47
41
|
authoring?: ZoraComponentPropAuthoring;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
export interface ZoraComponentBlueprint {
|
|
51
45
|
label: string;
|
|
52
|
-
icon?: {
|
|
53
|
-
name: string;
|
|
54
|
-
provider?: string;
|
|
55
|
-
};
|
|
46
|
+
icon?: { name: string; provider?: string };
|
|
56
47
|
defaultProps?: Readonly<Record<string, ZoraComponentPropValue>>;
|
|
57
48
|
}
|
|
58
49
|
|
|
59
50
|
export interface ZoraComponentI18nMeta {
|
|
60
|
-
fields: readonly {
|
|
61
|
-
keyProp: string;
|
|
62
|
-
defaultTextProp: string;
|
|
63
|
-
}[];
|
|
51
|
+
fields: readonly { keyProp: string; defaultTextProp: string }[];
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
export type ZoraComponentEventPayloadKind = ComponentEventDtoKind | (string & {});
|
|
67
|
-
|
|
68
55
|
export type ZoraComponentEventPayloadFieldType =
|
|
69
56
|
| 'boolean'
|
|
70
57
|
| 'number'
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
import { Card } from '../../components/card';
|
|
4
|
+
import { resolveCardThemeRecipe } from '../../components/card/resolveCardThemeRecipe';
|
|
5
|
+
import { useZoraThemeRecipe } from '../../theme/useZoraThemeRecipe';
|
|
4
6
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
5
7
|
import type { PanelProps } from './types';
|
|
6
8
|
|
|
7
|
-
function PanelInner({
|
|
8
|
-
|
|
9
|
+
function PanelInner({
|
|
10
|
+
themeId: _themeId,
|
|
11
|
+
mode: _mode,
|
|
12
|
+
tone,
|
|
13
|
+
compact,
|
|
14
|
+
padding,
|
|
15
|
+
radius,
|
|
16
|
+
...props
|
|
17
|
+
}: PanelProps) {
|
|
18
|
+
const themeFields = useZoraThemeRecipe('Panel');
|
|
19
|
+
const recipe = resolveCardThemeRecipe({ tone, compact, padding, radius, themeFields });
|
|
20
|
+
return <Card {...props} {...recipe} />;
|
|
9
21
|
}
|
|
10
22
|
|
|
11
23
|
/***
|
|
12
24
|
* Semantic wrapper around `Card` for panel-style page sections.
|
|
13
|
-
*
|
|
14
|
-
|
|
15
25
|
*/
|
|
16
26
|
export const Panel = withZoraThemeScope(PanelInner);
|
|
@@ -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: 'pattern',
|
|
7
|
+
allowInstanceOverride: true,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
4
10
|
export const panelMeta = {
|
|
5
11
|
name: 'Panel',
|
|
6
12
|
category: 'pattern',
|
|
7
13
|
directManifestNode: true,
|
|
8
14
|
allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
|
|
9
|
-
blueprint: {
|
|
10
|
-
label: 'Panel',
|
|
11
|
-
defaultProps: {
|
|
12
|
-
title: 'Panel',
|
|
13
|
-
tone: 'default',
|
|
14
|
-
},
|
|
15
|
-
},
|
|
15
|
+
blueprint: { label: 'Panel', defaultProps: { title: 'Panel' } },
|
|
16
16
|
props: {
|
|
17
17
|
title: {
|
|
18
18
|
type: 'string',
|
|
@@ -37,15 +37,15 @@ export const panelMeta = {
|
|
|
37
37
|
category: 'Style',
|
|
38
38
|
label: 'Tone',
|
|
39
39
|
enum: ['default', 'subtle', 'outline'],
|
|
40
|
-
|
|
41
|
-
authoring: { authority: 'theme', scope: 'pattern' },
|
|
40
|
+
authoring: themeAuthoring,
|
|
42
41
|
},
|
|
43
|
-
compact: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ThemeConfig } from '@ankhorage/contracts';
|
|
1
2
|
import { ThemeProvider } from '@ankhorage/surface';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
|
|
@@ -9,30 +10,26 @@ import { ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';
|
|
|
9
10
|
export interface ZoraProviderProps {
|
|
10
11
|
children: React.ReactNode;
|
|
11
12
|
theme?: ZoraTheme;
|
|
13
|
+
themeConfig?: ThemeConfig;
|
|
12
14
|
initialMode?: ZoraThemeMode;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
* Installs the ZORA theme runtime and underlying Surface theme provider.
|
|
17
|
-
*
|
|
18
|
-
* Wrap an app with `ZoraProvider` once near the root so components, patterns,
|
|
19
|
-
* layouts, and theme hooks resolve the same design tokens and color mode.
|
|
20
|
-
*
|
|
21
|
-
* @example App provider
|
|
22
|
-
* ```tsx
|
|
23
|
-
* <ZoraProvider initialMode="light"><App /></ZoraProvider>
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
17
|
+
/** Installs the ZORA theme runtime and underlying Surface theme provider. */
|
|
26
18
|
export function ZoraProvider({
|
|
27
19
|
children,
|
|
28
20
|
theme = zoraDefaultTheme,
|
|
21
|
+
themeConfig,
|
|
29
22
|
initialMode = 'light',
|
|
30
23
|
}: ZoraProviderProps) {
|
|
31
|
-
const
|
|
24
|
+
const resolvedConfig = React.useMemo(
|
|
25
|
+
() => themeConfig ?? createZoraThemeConfig(theme),
|
|
26
|
+
[theme, themeConfig],
|
|
27
|
+
);
|
|
28
|
+
const runtimeValue = React.useMemo(() => ({ themeId: resolvedConfig.id }), [resolvedConfig.id]);
|
|
32
29
|
|
|
33
30
|
return (
|
|
34
31
|
<ZoraThemeRuntimeContext.Provider value={runtimeValue}>
|
|
35
|
-
<ThemeProvider initialConfig={
|
|
32
|
+
<ThemeProvider initialConfig={resolvedConfig} initialMode={initialMode}>
|
|
36
33
|
{children}
|
|
37
34
|
</ThemeProvider>
|
|
38
35
|
</ZoraThemeRuntimeContext.Provider>
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ZoraThemeId } from './types';
|
|
4
4
|
import { zoraDefaultTheme } from './zoraDefaultTheme';
|
|
5
5
|
|
|
6
6
|
interface ZoraThemeRuntime {
|
|
7
|
-
sourceTheme: ZoraTheme;
|
|
8
7
|
themeId: ZoraThemeId;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export const ZoraThemeRuntimeContext = createContext<ZoraThemeRuntime>({
|
|
12
|
-
sourceTheme: zoraDefaultTheme,
|
|
13
11
|
themeId: zoraDefaultTheme.id,
|
|
14
12
|
});
|
|
15
13
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createTheme, ThemeContext, useFontContext, useTheme } from '@ankhorage/surface';
|
|
2
2
|
import React, { useMemo } from 'react';
|
|
3
3
|
|
|
4
|
-
import { createZoraThemeConfig } from './createZoraThemeConfig';
|
|
5
4
|
import { resolveZoraScopedThemeId } from './resolveZoraScopedThemeId';
|
|
6
5
|
import type { ZoraThemeId, ZoraThemeMode } from './types';
|
|
7
6
|
import { useZoraThemeRuntime, ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';
|
|
@@ -16,25 +15,16 @@ function ZoraThemeScopeInner({ children, themeId, mode }: ZoraThemeScopeProps) {
|
|
|
16
15
|
const parentSurface = useTheme();
|
|
17
16
|
const parentRuntime = useZoraThemeRuntime();
|
|
18
17
|
const { activeFontId } = useFontContext();
|
|
19
|
-
|
|
20
18
|
const scopedThemeId = resolveZoraScopedThemeId({
|
|
21
19
|
desiredThemeId: themeId,
|
|
22
20
|
inheritedThemeId: parentRuntime.themeId,
|
|
23
21
|
});
|
|
24
|
-
|
|
25
22
|
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
|
-
|
|
23
|
+
const sourceConfig = parentSurface.theme.config;
|
|
33
24
|
const scopedTheme = useMemo(
|
|
34
|
-
() => createTheme(
|
|
35
|
-
[
|
|
25
|
+
() => createTheme(sourceConfig, scopedMode, activeFontId),
|
|
26
|
+
[sourceConfig, scopedMode, activeFontId],
|
|
36
27
|
);
|
|
37
|
-
|
|
38
28
|
const scopedSurfaceValue = useMemo(
|
|
39
29
|
() => ({
|
|
40
30
|
theme: scopedTheme,
|
|
@@ -45,14 +35,7 @@ function ZoraThemeScopeInner({ children, themeId, mode }: ZoraThemeScopeProps) {
|
|
|
45
35
|
}),
|
|
46
36
|
[parentSurface.setMode, parentSurface.setThemeConfig, scopedMode, scopedTheme],
|
|
47
37
|
);
|
|
48
|
-
|
|
49
|
-
const scopedRuntimeValue = useMemo(
|
|
50
|
-
() => ({
|
|
51
|
-
sourceTheme: parentRuntime.sourceTheme,
|
|
52
|
-
themeId: scopedThemeId,
|
|
53
|
-
}),
|
|
54
|
-
[parentRuntime.sourceTheme, scopedThemeId],
|
|
55
|
-
);
|
|
38
|
+
const scopedRuntimeValue = useMemo(() => ({ themeId: scopedThemeId }), [scopedThemeId]);
|
|
56
39
|
|
|
57
40
|
return (
|
|
58
41
|
<ZoraThemeRuntimeContext.Provider value={scopedRuntimeValue}>
|
|
@@ -62,10 +45,7 @@ function ZoraThemeScopeInner({ children, themeId, mode }: ZoraThemeScopeProps) {
|
|
|
62
45
|
}
|
|
63
46
|
|
|
64
47
|
export function ZoraThemeScope({ children, themeId, mode }: ZoraThemeScopeProps) {
|
|
65
|
-
if (mode === undefined && themeId === undefined)
|
|
66
|
-
return children;
|
|
67
|
-
}
|
|
68
|
-
|
|
48
|
+
if (mode === undefined && themeId === undefined) return children;
|
|
69
49
|
return (
|
|
70
50
|
<ZoraThemeScopeInner mode={mode} themeId={themeId}>
|
|
71
51
|
{children}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ThemeConfig } from '@ankhorage/contracts';
|
|
2
|
+
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import { resolveZoraThemeRecipe } from './resolveZoraThemeRecipe';
|
|
5
|
+
|
|
6
|
+
const baseConfig: ThemeConfig = {
|
|
7
|
+
id: 'test',
|
|
8
|
+
name: 'Test',
|
|
9
|
+
light: { primaryColor: '#3B82F6', harmony: 'monochromatic' },
|
|
10
|
+
dark: { primaryColor: '#3B82F6', harmony: 'monochromatic' },
|
|
11
|
+
tokens: { spacing: { hero: 64 } },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function runtimeTheme(config: ThemeConfig) {
|
|
15
|
+
return {
|
|
16
|
+
config,
|
|
17
|
+
colors: { primary: '#3B82F6' },
|
|
18
|
+
spacing: { m: 16, l: 24, hero: 64 },
|
|
19
|
+
radii: { m: 8, l: 16 },
|
|
20
|
+
shadows: { soft: 2 },
|
|
21
|
+
typography: {
|
|
22
|
+
sizes: { m: 16 },
|
|
23
|
+
weights: { regular: '400' },
|
|
24
|
+
headings: { 1: { size: 32 } },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withRecipes(recipes: ThemeConfig['recipes']): ThemeConfig {
|
|
30
|
+
return { ...baseConfig, recipes };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('resolveZoraThemeRecipe', () => {
|
|
34
|
+
test('merges metadata defaults with persisted known fields', () => {
|
|
35
|
+
const theme = runtimeTheme(
|
|
36
|
+
withRecipes({ components: { Card: { tone: 'outline', padding: 'hero' } } }),
|
|
37
|
+
);
|
|
38
|
+
expect(resolveZoraThemeRecipe(theme, 'Card')).toEqual({
|
|
39
|
+
tone: 'outline',
|
|
40
|
+
padding: 'hero',
|
|
41
|
+
radius: 'l',
|
|
42
|
+
compact: false,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('ignores stale unknown persisted fields without guessing metadata', () => {
|
|
47
|
+
const theme = runtimeTheme(
|
|
48
|
+
withRecipes({ components: { Button: { variant: 'soft', retiredField: 'legacy' } } }),
|
|
49
|
+
);
|
|
50
|
+
expect(resolveZoraThemeRecipe(theme, 'Button')).toEqual({
|
|
51
|
+
color: 'primary',
|
|
52
|
+
variant: 'soft',
|
|
53
|
+
size: 'l',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('rejects invalid known choices and token references', () => {
|
|
58
|
+
const choiceTheme = runtimeTheme(withRecipes({ components: { Button: { variant: 'neon' } } }));
|
|
59
|
+
expect(() => resolveZoraThemeRecipe(choiceTheme, 'Button')).toThrow(
|
|
60
|
+
'Invalid theme recipe choice',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const tokenTheme = runtimeTheme(withRecipes({ components: { Card: { padding: 'missing' } } }));
|
|
64
|
+
expect(() => resolveZoraThemeRecipe(tokenTheme, 'Card')).toThrow('Unknown spacing token');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('rejects code requests for unknown recipe names', () => {
|
|
68
|
+
expect(() => resolveZoraThemeRecipe(runtimeTheme(baseConfig), 'Missing')).toThrow(
|
|
69
|
+
'Unknown ZORA theme recipe',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ThemeConfig, ThemeRecipeOverrideValue } from '@ankhorage/contracts';
|
|
2
|
+
|
|
3
|
+
import { ZORA_THEME_RECIPE_META } from '../metadata/themeRecipeMeta';
|
|
4
|
+
import type { ZoraThemeRecipeFieldMeta, ZoraThemeTokenFamily } from '../metadata/themeRecipeTypes';
|
|
5
|
+
|
|
6
|
+
interface ZoraThemeRecipeRuntimeTheme {
|
|
7
|
+
readonly config: ThemeConfig;
|
|
8
|
+
readonly colors: object;
|
|
9
|
+
readonly spacing: object;
|
|
10
|
+
readonly radii: object;
|
|
11
|
+
readonly shadows: object;
|
|
12
|
+
readonly typography: {
|
|
13
|
+
readonly sizes: object;
|
|
14
|
+
readonly weights: object;
|
|
15
|
+
readonly headings: object;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const recipeMetaByName = new Map(Object.entries(ZORA_THEME_RECIPE_META));
|
|
20
|
+
|
|
21
|
+
export function resolveZoraThemeRecipe(
|
|
22
|
+
theme: ZoraThemeRecipeRuntimeTheme,
|
|
23
|
+
recipeName: string,
|
|
24
|
+
): Readonly<Record<string, ThemeRecipeOverrideValue>> {
|
|
25
|
+
const meta = recipeMetaByName.get(recipeName);
|
|
26
|
+
if (!meta) throw new RangeError(`Unknown ZORA theme recipe: ${recipeName}.`);
|
|
27
|
+
|
|
28
|
+
const recipes =
|
|
29
|
+
meta.kind === 'component' ? theme.config.recipes?.components : theme.config.recipes?.patterns;
|
|
30
|
+
const overrides = new Map(Object.entries(recipes ?? {})).get(recipeName);
|
|
31
|
+
const overrideByField = new Map(Object.entries(overrides ?? {}));
|
|
32
|
+
const resolved: [string, ThemeRecipeOverrideValue][] = [];
|
|
33
|
+
|
|
34
|
+
for (const [fieldName, fieldMeta] of Object.entries(meta.fields)) {
|
|
35
|
+
const override = overrideByField.get(fieldName);
|
|
36
|
+
if (override !== undefined) {
|
|
37
|
+
validateFieldValue(theme, recipeName, fieldName, fieldMeta, override);
|
|
38
|
+
resolved.push([fieldName, override]);
|
|
39
|
+
} else if (fieldMeta.default !== undefined) {
|
|
40
|
+
resolved.push([fieldName, fieldMeta.default]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Object.fromEntries(resolved);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateFieldValue(
|
|
48
|
+
theme: ZoraThemeRecipeRuntimeTheme,
|
|
49
|
+
recipeName: string,
|
|
50
|
+
fieldName: string,
|
|
51
|
+
meta: ZoraThemeRecipeFieldMeta,
|
|
52
|
+
value: ThemeRecipeOverrideValue,
|
|
53
|
+
): void {
|
|
54
|
+
if (meta.type === 'boolean') {
|
|
55
|
+
if (typeof value !== 'boolean') {
|
|
56
|
+
throw new TypeError(`Theme recipe ${recipeName}.${fieldName} must be boolean.`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value !== 'string') {
|
|
61
|
+
throw new TypeError(`Theme recipe ${recipeName}.${fieldName} must be a string.`);
|
|
62
|
+
}
|
|
63
|
+
if (meta.type === 'choice' && !meta.options.includes(value)) {
|
|
64
|
+
throw new RangeError(`Invalid theme recipe choice for ${recipeName}.${fieldName}: ${value}.`);
|
|
65
|
+
}
|
|
66
|
+
if (meta.type === 'token' && !hasThemeToken(theme, meta.tokenFamily, value)) {
|
|
67
|
+
throw new RangeError(
|
|
68
|
+
`Unknown ${meta.tokenFamily} token for ${recipeName}.${fieldName}: ${value}.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasThemeToken(
|
|
74
|
+
theme: ZoraThemeRecipeRuntimeTheme,
|
|
75
|
+
family: ZoraThemeTokenFamily,
|
|
76
|
+
token: string,
|
|
77
|
+
): boolean {
|
|
78
|
+
if (family === 'colors') return hasOwn(theme.colors, token);
|
|
79
|
+
if (family === 'spacing') return hasOwn(theme.spacing, token);
|
|
80
|
+
if (family === 'radii') return hasOwn(theme.radii, token);
|
|
81
|
+
if (family === 'shadows') return hasOwn(theme.shadows, token);
|
|
82
|
+
return (
|
|
83
|
+
hasOwn(theme.typography.sizes, token) ||
|
|
84
|
+
hasOwn(theme.typography.weights, token) ||
|
|
85
|
+
hasOwn(theme.typography.headings, token)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasOwn(value: object, key: PropertyKey): boolean {
|
|
90
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
91
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
const themeDir = import.meta.dir;
|
|
7
|
+
|
|
8
|
+
describe('canonical ThemeConfig scope propagation', () => {
|
|
9
|
+
test('provider accepts the canonical ThemeConfig without duplicating live config state', () => {
|
|
10
|
+
const provider = readFileSync(join(themeDir, 'ZoraProvider.tsx'), 'utf8');
|
|
11
|
+
const context = readFileSync(join(themeDir, 'ZoraThemeRuntimeContext.tsx'), 'utf8');
|
|
12
|
+
expect(provider).toContain('themeConfig?: ThemeConfig');
|
|
13
|
+
expect(provider).toContain('themeConfig ?? createZoraThemeConfig(theme)');
|
|
14
|
+
expect(context).not.toContain('ThemeConfig');
|
|
15
|
+
expect(context).not.toContain('sourceTheme');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('nested scopes derive from the current Surface config authority', () => {
|
|
19
|
+
const scope = readFileSync(join(themeDir, 'ZoraThemeScope.tsx'), 'utf8');
|
|
20
|
+
expect(scope).toContain('const sourceConfig = parentSurface.theme.config;');
|
|
21
|
+
expect(scope).toContain('createTheme(sourceConfig');
|
|
22
|
+
expect(scope).not.toContain('createZoraThemeConfig');
|
|
23
|
+
expect(scope).not.toContain('sourceTheme');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveZoraThemeRecipe } from './resolveZoraThemeRecipe';
|
|
2
|
+
import { useZoraTheme } from './useZoraTheme';
|
|
3
|
+
|
|
4
|
+
export function useZoraThemeRecipe(recipeName: string) {
|
|
5
|
+
const { theme } = useZoraTheme();
|
|
6
|
+
return resolveZoraThemeRecipe(theme, recipeName);
|
|
7
|
+
}
|