@saltcorn/builder 1.6.0-alpha.12 → 1.6.0-alpha.13

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.
@@ -135,11 +135,21 @@ const Container = ({
135
135
  const {
136
136
  selected,
137
137
  connectors: { connect, drag },
138
- } = useNode((node) => ({ selected: node.events.selected }));
138
+ mobileWidth,
139
+ tabletWidth,
140
+ mobileHeight,
141
+ tabletHeight,
142
+ } = useNode((node) => ({
143
+ selected: node.events.selected,
144
+ mobileWidth: node.data.props.mobileWidth,
145
+ tabletWidth: node.data.props.tabletWidth,
146
+ mobileHeight: node.data.props.mobileHeight,
147
+ tabletHeight: node.data.props.tabletHeight,
148
+ }));
139
149
  const { previewDevice } = useContext(previewCtx);
140
150
 
141
151
  const BP_MIN = { "": 0, sm: 576, md: 768, lg: 992, xl: 1200 };
142
- const DEVICE_W = { desktop: Infinity, tablet: 768, mobile: 375 };
152
+ const DEVICE_W = { desktop: Infinity, tablet: 768, mobile: 576 };
143
153
  {
144
154
  const dw = DEVICE_W[previewDevice] || Infinity;
145
155
  if (minScreenWidth && dw < (BP_MIN[minScreenWidth] || 0)) {
@@ -175,7 +185,7 @@ const Container = ({
175
185
  style: {
176
186
  ...parseStyles(customCSS || ""),
177
187
  ...reactifyStyles(style, transform, rotate),
178
- display,
188
+ ...(display && display !== "block" ? { display } : {}),
179
189
  minHeight: minHeight ? `${minHeight}${minHeightUnit || "px"}` : null,
180
190
  ...(bgType === "Image" && bgFileId
181
191
  ? {
@@ -207,16 +217,20 @@ const Container = ({
207
217
  color: textColor,
208
218
  }
209
219
  : {}),
210
- ...(typeof height !== "undefined"
220
+ ...(height
211
221
  ? {
212
222
  height: `${height}${heightUnit || "px"}`,
213
223
  }
214
224
  : {}),
215
- ...(typeof width !== "undefined"
225
+ ...(width
216
226
  ? {
217
227
  width: `${width}${widthUnit || "px"}`,
218
228
  }
219
229
  : {}),
230
+ ...(previewDevice === "mobile" && mobileWidth ? { width: mobileWidth } : {}),
231
+ ...(previewDevice === "mobile" && mobileHeight ? { height: mobileHeight } : {}),
232
+ ...(previewDevice === "tablet" && tabletWidth ? { width: tabletWidth } : {}),
233
+ ...(previewDevice === "tablet" && tabletHeight ? { height: tabletHeight } : {}),
220
234
  },
221
235
  },
222
236
  <Element canvas id="container-canvas" is={Column}>
@@ -238,6 +252,10 @@ const ContainerSettings = () => {
238
252
  minHeight: node.data.props.minHeight,
239
253
  height: node.data.props.height,
240
254
  width: node.data.props.width,
255
+ mobileWidth: node.data.props.mobileWidth,
256
+ tabletWidth: node.data.props.tabletWidth,
257
+ mobileHeight: node.data.props.mobileHeight,
258
+ tabletHeight: node.data.props.tabletHeight,
241
259
  minHeightUnit: node.data.props.minHeightUnit,
242
260
  heightUnit: node.data.props.heightUnit,
243
261
  widthUnit: node.data.props.widthUnit,
@@ -1259,6 +1277,10 @@ Container.craft = {
1259
1277
  { name: "minHeight", default: 20 },
1260
1278
  { name: "height" },
1261
1279
  { name: "width" },
1280
+ { name: "mobileWidth" },
1281
+ { name: "tabletWidth" },
1282
+ { name: "mobileHeight" },
1283
+ { name: "tabletHeight" },
1262
1284
  { name: "click_action" },
1263
1285
  { name: "url", canBeFormula: true },
1264
1286
  { name: "hoverColor" },
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @category saltcorn-builder
3
+ * @module components/elements/Prompt
4
+ * @subcategory components / elements
5
+ */
6
+
7
+ import React, { useState, useContext, Fragment } from "react";
8
+ import { useNode, useEditor } from "@craftjs/core";
9
+ import useTranslation from "../../hooks/useTranslation";
10
+ import optionsCtx from "../context";
11
+ import StorageCtx from "../storage_context";
12
+
13
+ const PROMPT_ICONS = {
14
+ container: "fas fa-box",
15
+ view: "fas fa-eye",
16
+ field: "fas fa-i-cursor",
17
+ action: "fas fa-bolt",
18
+ };
19
+
20
+ const PROMPT_LABELS = {
21
+ container: "Container",
22
+ view: "View",
23
+ field: "Field",
24
+ action: "Action",
25
+ };
26
+
27
+ export const Prompt = ({ promptType, promptText }) => {
28
+ const {
29
+ connectors: { connect, drag },
30
+ selected,
31
+ actions: { setProp },
32
+ id,
33
+ parent,
34
+ } = useNode((state) => ({
35
+ selected: state.events.selected,
36
+ parent: state.data.parent,
37
+ }));
38
+
39
+ const { query, actions: editorActions } = useEditor();
40
+ const options = useContext(optionsCtx);
41
+ const { layoutToNodes } = useContext(StorageCtx);
42
+ const { t } = useTranslation();
43
+
44
+ const [generating, setGenerating] = useState(false);
45
+
46
+ const icon = PROMPT_ICONS[promptType] || "fas fa-robot";
47
+
48
+ const handleGenerate = async (e) => {
49
+ e.stopPropagation();
50
+ if (!promptText.trim()) return;
51
+
52
+ setGenerating(true);
53
+ setProp((props) => {
54
+ props.generateError = null;
55
+ });
56
+
57
+ try {
58
+ const combinedPrompt = `[${promptType}]: ${promptText}`;
59
+
60
+ const res = await fetch("/viewedit/copilot-generate-layout", {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ "CSRF-Token": options.csrfToken,
65
+ "X-Requested-With": "XMLHttpRequest",
66
+ },
67
+ body: JSON.stringify({
68
+ prompt: combinedPrompt,
69
+ mode: options.mode,
70
+ table: options.tableName,
71
+ }),
72
+ });
73
+
74
+ const data = await res.json();
75
+ if (data.error) {
76
+ setProp((props) => {
77
+ props.generateError = data.error;
78
+ });
79
+ } else if (data.layout) {
80
+ editorActions.delete(id);
81
+ layoutToNodes(data.layout, query, editorActions, parent, options);
82
+ }
83
+ } catch (err) {
84
+ setProp((props) => {
85
+ props.generateError = err.message || "Generation failed";
86
+ });
87
+ } finally {
88
+ setGenerating(false);
89
+ }
90
+ };
91
+
92
+ return (
93
+ <div
94
+ ref={(dom) => connect(drag(dom))}
95
+ className={`prompt-placeholder ${selected ? "selected-node" : ""}`}
96
+ style={{
97
+ border: "2px dashed #6c8ebf",
98
+ borderRadius: "8px",
99
+ padding: "12px",
100
+ margin: "4px 0",
101
+ backgroundColor: "#e8f0fe",
102
+ minHeight: "60px",
103
+ }}
104
+ >
105
+ <div
106
+ style={{
107
+ display: "flex",
108
+ alignItems: "center",
109
+ gap: "6px",
110
+ marginBottom: "6px",
111
+ fontWeight: "bold",
112
+ fontSize: "13px",
113
+ color: "#1a73e8",
114
+ }}
115
+ >
116
+ <i className={icon}></i>
117
+ <span>{t("Prompt")}</span>
118
+ </div>
119
+ <textarea
120
+ rows="3"
121
+ className="form-control form-control-sm"
122
+ style={{
123
+ fontSize: "13px",
124
+ backgroundColor: "transparent",
125
+ border: "1px solid #b0c4de",
126
+ resize: "vertical",
127
+ marginBottom: "8px",
128
+ }}
129
+ value={promptText}
130
+ placeholder={t("Describe what you want to generate...")}
131
+ onChange={(e) =>
132
+ setProp((props) => {
133
+ props.promptText = e.target.value;
134
+ })
135
+ }
136
+ onClick={(e) => e.stopPropagation()}
137
+ />
138
+ <button
139
+ className="btn btn-sm btn-success w-100"
140
+ onClick={handleGenerate}
141
+ disabled={generating || !promptText.trim()}
142
+ style={{ fontSize: "12px" }}
143
+ >
144
+ {generating ? (
145
+ <Fragment>
146
+ <span
147
+ className="spinner-border spinner-border-sm me-1"
148
+ role="status"
149
+ ></span>
150
+ {t("Generating...")}
151
+ </Fragment>
152
+ ) : (
153
+ <Fragment>
154
+ <i className="fas fa-robot me-1"></i>
155
+ {t("Generate")}
156
+ </Fragment>
157
+ )}
158
+ </button>
159
+ </div>
160
+ );
161
+ };
162
+
163
+ const PromptSettings = () => {
164
+ const { t } = useTranslation();
165
+ const {
166
+ actions: { setProp },
167
+ promptType,
168
+ promptText,
169
+ generateError,
170
+ id,
171
+ parent,
172
+ } = useNode((node) => ({
173
+ promptType: node.data.props.promptType,
174
+ promptText: node.data.props.promptText,
175
+ generateError: node.data.props.generateError,
176
+ id: node.id,
177
+ parent: node.data.parent,
178
+ }));
179
+
180
+ const { query, actions: editorActions } = useEditor();
181
+ const options = useContext(optionsCtx);
182
+ const { layoutToNodes } = useContext(StorageCtx);
183
+
184
+ const [generating, setGenerating] = useState(false);
185
+
186
+ const handleGenerate = async () => {
187
+ if (!promptText.trim()) return;
188
+
189
+ setGenerating(true);
190
+ setProp((props) => {
191
+ props.generateError = null;
192
+ });
193
+
194
+ try {
195
+ const combinedPrompt = `[${promptType}]: ${promptText}`;
196
+
197
+ const res = await fetch("/viewedit/copilot-generate-layout", {
198
+ method: "POST",
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ "CSRF-Token": options.csrfToken,
202
+ "X-Requested-With": "XMLHttpRequest",
203
+ },
204
+ body: JSON.stringify({
205
+ prompt: combinedPrompt,
206
+ mode: options.mode,
207
+ table: options.tableName,
208
+ }),
209
+ });
210
+
211
+ const data = await res.json();
212
+ if (data.error) {
213
+ setProp((props) => {
214
+ props.generateError = data.error;
215
+ });
216
+ } else if (data.layout) {
217
+ editorActions.delete(id);
218
+ layoutToNodes(data.layout, query, editorActions, parent, options);
219
+ }
220
+ } catch (err) {
221
+ setProp((props) => {
222
+ props.generateError = err.message || "Generation failed";
223
+ });
224
+ } finally {
225
+ setGenerating(false);
226
+ }
227
+ };
228
+
229
+ return (
230
+ <div>
231
+ <div className="mb-2">
232
+ <label className="form-label">{t("Prompt")}</label>
233
+ <textarea
234
+ rows="4"
235
+ className="form-control"
236
+ value={promptText}
237
+ placeholder={t("Describe what you want to generate...")}
238
+ onChange={(e) =>
239
+ setProp((props) => {
240
+ props.promptText = e.target.value;
241
+ })
242
+ }
243
+ />
244
+ {generateError && (
245
+ <div className="text-danger small mt-1">{generateError}</div>
246
+ )}
247
+ </div>
248
+ <div className="mb-2">
249
+ <button
250
+ className="btn btn-sm btn-success w-100"
251
+ onClick={handleGenerate}
252
+ disabled={generating || !promptText.trim()}
253
+ >
254
+ {generating ? (
255
+ <Fragment>
256
+ <span
257
+ className="spinner-border spinner-border-sm me-1"
258
+ role="status"
259
+ ></span>
260
+ {t("Generating...")}
261
+ </Fragment>
262
+ ) : (
263
+ <Fragment>
264
+ <i className="fas fa-robot me-1"></i>
265
+ {t("Generate")}
266
+ </Fragment>
267
+ )}
268
+ </button>
269
+ </div>
270
+ </div>
271
+ );
272
+ };
273
+
274
+ Prompt.craft = {
275
+ displayName: "Prompt",
276
+ defaultProps: {
277
+ promptType: "container",
278
+ promptText: "",
279
+ },
280
+ related: {
281
+ settings: PromptSettings,
282
+ segment_type: "prompt",
283
+ fields: ["promptType", "promptText"],
284
+ },
285
+ };
@@ -19,8 +19,10 @@ import {
19
19
  SettingsRow,
20
20
  setAPropGen,
21
21
  } from "./utils";
22
+ import { getDeviceValue } from "../../utils/responsive_utils";
22
23
  import ContentEditable from "react-contenteditable";
23
24
  import optionsCtx from "../context";
25
+ import PreviewCtx from "../preview_context";
24
26
  import { CKEditor } from "ckeditor4-react";
25
27
  import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
26
28
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -89,6 +91,8 @@ const Text = ({
89
91
  font,
90
92
  style,
91
93
  customClass,
94
+ mobileFontSize,
95
+ tabletFontSize,
92
96
  }) => {
93
97
  const {
94
98
  connectors: { connect, drag },
@@ -99,6 +103,19 @@ const Text = ({
99
103
  dragged: state.events.dragged,
100
104
  }));
101
105
  const [editable, setEditable] = useState(false);
106
+ const { previewDevice } = useContext(PreviewCtx);
107
+
108
+ const baseStyle = {
109
+ ...(font ? { fontFamily: font } : {}),
110
+ ...reactifyStyles(style || {}),
111
+ };
112
+ const activeFontSize = getDeviceValue(
113
+ baseStyle.fontSize,
114
+ tabletFontSize,
115
+ mobileFontSize,
116
+ previewDevice
117
+ );
118
+ if (activeFontSize) baseStyle.fontSize = activeFontSize;
102
119
 
103
120
  useEffect(() => {
104
121
  !selected && setEditable(false);
@@ -112,10 +129,7 @@ const Text = ({
112
129
  } ${selected ? "selected-node" : ""}`}
113
130
  ref={(dom) => connect(drag(dom))}
114
131
  onDoubleClick={(e) => selected && setEditable(true)}
115
- style={{
116
- ...(font ? { fontFamily: font } : {}),
117
- ...reactifyStyles(style || {}),
118
- }}
132
+ style={baseStyle}
119
133
  >
120
134
  <DynamicFontAwesomeIcon icon={icon} className="me-1" />
121
135
  {isFormula.text ? (
@@ -158,6 +172,7 @@ export /**
158
172
  */
159
173
  const TextSettings = () => {
160
174
  const { t } = useTranslation();
175
+ const { previewDevice } = useContext(PreviewCtx);
161
176
  const node = useNode((node) => ({
162
177
  id: node.id,
163
178
  text: node.data.props.text,
@@ -170,6 +185,8 @@ const TextSettings = () => {
170
185
  icon: node.data.props.icon,
171
186
  font: node.data.props.font,
172
187
  style: node.data.props.style,
188
+ mobileFontSize: node.data.props.mobileFontSize,
189
+ tabletFontSize: node.data.props.tabletFontSize,
173
190
  }));
174
191
  const {
175
192
  actions: { setProp },
@@ -183,6 +200,8 @@ const TextSettings = () => {
183
200
  font,
184
201
  style,
185
202
  customClass,
203
+ mobileFontSize,
204
+ tabletFontSize,
186
205
  } = node;
187
206
  const { mode, fields, icons } = useContext(optionsCtx);
188
207
  const setAProp = setAPropGen(setProp);
@@ -269,16 +288,41 @@ const TextSettings = () => {
269
288
  node={node}
270
289
  setProp={setProp}
271
290
  />
272
- <SettingsRow
273
- field={{
274
- name: "font-size",
275
- label: t("Font size"),
276
- type: "DimUnits",
277
- }}
278
- node={node}
279
- setProp={setProp}
280
- isStyle={true}
281
- />
291
+ {previewDevice === "desktop" ? (
292
+ <SettingsRow
293
+ field={{
294
+ name: "font-size",
295
+ label: t("Font size"),
296
+ type: "DimUnits",
297
+ }}
298
+ node={node}
299
+ setProp={setProp}
300
+ isStyle={true}
301
+ />
302
+ ) : (
303
+ <SettingsRow
304
+ field={{
305
+ name: "font-size",
306
+ label: `${t("Font size")} (${previewDevice})`,
307
+ type: "DimUnits",
308
+ }}
309
+ node={{
310
+ ...node,
311
+ style: {
312
+ "font-size": previewDevice === "mobile" ? mobileFontSize : tabletFontSize,
313
+ },
314
+ }}
315
+ setProp={(fn) => {
316
+ // Write to mobileFontSize/tabletFontSize instead of style
317
+ const proxy = { style: {} };
318
+ fn(proxy);
319
+ const val = proxy.style["font-size"];
320
+ const propName = previewDevice === "mobile" ? "mobileFontSize" : "tabletFontSize";
321
+ setProp((prop) => { prop[propName] = val; });
322
+ }}
323
+ isStyle={true}
324
+ />
325
+ )}
282
326
  <SettingsRow
283
327
  field={{
284
328
  name: "font-weight",
@@ -1350,7 +1350,9 @@ const ConfigField = ({
1350
1350
  e?.target &&
1351
1351
  myOnChange(
1352
1352
  isStyle || subProp
1353
- ? `${e.target.value}${styleDim || "px"}`
1353
+ ? e.target.value === ""
1354
+ ? ""
1355
+ : `${e.target.value}${styleDim || "px"}`
1354
1356
  : e.target.value
1355
1357
  )
1356
1358
  }
@@ -30,6 +30,7 @@ import { DropDownFilter } from "./elements/DropDownFilter";
30
30
  import { ToggleFilter } from "./elements/ToggleFilter";
31
31
  import { DropMenu } from "./elements/DropMenu";
32
32
  import { Container } from "./elements/Container";
33
+ import { Prompt } from "./elements/Prompt";
33
34
  import { rand_ident } from "./elements/utils";
34
35
 
35
36
  /**
@@ -80,6 +81,7 @@ const allElements = [
80
81
  Table,
81
82
  ListColumn,
82
83
  ListColumns,
84
+ Prompt,
83
85
  ];
84
86
 
85
87
  export /**
@@ -183,6 +185,8 @@ const layoutToNodes = (
183
185
  style={segment.style || {}}
184
186
  icon={segment.icon}
185
187
  font={segment.font || ""}
188
+ mobileFontSize={segment.mobileFontSize}
189
+ tabletFontSize={segment.tabletFontSize}
186
190
  />
187
191
  );
188
192
  } else if (segment.type === "view") {
@@ -318,6 +322,12 @@ const layoutToNodes = (
318
322
  colClasses={segment.colClasses}
319
323
  colStyles={segment.colStyles}
320
324
  aligns={segment.aligns}
325
+ mobileAligns={segment.mobileAligns}
326
+ tabletAligns={segment.tabletAligns}
327
+ mobileWidth={segment.mobileWidth}
328
+ tabletWidth={segment.tabletWidth}
329
+ mobileHeight={segment.mobileHeight}
330
+ tabletHeight={segment.tabletHeight}
321
331
  setting_col_n={segment.setting_col_n !== undefined ? segment.setting_col_n : 0}
322
332
  contents={segment.besides.map(toTag)}
323
333
  />
@@ -354,6 +364,12 @@ const layoutToNodes = (
354
364
  colClasses={segment.colClasses}
355
365
  colStyles={segment.colStyles}
356
366
  aligns={segment.aligns}
367
+ mobileAligns={segment.mobileAligns}
368
+ tabletAligns={segment.tabletAligns}
369
+ mobileWidth={segment.mobileWidth}
370
+ tabletWidth={segment.tabletWidth}
371
+ mobileHeight={segment.mobileHeight}
372
+ tabletHeight={segment.tabletHeight}
357
373
  setting_col_n={segment.setting_col_n !== undefined ? segment.setting_col_n : 0}
358
374
  contents={segment.besides.map(toTag)}
359
375
  />
@@ -489,6 +505,8 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
489
505
  style: node.props.style,
490
506
  icon: node.props.icon,
491
507
  font: node.props.font,
508
+ mobileFontSize: node.props.mobileFontSize,
509
+ tabletFontSize: node.props.tabletFontSize,
492
510
  ...customProps,
493
511
  };
494
512
  }
@@ -525,10 +543,16 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
525
543
  gx: node.props.gx != null ? +node.props.gx : undefined,
526
544
  gy: node.props.gy != null ? +node.props.gy : undefined,
527
545
  aligns: node.props.aligns,
546
+ mobileAligns: node.props.mobileAligns,
547
+ tabletAligns: node.props.tabletAligns,
528
548
  vAligns: node.props.vAligns,
529
549
  colClasses: node.props.colClasses,
530
550
  colStyles: node.props.colStyles,
531
551
  style: node.props.style,
552
+ mobileWidth: node.props.mobileWidth,
553
+ tabletWidth: node.props.tabletWidth,
554
+ mobileHeight: node.props.mobileHeight,
555
+ tabletHeight: node.props.tabletHeight,
532
556
  widths,
533
557
  setting_col_n: node.props.setting_col_n,
534
558
  ...customProps,