funicular 0.0.1 → 0.1.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -1
  3. data/README.md +58 -20
  4. data/Rakefile +74 -2
  5. data/demo/keymap_editor.html +582 -0
  6. data/demo/test_cable.html +179 -0
  7. data/demo/test_chartjs.html +235 -0
  8. data/demo/test_component.html +201 -0
  9. data/demo/test_diff_patch.html +146 -0
  10. data/demo/test_error_boundary.html +284 -0
  11. data/demo/test_router.html +257 -0
  12. data/demo/test_vdom.html +100 -0
  13. data/demo/tic-tac-toe.html +201 -0
  14. data/docs/README.md +419 -0
  15. data/docs/advanced-features.md +632 -0
  16. data/docs/architecture.md +409 -0
  17. data/docs/components-and-state.md +539 -0
  18. data/docs/data-fetching.md +528 -0
  19. data/docs/forms.md +446 -0
  20. data/docs/rails-integration.md +426 -0
  21. data/docs/realtime.md +543 -0
  22. data/docs/routing-and-navigation.md +427 -0
  23. data/docs/styling.md +285 -0
  24. data/exe/funicular +32 -0
  25. data/lib/funicular/assets/funicular.rb +21 -0
  26. data/lib/funicular/assets/funicular_debug.css +73 -0
  27. data/lib/funicular/assets/funicular_debug.js +183 -0
  28. data/lib/funicular/commands/routes.rb +69 -0
  29. data/lib/funicular/compiler.rb +135 -0
  30. data/lib/funicular/configuration.rb +76 -0
  31. data/lib/funicular/helpers/picoruby_helper.rb +50 -0
  32. data/lib/funicular/middleware.rb +98 -0
  33. data/lib/funicular/railtie.rb +26 -0
  34. data/lib/funicular/route_parser.rb +137 -0
  35. data/lib/funicular/vendor/picorbc/VERSION +1 -0
  36. data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
  37. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
  38. data/lib/funicular/vendor/picoruby/VERSION +1 -0
  39. data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
  40. data/lib/funicular/vendor/picoruby/debug/picoruby.js +6404 -0
  41. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  42. data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
  43. data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
  44. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  45. data/lib/funicular/version.rb +1 -1
  46. data/lib/funicular.rb +29 -1
  47. data/lib/tasks/funicular.rake +135 -0
  48. data/minitest/funicular_test.rb +13 -0
  49. data/minitest/test_helper.rb +7 -0
  50. data/mrbgem.rake +15 -0
  51. data/mrblib/cable.rb +417 -0
  52. data/mrblib/component.rb +911 -0
  53. data/mrblib/debug.rb +205 -0
  54. data/mrblib/differ.rb +244 -0
  55. data/mrblib/environment_inquirer.rb +34 -0
  56. data/mrblib/error_boundary.rb +125 -0
  57. data/mrblib/file_upload.rb +184 -0
  58. data/mrblib/form_builder.rb +284 -0
  59. data/mrblib/funicular.rb +156 -0
  60. data/mrblib/http.rb +89 -0
  61. data/mrblib/model.rb +146 -0
  62. data/mrblib/patcher.rb +203 -0
  63. data/mrblib/router.rb +229 -0
  64. data/mrblib/styles.rb +83 -0
  65. data/mrblib/vdom.rb +273 -0
  66. data/sig/cable.rbs +65 -0
  67. data/sig/component.rbs +141 -0
  68. data/sig/debug.rbs +28 -0
  69. data/sig/differ.rbs +18 -0
  70. data/sig/environment_iquirer.rbs +10 -0
  71. data/sig/error_boundary.rbs +14 -0
  72. data/sig/file_upload.rbs +18 -0
  73. data/sig/form_builder.rbs +29 -0
  74. data/sig/funicular.rbs +11 -1
  75. data/sig/http.rbs +22 -0
  76. data/sig/model.rbs +23 -0
  77. data/sig/patcher.rbs +15 -0
  78. data/sig/router.rbs +43 -0
  79. data/sig/styles.rbs +25 -0
  80. data/sig/vdom.rbs +59 -0
  81. metadata +119 -8
