@superleapai/flow-ui 2.3.5 → 2.3.7

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.
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Rich Text Editor Component (vanilla JS)
3
+ * Toolbar + contenteditable area with formatting (bold, italic, underline, headings, lists, alignment, link, image, code block, undo/redo).
4
+ * Styling matches design: rounded-12, toolbar bg-fill-tertiary-fill-light-gray, content area with min-height.
5
+ */
6
+
7
+ (function (global) {
8
+ "use strict";
9
+
10
+ var RICH_TEXT_CONTENT_STYLES =
11
+ ".rich-text-editor-content ul{list-style-type:disc;padding-left:1.5em;margin:0.5em 0}" +
12
+ ".rich-text-editor-content ol{list-style-type:decimal;padding-left:1.5em;margin:0.5em 0}" +
13
+ ".rich-text-editor-content li{margin:0.25em 0}" +
14
+ ".rich-text-editor-content li p{margin:0}" +
15
+ ".rich-text-editor-content h1{font-size:1.5rem;font-weight:700;line-height:1.3;margin:0.75em 0 0.5em}" +
16
+ ".rich-text-editor-content h2{font-size:1.25rem;font-weight:600;line-height:1.3;margin:0.75em 0 0.5em}" +
17
+ ".rich-text-editor-content h3{font-size:1.125rem;font-weight:600;line-height:1.3;margin:0.75em 0 0.5em}" +
18
+ ".rich-text-editor-content p{margin:0.5em 0}" +
19
+ ".rich-text-editor-content a{color:var(--color-primary-500);text-decoration:underline;cursor:pointer}" +
20
+ ".rich-text-editor-content pre{background:var(--color-neutral-100);border-radius:var(--sizes-size-8);padding:0.75em 1em;margin:0.5em 0;overflow-x:auto}" +
21
+ ".rich-text-editor-content code{font-family:ui-monospace,monospace;font-size:0.875em}" +
22
+ ".rich-text-editor-content img{max-width:100%;height:auto;margin:0.5em 0}" +
23
+ ".rich-text-editor-content blockquote{border-left:3px solid var(--color-neutral-200);padding-left:1em;margin:0.5em 0;color:var(--color-neutral-600)}" +
24
+ ".rich-text-editor-content hr{border:none;border-top:1px solid var(--color-neutral-150);margin:1em 0}" +
25
+ ".rich-text-editor-content .ProseMirror,.rich-text-editor-content [contenteditable]{outline:none}";
26
+
27
+ function join() {
28
+ return Array.prototype.filter.call(arguments, Boolean).join(" ");
29
+ }
30
+
31
+ function getDep(name) {
32
+ if (typeof global.FlowUI !== "undefined" && typeof global.FlowUI._getComponent === "function") {
33
+ var c = global.FlowUI._getComponent(name);
34
+ if (c) return c;
35
+ }
36
+ return global[name];
37
+ }
38
+
39
+ /** Get Tabler icon element (16px) from Icon component for toolbar. */
40
+ function getTablerIcon(iconName) {
41
+ var Icon = getDep("Icon");
42
+ if (!Icon || !Icon.iconMap || !Icon.iconMap[iconName]) return null;
43
+ var svgStr = Icon.iconMap[iconName];
44
+ var s16 = svgStr.replace(/width="24"/, 'width="16"').replace(/height="24"/, 'height="16"').replace(/width="20"/, 'width="16"').replace(/height="20"/, 'height="16"');
45
+ var span = document.createElement("span");
46
+ span.className = "flex items-center justify-center size-16";
47
+ span.innerHTML = s16;
48
+ return span;
49
+ }
50
+
51
+ function createToolbarButton(opts) {
52
+ var Button = getDep("Button");
53
+ if (!Button || typeof Button.create !== "function") {
54
+ throw new Error("RichTextEditor requires Button");
55
+ }
56
+ var icon = opts.iconStr ? getTablerIcon(opts.iconStr) : null;
57
+ return Button.create({
58
+ variant: opts.active ? "primary" : "outline",
59
+ size: "default",
60
+ title: opts.title,
61
+ icon: icon,
62
+ onClick: opts.onClick,
63
+ disabled: opts.disabled,
64
+ });
65
+ }
66
+
67
+ function createSeparator() {
68
+ var sep = document.createElement("div");
69
+ sep.className = "w-px h-16 bg-border-primary mx-4";
70
+ sep.setAttribute("aria-hidden", "true");
71
+ return sep;
72
+ }
73
+
74
+ /**
75
+ * Create a rich text editor
76
+ * @param {Object} config
77
+ * @param {string} [config.value] - Initial HTML content
78
+ * @param {string} [config.placeholder] - Placeholder when empty
79
+ * @param {number} [config.minHeightPx] - Min height of editor area (default 400)
80
+ * @param {boolean} [config.disabled]
81
+ * @param {Function} [config.onChange] - (html: string) => void
82
+ * @returns {HTMLElement} Wrapper element with getValue/setValue/setDisabled/getInput
83
+ */
84
+ function create(config) {
85
+ var value = config.value != null ? String(config.value) : "";
86
+ var placeholder = config.placeholder != null ? config.placeholder : "";
87
+ var minHeightPx = config.minHeightPx != null ? config.minHeightPx : 400;
88
+ var disabled = !!config.disabled;
89
+ var onChange = config.onChange;
90
+
91
+ var wrapper = document.createElement("div");
92
+ wrapper.className = "w-full rounded-12 border border-borderColor-border-primary shadow-soft-2x-small";
93
+
94
+ var styleEl = document.createElement("style");
95
+ styleEl.textContent = RICH_TEXT_CONTENT_STYLES;
96
+ wrapper.appendChild(styleEl);
97
+
98
+ var toolbar = document.createElement("div");
99
+ toolbar.className =
100
+ "flex flex-wrap gap-4 rounded-t-12 border-borderColor-border-primary bg-fill-tertiary-fill-light-gray p-6";
101
+ toolbar.setAttribute("role", "toolbar");
102
+
103
+ var editorEl = document.createElement("div");
104
+ editorEl.contentEditable = !disabled;
105
+ editorEl.className = join(
106
+ "rich-text-editor-content max-w-none rounded-b-12 border-t border-borderColor-border-primary p-8 text-reg-14 text-typography-primary-text"
107
+ );
108
+ editorEl.style.minHeight = minHeightPx + "px";
109
+ if (value) editorEl.innerHTML = value;
110
+ editorEl.setAttribute("data-placeholder", placeholder);
111
+
112
+ function isEmpty() {
113
+ var text = (editorEl.textContent || "").trim();
114
+ if (text) return false;
115
+ var html = (editorEl.innerHTML || "").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
116
+ return !html.trim();
117
+ }
118
+ function updatePlaceholder() {
119
+ if (placeholder && isEmpty()) {
120
+ editorEl.classList.add("empty");
121
+ editorEl.setAttribute("data-placeholder", placeholder);
122
+ } else {
123
+ editorEl.classList.remove("empty");
124
+ editorEl.removeAttribute("data-placeholder");
125
+ }
126
+ }
127
+ updatePlaceholder();
128
+
129
+ function getHtml() {
130
+ return editorEl.innerHTML;
131
+ }
132
+ function setHtml(html) {
133
+ editorEl.innerHTML = html || "";
134
+ updatePlaceholder();
135
+ }
136
+ function notifyChange() {
137
+ if (typeof onChange === "function") onChange(getHtml());
138
+ }
139
+
140
+ function isActive(cmd, val) {
141
+ try {
142
+ return document.queryCommandState(cmd);
143
+ } catch (e) {
144
+ return false;
145
+ }
146
+ }
147
+ function blockTag() {
148
+ var sel = window.getSelection();
149
+ if (!sel || sel.rangeCount === 0) return null;
150
+ var node = sel.anchorNode;
151
+ while (node && node !== editorEl) {
152
+ if (node.nodeType === 1) {
153
+ var n = node.nodeName.toLowerCase();
154
+ if (["h1", "h2", "h3", "p", "div", "pre", "blockquote"].indexOf(n) !== -1) return n;
155
+ }
156
+ node = node.parentNode;
157
+ }
158
+ return null;
159
+ }
160
+ function isAlignment(align) {
161
+ try {
162
+ if (align === "left") return document.queryCommandState("justifyLeft");
163
+ if (align === "center") return document.queryCommandState("justifyCenter");
164
+ if (align === "right") return document.queryCommandState("justifyRight");
165
+ } catch (e) {}
166
+ return false;
167
+ }
168
+
169
+ function refreshToolbar() {
170
+ toolbar.querySelectorAll("button").forEach(function (btn) {
171
+ var cmd = btn.getAttribute("data-command");
172
+ var val = btn.getAttribute("data-value");
173
+ if (!cmd) return;
174
+ var active = false;
175
+ if (cmd === "formatBlock") active = blockTag() === val;
176
+ else if (cmd === "justifyLeft" && val === "left") active = isAlignment("left");
177
+ else if (cmd === "justifyCenter" && val === "center") active = isAlignment("center");
178
+ else if (cmd === "justifyRight" && val === "right") active = isAlignment("right");
179
+ else active = isActive(cmd);
180
+ btn.classList.toggle("bg-primary-base", active);
181
+ btn.classList.toggle("border-primary-base", active);
182
+ btn.classList.toggle("text-typography-invert-text", active);
183
+ btn.classList.toggle("bg-fill-quarternary-fill-white", !active);
184
+ btn.classList.toggle("border-border-primary", !active);
185
+ btn.classList.toggle("text-typography-primary-text", !active);
186
+ });
187
+ }
188
+
189
+ function exec(cmd, value) {
190
+ editorEl.focus();
191
+ document.execCommand(cmd, false, value != null ? value : null);
192
+ refreshToolbar();
193
+ notifyChange();
194
+ }
195
+
196
+ function insertCodeBlock() {
197
+ editorEl.focus();
198
+ var sel = window.getSelection();
199
+ if (sel && sel.rangeCount) {
200
+ var range = sel.getRangeAt(0);
201
+ var pre = document.createElement("pre");
202
+ var code = document.createElement("code");
203
+ code.textContent = "code here";
204
+ pre.appendChild(code);
205
+ range.insertNode(pre);
206
+ range.setStart(code, 0);
207
+ range.setEnd(code, 0);
208
+ sel.removeAllRanges();
209
+ sel.addRange(range);
210
+ }
211
+ refreshToolbar();
212
+ notifyChange();
213
+ }
214
+
215
+ function addLink() {
216
+ var url = window.prompt("Enter the URL:", "https://");
217
+ if (url) {
218
+ exec("createLink", url);
219
+ }
220
+ }
221
+
222
+ function addImage() {
223
+ var input = document.createElement("input");
224
+ input.type = "file";
225
+ input.accept = "image/*";
226
+ input.onchange = function (e) {
227
+ var file = e.target && e.target.files && e.target.files[0];
228
+ if (file) {
229
+ var reader = new FileReader();
230
+ reader.onload = function (ev) {
231
+ var src = ev.target && ev.target.result;
232
+ if (src) {
233
+ editorEl.focus();
234
+ document.execCommand("insertImage", false, src);
235
+ notifyChange();
236
+ }
237
+ };
238
+ reader.readAsDataURL(file);
239
+ }
240
+ };
241
+ input.click();
242
+ }
243
+
244
+ function undo() {
245
+ editorEl.focus();
246
+ document.execCommand("undo", false, null);
247
+ refreshToolbar();
248
+ notifyChange();
249
+ }
250
+ function redo() {
251
+ editorEl.focus();
252
+ document.execCommand("redo", false, null);
253
+ refreshToolbar();
254
+ notifyChange();
255
+ }
256
+
257
+ function addBtn(iconStr, title, onClick, dataCommand, dataValue) {
258
+ var btn = createToolbarButton({
259
+ iconStr: iconStr,
260
+ title: title,
261
+ onClick: function () {
262
+ if (disabled) return;
263
+ onClick();
264
+ },
265
+ active: false,
266
+ });
267
+ if (dataCommand) btn.setAttribute("data-command", dataCommand);
268
+ if (dataValue != null) btn.setAttribute("data-value", dataValue);
269
+ toolbar.appendChild(btn);
270
+ }
271
+
272
+ addBtn("IconBold", "Bold (Ctrl+B)", function () { exec("bold"); }, "bold");
273
+ addBtn("IconItalic", "Italic (Ctrl+I)", function () { exec("italic"); }, "italic");
274
+ addBtn("IconUnderline", "Underline (Ctrl+U)", function () { exec("underline"); }, "underline");
275
+ toolbar.appendChild(createSeparator());
276
+ addBtn("IconH1", "Heading 1", function () { exec("formatBlock", "h1"); }, "formatBlock", "h1");
277
+ addBtn("IconH2", "Heading 2", function () { exec("formatBlock", "h2"); }, "formatBlock", "h2");
278
+ addBtn("IconH3", "Heading 3", function () { exec("formatBlock", "h3"); }, "formatBlock", "h3");
279
+ toolbar.appendChild(createSeparator());
280
+ addBtn("IconList", "Bullet List", function () { exec("insertUnorderedList"); }, "insertUnorderedList");
281
+ addBtn("IconListNumbers", "Ordered List", function () { exec("insertOrderedList"); }, "insertOrderedList");
282
+ toolbar.appendChild(createSeparator());
283
+ addBtn("IconAlignLeft", "Align Left", function () { exec("justifyLeft"); }, "justifyLeft", "left");
284
+ addBtn("IconAlignCenter", "Align Center", function () { exec("justifyCenter"); }, "justifyCenter", "center");
285
+ addBtn("IconAlignRight", "Align Right", function () { exec("justifyRight"); }, "justifyRight", "right");
286
+ toolbar.appendChild(createSeparator());
287
+ addBtn("IconCode", "Code Block", insertCodeBlock, "formatBlock", "pre");
288
+ addBtn("IconLink", "Add Link", addLink, "link");
289
+ addBtn("IconPhoto", "Insert Image", addImage);
290
+ toolbar.appendChild(createSeparator());
291
+ addBtn("IconArrowBackUp", "Undo", undo);
292
+ addBtn("IconArrowForwardUp", "Redo", redo);
293
+
294
+ editorEl.addEventListener("input", function () {
295
+ updatePlaceholder();
296
+ refreshToolbar();
297
+ notifyChange();
298
+ });
299
+ editorEl.addEventListener("keyup", refreshToolbar);
300
+ editorEl.addEventListener("mouseup", refreshToolbar);
301
+ editorEl.addEventListener("focus", refreshToolbar);
302
+
303
+ if (placeholder) {
304
+ var placeholderStyles = document.createElement("style");
305
+ placeholderStyles.textContent =
306
+ ".rich-text-editor-content.empty:before{content:attr(data-placeholder);color:var(--color-neutral-400);pointer-events:none}";
307
+ wrapper.appendChild(placeholderStyles);
308
+ }
309
+
310
+ wrapper.appendChild(toolbar);
311
+ wrapper.appendChild(editorEl);
312
+
313
+ wrapper.getInput = function () {
314
+ return editorEl;
315
+ };
316
+ wrapper.getValue = function () {
317
+ return getHtml();
318
+ };
319
+ wrapper.setValue = function (v) {
320
+ setHtml(v);
321
+ };
322
+ wrapper.setDisabled = function (d) {
323
+ disabled = !!d;
324
+ editorEl.contentEditable = !disabled;
325
+ toolbar.querySelectorAll("button").forEach(function (b) {
326
+ b.disabled = disabled;
327
+ });
328
+ };
329
+
330
+ return wrapper;
331
+ }
332
+
333
+ global.RichTextEditorComponent = {
334
+ create: create,
335
+ };
336
+ })(typeof window !== "undefined" ? window : this);
@@ -21,7 +21,7 @@
21
21
  warning:
