funicular 0.2.1 → 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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +2 -2
  4. data/Rakefile +23 -15
  5. data/demo/test_chartjs.html +17 -17
  6. data/demo/test_component.html +15 -15
  7. data/demo/test_error_boundary.html +41 -41
  8. data/demo/test_router.html +57 -57
  9. data/demo/tic-tac-toe.html +29 -29
  10. data/docs/architecture.md +43 -30
  11. data/lib/funicular/compiler.rb +13 -13
  12. data/lib/funicular/helpers/picoruby_helper.rb +24 -1
  13. data/lib/funicular/ssr/runtime.rb +2 -0
  14. data/lib/funicular/ssr.rb +2 -1
  15. data/lib/funicular/testing/node_runner.rb +1 -1
  16. data/lib/funicular/vendor/mrbc/VERSION +1 -0
  17. data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
  18. data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
  19. data/lib/funicular/vendor/picoruby/VERSION +1 -1
  20. data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
  21. data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
  22. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  23. data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
  24. data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
  25. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  26. data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
  27. data/lib/funicular/vendor/picoruby-test-node/picoruby.js +85 -84
  28. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
  29. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
  30. data/lib/funicular/version.rb +1 -1
  31. data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +47 -46
  32. data/lib/tasks/funicular.rake +1 -1
  33. data/minitest/commands_routes_test.rb +97 -0
  34. data/minitest/compiler_test.rb +195 -0
  35. data/minitest/configuration_test.rb +64 -0
  36. data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
  37. data/minitest/form_for_test.rb +2 -2
  38. data/minitest/funicular_test.rb +28 -2
  39. data/minitest/hydration_test.rb +2 -2
  40. data/minitest/middleware_test.rb +154 -0
  41. data/minitest/picoruby_helper_test.rb +139 -0
  42. data/minitest/route_parser_test.rb +139 -0
  43. data/minitest/schema_test.rb +23 -0
  44. data/minitest/ssr_test.rb +57 -0
  45. data/minitest/support/rails_stub.rb +59 -0
  46. data/minitest/test_helper.rb +20 -0
  47. data/minitest/testing_test.rb +267 -0
  48. data/minitest/view_context_test.rb +101 -0
  49. data/mrblib/component.rb +158 -237
  50. data/mrblib/debug.rb +7 -6
  51. data/mrblib/differ.rb +3 -1
  52. data/mrblib/error_boundary.rb +19 -27
  53. data/mrblib/form_builder.rb +28 -24
  54. data/mrblib/funicular.rb +2 -1
  55. data/mrblib/html_serializer.rb +14 -13
  56. data/mrblib/http.rb +12 -1
  57. data/mrblib/patcher.rb +12 -9
  58. data/mrblib/router.rb +9 -5
  59. data/mrblib/runtime.rb +28 -0
  60. data/mrblib/styles.rb +13 -17
  61. data/mrblib/vdom.rb +90 -9
  62. data/mrblib/view_context.rb +135 -0
  63. data/sig/component.rbs +34 -85
  64. data/sig/error_boundary.rbs +4 -4
  65. data/sig/form_builder.rbs +2 -1
  66. data/sig/html_serializer.rbs +2 -1
  67. data/sig/http.rbs +1 -0
  68. data/sig/patcher.rbs +1 -1
  69. data/sig/router.rbs +2 -13
  70. data/sig/runtime.rbs +13 -0
  71. data/sig/styles.rbs +3 -4
  72. data/sig/vdom.rbs +11 -2
  73. data/sig/view_context.rbs +47 -0
  74. metadata +18 -5
  75. data/lib/funicular/vendor/picorbc/VERSION +0 -1
  76. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa8a7ba40464c4e28ecc1b784612a9a62a8a88af985efd2508f142e03443fe9a
