@benqoder/beam 0.1.3 → 0.3.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 CHANGED
@@ -11,6 +11,10 @@ A lightweight, declarative UI framework for building interactive web application
11
11
  - **Smart Loading** - Per-action loading indicators with parameter matching
12
12
  - **DOM Morphing** - Smooth updates via Idiomorph
13
13
  - **Real-time Validation** - Validate forms as users type
14
+ - **Input Watchers** - Trigger actions on input/change events with debounce/throttle
15
+ - **Conditional Triggers** - Only trigger when conditions are met (`beam-watch-if`)
16
+ - **Dirty Form Tracking** - Track unsaved changes with indicators and warnings
17
+ - **Conditional Fields** - Enable/disable/show/hide fields based on other values
14
18
  - **Deferred Loading** - Load content when scrolled into view
15
19
  - **Polling** - Auto-refresh content at intervals
16
20
  - **Hungry Elements** - Auto-update elements across actions
@@ -28,6 +32,8 @@ A lightweight, declarative UI framework for building interactive web application
28
32
  - **Dropdowns** - Click-outside closing, Escape key support (no server)
29
33
  - **Collapse** - Expand/collapse with text swap (no server)
30
34
  - **Class Toggle** - Toggle CSS classes on elements (no server)
35
+ - **Multi-Render** - Update multiple targets in a single action response
36
+ - **Async Components** - Full support for HonoX async components in `ctx.render()`
31
37
 
32
38
  ## Installation
33
39
 
@@ -47,8 +53,6 @@ export default defineConfig({
47
53
  plugins: [
48
54
  beamPlugin({
49
55
  actions: './actions/*.tsx',
50
- modals: './modals/*.tsx',
51
- drawers: './drawers/*.tsx',
52
56
  }),
53
57
  ],
54
58
  })
@@ -123,57 +127,176 @@ export function greet(c) {
123
127
 
124
128
  ### Modals
125
129
 
126
- Modals are overlay dialogs rendered from server components.
130
+ Two ways to open modals:
127
131
 
128
- ```tsx
129
- // app/modals/confirm.tsx
130
- import { ModalFrame } from '@benqoder/beam'
132
+ **1. `beam-modal` attribute** - Explicitly opens the action result in a modal, with optional placeholder:
131
133
 
132
- export function confirmDelete(c) {
133
- const id = c.req.query('id')
134
- return (
135
- <ModalFrame title="Confirm Delete">
134
+ ```html
135
+ <!-- Shows placeholder while loading, then replaces with action result -->
136
+ <button beam-modal="confirmDelete" beam-data-id="123" beam-size="small"
137
+ beam-placeholder="<div>Loading...</div>">
138
+ Delete Item
139
+ </button>
140
+ ```
141
+
142
+ **2. `beam-action` with `ctx.modal()`** - Action decides to return a modal:
143
+
144
+ ```tsx
145
+ // app/actions/confirm.tsx
146
+ export function confirmDelete(ctx: BeamContext<Env>, { id }: Record<string, unknown>) {
147
+ return ctx.modal(
148
+ <div>
149
+ <h2>Confirm Delete</h2>
136
150
  <p>Are you sure you want to delete item {id}?</p>
137
- <button beam-action="deleteItem" beam-data-id={id} beam-close>
138
- Delete
139
- </button>
151
+ <button beam-action="deleteItem" beam-data-id={id} beam-close>Delete</button>
140
152
  <button beam-close>Cancel</button>
141
- </ModalFrame>
142
- )
153
+ </div>
154
+ , { size: 'small' })
143
155
  }
144
156
  ```
145
157
 
158
+ `ctx.modal()` accepts JSX directly - no wrapper function needed. Options: `size` ('small' | 'medium' | 'large'), `spacing` (padding in pixels).
159
+
146
160
  ```html
