@superleapai/flow-ui 2.3.4 → 2.3.6

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.
@@ -35,10 +35,18 @@
35
35
  };
36
36
 
37
37
  var CHECKBOX_BASE_CLASS =
38
- "flex items-center justify-center rounded-2 border-1/2 border-borderColor-border-primary bg-fill-quarternary-fill-white p-4 transition-all hover:border-primary-base hover:shadow-primary-focused disabled:cursor-not-allowed disabled:border-borderColor-border-primary disabled:opacity-50 disabled:hover:shadow-none";
38
+ "flex items-center justify-center rounded-2 border-1/2 border-borderColor-border-primary bg-fill-quarternary-fill-white p-4 transition-all";
39
+
40
+ var CHECKBOX_ACTIVE_CLASS =
41
+ "hover:border-primary-base hover:shadow-primary-focused cursor-pointer";
42
+
43
+ var CHECKBOX_DISABLED_CLASS =
44
+ "cursor-not-allowed opacity-50";
39
45
 
40
46
  var CHECKBOX_CHECKED_CLASS =
41
- "data-checked:border-transparent data-checked:bg-primary-base data-checked:hover:border-primary-base data-checked:hover:shadow-primary-focused data-checked:disabled:border-borderColor-border-primary";
47
+ "data-checked:border-transparent data-checked:bg-primary-base";
48
+ var CHECKBOX_CHECKED_ACTIVE_CLASS =
49
+ "data-checked:hover:border-primary-base data-checked:hover:shadow-primary-focused";
42
50
 
43
51
  var LABEL_BASE_CLASS =
44
52
  "cursor-pointer pb-0 text-reg-12 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70";
@@ -102,12 +110,16 @@
102
110
 
103
111
  // Custom checkbox visual
104
112
  var checkboxBox = document.createElement("div");
105
- checkboxBox.className = join(
106
- CHECKBOX_BASE_CLASS,
107
- CHECKBOX_CHECKED_CLASS,
108
- CHECKBOX_SIZES[size] || CHECKBOX_SIZES.default,
109
- "cursor-pointer"
110
- );
113
+ function applyCheckboxBoxClasses() {
114
+ var sizeClass = CHECKBOX_SIZES[size] || CHECKBOX_SIZES.default;
115
+ checkboxBox.className = join(
116
+ CHECKBOX_BASE_CLASS,
117
+ disabled ? CHECKBOX_DISABLED_CLASS : join(CHECKBOX_ACTIVE_CLASS, CHECKBOX_CHECKED_ACTIVE_CLASS),
118
+ CHECKBOX_CHECKED_CLASS,
119
+ sizeClass
120
+ );
121
+ }
122
+ applyCheckboxBoxClasses();
111
123
  checkboxBox.setAttribute("role", "checkbox");
112
124
  checkboxBox.setAttribute("tabindex", disabled ? "-1" : "0");
113
125
  checkboxBox.setAttribute("aria-checked", indeterminate ? "mixed" : checked ? "true" : "false");
@@ -224,6 +236,7 @@
224
236
  } else {
225
237
  checkboxBox.removeAttribute("aria-disabled");
226
238
  }
239
+ applyCheckboxBoxClasses();
227
240
  updateCheckedState();
228
241
  };
229
242
 
@@ -26,7 +26,7 @@
26
26
  sizeLarge: "",
27
27
  sizeSmall: "",
28
28
  disabled:
29
- "cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary",
29
+ "pointer-events-none cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary",
30
30
  };
31
31
 
32
32
  // Currency type label: fit-content, separator (border-r) on type only, full height
@@ -213,7 +213,12 @@
213
213
  wrapper.setDisabled = function (d) {
214
214
  disabled = !!d;
215
215
  input.disabled = disabled;
216
- wrapper.classList.toggle("cursor-not-allowed", disabled);
216
+ wrapper.className = join(
217
+ WRAPPER_CLASS.base,
218
+ WRAPPER_CLASS[variant] != null ? WRAPPER_CLASS[variant] : WRAPPER_CLASS.default,
219
+ disabled ? WRAPPER_CLASS.disabled : "",
220
+ config.className || ""
221
+ );
217
222
  };
