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,146 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Test Diff/Patch - 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>Diff/Patch 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("Diff text change") do
41
+ old = Funicular::VDOM.create_text('Hello')
42
+ new = Funicular::VDOM.create_text('World')
43
+ patches = Funicular::VDOM.diff(old, new)
44
+ raise "Diff failed" if patches.empty?
45
+ raise "Wrong patch type" unless patches[0][0] == :replace
46
+ end
47
+
48
+ test_case("Diff props change") do
49
+ old = Funicular::VDOM.create_element('button', { class: 'red' })
50
+ new = Funicular::VDOM.create_element('button', { class: 'blue' })
51
+ patches = Funicular::VDOM.diff(old, new)
52
+ raise "Diff failed" if patches.empty?
53
+ raise "Wrong patch type" unless patches[0][0] == :props
54
+ raise "Wrong prop value" unless patches[0][1][:class] == 'blue'
55
+ end
56
+
57
+ test_case("Diff no change") do
58
+ old = Funicular::VDOM.create_element('div', { id: 'test' }, 'Same')
59
+ new = Funicular::VDOM.create_element('div', { id: 'test' }, 'Same')
60
+ patches = Funicular::VDOM.diff(old, new)
61
+ raise "Should have no patches" unless patches.empty?
62
+ end
63
+
64
+ test_case("Diff tag change") do
65
+ old = Funicular::VDOM.create_element('div')
66
+ new = Funicular::VDOM.create_element('span')
67
+ patches = Funicular::VDOM.diff(old, new)
68
+ raise "Diff failed" if patches.empty?
69
+ raise "Wrong patch type" unless patches[0][0] == :replace
70
+ end
71
+
72
+ test_case("Diff child addition") do
73
+ old = Funicular::VDOM.create_element('ul')
74
+ new = Funicular::VDOM.create_element('ul', {},
75
+ Funicular::VDOM.create_element('li', {}, 'Item 1')
76
+ )
77
+ patches = Funicular::VDOM.diff(old, new)
78
+ raise "Diff failed" if patches.empty?
79
+ raise "Wrong patch structure" unless patches[0].is_a?(Array)
80
+ raise "Wrong child index" unless patches[0][0] == 0
81
+ end
82
+
83
+ test_case("Patch text content") do
84
+ container = doc.getElementById('container')
85
+ container.innerHTML = ''
86
+
87
+ old_vnode = Funicular::VDOM.create_element('p', {}, 'Old text')
88
+ dom_node = Funicular::VDOM.render(old_vnode, container)
89
+
90
+ new_vnode = Funicular::VDOM.create_element('p', {}, 'New text')
91
+ patches = Funicular::VDOM.diff(old_vnode, new_vnode)
92
+
93
+ child = container.children[0]
94
+ Funicular::VDOM.patch(child, patches)
95
+
96
+ updated_text = child[:textContent].to_s
97
+ raise "Patch failed: got '#{updated_text}'" unless updated_text == 'New text'
98
+ end
99
+
100
+ test_case("Patch attributes") do
101
+ container = doc.getElementById('container')
102
+ container.innerHTML = ''
103
+
104
+ old_vnode = Funicular::VDOM.create_element('div', { class: 'old', id: 'test' })
105
+ dom_node = Funicular::VDOM.render(old_vnode, container)
106
+
107
+ new_vnode = Funicular::VDOM.create_element('div', { class: 'new', id: 'test' })
108
+ patches = Funicular::VDOM.diff(old_vnode, new_vnode)
109
+
110
+ child = container.children[0]
111
+ Funicular::VDOM.patch(child, patches)
112
+
113
+ updated_class = child[:className].to_s
114
+ raise "Patch failed: got '#{updated_class}'" unless updated_class == 'new'
115
+ end
116
+
117
+ test_case("Patch nested elements") do
118
+ container = doc.getElementById('container')
119
+ container.innerHTML = ''
120
+
121
+ old_vnode = Funicular::VDOM.create_element('div', {},
122
+ Funicular::VDOM.create_element('p', {}, 'Old')
123
+ )
124
+ dom_node = Funicular::VDOM.render(old_vnode, container)
125
+
126
+ new_vnode = Funicular::VDOM.create_element('div', {},
127
+ Funicular::VDOM.create_element('p', {}, 'New')
128
+ )
129
+ patches = Funicular::VDOM.diff(old_vnode, new_vnode)
130
+
131
+ child = container.children[0]
132
+ Funicular::VDOM.patch(child, patches)
133
+
134
+ p_element = child[:children][0]
135
+ updated_text = p_element[:textContent].to_s
136
+ raise "Patch failed: got '#{updated_text}'" unless updated_text == 'New'
137
+ end
138
+
139
+ summary = doc.createElement('h2')
140
+ summary.textContent = 'All tests completed!'
141
+ summary.style = 'color: green; margin-top: 20px;'
142
+ results.appendChild(summary)
143
+ </script>
144
+ <script src="../init.iife.js"></script>
145
+ </body>
146
+ </html>
@@ -0,0 +1,284 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Funicular Error Boundary Test</title>
6
+ <style>
7
+ body {
8
+ font-family: Arial, sans-serif;
9
+ padding: 20px;
10
+ max-width: 900px;
11
+ margin: 0 auto;
12
+ }
13
+ .test-section {
14
+ margin: 20px 0;
15
+ padding: 15px;
16
+ border: 1px solid #ccc;
17
+ border-radius: 5px;
18
+ }
19
+ .test-result {
20
+ margin: 10px 0;
21
+ padding: 10px;
22
+ border-radius: 4px;
23
+ }
24
+ .test-result.pass {
25
+ background-color: #d4edda;
26
+ color: #155724;
27
+ }
28
+ .test-result.fail {
29
+ background-color: #f8d7da;
30
+ color: #721c24;
31
+ }
32
+ #app {
33
+ margin-top: 20px;
34
+ padding: 20px;
35
+ border: 2px solid #007bff;
36
+ border-radius: 5px;
37
+ }
38
+ button {
39
+ margin: 5px;
40
+ padding: 8px 16px;
41
+ cursor: pointer;
42
+ }
43
+ .demo-card {
44
+ border: 1px solid #ddd;
45
+ border-radius: 8px;
46
+ padding: 15px;
47
+ margin: 10px 0;
48
+ background: #f9f9f9;
49
+ }
50
+ .demo-card h3 {
51
+ margin-top: 0;
52
+ color: #333;
53
+ }
54
+ .debug-panel {
55
+ background: #e9ecef;
56
+ border-radius: 4px;
57
+ padding: 15px;
58
+ margin-top: 20px;
59
+ }
60
+ .debug-panel h4 {
61
+ margin-top: 0;
62
+ color: #495057;
63
+ }
64
+ pre {
65
+ background: #1e1e1e;
66
+ color: #d4d4d4;
67
+ padding: 10px;
68
+ border-radius: 4px;
69
+ overflow-x: auto;
70
+ font-size: 12px;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div><a href="../index.html">Back</a></div>
76
+ <h1>Funicular Error Boundary Demo</h1>
77
+
78
+ <p>This demo shows how ErrorBoundary catches errors from child components and displays fallback UI.</p>
79
+
80
+ <div id="test-results"></div>
81
+ <div id="app"></div>
82
+
83
+ <div class="debug-panel">
84
+ <h4>Debug Information</h4>
85
+ <p>Open browser console to see error logs.</p>
86
+ <button id="show-errors">Show Caught Errors</button>
87
+ <button id="show-tree">Show Component Tree</button>
88
+ <pre id="debug-output"></pre>
89
+ </div>
90
+
91
+ <script type="text/ruby">
92
+ # A component that always throws an error during render
93
+ class BrokenComponent < Funicular::Component
94
+ def render
95
+ raise "BrokenComponent intentionally threw this error!"
96
+ end
97
+ end
98
+
99
+ # A working component to show mixed content
100
+ class WorkingComponent < Funicular::Component
101
+ def initialize_state
102
+ { count: 0 }
103
+ end
104
+
105
+ def increment(event)
106
+ event.preventDefault
107
+ patch(count: state[:count] + 1)
108
+ end
109
+
110
+ def render
111
+ div(class: 'demo-card') do
112
+ h3 { "Working Component" }
113
+ p { "Count: #{state[:count]}" }
114
+ button(onclick: :increment) { "Increment" }
115
+ end
116
+ end
117
+ end
118
+
119
+ # Main demo application
120
+ class ErrorBoundaryDemo < Funicular::Component
121
+ def render
122
+ error_handler = ->(error, info) {
123
+ puts "[Demo] Error caught by ErrorBoundary:"
124
+ puts " Error: #{error.message}"
125
+ puts " Component: #{info&.dig(:component_class)}"
126
+ }
127
+
128
+ custom_fallback = ->(error) {
129
+ div(style: 'background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; padding: 20px; border-radius: 8px;') do
130
+ h3(style: 'margin-top: 0;') { "Custom Error Handler" }
131
+ p { "Caught: #{error.message}" }
132
+ p(style: 'font-size: 12px; opacity: 0.8;') { "This is a custom fallback provided via props." }
133
+ end
134
+ }
135
+
136
+ div do
137
+ h2 { "Error Boundary Demonstrations" }
138
+
139
+ # Demo 1: Default fallback
140
+ div(class: 'demo-card') do
141
+ h3 { "Demo 1: Default Fallback UI" }
142
+ p { "ErrorBoundary wraps a component that always throws. Default fallback is shown." }
143
+ component(Funicular::ErrorBoundary, on_error: error_handler) do
144
+ component(BrokenComponent)
145
+ end
146
+ end
147
+
148
+ # Demo 2: Custom fallback
149
+ div(class: 'demo-card') do
150
+ h3 { "Demo 2: Custom Fallback UI" }
151
+ p { "ErrorBoundary with a custom fallback prop." }
152
+ component(Funicular::ErrorBoundary,
153
+ fallback: custom_fallback,
154
+ on_error: error_handler
155
+ ) do
156
+ component(BrokenComponent)
157
+ end
158
+ end
159
+
160
+ # Demo 3: Mixed content - some work, some fail
161
+ div(class: 'demo-card') do
162
+ h3 { "Demo 3: Isolated Error Boundaries" }
163
+ p { "Each component has its own ErrorBoundary. One failure doesn't affect others." }
164
+
165
+ div(style: 'display: flex; gap: 15px; flex-wrap: wrap;') do
166
+ div(style: 'flex: 1; min-width: 200px;') do
167
+ h4 { "Working:" }
168
+ component(Funicular::ErrorBoundary) do
169
+ component(WorkingComponent)
170
+ end
171
+ end
172
+
173
+ div(style: 'flex: 1; min-width: 200px;') do
174
+ h4 { "Broken:" }
175
+ component(Funicular::ErrorBoundary) do
176
+ component(BrokenComponent)
177
+ end
178
+ end
179
+
180
+ div(style: 'flex: 1; min-width: 200px;') do
181
+ h4 { "Also Working:" }
182
+ component(Funicular::ErrorBoundary) do
183
+ component(WorkingComponent)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ def log_test(name, passed)
193
+ results = JS.document.getElementById('test-results')
194
+ div = JS.document.createElement('div')
195
+ div.className = passed ? 'test-result pass' : 'test-result fail'
196
+ div.textContent = "#{passed ? 'PASS' : 'FAIL'}: #{name}"
197
+ results.appendChild(div)
198
+ puts "#{passed ? 'PASS' : 'FAIL'}: #{name}"
199
+ end
200
+
201
+ def run_tests
202
+ puts "Starting Error Boundary tests..."
203
+
204
+ # Test 1: ErrorBoundary class exists
205
+ begin
206
+ exists = Funicular.const_defined?(:ErrorBoundary) && Funicular::ErrorBoundary.is_a?(Class)
207
+ log_test("ErrorBoundary class exists", exists)
208
+ rescue => e
209
+ log_test("ErrorBoundary class exists", false)
210
+ puts "Error: #{e.message}"
211
+ end
212
+
213
+ # Test 2: ErrorBoundary inherits from Component
214
+ begin
215
+ inherits = Funicular::ErrorBoundary.ancestors.include?(Funicular::Component)
216
+ log_test("ErrorBoundary inherits from Component", inherits)
217
+ rescue => e
218
+ log_test("ErrorBoundary inherits from Component", false)
219
+ puts "Error: #{e.message}"
220
+ end
221
+
222
+ # Test 3: ErrorBoundary has initial state
223
+ begin
224
+ boundary = Funicular::ErrorBoundary.new
225
+ has_state = boundary.state[:has_error] == false && boundary.state[:error].nil?
226
+ log_test("ErrorBoundary has correct initial state", has_state)
227
+ rescue => e
228
+ log_test("ErrorBoundary has correct initial state", false)
229
+ puts "Error: #{e.message}"
230
+ end
231
+
232
+ # Test 4: catch_error method works
233
+ begin
234
+ boundary = Funicular::ErrorBoundary.new
235
+ test_error = RuntimeError.new("Test error")
236
+ boundary.catch_error(test_error, { component_class: "TestComponent" })
237
+ caught = boundary.state[:has_error] == true && boundary.state[:error] == test_error
238
+ log_test("catch_error updates state correctly", caught)
239
+ rescue => e
240
+ log_test("catch_error updates state correctly", false)
241
+ puts "Error: #{e.message}"
242
+ end
243
+
244
+ # Test 5: Debug module tracks errors
245
+ begin
246
+ Funicular::Debug.clear_errors
247
+ boundary = Funicular::ErrorBoundary.new
248
+ test_error = RuntimeError.new("Debug test error")
249
+ boundary.catch_error(test_error, { component_class: "DebugTestComponent" })
250
+
251
+ last_err = Funicular::Debug.last_error
252
+ tracked = last_err && last_err[:error_message] == "Debug test error"
253
+ log_test("Debug module tracks errors", tracked)
254
+ rescue => e
255
+ log_test("Debug module tracks errors", false)
256
+ puts "Error: #{e.message}"
257
+ end
258
+
259
+ puts "Unit tests completed! Starting demo..."
260
+ end
261
+
262
+ # Run tests first
263
+ run_tests
264
+
265
+ # Setup debug panel buttons
266
+ JS.document.getElementById('show-errors').addEventListener('click') do
267
+ output = JS.document.getElementById('debug-output')
268
+ errors = Funicular::Debug.error_list
269
+ output.textContent = "Caught Errors:\n#{errors}"
270
+ end
271
+
272
+ JS.document.getElementById('show-tree').addEventListener('click') do
273
+ output = JS.document.getElementById('debug-output')
274
+ tree = Funicular::Debug.component_tree
275
+ output.textContent = "Component Tree:\n#{tree}"
276
+ end
277
+
278
+ # Start the demo application
279
+ container = JS.document.getElementById('app')
280
+ Funicular.start(ErrorBoundaryDemo, container: container)
281
+ </script>
282
+ <script src="../init.iife.js"></script>
283
+ </body>
284
+ </html>
@@ -0,0 +1,257 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Funicular Router Test</title>
6
+ <style>
7
+ body { font-family: Arial, sans-serif; padding: 20px; }
8
+ nav { margin-bottom: 20px; padding: 10px; background: #f0f0f0; border-radius: 5px; }
9
+ nav a { margin-right: 15px; text-decoration: none; color: #007bff; cursor: pointer; }
10
+ nav a:hover { text-decoration: underline; }
11
+ #app { padding: 20px; border: 2px solid #007bff; border-radius: 5px; min-height: 200px; }
12
+ .page-title { color: #333; margin-bottom: 15px; }
13
+ button { padding: 8px 16px; margin: 5px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 4px; }
14
+ button:hover { background: #0056b3; }
15
+ .status { margin-top: 10px; padding: 10px; background: #e7f3ff; border-radius: 4px; }
16
+ .info { margin-bottom: 10px; padding: 10px; background: #fff3cd; border-radius: 4px; font-size: 14px; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div><a href="../index.html">Back</a></div>
21
+ <h1>Funicular Router Test</h1>
22
+
23
+ <div class="info">
24
+ This demo uses pathname-based routing (History API).
25
+ Navigation links use <code>Funicular.router.navigate()</code>.
26
+ </div>
27
+
28
+ <nav id="nav"></nav>
29
+
30
+ <div id="app"></div>
31
+
32
+ <script type="text/ruby">
33
+ # Navigation component (shared across all pages)
34
+ class NavComponent < Funicular::Component
35
+ def go_to(path)
36
+ ->(event) {
37
+ event.preventDefault
38
+ Funicular.router.navigate(path)
39
+ }
40
+ end
41
+
42
+ def render
43
+ nav do
44
+ a(href: '/login', onclick: go_to('/login')) { 'Login' }
45
+ a(href: '/home', onclick: go_to('/home')) { 'Home' }
46
+ a(href: '/settings', onclick: go_to('/settings')) { 'Settings' }
47
+ a(href: '/users', onclick: go_to('/users')) { 'Users' }
48
+ end
49
+ end
50
+ end
51
+
52
+ class LoginComponent < Funicular::Component
53
+ def initialize_state
54
+ { username: '' }
55
+ end
56
+
57
+ def on_username_input(event)
58
+ patch(username: event.target[:value].to_s)
59
+ end
60
+
61
+ def on_login_click(event)
62
+ event.preventDefault
63
+ Funicular.router.navigate('/home')
64
+ end
65
+
66
+ def render
67
+ div(class: 'login-page') do
68
+ h2(class: 'page-title') { 'Login Page' }
69
+
70
+ div do
71
+ input(
72
+ type: 'text',
73
+ placeholder: 'Username',
74
+ value: state.username,
75
+ oninput: :on_username_input
76
+ )
77
+ end
78
+
79
+ div do
80
+ button(onclick: :on_login_click) { 'Login' }
81
+ end
82
+
83
+ div(class: 'status') do
84
+ "Username: #{state.username}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ class HomeComponent < Funicular::Component
91
+ def initialize_state
92
+ { count: 0 }
93
+ end
94
+
95
+ def increment(event)
96
+ event.preventDefault
97
+ patch(count: state.count + 1)
98
+ end
99
+
100
+ def go_to_settings(event)
101
+ event.preventDefault
102
+ Funicular.router.navigate('/settings')
103
+ end
104
+
105
+ def render
106
+ div(class: 'home-page') do
107
+ h2(class: 'page-title') { 'Home Page' }
108
+
109
+ div do
110
+ "Counter: #{state.count}"
111
+ end
112
+
113
+ div do
114
+ button(onclick: :increment) { 'Increment' }
115
+ button(onclick: :go_to_settings) { 'Go to Settings' }
116
+ end
117
+
118
+ div(class: 'status') do
119
+ "You are on the home page. Click around to test routing!"
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ class SettingsComponent < Funicular::Component
126
+ def initialize_state
127
+ { theme: 'light', notifications: true }
128
+ end
129
+
130
+ def toggle_theme(event)
131
+ event.preventDefault
132
+ new_theme = state.theme == 'light' ? 'dark' : 'light'
133
+ patch(theme: new_theme)
134
+ end
135
+
136
+ def toggle_notifications(event)
137
+ event.preventDefault
138
+ patch(notifications: !state.notifications)
139
+ end
140
+
141
+ def go_to_home(event)
142
+ event.preventDefault
143
+ Funicular.router.navigate('/home')
144
+ end
145
+
146
+ def render
147
+ div(class: 'settings-page') do
148
+ h2(class: 'page-title') { 'Settings Page' }
149
+
150
+ div do
151
+ span { "Theme: #{state.theme}" }
152
+ button(onclick: :toggle_theme) { 'Toggle Theme' }
153
+ end
154
+
155
+ div do
156
+ span { "Notifications: #{state.notifications ? 'ON' : 'OFF'}" }
157
+ button(onclick: :toggle_notifications) { 'Toggle Notifications' }
158
+ end
159
+
160
+ div do
161
+ button(onclick: :go_to_home) { 'Back to Home' }
162
+ end
163
+
164
+ div(class: 'status') do
165
+ "Configure your settings here"
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ # Demonstrates dynamic route with :id parameter
172
+ class UsersComponent < Funicular::Component
173
+ def initialize_state
174
+ { users: [
175
+ { id: 1, name: 'Alice' },
176
+ { id: 2, name: 'Bob' },
177
+ { id: 3, name: 'Charlie' }
178
+ ]}
179
+ end
180
+
181
+ def go_to_user(id)
182
+ ->(event) {
183
+ event.preventDefault
184
+ Funicular.router.navigate("/users/#{id}")
185
+ }
186
+ end
187
+
188
+ def render
189
+ div(class: 'users-page') do
190
+ h2(class: 'page-title') { 'Users List' }
191
+
192
+ ul do
193
+ state.users.each do |user|
194
+ li do
195
+ a(href: "/users/#{user[:id]}", onclick: go_to_user(user[:id])) do
196
+ user[:name]
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ div(class: 'status') do
203
+ "Click a user to see dynamic route with :id parameter"
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ class UserDetailComponent < Funicular::Component
210
+ def initialize_state
211
+ { user_id: @props[:id] }
212
+ end
213
+
214
+ def go_back(event)
215
+ event.preventDefault
216
+ Funicular.router.navigate('/users')
217
+ end
218
+
219
+ def render
220
+ div(class: 'user-detail-page') do
221
+ h2(class: 'page-title') { "User Detail" }
222
+
223
+ div do
224
+ "Viewing user ID: #{state.user_id}"
225
+ end
226
+
227
+ div do
228
+ button(onclick: :go_back) { 'Back to Users' }
229
+ end
230
+
231
+ div(class: 'status') do
232
+ "This page demonstrates dynamic routing with params[:id] = #{state.user_id}"
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ # Start router with Rails-style DSL
239
+ puts "Starting Funicular Router..."
240
+ Funicular.start(container: 'app') do |router|
241
+ # Rails-style route definitions with named routes
242
+ router.get '/login', to: LoginComponent, as: :login
243
+ router.get '/home', to: HomeComponent, as: :home
244
+ router.get '/settings', to: SettingsComponent, as: :settings
245
+ router.get '/users', to: UsersComponent, as: :users
246
+ router.get '/users/:id', to: UserDetailComponent, as: :user
247
+
248
+ router.set_default('/login')
249
+ end
250
+ puts "Router started!"
251
+
252
+ # Mount navigation component separately
253
+ NavComponent.new.mount(JS.global.document.getElementById('nav'))
254
+ </script>
255
+ <script src="../init.iife.js"></script>
256
+ </body>
257
+ </html>