@benqoder/beam 0.1.3 → 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 CHANGED
@@ -28,6 +28,8 @@ A lightweight, declarative UI framework for building interactive web application
28
28
  - **Dropdowns** - Click-outside closing, Escape key support (no server)
29
29
  - **Collapse** - Expand/collapse with text swap (no server)
30
30
  - **Class Toggle** - Toggle CSS classes on elements (no server)
31
+ - **Multi-Render** - Update multiple targets in a single action response
32
+ - **Async Components** - Full support for HonoX async components in `ctx.render()`
31
33
 
32
34
  ## Installation
33
35
 
@@ -47,8 +49,6 @@ export default defineConfig({
47
49
  plugins: [
48
50
  beamPlugin({
49
51
  actions: './actions/*.tsx',
50
- modals: './modals/*.tsx',
51
- drawers: './drawers/*.tsx',
52
52
  }),
53
53
  ],
54
54
  })
@@ -123,57 +123,176 @@ export function greet(c) {
123
123
 
124
124
  ### Modals
125
125
 
126
- Modals are overlay dialogs rendered from server components.
126
+ Two ways to open modals:
127
127
 
128
- ```tsx
129
- // app/modals/confirm.tsx
130
- import { ModalFrame } from '@benqoder/beam'
128
+ **1. `beam-modal` attribute** - Explicitly opens the action result in a modal, with optional placeholder:
131
129
 
132
- export function confirmDelete(c) {
133
- const id = c.req.query('id')
134
- return (
135
- <ModalFrame title="Confirm Delete">
130
+ ```html
131
+ <!-- Shows placeholder while loading, then replaces with action result -->
132
+ <button beam-modal="confirmDelete" beam-data-id="123" beam-size="small"
133
+ beam-placeholder="<div>Loading...</div>">
134
+ Delete Item
135
+ </button>
136
+ ```
137
+
138
+ **2. `beam-action` with `ctx.modal()`** - Action decides to return a modal:
139
+
140
+ ```tsx
141
+ // app/actions/confirm.tsx
142
+ export function confirmDelete(ctx: BeamContext<Env>, { id }: Record<string, unknown>) {
143
+ return ctx.modal(
144
+ <div>
145
+ <h2>Confirm Delete</h2>
136
146
  <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>
147
+ <button beam-action="deleteItem" beam-data-id={id} beam-close>Delete</button>
140
148
  <button beam-close>Cancel</button>
141
- </ModalFrame>
142
- )
149
+ </div>
150
+ , { size: 'small' })
143
151
  }
144
152
  ```
145
153
 
154
+ `ctx.modal()` accepts JSX directly - no wrapper function needed. Options: `size` ('small' | 'medium' | 'large'), `spacing` (padding in pixels).
155
+
146
156
  ```html
147
- <button beam-modal="confirmDelete" beam-data-id="123">
148
- Delete Item
149
- </button>
157
+ <button beam-action="confirmDelete" beam-data-id="123">Delete Item</button>
150
158
  ```
151
159
 
152
160
  ### Drawers
153
161
 
154
- Drawers are slide-in panels from the left or right edge.
162
+ Two ways to open drawers:
163
+
164
+ **1. `beam-drawer` attribute** - Explicitly opens in a drawer:
165
+
166
+ ```html
167
+ <button beam-drawer="openCart" beam-position="right" beam-size="medium"
168
+ beam-placeholder="<div>Loading cart...</div>">
169
+ Open Cart
170
+ </button>
171
+ ```
172
+
173
+ **2. `beam-action` with `ctx.drawer()`** - Action returns a drawer:
155
174
 
156
175
  ```tsx
