@ankhorage/zora 1.0.0 → 1.0.2
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 +34 -0
- package/README.md +16 -9
- package/dist/components/card/Card.d.ts.map +1 -1
- package/dist/components/card/Card.js +2 -1
- package/dist/components/card/Card.js.map +1 -1
- package/dist/components/checkbox/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/checkbox/CheckboxGroup.js +2 -1
- package/dist/components/checkbox/CheckboxGroup.js.map +1 -1
- package/dist/components/drawer/Drawer.d.ts.map +1 -1
- package/dist/components/drawer/Drawer.js +2 -1
- package/dist/components/drawer/Drawer.js.map +1 -1
- package/dist/components/form/Form.d.ts.map +1 -1
- package/dist/components/form/Form.js +1 -1
- package/dist/components/form/Form.js.map +1 -1
- package/dist/components/form/FormActions.d.ts.map +1 -1
- package/dist/components/form/FormActions.js +1 -1
- package/dist/components/form/FormActions.js.map +1 -1
- package/dist/components/form/FormError.d.ts.map +1 -1
- package/dist/components/form/FormError.js +1 -1
- package/dist/components/form/FormError.js.map +1 -1
- package/dist/components/form/FormField.d.ts.map +1 -1
- package/dist/components/form/FormField.js +2 -1
- package/dist/components/form/FormField.js.map +1 -1
- package/dist/components/modal/Modal.d.ts.map +1 -1
- package/dist/components/modal/Modal.js +2 -1
- package/dist/components/modal/Modal.js.map +1 -1
- package/dist/components/radio/RadioGroup.d.ts.map +1 -1
- package/dist/components/radio/RadioGroup.js +2 -1
- package/dist/components/radio/RadioGroup.js.map +1 -1
- package/dist/components/select/Select.d.ts.map +1 -1
- package/dist/components/select/Select.js +1 -1
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/tabs/Tabs.d.ts.map +1 -1
- package/dist/components/tabs/Tabs.js +1 -1
- package/dist/components/tabs/Tabs.js.map +1 -1
- package/dist/components/toolbar/Toolbar.d.ts.map +1 -1
- package/dist/components/toolbar/Toolbar.js +1 -1
- package/dist/components/toolbar/Toolbar.js.map +1 -1
- package/dist/layout/app-shell/AppShell.d.ts.map +1 -1
- package/dist/layout/app-shell/AppShell.js +1 -1
- package/dist/layout/app-shell/AppShell.js.map +1 -1
- package/dist/layout/auth-layout/AuthLayout.d.ts.map +1 -1
- package/dist/layout/auth-layout/AuthLayout.js +1 -1
- package/dist/layout/auth-layout/AuthLayout.js.map +1 -1
- package/dist/layout/page/Page.d.ts.map +1 -1
- package/dist/layout/page/Page.js +1 -1
- package/dist/layout/page/Page.js.map +1 -1
- package/dist/layout/page-header/PageHeader.d.ts.map +1 -1
- package/dist/layout/page-header/PageHeader.js +1 -1
- package/dist/layout/page-header/PageHeader.js.map +1 -1
- package/dist/layout/page-section/PageSection.d.ts.map +1 -1
- package/dist/layout/page-section/PageSection.js +1 -1
- package/dist/layout/page-section/PageSection.js.map +1 -1
- package/dist/layout/sidebar-layout/SidebarLayout.d.ts.map +1 -1
- package/dist/layout/sidebar-layout/SidebarLayout.js +1 -1
- package/dist/layout/sidebar-layout/SidebarLayout.js.map +1 -1
- package/dist/layout/topbar-layout/TopbarLayout.d.ts.map +1 -1
- package/dist/layout/topbar-layout/TopbarLayout.js +1 -1
- package/dist/layout/topbar-layout/TopbarLayout.js.map +1 -1
- package/dist/patterns/auth/ForgotPasswordForm.d.ts.map +1 -1
- package/dist/patterns/auth/ForgotPasswordForm.js +1 -1
- package/dist/patterns/auth/ForgotPasswordForm.js.map +1 -1
- package/dist/patterns/auth/OtpForm.d.ts.map +1 -1
- package/dist/patterns/auth/OtpForm.js +1 -1
- package/dist/patterns/auth/OtpForm.js.map +1 -1
- package/dist/patterns/auth/SignInForm.d.ts.map +1 -1
- package/dist/patterns/auth/SignInForm.js +1 -1
- package/dist/patterns/auth/SignInForm.js.map +1 -1
- package/dist/patterns/auth/SignUpForm.d.ts.map +1 -1
- package/dist/patterns/auth/SignUpForm.js +1 -1
- package/dist/patterns/auth/SignUpForm.js.map +1 -1
- package/dist/patterns/collection-editor/CollectionEditor.d.ts.map +1 -1
- package/dist/patterns/collection-editor/CollectionEditor.js +1 -1
- package/dist/patterns/collection-editor/CollectionEditor.js.map +1 -1
- package/dist/patterns/confirm-dialog/ConfirmDialog.d.ts.map +1 -1
- package/dist/patterns/confirm-dialog/ConfirmDialog.js +1 -1
- package/dist/patterns/confirm-dialog/ConfirmDialog.js.map +1 -1
- package/dist/patterns/disclosure-section/DisclosureSection.d.ts.map +1 -1
- package/dist/patterns/disclosure-section/DisclosureSection.js +1 -1
- package/dist/patterns/disclosure-section/DisclosureSection.js.map +1 -1
- package/dist/patterns/empty-state/EmptyState.d.ts.map +1 -1
- package/dist/patterns/empty-state/EmptyState.js +1 -1
- package/dist/patterns/empty-state/EmptyState.js.map +1 -1
- package/dist/patterns/form-field/FormField.d.ts.map +1 -1
- package/dist/patterns/form-field/FormField.js +2 -1
- package/dist/patterns/form-field/FormField.js.map +1 -1
- package/dist/patterns/inspector-field/InspectorField.d.ts.map +1 -1
- package/dist/patterns/inspector-field/InspectorField.js +1 -1
- package/dist/patterns/inspector-field/InspectorField.js.map +1 -1
- package/dist/patterns/notice/Notice.d.ts.map +1 -1
- package/dist/patterns/notice/Notice.js +1 -1
- package/dist/patterns/notice/Notice.js.map +1 -1
- package/dist/patterns/section-header/SectionHeader.d.ts.map +1 -1
- package/dist/patterns/section-header/SectionHeader.js +1 -1
- package/dist/patterns/section-header/SectionHeader.js.map +1 -1
- package/dist/patterns/settings-row/SettingsRow.d.ts.map +1 -1
- package/dist/patterns/settings-row/SettingsRow.js +1 -1
- package/dist/patterns/settings-row/SettingsRow.js.map +1 -1
- package/dist/patterns/theme-composer/ThemeComposer.d.ts.map +1 -1
- package/dist/patterns/theme-composer/ThemeComposer.js +98 -6
- 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/dist/patterns/tile-grid/PaletteItem.d.ts.map +1 -1
- package/dist/patterns/tile-grid/PaletteItem.js +1 -1
- package/dist/patterns/tile-grid/PaletteItem.js.map +1 -1
- package/dist/patterns/tile-grid/TileGrid.d.ts.map +1 -1
- package/dist/patterns/tile-grid/TileGrid.js +1 -1
- package/dist/patterns/tile-grid/TileGrid.js.map +1 -1
- package/dist/patterns/tree-view/TreeItem.d.ts.map +1 -1
- package/dist/patterns/tree-view/TreeItem.js +1 -1
- package/dist/patterns/tree-view/TreeItem.js.map +1 -1
- package/dist/patterns/tree-view/TreeView.d.ts.map +1 -1
- package/dist/patterns/tree-view/TreeView.js +1 -1
- package/dist/patterns/tree-view/TreeView.js.map +1 -1
- package/package.json +1 -1
- package/src/audit.test.ts +242 -0
- package/src/components/card/Card.tsx +2 -1
- package/src/components/checkbox/CheckboxGroup.tsx +2 -1
- package/src/components/drawer/Drawer.tsx +2 -1
- package/src/components/form/Form.tsx +1 -1
- package/src/components/form/FormActions.tsx +1 -1
- package/src/components/form/FormError.tsx +1 -1
- package/src/components/form/FormField.tsx +2 -1
- package/src/components/modal/Modal.tsx +2 -1
- package/src/components/radio/RadioGroup.tsx +2 -1
- package/src/components/select/Select.tsx +1 -1
- package/src/components/tabs/Tabs.tsx +1 -1
- package/src/components/toolbar/Toolbar.tsx +1 -1
- package/src/layout/app-shell/AppShell.tsx +1 -1
- package/src/layout/auth-layout/AuthLayout.tsx +1 -1
- package/src/layout/page/Page.tsx +1 -1
- package/src/layout/page-header/PageHeader.tsx +1 -1
- package/src/layout/page-section/PageSection.tsx +1 -1
- package/src/layout/sidebar-layout/SidebarLayout.tsx +1 -1
- package/src/layout/topbar-layout/TopbarLayout.tsx +1 -1
- package/src/patterns/auth/ForgotPasswordForm.tsx +1 -1
- package/src/patterns/auth/OtpForm.tsx +1 -1
- package/src/patterns/auth/SignInForm.tsx +1 -1
- package/src/patterns/auth/SignUpForm.tsx +1 -1
- package/src/patterns/collection-editor/CollectionEditor.tsx +1 -1
- package/src/patterns/confirm-dialog/ConfirmDialog.tsx +1 -1
- package/src/patterns/disclosure-section/DisclosureSection.tsx +1 -1
- package/src/patterns/empty-state/EmptyState.tsx +1 -1
- package/src/patterns/form-field/FormField.tsx +2 -1
- package/src/patterns/inspector-field/InspectorField.tsx +1 -1
- package/src/patterns/notice/Notice.tsx +1 -1
- package/src/patterns/section-header/SectionHeader.tsx +1 -1
- package/src/patterns/settings-row/SettingsRow.tsx +1 -1
- package/src/patterns/theme-composer/ThemeComposer.test.ts +148 -2
- package/src/patterns/theme-composer/ThemeComposer.tsx +129 -5
- package/src/patterns/theme-composer/types.ts +3 -0
- package/src/patterns/tile-grid/PaletteItem.tsx +1 -1
- package/src/patterns/tile-grid/TileGrid.tsx +1 -1
- package/src/patterns/tree-view/TreeItem.tsx +1 -1
- package/src/patterns/tree-view/TreeView.tsx +1 -1
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { COLOR_HARMONIES, parseHexColorOrThrow } from '@ankhorage/color-theory';
|
|
2
|
-
import {
|
|
2
|
+
import type { AppCategory } from '@ankhorage/contracts';
|
|
3
|
+
import { APP_CATEGORIES } from '@ankhorage/contracts';
|
|
3
4
|
import React from 'react';
|
|
4
5
|
|
|
5
6
|
import { Badge } from '../../components/badge';
|
|
@@ -10,11 +11,16 @@ import { Input } from '../../components/input';
|
|
|
10
11
|
import { Select } from '../../components/select';
|
|
11
12
|
import { Tabs } from '../../components/tabs';
|
|
12
13
|
import { Text } from '../../components/text';
|
|
14
|
+
import { Box, Stack } from '../../foundation';
|
|
13
15
|
import type { ZoraThemeMode } from '../../theme/types';
|
|
14
16
|
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.';
|
|
21
|
+
const HEX_INPUT_PLACEHOLDER = 'Enter hex color';
|
|
22
|
+
const NAME_ERROR_MESSAGE = 'Theme name cannot be empty.';
|
|
23
|
+
|
|
18
24
|
function isValidHex(value: string): boolean {
|
|
19
25
|
try {
|
|
20
26
|
parseHexColorOrThrow(value);
|
|
@@ -24,6 +30,13 @@ function isValidHex(value: string): boolean {
|
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
function formatAppCategoryLabel(category: AppCategory): string {
|
|
34
|
+
return category
|
|
35
|
+
.split('_')
|
|
36
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
37
|
+
.join(' ');
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
const HARMONY_OPTIONS = COLOR_HARMONIES.map((h) => ({ value: h, label: h }));
|
|
28
41
|
|
|
29
42
|
const MODE_TABS = [
|
|
@@ -38,6 +51,7 @@ function ThemeComposerInner({
|
|
|
38
51
|
mode,
|
|
39
52
|
onModeChange,
|
|
40
53
|
onSubmit,
|
|
54
|
+
appCategories,
|
|
41
55
|
testID,
|
|
42
56
|
}: ThemeComposerProps) {
|
|
43
57
|
const { theme } = useZoraTheme();
|
|
@@ -45,12 +59,30 @@ function ThemeComposerInner({
|
|
|
45
59
|
const [hexInput, setHexInput] = React.useState<string>(value.primaryColor);
|
|
46
60
|
const [hexError, setHexError] = React.useState<string | undefined>(undefined);
|
|
47
61
|
|
|
48
|
-
|
|
62
|
+
const [nameInput, setNameInput] = React.useState<string>(value.name);
|
|
63
|
+
const [nameError, setNameError] = React.useState<string | undefined>(undefined);
|
|
64
|
+
|
|
65
|
+
// Keep local inputs in sync when value changes externally
|
|
49
66
|
React.useEffect(() => {
|
|
50
67
|
setHexInput(value.primaryColor);
|
|
51
68
|
setHexError(undefined);
|
|
52
69
|
}, [value.primaryColor]);
|
|
53
70
|
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
setNameInput(value.name);
|
|
73
|
+
setNameError(undefined);
|
|
74
|
+
}, [value.name]);
|
|
75
|
+
|
|
76
|
+
function handleNameChange(text: string) {
|
|
77
|
+
setNameInput(text);
|
|
78
|
+
if (text.trim().length === 0) {
|
|
79
|
+
setNameError(NAME_ERROR_MESSAGE);
|
|
80
|
+
} else {
|
|
81
|
+
setNameError(undefined);
|
|
82
|
+
onChange({ ...value, name: text });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
function handleHexChange(text: string) {
|
|
55
87
|
// Ensure leading hash
|
|
56
88
|
const normalized = text.startsWith('#') ? text : `#${text}`;
|
|
@@ -60,14 +92,86 @@ function ThemeComposerInner({
|
|
|
60
92
|
setHexError(undefined);
|
|
61
93
|
onChange({ ...value, primaryColor: normalized });
|
|
62
94
|
} else {
|
|
63
|
-
setHexError(
|
|
95
|
+
setHexError(HEX_ERROR_MESSAGE);
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
function handleSubmit() {
|
|
100
|
+
const hasValidName = nameInput.trim().length > 0;
|
|
101
|
+
const hasValidHex = isValidHex(hexInput);
|
|
102
|
+
|
|
103
|
+
if (!hasValidName) {
|
|
104
|
+
setNameError(NAME_ERROR_MESSAGE);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!hasValidHex) {
|
|
108
|
+
setHexError(HEX_ERROR_MESSAGE);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!hasValidName || !hasValidHex) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onSubmit?.({
|
|
116
|
+
...value,
|
|
117
|
+
name: nameInput,
|
|
118
|
+
primaryColor: hexInput,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
67
122
|
const activeMode = mode ?? 'light';
|
|
123
|
+
const categoryOptions = (appCategories ?? APP_CATEGORIES).map((c) => ({
|
|
124
|
+
value: c,
|
|
125
|
+
label: formatAppCategoryLabel(c),
|
|
126
|
+
}));
|
|
68
127
|
|
|
69
128
|
return (
|
|
70
129
|
<Stack gap="l" testID={testID}>
|
|
130
|
+
{/* Section: Theme identity */}
|
|
131
|
+
<Card
|
|
132
|
+
title="Theme identity"
|
|
133
|
+
description="Name your theme. The ID is assigned automatically and shown for reference."
|
|
134
|
+
>
|
|
135
|
+
<Stack gap="m">
|
|
136
|
+
<Stack gap="xs">
|
|
137
|
+
<Text variant="label">Name</Text>
|
|
138
|
+
<Input
|
|
139
|
+
value={nameInput}
|
|
140
|
+
onChangeText={handleNameChange}
|
|
141
|
+
placeholder="My theme"
|
|
142
|
+
autoCorrect={false}
|
|
143
|
+
invalid={nameError !== undefined}
|
|
144
|
+
testID={testID ? `${testID}-name-input` : undefined}
|
|
145
|
+
/>
|
|
146
|
+
{nameError ? (
|
|
147
|
+
<Text tone="danger" variant="bodySmall">
|
|
148
|
+
{nameError}
|
|
149
|
+
</Text>
|
|
150
|
+
) : null}
|
|
151
|
+
</Stack>
|
|
152
|
+
<Stack gap="xs">
|
|
153
|
+
<Text variant="label">ID</Text>
|
|
154
|
+
<Text
|
|
155
|
+
tone="muted"
|
|
156
|
+
variant="bodySmall"
|
|
157
|
+
testID={testID ? `${testID}-id-display` : undefined}
|
|
158
|
+
>
|
|
159
|
+
{value.id}
|
|
160
|
+
</Text>
|
|
161
|
+
</Stack>
|
|
162
|
+
</Stack>
|
|
163
|
+
</Card>
|
|
164
|
+
|
|
165
|
+
{/* Section: App category */}
|
|
166
|
+
<Card title="App category" description="Choose the category that best describes this app.">
|
|
167
|
+
<Select
|
|
168
|
+
value={value.appCategory}
|
|
169
|
+
options={categoryOptions}
|
|
170
|
+
onValueChange={(c) => onChange({ ...value, appCategory: c })}
|
|
171
|
+
testID={testID ? `${testID}-category-select` : undefined}
|
|
172
|
+
/>
|
|
173
|
+
</Card>
|
|
174
|
+
|
|
71
175
|
{/* Section: Primary Color */}
|
|
72
176
|
<Card title="Primary color" description="Set the seed color for your theme palette.">
|
|
73
177
|
<Stack gap="m">
|
|
@@ -76,7 +180,7 @@ function ThemeComposerInner({
|
|
|
76
180
|
<Input
|
|
77
181
|
value={hexInput}
|
|
78
182
|
onChangeText={handleHexChange}
|
|
79
|
-
placeholder=
|
|
183
|
+
placeholder={HEX_INPUT_PLACEHOLDER}
|
|
80
184
|
autoCapitalize="none"
|
|
81
185
|
autoCorrect={false}
|
|
82
186
|
maxLength={7}
|
|
@@ -131,6 +235,26 @@ function ThemeComposerInner({
|
|
|
131
235
|
{/* Section: Preview */}
|
|
132
236
|
<Card title="Preview" description="A quick look at how your theme renders common controls.">
|
|
133
237
|
<Stack gap="m">
|
|
238
|
+
<Stack gap="xs">
|
|
239
|
+
<Text variant="label">Name</Text>
|
|
240
|
+
<Text>{value.name}</Text>
|
|
241
|
+
</Stack>
|
|
242
|
+
<Stack gap="xs">
|
|
243
|
+
<Text variant="label">Category</Text>
|
|
244
|
+
<Text>{formatAppCategoryLabel(value.appCategory)}</Text>
|
|
245
|
+
</Stack>
|
|
246
|
+
<Stack gap="xs">
|
|
247
|
+
<Text variant="label">Primary color</Text>
|
|
248
|
+
<Text tone="muted" variant="bodySmall">
|
|
249
|
+
{value.primaryColor}
|
|
250
|
+
</Text>
|
|
251
|
+
</Stack>
|
|
252
|
+
<Stack gap="xs">
|
|
253
|
+
<Text variant="label">Harmony</Text>
|
|
254
|
+
<Text tone="muted" variant="bodySmall">
|
|
255
|
+
{value.harmony}
|
|
256
|
+
</Text>
|
|
257
|
+
</Stack>
|
|
134
258
|
<Heading level={4}>Heading</Heading>
|
|
135
259
|
<Text>Body text — this shows default text color and weight.</Text>
|
|
136
260
|
<Text tone="muted" variant="bodySmall">
|
|
@@ -173,7 +297,7 @@ function ThemeComposerInner({
|
|
|
173
297
|
<Button
|
|
174
298
|
tone="primary"
|
|
175
299
|
emphasis="solid"
|
|
176
|
-
onPress={
|
|
300
|
+
onPress={handleSubmit}
|
|
177
301
|
testID={testID ? `${testID}-submit` : undefined}
|
|
178
302
|
>
|
|
179
303
|
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
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Box } from '@ankhorage/surface';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
|
|
4
3
|
import { Card } from '../../components/card';
|
|
5
4
|
import { Heading } from '../../components/heading';
|
|
6
5
|
import { Text } from '../../components/text';
|
|
6
|
+
import { Box } from '../../foundation';
|
|
7
7
|
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
8
8
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
9
9
|
import type { PaletteItemProps } from './types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Box, Stack } from '@ankhorage/surface';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
|
|
4
3
|
import { IconButton } from '../../components/icon-button';
|
|
4
|
+
import { Box, Stack } from '../../foundation';
|
|
5
5
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
6
6
|
import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
|
|
7
7
|
import { SettingsRow } from '../settings-row';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Stack } from '@ankhorage/surface';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
|
|
3
|
+
import { Stack } from '../../foundation';
|
|
4
4
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
5
5
|
import { TreeItem } from './TreeItem';
|
|
6
6
|
import type { TreeViewProps } from './types';
|