218
223
 
219
224
  return wrapper;
@@ -243,13 +243,15 @@
243
243
  var displayMonth = validDate ? new Date(validDate.getTime()) : new Date();
244
244
 
245
245
  var triggerWrapper = document.createElement("div");
246
- var triggerClasses = join(
247
- "group flex items-center border-1/2 border-border-primary rounded-4 text-typography-primary-text gap-x-8 w-full transition-all ease-in-out",
248
- "bg-fill-quarternary-fill-white hover:border-primary-base focus-within:border-primary-base",
249
- size === "large" ? "px-12 py-8" : size === "small" ? "px-12 py-4" : "px-12 py-6",
250
- disabled ? "cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary" : "cursor-pointer"
251
- );
252
- triggerWrapper.className = triggerClasses;
246
+ function getTriggerClassName(disabledState) {
247
+ return join(
248
+ "group flex items-center border-1/2 border-border-primary rounded-4 text-typography-primary-text gap-x-8 w-full transition-all ease-in-out",
249
+ "bg-fill-quarternary-fill-white hover:border-primary-base focus-within:border-primary-base",
250
+ size === "large" ? "px-12 py-8" : size === "small" ? "px-12 py-4" : "px-12 py-6",
251
+ disabledState ? "pointer-events-none cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary" : "cursor-pointer"
252
+ );
253
+ }
254
+ triggerWrapper.className = getTriggerClassName(disabled);
253
255
  triggerWrapper.setAttribute("role", "button");
254
256
  triggerWrapper.setAttribute("tabindex", disabled ? "-1" : "0");
255
257
  triggerWrapper.setAttribute("aria-haspopup", "dialog");
@@ -517,8 +519,7 @@
517
519
  };
518
520
  container.setDisabled = function (d) {
519
521
  disabled = !!d;
520
- triggerWrapper.classList.toggle("cursor-not-allowed", disabled);
521
- triggerWrapper.classList.toggle("opacity-60", disabled);
522
+ triggerWrapper.className = getTriggerClassName(disabled);
522
523
  triggerWrapper.setAttribute("tabindex", disabled ? "-1" : "0");
523
524
  if (disabled) popoverApi.hide();
524
525
  };
@@ -40,7 +40,7 @@
40
40
  sizeLarge: "px-12 py-8",
41
41
  sizeSmall: "px-12 py-4",
42
42
  disabled:
43
- "cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary opacity-60",
43
+ "pointer-events-none cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary",
44
44
  };
45
45
 
46
46
  function join() {
@@ -433,8 +433,13 @@
433
433
  };
434
434
  container.setDisabled = function (d) {
435
435
  disabled = !!d;
436
- triggerWrapper.classList.toggle("cursor-not-allowed", disabled);
437
- triggerWrapper.classList.toggle("opacity-60", disabled);
436
+ triggerWrapper.className = join(
437
+ TRIGGER_CLASS.base,
438
+ TRIGGER_CLASS[variant] != null ? TRIGGER_CLASS[variant] : TRIGGER_CLASS.default,
439
+ size === "large" ? TRIGGER_CLASS.sizeLarge : size === "small" ? TRIGGER_CLASS.sizeSmall : TRIGGER_CLASS.sizeDefault,
440
+ disabled ? TRIGGER_CLASS.disabled : "",
441
+ className
442
+ );
438
443
  triggerWrapper.setAttribute("tabindex", disabled ? "-1" : "0");
439
444
  if (disabled) popoverApi.hide();
440
445
  };
@@ -8,7 +8,7 @@
8
8
  "use strict";
9
9
 
10
10
  var BASE_CLASS =