4
- data.tar.gz: a81683de0aa13f2d91f21ccbddf6b8ed802f673567591adee4b12de6d0453d91
3
+ metadata.gz: 56720ac2a4a7635871515e66f55bf5ed272bb49cb7cc3a403671494720ef16d7
4
+ data.tar.gz: 7bf29c93a9480501c53e9b10cb33d29eb73ec22964300aed1c3e124206a3ab40
5
5
  SHA512:
6
- metadata.gz: b2809132d4fc00812c2443c2fb42e6744bbb25449beee2cb03c2b6cd2302792da21ba7d6a98ac462f7bcdc10b17af87988fa8fb38e635fa747d3bbba1e346c60
7
- data.tar.gz: df0f03841a9f97ab6269f088be4b16a488430419cf7b5d196615488d098d149c6241bc08f495b477f3f539a72df13725412be09768fb90c3d96560677c9e2390
6
+ metadata.gz: 297df4162ffbe5259e2fe5c7738a310714a42ff3e1704386560c65d27e9a6d5aa19f5374e700915b388e37f9879926e2454296e9a39bfee4080d04310e2df295
7
+ data.tar.gz: c56295e03efd38afc789b876717f3ce05a0631f02197843362ff627084db12fc3ecdd925b9c8ce437dbc8cff1b6fca9ff9b2b9396a5c22e24fd203fbcf50740d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,60 @@
2
2
 
3
3
  ### Added
4
4
 
5
+ - 0.3.0 rendering architecture: `render(h)` now receives a `ViewContext`
6
+ facade for elements, components, forms, styles, resources, and routes.
7
+ - Per-app `Runtime` context for route helpers and renderer/serializer
8
+ propagation, enabling isolated route helper sets across multiple apps.
9
+
10
+ ### Breaking Changes
11
+
12
+ - **0.3.0 is a deliberate breaking DSL redesign. Existing Funicular
13
+ components written for 0.2.x require source changes.**
14
+ - Component render methods must now accept a view context:
15
+ `def render(h)`. The former implicit component-level DSL methods for HTML
16
+ tags, `component`, `form_for`, `link_to`, `button_to`, `suspense`, styles,
17
+ resources, and route helpers have been removed.
18
+ - HTML and framework helpers are now called through `h`, for example
19
+ `h.div`, `h.component(...)`, `h.form_for(...)`, `h.link_to(...)`,
20
+ `h.suspense(...)`, `h.styles[...]`, `h.resources[...]`, and `h.routes`.
21
+ - Component state reads are explicit: use `state[:key]`, `state.fetch(:key)`,
22
+ or `h.state[:key]`. The old `state.key_name` method-style access has been
23
+ removed.
24
+ - Style definitions are explicit: use `styles { |css| css.define(...) }`.
25
+ The old dynamic style definition DSL has been removed.
26
+ - Component children are stored as `VDOM::Component#children`. The old
27
+ `children_block` prop path has been removed and no compatibility shim is
28
+ provided.
29
+ - Route helpers are scoped by `Funicular::Runtime`; global
30
+ `Funicular::RouteHelpers` injection has been removed. Code that depends on
31
+ route helpers should use `h.routes`.
32
+ - `FormBuilder`, `ErrorBoundary`, SSR, hydration, renderer, patcher, and HTML
33
+ serialization now operate through the same `ViewContext` / `Runtime`
34
+ architecture.
35
+
36
+ ### Changed
37
+
38
+ - Since mruby-compiler-prism, which used to be mruby-compiler2 producing
39
+ picorbc, has become the default compiler for mruby, we changed the name
40
+ from picorbc to mrbc.
41
+
42
+ ### Fixed
43
+
44
+ - Harden VDOM rendering against HTML and script injection in both SSR and
45
+ browser rendering: validate tag and attribute names, reject `script`
46
+ elements, and consistently block case-obfuscated event handlers, `srcdoc`,
47
+ and unsafe URL schemes including control-character variants.
48
+
49
+ ## [0.2.1] - 2026-06-15
50
+
51
+ ### Added
52
+
53
+ - **Funicular::Component**: Add `name` field to form to find state changed.
54
+
55
+ ## [0.2.0] - 2026-06-11
56
+
57
+ ### Added
58
+
5
59
  - **Funicular::Store DSL**: Declarative client-side stores backed by
