@developer_tribe/react-builder 1.2.32 → 1.2.33
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/assets/prompt-scheme-onboard.generated.d.ts +1 -0
- package/dist/assets/prompt-scheme-paywall.generated.d.ts +1 -0
- package/dist/components/BuilderProvider.d.ts +2 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +6 -6
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +6 -6
- package/dist/index.web.esm.js.map +1 -1
- package/dist/modals/PromptManagerModal.d.ts +9 -0
- package/dist/modals/index.d.ts +1 -0
- package/dist/styles.css +1 -1
- package/dist/utils/nodeXml.d.ts +11 -0
- package/package.json +5 -1
- package/scripts/prebuild/assets/prompt_scheme.md +44 -0
- package/scripts/prebuild/generate-prompt-schemes.js +328 -0
- package/scripts/prebuild/prebuild.js +4 -0
- package/src/RenderPage.tsx +6 -6
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +4 -0
- package/src/assets/prompt-scheme-paywall.generated.ts +4 -0
- package/src/attribute-analyser/style/native/useExtractImageStyle.ts +1 -1
- package/src/attribute-analyser/style/native/useExtractTextStyle.ts +1 -1
- package/src/attribute-analyser/style/native/useExtractViewStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractImageStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractTextStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractViewStyle.ts +1 -1
- package/src/build-components/BIcon/pattern.json +1 -3
- package/src/build-components/BackgroundImage/pattern.json +2 -10
- package/src/build-components/Button/pattern.json +1 -3
- package/src/build-components/Carousel/pattern.json +2 -8
- package/src/build-components/CarouselDots/CarouselDots.tsx +1 -1
- package/src/build-components/CarouselProvider/pattern.json +1 -4
- package/src/build-components/CountDown/pattern.json +1 -3
- package/src/build-components/Image/pattern.json +2 -9
- package/src/build-components/Main/pattern.json +1 -3
- package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +1 -1
- package/src/build-components/NavigationBarColor/pattern.json +1 -3
- package/src/build-components/Onboard/pattern.json +2 -6
- package/src/build-components/OnboardButton/OnboardButton.tsx +1 -1
- package/src/build-components/OnboardButton/pattern.json +3 -14
- package/src/build-components/OnboardButtons/pattern.json +4 -15
- package/src/build-components/OnboardDot/OnboardDot.tsx +1 -1
- package/src/build-components/OnboardDot/pattern.json +1 -3
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +3 -3
- package/src/build-components/OnboardFooter/pattern.json +1 -3
- package/src/build-components/OnboardItem/pattern.json +3 -11
- package/src/build-components/OnboardProvider/pattern.json +2 -8
- package/src/build-components/OnboardSubtitle/pattern.json +1 -4
- package/src/build-components/OnboardTitle/pattern.json +1 -4
- package/src/build-components/PaywallBackground/pattern.json +1 -3
- package/src/build-components/PaywallCloseButton/pattern.json +1 -3
- package/src/build-components/PaywallOptions/pattern.json +1 -3
- package/src/build-components/PaywallProvider/pattern.json +1 -3
- package/src/build-components/PaywallSubscribeButton/pattern.json +1 -3
- package/src/build-components/PriceTag/pattern.json +2 -8
- package/src/build-components/Pricing/pattern.json +1 -3
- package/src/build-components/Promo/pattern.json +1 -3
- package/src/build-components/Separator/pattern.json +1 -3
- package/src/build-components/StatusBarColor/StatusBarColor.tsx +1 -1
- package/src/build-components/StatusBarColor/pattern.json +1 -3
- package/src/build-components/Text/pattern.json +1 -3
- package/src/build-components/View/pattern.json +4 -16
- package/src/components/BottomBar.tsx +28 -1
- package/src/components/BuilderProvider.tsx +5 -14
- package/src/hooks/useLocalize.ts +1 -1
- package/src/modals/PromptManagerModal.tsx +270 -0
- package/src/modals/index.ts +1 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/modals/_prompt-manager-modal.scss +95 -0
- package/src/utils/nodeXml.ts +196 -0
|
@@ -3,7 +3,12 @@ import { Icon } from './Icon.generated';
|
|
|
3
3
|
import type { IconsType } from '../types/Icons.generated';
|
|
4
4
|
import { useRenderStore } from '../store';
|
|
5
5
|
import type { Localication } from '../types/PreviewConfig';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
InspectModal,
|
|
8
|
+
LocalicationModal,
|
|
9
|
+
Modal,
|
|
10
|
+
PromptManagerModal,
|
|
11
|
+
} from '../modals';
|
|
7
12
|
import type { Node } from '../types/Node';
|
|
8
13
|
import type { Project } from '../types/Project';
|
|
9
14
|
import { DebugJsonPage } from '../pages/DebugJsonPage';
|
|
@@ -29,6 +34,7 @@ export function BottomBar({
|
|
|
29
34
|
const debugIcon: IconsType = 'speedometer-03';
|
|
30
35
|
const localizationIcon: IconsType = 'globe-01';
|
|
31
36
|
const inspectIcon: IconsType = 'info-circle';
|
|
37
|
+
const promptAiIcon: IconsType = 'star';
|
|
32
38
|
|
|
33
39
|
const {
|
|
34
40
|
localization,
|
|
@@ -57,6 +63,7 @@ export function BottomBar({
|
|
|
57
63
|
const [isDebugOpen, setIsDebugOpen] = useState(false);
|
|
58
64
|
const [isLocalizationOpen, setIsLocalizationOpen] = useState(false);
|
|
59
65
|
const [isInspectOpen, setIsInspectOpen] = useState(false);
|
|
66
|
+
const [isPromptManagerOpen, setIsPromptManagerOpen] = useState(false);
|
|
60
67
|
|
|
61
68
|
const languages = useMemo(() => ['en', 'tr', 'ar'], []);
|
|
62
69
|
const activeLanguage = defaultLanguage;
|
|
@@ -150,6 +157,18 @@ export function BottomBar({
|
|
|
150
157
|
<Icon iconType={inspectIcon} size={20} color="currentColor" alt="" />
|
|
151
158
|
</button>
|
|
152
159
|
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className={`rb-bottom-bar__button${
|
|
163
|
+
isPromptManagerOpen ? ' is-active' : ''
|
|
164
|
+
}`}
|
|
165
|
+
aria-label="Prompt Manager"
|
|
166
|
+
aria-pressed={isPromptManagerOpen}
|
|
167
|
+
onClick={() => setIsPromptManagerOpen(true)}
|
|
168
|
+
>
|
|
169
|
+
<Icon iconType={promptAiIcon} size={20} color="currentColor" alt="" />
|
|
170
|
+
</button>
|
|
171
|
+
|
|
153
172
|
<div className="rb-bottom-bar__spacer" />
|
|
154
173
|
|
|
155
174
|
<div className="rb-bottom-bar__langs" aria-label="Language">
|
|
@@ -199,6 +218,14 @@ export function BottomBar({
|
|
|
199
218
|
{isInspectOpen && (
|
|
200
219
|
<InspectModal onClose={() => setIsInspectOpen(false)} />
|
|
201
220
|
)}
|
|
221
|
+
|
|
222
|
+
{isPromptManagerOpen && (
|
|
223
|
+
<PromptManagerModal
|
|
224
|
+
data={data}
|
|
225
|
+
setData={setData}
|
|
226
|
+
onClose={() => setIsPromptManagerOpen(false)}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
202
229
|
</>
|
|
203
230
|
);
|
|
204
231
|
}
|
|
@@ -36,19 +36,14 @@ export type BuilderProviderParams = {
|
|
|
36
36
|
// but internally we prefer store values.
|
|
37
37
|
mockProducts?: Products[];
|
|
38
38
|
mockBenefits?: PaywallBenefits;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
selectedLanguage?: string;
|
|
40
|
+
selectedTheme?: Theme;
|
|
41
41
|
localization?: Localication;
|
|
42
42
|
baseSize?: BaseSize;
|
|
43
|
-
// projectColors, fonts, previewMode, selectedKey kept as they are commonly used and not explicitly asked to rename
|
|
44
43
|
projectColors?: ProjectColors;
|
|
45
44
|
fonts?: Fonts;
|
|
46
45
|
previewMode?: boolean;
|
|
47
46
|
selectedKey?: string;
|
|
48
|
-
|
|
49
|
-
// Derived / Convenience values provided by the context (populated from mock* if present)
|
|
50
|
-
theme?: Theme;
|
|
51
|
-
defaultLanguage?: string;
|
|
52
47
|
};
|
|
53
48
|
|
|
54
49
|
type BuilderProviderProps = {
|
|
@@ -63,17 +58,13 @@ const builderContext = createContext<BuilderProviderParams | undefined>(
|
|
|
63
58
|
export function BuilderProvider({ params, children }: BuilderProviderProps) {
|
|
64
59
|
// We rely purely on params passed from parent (which reads from store)
|
|
65
60
|
const value = useMemo<BuilderProviderParams>(() => {
|
|
66
|
-
const
|
|
67
|
-
const lang = params.mockDefaultLanguage ?? defaultLanguage;
|
|
61
|
+
const lang = params.selectedLanguage ?? defaultLanguage;
|
|
68
62
|
return {
|
|
69
63
|
...params,
|
|
70
|
-
// Ensure defaults if mxissing
|
|
71
64
|
mockProducts: params.mockProducts ?? [],
|
|
72
65
|
mockBenefits: params.mockBenefits ?? {},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
theme,
|
|
76
|
-
defaultLanguage: lang,
|
|
66
|
+
selectedLanguage: lang,
|
|
67
|
+
selectedTheme: params.selectedTheme ?? defaultTheme,
|
|
77
68
|
baseSize: params.baseSize ?? defaultBaseSize,
|
|
78
69
|
projectColors: params.projectColors
|
|
79
70
|
? mergeProjectColors(defaultProjectColors, params.projectColors)
|
package/src/hooks/useLocalize.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function useLocalize(options?: {
|
|
|
12
12
|
}): LocalizeFn {
|
|
13
13
|
const {
|
|
14
14
|
localization: builderLocalization,
|
|
15
|
-
|
|
15
|
+
selectedLanguage: builderDefaultLanguage,
|
|
16
16
|
} = useBuilderParams();
|
|
17
17
|
//TODO: genelle (react-native ksımına export et)
|
|
18
18
|
const localization =
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import Modal from './Modal';
|
|
3
|
+
import { useRenderStore } from '../store';
|
|
4
|
+
import type { Node } from '../types/Node';
|
|
5
|
+
import { nodeToXml, xmlToNode } from '../utils/nodeXml';
|
|
6
|
+
import { analyseAndProccess } from '../utils/analyseNode';
|
|
7
|
+
import { onboardPromptScheme } from '../assets/prompt-scheme-onboard.generated';
|
|
8
|
+
import { paywallPromptScheme } from '../assets/prompt-scheme-paywall.generated';
|
|
9
|
+
|
|
10
|
+
type PromptManagerTab = 'xml-editor' | 'prompt-schema';
|
|
11
|
+
|
|
12
|
+
type PromptManagerModalProps = {
|
|
13
|
+
data: Node;
|
|
14
|
+
setData: React.Dispatch<React.SetStateAction<Node>>;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function PromptManagerModal({
|
|
19
|
+
data,
|
|
20
|
+
setData,
|
|
21
|
+
onClose,
|
|
22
|
+
}: PromptManagerModalProps) {
|
|
23
|
+
const [activeTab, setActiveTab] = useState<PromptManagerTab>('xml-editor');
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Modal
|
|
27
|
+
onClose={onClose}
|
|
28
|
+
ariaLabelledBy="prompt-manager-modal-title"
|
|
29
|
+
className="modal--large modal--scrollable"
|
|
30
|
+
contentClassName="prompt-manager-modal__content"
|
|
31
|
+
>
|
|
32
|
+
<div className="modal__header prompt-manager-modal__header">
|
|
33
|
+
<h3 id="prompt-manager-modal-title" className="modal__title">
|
|
34
|
+
Prompt Manager
|
|
35
|
+
</h3>
|
|
36
|
+
<button type="button" className="editor-button" onClick={onClose}>
|
|
37
|
+
Close
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="prompt-manager-modal__tabs">
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className={`prompt-manager-modal__tab${
|
|
45
|
+
activeTab === 'xml-editor' ? ' is-active' : ''
|
|
46
|
+
}`}
|
|
47
|
+
onClick={() => setActiveTab('xml-editor')}
|
|
48
|
+
>
|
|
49
|
+
XML Editor
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className={`prompt-manager-modal__tab${
|
|
54
|
+
activeTab === 'prompt-schema' ? ' is-active' : ''
|
|
55
|
+
}`}
|
|
56
|
+
onClick={() => setActiveTab('prompt-schema')}
|
|
57
|
+
>
|
|
58
|
+
Prompt Şeması
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="prompt-manager-modal__body">
|
|
63
|
+
{activeTab === 'xml-editor' && (
|
|
64
|
+
<XmlEditorTab data={data} setData={setData} />
|
|
65
|
+
)}
|
|
66
|
+
{activeTab === 'prompt-schema' && <PromptSchemaTab />}
|
|
67
|
+
</div>
|
|
68
|
+
</Modal>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/* Tab 1 – XML Editor */
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
|
|
76
|
+
function XmlEditorTab({
|
|
77
|
+
data,
|
|
78
|
+
setData,
|
|
79
|
+
}: {
|
|
80
|
+
data: Node;
|
|
81
|
+
setData: React.Dispatch<React.SetStateAction<Node>>;
|
|
82
|
+
}) {
|
|
83
|
+
const setCurrent = useRenderStore((s) => s.setCurrent);
|
|
84
|
+
|
|
85
|
+
const initialXml = useMemo(() => {
|
|
86
|
+
try {
|
|
87
|
+
return nodeToXml(data);
|
|
88
|
+
} catch {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
}, [data]);
|
|
92
|
+
|
|
93
|
+
const [text, setText] = useState(initialXml);
|
|
94
|
+
const [parseError, setParseError] = useState<string | null>(null);
|
|
95
|
+
const [applyError, setApplyError] = useState<string | null>(null);
|
|
96
|
+
const [applySuccess, setApplySuccess] = useState(false);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setText(initialXml);
|
|
100
|
+
setParseError(null);
|
|
101
|
+
setApplyError(null);
|
|
102
|
+
setApplySuccess(false);
|
|
103
|
+
}, [initialXml]);
|
|
104
|
+
|
|
105
|
+
const handleChange = useCallback((value: string) => {
|
|
106
|
+
setText(value);
|
|
107
|
+
setApplyError(null);
|
|
108
|
+
setApplySuccess(false);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const handleApply = () => {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = xmlToNode(text);
|
|
114
|
+
setParseError(null);
|
|
115
|
+
setApplyError(null);
|
|
116
|
+
setApplySuccess(false);
|
|
117
|
+
try {
|
|
118
|
+
const processed = analyseAndProccess(parsed as Node) as Node;
|
|
119
|
+
setData(processed);
|
|
120
|
+
// Keep selection in sync with the new root.
|
|
121
|
+
setCurrent(processed);
|
|
122
|
+
setApplySuccess(true);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
setApplyError(e instanceof Error ? e.message : 'Failed to apply XML');
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
setParseError(e instanceof Error ? e.message : 'Invalid XML');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleCopy = async () => {
|
|
132
|
+
try {
|
|
133
|
+
await navigator.clipboard.writeText(text);
|
|
134
|
+
} catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="prompt-manager-modal__editor-wrap">
|
|
141
|
+
<div className="prompt-manager-modal__toolbar">
|
|
142
|
+
<div style={{ fontSize: 12, opacity: 0.75 }}>node.xml</div>
|
|
143
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
144
|
+
<button type="button" className="editor-button" onClick={handleCopy}>
|
|
145
|
+
Copy
|
|
146
|
+
</button>
|
|
147
|
+
<button type="button" className="editor-button" onClick={handleApply}>
|
|
148
|
+
Apply
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<textarea
|
|
154
|
+
className="prompt-manager-modal__editor"
|
|
155
|
+
value={text}
|
|
156
|
+
onChange={(e) => {
|
|
157
|
+
setText(e.target.value);
|
|
158
|
+
setApplyError(null);
|
|
159
|
+
setApplySuccess(false);
|
|
160
|
+
}}
|
|
161
|
+
spellCheck={false}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
{parseError ? (
|
|
165
|
+
<div style={{ fontSize: 12, color: '#d12f2f' }}>
|
|
166
|
+
Invalid XML: {parseError}
|
|
167
|
+
</div>
|
|
168
|
+
) : applyError ? (
|
|
169
|
+
<div style={{ fontSize: 12, color: '#d12f2f' }}>
|
|
170
|
+
Could not apply: {applyError}
|
|
171
|
+
</div>
|
|
172
|
+
) : applySuccess ? (
|
|
173
|
+
<div style={{ fontSize: 12, color: '#2e7d32' }}>
|
|
174
|
+
✓ Applied successfully
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
|
178
|
+
Edit XML and press Apply to update the node tree.
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ------------------------------------------------------------------ */
|
|
186
|
+
/* Tab 2 – Prompt Şeması */
|
|
187
|
+
/* ------------------------------------------------------------------ */
|
|
188
|
+
|
|
189
|
+
type PromptSchemaSubTab = 'onboard' | 'paywall';
|
|
190
|
+
|
|
191
|
+
function PromptSchemaTab() {
|
|
192
|
+
const [subTab, setSubTab] = useState<PromptSchemaSubTab>('onboard');
|
|
193
|
+
const [copied, setCopied] = useState(false);
|
|
194
|
+
|
|
195
|
+
const activeScheme =
|
|
196
|
+
subTab === 'onboard' ? onboardPromptScheme : paywallPromptScheme;
|
|
197
|
+
|
|
198
|
+
const handleCopy = async () => {
|
|
199
|
+
try {
|
|
200
|
+
await navigator.clipboard.writeText(activeScheme);
|
|
201
|
+
setCopied(true);
|
|
202
|
+
setTimeout(() => setCopied(false), 1500);
|
|
203
|
+
} catch {
|
|
204
|
+
// ignore
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="prompt-manager-modal__editor-wrap">
|
|
210
|
+
{/* Sub-tab selector + copy */}
|
|
211
|
+
<div className="prompt-manager-modal__toolbar">
|
|
212
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
className="editor-button"
|
|
216
|
+
onClick={() => {
|
|
217
|
+
setSubTab('onboard');
|
|
218
|
+
setCopied(false);
|
|
219
|
+
}}
|
|
220
|
+
style={
|
|
221
|
+
subTab === 'onboard'
|
|
222
|
+
? {
|
|
223
|
+
background: '#1565c0',
|
|
224
|
+
color: '#fff',
|
|
225
|
+
borderColor: '#1565c0',
|
|
226
|
+
}
|
|
227
|
+
: undefined
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
Onboard
|
|
231
|
+
</button>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
className="editor-button"
|
|
235
|
+
onClick={() => {
|
|
236
|
+
setSubTab('paywall');
|
|
237
|
+
setCopied(false);
|
|
238
|
+
}}
|
|
239
|
+
style={
|
|
240
|
+
subTab === 'paywall'
|
|
241
|
+
? {
|
|
242
|
+
background: '#6a1b9a',
|
|
243
|
+
color: '#fff',
|
|
244
|
+
borderColor: '#6a1b9a',
|
|
245
|
+
}
|
|
246
|
+
: undefined
|
|
247
|
+
}
|
|
248
|
+
>
|
|
249
|
+
Paywall
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
<button type="button" className="editor-button" onClick={handleCopy}>
|
|
253
|
+
{copied ? '✓ Copied' : 'Copy'}
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<textarea
|
|
258
|
+
className="prompt-manager-modal__editor"
|
|
259
|
+
value={activeScheme}
|
|
260
|
+
readOnly
|
|
261
|
+
spellCheck={false}
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
|
265
|
+
{activeScheme.length.toLocaleString()} chars · read-only
|
|
266
|
+
(auto-generated)
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
package/src/modals/index.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { ProductPresetsModal } from './ProductPresetsModal';
|
|
|
10
10
|
export { BenefitEditModal } from './BenefitEditModal';
|
|
11
11
|
export { BenefitPresetsModal } from './BenefitPresetsModal';
|
|
12
12
|
export { InspectModal } from './InspectModal';
|
|
13
|
+
export { PromptManagerModal } from './PromptManagerModal';
|
package/src/styles/index.scss
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@use '../foundation/colors' as colors;
|
|
2
|
+
@use '../foundation/sizes' as sizes;
|
|
3
|
+
@use '../foundation/typography' as typography;
|
|
4
|
+
|
|
5
|
+
.prompt-manager-modal__content {
|
|
6
|
+
width: calc(100vw - 32px);
|
|
7
|
+
height: calc(100vh - 32px);
|
|
8
|
+
max-width: 1400px;
|
|
9
|
+
max-height: calc(100vh - 32px);
|
|
10
|
+
padding: 0;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.prompt-manager-modal__header {
|
|
16
|
+
padding: sizes.$spaceComfy sizes.$spaceRoomy;
|
|
17
|
+
border-bottom: 1px solid colors.$borderColor;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.prompt-manager-modal__tabs {
|
|
24
|
+
display: flex;
|
|
25
|
+
padding: 0 sizes.$spaceRoomy;
|
|
26
|
+
border-bottom: 1px solid colors.$borderColor;
|
|
27
|
+
background: colors.$canvasColor;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.prompt-manager-modal__tab {
|
|
31
|
+
padding: sizes.$spaceComfy sizes.$spaceRoomy;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
color: colors.$mutedTextColor;
|
|
35
|
+
background: none;
|
|
36
|
+
border: none;
|
|
37
|
+
border-bottom: 2px solid transparent;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
transition: all 0.2s ease;
|
|
40
|
+
|
|
41
|
+
&:hover {
|
|
42
|
+
color: colors.$textColor;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&.is-active {
|
|
46
|
+
color: colors.$primary;
|
|
47
|
+
border-bottom-color: colors.$primary;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.prompt-manager-modal__body {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-height: 0;
|
|
54
|
+
padding: sizes.$spaceRoomy;
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.prompt-manager-modal__editor-wrap {
|
|
61
|
+
flex: 1;
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: sizes.$spaceComfy;
|
|
65
|
+
min-height: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.prompt-manager-modal__toolbar {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.prompt-manager-modal__editor {
|
|
75
|
+
flex: 1;
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
resize: none;
|
|
79
|
+
border: 1px solid colors.$borderColor;
|
|
80
|
+
border-radius: sizes.$radiusRounded;
|
|
81
|
+
padding: sizes.$spaceComfy;
|
|
82
|
+
font-family:
|
|
83
|
+
'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
|
|
84
|
+
'Liberation Mono', 'Courier New', monospace;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
line-height: 1.5;
|
|
87
|
+
background: colors.$muted;
|
|
88
|
+
color: colors.$textColor;
|
|
89
|
+
outline: none;
|
|
90
|
+
|
|
91
|
+
&:focus {
|
|
92
|
+
border-color: colors.$primary;
|
|
93
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, colors.$primary, transparent 90%);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Node, NodeData } from '../types/Node';
|
|
2
|
+
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* Node → XML */
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
|
|
7
|
+
function escapeXmlAttr(str: string, quote: '"' | "'"): string {
|
|
8
|
+
let result = str
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>');
|
|
12
|
+
if (quote === '"') {
|
|
13
|
+
result = result.replace(/"/g, '"');
|
|
14
|
+
} else {
|
|
15
|
+
result = result.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function attrValueToString(value: unknown): string {
|
|
21
|
+
if (value === null || value === undefined) return '';
|
|
22
|
+
if (typeof value === 'string') return value;
|
|
23
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
24
|
+
return String(value);
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nodeToXmlInner(node: Node, indent: number): string {
|
|
29
|
+
const pad = ' '.repeat(indent);
|
|
30
|
+
|
|
31
|
+
if (node === null || node === undefined) return '';
|
|
32
|
+
if (typeof node === 'string')
|
|
33
|
+
return `${pad}${escapeXmlAttr(node, '"').replace(/"/g, '"')}`; // Text content doesn't need to escape quotes
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(node)) {
|
|
36
|
+
return node.map((child) => nodeToXmlInner(child, indent)).join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof node !== 'object') return '';
|
|
40
|
+
|
|
41
|
+
const record = node as NodeData;
|
|
42
|
+
const type = record.type;
|
|
43
|
+
if (!type) return '';
|
|
44
|
+
|
|
45
|
+
// Build attribute string
|
|
46
|
+
const attrs: string[] = [];
|
|
47
|
+
|
|
48
|
+
const addAttr = (key: string, val: unknown) => {
|
|
49
|
+
const strValue = attrValueToString(val);
|
|
50
|
+
// Smart quote selection:
|
|
51
|
+
// If value contains double quotes but NOT single quotes, use single quotes.
|
|
52
|
+
// Otherwise default to double quotes (and escape internal double quotes).
|
|
53
|
+
const useSingle = strValue.includes('"') && !strValue.includes("'");
|
|
54
|
+
const quote = useSingle ? "'" : '"';
|
|
55
|
+
const escaped = escapeXmlAttr(strValue, quote);
|
|
56
|
+
attrs.push(`${key}=${quote}${escaped}${quote}`);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (record.key) addAttr('key', record.key);
|
|
60
|
+
if (record.sourceType) addAttr('sourceType', record.sourceType);
|
|
61
|
+
if (record.isMain) addAttr('isMain', 'true');
|
|
62
|
+
|
|
63
|
+
if (record.attributes && typeof record.attributes === 'object') {
|
|
64
|
+
for (const [k, v] of Object.entries(record.attributes)) {
|
|
65
|
+
addAttr(k, v);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
70
|
+
|
|
71
|
+
// Children
|
|
72
|
+
const children = record.children;
|
|
73
|
+
if (
|
|
74
|
+
children === null ||
|
|
75
|
+
children === undefined ||
|
|
76
|
+
(Array.isArray(children) && children.length === 0)
|
|
77
|
+
) {
|
|
78
|
+
return `${pad}<${type}${attrStr} />`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const childXml = nodeToXmlInner(children, indent + 1);
|
|
82
|
+
return `${pad}<${type}${attrStr}>\n${childXml}\n${pad}</${type}>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Serialize a Node tree to an XML string.
|
|
87
|
+
*/
|
|
88
|
+
export function nodeToXml(node: Node): string {
|
|
89
|
+
if (node === null || node === undefined) return '';
|
|
90
|
+
return nodeToXmlInner(node, 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
/* XML → Node */
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
|
|
97
|
+
function unescapeXml(str: string): string {
|
|
98
|
+
return str
|
|
99
|
+
.replace(/"/g, '"')
|
|
100
|
+
.replace(/>/g, '>')
|
|
101
|
+
.replace(/</g, '<')
|
|
102
|
+
.replace(/&/g, '&');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseAttrValue(raw: string): unknown {
|
|
106
|
+
const str = unescapeXml(raw);
|
|
107
|
+
|
|
108
|
+
// Boolean
|
|
109
|
+
if (str === 'true') return true;
|
|
110
|
+
if (str === 'false') return false;
|
|
111
|
+
|
|
112
|
+
// Number
|
|
113
|
+
if (str !== '' && !Number.isNaN(Number(str))) return Number(str);
|
|
114
|
+
|
|
115
|
+
// JSON object / array
|
|
116
|
+
if (
|
|
117
|
+
(str.startsWith('{') && str.endsWith('}')) ||
|
|
118
|
+
(str.startsWith('[') && str.endsWith(']'))
|
|
119
|
+
) {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(str);
|
|
122
|
+
} catch {
|
|
123
|
+
// fall through to string
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return str;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function domElementToNode(el: Element): NodeData {
|
|
131
|
+
const type = el.tagName;
|
|
132
|
+
const nodeData: NodeData = { type, children: null };
|
|
133
|
+
|
|
134
|
+
// Reserved XML attributes → NodeData fields
|
|
135
|
+
const reserved = new Set(['key', 'sourceType', 'isMain']);
|
|
136
|
+
const attributes: Record<string, unknown> = {};
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
139
|
+
const attr = el.attributes[i];
|
|
140
|
+
if (reserved.has(attr.name)) {
|
|
141
|
+
if (attr.name === 'key') nodeData.key = attr.value;
|
|
142
|
+
else if (attr.name === 'sourceType') nodeData.sourceType = attr.value;
|
|
143
|
+
else if (attr.name === 'isMain') nodeData.isMain = attr.value === 'true';
|
|
144
|
+
} else {
|
|
145
|
+
attributes[attr.name] = parseAttrValue(attr.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Object.keys(attributes).length > 0) {
|
|
150
|
+
nodeData.attributes = attributes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Children
|
|
154
|
+
const childNodes: Node[] = [];
|
|
155
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
156
|
+
const child = el.childNodes[i];
|
|
157
|
+
if (child.nodeType === 1) {
|
|
158
|
+
// Element
|
|
159
|
+
childNodes.push(domElementToNode(child as Element));
|
|
160
|
+
} else if (child.nodeType === 3) {
|
|
161
|
+
// Text
|
|
162
|
+
const text = (child.textContent ?? '').trim();
|
|
163
|
+
if (text) childNodes.push(text);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (childNodes.length === 0) {
|
|
168
|
+
nodeData.children = null;
|
|
169
|
+
} else if (childNodes.length === 1) {
|
|
170
|
+
nodeData.children = childNodes[0];
|
|
171
|
+
} else {
|
|
172
|
+
nodeData.children = childNodes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return nodeData;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse an XML string back into a Node tree.
|
|
180
|
+
*
|
|
181
|
+
* Uses the browser's built-in DOMParser.
|
|
182
|
+
*/
|
|
183
|
+
export function xmlToNode(xml: string): Node {
|
|
184
|
+
if (!xml.trim()) return null;
|
|
185
|
+
|
|
186
|
+
const parser = new DOMParser();
|
|
187
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
188
|
+
|
|
189
|
+
const parseError = doc.querySelector('parsererror');
|
|
190
|
+
if (parseError) {
|
|
191
|
+
throw new Error(parseError.textContent ?? 'XML parse error');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const root = doc.documentElement;
|
|
195
|
+
return domElementToNode(root);
|
|
196
|
+
}
|