11
- "flex items-center border rounded-4 text-typography-primary-text gap-4 !text-reg-13";
11
+ "flex items-center border-1/2 rounded-4 text-typography-primary-text gap-4 !text-reg-13";
12
12
 
13
13
  var VARIANTS = {
14
14
  default:
@@ -32,7 +32,7 @@
32
32
  };
33
33
 
34
34
  var DISABLED_CLASS =
35
- "pointer-events-none !cursor-not-allowed opacity-50";
35
+ "pointer-events-none !cursor-not-allowed opacity-50 bg-fill-tertiary-fill-light-gray";
36
36
  var READONLY_CLASS = "pointer-events-none";
37
37
 
38
38
  var ITEM_BASE_CLASS =
@@ -97,14 +97,17 @@
97
97
  var children = opts.children;
98
98
 
99
99
  var wrapper = document.createElement("div");
100
- wrapper.className = join(
101
- BASE_CLASS,
102
- VARIANTS[variant] != null ? VARIANTS[variant] : VARIANTS.default,
103
- SIZES[size] != null ? SIZES[size] : SIZES.default,
104
- disabled ? DISABLED_CLASS : "",
105
- readOnly ? READONLY_CLASS : "",
106
- className
107
- );
100
+ function applyWrapperClasses() {
101
+ wrapper.className = join(
102
+ BASE_CLASS,
103
+ VARIANTS[variant] != null ? VARIANTS[variant] : VARIANTS.default,
104
+ SIZES[size] != null ? SIZES[size] : SIZES.default,
105
+ disabled ? DISABLED_CLASS : "",
106
+ readOnly ? READONLY_CLASS : "",
107
+ className
108
+ );
109
+ }
110
+ applyWrapperClasses();
108
111
  wrapper.setAttribute("data-enumeration-variant", variant);
109
112
 
110
113
  var count =
@@ -174,6 +177,22 @@
174
177
  }
175
178
  }
176
179
 
180
+ wrapper.setDisabled = function (d) {
181
+ disabled = d === true;
182
+ for (var j = 0; j < itemElements.length; j++) {
183
+ itemElements[j].setAttribute("tabindex", disabled || readOnly ? "-1" : "0");
184
+ }
185
+ applyWrapperClasses();
186
+ };
187
+
188
+ wrapper.setReadOnly = function (r) {
189
+ readOnly = r === true;
190
+ for (var j = 0; j < itemElements.length; j++) {
191
+ itemElements[j].setAttribute("tabindex", disabled || readOnly ? "-1" : "0");
192
+ }
193
+ applyWrapperClasses();
194
+ };
195
+
177
196
  return wrapper;
178
197
  }
179
198
 
@@ -114,8 +114,36 @@
114
114
  * @param {boolean} config.isPrivate - Whether files should be private
115
115
  * @param {number} config.maxFiles - Maximum number of files (for multiple mode)
116
116
  * @param {number} config.maxFileSize - Maximum file size in bytes
117
+ * @param {boolean} [config.disabled] - Whether the file upload is disabled
118
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'success' | 'borderless' | 'inline'
119
+ * @param {string} [config.inputSize] - 'default' | 'large' | 'small'
120
+ * @param {string} [config.className] - Extra class on upload wrapper
117
121
  * @returns {HTMLElement} Field element
118
122
  */
