@benqoder/beam 0.1.2 → 0.2.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/README.md +308 -89
- package/dist/client.d.ts +14 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +307 -129
- package/dist/collect.d.ts +1 -47
- package/dist/collect.d.ts.map +1 -1
- package/dist/collect.js +0 -64
- package/dist/createBeam.d.ts +50 -17
- package/dist/createBeam.d.ts.map +1 -1
- package/dist/createBeam.js +265 -86
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/types.d.ts +83 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/vite.d.ts +0 -12
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +4 -10
- package/package.json +2 -2
- package/src/beam.css +2 -0
- package/dist/DrawerFrame.d.ts +0 -16
- package/dist/DrawerFrame.d.ts.map +0 -1
- package/dist/DrawerFrame.js +0 -12
- package/dist/ModalFrame.d.ts +0 -12
- package/dist/ModalFrame.d.ts.map +0 -1
- package/dist/ModalFrame.js +0 -8
package/dist/client.js
CHANGED
|
@@ -7,12 +7,23 @@ import { newWebSocketRpcSession } from 'capnweb';
|
|
|
7
7
|
// - Bidirectional RPC (server can call client callbacks)
|
|
8
8
|
// - Automatic reconnection
|
|
9
9
|
// - Type-safe method calls
|
|
10
|
+
//
|
|
11
|
+
// SECURITY: Implements in-band authentication pattern
|
|
12
|
+
// - WebSocket connections start unauthenticated (PublicApi)
|
|
13
|
+
// - Client must call authenticate(token) to get AuthenticatedApi
|
|
14
|
+
// - Token is obtained from same-origin page (prevents CSWSH attacks)
|
|
10
15
|
// Get endpoint from meta tag or default to /beam
|
|
11
16
|
// Usage: <meta name="beam-endpoint" content="/custom-endpoint">
|
|
12
17
|
function getEndpoint() {
|
|
13
18
|
const meta = document.querySelector('meta[name="beam-endpoint"]');
|
|
14
19
|
return meta?.getAttribute('content') ?? '/beam';
|
|
15
20
|
}
|
|
21
|
+
// Get auth token from meta tag
|
|
22
|
+
// Usage: <meta name="beam-token" content="...">
|
|
23
|
+
function getAuthToken() {
|
|
24
|
+
const meta = document.querySelector('meta[name="beam-token"]');
|
|
25
|
+
return meta?.getAttribute('content') ?? '';
|
|
26
|
+
}
|
|
16
27
|
let isOnline = navigator.onLine;
|
|
17
28
|
let rpcSession = null;
|
|
18
29
|
let connectingPromise = null;
|
|
@@ -38,27 +49,36 @@ function connect() {
|
|
|
38
49
|
if (rpcSession) {
|
|
39
50
|
return Promise.resolve(rpcSession);
|
|
40
51
|
}
|
|
41
|
-
connectingPromise =
|
|
52
|
+
connectingPromise = (async () => {
|
|
42
53
|
try {
|
|
43
54
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
44
55
|
const endpoint = getEndpoint();
|
|
45
56
|
const url = `${protocol}//${location.host}${endpoint}`;
|
|
46
|
-
//
|
|
47
|
-
const
|
|
57
|
+
// Get auth token from page (proves same-origin access)
|
|
58
|
+
const token = getAuthToken();
|
|
59
|
+
if (!token) {
|
|
60
|
+
throw new Error('No auth token found. Ensure <meta name="beam-token" content="..."> is set.');
|
|
61
|
+
}
|
|
62
|
+
// Create capnweb RPC session - starts with PublicBeamServer
|
|
63
|
+
const publicSession = newWebSocketRpcSession(url);
|
|
64
|
+
// Authenticate to get the full BeamServer API
|
|
65
|
+
// This is the capnweb in-band authentication pattern
|
|
66
|
+
// @ts-ignore - capnweb stub methods are dynamically typed
|
|
67
|
+
const authenticatedSession = await publicSession.authenticate(token);
|
|
48
68
|
// Register client callback for bidirectional communication
|
|
49
69
|
// @ts-ignore - capnweb stub methods are dynamically typed
|
|
50
|
-
|
|
70
|
+
authenticatedSession.registerCallback?.(handleServerEvent)?.catch?.(() => {
|
|
51
71
|
// Server may not support callbacks, that's ok
|
|
52
72
|
});
|
|
53
|
-
rpcSession =
|
|
73
|
+
rpcSession = authenticatedSession;
|
|
54
74
|
connectingPromise = null;
|
|
55
|
-
|
|
75
|
+
return authenticatedSession;
|
|
56
76
|
}
|
|
57
77
|
catch (err) {
|
|
58
78
|
connectingPromise = null;
|
|
59
|
-
|
|
79
|
+
throw err;
|
|
60
80
|
}
|
|
61
|
-
});
|
|
81
|
+
})();
|
|
62
82
|
return connectingPromise;
|
|
63
83
|
}
|
|
64
84
|
async function ensureConnected() {
|
|
@@ -85,16 +105,6 @@ const api = {
|
|
|
85
105
|
// @ts-ignore - capnweb stub methods are dynamically typed
|
|
86
106
|
return session.call(action, data);
|
|
87
107
|
},
|
|
88
|
-
async modal(modalId, params = {}) {
|
|
89
|
-
const session = await ensureConnected();
|
|
90
|
-
// @ts-ignore - capnweb stub methods are dynamically typed
|
|
91
|
-
return session.modal(modalId, params);
|
|
92
|
-
},
|
|
93
|
-
async drawer(drawerId, params = {}) {
|
|
94
|
-
const session = await ensureConnected();
|
|
95
|
-
// @ts-ignore - capnweb stub methods are dynamically typed
|
|
96
|
-
return session.drawer(drawerId, params);
|
|
97
|
-
},
|
|
98
108
|
// Direct access to RPC session for advanced usage (promise pipelining, etc.)
|
|
99
109
|
async getSession() {
|
|
100
110
|
return ensureConnected();
|
|
@@ -436,6 +446,72 @@ function swap(target, html, mode, trigger) {
|
|
|
436
446
|
// Process hungry elements - auto-update elements that match IDs in response
|
|
437
447
|
processHungryElements(html);
|
|
438
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Handle HTML response - supports both single string and array of HTML strings.
|
|
451
|
+
* Target resolution order (server wins, frontend is fallback):
|
|
452
|
+
* 1. Server target from comma-separated list (by index)
|
|
453
|
+
* - Use "!selector" to exclude that selector (blocks frontend fallback too)
|
|
454
|
+
* 2. Frontend target (beam-target) as fallback for remaining items
|
|
455
|
+
* 3. ID from HTML fragment's root element
|
|
456
|
+
* 4. Skip if none found
|
|
457
|
+
*/
|
|
458
|
+
function handleHtmlResponse(response, frontendTarget, frontendSwap, trigger) {
|
|
459
|
+
if (!response.html)
|
|
460
|
+
return;
|
|
461
|
+
const htmlArray = Array.isArray(response.html) ? response.html : [response.html];
|
|
462
|
+
// Server targets take priority, collect exclusions
|
|
463
|
+
const serverTargets = response.target ? response.target.split(',').map(s => s.trim()) : [];
|
|
464
|
+
const excluded = new Set(serverTargets.filter(t => t.startsWith('!')).map(t => t.slice(1)));
|
|
465
|
+
const swapMode = response.swap || frontendSwap;
|
|
466
|
+
htmlArray.forEach((htmlItem, index) => {
|
|
467
|
+
const serverTarget = serverTargets[index];
|
|
468
|
+
// Skip if this is an exclusion marker
|
|
469
|
+
if (serverTarget?.startsWith('!')) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Priority 1: Server target (by index)
|
|
473
|
+
let explicitTarget = serverTarget;
|
|
474
|
+
// Priority 2: Frontend target as fallback (only if no server target and not excluded)
|
|
475
|
+
if (!explicitTarget && frontendTarget && !excluded.has(frontendTarget)) {
|
|
476
|
+
explicitTarget = frontendTarget;
|
|
477
|
+
}
|
|
478
|
+
if (explicitTarget) {
|
|
479
|
+
// Explicit target provided - use normal swap
|
|
480
|
+
const target = $(explicitTarget);
|
|
481
|
+
if (target) {
|
|
482
|
+
swap(target, htmlItem, swapMode, trigger);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
console.warn(`[beam] Target "${explicitTarget}" not found on page, skipping`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Priority 3: id, beam-id, or beam-item-id on root element
|
|
490
|
+
const temp = document.createElement('div');
|
|
491
|
+
temp.innerHTML = htmlItem.trim();
|
|
492
|
+
const rootEl = temp.firstElementChild;
|
|
493
|
+
// Check id first, then beam-id, then beam-item-id
|
|
494
|
+
const id = rootEl?.id;
|
|
495
|
+
const beamId = rootEl?.getAttribute('beam-id');
|
|
496
|
+
const beamItemId = rootEl?.getAttribute('beam-item-id');
|
|
497
|
+
const selector = id ? `#${id}`
|
|
498
|
+
: beamId ? `[beam-id="${beamId}"]`
|
|
499
|
+
: beamItemId ? `[beam-item-id="${beamItemId}"]`
|
|
500
|
+
: null;
|
|
501
|
+
if (selector && !excluded.has(selector)) {
|
|
502
|
+
const target = $(selector);
|
|
503
|
+
if (target) {
|
|
504
|
+
// Replace entire element using outerHTML (preserves styles/classes)
|
|
505
|
+
target.outerHTML = htmlItem.trim();
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
console.warn(`[beam] Target "${selector}" (from HTML) not found on page, skipping`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// If no id/beam-id/beam-item-id found or excluded, skip silently
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
439
515
|
function parseOobSwaps(html) {
|
|
440
516
|
const temp = document.createElement('div');
|
|
441
517
|
temp.innerHTML = html;
|
|
@@ -452,8 +528,8 @@ function parseOobSwaps(html) {
|
|
|
452
528
|
}
|
|
453
529
|
// ============ RPC WRAPPER ============
|
|
454
530
|
async function rpc(action, data, el) {
|
|
455
|
-
const
|
|
456
|
-
const
|
|
531
|
+
const frontendTarget = el.getAttribute('beam-target');
|
|
532
|
+
const frontendSwap = el.getAttribute('beam-swap') || 'morph';
|
|
457
533
|
const opt = optimistic(el);
|
|
458
534
|
const placeholder = showPlaceholder(el);
|
|
459
535
|
setLoading(el, true, action, data);
|
|
@@ -464,13 +540,20 @@ async function rpc(action, data, el) {
|
|
|
464
540
|
location.href = response.redirect;
|
|
465
541
|
return;
|
|
466
542
|
}
|
|
467
|
-
// Handle
|
|
468
|
-
if (response.
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
543
|
+
// Handle modal (if present)
|
|
544
|
+
if (response.modal) {
|
|
545
|
+
const modalData = typeof response.modal === 'string'
|
|
546
|
+
? { html: response.modal } : response.modal;
|
|
547
|
+
openModal(modalData.html, modalData.size || 'medium', modalData.spacing);
|
|
548
|
+
}
|
|
549
|
+
// Handle drawer (if present)
|
|
550
|
+
if (response.drawer) {
|
|
551
|
+
const drawerData = typeof response.drawer === 'string'
|
|
552
|
+
? { html: response.drawer } : response.drawer;
|
|
553
|
+
openDrawer(drawerData.html, drawerData.position || 'right', drawerData.size || 'medium', drawerData.spacing);
|
|
473
554
|
}
|
|
555
|
+
// Handle HTML (if present) - supports single string or array
|
|
556
|
+
handleHtmlResponse(response, frontendTarget, frontendSwap, el);
|
|
474
557
|
// Execute script (if present)
|
|
475
558
|
if (response.script) {
|
|
476
559
|
executeScript(response.script);
|
|
@@ -560,8 +643,9 @@ document.addEventListener('click', async (e) => {
|
|
|
560
643
|
closeDrawer();
|
|
561
644
|
}
|
|
562
645
|
});
|
|
563
|
-
// ============ MODALS ============
|
|
564
|
-
|
|
646
|
+
// ============ MODALS & DRAWERS ============
|
|
647
|
+
// beam-modal trigger - calls action and opens result in modal
|
|
648
|
+
document.addEventListener('click', async (e) => {
|
|
565
649
|
const target = e.target;
|
|
566
650
|
if (!target?.closest)
|
|
567
651
|
return;
|
|
@@ -571,30 +655,48 @@ document.addEventListener('click', (e) => {
|
|
|
571
655
|
// Check confirmation
|
|
572
656
|
if (!checkConfirm(trigger))
|
|
573
657
|
return;
|
|
574
|
-
const
|
|
658
|
+
const action = trigger.getAttribute('beam-modal');
|
|
659
|
+
if (!action)
|
|
660
|
+
return;
|
|
575
661
|
const size = trigger.getAttribute('beam-size') || 'medium';
|
|
576
662
|
const params = getParams(trigger);
|
|
577
|
-
|
|
578
|
-
|
|
663
|
+
const placeholder = trigger.getAttribute('beam-placeholder');
|
|
664
|
+
// Show placeholder modal while loading
|
|
665
|
+
if (placeholder) {
|
|
666
|
+
openModal(placeholder, size);
|
|
579
667
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
668
|
+
setLoading(trigger, true, action, params);
|
|
669
|
+
try {
|
|
670
|
+
const response = await api.call(action, params);
|
|
671
|
+
// Handle the response - if it returns modal, use that, otherwise use html
|
|
672
|
+
if (response.modal) {
|
|
673
|
+
const modalData = typeof response.modal === 'string'
|
|
674
|
+
? { html: response.modal } : response.modal;
|
|
675
|
+
openModal(modalData.html, modalData.size || size, modalData.spacing);
|
|
676
|
+
}
|
|
677
|
+
else if (response.html) {
|
|
678
|
+
// For modals, use first item if array, otherwise use as-is
|
|
679
|
+
const htmlStr = Array.isArray(response.html) ? response.html[0] : response.html;
|
|
680
|
+
if (htmlStr)
|
|
681
|
+
openModal(htmlStr, size);
|
|
682
|
+
}
|
|
683
|
+
// Execute script if present
|
|
684
|
+
if (response.script) {
|
|
685
|
+
executeScript(response.script);
|
|
686
|
+
}
|
|
590
687
|
}
|
|
591
|
-
|
|
688
|
+
catch (err) {
|
|
592
689
|
closeModal();
|
|
690
|
+
showToast('Failed to open modal.', 'error');
|
|
691
|
+
console.error('Modal error:', err);
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
setLoading(trigger, false, action, params);
|
|
593
695
|
}
|
|
594
696
|
}
|
|
595
697
|
});
|
|
596
|
-
//
|
|
597
|
-
document.addEventListener('click', (e) => {
|
|
698
|
+
// beam-drawer trigger - calls action and opens result in drawer
|
|
699
|
+
document.addEventListener('click', async (e) => {
|
|
598
700
|
const target = e.target;
|
|
599
701
|
if (!target?.closest)
|
|
600
702
|
return;
|
|
@@ -604,18 +706,69 @@ document.addEventListener('click', (e) => {
|
|
|
604
706
|
// Check confirmation
|
|
605
707
|
if (!checkConfirm(trigger))
|
|
606
708
|
return;
|
|
607
|
-
const
|
|
709
|
+
const action = trigger.getAttribute('beam-drawer');
|
|
710
|
+
if (!action)
|
|
711
|
+
return;
|
|
608
712
|
const position = trigger.getAttribute('beam-position') || 'right';
|
|
609
713
|
const size = trigger.getAttribute('beam-size') || 'medium';
|
|
610
714
|
const params = getParams(trigger);
|
|
611
|
-
|
|
612
|
-
|
|
715
|
+
const placeholder = trigger.getAttribute('beam-placeholder');
|
|
716
|
+
// Show placeholder drawer while loading
|
|
717
|
+
if (placeholder) {
|
|
718
|
+
openDrawer(placeholder, position, size);
|
|
719
|
+
}
|
|
720
|
+
setLoading(trigger, true, action, params);
|
|
721
|
+
try {
|
|
722
|
+
const response = await api.call(action, params);
|
|
723
|
+
// Handle the response - if it returns drawer, use that, otherwise use html
|
|
724
|
+
if (response.drawer) {
|
|
725
|
+
const drawerData = typeof response.drawer === 'string'
|
|
726
|
+
? { html: response.drawer } : response.drawer;
|
|
727
|
+
openDrawer(drawerData.html, drawerData.position || position, drawerData.size || size, drawerData.spacing);
|
|
728
|
+
}
|
|
729
|
+
else if (response.html) {
|
|
730
|
+
// For drawers, use first item if array, otherwise use as-is
|
|
731
|
+
const htmlStr = Array.isArray(response.html) ? response.html[0] : response.html;
|
|
732
|
+
if (htmlStr)
|
|
733
|
+
openDrawer(htmlStr, position, size);
|
|
734
|
+
}
|
|
735
|
+
// Execute script if present
|
|
736
|
+
if (response.script) {
|
|
737
|
+
executeScript(response.script);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
closeDrawer();
|
|
742
|
+
showToast('Failed to open drawer.', 'error');
|
|
743
|
+
console.error('Drawer error:', err);
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
setLoading(trigger, false, action, params);
|
|
613
747
|
}
|
|
614
748
|
}
|
|
749
|
+
});
|
|
750
|
+
// Close handlers
|
|
751
|
+
document.addEventListener('click', (e) => {
|
|
752
|
+
const target = e.target;
|
|
753
|
+
if (!target?.closest)
|
|
754
|
+
return;
|
|
615
755
|
// Close on backdrop click
|
|
756
|
+
if (target.matches?.('#modal-backdrop')) {
|
|
757
|
+
closeModal();
|
|
758
|
+
}
|
|
616
759
|
if (target.matches?.('#drawer-backdrop')) {
|
|
617
760
|
closeDrawer();
|
|
618
761
|
}
|
|
762
|
+
// Close button (handles both modal and drawer)
|
|
763
|
+
const closeBtn = target.closest('[beam-close]');
|
|
764
|
+
if (closeBtn && !closeBtn.hasAttribute('beam-action')) {
|
|
765
|
+
if (activeDrawer) {
|
|
766
|
+
closeDrawer();
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
closeModal();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
619
772
|
});
|
|
620
773
|
document.addEventListener('keydown', (e) => {
|
|
621
774
|
if (e.key === 'Escape') {
|
|
@@ -627,33 +780,30 @@ document.addEventListener('keydown', (e) => {
|
|
|
627
780
|
}
|
|
628
781
|
}
|
|
629
782
|
});
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (!backdrop) {
|
|
635
|
-
backdrop = document.createElement('div');
|
|
636
|
-
backdrop.id = 'modal-backdrop';
|
|
637
|
-
document.body.appendChild(backdrop);
|
|
638
|
-
}
|
|
639
|
-
const { size } = options;
|
|
640
|
-
backdrop.innerHTML = `
|
|
641
|
-
<div id="modal-content" role="dialog" aria-modal="true" data-size="${size}">
|
|
642
|
-
${html}
|
|
643
|
-
</div>
|
|
644
|
-
`;
|
|
645
|
-
backdrop.offsetHeight;
|
|
646
|
-
backdrop.classList.add('open');
|
|
647
|
-
document.body.classList.add('modal-open');
|
|
648
|
-
activeModal = $('#modal-content');
|
|
649
|
-
const autoFocus = activeModal?.querySelector('[autofocus]');
|
|
650
|
-
const firstInput = activeModal?.querySelector('input, button, textarea, select');
|
|
651
|
-
(autoFocus || firstInput)?.focus();
|
|
652
|
-
}
|
|
653
|
-
catch (err) {
|
|
654
|
-
showToast('Failed to open modal.', 'error');
|
|
655
|
-
console.error('Modal error:', err);
|
|
783
|
+
function openModal(html, size = 'medium', spacing) {
|
|
784
|
+
// Close any existing drawer first (modal takes priority)
|
|
785
|
+
if (activeDrawer) {
|
|
786
|
+
closeDrawer();
|
|
656
787
|
}
|
|
788
|
+
let backdrop = $('#modal-backdrop');
|
|
789
|
+
if (!backdrop) {
|
|
790
|
+
backdrop = document.createElement('div');
|
|
791
|
+
backdrop.id = 'modal-backdrop';
|
|
792
|
+
document.body.appendChild(backdrop);
|
|
793
|
+
}
|
|
794
|
+
const style = spacing !== undefined ? `padding: ${spacing}px;` : '';
|
|
795
|
+
backdrop.innerHTML = `
|
|
796
|
+
<div id="modal-content" role="dialog" aria-modal="true" data-size="${size}" style="${style}">
|
|
797
|
+
${html}
|
|
798
|
+
</div>
|
|
799
|
+
`;
|
|
800
|
+
backdrop.offsetHeight;
|
|
801
|
+
backdrop.classList.add('open');
|
|
802
|
+
document.body.classList.add('modal-open');
|
|
803
|
+
activeModal = $('#modal-content');
|
|
804
|
+
const autoFocus = activeModal?.querySelector('[autofocus]');
|
|
805
|
+
const firstInput = activeModal?.querySelector('input, button, textarea, select');
|
|
806
|
+
(autoFocus || firstInput)?.focus();
|
|
657
807
|
}
|
|
658
808
|
function closeModal() {
|
|
659
809
|
const backdrop = $('#modal-backdrop');
|
|
@@ -666,34 +816,31 @@ function closeModal() {
|
|
|
666
816
|
document.body.classList.remove('modal-open');
|
|
667
817
|
activeModal = null;
|
|
668
818
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
backdrop = document.createElement('div');
|
|
675
|
-
backdrop.id = 'drawer-backdrop';
|
|
676
|
-
document.body.appendChild(backdrop);
|
|
677
|
-
}
|
|
678
|
-
// Set position and size as data attributes for CSS styling
|
|
679
|
-
const { position, size } = options;
|
|
680
|
-
backdrop.innerHTML = `
|
|
681
|
-
<div id="drawer-content" role="dialog" aria-modal="true" data-position="${position}" data-size="${size}">
|
|
682
|
-
${html}
|
|
683
|
-
</div>
|
|
684
|
-
`;
|
|
685
|
-
backdrop.offsetHeight; // Force reflow
|
|
686
|
-
backdrop.classList.add('open');
|
|
687
|
-
document.body.classList.add('drawer-open');
|
|
688
|
-
activeDrawer = $('#drawer-content');
|
|
689
|
-
const autoFocus = activeDrawer?.querySelector('[autofocus]');
|
|
690
|
-
const firstInput = activeDrawer?.querySelector('input, button, textarea, select');
|
|
691
|
-
(autoFocus || firstInput)?.focus();
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
|
-
showToast('Failed to open drawer.', 'error');
|
|
695
|
-
console.error('Drawer error:', err);
|
|
819
|
+
// ============ DRAWERS ============
|
|
820
|
+
function openDrawer(html, position = 'right', size = 'medium', spacing) {
|
|
821
|
+
// Close any existing modal first (drawer takes priority)
|
|
822
|
+
if (activeModal) {
|
|
823
|
+
closeModal();
|
|
696
824
|
}
|
|
825
|
+
let backdrop = $('#drawer-backdrop');
|
|
826
|
+
if (!backdrop) {
|
|
827
|
+
backdrop = document.createElement('div');
|
|
828
|
+
backdrop.id = 'drawer-backdrop';
|
|
829
|
+
document.body.appendChild(backdrop);
|
|
830
|
+
}
|
|
831
|
+
const style = spacing !== undefined ? `padding: ${spacing}px;` : '';
|
|
832
|
+
backdrop.innerHTML = `
|
|
833
|
+
<div id="drawer-content" role="dialog" aria-modal="true" data-position="${position}" data-size="${size}" style="${style}">
|
|
834
|
+
${html}
|
|
835
|
+
</div>
|
|
836
|
+
`;
|
|
837
|
+
backdrop.offsetHeight; // Force reflow
|
|
838
|
+
backdrop.classList.add('open');
|
|
839
|
+
document.body.classList.add('drawer-open');
|
|
840
|
+
activeDrawer = $('#drawer-content');
|
|
841
|
+
const autoFocus = activeDrawer?.querySelector('[autofocus]');
|
|
842
|
+
const firstInput = activeDrawer?.querySelector('input, button, textarea, select');
|
|
843
|
+
(autoFocus || firstInput)?.focus();
|
|
697
844
|
}
|
|
698
845
|
function closeDrawer() {
|
|
699
846
|
const backdrop = $('#drawer-backdrop');
|
|
@@ -1102,9 +1249,14 @@ const infiniteObserver = new IntersectionObserver(async (entries) => {
|
|
|
1102
1249
|
setLoading(sentinel, true, action, params);
|
|
1103
1250
|
try {
|
|
1104
1251
|
const response = await api.call(action, params);
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1252
|
+
if (response.html) {
|
|
1253
|
+
// For infinite scroll, always use the specified target (not auto-detect from HTML)
|
|
1254
|
+
const target = $(targetSelector);
|
|
1255
|
+
if (target) {
|
|
1256
|
+
// Handle single html string for infinite scroll (array not typically used here)
|
|
1257
|
+
const htmlStr = Array.isArray(response.html) ? response.html.join('') : response.html;
|
|
1258
|
+
swap(target, htmlStr, swapMode, sentinel);
|
|
1259
|
+
}
|
|
1108
1260
|
// Save scroll state after content is loaded
|
|
1109
1261
|
requestAnimationFrame(() => {
|
|
1110
1262
|
saveScrollState(targetSelector, action);
|
|
@@ -1163,9 +1315,14 @@ document.addEventListener('click', async (e) => {
|
|
|
1163
1315
|
setLoading(trigger, true, action, params);
|
|
1164
1316
|
try {
|
|
1165
1317
|
const response = await api.call(action, params);
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1318
|
+
if (response.html) {
|
|
1319
|
+
// For load more, always use the specified target (not auto-detect from HTML)
|
|
1320
|
+
const targetEl = $(targetSelector);
|
|
1321
|
+
if (targetEl) {
|
|
1322
|
+
// Handle single html string for load more (array not typically used here)
|
|
1323
|
+
const htmlStr = Array.isArray(response.html) ? response.html.join('') : response.html;
|
|
1324
|
+
swap(targetEl, htmlStr, swapMode, trigger);
|
|
1325
|
+
}
|
|
1169
1326
|
// Save scroll state after content is loaded
|
|
1170
1327
|
requestAnimationFrame(() => {
|
|
1171
1328
|
saveScrollState(targetSelector, action);
|
|
@@ -1349,13 +1506,8 @@ document.addEventListener('click', async (e) => {
|
|
|
1349
1506
|
location.href = response.redirect;
|
|
1350
1507
|
return;
|
|
1351
1508
|
}
|
|
1352
|
-
// Handle HTML (if present)
|
|
1353
|
-
|
|
1354
|
-
const target = $(targetSelector);
|
|
1355
|
-
if (target) {
|
|
1356
|
-
swap(target, response.html, swapMode, link);
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1509
|
+
// Handle HTML (if present) - supports single string or array
|
|
1510
|
+
handleHtmlResponse(response, targetSelector, swapMode, link);
|
|
1359
1511
|
// Execute script (if present)
|
|
1360
1512
|
if (response.script) {
|
|
1361
1513
|
executeScript(response.script);
|
|
@@ -1405,13 +1557,20 @@ document.addEventListener('submit', async (e) => {
|
|
|
1405
1557
|
location.href = response.redirect;
|
|
1406
1558
|
return;
|
|
1407
1559
|
}
|
|
1408
|
-
// Handle
|
|
1409
|
-
if (response.
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
}
|
|
1560
|
+
// Handle modal (if present)
|
|
1561
|
+
if (response.modal) {
|
|
1562
|
+
const modalData = typeof response.modal === 'string'
|
|
1563
|
+
? { html: response.modal } : response.modal;
|
|
1564
|
+
openModal(modalData.html, modalData.size || 'medium', modalData.spacing);
|
|
1414
1565
|
}
|
|
1566
|
+
// Handle drawer (if present)
|
|
1567
|
+
if (response.drawer) {
|
|
1568
|
+
const drawerData = typeof response.drawer === 'string'
|
|
1569
|
+
? { html: response.drawer } : response.drawer;
|
|
1570
|
+
openDrawer(drawerData.html, drawerData.position || 'right', drawerData.size || 'medium', drawerData.spacing);
|
|
1571
|
+
}
|
|
1572
|
+
// Handle HTML (if present) - supports single string or array
|
|
1573
|
+
handleHtmlResponse(response, targetSelector, swapMode);
|
|
1415
1574
|
// Execute script (if present)
|
|
1416
1575
|
if (response.script) {
|
|
1417
1576
|
executeScript(response.script);
|
|
@@ -1460,7 +1619,10 @@ function setupValidation(el) {
|
|
|
1460
1619
|
if (response.html) {
|
|
1461
1620
|
const target = $(targetSelector);
|
|
1462
1621
|
if (target) {
|
|
1463
|
-
|
|
1622
|
+
// For validation, use first item if array, otherwise use as-is
|
|
1623
|
+
const htmlStr = Array.isArray(response.html) ? response.html[0] : response.html;
|
|
1624
|
+
if (htmlStr)
|
|
1625
|
+
morph(target, htmlStr);
|
|
1464
1626
|
}
|
|
1465
1627
|
}
|
|
1466
1628
|
// Execute script (if present)
|
|
@@ -1510,7 +1672,10 @@ const deferObserver = new IntersectionObserver(async (entries) => {
|
|
|
1510
1672
|
if (response.html) {
|
|
1511
1673
|
const target = targetSelector ? $(targetSelector) : el;
|
|
1512
1674
|
if (target) {
|
|
1513
|
-
|
|
1675
|
+
// For deferred loading, use first item if array, otherwise use as-is
|
|
1676
|
+
const htmlStr = Array.isArray(response.html) ? response.html[0] : response.html;
|
|
1677
|
+
if (htmlStr)
|
|
1678
|
+
swap(target, htmlStr, swapMode);
|
|
1514
1679
|
}
|
|
1515
1680
|
}
|
|
1516
1681
|
// Execute script (if present)
|
|
@@ -1566,7 +1731,10 @@ function startPolling(el) {
|
|
|
1566
1731
|
if (response.html) {
|
|
1567
1732
|
const target = targetSelector ? $(targetSelector) : el;
|
|
1568
1733
|
if (target) {
|
|
1569
|
-
|
|
1734
|
+
// For polling, use first item if array, otherwise use as-is
|
|
1735
|
+
const htmlStr = Array.isArray(response.html) ? response.html[0] : response.html;
|
|
1736
|
+
if (htmlStr)
|
|
1737
|
+
swap(target, htmlStr, swapMode);
|
|
1570
1738
|
}
|
|
1571
1739
|
}
|
|
1572
1740
|
// Execute script (if present)
|
|
@@ -1822,17 +1990,27 @@ window.beam = new Proxy(beamUtils, {
|
|
|
1822
1990
|
location.href = response.redirect;
|
|
1823
1991
|
return response;
|
|
1824
1992
|
}
|
|
1993
|
+
// Handle modal (if present)
|
|
1994
|
+
if (response.modal) {
|
|
1995
|
+
const modalData = typeof response.modal === 'string'
|
|
1996
|
+
? { html: response.modal } : response.modal;
|
|
1997
|
+
openModal(modalData.html, modalData.size || 'medium', modalData.spacing);
|
|
1998
|
+
}
|
|
1999
|
+
// Handle drawer (if present)
|
|
2000
|
+
if (response.drawer) {
|
|
2001
|
+
const drawerData = typeof response.drawer === 'string'
|
|
2002
|
+
? { html: response.drawer } : response.drawer;
|
|
2003
|
+
openDrawer(drawerData.html, drawerData.position || 'right', drawerData.size || 'medium', drawerData.spacing);
|
|
2004
|
+
}
|
|
1825
2005
|
// Normalize options: string is shorthand for { target: string }
|
|
1826
2006
|
const opts = typeof options === 'string'
|
|
1827
2007
|
? { target: options }
|
|
1828
2008
|
: (options || {});
|
|
1829
|
-
//
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
2009
|
+
// Server target/swap override frontend options
|
|
2010
|
+
const targetSelector = response.target || opts.target || null;
|
|
2011
|
+
const swapMode = response.swap || opts.swap || 'morph';
|
|
2012
|
+
// Handle HTML swap - supports single string or array
|
|
2013
|
+
handleHtmlResponse(response, targetSelector, swapMode);
|
|
1836
2014
|
// Execute script if present
|
|
1837
2015
|
if (response.script) {
|
|
1838
2016
|
executeScript(response.script);
|
package/dist/collect.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionHandler
|
|
1
|
+
import type { ActionHandler } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Type for glob import results from import.meta.glob
|
|
4
4
|
*/
|
|
@@ -18,51 +18,5 @@ type GlobImport = Record<string, Record<string, unknown>>;
|
|
|
18
18
|
* - `./actions/cart.tsx` exports `addToCart` → `addToCart`
|
|
19
19
|
*/
|
|
20
20
|
export declare function collectActions<TEnv = object>(glob: GlobImport): Record<string, ActionHandler<TEnv>>;
|
|
21
|
-
/**
|
|
22
|
-
* Collects modal handlers from glob imports.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```typescript
|
|
26
|
-
* const modals = collectModals<Env>(
|
|
27
|
-
* import.meta.glob('./modals/*.tsx', { eager: true })
|
|
28
|
-
* )
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export declare function collectModals<TEnv = object>(glob: GlobImport): Record<string, ModalHandler<TEnv>>;
|
|
32
|
-
/**
|
|
33
|
-
* Collects drawer handlers from glob imports.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```typescript
|
|
37
|
-
* const drawers = collectDrawers<Env>(
|
|
38
|
-
* import.meta.glob('./drawers/*.tsx', { eager: true })
|
|
39
|
-
* )
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export declare function collectDrawers<TEnv = object>(glob: GlobImport): Record<string, DrawerHandler<TEnv>>;
|
|
43
|
-
/**
|
|
44
|
-
* Collects all handlers (actions, modals, drawers) from glob imports.
|
|
45
|
-
* This is a convenience function that collects all three at once.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```typescript
|
|
49
|
-
* const { actions, modals, drawers } = collectHandlers<Env>({
|
|
50
|
-
* actions: import.meta.glob('./actions/*.tsx', { eager: true }),
|
|
51
|
-
* modals: import.meta.glob('./modals/*.tsx', { eager: true }),
|
|
52
|
-
* drawers: import.meta.glob('./drawers/*.tsx', { eager: true }),
|
|
53
|
-
* })
|
|
54
|
-
*
|
|
55
|
-
* export const beam = createBeam<Env>({ actions, modals, drawers })
|
|
56
|
-
* ```
|
|
57
|
-
*/
|
|
58
|
-
export declare function collectHandlers<TEnv = object>(globs: {
|
|
59
|
-
actions?: GlobImport;
|
|
60
|
-
modals?: GlobImport;
|
|
61
|
-
drawers?: GlobImport;
|
|
62
|
-
}): {
|
|
63
|
-
actions: Record<string, ActionHandler<TEnv>>;
|
|
64
|
-
modals: Record<string, ModalHandler<TEnv>>;
|
|
65
|
-
drawers: Record<string, DrawerHandler<TEnv>>;
|
|
66
|
-
};
|
|
67
21
|
export {};
|
|
68
22
|
//# sourceMappingURL=collect.d.ts.map
|
package/dist/collect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C;;GAEG;AACH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEzD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,GAAG,MAAM,EAC1C,IAAI,EAAE,UAAU,GACf,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAarC"}
|