157
- // app/drawers/cart.tsx
158
- import { DrawerFrame } from '@benqoder/beam'
176
+ // app/actions/cart.tsx
177
+ export function openCart(ctx: BeamContext<Env>) {
178
+ return ctx.drawer(
179
+ <div>
180
+ <h2>Shopping Cart</h2>
181
+ <div class="cart-items">{/* Cart contents */}</div>
182
+ <button beam-close>Close</button>
183
+ </div>
184
+ , { position: 'right', size: 'medium' })
185
+ }
186
+ ```
159
187
 
160
- export function shoppingCart(c) {
161
- return (
162
- <DrawerFrame title="Shopping Cart">
163
- <div class="cart-items">
164
- {/* Cart contents */}
165
- </div>
166
- </DrawerFrame>
188
+ `ctx.drawer()` accepts JSX directly. Options: `position` ('left' | 'right'), `size` ('small' | 'medium' | 'large'), `spacing` (padding in pixels).
189
+
190
+ ```html
191
+ <button beam-action="openCart">Open Cart</button>
192
+ ```
193
+
194
+ ### Multi-Render Array API
195
+
196
+ Update multiple targets in a single action response using `ctx.render()` with arrays:
197
+
198
+ **1. Explicit targets (comma-separated)**
199
+
200
+ ```tsx
201
+ export function refreshDashboard(ctx: BeamContext<Env>) {
202
+ return ctx.render(
203
+ [
204
+ <div class="stat-card">Visits: {visits}</div>,
205
+ <div class="stat-card">Users: {users}</div>,
206
+ <div class="stat-card">Revenue: ${revenue}</div>,
207
+ ],
208
+ { target: '#stats, #users, #revenue' }
167
209
  )
168
210
  }
169
211
  ```
170
212
 
171
- ```html
172
- <button beam-drawer="shoppingCart" beam-position="right" beam-size="medium">
173
- Open Cart
174
- </button>
213
+ **2. Auto-detect by ID (no targets needed)**
214
+
215
+ ```tsx
216
+ export function refreshDashboard(ctx: BeamContext<Env>) {
217
+ // Client automatically finds elements by id, beam-id, or beam-item-id
218
+ return ctx.render([
219
+ <div id="stats">Visits: {visits}</div>,
220
+ <div id="users">Users: {users}</div>,
221
+ <div id="revenue">Revenue: ${revenue}</div>,
222
+ ])
223
+ }
175
224
  ```
176
225
 
226
+ **3. Mixed approach**
227
+
228
+ ```tsx
229
+ export function updateDashboard(ctx: BeamContext<Env>) {
230
+ return ctx.render(
231
+ [
232
+ <div>Header content</div>, // Uses explicit target
233
+ <div id="content">Main content</div>, // Auto-detected by ID
234
+ ],
235
+ { target: '#header' } // Only first item gets explicit target
236
+ )
237
+ }
238
+ ```
239
+
240
+ **Target Resolution Order:**
241
+ 1. Explicit target from comma-separated list (by index)
242
+ 2. ID from the HTML fragment's root element (`id`, `beam-id`, or `beam-item-id`)
243
+ 3. Frontend fallback (`beam-target` on the triggering element)
244
+ 4. Skip if no target found
245
+
246
+ **Exclusion:** Use `!selector` to explicitly skip an item:
247
+ ```tsx
248
+ ctx.render(
249
+ [<Box1 />, <Box2 />, <Box3 />],
250
+ { target: '#a, !#skip, #c' } // Box2 is skipped
251
+ )
252
+ ```
253
+
254
+ ### Async Components
255
+
256
+ `ctx.render()` fully supports HonoX async components:
257
+
258
+ ```tsx
259
+ // Async component that fetches data
260
+ async function UserCard({ userId }: { userId: string }) {
261
+ const user = await db.getUser(userId) // Async data fetch
262
+ return (
263
+ <div class="user-card">
264
+ <h3>{user.name}</h3>
265
+ <p>{user.email}</p>
266
+ </div>
267
+ )
268
+ }
269
+
270
+ // Use directly in ctx.render() - no wrapper needed
271
+ export function loadUser(ctx: BeamContext<Env>, { id }: Record<string, unknown>) {
272
+ return ctx.render(<UserCard userId={id as string} />, { target: '#user' })
273
+ }
274
+
275
+ // Works with arrays too
276
+ export function loadUsers(ctx: BeamContext<Env>) {
277
+ return ctx.render([
278
+ <UserCard userId="1" />,
279
+ <UserCard userId="2" />,
280
+ <UserCard userId="3" />,
281
+ ], { target: '#user1, #user2, #user3' })
282
+ }
283
+
284
+ // Mixed sync and async
285
+ export function loadDashboard(ctx: BeamContext<Env>) {
286
+ return ctx.render([
287
+ <div>Static header</div>, // Sync
288
+ <UserCard userId="current" />, // Async
289
+ <StatsWidget />, // Async
290
+ ])
291
+ }
292
+ ```
293
+
294
+ Async components are awaited automatically - no manual `Promise.resolve()` or helper functions needed.
295
+
177
296
  ---