123
+ var UPLOAD_WRAPPER_CLASS = {
124
+ base:
125
+ "group relative flex w-full items-center justify-between border-1/2 rounded-4 text-typography-primary-text w-full transition-all ease-in-out focus-within:outline-none group-has-[:disabled]:cursor-not-allowed group-has-[:disabled]:border-border-primary group-has-[:disabled]:bg-fill-tertiary-fill-light-gray group-has-[:disabled]:text-typography-quaternary-text group-has-[:disabled]:hover:border-border-primary",
126
+ default:
127
+ "border-border-primary bg-fill-quarternary-fill-white hover:border-primary-base focus-within:border-primary-base",
128
+ error:
129
+ "border-error-base bg-fill-quarternary-fill-white hover:border-error-base focus-within:border-error-base",
130
+ warning:
131
+ "border-warning-base bg-fill-quarternary-fill-white hover:border-warning-base focus-within:border-warning-base",
132
+ success:
133
+ "border-success-base bg-fill-quarternary-fill-white hover:border-success-base focus-within:border-success-base",
134
+ borderless:
135
+ "border-none shadow-none rounded-0 bg-fill-quarternary-fill-white",
136
+ inline:
137
+ "border-transparent shadow-none rounded-0 bg-fill-quarternary-fill-white hover:bg-fill-tertiary-fill-light-gray focus-within:border-transparent focus-within:bg-fill-tertiary-fill-light-gray",
138
+ sizeDefault: "px-12 py-4",
139
+ sizeLarge: "px-12 py-6",
140
+ sizeSmall: "px-12 py-2",
141
+ };
142
+
143
+ function joinClasses() {
144
+ return Array.prototype.filter.call(arguments, Boolean).join(" ");
145
+ }
146
+
119
147
  function create(config) {
120
148
  const {
121
149
  label,
@@ -127,7 +155,14 @@
127
155
  isPrivate = false,
128
156
  maxFiles = null,
129
157
  maxFileSize = 10 * 1024 * 1024, // 10MB default
158
+ disabled = false,
159
+ variant = "default",
160
+ inputSize = "default",
161
+ className = "",
130
162
  } = config;
163
+ let disabledState = !!disabled;
164
+ let currentVariant = variant;
165
+ let currentInputSize = inputSize;
131
166
 
132
167
  if (!global.FlowUI) {
133
168
  throw new Error("FlowUI not available");
@@ -145,12 +180,33 @@
145
180
 
146
181
  // Upload row: button + status + optional end spinner (match Select/MultiSelect trigger)
147
182
  const uploadWrapper = document.createElement("div");
148
- uploadWrapper.className = "relative flex w-full items-center justify-between rounded-4 border-1/2 border-border-primary bg-fill-quarternary-fill-white px-8 py-4 hover:border-primary-border focus-within:outline-none focus-within:border-1/2 focus-within:border-primary-border";
183
+ var sizeClass =
184
+ currentInputSize === "large"
185
+ ? UPLOAD_WRAPPER_CLASS.sizeLarge
186
+ : currentInputSize === "small"
187
+ ? UPLOAD_WRAPPER_CLASS.sizeSmall
188
+ : UPLOAD_WRAPPER_CLASS.sizeDefault;
189
+ function applyWrapperClasses() {
190
+ uploadWrapper.className = joinClasses(
191
+ UPLOAD_WRAPPER_CLASS.base,
192
+ UPLOAD_WRAPPER_CLASS[currentVariant] || UPLOAD_WRAPPER_CLASS.default,
193
+ sizeClass,
194
+ className
195
+ );
196
+ }
197
+ applyWrapperClasses();
198
+ uploadWrapper.setAttribute("data-file-input-variant", currentVariant);
199
+ uploadWrapper.setAttribute("data-file-input-size", currentInputSize);
200
+ uploadWrapper.setAttribute("data-disabled", disabledState ? "true" : "false");
149
201
 
150
202
  // Left content (button + status) – pointer-events-none so overlay input receives clicks
151
203
  const leftContent = document.createElement("div");
152
204
  leftContent.className = "pointer-events-none flex min-w-0 flex-1 items-center gap-8 truncate";
153
205
 
206
+ var disabledChildClasses =
207
+ "group-has-[:disabled]:text-typography-quaternary-text group-has-[:disabled]:bg-fill-tertiary-fill-light-gray group-has-[:disabled]:hover:bg-fill-tertiary-fill-light-gray";
208
+ var statusTextBaseClass = "text-reg-13 min-w-0 flex-1 truncate";
209
+ var statusTextDisabledClass = " group-has-[:disabled]:text-typography-quaternary-text";
154
210
  const useButtonComponent = global.Button && typeof global.Button.create === "function";
155
211
  const initialButtonText = multiple ? "Choose files" : "Choose a file";
156
212
  const btn = useButtonComponent
@@ -158,31 +214,41 @@
158
214
  variant: "outline",
159
215
  size: "small",
160
216
  text: initialButtonText,
161
- className: "shrink-0 truncate",
217
+ className: "shrink-0 truncate " + disabledChildClasses,
162
218
  })