@@ -0,0 +1,100 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Test VDOM - PicoRuby Funicular</title>
6
+ <link rel="stylesheet" href="../test.css">
7
+ </head>
8
+ <body>
9
+ <div><a href="../index.html">Back</a></div>
10
+ <h1>VDOM Test Suite</h1>
11
+ <div id="test-results"></div>
12
+ <div id="container"></div>
13
+
14
+ <script type="text/ruby">
15
+ require 'js'
16
+ require 'funicular'
17
+
18
+ doc = JS.document
19
+ results = doc.getElementById('test-results')
20
+ container = doc.getElementById('container')
21
+
22
+ def test_case(name, &block)
23
+ doc = JS.document
24
+ results = doc.getElementById('test-results')
25
+ div = doc.createElement('div')
26
+ div.className = 'test-case'
27
+
28
+ begin
29
+ block.call
30
+ div.className = 'test-case pass'
31
+ div.textContent = "PASS: #{name}"
32
+ rescue => e
33
+ div.className = 'test-case fail'
34
+ div.textContent = "FAIL: #{name} - #{e.message}"
35
+ end
36
+
37
+ results.appendChild(div)
38
+ end
39
+
40
+ test_case("Create Element VNode") do
41
+ vnode = Funicular::VDOM.create_element('div', { id: 'test' }, 'Hello')
42
+ raise "Element creation failed" unless vnode.is_a?(Funicular::VDOM::Element)
43
+ raise "Tag mismatch" unless vnode.tag == 'div'
44
+ raise "Props mismatch" unless vnode.props[:id] == 'test'
45
+ end
46
+
47
+ test_case("Create Text VNode") do
48
+ vnode = Funicular::VDOM.create_text('Hello World')
49
+ raise "Text creation failed" unless vnode.is_a?(Funicular::VDOM::Text)
50
+ raise "Content mismatch" unless vnode.content == 'Hello World'
51
+ end
52
+
53
+ test_case("Render simple element") do
54
+ container = doc.getElementById('container')
55
+ vnode = Funicular::VDOM.create_element('p', {}, 'Test paragraph')
56
+ Funicular::VDOM.render(vnode, container)
57
+ raise "Rendering failed" unless container.children.length.to_i > 0
58
+ end
59
+
60
+ test_case("Render element with attributes") do
61
+ container = doc.getElementById('container')
62
+ vnode = Funicular::VDOM.create_element('div', { id: 'test-id', class: 'test-class' })
63
+ Funicular::VDOM.render(vnode, container)
64
+ child = container.children[0]
65
+ raise "Attribute not set" unless child && child[:id].to_s == 'test-id'
66
+ end
67
+
68
+ test_case("Render nested elements") do
69
+ container = doc.getElementById('container')
70
+ vnode = Funicular::VDOM.create_element(
71
+ 'div',
72
+ {},
73
+ Funicular::VDOM.create_element('h2', {}, 'Title'),
74
+ Funicular::VDOM.create_element('p', {}, 'Content')
75
+ )
76
+ Funicular::VDOM.render(vnode, container)
77
+ child = container.children[0]
78
+ raise "Nested rendering failed" unless child && child[:children].length.to_i == 2
79
+ end
80
+
81
+ test_case("Render text nodes") do
82
+ container = doc.getElementById('container')
83
+ vnode = Funicular::VDOM.create_element(
84
+ 'p',
85
+ {},
86
+ Funicular::VDOM.create_text('Plain text')
87
+ )
88
+ Funicular::VDOM.render(vnode, container)
89
+ child = container.children[0]
90
+ raise "Text rendering failed" unless child && child[:textContent].to_s == 'Plain text'
91
+ end
92
+
93
+ summary = doc.createElement('h2')
94
+ summary.textContent = 'All tests completed!'
95
+ summary.style = 'color: green; margin-top: 20px;'
96
+ results.appendChild(summary)
97
+ </script>
98
+ <script src="../init.iife.js"></script>
99
+ </body>
100
+ </html>
@@ -0,0 +1,201 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Tic-Tac-Toe - Funicular</title>
6
+ </head>
7
+ <body>
8
+ <div><a href="../index.html">Back</a></div>
9
+ <h1>Tic-Tac-Toe (Funicular)</h1>
10
+ <div id="app"></div>
11
+ <p>This app is ported from the ReactJS Tic-Tac-Toe tutorial: <a href="https://react.dev/learn/tutorial-tic-tac-toe" target="_blank">https://react.dev/learn/tutorial-tic-tac-toe</a></p>
12
+
13
+ <script type="text/ruby">
14
+ def calculate_winner(squares)
15
+ lines = [
16
+ [0, 1, 2],
17
+ [3, 4, 5],
18
+ [6, 7, 8],
19
+ [0, 3, 6],
20
+ [1, 4, 7],
21
+ [2, 5, 8],
22
+ [0, 4, 8],
23
+ [2, 4, 6],
24
+ ]
25
+ lines.each do |line|
26
+ a, b, c = line
27
+ if squares[a] && squares[a] == squares[b] && squares[a] == squares[c]
28
+ return squares[a]
29
+ end
30
+ end
31
+ nil
32
+ end
33
+
34
+ class Square < Funicular::Component
35
+ def handle_click(event)
36
+ event.preventDefault
37
+ on_click = props[:on_click]
38
+ on_click.call if on_click
39
+ end
40
+
41
+ def render
42
+ button(class: 'square', onclick: :handle_click) do
43
+ props[:value] || ''
44
+ end
45
+ end
46
+ end
47
+
48
+ class Board < Funicular::Component
49
+ def handle_click(i)
50
+ -> {
51
+ squares = props[:squares]
52
+ return if calculate_winner(squares) || squares[i]
53
+ on_play = props[:on_play]
54
+ on_play.call(i) if on_play
55
+ }
56
+ end
57
+
58
+ def render
59
+ squares = props[:squares]
60
+ x_is_next = props[:x_is_next]
61
+
62
+ winner = calculate_winner(squares)
63
+ status = if winner
64
+ "Winner: #{winner}"
65
+ else
66
+ "Next player: #{x_is_next ? 'X' : 'O'}"
67
+ end
68
+
69
+ div do
70
+ div(class: 'status') { status }
71
+ div(class: 'board-row') do
72
+ component(Square, value: squares[0], on_click: handle_click(0))
73
+ component(Square, value: squares[1], on_click: handle_click(1))
74
+ component(Square, value: squares[2], on_click: handle_click(2))
75
+ end
76
+ div(class: 'board-row') do
77
+ component(Square, value: squares[3], on_click: handle_click(3))
78
+ component(Square, value: squares[4], on_click: handle_click(4))
79
+ component(Square, value: squares[5], on_click: handle_click(5))
80
+ end
81
+ div(class: 'board-row') do
82
+ component(Square, value: squares[6], on_click: handle_click(6))
83
+ component(Square, value: squares[7], on_click: handle_click(7))
84
+ component(Square, value: squares[8], on_click: handle_click(8))
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ class Game < Funicular::Component
91
+ def initialize_state
92
+ {
93
+ history: [Array.new(9, nil)],
94
+ current_move: 0
95
+ }
96
+ end
97
+
98
+ def handle_play(i)
99
+ history = state.history
100
+ current_move = state.current_move
101
+ current_squares = history[current_move].dup
102
+
103
+ current_squares[i] = (current_move % 2 == 0) ? 'X' : 'O'
104
+
105
+ next_history = history[0..current_move] + [current_squares]
106
+ patch(
107
+ history: next_history,
108
+ current_move: next_history.length - 1
109
+ )
110
+ end
111
+
112
+ def jump_to(move)
113
+ -> {
114
+ patch(current_move: move)
115
+ }
116
+ end
117
+
118
+ def render
119
+ history = state.history
120
+ current_move = state.current_move
121
+ x_is_next = (current_move % 2 == 0)
122
+ current_squares = history[current_move]
123
+
124
+ div(class: 'game') do
125
+ div(class: 'game-board') do
126
+ component(Board,
127
+ x_is_next: x_is_next,
128
+ squares: current_squares,
129
+ on_play: ->(i) { handle_play(i) }
130
+ )
131
+ end
132
+ div(class: 'game-info') do
133
+ ol do
134
+ history.each_with_index do |_squares, move|
135
+ description = if move > 0
136
+ "Go to move ##{move}"
137
+ else
138
+ 'Go to game start'
139
+ end
140
+ li(key: move) do
141
+ button(onclick: jump_to(move)) { description }
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ puts "Starting Tic-Tac-Toe..."
151
+ Funicular.start(Game, container: 'app')
152
+ puts "Game started!"
153
+ </script>
154
+ <script src="../init.iife.js"></script>
155
+ </body>
156
+ <style>
157
+ * {
158
+ box-sizing: border-box;
159
+ }
160
+
161
+ body {
162
+ font-family: sans-serif;
163
+ margin: 20px;
164
+ padding: 0;
165
+ }
166
+
167
+ .square {
168
+ background: #fff;
169
+ border: 1px solid #999;
170
+ float: left;
171
+ font-size: 24px;
172
+ font-weight: bold;
173
+ line-height: 34px;
174
+ height: 34px;
175
+ margin-right: -1px;
176
+ margin-top: -1px;
177
+ padding: 0;
178
+ text-align: center;
179
+ width: 34px;
180
+ }
181
+
182
+ .board-row:after {
183
+ clear: both;
184
+ content: '';
185
+ display: table;
186
+ }
187
+
188
+ .status {
189
+ margin-bottom: 10px;
190
+ }
191
+
192
+ .game {
193
+ display: flex;
194
+ flex-direction: row;
195
+ }
196
+
197
+ .game-info {
198
+ margin-left: 20px;
199
+ }
200
+ </style>
201
+ </html>
data/docs/README.md ADDED
@@ -0,0 +1,419 @@
1
+ # Funicular
2
+
3
+ Funicular is named after a cable-driven railway, it lets you build apps using pure Ruby, with no JavaScript or HTML required.[^1]
4
+
5
+ [^1]: 子曰知止而后有定 / Confucius, *When one knows where to stop, one can be steady.*
6
+
7
+ ## Architecture Overview
8
+
9
+ Funicular is a **unidirectional, Virtual DOM-based SPA framework** that adopts design patterns similar to React while embracing Ruby's expressiveness and integrating seamlessly with Rails.
10
+
11
+ ### Core Design Decisions
12
+
13
+ | Aspect | Choice | Philosophy |
14
+ |--------|--------|------------|
15
+ | **Data Flow** | Unidirectional | Explicit state updates via `patch()` for predictability |
16
+ | **Rendering** | Virtual DOM + Diffing | Efficient DOM updates, declarative UI |
17
+ | **Components** | Class-based | Clear organization with lifecycle hooks |
18
+ | **State Management** | Local + Props | Simple by default, no global store |
19
+ | **Routing** | Client-side (History API) | SPA navigation with Rails-style DSL |
20
+ | **Templates** | Ruby DSL | Full Ruby expressiveness, no JSX |
21
+ | **Build Strategy** | Runtime (No build step) | Instant feedback during development |
22
+ | **Reactivity** | Explicit | Manual `patch()` calls, no auto-tracking magic |
23
+
24
+ ### Framework Comparison
25
+
26
+ **Funicular vs React**:
27
+ - Similar: Unidirectional flow, VDOM, component-based
28
+ - Different: Ruby DSL instead of JSX, no build step, Rails integration
29
+
30
+ **Funicular vs Vue**:
31
+ - Similar: Component-based, developer-friendly
32
+ - Different: Unidirectional (not v-model), explicit updates (not reactive proxy)
33
+
34
+ **Key Differentiator**: Pure Ruby browser applications with zero JavaScript, powered by PicoRuby.wasm.
35
+
36
+ **Read more**: [Architecture Deep Dive](docs/architecture.md)
37
+
38
+ ## Features
39
+
40
+ - **[Pure Ruby Browser App](#pure-ruby-browser-app)** - Write frontend code entirely in Ruby
41
+ - **[Object-REST Mapper](#object-rest-mapper-orm)** - ActiveRecord-style API for REST backends
42
+ - **[ActionCable WebSocket](#actioncable-compatible-websocket)** - Real-time features with Rails integration
43
+ - **[Rails Integration](#rails-integration)** - Seamless Rails API communication with CSRF handling
44
+ - **[CSS-in-Ruby](#css-in-ruby-with-styles-dsl)** - Scoped styles with conditional variants
45
+ - **[Form Builder](#rails-style-form-builder)** - Rails-style forms with validation errors
46
+ - **[Routing & Navigation](#routing-and-navigation)** - Client-side routing with URL helpers
47
+ - **[Error Boundary](#error-boundary)** - Graceful error handling for component failures
48
+ - **[Suspense](#suspense--loading-state)** - Declarative async data loading with loading states
49
+ - **[JS Integration](#js-integration)** - Delegation model for Chart.js, D3.js, etc.
50
+
51
+ ## Quick Start
52
+
53
+ ### Hello World
54
+
55
+ ```ruby
56
+ class Counter < Funicular::Component
57
+ def initialize_state
58
+ { count: 0 }
59
+ end
60
+
61
+ def increment
62
+ patch(count: state.count + 1)
63
+ end
64
+
65
+ def decrement
66
+ patch(count: state.count - 1)
67
+ end
68
+
69
+ def render
70
+ div do
71
+ h1 { "Counter" }
72
+ p { "Current count: #{state.count}" }
73
+ button(onclick: :increment) { "Increment" }
74
+ button(onclick: :decrement) { "Decrement" }
75
+ # Or use inline lambda for simple logic
76
+ # button(onclick: -> { patch(count: state.count + 1) }) { "Increment" }
77
+ end
78
+ end
79
+ end
80
+
81
+ Funicular.start(Counter, container: "app")
82
+ ```
83
+
84
+ ### With Router
85
+
86
+ ```ruby
87
+ Funicular.start(container: 'app') do |router|
88
+ router.get('/login', to: LoginComponent, as: 'login')
89
+ router.get('/dashboard', to: DashboardComponent, as: 'dashboard')
90
+ router.set_default('/login')
91
+ end
92
+ ```
93
+
94
+ ## Pure Ruby Browser App
95
+
96
+ Funicular is a component-based frontend framework that allows you to build browser applications entirely in Ruby, powered by PicoRuby.wasm. It uses a Virtual DOM (VDOM) to efficiently update the UI.
97
+
98
+ ```ruby
99
+ class TodoApp < Funicular::Component
100
+ def initialize_state
101
+ { todos: [], input: "" }
102
+ end
103
+
104
+ def handle_input(event)
105
+ patch(input: event.target[:value])
106
+ end
107
+
108
+ def handle_add
109
+ new_todo = { id: Time.now.to_i, text: state.input, done: false }
110
+ patch(todos: state.todos + [new_todo], input: "")
111
+ end
112
+
113
+ def render
114
+ div do
115
+ h1 { "Todo List" }
116
+ input(
117
+ value: state.input,
118
+ oninput: :handle_input
119
+ )
120
+ button(onclick: :handle_add) { "Add" }
121
+
122
+ state.todos.each do |todo|
123
+ div(key: todo[:id]) { todo[:text] }
124
+ end
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ **Learn more**: [Components and State Management](docs/components-and-state.md)
131
+
132
+ ## Object-REST Mapper (O-R-M)
133
+
134
+ Funicular includes a built-in Object-REST Mapper that provides an ActiveRecord-like interface for interacting with REST APIs.
135
+
136
+ ```ruby
137
+ class User < Funicular::Model
138
+ end
139
+
140
+ # Fetch all users
141
+ User.all do |users, error|
142
+ patch(users: users)
143
+ end
144
+
145
+ # Find specific user
146
+ User.find(123) do |user, error|
147
+ patch(user: user)
148
+ end
149
+
150
+ # Create user
151
+ User.create(name: "Alice", email: "alice@example.com") do |user, errors|
152
+ if errors
153
+ patch(errors: errors)
154
+ else
155
+ patch(user: user, success: "User created!")
156
+ end
157
+ end
158
+
159
+ # Update user
160
+ user.update(email: "newemail@example.com") do |updated_user, errors|
161
+ patch(user: updated_user)
162
+ end
163
+ ```
164
+
165
+ **Learn more**: [Data Fetching](docs/data-fetching.md)
166
+
167
+ ## ActionCable-compatible WebSocket
168
+
169
+ Real-time features powered by ActionCable-compatible WebSocket client.
170
+
171
+ ```ruby
172
+ class ChatComponent < Funicular::Component
173
+ def component_mounted
174
+ consumer = Funicular::Cable.create_consumer("/cable")
175
+
176
+ @subscription = consumer.subscriptions.create(
177
+ channel: "ChatChannel",
178
+ room: "lobby"
179
+ ) do |message|
180
+ patch(messages: state.messages + [message])
181
+ end
182
+ end
183
+
184
+ def handle_input(event)
185
+ patch(input: event.target[:value])
186
+ end
187
+
188
+ def handle_send
189
+ @subscription.perform("speak", message: state.input)
190
+ patch(input: "")
191
+ end
192
+
193
+ def render
194
+ div do
195
+ state.messages.each { |msg| div { msg["content"] } }
196
+ input(value: state.input, oninput: :handle_input)
197
+ button(onclick: :handle_send) { "Send" }
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ **Learn more**: [ActionCable Integration](docs/realtime.md)
204
+
205
+ ## Rails Integration
206
+
207
+ - **CSRF Token Handling**: Automatic inclusion in POST/PATCH/PUT/DELETE requests
208
+ - **ActionCable Compatible**: Real-time features with Rails channels
209
+ - **Schema Loading**: Dynamic model attribute definitions from Rails API
210
+ - **Zero JS Workflow**: Full-stack Ruby development
211
+
212
+ **Learn more**: [Data Fetching](docs/data-fetching.md), [Real-time Features](docs/realtime.md)
213
+
214
+ ## CSS-in-Ruby with Styles DSL
215
+
216
+ Keep your styles organized and scoped within each component.
217
+
218
+ ```ruby
219
+ class LoginComponent < Funicular::Component
220
+ styles do
221
+ container "min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600"
222
+ card "bg-white p-8 rounded-lg shadow-2xl w-96"
223
+ title "text-3xl font-bold text-center mb-8 text-gray-800"
224
+
225
+ # Conditional styles
226
+ button base: "px-4 py-2 rounded font-semibold",
227
+ variants: {
228
+ primary: "bg-blue-600 text-white hover:bg-blue-700",
229
+ danger: "bg-red-600 text-white hover:bg-red-700"
230
+ }
231
+ end
232
+
233
+ def render
234
+ div(class: s.container) do
235
+ div(class: s.card) do
236
+ h1(class: s.title) { "Welcome" }
237
+ button(class: s.button(:primary)) { "Login" }
238
+ end
239
+ end
240
+ end
241
+ end
242
+ ```
243
+
244
+ **Learn more**: [Styling Guide](docs/styling.md)
245
+
246
+ ## Rails-style Form Builder
247
+
248
+ Automatic form state management and error display.
249
+
250
+ ```ruby
251
+ class SignupComponent < Funicular::Component
252
+ def initialize_state
253
+ { user: { username: "", email: "" }, errors: {} }
254
+ end
255
+
256
+ def handle_submit(form_data)
257
+ User.create(form_data) do |user, errors|
258
+ if errors
259
+ patch(errors: errors)
260
+ else
261
+ Funicular.router.navigate('/dashboard')
262
+ end
263
+ end
264
+ end
265
+
266
+ def render
267
+ form_for(:user, on_submit: :handle_submit) do |f|
268
+ f.label(:username)
269
+ f.text_field(:username, autofocus: true)
270
+
271
+ f.label(:email)
272
+ f.email_field(:email)
273
+
274
+ f.submit("Sign Up")
275
+ end
276
+ end
277
+ end
278
+ ```
279
+
280
+ **Learn more**: [Forms Guide](docs/forms.md)
281
+
282
+ ## Routing and Navigation
283
+
284
+ Client-side routing with Rails-style DSL and URL helpers.
285
+
286
+ ```ruby
287
+ Funicular.start(container: 'app') do |router|
288
+ router.get('/users/:id', to: UserProfileComponent, as: 'user')
289
+ router.get('/settings', to: SettingsComponent, as: 'settings')
290
+ end
291
+
292
+ # Use URL helpers
293
+ include Funicular::RouteHelpers
294
+
295
+ link_to user_path(user), navigate: true do
296
+ span { user.name }
297
+ end
298
+
299
+ link_to settings_path, navigate: true, class: "nav-link" do
300
+ span { "Settings" }
301
+ end
302
+ ```
303
+
304
+ **Learn more**: [Routing and Navigation](docs/routing-and-navigation.md)
305
+
306
+ ## Error Boundary
307
+
308
+ Catch errors from child components and display fallback UI.
309
+
310
+ ```ruby
311
+ component(Funicular::ErrorBoundary,
312
+ fallback: ->(error) {
313
+ div(class: "error") do
314
+ h3 { "Oops! Something went wrong" }
315
+ p { "Error: #{error.message}" }
316
+ end
317
+ }
318
+ ) do
319
+ component(RiskyComponent)
320
+ end
321
+ ```
322
+
323
+ **Learn more**: [Advanced Features - Error Boundary](docs/advanced-features.md#error-boundary)
324
+
325
+ ## Suspense / Loading State
326
+
327
+ Declarative async data loading with loading states.
328
+
329
+ ```ruby
330
+ class UserProfile < Funicular::Component
331
+ use_suspense :user,
332
+ ->(resolve, reject) {
333
+ User.find(props[:id]) do |user, error|
334
+ error ? reject.call(error) : resolve.call(user)
335
+ end
336
+ }
337
+
338
+ def render
339
+ suspense(
340
+ fallback: -> { div { "Loading..." } },
341
+ error: ->(e) { div { "Failed: #{e}" } }
342
+ ) do
343
+ div do
344
+ h1 { user.name }
345
+ p { user.email }
346
+ end
347
+ end
348
+ end
349
+ end
350
+ ```
351
+
352
+ **Learn more**: [Data Fetching - Suspense](docs/data-fetching.md#suspense--loading-states)
353
+
354
+ ## JS Integration
355
+
356
+ Delegation model for integrating JavaScript libraries (Chart.js, D3.js, etc.).
357
+
358
+ ```ruby
359
+ class ChartComponent < Funicular::Component
360
+ def component_mounted
361
+ canvas = refs[:chart_canvas]
362
+ @chart = JS.global.Chart.new(canvas, {
363
+ type: 'bar',
364
+ data: { labels: state.labels, datasets: [...] }
365
+ })
366
+ end
367
+
368
+ def component_unmounted
369
+ @chart&.destroy()
370
+ end
371
+
372
+ def render
373
+ div { canvas(ref: :chart_canvas) }
374
+ end
375
+ end
376
+ ```
377
+
378
+ **Learn more**: [Advanced Features - JS Integration](docs/advanced-features.md#js-integration-via-delegation-model)
379
+
380
+ ## Documentation
381
+
382
+ ### Architecture
383
+ - [**Architecture Deep Dive**](docs/architecture.md) - Design decisions, comparisons, trade-offs
384
+
385
+ ### Core Concepts
386
+ - [**Components and State**](docs/components-and-state.md) - Component lifecycle, state management, props
387
+ - [**Styling**](docs/styling.md) - CSS-in-Ruby Styles DSL, conditional styles, variants
388
+ - [**Forms**](docs/forms.md) - Form builder, validation, error handling
389
+
390
+ ### Features
391
+ - [**Routing and Navigation**](docs/routing-and-navigation.md) - Router, URL helpers, link_to
392
+ - [**Data Fetching**](docs/data-fetching.md) - HTTP client, Model (O-R-M), Suspense
393
+ - [**Real-time**](docs/realtime.md) - ActionCable WebSocket integration
394
+
395
+ ### Advanced
396
+ - [**Advanced Features**](docs/advanced-features.md) - Error Boundary, CSS Transitions, JS Integration
397
+
398
+ ### Related Documentation
399
+ - [picoruby-wasm/docs/callback.md](../picoruby-wasm/docs/callback.md) - Callback system
400
+ - [picoruby-wasm/docs/interoperability_between_js_and_ruby.md](../picoruby-wasm/docs/interoperability_between_js_and_ruby.md) - JS::Bridge for Ruby/JS interop
401
+
402
+ ## Best Use Cases
403
+
404
+ ### Well-Suited For
405
+
406
+ - Rails applications with SPA features
407
+ - Small to medium SPAs (dashboards, admin panels, chat apps)
408
+ - Ruby teams doing frontend development
409
+ - Rapid prototyping of interactive UIs
410
+
411
+ ### Not Ideal For
412
+
413
+ - SEO-critical applications (no SSR support for now, but planning...)
414
+ - Large-scale SPAs with complex state management needs
415
+ - Performance-critical applications (WebAssembly overhead)
416
+ - Mobile applications (use React Native or native solutions)
417
+
418
+ **Read more**: [Architecture - Trade-offs](docs/architecture.md#trade-offs)
419
+