22
22
  "min-h-[80px] border-warning-base hover:border-warning-base focus:border-warning-base",
23
23
  disabled:
24
- "cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary",
24
+ "pointer-events-none cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary",
25
25
  };
26
26
 
27
27
  function join() {
package/core/flow.js CHANGED
@@ -248,6 +248,48 @@
248
248
  return field;
249
249
  }
250
250
 
251
+ /**
252
+ * Create a rich text editor field
253
+ * @param {Object} config - Configuration object
254
+ * @param {string} config.label - Field label
255
+ * @param {string} config.fieldId - State key for this field
256
+ * @param {string} [config.placeholder] - Placeholder when empty
257
+ * @param {boolean} [config.required] - Whether field is required
258
+ * @param {string} [config.helpText] - Optional help text for tooltip
259
+ * @param {number} [config.minHeightPx] - Min height of editor area in pixels (default 400)
260
+ * @param {boolean} [config.disabled] - Whether editor is disabled
261
+ * @returns {HTMLElement} Field element
262
+ */
263
+ function createRichTextEditor(config) {
264
+ const { label, fieldId, placeholder, required = false, helpText = null, minHeightPx = 400, disabled = false } = config;
265
+
266
+ const field = createFieldWrapper(label, required, helpText);
267
+ field.setAttribute("data-field-id", fieldId);
268
+
269
+ if (getComponent("RichTextEditorComponent") && getComponent("RichTextEditorComponent").create) {
270
+ const currentValue = get(fieldId) || "";
271
+ const editorEl = getComponent("RichTextEditorComponent").create({
272
+ value: currentValue,
273
+ placeholder: placeholder || "",
274
+ minHeightPx,
275
+ disabled,
276
+ onChange: (html) => set(fieldId, html),
277
+ });
278
+ editorEl._fieldId = fieldId;
279
+ field.appendChild(editorEl);
280
+ return field;
281
+ }
282
+
283
+ const fallback = document.createElement("textarea");
284
+ fallback.className = "textarea min-h-[400px]";
285
+ fallback.placeholder = placeholder || `Enter ${label.toLowerCase()}`;
286
+ fallback.value = get(fieldId) || "";
287
+ fallback.disabled = disabled;
288
+ fallback.addEventListener("change", (e) => set(fieldId, e.target.value));
289
+ field.appendChild(fallback);
290
+ return field;
291
+ }
292
+
251
293
  /**
252
294
  * Create a select dropdown field (using custom select component)
253
295
  * @param {Object} config - Configuration object
@@ -1494,6 +1536,34 @@
1494
1536
  return field;
1495
1537
  }
1496
1538
 
1539
+ // ============================================================================
1540
+ // ALERTS
1541
+ // ============================================================================
1542
+
1543
+ /**
1544
+ * Render multiple alert messages into a container
1545
+ * @param {HTMLElement} container - Container to append alerts to
1546
+ * @param {string[]} messages - Array of message strings
1547
+ * @param {string} [variant='default'] - 'default' | 'error' | 'warning' | 'success' | 'info' | 'destructive'
1548
+ */
1549
+ function renderAlerts(container, messages, variant = "default") {
1550
+ if (!container || !Array.isArray(messages)) return;
1551
+ const Alert = getComponent("Alert");
1552
+ if (Alert && typeof Alert.simple === "function") {
1553
+ messages.forEach((msg) => {
1554
+ const el = Alert.simple(msg, variant);
1555
+ if (el) container.appendChild(el);
1556
+ });
1557
+ } else {
1558
+ messages.forEach((msg) => {
1559
+ const div = document.createElement("div");
1560
+ div.className = "rounded border p-2 text-sm " + (variant === "error" ? "bg-red-50 border-red-200 text-red-800" : "bg-gray-50 border-gray-200");
1561
+ div.textContent = msg;
1562
+ container.appendChild(div);
1563
+ });
1564
+ }
1565
+ }
1566
+
1497
1567
  // ============================================================================
1498
1568
  // TOAST NOTIFICATIONS
1499
1569
  // ============================================================================
@@ -1630,6 +1700,7 @@
1630
1700
  // Form components
1631
1701
  createInput,
1632
1702
  createTextarea,
1703
+ createRichTextEditor,
1633
1704
  createSelect,
1634
1705
  createTimePicker,
1635
1706
  createDateTimePicker,
@@ -1667,6 +1738,7 @@
1667
1738
  renderStepper,
1668
1739
 
1669
1740
  // Alerts
1741
+ renderAlerts,
1670
1742
  showToast,
1671
1743
 
1672
1744
  // Table