163
219
  : (function () {
164
220
  const el = document.createElement("div");
165
- el.className = "shrink-0 truncate rounded-2 border-1/2 border-border-primary bg-fill-tertiary-fill-light-gray px-8 py-1 text-reg-13 text-typography-primary-text transition-colors duration-150 hover:bg-fill-secondary-fill-gray";
221
+ el.className =
222
+ "shrink-0 truncate rounded-2 border-1/2 border-border-primary bg-fill-tertiary-fill-light-gray px-8 py-1 text-reg-13 text-typography-primary-text transition-colors duration-150 hover:bg-fill-secondary-fill-gray " +
223
+ disabledChildClasses;
166
224
  el.textContent = initialButtonText;
167
225
  return el;
168
226
  })();
169
227
 
170
228
  // Status text: "No files chosen" (quaternary) or "X file(s) selected"
171
229
  const statusText = document.createElement("p");
172
- statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-quaternary-text";
230
+ statusText.className = statusTextBaseClass + " text-typography-quaternary-text" + statusTextDisabledClass;
173
231
 
174
232
  // Hidden file input – overlays row, high z-index so it receives clicks
175
233
  const input = document.createElement("input");
176
234
  input.type = "file";
177
- input.className = "absolute inset-0 z-10 w-full cursor-pointer opacity-0";
235
+ const inputBaseClass = "absolute inset-0 z-10 w-full opacity-0";
236
+ function applyInputClasses() {
237
+ input.className =
238
+ inputBaseClass +
239
+ (disabledState ? " !pointer-events-none !cursor-not-allowed" : " cursor-pointer");
240
+ }
241
+ applyInputClasses();
178
242
  input.multiple = multiple;
243
+ input.disabled = disabledState;
179
244
  if (accept !== "*") {
180
245
  input.accept = accept;
181
246
  }
182
247
 
183
248
  // End icon slot: spinner when uploading (match Select chevron position)
184
249
  const endIconSlot = document.createElement("div");
185
- endIconSlot.className = "ml-4 flex size-16 items-center justify-center shrink-0 text-typography-quaternary-text";
250
+ endIconSlot.className =
251
+ "ml-4 flex size-16 items-center justify-center shrink-0 text-typography-quaternary-text group-has-[:disabled]:text-typography-quaternary-text";
186
252
  endIconSlot.style.display = "none";
187
253
 
188
254
  // Uploaded files: badge list (match UploadedFilePreviewer – flex-wrap gap-2)
@@ -363,13 +429,13 @@
363
429
 
364
430
  if (!filesChosen) {
365
431
  statusText.textContent = "No files chosen";
366
- statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-quaternary-text";
432
+ statusText.className = statusTextBaseClass + " text-typography-quaternary-text" + statusTextDisabledClass;
367
433
  } else if (uploadingCount > 0) {
368
434
  statusText.textContent = "Uploading…";
369
- statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-quaternary-text";
435
+ statusText.className = statusTextBaseClass + " text-typography-quaternary-text" + statusTextDisabledClass;
370
436
  } else {
371
437
  statusText.textContent = `${uploadedCount} file${uploadedCount !== 1 ? "s" : ""} selected`;
372
- statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-primary-text";
438
+ statusText.className = statusTextBaseClass + " text-typography-primary-text" + statusTextDisabledClass;
373
439
  }
