@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +16 -9
  3. package/dist/components/card/Card.d.ts.map +1 -1
  4. package/dist/components/card/Card.js +2 -1
  5. package/dist/components/card/Card.js.map +1 -1
  6. package/dist/components/checkbox/CheckboxGroup.d.ts.map +1 -1
  7. package/dist/components/checkbox/CheckboxGroup.js +2 -1
  8. package/dist/components/checkbox/CheckboxGroup.js.map +1 -1
  9. package/dist/components/drawer/Drawer.d.ts.map +1 -1
  10. package/dist/components/drawer/Drawer.js +2 -1
  11. package/dist/components/drawer/Drawer.js.map +1 -1
  12. package/dist/components/form/Form.d.ts.map +1 -1
  13. package/dist/components/form/Form.js +1 -1
  14. package/dist/components/form/Form.js.map +1 -1
  15. package/dist/components/form/FormActions.d.ts.map +1 -1
  16. package/dist/components/form/FormActions.js +1 -1
  17. package/dist/components/form/FormActions.js.map +1 -1
  18. package/dist/components/form/FormError.d.ts.map +1 -1
  19. package/dist/components/form/FormError.js +1 -1
  20. package/dist/components/form/FormError.js.map +1 -1
  21. package/dist/components/form/FormField.d.ts.map +1 -1
  22. package/dist/components/form/FormField.js +2 -1
  23. package/dist/components/form/FormField.js.map +1 -1
  24. package/dist/components/modal/Modal.d.ts.map +1 -1
  25. package/dist/components/modal/Modal.js +2 -1
  26. package/dist/components/modal/Modal.js.map +1 -1
  27. package/dist/components/radio/RadioGroup.d.ts.map +1 -1
  28. package/dist/components/radio/RadioGroup.js +2 -1
  29. package/dist/components/radio/RadioGroup.js.map +1 -1
  30. package/dist/components/select/Select.d.ts.map +1 -1
  31. package/dist/components/select/Select.js +1 -1
  32. package/dist/components/select/Select.js.map +1 -1
  33. package/dist/components/tabs/Tabs.d.ts.map +1 -1
  34. package/dist/components/tabs/Tabs.js +1 -1
  35. package/dist/components/tabs/Tabs.js.map +1 -1
  36. package/dist/components/toolbar/Toolbar.d.ts.map +1 -1
  37. package/dist/components/toolbar/Toolbar.js +1 -1
  38. package/dist/components/toolbar/Toolbar.js.map +1 -1
  39. package/dist/layout/app-shell/AppShell.d.ts.map +1 -1
  40. package/dist/layout/app-shell/AppShell.js +1 -1
  41. package/dist/layout/app-shell/AppShell.js.map +1 -1
  42. package/dist/layout/auth-layout/AuthLayout.d.ts.map +1 -1
  43. package/dist/layout/auth-layout/AuthLayout.js +1 -1
  44. package/dist/layout/auth-layout/AuthLayout.js.map +1 -1
  45. package/dist/layout/page/Page.d.ts.map +1 -1
  46. package/dist/layout/page/Page.js +1 -1
  47. package/dist/layout/page/Page.js.map +1 -1
  48. package/dist/layout/page-header/PageHeader.d.ts.map +1 -1
  49. package/dist/layout/page-header/PageHeader.js +1 -1
  50. package/dist/layout/page-header/PageHeader.js.map +1 -1
  51. package/dist/layout/page-section/PageSection.d.ts.map +1 -1
  52. package/dist/layout/page-section/PageSection.js +1 -1
  53. package/dist/layout/page-section/PageSection.js.map +1 -1
  54. package/dist/layout/sidebar-layout/SidebarLayout.d.ts.map +1 -1
  55. package/dist/layout/sidebar-layout/SidebarLayout.js +1 -1
  56. package/dist/layout/sidebar-layout/SidebarLayout.js.map +1 -1
  57. package/dist/layout/topbar-layout/TopbarLayout.d.ts.map +1 -1
  58. package/dist/layout/topbar-layout/TopbarLayout.js +1 -1
  59. package/dist/layout/topbar-layout/TopbarLayout.js.map +1 -1
  60. package/dist/patterns/auth/ForgotPasswordForm.d.ts.map +1 -1
  61. package/dist/patterns/auth/ForgotPasswordForm.js +1 -1
  62. package/dist/patterns/auth/ForgotPasswordForm.js.map +1 -1
  63. package/dist/patterns/auth/OtpForm.d.ts.map +1 -1
  64. package/dist/patterns/auth/OtpForm.js +1 -1
  65. package/dist/patterns/auth/OtpForm.js.map +1 -1
  66. package/dist/patterns/auth/SignInForm.d.ts.map +1 -1
  67. package/dist/patterns/auth/SignInForm.js +1 -1
  68. package/dist/patterns/auth/SignInForm.js.map +1 -1
  69. package/dist/patterns/auth/SignUpForm.d.ts.map +1 -1
  70. package/dist/patterns/auth/SignUpForm.js +1 -1
  71. package/dist/patterns/auth/SignUpForm.js.map +1 -1
  72. package/dist/patterns/collection-editor/CollectionEditor.d.ts.map +1 -1
  73. package/dist/patterns/collection-editor/CollectionEditor.js +1 -1
  74. package/dist/patterns/collection-editor/CollectionEditor.js.map +1 -1
  75. package/dist/patterns/confirm-dialog/ConfirmDialog.d.ts.map +1 -1
  76. package/dist/patterns/confirm-dialog/ConfirmDialog.js +1 -1
  77. package/dist/patterns/confirm-dialog/ConfirmDialog.js.map +1 -1
  78. package/dist/patterns/disclosure-section/DisclosureSection.d.ts.map +1 -1
  79. package/dist/patterns/disclosure-section/DisclosureSection.js +1 -1
  80. package/dist/patterns/disclosure-section/DisclosureSection.js.map +1 -1
  81. package/dist/patterns/empty-state/EmptyState.d.ts.map +1 -1
  82. package/dist/patterns/empty-state/EmptyState.js +1 -1
  83. package/dist/patterns/empty-state/EmptyState.js.map +1 -1
  84. package/dist/patterns/form-field/FormField.d.ts.map +1 -1
  85. package/dist/patterns/form-field/FormField.js +2 -1
  86. package/dist/patterns/form-field/FormField.js.map +1 -1
  87. package/dist/patterns/inspector-field/InspectorField.d.ts.map +1 -1
  88. package/dist/patterns/inspector-field/InspectorField.js +1 -1
  89. package/dist/patterns/inspector-field/InspectorField.js.map +1 -1
  90. package/dist/patterns/notice/Notice.d.ts.map +1 -1
  91. package/dist/patterns/notice/Notice.js +1 -1
  92. package/dist/patterns/notice/Notice.js.map +1 -1
  93. package/dist/patterns/section-header/SectionHeader.d.ts.map +1 -1
  94. package/dist/patterns/section-header/SectionHeader.js +1 -1
  95. package/dist/patterns/section-header/SectionHeader.js.map +1 -1
  96. package/dist/patterns/settings-row/SettingsRow.d.ts.map +1 -1
  97. package/dist/patterns/settings-row/SettingsRow.js +1 -1
  98. package/dist/patterns/settings-row/SettingsRow.js.map +1 -1
  99. package/dist/patterns/theme-composer/ThemeComposer.d.ts.map +1 -1
  100. package/dist/patterns/theme-composer/ThemeComposer.js +98 -6
  101. package/dist/patterns/theme-composer/ThemeComposer.js.map +1 -1
  102. package/dist/patterns/theme-composer/types.d.ts +2 -0
  103. package/dist/patterns/theme-composer/types.d.ts.map +1 -1
  104. package/dist/patterns/theme-composer/types.js.map +1 -1
  105. package/dist/patterns/tile-grid/PaletteItem.d.ts.map +1 -1
  106. package/dist/patterns/tile-grid/PaletteItem.js +1 -1
  107. package/dist/patterns/tile-grid/PaletteItem.js.map +1 -1
  108. package/dist/patterns/tile-grid/TileGrid.d.ts.map +1 -1
  109. package/dist/patterns/tile-grid/TileGrid.js +1 -1
  110. package/dist/patterns/tile-grid/TileGrid.js.map +1 -1
  111. package/dist/patterns/tree-view/TreeItem.d.ts.map +1 -1
  112. package/dist/patterns/tree-view/TreeItem.js +1 -1
  113. package/dist/patterns/tree-view/TreeItem.js.map +1 -1
  114. package/dist/patterns/tree-view/TreeView.d.ts.map +1 -1
  115. package/dist/patterns/tree-view/TreeView.js +1 -1
  116. package/dist/patterns/tree-view/TreeView.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/audit.test.ts +242 -0
  119. package/src/components/card/Card.tsx +2 -1
  120. package/src/components/checkbox/CheckboxGroup.tsx +2 -1
  121. package/src/components/drawer/Drawer.tsx +2 -1
  122. package/src/components/form/Form.tsx +1 -1
  123. package/src/components/form/FormActions.tsx +1 -1
  124. package/src/components/form/FormError.tsx +1 -1
  125. package/src/components/form/FormField.tsx +2 -1
  126. package/src/components/modal/Modal.tsx +2 -1
  127. package/src/components/radio/RadioGroup.tsx +2 -1
  128. package/src/components/select/Select.tsx +1 -1
  129. package/src/components/tabs/Tabs.tsx +1 -1
  130. package/src/components/toolbar/Toolbar.tsx +1 -1
  131. package/src/layout/app-shell/AppShell.tsx +1 -1
  132. package/src/layout/auth-layout/AuthLayout.tsx +1 -1
  133. package/src/layout/page/Page.tsx +1 -1
  134. package/src/layout/page-header/PageHeader.tsx +1 -1
  135. package/src/layout/page-section/PageSection.tsx +1 -1
  136. package/src/layout/sidebar-layout/SidebarLayout.tsx +1 -1
  137. package/src/layout/topbar-layout/TopbarLayout.tsx +1 -1
  138. package/src/patterns/auth/ForgotPasswordForm.tsx +1 -1
  139. package/src/patterns/auth/OtpForm.tsx +1 -1
  140. package/src/patterns/auth/SignInForm.tsx +1 -1
  141. package/src/patterns/auth/SignUpForm.tsx +1 -1
  142. package/src/patterns/collection-editor/CollectionEditor.tsx +1 -1
  143. package/src/patterns/confirm-dialog/ConfirmDialog.tsx +1 -1
  144. package/src/patterns/disclosure-section/DisclosureSection.tsx +1 -1
  145. package/src/patterns/empty-state/EmptyState.tsx +1 -1
  146. package/src/patterns/form-field/FormField.tsx +2 -1
  147. package/src/patterns/inspector-field/InspectorField.tsx +1 -1
  148. package/src/patterns/notice/Notice.tsx +1 -1
  149. package/src/patterns/section-header/SectionHeader.tsx +1 -1
  150. package/src/patterns/settings-row/SettingsRow.tsx +1 -1
  151. package/src/patterns/theme-composer/ThemeComposer.test.ts +148 -2
  152. package/src/patterns/theme-composer/ThemeComposer.tsx +129 -5
  153. package/src/patterns/theme-composer/types.ts +3 -0
  154. package/src/patterns/tile-grid/PaletteItem.tsx +1 -1
  155. package/src/patterns/tile-grid/TileGrid.tsx +1 -1
  156. package/src/patterns/tree-view/TreeItem.tsx +1 -1
  157. 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
- // Validate the exported types compile correctly by asserting shape
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 { Box, Stack } from '@ankhorage/surface';
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
- // Keep local hex input in sync when value.primaryColor changes externally
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('Enter a valid 6-digit hex color (e.g. #0f766e).');
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="#0f766e"
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={() => onSubmit(value)}
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,6 +1,6 @@
1
- import { Box } from '@ankhorage/surface';
2
1
  import React from 'react';
3
2
 
3
+ import { Box } from '../../foundation';
4
4
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
5
  import type { TileGridProps } from './types';
6
6
 
@@ -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';