@developer_tribe/react-builder 1.0.3 → 1.0.4
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/android.svg +43 -0
- package/dist/apple.svg +16 -0
- package/dist/attributes-editor/Field.d.ts +2 -1
- package/dist/attributes-editor/SizeField.d.ts +9 -0
- package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +1 -0
- package/dist/build-components/Button/ButtonProps.generated.d.ts +1 -0
- package/dist/build-components/Carousel/CarouselProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +1 -0
- package/dist/build-components/Image/ImageProps.generated.d.ts +1 -0
- package/dist/build-components/Onboard/OnboardProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +1 -1
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +2 -3
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +2 -1
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +1 -0
- package/dist/build-components/Text/TextProps.generated.d.ts +1 -0
- package/dist/build-components/View/ViewProps.generated.d.ts +1 -0
- package/dist/build-components/patterns.generated.d.ts +194 -57
- package/dist/components/JsonTextEditor.d.ts +9 -0
- package/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +5 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/pages/tabs/SideTool.d.ts +2 -1
- package/dist/store.d.ts +2 -0
- package/dist/styles.css +1 -1
- package/dist/utils/extractImageStyle.d.ts +2 -1
- package/dist/utils/extractViewStyle.d.ts +1 -2
- package/dist/utils/selection.d.ts +7 -0
- package/dist/utils/useMergedStyle.d.ts +2 -0
- package/package.json +2 -5
- package/src/.DS_Store +0 -0
- package/src/AttributesEditor.tsx +7 -2
- package/src/RenderPage.tsx +10 -6
- package/src/attributes-editor/Field.tsx +48 -160
- package/src/attributes-editor/SizeField.tsx +184 -0
- package/src/attributes-editor/SpecialCategorySection.tsx +10 -3
- package/src/build-components/BackgroundImage/BackgroundImage.tsx +7 -17
- package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +1 -0
- package/src/build-components/Button/Button.tsx +7 -9
- package/src/build-components/Button/ButtonProps.generated.ts +1 -0
- package/src/build-components/Carousel/Carousel.tsx +7 -9
- package/src/build-components/Carousel/CarouselProps.generated.ts +1 -0
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +1 -0
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +1 -0
- package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +1 -0
- package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +1 -0
- package/src/build-components/Image/Image.tsx +11 -18
- package/src/build-components/Image/ImageProps.generated.ts +1 -0
- package/src/build-components/Image/pattern.json +1 -9
- package/src/build-components/Onboard/OnboardProps.generated.ts +1 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +0 -3
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +1 -1
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +1 -0
- package/src/build-components/OnboardDot/OnboardDot.tsx +59 -39
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +2 -3
- package/src/build-components/OnboardDot/pattern.json +2 -18
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +28 -15
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +1 -0
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +2 -1
- package/src/build-components/OnboardItem/OnboardItem.tsx +1 -11
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +1 -0
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +1 -8
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +1 -0
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +1 -0
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +1 -0
- package/src/build-components/Text/Text.tsx +9 -15
- package/src/build-components/Text/TextProps.generated.ts +1 -0
- package/src/build-components/View/View.tsx +7 -9
- package/src/build-components/View/ViewProps.generated.ts +1 -0
- package/src/build-components/View/pattern.json +9 -1
- package/src/build-components/patterns.generated.ts +194 -57
- package/src/components/Builder.tsx +61 -17
- package/src/components/DeviceNavigationBar.tsx +0 -1
- package/src/components/EditorHeader.tsx +11 -1
- package/src/components/JsonTextEditor.tsx +185 -0
- package/src/mockOS/components/MockOSRouter.tsx +6 -0
- package/src/mockOS/context/MockOSContext.tsx +0 -5
- package/src/mockOS/managers/mockPermissionManager.ts +0 -4
- package/src/mockOS/managers/navigationManager.ts +1 -6
- package/src/modals/ColorModal.tsx +103 -25
- package/src/modals/LocalicationModal.tsx +4 -5
- package/src/modals/Modal.tsx +8 -1
- package/src/pages/ProjectPage.tsx +7 -1
- package/src/pages/tabs/SideTool.tsx +10 -9
- package/src/store.ts +5 -0
- package/src/styles/base/_global.scss +5 -0
- package/src/styles/components/_editor-shell.scss +4 -2
- package/src/styles/modals/_color-modal.scss +30 -1
- package/src/styles/utilities/_carousel.scss +9 -8
- package/src/utils/extractImageStyle.ts +3 -6
- package/src/utils/extractTextStyle.ts +2 -81
- package/src/utils/extractViewStyle.ts +20 -15
- package/src/utils/selection.ts +24 -0
- package/src/utils/useMergedStyle.ts +16 -0
|
@@ -41,9 +41,6 @@ function BuilderComponent({
|
|
|
41
41
|
onMoveChildUp,
|
|
42
42
|
onMoveChildDown,
|
|
43
43
|
}: BuilderEditorComponentProps) {
|
|
44
|
-
if (isNodeNullOrUndefined(node)) {
|
|
45
|
-
return <div className="builder__placeholder">Null or undefined</div>;
|
|
46
|
-
}
|
|
47
44
|
if (isNodeString(node)) {
|
|
48
45
|
return (
|
|
49
46
|
<div className="builder__text">
|
|
@@ -105,7 +102,7 @@ function BuilderComponent({
|
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
const nodeData = node as NodeData<NodeDefaultAttribute>;
|
|
108
|
-
const rawChildren = nodeData
|
|
105
|
+
const rawChildren = nodeData?.children;
|
|
109
106
|
const hasArrayChildren = isNodeArray(rawChildren);
|
|
110
107
|
const children = rawChildren
|
|
111
108
|
? hasArrayChildren
|
|
@@ -195,16 +192,55 @@ export function Builder({
|
|
|
195
192
|
|
|
196
193
|
const handleAddChild = useCallback(
|
|
197
194
|
(type: string) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
const nextChild = createDefaultNode(type);
|
|
196
|
+
|
|
197
|
+
// Root (or selection) can be empty/null-ish: allow creating the first node.
|
|
198
|
+
if (isNodeNullOrUndefined(current)) {
|
|
199
|
+
// If the project itself is empty (or a placeholder string), replace it with the first node.
|
|
200
|
+
if (isNodeNullOrUndefined(data) || isNodeString(data)) {
|
|
201
|
+
setData(nextChild);
|
|
202
|
+
setCurrent(nextChild);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// If the project root is a list, append into it.
|
|
207
|
+
if (Array.isArray(data)) {
|
|
208
|
+
const nextList = [...data, nextChild];
|
|
209
|
+
setData(nextList);
|
|
210
|
+
setCurrent(nextList);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Otherwise default to adding into the root node.
|
|
215
|
+
const parent = data as NodeData<NodeDefaultAttribute>;
|
|
216
|
+
const updatedParent: NodeData<NodeDefaultAttribute> = {
|
|
217
|
+
...parent,
|
|
218
|
+
children: appendChild(parent.children, nextChild),
|
|
219
|
+
};
|
|
220
|
+
setData(updatedParent);
|
|
221
|
+
setCurrent(updatedParent);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// If the root is a list, allow adding to it directly.
|
|
226
|
+
if (isNodeArray(current)) {
|
|
227
|
+
const nextList = [...(current as Node[]), nextChild];
|
|
228
|
+
const updatedRoot = replaceNode(data, current, nextList);
|
|
229
|
+
setData(updatedRoot);
|
|
230
|
+
setCurrent(nextList);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If the root is a placeholder string, allow replacing it with the first node.
|
|
235
|
+
if (isNodeString(current)) {
|
|
236
|
+
if (current === data) {
|
|
237
|
+
setData(nextChild);
|
|
238
|
+
setCurrent(nextChild);
|
|
239
|
+
}
|
|
203
240
|
return;
|
|
204
241
|
}
|
|
205
242
|
|
|
206
243
|
const parent = current as NodeData<NodeDefaultAttribute>;
|
|
207
|
-
const nextChild = createDefaultNode(type);
|
|
208
244
|
const updatedParent: NodeData<NodeDefaultAttribute> = {
|
|
209
245
|
...parent,
|
|
210
246
|
children: appendChild(parent.children, nextChild),
|
|
@@ -230,7 +266,11 @@ export function Builder({
|
|
|
230
266
|
}
|
|
231
267
|
return (current as NodeData<NodeDefaultAttribute>).type ?? null;
|
|
232
268
|
}, [current]);
|
|
233
|
-
const canAddChild =
|
|
269
|
+
const canAddChild =
|
|
270
|
+
allowedChildTypes.length > 0 ||
|
|
271
|
+
data === undefined ||
|
|
272
|
+
data === null ||
|
|
273
|
+
(Array.isArray(data) && data.length === 0);
|
|
234
274
|
|
|
235
275
|
const handleOpenAddModal = useCallback(() => {
|
|
236
276
|
if (!canAddChild) return;
|
|
@@ -373,12 +413,16 @@ export function Builder({
|
|
|
373
413
|
}
|
|
374
414
|
|
|
375
415
|
function getAllowedChildTypes(parent: Node): string[] {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
416
|
+
// Treat non-node containers (root empty, root arrays) as "View-like" containers:
|
|
417
|
+
// allow inserting any component type as a child.
|
|
418
|
+
if (isNodeNullOrUndefined(parent)) return [...allcomponentNames];
|
|
419
|
+
if (isNodeArray(parent) && (parent as Node[]).length === 0)
|
|
420
|
+
return [...allcomponentNames];
|
|
421
|
+
if (isNodeString(parent)) {
|
|
422
|
+
// Only allow adding when the string is the root placeholder.
|
|
423
|
+
return parent === data ? [...allcomponentNames] : [];
|
|
424
|
+
}
|
|
425
|
+
|
|
382
426
|
const parentData = parent as NodeData;
|
|
383
427
|
const parentType = parentData.type;
|
|
384
428
|
// Special rule: limit OnboardButtons to OnboardButton only
|
|
@@ -24,9 +24,14 @@ export function EditorHeader({
|
|
|
24
24
|
useLogRender('EditorHeader');
|
|
25
25
|
const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
|
|
26
26
|
const copiedNode = useRenderStore((s) => s.copiedNode);
|
|
27
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
device: selectedDevice,
|
|
29
|
+
setDevice,
|
|
30
|
+
setCurrent,
|
|
31
|
+
} = useRenderStore((s) => ({
|
|
28
32
|
device: s.device,
|
|
29
33
|
setDevice: s.setDevice,
|
|
34
|
+
setCurrent: s.setCurrent,
|
|
30
35
|
}));
|
|
31
36
|
|
|
32
37
|
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
@@ -67,6 +72,11 @@ export function EditorHeader({
|
|
|
67
72
|
copiedNode: null,
|
|
68
73
|
});
|
|
69
74
|
setEditorData(updated);
|
|
75
|
+
//TODO: current and editor must be sync!! and tested more
|
|
76
|
+
// Important: selection is stored by reference. After replacing `current` in the tree,
|
|
77
|
+
// we must point selection to the new (cloned) node reference to keep "current node"
|
|
78
|
+
// in sync with what’s rendered/edited.
|
|
79
|
+
setCurrent(cloned);
|
|
70
80
|
};
|
|
71
81
|
return (
|
|
72
82
|
<div
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type JsonTextEditorProps = {
|
|
4
|
+
value: unknown;
|
|
5
|
+
onChange?: (next: unknown) => void;
|
|
6
|
+
rootName?: string;
|
|
7
|
+
readOnly?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function safeStringify(value: unknown) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(value, null, 2) ?? '';
|
|
14
|
+
} catch {
|
|
15
|
+
// Fallback for circular structures or non-serializable values
|
|
16
|
+
return String(value ?? '');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function JsonTextEditor({
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
rootName,
|
|
24
|
+
readOnly = false,
|
|
25
|
+
className,
|
|
26
|
+
}: JsonTextEditorProps) {
|
|
27
|
+
const initialText = useMemo(() => safeStringify(value), [value]);
|
|
28
|
+
const [text, setText] = useState(initialText);
|
|
29
|
+
const [parseError, setParseError] = useState<string | null>(null);
|
|
30
|
+
const [applyError, setApplyError] = useState<string | null>(null);
|
|
31
|
+
const [parsedValue, setParsedValue] = useState<unknown>(value);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setText(initialText);
|
|
35
|
+
setParseError(null);
|
|
36
|
+
setApplyError(null);
|
|
37
|
+
setParsedValue(value);
|
|
38
|
+
}, [initialText, value]);
|
|
39
|
+
|
|
40
|
+
const handleCopy = async () => {
|
|
41
|
+
try {
|
|
42
|
+
await navigator.clipboard.writeText(text);
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore (e.g. non-secure context)
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleFormat = () => {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(text);
|
|
51
|
+
setText(JSON.stringify(parsed, null, 2));
|
|
52
|
+
setParseError(null);
|
|
53
|
+
setApplyError(null);
|
|
54
|
+
setParsedValue(parsed);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
setParseError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleApply = () => {
|
|
61
|
+
if (!onChange) return;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(text);
|
|
64
|
+
setParseError(null);
|
|
65
|
+
setApplyError(null);
|
|
66
|
+
setParsedValue(parsed);
|
|
67
|
+
try {
|
|
68
|
+
onChange(parsed);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
setApplyError(e instanceof Error ? e.message : 'Failed to apply JSON');
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
setParseError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const headerLabel = rootName ? `${rootName}.json` : 'data.json';
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={className}
|
|
82
|
+
style={{
|
|
83
|
+
height: '100%',
|
|
84
|
+
width: '100%',
|
|
85
|
+
display: 'flex',
|
|
86
|
+
flexDirection: 'column',
|
|
87
|
+
gap: 10,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
display: 'flex',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
justifyContent: 'space-between',
|
|
95
|
+
gap: 8,
|
|
96
|
+
flexWrap: 'wrap',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<div style={{ fontSize: 12, opacity: 0.75 }}>{headerLabel}</div>
|
|
100
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
101
|
+
<button type="button" className="editor-button" onClick={handleCopy}>
|
|
102
|
+
Copy
|
|
103
|
+
</button>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className="editor-button"
|
|
107
|
+
onClick={handleFormat}
|
|
108
|
+
>
|
|
109
|
+
Format
|
|
110
|
+
</button>
|
|
111
|
+
{!readOnly && (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className="editor-button"
|
|
115
|
+
onClick={handleApply}
|
|
116
|
+
disabled={!onChange}
|
|
117
|
+
title={onChange ? 'Apply JSON changes' : 'Read only'}
|
|
118
|
+
>
|
|
119
|
+
Apply
|
|
120
|
+
</button>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div
|
|
126
|
+
style={{
|
|
127
|
+
display: 'grid',
|
|
128
|
+
gridTemplateColumns: 'minmax(0, 1fr)',
|
|
129
|
+
gap: 10,
|
|
130
|
+
flex: 1,
|
|
131
|
+
minHeight: 0,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<textarea
|
|
135
|
+
value={text}
|
|
136
|
+
onChange={(e) => {
|
|
137
|
+
const nextText = e.target.value;
|
|
138
|
+
setText(nextText);
|
|
139
|
+
setApplyError(null);
|
|
140
|
+
if (readOnly) return;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(nextText);
|
|
143
|
+
setParseError(null);
|
|
144
|
+
setParsedValue(parsed);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
setParseError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
readOnly={readOnly}
|
|
150
|
+
spellCheck={false}
|
|
151
|
+
style={{
|
|
152
|
+
width: '100%',
|
|
153
|
+
height: '100%',
|
|
154
|
+
minHeight: 320,
|
|
155
|
+
resize: 'none',
|
|
156
|
+
border: '1px solid rgba(0,0,0,0.12)',
|
|
157
|
+
borderRadius: 10,
|
|
158
|
+
padding: 12,
|
|
159
|
+
fontFamily:
|
|
160
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
161
|
+
fontSize: 12,
|
|
162
|
+
lineHeight: 1.5,
|
|
163
|
+
background: 'transparent',
|
|
164
|
+
outline: 'none',
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{parseError ? (
|
|
170
|
+
<div style={{ fontSize: 12, color: '#d12f2f' }}>
|
|
171
|
+
Invalid JSON: {parseError}
|
|
172
|
+
</div>
|
|
173
|
+
) : applyError ? (
|
|
174
|
+
<div style={{ fontSize: 12, color: '#d12f2f' }}>
|
|
175
|
+
Could not apply: {applyError}
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
|
179
|
+
Valid JSON ({safeStringify(parsedValue).length.toLocaleString()}{' '}
|
|
180
|
+
chars)
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { ReactNode, useCallback, useEffect } from 'react';
|
|
2
2
|
import { useMockOSContext } from '../context/MockOSContext';
|
|
3
3
|
import { MockLaunchScreenComponent } from './MockLaunchScreenComponent';
|
|
4
|
+
import { useRenderStore } from '../../store';
|
|
4
5
|
// Note: We might use react-router or similar library in the future for more complex routing
|
|
5
6
|
|
|
6
7
|
interface MockOSRouterProps {
|
|
@@ -85,6 +86,7 @@ export function MockOSRouter({
|
|
|
85
86
|
appName = 'My App',
|
|
86
87
|
}: MockOSRouterProps) {
|
|
87
88
|
const context = useMockOSContext();
|
|
89
|
+
const incForceRender = useRenderStore((s) => s.incForceRender);
|
|
88
90
|
|
|
89
91
|
if (!context) {
|
|
90
92
|
throw new Error('MockOSRouter must be used within MockOSProvider');
|
|
@@ -106,6 +108,10 @@ export function MockOSRouter({
|
|
|
106
108
|
return () => clearTimeout(timer);
|
|
107
109
|
}, [handleLaunchApp]);
|
|
108
110
|
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
incForceRender();
|
|
113
|
+
}, [currentRoute, incForceRender]);
|
|
114
|
+
|
|
109
115
|
return (
|
|
110
116
|
<div className="mockos-router">
|
|
111
117
|
<ScreenRenderer
|
|
@@ -81,7 +81,6 @@ export function MockOSProvider({
|
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
83
|
const navigation = useCallback((route: RouteType) => {
|
|
84
|
-
console.log(`[Mock OS] Navigating to: ${route}`);
|
|
85
84
|
setCurrentRoute(route);
|
|
86
85
|
|
|
87
86
|
// If navigating from launchscreen and the last item in stack is launchscreen,
|
|
@@ -109,12 +108,10 @@ export function MockOSProvider({
|
|
|
109
108
|
newStack.pop();
|
|
110
109
|
const previousRoute = newStack[newStack.length - 1];
|
|
111
110
|
|
|
112
|
-
console.log(`[Mock OS] Going back to: ${previousRoute.route}`);
|
|
113
111
|
setCurrentRoute(previousRoute.route);
|
|
114
112
|
setNavigationStack(newStack);
|
|
115
113
|
return true;
|
|
116
114
|
}
|
|
117
|
-
console.log('[Mock OS] Cannot go back - at root');
|
|
118
115
|
return false;
|
|
119
116
|
}, [navigationStack]);
|
|
120
117
|
|
|
@@ -129,12 +126,10 @@ export function MockOSProvider({
|
|
|
129
126
|
};
|
|
130
127
|
|
|
131
128
|
const handleAllow = () => {
|
|
132
|
-
console.log(`[Mock OS] Permission granted: ${permission}`);
|
|
133
129
|
setPermission(null);
|
|
134
130
|
};
|
|
135
131
|
|
|
136
132
|
const handleDeny = () => {
|
|
137
|
-
console.log(`[Mock OS] Permission denied: ${permission}`);
|
|
138
133
|
setPermission(null);
|
|
139
134
|
};
|
|
140
135
|
|
|
@@ -26,7 +26,6 @@ export class MockPermissionManager {
|
|
|
26
26
|
return 'not-determined';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
console.log(`[Mock OS] Permission requested: ${permission}`);
|
|
30
29
|
// Set permission to trigger modal display
|
|
31
30
|
this.context.setPermission(permission);
|
|
32
31
|
// Default behavior: grant all permissions in mock environment
|
|
@@ -39,7 +38,6 @@ export class MockPermissionManager {
|
|
|
39
38
|
return 'not-determined';
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
console.log(`[Mock OS] Permission checked: ${permission}`);
|
|
43
41
|
return 'granted';
|
|
44
42
|
}
|
|
45
43
|
|
|
@@ -48,7 +46,5 @@ export class MockPermissionManager {
|
|
|
48
46
|
alert('Opening Settings\n(Mock OS context not available)');
|
|
49
47
|
return;
|
|
50
48
|
}
|
|
51
|
-
|
|
52
|
-
console.log('[Mock OS] Opening Settings');
|
|
53
49
|
}
|
|
54
50
|
}
|
|
@@ -31,7 +31,6 @@ export class MockNavigationManager {
|
|
|
31
31
|
timestamp: Date.now(),
|
|
32
32
|
};
|
|
33
33
|
this.stack.push(item);
|
|
34
|
-
console.log('[Mock OS] Navigate to Home', { stack: this.stack });
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
goToSubscriptions(): void {
|
|
@@ -45,7 +44,6 @@ export class MockNavigationManager {
|
|
|
45
44
|
timestamp: Date.now(),
|
|
46
45
|
};
|
|
47
46
|
this.stack.push(item);
|
|
48
|
-
console.log('[Mock OS] Navigate to Subscriptions', { stack: this.stack });
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
goToLaunchApp(): void {
|
|
@@ -59,7 +57,6 @@ export class MockNavigationManager {
|
|
|
59
57
|
timestamp: Date.now(),
|
|
60
58
|
};
|
|
61
59
|
this.stack.push(item);
|
|
62
|
-
console.log('[Mock OS] Navigate to Launch App', { stack: this.stack });
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
goBack(): boolean {
|
|
@@ -70,11 +67,9 @@ export class MockNavigationManager {
|
|
|
70
67
|
|
|
71
68
|
if (this.stack.length > 0) {
|
|
72
69
|
const popped = this.stack.pop();
|
|
73
|
-
console.log('[Mock OS] Go Back', { popped, stack: this.stack });
|
|
74
70
|
return true;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
console.log('[Mock OS] Cannot go back - stack is empty');
|
|
78
73
|
return false;
|
|
79
74
|
}
|
|
80
75
|
|
|
@@ -85,7 +80,7 @@ export class MockNavigationManager {
|
|
|
85
80
|
clearStack(): void {
|
|
86
81
|
this.stack = [];
|
|
87
82
|
if (this.context) {
|
|
88
|
-
console.
|
|
83
|
+
console.info('[Mock OS] Navigation stack cleared');
|
|
89
84
|
}
|
|
90
85
|
}
|
|
91
86
|
}
|
|
@@ -65,7 +65,11 @@ const readSavedColors = (): string[] => {
|
|
|
65
65
|
const stored = window.localStorage.getItem(SAVED_COLORS_KEY);
|
|
66
66
|
if (!stored) return [];
|
|
67
67
|
const parsed = JSON.parse(stored);
|
|
68
|
-
|
|
68
|
+
if (!Array.isArray(parsed)) return [];
|
|
69
|
+
return parsed
|
|
70
|
+
.filter((value) => typeof value === 'string')
|
|
71
|
+
.map((value) => value.trim().toLowerCase())
|
|
72
|
+
.filter(Boolean);
|
|
69
73
|
} catch {
|
|
70
74
|
return [];
|
|
71
75
|
}
|
|
@@ -143,6 +147,7 @@ export function ColorModal({
|
|
|
143
147
|
const [useColorNames, setUseColorNames] = useState(true);
|
|
144
148
|
const colorInputRef = useRef<HTMLInputElement | null>(null);
|
|
145
149
|
const colorNameToggleId = useId();
|
|
150
|
+
const colorPickerInputId = useId();
|
|
146
151
|
|
|
147
152
|
const selectedColorPreview = useMemo(
|
|
148
153
|
() => resolveProjectColorValue(value, projectColors) ?? value,
|
|
@@ -195,21 +200,10 @@ export function ColorModal({
|
|
|
195
200
|
[savedColors],
|
|
196
201
|
);
|
|
197
202
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const handlePreviewKeyDown = (
|
|
203
|
-
event: React.KeyboardEvent<HTMLSpanElement>,
|
|
204
|
-
) => {
|
|
205
|
-
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
206
|
-
event.preventDefault();
|
|
207
|
-
handleAddColorClick();
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const handleColorPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
211
|
-
const picked = event.target.value;
|
|
203
|
+
const applyPickedColor = (raw: string | undefined) => {
|
|
204
|
+
const picked = raw?.trim().toLowerCase();
|
|
212
205
|
if (!picked) return;
|
|
206
|
+
|
|
213
207
|
setSavedColors((prev) => {
|
|
214
208
|
if (prev.includes(picked)) return prev;
|
|
215
209
|
const next = [...prev, picked];
|
|
@@ -218,7 +212,85 @@ export function ColorModal({
|
|
|
218
212
|
});
|
|
219
213
|
onSelect(picked);
|
|
220
214
|
onClose();
|
|
221
|
-
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const openPickerWithTempInput = () => {
|
|
218
|
+
if (typeof document === 'undefined') return;
|
|
219
|
+
|
|
220
|
+
const temp = document.createElement('input');
|
|
221
|
+
temp.type = 'color';
|
|
222
|
+
temp.value = selectedColorPreview?.toString() || '#000000';
|
|
223
|
+
|
|
224
|
+
// Keep it in the DOM and "not display:none" so Safari/iOS reliably opens it.
|
|
225
|
+
temp.style.position = 'fixed';
|
|
226
|
+
temp.style.left = '-1000px';
|
|
227
|
+
temp.style.top = '0';
|
|
228
|
+
temp.style.width = '40px';
|
|
229
|
+
temp.style.height = '40px';
|
|
230
|
+
temp.style.opacity = '0';
|
|
231
|
+
|
|
232
|
+
const cleanup = () => {
|
|
233
|
+
temp.removeEventListener('change', onChange);
|
|
234
|
+
temp.removeEventListener('input', onChange);
|
|
235
|
+
temp.removeEventListener('blur', cleanup);
|
|
236
|
+
temp.remove();
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const onChange = () => {
|
|
240
|
+
applyPickedColor(temp.value);
|
|
241
|
+
cleanup();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
temp.addEventListener('change', onChange);
|
|
245
|
+
temp.addEventListener('input', onChange);
|
|
246
|
+
temp.addEventListener('blur', cleanup);
|
|
247
|
+
|
|
248
|
+
document.body.appendChild(temp);
|
|
249
|
+
try {
|
|
250
|
+
temp.focus({ preventScroll: true });
|
|
251
|
+
} catch {
|
|
252
|
+
// no-op
|
|
253
|
+
}
|
|
254
|
+
// Next tick helps some browsers recognize it as a user-gesture flow.
|
|
255
|
+
requestAnimationFrame(() => temp.click());
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const handleAddColorClick = () => {
|
|
259
|
+
const input = colorInputRef.current;
|
|
260
|
+
|
|
261
|
+
// Prefer showPicker when available (Chromium).
|
|
262
|
+
const maybeShowPicker = input
|
|
263
|
+
? (
|
|
264
|
+
input as HTMLInputElement & {
|
|
265
|
+
showPicker?: () => void;
|
|
266
|
+
}
|
|
267
|
+
).showPicker
|
|
268
|
+
: undefined;
|
|
269
|
+
|
|
270
|
+
if (input && typeof maybeShowPicker === 'function') {
|
|
271
|
+
try {
|
|
272
|
+
input.focus({ preventScroll: true });
|
|
273
|
+
} catch {
|
|
274
|
+
// no-op
|
|
275
|
+
}
|
|
276
|
+
maybeShowPicker.call(input);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Fallback: temporary input for Safari/iOS and browsers that block click on hidden inputs.
|
|
281
|
+
openPickerWithTempInput();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const handlePreviewKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
285
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
handleAddColorClick();
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handleColorPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
291
|
+
applyPickedColor(event.target.value);
|
|
292
|
+
// Keep the input value valid for type="color" (empty string can be invalid).
|
|
293
|
+
event.target.value = '#000000';
|
|
222
294
|
};
|
|
223
295
|
|
|
224
296
|
const handleSelectColor = (option: ColorOption) => {
|
|
@@ -245,16 +317,16 @@ export function ColorModal({
|
|
|
245
317
|
|
|
246
318
|
<div className="color-modal__selected">
|
|
247
319
|
<div className="color-modal__selected-info">
|
|
248
|
-
<
|
|
320
|
+
<label
|
|
321
|
+
htmlFor={colorPickerInputId}
|
|
249
322
|
role="button"
|
|
250
323
|
aria-label="Open color picker"
|
|
251
324
|
tabIndex={0}
|
|
252
325
|
title="Pick a color"
|
|
253
326
|
className="color-modal__selected-preview"
|
|
254
327
|
style={{ background: selectedColorPreview ?? 'transparent' }}
|
|
255
|
-
onClick={handleAddColorClick}
|
|
256
328
|
onKeyDown={handlePreviewKeyDown}
|
|
257
|
-
|
|
329
|
+
></label>
|
|
258
330
|
<div>
|
|
259
331
|
<p className="color-modal__selected-label">Selected color</p>
|
|
260
332
|
<p className="color-modal__selected-value">{value ?? 'None'}</p>
|
|
@@ -316,13 +388,16 @@ export function ColorModal({
|
|
|
316
388
|
options={savedColorOptions}
|
|
317
389
|
emptyMessage="Add colors you use often for quick access."
|
|
318
390
|
action={
|
|
319
|
-
<button
|
|
320
|
-
type="button"
|
|
321
|
-
className="color-modal__link-button"
|
|
322
|
-
onClick={handleAddColorClick}
|
|
323
|
-
>
|
|
391
|
+
<span className="color-modal__link-button color-modal__add-color">
|
|
324
392
|
Add color
|
|
325
|
-
|
|
393
|
+
<input
|
|
394
|
+
type="color"
|
|
395
|
+
className="color-modal__add-color-input"
|
|
396
|
+
onChange={handleColorPicked}
|
|
397
|
+
defaultValue="#000000"
|
|
398
|
+
aria-label="Add color"
|
|
399
|
+
/>
|
|
400
|
+
</span>
|
|
326
401
|
}
|
|
327
402
|
activeValue={value}
|
|
328
403
|
onSelect={handleSelectColor}
|
|
@@ -330,9 +405,12 @@ export function ColorModal({
|
|
|
330
405
|
|
|
331
406
|
<input
|
|
332
407
|
ref={colorInputRef}
|
|
408
|
+
id={colorPickerInputId}
|
|
333
409
|
type="color"
|
|
334
410
|
className="color-modal__input"
|
|
335
411
|
onChange={handleColorPicked}
|
|
412
|
+
defaultValue="#000000"
|
|
413
|
+
tabIndex={-1}
|
|
336
414
|
/>
|
|
337
415
|
</Modal>
|
|
338
416
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { JsonTextEditor } from '../components/JsonTextEditor';
|
|
3
3
|
import { Localication } from '../types/PreviewConfig';
|
|
4
4
|
import Modal from './Modal';
|
|
5
5
|
|
|
@@ -38,12 +38,11 @@ export function LocalicationModal({
|
|
|
38
38
|
</div>
|
|
39
39
|
<div className="localication-modal__body">
|
|
40
40
|
<div className="localication-modal__editor">
|
|
41
|
-
<
|
|
41
|
+
<JsonTextEditor
|
|
42
42
|
rootName="localication"
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
value={normalizedData}
|
|
44
|
+
onChange={(next) => onChange(next as Localication)}
|
|
45
45
|
className="localication-modal__json-editor"
|
|
46
|
-
maxWidth={'100%'}
|
|
47
46
|
/>
|
|
48
47
|
</div>
|
|
49
48
|
</div>
|