funicular 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +66 -20
  4. data/Rakefile +103 -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/architecture.md +118 -0
  15. data/exe/funicular +32 -0
  16. data/lib/funicular/assets/funicular.css +23 -0
  17. data/lib/funicular/assets/funicular.rb +21 -0
  18. data/lib/funicular/assets/funicular_debug.css +73 -0
  19. data/lib/funicular/assets/funicular_debug.js +183 -0
  20. data/lib/funicular/commands/routes.rb +69 -0
  21. data/lib/funicular/compiler.rb +143 -0
  22. data/lib/funicular/configuration.rb +76 -0
  23. data/lib/funicular/helpers/picoruby_helper.rb +112 -0
  24. data/lib/funicular/middleware.rb +123 -0
  25. data/lib/funicular/plugin.rb +147 -0
  26. data/lib/funicular/railtie.rb +26 -0
  27. data/lib/funicular/route_parser.rb +137 -0
  28. data/lib/funicular/schema.rb +167 -0
  29. data/lib/funicular/ssr/runtime.rb +101 -0
  30. data/lib/funicular/ssr.rb +51 -0
  31. data/lib/funicular/testing/node_runner.mjs +293 -0
  32. data/lib/funicular/testing/node_runner.rb +190 -0
  33. data/lib/funicular/testing.rb +22 -0
  34. data/lib/funicular/vendor/picorbc/VERSION +1 -0
  35. data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
  36. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
  37. data/lib/funicular/vendor/picoruby/VERSION +1 -0
  38. data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
  39. data/lib/funicular/vendor/picoruby/debug/picoruby.js +6423 -0
  40. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  41. data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
  42. data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
  43. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  44. data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -0
  45. data/lib/funicular/vendor/picoruby-test-node/picoruby.js +6885 -0
  46. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
  47. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -0
  48. data/lib/funicular/version.rb +1 -1
  49. data/lib/funicular.rb +32 -1
  50. data/lib/generators/funicular/chat/chat_generator.rb +104 -0
  51. data/lib/generators/funicular/chat/templates/application_cable_channel.rb.tt +4 -0
  52. data/lib/generators/funicular/chat/templates/application_cable_connection.rb.tt +4 -0
  53. data/lib/generators/funicular/chat/templates/create_funicular_chat_messages.rb.tt +10 -0
  54. data/lib/generators/funicular/chat/templates/funicular_chat.css.tt +141 -0
  55. data/lib/generators/funicular/chat/templates/funicular_chat_channel.rb.tt +5 -0
  56. data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +135 -0
  57. data/lib/generators/funicular/chat/templates/funicular_chat_component_picotest.rb.tt +64 -0
  58. data/lib/generators/funicular/chat/templates/funicular_chat_controller.rb.tt +4 -0
  59. data/lib/generators/funicular/chat/templates/funicular_chat_message.rb.tt +13 -0
  60. data/lib/generators/funicular/chat/templates/funicular_chat_messages_controller.rb.tt +23 -0
  61. data/lib/generators/funicular/chat/templates/initializer.rb.tt +4 -0
  62. data/lib/generators/funicular/chat/templates/show.html.erb.tt +6 -0
  63. data/lib/tasks/funicular.rake +218 -0
  64. data/minitest/fixtures/funicular_app/components/greeting_component.rb +16 -0
  65. data/minitest/fixtures/funicular_app/initializer.rb +5 -0
  66. data/minitest/funicular_test.rb +13 -0
  67. data/minitest/hydration_test.rb +87 -0
  68. data/minitest/plugin_test.rb +51 -0
  69. data/minitest/schema_test.rb +106 -0
  70. data/minitest/ssr_test.rb +94 -0
  71. data/minitest/test_helper.rb +7 -0
  72. data/minitest/validations_test.rb +183 -0
  73. data/mrbgem.rake +16 -0
  74. data/mrblib/0_validations.rb +206 -0
  75. data/mrblib/1_validators.rb +180 -0
  76. data/mrblib/cable.rb +432 -0
  77. data/mrblib/component.rb +1050 -0
  78. data/mrblib/debug.rb +208 -0
  79. data/mrblib/differ.rb +254 -0
  80. data/mrblib/environment_inquirer.rb +34 -0
  81. data/mrblib/error_boundary.rb +125 -0
  82. data/mrblib/file_upload.rb +192 -0
  83. data/mrblib/form_builder.rb +300 -0
  84. data/mrblib/funicular.rb +245 -0
  85. data/mrblib/html_serializer.rb +121 -0
  86. data/mrblib/http.rb +183 -0
  87. data/mrblib/model.rb +196 -0
  88. data/mrblib/patcher.rb +269 -0
  89. data/mrblib/router.rb +266 -0
  90. data/mrblib/store.rb +304 -0
  91. data/mrblib/store_collection.rb +171 -0
  92. data/mrblib/store_singleton.rb +79 -0
  93. data/mrblib/styles.rb +83 -0
  94. data/mrblib/vdom.rb +273 -0
  95. data/sig/cable.rbs +66 -0
  96. data/sig/component.rbs +149 -0
  97. data/sig/debug.rbs +28 -0
  98. data/sig/differ.rbs +18 -0
  99. data/sig/environment_iquirer.rbs +10 -0
  100. data/sig/error_boundary.rbs +14 -0
  101. data/sig/file_upload.rbs +18 -0
  102. data/sig/form_builder.rbs +29 -0
  103. data/sig/funicular.rbs +24 -1
  104. data/sig/html_serializer.rbs +20 -0
  105. data/sig/http.rbs +37 -0
  106. data/sig/model.rbs +28 -0
  107. data/sig/patcher.rbs +18 -0
  108. data/sig/router.rbs +44 -0
  109. data/sig/store.rbs +89 -0
  110. data/sig/store_collection.rbs +43 -0
  111. data/sig/store_singleton.rbs +19 -0
  112. data/sig/styles.rbs +25 -0
  113. data/sig/validations.rbs +103 -0
  114. data/sig/vdom.rbs +59 -0
  115. metadata +154 -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>
