@superleapai/flow-ui 1.0.0
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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/components/alert.js +282 -0
- package/components/avatar.js +195 -0
- package/components/badge.js +135 -0
- package/components/button.js +201 -0
- package/components/checkbox.js +254 -0
- package/components/currency.js +227 -0
- package/components/date-time-picker/date-time-picker-utils.js +253 -0
- package/components/date-time-picker/date-time-picker.js +532 -0
- package/components/duration/duration-constants.js +46 -0
- package/components/duration/duration-utils.js +164 -0
- package/components/duration/duration.js +448 -0
- package/components/enum-multiselect.js +869 -0
- package/components/enum-select.js +831 -0
- package/components/enumeration.js +213 -0
- package/components/file-input.js +533 -0
- package/components/icon.js +200 -0
- package/components/input.js +259 -0
- package/components/label.js +111 -0
- package/components/multiselect.js +351 -0
- package/components/phone-input/phone-input.js +392 -0
- package/components/phone-input/phone-utils.js +157 -0
- package/components/popover.js +240 -0
- package/components/radio-group.js +435 -0
- package/components/record-multiselect.js +956 -0
- package/components/record-select.js +930 -0
- package/components/select.js +544 -0
- package/components/spinner.js +136 -0
- package/components/table.js +335 -0
- package/components/textarea.js +114 -0
- package/components/time-picker.js +357 -0
- package/components/toast.js +343 -0
- package/core/flow.js +1729 -0
- package/core/superleapClient.js +146 -0
- package/dist/output.css +2 -0
- package/index.d.ts +458 -0
- package/index.js +253 -0
- package/package.json +70 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Picker Component (vanilla JS, Tailwind)
|
|
3
|
+
* Dropdown with three vertical scrollable columns (hour, minute, AM/PM).
|
|
4
|
+
* Selected items: dark teal background, white text. Trigger shows time + clock icon.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function (global) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
function getDep(name) {
|
|
11
|
+
if (typeof global.FlowUI !== "undefined" && typeof global.FlowUI._getComponent === "function") {
|
|
12
|
+
var c = global.FlowUI._getComponent(name);
|
|
13
|
+
if (c) return c;
|
|
14
|
+
}
|
|
15
|
+
return global[name];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var CLOCK_SVG =
|
|
19
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
20
|
+
|
|
21
|
+
function pad(n) {
|
|
22
|
+
return String(n).padStart(2, "0");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseValue(value) {
|
|
26
|
+
if (!value || typeof value !== "string") {
|
|
27
|
+
return { hour: 0, minute: 0 };
|
|
28
|
+
}
|
|
29
|
+
var parts = value.trim().split(":");
|
|
30
|
+
var hour = Math.min(23, Math.max(0, parseInt(parts[0], 10) || 0));
|
|
31
|
+
var minute = Math.min(59, Math.max(0, parseInt(parts[1], 10) || 0));
|
|
32
|
+
return { hour: hour, minute: minute };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Internal value "HH:mm" (24h) */
|
|
36
|
+
function formatValue(hour, minute) {
|
|
37
|
+
return pad(hour) + ":" + pad(minute);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Display string: "12 : 00 AM" or "00 : 00" */
|
|
41
|
+
function formatDisplay(hour, minute, use24Hour) {
|
|
42
|
+
if (use24Hour) {
|
|
43
|
+
return pad(hour) + " : " + pad(minute);
|
|
44
|
+
}
|
|
45
|
+
var h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
|
46
|
+
var amPm = hour >= 12 ? "PM" : "AM";
|
|
47
|
+
return pad(h12) + " : " + pad(minute) + " " + amPm;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function triggerClasses(disabled, hasValue, size) {
|
|
51
|
+
var base =
|
|
52
|
+
"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 " +
|
|
53
|
+
"bg-fill-quarternary-fill-white hover:border-primary-base focus-within:border-primary-base focus:outline-none min-h-0 h-full truncate hover:cursor-pointer ";
|
|
54
|
+
var sizeClass = size === "small" ? "px-12 py-4 !text-reg-12" : "px-12 py-6 !text-reg-13";
|
|
55
|
+
var valueClass = hasValue ? "text-inherit" : "text-typography-quaternary-text";
|
|
56
|
+
var disabledClass = disabled
|
|
57
|
+
? " pointer-events-none cursor-not-allowed border-border-primary bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary"
|
|
58
|
+
: "";
|
|
59
|
+
return base + sizeClass + " " + valueClass + disabledClass;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a scrollable column of options; one item is selected (teal bg, white text).
|
|
64
|
+
* Uses Button component for each option.
|
|
65
|
+
* @param {Array<{value: string|number, label: string}>} options
|
|
66
|
+
* @param {string|number} selectedValue
|
|
67
|
+
* @param {function(string|number)} onSelect
|
|
68
|
+
* @param {boolean} [tabularNums] - use tabular-nums for digit alignment (hour/minute)
|
|
69
|
+
* @returns {{ el: HTMLElement, setSelected: function, scrollToSelected: function }}
|
|
70
|
+
*/
|
|
71
|
+
function createColumn(options, selectedValue, onSelect, tabularNums) {
|
|
72
|
+
var Button = getDep("Button");
|
|
73
|
+
var col = document.createElement("div");
|
|
74
|
+
col.className =
|
|
75
|
+
"flex flex-col flex-shrink-0 min-w-32 overflow-y-auto max-h-[200px] p-6 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:[display:none]";
|
|
76
|
+
col.setAttribute("role", "listbox");
|
|
77
|
+
|
|
78
|
+
var itemClassName = (tabularNums ? "tabular-nums" : "");
|
|
79
|
+
var items = [];
|
|
80
|
+
if (!Button || typeof Button.create !== "function") {
|
|
81
|
+
var fallbackCol = document.createElement("div");
|
|
82
|
+
fallbackCol.className = col.className;
|
|
83
|
+
fallbackCol.setAttribute("role", "listbox");
|
|
84
|
+
fallbackCol.textContent = "Button component required";
|
|
85
|
+
return { el: fallbackCol, setSelected: function () {}, scrollToSelected: function () {}, items: [] };
|
|
86
|
+
}
|
|
87
|
+
options.forEach(function (opt) {
|
|
88
|
+
var val = opt.value !== undefined ? opt.value : opt.label;
|
|
89
|
+
var label = opt.label !== undefined ? opt.label : String(val);
|
|
90
|
+
var isSelected = String(val) === String(selectedValue);
|
|
91
|
+
var item = Button.create({
|
|
92
|
+
variant: isSelected ? "primary" : "ghost",
|
|
93
|
+
size: "small",
|
|
94
|
+
text: label,
|
|
95
|
+
className: itemClassName,
|
|
96
|
+
onClick: function () {
|
|
97
|
+
onSelect(val);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
item.setAttribute("role", "option");
|
|
101
|
+
item.setAttribute("data-value", String(val));
|
|
102
|
+
item.setAttribute("aria-selected", isSelected);
|
|
103
|
+
col.appendChild(item);
|
|
104
|
+
items.push({ el: item, value: val, label: label });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function setSelected(val) {
|
|
108
|
+
items.forEach(function (x) {
|
|
109
|
+
var selected = String(x.value) === String(val);
|
|
110
|
+
x.el.setAttribute("aria-selected", selected);
|
|
111
|
+
x.el.className = Button.getButtonClasses({
|
|
112
|
+
variant: selected ? "primary" : "ghost",
|
|
113
|
+
size: "small",
|
|
114
|
+
}) + " " + itemClassName;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function scrollToSelected() {
|
|
119
|
+
var selectedEl = col.querySelector("[aria-selected=true]");
|
|
120
|
+
if (selectedEl) selectedEl.scrollIntoView({ block: "nearest", behavior: "auto" });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { el: col, setSelected: setSelected, scrollToSelected: scrollToSelected, items: items };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a time picker component
|
|
128
|
+
* @param {Object} config - Configuration object
|
|
129
|
+
* @param {string} config.fieldId - Field ID for state / form
|
|
130
|
+
* @param {string} config.value - Current value "HH:mm"
|
|
131
|
+
* @param {string} config.placeholder - Placeholder text
|
|
132
|
+
* @param {Function} config.onChange - Change handler (value: string "HH:mm")
|
|
133
|
+
* @param {boolean} config.disabled - Whether the picker is disabled
|
|
134
|
+
* @param {boolean} config.use24Hour - Use 24-hour format (default: false, i.e. 12-hour AM/PM)
|
|
135
|
+
* @param {string} [config.size] - 'default' | 'small' (matches Input/Select sizes)
|
|
136
|
+
* @returns {HTMLElement} Time picker container element
|
|
137
|
+
*/
|
|
138
|
+
function create(config) {
|
|
139
|
+
var fieldId = config.fieldId;
|
|
140
|
+
var initialValue = config.value !== undefined ? config.value : "";
|
|
141
|
+
var placeholder = config.placeholder || "Select time";
|
|
142
|
+
var onChange = config.onChange;
|
|
143
|
+
var disabled = config.disabled === true;
|
|
144
|
+
var use24Hour = config.use24Hour === true;
|
|
145
|
+
var size = config.size === "small" ? "small" : "default";
|
|
146
|
+
|
|
147
|
+
var value = typeof initialValue === "string" ? initialValue : "";
|
|
148
|
+
var parsed = parseValue(value);
|
|
149
|
+
var hour = parsed.hour;
|
|
150
|
+
var minute = parsed.minute;
|
|
151
|
+
var period = hour >= 12 ? "PM" : "AM";
|
|
152
|
+
var hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
|
153
|
+
|
|
154
|
+
var container = document.createElement("div");
|
|
155
|
+
container.className = "time-picker relative w-full group";
|
|
156
|
+
if (fieldId) container.setAttribute("data-field-id", fieldId);
|
|
157
|
+
|
|
158
|
+
var trigger = document.createElement("button");
|
|
159
|
+
trigger.type = "button";
|
|
160
|
+
trigger.disabled = disabled;
|
|
161
|
+
trigger.setAttribute("aria-haspopup", "listbox");
|
|
162
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
163
|
+
trigger.setAttribute("aria-label", placeholder);
|
|
164
|
+
trigger.className = triggerClasses(disabled, !!value, size);
|
|
165
|
+
|
|
166
|
+
var triggerText = document.createElement("span");
|
|
167
|
+
triggerText.className =
|
|
168
|
+
"flex-1 truncate text-left tabular-nums " +
|
|
169
|
+
(value ? "text-inherit" : "text-typography-quaternary-text");
|
|
170
|
+
triggerText.textContent = value ? formatDisplay(hour, minute, use24Hour) : placeholder;
|
|
171
|
+
trigger.appendChild(triggerText);
|
|
172
|
+
|
|
173
|
+
var iconWrap = document.createElement("span");
|
|
174
|
+
iconWrap.className = "flex size-16 shrink-0 items-center justify-center text-typography-primary-text [&_svg]:text-typography-primary-text";
|
|
175
|
+
iconWrap.setAttribute("aria-hidden", "true");
|
|
176
|
+
iconWrap.innerHTML = CLOCK_SVG;
|
|
177
|
+
trigger.appendChild(iconWrap);
|
|
178
|
+
|
|
179
|
+
var hourOptions = [];
|
|
180
|
+
if (use24Hour) {
|
|
181
|
+
for (var h = 0; h <= 23; h++) {
|
|
182
|
+
hourOptions.push({ value: h, label: pad(h) });
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
for (var h = 1; h <= 12; h++) {
|
|
186
|
+
hourOptions.push({ value: h, label: pad(h) });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
var minuteOptions = [];
|
|
190
|
+
for (var m = 0; m <= 59; m++) {
|
|
191
|
+
minuteOptions.push({ value: m, label: pad(m) });
|
|
192
|
+
}
|
|
193
|
+
var periodOptions = [{ value: "AM", label: "AM" }, { value: "PM", label: "PM" }];
|
|
194
|
+
|
|
195
|
+
var hourColVal = use24Hour ? hour : hour12;
|
|
196
|
+
var hourColumn = createColumn(hourOptions, hourColVal, function (v) {
|
|
197
|
+
if (use24Hour) {
|
|
198
|
+
hour = v;
|
|
199
|
+
} else {
|
|
200
|
+
hour12 = v;
|
|
201
|
+
hour = period === "PM" ? (v === 12 ? 12 : v + 12) : (v === 12 ? 0 : v);
|
|
202
|
+
}
|
|
203
|
+
hourColumn.setSelected(use24Hour ? hour : hour12);
|
|
204
|
+
hourColumn.scrollToSelected();
|
|
205
|
+
value = formatValue(hour, minute);
|
|
206
|
+
triggerText.textContent = formatDisplay(hour, minute, use24Hour);
|
|
207
|
+
triggerText.className =
|
|
208
|
+
"flex-1 truncate text-left tabular-nums " +
|
|
209
|
+
(value ? "text-inherit" : "text-typography-quaternary-text");
|
|
210
|
+
trigger.className = triggerClasses(disabled, !!value, size);
|
|
211
|
+
if (onChange) onChange(value);
|
|
212
|
+
}, true);
|
|
213
|
+
var minuteColumn = createColumn(minuteOptions, minute, function (v) {
|
|
214
|
+
minute = v;
|
|
215
|
+
minuteColumn.setSelected(minute);
|
|
216
|
+
minuteColumn.scrollToSelected();
|
|
217
|
+
value = formatValue(hour, minute);
|
|
218
|
+
triggerText.textContent = formatDisplay(hour, minute, use24Hour);
|
|
219
|
+
triggerText.className =
|
|
220
|
+
"flex-1 truncate text-left tabular-nums " +
|
|
221
|
+
(value ? "text-inherit" : "text-typography-quaternary-text");
|
|
222
|
+
trigger.className = triggerClasses(disabled, !!value, size);
|
|
223
|
+
if (onChange) onChange(value);
|
|
224
|
+
}, true);
|
|
225
|
+
var periodColumn = null;
|
|
226
|
+
if (!use24Hour) {
|
|
227
|
+
periodColumn = createColumn(periodOptions, period, function (v) {
|
|
228
|
+
period = v;
|
|
229
|
+
hour = period === "PM" ? (hour12 === 12 ? 12 : hour12 + 12) : (hour12 === 12 ? 0 : hour12);
|
|
230
|
+
periodColumn.setSelected(period);
|
|
231
|
+
periodColumn.scrollToSelected();
|
|
232
|
+
value = formatValue(hour, minute);
|
|
233
|
+
triggerText.textContent = formatDisplay(hour, minute, use24Hour);
|
|
234
|
+
triggerText.className =
|
|
235
|
+
"flex-1 truncate text-left tabular-nums " +
|
|
236
|
+
(value ? "text-inherit" : "text-typography-quaternary-text");
|
|
237
|
+
trigger.className = triggerClasses(disabled, !!value, size);
|
|
238
|
+
if (onChange) onChange(value);
|
|
239
|
+
}, false);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
var columnsRow = document.createElement("div");
|
|
243
|
+
columnsRow.className = "flex flex-row flex-nowrap items-stretch gap-0";
|
|
244
|
+
// Condition-based borders: only add separator between columns (not before first / after last)
|
|
245
|
+
hourColumn.el.className += " border-r-1/2 border-border-primary";
|
|
246
|
+
minuteColumn.el.className += " border-l-1/2 border-border-primary";
|
|
247
|
+
if (periodColumn) {
|
|
248
|
+
minuteColumn.el.className += " border-r-1/2 border-border-primary";
|
|
249
|
+
periodColumn.el.className += " border-l-1/2 border-border-primary";
|
|
250
|
+
}
|
|
251
|
+
columnsRow.appendChild(hourColumn.el);
|
|
252
|
+
columnsRow.appendChild(minuteColumn.el);
|
|
253
|
+
if (periodColumn) columnsRow.appendChild(periodColumn.el);
|
|
254
|
+
|
|
255
|
+
var hidePopover = null;
|
|
256
|
+
var ButtonForClose = getDep("Button");
|
|
257
|
+
var closeBtn = ButtonForClose && typeof ButtonForClose.create === "function"
|
|
258
|
+
? ButtonForClose.create({
|
|
259
|
+
variant: "outline",
|
|
260
|
+
size: "small",
|
|
261
|
+
text: "Close",
|
|
262
|
+
onClick: function () {
|
|
263
|
+
if (hidePopover) hidePopover();
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
: (function () {
|
|
267
|
+
var b = document.createElement("button");
|
|
268
|
+
b.type = "button";
|
|
269
|
+
b.className = "px-2 py-1 border rounded text-sm";
|
|
270
|
+
b.textContent = "Close";
|
|
271
|
+
b.onclick = function () { if (hidePopover) hidePopover(); };
|
|
272
|
+
return b;
|
|
273
|
+
})();
|
|
274
|
+
|
|
275
|
+
var closeRow = document.createElement("div");
|
|
276
|
+
closeRow.className =
|
|
277
|
+
"flex justify-end border-t-1/2 border-border-primary pt-6 px-8 pb-12";
|
|
278
|
+
closeRow.appendChild(closeBtn);
|
|
279
|
+
|
|
280
|
+
var popoverContent = document.createElement("div");
|
|
281
|
+
popoverContent.setAttribute("role", "listbox");
|
|
282
|
+
popoverContent.setAttribute("data-time-picker-dropdown", "true");
|
|
283
|
+
popoverContent.appendChild(columnsRow);
|
|
284
|
+
popoverContent.appendChild(closeRow);
|
|
285
|
+
|
|
286
|
+
function syncColumnsAndScroll() {
|
|
287
|
+
if (disabled) return;
|
|
288
|
+
hourColumn.setSelected(use24Hour ? hour : hour12);
|
|
289
|
+
minuteColumn.setSelected(minute);
|
|
290
|
+
if (periodColumn) {
|
|
291
|
+
period = hour >= 12 ? "PM" : "AM";
|
|
292
|
+
periodColumn.setSelected(period);
|
|
293
|
+
}
|
|
294
|
+
hourColumn.scrollToSelected();
|
|
295
|
+
minuteColumn.scrollToSelected();
|
|
296
|
+
if (periodColumn) periodColumn.scrollToSelected();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
var Popover = getDep("Popover");
|
|
300
|
+
if (!Popover || typeof Popover.create !== "function") {
|
|
301
|
+
container.appendChild(trigger);
|
|
302
|
+
container.updateValue = function (newVal) { value = typeof newVal === "string" ? newVal : ""; };
|
|
303
|
+
container.setDisabled = function (isDisabled) { trigger.disabled = !!isDisabled; };
|
|
304
|
+
return container;
|
|
305
|
+
}
|
|
306
|
+
var popover = Popover.create({
|
|
307
|
+
trigger: trigger,
|
|
308
|
+
content: popoverContent,
|
|
309
|
+
placement: "bottom",
|
|
310
|
+
closeOnClickOutside: true,
|
|
311
|
+
bodyClassName: "p-0",
|
|
312
|
+
panelClassName: "!w-auto min-w-max",
|
|
313
|
+
onOpen: function () {
|
|
314
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
315
|
+
container.classList.add("open");
|
|
316
|
+
syncColumnsAndScroll();
|
|
317
|
+
},
|
|
318
|
+
onClose: function () {
|
|
319
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
320
|
+
container.classList.remove("open");
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
hidePopover = popover.hide;
|
|
324
|
+
|
|
325
|
+
container.appendChild(trigger);
|
|
326
|
+
|
|
327
|
+
container.updateValue = function (newVal) {
|
|
328
|
+
value = typeof newVal === "string" ? newVal : "";
|
|
329
|
+
var p = parseValue(value);
|
|
330
|
+
hour = p.hour;
|
|
331
|
+
minute = p.minute;
|
|
332
|
+
hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
|
333
|
+
period = hour >= 12 ? "PM" : "AM";
|
|
334
|
+
triggerText.textContent = value ? formatDisplay(hour, minute, use24Hour) : placeholder;
|
|
335
|
+
triggerText.className =
|
|
336
|
+
"flex-1 truncate text-left tabular-nums " +
|
|
337
|
+
(value ? "text-inherit" : "text-typography-quaternary-text");
|
|
338
|
+
trigger.className = triggerClasses(disabled, !!value, size);
|
|
339
|
+
hourColumn.setSelected(use24Hour ? hour : hour12);
|
|
340
|
+
minuteColumn.setSelected(minute);
|
|
341
|
+
if (periodColumn) periodColumn.setSelected(period);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
container.setDisabled = function (isDisabled) {
|
|
345
|
+
disabled = !!isDisabled;
|
|
346
|
+
trigger.disabled = disabled;
|
|
347
|
+
trigger.className = triggerClasses(disabled, !!value, size);
|
|
348
|
+
if (disabled) popover.hide();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return container;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
global.TimePicker = {
|
|
355
|
+
create: create,
|
|
356
|
+
};
|
|
357
|
+
})(typeof window !== "undefined" ? window : this);
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast Component (vanilla JS, Tailwind)
|
|
3
|
+
* Toast notification system with multiple variants.
|
|
4
|
+
* Adapted from Radix UI Toast primitives.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function (global) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
// SVG Icons
|
|
11
|
+
const ICONS = {
|
|
12
|
+
alertCircle: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
|
|
13
|
+
circleCheck: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`,
|
|
14
|
+
infoCircle: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`,
|
|
15
|
+
loader: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>`,
|
|
16
|
+
playstationX: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`,
|
|
17
|
+
x: `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Variant to icon mapping
|
|
21
|
+
const VARIANT_ICONS = {
|
|
22
|
+
loading: "loader",
|
|
23
|
+
default: "alertCircle",
|
|
24
|
+
warning: "alertCircle",
|
|
25
|
+
error: "playstationX",
|
|
26
|
+
success: "circleCheck",
|
|
27
|
+
info: "infoCircle",
|
|
28
|
+
notification: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Toast container singleton
|
|
32
|
+
let toastViewport = null;
|
|
33
|
+
let toastCount = 0;
|
|
34
|
+
let styleInjected = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Inject toast styles into document
|
|
38
|
+
*/
|
|
39
|
+
function injectStyles() {
|
|
40
|
+
if (styleInjected) return;
|
|
41
|
+
|
|
42
|
+
const style = document.createElement("style");
|
|
43
|
+
style.id = "toast-styles";
|
|
44
|
+
style.textContent = `
|
|
45
|
+
@keyframes toast-slide-in {
|
|
46
|
+
from {
|
|
47
|
+
opacity: 0;
|
|
48
|
+
transform: translateY(-100%);
|
|
49
|
+
}
|
|
50
|
+
to {
|
|
51
|
+
opacity: 1;
|
|
52
|
+
transform: translateY(0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
@keyframes toast-slide-out {
|
|
56
|
+
from {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
transform: translateY(0);
|
|
59
|
+
}
|
|
60
|
+
to {
|
|
61
|
+
opacity: 0;
|
|
62
|
+
transform: translateY(-100%);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
@keyframes toast-spin {
|
|
66
|
+
from { transform: rotate(0deg); }
|
|
67
|
+
to { transform: rotate(360deg); }
|
|
68
|
+
}
|
|
69
|
+
.toast-viewport {
|
|
70
|
+
position: fixed;
|
|
71
|
+
left: 50%;
|
|
72
|
+
top: 5%;
|
|
73
|
+
z-index: 100;
|
|
74
|
+
max-height: 100vh;
|
|
75
|
+
width: fit-content;
|
|
76
|
+
transform: translateX(-50%);
|
|
77
|
+
padding: 1rem;
|
|
78
|
+
max-width: 26rem;
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 0.5rem;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
}
|
|
84
|
+
.toast-root {
|
|
85
|
+
pointer-events: auto;
|
|
86
|
+
width: 100%;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
position: relative;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.5rem;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
padding: 0.75rem 1rem;
|
|
94
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
95
|
+
animation: toast-slide-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
96
|
+
}
|
|
97
|
+
.toast-root[data-state="closed"] {
|
|
98
|
+
animation: toast-slide-out 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
99
|
+
}
|
|
100
|
+
.toast-icon-spin {
|
|
101
|
+
animation: toast-spin 1s linear infinite;
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
document.head.appendChild(style);
|
|
105
|
+
styleInjected = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initialize or get the toast viewport container
|
|
110
|
+
*/
|
|
111
|
+
function initToastViewport() {
|
|
112
|
+
if (toastViewport && document.body.contains(toastViewport)) {
|
|
113
|
+
return toastViewport;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
injectStyles();
|
|
117
|
+
|
|
118
|
+
toastViewport = document.createElement("div");
|
|
119
|
+
toastViewport.className = "toast-viewport";
|
|
120
|
+
toastViewport.setAttribute("role", "region");
|
|
121
|
+
toastViewport.setAttribute("aria-label", "Notifications");
|
|
122
|
+
document.body.appendChild(toastViewport);
|
|
123
|
+
|
|
124
|
+
return toastViewport;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get variant-specific Tailwind classes
|
|
129
|
+
*/
|
|
130
|
+
function getVariantClasses(variant) {
|
|
131
|
+
const variants = {
|
|
132
|
+
loading: "bg-fill-tertiary-fill-light-gray border-borderColor-border-primary text-typography-primary-text",
|
|
133
|
+
default: "bg-fill-quarternary-fill-white border-borderColor-border-primary text-typography-primary-text",
|
|
134
|
+
warning: "bg-warning-surface border-warning-border text-warning-text-base",
|
|
135
|
+
error: "bg-error-surface border-error-border text-error-text-base",
|
|
136
|
+
success: "bg-success-surface border-success-border text-success-text-base",
|
|
137
|
+
info: "bg-info-surface border-info-border text-info-text-base",
|
|
138
|
+
notification: "bg-fill-quarternary-fill-white border-borderColor-border-primary text-typography-primary-text",
|
|
139
|
+
};
|
|
140
|
+
return variants[variant] || variants.default;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a toast notification
|
|
145
|
+
* @param {Object} config - Configuration object
|
|
146
|
+
* @param {string} config.title - Toast title (optional)
|
|
147
|
+
* @param {string} config.description - Toast description/message
|
|
148
|
+
* @param {string} [config.variant='default'] - 'loading' | 'default' | 'warning' | 'error' | 'success' | 'info' | 'notification'
|
|
149
|
+
* @param {number} [config.duration=4000] - Duration in ms (0 for persistent)
|
|
150
|
+
* @param {Function} [config.onClose] - Called when toast closes
|
|
151
|
+
* @param {boolean} [config.showClose=true] - Show close button
|
|
152
|
+
* @returns {Object} Toast API {close, element}
|
|
153
|
+
*/
|
|
154
|
+
function create(config = {}) {
|
|
155
|
+
const {
|
|
156
|
+
title = "",
|
|
157
|
+
description = "",
|
|
158
|
+
variant = "default",
|
|
159
|
+
duration = 4000,
|
|
160
|
+
onClose = null,
|
|
161
|
+
showClose = true,
|
|
162
|
+
} = config;
|
|
163
|
+
|
|
164
|
+
if (!description && !title) {
|
|
165
|
+
console.warn("[Toast] No title or description provided");
|
|
166
|
+
return { close: noop, element: null };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const viewport = initToastViewport();
|
|
170
|
+
const id = ++toastCount;
|
|
171
|
+
const variantClasses = getVariantClasses(variant);
|
|
172
|
+
|
|
173
|
+
// Create toast root element
|
|
174
|
+
const toast = document.createElement("div");
|
|
175
|
+
toast.className = `toast-root border-1/2 ${variantClasses}`;
|
|
176
|
+
toast.setAttribute("data-toast-id", id);
|
|
177
|
+
toast.setAttribute("data-state", "closed");
|
|
178
|
+
toast.setAttribute("role", "status");
|
|
179
|
+
toast.setAttribute("aria-live", "polite");
|
|
180
|
+
toast.setAttribute("aria-atomic", "true");
|
|
181
|
+
|
|
182
|
+
// Create content wrapper
|
|
183
|
+
const contentWrapper = document.createElement("div");
|
|
184
|
+
contentWrapper.className = "flex w-full items-center justify-start gap-8";
|
|
185
|
+
|
|
186
|
+
// Add icon if variant has one
|
|
187
|
+
const iconKey = VARIANT_ICONS[variant];
|
|
188
|
+
if (iconKey) {
|
|
189
|
+
const iconWrapper = document.createElement("div");
|
|
190
|
+
iconWrapper.className = `flex items-center justify-center ${variant === "loading" ? "toast-icon-spin" : ""}`;
|
|
191
|
+
iconWrapper.style.cssText = "width: 16px; height: 16px; flex-shrink: 0;";
|
|
192
|
+
iconWrapper.innerHTML = ICONS[iconKey];
|
|
193
|
+
contentWrapper.appendChild(iconWrapper);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Create text content container
|
|
197
|
+
const textContainer = document.createElement("div");
|
|
198
|
+
textContainer.className = "flex-1 text-reg-12";
|
|
199
|
+
|
|
200
|
+
if (title) {
|
|
201
|
+
const titleEl = document.createElement("div");
|
|
202
|
+
titleEl.className = "text-med-12 mb-4";
|
|
203
|
+
titleEl.textContent = title;
|
|
204
|
+
textContainer.appendChild(titleEl);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (description) {
|
|
208
|
+
const descEl = document.createElement("div");
|
|
209
|
+
descEl.className = "text-reg-12";
|
|
210
|
+
descEl.textContent = description;
|
|
211
|
+
textContainer.appendChild(descEl);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
contentWrapper.appendChild(textContainer);
|
|
215
|
+
|
|
216
|
+
// Add close button
|
|
217
|
+
if (showClose) {
|
|
218
|
+
const closeBtn = document.createElement("button");
|
|
219
|
+
closeBtn.type = "button";
|
|
220
|
+
closeBtn.className = "ml-auto flex items-center justify-center rounded-4 p-4 transition-opacity hover:opacity-100 focus:opacity-100 focus:outline-none";
|
|
221
|
+
closeBtn.style.cssText = "flex-shrink: 0; width: 16px; height: 16px; border: none; background: transparent; opacity: 0.6; cursor: pointer;";
|
|
222
|
+
closeBtn.innerHTML = ICONS.x;
|
|
223
|
+
closeBtn.addEventListener("click", close);
|
|
224
|
+
contentWrapper.appendChild(closeBtn);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
toast.appendChild(contentWrapper);
|
|
228
|
+
|
|
229
|
+
function noop() {}
|
|
230
|
+
|
|
231
|
+
function close() {
|
|
232
|
+
toast.setAttribute("data-state", "closed");
|
|
233
|
+
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
if (toast.parentElement) {
|
|
236
|
+
toast.parentElement.removeChild(toast);
|
|
237
|
+
}
|
|
238
|
+
// Clean up viewport if empty
|
|
239
|
+
if (toastViewport && toastViewport.children.length === 0) {
|
|
240
|
+
toastViewport.remove();
|
|
241
|
+
toastViewport = null;
|
|
242
|
+
}
|
|
243
|
+
if (onClose) onClose();
|
|
244
|
+
}, 300); // Match animation duration
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Append and animate in
|
|
248
|
+
viewport.appendChild(toast);
|
|
249
|
+
|
|
250
|
+
// Trigger animation
|
|
251
|
+
requestAnimationFrame(() => {
|
|
252
|
+
toast.setAttribute("data-state", "open");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Auto-dismiss if duration is set
|
|
256
|
+
if (duration > 0) {
|
|
257
|
+
setTimeout(close, duration);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
close,
|
|
262
|
+
element: toast,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Show a simple toast with message and type
|
|
268
|
+
* Convenience method compatible with the old showAlert API
|
|
269
|
+
* @param {string} message - Toast message
|
|
270
|
+
* @param {string} [type='info'] - Toast type: 'success' | 'error' | 'warning' | 'info' | 'loading'
|
|
271
|
+
* @param {number} [duration=4000] - Duration in ms (0 for persistent)
|
|
272
|
+
* @returns {Object} Toast API {close, element}
|
|
273
|
+
*/
|
|
274
|
+
function show(message, type = "info", duration = 4000) {
|
|
275
|
+
console.log("[Toast] Showing toast:", message, "Type:", type);
|
|
276
|
+
return create({
|
|
277
|
+
description: message,
|
|
278
|
+
variant: type,
|
|
279
|
+
duration: duration,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Show a success toast
|
|
285
|
+
* @param {string} message - Toast message
|
|
286
|
+
* @param {number} [duration=4000] - Duration in ms
|
|
287
|
+
* @returns {Object} Toast API {close, element}
|
|
288
|
+
*/
|
|
289
|
+
function success(message, duration = 4000) {
|
|
290
|
+
return show(message, "success", duration);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Show an error toast
|
|
295
|
+
* @param {string} message - Toast message
|
|
296
|
+
* @param {number} [duration=4000] - Duration in ms
|
|
297
|
+
* @returns {Object} Toast API {close, element}
|
|
298
|
+
*/
|
|
299
|
+
function error(message, duration = 4000) {
|
|
300
|
+
return show(message, "error", duration);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Show a warning toast
|
|
305
|
+
* @param {string} message - Toast message
|
|
306
|
+
* @param {number} [duration=4000] - Duration in ms
|
|
307
|
+
* @returns {Object} Toast API {close, element}
|
|
308
|
+
*/
|
|
309
|
+
function warning(message, duration = 4000) {
|
|
310
|
+
return show(message, "warning", duration);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Show an info toast
|
|
315
|
+
* @param {string} message - Toast message
|
|
316
|
+
* @param {number} [duration=4000] - Duration in ms
|
|
317
|
+
* @returns {Object} Toast API {close, element}
|
|
318
|
+
*/
|
|
319
|
+
function info(message, duration = 4000) {
|
|
320
|
+
return show(message, "info", duration);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Show a loading toast
|
|
325
|
+
* @param {string} message - Toast message
|
|
326
|
+
* @param {number} [duration=0] - Duration in ms (0 for persistent by default)
|
|
327
|
+
* @returns {Object} Toast API {close, element}
|
|
328
|
+
*/
|
|
329
|
+
function loading(message, duration = 0) {
|
|
330
|
+
return show(message, "loading", duration);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Export to global namespace
|
|
334
|
+
global.Toast = {
|
|
335
|
+
create,
|
|
336
|
+
show,
|
|
337
|
+
success,
|
|
338
|
+
error,
|
|
339
|
+
warning,
|
|
340
|
+
info,
|
|
341
|
+
loading,
|
|
342
|
+
};
|
|
343
|
+
})(typeof window !== "undefined" ? window : this);
|