sibujs 1.0.7 → 1.0.8

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,937 @@
1
+ import {
2
+ bindAttribute
3
+ } from "./chunk-SDLZDHKP.js";
4
+ import {
5
+ derived
6
+ } from "./chunk-FGOEVHY3.js";
7
+ import {
8
+ effect
9
+ } from "./chunk-PZEGYCF5.js";
10
+ import {
11
+ signal,
12
+ track
13
+ } from "./chunk-YECR7UIA.js";
14
+
15
+ // src/ui/form.ts
16
+ function required(message = "This field is required") {
17
+ return (value) => {
18
+ if (value == null || value === "" || Array.isArray(value) && value.length === 0) {
19
+ return message;
20
+ }
21
+ return null;
22
+ };
23
+ }
24
+ function minLength(min2, message) {
25
+ return (value) => {
26
+ if (value && value.length < min2) {
27
+ return message || `Must be at least ${min2} characters`;
28
+ }
29
+ return null;
30
+ };
31
+ }
32
+ function maxLength(max2, message) {
33
+ return (value) => {
34
+ if (value && value.length > max2) {
35
+ return message || `Must be at most ${max2} characters`;
36
+ }
37
+ return null;
38
+ };
39
+ }
40
+ function matchesPattern(regex, message = "Invalid format") {
41
+ return (value) => {
42
+ if (value && !regex.test(value)) {
43
+ return message;
44
+ }
45
+ return null;
46
+ };
47
+ }
48
+ function email(message = "Invalid email address") {
49
+ return matchesPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, message);
50
+ }
51
+ function min(minVal, message) {
52
+ return (value) => {
53
+ if (value != null && value < minVal) {
54
+ return message || `Must be at least ${minVal}`;
55
+ }
56
+ return null;
57
+ };
58
+ }
59
+ function max(maxVal, message) {
60
+ return (value) => {
61
+ if (value != null && value > maxVal) {
62
+ return message || `Must be at most ${maxVal}`;
63
+ }
64
+ return null;
65
+ };
66
+ }
67
+ function custom(fn, message) {
68
+ return (value) => fn(value) ? null : message;
69
+ }
70
+ function bindField(field, extras) {
71
+ const fieldOn = {
72
+ input: (e) => {
73
+ const target = e.target;
74
+ if (target.type === "checkbox") {
75
+ field.set(target.checked);
76
+ } else {
77
+ field.set(target.value);
78
+ }
79
+ },
80
+ change: (e) => {
81
+ const target = e.target;
82
+ if ("checked" in target && target.type === "checkbox") {
83
+ field.set(target.checked);
84
+ } else {
85
+ field.set(target.value);
86
+ }
87
+ },
88
+ blur: () => field.touch()
89
+ };
90
+ const { on: extraOn, value: _ignoreValue, ...restExtras } = extras ?? {};
91
+ const mergedOn = extraOn && typeof extraOn === "object" ? { ...fieldOn, ...extraOn } : fieldOn;
92
+ return {
93
+ value: field.value,
94
+ on: mergedOn,
95
+ ...restExtras
96
+ };
97
+ }
98
+ function form(config) {
99
+ const fieldEntries = Object.entries(config);
100
+ const fieldMap = {};
101
+ const [manualErrors, setManualErrors] = signal({});
102
+ for (const [name, cfg] of fieldEntries) {
103
+ const [value, setValue] = signal(cfg.initial);
104
+ const [isTouched, setTouched] = signal(false);
105
+ const error = derived(() => {
106
+ const manual = manualErrors();
107
+ if (manual[name]) return manual[name];
108
+ const val = value();
109
+ if (!cfg.validators) return null;
110
+ for (const validator of cfg.validators) {
111
+ const msg = validator(val);
112
+ if (msg) return msg;
113
+ }
114
+ return null;
115
+ });
116
+ fieldMap[name] = {
117
+ value,
118
+ set: setValue,
119
+ error,
120
+ touched: isTouched,
121
+ touch: () => setTouched(true),
122
+ reset: () => {
123
+ setValue(cfg.initial);
124
+ setTouched(false);
125
+ setManualErrors((prev) => ({ ...prev, [name]: null }));
126
+ }
127
+ };
128
+ }
129
+ const errors = derived(() => {
130
+ const result = {};
131
+ for (const [name, field] of Object.entries(fieldMap)) {
132
+ result[name] = field.error();
133
+ }
134
+ return result;
135
+ });
136
+ const isValid = derived(() => {
137
+ for (const field of Object.values(fieldMap)) {
138
+ if (field.error() != null) return false;
139
+ }
140
+ return true;
141
+ });
142
+ const isDirty = derived(() => {
143
+ for (const [name, cfg] of fieldEntries) {
144
+ if (!Object.is(fieldMap[name].value(), cfg.initial)) return true;
145
+ }
146
+ return false;
147
+ });
148
+ const touchedState = derived(() => {
149
+ const result = {};
150
+ for (const [name, field] of Object.entries(fieldMap)) {
151
+ result[name] = field.touched();
152
+ }
153
+ return result;
154
+ });
155
+ const values = derived(() => {
156
+ const result = {};
157
+ for (const [name, field] of Object.entries(fieldMap)) {
158
+ result[name] = field.value();
159
+ }
160
+ return result;
161
+ });
162
+ function handleSubmit(onSubmit) {
163
+ return (e) => {
164
+ if (e) e.preventDefault();
165
+ for (const field of Object.values(fieldMap)) {
166
+ field.touch();
167
+ }
168
+ if (isValid()) {
169
+ onSubmit(values());
170
+ }
171
+ };
172
+ }
173
+ function reset() {
174
+ for (const field of Object.values(fieldMap)) {
175
+ field.reset();
176
+ }
177
+ }
178
+ function setError(field, message) {
179
+ setManualErrors((prev) => ({ ...prev, [field]: message }));
180
+ }
181
+ return {
182
+ fields: fieldMap,
183
+ errors,
184
+ isValid,
185
+ isDirty,
186
+ touched: touchedState,
187
+ values,
188
+ handleSubmit,
189
+ reset,
190
+ setError
191
+ };
192
+ }
193
+
194
+ // src/ui/virtualList.ts
195
+ function VirtualList(props) {
196
+ const overscan = props.overscan ?? 3;
197
+ const [scrollTop, setScrollTop] = signal(0);
198
+ const container = document.createElement("div");
199
+ container.style.overflow = "auto";
200
+ container.style.height = `${props.containerHeight}px`;
201
+ container.style.position = "relative";
202
+ if (props.class) container.className = props.class;
203
+ const spacer = document.createElement("div");
204
+ spacer.style.position = "relative";
205
+ const content = document.createElement("div");
206
+ content.style.position = "absolute";
207
+ content.style.left = "0";
208
+ content.style.right = "0";
209
+ spacer.appendChild(content);
210
+ container.appendChild(spacer);
211
+ container.addEventListener("scroll", () => {
212
+ setScrollTop(container.scrollTop);
213
+ });
214
+ const update = () => {
215
+ const items = props.items();
216
+ const totalHeight = items.length * props.itemHeight;
217
+ spacer.style.height = `${totalHeight}px`;
218
+ const currentScroll = scrollTop();
219
+ const startIndex = Math.max(0, Math.floor(currentScroll / props.itemHeight) - overscan);
220
+ const visibleCount = Math.ceil(props.containerHeight / props.itemHeight) + 2 * overscan;
221
+ const endIndex = Math.min(items.length, startIndex + visibleCount);
222
+ content.style.top = `${startIndex * props.itemHeight}px`;
223
+ content.innerHTML = "";
224
+ for (let i = startIndex; i < endIndex; i++) {
225
+ const itemEl = props.renderItem(items[i], i);
226
+ itemEl.style.height = `${props.itemHeight}px`;
227
+ itemEl.style.boxSizing = "border-box";
228
+ content.appendChild(itemEl);
229
+ }
230
+ };
231
+ effect(update);
232
+ return container;
233
+ }
234
+
235
+ // src/ui/intersection.ts
236
+ function intersection(options) {
237
+ const [isIntersecting, setIsIntersecting] = signal(false);
238
+ const [ratio, setRatio] = signal(0);
239
+ let observer = null;
240
+ let currentElement = null;
241
+ function observe(element) {
242
+ unobserve();
243
+ currentElement = element;
244
+ observer = new IntersectionObserver((entries) => {
245
+ const entry = entries[0];
246
+ if (entry) {
247
+ setIsIntersecting(entry.isIntersecting);
248
+ setRatio(entry.intersectionRatio);
249
+ }
250
+ }, options);
251
+ observer.observe(element);
252
+ }
253
+ function unobserve() {
254
+ if (observer) {
255
+ if (currentElement) observer.unobserve(currentElement);
256
+ observer.disconnect();
257
+ observer = null;
258
+ currentElement = null;
259
+ }
260
+ }
261
+ return {
262
+ isIntersecting,
263
+ intersectionRatio: ratio,
264
+ observe,
265
+ unobserve
266
+ };
267
+ }
268
+ function lazyLoad(element, loader, options) {
269
+ const observer = new IntersectionObserver((entries) => {
270
+ for (const entry of entries) {
271
+ if (entry.isIntersecting) {
272
+ loader();
273
+ observer.disconnect();
274
+ break;
275
+ }
276
+ }
277
+ }, options);
278
+ observer.observe(element);
279
+ return () => observer.disconnect();
280
+ }
281
+
282
+ // src/ui/inputMask.ts
283
+ function inputMask(options) {
284
+ const placeholder = options.placeholder || "_";
285
+ const [value, setValue] = signal("");
286
+ const [rawValue, setRawValue] = signal("");
287
+ function applyMask(raw) {
288
+ let result = "";
289
+ let rawIndex = 0;
290
+ for (let i = 0; i < options.pattern.length && rawIndex < raw.length; i++) {
291
+ const maskChar = options.pattern[i];
292
+ if (maskChar === "9") {
293
+ while (rawIndex < raw.length && !/\d/.test(raw[rawIndex])) rawIndex++;
294
+ if (rawIndex < raw.length) {
295
+ result += raw[rawIndex++];
296
+ }
297
+ } else if (maskChar === "A") {
298
+ while (rawIndex < raw.length && !/[a-zA-Z]/.test(raw[rawIndex])) rawIndex++;
299
+ if (rawIndex < raw.length) {
300
+ result += raw[rawIndex++];
301
+ }
302
+ } else if (maskChar === "*") {
303
+ result += raw[rawIndex++];
304
+ } else {
305
+ result += maskChar;
306
+ if (raw[rawIndex] === maskChar) rawIndex++;
307
+ }
308
+ }
309
+ return result;
310
+ }
311
+ function extractRaw(masked) {
312
+ let raw = "";
313
+ for (let i = 0; i < masked.length && i < options.pattern.length; i++) {
314
+ const maskChar = options.pattern[i];
315
+ if (maskChar === "9" || maskChar === "A" || maskChar === "*") {
316
+ raw += masked[i];
317
+ }
318
+ }
319
+ return raw;
320
+ }
321
+ function bind(input) {
322
+ input.addEventListener("input", () => {
323
+ const raw = input.value.replace(/[^a-zA-Z0-9]/g, "");
324
+ const masked = applyMask(raw);
325
+ setValue(masked);
326
+ setRawValue(extractRaw(masked));
327
+ input.value = masked;
328
+ });
329
+ input.addEventListener("focus", () => {
330
+ if (!input.value) {
331
+ const display = options.pattern.replace(/9/g, placeholder).replace(/A/g, placeholder).replace(/\*/g, placeholder);
332
+ input.placeholder = display;
333
+ }
334
+ });
335
+ }
336
+ return { value, rawValue, bind };
337
+ }
338
+ function phoneMask() {
339
+ return { pattern: "(999) 999-9999" };
340
+ }
341
+ function dateMask() {
342
+ return { pattern: "99/99/9999" };
343
+ }
344
+ function creditCardMask() {
345
+ return { pattern: "9999 9999 9999 9999" };
346
+ }
347
+ function timeMask() {
348
+ return { pattern: "99:99" };
349
+ }
350
+ function ssnMask() {
351
+ return { pattern: "999-99-9999" };
352
+ }
353
+ function zipMask() {
354
+ return { pattern: "99999" };
355
+ }
356
+
357
+ // src/ui/a11y.ts
358
+ function aria(element, attrs) {
359
+ for (const [key, value] of Object.entries(attrs)) {
360
+ const ariaKey = key.startsWith("aria-") ? key : `aria-${key}`;
361
+ if (typeof value === "function") {
362
+ const getter = value;
363
+ track(() => {
364
+ element.setAttribute(ariaKey, String(getter()));
365
+ });
366
+ } else {
367
+ element.setAttribute(ariaKey, String(value));
368
+ }
369
+ }
370
+ }
371
+ function focus() {
372
+ const [isFocused, setIsFocused] = signal(false);
373
+ let currentElement = null;
374
+ function bind(element) {
375
+ currentElement = element;
376
+ element.addEventListener("focus", () => setIsFocused(true));
377
+ element.addEventListener("blur", () => setIsFocused(false));
378
+ }
379
+ function focus2() {
380
+ currentElement?.focus();
381
+ }
382
+ function blur() {
383
+ currentElement?.blur();
384
+ }
385
+ return { isFocused, focus: focus2, blur, bind };
386
+ }
387
+ function FocusTrap(nodes, options = {}) {
388
+ const container = document.createElement("div");
389
+ container.setAttribute("data-sibu-focus-trap", "true");
390
+ container.appendChild(nodes);
391
+ const previouslyFocused = document.activeElement;
392
+ container.addEventListener("keydown", (e) => {
393
+ if (e.key !== "Tab") return;
394
+ const focusable = container.querySelectorAll(
395
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
396
+ );
397
+ if (focusable.length === 0) return;
398
+ const first = focusable[0];
399
+ const last = focusable[focusable.length - 1];
400
+ if (e.shiftKey) {
401
+ if (document.activeElement === first) {
402
+ e.preventDefault();
403
+ last.focus();
404
+ }
405
+ } else {
406
+ if (document.activeElement === last) {
407
+ e.preventDefault();
408
+ first.focus();
409
+ }
410
+ }
411
+ });
412
+ if (options.autoFocus !== false) {
413
+ queueMicrotask(() => {
414
+ const first = container.querySelector(
415
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
416
+ );
417
+ first?.focus();
418
+ });
419
+ }
420
+ if (options.restoreFocus !== false) {
421
+ const observer = new MutationObserver((mutations) => {
422
+ for (const mutation of mutations) {
423
+ for (const removed of Array.from(mutation.removedNodes)) {
424
+ if (removed === container || removed.contains(container)) {
425
+ previouslyFocused?.focus();
426
+ observer.disconnect();
427
+ return;
428
+ }
429
+ }
430
+ }
431
+ });
432
+ queueMicrotask(() => {
433
+ if (container.parentNode) {
434
+ observer.observe(container.parentNode, { childList: true });
435
+ }
436
+ });
437
+ }
438
+ return container;
439
+ }
440
+ function hotkey(combo, handler, options = {}) {
441
+ let key = combo;
442
+ let needCtrl = options.ctrl ?? false;
443
+ let needShift = options.shift ?? false;
444
+ let needAlt = options.alt ?? false;
445
+ let needMeta = options.meta ?? false;
446
+ if (combo.includes("+")) {
447
+ const parts = combo.toLowerCase().split("+");
448
+ key = parts[parts.length - 1];
449
+ for (let i = 0; i < parts.length - 1; i++) {
450
+ const mod = parts[i];
451
+ if (mod === "ctrl" || mod === "control") needCtrl = true;
452
+ else if (mod === "shift") needShift = true;
453
+ else if (mod === "alt") needAlt = true;
454
+ else if (mod === "meta" || mod === "cmd" || mod === "command") needMeta = true;
455
+ }
456
+ }
457
+ const listener = (e) => {
458
+ const ke = e;
459
+ if (ke.key.toLowerCase() !== key.toLowerCase()) return;
460
+ if (needCtrl !== ke.ctrlKey) return;
461
+ if (needShift !== ke.shiftKey) return;
462
+ if (needAlt !== ke.altKey) return;
463
+ if (needMeta !== ke.metaKey) return;
464
+ if (options.preventDefault) ke.preventDefault();
465
+ handler(ke);
466
+ };
467
+ document.addEventListener("keydown", listener);
468
+ return () => document.removeEventListener("keydown", listener);
469
+ }
470
+ function announce(message, priority = "polite") {
471
+ let region = document.getElementById(`sibu-announce-${priority}`);
472
+ if (!region) {
473
+ region = document.createElement("div");
474
+ region.id = `sibu-announce-${priority}`;
475
+ region.setAttribute("aria-live", priority);
476
+ region.setAttribute("aria-atomic", "true");
477
+ region.setAttribute("role", priority === "assertive" ? "alert" : "status");
478
+ region.style.cssText = "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;";
479
+ document.body.appendChild(region);
480
+ }
481
+ region.textContent = "";
482
+ requestAnimationFrame(() => {
483
+ if (region) region.textContent = message;
484
+ });
485
+ }
486
+
487
+ // src/ui/scopedStyle.ts
488
+ var scopeCounter = 0;
489
+ function sanitizeCSS(css) {
490
+ let sanitized = css.replace(/@import\s+[^;]+;/gi, "/* @import removed */");
491
+ sanitized = sanitized.replace(/url\s*\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "/* url() removed */");
492
+ sanitized = sanitized.replace(/expression\s*\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "/* expression() removed */");
493
+ sanitized = sanitized.replace(/-moz-binding\s*:[^;]+;/gi, "/* -moz-binding removed */");
494
+ sanitized = sanitized.replace(/behavior\s*:[^;]+;/gi, "/* behavior removed */");
495
+ return sanitized;
496
+ }
497
+ function scopedStyle(css) {
498
+ const id = `sibu-s${scopeCounter++}`;
499
+ const attr = `data-${id}`;
500
+ const safeCss = sanitizeCSS(css);
501
+ const scopedCSS = safeCss.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g, (match, selector, delimiter) => {
502
+ const trimmed = selector.trim();
503
+ if (trimmed.startsWith("@") || trimmed.startsWith("from") || trimmed.startsWith("to") || /^\d+%$/.test(trimmed)) {
504
+ return match;
505
+ }
506
+ return `${trimmed}[${attr}]${delimiter}`;
507
+ });
508
+ if (typeof document !== "undefined") {
509
+ const styleEl = document.createElement("style");
510
+ styleEl.setAttribute("data-sibu-scope", id);
511
+ styleEl.textContent = scopedCSS;
512
+ document.head.appendChild(styleEl);
513
+ }
514
+ return { scope: id, attr };
515
+ }
516
+ function withScopedStyle(css, component) {
517
+ let style = null;
518
+ return (props) => {
519
+ if (!style) {
520
+ style = scopedStyle(css);
521
+ }
522
+ const el = component(props);
523
+ applyScopeRecursive(el, style.attr);
524
+ return el;
525
+ };
526
+ }
527
+ function applyScopeRecursive(element, attr) {
528
+ element.setAttribute(attr, "");
529
+ for (const child of Array.from(element.children)) {
530
+ if (child instanceof HTMLElement) {
531
+ applyScopeRecursive(child, attr);
532
+ }
533
+ }
534
+ }
535
+ function removeScopedStyle(scopeId) {
536
+ const el = document.head.querySelector(`style[data-sibu-scope="${scopeId}"]`);
537
+ if (el) el.remove();
538
+ }
539
+
540
+ // src/ui/reactiveAttr.ts
541
+ function bindAttrs(el, attrs) {
542
+ const teardowns = [];
543
+ for (const [attr, value] of Object.entries(attrs)) {
544
+ if (typeof value === "function") {
545
+ const teardown = bindAttribute(el, attr, value);
546
+ teardowns.push(teardown);
547
+ } else if (typeof value === "boolean") {
548
+ if (value) {
549
+ el.setAttribute(attr, "");
550
+ } else {
551
+ el.removeAttribute(attr);
552
+ }
553
+ } else {
554
+ el.setAttribute(attr, String(value));
555
+ }
556
+ }
557
+ return () => {
558
+ for (const td of teardowns) {
559
+ td();
560
+ }
561
+ };
562
+ }
563
+ function bindBoolAttr(el, attr, getter) {
564
+ if (typeof getter !== "function") {
565
+ if (getter) {
566
+ el.setAttribute(attr, "");
567
+ } else {
568
+ el.removeAttribute(attr);
569
+ }
570
+ return () => {
571
+ };
572
+ }
573
+ const reactiveGetter = getter;
574
+ function commit() {
575
+ let value;
576
+ try {
577
+ value = reactiveGetter();
578
+ } catch {
579
+ return;
580
+ }
581
+ if (value) {
582
+ el.setAttribute(attr, "");
583
+ } else {
584
+ el.removeAttribute(attr);
585
+ }
586
+ }
587
+ const teardown = track(commit);
588
+ return teardown;
589
+ }
590
+ function bindData(el, key, getter) {
591
+ const dataAttr = `data-${key}`;
592
+ if (typeof getter !== "function") {
593
+ el.setAttribute(dataAttr, String(getter));
594
+ return () => {
595
+ };
596
+ }
597
+ return bindAttribute(el, dataAttr, getter);
598
+ }
599
+
600
+ // src/ui/dialog.ts
601
+ function dialog() {
602
+ const [isOpen, setIsOpen] = signal(false);
603
+ let listenerAttached = false;
604
+ function handleKeydown(event) {
605
+ if (event.key === "Escape") {
606
+ close();
607
+ }
608
+ }
609
+ function open() {
610
+ setIsOpen(true);
611
+ if (typeof window !== "undefined" && !listenerAttached) {
612
+ window.addEventListener("keydown", handleKeydown);
613
+ listenerAttached = true;
614
+ }
615
+ }
616
+ function close() {
617
+ setIsOpen(false);
618
+ if (typeof window !== "undefined" && listenerAttached) {
619
+ window.removeEventListener("keydown", handleKeydown);
620
+ listenerAttached = false;
621
+ }
622
+ }
623
+ function toggle() {
624
+ if (isOpen()) {
625
+ close();
626
+ } else {
627
+ open();
628
+ }
629
+ }
630
+ return { open, close, isOpen, toggle };
631
+ }
632
+
633
+ // src/ui/toast.ts
634
+ var toastCounter = 0;
635
+ function toast(options) {
636
+ const duration = options?.duration ?? 3e3;
637
+ const maxToasts = options?.maxToasts ?? Infinity;
638
+ const [toasts, setToasts] = signal([]);
639
+ const timers = /* @__PURE__ */ new Map();
640
+ function show(message, type) {
641
+ const id = `toast-${++toastCounter}`;
642
+ const toast2 = { id, message, type };
643
+ setToasts((prev) => {
644
+ const next = [...prev, toast2];
645
+ if (next.length > maxToasts) {
646
+ const removed = next.splice(0, next.length - maxToasts);
647
+ for (const r of removed) {
648
+ clearTimerForToast(r.id);
649
+ }
650
+ }
651
+ return next;
652
+ });
653
+ if (duration > 0) {
654
+ const timer = setTimeout(() => {
655
+ dismiss(id);
656
+ }, duration);
657
+ timers.set(id, timer);
658
+ }
659
+ return id;
660
+ }
661
+ function dismiss(id) {
662
+ clearTimerForToast(id);
663
+ setToasts((prev) => prev.filter((t) => t.id !== id));
664
+ }
665
+ function dismissAll() {
666
+ for (const timer of timers.values()) {
667
+ clearTimeout(timer);
668
+ }
669
+ timers.clear();
670
+ setToasts([]);
671
+ }
672
+ function clearTimerForToast(id) {
673
+ const timer = timers.get(id);
674
+ if (timer !== void 0) {
675
+ clearTimeout(timer);
676
+ timers.delete(id);
677
+ }
678
+ }
679
+ return {
680
+ toasts,
681
+ show,
682
+ info: (message) => show(message, "info"),
683
+ success: (message) => show(message, "success"),
684
+ error: (message) => show(message, "error"),
685
+ warning: (message) => show(message, "warning"),
686
+ dismiss,
687
+ dismissAll
688
+ };
689
+ }
690
+
691
+ // src/ui/infiniteScroll.ts
692
+ function infiniteScroll(options) {
693
+ const { onLoadMore, hasMore, threshold = 0 } = options;
694
+ const [loading, setLoading] = signal(false);
695
+ const sentinelRef = { current: null };
696
+ let observer = null;
697
+ let disposed = false;
698
+ function createObserver() {
699
+ if (typeof IntersectionObserver === "undefined") return;
700
+ observer = new IntersectionObserver(
701
+ (entries) => {
702
+ const entry = entries[0];
703
+ if (entry?.isIntersecting && !loading() && hasMore() && !disposed) {
704
+ loadMore();
705
+ }
706
+ },
707
+ { threshold }
708
+ );
709
+ if (sentinelRef.current) {
710
+ observer.observe(sentinelRef.current);
711
+ }
712
+ }
713
+ async function loadMore() {
714
+ setLoading(true);
715
+ try {
716
+ await onLoadMore();
717
+ } finally {
718
+ setLoading(false);
719
+ }
720
+ }
721
+ const originalRef = sentinelRef;
722
+ let _current = null;
723
+ Object.defineProperty(originalRef, "current", {
724
+ get() {
725
+ return _current;
726
+ },
727
+ set(el) {
728
+ _current = el;
729
+ if (observer) {
730
+ observer.disconnect();
731
+ observer = null;
732
+ }
733
+ if (el && !disposed) {
734
+ createObserver();
735
+ }
736
+ },
737
+ configurable: true
738
+ });
739
+ function dispose() {
740
+ disposed = true;
741
+ if (observer) {
742
+ observer.disconnect();
743
+ observer = null;
744
+ }
745
+ }
746
+ return { sentinelRef: originalRef, loading, dispose };
747
+ }
748
+
749
+ // src/ui/pagination.ts
750
+ function pagination(options) {
751
+ const pageSizeValue = options.pageSize ?? 10;
752
+ const [page, setPage] = signal(options.initialPage ?? 1);
753
+ const [pageSize] = signal(pageSizeValue);
754
+ const totalPages = derived(() => {
755
+ const total = options.totalItems();
756
+ return Math.max(1, Math.ceil(total / pageSizeValue));
757
+ });
758
+ const startIndex = derived(() => {
759
+ return (page() - 1) * pageSizeValue;
760
+ });
761
+ const endIndex = derived(() => {
762
+ const end = page() * pageSizeValue;
763
+ const total = options.totalItems();
764
+ return Math.min(end, total);
765
+ });
766
+ function next() {
767
+ if (page() < totalPages()) {
768
+ setPage((p) => p + 1);
769
+ }
770
+ }
771
+ function prev() {
772
+ if (page() > 1) {
773
+ setPage((p) => p - 1);
774
+ }
775
+ }
776
+ function goTo(target) {
777
+ const clamped = Math.max(1, Math.min(target, totalPages()));
778
+ setPage(clamped);
779
+ }
780
+ return { page, pageSize, totalPages, next, prev, goTo, startIndex, endIndex };
781
+ }
782
+
783
+ // src/ui/eventBus.ts
784
+ function eventBus() {
785
+ const listeners = /* @__PURE__ */ new Map();
786
+ function on(event, handler) {
787
+ let set = listeners.get(event);
788
+ if (!set) {
789
+ set = /* @__PURE__ */ new Set();
790
+ listeners.set(event, set);
791
+ }
792
+ set.add(handler);
793
+ return () => off(event, handler);
794
+ }
795
+ function emit(event, data) {
796
+ const set = listeners.get(event);
797
+ if (set) {
798
+ for (const handler of set) {
799
+ handler(data);
800
+ }
801
+ }
802
+ }
803
+ function off(event, handler) {
804
+ const set = listeners.get(event);
805
+ if (set) {
806
+ set.delete(handler);
807
+ if (set.size === 0) {
808
+ listeners.delete(event);
809
+ }
810
+ }
811
+ }
812
+ function clear() {
813
+ listeners.clear();
814
+ }
815
+ return { on, emit, off, clear };
816
+ }
817
+
818
+ // src/platform/customElement.ts
819
+ function defineElement(name, component, options = {}) {
820
+ if (customElements.get(name)) return;
821
+ const observed = options.observedAttributes || [];
822
+ class SibuElement extends HTMLElement {
823
+ constructor() {
824
+ super();
825
+ this._rendered = false;
826
+ if (options.shadow !== false) {
827
+ this._root = this.attachShadow({ mode: options.mode || "open" });
828
+ } else {
829
+ this._root = this;
830
+ }
831
+ }
832
+ static get observedAttributes() {
833
+ return observed;
834
+ }
835
+ connectedCallback() {
836
+ this._render();
837
+ }
838
+ disconnectedCallback() {
839
+ if (this._root instanceof ShadowRoot) {
840
+ this._root.innerHTML = "";
841
+ }
842
+ this._rendered = false;
843
+ }
844
+ attributeChangedCallback() {
845
+ if (this._rendered) {
846
+ this._render();
847
+ }
848
+ }
849
+ _render() {
850
+ const props = this._getProps();
851
+ if (this._root instanceof ShadowRoot) {
852
+ this._root.innerHTML = "";
853
+ } else {
854
+ while (this._root.firstChild) {
855
+ this._root.removeChild(this._root.firstChild);
856
+ }
857
+ }
858
+ if (options.styles && this._root instanceof ShadowRoot) {
859
+ const styleEl = document.createElement("style");
860
+ styleEl.textContent = options.styles;
861
+ this._root.appendChild(styleEl);
862
+ }
863
+ const el = component(props, this);
864
+ this._root.appendChild(el);
865
+ this._rendered = true;
866
+ }
867
+ _getProps() {
868
+ const props = {};
869
+ for (const attr of this.attributes) {
870
+ props[attr.name] = attr.value;
871
+ }
872
+ return props;
873
+ }
874
+ }
875
+ customElements.define(name, SibuElement);
876
+ }
877
+ function svgElement(tag, props = {}, ...nodes) {
878
+ const SVG_NS = "http://www.w3.org/2000/svg";
879
+ const el = document.createElementNS(SVG_NS, tag);
880
+ for (const [key, value] of Object.entries(props)) {
881
+ if (key === "nodes") continue;
882
+ if (typeof value === "function" && key.startsWith("on")) {
883
+ el.addEventListener(key.slice(2).toLowerCase(), value);
884
+ } else if (value != null) {
885
+ el.setAttribute(key, String(value));
886
+ }
887
+ }
888
+ for (const child of nodes) {
889
+ if (typeof child === "string") {
890
+ el.appendChild(document.createTextNode(child));
891
+ } else if (child instanceof Node) {
892
+ el.appendChild(child);
893
+ }
894
+ }
895
+ return el;
896
+ }
897
+
898
+ export {
899
+ required,
900
+ minLength,
901
+ maxLength,
902
+ matchesPattern,
903
+ email,
904
+ min,
905
+ max,
906
+ custom,
907
+ bindField,
908
+ form,
909
+ VirtualList,
910
+ intersection,
911
+ lazyLoad,
912
+ inputMask,
913
+ phoneMask,
914
+ dateMask,
915
+ creditCardMask,
916
+ timeMask,
917
+ ssnMask,
918
+ zipMask,
919
+ aria,
920
+ focus,
921
+ FocusTrap,
922
+ hotkey,
923
+ announce,
924
+ scopedStyle,
925
+ withScopedStyle,
926
+ removeScopedStyle,
927
+ bindAttrs,
928
+ bindBoolAttr,
929
+ bindData,
930
+ dialog,
931
+ toast,
932
+ infiniteScroll,
933
+ pagination,
934
+ eventBus,
935
+ defineElement,
936
+ svgElement
937
+ };