@@ -0,0 +1,118 @@
1
+ # Architecture (contributor guide)
2
+
3
+ This document is for people working **on** Funicular itself. User-facing
4
+ documentation -- how to build apps with Funicular -- lives at
5
+ [picoruby.org/wasm](https://picoruby.org/funicular-getting-started).
6
+
7
+ Funicular is a unidirectional, Virtual DOM-based SPA framework for
8
+ PicoRuby.wasm. State flows down to the DOM; events flow up through `patch()` to
9
+ update state and trigger a re-render. There is no global store, no auto-tracking
10
+ reactivity, and no separate build tool -- compilation rides on the Rails asset
11
+ pipeline.
12
+
13
+ ## Two sides of one repository
14
+
15
+ Funicular ships as two cooperating pieces (plus a Chrome extension):
16
+
17
+ - **PicoGem `picoruby-funicular`** (`mrblib/`) -- the runtime that executes in
18
+ the browser under PicoRuby.wasm. This is the framework proper.
19
+ - **CRubyGem `funicular`** (`lib/`) -- the Rails integration: the compiler
20
+ wrapper, middleware, railtie, view helpers, and the server-side rendering
21
+ runtime.
22
+
23
+ The same `mrblib/` code also runs under CRuby during SSR (see below), so it must
24
+ stay free of browser-only calls on any server code path
25
+ (`Funicular.server?` is true there).
26
+
27
+ ## `mrblib/` runtime: responsibilities
28
+
29
+ | File(s) | Responsibility |
30
+ |--------------------------------------------------|---------------------------------------------------------------------------|
31
+ | `funicular.rb` | Top-level module: `start`, `router`, `server?`, `debug_color` export |
32
+ | `component.rb` | `Funicular::Component` base: state, props, lifecycle, suspense, refs, styles |
33
+ | `vdom.rb` | Virtual DOM nodes and the element-factory DSL (`div`, `button`, ...) |
34
+ | `differ.rb` | `Differ.diff(old, new)` -- minimal patch set, key-based list reconciliation |
35
+ | `patcher.rb` | `Patcher.apply(dom, patches)` -- apply patches to the real DOM |
36
+ | `html_serializer.rb` | `VDOM::HTMLSerializer` -- VDOM to HTML string (used by SSR) |
37
+ | `router.rb` | Client-side router, route DSL, `RouteHelpers` generation, History API |
38
+ | `model.rb` | Object-REST Mapper (`all`/`find`/`create`/`update`/`destroy`) |
39
+ | `http.rb` | Low-level fetch wrapper, CSRF, IndexedDB response cache |
40
+ | `cable.rb` | ActionCable-compatible consumer/subscription client |
41
+ | `store.rb`, `store_singleton.rb`, `store_collection.rb` | IndexedDB-backed stores, scope API, `subscribes_to`, event dispatch |
42
+ | `form_builder.rb` | `form_for` and field helpers with inline error rendering |
43
+ | `0_validations.rb`, `1_validators.rb` | ActiveModel-style validators and `errors` |
44
+ | `styles.rb` | CSS-in-Ruby `styles` DSL and the `s` helper |
45
+ | `error_boundary.rb` | `ErrorBoundary` component |
46
+ | `file_upload.rb` | File / FormData upload helper |
47
+ | `debug.rb` | Development-only component/error registry for the DevTools extension |
48
+ | `environment_inquirer.rb` | Environment detection (`server?`, `development?`) |
49
+
50
+ The render cycle: a state change calls `patch()`, which rebuilds the component's
51
+ VDOM, diffs it against the previous VDOM with `Differ`, and applies the result
52
+ with `Patcher`. Event handlers are native DOM listeners, re-bound on each render.
53
+
54
+ ## `lib/` Rails integration
55
+
56
+ - `compiler.rb` -- runs the vendored `picorbc` (WebAssembly, via Node.js) to
57
+ compile `app/funicular/**/*.rb` (models, then stores, then components, then
58
+ initializers) into a single `app/assets/builds/app.mrb`. `-g` is added in
59
+ development for debug symbols.
60
+ - `middleware.rb` -- development only; watches `app/funicular/` and recompiles on
61
+ change, then invalidates the Propshaft asset cache.
62
+ - `railtie.rb` -- inserts the middleware, exposes view helpers, loads the rake
63
+ tasks.
64
+ - `helpers/picoruby_helper.rb` -- `picoruby_include_tag`,
65
+ `funicular_app_container`, `funicular_state_tag`.
66
+ - `configuration.rb` -- per-environment runtime source selection
67
+ (`:local_debug` / `:local_dist` / `:cdn`).
68
+ - `ssr.rb`, `ssr/runtime.rb` -- load the `mrblib/` runtime into the Rails process
69
+ and render a route's VDOM to HTML, injecting state for client hydration.
70
+ - `schema.rb` -- introspect an ActiveRecord model's `validators_on` and emit
71
+ client-side validators inline with the schema.
72
+
73
+ ## Vendored artifacts
74
+
75
+ `rake copy_wasm` (run by `rake build`) copies the PicoRuby.wasm runtime and the
76
+ `picorbc` compiler from the sibling `mrbgems/picoruby-wasm/npm/` directory into
77
+ `lib/funicular/vendor/`:
78
+
79
+ - `vendor/picoruby/dist/` -- production runtime build
80
+ - `vendor/picoruby/debug/` -- development runtime build (debug symbols)
81
+ - `vendor/picorbc/` -- the mruby compiler (run through Node.js)
82
+
83
+ Because `copy_wasm` reads sibling directories inside the picoruby repository, it
84
+ only works from within that checkout -- see Development below.
85
+
86
+ ## Server-side rendering, briefly
87
+
88
+ For SSR the `mrblib/` framework is loaded into the Rails process under CRuby.
89
+ `Funicular::SSR.render(path:, state:)` resolves the path against the routes in
90
+ `app/funicular/initializer.rb`, builds the component's VDOM, and serializes it
91
+ with `HTMLSerializer`. The state is also embedded as `window.__FUNICULAR_STATE__`
92
+ so the browser can hydrate the markup rather than rebuild it. Keep `render`
93
+ deterministic and free of browser-only calls so the same code is safe on both
94
+ sides.
95
+
96
+ ## Development
97
+
98
+ This repository is a submodule of
99
+ [picoruby/picoruby](https://github.com/picoruby/picoruby). Do not check it out
100
+ standalone; clone the parent and work from there:
101
+
102
+ ```console
103
+ git clone --recurse-submodules https://github.com/picoruby/picoruby.git
104
+ cd picoruby/mrbgems/picoruby-funicular
105
+ ```
106
+
107
+ The CRubyGem side (`lib/`, `funicular.gemspec`) can be developed and tested
108
+ independently inside that directory, but `rake copy_wasm` relies on sibling
109
+ directories within the picoruby repository and fails from a standalone checkout.
110
+
111
+ PicoGem dependencies are declared in `mrbgem.rake` (picoruby-wasm,
112
+ picoruby-indexeddb, picoruby-json, and the mruby `*-ext` gems).
113
+
114
+ ## Testing
115
+
116
+ - CRubyGem (Rails integration): `rake test` in this repository.
117
+ - PicoGem runtime: `rake test:gems:picoruby[picoruby-funicular]` in the parent
118
+ picoruby repository, where `mrbgems/picoruby-funicular` exists as a submodule.
data/exe/funicular ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "funicular"
6
+
7
+ command = ARGV[0]
8
+
9
+ case command
10
+ when "routes"
11
+ require_relative "../lib/funicular/commands/routes"
12
+ Funicular::Commands::Routes.new.execute
13
+ when "version", "-v", "--version"
14
+ puts Funicular::VERSION
15
+ when "help", "-h", "--help", nil
16
+ puts <<~HELP
17
+ Funicular CLI
18
+
19
+ Usage:
20
+ funicular routes Show all Funicular routes
21
+ funicular version Show Funicular version
22
+ funicular help Show this help message
23
+
24
+ Options:
25
+ -h, --help Show help
26
+ -v, --version Show version
27
+ HELP
28
+ else
29
+ puts "Unknown command: #{command}"
30
+ puts "Run 'funicular help' for usage"
31
+ exit 1
32
+ end
@@ -0,0 +1,23 @@
1
+ /* Funicular base styles.
2
+ *
3
+ * Injected into the page by picoruby_include_tag so that class names emitted
4
+ * from inside the gem (which the host app's CSS pipeline never sees -- e.g.
5
+ * Tailwind only scans the app's own sources) still render. Keep this minimal
6
+ * and namespaced under .funicular-* so it cannot clash with app styles.
7
+ *
8
+ * Apps that prefer their own utilities can override per form via
9
+ * form_for(..., field_error_class: "...", error_class: "..."). */
10
+
11
+ .funicular-field-error {
12
+ border-color: #ef4444 !important; /* red-500, wins over a base border color */
13
+ background-color: #fef2f2; /* red-50 */
14
+ box-shadow: 0 0 0 1px #ef4444;
15
+ }
16
+
17
+ .funicular-error {
18
+ margin-top: 0.25rem;
19
+ color: #dc2626; /* red-600 */
20
+ font-size: 0.875rem;
21
+ line-height: 1.25rem;
22
+ font-weight: 500;
23
+ }
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Exclude app/funicular from autoloading (PicoRuby.wasm code, not for CRuby)
4
+ Rails.autoloaders.main.ignore(Rails.root.join("app/funicular"))
5
+
6
+ # Choose where picoruby_include_tag loads PicoRuby.wasm from, per environment.
7
+ #
8
+ # Available sources:
9
+ # :local_debug - public/picoruby/debug/init.iife.js (debug build, larger, with symbols)
10
+ # :local_dist - public/picoruby/dist/init.iife.js (production build, smaller)
11
+ # :cdn - https://cdn.jsdelivr.net/npm/@picoruby/wasm-wasi@<version>/dist/init.iife.js
12
+ #
13
+ # Defaults:
14
+ # development -> :local_debug
15
+ # test -> :local_debug
16
+ # production -> :local_dist
17
+ #
18
+ # Funicular.configure do |config|
19
+ # config.production_source = :cdn
20
+ # # config.cdn_version = "4.0.0" # defaults to the version vendored in the gem
21
+ # end
@@ -0,0 +1,73 @@
1
+ /* Funicular Component Highlighter - Development Mode Only */
2
+ /* Highlights components with data-component attributes for easier debugging */
3
+
4
+ .funicular-debug-highlight {
5
+ outline: 2px solid var(--funicular-highlight-color, #00ff00) !important;
6
+ outline-offset: -2px !important;
7
+ position: relative !important;
8
+ box-shadow: inset 0 0 0 2px rgba(0, 255, 0, 0.2) !important;
9
+ }
10
+
11
+ .funicular-debug-indicator {
12
+ position: absolute !important;
13
+ bottom: 0 !important;
14
+ right: 0 !important;
15
+ width: 0 !important;
16
+ height: 0 !important;
17
+ border-style: solid !important;
18
+ border-width: 0 0 14px 14px !important;
19
+ border-color: transparent transparent var(--funicular-highlight-color, #00ff00) transparent !important;
20
+ pointer-events: auto !important;
21
+ z-index: 999999 !important;
22
+ cursor: help !important;
23
+ }
24
+
25
+ .funicular-debug-indicator::before {
26
+ content: attr(data-tooltip) !important;
27
+ position: absolute !important;
28
+ bottom: -14px !important;
29
+ right: 0 !important;
30
+ background-color: rgba(0, 0, 0, 0.95) !important;
31
+ color: white !important;
32
+ padding: 6px 10px !important;
33
+ border-radius: 4px !important;
34
+ border: 1px solid var(--funicular-highlight-color, #00ff00) !important;
35
+ font-size: 13px !important;
36
+ font-family: 'Courier New', monospace !important;
37
+ white-space: nowrap !important;
38
+ opacity: 0 !important;
39
+ pointer-events: none !important;
40
+ transform: translateY(8px) !important;
41
+ transition: opacity 0.2s, transform 0.2s !important;
42
+ z-index: 1000000 !important;
43
+ box-shadow: 0 2px 8px rgba(0,0,0,0.5) !important;
44
+ }
45
+
46
+ .funicular-debug-indicator:hover::before {
47
+ opacity: 1 !important;
48
+ transform: translateY(0) !important;
49
+ }
50
+
51
+ /* PicoRuby DevTools Selected Component Highlight */
52
+ .picoruby-devtools-selected {
53
+ position: relative !important;
54
+ }
55
+
56
+ .picoruby-devtools-selected::after {
57
+ content: '' !important;
58
+ position: absolute !important;
59
+ top: 0 !important;
60
+ left: 0 !important;
61
+ right: 0 !important;
62
+ bottom: 0 !important;
63
+ background-color: rgba(0, 122, 204, 0.1) !important;
64
+ background-image: repeating-linear-gradient(
65
+ 45deg,
66
+ transparent,
67
+ transparent 10px,
68
+ rgba(0, 122, 204, 0.15) 10px,
69
+ rgba(0, 122, 204, 0.15) 20px
70
+ ) !important;
71
+ pointer-events: none !important;
72
+ z-index: 999998 !important;
73
+ }