374
440
 
375
441
  endIconSlot.style.display = uploadingCount > 0 ? "flex" : "none";
@@ -506,6 +572,32 @@
506
572
  loadExistingFiles();
507
573
  updateStatus();
508
574
 
575
+ field.setDisabled = function (d) {
576
+ disabledState = !!d;
577
+ input.disabled = disabledState;
578
+ applyInputClasses();
579
+ applyWrapperClasses();
580
+ uploadWrapper.setAttribute("data-disabled", disabledState ? "true" : "false");
581
+ };
582
+
583
+ field.setVariant = function (v) {
584
+ currentVariant = v || "default";
585
+ uploadWrapper.setAttribute("data-file-input-variant", currentVariant);
586
+ applyWrapperClasses();
587
+ };
588
+
589
+ field.setInputSize = function (s) {
590
+ currentInputSize = s || "default";
591
+ sizeClass =
592
+ currentInputSize === "large"
593
+ ? UPLOAD_WRAPPER_CLASS.sizeLarge
594
+ : currentInputSize === "small"
595
+ ? UPLOAD_WRAPPER_CLASS.sizeSmall
596
+ : UPLOAD_WRAPPER_CLASS.sizeDefault;
597
+ uploadWrapper.setAttribute("data-file-input-size", currentInputSize);
598
+ applyWrapperClasses();
599
+ };
600
+
509
601
  return field;
510
602
  }