6
60
  IndexedDB. Subclass `Funicular::Store::Singleton` (one value per scope)
7
61
  or `Funicular::Store::Collection` (ordered list per scope) and use
data/README.md CHANGED
@@ -72,13 +72,13 @@ git clone --recurse-submodules https://github.com/picoruby/picoruby.git
72
72
  cd picoruby/mrbgems/picoruby-funicular
73
73
  ```
74
74
 
75
- The CRubyGem side (`lib/`, `funicular.gemspec`, etc.) can be developed and tested independently inside that directory, but `rake copy_wasm` — which vendors the PicoRuby.wasm and picorbc wasm artifacts into the gem — relies on sibling directories within the picoruby repository (`mrbgems/picoruby-wasm/npm/`).
75
+ The CRubyGem side (`lib/`, `funicular.gemspec`, etc.) can be developed and tested independently inside that directory, but `rake copy_wasm` — which vendors the PicoRuby.wasm and mrbc wasm artifacts into the gem — relies on sibling directories within the picoruby repository (`mrbgems/picoruby-wasm/npm/`).
76
76
  Running it from a standalone checkout will fail.
77
77
 
78
78
  ## Testing
79
79
 
80
80
  - CRubygem (Rails integration) test: `rake test` in this repository
81
- - PicoGem Funicular test: `rake test:gems:picoruby[picoruby-funicular]` in picoruby where mrbgems/picoruby-funicular exists as a submodule
81
+ - PicoGem Funicular test: `rake test:gems:wasm[picoruby-funicular]` in picoruby where mrbgems/picoruby-funicular exists as a submodule
82
82
 
83
83
  ## Contributing
84
84
 
data/Rakefile CHANGED
@@ -9,9 +9,17 @@ Rake::TestTask.new(:test) do |t|
9
9
  t.test_files = FileList["minitest/**/*_test.rb"]
10
10
  end
11
11
 
12
+ namespace :test do
13
+ desc "Run the test suite with SimpleCov coverage measurement (report in coverage/)"
14
+ task :coverage do
15
+ ENV["COVERAGE"] = "1"
16
+ Rake::Task["test"].invoke
17
+ end
18
+ end
19
+
12
20
  task default: :test
13
21
 
14
- desc "Copy picoruby and picorbc wasm artifacts from picoruby-wasm into the gem"
22
+ desc "Copy picoruby and mrbc wasm artifacts from picoruby-wasm into the gem"
15
23
  task :copy_wasm do
16
24
  require "fileutils"
17
25
  require "json"
@@ -59,25 +67,25 @@ task :copy_wasm do
59
67
  puts " wrote picoruby/VERSION (#{picoruby_version})"
60
68
 
61
69
  # ------------------------------------------------------------------
62
- # 2) picorbc compiler (node CLI, run by Funicular::Compiler)
70
+ # 2) mrbc compiler (node CLI, run by Funicular::Compiler)
63
71
  # ------------------------------------------------------------------
64
- picorbc_src = File.join(npm_root, "picorbc", "debug")
65
- picorbc_dest = File.join(vendor_root, "picorbc")
66
- abort "Missing #{picorbc_src}" unless Dir.exist?(picorbc_src)
72
+ mrbc_src = File.join(npm_root, "mrbc", "debug")
73
+ mrbc_dest = File.join(vendor_root, "mrbc")
74
+ abort "Missing #{mrbc_src}" unless Dir.exist?(mrbc_src)
67
75
 
68
- picorbc_version = JSON.parse(File.read(File.join(npm_root, "picorbc", "package.json"))).fetch("version")
69
- picorbc_files = %w[picorbc.js picorbc.wasm]
76
+ mrbc_version = JSON.parse(File.read(File.join(npm_root, "mrbc", "package.json"))).fetch("version")
77
+ mrbc_files = %w[mrbc.js mrbc.wasm]
70
78
 
71
- FileUtils.rm_rf(picorbc_dest)
72
- FileUtils.mkdir_p(picorbc_dest)
73
- picorbc_files.each do |fname|
74
- src_file = File.join(picorbc_src, fname)
79
+ FileUtils.rm_rf(mrbc_dest)
80
+ FileUtils.mkdir_p(mrbc_dest)
81
+ mrbc_files.each do |fname|
82
+ src_file = File.join(mrbc_src, fname)
75
83
  abort "Missing file: #{src_file}" unless File.exist?(src_file)
76
- FileUtils.copy_file(src_file, File.join(picorbc_dest, fname))
84
+ FileUtils.copy_file(src_file, File.join(mrbc_dest, fname))
77
85
  end
78
- File.chmod(0755, File.join(picorbc_dest, "picorbc.js"))
79
- File.write(File.join(picorbc_dest, "VERSION"), "#{picorbc_version}\n")
80
- puts " copied picorbc (#{picorbc_version})"
86
+ File.chmod(0755, File.join(mrbc_dest, "mrbc.js"))
87
+ File.write(File.join(mrbc_dest, "VERSION"), "#{mrbc_version}\n")
88
+ puts " copied mrbc (#{mrbc_version})"
81
89
 
82
90
  # ------------------------------------------------------------------
83
91
  # 3) PicoRuby runtime for DOM-backed Node.js tests
@@ -44,7 +44,7 @@
44
44
  return
45
45
  end
46
46
 
47
- current_data = state.data_sets[state.month_index]
47
+ current_data = state[:data_sets][state[:month_index]]
48
48
 
49
49
  config = {
50
50
  type: 'line',
@@ -69,7 +69,7 @@
69
69
  end
70
70
 
71
71
  def component_updated
72
- puts "component_updated: Updating Chart.js with data index #{state.month_index}"
72
+ puts "component_updated: Updating Chart.js with data index #{state[:month_index]}"
73
73
  update_chart
74
74
  end
75
75
 
@@ -80,22 +80,22 @@
80
80
 
81
81
  def next_data_set(event)
82
82
  event.preventDefault if event
83
- new_index = (state.month_index + 1) % state.data_sets.size
84
- puts "next_data_set: Changing index from #{state.month_index} to #{new_index}"
83
+ new_index = (state[:month_index] + 1) % state[:data_sets].size
84
+ puts "next_data_set: Changing index from #{state[:month_index]} to #{new_index}"
85
85
  patch(month_index: new_index)
86
86
  end
87
87
 
88
88
  def prev_data_set(event)
89
89
  event.preventDefault if event
90
- new_index = (state.month_index - 1) % state.data_sets.size
91
- puts "prev_data_set: Changing index from #{state.month_index} to #{new_index}"
90
+ new_index = (state[:month_index] - 1) % state[:data_sets].size
91
+ puts "prev_data_set: Changing index from #{state[:month_index]} to #{new_index}"
92
92
  patch(month_index: new_index)
93
93
  end
94
94
 
95
95
  def update_chart
96
96
  return unless @chart
97
97
 
98
- current_data = state.data_sets[state.month_index]
98
+ current_data = state[:data_sets][state[:month_index]]
99
99
  puts "update_chart: Setting data to #{current_data[:labels].join(', ')}"
100
100
 
101
101
  # Convert to JS objects
@@ -118,18 +118,18 @@
118
118
  puts "update_chart: Chart.js update('active') called"
119
119
  end
120
120
 
121
- def render
122
- div(id: "dashboard") do
123
- h2 { "Sales Dashboard" }
124
- div(id: "chart-container") do
125
- canvas(ref: :chart_canvas, id: "chart")
121
+ def render(h)
122
+ h.div(id: "dashboard") do |hh|
123
+ hh.h2 { "Sales Dashboard" }
124
+ hh.div(id: "chart-container") do |hhh|
125
+ hhh.canvas(ref: :chart_canvas, id: "chart")
126
126
  end
127
- div do
128
- button(id: "prev-btn", onclick: :prev_data_set) { "Previous Quarter" }
129
- button(id: "next-btn", onclick: :next_data_set) { "Next Quarter" }
127
+ hh.div do |hhh|
128
+ hhh.button(id: "prev-btn", onclick: :prev_data_set) { "Previous Quarter" }
129
+ hhh.button(id: "next-btn", onclick: :next_data_set) { "Next Quarter" }
130
130
  end
131
- div(id: "info") do
132
- current = state.data_sets[state.month_index]
131
+ hh.div(id: "info") do
132
+ current = state[:data_sets][state[:month_index]]
133
133
  "Current data: #{current[:labels].join(', ')}"
134
134
  end
135
135
  end
@@ -40,26 +40,26 @@
40
40
 
41
41
  def increment(event)
42
42
  event.preventDefault if event
43
- patch(count: state.count + 1)
43
+ patch(count: state[:count] + 1)
44
44
  end
45
45
 
46
46
  def decrement(event)
47
47
  event.preventDefault if event
48
- patch(count: state.count - 1)
48
+ patch(count: state[:count] - 1)
49
49
  end
50
50
 
51
- def render
52
- div(id: "counter-app") do
53
- h2 { "Counter: #{state.count}" }
54
- div(id: "status") do
55
- "Mounted: #{state.mounted}"
51
+ def render(h)
52
+ h.div(id: "counter-app") do |hh|
53
+ hh.h2 { "Counter: #{state[:count]}" }
54
+ hh.div(id: "status") do
55
+ "Mounted: #{state[:mounted]}"
56
56
  end
57
- div do
58
- button(id: "increment-btn", onclick: :increment) { "+1" }
59
- button(id: "decrement-btn", onclick: :decrement) { "-1" }
57
+ hh.div do |hhh|
58
+ hhh.button(id: "increment-btn", onclick: :increment) { "+1" }
59
+ hhh.button(id: "decrement-btn", onclick: :decrement) { "-1" }
60
60
  end
61
- div(ref: :display, id: "display") do
62
- "Current value: #{state.count}"
61
+ hh.div(ref: :display, id: "display") do
62
+ "Current value: #{state[:count]}"
63
63
  end
64
64
  end
65
65
  end
@@ -89,7 +89,7 @@
89
89
  # Test 2: Initial state
90
90
  begin
91
91
  counter = TestCounter.new
92
- initial_state = counter.state.count == 0
92
+ initial_state = counter.state[:count] == 0
93
93
  log_test("Initial state", initial_state)
94
94
  rescue => e
95
95
  log_test("Initial state", false)
@@ -139,13 +139,13 @@
139
139
  has_button = btn != nil
140
140
 
141
141
  # Test handler logic by calling it directly (standard testing approach)
142
- initial_count = counter.state.count
142
+ initial_count = counter.state[:count]
143
143
  counter.increment(nil)
144
144
  sleep 0.05
145
145
 
146
146
  h2_element = JS.document.querySelector('#counter-app h2')
147
147
  text = h2_element.textContent.to_s
148
- handler_works = counter.state.count == initial_count + 1 && text.include?("#{initial_count + 1}")
148
+ handler_works = counter.state[:count] == initial_count + 1 && text.include?("#{initial_count + 1}")
149
149
 
150
150
  log_test("Event handler (click)", has_button && handler_works)
151
151
  rescue => e
@@ -91,7 +91,7 @@
91
91
  <script type="text/ruby">
92
92
  # A component that always throws an error during render
93
93
  class BrokenComponent < Funicular::Component
94
- def render
94
+ def render(h)
95
95
  raise "BrokenComponent intentionally threw this error!"
96
96
  end
97
97
  end
@@ -107,80 +107,80 @@
107
107
  patch(count: state[:count] + 1)
108
108
  end
109
109
 
110
- def render
111
- div(class: 'demo-card') do
112
- h3 { "Working Component" }
113
- p { "Count: #{state[:count]}" }
114
- button(onclick: :increment) { "Increment" }
110
+ def render(h)
111
+ h.div(class: 'demo-card') do |hh|
112
+ hh.h3 { "Working Component" }
113
+ hh.p { "Count: #{state[:count]}" }
114
+ hh.button(onclick: :increment) { "Increment" }
115
115
  end
116
116
  end
117
117
  end
118
118
 
119
119
  # Main demo application
120
120
  class ErrorBoundaryDemo < Funicular::Component
121
- def render
121
+ def render(h)
122
122
  error_handler = ->(error, info) {
123
123
  puts "[Demo] Error caught by ErrorBoundary:"
124
124
  puts " Error: #{error.message}"
125
125
  puts " Component: #{info&.dig(:component_class)}"
126
126
  }
127
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." }
128
+ custom_fallback = ->(hh, error) {
129
+ hh.div(style: 'background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; padding: 20px; border-radius: 8px;') do |hhh|
130
+ hhh.h3(style: 'margin-top: 0;') { "Custom Error Handler" }
131
+ hhh.p { "Caught: #{error.message}" }
132
+ hhh.p(style: 'font-size: 12px; opacity: 0.8;') { "This is a custom fallback provided via props." }
133
133
  end
134
134
  }
135
135
 
136
- div do
137
- h2 { "Error Boundary Demonstrations" }
136
+ h.div do |hh|
137
+ hh.h2 { "Error Boundary Demonstrations" }
138
138
 
139
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)
140
+ hh.div(class: 'demo-card') do |card|
141
+ card.h3 { "Demo 1: Default Fallback UI" }
142
+ card.p { "ErrorBoundary wraps a component that always throws. Default fallback is shown." }
143
+ card.component(Funicular::ErrorBoundary, on_error: error_handler) do |boundary|
144
+ boundary.component(BrokenComponent)
145
145
  end
146
146
  end
147
147
 
148
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,
149
+ hh.div(class: 'demo-card') do |card|
150
+ card.h3 { "Demo 2: Custom Fallback UI" }
151
+ card.p { "ErrorBoundary with a custom fallback prop." }
152
+ card.component(Funicular::ErrorBoundary,
153
153
  fallback: custom_fallback,
154
154
  on_error: error_handler
155
- ) do
156
- component(BrokenComponent)
155
+ ) do |boundary|
156
+ boundary.component(BrokenComponent)
157
157
  end
158
158
  end
159
159
 
160
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." }
161
+ hh.div(class: 'demo-card') do |card|
162
+ card.h3 { "Demo 3: Isolated Error Boundaries" }
163
+ card.p { "Each component has its own ErrorBoundary. One failure doesn't affect others." }
164
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)
165
+ card.div(style: 'display: flex; gap: 15px; flex-wrap: wrap;') do |row|
166
+ row.div(style: 'flex: 1; min-width: 200px;') do |col|
167
+ col.h4 { "Working:" }
168
+ col.component(Funicular::ErrorBoundary) do |boundary|
169
+ boundary.component(WorkingComponent)
170
170
  end
171
171
  end
172
172
 
173
- div(style: 'flex: 1; min-width: 200px;') do
174
- h4 { "Broken:" }
175
- component(Funicular::ErrorBoundary) do
176
- component(BrokenComponent)
173
+ row.div(style: 'flex: 1; min-width: 200px;') do |col|
174
+ col.h4 { "Broken:" }
175
+ col.component(Funicular::ErrorBoundary) do |boundary|
176
+ boundary.component(BrokenComponent)
177
177
  end
178
178
  end
179
179
 
180
- div(style: 'flex: 1; min-width: 200px;') do
181
- h4 { "Also Working:" }
182
- component(Funicular::ErrorBoundary) do
183
- component(WorkingComponent)
180
+ row.div(style: 'flex: 1; min-width: 200px;') do |col|
181
+ col.h4 { "Also Working:" }
182
+ col.component(Funicular::ErrorBoundary) do |boundary|
183
+ boundary.component(WorkingComponent)
184
184
  end
185
185
  end
186
186
  end
@@ -39,12 +39,12 @@
39
39
  }
40
40
  end
41
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' }
42
+ def render(h)
43
+ h.nav do |hh|
44
+ hh.a(href: '/login', onclick: go_to('/login')) { 'Login' }
45
+ hh.a(href: '/home', onclick: go_to('/home')) { 'Home' }
46
+ hh.a(href: '/settings', onclick: go_to('/settings')) { 'Settings' }
47
+ hh.a(href: '/users', onclick: go_to('/users')) { 'Users' }
48
48
  end
49
49
  end
50
50
  end
@@ -63,25 +63,25 @@
63
63
  Funicular.router.navigate('/home')
64
64
  end
65
65
 
66
- def render
67
- div(class: 'login-page') do
68
- h2(class: 'page-title') { 'Login Page' }
66
+ def render(h)
67
+ h.div(class: 'login-page') do |hh|
68
+ hh.h2(class: 'page-title') { 'Login Page' }
69
69
 
70
- div do
71
- input(
70
+ hh.div do |hhh|
71
+ hhh.input(
72
72
  type: 'text',
73
73
  placeholder: 'Username',
74
- value: state.username,
74
+ value: state[:username],
75
75
  oninput: :on_username_input
76
76
  )
77
77
  end
78
78
 
79
- div do
80
- button(onclick: :on_login_click) { 'Login' }
79
+ hh.div do |hhh|
80
+ hhh.button(onclick: :on_login_click) { 'Login' }
81
81
  end
82
82
 
83
- div(class: 'status') do
84
- "Username: #{state.username}"
83
+ hh.div(class: 'status') do
84
+ "Username: #{state[:username]}"
85
85
  end
86
86
  end
87
87
  end
@@ -94,7 +94,7 @@
94
94
 
95
95
  def increment(event)
96
96
  event.preventDefault
97
- patch(count: state.count + 1)
97
+ patch(count: state[:count] + 1)
98
98
  end
99
99
 
100
100
  def go_to_settings(event)
@@ -102,20 +102,20 @@
102
102
  Funicular.router.navigate('/settings')
103
103
  end
104
104
 
105
- def render
106
- div(class: 'home-page') do
107
- h2(class: 'page-title') { 'Home Page' }
105
+ def render(h)
106
+ h.div(class: 'home-page') do |hh|
107
+ hh.h2(class: 'page-title') { 'Home Page' }
108
108
 
109
- div do
110
- "Counter: #{state.count}"
109
+ hh.div do
110
+ "Counter: #{state[:count]}"
111
111
  end
112
112
 
113
- div do
114
- button(onclick: :increment) { 'Increment' }
115
- button(onclick: :go_to_settings) { 'Go to Settings' }
113
+ hh.div do |hhh|
114
+ hhh.button(onclick: :increment) { 'Increment' }
115
+ hhh.button(onclick: :go_to_settings) { 'Go to Settings' }
116
116
  end
117
117
 
118
- div(class: 'status') do
118
+ hh.div(class: 'status') do
119
119
  "You are on the home page. Click around to test routing!"
120
120
  end
121
121
  end
@@ -129,13 +129,13 @@
129
129
 
130
130
  def toggle_theme(event)
131
131
  event.preventDefault
132
- new_theme = state.theme == 'light' ? 'dark' : 'light'
132
+ new_theme = state[:theme] == 'light' ? 'dark' : 'light'
133
133
  patch(theme: new_theme)
134
134
  end
135
135
 
136
136
  def toggle_notifications(event)
137
137
  event.preventDefault
138
- patch(notifications: !state.notifications)
138
+ patch(notifications: !state[:notifications])
139
139
  end
140
140
 
141
141
  def go_to_home(event)
@@ -143,25 +143,25 @@
143
143
  Funicular.router.navigate('/home')
144
144
  end
145
145
 
146
- def render
147
- div(class: 'settings-page') do
148
- h2(class: 'page-title') { 'Settings Page' }
146
+ def render(h)
147
+ h.div(class: 'settings-page') do |hh|
148
+ hh.h2(class: 'page-title') { 'Settings Page' }
149
149
 
150
- div do
151
- span { "Theme: #{state.theme}" }
152
- button(onclick: :toggle_theme) { 'Toggle Theme' }
150
+ hh.div do |hhh|
151
+ hhh.span { "Theme: #{state[:theme]}" }
152
+ hhh.button(onclick: :toggle_theme) { 'Toggle Theme' }
153
153
  end
154
154
 
155
- div do
156
- span { "Notifications: #{state.notifications ? 'ON' : 'OFF'}" }
157
- button(onclick: :toggle_notifications) { 'Toggle Notifications' }
155
+ hh.div do |hhh|
156
+ hhh.span { "Notifications: #{state[:notifications] ? 'ON' : 'OFF'}" }
157
+ hhh.button(onclick: :toggle_notifications) { 'Toggle Notifications' }
158
158
  end
159
159
 
160
- div do
161
- button(onclick: :go_to_home) { 'Back to Home' }
160
+ hh.div do |hhh|
161
+ hhh.button(onclick: :go_to_home) { 'Back to Home' }
162
162
  end
163
163
 
164
- div(class: 'status') do
164
+ hh.div(class: 'status') do
165
165
  "Configure your settings here"
166
166
  end
167
167
  end
@@ -185,21 +185,21 @@
185
185
  }
186
186
  end
187
187
 
188
- def render
189
- div(class: 'users-page') do
190
- h2(class: 'page-title') { 'Users List' }
188
+ def render(h)
189
+ h.div(class: 'users-page') do |hh|
190
+ hh.h2(class: 'page-title') { 'Users List' }
191
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
192
+ hh.ul do |hhh|
193
+ state[:users].each do |user|
194
+ hhh.li do |item|
195
+ item.a(href: "/users/#{user[:id]}", onclick: go_to_user(user[:id])) do
196
196
  user[:name]
197
197
  end
198
198
  end
199
199
  end
200
200
  end
201
201
 
202
- div(class: 'status') do
202
+ hh.div(class: 'status') do
203
203
  "Click a user to see dynamic route with :id parameter"
204
204
  end
205
205
  end
@@ -216,20 +216,20 @@
216
216
  Funicular.router.navigate('/users')
217
217
  end
218
218
 
219
- def render
220
- div(class: 'user-detail-page') do
221
- h2(class: 'page-title') { "User Detail" }
219
+ def render(h)
220
+ h.div(class: 'user-detail-page') do |hh|
221
+ hh.h2(class: 'page-title') { "User Detail" }
222
222
 
223
- div do
224
- "Viewing user ID: #{state.user_id}"
223
+ hh.div do
224
+ "Viewing user ID: #{state[:user_id]}"
225
225
  end
226
226
 
227
- div do
228
- button(onclick: :go_back) { 'Back to Users' }
227
+ hh.div do |hhh|
228
+ hhh.button(onclick: :go_back) { 'Back to Users' }
229
229
  end
230
230
 
231
- div(class: 'status') do
232
- "This page demonstrates dynamic routing with params[:id] = #{state.user_id}"
231
+ hh.div(class: 'status') do
232
+ "This page demonstrates dynamic routing with params[:id] = #{state[:user_id]}"
233
233
  end
234
234
  end
235
235
  end