147
- <button beam-modal="confirmDelete" beam-data-id="123">
148
- Delete Item
149
- </button>
161
+ <button beam-action="confirmDelete" beam-data-id="123">Delete Item</button>
150
162
  ```
151
163
 
152
164
  ### Drawers
153
165
 
154
- Drawers are slide-in panels from the left or right edge.
166
+ Two ways to open drawers:
167
+
168
+ **1. `beam-drawer` attribute** - Explicitly opens in a drawer:
169
+
170
+ ```html
171
+ <button beam-drawer="openCart" beam-position="right" beam-size="medium"
172
+ beam-placeholder="<div>Loading cart...</div>">
173
+ Open Cart
174
+ </button>
175
+ ```
176
+
177
+ **2. `beam-action` with `ctx.drawer()`** - Action returns a drawer:
155
178
 
156
179
  ```tsx
157
- // app/drawers/cart.tsx
158
- import { DrawerFrame } from '@benqoder/beam'
180
+ // app/actions/cart.tsx
181
+ export function openCart(ctx: BeamContext<Env>) {
182
+ return ctx.drawer(
183
+ <div>
184
+ <h2>Shopping Cart</h2>
185
+ <div class="cart-items">{/* Cart contents */}</div>
186
+ <button beam-close>Close</button>
187
+ </div>
188
+ , { position: 'right', size: 'medium' })
189
+ }
190
+ ```
159
191
 
160
- export function shoppingCart(c) {
161
- return (
162
- <DrawerFrame title="Shopping Cart">
163
- <div class="cart-items">
164
- {/* Cart contents */}
165
- </div>
166
- </DrawerFrame>
192
+ `ctx.drawer()` accepts JSX directly. Options: `position` ('left' | 'right'), `size` ('small' | 'medium' | 'large'), `spacing` (padding in pixels).
193
+
194
+ ```html
195
+ <button beam-action="openCart">Open Cart</button>
196
+ ```
197
+
198
+ ### Multi-Render Array API
199
+
200
+ Update multiple targets in a single action response using `ctx.render()` with arrays:
201
+
202
+ **1. Explicit targets (comma-separated)**
203
+
204
+ ```tsx
205
+ export function refreshDashboard(ctx: BeamContext<Env>) {
206
+ return ctx.render(
207
+ [
208
+ <div class="stat-card">Visits: {visits}</div>,
209
+ <div class="stat-card">Users: {users}</div>,
210
+ <div class="stat-card">Revenue: ${revenue}</div>,
211
+ ],
212
+ { target: '#stats, #users, #revenue' }
167
213
  )
168
214
  }
169
215
  ```
170
216
 
171
- ```html
172
- <button beam-drawer="shoppingCart" beam-position="right" beam-size="medium">
173
- Open Cart
174
- </button>
217
+ **2. Auto-detect by ID (no targets needed)**
218
+
219
+ ```tsx
220
+ export function refreshDashboard(ctx: BeamContext<Env>) {
221
+ // Client automatically finds elements by id, beam-id, or beam-item-id
222
+ return ctx.render([
223
+ <div id="stats">Visits: {visits}</div>,
224
+ <div id="users">Users: {users}</div>,
225
+ <div id="revenue">Revenue: ${revenue}</div>,
226
+ ])
227
+ }
228
+ ```
229
+
230
+ **3. Mixed approach**
231
+
232
+ ```tsx
233
+ export function updateDashboard(ctx: BeamContext<Env>) {
234
+ return ctx.render(
235
+ [
236
+ <div>Header content</div>, // Uses explicit target
237
+ <div id="content">Main content</div>, // Auto-detected by ID
238
+ ],
239
+ { target: '#header' } // Only first item gets explicit target
240
+ )
241
+ }
242
+ ```
243
+
244
+ **Target Resolution Order:**
245
+ 1. Explicit target from comma-separated list (by index)
246
+ 2. ID from the HTML fragment's root element (`id`, `beam-id`, or `beam-item-id`)
247
+ 3. Frontend fallback (`beam-target` on the triggering element)
248
+ 4. Skip if no target found
249
+
250
+ **Exclusion:** Use `!selector` to explicitly skip an item:
251
+ ```tsx
252
+ ctx.render(
253
+ [<Box1 />, <Box2 />, <Box3 />],
254
+ { target: '#a, !#skip, #c' } // Box2 is skipped
255
+ )
256
+ ```
257
+
258
+ ### Async Components
259
+
260
+ `ctx.render()` fully supports HonoX async components:
261
+
262
+ ```tsx
263
+ // Async component that fetches data
264
+ async function UserCard({ userId }: { userId: string }) {
265
+ const user = await db.getUser(userId) // Async data fetch
266
+ return (
267
+ <div class="user-card">
268
+ <h3>{user.name}</h3>
269
+ <p>{user.email}</p>
270
+ </div>
271
+ )
272
+ }
273
+
274
+ // Use directly in ctx.render() - no wrapper needed
275
+ export function loadUser(ctx: BeamContext<Env>, { id }: Record<string, unknown>) {
276
+ return ctx.render(<UserCard userId={id as string} />, { target: '#user' })
277
+ }
278
+
279
+ // Works with arrays too
280
+ export function loadUsers(ctx: BeamContext<Env>) {
281
+ return ctx.render([
282
+ <UserCard userId="1" />,
283
+ <UserCard userId="2" />,
284
+ <UserCard userId="3" />,
285
+ ], { target: '#user1, #user2, #user3' })
286
+ }
287
+
288
+ // Mixed sync and async
289
+ export function loadDashboard(ctx: BeamContext<Env>) {
290
+ return ctx.render([
291
+ <div>Static header</div>, // Sync
292
+ <UserCard userId="current" />, // Async
293
+ <StatsWidget />, // Async
294
+ ])
295
+ }
175
296
  ```
