@ankhorage/zora 0.15.4 → 0.16.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 +6 -0
- package/dist/patterns/theme-composer/ThemeComposer.d.ts.map +1 -1
- package/dist/patterns/theme-composer/ThemeComposer.js +36 -1
- package/dist/patterns/theme-composer/ThemeComposer.js.map +1 -1
- package/dist/patterns/theme-composer/index.d.ts +1 -1
- package/dist/patterns/theme-composer/index.d.ts.map +1 -1
- package/dist/patterns/theme-composer/index.js.map +1 -1
- package/dist/patterns/theme-composer/recommendations.d.ts +14 -0
- package/dist/patterns/theme-composer/recommendations.d.ts.map +1 -0
- package/dist/patterns/theme-composer/recommendations.js +58 -0
- package/dist/patterns/theme-composer/recommendations.js.map +1 -0
- package/dist/patterns/theme-composer/types.d.ts +13 -1
- package/dist/patterns/theme-composer/types.d.ts.map +1 -1
- package/dist/patterns/theme-composer/types.js.map +1 -1
- package/package.json +1 -1
- package/src/patterns/theme-composer/ThemeComposer.test.ts +110 -5
- package/src/patterns/theme-composer/ThemeComposer.tsx +62 -0
- package/src/patterns/theme-composer/index.ts +6 -1
- package/src/patterns/theme-composer/recommendations.ts +85 -0
- package/src/patterns/theme-composer/types.ts +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeComposer.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ThemeComposer.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAwB1B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAyRlD,eAAO,MAAM,aAAa,0DAAyC,CAAC"}
|
|
@@ -11,6 +11,7 @@ import { Text } from '../../components/text';
|
|
|
11
11
|
import { ZORA_COLOR_HARMONIES, ZORA_COLOR_TONES, } from '../../theme/types';
|
|
12
12
|
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
13
13
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
14
|
+
import { createThemeFromThemeComposerRecommendation, findThemeComposerRecommendation, formatThemeComposerLabel, } from './recommendations';
|
|
14
15
|
const HEX_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
15
16
|
function isValidHex(value) {
|
|
16
17
|
return HEX_RE.test(value);
|
|
@@ -45,7 +46,7 @@ const COLOR_TONE_FOREGROUND_TONE = {
|
|
|
45
46
|
vaporwave: 'Fluorescent',
|
|
46
47
|
monochromeAccent: 'Jewel',
|
|
47
48
|
};
|
|
48
|
-
function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeChange, onSubmit, testID, }) {
|
|
49
|
+
function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeChange, onSubmit, appCategory, appMood, recommendations, testID, }) {
|
|
49
50
|
const { theme } = useZoraTheme();
|
|
50
51
|
const [hexInput, setHexInput] = React.useState(value.primaryColor);
|
|
51
52
|
const [hexError, setHexError] = React.useState(undefined);
|
|
@@ -67,7 +68,41 @@ function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeCh
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
const activeMode = mode ?? 'light';
|
|
71
|
+
const recommendation = findThemeComposerRecommendation({
|
|
72
|
+
appCategory,
|
|
73
|
+
appMood,
|
|
74
|
+
recommendations,
|
|
75
|
+
});
|
|
70
76
|
return (<Stack gap="l" testID={testID}>
|
|
77
|
+
{recommendation ? (<Card title="Recommended starting point" description="Use this as an optional starting point. It is only applied when you choose it." actions={<Button size="s" emphasis="soft" tone="primary" onPress={() => onChange(createThemeFromThemeComposerRecommendation({
|
|
78
|
+
value,
|
|
79
|
+
recommendation,
|
|
80
|
+
}))} testID={testID ? `${testID}-apply-recommendation` : undefined}>
|
|
81
|
+
Apply recommendation
|
|
82
|
+
</Button>}>
|
|
83
|
+
<Stack gap="s">
|
|
84
|
+
<Stack direction="row" gap="s" wrap="wrap">
|
|
85
|
+
<Badge tone="primary" emphasis="soft">
|
|
86
|
+
{formatThemeComposerLabel(recommendation.appMood)} mood
|
|
87
|
+
</Badge>
|
|
88
|
+
<Badge tone="neutral" emphasis="soft">
|
|
89
|
+
{formatThemeComposerLabel(recommendation.suggestedColorTone)} color tone
|
|
90
|
+
</Badge>
|
|
91
|
+
<Badge tone="neutral" emphasis="soft">
|
|
92
|
+
{formatThemeComposerLabel(recommendation.suggestedHarmony)} harmony
|
|
93
|
+
</Badge>
|
|
94
|
+
{recommendation.suggestedPrimaryHueDegrees === undefined ? null : (<Badge tone="neutral" emphasis="soft">
|
|
95
|
+
{recommendation.suggestedPrimaryHueDegrees}° hue
|
|
96
|
+
</Badge>)}
|
|
97
|
+
</Stack>
|
|
98
|
+
<Text tone="muted" variant="bodySmall">
|
|
99
|
+
Suggested for {formatThemeComposerLabel(recommendation.appCategory)}. The color tone
|
|
100
|
+
controls palette character, harmony controls accent relationships, and hue sets the
|
|
101
|
+
starting primary color when available.
|
|
102
|
+
</Text>
|
|
103
|
+
</Stack>
|
|
104
|
+
</Card>) : null}
|
|
105
|
+
|
|
71
106
|
{/* Section: Primary Color */}
|
|
72
107
|
<Card title="Primary color" description="Set the seed color for your theme palette.">
|
|
73
108
|
<Stack gap="m">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeComposer.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Box, Stack } from '@ankhorage/surface';\nimport React from 'react';\n\nimport { Badge } from '../../components/badge';\nimport { Button } from '../../components/button';\nimport { Card } from '../../components/card';\nimport { Heading } from '../../components/heading';\nimport { Input } from '../../components/input';\nimport { Select } from '../../components/select';\nimport { Tabs } from '../../components/tabs';\nimport { Text } from '../../components/text';\nimport {\n ZORA_COLOR_HARMONIES,\n ZORA_COLOR_TONES,\n type ZoraColorTone,\n type ZoraTheme,\n type ZoraThemeMode,\n} from '../../theme/types';\nimport { useZoraTheme } from '../../theme/useZoraTheme';\nimport { withZoraThemeScope } from '../../theme/withZoraThemeScope';\nimport type { ThemeComposerProps } from './types';\n\nconst HEX_RE = /^#[0-9A-Fa-f]{6}$/;\n\nfunction isValidHex(value: string): boolean {\n return HEX_RE.test(value);\n}\n\nconst HARMONY_OPTIONS = ZORA_COLOR_HARMONIES.map((h) => ({ value: h, label: h }));\n\nconst TONE_OPTIONS = ZORA_COLOR_TONES.map((t) => ({ value: t, label: t }));\n\nconst MODE_TABS = [\n { value: 'light' as ZoraThemeMode, label: 'Light' },\n { value: 'dark' as ZoraThemeMode, label: 'Dark' },\n];\n\nconst COLOR_TONE_BACKGROUND_TONE: Record<ZoraColorTone, string> = {\n neutral: 'Neutral',\n pastel: 'Pastel',\n earth: 'Earth',\n mineral: 'Mineral',\n muted: 'Muted',\n jewel: 'Neutral',\n fluorescent: 'Obsidian',\n obsidian: 'Obsidian',\n vaporwave: 'Pastel',\n monochromeAccent: 'Neutral',\n};\n\nconst COLOR_TONE_FOREGROUND_TONE: Record<ZoraColorTone, string> = {\n neutral: 'Jewel',\n pastel: 'Jewel',\n earth: 'Mineral',\n mineral: 'Jewel',\n muted: 'Jewel',\n jewel: 'Jewel',\n fluorescent: 'Fluorescent',\n obsidian: 'Fluorescent',\n vaporwave: 'Fluorescent',\n monochromeAccent: 'Jewel',\n};\n\nfunction ThemeComposerInner({\n themeId: _themeId,\n value,\n onChange,\n mode,\n onModeChange,\n onSubmit,\n testID,\n}: ThemeComposerProps) {\n const { theme } = useZoraTheme();\n\n const [hexInput, setHexInput] = React.useState<string>(value.primaryColor);\n const [hexError, setHexError] = React.useState<string | undefined>(undefined);\n\n // Keep local hex input in sync when value.primaryColor changes externally\n React.useEffect(() => {\n setHexInput(value.primaryColor);\n setHexError(undefined);\n }, [value.primaryColor]);\n\n function handleHexChange(text: string) {\n // Ensure leading hash\n const normalized = text.startsWith('#') ? text : `#${text}`;\n setHexInput(normalized);\n\n if (isValidHex(normalized)) {\n setHexError(undefined);\n onChange({ ...value, primaryColor: normalized as ZoraTheme['primaryColor'] });\n } else {\n setHexError('Enter a valid 6-digit hex color (e.g. #0f766e).');\n }\n }\n\n const activeMode = mode ?? 'light';\n\n return (\n <Stack gap=\"l\" testID={testID}>\n {/* Section: Primary Color */}\n <Card title=\"Primary color\" description=\"Set the seed color for your theme palette.\">\n <Stack gap=\"m\">\n <Stack direction=\"row\" gap=\"m\" align=\"center\">\n <Box flex={1}>\n <Input\n value={hexInput}\n onChangeText={handleHexChange}\n placeholder=\"#0f766e\"\n autoCapitalize=\"none\"\n autoCorrect={false}\n maxLength={7}\n invalid={hexError !== undefined}\n testID={testID ? `${testID}-hex-input` : undefined}\n />\n </Box>\n {/* Color preview chip */}\n <Box\n width={36}\n height={36}\n radius=\"m\"\n style={{\n backgroundColor: isValidHex(hexInput) ? hexInput : theme.colors.border,\n borderWidth: 1,\n borderColor: theme.colors.border,\n }}\n />\n </Stack>\n {hexError ? (\n <Text tone=\"danger\" variant=\"bodySmall\">\n {hexError}\n </Text>\n ) : null}\n </Stack>\n </Card>\n\n {/* Section: Harmony */}\n <Card\n title=\"Harmony\"\n description=\"Choose how accent hues are generated from your primary color.\"\n >\n <Select\n value={value.harmony}\n options={HARMONY_OPTIONS}\n onValueChange={(h) => onChange({ ...value, harmony: h })}\n testID={testID ? `${testID}-harmony-select` : undefined}\n />\n </Card>\n\n {/* Section: Color tone */}\n <Card\n title=\"Color tone\"\n description=\"Controls the vibrancy and saturation style of the palette.\"\n >\n <Stack gap=\"s\">\n <Select\n value={value.colorTone}\n options={TONE_OPTIONS}\n onValueChange={(t) => onChange({ ...value, colorTone: t })}\n testID={testID ? `${testID}-tone-select` : undefined}\n />\n <Stack direction=\"row\" gap=\"s\" align=\"center\">\n <Text tone=\"muted\" variant=\"caption\">\n Background:\n </Text>\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {COLOR_TONE_BACKGROUND_TONE[value.colorTone]}\n </Badge>\n <Text tone=\"muted\" variant=\"caption\">\n Foreground:\n </Text>\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {COLOR_TONE_FOREGROUND_TONE[value.colorTone]}\n </Badge>\n </Stack>\n </Stack>\n </Card>\n\n {/* Section: Mode */}\n <Card title=\"Mode\" description=\"Switch between light and dark presentation.\">\n <Tabs\n value={activeMode}\n items={MODE_TABS}\n onValueChange={(m) => onModeChange?.(m)}\n variant=\"segmented\"\n testID={testID ? `${testID}-mode-tabs` : undefined}\n />\n </Card>\n\n {/* Section: Preview */}\n <Card title=\"Preview\" description=\"A quick look at how your theme renders common controls.\">\n <Stack gap=\"m\">\n <Heading level={4}>Heading</Heading>\n <Text>Body text — this shows default text color and weight.</Text>\n <Text tone=\"muted\" variant=\"bodySmall\">\n Muted caption text.\n </Text>\n <Stack direction=\"row\" gap=\"s\" align=\"center\">\n <Button tone=\"primary\" emphasis=\"solid\" size=\"m\">\n Primary\n </Button>\n <Button tone=\"neutral\" emphasis=\"soft\" size=\"m\">\n Neutral\n </Button>\n <Button tone=\"danger\" emphasis=\"ghost\" size=\"m\">\n Danger\n </Button>\n </Stack>\n <Stack direction=\"row\" gap=\"s\" align=\"center\">\n <Badge tone=\"primary\">Primary</Badge>\n <Badge tone=\"success\" emphasis=\"soft\">\n Success\n </Badge>\n <Badge tone=\"warning\" emphasis=\"soft\">\n Warning\n </Badge>\n <Badge tone=\"danger\" emphasis=\"soft\">\n Danger\n </Badge>\n </Stack>\n <Card\n tone=\"subtle\"\n title=\"Nested card\"\n description=\"Subtle tone inside the preview.\"\n compact\n />\n </Stack>\n </Card>\n\n {/* Submit */}\n {onSubmit ? (\n <Button\n tone=\"primary\"\n emphasis=\"solid\"\n onPress={() => onSubmit(value)}\n testID={testID ? `${testID}-submit` : undefined}\n >\n Apply theme\n </Button>\n ) : null}\n </Stack>\n );\n}\n\nexport const ThemeComposer = withZoraThemeScope(ThemeComposerInner);\n"]}
|
|
1
|
+
{"version":3,"file":"ThemeComposer.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Box, Stack } from '@ankhorage/surface';\nimport React from 'react';\n\nimport { Badge } from '../../components/badge';\nimport { Button } from '../../components/button';\nimport { Card } from '../../components/card';\nimport { Heading } from '../../components/heading';\nimport { Input } from '../../components/input';\nimport { Select } from '../../components/select';\nimport { Tabs } from '../../components/tabs';\nimport { Text } from '../../components/text';\nimport {\n ZORA_COLOR_HARMONIES,\n ZORA_COLOR_TONES,\n type ZoraColorTone,\n type ZoraTheme,\n type ZoraThemeMode,\n} from '../../theme/types';\nimport { useZoraTheme } from '../../theme/useZoraTheme';\nimport { withZoraThemeScope } from '../../theme/withZoraThemeScope';\nimport {\n createThemeFromThemeComposerRecommendation,\n findThemeComposerRecommendation,\n formatThemeComposerLabel,\n} from './recommendations';\nimport type { ThemeComposerProps } from './types';\n\nconst HEX_RE = /^#[0-9A-Fa-f]{6}$/;\n\nfunction isValidHex(value: string): boolean {\n return HEX_RE.test(value);\n}\n\nconst HARMONY_OPTIONS = ZORA_COLOR_HARMONIES.map((h) => ({ value: h, label: h }));\n\nconst TONE_OPTIONS = ZORA_COLOR_TONES.map((t) => ({ value: t, label: t }));\n\nconst MODE_TABS = [\n { value: 'light' as ZoraThemeMode, label: 'Light' },\n { value: 'dark' as ZoraThemeMode, label: 'Dark' },\n];\n\nconst COLOR_TONE_BACKGROUND_TONE: Record<ZoraColorTone, string> = {\n neutral: 'Neutral',\n pastel: 'Pastel',\n earth: 'Earth',\n mineral: 'Mineral',\n muted: 'Muted',\n jewel: 'Neutral',\n fluorescent: 'Obsidian',\n obsidian: 'Obsidian',\n vaporwave: 'Pastel',\n monochromeAccent: 'Neutral',\n};\n\nconst COLOR_TONE_FOREGROUND_TONE: Record<ZoraColorTone, string> = {\n neutral: 'Jewel',\n pastel: 'Jewel',\n earth: 'Mineral',\n mineral: 'Jewel',\n muted: 'Jewel',\n jewel: 'Jewel',\n fluorescent: 'Fluorescent',\n obsidian: 'Fluorescent',\n vaporwave: 'Fluorescent',\n monochromeAccent: 'Jewel',\n};\n\nfunction ThemeComposerInner({\n themeId: _themeId,\n value,\n onChange,\n mode,\n onModeChange,\n onSubmit,\n appCategory,\n appMood,\n recommendations,\n testID,\n}: ThemeComposerProps) {\n const { theme } = useZoraTheme();\n\n const [hexInput, setHexInput] = React.useState<string>(value.primaryColor);\n const [hexError, setHexError] = React.useState<string | undefined>(undefined);\n\n // Keep local hex input in sync when value.primaryColor changes externally\n React.useEffect(() => {\n setHexInput(value.primaryColor);\n setHexError(undefined);\n }, [value.primaryColor]);\n\n function handleHexChange(text: string) {\n // Ensure leading hash\n const normalized = text.startsWith('#') ? text : `#${text}`;\n setHexInput(normalized);\n\n if (isValidHex(normalized)) {\n setHexError(undefined);\n onChange({ ...value, primaryColor: normalized as ZoraTheme['primaryColor'] });\n } else {\n setHexError('Enter a valid 6-digit hex color (e.g. #0f766e).');\n }\n }\n\n const activeMode = mode ?? 'light';\n const recommendation = findThemeComposerRecommendation({\n appCategory,\n appMood,\n recommendations,\n });\n\n return (\n <Stack gap=\"l\" testID={testID}>\n {recommendation ? (\n <Card\n title=\"Recommended starting point\"\n description=\"Use this as an optional starting point. It is only applied when you choose it.\"\n actions={\n <Button\n size=\"s\"\n emphasis=\"soft\"\n tone=\"primary\"\n onPress={() =>\n onChange(\n createThemeFromThemeComposerRecommendation({\n value,\n recommendation,\n }),\n )\n }\n testID={testID ? `${testID}-apply-recommendation` : undefined}\n >\n Apply recommendation\n </Button>\n }\n >\n <Stack gap=\"s\">\n <Stack direction=\"row\" gap=\"s\" wrap=\"wrap\">\n <Badge tone=\"primary\" emphasis=\"soft\">\n {formatThemeComposerLabel(recommendation.appMood)} mood\n </Badge>\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {formatThemeComposerLabel(recommendation.suggestedColorTone)} color tone\n </Badge>\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {formatThemeComposerLabel(recommendation.suggestedHarmony)} harmony\n </Badge>\n {recommendation.suggestedPrimaryHueDegrees === undefined ? null : (\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {recommendation.suggestedPrimaryHueDegrees}° hue\n </Badge>\n )}\n </Stack>\n <Text tone=\"muted\" variant=\"bodySmall\">\n Suggested for {formatThemeComposerLabel(recommendation.appCategory)}. The color tone\n controls palette character, harmony controls accent relationships, and hue sets the\n starting primary color when available.\n </Text>\n </Stack>\n </Card>\n ) : null}\n\n {/* Section: Primary Color */}\n <Card title=\"Primary color\" description=\"Set the seed color for your theme palette.\">\n <Stack gap=\"m\">\n <Stack direction=\"row\" gap=\"m\" align=\"center\">\n <Box flex={1}>\n <Input\n value={hexInput}\n onChangeText={handleHexChange}\n placeholder=\"#0f766e\"\n autoCapitalize=\"none\"\n autoCorrect={false}\n maxLength={7}\n invalid={hexError !== undefined}\n testID={testID ? `${testID}-hex-input` : undefined}\n />\n </Box>\n {/* Color preview chip */}\n <Box\n width={36}\n height={36}\n radius=\"m\"\n style={{\n backgroundColor: isValidHex(hexInput) ? hexInput : theme.colors.border,\n borderWidth: 1,\n borderColor: theme.colors.border,\n }}\n />\n </Stack>\n {hexError ? (\n <Text tone=\"danger\" variant=\"bodySmall\">\n {hexError}\n </Text>\n ) : null}\n </Stack>\n </Card>\n\n {/* Section: Harmony */}\n <Card\n title=\"Harmony\"\n description=\"Choose how accent hues are generated from your primary color.\"\n >\n <Select\n value={value.harmony}\n options={HARMONY_OPTIONS}\n onValueChange={(h) => onChange({ ...value, harmony: h })}\n testID={testID ? `${testID}-harmony-select` : undefined}\n />\n </Card>\n\n {/* Section: Color tone */}\n <Card\n title=\"Color tone\"\n description=\"Controls the vibrancy and saturation style of the palette.\"\n >\n <Stack gap=\"s\">\n <Select\n value={value.colorTone}\n options={TONE_OPTIONS}\n onValueChange={(t) => onChange({ ...value, colorTone: t })}\n testID={testID ? `${testID}-tone-select` : undefined}\n />\n <Stack direction=\"row\" gap=\"s\" align=\"center\">\n <Text tone=\"muted\" variant=\"caption\">\n Background:\n </Text>\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {COLOR_TONE_BACKGROUND_TONE[value.colorTone]}\n </Badge>\n <Text tone=\"muted\" variant=\"caption\">\n Foreground:\n </Text>\n <Badge tone=\"neutral\" emphasis=\"soft\">\n {COLOR_TONE_FOREGROUND_TONE[value.colorTone]}\n </Badge>\n </Stack>\n </Stack>\n </Card>\n\n {/* Section: Mode */}\n <Card title=\"Mode\" description=\"Switch between light and dark presentation.\">\n <Tabs\n value={activeMode}\n items={MODE_TABS}\n onValueChange={(m) => onModeChange?.(m)}\n variant=\"segmented\"\n testID={testID ? `${testID}-mode-tabs` : undefined}\n />\n </Card>\n\n {/* Section: Preview */}\n <Card title=\"Preview\" description=\"A quick look at how your theme renders common controls.\">\n <Stack gap=\"m\">\n <Heading level={4}>Heading</Heading>\n <Text>Body text — this shows default text color and weight.</Text>\n <Text tone=\"muted\" variant=\"bodySmall\">\n Muted caption text.\n </Text>\n <Stack direction=\"row\" gap=\"s\" align=\"center\">\n <Button tone=\"primary\" emphasis=\"solid\" size=\"m\">\n Primary\n </Button>\n <Button tone=\"neutral\" emphasis=\"soft\" size=\"m\">\n Neutral\n </Button>\n <Button tone=\"danger\" emphasis=\"ghost\" size=\"m\">\n Danger\n </Button>\n </Stack>\n <Stack direction=\"row\" gap=\"s\" align=\"center\">\n <Badge tone=\"primary\">Primary</Badge>\n <Badge tone=\"success\" emphasis=\"soft\">\n Success\n </Badge>\n <Badge tone=\"warning\" emphasis=\"soft\">\n Warning\n </Badge>\n <Badge tone=\"danger\" emphasis=\"soft\">\n Danger\n </Badge>\n </Stack>\n <Card\n tone=\"subtle\"\n title=\"Nested card\"\n description=\"Subtle tone inside the preview.\"\n compact\n />\n </Stack>\n </Card>\n\n {/* Submit */}\n {onSubmit ? (\n <Button\n tone=\"primary\"\n emphasis=\"solid\"\n onPress={() => onSubmit(value)}\n testID={testID ? `${testID}-submit` : undefined}\n >\n Apply theme\n </Button>\n ) : null}\n </Stack>\n );\n}\n\nexport const ThemeComposer = withZoraThemeScope(ThemeComposerInner);\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,2BAA2B,GAC5B,MAAM,SAAS,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export { ThemeComposer } from './ThemeComposer';\nexport type {
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export { ThemeComposer } from './ThemeComposer';\nexport type {\n ThemeComposerAppCategory,\n ThemeComposerAppMood,\n ThemeComposerProps,\n ThemeComposerRecommendation,\n} from './types';\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ZoraHexColor, ZoraTheme } from '../../theme/types';
|
|
2
|
+
import type { ThemeComposerAppCategory, ThemeComposerAppMood, ThemeComposerRecommendation } from './types';
|
|
3
|
+
export declare function findThemeComposerRecommendation(options: {
|
|
4
|
+
appCategory?: ThemeComposerAppCategory;
|
|
5
|
+
appMood?: ThemeComposerAppMood;
|
|
6
|
+
recommendations?: readonly ThemeComposerRecommendation[];
|
|
7
|
+
}): ThemeComposerRecommendation | undefined;
|
|
8
|
+
export declare function formatThemeComposerLabel(value: string): string;
|
|
9
|
+
export declare function hueDegreesToZoraHexColor(hueDegrees: number): ZoraHexColor;
|
|
10
|
+
export declare function createThemeFromThemeComposerRecommendation(options: {
|
|
11
|
+
value: ZoraTheme;
|
|
12
|
+
recommendation: ThemeComposerRecommendation;
|
|
13
|
+
}): ZoraTheme;
|
|
14
|
+
//# sourceMappingURL=recommendations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendations.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/recommendations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACvD,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAC1D,GAAG,2BAA2B,GAAG,SAAS,CAU1C;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW9D;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,CA+BzE;AAED,wBAAgB,0CAA0C,CAAC,OAAO,EAAE;IAClE,KAAK,EAAE,SAAS,CAAC;IACjB,cAAc,EAAE,2BAA2B,CAAC;CAC7C,GAAG,SAAS,CAYZ"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function findThemeComposerRecommendation(options) {
|
|
2
|
+
if (options.appCategory === undefined || options.recommendations === undefined) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
return options.recommendations.find((recommendation) => recommendation.appCategory === options.appCategory &&
|
|
6
|
+
(options.appMood === undefined || recommendation.appMood === options.appMood));
|
|
7
|
+
}
|
|
8
|
+
export function formatThemeComposerLabel(value) {
|
|
9
|
+
const spaced = value
|
|
10
|
+
.replace(/_/g, ' ')
|
|
11
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
12
|
+
.trim();
|
|
13
|
+
if (spaced.length === 0) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return `${spaced.slice(0, 1).toUpperCase()}${spaced.slice(1)}`;
|
|
17
|
+
}
|
|
18
|
+
export function hueDegreesToZoraHexColor(hueDegrees) {
|
|
19
|
+
const normalizedHue = ((hueDegrees % 360) + 360) % 360;
|
|
20
|
+
const chroma = 0.56;
|
|
21
|
+
const lightness = 0.46;
|
|
22
|
+
const second = chroma * (1 - Math.abs(((normalizedHue / 60) % 2) - 1));
|
|
23
|
+
const match = lightness - chroma / 2;
|
|
24
|
+
const hueSector = Math.floor(normalizedHue / 60);
|
|
25
|
+
const [redPrime, greenPrime, bluePrime] = (() => {
|
|
26
|
+
switch (hueSector) {
|
|
27
|
+
case 0:
|
|
28
|
+
return [chroma, second, 0];
|
|
29
|
+
case 1:
|
|
30
|
+
return [second, chroma, 0];
|
|
31
|
+
case 2:
|
|
32
|
+
return [0, chroma, second];
|
|
33
|
+
case 3:
|
|
34
|
+
return [0, second, chroma];
|
|
35
|
+
case 4:
|
|
36
|
+
return [second, 0, chroma];
|
|
37
|
+
default:
|
|
38
|
+
return [chroma, 0, second];
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
const toHexChannel = (channel) => {
|
|
42
|
+
const value = Math.round(Math.min(1, Math.max(0, channel + match)) * 255);
|
|
43
|
+
return value.toString(16).padStart(2, '0');
|
|
44
|
+
};
|
|
45
|
+
return `#${toHexChannel(redPrime)}${toHexChannel(greenPrime)}${toHexChannel(bluePrime)}`;
|
|
46
|
+
}
|
|
47
|
+
export function createThemeFromThemeComposerRecommendation(options) {
|
|
48
|
+
const suggestedPrimaryColor = options.recommendation.suggestedPrimaryHueDegrees === undefined
|
|
49
|
+
? options.value.primaryColor
|
|
50
|
+
: hueDegreesToZoraHexColor(options.recommendation.suggestedPrimaryHueDegrees);
|
|
51
|
+
return {
|
|
52
|
+
...options.value,
|
|
53
|
+
primaryColor: suggestedPrimaryColor,
|
|
54
|
+
harmony: options.recommendation.suggestedHarmony,
|
|
55
|
+
colorTone: options.recommendation.suggestedColorTone,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=recommendations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendations.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/recommendations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraHexColor, ZoraTheme } from '../../theme/types';\nimport type {\n ThemeComposerAppCategory,\n ThemeComposerAppMood,\n ThemeComposerRecommendation,\n} from './types';\n\nexport function findThemeComposerRecommendation(options: {\n appCategory?: ThemeComposerAppCategory;\n appMood?: ThemeComposerAppMood;\n recommendations?: readonly ThemeComposerRecommendation[];\n}): ThemeComposerRecommendation | undefined {\n if (options.appCategory === undefined || options.recommendations === undefined) {\n return undefined;\n }\n\n return options.recommendations.find(\n (recommendation) =>\n recommendation.appCategory === options.appCategory &&\n (options.appMood === undefined || recommendation.appMood === options.appMood),\n );\n}\n\nexport function formatThemeComposerLabel(value: string): string {\n const spaced = value\n .replace(/_/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim();\n\n if (spaced.length === 0) {\n return value;\n }\n\n return `${spaced.slice(0, 1).toUpperCase()}${spaced.slice(1)}`;\n}\n\nexport function hueDegreesToZoraHexColor(hueDegrees: number): ZoraHexColor {\n const normalizedHue = ((hueDegrees % 360) + 360) % 360;\n const chroma = 0.56;\n const lightness = 0.46;\n const second = chroma * (1 - Math.abs(((normalizedHue / 60) % 2) - 1));\n const match = lightness - chroma / 2;\n\n const hueSector = Math.floor(normalizedHue / 60);\n const [redPrime, greenPrime, bluePrime] = (() => {\n switch (hueSector) {\n case 0:\n return [chroma, second, 0] as const;\n case 1:\n return [second, chroma, 0] as const;\n case 2:\n return [0, chroma, second] as const;\n case 3:\n return [0, second, chroma] as const;\n case 4:\n return [second, 0, chroma] as const;\n default:\n return [chroma, 0, second] as const;\n }\n })();\n\n const toHexChannel = (channel: number): string => {\n const value = Math.round(Math.min(1, Math.max(0, channel + match)) * 255);\n return value.toString(16).padStart(2, '0');\n };\n\n return `#${toHexChannel(redPrime)}${toHexChannel(greenPrime)}${toHexChannel(bluePrime)}`;\n}\n\nexport function createThemeFromThemeComposerRecommendation(options: {\n value: ZoraTheme;\n recommendation: ThemeComposerRecommendation;\n}): ZoraTheme {\n const suggestedPrimaryColor =\n options.recommendation.suggestedPrimaryHueDegrees === undefined\n ? options.value.primaryColor\n : hueDegreesToZoraHexColor(options.recommendation.suggestedPrimaryHueDegrees);\n\n return {\n ...options.value,\n primaryColor: suggestedPrimaryColor,\n harmony: options.recommendation.suggestedHarmony,\n colorTone: options.recommendation.suggestedColorTone,\n };\n}\n"]}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import type { ZoraTheme, ZoraThemeMode } from '../../theme/types';
|
|
1
|
+
import type { ZoraColorHarmony, ZoraColorTone, ZoraTheme, ZoraThemeMode } from '../../theme/types';
|
|
2
2
|
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
3
|
+
export type ThemeComposerAppCategory = string;
|
|
4
|
+
export type ThemeComposerAppMood = string;
|
|
5
|
+
export interface ThemeComposerRecommendation {
|
|
6
|
+
appCategory: ThemeComposerAppCategory;
|
|
7
|
+
appMood: ThemeComposerAppMood;
|
|
8
|
+
suggestedColorTone: ZoraColorTone;
|
|
9
|
+
suggestedHarmony: ZoraColorHarmony;
|
|
10
|
+
suggestedPrimaryHueDegrees?: number;
|
|
11
|
+
}
|
|
3
12
|
export interface ThemeComposerProps extends ZoraBaseProps {
|
|
4
13
|
value: ZoraTheme;
|
|
5
14
|
onChange: (theme: ZoraTheme) => void;
|
|
6
15
|
mode?: ZoraThemeMode;
|
|
7
16
|
onModeChange?: (mode: ZoraThemeMode) => void;
|
|
8
17
|
onSubmit?: (theme: ZoraTheme) => void;
|
|
18
|
+
appCategory?: ThemeComposerAppCategory;
|
|
19
|
+
appMood?: ThemeComposerAppMood;
|
|
20
|
+
recommendations?: readonly ThemeComposerRecommendation[];
|
|
9
21
|
}
|
|
10
22
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACnG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,MAAM,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAC9C,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE1C,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,wBAAwB,CAAC;IACtC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,kBAAkB,EAAE,aAAa,CAAC;IAClC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACvD,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACrC,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACtC,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAC1D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ZoraTheme, ZoraThemeMode } from '../../theme/types';\nimport type { ZoraBaseProps } from '../../theme/ZoraBaseProps';\n\nexport interface ThemeComposerProps extends ZoraBaseProps {\n value: ZoraTheme;\n onChange: (theme: ZoraTheme) => void;\n mode?: ZoraThemeMode;\n onModeChange?: (mode: ZoraThemeMode) => void;\n onSubmit?: (theme: ZoraTheme) => void;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ZoraColorHarmony, ZoraColorTone, ZoraTheme, ZoraThemeMode } from '../../theme/types';\nimport type { ZoraBaseProps } from '../../theme/ZoraBaseProps';\n\nexport type ThemeComposerAppCategory = string;\nexport type ThemeComposerAppMood = string;\n\nexport interface ThemeComposerRecommendation {\n appCategory: ThemeComposerAppCategory;\n appMood: ThemeComposerAppMood;\n suggestedColorTone: ZoraColorTone;\n suggestedHarmony: ZoraColorHarmony;\n suggestedPrimaryHueDegrees?: number;\n}\n\nexport interface ThemeComposerProps extends ZoraBaseProps {\n value: ZoraTheme;\n onChange: (theme: ZoraTheme) => void;\n mode?: ZoraThemeMode;\n onModeChange?: (mode: ZoraThemeMode) => void;\n onSubmit?: (theme: ZoraTheme) => void;\n appCategory?: ThemeComposerAppCategory;\n appMood?: ThemeComposerAppMood;\n recommendations?: readonly ThemeComposerRecommendation[];\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.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": {
|
|
@@ -2,7 +2,13 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
|
|
3
3
|
import { ZORA_COLOR_HARMONIES, ZORA_COLOR_TONES } from '../../theme/types';
|
|
4
4
|
import { zoraDefaultTheme } from '../../theme/zoraDefaultTheme';
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
createThemeFromThemeComposerRecommendation,
|
|
7
|
+
findThemeComposerRecommendation,
|
|
8
|
+
formatThemeComposerLabel,
|
|
9
|
+
hueDegreesToZoraHexColor,
|
|
10
|
+
} from './recommendations';
|
|
11
|
+
import type { ThemeComposerProps, ThemeComposerRecommendation } from './types';
|
|
6
12
|
|
|
7
13
|
// Validate the exported types compile correctly by asserting shape
|
|
8
14
|
describe('ThemeComposerProps', () => {
|
|
@@ -38,6 +44,25 @@ describe('ThemeComposerProps', () => {
|
|
|
38
44
|
props.onSubmit?.(zoraDefaultTheme);
|
|
39
45
|
expect(submitted).toBe(true);
|
|
40
46
|
});
|
|
47
|
+
|
|
48
|
+
test('accepts optional recommendation inputs', () => {
|
|
49
|
+
const recommendation: ThemeComposerRecommendation = {
|
|
50
|
+
appCategory: 'finance_money',
|
|
51
|
+
appMood: 'trustworthy',
|
|
52
|
+
suggestedColorTone: 'mineral',
|
|
53
|
+
suggestedHarmony: 'analogous',
|
|
54
|
+
suggestedPrimaryHueDegrees: 195,
|
|
55
|
+
};
|
|
56
|
+
const props: ThemeComposerProps = {
|
|
57
|
+
value: zoraDefaultTheme,
|
|
58
|
+
onChange: () => undefined,
|
|
59
|
+
appCategory: 'finance_money',
|
|
60
|
+
appMood: 'trustworthy',
|
|
61
|
+
recommendations: [recommendation],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(props.recommendations?.[0]?.suggestedColorTone).toBe('mineral');
|
|
65
|
+
});
|
|
41
66
|
});
|
|
42
67
|
|
|
43
68
|
describe('ZORA_COLOR_HARMONIES coverage for ThemeComposer', () => {
|
|
@@ -74,7 +99,7 @@ describe('ZORA_COLOR_TONES coverage for ThemeComposer', () => {
|
|
|
74
99
|
|
|
75
100
|
describe('onChange propagation contract', () => {
|
|
76
101
|
test('onChange receives updated theme with new harmony', () => {
|
|
77
|
-
const received:
|
|
102
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
78
103
|
const props: ThemeComposerProps = {
|
|
79
104
|
value: zoraDefaultTheme,
|
|
80
105
|
onChange: (t) => received.push(t),
|
|
@@ -82,16 +107,96 @@ describe('onChange propagation contract', () => {
|
|
|
82
107
|
// Simulate what the component does internally
|
|
83
108
|
props.onChange({ ...zoraDefaultTheme, harmony: 'triadic' });
|
|
84
109
|
expect(received).toHaveLength(1);
|
|
85
|
-
expect(
|
|
110
|
+
expect(received[0]?.harmony).toBe('triadic');
|
|
86
111
|
});
|
|
87
112
|
|
|
88
113
|
test('onChange receives updated theme with new colorTone', () => {
|
|
89
|
-
const received:
|
|
114
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
90
115
|
const props: ThemeComposerProps = {
|
|
91
116
|
value: zoraDefaultTheme,
|
|
92
117
|
onChange: (t) => received.push(t),
|
|
93
118
|
};
|
|
94
119
|
props.onChange({ ...zoraDefaultTheme, colorTone: 'obsidian' });
|
|
95
|
-
expect(
|
|
120
|
+
expect(received[0]?.colorTone).toBe('obsidian');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('ThemeComposer recommendation helpers', () => {
|
|
125
|
+
const recommendations: readonly ThemeComposerRecommendation[] = [
|
|
126
|
+
{
|
|
127
|
+
appCategory: 'finance_money',
|
|
128
|
+
appMood: 'trustworthy',
|
|
129
|
+
suggestedColorTone: 'mineral',
|
|
130
|
+
suggestedHarmony: 'analogous',
|
|
131
|
+
suggestedPrimaryHueDegrees: 195,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
appCategory: 'graphics_design',
|
|
135
|
+
appMood: 'creative',
|
|
136
|
+
suggestedColorTone: 'vaporwave',
|
|
137
|
+
suggestedHarmony: 'splitComplementary',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
test('finds a recommendation by app category and mood', () => {
|
|
142
|
+
const found = findThemeComposerRecommendation({
|
|
143
|
+
appCategory: 'finance_money',
|
|
144
|
+
appMood: 'trustworthy',
|
|
145
|
+
recommendations,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(found?.suggestedColorTone).toBe('mineral');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('does not find a recommendation with mismatched mood', () => {
|
|
152
|
+
const found = findThemeComposerRecommendation({
|
|
153
|
+
appCategory: 'finance_money',
|
|
154
|
+
appMood: 'creative',
|
|
155
|
+
recommendations,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(found).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('creates an updated theme only when recommendation is explicitly applied', () => {
|
|
162
|
+
const [recommendation] = recommendations;
|
|
163
|
+
if (recommendation === undefined) {
|
|
164
|
+
throw new Error('Expected fixture recommendation.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updated = createThemeFromThemeComposerRecommendation({
|
|
168
|
+
value: zoraDefaultTheme,
|
|
169
|
+
recommendation,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(zoraDefaultTheme.colorTone).toBe('jewel');
|
|
173
|
+
expect(updated.colorTone).toBe('mineral');
|
|
174
|
+
expect(updated.harmony).toBe('analogous');
|
|
175
|
+
expect(updated.primaryColor).not.toBe(zoraDefaultTheme.primaryColor);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('keeps the current primary color when recommendation has no hue', () => {
|
|
179
|
+
const [, recommendation] = recommendations;
|
|
180
|
+
if (recommendation === undefined) {
|
|
181
|
+
throw new Error('Expected fixture recommendation.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const updated = createThemeFromThemeComposerRecommendation({
|
|
185
|
+
value: zoraDefaultTheme,
|
|
186
|
+
recommendation,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(updated.primaryColor).toBe(zoraDefaultTheme.primaryColor);
|
|
190
|
+
expect(updated.colorTone).toBe('vaporwave');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('formats serialized labels for display', () => {
|
|
194
|
+
expect(formatThemeComposerLabel('finance_money')).toBe('Finance money');
|
|
195
|
+
expect(formatThemeComposerLabel('splitComplementary')).toBe('Split Complementary');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('converts hue degrees to lowercase hex', () => {
|
|
199
|
+
expect(hueDegreesToZoraHexColor(195)).toMatch(/^#[0-9a-f]{6}$/);
|
|
200
|
+
expect(hueDegreesToZoraHexColor(555)).toBe(hueDegreesToZoraHexColor(195));
|
|
96
201
|
});
|
|
97
202
|
});
|
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
} from '../../theme/types';
|
|
19
19
|
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
20
20
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
21
|
+
import {
|
|
22
|
+
createThemeFromThemeComposerRecommendation,
|
|
23
|
+
findThemeComposerRecommendation,
|
|
24
|
+
formatThemeComposerLabel,
|
|
25
|
+
} from './recommendations';
|
|
21
26
|
import type { ThemeComposerProps } from './types';
|
|
22
27
|
|
|
23
28
|
const HEX_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
@@ -68,6 +73,9 @@ function ThemeComposerInner({
|
|
|
68
73
|
mode,
|
|
69
74
|
onModeChange,
|
|
70
75
|
onSubmit,
|
|
76
|
+
appCategory,
|
|
77
|
+
appMood,
|
|
78
|
+
recommendations,
|
|
71
79
|
testID,
|
|
72
80
|
}: ThemeComposerProps) {
|
|
73
81
|
const { theme } = useZoraTheme();
|
|
@@ -95,9 +103,63 @@ function ThemeComposerInner({
|
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
const activeMode = mode ?? 'light';
|
|
106
|
+
const recommendation = findThemeComposerRecommendation({
|
|
107
|
+
appCategory,
|
|
108
|
+
appMood,
|
|
109
|
+
recommendations,
|
|
110
|
+
});
|
|
98
111
|
|
|
99
112
|
return (
|
|
100
113
|
<Stack gap="l" testID={testID}>
|
|
114
|
+
{recommendation ? (
|
|
115
|
+
<Card
|
|
116
|
+
title="Recommended starting point"
|
|
117
|
+
description="Use this as an optional starting point. It is only applied when you choose it."
|
|
118
|
+
actions={
|
|
119
|
+
<Button
|
|
120
|
+
size="s"
|
|
121
|
+
emphasis="soft"
|
|
122
|
+
tone="primary"
|
|
123
|
+
onPress={() =>
|
|
124
|
+
onChange(
|
|
125
|
+
createThemeFromThemeComposerRecommendation({
|
|
126
|
+
value,
|
|
127
|
+
recommendation,
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
testID={testID ? `${testID}-apply-recommendation` : undefined}
|
|
132
|
+
>
|
|
133
|
+
Apply recommendation
|
|
134
|
+
</Button>
|
|
135
|
+
}
|
|
136
|
+
>
|
|
137
|
+
<Stack gap="s">
|
|
138
|
+
<Stack direction="row" gap="s" wrap="wrap">
|
|
139
|
+
<Badge tone="primary" emphasis="soft">
|
|
140
|
+
{formatThemeComposerLabel(recommendation.appMood)} mood
|
|
141
|
+
</Badge>
|
|
142
|
+
<Badge tone="neutral" emphasis="soft">
|
|
143
|
+
{formatThemeComposerLabel(recommendation.suggestedColorTone)} color tone
|
|
144
|
+
</Badge>
|
|
145
|
+
<Badge tone="neutral" emphasis="soft">
|
|
146
|
+
{formatThemeComposerLabel(recommendation.suggestedHarmony)} harmony
|
|
147
|
+
</Badge>
|
|
148
|
+
{recommendation.suggestedPrimaryHueDegrees === undefined ? null : (
|
|
149
|
+
<Badge tone="neutral" emphasis="soft">
|
|
150
|
+
{recommendation.suggestedPrimaryHueDegrees}° hue
|
|
151
|
+
</Badge>
|
|
152
|
+
)}
|
|
153
|
+
</Stack>
|
|
154
|
+
<Text tone="muted" variant="bodySmall">
|
|
155
|
+
Suggested for {formatThemeComposerLabel(recommendation.appCategory)}. The color tone
|
|
156
|
+
controls palette character, harmony controls accent relationships, and hue sets the
|
|
157
|
+
starting primary color when available.
|
|
158
|
+
</Text>
|
|
159
|
+
</Stack>
|
|
160
|
+
</Card>
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
101
163
|
{/* Section: Primary Color */}
|
|
102
164
|
<Card title="Primary color" description="Set the seed color for your theme palette.">
|
|
103
165
|
<Stack gap="m">
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ZoraHexColor, ZoraTheme } from '../../theme/types';
|
|
2
|
+
import type {
|
|
3
|
+
ThemeComposerAppCategory,
|
|
4
|
+
ThemeComposerAppMood,
|
|
5
|
+
ThemeComposerRecommendation,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
export function findThemeComposerRecommendation(options: {
|
|
9
|
+
appCategory?: ThemeComposerAppCategory;
|
|
10
|
+
appMood?: ThemeComposerAppMood;
|
|
11
|
+
recommendations?: readonly ThemeComposerRecommendation[];
|
|
12
|
+
}): ThemeComposerRecommendation | undefined {
|
|
13
|
+
if (options.appCategory === undefined || options.recommendations === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return options.recommendations.find(
|
|
18
|
+
(recommendation) =>
|
|
19
|
+
recommendation.appCategory === options.appCategory &&
|
|
20
|
+
(options.appMood === undefined || recommendation.appMood === options.appMood),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatThemeComposerLabel(value: string): string {
|
|
25
|
+
const spaced = value
|
|
26
|
+
.replace(/_/g, ' ')
|
|
27
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
28
|
+
.trim();
|
|
29
|
+
|
|
30
|
+
if (spaced.length === 0) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return `${spaced.slice(0, 1).toUpperCase()}${spaced.slice(1)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hueDegreesToZoraHexColor(hueDegrees: number): ZoraHexColor {
|
|
38
|
+
const normalizedHue = ((hueDegrees % 360) + 360) % 360;
|
|
39
|
+
const chroma = 0.56;
|
|
40
|
+
const lightness = 0.46;
|
|
41
|
+
const second = chroma * (1 - Math.abs(((normalizedHue / 60) % 2) - 1));
|
|
42
|
+
const match = lightness - chroma / 2;
|
|
43
|
+
|
|
44
|
+
const hueSector = Math.floor(normalizedHue / 60);
|
|
45
|
+
const [redPrime, greenPrime, bluePrime] = (() => {
|
|
46
|
+
switch (hueSector) {
|
|
47
|
+
case 0:
|
|
48
|
+
return [chroma, second, 0] as const;
|
|
49
|
+
case 1:
|
|
50
|
+
return [second, chroma, 0] as const;
|
|
51
|
+
case 2:
|
|
52
|
+
return [0, chroma, second] as const;
|
|
53
|
+
case 3:
|
|
54
|
+
return [0, second, chroma] as const;
|
|
55
|
+
case 4:
|
|
56
|
+
return [second, 0, chroma] as const;
|
|
57
|
+
default:
|
|
58
|
+
return [chroma, 0, second] as const;
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
const toHexChannel = (channel: number): string => {
|
|
63
|
+
const value = Math.round(Math.min(1, Math.max(0, channel + match)) * 255);
|
|
64
|
+
return value.toString(16).padStart(2, '0');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return `#${toHexChannel(redPrime)}${toHexChannel(greenPrime)}${toHexChannel(bluePrime)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createThemeFromThemeComposerRecommendation(options: {
|
|
71
|
+
value: ZoraTheme;
|
|
72
|
+
recommendation: ThemeComposerRecommendation;
|
|
73
|
+
}): ZoraTheme {
|
|
74
|
+
const suggestedPrimaryColor =
|
|
75
|
+
options.recommendation.suggestedPrimaryHueDegrees === undefined
|
|
76
|
+
? options.value.primaryColor
|
|
77
|
+
: hueDegreesToZoraHexColor(options.recommendation.suggestedPrimaryHueDegrees);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
...options.value,
|
|
81
|
+
primaryColor: suggestedPrimaryColor,
|
|
82
|
+
harmony: options.recommendation.suggestedHarmony,
|
|
83
|
+
colorTone: options.recommendation.suggestedColorTone,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import type { ZoraTheme, ZoraThemeMode } from '../../theme/types';
|
|
1
|
+
import type { ZoraColorHarmony, ZoraColorTone, ZoraTheme, ZoraThemeMode } from '../../theme/types';
|
|
2
2
|
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
3
3
|
|
|
4
|
+
export type ThemeComposerAppCategory = string;
|
|
5
|
+
export type ThemeComposerAppMood = string;
|
|
6
|
+
|
|
7
|
+
export interface ThemeComposerRecommendation {
|
|
8
|
+
appCategory: ThemeComposerAppCategory;
|
|
9
|
+
appMood: ThemeComposerAppMood;
|
|
10
|
+
suggestedColorTone: ZoraColorTone;
|
|
11
|
+
suggestedHarmony: ZoraColorHarmony;
|
|
12
|
+
suggestedPrimaryHueDegrees?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
export interface ThemeComposerProps extends ZoraBaseProps {
|
|
5
16
|
value: ZoraTheme;
|
|
6
17
|
onChange: (theme: ZoraTheme) => void;
|
|
7
18
|
mode?: ZoraThemeMode;
|
|
8
19
|
onModeChange?: (mode: ZoraThemeMode) => void;
|
|
9
20
|
onSubmit?: (theme: ZoraTheme) => void;
|
|
21
|
+
appCategory?: ThemeComposerAppCategory;
|
|
22
|
+
appMood?: ThemeComposerAppMood;
|
|
23
|
+
recommendations?: readonly ThemeComposerRecommendation[];
|
|
10
24
|
}
|