178
297
 
179
298
  ## Attribute Reference
@@ -194,21 +313,26 @@ export function shoppingCart(c) {
194
313
  | `beam-push` | Push URL to browser history after action | `beam-push="/new-url"` |
195
314
  | `beam-replace` | Replace current URL in history | `beam-replace="?page=2"` |
196
315
 
197
- ### Modals
316
+ ### Modals & Drawers
198
317
 
199
318
  | Attribute | Description | Example |
200
319
  |-----------|-------------|---------|
201
- | `beam-modal` | Modal handler name to open | `beam-modal="editUser"` |
202
- | `beam-close` | Close the current modal when clicked | `beam-close` |
320
+ | `beam-modal` | Action to call and display result in modal | `beam-modal="editUser"` |
321
+ | `beam-drawer` | Action to call and display result in drawer | `beam-drawer="openCart"` |
322
+ | `beam-size` | Size for modal/drawer: `small`, `medium`, `large` | `beam-size="large"` |
323
+ | `beam-position` | Drawer position: `left`, `right` | `beam-position="left"` |
324
+ | `beam-placeholder` | HTML to show while loading | `beam-placeholder="<p>Loading...</p>"` |
325
+ | `beam-close` | Close the current modal/drawer when clicked | `beam-close` |
203
326
 
204
- ### Drawers
327
+ Modals and drawers can also be returned from `beam-action` using context helpers:
205
328
 
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` |
329
+ ```tsx
330
+ // Modal with options
331
+ return ctx.modal(render(<MyModal />), { size: 'large', spacing: 20 })
332
+
333
+ // Drawer with options
334
+ return ctx.drawer(render(<MyDrawer />), { position: 'left', size: 'medium' })
335
+ ```
212
336
 
213
337
  ### Forms
214
338
 
@@ -969,44 +1093,10 @@ Creates a Beam instance with handlers:
969
1093
  import { createBeam } from '@benqoder/beam'
970
1094
 
971
1095
  const beam = createBeam<Env>({
972
- actions: { increment, decrement },
973
- modals: { editUser, confirmDelete },
974
- drawers: { settings, cart },
1096
+ actions: { increment, decrement, openModal, openCart },
975
1097
  })
976
1098
  ```
977
1099
 
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
1100
  ### render
1011
1101
 
1012
1102
  Utility to render JSX to HTML string:
@@ -1023,10 +1113,8 @@ const html = render(<div>Hello</div>)
1023
1113
 
1024
1114
  ```typescript
1025
1115
  beamPlugin({
1026
- // Glob patterns for handler files (must start with '/' for virtual modules)
1116
+ // Glob pattern for action handler files (must start with '/' for virtual modules)
1027
1117
  actions: '/app/actions/*.tsx', // default
1028
- modals: '/app/modals/*.tsx', // default
1029
- drawers: '/app/drawers/*.tsx', // default
1030
1118
  })
1031
1119
  ```
1032
1120
 
@@ -1037,18 +1125,22 @@ beamPlugin({
1037
1125
  ### Handler Types
1038
1126
 
1039
1127
  ```typescript
1040
- import type { ActionHandler, ModalHandler, DrawerHandler } from '@benqoder/beam'
1128
+ import type { ActionHandler, ActionResponse, BeamContext } from '@benqoder/beam'
1129
+ import { render } from '@benqoder/beam'
1041
1130
 
1042
- const myAction: ActionHandler<Env> = (c) => {
1043
- return <div>Hello</div>
1131
+ // Action that returns HTML string
1132
+ const myAction: ActionHandler<Env> = async (ctx, params) => {
1133
+ return '<div>Hello</div>'
1044
1134
  }
1045
1135
 
1046
- const myModal: ModalHandler<Env> = (c) => {
1047
- return <ModalFrame title="Hi"><p>Content</p></ModalFrame>
1136
+ // Action that returns ActionResponse with modal
1137
+ const openModal: ActionHandler<Env> = async (ctx, params) => {
1138
+ return ctx.modal(render(<div>Modal content</div>), { size: 'medium' })
1048
1139
  }
1049
1140
 
1050
- const myDrawer: DrawerHandler<Env> = (c) => {
1051
- return <DrawerFrame title="Hi"><p>Content</p></DrawerFrame>
1141
+ // Action that returns ActionResponse with drawer
1142
+ const openDrawer: ActionHandler<Env> = async (ctx, params) => {
1143
+ return ctx.drawer(render(<div>Drawer content</div>), { position: 'right' })
1052
1144
  }
1053
1145
  ```
1054
1146
 
@@ -1179,7 +1271,6 @@ Beam provides automatic session management with a simple `ctx.session` API. No b
1179
1271
  ```typescript
1180
1272
  beamPlugin({
1181
1273
  actions: '/app/actions/*.tsx',
1182
- modals: '/app/modals/*.tsx',
1183
1274
  session: true, // Enable with defaults (cookie storage)
1184
1275
  })
1185
1276
  ```
@@ -1342,7 +1433,6 @@ The auth token is tied to sessions:
1342
1433
  // vite.config.ts
1343
1434
  beamPlugin({
1344
1435
  actions: '/app/actions/*.tsx',
1345
- modals: '/app/modals/*.tsx',
1346
1436
  session: true, // Uses env.SESSION_SECRET
1347
1437
  })
1348
1438
  ```
@@ -1470,7 +1560,7 @@ window.beam.actionName(data?, options?) → Promise<ActionResponse>
1470
1560
  // - string shorthand: treated as target selector
1471
1561
  // - object: full options with target and swap mode
1472
1562
 
1473
- // ActionResponse: { html?: string, script?: string, redirect?: string }
1563
+ // ActionResponse: { html?: string | string[], script?: string, redirect?: string, target?: string }
1474
1564
  ```
1475
1565
 
1476
1566
  ### Response Handling
package/dist/client.d.ts CHANGED
@@ -1,15 +1,24 @@
1
1
  import { type RpcStub } from 'capnweb';
2
2
  interface ActionResponse {
3
- html?: string;
3
+ html?: string | string[];
4
4
  script?: string;
5
5
  redirect?: string;
6
6
  target?: string;
7
7
  swap?: string;
8
+ modal?: string | {
9
+ html: string;
10
+ size?: string;
11
+ spacing?: number;
12
+ };
13
+ drawer?: string | {
14
+ html: string;
15
+ position?: string;
16
+ size?: string;
17
+ spacing?: number;
18
+ };
8
19
  }
9
20
  interface BeamServer {
10
21
  call(action: string, data?: Record<string, unknown>): Promise<ActionResponse>;
11
- modal(modalId: string, data?: Record<string, unknown>): Promise<string>;
12
- drawer(drawerId: string, data?: Record<string, unknown>): Promise<string>;
13
22
  registerCallback(callback: (event: string, data: unknown) => void): Promise<void>;
14
23
  }
15
24
  type BeamServerStub = RpcStub<BeamServer>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AA8B9D,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAGD,UAAU,UAAU;IAClB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC7E,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACzE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClF;AAQD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAivBzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AA2CD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAwqBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AAogBD,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAMD,iBAAS,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B9D;AAGD,QAAA,MAAM,SAAS;;;;;;;sBAz7DO,OAAO,CAAC,cAAc,CAAC;CAi8D5C,CAAA;AAGD,KAAK,YAAY,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAE/G,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,OAAO,SAAS,GAAG;YACvB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAA;SAC/B,CAAA;KACF;CACF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AA8B9D,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACvF;AAGD,UAAU,UAAU;IAClB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC7E,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClF;AAQD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAu3BzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AAkCD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAkrBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AA8gBD,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAMD,iBAAS,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B9D;AAGD,QAAA,MAAM,SAAS;;;;;;;sBAtlEO,OAAO,CAAC,cAAc,CAAC;CA8lE5C,CAAA;AAGD,KAAK,YAAY,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAE/G,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,OAAO,SAAS,GAAG;YACvB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAA;SAC/B,CAAA;KACF;CACF"}