511
603
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  var WRAPPER_CLASS = {
25
25
  base:
26
- "group flex items-center border-1/2 border-border-primary rounded-4 text-typography-primary-text gap-x-8 w-full transition-all ease-in-out",
26
+ "group flex items-center border-1/2 border-border-primary rounded-4 text-typography-primary-text gap-x-8 w-full transition-all ease-in-out group-has-[:disabled]:cursor-not-allowed group-has-[:disabled]:border-border-primary group-has-[:disabled]:bg-fill-tertiary-fill-light-gray group-has-[:disabled]:text-typography-quaternary-text group-has-[:disabled]:hover:border-border-primary group-has-[:disabled]:[&_input]:cursor-not-allowed group-has-[:disabled]:[&_input]:text-typography-quaternary-text",
27
27
  default:
28
28
  "bg-fill-quarternary-fill-white hover:border-primary-base focus-within:border-primary-base",
29
29
  error:
@@ -98,13 +98,16 @@
98
98
  var isPassword = type === "password";
99
99
 
100
100
  var wrapper = document.createElement("div");
101
- wrapper.className = join(
102
- WRAPPER_CLASS.base,
103
- WRAPPER_CLASS[variant] || WRAPPER_CLASS.default,
104
- inputSize === "large" ? WRAPPER_CLASS.sizeLarge : inputSize === "small" ? WRAPPER_CLASS.sizeSmall : WRAPPER_CLASS.sizeDefault,
105
- disabled ? WRAPPER_CLASS.disabled : "",
106
- config.className || ""
107
- );
101
+ var sizeClass = inputSize === "large" ? WRAPPER_CLASS.sizeLarge : inputSize === "small" ? WRAPPER_CLASS.sizeSmall : WRAPPER_CLASS.sizeDefault;
102
+ function applyWrapperClasses() {
103
+ wrapper.className = join(
104
+ WRAPPER_CLASS.base,
105
+ disabled ? WRAPPER_CLASS.disabled : (WRAPPER_CLASS[variant] || WRAPPER_CLASS.default),
106
+ sizeClass,
107
+ config.className || ""
108
+ );
109
+ }
110
+ applyWrapperClasses();
108
111
  wrapper.setAttribute("data-input-variant", variant);
109
112
 
110
113
  if (config.prefixNode) {
@@ -234,19 +237,12 @@
234
237
  wrapper.setVariant = function (v) {
235
238
  variant = v;
236
239
  wrapper.setAttribute("data-input-variant", v);
237
- wrapper.className = join(
238
- WRAPPER_CLASS.base,
239
- WRAPPER_CLASS[variant] || WRAPPER_CLASS.default,
240
- inputSize === "large" ? WRAPPER_CLASS.sizeLarge : inputSize === "small" ? WRAPPER_CLASS.sizeSmall : WRAPPER_CLASS.sizeDefault,
241
- disabled ? WRAPPER_CLASS.disabled : "",
242
- config.className || ""
243
- );
240
+ applyWrapperClasses();
244
241
  };
245
242
  wrapper.setDisabled = function (d) {
246
243
  disabled = !!d;
247
244
  input.disabled = disabled;
248
- wrapper.classList.toggle("cursor-not-allowed", disabled);
249
- wrapper.classList.toggle("opacity-60", disabled);
245
+ applyWrapperClasses();
250
246
  };
251
247
 
252
248
  return wrapper;
@@ -19,6 +19,7 @@
19
19
  * @param {Function} [config.onOpen] - Called when popover opens (before positioning)
20
20
  * @param {string} [config.bodyClassName] - Optional class for body wrapper (overrides default padding)
21
21
  * @param {string} [config.panelClassName] - Optional class to add to panel (e.g. for width)
22
+ * @param {boolean} [config.modal=false] - If true, lock body scroll and show backdrop; only popover and trigger are interactive
22
23
  * @returns {Object} Popover API {show, hide, destroy, element}
23
24
  */
24
25
  function create(config = {}) {
@@ -33,6 +34,7 @@
33
34
  onOpen = null,
34
35
  bodyClassName = "",
35
36
  panelClassName = "",
37
+ modal = true,
36
38
  } = config;
37
39
 
38
40
  const triggerEl =
@@ -91,20 +93,68 @@
91
93
  wrapper.appendChild(panel);
92
94
  container.appendChild(wrapper);
93
95
 
96
+ var backdropEl = null;
97
+
98
+ function onBackdropWheel(e) {
99
+ e.preventDefault();
100
+ }
101
+
102
+ function applyModalOpen() {
103
+ if (!modal) return;
104
+ if (!backdropEl) {
105
+ backdropEl = document.createElement("div");
106
+ backdropEl.className = "fixed inset-0 z-[9998] bg-transparent pointer-events-auto";
107
+ backdropEl.setAttribute("aria-hidden", "true");
108
+ backdropEl.addEventListener("click", function () {
109
+ hide();
110
+ });
111
+ backdropEl.addEventListener("wheel", onBackdropWheel, { passive: false });
112
+ }
113
+ document.body.appendChild(backdropEl);
114
+ container.style.zIndex = "9999";
115
+ }
116
+
117
+ function applyModalClose() {
118
+ if (!modal) return;
119
+ if (backdropEl && backdropEl.parentNode) {
120
+ backdropEl.parentNode.removeChild(backdropEl);
121
+ }
122
+ container.style.zIndex = "";
123
+ }
124
+
94
125
  function noop() {}
95
126
 
96
127
  function position() {
97
128
  const triggerRect = triggerEl.getBoundingClientRect();
98
129
  const panelRect = panel.getBoundingClientRect();
99
130
  const gap = 8;
131
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
132
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
133
+ const spaceBelow = viewportHeight - triggerRect.bottom;
134
+ const spaceAbove = triggerRect.top;
135
+ const spaceRight = viewportWidth - triggerRect.right;
136
+ const spaceLeft = triggerRect.left;
137
+
138
+ // Flip placement when there is not enough space (prefer requested side, flip only when needed)
139
+ let effectivePlacement = placement;
140
+ if (placement === "bottom" && spaceBelow < panelRect.height + gap && spaceAbove >= panelRect.height + gap) {
141
+ effectivePlacement = "top";
142
+ } else if (placement === "top" && spaceAbove < panelRect.height + gap && spaceBelow >= panelRect.height + gap) {
143
+ effectivePlacement = "bottom";
144
+ } else if (placement === "right" && spaceRight < panelRect.width + gap && spaceLeft >= panelRect.width + gap) {
145
+ effectivePlacement = "left";
146
+ } else if (placement === "left" && spaceLeft < panelRect.width + gap && spaceRight >= panelRect.width + gap) {
147
+ effectivePlacement = "right";
148
+ }
149
+
150
+ panel.setAttribute("data-side", effectivePlacement);
151
+
100
152
  let top = 0;
101
153
  let left = 0;
102
-
103
- // Alignment offset: start = 0, center = half diff, end = full diff
104
154
  const alignLeft = (align === "center" ? (triggerRect.width - panelRect.width) / 2 : align === "end" ? triggerRect.width - panelRect.width : 0);
105
155
  const alignTop = (align === "center" ? (triggerRect.height - panelRect.height) / 2 : align === "end" ? triggerRect.height - panelRect.height : 0);
106
156
 
107
- switch (placement) {
157
+ switch (effectivePlacement) {
108
158
  case "bottom":
109
159
  top = triggerRect.height + gap;
110
160
  left = alignLeft;
@@ -137,11 +187,13 @@
137
187
  wrapper.classList.add("invisible", "opacity-0", "pointer-events-none");
138
188
  wrapper.classList.remove("visible", "opacity-100", "pointer-events-auto");
139
189
  wrapper.setAttribute("aria-hidden", "true");
190
+ applyModalClose();
140
191
  if (onClose) onClose();
141
192
  }
142
193
 
143
194
  function show() {
144
195
  if (onOpen) onOpen();
196
+ applyModalOpen();
145
197
  requestAnimationFrame(function () {
146
198
  position();
147
199
  wrapper.classList.remove("invisible", "opacity-0", "pointer-events-none");
@@ -157,6 +209,10 @@
157
209
 
158
210
  function destroy() {
159
211
  hide();
212
+ applyModalClose();
213
+ if (backdropEl && backdropEl.parentNode) {
214
+ backdropEl.parentNode.removeChild(backdropEl);
215
+ }
160
216
  if (wrapper.parentNode) {
161
217
  wrapper.parentNode.removeChild(wrapper);
162
218
  }
@@ -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
@@ -1494,6 +1494,34 @@
1494
1494
  return field;
1495
1495
  }
1496
1496
 
1497
+ // ============================================================================
1498
+ // ALERTS
1499
+ // ============================================================================
1500
+
1501
+ /**
1502
+ * Render multiple alert messages into a container
1503
+ * @param {HTMLElement} container - Container to append alerts to
1504
+ * @param {string[]} messages - Array of message strings
1505
+ * @param {string} [variant='default'] - 'default' | 'error' | 'warning' | 'success' | 'info' | 'destructive'
1506
+ */
1507
+ function renderAlerts(container, messages, variant = "default") {
1508
+ if (!container || !Array.isArray(messages)) return;
1509
+ const Alert = getComponent("Alert");
1510
+ if (Alert && typeof Alert.simple === "function") {
1511
+ messages.forEach((msg) => {
1512
+ const el = Alert.simple(msg, variant);
1513
+ if (el) container.appendChild(el);
1514
+ });
1515
+ } else {
1516
+ messages.forEach((msg) => {
1517
+ const div = document.createElement("div");
1518
+ 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");
1519
+ div.textContent = msg;
1520
+ container.appendChild(div);
1521
+ });
1522
+ }
1523
+ }
1524
+
1497
1525
  // ============================================================================
1498
1526
  // TOAST NOTIFICATIONS
1499
1527
  // ============================================================================
@@ -1667,6 +1695,7 @@
1667
1695
  renderStepper,
1668
1696
 
1669
1697
  // Alerts
1698
+ renderAlerts,
1670
1699
  showToast,
1671
1700
 
1672
1701
  // Table