@ankhorage/zora 1.0.0 → 1.0.1
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 +16 -9
- package/dist/patterns/theme-composer/ThemeComposer.d.ts.map +1 -1
- package/dist/patterns/theme-composer/ThemeComposer.js +95 -4
- package/dist/patterns/theme-composer/ThemeComposer.js.map +1 -1
- package/dist/patterns/theme-composer/types.d.ts +2 -0
- 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 +148 -2
- package/src/patterns/theme-composer/ThemeComposer.tsx +126 -3
- package/src/patterns/theme-composer/types.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 938bcfe: ThemeComposer now edits the full ZoraTheme source model.
|
|
8
|
+
- ThemeComposer adds name editing with empty-name validation.
|
|
9
|
+
- ThemeComposer adds app category editing via a Select using APP_CATEGORIES from @ankhorage/contracts.
|
|
10
|
+
- ThemeComposer supports optional `appCategories` prop for narrowing the category options list.
|
|
11
|
+
- ThemeComposer validates primary color input with parseHexColorOrThrow from @ankhorage/color-theory while keeping public `primaryColor` as `string`.
|
|
12
|
+
- ThemeComposer preview shows name, appCategory, primaryColor, and harmony metadata.
|
|
13
|
+
- README and examples app updated to reflect the new API.
|
|
14
|
+
|
|
3
15
|
## 1.0.0
|
|
4
16
|
|
|
5
17
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -1368,7 +1368,13 @@ Pass your current theme as `value` and handle updates through `onChange`. Wrap b
|
|
|
1368
1368
|
in a `ZoraProvider` so the preview area reflects every change immediately.
|
|
1369
1369
|
|
|
1370
1370
|
```tsx
|
|
1371
|
-
const [theme, setTheme] = React.useState<ZoraTheme>(
|
|
1371
|
+
const [theme, setTheme] = React.useState<ZoraTheme>({
|
|
1372
|
+
id: 'my-app',
|
|
1373
|
+
name: 'My App',
|
|
1374
|
+
appCategory: 'developer_tools',
|
|
1375
|
+
primaryColor: '#0f766e',
|
|
1376
|
+
harmony: 'analogous',
|
|
1377
|
+
});
|
|
1372
1378
|
const [mode, setMode] = React.useState<ZoraThemeMode>('light');
|
|
1373
1379
|
|
|
1374
1380
|
return (
|
|
@@ -1389,14 +1395,15 @@ return (
|
|
|
1389
1395
|
|
|
1390
1396
|
ZORA props:
|
|
1391
1397
|
|
|
1392
|
-
| Prop
|
|
1393
|
-
|
|
|
1394
|
-
| `value`
|
|
1395
|
-
| `onChange`
|
|
1396
|
-
| `mode`
|
|
1397
|
-
| `onModeChange`
|
|
1398
|
-
| `onSubmit`
|
|
1399
|
-
| `
|
|
1398
|
+
| Prop | Type | Default | Notes |
|
|
1399
|
+
| --------------- | ------------------------------- | ---------------- | ---------------------------------------------------- |
|
|
1400
|
+
| `value` | `ZoraTheme` | - | Required controlled theme seed. |
|
|
1401
|
+
| `onChange` | `(theme: ZoraTheme) => void` | - | Required. Fires on every valid change. |
|
|
1402
|
+
| `mode` | `ZoraThemeMode` | - | Current light/dark mode shown in the mode toggle. |
|
|
1403
|
+
| `onModeChange` | `(mode: ZoraThemeMode) => void` | - | Called when the user switches the mode toggle. |
|
|
1404
|
+
| `onSubmit` | `(theme: ZoraTheme) => void` | - | Optional. Renders an "Apply theme" button if set. |
|
|
1405
|
+
| `appCategories` | `readonly AppCategory[]` | `APP_CATEGORIES` | Optional override for the app category options list. |
|
|
1406
|
+
| `testID` | `string` | - | Forwarded to the root element and child test points. |
|
|
1400
1407
|
|
|
1401
1408
|
</details>
|
|
1402
1409
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeComposer.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ThemeComposer.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAa1B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAmSlD,eAAO,MAAM,aAAa,0DAAyC,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { COLOR_HARMONIES, parseHexColorOrThrow } from '@ankhorage/color-theory';
|
|
2
|
+
import { APP_CATEGORIES } from '@ankhorage/contracts';
|
|
2
3
|
import { Box, Stack } from '@ankhorage/surface';
|
|
3
4
|
import React from 'react';
|
|
4
5
|
import { Badge } from '../../components/badge';
|
|
@@ -11,6 +12,8 @@ import { Tabs } from '../../components/tabs';
|
|
|
11
12
|
import { Text } from '../../components/text';
|
|
12
13
|
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
13
14
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
15
|
+
const HEX_ERROR_MESSAGE = 'Enter a valid 6-digit hex color (e.g. #0f766e).';
|
|
16
|
+
const NAME_ERROR_MESSAGE = 'Theme name cannot be empty.';
|
|
14
17
|
function isValidHex(value) {
|
|
15
18
|
try {
|
|
16
19
|
parseHexColorOrThrow(value);
|
|
@@ -20,20 +23,42 @@ function isValidHex(value) {
|
|
|
20
23
|
return false;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
function formatAppCategoryLabel(category) {
|
|
27
|
+
return category
|
|
28
|
+
.split('_')
|
|
29
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
30
|
+
.join(' ');
|
|
31
|
+
}
|
|
23
32
|
const HARMONY_OPTIONS = COLOR_HARMONIES.map((h) => ({ value: h, label: h }));
|
|
24
33
|
const MODE_TABS = [
|
|
25
34
|
{ value: 'light', label: 'Light' },
|
|
26
35
|
{ value: 'dark', label: 'Dark' },
|
|
27
36
|
];
|
|
28
|
-
function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeChange, onSubmit, testID, }) {
|
|
37
|
+
function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeChange, onSubmit, appCategories, testID, }) {
|
|
29
38
|
const { theme } = useZoraTheme();
|
|
30
39
|
const [hexInput, setHexInput] = React.useState(value.primaryColor);
|
|
31
40
|
const [hexError, setHexError] = React.useState(undefined);
|
|
32
|
-
|
|
41
|
+
const [nameInput, setNameInput] = React.useState(value.name);
|
|
42
|
+
const [nameError, setNameError] = React.useState(undefined);
|
|
43
|
+
// Keep local inputs in sync when value changes externally
|
|
33
44
|
React.useEffect(() => {
|
|
34
45
|
setHexInput(value.primaryColor);
|
|
35
46
|
setHexError(undefined);
|
|
36
47
|
}, [value.primaryColor]);
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
setNameInput(value.name);
|
|
50
|
+
setNameError(undefined);
|
|
51
|
+
}, [value.name]);
|
|
52
|
+
function handleNameChange(text) {
|
|
53
|
+
setNameInput(text);
|
|
54
|
+
if (text.trim().length === 0) {
|
|
55
|
+
setNameError(NAME_ERROR_MESSAGE);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setNameError(undefined);
|
|
59
|
+
onChange({ ...value, name: text });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
37
62
|
function handleHexChange(text) {
|
|
38
63
|
// Ensure leading hash
|
|
39
64
|
const normalized = text.startsWith('#') ? text : `#${text}`;
|
|
@@ -43,11 +68,57 @@ function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeCh
|
|
|
43
68
|
onChange({ ...value, primaryColor: normalized });
|
|
44
69
|
}
|
|
45
70
|
else {
|
|
46
|
-
setHexError(
|
|
71
|
+
setHexError(HEX_ERROR_MESSAGE);
|
|
47
72
|
}
|
|
48
73
|
}
|
|
74
|
+
function handleSubmit() {
|
|
75
|
+
const hasValidName = nameInput.trim().length > 0;
|
|
76
|
+
const hasValidHex = isValidHex(hexInput);
|
|
77
|
+
if (!hasValidName) {
|
|
78
|
+
setNameError(NAME_ERROR_MESSAGE);
|
|
79
|
+
}
|
|
80
|
+
if (!hasValidHex) {
|
|
81
|
+
setHexError(HEX_ERROR_MESSAGE);
|
|
82
|
+
}
|
|
83
|
+
if (!hasValidName || !hasValidHex) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
onSubmit?.({
|
|
87
|
+
...value,
|
|
88
|
+
name: nameInput,
|
|
89
|
+
primaryColor: hexInput,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
49
92
|
const activeMode = mode ?? 'light';
|
|
93
|
+
const categoryOptions = (appCategories ?? APP_CATEGORIES).map((c) => ({
|
|
94
|
+
value: c,
|
|
95
|
+
label: formatAppCategoryLabel(c),
|
|
96
|
+
}));
|
|
50
97
|
return (<Stack gap="l" testID={testID}>
|
|
98
|
+
{/* Section: Theme identity */}
|
|
99
|
+
<Card title="Theme identity" description="Name your theme. The ID is assigned automatically and shown for reference.">
|
|
100
|
+
<Stack gap="m">
|
|
101
|
+
<Stack gap="xs">
|
|
102
|
+
<Text variant="label">Name</Text>
|
|
103
|
+
<Input value={nameInput} onChangeText={handleNameChange} placeholder="My theme" autoCorrect={false} invalid={nameError !== undefined} testID={testID ? `${testID}-name-input` : undefined}/>
|
|
104
|
+
{nameError ? (<Text tone="danger" variant="bodySmall">
|
|
105
|
+
{nameError}
|
|
106
|
+
</Text>) : null}
|
|
107
|
+
</Stack>
|
|
108
|
+
<Stack gap="xs">
|
|
109
|
+
<Text variant="label">ID</Text>
|
|
110
|
+
<Text tone="muted" variant="bodySmall" testID={testID ? `${testID}-id-display` : undefined}>
|
|
111
|
+
{value.id}
|
|
112
|
+
</Text>
|
|
113
|
+
</Stack>
|
|
114
|
+
</Stack>
|
|
115
|
+
</Card>
|
|
116
|
+
|
|
117
|
+
{/* Section: App category */}
|
|
118
|
+
<Card title="App category" description="Choose the category that best describes this app.">
|
|
119
|
+
<Select value={value.appCategory} options={categoryOptions} onValueChange={(c) => onChange({ ...value, appCategory: c })} testID={testID ? `${testID}-category-select` : undefined}/>
|
|
120
|
+
</Card>
|
|
121
|
+
|
|
51
122
|
{/* Section: Primary Color */}
|
|
52
123
|
<Card title="Primary color" description="Set the seed color for your theme palette.">
|
|
53
124
|
<Stack gap="m">
|
|
@@ -81,6 +152,26 @@ function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeCh
|
|
|
81
152
|
{/* Section: Preview */}
|
|
82
153
|
<Card title="Preview" description="A quick look at how your theme renders common controls.">
|
|
83
154
|
<Stack gap="m">
|
|
155
|
+
<Stack gap="xs">
|
|
156
|
+
<Text variant="label">Name</Text>
|
|
157
|
+
<Text>{value.name}</Text>
|
|
158
|
+
</Stack>
|
|
159
|
+
<Stack gap="xs">
|
|
160
|
+
<Text variant="label">Category</Text>
|
|
161
|
+
<Text>{formatAppCategoryLabel(value.appCategory)}</Text>
|
|
162
|
+
</Stack>
|
|
163
|
+
<Stack gap="xs">
|
|
164
|
+
<Text variant="label">Primary color</Text>
|
|
165
|
+
<Text tone="muted" variant="bodySmall">
|
|
166
|
+
{value.primaryColor}
|
|
167
|
+
</Text>
|
|
168
|
+
</Stack>
|
|
169
|
+
<Stack gap="xs">
|
|
170
|
+
<Text variant="label">Harmony</Text>
|
|
171
|
+
<Text tone="muted" variant="bodySmall">
|
|
172
|
+
{value.harmony}
|
|
173
|
+
</Text>
|
|
174
|
+
</Stack>
|
|
84
175
|
<Heading level={4}>Heading</Heading>
|
|
85
176
|
<Text>Body text — this shows default text color and weight.</Text>
|
|
86
177
|
<Text tone="muted" variant="bodySmall">
|
|
@@ -114,7 +205,7 @@ function ThemeComposerInner({ themeId: _themeId, value, onChange, mode, onModeCh
|
|
|
114
205
|
</Card>
|
|
115
206
|
|
|
116
207
|
{/* Submit */}
|
|
117
|
-
{onSubmit ? (<Button tone="primary" emphasis="solid" onPress={
|
|
208
|
+
{onSubmit ? (<Button tone="primary" emphasis="solid" onPress={handleSubmit} testID={testID ? `${testID}-submit` : undefined}>
|
|
118
209
|
Apply theme
|
|
119
210
|
</Button>) : null}
|
|
120
211
|
</Stack>);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeComposer.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/ThemeComposer.tsx"],"names":[],"mappings":"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","sourcesContent":["import { COLOR_HARMONIES, parseHexColorOrThrow } from '@ankhorage/color-theory';\nimport { 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 type { ZoraThemeMode } from '../../theme/types';\nimport { useZoraTheme } from '../../theme/useZoraTheme';\nimport { withZoraThemeScope } from '../../theme/withZoraThemeScope';\nimport type { ThemeComposerProps } from './types';\n\nfunction isValidHex(value: string): boolean {\n try {\n parseHexColorOrThrow(value);\n return true;\n } catch {\n return false;\n }\n}\n\nconst HARMONY_OPTIONS = COLOR_HARMONIES.map((h) => ({ value: h, label: h }));\n\nconst MODE_TABS = [\n { value: 'light' as ZoraThemeMode, label: 'Light' },\n { value: 'dark' as ZoraThemeMode, label: 'Dark' },\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 });\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: 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 { COLOR_HARMONIES, parseHexColorOrThrow } from '@ankhorage/color-theory';\nimport type { AppCategory } from '@ankhorage/contracts';\nimport { APP_CATEGORIES } from '@ankhorage/contracts';\nimport { 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 type { ZoraThemeMode } from '../../theme/types';\nimport { useZoraTheme } from '../../theme/useZoraTheme';\nimport { withZoraThemeScope } from '../../theme/withZoraThemeScope';\nimport type { ThemeComposerProps } from './types';\n\nconst HEX_ERROR_MESSAGE = 'Enter a valid 6-digit hex color (e.g. #0f766e).';\nconst NAME_ERROR_MESSAGE = 'Theme name cannot be empty.';\n\nfunction isValidHex(value: string): boolean {\n try {\n parseHexColorOrThrow(value);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction formatAppCategoryLabel(category: AppCategory): string {\n return category\n .split('_')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ');\n}\n\nconst HARMONY_OPTIONS = COLOR_HARMONIES.map((h) => ({ value: h, label: h }));\n\nconst MODE_TABS = [\n { value: 'light' as ZoraThemeMode, label: 'Light' },\n { value: 'dark' as ZoraThemeMode, label: 'Dark' },\n];\n\nfunction ThemeComposerInner({\n themeId: _themeId,\n value,\n onChange,\n mode,\n onModeChange,\n onSubmit,\n appCategories,\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 const [nameInput, setNameInput] = React.useState<string>(value.name);\n const [nameError, setNameError] = React.useState<string | undefined>(undefined);\n\n // Keep local inputs in sync when value changes externally\n React.useEffect(() => {\n setHexInput(value.primaryColor);\n setHexError(undefined);\n }, [value.primaryColor]);\n\n React.useEffect(() => {\n setNameInput(value.name);\n setNameError(undefined);\n }, [value.name]);\n\n function handleNameChange(text: string) {\n setNameInput(text);\n if (text.trim().length === 0) {\n setNameError(NAME_ERROR_MESSAGE);\n } else {\n setNameError(undefined);\n onChange({ ...value, name: text });\n }\n }\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 });\n } else {\n setHexError(HEX_ERROR_MESSAGE);\n }\n }\n\n function handleSubmit() {\n const hasValidName = nameInput.trim().length > 0;\n const hasValidHex = isValidHex(hexInput);\n\n if (!hasValidName) {\n setNameError(NAME_ERROR_MESSAGE);\n }\n\n if (!hasValidHex) {\n setHexError(HEX_ERROR_MESSAGE);\n }\n\n if (!hasValidName || !hasValidHex) {\n return;\n }\n\n onSubmit?.({\n ...value,\n name: nameInput,\n primaryColor: hexInput,\n });\n }\n\n const activeMode = mode ?? 'light';\n const categoryOptions = (appCategories ?? APP_CATEGORIES).map((c) => ({\n value: c,\n label: formatAppCategoryLabel(c),\n }));\n\n return (\n <Stack gap=\"l\" testID={testID}>\n {/* Section: Theme identity */}\n <Card\n title=\"Theme identity\"\n description=\"Name your theme. The ID is assigned automatically and shown for reference.\"\n >\n <Stack gap=\"m\">\n <Stack gap=\"xs\">\n <Text variant=\"label\">Name</Text>\n <Input\n value={nameInput}\n onChangeText={handleNameChange}\n placeholder=\"My theme\"\n autoCorrect={false}\n invalid={nameError !== undefined}\n testID={testID ? `${testID}-name-input` : undefined}\n />\n {nameError ? (\n <Text tone=\"danger\" variant=\"bodySmall\">\n {nameError}\n </Text>\n ) : null}\n </Stack>\n <Stack gap=\"xs\">\n <Text variant=\"label\">ID</Text>\n <Text\n tone=\"muted\"\n variant=\"bodySmall\"\n testID={testID ? `${testID}-id-display` : undefined}\n >\n {value.id}\n </Text>\n </Stack>\n </Stack>\n </Card>\n\n {/* Section: App category */}\n <Card title=\"App category\" description=\"Choose the category that best describes this app.\">\n <Select\n value={value.appCategory}\n options={categoryOptions}\n onValueChange={(c) => onChange({ ...value, appCategory: c })}\n testID={testID ? `${testID}-category-select` : undefined}\n />\n </Card>\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: 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 <Stack gap=\"xs\">\n <Text variant=\"label\">Name</Text>\n <Text>{value.name}</Text>\n </Stack>\n <Stack gap=\"xs\">\n <Text variant=\"label\">Category</Text>\n <Text>{formatAppCategoryLabel(value.appCategory)}</Text>\n </Stack>\n <Stack gap=\"xs\">\n <Text variant=\"label\">Primary color</Text>\n <Text tone=\"muted\" variant=\"bodySmall\">\n {value.primaryColor}\n </Text>\n </Stack>\n <Stack gap=\"xs\">\n <Text variant=\"label\">Harmony</Text>\n <Text tone=\"muted\" variant=\"bodySmall\">\n {value.harmony}\n </Text>\n </Stack>\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={handleSubmit}\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,3 +1,4 @@
|
|
|
1
|
+
import type { AppCategory } from '@ankhorage/contracts';
|
|
1
2
|
import type { ZoraTheme, ZoraThemeMode } from '../../theme/types';
|
|
2
3
|
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
3
4
|
export interface ThemeComposerProps extends ZoraBaseProps {
|
|
@@ -6,5 +7,6 @@ export interface ThemeComposerProps extends ZoraBaseProps {
|
|
|
6
7
|
mode?: ZoraThemeMode;
|
|
7
8
|
onModeChange?: (mode: ZoraThemeMode) => void;
|
|
8
9
|
onSubmit?: (theme: ZoraTheme) => void;
|
|
10
|
+
appCategories?: readonly AppCategory[];
|
|
9
11
|
}
|
|
10
12
|
//# 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;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,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;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,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,aAAa,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACxC"}
|
|
@@ -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 { AppCategory } from '@ankhorage/contracts';\n\nimport 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 appCategories?: readonly AppCategory[];\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
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": {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { COLOR_HARMONIES } from '@ankhorage/color-theory';
|
|
1
|
+
import { COLOR_HARMONIES, parseHexColorOrThrow } from '@ankhorage/color-theory';
|
|
2
|
+
import { APP_CATEGORIES } from '@ankhorage/contracts';
|
|
2
3
|
import { describe, expect, test } from 'bun:test';
|
|
3
4
|
|
|
4
5
|
import { zoraDefaultTheme } from '../../theme/zoraDefaultTheme';
|
|
5
6
|
import type { ThemeComposerProps } from './types';
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// ThemeComposerProps shape tests
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
8
12
|
describe('ThemeComposerProps', () => {
|
|
9
13
|
test('accepts a valid ZoraTheme as value', () => {
|
|
10
14
|
const props: ThemeComposerProps = {
|
|
@@ -39,8 +43,22 @@ describe('ThemeComposerProps', () => {
|
|
|
39
43
|
props.onSubmit?.(zoraDefaultTheme);
|
|
40
44
|
expect(submitted).toBe(true);
|
|
41
45
|
});
|
|
46
|
+
|
|
47
|
+
test('accepts optional appCategories as option list override', () => {
|
|
48
|
+
const narrow = ['developer_tools', 'utilities_tools'] as const;
|
|
49
|
+
const props: ThemeComposerProps = {
|
|
50
|
+
value: zoraDefaultTheme,
|
|
51
|
+
onChange: () => undefined,
|
|
52
|
+
appCategories: narrow,
|
|
53
|
+
};
|
|
54
|
+
expect(props.appCategories).toEqual(narrow);
|
|
55
|
+
});
|
|
42
56
|
});
|
|
43
57
|
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// COLOR_HARMONIES coverage
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
44
62
|
describe('COLOR_HARMONIES coverage for ThemeComposer', () => {
|
|
45
63
|
test('all canonical harmony values are present for the Select options', () => {
|
|
46
64
|
const expected = [
|
|
@@ -55,6 +73,42 @@ describe('COLOR_HARMONIES coverage for ThemeComposer', () => {
|
|
|
55
73
|
});
|
|
56
74
|
});
|
|
57
75
|
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// APP_CATEGORIES default behavior
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe('APP_CATEGORIES defaults', () => {
|
|
81
|
+
test('APP_CATEGORIES from contracts contains expected entries', () => {
|
|
82
|
+
expect(APP_CATEGORIES).toContain('developer_tools');
|
|
83
|
+
expect(APP_CATEGORIES).toContain('utilities_tools');
|
|
84
|
+
expect(APP_CATEGORIES).toContain('lifestyle');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('ThemeComposer appCategories prop is optional — falls back to APP_CATEGORIES', () => {
|
|
88
|
+
const props: ThemeComposerProps = {
|
|
89
|
+
value: zoraDefaultTheme,
|
|
90
|
+
onChange: () => undefined,
|
|
91
|
+
};
|
|
92
|
+
// No appCategories provided — component uses APP_CATEGORIES internally.
|
|
93
|
+
expect(props.appCategories).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('ThemeComposer appCategories narrows the available options when provided', () => {
|
|
97
|
+
const narrow = ['developer_tools', 'utilities_tools'] as const;
|
|
98
|
+
const props: ThemeComposerProps = {
|
|
99
|
+
value: zoraDefaultTheme,
|
|
100
|
+
onChange: () => undefined,
|
|
101
|
+
appCategories: narrow,
|
|
102
|
+
};
|
|
103
|
+
expect(props.appCategories).toHaveLength(2);
|
|
104
|
+
expect(props.appCategories).toContain('developer_tools');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// onChange propagation contracts
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
58
112
|
describe('onChange propagation contract', () => {
|
|
59
113
|
test('onChange receives updated theme with new harmony', () => {
|
|
60
114
|
const received: (typeof zoraDefaultTheme)[] = [];
|
|
@@ -67,4 +121,96 @@ describe('onChange propagation contract', () => {
|
|
|
67
121
|
expect(received).toHaveLength(1);
|
|
68
122
|
expect(received[0]?.harmony).toBe('triadic');
|
|
69
123
|
});
|
|
124
|
+
|
|
125
|
+
test('onChange receives updated theme with new appCategory', () => {
|
|
126
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
127
|
+
const props: ThemeComposerProps = {
|
|
128
|
+
value: zoraDefaultTheme,
|
|
129
|
+
onChange: (t) => received.push(t),
|
|
130
|
+
};
|
|
131
|
+
props.onChange({ ...zoraDefaultTheme, appCategory: 'lifestyle' });
|
|
132
|
+
expect(received).toHaveLength(1);
|
|
133
|
+
expect(received[0]?.appCategory).toBe('lifestyle');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('onChange receives updated theme with new valid name', () => {
|
|
137
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
138
|
+
const props: ThemeComposerProps = {
|
|
139
|
+
value: zoraDefaultTheme,
|
|
140
|
+
onChange: (t) => received.push(t),
|
|
141
|
+
};
|
|
142
|
+
props.onChange({ ...zoraDefaultTheme, name: 'My Custom Theme' });
|
|
143
|
+
expect(received).toHaveLength(1);
|
|
144
|
+
expect(received[0]?.name).toBe('My Custom Theme');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('onChange is not called with an empty name', () => {
|
|
148
|
+
// Simulate the component's name validation logic
|
|
149
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
150
|
+
const onChange = (t: typeof zoraDefaultTheme) => received.push(t);
|
|
151
|
+
|
|
152
|
+
const newName = '';
|
|
153
|
+
if (newName.trim().length > 0) {
|
|
154
|
+
onChange({ ...zoraDefaultTheme, name: newName });
|
|
155
|
+
}
|
|
156
|
+
// Empty name should not propagate
|
|
157
|
+
expect(received).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('onChange is not called with invalid primary color', () => {
|
|
161
|
+
// Simulate the component's hex validation logic
|
|
162
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
163
|
+
const onChange = (t: typeof zoraDefaultTheme) => received.push(t);
|
|
164
|
+
|
|
165
|
+
const badColor = '#xyz';
|
|
166
|
+
try {
|
|
167
|
+
parseHexColorOrThrow(badColor);
|
|
168
|
+
// Should not reach here for an invalid color
|
|
169
|
+
onChange({ ...zoraDefaultTheme, primaryColor: badColor });
|
|
170
|
+
} catch {
|
|
171
|
+
// Expected: invalid color — do not call onChange
|
|
172
|
+
}
|
|
173
|
+
expect(received).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('onChange is called with a valid primary color', () => {
|
|
177
|
+
const received: (typeof zoraDefaultTheme)[] = [];
|
|
178
|
+
const onChange = (t: typeof zoraDefaultTheme) => received.push(t);
|
|
179
|
+
|
|
180
|
+
const goodColor = '#c084fc';
|
|
181
|
+
try {
|
|
182
|
+
parseHexColorOrThrow(goodColor);
|
|
183
|
+
onChange({ ...zoraDefaultTheme, primaryColor: goodColor });
|
|
184
|
+
} catch {
|
|
185
|
+
// Should not reach here for a valid color
|
|
186
|
+
}
|
|
187
|
+
expect(received).toHaveLength(1);
|
|
188
|
+
expect(received[0]?.primaryColor).toBe(goodColor);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Legacy API absence checks
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe('No legacy APIs', () => {
|
|
197
|
+
test('ThemeComposerProps value carries no colorTone field', () => {
|
|
198
|
+
const theme = zoraDefaultTheme;
|
|
199
|
+
expect('colorTone' in theme).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('ThemeComposerProps value carries no appMood field', () => {
|
|
203
|
+
const theme = zoraDefaultTheme;
|
|
204
|
+
expect('appMood' in theme).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('value.name is required — zoraDefaultTheme.name is a non-empty string', () => {
|
|
208
|
+
expect(typeof zoraDefaultTheme.name).toBe('string');
|
|
209
|
+
expect(zoraDefaultTheme.name.length).toBeGreaterThan(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('value.appCategory is required — zoraDefaultTheme.appCategory is set', () => {
|
|
213
|
+
expect(typeof zoraDefaultTheme.appCategory).toBe('string');
|
|
214
|
+
expect(zoraDefaultTheme.appCategory.length).toBeGreaterThan(0);
|
|
215
|
+
});
|
|
70
216
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { COLOR_HARMONIES, parseHexColorOrThrow } from '@ankhorage/color-theory';
|
|
2
|
+
import type { AppCategory } from '@ankhorage/contracts';
|
|
3
|
+
import { APP_CATEGORIES } from '@ankhorage/contracts';
|
|
2
4
|
import { Box, Stack } from '@ankhorage/surface';
|
|
3
5
|
import React from 'react';
|
|
4
6
|
|
|
@@ -15,6 +17,9 @@ import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
|
15
17
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
16
18
|
import type { ThemeComposerProps } from './types';
|
|
17
19
|
|
|
20
|
+
const HEX_ERROR_MESSAGE = 'Enter a valid 6-digit hex color (e.g. #0f766e).';
|
|
21
|
+
const NAME_ERROR_MESSAGE = 'Theme name cannot be empty.';
|
|
22
|
+
|
|
18
23
|
function isValidHex(value: string): boolean {
|
|
19
24
|
try {
|
|
20
25
|
parseHexColorOrThrow(value);
|
|
@@ -24,6 +29,13 @@ function isValidHex(value: string): boolean {
|
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
function formatAppCategoryLabel(category: AppCategory): string {
|
|
33
|
+
return category
|
|
34
|
+
.split('_')
|
|
35
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
36
|
+
.join(' ');
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
const HARMONY_OPTIONS = COLOR_HARMONIES.map((h) => ({ value: h, label: h }));
|
|
28
40
|
|
|
29
41
|
const MODE_TABS = [
|
|
@@ -38,6 +50,7 @@ function ThemeComposerInner({
|
|
|
38
50
|
mode,
|
|
39
51
|
onModeChange,
|
|
40
52
|
onSubmit,
|
|
53
|
+
appCategories,
|
|
41
54
|
testID,
|
|
42
55
|
}: ThemeComposerProps) {
|
|
43
56
|
const { theme } = useZoraTheme();
|
|
@@ -45,12 +58,30 @@ function ThemeComposerInner({
|
|
|
45
58
|
const [hexInput, setHexInput] = React.useState<string>(value.primaryColor);
|
|
46
59
|
const [hexError, setHexError] = React.useState<string | undefined>(undefined);
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
const [nameInput, setNameInput] = React.useState<string>(value.name);
|
|
62
|
+
const [nameError, setNameError] = React.useState<string | undefined>(undefined);
|
|
63
|
+
|
|
64
|
+
// Keep local inputs in sync when value changes externally
|
|
49
65
|
React.useEffect(() => {
|
|
50
66
|
setHexInput(value.primaryColor);
|
|
51
67
|
setHexError(undefined);
|
|
52
68
|
}, [value.primaryColor]);
|
|
53
69
|
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
setNameInput(value.name);
|
|
72
|
+
setNameError(undefined);
|
|
73
|
+
}, [value.name]);
|
|
74
|
+
|
|
75
|
+
function handleNameChange(text: string) {
|
|
76
|
+
setNameInput(text);
|
|
77
|
+
if (text.trim().length === 0) {
|
|
78
|
+
setNameError(NAME_ERROR_MESSAGE);
|
|
79
|
+
} else {
|
|
80
|
+
setNameError(undefined);
|
|
81
|
+
onChange({ ...value, name: text });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
function handleHexChange(text: string) {
|
|
55
86
|
// Ensure leading hash
|
|
56
87
|
const normalized = text.startsWith('#') ? text : `#${text}`;
|
|
@@ -60,14 +91,86 @@ function ThemeComposerInner({
|
|
|
60
91
|
setHexError(undefined);
|
|
61
92
|
onChange({ ...value, primaryColor: normalized });
|
|
62
93
|
} else {
|
|
63
|
-
setHexError(
|
|
94
|
+
setHexError(HEX_ERROR_MESSAGE);
|
|
64
95
|
}
|
|
65
96
|
}
|
|
66
97
|
|
|
98
|
+
function handleSubmit() {
|
|
99
|
+
const hasValidName = nameInput.trim().length > 0;
|
|
100
|
+
const hasValidHex = isValidHex(hexInput);
|
|
101
|
+
|
|
102
|
+
if (!hasValidName) {
|
|
103
|
+
setNameError(NAME_ERROR_MESSAGE);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!hasValidHex) {
|
|
107
|
+
setHexError(HEX_ERROR_MESSAGE);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!hasValidName || !hasValidHex) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onSubmit?.({
|
|
115
|
+
...value,
|
|
116
|
+
name: nameInput,
|
|
117
|
+
primaryColor: hexInput,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
67
121
|
const activeMode = mode ?? 'light';
|
|
122
|
+
const categoryOptions = (appCategories ?? APP_CATEGORIES).map((c) => ({
|
|
123
|
+
value: c,
|
|
124
|
+
label: formatAppCategoryLabel(c),
|
|
125
|
+
}));
|
|
68
126
|
|
|
69
127
|
return (
|
|
70
128
|
<Stack gap="l" testID={testID}>
|
|
129
|
+
{/* Section: Theme identity */}
|
|
130
|
+
<Card
|
|
131
|
+
title="Theme identity"
|
|
132
|
+
description="Name your theme. The ID is assigned automatically and shown for reference."
|
|
133
|
+
>
|
|
134
|
+
<Stack gap="m">
|
|
135
|
+
<Stack gap="xs">
|
|
136
|
+
<Text variant="label">Name</Text>
|
|
137
|
+
<Input
|
|
138
|
+
value={nameInput}
|
|
139
|
+
onChangeText={handleNameChange}
|
|
140
|
+
placeholder="My theme"
|
|
141
|
+
autoCorrect={false}
|
|
142
|
+
invalid={nameError !== undefined}
|
|
143
|
+
testID={testID ? `${testID}-name-input` : undefined}
|
|
144
|
+
/>
|
|
145
|
+
{nameError ? (
|
|
146
|
+
<Text tone="danger" variant="bodySmall">
|
|
147
|
+
{nameError}
|
|
148
|
+
</Text>
|
|
149
|
+
) : null}
|
|
150
|
+
</Stack>
|
|
151
|
+
<Stack gap="xs">
|
|
152
|
+
<Text variant="label">ID</Text>
|
|
153
|
+
<Text
|
|
154
|
+
tone="muted"
|
|
155
|
+
variant="bodySmall"
|
|
156
|
+
testID={testID ? `${testID}-id-display` : undefined}
|
|
157
|
+
>
|
|
158
|
+
{value.id}
|
|
159
|
+
</Text>
|
|
160
|
+
</Stack>
|
|
161
|
+
</Stack>
|
|
162
|
+
</Card>
|
|
163
|
+
|
|
164
|
+
{/* Section: App category */}
|
|
165
|
+
<Card title="App category" description="Choose the category that best describes this app.">
|
|
166
|
+
<Select
|
|
167
|
+
value={value.appCategory}
|
|
168
|
+
options={categoryOptions}
|
|
169
|
+
onValueChange={(c) => onChange({ ...value, appCategory: c })}
|
|
170
|
+
testID={testID ? `${testID}-category-select` : undefined}
|
|
171
|
+
/>
|
|
172
|
+
</Card>
|
|
173
|
+
|
|
71
174
|
{/* Section: Primary Color */}
|
|
72
175
|
<Card title="Primary color" description="Set the seed color for your theme palette.">
|
|
73
176
|
<Stack gap="m">
|
|
@@ -131,6 +234,26 @@ function ThemeComposerInner({
|
|
|
131
234
|
{/* Section: Preview */}
|
|
132
235
|
<Card title="Preview" description="A quick look at how your theme renders common controls.">
|
|
133
236
|
<Stack gap="m">
|
|
237
|
+
<Stack gap="xs">
|
|
238
|
+
<Text variant="label">Name</Text>
|
|
239
|
+
<Text>{value.name}</Text>
|
|
240
|
+
</Stack>
|
|
241
|
+
<Stack gap="xs">
|
|
242
|
+
<Text variant="label">Category</Text>
|
|
243
|
+
<Text>{formatAppCategoryLabel(value.appCategory)}</Text>
|
|
244
|
+
</Stack>
|
|
245
|
+
<Stack gap="xs">
|
|
246
|
+
<Text variant="label">Primary color</Text>
|
|
247
|
+
<Text tone="muted" variant="bodySmall">
|
|
248
|
+
{value.primaryColor}
|
|
249
|
+
</Text>
|
|
250
|
+
</Stack>
|
|
251
|
+
<Stack gap="xs">
|
|
252
|
+
<Text variant="label">Harmony</Text>
|
|
253
|
+
<Text tone="muted" variant="bodySmall">
|
|
254
|
+
{value.harmony}
|
|
255
|
+
</Text>
|
|
256
|
+
</Stack>
|
|
134
257
|
<Heading level={4}>Heading</Heading>
|
|
135
258
|
<Text>Body text — this shows default text color and weight.</Text>
|
|
136
259
|
<Text tone="muted" variant="bodySmall">
|
|
@@ -173,7 +296,7 @@ function ThemeComposerInner({
|
|
|
173
296
|
<Button
|
|
174
297
|
tone="primary"
|
|
175
298
|
emphasis="solid"
|
|
176
|
-
onPress={
|
|
299
|
+
onPress={handleSubmit}
|
|
177
300
|
testID={testID ? `${testID}-submit` : undefined}
|
|
178
301
|
>
|
|
179
302
|
Apply theme
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AppCategory } from '@ankhorage/contracts';
|
|
2
|
+
|
|
1
3
|
import type { ZoraTheme, ZoraThemeMode } from '../../theme/types';
|
|
2
4
|
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
3
5
|
|
|
@@ -7,4 +9,5 @@ export interface ThemeComposerProps extends ZoraBaseProps {
|
|
|
7
9
|
mode?: ZoraThemeMode;
|
|
8
10
|
onModeChange?: (mode: ZoraThemeMode) => void;
|
|
9
11
|
onSubmit?: (theme: ZoraTheme) => void;
|
|
12
|
+
appCategories?: readonly AppCategory[];
|
|
10
13
|
}
|