176
297
 
298
+ Async components are awaited automatically - no manual `Promise.resolve()` or helper functions needed.
299
+
177
300
  ---
178
301
 
179
302
  ## Attribute Reference
@@ -194,21 +317,26 @@ export function shoppingCart(c) {
194
317
  | `beam-push` | Push URL to browser history after action | `beam-push="/new-url"` |
195
318
  | `beam-replace` | Replace current URL in history | `beam-replace="?page=2"` |
196
319
 
197
- ### Modals
320
+ ### Modals & Drawers
198
321
 
199
322
  | Attribute | Description | Example |
200
323
  |-----------|-------------|---------|
201
- | `beam-modal` | Modal handler name to open | `beam-modal="editUser"` |
202
- | `beam-close` | Close the current modal when clicked | `beam-close` |
324
+ | `beam-modal` | Action to call and display result in modal | `beam-modal="editUser"` |
325
+ | `beam-drawer` | Action to call and display result in drawer | `beam-drawer="openCart"` |
326
+ | `beam-size` | Size for modal/drawer: `small`, `medium`, `large` | `beam-size="large"` |
327
+ | `beam-position` | Drawer position: `left`, `right` | `beam-position="left"` |
328
+ | `beam-placeholder` | HTML to show while loading | `beam-placeholder="<p>Loading...</p>"` |
329
+ | `beam-close` | Close the current modal/drawer when clicked | `beam-close` |
203
330
 
204
- ### Drawers
331
+ Modals and drawers can also be returned from `beam-action` using context helpers:
205
332
 
206
- | Attribute | Description | Example |
207
- |-----------|-------------|---------|
208
- | `beam-drawer` | Drawer handler name to open | `beam-drawer="settings"` |
209
- | `beam-position` | Side to open from: `left`, `right` | `beam-position="left"` |
210
- | `beam-size` | Drawer width: `small`, `medium`, `large` | `beam-size="large"` |
211
- | `beam-close` | Close the current drawer when clicked | `beam-close` |
333
+ ```tsx
334
+ // Modal with options
335
+ return ctx.modal(render(<MyModal />), { size: 'large', spacing: 20 })
336
+
337
+ // Drawer with options
338
+ return ctx.drawer(render(<MyDrawer />), { position: 'left', size: 'medium' })
339
+ ```
212
340
 
213
341
  ### Forms
214
342
 
@@ -235,6 +363,40 @@ export function shoppingCart(c) {
235
363
  | `beam-watch` | Event to trigger validation: `input`, `change` | `beam-watch="input"` |
236
364
  | `beam-debounce` | Debounce delay in milliseconds | `beam-debounce="300"` |
237
365
 
366
+ ### Input Watchers
367
+
368
+ | Attribute | Description | Example |
369
+ |-----------|-------------|---------|
370
+ | `beam-watch` | Event to trigger action: `input`, `change` | `beam-watch="input"` |
371
+ | `beam-debounce` | Debounce delay in milliseconds | `beam-debounce="300"` |
372
+ | `beam-throttle` | Throttle interval in milliseconds (alternative to debounce) | `beam-throttle="100"` |
373
+ | `beam-watch-if` | Condition that must be true to trigger | `beam-watch-if="value.length >= 3"` |
374
+ | `beam-cast` | Cast input value: `number`, `integer`, `boolean`, `trim` | `beam-cast="number"` |
375
+ | `beam-loading-class` | Add class to input while request is in progress | `beam-loading-class="loading"` |
376
+ | `beam-keep` | Preserve input focus and cursor position after morph | `beam-keep` |
377
+
378
+ ### Dirty Form Tracking
379
+
380
+ | Attribute | Description | Example |
381
+ |-----------|-------------|---------|
382
+ | `beam-dirty-track` | Enable dirty tracking on a form | `<form beam-dirty-track>` |
383
+ | `beam-dirty-indicator` | Show element when form is dirty | `beam-dirty-indicator="#my-form"` |
384
+ | `beam-dirty-class` | Toggle class instead of visibility | `beam-dirty-class="has-changes"` |
385
+ | `beam-warn-unsaved` | Warn before leaving page with unsaved changes | `<form beam-warn-unsaved>` |
386
+ | `beam-revert` | Button to revert form to original values | `beam-revert="#my-form"` |
387
+ | `beam-show-if-dirty` | Show element when form is dirty | `beam-show-if-dirty="#my-form"` |
388
+ | `beam-hide-if-dirty` | Hide element when form is dirty | `beam-hide-if-dirty="#my-form"` |
389
+
390
+ ### Conditional Form Fields
391
+
392
+ | Attribute | Description | Example |
393
+ |-----------|-------------|---------|
394
+ | `beam-enable-if` | Enable field when condition is true | `beam-enable-if="#subscribe:checked"` |
395
+ | `beam-disable-if` | Disable field when condition is true | `beam-disable-if="#country[value='']"` |
396
+ | `beam-visible-if` | Show field when condition is true | `beam-visible-if="#source[value='other']"` |
397
+ | `beam-hidden-if` | Hide field when condition is true | `beam-hidden-if="#premium:checked"` |
398
+ | `beam-required-if` | Make field required when condition is true | `beam-required-if="#business:checked"` |
399
+
238
400
  ### Deferred Loading
239
401
 
240
402
  | Attribute | Description | Example |
@@ -478,6 +640,308 @@ export function submitForm(c) {
478
640
 
479
641
  ---
480
642
 
643
+ ## Input Watchers
644
+
645
+ Trigger actions on input events without forms. Great for live search, auto-save, and real-time updates.
646
+
647
+ ### Basic Usage
648
+
649
+ ```html
650
+ <!-- Live search with debounce -->
651
+ <input
652
+ name="q"
653
+ placeholder="Search..."
654
+ beam-action="search"
655
+ beam-target="#results"
656
+ beam-watch="input"
657
+ beam-debounce="300"
658
+ />
659
+ <div id="results"></div>
660
+ ```
661
+
662
+ ### Throttle vs Debounce
663
+
664
+ Use `beam-throttle` for real-time updates (like range sliders) where you want periodic updates:
665
+
666
+ ```html
667
+ <!-- Range slider with throttle - updates every 100ms while dragging -->
668
+ <input
669
+ type="range"
670
+ name="price"
671
+ beam-action="updatePrice"
672
+ beam-target="#price-display"
673
+ beam-watch="input"
674
+ beam-throttle="100"
675
+ />
676
+
677
+ <!-- Search with debounce - waits 300ms after user stops typing -->
678
+ <input
679
+ name="q"
680
+ beam-action="search"
681
+ beam-target="#results"
682
+ beam-watch="input"
683
+ beam-debounce="300"
684
+ />
685
+ ```
686
+
687
+ ### Conditional Triggers
688
+
689
+ Only trigger action when a condition is met:
690
+
691
+ ```html
692
+ <!-- Only search when 3+ characters are typed -->
693
+ <input
694
+ name="q"
695
+ placeholder="Type 3+ chars to search..."
696
+ beam-action="search"
697
+ beam-target="#results"
698
+ beam-watch="input"
699
+ beam-watch-if="value.length >= 3"
700
+ beam-debounce="300"
701
+ />
702
+ ```
703
+
704
+ The condition has access to `value` (current input value) and `this` (the element).
705
+
706
+ ### Type Casting
707
+
708
+ Cast input values before sending to the server:
709
+
710
+ ```html
711
+ <!-- Send as number instead of string -->
712
+ <input
713
+ type="range"
714
+ name="quantity"
715
+ beam-action="updateQuantity"
716
+ beam-cast="number"
717
+ beam-watch="input"
718
+ />
719
+ ```
720
+
721
+ Cast types:
722
+ - `number` - Parse as float
723
+ - `integer` - Parse as integer
724
+ - `boolean` - Convert "true"/"1"/"yes" to true
725
+ - `trim` - Trim whitespace
726
+
727
+ ### Loading Feedback
728
+
729
+ Add a class to the input while the request is in progress:
730
+
731
+ ```html
732
+ <input
733
+ name="q"
734
+ placeholder="Search..."
735
+ beam-action="search"
736
+ beam-target="#results"
737
+ beam-watch="input"
738
+ beam-loading-class="input-loading"
739
+ />
740
+
741
+ <style>
742
+ .input-loading {
743
+ border-color: blue;
744
+ animation: pulse 1s infinite;
745
+ }
746
+ </style>
747
+ ```
748
+
749
+ ### Preserving Focus
750
+
751
+ Use `beam-keep` to preserve focus and cursor position after the response morphs the DOM:
752
+
753
+ ```html
754
+ <input
755
+ name="bio"
756
+ beam-action="validateBio"
757
+ beam-target="#bio-feedback"
758
+ beam-watch="input"
759
+ beam-keep
760
+ />
761
+ ```
762
+
763
+ ### Auto-Save on Blur
764
+
765
+ Trigger action when the user leaves the field:
766
+
767
+ ```html
768
+ <input
769
+ name="username"
770
+ beam-action="saveField"
771
+ beam-data-field="username"
772
+ beam-target="#save-status"
773
+ beam-watch="change"
774
+ beam-keep
775
+ />
776
+ <div id="save-status">Not saved yet</div>
777
+ ```
778
+
779
+ ---
780
+
781
+ ## Dirty Form Tracking
782
+
783
+ Track form changes and warn users before losing unsaved work.
784
+
785
+ ### Basic Usage
786
+
787
+ ```html
788
+ <form id="profile-form" beam-dirty-track>
789
+ <input name="username" value="johndoe" />
790
+ <input name="email" value="john@example.com" />
791
+ <button type="submit">Save</button>
792
+ </form>
793
+ ```
794
+
795
+ The form gets a `beam-dirty` attribute when modified.
796
+
797
+ ### Dirty Indicator
798
+
799
+ Show an indicator when the form has unsaved changes:
800
+
801
+ ```html
802
+ <h2>
803
+ Profile Settings
804
+ <span beam-dirty-indicator="#profile-form" class="unsaved-badge">*</span>
805
+ </h2>
806
+
807
+ <form id="profile-form" beam-dirty-track>
808
+ <!-- form fields -->
809
+ </form>
810
+
811
+ <style>
812
+ [beam-dirty-indicator] { display: none; color: orange; }
813
+ </style>
814
+ ```
815
+
816
+ ### Revert Changes
817
+
818
+ Add a button to restore original values:
819
+
820
+ ```html
821
+ <form id="profile-form" beam-dirty-track>
822
+ <input name="username" value="johndoe" />
823
+ <input name="email" value="john@example.com" />
824
+
825
+ <button type="button" beam-revert="#profile-form" beam-show-if-dirty="#profile-form">
826
+ Revert Changes
827
+ </button>
828
+ <button type="submit">Save</button>
829
+ </form>
830
+ ```
831
+
832
+ The revert button only shows when the form is dirty.
833
+
834
+ ### Unsaved Changes Warning
835
+
836
+ Warn users before navigating away with unsaved changes:
837
+
838
+ ```html
839
+ <form beam-dirty-track beam-warn-unsaved>
840
+ <input name="important-data" />
841
+ <button type="submit">Save</button>
842
+ </form>
843
+ ```
844
+
845
+ The browser will show a confirmation dialog if the user tries to close the tab or navigate away.
846
+
847
+ ### Conditional Visibility
848
+
849
+ Show/hide elements based on dirty state:
850
+
851
+ ```html
852
+ <form id="settings" beam-dirty-track>
853
+ <!-- Show when dirty -->
854
+ <div beam-show-if-dirty="#settings" class="warning">
855
+ You have unsaved changes
856
+ </div>
857
+
858
+ <!-- Hide when dirty -->
859
+ <div beam-hide-if-dirty="#settings">
860
+ All changes saved
861
+ </div>
862
+ </form>
863
+ ```
864
+
865
+ ---
866
+
867
+ ## Conditional Form Fields
868
+
869
+ Enable, disable, show, or hide fields based on other field values—all client-side, no server round-trip.
870
+
871
+ ### Enable/Disable Fields
872
+
873
+ ```html
874
+ <label>
875
+ <input type="checkbox" id="subscribe" name="subscribe" />
876
+ Subscribe to newsletter
877
+ </label>
878
+
879
+ <!-- Enabled only when checkbox is checked -->
880
+ <input
881
+ type="email"
882
+ name="email"
883
+ placeholder="Enter your email..."
884
+ beam-enable-if="#subscribe:checked"
885
+ disabled
886
+ />
887
+ ```
888
+
889
+ ### Show/Hide Fields
890
+
891
+ ```html
892
+ <select name="source" id="source">
893
+ <option value="">-- Select --</option>
894
+ <option value="google">Google</option>
895
+ <option value="friend">Friend</option>
896
+ <option value="other">Other</option>
897
+ </select>
898
+
899
+ <!-- Only visible when "other" is selected -->
900
+ <div beam-visible-if="#source[value='other']">
901
+ <label>Please specify</label>
902
+ <input type="text" name="source-other" />
903
+ </div>
904
+ ```
905
+
906
+ ### Required Fields
907
+
908
+ ```html
909
+ <label>
910
+ <input type="checkbox" id="business" name="is-business" />
911
+ This is a business account
912
+ </label>
913
+
914
+ <!-- Required only when checkbox is checked -->
915
+ <input
916
+ type="text"
917
+ name="company"
918
+ placeholder="Company name"
919
+ beam-required-if="#business:checked"
920
+ />
921
+ ```
922
+
923
+ ### Condition Syntax
924
+
925
+ Conditions support:
926
+ - `:checked` - Checkbox/radio is checked
927
+ - `:disabled` - Element is disabled
928
+ - `:empty` - Input has no value
929
+ - `[value='x']` - Input value equals 'x'
930
+ - `[value!='x']` - Input value not equals 'x'
931
+ - `[value>'5']` - Numeric comparison
932
+
933
+ ```html
934
+ <!-- Enable when country is selected -->
935
+ <select beam-disable-if="#country[value='']" name="state">
936
+
937
+ <!-- Show when amount is over 100 -->
938
+ <div beam-visible-if="#amount[value>'100']">
939
+ Large order discount applied!
940
+ </div>
941
+ ```
942
+
943
+ ---
944
+
481
945
  ## Deferred Loading
482
946
 
483
947
  Load content only when it enters the viewport:
@@ -969,44 +1433,10 @@ Creates a Beam instance with handlers:
969
1433
  import { createBeam } from '@benqoder/beam'
970
1434
 
971
1435
  const beam = createBeam<Env>({
972
- actions: { increment, decrement },
973
- modals: { editUser, confirmDelete },
974
- drawers: { settings, cart },
1436
+ actions: { increment, decrement, openModal, openCart },
975
1437
  })
976
1438
  ```
977
1439
 
978
- ### ModalFrame
979
-
980
- Wrapper component for modals:
981
-
982
- ```tsx
983
- import { ModalFrame } from '@benqoder/beam'
984
-
985
- export function myModal(c) {
986
- return (
987
- <ModalFrame title="Modal Title">
988
- <p>Modal content</p>
989
- </ModalFrame>
990
- )
991
- }
992
- ```
993
-
994
- ### DrawerFrame
995
-
996
- Wrapper component for drawers:
997
-
998
- ```tsx
999
- import { DrawerFrame } from '@benqoder/beam'
1000
-
1001
- export function myDrawer(c) {
1002
- return (
1003
- <DrawerFrame title="Drawer Title">
1004
- <p>Drawer content</p>
1005
- </DrawerFrame>
1006
- )
1007
- }
1008
- ```
1009
-
1010
1440
  ### render
1011
1441
 
1012
1442
  Utility to render JSX to HTML string:
@@ -1023,10 +1453,8 @@ const html = render(<div>Hello</div>)
1023
1453
 
1024
1454
  ```typescript
1025
1455
  beamPlugin({
1026
- // Glob patterns for handler files (must start with '/' for virtual modules)
1456
+ // Glob pattern for action handler files (must start with '/' for virtual modules)
1027
1457
  actions: '/app/actions/*.tsx', // default
1028
- modals: '/app/modals/*.tsx', // default
1029
- drawers: '/app/drawers/*.tsx', // default
1030
1458
  })
1031
1459
  ```
1032
1460
 
@@ -1037,18 +1465,22 @@ beamPlugin({
1037
1465
  ### Handler Types
1038
1466
 
1039
1467
  ```typescript
1040
- import type { ActionHandler, ModalHandler, DrawerHandler } from '@benqoder/beam'
1468
+ import type { ActionHandler, ActionResponse, BeamContext } from '@benqoder/beam'
1469
+ import { render } from '@benqoder/beam'
1041
1470
 
1042
- const myAction: ActionHandler<Env> = (c) => {
1043
- return <div>Hello</div>
1471
+ // Action that returns HTML string
1472
+ const myAction: ActionHandler<Env> = async (ctx, params) => {
1473
+ return '<div>Hello</div>'
1044
1474
  }
1045
1475
 
1046
- const myModal: ModalHandler<Env> = (c) => {
1047
- return <ModalFrame title="Hi"><p>Content</p></ModalFrame>
1476
+ // Action that returns ActionResponse with modal
1477
+ const openModal: ActionHandler<Env> = async (ctx, params) => {
1478
+ return ctx.modal(render(<div>Modal content</div>), { size: 'medium' })
1048
1479
  }
1049
1480
 
1050
- const myDrawer: DrawerHandler<Env> = (c) => {
1051
- return <DrawerFrame title="Hi"><p>Content</p></DrawerFrame>
1481
+ // Action that returns ActionResponse with drawer
1482
+ const openDrawer: ActionHandler<Env> = async (ctx, params) => {
1483
+ return ctx.drawer(render(<div>Drawer content</div>), { position: 'right' })
1052
1484
  }
1053
1485
  ```
1054
1486
 
@@ -1179,7 +1611,6 @@ Beam provides automatic session management with a simple `ctx.session` API. No b
1179
1611
  ```typescript
1180
1612
  beamPlugin({
1181
1613
  actions: '/app/actions/*.tsx',
1182
- modals: '/app/modals/*.tsx',
1183
1614
  session: true, // Enable with defaults (cookie storage)
1184
1615
  })
1185
1616
  ```
@@ -1342,7 +1773,6 @@ The auth token is tied to sessions:
1342
1773
  // vite.config.ts
1343
1774
  beamPlugin({
1344
1775
  actions: '/app/actions/*.tsx',
1345
- modals: '/app/modals/*.tsx',
1346
1776
  session: true, // Uses env.SESSION_SECRET
1347
1777
  })
1348
1778
  ```
@@ -1470,7 +1900,7 @@ window.beam.actionName(data?, options?) → Promise<ActionResponse>
1470
1900
  // - string shorthand: treated as target selector
1471
1901
  // - object: full options with target and swap mode
1472
1902
 
1473
- // ActionResponse: { html?: string, script?: string, redirect?: string }
1903
+ // ActionResponse: { html?: string | string[], script?: string, redirect?: string, target?: string }
1474
1904
  ```
1475
1905
 
1476
1906
  ### Response Handling