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
data/mrblib/vdom.rb ADDED
@@ -0,0 +1,273 @@
1
+ module Funicular
2
+ module VDOM
3
+ BOOLEAN_ATTRIBUTES = %w[
4
+ disabled checked selected readonly required autofocus multiple
5
+ ]
6
+
7
+ URL_ATTRIBUTES = %w[href src action formaction data poster xlink:href]
8
+
9
+ class VNode
10
+ attr_reader :type, :key
11
+
12
+ def initialize(type)
13
+ @type = type
14
+ end
15
+ end
16
+
17
+ class Element < VNode
18
+ attr_reader :tag, :props, :children
19
+
20
+ def initialize(tag, props = {}, children = [])
21
+ super(:element)
22
+ @tag = tag.to_s
23
+ @key = props.delete(:key)
24
+ @props = props || {}
25
+ @children = normalize_children(children || [])
26
+ end
27
+
28
+ private
29
+
30
+ def normalize_children(children)
31
+ result = [] #: Array[child_t]
32
+ children.each do |child|
33
+ case child
34
+ when VNode
35
+ result << child
36
+ when String
37
+ result << child
38
+ when Array
39
+ # Flatten arrays (typically from .each or .map return values)
40
+ # Recursively normalize nested arrays
41
+ # @type var child: Array[Funicular::VDOM::child_t]
42
+ result.concat(normalize_children(child))
43
+ when nil
44
+ # Skip nil values
45
+ else
46
+ # Convert other types to strings
47
+ result << child.to_s
48
+ end
49
+ end
50
+ result
51
+ end
52
+
53
+ def ==(other)
54
+ return false unless other.is_a?(Element)
55
+ @tag == other.tag && @props == other.props && @children == other.children
56
+ end
57
+ end
58
+
59
+ class Text < VNode
60
+ attr_reader :content
61
+
62
+ def initialize(content)
63
+ super(:text)
64
+ @content = content.to_s
65
+ end
66
+
67
+ def ==(other)
68
+ return false unless other.is_a?(Text)
69
+ @content == other.content
70
+ end
71
+ end
72
+
73
+ class Component < VNode
74
+ attr_reader :component_class, :props
75
+ attr_accessor :instance
76
+
77
+ def initialize(component_class, props = {})
78
+ super(:component)
79
+ @component_class = component_class
80
+ @key = props.delete(:key)
81
+ @props = props
82
+ @instance = nil
83
+ end
84
+
85
+ def ==(other)
86
+ return false unless other.is_a?(Component)
87
+ @component_class == other.component_class && @props == other.props
88
+ end
89
+ end
90
+
91
+ class Renderer
92
+ def initialize(doc = nil)
93
+ @doc = doc || JS.document
94
+ @error_boundary_stack = []
95
+ end
96
+
97
+ def render(vnode, parent = nil)
98
+ case vnode&.type
99
+ when :element
100
+ # @type var vnode: Funicular::VDOM::Element
101
+ render_element(vnode, parent)
102
+ when :text
103
+ # @type var vnode: Funicular::VDOM::Text
104
+ render_text(vnode, parent)
105
+ when :component
106
+ # @type var vnode: Funicular::VDOM::Component
107
+ render_component(vnode, parent)
108
+ else
109
+ raise "Unknown vnode type: #{vnode&.type}"
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # Find the nearest error boundary instance on the stack
116
+ def current_error_boundary
117
+ @error_boundary_stack.last
118
+ end
119
+
120
+ def render_element(element, parent)
121
+ dom_node = @doc.createElement(element.tag)
122
+
123
+ element.props.each do |key, value|
124
+ key_str = key.to_s
125
+ if key_str.start_with?('on')
126
+ # Event handlers are handled by Funicular::Component and should not be set as attributes.
127
+ # warn "Funicular: Attempted to set event handler '#{key_str}' as an attribute. This will be ignored."
128
+ elsif URL_ATTRIBUTES.include?(key_str) && value.to_s.strip.downcase.start_with?('javascript:')
129
+ # Prevent XSS attacks by blocking javascript: URIs in URL attributes
130
+ puts "[WARN] Funicular: Blocked potentially malicious value for attribute '#{key_str}'."
131
+ elsif BOOLEAN_ATTRIBUTES.include?(key_str)
132
+ # Handle boolean attributes
133
+ if value.nil? || value.to_s == "false"
134
+ # Do not set attribute (leave it absent)
135
+ else
136
+ dom_node.setAttribute(key_str, key_str)
137
+ end
138
+ else
139
+ # Attribute
140
+ dom_node.setAttribute(key_str, value.to_s)
141
+ end
142
+ end
143
+
144
+ element.children.each do |child|
145
+ if child.is_a?(VNode)
146
+ child_dom = render(child)
147
+ dom_node.appendChild(child_dom)
148
+ elsif child.is_a?(String)
149
+ text_node = @doc.createTextNode(child)
150
+ dom_node.appendChild(text_node)
151
+ elsif child.is_a?(Array)
152
+ child.each do |c|
153
+ if c.is_a?(VNode)
154
+ child_dom = render(c)
155
+ dom_node.appendChild(child_dom)
156
+ elsif c.is_a?(String)
157
+ text_node = @doc.createTextNode(c)
158
+ dom_node.appendChild(text_node)
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ parent.appendChild(dom_node) if parent
165
+
166
+ dom_node
167
+ end
168
+
169
+ def render_text(text, parent)
170
+ dom_node = @doc.createTextNode(text.content)
171
+ parent.appendChild(dom_node) if parent
172
+ dom_node
173
+ end
174
+
175
+ def render_component(component_vnode, parent)
176
+ instance = component_vnode.component_class.new(component_vnode.props)
177
+ component_vnode.instance = instance
178
+
179
+ is_error_boundary = instance.is_a?(Funicular::ErrorBoundary)
180
+
181
+ # Push error boundary to stack if this component is one
182
+ @error_boundary_stack.push(instance) if is_error_boundary
183
+
184
+ begin
185
+ component_vdom = instance.build_vdom
186
+ dom_node = render(component_vdom, parent)
187
+
188
+ # Check if this ErrorBoundary caught an error during child rendering
189
+ # If so, its @vdom was already set to fallback in the rescue block
190
+ error_was_caught = is_error_boundary && instance.error_caught_during_render
191
+
192
+ if error_was_caught
193
+ # ErrorBoundary caught an error - use the fallback vdom/dom that were set in rescue
194
+ # Note: The div.error-boundary-content created during initial render
195
+ # will be orphaned, but that's acceptable as it's not attached to the DOM
196
+ fallback_vdom = instance.vdom
197
+ fallback_dom = instance.dom_element
198
+
199
+ # Bind events on the fallback DOM
200
+ instance.bind_events(fallback_dom, fallback_vdom)
201
+ instance.collect_refs(fallback_dom, fallback_vdom)
202
+
203
+ # Return the fallback DOM
204
+ fallback_dom
205
+ else
206
+ # Normal case - store VDOM and DOM element
207
+ instance.vdom = component_vdom
208
+ instance.dom_element = dom_node
209
+ instance.bind_events(dom_node, component_vdom)
210
+ instance.collect_refs(dom_node, component_vdom)
211
+ dom_node
212
+ end
213
+ rescue => e
214
+ # Pop error boundary from stack before handling
215
+ @error_boundary_stack.pop if is_error_boundary
216
+
217
+ # Try to find an error boundary to handle this error
218
+ boundary = current_error_boundary
219
+ if boundary && !is_error_boundary
220
+ error_info = {
221
+ component_class: component_vnode.component_class.to_s,
222
+ props: component_vnode.props
223
+ }
224
+
225
+ # Let the error boundary handle the error
226
+ boundary.catch_error(e, error_info)
227
+
228
+ # Re-render the error boundary with fallback UI
229
+ boundary_vdom = boundary.build_vdom
230
+ fallback_dom = render(boundary_vdom, nil)
231
+
232
+ # Update boundary's internal state
233
+ boundary.vdom = boundary_vdom
234
+ boundary.dom_element = fallback_dom
235
+ boundary.mounted = true
236
+ boundary.bind_events(fallback_dom, boundary_vdom)
237
+
238
+ fallback_dom
239
+ else
240
+ # No error boundary to catch this error, let it propagate
241
+ raise e
242
+ end
243
+ ensure
244
+ # Pop error boundary from stack after successful render
245
+ @error_boundary_stack.pop if is_error_boundary && @error_boundary_stack.last == instance
246
+ end
247
+ end
248
+ end
249
+
250
+ def self.create_element(tag, props = {}, *children)
251
+ Element.new(tag, props, children.flatten)
252
+ end
253
+
254
+ def self.create_text(content)
255
+ Text.new(content)
256
+ end
257
+
258
+ def self.render(vnode, container)
259
+ renderer = Renderer.new
260
+ container.innerHTML = ''
261
+ renderer.render(vnode, container)
262
+ end
263
+
264
+ def self.diff(old_vnode, new_vnode)
265
+ Differ.diff(old_vnode, new_vnode)
266
+ end
267
+
268
+ def self.patch(element, patches)
269
+ patcher = Patcher.new
270
+ patcher.apply(element, patches)
271
+ end
272
+ end
273
+ end
data/sig/cable.rbs ADDED
@@ -0,0 +1,65 @@
1
+ module Funicular
2
+ module Cable
3
+ STORAGE_KEY: String
4
+
5
+ def self.create_consumer: (String url) -> Consumer
6
+
7
+ class Consumer
8
+ attr_reader url: String
9
+ attr_reader subscriptions: Subscriptions
10
+ @reconnect_attempts: Integer
11
+
12
+ def initialize: (String url) -> void
13
+ def connect: () -> void
14
+ def send_command: (Hash[Symbol, untyped] command) -> void
15
+ def disconnect: () -> void
16
+ def cleanup: () -> void
17
+ def cleanup_event_listeners: () -> void
18
+
19
+ private def handle_message: (String data) -> void
20
+ private def flush_pending_commands: () -> void
21
+ private def schedule_reconnect: () -> void
22
+ private def calculate_backoff_delay: () -> Integer
23
+ private def setup_visibility_handler: () -> void
24
+ private def schedule_suspend: () -> void
25
+ private def cancel_suspend: () -> void
26
+ private def suspend_connection: () -> void
27
+ private def ensure_connected: () -> void
28
+ private def setup_beforeunload_handler: () -> void
29
+ private def save_pending_to_storage: () -> void
30
+ private def load_pending_from_storage: () -> Array[Hash[Symbol, untyped]]
31
+ private def clear_pending_storage: () -> void
32
+ end
33
+
34
+ class Subscriptions
35
+ @subscriptions: Hash[String, Subscription]
36
+
37
+ def initialize: (Consumer consumer) -> void
38
+ def create: (Hash[Symbol, untyped] params) ?{ (untyped message) -> void } -> Subscription
39
+ def find: (String identifier) -> Subscription?
40
+ def remove: (Subscription subscription) -> void
41
+ def notify_subscription_confirmed: (String identifier) -> void
42
+ def notify_subscription_rejected: (String identifier) -> void
43
+ def notify_message: (String identifier, untyped message) -> void
44
+ end
45
+
46
+ class Subscription
47
+ attr_reader consumer: Consumer
48
+ attr_reader identifier: String
49
+ attr_reader params: Hash[Symbol, untyped]
50
+
51
+ def initialize: (Consumer consumer, String identifier, Hash[Symbol, untyped] params) ?{ (untyped message) -> void } -> void
52
+ def subscribe: () -> void
53
+ def unsubscribe: () -> void
54
+ def perform: (String action, ?Hash[Symbol, untyped] data) -> void
55
+ def on_connected: () { () -> void } -> void
56
+ def on_disconnected: () { () -> void } -> void
57
+ def on_rejected: () { () -> void } -> void
58
+ def notify_connected: () -> void
59
+ def notify_rejected: () -> void
60
+ def notify_received: (untyped message) -> void
61
+
62
+ private def generate_idempotency_key: () -> String
63
+ end
64
+ end
65
+ end
data/sig/component.rbs ADDED
@@ -0,0 +1,141 @@
1
+ module Funicular
2
+ class Component
3
+ class StateAccessor
4
+ def initialize: (Hash[Symbol, untyped] state_hash) -> void
5
+ def []: (Symbol key) -> untyped
6
+ def method_missing: (Symbol method, *untyped args) -> untyped
7
+ def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
8
+ def errors: () -> Hash[Symbol, String]
9
+ end
10
+
11
+ HTML_TAGS: Array[String]
12
+
13
+ attr_accessor props: Hash[Symbol, untyped]
14
+ attr_accessor vdom: VDOM::VNode | VDOM::Text | nil
15
+ attr_accessor dom_element: JS::Object
16
+ attr_accessor mounted: bool
17
+ attr_reader refs: Hash[Symbol, JS::Object]
18
+ @event_listeners: Array[Integer]
19
+ @child_components: Array[Component]
20
+ @suspense_data: Hash[Symbol, untyped]
21
+ @suspense_states: Hash[Symbol, Symbol]
22
+ @suspense_errors: Hash[Symbol, untyped]
23
+ @suspense_pending_timers: Array[Integer]
24
+ self.@suspense_definitions: Hash[Symbol, suspense_definition]
25
+
26
+ def initialize: (?Hash[Symbol, untyped] props) -> void
27
+
28
+ # Styles DSL class methods
29
+ def self.styles: () { (StyleBuilder) -> void } -> void
30
+ def self.styles_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
31
+
32
+ # Suspense DSL class methods
33
+ # Note: Using untyped for Proc types to avoid instance_exec block type mismatch warnings
34
+ type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
35
+ def self.use_suspense: (Symbol name, untyped loader, ?on_resolve: untyped, ?min_delay: Integer) -> void
36
+ def self.suspense_definitions: () -> Hash[Symbol, suspense_definition]
37
+
38
+ def state: () -> StateAccessor
39
+
40
+ # Styles DSL instance method
41
+ def s: () -> StyleAccessor
42
+
43
+ # Public API
44
+ def initialize_state: () -> Hash[Symbol, untyped]
45
+
46
+ # Suspense instance methods
47
+ def load_suspense_data: () -> void
48
+ def load_single_suspense: (Symbol name, ?suspense_definition? definition) -> void
49
+ def reload_suspense: (Symbol name) -> void
50
+ def suspense_loading?: (*Symbol names) -> bool
51
+ def suspense_error?: (Symbol name) -> bool
52
+ def suspense_error: (Symbol name) -> untyped
53
+ def suspense: (fallback: ^() -> void, ?error: ^(untyped) -> void) { () -> void } -> void
54
+
55
+ def patch: (Hash[Symbol, untyped] new_state) -> void
56
+ def mount: (JS::Object container) -> void
57
+ def unmount: () -> void
58
+ def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
59
+ def bind_events: (JS::Object dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
60
+ def build_vdom: () -> (VDOM::VNode | VDOM::Text | nil)
61
+
62
+ # Lifecycle hooks (public, can be overridden in subclasses)
63
+ def component_will_mount: () -> void
64
+ def component_mounted: () -> void
65
+ def component_will_update: () -> void
66
+ def component_updated: () -> void
67
+ def component_will_unmount: () -> void
68
+ def component_unmounted: () -> void
69
+ def component_raised: (Exception e) -> void
70
+
71
+ # Child component helper
72
+ def component: (Class component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
73
+
74
+ # Rails-style form helper
75
+ def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
76
+
77
+ # Rails-style routing helpers
78
+ def link_to: (String path, ?method: Symbol, ?navigate: bool, **untyped options) { -> untyped } -> VDOM::Element
79
+ def method_missing: (Symbol method, *untyped args) -> untyped
80
+ def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
81
+
82
+ # Transition helpers
83
+ def remove_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
84
+ def add_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
85
+
86
+ # Private methods
87
+ private def handle_link_click: (String path) -> void
88
+ private def handle_link_with_method: (String path, Symbol method) -> void
89
+ private def handle_link_response: (HTTP::Response response, String path, Symbol method) -> void
90
+ private def normalize_state_value: (untyped value) -> untyped
91
+ private def re_render: () -> void
92
+ private def normalize_vnode: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
93
+ private def add_data_component_attribute: (VDOM::VNode vnode) -> void
94
+ private def collect_refs: (JS::Object dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Object] refs_map) -> Hash[Symbol, JS::Object]
95
+ private def cleanup_events: () -> void
96
+ private def cleanup_suspense_timers: () -> void
97
+ private def add_child: (untyped child) -> void
98
+ private def collect_child_components: (VDOM::VNode | VDOM::Text | nil vnode) -> void
99
+ private def collect_child_components_recursive: (VDOM::VNode | VDOM::Text | nil vnode, Array[Component] components) -> void
100
+
101
+ # HTML element methods (DSL)
102
+ def div: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
103
+ def span: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
104
+ def p: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
105
+ def a: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
106
+ def h1: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
107
+ def h2: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
108
+ def h3: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
109
+ def h4: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
110
+ def h5: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
111
+ def h6: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
112
+ def ul: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
113
+ def ol: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
114
+ def li: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
115
+ def table: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
116
+ def thead: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
117
+ def tbody: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
118
+ def tr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
119
+ def th: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
120
+ def td: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
121
+ def form: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
122
+ def input: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
123
+ def textarea: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
124
+ def button: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
125
+ def select: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
126
+ def option: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
127
+ def label: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
128
+ def header: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
129
+ def footer: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
130
+ def nav: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
131
+ def section: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
132
+ def article: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
133
+ def aside: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
134
+ def img: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
135
+ def video: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
136
+ def audio: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
137
+ def canvas: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
138
+ def br: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
139
+ def hr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
140
+ end
141
+ end
data/sig/debug.rbs ADDED
@@ -0,0 +1,28 @@
1
+ $__funicular_debug__: Funicular::Debug
2
+
3
+ module Funicular
4
+ module Debug
5
+ self.@error_registry: Array[Hash[Symbol, untyped]]
6
+
7
+ def self.enabled?: () -> bool
8
+ def self.component_registry: () -> Hash[Integer, Funicular::Component]
9
+ def self.register_component: (Funicular::Component) -> Integer?
10
+ def self.unregister_component: (Integer) -> void
11
+ def self.get_component: (Integer) -> Funicular::Component?
12
+ def self.all_components: () -> Array[Funicular::Component]
13
+ def self.component_tree: () -> String
14
+ def self.get_component_state: (Integer) -> String
15
+ def self.get_component_instance_variables: (Integer) -> String
16
+ def self.expose_to_global: () -> void
17
+ def self.report_error: (ErrorBoundary boundary, Exception error, ?(Hash[Symbol, Component] | nil) error_info) -> Hash[Symbol, untyped]?
18
+ def self.clear_errors: () -> void
19
+ def self.error_list: () -> String
20
+ def self.last_error: () -> Hash[Symbol, untyped]?
21
+ def self.error_count: () -> Integer
22
+
23
+ private def self.error_registry: () -> Array[Hash[Symbol, untyped]]
24
+ private def self.get_state_keys: (Funicular::Component) -> Array[String]
25
+ private def self.get_child_ids: (Funicular::Component) -> Array[Integer]
26
+ private def self.collect_direct_children: (Funicular::VDOM::VNode, Array[Funicular::Component]) -> void
27
+ end
28
+ end
data/sig/differ.rbs ADDED
@@ -0,0 +1,18 @@
1
+ module Funicular
2
+ module VDOM
3
+ class Differ
4
+ type children_t = Array[child_t]
5
+ def self.diff: (untyped old_node, untyped new_node) -> Array[patch_t]
6
+
7
+ private def self.diff_text: (Text old_node, Text new_node) -> Array[patch_t]
8
+ private def self.diff_element: (Element old_node, Element new_node) -> Array[patch_t]
9
+ private def self.diff_props: (Hash[Symbol, untyped] old_props, Hash[Symbol, untyped] new_props) -> Hash[Symbol, untyped]
10
+ private def self.diff_children: (children_t old_children, children_t new_children) -> Array[patch_t]
11
+ private def self.diff_children_with_keys: (children_t old_children, children_t new_children) -> Array[patch_t]
12
+ private def self.diff_children_by_index: (children_t old_children, children_t new_children) -> Array[patch_t]
13
+ private def self.diff_component: (VDOM::Component old_node, VDOM::Component new_node) -> Array[patch_t]
14
+
15
+ private def self.props_changed_excluding_procs?: (Hash[Symbol, untyped] old_props, Hash[Symbol, untyped] new_props) -> bool
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ module Funicular
2
+ class EnvironmentInquirer
3
+ def initialize: (EnvironmentInquirer | String env) -> void
4
+ def to_s: () -> String
5
+ def inspect: () -> String
6
+ def ==: (String other) -> bool
7
+ def development?: () -> bool
8
+ end
9
+ end
10
+
@@ -0,0 +1,14 @@
1
+ module Funicular
2
+ class ErrorBoundary < Component
3
+ attr_accessor error_caught_during_render: bool?
4
+
5
+ def initialize_state: () -> Hash[Symbol, untyped]
6
+ def catch_error: (Exception error, ?Hash[Symbol, untyped]? error_info) -> bool
7
+ def reset: () -> void
8
+ def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
9
+
10
+ private def render_fallback: () -> (VDOM::VNode | String)
11
+ private def default_fallback: () -> VDOM::VNode
12
+ private def render_children: () -> VDOM::VNode
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module Funicular
2
+ module FileUpload
3
+ self.@callback_counters: Array[Integer]
4
+
5
+ JS_HELPER_CODE: String
6
+
7
+ def self.mount: () -> void
8
+ def self.select_file_with_preview: (String input_id) { (JS::Object? file, String? preview_url) -> void } -> void
9
+ def self.upload_with_formdata: (
10
+ String url,
11
+ ?fields: Hash[untyped, untyped],
12
+ ?file_field: String?,
13
+ ?file: JS::Object?
14
+ ) { (Hash[untyped, untyped] result) -> void } -> void
15
+ def self.store_file: (String input_id, ?String storage_key) -> JS::Object?
16
+ def self.retrieve_file: (?String storage_key) -> JS::Object?
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ module Funicular
2
+ class FormBuilder
3
+ attr_reader component: Component
4
+ attr_reader model_key: Symbol
5
+ attr_reader options: Hash[Symbol, untyped]
6
+
7
+ def initialize: (Component component, Symbol model_key, ?Hash[Symbol, untyped] options) -> void
8
+
9
+ # Field builder methods
10
+ def text_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
11
+ def password_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
12
+ def email_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
13
+ def number_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
14
+ def textarea: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
15
+ def checkbox: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
16
+ def select: (Symbol field_name, Array[untyped] choices, ?Hash[Symbol, untyped] options) -> VDOM::Element
17
+ def file_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
18
+ def submit: (?String label, ?Hash[Symbol, untyped] options) -> VDOM::Element
19
+ def label: (Symbol field_name, ?String? text, ?Hash[Symbol, untyped] options) -> VDOM::Element
20
+
21
+ # Generic field builder
22
+ private def build_field: (Symbol field_name, String field_type, Hash[Symbol, untyped] field_options) -> VDOM::Element
23
+
24
+ # Helper methods for nested state access
25
+ private def get_nested_value: (Component::StateAccessor state, String key_path) -> untyped
26
+ private def set_nested_value: (Symbol model_key, String field_key, untyped new_value) -> void
27
+ private def deep_merge_value: (Hash[Symbol, untyped] hash, Array[String] keys, untyped value) -> Hash[Symbol, untyped]
28
+ end
29
+ end
data/sig/funicular.rbs CHANGED
@@ -1,4 +1,14 @@
1
1
  module Funicular
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ def self.version: () -> String
5
+ def self.env: () -> EnvironmentInquirer
6
+ def self.env=: (EnvironmentInquirer | String environment) -> EnvironmentInquirer?
7
+ def self.router: () -> Router?
8
+ def self.load_schemas: (Hash[singleton(Model), String] models) ?{ () -> void } -> void
9
+ def self.start: (?singleton(Component)? component_class, ?container: String | JS::Object, ?props: Hash[Symbol, untyped]) ?{ (Router router) -> void } -> (Component | Router)
10
+ def self.configure_forms: () ?{ (Hash[Symbol, String]) -> void } -> void
11
+ def self.configure_debug: () ?{ (self) -> void } -> void
12
+ def self.export_debug_config: () -> void
13
+
4
14
  end
data/sig/http.rbs ADDED
@@ -0,0 +1,22 @@
1
+ module Funicular
2
+ module HTTP
3
+ class Response
4
+ attr_reader data: untyped
5
+ attr_reader status: Integer
6
+ attr_reader ok: bool
7
+
8
+ def initialize: (Integer status, untyped data) -> void
9
+ def error?: () -> bool
10
+ def error_message: () -> String?
11
+ end
12
+
13
+ def self.get: (String url) { (Response) -> void } -> void
14
+ def self.post: (String url, ?Hash[untyped, untyped]? body) { (Response) -> void } -> void
15
+ def self.patch: (String url, ?Hash[untyped, untyped]? body) { (Response) -> void } -> void
16
+ def self.delete: (String url) { (Response) -> void } -> void
17
+ def self.put: (String url, ?Hash[untyped, untyped]? body) { (Response) -> void } -> void
18
+ def self.csrf_token: () -> String?
19
+
20
+ private def self.request: (String method, String url, Hash[untyped, untyped]? body) { (Response) -> void } -> void
21
+ end
22
+ end
data/sig/model.rbs ADDED
@@ -0,0 +1,23 @@
1
+ module Funicular
2
+ class Model
3
+ attr_reader id: untyped
4
+ @changed_attributes: Hash[String, untyped]
5
+
6
+ def self.schema: () -> Hash[String, Hash[String, untyped]]
7
+ def self.schema=: (Hash[String, Hash[String, untyped]] schema) -> Hash[String, Hash[String, untyped]]
8
+ def self.endpoints: () -> Hash[String, Hash[String, String]]
9
+ def self.endpoints=: (Hash[String, Hash[String, String]] endpoints) -> Hash[String, Hash[String, String]]
10
+
11
+ def initialize: (?Hash[untyped, untyped] attributes) -> void
12
+ def self.load_schema: (Hash[String, untyped] schema_data) -> void
13
+
14
+ def self.all: (?Hash[untyped, untyped] params) ?{ (Array[Model]? instances, String? error) -> void } -> void
15
+ def self.find: (?untyped id, ?endpoint_name: String, ?model_class: singleton(Model)) ?{ (Model? instance, String? error) -> void } -> void
16
+ def self.create: (Hash[untyped, untyped] attrs, ?model_class: singleton(Model)) ?{ (Model? instance, String? error) -> void } -> void
17
+ def self.destroy: (?untyped id) ?{ (bool success, untyped result) -> void } -> void
18
+
19
+ def update: (?Hash[untyped, untyped]? attrs) ?{ (bool success, untyped result) -> void } -> void
20
+ def destroy: () ?{ (bool success, untyped result) -> void } -> void
21
+ def reload: () ?{ (Model? instance, String? error) -> void } -> void
22
+ end
23
+ end