@mr.dj2u/cli 0.1.7 → 0.1.9
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev-tools.d.ts.map +1 -1
- package/dist/commands/dev-tools.js +22 -4
- package/dist/commands/dev-tools.js.map +1 -1
- package/dist/commands/eject.d.ts +12 -0
- package/dist/commands/eject.d.ts.map +1 -0
- package/dist/commands/eject.js +328 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/onboard.d.ts +3 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +15 -3
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/stylist.d.ts +25 -0
- package/dist/commands/stylist.d.ts.map +1 -0
- package/dist/commands/stylist.js +392 -0
- package/dist/commands/stylist.js.map +1 -0
- package/dist/project-memory.d.ts +1 -0
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +3026 -390
- package/dist/project-memory.js.map +1 -1
- package/dist/stylist-theme.d.ts +104 -0
- package/dist/stylist-theme.d.ts.map +1 -0
- package/dist/stylist-theme.js +1374 -0
- package/dist/stylist-theme.js.map +1 -0
- package/package.json +66 -65
- package/templates/embedded-fonts.template.ts +72 -0
- package/templates/expo-sdk-56-screen-universal.template.tsx +709 -0
- package/templates/project/guidelines.md +4 -3
- package/templates/stylist-screen.template.tsx +3446 -0
|
@@ -0,0 +1,3446 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
/* eslint-disable react-hooks/set-state-in-effect */
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
Animated,
|
|
7
|
+
Easing,
|
|
8
|
+
Modal,
|
|
9
|
+
Platform,
|
|
10
|
+
Pressable,
|
|
11
|
+
ScrollView,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Text,
|
|
14
|
+
TextInput,
|
|
15
|
+
View,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
import ColorPicker, { HueSlider, Panel1, Preview } from 'reanimated-color-picker';
|
|
18
|
+
import tailwindColors from 'tailwindcss/colors';
|
|
19
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
20
|
+
|
|
21
|
+
import { AnimatedPressable } from '../../components/exposition';
|
|
22
|
+
import { EMBEDDED_GOOGLE_FONTS } from './embedded-fonts';
|
|
23
|
+
import stylistThemeTokens, {
|
|
24
|
+
type StylistColorMode,
|
|
25
|
+
type StylistColorPalette,
|
|
26
|
+
type StylistColorScheme,
|
|
27
|
+
type StylistFamilyMode,
|
|
28
|
+
type StylistSemanticFamilies,
|
|
29
|
+
type StylistThemeTokens,
|
|
30
|
+
} from '../../theme/tokens';
|
|
31
|
+
import { useSetAppTheme } from '../../theme/provider';
|
|
32
|
+
|
|
33
|
+
type SemanticColorKey = keyof StylistSemanticFamilies;
|
|
34
|
+
type PaletteColorKey = keyof StylistColorPalette;
|
|
35
|
+
type TailwindShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
|
|
36
|
+
type ColorInputMode = 'picker' | 'families';
|
|
37
|
+
type WritePolicy = 'managed' | 'overwrite';
|
|
38
|
+
type SaveMessageTone = 'info' | 'success' | 'error';
|
|
39
|
+
type PaletteFamilies = Record<PaletteColorKey, TailwindColorFamily>;
|
|
40
|
+
type PaletteShades = Record<PaletteColorKey, TailwindShade>;
|
|
41
|
+
type LivePickerPreview = {
|
|
42
|
+
colorInputMode: ColorInputMode;
|
|
43
|
+
colorMode: StylistColorMode;
|
|
44
|
+
hex: string;
|
|
45
|
+
key: PaletteColorKey;
|
|
46
|
+
scheme: StylistColorScheme;
|
|
47
|
+
};
|
|
48
|
+
type FontRoleKey =
|
|
49
|
+
| 'fontDisplay'
|
|
50
|
+
| 'fontTitle'
|
|
51
|
+
| 'fontSubtitle'
|
|
52
|
+
| 'fontBody'
|
|
53
|
+
| 'fontCaption'
|
|
54
|
+
| 'fontMono';
|
|
55
|
+
type StylistTypographyRoles = {
|
|
56
|
+
fontDisplay: string;
|
|
57
|
+
fontTitle: string;
|
|
58
|
+
fontSubtitle: string;
|
|
59
|
+
fontBody: string;
|
|
60
|
+
fontCaption: string;
|
|
61
|
+
fontMono: string;
|
|
62
|
+
};
|
|
63
|
+
type ExtendedStylistThemeTokens = Omit<StylistThemeTokens, 'typography'> & {
|
|
64
|
+
typography: StylistThemeTokens['typography'] & StylistTypographyRoles;
|
|
65
|
+
};
|
|
66
|
+
type TopToggleHelpKey = 'colorMode' | 'preview' | 'familyStrategy';
|
|
67
|
+
|
|
68
|
+
type TailwindColorFamily =
|
|
69
|
+
| 'slate'
|
|
70
|
+
| 'gray'
|
|
71
|
+
| 'zinc'
|
|
72
|
+
| 'neutral'
|
|
73
|
+
| 'stone'
|
|
74
|
+
| 'red'
|
|
75
|
+
| 'orange'
|
|
76
|
+
| 'amber'
|
|
77
|
+
| 'yellow'
|
|
78
|
+
| 'lime'
|
|
79
|
+
| 'green'
|
|
80
|
+
| 'emerald'
|
|
81
|
+
| 'teal'
|
|
82
|
+
| 'cyan'
|
|
83
|
+
| 'sky'
|
|
84
|
+
| 'blue'
|
|
85
|
+
| 'indigo'
|
|
86
|
+
| 'violet'
|
|
87
|
+
| 'purple'
|
|
88
|
+
| 'fuchsia'
|
|
89
|
+
| 'pink'
|
|
90
|
+
| 'rose';
|
|
91
|
+
|
|
92
|
+
const paletteColorKeys: PaletteColorKey[] = [
|
|
93
|
+
'background',
|
|
94
|
+
'surface',
|
|
95
|
+
'text',
|
|
96
|
+
'primary',
|
|
97
|
+
'secondary',
|
|
98
|
+
'success',
|
|
99
|
+
'warning',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const semanticColorKeys: SemanticColorKey[] = ['primary', 'secondary', 'success', 'warning'];
|
|
103
|
+
const spacingKeys: Array<keyof StylistThemeTokens['layout']['spacing']> = [
|
|
104
|
+
'xs',
|
|
105
|
+
'sm',
|
|
106
|
+
'md',
|
|
107
|
+
'lg',
|
|
108
|
+
'xl',
|
|
109
|
+
];
|
|
110
|
+
const schemeKeys: StylistColorScheme[] = ['light', 'dark'];
|
|
111
|
+
const familyModeOptions: Array<{ label: string; value: StylistFamilyMode }> = [
|
|
112
|
+
{ label: '1 family', value: 'one' },
|
|
113
|
+
{ label: '2 families', value: 'two' },
|
|
114
|
+
];
|
|
115
|
+
const colorModeOptions: Array<{ label: string; value: StylistColorMode }> = [
|
|
116
|
+
{ label: 'BG Color', value: 'bg' },
|
|
117
|
+
{ label: 'Automatic', value: 'automatic' },
|
|
118
|
+
];
|
|
119
|
+
const colorInputModeOptions: Array<{ label: string; value: ColorInputMode }> = [
|
|
120
|
+
{ label: 'Color Picker', value: 'picker' },
|
|
121
|
+
{ label: 'Tailwind Families', value: 'families' },
|
|
122
|
+
];
|
|
123
|
+
const NATIVE_SAVE_COMMAND = 'npm run stylist:sync:android';
|
|
124
|
+
const GOOGLE_FONTS_KEY_STORAGE = 'mds.stylist.googleFontsApiKey';
|
|
125
|
+
const GOOGLE_FONTS_BANNER_DISMISSED_STORAGE = 'mds.stylist.googleFontsBannerDismissed';
|
|
126
|
+
const GOOGLE_FONTS_API_URL = 'https://www.googleapis.com/webfonts/v1/webfonts';
|
|
127
|
+
const WEB_LAST_SYNC_MESSAGE_STORAGE = 'mds.stylist.lastSyncMessage';
|
|
128
|
+
const WEB_SYSTEM_FONTS = new Set([
|
|
129
|
+
'system',
|
|
130
|
+
'arial',
|
|
131
|
+
'helvetica',
|
|
132
|
+
'times new roman',
|
|
133
|
+
'georgia',
|
|
134
|
+
'courier new',
|
|
135
|
+
'monospace',
|
|
136
|
+
'sans-serif',
|
|
137
|
+
'serif',
|
|
138
|
+
'ui-sans-serif',
|
|
139
|
+
]);
|
|
140
|
+
const NATIVE_SAFE_FONTS = new Set([
|
|
141
|
+
'system',
|
|
142
|
+
'arial',
|
|
143
|
+
'helvetica',
|
|
144
|
+
'times new roman',
|
|
145
|
+
'georgia',
|
|
146
|
+
'courier new',
|
|
147
|
+
'monospace',
|
|
148
|
+
'sans-serif',
|
|
149
|
+
'serif',
|
|
150
|
+
'notoserif',
|
|
151
|
+
'noto sans',
|
|
152
|
+
]);
|
|
153
|
+
const fontRoleFields: Array<{
|
|
154
|
+
key: FontRoleKey;
|
|
155
|
+
label: string;
|
|
156
|
+
placeholder: string;
|
|
157
|
+
}> = [
|
|
158
|
+
{ key: 'fontDisplay', label: 'Display', placeholder: 'Display font family' },
|
|
159
|
+
{ key: 'fontTitle', label: 'Title', placeholder: 'Title font family' },
|
|
160
|
+
{
|
|
161
|
+
key: 'fontSubtitle',
|
|
162
|
+
label: 'Subtitle',
|
|
163
|
+
placeholder: 'Subtitle font family',
|
|
164
|
+
},
|
|
165
|
+
{ key: 'fontBody', label: 'Body', placeholder: 'Body font family' },
|
|
166
|
+
{ key: 'fontCaption', label: 'Caption', placeholder: 'Caption font family' },
|
|
167
|
+
{ key: 'fontMono', label: 'Mono', placeholder: 'Mono font family' },
|
|
168
|
+
];
|
|
169
|
+
const builtInFontChoices = [
|
|
170
|
+
'System',
|
|
171
|
+
'monospace',
|
|
172
|
+
'Arial',
|
|
173
|
+
'Helvetica',
|
|
174
|
+
'Times New Roman',
|
|
175
|
+
'Georgia',
|
|
176
|
+
'Courier New',
|
|
177
|
+
'sans-serif',
|
|
178
|
+
'serif',
|
|
179
|
+
];
|
|
180
|
+
const loadedWebFonts = new Set<string>();
|
|
181
|
+
|
|
182
|
+
type WebSyncMessageSnapshot = { message: string; timestamp: number };
|
|
183
|
+
|
|
184
|
+
function readWebSyncMessageSnapshot(): WebSyncMessageSnapshot | null {
|
|
185
|
+
if (Platform.OS !== 'web') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const raw = (globalThis as any).sessionStorage?.getItem(WEB_LAST_SYNC_MESSAGE_STORAGE);
|
|
191
|
+
if (!raw) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const parsed = JSON.parse(raw) as Partial<WebSyncMessageSnapshot>;
|
|
195
|
+
if (!parsed.message || typeof parsed.message !== 'string') {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (typeof parsed.timestamp !== 'number') {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return { message: parsed.message, timestamp: parsed.timestamp };
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function writeWebSyncMessageSnapshot(message: string) {
|
|
208
|
+
if (Platform.OS !== 'web') {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
(globalThis as any).sessionStorage?.setItem(
|
|
214
|
+
WEB_LAST_SYNC_MESSAGE_STORAGE,
|
|
215
|
+
JSON.stringify({ message, timestamp: Date.now() } satisfies WebSyncMessageSnapshot)
|
|
216
|
+
);
|
|
217
|
+
} catch {
|
|
218
|
+
// no-op
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clearWebSyncMessageSnapshot() {
|
|
223
|
+
if (Platform.OS !== 'web') {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
(globalThis as any).sessionStorage?.removeItem(WEB_LAST_SYNC_MESSAGE_STORAGE);
|
|
229
|
+
} catch {
|
|
230
|
+
// no-op
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const tailwindFamilies: TailwindColorFamily[] = [
|
|
235
|
+
'slate',
|
|
236
|
+
'gray',
|
|
237
|
+
'zinc',
|
|
238
|
+
'neutral',
|
|
239
|
+
'stone',
|
|
240
|
+
'red',
|
|
241
|
+
'orange',
|
|
242
|
+
'amber',
|
|
243
|
+
'yellow',
|
|
244
|
+
'lime',
|
|
245
|
+
'green',
|
|
246
|
+
'emerald',
|
|
247
|
+
'teal',
|
|
248
|
+
'cyan',
|
|
249
|
+
'sky',
|
|
250
|
+
'blue',
|
|
251
|
+
'indigo',
|
|
252
|
+
'violet',
|
|
253
|
+
'purple',
|
|
254
|
+
'fuchsia',
|
|
255
|
+
'pink',
|
|
256
|
+
'rose',
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const pickerSwatches = [
|
|
260
|
+
'#f44336',
|
|
261
|
+
'#e91e63',
|
|
262
|
+
'#9c27b0',
|
|
263
|
+
'#673ab7',
|
|
264
|
+
'#3f51b5',
|
|
265
|
+
'#2196f3',
|
|
266
|
+
'#03a9f4',
|
|
267
|
+
'#00bcd4',
|
|
268
|
+
'#009688',
|
|
269
|
+
'#4caf50',
|
|
270
|
+
'#8bc34a',
|
|
271
|
+
'#cddc39',
|
|
272
|
+
'#ffeb3b',
|
|
273
|
+
'#ffc107',
|
|
274
|
+
'#ff9800',
|
|
275
|
+
'#ff5722',
|
|
276
|
+
'#795548',
|
|
277
|
+
'#9e9e9e',
|
|
278
|
+
'#607d8b',
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const tailwindPalette = tailwindColors as unknown as Record<
|
|
282
|
+
string,
|
|
283
|
+
Partial<Record<TailwindShade, string>>
|
|
284
|
+
>;
|
|
285
|
+
const automaticLockedKeys: PaletteColorKey[] = ['background', 'surface', 'text'];
|
|
286
|
+
const shadeOptions: TailwindShade[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
287
|
+
const topToggleHelpCopy: Record<TopToggleHelpKey, { title: string; body: string }> = {
|
|
288
|
+
colorMode: {
|
|
289
|
+
title: 'Color Mode',
|
|
290
|
+
body: 'Automatic derives background, surface, and text from primary. BG Color lets you set each palette color directly.',
|
|
291
|
+
},
|
|
292
|
+
preview: {
|
|
293
|
+
title: 'Preview',
|
|
294
|
+
body: 'Preview switches light/dark editing context. Saved tokens still include both schemes.',
|
|
295
|
+
},
|
|
296
|
+
familyStrategy: {
|
|
297
|
+
title: 'Family Strategy',
|
|
298
|
+
body: '1 family mirrors semantic color families across light and dark. 2 families lets each scheme use its own family mapping.',
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const defaultBgFamilies: Record<StylistColorScheme, PaletteFamilies> = {
|
|
303
|
+
light: {
|
|
304
|
+
background: 'slate',
|
|
305
|
+
surface: 'gray',
|
|
306
|
+
text: 'slate',
|
|
307
|
+
primary: 'blue',
|
|
308
|
+
secondary: 'violet',
|
|
309
|
+
success: 'emerald',
|
|
310
|
+
warning: 'amber',
|
|
311
|
+
},
|
|
312
|
+
dark: {
|
|
313
|
+
background: 'slate',
|
|
314
|
+
surface: 'gray',
|
|
315
|
+
text: 'slate',
|
|
316
|
+
primary: 'blue',
|
|
317
|
+
secondary: 'violet',
|
|
318
|
+
success: 'emerald',
|
|
319
|
+
warning: 'amber',
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const defaultBgFamilyShades: Record<StylistColorScheme, PaletteShades> = {
|
|
324
|
+
light: {
|
|
325
|
+
background: 50,
|
|
326
|
+
surface: 100,
|
|
327
|
+
text: 900,
|
|
328
|
+
primary: 500,
|
|
329
|
+
secondary: 500,
|
|
330
|
+
success: 500,
|
|
331
|
+
warning: 500,
|
|
332
|
+
},
|
|
333
|
+
dark: {
|
|
334
|
+
background: 950,
|
|
335
|
+
surface: 900,
|
|
336
|
+
text: 50,
|
|
337
|
+
primary: 400,
|
|
338
|
+
secondary: 400,
|
|
339
|
+
success: 400,
|
|
340
|
+
warning: 400,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
345
|
+
const normalized = hex.replace('#', '').trim();
|
|
346
|
+
const value =
|
|
347
|
+
normalized.length === 3
|
|
348
|
+
? normalized
|
|
349
|
+
.split('')
|
|
350
|
+
.map((char) => `${char}${char}`)
|
|
351
|
+
.join('')
|
|
352
|
+
: normalized;
|
|
353
|
+
|
|
354
|
+
const int = Number.parseInt(value, 16);
|
|
355
|
+
return {
|
|
356
|
+
r: (int >> 16) & 255,
|
|
357
|
+
g: (int >> 8) & 255,
|
|
358
|
+
b: int & 255,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
363
|
+
const clamp = (value: number) => Math.max(0, Math.min(255, Math.round(value)));
|
|
364
|
+
const toHex = (value: number) => clamp(value).toString(16).padStart(2, '0');
|
|
365
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function mixHex(base: string, mix: string, amount: number): string {
|
|
369
|
+
const a = hexToRgb(base);
|
|
370
|
+
const b = hexToRgb(mix);
|
|
371
|
+
return rgbToHex(
|
|
372
|
+
a.r + (b.r - a.r) * amount,
|
|
373
|
+
a.g + (b.g - a.g) * amount,
|
|
374
|
+
a.b + (b.b - a.b) * amount
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function relativeLuminance(hex: string): number {
|
|
379
|
+
const { r, g, b } = hexToRgb(hex);
|
|
380
|
+
const toLinear = (channel: number) => {
|
|
381
|
+
const value = channel / 255;
|
|
382
|
+
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
383
|
+
};
|
|
384
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function contrastRatio(a: string, b: string): number {
|
|
388
|
+
const lighter = Math.max(relativeLuminance(a), relativeLuminance(b));
|
|
389
|
+
const darker = Math.min(relativeLuminance(a), relativeLuminance(b));
|
|
390
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ensureReadableTextColor(
|
|
394
|
+
text: string,
|
|
395
|
+
surface: string,
|
|
396
|
+
scheme: StylistColorScheme
|
|
397
|
+
): string {
|
|
398
|
+
if (contrastRatio(text, surface) >= 7) {
|
|
399
|
+
return text;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const target = scheme === 'dark' ? '#ffffff' : '#000000';
|
|
403
|
+
for (const amount of [0.25, 0.4, 0.55, 0.7, 0.85, 1]) {
|
|
404
|
+
const candidate = mixHex(text, target, amount);
|
|
405
|
+
if (contrastRatio(candidate, surface) >= 7) {
|
|
406
|
+
return candidate;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return target;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getReadableChipTextColor(background: string): string {
|
|
414
|
+
return contrastRatio('#ffffff', background) >= contrastRatio('#111827', background)
|
|
415
|
+
? '#ffffff'
|
|
416
|
+
: '#111827';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function generateTailwindLikeScale(primaryHex: string): Record<TailwindShade, string> {
|
|
420
|
+
const white = '#ffffff';
|
|
421
|
+
const black = '#000000';
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
50: mixHex(primaryHex, white, 0.95),
|
|
425
|
+
100: mixHex(primaryHex, white, 0.88),
|
|
426
|
+
200: mixHex(primaryHex, white, 0.74),
|
|
427
|
+
300: mixHex(primaryHex, white, 0.58),
|
|
428
|
+
400: mixHex(primaryHex, white, 0.32),
|
|
429
|
+
500: primaryHex.toLowerCase(),
|
|
430
|
+
600: mixHex(primaryHex, black, 0.16),
|
|
431
|
+
700: mixHex(primaryHex, black, 0.32),
|
|
432
|
+
800: mixHex(primaryHex, black, 0.5),
|
|
433
|
+
900: mixHex(primaryHex, black, 0.68),
|
|
434
|
+
950: mixHex(primaryHex, black, 0.8),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function linearToSrgb(value: number): number {
|
|
439
|
+
if (value <= 0.0031308) {
|
|
440
|
+
return 12.92 * value;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function clamp01(value: number): number {
|
|
447
|
+
return Math.max(0, Math.min(1, value));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function toHexChannel(value: number): string {
|
|
451
|
+
const channel = Math.round(clamp01(value) * 255);
|
|
452
|
+
return channel.toString(16).padStart(2, '0');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function oklchToHex(l: number, c: number, hDeg: number): string {
|
|
456
|
+
const hRad = (hDeg * Math.PI) / 180;
|
|
457
|
+
const a = c * Math.cos(hRad);
|
|
458
|
+
const b = c * Math.sin(hRad);
|
|
459
|
+
|
|
460
|
+
const lPrime = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
461
|
+
const mPrime = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
462
|
+
const sPrime = l - 0.0894841775 * a - 1.291485548 * b;
|
|
463
|
+
|
|
464
|
+
const lCube = lPrime ** 3;
|
|
465
|
+
const mCube = mPrime ** 3;
|
|
466
|
+
const sCube = sPrime ** 3;
|
|
467
|
+
|
|
468
|
+
const rLinear = 4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube;
|
|
469
|
+
const gLinear = -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube;
|
|
470
|
+
const bLinear = -0.0041960863 * lCube - 0.7034186147 * mCube + 1.707614701 * sCube;
|
|
471
|
+
|
|
472
|
+
const r = linearToSrgb(rLinear);
|
|
473
|
+
const g = linearToSrgb(gLinear);
|
|
474
|
+
const bOut = linearToSrgb(bLinear);
|
|
475
|
+
|
|
476
|
+
return `#${toHexChannel(r)}${toHexChannel(g)}${toHexChannel(bOut)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function parseOklch(color: string): { l: number; c: number; h: number } | null {
|
|
480
|
+
const match = color
|
|
481
|
+
.trim()
|
|
482
|
+
.match(/^oklch\(\s*([0-9.]+)%\s+([0-9.]+)\s+([0-9.]+)(?:\s*\/\s*[0-9.%]+)?\s*\)$/i);
|
|
483
|
+
|
|
484
|
+
if (!match) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const l = Number.parseFloat(match[1] ?? '');
|
|
489
|
+
const c = Number.parseFloat(match[2] ?? '');
|
|
490
|
+
const h = Number.parseFloat(match[3] ?? '');
|
|
491
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { l: l / 100, c, h };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getTailwindColor(family: string, shade: TailwindShade): string {
|
|
499
|
+
const familyScale = tailwindPalette[family] ?? tailwindPalette.blue;
|
|
500
|
+
const value = familyScale?.[shade] ?? tailwindPalette.blue?.[shade] ?? '#3b82f6';
|
|
501
|
+
const normalized = value.trim().toLowerCase();
|
|
502
|
+
if (normalized.startsWith('#')) {
|
|
503
|
+
return normalized;
|
|
504
|
+
}
|
|
505
|
+
if (normalized.startsWith('oklch(')) {
|
|
506
|
+
const parsed = parseOklch(normalized);
|
|
507
|
+
if (parsed) {
|
|
508
|
+
return oklchToHex(parsed.l, parsed.c, parsed.h);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return '#3b82f6';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function nudgeShadeTowardCenter(shade: TailwindShade): TailwindShade {
|
|
516
|
+
const order: TailwindShade[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
517
|
+
const index = order.indexOf(shade);
|
|
518
|
+
if (index <= 0) return 100;
|
|
519
|
+
if (index >= order.length - 1) return 900;
|
|
520
|
+
const next = shade < 500 ? index + 1 : index - 1;
|
|
521
|
+
return order[next] ?? shade;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function deriveAutomaticPalette(
|
|
525
|
+
families: StylistSemanticFamilies,
|
|
526
|
+
scheme: StylistColorScheme,
|
|
527
|
+
useSharedSemanticShades = false
|
|
528
|
+
): StylistColorPalette {
|
|
529
|
+
const semanticShade: TailwindShade = useSharedSemanticShades
|
|
530
|
+
? 500
|
|
531
|
+
: scheme === 'light'
|
|
532
|
+
? 500
|
|
533
|
+
: 400;
|
|
534
|
+
const backgroundShade: TailwindShade = scheme === 'light' ? 50 : 950;
|
|
535
|
+
let surfaceShade: TailwindShade = scheme === 'light' ? 100 : 900;
|
|
536
|
+
|
|
537
|
+
const background = getTailwindColor(families.primary, backgroundShade);
|
|
538
|
+
let surface = getTailwindColor(families.primary, surfaceShade);
|
|
539
|
+
|
|
540
|
+
if (background === surface) {
|
|
541
|
+
surfaceShade = nudgeShadeTowardCenter(surfaceShade);
|
|
542
|
+
surface = getTailwindColor(families.primary, surfaceShade);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const text = ensureReadableTextColor(
|
|
546
|
+
getTailwindColor(families.primary, scheme === 'light' ? 900 : 200),
|
|
547
|
+
surface,
|
|
548
|
+
scheme
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
background,
|
|
553
|
+
surface,
|
|
554
|
+
text,
|
|
555
|
+
primary: getTailwindColor(families.primary, semanticShade),
|
|
556
|
+
secondary: getTailwindColor(families.secondary, semanticShade),
|
|
557
|
+
success: getTailwindColor(families.success, semanticShade),
|
|
558
|
+
warning: getTailwindColor(families.warning, semanticShade),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function deriveBgPaletteFromFamilies(
|
|
563
|
+
families: PaletteFamilies,
|
|
564
|
+
shades: PaletteShades
|
|
565
|
+
): StylistColorPalette {
|
|
566
|
+
return {
|
|
567
|
+
background: getTailwindColor(families.background, shades.background),
|
|
568
|
+
surface: getTailwindColor(families.surface, shades.surface),
|
|
569
|
+
text: getTailwindColor(families.text, shades.text),
|
|
570
|
+
primary: getTailwindColor(families.primary, shades.primary),
|
|
571
|
+
secondary: getTailwindColor(families.secondary, shades.secondary),
|
|
572
|
+
success: getTailwindColor(families.success, shades.success),
|
|
573
|
+
warning: getTailwindColor(families.warning, shades.warning),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function deriveAutomaticPaletteFromPrimary(
|
|
578
|
+
source: StylistColorPalette,
|
|
579
|
+
scheme: StylistColorScheme
|
|
580
|
+
): StylistColorPalette {
|
|
581
|
+
const shades = generateTailwindLikeScale(source.primary);
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
...source,
|
|
585
|
+
background: shades[scheme === 'light' ? 50 : 950],
|
|
586
|
+
surface: shades[scheme === 'light' ? 100 : 900],
|
|
587
|
+
text: ensureReadableTextColor(
|
|
588
|
+
shades[scheme === 'light' ? 900 : 200],
|
|
589
|
+
shades[scheme === 'light' ? 100 : 900],
|
|
590
|
+
scheme
|
|
591
|
+
),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function ensureDistinctBackgroundSurface(
|
|
596
|
+
palette: StylistColorPalette,
|
|
597
|
+
scheme: StylistColorScheme
|
|
598
|
+
): StylistColorPalette {
|
|
599
|
+
if (palette.background !== palette.surface) {
|
|
600
|
+
return palette;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const fallbackSurface = scheme === 'light' ? '#e2e8f0' : '#18181b';
|
|
604
|
+
const fallbackAlternate = scheme === 'light' ? '#f1f5f9' : '#27272a';
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
...palette,
|
|
608
|
+
surface: palette.background === fallbackSurface ? fallbackAlternate : fallbackSurface,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function reconcileTheme(
|
|
613
|
+
theme: StylistThemeTokens,
|
|
614
|
+
colorInputMode: ColorInputMode,
|
|
615
|
+
bgFamilies: Record<StylistColorScheme, PaletteFamilies>,
|
|
616
|
+
bgFamilyShades: Record<StylistColorScheme, PaletteShades>
|
|
617
|
+
): StylistThemeTokens {
|
|
618
|
+
const familiesDark =
|
|
619
|
+
theme.colorSystem.familyMode === 'one' ? { ...theme.families.light } : theme.families.dark;
|
|
620
|
+
|
|
621
|
+
const bgLightSource =
|
|
622
|
+
colorInputMode === 'families'
|
|
623
|
+
? deriveBgPaletteFromFamilies(bgFamilies.light, bgFamilyShades.light)
|
|
624
|
+
: theme.palettes.bg.light;
|
|
625
|
+
const bgDarkSource =
|
|
626
|
+
colorInputMode === 'families'
|
|
627
|
+
? deriveBgPaletteFromFamilies(
|
|
628
|
+
theme.colorSystem.familyMode === 'one' ? bgFamilies.light : bgFamilies.dark,
|
|
629
|
+
theme.colorSystem.familyMode === 'one' ? bgFamilyShades.light : bgFamilyShades.dark
|
|
630
|
+
)
|
|
631
|
+
: theme.palettes.bg.dark;
|
|
632
|
+
|
|
633
|
+
const bgLight = ensureDistinctBackgroundSurface(bgLightSource, 'light');
|
|
634
|
+
const bgDark = ensureDistinctBackgroundSurface(bgDarkSource, 'dark');
|
|
635
|
+
|
|
636
|
+
const useSharedSemanticShades = theme.colorSystem.familyMode === 'one';
|
|
637
|
+
const automaticFamilyLight = deriveAutomaticPalette(
|
|
638
|
+
theme.families.light,
|
|
639
|
+
'light',
|
|
640
|
+
useSharedSemanticShades
|
|
641
|
+
);
|
|
642
|
+
const automaticFamilyDark = deriveAutomaticPalette(familiesDark, 'dark', useSharedSemanticShades);
|
|
643
|
+
|
|
644
|
+
const automaticLightSource =
|
|
645
|
+
colorInputMode === 'families'
|
|
646
|
+
? automaticFamilyLight
|
|
647
|
+
: deriveAutomaticPaletteFromPrimary(theme.palettes.automatic.light, 'light');
|
|
648
|
+
const automaticDarkSource =
|
|
649
|
+
colorInputMode === 'families'
|
|
650
|
+
? automaticFamilyDark
|
|
651
|
+
: deriveAutomaticPaletteFromPrimary(theme.palettes.automatic.dark, 'dark');
|
|
652
|
+
|
|
653
|
+
const automaticLight = ensureDistinctBackgroundSurface(automaticLightSource, 'light');
|
|
654
|
+
const automaticDark = ensureDistinctBackgroundSurface(automaticDarkSource, 'dark');
|
|
655
|
+
|
|
656
|
+
const resolvedColors =
|
|
657
|
+
theme.colorSystem.mode === 'automatic'
|
|
658
|
+
? { light: automaticLight, dark: automaticDark }
|
|
659
|
+
: { light: bgLight, dark: bgDark };
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
...theme,
|
|
663
|
+
families: {
|
|
664
|
+
...theme.families,
|
|
665
|
+
dark: familiesDark,
|
|
666
|
+
},
|
|
667
|
+
palettes: {
|
|
668
|
+
...theme.palettes,
|
|
669
|
+
bg: {
|
|
670
|
+
light: bgLight,
|
|
671
|
+
dark: bgDark,
|
|
672
|
+
},
|
|
673
|
+
automatic: {
|
|
674
|
+
light: automaticLight,
|
|
675
|
+
dark: automaticDark,
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
colors: resolvedColors,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function isLockedAutomaticPickerKey(
|
|
683
|
+
mode: StylistColorMode,
|
|
684
|
+
inputMode: ColorInputMode,
|
|
685
|
+
key: PaletteColorKey
|
|
686
|
+
): boolean {
|
|
687
|
+
return mode === 'automatic' && inputMode === 'picker' && automaticLockedKeys.includes(key);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function sanitizeHexDraftInput(raw: string): string {
|
|
691
|
+
const hexOnly = raw
|
|
692
|
+
.trim()
|
|
693
|
+
.replace(/[^0-9a-fA-F]/g, '')
|
|
694
|
+
.slice(0, 6)
|
|
695
|
+
.toLowerCase();
|
|
696
|
+
return `#${hexOnly}`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function isCommitReadyHex(raw: string): boolean {
|
|
700
|
+
const length = sanitizeHexDraftInput(raw).slice(1).length;
|
|
701
|
+
return length === 3 || length === 6;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function isImmediateApplyHex(raw: string): boolean {
|
|
705
|
+
return sanitizeHexDraftInput(raw).slice(1).length === 6;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function normalizeHexForTheme(raw: string): string {
|
|
709
|
+
const sanitized = sanitizeHexDraftInput(raw);
|
|
710
|
+
const hexBody = sanitized.slice(1);
|
|
711
|
+
if (hexBody.length === 3) {
|
|
712
|
+
return `#${hexBody
|
|
713
|
+
.split('')
|
|
714
|
+
.map((char) => `${char}${char}`)
|
|
715
|
+
.join('')}`;
|
|
716
|
+
}
|
|
717
|
+
return sanitized;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function normalizeFontFamilyName(value: string): string {
|
|
721
|
+
return value.replace(/^['"]|['"]$/g, '').trim();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function humanizeSaveError(rawMessage: string): string {
|
|
725
|
+
const message = rawMessage.trim();
|
|
726
|
+
if (!message) {
|
|
727
|
+
return 'Theme save failed for an unknown reason.';
|
|
728
|
+
}
|
|
729
|
+
if (message.includes('spawn EINVAL')) {
|
|
730
|
+
return 'Theme save could not start the sync command on this machine (spawn EINVAL). Check your local Node/npm shell setup, then retry.';
|
|
731
|
+
}
|
|
732
|
+
if (message.includes('Failed to parse stylist sync output')) {
|
|
733
|
+
return 'Theme save finished with unreadable CLI output. Please rerun Save Theme and check terminal logs.';
|
|
734
|
+
}
|
|
735
|
+
return message;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function normalizeThemeTypography(theme: StylistThemeTokens): ExtendedStylistThemeTokens {
|
|
739
|
+
const fallback = normalizeFontFamilyName(theme.typography.fontFamily) || 'System';
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
...theme,
|
|
743
|
+
typography: {
|
|
744
|
+
...theme.typography,
|
|
745
|
+
fontFamily: fallback,
|
|
746
|
+
fontDisplay:
|
|
747
|
+
normalizeFontFamilyName(
|
|
748
|
+
(theme.typography as Partial<StylistTypographyRoles>).fontDisplay ?? fallback
|
|
749
|
+
) || fallback,
|
|
750
|
+
fontTitle:
|
|
751
|
+
normalizeFontFamilyName(
|
|
752
|
+
(theme.typography as Partial<StylistTypographyRoles>).fontTitle ?? fallback
|
|
753
|
+
) || fallback,
|
|
754
|
+
fontSubtitle:
|
|
755
|
+
normalizeFontFamilyName(
|
|
756
|
+
(theme.typography as Partial<StylistTypographyRoles>).fontSubtitle ?? fallback
|
|
757
|
+
) || fallback,
|
|
758
|
+
fontBody:
|
|
759
|
+
normalizeFontFamilyName(
|
|
760
|
+
(theme.typography as Partial<StylistTypographyRoles>).fontBody ?? fallback
|
|
761
|
+
) || fallback,
|
|
762
|
+
fontCaption:
|
|
763
|
+
normalizeFontFamilyName(
|
|
764
|
+
(theme.typography as Partial<StylistTypographyRoles>).fontCaption ?? fallback
|
|
765
|
+
) || fallback,
|
|
766
|
+
fontMono:
|
|
767
|
+
normalizeFontFamilyName(
|
|
768
|
+
(theme.typography as Partial<StylistTypographyRoles>).fontMono ?? 'monospace'
|
|
769
|
+
) || 'monospace',
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function withFontFamilyAlias(theme: ExtendedStylistThemeTokens): ExtendedStylistThemeTokens {
|
|
775
|
+
return {
|
|
776
|
+
...theme,
|
|
777
|
+
typography: {
|
|
778
|
+
...theme.typography,
|
|
779
|
+
fontFamily: normalizeFontFamilyName(theme.typography.fontDisplay) || 'System',
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function areThemesEqual(
|
|
785
|
+
left: ExtendedStylistThemeTokens,
|
|
786
|
+
right: ExtendedStylistThemeTokens
|
|
787
|
+
): boolean {
|
|
788
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function makeFontCssFamily(fontFamily: string): string {
|
|
792
|
+
const normalized = normalizeFontFamilyName(fontFamily);
|
|
793
|
+
if (!normalized) {
|
|
794
|
+
return 'System';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return normalized.includes(' ') ? `"${normalized}"` : normalized;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function makeNativeFontFamily(fontFamily: string): string {
|
|
801
|
+
const normalized = normalizeFontFamilyName(fontFamily);
|
|
802
|
+
return normalized || 'System';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function isNativeUnsafeFont(fontFamily: string): boolean {
|
|
806
|
+
const key = normalizeFontFamilyName(fontFamily).toLowerCase();
|
|
807
|
+
if (!key) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return !NATIVE_SAFE_FONTS.has(key);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function resolvePreviewFontFamily(fontFamily: string): string {
|
|
815
|
+
return Platform.OS === 'web' ? makeFontCssFamily(fontFamily) : makeNativeFontFamily(fontFamily);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function resolvePreviewFontWeight(fontFamily: string, weight: string): string {
|
|
819
|
+
if (Platform.OS === 'web') {
|
|
820
|
+
return weight;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Many Google Fonts are single-weight on native until the sync downloads assets, so avoid
|
|
824
|
+
// requesting unsupported weights that can cause the font to fall back.
|
|
825
|
+
return isNativeUnsafeFont(fontFamily) ? 'normal' : weight;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function buildGoogleFontsStylesheetUrl(fontFamily: string): string {
|
|
829
|
+
const familyParam = normalizeFontFamilyName(fontFamily).replace(/\s+/g, '+');
|
|
830
|
+
return `https://fonts.googleapis.com/css2?family=${familyParam}:wght@400;500;700;800&display=swap`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function ensureWebFontLoaded(fontFamily: string): void {
|
|
834
|
+
if (Platform.OS !== 'web' || typeof document === 'undefined') {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const normalized = normalizeFontFamilyName(fontFamily);
|
|
839
|
+
if (!normalized) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (WEB_SYSTEM_FONTS.has(normalized.toLowerCase()) || loadedWebFonts.has(normalized)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const existing = document.querySelector(`link[data-stylist-font="${normalized}"]`);
|
|
848
|
+
if (existing) {
|
|
849
|
+
loadedWebFonts.add(normalized);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const link = document.createElement('link');
|
|
854
|
+
link.rel = 'stylesheet';
|
|
855
|
+
link.href = buildGoogleFontsStylesheetUrl(normalized);
|
|
856
|
+
link.setAttribute('data-stylist-font', normalized);
|
|
857
|
+
document.head.appendChild(link);
|
|
858
|
+
loadedWebFonts.add(normalized);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export default function StylistScreen() {
|
|
862
|
+
const insets = useSafeAreaInsets();
|
|
863
|
+
const setAppTheme = useSetAppTheme();
|
|
864
|
+
const [colorInputMode, setColorInputMode] = useState<ColorInputMode>('picker');
|
|
865
|
+
const [bgFamilies, setBgFamilies] =
|
|
866
|
+
useState<Record<StylistColorScheme, PaletteFamilies>>(defaultBgFamilies);
|
|
867
|
+
const [bgFamilyShades, setBgFamilyShades] =
|
|
868
|
+
useState<Record<StylistColorScheme, PaletteShades>>(defaultBgFamilyShades);
|
|
869
|
+
const [theme, setTheme] = useState<ExtendedStylistThemeTokens>(
|
|
870
|
+
withFontFamilyAlias(
|
|
871
|
+
normalizeThemeTypography(
|
|
872
|
+
reconcileTheme(stylistThemeTokens, 'picker', defaultBgFamilies, defaultBgFamilyShades)
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
);
|
|
876
|
+
const [selectedColor, setSelectedColor] = useState<PaletteColorKey>('primary');
|
|
877
|
+
const [pickerHexDraft, setPickerHexDraft] = useState('#');
|
|
878
|
+
const [livePickerPreview, setLivePickerPreview] = useState<LivePickerPreview | null>(null);
|
|
879
|
+
const [saveMessage, setSaveMessage] = useState('');
|
|
880
|
+
const [saveMessageTone, setSaveMessageTone] = useState<SaveMessageTone>('info');
|
|
881
|
+
const [nativeDraft, setNativeDraft] = useState('');
|
|
882
|
+
const [saving, setSaving] = useState(false);
|
|
883
|
+
const [saveMessageNonce, setSaveMessageNonce] = useState(0);
|
|
884
|
+
const [writePolicy, setWritePolicy] = useState<WritePolicy | null>(null);
|
|
885
|
+
const [showWritePolicyModal, setShowWritePolicyModal] = useState(false);
|
|
886
|
+
const [activeTopToggleHelp, setActiveTopToggleHelp] = useState<TopToggleHelpKey | null>(null);
|
|
887
|
+
const [writePolicyLoaded, setWritePolicyLoaded] = useState(Platform.OS !== 'web');
|
|
888
|
+
const [apiKeyDraft, setApiKeyDraft] = useState('');
|
|
889
|
+
const [storedApiKey, setStoredApiKey] = useState('');
|
|
890
|
+
const [fontBannerDismissed, setFontBannerDismissed] = useState(false);
|
|
891
|
+
const [remoteFonts, setRemoteFonts] = useState<string[]>([]);
|
|
892
|
+
const [loadingFonts, setLoadingFonts] = useState(false);
|
|
893
|
+
const [fontFetchError, setFontFetchError] = useState('');
|
|
894
|
+
const [fontRefreshIndex, setFontRefreshIndex] = useState(0);
|
|
895
|
+
const saveStatusTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
896
|
+
const saveMessageKind = useRef<'none' | 'load' | 'sync' | 'status'>('none');
|
|
897
|
+
const [explicitFontRoles, setExplicitFontRoles] = useState<Record<FontRoleKey, boolean>>({
|
|
898
|
+
fontDisplay: true,
|
|
899
|
+
fontTitle: true,
|
|
900
|
+
fontSubtitle: true,
|
|
901
|
+
fontBody: true,
|
|
902
|
+
fontCaption: true,
|
|
903
|
+
fontMono: true,
|
|
904
|
+
});
|
|
905
|
+
const saveButtonScale = useMemo(() => new Animated.Value(1), []);
|
|
906
|
+
const saveBannerOpacity = useMemo(() => new Animated.Value(1), []);
|
|
907
|
+
|
|
908
|
+
function pulseSaveButton() {
|
|
909
|
+
saveButtonScale.stopAnimation();
|
|
910
|
+
Animated.sequence([
|
|
911
|
+
Animated.timing(saveButtonScale, {
|
|
912
|
+
toValue: 0.96,
|
|
913
|
+
duration: 90,
|
|
914
|
+
easing: Easing.out(Easing.quad),
|
|
915
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
916
|
+
}),
|
|
917
|
+
Animated.spring(saveButtonScale, {
|
|
918
|
+
toValue: 1,
|
|
919
|
+
speed: 16,
|
|
920
|
+
bounciness: 6,
|
|
921
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
922
|
+
}),
|
|
923
|
+
]).start();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function publishSaveMessage(tone: SaveMessageTone, message: string) {
|
|
927
|
+
if (saveStatusTimeout.current) {
|
|
928
|
+
clearTimeout(saveStatusTimeout.current);
|
|
929
|
+
saveStatusTimeout.current = null;
|
|
930
|
+
}
|
|
931
|
+
const stamp = new Date().toLocaleTimeString([], {
|
|
932
|
+
hour: '2-digit',
|
|
933
|
+
minute: '2-digit',
|
|
934
|
+
second: '2-digit',
|
|
935
|
+
hour12: false,
|
|
936
|
+
});
|
|
937
|
+
setSaveMessageTone(tone);
|
|
938
|
+
setSaveMessage(`${message} (${stamp})`);
|
|
939
|
+
setSaveMessageNonce((value) => value + 1);
|
|
940
|
+
saveBannerOpacity.stopAnimation();
|
|
941
|
+
saveBannerOpacity.setValue(0.2);
|
|
942
|
+
Animated.timing(saveBannerOpacity, {
|
|
943
|
+
toValue: 1,
|
|
944
|
+
duration: 220,
|
|
945
|
+
easing: Easing.out(Easing.quad),
|
|
946
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
947
|
+
}).start();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function publishSyncMessage(message: string) {
|
|
951
|
+
saveMessageKind.current = 'sync';
|
|
952
|
+
publishSaveMessage('success', message);
|
|
953
|
+
writeWebSyncMessageSnapshot(message);
|
|
954
|
+
saveStatusTimeout.current = setTimeout(() => {
|
|
955
|
+
saveMessageKind.current = 'status';
|
|
956
|
+
clearWebSyncMessageSnapshot();
|
|
957
|
+
publishSaveMessage('info', 'Current theme files and preview are in sync.');
|
|
958
|
+
}, 3000);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
useEffect(() => {
|
|
962
|
+
if (Platform.OS !== 'web') {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const restored = readWebSyncMessageSnapshot();
|
|
967
|
+
if (restored) {
|
|
968
|
+
saveMessageKind.current = 'sync';
|
|
969
|
+
publishSaveMessage('success', restored.message);
|
|
970
|
+
const elapsed = Date.now() - restored.timestamp;
|
|
971
|
+
const remaining = Math.max(0, 3000 - elapsed);
|
|
972
|
+
saveStatusTimeout.current = setTimeout(() => {
|
|
973
|
+
saveMessageKind.current = 'status';
|
|
974
|
+
clearWebSyncMessageSnapshot();
|
|
975
|
+
publishSaveMessage('info', 'Current theme files and preview are in sync.');
|
|
976
|
+
}, remaining);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
let cancelled = false;
|
|
980
|
+
async function hydrateWritePolicy() {
|
|
981
|
+
try {
|
|
982
|
+
const response = await fetch('/exposition/stylist-sync');
|
|
983
|
+
if (!response.ok) {
|
|
984
|
+
if (!cancelled) {
|
|
985
|
+
setWritePolicyLoaded(true);
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const payload = (await response.json()) as {
|
|
990
|
+
hasConfig?: boolean;
|
|
991
|
+
writePolicy?: WritePolicy;
|
|
992
|
+
theme?: StylistThemeTokens;
|
|
993
|
+
mismatchDetected?: boolean;
|
|
994
|
+
themeSource?: 'theme.json' | 'style.md' | 'default';
|
|
995
|
+
};
|
|
996
|
+
if (cancelled) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (payload.theme) {
|
|
1000
|
+
const nextTheme = withFontFamilyAlias(
|
|
1001
|
+
normalizeThemeTypography(
|
|
1002
|
+
reconcileTheme(payload.theme, 'picker', defaultBgFamilies, defaultBgFamilyShades)
|
|
1003
|
+
)
|
|
1004
|
+
);
|
|
1005
|
+
setTheme((prev) => (areThemesEqual(prev, nextTheme) ? prev : nextTheme));
|
|
1006
|
+
setAppTheme(nextTheme);
|
|
1007
|
+
setExplicitFontRoles({
|
|
1008
|
+
fontDisplay: true,
|
|
1009
|
+
fontTitle: true,
|
|
1010
|
+
fontSubtitle: true,
|
|
1011
|
+
fontBody: true,
|
|
1012
|
+
fontCaption: true,
|
|
1013
|
+
fontMono: true,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
if (payload.hasConfig && payload.writePolicy) {
|
|
1017
|
+
setWritePolicy(payload.writePolicy);
|
|
1018
|
+
}
|
|
1019
|
+
if (payload.themeSource === 'style.md' && payload.mismatchDetected) {
|
|
1020
|
+
if (saveMessageKind.current === 'none') {
|
|
1021
|
+
saveMessageKind.current = 'load';
|
|
1022
|
+
setSaveMessageTone('info');
|
|
1023
|
+
setSaveMessage(
|
|
1024
|
+
'Detected mismatch between project/style.md managed block and project/theme.json. Loaded project/style.md by startup priority.'
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
} else if (payload.themeSource === 'style.md') {
|
|
1028
|
+
if (saveMessageKind.current === 'none') {
|
|
1029
|
+
saveMessageKind.current = 'load';
|
|
1030
|
+
setSaveMessageTone('info');
|
|
1031
|
+
setSaveMessage('Loaded theme from project/style.md managed block.');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} finally {
|
|
1035
|
+
if (!cancelled) {
|
|
1036
|
+
setWritePolicyLoaded(true);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
void hydrateWritePolicy();
|
|
1042
|
+
return () => {
|
|
1043
|
+
cancelled = true;
|
|
1044
|
+
};
|
|
1045
|
+
}, [setAppTheme]);
|
|
1046
|
+
|
|
1047
|
+
const activeScheme = theme.colorSystem.previewScheme;
|
|
1048
|
+
const basePreviewColors = theme.colors[activeScheme];
|
|
1049
|
+
const baseEditablePalette =
|
|
1050
|
+
theme.colorSystem.mode === 'automatic'
|
|
1051
|
+
? theme.palettes.automatic[activeScheme]
|
|
1052
|
+
: theme.palettes.bg[activeScheme];
|
|
1053
|
+
const livePickerMatchesActiveView =
|
|
1054
|
+
livePickerPreview?.scheme === activeScheme &&
|
|
1055
|
+
livePickerPreview.colorMode === theme.colorSystem.mode &&
|
|
1056
|
+
livePickerPreview.colorInputMode === colorInputMode;
|
|
1057
|
+
const previewColors = useMemo(() => {
|
|
1058
|
+
if (!livePickerPreview || !livePickerMatchesActiveView) {
|
|
1059
|
+
return basePreviewColors;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (theme.colorSystem.mode === 'automatic') {
|
|
1063
|
+
return ensureDistinctBackgroundSurface(
|
|
1064
|
+
deriveAutomaticPaletteFromPrimary(
|
|
1065
|
+
{
|
|
1066
|
+
...theme.palettes.automatic[activeScheme],
|
|
1067
|
+
[livePickerPreview.key]: livePickerPreview.hex,
|
|
1068
|
+
},
|
|
1069
|
+
activeScheme
|
|
1070
|
+
),
|
|
1071
|
+
activeScheme
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return ensureDistinctBackgroundSurface(
|
|
1076
|
+
{
|
|
1077
|
+
...theme.palettes.bg[activeScheme],
|
|
1078
|
+
[livePickerPreview.key]: livePickerPreview.hex,
|
|
1079
|
+
},
|
|
1080
|
+
activeScheme
|
|
1081
|
+
);
|
|
1082
|
+
}, [
|
|
1083
|
+
activeScheme,
|
|
1084
|
+
basePreviewColors,
|
|
1085
|
+
livePickerMatchesActiveView,
|
|
1086
|
+
livePickerPreview,
|
|
1087
|
+
theme.colorSystem.mode,
|
|
1088
|
+
theme.palettes.automatic,
|
|
1089
|
+
theme.palettes.bg,
|
|
1090
|
+
]);
|
|
1091
|
+
const controlTextColor = ensureReadableTextColor(
|
|
1092
|
+
previewColors.text,
|
|
1093
|
+
previewColors.surface,
|
|
1094
|
+
activeScheme
|
|
1095
|
+
);
|
|
1096
|
+
const editablePalette = useMemo(() => {
|
|
1097
|
+
if (!livePickerPreview || !livePickerMatchesActiveView) {
|
|
1098
|
+
return baseEditablePalette;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return {
|
|
1102
|
+
...baseEditablePalette,
|
|
1103
|
+
[livePickerPreview.key]: livePickerPreview.hex,
|
|
1104
|
+
};
|
|
1105
|
+
}, [baseEditablePalette, livePickerMatchesActiveView, livePickerPreview]);
|
|
1106
|
+
const pickerTargetKey = getPickerTargetKey();
|
|
1107
|
+
const pickerDisplayHex = editablePalette[pickerTargetKey].toLowerCase();
|
|
1108
|
+
const activeFamilyScheme: StylistColorScheme =
|
|
1109
|
+
theme.colorSystem.familyMode === 'one' ? 'light' : activeScheme;
|
|
1110
|
+
const activeFamilies = theme.families[activeFamilyScheme];
|
|
1111
|
+
const activeBgFamilies = bgFamilies[activeFamilyScheme];
|
|
1112
|
+
const activeBgFamilyShades = bgFamilyShades[activeFamilyScheme];
|
|
1113
|
+
|
|
1114
|
+
const previewCard = useMemo(
|
|
1115
|
+
() => ({
|
|
1116
|
+
backgroundColor: previewColors.surface,
|
|
1117
|
+
borderColor: previewColors.primary,
|
|
1118
|
+
borderRadius: theme.layout.radius,
|
|
1119
|
+
borderWidth: 1,
|
|
1120
|
+
padding: theme.layout.spacing.md,
|
|
1121
|
+
gap: theme.layout.spacing.sm,
|
|
1122
|
+
}),
|
|
1123
|
+
[previewColors, theme.layout]
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
const galleryTokens = useMemo(
|
|
1127
|
+
() => ({
|
|
1128
|
+
rowGap: theme.layout.spacing.sm,
|
|
1129
|
+
compactGap: theme.layout.spacing.xs,
|
|
1130
|
+
cardPadding: theme.layout.spacing.md,
|
|
1131
|
+
sectionPadding: theme.layout.spacing.lg,
|
|
1132
|
+
pillPaddingHorizontal: theme.layout.spacing.sm,
|
|
1133
|
+
pillPaddingVertical: Math.max(4, theme.layout.spacing.xs),
|
|
1134
|
+
inputGap: theme.layout.spacing.sm,
|
|
1135
|
+
inputMinHeight: Math.max(42, theme.layout.spacing.xl),
|
|
1136
|
+
radius: theme.layout.radius,
|
|
1137
|
+
}),
|
|
1138
|
+
[theme.layout]
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
const availableFontFamilies = useMemo(() => {
|
|
1142
|
+
const merged = new Map<string, string>();
|
|
1143
|
+
for (const family of [...builtInFontChoices, ...EMBEDDED_GOOGLE_FONTS, ...remoteFonts]) {
|
|
1144
|
+
const normalized = normalizeFontFamilyName(family);
|
|
1145
|
+
if (!normalized) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const key = normalized.toLowerCase();
|
|
1149
|
+
if (!merged.has(key)) {
|
|
1150
|
+
merged.set(key, normalized);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return Array.from(merged.values()).sort((a, b) => a.localeCompare(b));
|
|
1154
|
+
}, [remoteFonts]);
|
|
1155
|
+
|
|
1156
|
+
useEffect(() => {
|
|
1157
|
+
setTheme((prev) => {
|
|
1158
|
+
const nextTheme = withFontFamilyAlias(
|
|
1159
|
+
normalizeThemeTypography(reconcileTheme(prev, colorInputMode, bgFamilies, bgFamilyShades))
|
|
1160
|
+
);
|
|
1161
|
+
return areThemesEqual(prev, nextTheme) ? prev : nextTheme;
|
|
1162
|
+
});
|
|
1163
|
+
}, [bgFamilies, bgFamilyShades, colorInputMode]);
|
|
1164
|
+
|
|
1165
|
+
useEffect(() => {
|
|
1166
|
+
const lockedInAutomaticPicker = isLockedAutomaticPickerKey(
|
|
1167
|
+
theme.colorSystem.mode,
|
|
1168
|
+
colorInputMode,
|
|
1169
|
+
selectedColor
|
|
1170
|
+
);
|
|
1171
|
+
if (lockedInAutomaticPicker) {
|
|
1172
|
+
setSelectedColor('primary');
|
|
1173
|
+
}
|
|
1174
|
+
}, [colorInputMode, selectedColor, theme.colorSystem.mode]);
|
|
1175
|
+
|
|
1176
|
+
useEffect(() => {
|
|
1177
|
+
setPickerHexDraft((prev) => (prev === pickerDisplayHex ? prev : pickerDisplayHex));
|
|
1178
|
+
}, [pickerDisplayHex]);
|
|
1179
|
+
|
|
1180
|
+
useEffect(() => {
|
|
1181
|
+
let isMounted = true;
|
|
1182
|
+
|
|
1183
|
+
async function hydrateFontSettings() {
|
|
1184
|
+
try {
|
|
1185
|
+
const [savedKey, dismissed] = await Promise.all([
|
|
1186
|
+
AsyncStorage.getItem(GOOGLE_FONTS_KEY_STORAGE),
|
|
1187
|
+
AsyncStorage.getItem(GOOGLE_FONTS_BANNER_DISMISSED_STORAGE),
|
|
1188
|
+
]);
|
|
1189
|
+
if (!isMounted) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const nextKey = savedKey?.trim() ?? '';
|
|
1193
|
+
setStoredApiKey(nextKey);
|
|
1194
|
+
setApiKeyDraft(nextKey);
|
|
1195
|
+
setFontBannerDismissed(dismissed === 'true' && !nextKey);
|
|
1196
|
+
} catch {
|
|
1197
|
+
if (isMounted) {
|
|
1198
|
+
setFontBannerDismissed(false);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
void hydrateFontSettings();
|
|
1204
|
+
|
|
1205
|
+
return () => {
|
|
1206
|
+
isMounted = false;
|
|
1207
|
+
};
|
|
1208
|
+
}, []);
|
|
1209
|
+
|
|
1210
|
+
useEffect(() => {
|
|
1211
|
+
if (Platform.OS !== 'web') {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
ensureWebFontLoaded(theme.typography.fontDisplay);
|
|
1216
|
+
ensureWebFontLoaded(theme.typography.fontTitle);
|
|
1217
|
+
ensureWebFontLoaded(theme.typography.fontSubtitle);
|
|
1218
|
+
ensureWebFontLoaded(theme.typography.fontBody);
|
|
1219
|
+
ensureWebFontLoaded(theme.typography.fontCaption);
|
|
1220
|
+
ensureWebFontLoaded(theme.typography.fontMono);
|
|
1221
|
+
}, [
|
|
1222
|
+
theme.typography.fontDisplay,
|
|
1223
|
+
theme.typography.fontTitle,
|
|
1224
|
+
theme.typography.fontSubtitle,
|
|
1225
|
+
theme.typography.fontBody,
|
|
1226
|
+
theme.typography.fontCaption,
|
|
1227
|
+
theme.typography.fontMono,
|
|
1228
|
+
]);
|
|
1229
|
+
|
|
1230
|
+
useEffect(() => {
|
|
1231
|
+
let cancelled = false;
|
|
1232
|
+
|
|
1233
|
+
async function loadLiveFonts(apiKey: string) {
|
|
1234
|
+
if (!apiKey) {
|
|
1235
|
+
setRemoteFonts([]);
|
|
1236
|
+
setFontFetchError('');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
setLoadingFonts(true);
|
|
1241
|
+
setFontFetchError('');
|
|
1242
|
+
try {
|
|
1243
|
+
const response = await fetch(
|
|
1244
|
+
`${GOOGLE_FONTS_API_URL}?sort=popularity&key=${encodeURIComponent(apiKey)}`
|
|
1245
|
+
);
|
|
1246
|
+
const payload = (await response.json()) as {
|
|
1247
|
+
items?: Array<{ family?: string }>;
|
|
1248
|
+
error?: { message?: string };
|
|
1249
|
+
};
|
|
1250
|
+
if (!response.ok) {
|
|
1251
|
+
throw new Error(payload?.error?.message ?? 'Could not load Google Fonts list.');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const fetched = (payload.items ?? [])
|
|
1255
|
+
.map((item) => normalizeFontFamilyName(item.family ?? ''))
|
|
1256
|
+
.filter((value) => value.length > 0);
|
|
1257
|
+
|
|
1258
|
+
if (!cancelled) {
|
|
1259
|
+
setRemoteFonts(fetched);
|
|
1260
|
+
setFontFetchError('');
|
|
1261
|
+
}
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
if (!cancelled) {
|
|
1264
|
+
setRemoteFonts([]);
|
|
1265
|
+
setFontFetchError(
|
|
1266
|
+
error instanceof Error ? error.message : 'Could not load Google Fonts list.'
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
} finally {
|
|
1270
|
+
if (!cancelled) {
|
|
1271
|
+
setLoadingFonts(false);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
void loadLiveFonts(storedApiKey);
|
|
1277
|
+
|
|
1278
|
+
return () => {
|
|
1279
|
+
cancelled = true;
|
|
1280
|
+
};
|
|
1281
|
+
}, [storedApiKey, fontRefreshIndex]);
|
|
1282
|
+
|
|
1283
|
+
function updateTheme(mutator: (prev: ExtendedStylistThemeTokens) => ExtendedStylistThemeTokens) {
|
|
1284
|
+
setTheme((prev) =>
|
|
1285
|
+
withFontFamilyAlias(
|
|
1286
|
+
normalizeThemeTypography(
|
|
1287
|
+
reconcileTheme(mutator(prev), colorInputMode, bgFamilies, bgFamilyShades)
|
|
1288
|
+
)
|
|
1289
|
+
)
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function updateNumeric(path: string, raw: string) {
|
|
1294
|
+
const value = Number.parseFloat(raw);
|
|
1295
|
+
if (!Number.isFinite(value)) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
updateTheme((prev) => {
|
|
1300
|
+
if (path === 'displaySize') {
|
|
1301
|
+
return {
|
|
1302
|
+
...prev,
|
|
1303
|
+
typography: { ...prev.typography, displaySize: value },
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
if (path === 'headingSize') {
|
|
1307
|
+
return {
|
|
1308
|
+
...prev,
|
|
1309
|
+
typography: { ...prev.typography, headingSize: value },
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (path === 'bodySize') {
|
|
1313
|
+
return { ...prev, typography: { ...prev.typography, bodySize: value } };
|
|
1314
|
+
}
|
|
1315
|
+
if (path === 'captionSize') {
|
|
1316
|
+
return {
|
|
1317
|
+
...prev,
|
|
1318
|
+
typography: { ...prev.typography, captionSize: value },
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
if (path === 'radius') {
|
|
1322
|
+
return { ...prev, layout: { ...prev.layout, radius: value } };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return {
|
|
1326
|
+
...prev,
|
|
1327
|
+
layout: {
|
|
1328
|
+
...prev.layout,
|
|
1329
|
+
spacing: { ...prev.layout.spacing, [path]: value },
|
|
1330
|
+
},
|
|
1331
|
+
};
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function updateColorMode(mode: StylistColorMode) {
|
|
1336
|
+
updateTheme((prev) => ({
|
|
1337
|
+
...prev,
|
|
1338
|
+
colorSystem: {
|
|
1339
|
+
...prev.colorSystem,
|
|
1340
|
+
mode,
|
|
1341
|
+
},
|
|
1342
|
+
}));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function updatePreviewScheme(scheme: StylistColorScheme) {
|
|
1346
|
+
updateTheme((prev) => ({
|
|
1347
|
+
...prev,
|
|
1348
|
+
colorSystem: {
|
|
1349
|
+
...prev.colorSystem,
|
|
1350
|
+
previewScheme: scheme,
|
|
1351
|
+
},
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function updateFamilyMode(mode: StylistFamilyMode) {
|
|
1356
|
+
setBgFamilies((prev) => (mode === 'one' ? { ...prev, dark: { ...prev.light } } : prev));
|
|
1357
|
+
setBgFamilyShades((prev) => (mode === 'one' ? { ...prev, dark: { ...prev.light } } : prev));
|
|
1358
|
+
updateTheme((prev) => ({
|
|
1359
|
+
...prev,
|
|
1360
|
+
colorSystem: {
|
|
1361
|
+
...prev.colorSystem,
|
|
1362
|
+
familyMode: mode,
|
|
1363
|
+
},
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function updateFamily(key: SemanticColorKey, family: string) {
|
|
1368
|
+
updateTheme((prev) => {
|
|
1369
|
+
if (prev.colorSystem.familyMode === 'one') {
|
|
1370
|
+
const nextLight = { ...prev.families.light, [key]: family };
|
|
1371
|
+
return {
|
|
1372
|
+
...prev,
|
|
1373
|
+
families: {
|
|
1374
|
+
light: nextLight,
|
|
1375
|
+
dark: { ...nextLight },
|
|
1376
|
+
},
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
return {
|
|
1381
|
+
...prev,
|
|
1382
|
+
families: {
|
|
1383
|
+
...prev.families,
|
|
1384
|
+
[activeScheme]: {
|
|
1385
|
+
...prev.families[activeScheme],
|
|
1386
|
+
[key]: family,
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function updateBgFamily(key: PaletteColorKey, family: TailwindColorFamily) {
|
|
1394
|
+
setBgFamilies((prev) => {
|
|
1395
|
+
if (theme.colorSystem.familyMode === 'one') {
|
|
1396
|
+
const nextLight = { ...prev.light, [key]: family };
|
|
1397
|
+
return {
|
|
1398
|
+
light: nextLight,
|
|
1399
|
+
dark: { ...nextLight },
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
...prev,
|
|
1405
|
+
[activeScheme]: {
|
|
1406
|
+
...prev[activeScheme],
|
|
1407
|
+
[key]: family,
|
|
1408
|
+
},
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function updateBgShade(key: PaletteColorKey, shade: TailwindShade) {
|
|
1414
|
+
setBgFamilyShades((prev) => {
|
|
1415
|
+
if (theme.colorSystem.familyMode === 'one') {
|
|
1416
|
+
const nextLight = { ...prev.light, [key]: shade };
|
|
1417
|
+
return {
|
|
1418
|
+
light: nextLight,
|
|
1419
|
+
dark: { ...nextLight },
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
...prev,
|
|
1425
|
+
[activeScheme]: {
|
|
1426
|
+
...prev[activeScheme],
|
|
1427
|
+
[key]: shade,
|
|
1428
|
+
},
|
|
1429
|
+
};
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function updateManualColor(key: PaletteColorKey, hex: string) {
|
|
1434
|
+
const targetPalette = theme.colorSystem.mode === 'automatic' ? 'automatic' : 'bg';
|
|
1435
|
+
updateTheme((prev) => ({
|
|
1436
|
+
...prev,
|
|
1437
|
+
palettes: {
|
|
1438
|
+
...prev.palettes,
|
|
1439
|
+
[targetPalette]: {
|
|
1440
|
+
...prev.palettes[targetPalette],
|
|
1441
|
+
...(prev.colorSystem.familyMode === 'one'
|
|
1442
|
+
? {
|
|
1443
|
+
light: {
|
|
1444
|
+
...prev.palettes[targetPalette].light,
|
|
1445
|
+
[key]: hex,
|
|
1446
|
+
},
|
|
1447
|
+
dark: {
|
|
1448
|
+
...prev.palettes[targetPalette].dark,
|
|
1449
|
+
[key]: hex,
|
|
1450
|
+
},
|
|
1451
|
+
}
|
|
1452
|
+
: {
|
|
1453
|
+
[activeScheme]: {
|
|
1454
|
+
...prev.palettes[targetPalette][activeScheme],
|
|
1455
|
+
[key]: hex,
|
|
1456
|
+
},
|
|
1457
|
+
}),
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function getPickerTargetKey(): PaletteColorKey {
|
|
1464
|
+
return isLockedAutomaticPickerKey(theme.colorSystem.mode, colorInputMode, selectedColor)
|
|
1465
|
+
? 'primary'
|
|
1466
|
+
: selectedColor;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function previewPickerColor(hex: string) {
|
|
1470
|
+
const nextPreview = {
|
|
1471
|
+
colorInputMode,
|
|
1472
|
+
colorMode: theme.colorSystem.mode,
|
|
1473
|
+
hex: hex.toLowerCase(),
|
|
1474
|
+
key: getPickerTargetKey(),
|
|
1475
|
+
scheme: activeScheme,
|
|
1476
|
+
};
|
|
1477
|
+
setLivePickerPreview((prev) => {
|
|
1478
|
+
if (
|
|
1479
|
+
prev?.hex === nextPreview.hex &&
|
|
1480
|
+
prev.key === nextPreview.key &&
|
|
1481
|
+
prev.scheme === nextPreview.scheme &&
|
|
1482
|
+
prev.colorMode === nextPreview.colorMode &&
|
|
1483
|
+
prev.colorInputMode === nextPreview.colorInputMode
|
|
1484
|
+
) {
|
|
1485
|
+
return prev;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return nextPreview;
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function applyPickerColor(hex: string) {
|
|
1493
|
+
const targetKey = getPickerTargetKey();
|
|
1494
|
+
const nextHex = hex.toLowerCase();
|
|
1495
|
+
setLivePickerPreview(null);
|
|
1496
|
+
if (baseEditablePalette[targetKey].toLowerCase() === nextHex) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
updateManualColor(targetKey, nextHex);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function handlePickerHexChange(raw: string) {
|
|
1504
|
+
const nextDraft = sanitizeHexDraftInput(raw);
|
|
1505
|
+
setPickerHexDraft(nextDraft);
|
|
1506
|
+
if (!isImmediateApplyHex(nextDraft)) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const nextHex = normalizeHexForTheme(nextDraft);
|
|
1510
|
+
setPickerHexDraft(nextHex);
|
|
1511
|
+
applyPickerColor(nextHex);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function commitPickerHexDraft() {
|
|
1515
|
+
if (!isCommitReadyHex(pickerHexDraft)) {
|
|
1516
|
+
setPickerHexDraft(pickerDisplayHex);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const nextHex = normalizeHexForTheme(pickerHexDraft);
|
|
1520
|
+
setPickerHexDraft(nextHex);
|
|
1521
|
+
applyPickerColor(nextHex);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function updateFontRole(role: FontRoleKey, fontFamily: string) {
|
|
1525
|
+
const normalized = normalizeFontFamilyName(fontFamily);
|
|
1526
|
+
if (!normalized) {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
setExplicitFontRoles((prev) => ({ ...prev, [role]: true }));
|
|
1531
|
+
updateTheme((prev) => ({
|
|
1532
|
+
...prev,
|
|
1533
|
+
typography: {
|
|
1534
|
+
...prev.typography,
|
|
1535
|
+
[role]: normalized,
|
|
1536
|
+
fontFamily: role === 'fontDisplay' ? normalized : prev.typography.fontFamily,
|
|
1537
|
+
},
|
|
1538
|
+
}));
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function applyNotoFonts() {
|
|
1542
|
+
const notoRoles: Record<FontRoleKey, string> = {
|
|
1543
|
+
fontDisplay: 'Noto Sans Display',
|
|
1544
|
+
fontTitle: 'Noto Sans Display',
|
|
1545
|
+
fontSubtitle: 'Noto Serif Display',
|
|
1546
|
+
fontBody: 'Noto Sans',
|
|
1547
|
+
fontCaption: 'Noto Serif',
|
|
1548
|
+
fontMono: 'Noto Sans Mono',
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
setExplicitFontRoles({
|
|
1552
|
+
fontDisplay: true,
|
|
1553
|
+
fontTitle: true,
|
|
1554
|
+
fontSubtitle: true,
|
|
1555
|
+
fontBody: true,
|
|
1556
|
+
fontCaption: true,
|
|
1557
|
+
fontMono: true,
|
|
1558
|
+
});
|
|
1559
|
+
updateTheme((prev) => ({
|
|
1560
|
+
...prev,
|
|
1561
|
+
typography: {
|
|
1562
|
+
...prev.typography,
|
|
1563
|
+
...notoRoles,
|
|
1564
|
+
fontFamily: 'Noto Sans Display',
|
|
1565
|
+
},
|
|
1566
|
+
}));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
async function saveFontApiSettings() {
|
|
1570
|
+
const nextKey = apiKeyDraft.trim();
|
|
1571
|
+
try {
|
|
1572
|
+
await AsyncStorage.setItem(GOOGLE_FONTS_KEY_STORAGE, nextKey);
|
|
1573
|
+
await AsyncStorage.setItem(
|
|
1574
|
+
GOOGLE_FONTS_BANNER_DISMISSED_STORAGE,
|
|
1575
|
+
nextKey ? 'false' : String(fontBannerDismissed)
|
|
1576
|
+
);
|
|
1577
|
+
setStoredApiKey(nextKey);
|
|
1578
|
+
setFontBannerDismissed(false);
|
|
1579
|
+
publishSaveMessage(
|
|
1580
|
+
nextKey ? 'success' : 'info',
|
|
1581
|
+
nextKey ? 'Google Fonts key saved. Live list is refreshing.' : 'Google Fonts key cleared.'
|
|
1582
|
+
);
|
|
1583
|
+
} catch {
|
|
1584
|
+
publishSaveMessage('error', 'Unable to save Google Fonts API key on this device.');
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async function dismissFontBanner() {
|
|
1589
|
+
try {
|
|
1590
|
+
await AsyncStorage.setItem(GOOGLE_FONTS_BANNER_DISMISSED_STORAGE, 'true');
|
|
1591
|
+
} catch {
|
|
1592
|
+
// no-op
|
|
1593
|
+
}
|
|
1594
|
+
setFontBannerDismissed(true);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async function saveTheme(policyOverride?: WritePolicy) {
|
|
1598
|
+
const payloadTheme = withFontFamilyAlias(
|
|
1599
|
+
normalizeThemeTypography(reconcileTheme(theme, colorInputMode, bgFamilies, bgFamilyShades))
|
|
1600
|
+
);
|
|
1601
|
+
const resolvedPolicy = policyOverride ?? writePolicy ?? 'managed';
|
|
1602
|
+
pulseSaveButton();
|
|
1603
|
+
setSaving(true);
|
|
1604
|
+
publishSaveMessage('info', 'Saving theme...');
|
|
1605
|
+
try {
|
|
1606
|
+
const response = await fetch('/exposition/stylist-sync', {
|
|
1607
|
+
method: 'POST',
|
|
1608
|
+
headers: { 'content-type': 'application/json' },
|
|
1609
|
+
body: JSON.stringify({
|
|
1610
|
+
theme: payloadTheme,
|
|
1611
|
+
metadata: {
|
|
1612
|
+
writePolicy: resolvedPolicy,
|
|
1613
|
+
},
|
|
1614
|
+
}),
|
|
1615
|
+
});
|
|
1616
|
+
const payload = await response.json();
|
|
1617
|
+
if (!response.ok) {
|
|
1618
|
+
throw new Error(payload?.error ?? 'Stylist sync failed.');
|
|
1619
|
+
}
|
|
1620
|
+
const readBackResponse = await fetch('/exposition/stylist-sync', {
|
|
1621
|
+
method: 'GET',
|
|
1622
|
+
headers: { 'cache-control': 'no-store' },
|
|
1623
|
+
});
|
|
1624
|
+
let canonicalTheme = payloadTheme;
|
|
1625
|
+
if (readBackResponse.ok) {
|
|
1626
|
+
const readBackPayload = (await readBackResponse.json()) as {
|
|
1627
|
+
theme?: StylistThemeTokens;
|
|
1628
|
+
writePolicy?: WritePolicy;
|
|
1629
|
+
};
|
|
1630
|
+
if (readBackPayload.theme) {
|
|
1631
|
+
canonicalTheme = withFontFamilyAlias(
|
|
1632
|
+
normalizeThemeTypography(
|
|
1633
|
+
reconcileTheme(readBackPayload.theme, colorInputMode, bgFamilies, bgFamilyShades)
|
|
1634
|
+
)
|
|
1635
|
+
);
|
|
1636
|
+
setTheme((prev) => (areThemesEqual(prev, canonicalTheme) ? prev : canonicalTheme));
|
|
1637
|
+
setAppTheme(canonicalTheme);
|
|
1638
|
+
setExplicitFontRoles({
|
|
1639
|
+
fontDisplay: true,
|
|
1640
|
+
fontTitle: true,
|
|
1641
|
+
fontSubtitle: true,
|
|
1642
|
+
fontBody: true,
|
|
1643
|
+
fontCaption: true,
|
|
1644
|
+
fontMono: true,
|
|
1645
|
+
});
|
|
1646
|
+
} else {
|
|
1647
|
+
setAppTheme(payloadTheme);
|
|
1648
|
+
}
|
|
1649
|
+
setWritePolicy(readBackPayload.writePolicy ?? payload.writePolicy ?? resolvedPolicy);
|
|
1650
|
+
} else {
|
|
1651
|
+
setWritePolicy(payload.writePolicy ?? resolvedPolicy);
|
|
1652
|
+
setAppTheme(payloadTheme);
|
|
1653
|
+
}
|
|
1654
|
+
setNativeDraft('');
|
|
1655
|
+
publishSyncMessage(`Synced ${payload.updatedFiles?.length ?? 0} files from Stylist.`);
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
if (Platform.OS !== 'web') {
|
|
1658
|
+
const draft = JSON.stringify(payloadTheme, null, 2);
|
|
1659
|
+
setNativeDraft(draft);
|
|
1660
|
+
setWritePolicy(resolvedPolicy);
|
|
1661
|
+
setAppTheme(payloadTheme);
|
|
1662
|
+
publishSaveMessage(
|
|
1663
|
+
'success',
|
|
1664
|
+
'Draft saved in Stylist. Run the sync command from your project root terminal.'
|
|
1665
|
+
);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const message = humanizeSaveError(
|
|
1669
|
+
error instanceof Error ? error.message : 'Unknown save error.'
|
|
1670
|
+
);
|
|
1671
|
+
Alert.alert('Stylist save failed', message);
|
|
1672
|
+
publishSaveMessage('error', message);
|
|
1673
|
+
} finally {
|
|
1674
|
+
setSaving(false);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function handleSaveThemePress() {
|
|
1679
|
+
if (saving) {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
if (Platform.OS === 'web' && writePolicyLoaded && !writePolicy) {
|
|
1684
|
+
setShowWritePolicyModal(true);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
void saveTheme();
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function chooseWritePolicyAndSave(nextPolicy: WritePolicy) {
|
|
1692
|
+
setWritePolicy(nextPolicy);
|
|
1693
|
+
setShowWritePolicyModal(false);
|
|
1694
|
+
void saveTheme(nextPolicy);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const nativeSaveCommand = NATIVE_SAVE_COMMAND;
|
|
1698
|
+
const saveMessageColors =
|
|
1699
|
+
saveMessageTone === 'success'
|
|
1700
|
+
? { backgroundColor: '#dcfce7', borderColor: '#86efac', color: '#166534' }
|
|
1701
|
+
: saveMessageTone === 'error'
|
|
1702
|
+
? {
|
|
1703
|
+
backgroundColor: '#fee2e2',
|
|
1704
|
+
borderColor: '#fca5a5',
|
|
1705
|
+
color: '#991b1b',
|
|
1706
|
+
}
|
|
1707
|
+
: {
|
|
1708
|
+
backgroundColor: '#dbeafe',
|
|
1709
|
+
borderColor: '#93c5fd',
|
|
1710
|
+
color: '#1e3a8a',
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
return (
|
|
1714
|
+
<View style={styles.root}>
|
|
1715
|
+
<ScrollView
|
|
1716
|
+
contentInsetAdjustmentBehavior="automatic"
|
|
1717
|
+
keyboardShouldPersistTaps="handled"
|
|
1718
|
+
contentContainerStyle={[
|
|
1719
|
+
styles.content,
|
|
1720
|
+
{
|
|
1721
|
+
paddingTop: Platform.OS === 'web' ? 220 : Math.max(insets.top + 132, 160),
|
|
1722
|
+
paddingBottom: Math.max(insets.bottom + 40, 96),
|
|
1723
|
+
},
|
|
1724
|
+
]}
|
|
1725
|
+
style={[styles.screen, { backgroundColor: previewColors.background }]}
|
|
1726
|
+
>
|
|
1727
|
+
<View style={styles.titleRow}>
|
|
1728
|
+
<Text style={[styles.title, { color: previewColors.text }]}>
|
|
1729
|
+
__MDS_APP_NAME__ Stylist
|
|
1730
|
+
</Text>
|
|
1731
|
+
<Animated.View style={{ transform: [{ scale: saveButtonScale }] }}>
|
|
1732
|
+
<Pressable
|
|
1733
|
+
onPress={handleSaveThemePress}
|
|
1734
|
+
disabled={saving}
|
|
1735
|
+
style={[
|
|
1736
|
+
styles.saveButton,
|
|
1737
|
+
styles.saveButtonInline,
|
|
1738
|
+
{ backgroundColor: previewColors.primary },
|
|
1739
|
+
]}
|
|
1740
|
+
>
|
|
1741
|
+
<Text style={styles.saveButtonText}>{saving ? 'Saving...' : 'Save Theme'}</Text>
|
|
1742
|
+
</Pressable>
|
|
1743
|
+
</Animated.View>
|
|
1744
|
+
</View>
|
|
1745
|
+
<Text style={[styles.intro, { color: previewColors.text }]}>
|
|
1746
|
+
Adjust design tokens - do not forget to hit Save Theme so the changes will be applied to
|
|
1747
|
+
your app color theme files.
|
|
1748
|
+
</Text>
|
|
1749
|
+
{saveMessage ? (
|
|
1750
|
+
<Animated.View
|
|
1751
|
+
key={`save-message-${saveMessageNonce}`}
|
|
1752
|
+
style={[
|
|
1753
|
+
styles.saveMessageBanner,
|
|
1754
|
+
{
|
|
1755
|
+
backgroundColor: saveMessageColors.backgroundColor,
|
|
1756
|
+
borderColor: saveMessageColors.borderColor,
|
|
1757
|
+
opacity: saveBannerOpacity,
|
|
1758
|
+
},
|
|
1759
|
+
]}
|
|
1760
|
+
>
|
|
1761
|
+
<Text style={[styles.saveMessageText, { color: saveMessageColors.color }]}>
|
|
1762
|
+
{saveMessage}
|
|
1763
|
+
</Text>
|
|
1764
|
+
</Animated.View>
|
|
1765
|
+
) : null}
|
|
1766
|
+
{!fontBannerDismissed || !storedApiKey ? (
|
|
1767
|
+
<View style={[styles.fontBanner, { borderColor: previewColors.primary }]}>
|
|
1768
|
+
<View style={styles.fontBannerHeader}>
|
|
1769
|
+
<Text style={styles.fontBannerTitle}>Google Fonts API Key (Optional)</Text>
|
|
1770
|
+
<Pressable onPress={dismissFontBanner} style={styles.bannerDismissButton}>
|
|
1771
|
+
<Text style={styles.bannerDismissText}>X</Text>
|
|
1772
|
+
</Pressable>
|
|
1773
|
+
</View>
|
|
1774
|
+
<Text style={styles.fontBannerBody}>
|
|
1775
|
+
Embedded curated fonts are already built in. Add your Google Fonts API key only if you
|
|
1776
|
+
want the live catalog sync. System and common fallback fonts are included too.
|
|
1777
|
+
</Text>
|
|
1778
|
+
<TextInput
|
|
1779
|
+
value={apiKeyDraft}
|
|
1780
|
+
onChangeText={setApiKeyDraft}
|
|
1781
|
+
placeholder="Paste Google Fonts API key"
|
|
1782
|
+
autoCapitalize="none"
|
|
1783
|
+
autoCorrect={false}
|
|
1784
|
+
style={styles.input}
|
|
1785
|
+
/>
|
|
1786
|
+
<View style={styles.fontBannerActions}>
|
|
1787
|
+
<Pressable
|
|
1788
|
+
onPress={saveFontApiSettings}
|
|
1789
|
+
style={[styles.bannerAction, { backgroundColor: previewColors.primary }]}
|
|
1790
|
+
>
|
|
1791
|
+
<Text style={styles.bannerActionText}>Save Key</Text>
|
|
1792
|
+
</Pressable>
|
|
1793
|
+
<Pressable
|
|
1794
|
+
onPress={() => setFontRefreshIndex((prev) => prev + 1)}
|
|
1795
|
+
style={[styles.bannerAction, styles.bannerActionGhost]}
|
|
1796
|
+
>
|
|
1797
|
+
<Text style={styles.bannerActionGhostText}>Refresh List</Text>
|
|
1798
|
+
</Pressable>
|
|
1799
|
+
</View>
|
|
1800
|
+
{loadingFonts ? (
|
|
1801
|
+
<Text style={styles.fontBannerBody}>Loading live Google Fonts list...</Text>
|
|
1802
|
+
) : null}
|
|
1803
|
+
{fontFetchError ? <Text style={styles.fontError}>{fontFetchError}</Text> : null}
|
|
1804
|
+
</View>
|
|
1805
|
+
) : null}
|
|
1806
|
+
<View
|
|
1807
|
+
style={[
|
|
1808
|
+
styles.section,
|
|
1809
|
+
styles.sectionOverlay,
|
|
1810
|
+
{
|
|
1811
|
+
backgroundColor: previewColors.surface,
|
|
1812
|
+
borderRadius: theme.layout.radius,
|
|
1813
|
+
},
|
|
1814
|
+
]}
|
|
1815
|
+
>
|
|
1816
|
+
<View style={styles.sectionHeaderRow}>
|
|
1817
|
+
<Text style={[styles.sectionTitle, { color: controlTextColor }]}>Colors</Text>
|
|
1818
|
+
<View style={styles.inlineToggle}>
|
|
1819
|
+
{colorInputModeOptions.map((option) => {
|
|
1820
|
+
const isSelected = colorInputMode === option.value;
|
|
1821
|
+
return (
|
|
1822
|
+
<Pressable
|
|
1823
|
+
key={option.value}
|
|
1824
|
+
onPress={() => setColorInputMode(option.value)}
|
|
1825
|
+
style={[
|
|
1826
|
+
styles.inlineToggleOption,
|
|
1827
|
+
isSelected && styles.inlineToggleOptionSelected,
|
|
1828
|
+
]}
|
|
1829
|
+
>
|
|
1830
|
+
<Text
|
|
1831
|
+
style={[
|
|
1832
|
+
styles.inlineToggleLabel,
|
|
1833
|
+
{ color: isSelected ? '#f8fafc' : previewColors.text },
|
|
1834
|
+
]}
|
|
1835
|
+
>
|
|
1836
|
+
{option.label}
|
|
1837
|
+
</Text>
|
|
1838
|
+
</Pressable>
|
|
1839
|
+
);
|
|
1840
|
+
})}
|
|
1841
|
+
</View>
|
|
1842
|
+
</View>
|
|
1843
|
+
|
|
1844
|
+
<Text style={[styles.helperText, { color: controlTextColor }]}>
|
|
1845
|
+
{theme.colorSystem.mode === 'automatic'
|
|
1846
|
+
? 'Automatic mode derives background, surface, and text from the primary color in real time.'
|
|
1847
|
+
: theme.colorSystem.familyMode === 'one'
|
|
1848
|
+
? 'BG mode lets you set one shared palette for both light and dark previews.'
|
|
1849
|
+
: `BG mode lets you set palette colors directly for the active ${activeScheme} scheme.`}
|
|
1850
|
+
</Text>
|
|
1851
|
+
|
|
1852
|
+
{theme.colorSystem.mode === 'automatic' && colorInputMode === 'picker' ? (
|
|
1853
|
+
<Text style={[styles.helperText, styles.lockedNote, { color: controlTextColor }]}>
|
|
1854
|
+
Background, surface, and text are disabled here because they are derived from the
|
|
1855
|
+
primary color.
|
|
1856
|
+
</Text>
|
|
1857
|
+
) : null}
|
|
1858
|
+
|
|
1859
|
+
{colorInputMode === 'picker' ? (
|
|
1860
|
+
<View style={styles.colorRow}>
|
|
1861
|
+
{paletteColorKeys.map((key) => {
|
|
1862
|
+
const locked =
|
|
1863
|
+
theme.colorSystem.mode === 'automatic' &&
|
|
1864
|
+
colorInputMode === 'picker' &&
|
|
1865
|
+
automaticLockedKeys.includes(key);
|
|
1866
|
+
const isSelected = selectedColor === key;
|
|
1867
|
+
|
|
1868
|
+
return (
|
|
1869
|
+
<Pressable
|
|
1870
|
+
key={key}
|
|
1871
|
+
disabled={locked}
|
|
1872
|
+
onPress={() => setSelectedColor(key)}
|
|
1873
|
+
style={[
|
|
1874
|
+
styles.colorChip,
|
|
1875
|
+
{
|
|
1876
|
+
backgroundColor: editablePalette[key],
|
|
1877
|
+
borderColor: isSelected ? previewColors.text : '#9ca3af',
|
|
1878
|
+
opacity: locked ? 0.45 : 1,
|
|
1879
|
+
},
|
|
1880
|
+
]}
|
|
1881
|
+
>
|
|
1882
|
+
<Text
|
|
1883
|
+
style={[
|
|
1884
|
+
styles.colorChipLabel,
|
|
1885
|
+
{
|
|
1886
|
+
color: getReadableChipTextColor(editablePalette[key]),
|
|
1887
|
+
},
|
|
1888
|
+
]}
|
|
1889
|
+
>
|
|
1890
|
+
{key}
|
|
1891
|
+
</Text>
|
|
1892
|
+
</Pressable>
|
|
1893
|
+
);
|
|
1894
|
+
})}
|
|
1895
|
+
</View>
|
|
1896
|
+
) : null}
|
|
1897
|
+
|
|
1898
|
+
{colorInputMode === 'picker' ? (
|
|
1899
|
+
<>
|
|
1900
|
+
<ColorPicker
|
|
1901
|
+
value={baseEditablePalette[pickerTargetKey]}
|
|
1902
|
+
onChangeJS={({ hex }: { hex: string }) => {
|
|
1903
|
+
previewPickerColor(hex);
|
|
1904
|
+
}}
|
|
1905
|
+
onCompleteJS={({ hex }: { hex: string }) => {
|
|
1906
|
+
applyPickerColor(hex);
|
|
1907
|
+
}}
|
|
1908
|
+
style={styles.picker}
|
|
1909
|
+
>
|
|
1910
|
+
<Preview hideInitialColor />
|
|
1911
|
+
<Panel1 />
|
|
1912
|
+
<HueSlider />
|
|
1913
|
+
</ColorPicker>
|
|
1914
|
+
<View style={styles.pickerHexRow}>
|
|
1915
|
+
<Text style={styles.pickerHexLabel}>Hex</Text>
|
|
1916
|
+
<TextInput
|
|
1917
|
+
value={pickerHexDraft}
|
|
1918
|
+
onChangeText={handlePickerHexChange}
|
|
1919
|
+
onBlur={commitPickerHexDraft}
|
|
1920
|
+
onSubmitEditing={commitPickerHexDraft}
|
|
1921
|
+
autoCorrect={false}
|
|
1922
|
+
autoCapitalize="none"
|
|
1923
|
+
maxLength={7}
|
|
1924
|
+
style={[
|
|
1925
|
+
styles.input,
|
|
1926
|
+
styles.pickerHexInput,
|
|
1927
|
+
{ color: '#111827', borderColor: previewColors.primary },
|
|
1928
|
+
]}
|
|
1929
|
+
placeholder="#rrggbb"
|
|
1930
|
+
placeholderTextColor="#6b7280"
|
|
1931
|
+
/>
|
|
1932
|
+
</View>
|
|
1933
|
+
<View style={styles.manualSwatchesRow}>
|
|
1934
|
+
{pickerSwatches.map((swatch) => (
|
|
1935
|
+
<Pressable
|
|
1936
|
+
key={swatch}
|
|
1937
|
+
onPress={() => applyPickerColor(swatch)}
|
|
1938
|
+
style={[
|
|
1939
|
+
styles.manualSwatch,
|
|
1940
|
+
{ backgroundColor: swatch },
|
|
1941
|
+
editablePalette[selectedColor].toLowerCase() === swatch
|
|
1942
|
+
? styles.manualSwatchSelected
|
|
1943
|
+
: null,
|
|
1944
|
+
]}
|
|
1945
|
+
/>
|
|
1946
|
+
))}
|
|
1947
|
+
</View>
|
|
1948
|
+
</>
|
|
1949
|
+
) : (
|
|
1950
|
+
<View style={styles.familyBlock}>
|
|
1951
|
+
{(theme.colorSystem.mode === 'automatic' ? semanticColorKeys : paletteColorKeys).map(
|
|
1952
|
+
(key) => {
|
|
1953
|
+
const familyValue =
|
|
1954
|
+
theme.colorSystem.mode === 'automatic'
|
|
1955
|
+
? activeFamilies[key as SemanticColorKey]
|
|
1956
|
+
: activeBgFamilies[key as PaletteColorKey];
|
|
1957
|
+
const shadeValue =
|
|
1958
|
+
theme.colorSystem.mode === 'automatic'
|
|
1959
|
+
? null
|
|
1960
|
+
: activeBgFamilyShades[key as PaletteColorKey];
|
|
1961
|
+
const familyChipColor =
|
|
1962
|
+
theme.colorSystem.mode === 'automatic'
|
|
1963
|
+
? previewColors[key as SemanticColorKey]
|
|
1964
|
+
: editablePalette[key as PaletteColorKey];
|
|
1965
|
+
|
|
1966
|
+
return (
|
|
1967
|
+
<View key={key} style={styles.familyRow}>
|
|
1968
|
+
<View style={styles.familyHeader}>
|
|
1969
|
+
<View
|
|
1970
|
+
style={[
|
|
1971
|
+
styles.familyTitleChip,
|
|
1972
|
+
{
|
|
1973
|
+
backgroundColor: familyChipColor,
|
|
1974
|
+
borderColor: previewColors.text,
|
|
1975
|
+
},
|
|
1976
|
+
]}
|
|
1977
|
+
>
|
|
1978
|
+
<Text
|
|
1979
|
+
style={[
|
|
1980
|
+
styles.familyTitleChipText,
|
|
1981
|
+
{
|
|
1982
|
+
color: getReadableChipTextColor(familyChipColor),
|
|
1983
|
+
},
|
|
1984
|
+
]}
|
|
1985
|
+
>
|
|
1986
|
+
{key}
|
|
1987
|
+
</Text>
|
|
1988
|
+
</View>
|
|
1989
|
+
{theme.colorSystem.familyMode === 'two' ? (
|
|
1990
|
+
<Text style={[styles.familySchemeHint, { color: previewColors.text }]}>
|
|
1991
|
+
({activeFamilyScheme})
|
|
1992
|
+
</Text>
|
|
1993
|
+
) : null}
|
|
1994
|
+
</View>
|
|
1995
|
+
<View style={styles.familyOptions}>
|
|
1996
|
+
{tailwindFamilies.map((family) => (
|
|
1997
|
+
<Pressable
|
|
1998
|
+
key={`${key}-${family}`}
|
|
1999
|
+
onPress={() => {
|
|
2000
|
+
if (theme.colorSystem.mode === 'automatic') {
|
|
2001
|
+
updateFamily(key as SemanticColorKey, family);
|
|
2002
|
+
} else {
|
|
2003
|
+
updateBgFamily(key as PaletteColorKey, family);
|
|
2004
|
+
}
|
|
2005
|
+
}}
|
|
2006
|
+
style={[
|
|
2007
|
+
styles.familyOption,
|
|
2008
|
+
familyValue === family && {
|
|
2009
|
+
backgroundColor: previewColors.primary,
|
|
2010
|
+
borderColor: previewColors.primary,
|
|
2011
|
+
},
|
|
2012
|
+
]}
|
|
2013
|
+
>
|
|
2014
|
+
<Text
|
|
2015
|
+
style={[
|
|
2016
|
+
styles.familyOptionText,
|
|
2017
|
+
{
|
|
2018
|
+
color: familyValue === family ? '#f8fafc' : previewColors.text,
|
|
2019
|
+
},
|
|
2020
|
+
]}
|
|
2021
|
+
>
|
|
2022
|
+
{family}
|
|
2023
|
+
</Text>
|
|
2024
|
+
</Pressable>
|
|
2025
|
+
))}
|
|
2026
|
+
</View>
|
|
2027
|
+
{theme.colorSystem.mode === 'bg' ? (
|
|
2028
|
+
<View style={styles.shadeRow}>
|
|
2029
|
+
{shadeOptions.map((shade) => {
|
|
2030
|
+
const shadeColor = getTailwindColor(familyValue, shade);
|
|
2031
|
+
const isSelectedShade = shadeValue === shade;
|
|
2032
|
+
return (
|
|
2033
|
+
<Pressable
|
|
2034
|
+
key={`${key}-${shade}`}
|
|
2035
|
+
onPress={() => updateBgShade(key as PaletteColorKey, shade)}
|
|
2036
|
+
style={[
|
|
2037
|
+
styles.shadeDot,
|
|
2038
|
+
{
|
|
2039
|
+
backgroundColor: shadeColor,
|
|
2040
|
+
borderColor: isSelectedShade ? previewColors.text : '#cbd5e1',
|
|
2041
|
+
borderWidth: isSelectedShade ? 3 : 1,
|
|
2042
|
+
},
|
|
2043
|
+
]}
|
|
2044
|
+
accessibilityLabel={`${key} shade ${shade}`}
|
|
2045
|
+
/>
|
|
2046
|
+
);
|
|
2047
|
+
})}
|
|
2048
|
+
</View>
|
|
2049
|
+
) : null}
|
|
2050
|
+
</View>
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
)}
|
|
2054
|
+
</View>
|
|
2055
|
+
)}
|
|
2056
|
+
</View>
|
|
2057
|
+
|
|
2058
|
+
<View style={previewCard}>
|
|
2059
|
+
<Text
|
|
2060
|
+
style={{
|
|
2061
|
+
color: previewColors.text,
|
|
2062
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontDisplay),
|
|
2063
|
+
fontSize: theme.typography.displaySize,
|
|
2064
|
+
fontWeight: resolvePreviewFontWeight(theme.typography.fontDisplay, '900'),
|
|
2065
|
+
}}
|
|
2066
|
+
>
|
|
2067
|
+
Display headline
|
|
2068
|
+
</Text>
|
|
2069
|
+
<Text
|
|
2070
|
+
style={{
|
|
2071
|
+
color: previewColors.text,
|
|
2072
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontTitle),
|
|
2073
|
+
fontSize: theme.typography.headingSize,
|
|
2074
|
+
fontWeight: resolvePreviewFontWeight(theme.typography.fontTitle, '800'),
|
|
2075
|
+
}}
|
|
2076
|
+
>
|
|
2077
|
+
Section heading
|
|
2078
|
+
</Text>
|
|
2079
|
+
<Text
|
|
2080
|
+
style={{
|
|
2081
|
+
color: previewColors.text,
|
|
2082
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontSubtitle),
|
|
2083
|
+
fontSize: theme.typography.bodySize + 1,
|
|
2084
|
+
fontWeight: resolvePreviewFontWeight(theme.typography.fontSubtitle, '600'),
|
|
2085
|
+
}}
|
|
2086
|
+
>
|
|
2087
|
+
Subtitle copy for card sections and hero support text.
|
|
2088
|
+
</Text>
|
|
2089
|
+
<Text
|
|
2090
|
+
style={{
|
|
2091
|
+
color: previewColors.text,
|
|
2092
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontBody),
|
|
2093
|
+
fontSize: theme.typography.bodySize,
|
|
2094
|
+
}}
|
|
2095
|
+
>
|
|
2096
|
+
Readable body copy for product screens, onboarding, settings, and forms.
|
|
2097
|
+
</Text>
|
|
2098
|
+
<Text
|
|
2099
|
+
style={{
|
|
2100
|
+
color: previewColors.text,
|
|
2101
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontCaption),
|
|
2102
|
+
fontSize: theme.typography.captionSize,
|
|
2103
|
+
textTransform: 'uppercase',
|
|
2104
|
+
}}
|
|
2105
|
+
>
|
|
2106
|
+
Caption and metadata text
|
|
2107
|
+
</Text>
|
|
2108
|
+
<Text
|
|
2109
|
+
style={{
|
|
2110
|
+
color: previewColors.text,
|
|
2111
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontMono),
|
|
2112
|
+
fontSize: 11,
|
|
2113
|
+
textTransform: 'uppercase',
|
|
2114
|
+
opacity: 0.82,
|
|
2115
|
+
}}
|
|
2116
|
+
>
|
|
2117
|
+
Monospaced sample
|
|
2118
|
+
</Text>
|
|
2119
|
+
<Text
|
|
2120
|
+
style={{
|
|
2121
|
+
color: previewColors.text,
|
|
2122
|
+
fontFamily: resolvePreviewFontFamily(theme.typography.fontMono),
|
|
2123
|
+
fontSize: 12,
|
|
2124
|
+
backgroundColor: previewColors.background,
|
|
2125
|
+
padding: 8,
|
|
2126
|
+
borderRadius: 8,
|
|
2127
|
+
}}
|
|
2128
|
+
>
|
|
2129
|
+
{'const typography = "monospaced";'}
|
|
2130
|
+
</Text>
|
|
2131
|
+
<AnimatedPressable
|
|
2132
|
+
label="Call to Action"
|
|
2133
|
+
backgroundColor={previewColors.secondary}
|
|
2134
|
+
textColor={previewColors.text}
|
|
2135
|
+
/>
|
|
2136
|
+
</View>
|
|
2137
|
+
|
|
2138
|
+
<View
|
|
2139
|
+
style={[
|
|
2140
|
+
styles.section,
|
|
2141
|
+
styles.typographySection,
|
|
2142
|
+
{
|
|
2143
|
+
backgroundColor: previewColors.surface,
|
|
2144
|
+
borderRadius: theme.layout.radius,
|
|
2145
|
+
},
|
|
2146
|
+
]}
|
|
2147
|
+
>
|
|
2148
|
+
<View style={styles.sectionHeaderRow}>
|
|
2149
|
+
<Text style={[styles.sectionTitle, { color: controlTextColor }]}>Typography</Text>
|
|
2150
|
+
<Pressable
|
|
2151
|
+
onPress={applyNotoFonts}
|
|
2152
|
+
style={[styles.presetButton, { borderColor: controlTextColor }]}
|
|
2153
|
+
>
|
|
2154
|
+
<Text style={[styles.presetButtonText, { color: controlTextColor }]}>Noto</Text>
|
|
2155
|
+
</Pressable>
|
|
2156
|
+
</View>
|
|
2157
|
+
<Text style={[styles.helperText, styles.lockedNote, { color: controlTextColor }]}>
|
|
2158
|
+
Search and choose a font per role. Display also syncs `fontFamily` for compatibility.
|
|
2159
|
+
</Text>
|
|
2160
|
+
<View style={[styles.grid, styles.fontPickerGrid]}>
|
|
2161
|
+
{fontRoleFields.map((field) => (
|
|
2162
|
+
<FontFamilyCombobox
|
|
2163
|
+
key={field.key}
|
|
2164
|
+
grid
|
|
2165
|
+
label={field.label}
|
|
2166
|
+
labelColor={controlTextColor}
|
|
2167
|
+
value={explicitFontRoles[field.key] ? theme.typography[field.key] : ''}
|
|
2168
|
+
placeholder=""
|
|
2169
|
+
options={availableFontFamilies}
|
|
2170
|
+
onSelect={(fontFamily) => updateFontRole(field.key, fontFamily)}
|
|
2171
|
+
/>
|
|
2172
|
+
))}
|
|
2173
|
+
</View>
|
|
2174
|
+
{Platform.OS !== 'web' &&
|
|
2175
|
+
fontRoleFields.some((field) => isNativeUnsafeFont(theme.typography[field.key])) ? (
|
|
2176
|
+
<Text style={[styles.helperText, styles.lockedNote, { color: controlTextColor }]}>
|
|
2177
|
+
Custom fonts apply on native after you press Save Theme and reload. Until the sync
|
|
2178
|
+
downloads the font assets, previews may render with System.
|
|
2179
|
+
</Text>
|
|
2180
|
+
) : null}
|
|
2181
|
+
<View style={[styles.grid, styles.tokenSizeGrid]}>
|
|
2182
|
+
<NumberField
|
|
2183
|
+
grid
|
|
2184
|
+
label="Display"
|
|
2185
|
+
labelColor={controlTextColor}
|
|
2186
|
+
value={theme.typography.displaySize}
|
|
2187
|
+
onChange={(value) => updateNumeric('displaySize', value)}
|
|
2188
|
+
/>
|
|
2189
|
+
<NumberField
|
|
2190
|
+
grid
|
|
2191
|
+
label="Heading"
|
|
2192
|
+
labelColor={controlTextColor}
|
|
2193
|
+
value={theme.typography.headingSize}
|
|
2194
|
+
onChange={(value) => updateNumeric('headingSize', value)}
|
|
2195
|
+
/>
|
|
2196
|
+
<NumberField
|
|
2197
|
+
grid
|
|
2198
|
+
label="Body"
|
|
2199
|
+
labelColor={controlTextColor}
|
|
2200
|
+
value={theme.typography.bodySize}
|
|
2201
|
+
onChange={(value) => updateNumeric('bodySize', value)}
|
|
2202
|
+
/>
|
|
2203
|
+
<NumberField
|
|
2204
|
+
grid
|
|
2205
|
+
label="Caption"
|
|
2206
|
+
labelColor={controlTextColor}
|
|
2207
|
+
value={theme.typography.captionSize}
|
|
2208
|
+
onChange={(value) => updateNumeric('captionSize', value)}
|
|
2209
|
+
/>
|
|
2210
|
+
</View>
|
|
2211
|
+
</View>
|
|
2212
|
+
|
|
2213
|
+
<View
|
|
2214
|
+
style={[
|
|
2215
|
+
styles.section,
|
|
2216
|
+
{
|
|
2217
|
+
backgroundColor: previewColors.surface,
|
|
2218
|
+
borderRadius: theme.layout.radius,
|
|
2219
|
+
padding: galleryTokens.sectionPadding,
|
|
2220
|
+
},
|
|
2221
|
+
]}
|
|
2222
|
+
>
|
|
2223
|
+
<Text style={[styles.sectionTitle, { color: controlTextColor }]}>Component Gallery</Text>
|
|
2224
|
+
<Text style={[styles.helperText, { color: controlTextColor }]}>
|
|
2225
|
+
Token previews using cards, status views, input states, and spacing rhythm. These are
|
|
2226
|
+
just example components.
|
|
2227
|
+
</Text>
|
|
2228
|
+
<View style={[styles.galleryRow, { gap: galleryTokens.rowGap }]}>
|
|
2229
|
+
<View
|
|
2230
|
+
style={[
|
|
2231
|
+
styles.galleryCard,
|
|
2232
|
+
{
|
|
2233
|
+
borderColor: previewColors.primary,
|
|
2234
|
+
backgroundColor: previewColors.background,
|
|
2235
|
+
borderRadius: galleryTokens.radius,
|
|
2236
|
+
gap: galleryTokens.compactGap,
|
|
2237
|
+
padding: galleryTokens.cardPadding,
|
|
2238
|
+
},
|
|
2239
|
+
]}
|
|
2240
|
+
>
|
|
2241
|
+
<Text style={[styles.galleryCardTitle, { color: previewColors.text }]}>
|
|
2242
|
+
Default Card
|
|
2243
|
+
</Text>
|
|
2244
|
+
<Text style={[styles.galleryCardBody, { color: previewColors.text }]}>
|
|
2245
|
+
Solid surface card for primary content blocks.
|
|
2246
|
+
</Text>
|
|
2247
|
+
</View>
|
|
2248
|
+
<View
|
|
2249
|
+
style={[
|
|
2250
|
+
styles.galleryCard,
|
|
2251
|
+
styles.galleryCardSoft,
|
|
2252
|
+
{
|
|
2253
|
+
borderColor: previewColors.secondary,
|
|
2254
|
+
borderRadius: galleryTokens.radius,
|
|
2255
|
+
gap: galleryTokens.compactGap,
|
|
2256
|
+
padding: galleryTokens.cardPadding,
|
|
2257
|
+
},
|
|
2258
|
+
]}
|
|
2259
|
+
>
|
|
2260
|
+
<Text style={[styles.galleryCardTitle, { color: previewColors.text }]}>
|
|
2261
|
+
Soft Card
|
|
2262
|
+
</Text>
|
|
2263
|
+
<Text style={[styles.galleryCardBody, { color: previewColors.text }]}>
|
|
2264
|
+
Layered panel with lower contrast for secondary content.
|
|
2265
|
+
</Text>
|
|
2266
|
+
</View>
|
|
2267
|
+
</View>
|
|
2268
|
+
<View style={[styles.statusRow, { gap: galleryTokens.compactGap }]}>
|
|
2269
|
+
{[
|
|
2270
|
+
{ label: 'Success', color: previewColors.success },
|
|
2271
|
+
{ label: 'Warning', color: previewColors.warning },
|
|
2272
|
+
{ label: 'Primary', color: previewColors.primary },
|
|
2273
|
+
{ label: 'Secondary', color: previewColors.secondary },
|
|
2274
|
+
].map((status) => (
|
|
2275
|
+
<View
|
|
2276
|
+
key={status.label}
|
|
2277
|
+
style={[
|
|
2278
|
+
styles.statusPill,
|
|
2279
|
+
{
|
|
2280
|
+
backgroundColor: status.color,
|
|
2281
|
+
borderRadius: galleryTokens.radius,
|
|
2282
|
+
paddingHorizontal: galleryTokens.pillPaddingHorizontal,
|
|
2283
|
+
paddingVertical: galleryTokens.pillPaddingVertical,
|
|
2284
|
+
},
|
|
2285
|
+
]}
|
|
2286
|
+
>
|
|
2287
|
+
<Text style={styles.statusPillText}>{status.label}</Text>
|
|
2288
|
+
</View>
|
|
2289
|
+
))}
|
|
2290
|
+
</View>
|
|
2291
|
+
<View style={[styles.inputStatesRow, { gap: galleryTokens.inputGap }]}>
|
|
2292
|
+
<TextInput
|
|
2293
|
+
style={[
|
|
2294
|
+
styles.input,
|
|
2295
|
+
{
|
|
2296
|
+
borderRadius: galleryTokens.radius,
|
|
2297
|
+
minHeight: galleryTokens.inputMinHeight,
|
|
2298
|
+
paddingHorizontal: galleryTokens.cardPadding,
|
|
2299
|
+
},
|
|
2300
|
+
]}
|
|
2301
|
+
placeholder="Default input"
|
|
2302
|
+
/>
|
|
2303
|
+
<TextInput
|
|
2304
|
+
style={[
|
|
2305
|
+
styles.input,
|
|
2306
|
+
{
|
|
2307
|
+
borderColor: previewColors.warning,
|
|
2308
|
+
borderRadius: galleryTokens.radius,
|
|
2309
|
+
minHeight: galleryTokens.inputMinHeight,
|
|
2310
|
+
paddingHorizontal: galleryTokens.cardPadding,
|
|
2311
|
+
},
|
|
2312
|
+
]}
|
|
2313
|
+
placeholder="Warning state input"
|
|
2314
|
+
/>
|
|
2315
|
+
<TextInput
|
|
2316
|
+
style={[
|
|
2317
|
+
styles.input,
|
|
2318
|
+
{
|
|
2319
|
+
borderRadius: galleryTokens.radius,
|
|
2320
|
+
minHeight: galleryTokens.inputMinHeight,
|
|
2321
|
+
opacity: 0.6,
|
|
2322
|
+
paddingHorizontal: galleryTokens.cardPadding,
|
|
2323
|
+
},
|
|
2324
|
+
]}
|
|
2325
|
+
placeholder="Disabled input"
|
|
2326
|
+
editable={false}
|
|
2327
|
+
/>
|
|
2328
|
+
</View>
|
|
2329
|
+
</View>
|
|
2330
|
+
|
|
2331
|
+
<View
|
|
2332
|
+
style={[
|
|
2333
|
+
styles.section,
|
|
2334
|
+
{
|
|
2335
|
+
backgroundColor: previewColors.surface,
|
|
2336
|
+
borderRadius: theme.layout.radius,
|
|
2337
|
+
},
|
|
2338
|
+
]}
|
|
2339
|
+
>
|
|
2340
|
+
<Text style={[styles.sectionTitle, { color: controlTextColor }]}>Layout Tokens</Text>
|
|
2341
|
+
<Text style={[styles.helperText, styles.lockedNote, { color: controlTextColor }]}>
|
|
2342
|
+
Hit enter or click out of the text box to take effect.
|
|
2343
|
+
</Text>
|
|
2344
|
+
<NumberField
|
|
2345
|
+
label="Corner Radius"
|
|
2346
|
+
labelColor={controlTextColor}
|
|
2347
|
+
value={theme.layout.radius}
|
|
2348
|
+
onChange={(value) => updateNumeric('radius', value)}
|
|
2349
|
+
/>
|
|
2350
|
+
<View style={styles.grid}>
|
|
2351
|
+
{spacingKeys.map((key) => (
|
|
2352
|
+
<NumberField
|
|
2353
|
+
grid
|
|
2354
|
+
key={key}
|
|
2355
|
+
label={`Spacing ${key}`}
|
|
2356
|
+
labelColor={controlTextColor}
|
|
2357
|
+
value={theme.layout.spacing[key]}
|
|
2358
|
+
onChange={(value) => updateNumeric(key, value)}
|
|
2359
|
+
/>
|
|
2360
|
+
))}
|
|
2361
|
+
</View>
|
|
2362
|
+
<Text style={[styles.helperText, styles.lockedNote, { color: controlTextColor }]}>
|
|
2363
|
+
Bar width mirrors each spacing token so scale jumps are easy to spot.
|
|
2364
|
+
</Text>
|
|
2365
|
+
<View style={styles.spacingPreview}>
|
|
2366
|
+
{spacingKeys.map((key) => (
|
|
2367
|
+
<View key={`preview-${key}`} style={styles.spacingPreviewRow}>
|
|
2368
|
+
<Text style={[styles.spacingLabel, { color: controlTextColor }]}>{key}</Text>
|
|
2369
|
+
<View
|
|
2370
|
+
style={[
|
|
2371
|
+
styles.spacingBar,
|
|
2372
|
+
{
|
|
2373
|
+
width: Math.max(10, theme.layout.spacing[key] * 3),
|
|
2374
|
+
backgroundColor: previewColors.primary,
|
|
2375
|
+
borderRadius: Math.max(4, theme.layout.radius / 2),
|
|
2376
|
+
},
|
|
2377
|
+
]}
|
|
2378
|
+
/>
|
|
2379
|
+
</View>
|
|
2380
|
+
))}
|
|
2381
|
+
</View>
|
|
2382
|
+
<Text style={[styles.helperText, styles.lockedNote, { color: controlTextColor }]}>
|
|
2383
|
+
Practical preview: XS controls list gaps, SM pads the first 3 items, and MD pads the
|
|
2384
|
+
list container.
|
|
2385
|
+
</Text>
|
|
2386
|
+
<View
|
|
2387
|
+
style={[
|
|
2388
|
+
styles.layoutPreviewContainer,
|
|
2389
|
+
{
|
|
2390
|
+
borderColor: previewColors.primary,
|
|
2391
|
+
borderRadius: theme.layout.radius,
|
|
2392
|
+
padding: theme.layout.spacing.md,
|
|
2393
|
+
gap: theme.layout.spacing.xs,
|
|
2394
|
+
},
|
|
2395
|
+
]}
|
|
2396
|
+
>
|
|
2397
|
+
{spacingKeys.map((spacingKey, index) => (
|
|
2398
|
+
<View
|
|
2399
|
+
key={`layout-preview-${spacingKey}`}
|
|
2400
|
+
style={[
|
|
2401
|
+
styles.layoutPreviewCard,
|
|
2402
|
+
{
|
|
2403
|
+
borderRadius: Math.max(6, theme.layout.radius - 2),
|
|
2404
|
+
padding: index < 3 ? theme.layout.spacing.sm : theme.layout.spacing[spacingKey],
|
|
2405
|
+
borderColor: previewColors.secondary,
|
|
2406
|
+
backgroundColor: previewColors.background,
|
|
2407
|
+
},
|
|
2408
|
+
]}
|
|
2409
|
+
>
|
|
2410
|
+
<Text style={[styles.layoutPreviewTitle, { color: previewColors.text }]}>
|
|
2411
|
+
{spacingKey.toUpperCase()} -{' '}
|
|
2412
|
+
{spacingKey === 'xs'
|
|
2413
|
+
? 'Extra Small'
|
|
2414
|
+
: spacingKey === 'sm'
|
|
2415
|
+
? 'Small'
|
|
2416
|
+
: spacingKey === 'md'
|
|
2417
|
+
? 'Medium'
|
|
2418
|
+
: spacingKey === 'lg'
|
|
2419
|
+
? 'Large'
|
|
2420
|
+
: 'Extra Large'}
|
|
2421
|
+
</Text>
|
|
2422
|
+
<Text style={[styles.layoutPreviewBody, { color: previewColors.text }]}>
|
|
2423
|
+
{spacingKey === 'xs'
|
|
2424
|
+
? 'Used for the gap between these list items.'
|
|
2425
|
+
: spacingKey === 'sm'
|
|
2426
|
+
? 'Used for the padding for the first 3 list items.'
|
|
2427
|
+
: spacingKey === 'md'
|
|
2428
|
+
? 'Used for the padding around the list.'
|
|
2429
|
+
: spacingKey === 'lg'
|
|
2430
|
+
? "Reserved for roomier layout sections (this list item's padding)."
|
|
2431
|
+
: "Reserved for extra-roomy layout sections (this list item's padding)."}
|
|
2432
|
+
</Text>
|
|
2433
|
+
</View>
|
|
2434
|
+
))}
|
|
2435
|
+
</View>
|
|
2436
|
+
</View>
|
|
2437
|
+
|
|
2438
|
+
<Animated.View style={{ transform: [{ scale: saveButtonScale }] }}>
|
|
2439
|
+
<Pressable
|
|
2440
|
+
onPress={handleSaveThemePress}
|
|
2441
|
+
disabled={saving}
|
|
2442
|
+
style={[styles.saveButton, { backgroundColor: previewColors.primary }]}
|
|
2443
|
+
>
|
|
2444
|
+
<Text style={styles.saveButtonText}>{saving ? 'Saving...' : 'Save Theme'}</Text>
|
|
2445
|
+
</Pressable>
|
|
2446
|
+
</Animated.View>
|
|
2447
|
+
|
|
2448
|
+
{saveMessage ? (
|
|
2449
|
+
<Animated.View
|
|
2450
|
+
key={`save-message-bottom-${saveMessageNonce}`}
|
|
2451
|
+
style={[
|
|
2452
|
+
styles.saveMessageBanner,
|
|
2453
|
+
{
|
|
2454
|
+
backgroundColor: saveMessageColors.backgroundColor,
|
|
2455
|
+
borderColor: saveMessageColors.borderColor,
|
|
2456
|
+
opacity: saveBannerOpacity,
|
|
2457
|
+
},
|
|
2458
|
+
]}
|
|
2459
|
+
>
|
|
2460
|
+
<Text style={[styles.saveMessageText, { color: saveMessageColors.color }]}>
|
|
2461
|
+
{saveMessage}
|
|
2462
|
+
</Text>
|
|
2463
|
+
</Animated.View>
|
|
2464
|
+
) : null}
|
|
2465
|
+
{Platform.OS !== 'web' ? (
|
|
2466
|
+
<View style={styles.nativeHelp}>
|
|
2467
|
+
<Text style={styles.nativeTitle}>Native fallback</Text>
|
|
2468
|
+
<Text style={styles.nativeBody}>Run this command in your app root terminal:</Text>
|
|
2469
|
+
<Text style={styles.command}>{nativeSaveCommand}</Text>
|
|
2470
|
+
{nativeDraft ? <Text style={styles.payload}>{nativeDraft}</Text> : null}
|
|
2471
|
+
</View>
|
|
2472
|
+
) : null}
|
|
2473
|
+
</ScrollView>
|
|
2474
|
+
|
|
2475
|
+
<Modal
|
|
2476
|
+
transparent
|
|
2477
|
+
animationType="fade"
|
|
2478
|
+
visible={showWritePolicyModal}
|
|
2479
|
+
onRequestClose={() => setShowWritePolicyModal(false)}
|
|
2480
|
+
>
|
|
2481
|
+
<View style={styles.modalBackdrop}>
|
|
2482
|
+
<View style={styles.modalCard}>
|
|
2483
|
+
<Text style={styles.modalTitle}>Choose Save Behavior</Text>
|
|
2484
|
+
<Text style={styles.modalBody}>
|
|
2485
|
+
Managed updates only Stylist-owned token blocks. Overwrite regenerates the full
|
|
2486
|
+
style-library target file.
|
|
2487
|
+
</Text>
|
|
2488
|
+
<View style={styles.modalActions}>
|
|
2489
|
+
<Pressable
|
|
2490
|
+
onPress={() => chooseWritePolicyAndSave('managed')}
|
|
2491
|
+
style={[styles.modalButton, styles.modalButtonPrimary]}
|
|
2492
|
+
>
|
|
2493
|
+
<Text style={styles.modalButtonPrimaryText}>Managed (Recommended)</Text>
|
|
2494
|
+
</Pressable>
|
|
2495
|
+
<Pressable
|
|
2496
|
+
onPress={() => chooseWritePolicyAndSave('overwrite')}
|
|
2497
|
+
style={[styles.modalButton, styles.modalButtonSecondary]}
|
|
2498
|
+
>
|
|
2499
|
+
<Text style={styles.modalButtonSecondaryText}>Overwrite Full File</Text>
|
|
2500
|
+
</Pressable>
|
|
2501
|
+
</View>
|
|
2502
|
+
</View>
|
|
2503
|
+
</View>
|
|
2504
|
+
</Modal>
|
|
2505
|
+
|
|
2506
|
+
<View
|
|
2507
|
+
style={[
|
|
2508
|
+
styles.controls,
|
|
2509
|
+
{
|
|
2510
|
+
backgroundColor: previewColors.surface,
|
|
2511
|
+
borderColor: previewColors.primary,
|
|
2512
|
+
top: Platform.OS === 'web' ? 76 : Math.max(insets.top + 8, 14),
|
|
2513
|
+
},
|
|
2514
|
+
]}
|
|
2515
|
+
>
|
|
2516
|
+
<ToggleRow
|
|
2517
|
+
label="Color Mode"
|
|
2518
|
+
options={colorModeOptions}
|
|
2519
|
+
value={theme.colorSystem.mode}
|
|
2520
|
+
onChange={(value) => updateColorMode(value as StylistColorMode)}
|
|
2521
|
+
infoText={topToggleHelpCopy.colorMode.body}
|
|
2522
|
+
onPressInfo={() => setActiveTopToggleHelp('colorMode')}
|
|
2523
|
+
/>
|
|
2524
|
+
<ToggleRow
|
|
2525
|
+
label="Preview"
|
|
2526
|
+
options={schemeKeys.map((scheme) => ({
|
|
2527
|
+
label: scheme,
|
|
2528
|
+
value: scheme,
|
|
2529
|
+
}))}
|
|
2530
|
+
value={theme.colorSystem.previewScheme}
|
|
2531
|
+
onChange={(value) => updatePreviewScheme(value as StylistColorScheme)}
|
|
2532
|
+
infoText={topToggleHelpCopy.preview.body}
|
|
2533
|
+
onPressInfo={() => setActiveTopToggleHelp('preview')}
|
|
2534
|
+
/>
|
|
2535
|
+
<ToggleRow
|
|
2536
|
+
label="Family Strategy"
|
|
2537
|
+
options={familyModeOptions}
|
|
2538
|
+
value={theme.colorSystem.familyMode}
|
|
2539
|
+
onChange={(value) => updateFamilyMode(value as StylistFamilyMode)}
|
|
2540
|
+
infoText={topToggleHelpCopy.familyStrategy.body}
|
|
2541
|
+
onPressInfo={() => setActiveTopToggleHelp('familyStrategy')}
|
|
2542
|
+
/>
|
|
2543
|
+
</View>
|
|
2544
|
+
|
|
2545
|
+
<Modal
|
|
2546
|
+
transparent
|
|
2547
|
+
animationType="fade"
|
|
2548
|
+
visible={Boolean(activeTopToggleHelp)}
|
|
2549
|
+
onRequestClose={() => setActiveTopToggleHelp(null)}
|
|
2550
|
+
>
|
|
2551
|
+
<View style={styles.modalBackdrop}>
|
|
2552
|
+
<View style={styles.modalCard}>
|
|
2553
|
+
<Text style={styles.modalTitle}>
|
|
2554
|
+
{activeTopToggleHelp ? topToggleHelpCopy[activeTopToggleHelp].title : 'Toggle Help'}
|
|
2555
|
+
</Text>
|
|
2556
|
+
<Text style={styles.modalBody}>
|
|
2557
|
+
{activeTopToggleHelp
|
|
2558
|
+
? topToggleHelpCopy[activeTopToggleHelp].body
|
|
2559
|
+
: 'No toggle help selected.'}
|
|
2560
|
+
</Text>
|
|
2561
|
+
<View style={styles.modalActions}>
|
|
2562
|
+
<Pressable
|
|
2563
|
+
onPress={() => setActiveTopToggleHelp(null)}
|
|
2564
|
+
style={[styles.modalButton, styles.modalButtonPrimary]}
|
|
2565
|
+
>
|
|
2566
|
+
<Text style={styles.modalButtonPrimaryText}>Close</Text>
|
|
2567
|
+
</Pressable>
|
|
2568
|
+
</View>
|
|
2569
|
+
</View>
|
|
2570
|
+
</View>
|
|
2571
|
+
</Modal>
|
|
2572
|
+
</View>
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
function ToggleRow(props: {
|
|
2577
|
+
label: string;
|
|
2578
|
+
infoText: string;
|
|
2579
|
+
value: string;
|
|
2580
|
+
options: Array<{ label: string; value: string }>;
|
|
2581
|
+
onChange: (value: string) => void;
|
|
2582
|
+
onPressInfo: () => void;
|
|
2583
|
+
}) {
|
|
2584
|
+
return (
|
|
2585
|
+
<View style={styles.toggleRow}>
|
|
2586
|
+
<View style={styles.toggleLabelRow}>
|
|
2587
|
+
<Text style={styles.toggleLabel}>{props.label}</Text>
|
|
2588
|
+
<Pressable
|
|
2589
|
+
accessibilityRole="button"
|
|
2590
|
+
accessibilityLabel={`${props.label} explanation`}
|
|
2591
|
+
accessibilityHint={props.infoText}
|
|
2592
|
+
onPress={props.onPressInfo}
|
|
2593
|
+
style={styles.toggleInfoButton}
|
|
2594
|
+
>
|
|
2595
|
+
<Text style={styles.toggleInfoButtonText}>i</Text>
|
|
2596
|
+
</Pressable>
|
|
2597
|
+
</View>
|
|
2598
|
+
<View style={styles.toggleOptions}>
|
|
2599
|
+
{props.options.map((option) => {
|
|
2600
|
+
const isSelected = props.value === option.value;
|
|
2601
|
+
return (
|
|
2602
|
+
<Pressable
|
|
2603
|
+
key={option.value}
|
|
2604
|
+
onPress={() => props.onChange(option.value)}
|
|
2605
|
+
style={[styles.toggleOption, isSelected && styles.toggleOptionSelected]}
|
|
2606
|
+
>
|
|
2607
|
+
<Text
|
|
2608
|
+
style={[styles.toggleOptionText, isSelected && styles.toggleOptionTextSelected]}
|
|
2609
|
+
>
|
|
2610
|
+
{option.label}
|
|
2611
|
+
</Text>
|
|
2612
|
+
</Pressable>
|
|
2613
|
+
);
|
|
2614
|
+
})}
|
|
2615
|
+
</View>
|
|
2616
|
+
</View>
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function NumberField(props: {
|
|
2621
|
+
label: string;
|
|
2622
|
+
value: number;
|
|
2623
|
+
onChange: (value: string) => void;
|
|
2624
|
+
grid?: boolean;
|
|
2625
|
+
labelColor?: string;
|
|
2626
|
+
}) {
|
|
2627
|
+
const [draft, setDraft] = useState(String(props.value));
|
|
2628
|
+
|
|
2629
|
+
return (
|
|
2630
|
+
<View style={[styles.field, props.grid ? styles.gridField : styles.fullField]}>
|
|
2631
|
+
<Text style={[styles.fieldLabel, props.labelColor ? { color: props.labelColor } : null]}>
|
|
2632
|
+
{props.label}
|
|
2633
|
+
</Text>
|
|
2634
|
+
<TextInput
|
|
2635
|
+
value={draft}
|
|
2636
|
+
onChangeText={setDraft}
|
|
2637
|
+
onBlur={() => {
|
|
2638
|
+
props.onChange(draft);
|
|
2639
|
+
const parsed = Number.parseFloat(draft);
|
|
2640
|
+
setDraft(Number.isFinite(parsed) ? String(parsed) : String(props.value));
|
|
2641
|
+
}}
|
|
2642
|
+
keyboardType="numeric"
|
|
2643
|
+
style={styles.input}
|
|
2644
|
+
/>
|
|
2645
|
+
</View>
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
function FontFamilyCombobox(props: {
|
|
2650
|
+
label: string;
|
|
2651
|
+
labelColor?: string;
|
|
2652
|
+
value: string;
|
|
2653
|
+
placeholder: string;
|
|
2654
|
+
options: string[];
|
|
2655
|
+
onSelect: (value: string) => void;
|
|
2656
|
+
grid?: boolean;
|
|
2657
|
+
}) {
|
|
2658
|
+
const [query, setQuery] = useState(props.value);
|
|
2659
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2660
|
+
|
|
2661
|
+
useEffect(() => {
|
|
2662
|
+
setQuery((prev) => (prev === props.value ? prev : props.value));
|
|
2663
|
+
}, [props.value]);
|
|
2664
|
+
|
|
2665
|
+
const filteredOptions = useMemo(() => {
|
|
2666
|
+
const q = query.trim().toLowerCase();
|
|
2667
|
+
if (!q) {
|
|
2668
|
+
return props.options.filter((option) => option.toLowerCase() !== 'system');
|
|
2669
|
+
}
|
|
2670
|
+
return props.options.filter((option) => option.toLowerCase().includes(q));
|
|
2671
|
+
}, [props.options, query]);
|
|
2672
|
+
|
|
2673
|
+
useEffect(() => {
|
|
2674
|
+
if (!isOpen || Platform.OS !== 'web') {
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
for (const option of filteredOptions.slice(0, 80)) {
|
|
2679
|
+
ensureWebFontLoaded(option);
|
|
2680
|
+
}
|
|
2681
|
+
}, [filteredOptions, isOpen]);
|
|
2682
|
+
|
|
2683
|
+
function commitValue(next: string) {
|
|
2684
|
+
const normalized = normalizeFontFamilyName(next);
|
|
2685
|
+
if (!normalized) {
|
|
2686
|
+
setQuery(props.value);
|
|
2687
|
+
setIsOpen(false);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
props.onSelect(normalized);
|
|
2691
|
+
setQuery(normalized);
|
|
2692
|
+
setIsOpen(false);
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
function openDropdown() {
|
|
2696
|
+
setIsOpen(true);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
return (
|
|
2700
|
+
<View
|
|
2701
|
+
style={[
|
|
2702
|
+
styles.field,
|
|
2703
|
+
props.grid ? styles.gridField : styles.fullField,
|
|
2704
|
+
isOpen ? styles.fieldOverlayOpen : null,
|
|
2705
|
+
]}
|
|
2706
|
+
>
|
|
2707
|
+
<Text style={[styles.fieldLabel, props.labelColor ? { color: props.labelColor } : null]}>
|
|
2708
|
+
{props.label}
|
|
2709
|
+
</Text>
|
|
2710
|
+
<View style={styles.comboboxWrap}>
|
|
2711
|
+
<View style={styles.inputWithAction}>
|
|
2712
|
+
<TextInput
|
|
2713
|
+
value={query}
|
|
2714
|
+
onChangeText={(value) => {
|
|
2715
|
+
setQuery(value);
|
|
2716
|
+
if (!isOpen) {
|
|
2717
|
+
openDropdown();
|
|
2718
|
+
}
|
|
2719
|
+
}}
|
|
2720
|
+
onFocus={() => {
|
|
2721
|
+
openDropdown();
|
|
2722
|
+
}}
|
|
2723
|
+
onSubmitEditing={() => commitValue(query)}
|
|
2724
|
+
blurOnSubmit
|
|
2725
|
+
returnKeyType="done"
|
|
2726
|
+
placeholder={props.placeholder}
|
|
2727
|
+
autoCorrect={false}
|
|
2728
|
+
autoCapitalize="none"
|
|
2729
|
+
style={[styles.input, styles.inputWithTrailingAction]}
|
|
2730
|
+
/>
|
|
2731
|
+
{query.trim().length > 0 ? (
|
|
2732
|
+
<Pressable
|
|
2733
|
+
accessibilityRole="button"
|
|
2734
|
+
accessibilityLabel={`Clear ${props.label} font`}
|
|
2735
|
+
onPress={() => {
|
|
2736
|
+
commitValue('System');
|
|
2737
|
+
}}
|
|
2738
|
+
style={styles.clearInputButton}
|
|
2739
|
+
>
|
|
2740
|
+
<Text style={styles.clearInputButtonText}>x</Text>
|
|
2741
|
+
</Pressable>
|
|
2742
|
+
) : null}
|
|
2743
|
+
</View>
|
|
2744
|
+
{isOpen ? (
|
|
2745
|
+
<View style={styles.comboboxDropdown}>
|
|
2746
|
+
<View style={styles.comboboxHeader}>
|
|
2747
|
+
<Text style={styles.comboboxHeaderText}>Choose a font</Text>
|
|
2748
|
+
<Pressable onPress={() => setIsOpen(false)} style={styles.comboboxCloseButton}>
|
|
2749
|
+
<Text style={styles.comboboxCloseText}>Close</Text>
|
|
2750
|
+
</Pressable>
|
|
2751
|
+
</View>
|
|
2752
|
+
<ScrollView
|
|
2753
|
+
keyboardShouldPersistTaps="handled"
|
|
2754
|
+
nestedScrollEnabled
|
|
2755
|
+
style={styles.comboboxScroll}
|
|
2756
|
+
>
|
|
2757
|
+
{filteredOptions.length === 0 ? (
|
|
2758
|
+
<Text style={styles.comboboxEmpty}>No matching fonts</Text>
|
|
2759
|
+
) : (
|
|
2760
|
+
filteredOptions.map((option) => (
|
|
2761
|
+
<Pressable
|
|
2762
|
+
key={`${props.label}-${option}`}
|
|
2763
|
+
onPress={() => {
|
|
2764
|
+
commitValue(option);
|
|
2765
|
+
}}
|
|
2766
|
+
style={styles.comboboxOption}
|
|
2767
|
+
>
|
|
2768
|
+
<Text
|
|
2769
|
+
style={[
|
|
2770
|
+
styles.comboboxOptionText,
|
|
2771
|
+
{ fontFamily: resolvePreviewFontFamily(option) },
|
|
2772
|
+
]}
|
|
2773
|
+
>
|
|
2774
|
+
{option}
|
|
2775
|
+
</Text>
|
|
2776
|
+
</Pressable>
|
|
2777
|
+
))
|
|
2778
|
+
)}
|
|
2779
|
+
</ScrollView>
|
|
2780
|
+
</View>
|
|
2781
|
+
) : null}
|
|
2782
|
+
</View>
|
|
2783
|
+
</View>
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
const styles = StyleSheet.create({
|
|
2788
|
+
root: {
|
|
2789
|
+
flex: 1,
|
|
2790
|
+
},
|
|
2791
|
+
screen: {
|
|
2792
|
+
flex: 1,
|
|
2793
|
+
},
|
|
2794
|
+
content: {
|
|
2795
|
+
flexGrow: 1,
|
|
2796
|
+
gap: 18,
|
|
2797
|
+
padding: 20,
|
|
2798
|
+
},
|
|
2799
|
+
controls: {
|
|
2800
|
+
alignItems: 'stretch',
|
|
2801
|
+
backgroundColor: '#ffffff',
|
|
2802
|
+
borderColor: '#d1d5db',
|
|
2803
|
+
borderRadius: 12,
|
|
2804
|
+
borderWidth: 1,
|
|
2805
|
+
flexDirection: 'row',
|
|
2806
|
+
gap: 10,
|
|
2807
|
+
left: 12,
|
|
2808
|
+
padding: 8,
|
|
2809
|
+
position: 'absolute',
|
|
2810
|
+
right: 12,
|
|
2811
|
+
zIndex: 30,
|
|
2812
|
+
},
|
|
2813
|
+
toggleRow: {
|
|
2814
|
+
backgroundColor: '#ffffff',
|
|
2815
|
+
borderColor: '#cbd5e1',
|
|
2816
|
+
borderRadius: 12,
|
|
2817
|
+
borderWidth: 1,
|
|
2818
|
+
flex: 1,
|
|
2819
|
+
gap: 6,
|
|
2820
|
+
padding: 8,
|
|
2821
|
+
},
|
|
2822
|
+
toggleLabel: {
|
|
2823
|
+
color: '#111827',
|
|
2824
|
+
fontSize: 11,
|
|
2825
|
+
fontWeight: '700',
|
|
2826
|
+
textTransform: 'uppercase',
|
|
2827
|
+
},
|
|
2828
|
+
toggleLabelRow: {
|
|
2829
|
+
alignItems: 'center',
|
|
2830
|
+
flexDirection: 'row',
|
|
2831
|
+
justifyContent: 'space-between',
|
|
2832
|
+
},
|
|
2833
|
+
toggleInfoButton: {
|
|
2834
|
+
alignItems: 'center',
|
|
2835
|
+
borderColor: '#cbd5e1',
|
|
2836
|
+
borderRadius: 999,
|
|
2837
|
+
borderWidth: 1,
|
|
2838
|
+
height: 20,
|
|
2839
|
+
justifyContent: 'center',
|
|
2840
|
+
width: 20,
|
|
2841
|
+
},
|
|
2842
|
+
toggleInfoButtonText: {
|
|
2843
|
+
color: '#111827',
|
|
2844
|
+
fontSize: 11,
|
|
2845
|
+
fontWeight: '800',
|
|
2846
|
+
},
|
|
2847
|
+
toggleOptions: {
|
|
2848
|
+
flexDirection: 'row',
|
|
2849
|
+
flexWrap: 'wrap',
|
|
2850
|
+
gap: 6,
|
|
2851
|
+
},
|
|
2852
|
+
toggleOption: {
|
|
2853
|
+
borderColor: '#cbd5e1',
|
|
2854
|
+
borderRadius: 999,
|
|
2855
|
+
borderWidth: 1,
|
|
2856
|
+
paddingHorizontal: 10,
|
|
2857
|
+
paddingVertical: 6,
|
|
2858
|
+
},
|
|
2859
|
+
toggleOptionSelected: {
|
|
2860
|
+
backgroundColor: '#111827',
|
|
2861
|
+
borderColor: '#111827',
|
|
2862
|
+
},
|
|
2863
|
+
toggleOptionText: {
|
|
2864
|
+
color: '#111827',
|
|
2865
|
+
fontSize: 12,
|
|
2866
|
+
fontWeight: '700',
|
|
2867
|
+
},
|
|
2868
|
+
toggleOptionTextSelected: {
|
|
2869
|
+
color: '#f9fafb',
|
|
2870
|
+
},
|
|
2871
|
+
title: {
|
|
2872
|
+
fontSize: 30,
|
|
2873
|
+
fontWeight: '900',
|
|
2874
|
+
},
|
|
2875
|
+
titleRow: {
|
|
2876
|
+
alignItems: 'center',
|
|
2877
|
+
flexDirection: 'row',
|
|
2878
|
+
gap: 12,
|
|
2879
|
+
justifyContent: 'space-between',
|
|
2880
|
+
},
|
|
2881
|
+
intro: {
|
|
2882
|
+
fontSize: 15,
|
|
2883
|
+
lineHeight: 22,
|
|
2884
|
+
},
|
|
2885
|
+
fontBanner: {
|
|
2886
|
+
backgroundColor: '#ffffff',
|
|
2887
|
+
borderRadius: 12,
|
|
2888
|
+
borderWidth: 1,
|
|
2889
|
+
gap: 10,
|
|
2890
|
+
padding: 12,
|
|
2891
|
+
},
|
|
2892
|
+
fontBannerHeader: {
|
|
2893
|
+
alignItems: 'center',
|
|
2894
|
+
flexDirection: 'row',
|
|
2895
|
+
justifyContent: 'space-between',
|
|
2896
|
+
},
|
|
2897
|
+
fontBannerTitle: {
|
|
2898
|
+
color: '#111827',
|
|
2899
|
+
fontSize: 14,
|
|
2900
|
+
fontWeight: '800',
|
|
2901
|
+
},
|
|
2902
|
+
fontBannerBody: {
|
|
2903
|
+
color: '#374151',
|
|
2904
|
+
fontSize: 12,
|
|
2905
|
+
lineHeight: 18,
|
|
2906
|
+
},
|
|
2907
|
+
bannerDismissButton: {
|
|
2908
|
+
borderColor: '#cbd5e1',
|
|
2909
|
+
borderRadius: 999,
|
|
2910
|
+
borderWidth: 1,
|
|
2911
|
+
height: 26,
|
|
2912
|
+
width: 26,
|
|
2913
|
+
alignItems: 'center',
|
|
2914
|
+
justifyContent: 'center',
|
|
2915
|
+
},
|
|
2916
|
+
bannerDismissText: {
|
|
2917
|
+
color: '#111827',
|
|
2918
|
+
fontSize: 12,
|
|
2919
|
+
fontWeight: '800',
|
|
2920
|
+
},
|
|
2921
|
+
fontBannerActions: {
|
|
2922
|
+
flexDirection: 'row',
|
|
2923
|
+
flexWrap: 'wrap',
|
|
2924
|
+
gap: 8,
|
|
2925
|
+
},
|
|
2926
|
+
bannerAction: {
|
|
2927
|
+
alignItems: 'center',
|
|
2928
|
+
borderRadius: 10,
|
|
2929
|
+
justifyContent: 'center',
|
|
2930
|
+
minHeight: 36,
|
|
2931
|
+
paddingHorizontal: 12,
|
|
2932
|
+
},
|
|
2933
|
+
bannerActionText: {
|
|
2934
|
+
color: '#ffffff',
|
|
2935
|
+
fontSize: 12,
|
|
2936
|
+
fontWeight: '800',
|
|
2937
|
+
},
|
|
2938
|
+
bannerActionGhost: {
|
|
2939
|
+
backgroundColor: '#ffffff',
|
|
2940
|
+
borderColor: '#cbd5e1',
|
|
2941
|
+
borderWidth: 1,
|
|
2942
|
+
},
|
|
2943
|
+
bannerActionGhostText: {
|
|
2944
|
+
color: '#111827',
|
|
2945
|
+
fontSize: 12,
|
|
2946
|
+
fontWeight: '700',
|
|
2947
|
+
},
|
|
2948
|
+
fontError: {
|
|
2949
|
+
color: '#b91c1c',
|
|
2950
|
+
fontSize: 12,
|
|
2951
|
+
fontWeight: '600',
|
|
2952
|
+
},
|
|
2953
|
+
section: {
|
|
2954
|
+
gap: 12,
|
|
2955
|
+
padding: 16,
|
|
2956
|
+
},
|
|
2957
|
+
sectionOverlay: {
|
|
2958
|
+
overflow: 'visible',
|
|
2959
|
+
zIndex: 40,
|
|
2960
|
+
},
|
|
2961
|
+
typographySection: {
|
|
2962
|
+
overflow: 'visible',
|
|
2963
|
+
zIndex: 90,
|
|
2964
|
+
},
|
|
2965
|
+
sectionHeaderRow: {
|
|
2966
|
+
alignItems: 'center',
|
|
2967
|
+
flexDirection: 'row',
|
|
2968
|
+
gap: 12,
|
|
2969
|
+
justifyContent: 'space-between',
|
|
2970
|
+
},
|
|
2971
|
+
sectionTitle: {
|
|
2972
|
+
fontSize: 18,
|
|
2973
|
+
fontWeight: '800',
|
|
2974
|
+
},
|
|
2975
|
+
inlineToggle: {
|
|
2976
|
+
flexDirection: 'row',
|
|
2977
|
+
flexWrap: 'wrap',
|
|
2978
|
+
gap: 6,
|
|
2979
|
+
justifyContent: 'flex-end',
|
|
2980
|
+
},
|
|
2981
|
+
inlineToggleOption: {
|
|
2982
|
+
borderColor: '#cbd5e1',
|
|
2983
|
+
borderRadius: 999,
|
|
2984
|
+
borderWidth: 1,
|
|
2985
|
+
paddingHorizontal: 10,
|
|
2986
|
+
paddingVertical: 6,
|
|
2987
|
+
},
|
|
2988
|
+
inlineToggleOptionSelected: {
|
|
2989
|
+
backgroundColor: '#111827',
|
|
2990
|
+
borderColor: '#111827',
|
|
2991
|
+
},
|
|
2992
|
+
inlineToggleLabel: {
|
|
2993
|
+
fontSize: 12,
|
|
2994
|
+
fontWeight: '700',
|
|
2995
|
+
},
|
|
2996
|
+
presetButton: {
|
|
2997
|
+
borderRadius: 999,
|
|
2998
|
+
borderWidth: 1,
|
|
2999
|
+
paddingHorizontal: 12,
|
|
3000
|
+
paddingVertical: 7,
|
|
3001
|
+
},
|
|
3002
|
+
presetButtonText: {
|
|
3003
|
+
fontSize: 12,
|
|
3004
|
+
fontWeight: '800',
|
|
3005
|
+
},
|
|
3006
|
+
helperText: {
|
|
3007
|
+
fontSize: 13,
|
|
3008
|
+
lineHeight: 19,
|
|
3009
|
+
opacity: 0.9,
|
|
3010
|
+
},
|
|
3011
|
+
lockedNote: {
|
|
3012
|
+
fontSize: 12,
|
|
3013
|
+
opacity: 0.75,
|
|
3014
|
+
},
|
|
3015
|
+
colorRow: {
|
|
3016
|
+
flexDirection: 'row',
|
|
3017
|
+
flexWrap: 'wrap',
|
|
3018
|
+
gap: 8,
|
|
3019
|
+
},
|
|
3020
|
+
colorChip: {
|
|
3021
|
+
borderRadius: 999,
|
|
3022
|
+
borderWidth: 2,
|
|
3023
|
+
minWidth: 100,
|
|
3024
|
+
paddingHorizontal: 10,
|
|
3025
|
+
paddingVertical: 8,
|
|
3026
|
+
},
|
|
3027
|
+
colorChipLabel: {
|
|
3028
|
+
color: '#d1d5db',
|
|
3029
|
+
fontSize: 12,
|
|
3030
|
+
fontWeight: '700',
|
|
3031
|
+
textTransform: 'capitalize',
|
|
3032
|
+
},
|
|
3033
|
+
picker: {
|
|
3034
|
+
gap: 12,
|
|
3035
|
+
width: '100%',
|
|
3036
|
+
},
|
|
3037
|
+
pickerHexRow: {
|
|
3038
|
+
alignItems: 'center',
|
|
3039
|
+
flexDirection: 'row',
|
|
3040
|
+
gap: 10,
|
|
3041
|
+
},
|
|
3042
|
+
pickerHexLabel: {
|
|
3043
|
+
color: '#111827',
|
|
3044
|
+
fontSize: 12,
|
|
3045
|
+
fontWeight: '800',
|
|
3046
|
+
textTransform: 'uppercase',
|
|
3047
|
+
},
|
|
3048
|
+
pickerHexInput: {
|
|
3049
|
+
flex: 1,
|
|
3050
|
+
fontFamily: 'monospace',
|
|
3051
|
+
minHeight: 38,
|
|
3052
|
+
},
|
|
3053
|
+
manualSwatchesRow: {
|
|
3054
|
+
flexDirection: 'row',
|
|
3055
|
+
flexWrap: 'nowrap',
|
|
3056
|
+
justifyContent: 'space-between',
|
|
3057
|
+
width: '100%',
|
|
3058
|
+
},
|
|
3059
|
+
manualSwatch: {
|
|
3060
|
+
borderColor: '#d1d5db',
|
|
3061
|
+
borderRadius: 999,
|
|
3062
|
+
borderWidth: 1,
|
|
3063
|
+
flexShrink: 1,
|
|
3064
|
+
height: 32,
|
|
3065
|
+
width: 32,
|
|
3066
|
+
},
|
|
3067
|
+
manualSwatchSelected: {
|
|
3068
|
+
borderColor: '#111827',
|
|
3069
|
+
borderWidth: 3,
|
|
3070
|
+
},
|
|
3071
|
+
familyBlock: {
|
|
3072
|
+
gap: 12,
|
|
3073
|
+
},
|
|
3074
|
+
familyRow: {
|
|
3075
|
+
gap: 6,
|
|
3076
|
+
},
|
|
3077
|
+
shadeRow: {
|
|
3078
|
+
alignItems: 'center',
|
|
3079
|
+
flexDirection: 'row',
|
|
3080
|
+
flexWrap: 'wrap',
|
|
3081
|
+
gap: 8,
|
|
3082
|
+
},
|
|
3083
|
+
shadeDot: {
|
|
3084
|
+
borderRadius: 999,
|
|
3085
|
+
height: 18,
|
|
3086
|
+
width: 18,
|
|
3087
|
+
},
|
|
3088
|
+
familyHeader: {
|
|
3089
|
+
alignItems: 'center',
|
|
3090
|
+
flexDirection: 'row',
|
|
3091
|
+
flexWrap: 'wrap',
|
|
3092
|
+
gap: 8,
|
|
3093
|
+
},
|
|
3094
|
+
familyTitle: {
|
|
3095
|
+
fontSize: 12,
|
|
3096
|
+
fontWeight: '700',
|
|
3097
|
+
textTransform: 'capitalize',
|
|
3098
|
+
},
|
|
3099
|
+
familyTitleChip: {
|
|
3100
|
+
borderRadius: 999,
|
|
3101
|
+
borderWidth: 1,
|
|
3102
|
+
paddingHorizontal: 12,
|
|
3103
|
+
paddingVertical: 7,
|
|
3104
|
+
},
|
|
3105
|
+
familyTitleChipText: {
|
|
3106
|
+
fontSize: 12,
|
|
3107
|
+
fontWeight: '800',
|
|
3108
|
+
textTransform: 'capitalize',
|
|
3109
|
+
},
|
|
3110
|
+
familySchemeHint: {
|
|
3111
|
+
fontSize: 12,
|
|
3112
|
+
fontWeight: '700',
|
|
3113
|
+
opacity: 0.72,
|
|
3114
|
+
textTransform: 'capitalize',
|
|
3115
|
+
},
|
|
3116
|
+
familyOptions: {
|
|
3117
|
+
flexDirection: 'row',
|
|
3118
|
+
flexWrap: 'wrap',
|
|
3119
|
+
gap: 6,
|
|
3120
|
+
},
|
|
3121
|
+
familyOption: {
|
|
3122
|
+
borderColor: '#cbd5e1',
|
|
3123
|
+
borderRadius: 999,
|
|
3124
|
+
borderWidth: 1,
|
|
3125
|
+
paddingHorizontal: 10,
|
|
3126
|
+
paddingVertical: 6,
|
|
3127
|
+
},
|
|
3128
|
+
familyOptionText: {
|
|
3129
|
+
fontSize: 12,
|
|
3130
|
+
fontWeight: '700',
|
|
3131
|
+
textTransform: 'capitalize',
|
|
3132
|
+
},
|
|
3133
|
+
galleryRow: {
|
|
3134
|
+
flexDirection: 'row',
|
|
3135
|
+
flexWrap: 'wrap',
|
|
3136
|
+
gap: 10,
|
|
3137
|
+
},
|
|
3138
|
+
galleryCard: {
|
|
3139
|
+
borderRadius: 12,
|
|
3140
|
+
borderWidth: 1,
|
|
3141
|
+
flex: 1,
|
|
3142
|
+
gap: 4,
|
|
3143
|
+
minWidth: 220,
|
|
3144
|
+
padding: 12,
|
|
3145
|
+
},
|
|
3146
|
+
galleryCardSoft: {
|
|
3147
|
+
backgroundColor: 'rgba(148,163,184,0.14)',
|
|
3148
|
+
},
|
|
3149
|
+
galleryCardTitle: {
|
|
3150
|
+
fontSize: 14,
|
|
3151
|
+
fontWeight: '800',
|
|
3152
|
+
},
|
|
3153
|
+
galleryCardBody: {
|
|
3154
|
+
fontSize: 12,
|
|
3155
|
+
lineHeight: 18,
|
|
3156
|
+
opacity: 0.92,
|
|
3157
|
+
},
|
|
3158
|
+
statusRow: {
|
|
3159
|
+
flexDirection: 'row',
|
|
3160
|
+
flexWrap: 'wrap',
|
|
3161
|
+
gap: 8,
|
|
3162
|
+
},
|
|
3163
|
+
statusPill: {
|
|
3164
|
+
borderRadius: 999,
|
|
3165
|
+
paddingHorizontal: 10,
|
|
3166
|
+
paddingVertical: 6,
|
|
3167
|
+
},
|
|
3168
|
+
statusPillText: {
|
|
3169
|
+
color: '#f8fafc',
|
|
3170
|
+
fontSize: 11,
|
|
3171
|
+
fontWeight: '800',
|
|
3172
|
+
textTransform: 'uppercase',
|
|
3173
|
+
},
|
|
3174
|
+
inputStatesRow: {
|
|
3175
|
+
gap: 8,
|
|
3176
|
+
},
|
|
3177
|
+
spacingPreview: {
|
|
3178
|
+
gap: 8,
|
|
3179
|
+
},
|
|
3180
|
+
spacingPreviewRow: {
|
|
3181
|
+
alignItems: 'center',
|
|
3182
|
+
flexDirection: 'row',
|
|
3183
|
+
gap: 8,
|
|
3184
|
+
},
|
|
3185
|
+
spacingLabel: {
|
|
3186
|
+
color: '#374151',
|
|
3187
|
+
fontSize: 11,
|
|
3188
|
+
fontWeight: '700',
|
|
3189
|
+
textTransform: 'uppercase',
|
|
3190
|
+
width: 32,
|
|
3191
|
+
},
|
|
3192
|
+
spacingBar: {
|
|
3193
|
+
height: 12,
|
|
3194
|
+
},
|
|
3195
|
+
layoutPreviewContainer: {
|
|
3196
|
+
borderWidth: 1,
|
|
3197
|
+
},
|
|
3198
|
+
layoutPreviewCard: {
|
|
3199
|
+
borderWidth: 1,
|
|
3200
|
+
},
|
|
3201
|
+
layoutPreviewTitle: {
|
|
3202
|
+
fontSize: 13,
|
|
3203
|
+
fontWeight: '800',
|
|
3204
|
+
marginBottom: 4,
|
|
3205
|
+
},
|
|
3206
|
+
layoutPreviewBody: {
|
|
3207
|
+
fontSize: 12,
|
|
3208
|
+
lineHeight: 17,
|
|
3209
|
+
opacity: 0.9,
|
|
3210
|
+
},
|
|
3211
|
+
grid: {
|
|
3212
|
+
flexDirection: 'row',
|
|
3213
|
+
flexWrap: 'wrap',
|
|
3214
|
+
gap: 10,
|
|
3215
|
+
},
|
|
3216
|
+
fontPickerGrid: {
|
|
3217
|
+
overflow: 'visible',
|
|
3218
|
+
zIndex: 120,
|
|
3219
|
+
},
|
|
3220
|
+
tokenSizeGrid: {
|
|
3221
|
+
zIndex: 1,
|
|
3222
|
+
},
|
|
3223
|
+
field: {
|
|
3224
|
+
gap: 6,
|
|
3225
|
+
},
|
|
3226
|
+
fieldOverlayOpen: {
|
|
3227
|
+
zIndex: 500,
|
|
3228
|
+
},
|
|
3229
|
+
fullField: {
|
|
3230
|
+
width: '100%',
|
|
3231
|
+
},
|
|
3232
|
+
gridField: {
|
|
3233
|
+
flexBasis: '48%',
|
|
3234
|
+
flexGrow: 1,
|
|
3235
|
+
minWidth: 220,
|
|
3236
|
+
},
|
|
3237
|
+
fieldLabel: {
|
|
3238
|
+
color: '#374151',
|
|
3239
|
+
fontSize: 12,
|
|
3240
|
+
fontWeight: '700',
|
|
3241
|
+
},
|
|
3242
|
+
comboboxWrap: {
|
|
3243
|
+
position: 'relative',
|
|
3244
|
+
},
|
|
3245
|
+
comboboxDropdown: {
|
|
3246
|
+
backgroundColor: '#ffffff',
|
|
3247
|
+
borderColor: '#cbd5e1',
|
|
3248
|
+
borderRadius: 10,
|
|
3249
|
+
borderWidth: 1,
|
|
3250
|
+
elevation: 4,
|
|
3251
|
+
marginTop: 6,
|
|
3252
|
+
maxHeight: 220,
|
|
3253
|
+
overflow: 'hidden',
|
|
3254
|
+
},
|
|
3255
|
+
comboboxScroll: {
|
|
3256
|
+
maxHeight: 168,
|
|
3257
|
+
},
|
|
3258
|
+
comboboxHeader: {
|
|
3259
|
+
alignItems: 'center',
|
|
3260
|
+
borderBottomColor: '#e5e7eb',
|
|
3261
|
+
borderBottomWidth: 1,
|
|
3262
|
+
flexDirection: 'row',
|
|
3263
|
+
justifyContent: 'space-between',
|
|
3264
|
+
paddingHorizontal: 12,
|
|
3265
|
+
paddingVertical: 8,
|
|
3266
|
+
},
|
|
3267
|
+
comboboxHeaderText: {
|
|
3268
|
+
color: '#374151',
|
|
3269
|
+
fontSize: 12,
|
|
3270
|
+
fontWeight: '800',
|
|
3271
|
+
},
|
|
3272
|
+
comboboxCloseButton: {
|
|
3273
|
+
borderColor: '#cbd5e1',
|
|
3274
|
+
borderRadius: 999,
|
|
3275
|
+
borderWidth: 1,
|
|
3276
|
+
paddingHorizontal: 10,
|
|
3277
|
+
paddingVertical: 5,
|
|
3278
|
+
},
|
|
3279
|
+
comboboxCloseText: {
|
|
3280
|
+
color: '#111827',
|
|
3281
|
+
fontSize: 12,
|
|
3282
|
+
fontWeight: '800',
|
|
3283
|
+
},
|
|
3284
|
+
comboboxOption: {
|
|
3285
|
+
borderBottomColor: '#e5e7eb',
|
|
3286
|
+
borderBottomWidth: 1,
|
|
3287
|
+
paddingHorizontal: 12,
|
|
3288
|
+
paddingVertical: 10,
|
|
3289
|
+
},
|
|
3290
|
+
comboboxOptionText: {
|
|
3291
|
+
color: '#111827',
|
|
3292
|
+
fontSize: 13,
|
|
3293
|
+
},
|
|
3294
|
+
comboboxEmpty: {
|
|
3295
|
+
color: '#6b7280',
|
|
3296
|
+
fontSize: 12,
|
|
3297
|
+
paddingHorizontal: 12,
|
|
3298
|
+
paddingVertical: 10,
|
|
3299
|
+
},
|
|
3300
|
+
input: {
|
|
3301
|
+
backgroundColor: '#ffffff',
|
|
3302
|
+
borderColor: '#d1d5db',
|
|
3303
|
+
borderRadius: 10,
|
|
3304
|
+
borderWidth: 1,
|
|
3305
|
+
minHeight: 42,
|
|
3306
|
+
paddingHorizontal: 12,
|
|
3307
|
+
},
|
|
3308
|
+
inputWithAction: {
|
|
3309
|
+
position: 'relative',
|
|
3310
|
+
},
|
|
3311
|
+
inputWithTrailingAction: {
|
|
3312
|
+
paddingRight: 38,
|
|
3313
|
+
},
|
|
3314
|
+
clearInputButton: {
|
|
3315
|
+
alignItems: 'center',
|
|
3316
|
+
borderColor: '#cbd5e1',
|
|
3317
|
+
borderRadius: 999,
|
|
3318
|
+
borderWidth: 1,
|
|
3319
|
+
height: 24,
|
|
3320
|
+
justifyContent: 'center',
|
|
3321
|
+
position: 'absolute',
|
|
3322
|
+
right: 10,
|
|
3323
|
+
top: 9,
|
|
3324
|
+
width: 24,
|
|
3325
|
+
},
|
|
3326
|
+
clearInputButtonText: {
|
|
3327
|
+
color: '#111827',
|
|
3328
|
+
fontSize: 12,
|
|
3329
|
+
fontWeight: '800',
|
|
3330
|
+
lineHeight: 14,
|
|
3331
|
+
textTransform: 'uppercase',
|
|
3332
|
+
},
|
|
3333
|
+
saveButton: {
|
|
3334
|
+
borderRadius: 12,
|
|
3335
|
+
minHeight: 48,
|
|
3336
|
+
alignItems: 'center',
|
|
3337
|
+
justifyContent: 'center',
|
|
3338
|
+
},
|
|
3339
|
+
saveButtonInline: {
|
|
3340
|
+
minHeight: 40,
|
|
3341
|
+
paddingHorizontal: 14,
|
|
3342
|
+
paddingVertical: 8,
|
|
3343
|
+
},
|
|
3344
|
+
saveButtonText: {
|
|
3345
|
+
color: '#ffffff',
|
|
3346
|
+
fontSize: 16,
|
|
3347
|
+
fontWeight: '800',
|
|
3348
|
+
},
|
|
3349
|
+
saveMessageBanner: {
|
|
3350
|
+
borderRadius: 10,
|
|
3351
|
+
borderWidth: 1,
|
|
3352
|
+
paddingHorizontal: 12,
|
|
3353
|
+
paddingVertical: 10,
|
|
3354
|
+
},
|
|
3355
|
+
saveMessageText: {
|
|
3356
|
+
fontSize: 13,
|
|
3357
|
+
fontWeight: '700',
|
|
3358
|
+
lineHeight: 18,
|
|
3359
|
+
},
|
|
3360
|
+
nativeHelp: {
|
|
3361
|
+
backgroundColor: '#ffffff',
|
|
3362
|
+
borderColor: '#e5e7eb',
|
|
3363
|
+
borderRadius: 12,
|
|
3364
|
+
borderWidth: 1,
|
|
3365
|
+
gap: 8,
|
|
3366
|
+
padding: 12,
|
|
3367
|
+
},
|
|
3368
|
+
nativeTitle: {
|
|
3369
|
+
color: '#111827',
|
|
3370
|
+
fontSize: 14,
|
|
3371
|
+
fontWeight: '800',
|
|
3372
|
+
},
|
|
3373
|
+
nativeBody: {
|
|
3374
|
+
color: '#374151',
|
|
3375
|
+
fontSize: 12,
|
|
3376
|
+
},
|
|
3377
|
+
command: {
|
|
3378
|
+
backgroundColor: '#111827',
|
|
3379
|
+
borderRadius: 8,
|
|
3380
|
+
color: '#f9fafb',
|
|
3381
|
+
fontFamily: 'monospace',
|
|
3382
|
+
fontSize: 12,
|
|
3383
|
+
padding: 10,
|
|
3384
|
+
},
|
|
3385
|
+
payload: {
|
|
3386
|
+
color: '#1f2937',
|
|
3387
|
+
fontFamily: 'monospace',
|
|
3388
|
+
fontSize: 11,
|
|
3389
|
+
lineHeight: 16,
|
|
3390
|
+
},
|
|
3391
|
+
modalBackdrop: {
|
|
3392
|
+
backgroundColor: 'rgba(15, 23, 42, 0.48)',
|
|
3393
|
+
flex: 1,
|
|
3394
|
+
alignItems: 'center',
|
|
3395
|
+
justifyContent: 'center',
|
|
3396
|
+
padding: 20,
|
|
3397
|
+
},
|
|
3398
|
+
modalCard: {
|
|
3399
|
+
backgroundColor: '#ffffff',
|
|
3400
|
+
borderColor: '#cbd5e1',
|
|
3401
|
+
borderRadius: 14,
|
|
3402
|
+
borderWidth: 1,
|
|
3403
|
+
gap: 10,
|
|
3404
|
+
maxWidth: 420,
|
|
3405
|
+
padding: 16,
|
|
3406
|
+
width: '100%',
|
|
3407
|
+
},
|
|
3408
|
+
modalTitle: {
|
|
3409
|
+
color: '#0f172a',
|
|
3410
|
+
fontSize: 18,
|
|
3411
|
+
fontWeight: '800',
|
|
3412
|
+
},
|
|
3413
|
+
modalBody: {
|
|
3414
|
+
color: '#334155',
|
|
3415
|
+
fontSize: 13,
|
|
3416
|
+
lineHeight: 19,
|
|
3417
|
+
},
|
|
3418
|
+
modalActions: {
|
|
3419
|
+
gap: 8,
|
|
3420
|
+
},
|
|
3421
|
+
modalButton: {
|
|
3422
|
+
borderRadius: 10,
|
|
3423
|
+
minHeight: 42,
|
|
3424
|
+
alignItems: 'center',
|
|
3425
|
+
justifyContent: 'center',
|
|
3426
|
+
paddingHorizontal: 12,
|
|
3427
|
+
},
|
|
3428
|
+
modalButtonPrimary: {
|
|
3429
|
+
backgroundColor: '#0f172a',
|
|
3430
|
+
},
|
|
3431
|
+
modalButtonSecondary: {
|
|
3432
|
+
backgroundColor: '#ffffff',
|
|
3433
|
+
borderColor: '#cbd5e1',
|
|
3434
|
+
borderWidth: 1,
|
|
3435
|
+
},
|
|
3436
|
+
modalButtonPrimaryText: {
|
|
3437
|
+
color: '#ffffff',
|
|
3438
|
+
fontSize: 13,
|
|
3439
|
+
fontWeight: '800',
|
|
3440
|
+
},
|
|
3441
|
+
modalButtonSecondaryText: {
|
|
3442
|
+
color: '#0f172a',
|
|
3443
|
+
fontSize: 13,
|
|
3444
|
+
fontWeight: '700',
|
|
3445
|
+
},
|
|
3446
|
+
});
|