zephyr_rb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +569 -0
  3. data/dist/zephyrRB.js +3511 -0
  4. data/lib/cli.rb +228 -0
  5. data/lib/version.rb +7 -0
  6. metadata +39 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0fc1f31f4d354cdcf713cae10fbb1ec76a8148e76017014fe01413d0be3c581
4
+ data.tar.gz: 1ca30c3b55b992b9b9ad63d6e97aba12a6b1b717acc83cb733cf58fcf90f123c
5
+ SHA512:
6
+ metadata.gz: 9d32ac92ee54d0877dc0511dccd3ab2cd4be6ec21a503122b5f39be7f7a99975248474f0c89b43e8d3db0c5ca84783ae6d8efe43dd0be8838d49374c17df9680
7
+ data.tar.gz: 1d6142bf99d6c2cf8da4cbc42595cdb878444bd8ec21b4ee244b02acdb0894d778066f34d54ea685cff440fdff5d610c5e1e0644cc9ea18e8120c9533b4166dc
data/README.md ADDED
@@ -0,0 +1,569 @@
1
+ # 💎 Zephyr WASM
2
+
3
+ **Build reactive web components using Ruby and WebAssembly**
4
+
5
+ Zephyr WASM is a lightweight framework for creating interactive web components using Ruby, compiled to WebAssembly and running entirely in the browser. Write your UI logic in Ruby with a declarative template syntax, and let the browser handle the rest.
6
+
7
+ ## ✨ Features
8
+
9
+ - **Pure Ruby** - Write components in idiomatic Ruby
10
+ - **Reactive State** - Automatic re-rendering on state changes
11
+ - **Web Components** - Standard custom elements that work anywhere
12
+ - **Zero Build Step** - Load Ruby files directly in the browser
13
+ - **Lifecycle Hooks** - `on_connect` and `on_disconnect` callbacks
14
+ - **Event Handling** - First-class support for DOM events
15
+ - **Template DSL** - Clean, declarative component templates
16
+
17
+ ## 🚀 Build ZephyrRB
18
+
19
+ run the following script:
20
+ ```bash
21
+ ruby build.rb
22
+ ```
23
+ this will generate the zephyrRB.js file
24
+
25
+ ## 🚀 Quick Start
26
+
27
+ ### 1. Serve Your Files
28
+
29
+ You need an HTTP server (ruby.wasm can't load files via `file://`):
30
+
31
+ ```bash
32
+ # Python
33
+ python3 -m http.server 8000
34
+
35
+ # Ruby
36
+ ruby -run -ehttpd . -p8000
37
+
38
+ # Node
39
+ npx http-server -p 8000
40
+ ```
41
+
42
+ ### 2. Create Your HTML
43
+
44
+ ```html
45
+ <!DOCTYPE html>
46
+ <html>
47
+ <head>
48
+ <title>My Zephyr App</title>
49
+ </head>
50
+ <body>
51
+ <!-- Use your components -->
52
+ <x-counter initial="5"></x-counter>
53
+
54
+ <!-- Load required items from ZephyrRB-->
55
+ <script src="/dist/zephyrRB.js"></script>
56
+ </body>
57
+ </html>
58
+ ```
59
+
60
+ ### 3. Define Components
61
+
62
+ Create `components.rb`:
63
+
64
+ ```ruby
65
+ # Counter component
66
+ ZephyrWasm.component('x-counter') do
67
+ observed_attributes :initial
68
+
69
+ on_connect do
70
+ count = (self['initial'] || '0').to_i
71
+ set_state(:count, count)
72
+ end
73
+
74
+ template do |b|
75
+ comp = self
76
+
77
+ b.div(class: 'counter') do
78
+ b.button(on_click: ->(_) { comp.set_state(:count, comp.state[:count] - 1) }) do
79
+ b.text('-')
80
+ end
81
+
82
+ b.span { b.text(comp.state[:count]) }
83
+
84
+ b.button(on_click: ->(_) { comp.set_state(:count, comp.state[:count] + 1) }) do
85
+ b.text('+')
86
+ end
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### 4. Open in Browser
93
+
94
+ Visit `http://localhost:8000` and see your component in action! 🎉
95
+
96
+ ## 📦 File Structure
97
+
98
+ ```
99
+ ZephyrRB/
100
+ ├── src/
101
+ │ ├── browser.script.iife.js
102
+ │ ├── component.rb
103
+ │ ├── components.rb
104
+ │ ├── dom_builder.rb
105
+ │ ├── registry.rb
106
+ │ ├── zephyr-bridge.js
107
+ │ └── zephyr_wasm.rb
108
+ ├── dist/
109
+ │ └── zephyrRB.js
110
+ ├── lib/
111
+ │ ├── cli.rb
112
+ │ └── version.rb
113
+ ├── README.md
114
+ ├── zephyr_rb.gemspec
115
+ └── build.rb
116
+ ```
117
+
118
+ ## 📚 Component API
119
+
120
+ ### Basic Structure
121
+
122
+ ```ruby
123
+ ZephyrWasm.component('x-my-component') do
124
+ # Declare observed HTML attributes
125
+ observed_attributes :foo, :bar
126
+
127
+ # Lifecycle: called when component is added to DOM
128
+ on_connect do
129
+ # Initialize state
130
+ set_state(:count, 0)
131
+ end
132
+
133
+ # Lifecycle: called when component is removed from DOM
134
+ on_disconnect do
135
+ # Cleanup if needed
136
+ end
137
+
138
+ # Define your component's UI
139
+ template do |b|
140
+ comp = self # Capture component reference
141
+
142
+ b.div(class: 'my-component') do
143
+ b.h1 { b.text("Hello from Ruby!") }
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+ ### State Management
150
+
151
+ ```ruby
152
+ # Set state (triggers re-render)
153
+ set_state(:key, value)
154
+
155
+ # Read state
156
+ state[:key]
157
+
158
+ # Multiple state updates
159
+ set_state(:count, 0)
160
+ set_state(:loading, false)
161
+ ```
162
+
163
+ ### Attributes
164
+
165
+ ```ruby
166
+ # Read HTML attributes
167
+ value = self['data-id']
168
+
169
+ # Write HTML attributes
170
+ self['data-id'] = 'new-value'
171
+
172
+ # Observed attributes automatically update state
173
+ observed_attributes :user_id
174
+
175
+ on_connect do
176
+ # state[:user_id] is automatically set from the attribute
177
+ puts state[:user_id]
178
+ end
179
+ ```
180
+
181
+ ### Event Handlers
182
+
183
+ ```ruby
184
+ template do |b|
185
+ comp = self
186
+
187
+ # Click handler
188
+ b.button(on_click: ->(_e) { comp.set_state(:clicked, true) }) do
189
+ b.text('Click me')
190
+ end
191
+
192
+ # Input handler
193
+ b.tag(:input,
194
+ type: 'text',
195
+ on_input: ->(e) { comp.set_state(:value, e[:target][:value].to_s) }
196
+ )
197
+
198
+ # Any DOM event works: on_change, on_submit, on_keydown, etc.
199
+ end
200
+ ```
201
+
202
+ ### Template DSL
203
+
204
+ ```ruby
205
+ template do |b|
206
+ comp = self
207
+
208
+ # HTML elements (method name = tag name)
209
+ b.div(class: 'container') do
210
+ b.h1 { b.text('Title') }
211
+ b.p { b.text('Paragraph') }
212
+ end
213
+
214
+ # Attributes
215
+ b.div(id: 'main', class: 'active', data_value: '123')
216
+
217
+ # Generic tag method
218
+ b.tag(:input, type: 'text', placeholder: 'Enter text...')
219
+
220
+ # Text nodes
221
+ b.span { b.text('Hello') }
222
+
223
+ # Conditional rendering
224
+ b.render_if(comp.state[:show]) do
225
+ b.p { b.text('Visible!') }
226
+ end
227
+
228
+ # List rendering
229
+ b.render_each(comp.state[:items] || []) do |item|
230
+ b.li { b.text(item[:name]) }
231
+ end
232
+
233
+ # Boolean properties (checked, disabled, selected)
234
+ b.tag(:input, type: 'checkbox', checked: true)
235
+
236
+ # Inline styles
237
+ b.div(style: { color: 'red', font_size: '16px' })
238
+ end
239
+ ```
240
+
241
+ ## 🎯 Complete Examples
242
+
243
+ ### Counter with Reset
244
+
245
+ ```ruby
246
+ ZephyrWasm.component('x-counter') do
247
+ observed_attributes :initial
248
+
249
+ on_connect do
250
+ initial = (self['initial'] || '0').to_i
251
+ set_state(:count, initial)
252
+ set_state(:initial, initial)
253
+ end
254
+
255
+ template do |b|
256
+ comp = self
257
+ count = comp.state[:count] || 0
258
+
259
+ b.div(class: 'counter') do
260
+ b.button(on_click: ->(_) { comp.set_state(:count, count - 1) }) do
261
+ b.text('-')
262
+ end
263
+
264
+ b.span(class: 'count') { b.text(count) }
265
+
266
+ b.button(on_click: ->(_) { comp.set_state(:count, count + 1) }) do
267
+ b.text('+')
268
+ end
269
+
270
+ b.button(on_click: ->(_) { comp.set_state(:count, comp.state[:initial]) }) do
271
+ b.text('Reset')
272
+ end
273
+ end
274
+ end
275
+ end
276
+ ```
277
+
278
+ ### Toggle Button
279
+
280
+ ```ruby
281
+ ZephyrWasm.component('x-toggle') do
282
+ observed_attributes :label, :checked
283
+
284
+ on_connect do
285
+ set_state(:checked, self['checked'] == 'true')
286
+ end
287
+
288
+ template do |b|
289
+ comp = self
290
+ is_checked = comp.state[:checked]
291
+ label = comp['label'] || 'Toggle'
292
+
293
+ b.button(
294
+ class: is_checked ? 'toggle active' : 'toggle',
295
+ on_click: ->(_) {
296
+ new_state = !comp.state[:checked]
297
+ comp.set_state(:checked, new_state)
298
+ comp['checked'] = new_state.to_s
299
+
300
+ # Dispatch custom event
301
+ event = JS.global[:CustomEvent].new(
302
+ 'toggle-change',
303
+ { bubbles: true, detail: { checked: new_state }.to_js }.to_js
304
+ )
305
+ comp.element.call(:dispatchEvent, event)
306
+ }
307
+ ) do
308
+ b.text(label)
309
+ end
310
+ end
311
+ end
312
+ ```
313
+
314
+ ### Todo List
315
+
316
+ ```ruby
317
+ ZephyrWasm.component('x-todo-list') do
318
+ on_connect do
319
+ set_state(:todos, [])
320
+ set_state(:input_value, '')
321
+ end
322
+
323
+ template do |b|
324
+ comp = self
325
+
326
+ b.div(class: 'todo-list') do
327
+ # Input section
328
+ b.div(class: 'input-group') do
329
+ b.tag(:input,
330
+ type: 'text',
331
+ placeholder: 'Enter a task...',
332
+ value: comp.state[:input_value] || '',
333
+ on_input: ->(e) { comp.set_state(:input_value, e[:target][:value].to_s) }
334
+ )
335
+
336
+ b.button(
337
+ on_click: ->(_) {
338
+ value = comp.state[:input_value]&.strip
339
+ if value && !value.empty?
340
+ todos = (comp.state[:todos] || []).dup
341
+ todos << {
342
+ id: JS.global[:Date].new.call(:getTime),
343
+ text: value,
344
+ done: false
345
+ }
346
+ comp.set_state(:todos, todos)
347
+ comp.set_state(:input_value, '')
348
+ end
349
+ }
350
+ ) { b.text('Add') }
351
+ end
352
+
353
+ # Todo items
354
+ b.tag(:ul) do
355
+ todos = comp.state[:todos] || []
356
+ b.render_each(todos) do |todo|
357
+ b.tag(:li, class: todo[:done] ? 'done' : '') do
358
+ b.tag(:input,
359
+ type: 'checkbox',
360
+ checked: !!todo[:done],
361
+ on_change: ->(e) {
362
+ updated_todos = (comp.state[:todos] || []).map { |t|
363
+ t[:id] == todo[:id] ? { **t, done: !!e[:target][:checked] } : t
364
+ }
365
+ comp.set_state(:todos, updated_todos)
366
+ }
367
+ )
368
+
369
+ b.span { b.text(todo[:text]) }
370
+
371
+ b.button(
372
+ class: 'delete',
373
+ on_click: ->(_) {
374
+ filtered = (comp.state[:todos] || []).reject { |t| t[:id] == todo[:id] }
375
+ comp.set_state(:todos, filtered)
376
+ }
377
+ ) { b.text('×') }
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+ ```
385
+
386
+ ## 🔧 Advanced Usage
387
+
388
+ ### Custom Events
389
+
390
+ ```ruby
391
+ # Dispatch custom events from your component
392
+ event = JS.global[:CustomEvent].new(
393
+ 'my-event',
394
+ {
395
+ bubbles: true,
396
+ detail: { foo: 'bar' }.to_js
397
+ }.to_js
398
+ )
399
+ element.call(:dispatchEvent, event)
400
+ ```
401
+
402
+ ### DOM Queries
403
+
404
+ ```ruby
405
+ on_connect do
406
+ # Query inside component
407
+ button = query('.my-button')
408
+
409
+ # Query all
410
+ items = query_all('.item')
411
+ end
412
+ ```
413
+
414
+ ### Accessing the Element
415
+
416
+ ```ruby
417
+ on_connect do
418
+ # Direct access to the DOM element
419
+ element[:id] = 'my-component'
420
+ element.call(:setAttribute, 'data-loaded', 'true')
421
+ end
422
+ ```
423
+
424
+ ### Working with JavaScript
425
+
426
+ ```ruby
427
+ # Call JavaScript functions
428
+ JS.global[:console].call(:log, 'Hello from Ruby!')
429
+
430
+ # Access global objects
431
+ date = JS.global[:Date].new
432
+ timestamp = date.call(:getTime)
433
+
434
+ # Call methods on JS objects
435
+ element.call(:scrollIntoView)
436
+ ```
437
+
438
+ ## 🎨 Styling
439
+
440
+ Add CSS to your HTML:
441
+
442
+ ```html
443
+ <style>
444
+ .counter {
445
+ display: flex;
446
+ gap: 1rem;
447
+ align-items: center;
448
+ }
449
+
450
+ .counter button {
451
+ padding: 0.5rem 1rem;
452
+ border: none;
453
+ border-radius: 4px;
454
+ background: #667eea;
455
+ color: white;
456
+ cursor: pointer;
457
+ }
458
+
459
+ .counter button:hover {
460
+ background: #5568d3;
461
+ }
462
+ </style>
463
+ ```
464
+
465
+ ## ⚠️ Important Notes
466
+
467
+ ### Must Use HTTP Server
468
+
469
+ Ruby WASM cannot load files via `file://` protocol. Always serve your files:
470
+
471
+ ```bash
472
+ python3 -m http.server 8000
473
+ ```
474
+
475
+ ### Component Names Must Include Hyphen
476
+
477
+ Custom element names require a hyphen:
478
+
479
+ ```ruby
480
+ # ✅ Good
481
+ ZephyrWasm.component('x-counter')
482
+ ZephyrWasm.component('my-button')
483
+
484
+ # ❌ Bad
485
+ ZephyrWasm.component('counter') # Missing hyphen!
486
+ ```
487
+
488
+ ### Capture Component Reference in Templates
489
+
490
+ Always capture `self` as a local variable in templates:
491
+
492
+ ```ruby
493
+ template do |b|
494
+ comp = self # ✅ Capture this!
495
+
496
+ b.button(on_click: ->(_) {
497
+ comp.set_state(:clicked, true) # Use comp, not self
498
+ })
499
+ end
500
+ ```
501
+
502
+ ### Event Handlers Return Procs
503
+
504
+ Event handlers should be Ruby procs that will be converted to JS:
505
+
506
+ ```ruby
507
+ # ✅ Good
508
+ on_click: ->(_e) { comp.set_state(:count, 1) }
509
+
510
+ # ❌ Bad - don't call .to_js yourself
511
+ on_click: ->(_e) { comp.set_state(:count, 1) }.to_js
512
+ ```
513
+
514
+ ## 🐛 Troubleshooting
515
+
516
+ ### Components Don't Appear
517
+
518
+ 1. Check browser console for errors
519
+ 2. Ensure you're using an HTTP server (not `file://`)
520
+ 3. Verify all `.rb` files are in the same directory as `index.html`
521
+ 4. Check Network tab for 404 errors
522
+
523
+ ### "Component Already Registered" Warning
524
+
525
+ This happens if you reload without refreshing. It's harmless but you can add:
526
+
527
+ ```ruby
528
+ ZephyrWasm::Registry.clear
529
+ ```
530
+
531
+ ### State Not Updating
532
+
533
+ Make sure you're using `set_state` and not modifying state directly:
534
+
535
+ ```ruby
536
+ # ✅ Good
537
+ set_state(:count, state[:count] + 1)
538
+
539
+ # ❌ Bad - won't trigger re-render
540
+ state[:count] += 1
541
+ ```
542
+
543
+ ## 🏗️ How It Works
544
+
545
+ 1. **Ruby Files Load**: Browser fetches your `.rb` files via `<script type="text/ruby" src="...">`
546
+ 2. **Components Register**: Ruby code registers component metadata in `window.ZephyrWasmRegistry`
547
+ 3. **Bridge Watches**: `zephyr-bridge.js` uses a Proxy to watch for new components
548
+ 4. **Custom Elements Defined**: Bridge defines custom elements using `setTimeout()` to avoid nested VM calls
549
+ 5. **Component Lifecycle**: When elements connect to DOM, Ruby component instances are created
550
+ 6. **Reactive Rendering**: State changes trigger re-renders using DocumentFragment for efficiency
551
+
552
+ ## 📄 License
553
+
554
+ MIT License - feel free to use in your projects!
555
+
556
+ ## 🤝 Contributing
557
+
558
+ This is an experimental framework. Issues and pull requests welcome!
559
+
560
+ ## 🙏 Credits
561
+
562
+ Built with:
563
+ - [ruby.wasm](https://github.com/ruby/ruby.wasm) - Ruby in the browser
564
+ - Web Components - Standard browser APIs
565
+ - Love for Ruby 💎
566
+
567
+ ---
568
+
569
+ **Happy coding with Ruby and WebAssembly!** 🚀