inquirex-ui 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b9f77805162e181fd5c4401a8f4d49842b12dddb2983f0a9d7776ca41f278c3b
4
+ data.tar.gz: aa6be396e7fb41a435c1d83d1c11222a8b0d8eab520498ce5f4df5e05a012c73
5
+ SHA512:
6
+ metadata.gz: 83867ad8179b78032bd8b2b69d5ee803ede04ec5d6aa6ad465ec38956a64531c2d3d0fbe38b2784e1765899fa477503bad29b0f7ea5201f0fd78957762643dd5
7
+ data.tar.gz: 9c9992150abd220d2af4cb8c7fc07b19ac2e152d87c12d31172d17f02adbc4acb0b24d953aa49cf83743e78243dc2ac819bee1e6cddc41434f8f93477d41872d
@@ -0,0 +1,153 @@
1
+ # Relaxed.Ruby.Style
2
+ ## Version 2.5
3
+
4
+ Style/Alias:
5
+ Enabled: false
6
+ StyleGuide: https://relaxed.ruby.style/#stylealias
7
+
8
+ Style/AsciiComments:
9
+ Enabled: false
10
+ StyleGuide: https://relaxed.ruby.style/#styleasciicomments
11
+
12
+ Style/BeginBlock:
13
+ Enabled: false
14
+ StyleGuide: https://relaxed.ruby.style/#stylebeginblock
15
+
16
+ Style/BlockDelimiters:
17
+ Enabled: false
18
+ StyleGuide: https://relaxed.ruby.style/#styleblockdelimiters
19
+
20
+ Style/CommentAnnotation:
21
+ Enabled: false
22
+ StyleGuide: https://relaxed.ruby.style/#stylecommentannotation
23
+
24
+ Style/Documentation:
25
+ Enabled: false
26
+ StyleGuide: https://relaxed.ruby.style/#styledocumentation
27
+
28
+ Layout/DotPosition:
29
+ Enabled: false
30
+ StyleGuide: https://relaxed.ruby.style/#layoutdotposition
31
+
32
+ Style/DoubleNegation:
33
+ Enabled: false
34
+ StyleGuide: https://relaxed.ruby.style/#styledoublenegation
35
+
36
+ Style/EndBlock:
37
+ Enabled: false
38
+ StyleGuide: https://relaxed.ruby.style/#styleendblock
39
+
40
+ Style/FormatString:
41
+ Enabled: false
42
+ StyleGuide: https://relaxed.ruby.style/#styleformatstring
43
+
44
+ Style/IfUnlessModifier:
45
+ Enabled: false
46
+ StyleGuide: https://relaxed.ruby.style/#styleifunlessmodifier
47
+
48
+ Style/Lambda:
49
+ Enabled: false
50
+ StyleGuide: https://relaxed.ruby.style/#stylelambda
51
+
52
+ Style/ModuleFunction:
53
+ Enabled: false
54
+ StyleGuide: https://relaxed.ruby.style/#stylemodulefunction
55
+
56
+ Style/MultilineBlockChain:
57
+ Enabled: false
58
+ StyleGuide: https://relaxed.ruby.style/#stylemultilineblockchain
59
+
60
+ Style/NegatedIf:
61
+ Enabled: false
62
+ StyleGuide: https://relaxed.ruby.style/#stylenegatedif
63
+
64
+ Style/NegatedWhile:
65
+ Enabled: false
66
+ StyleGuide: https://relaxed.ruby.style/#stylenegatedwhile
67
+
68
+ Style/NumericPredicate:
69
+ Enabled: false
70
+ StyleGuide: https://relaxed.ruby.style/#stylenumericpredicate
71
+
72
+ Style/ParallelAssignment:
73
+ Enabled: false
74
+ StyleGuide: https://relaxed.ruby.style/#styleparallelassignment
75
+
76
+ Style/PercentLiteralDelimiters:
77
+ Enabled: false
78
+ StyleGuide: https://relaxed.ruby.style/#stylepercentliteraldelimiters
79
+
80
+ Style/PerlBackrefs:
81
+ Enabled: false
82
+ StyleGuide: https://relaxed.ruby.style/#styleperlbackrefs
83
+
84
+ Style/Semicolon:
85
+ Enabled: false
86
+ StyleGuide: https://relaxed.ruby.style/#stylesemicolon
87
+
88
+ Style/SignalException:
89
+ Enabled: false
90
+ StyleGuide: https://relaxed.ruby.style/#stylesignalexception
91
+
92
+ Style/SingleLineBlockParams:
93
+ Enabled: false
94
+ StyleGuide: https://relaxed.ruby.style/#stylesinglelineblockparams
95
+
96
+ Style/SingleLineMethods:
97
+ Enabled: false
98
+ StyleGuide: https://relaxed.ruby.style/#stylesinglelinemethods
99
+
100
+ Layout/SpaceBeforeBlockBraces:
101
+ Enabled: false
102
+ StyleGuide: https://relaxed.ruby.style/#layoutspacebeforeblockbraces
103
+
104
+ Layout/SpaceInsideParens:
105
+ Enabled: false
106
+ StyleGuide: https://relaxed.ruby.style/#layoutspaceinsideparens
107
+
108
+ Style/SpecialGlobalVars:
109
+ Enabled: false
110
+ StyleGuide: https://relaxed.ruby.style/#stylespecialglobalvars
111
+
112
+ Style/StringLiterals:
113
+ Enabled: false
114
+ StyleGuide: https://relaxed.ruby.style/#stylestringliterals
115
+
116
+ Style/TrailingCommaInArguments:
117
+ Enabled: false
118
+ StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarguments
119
+
120
+ Style/TrailingCommaInArrayLiteral:
121
+ Enabled: false
122
+ StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarrayliteral
123
+
124
+ Style/TrailingCommaInHashLiteral:
125
+ Enabled: false
126
+ StyleGuide: https://relaxed.ruby.style/#styletrailingcommainhashliteral
127
+
128
+ Style/SymbolArray:
129
+ Enabled: false
130
+ StyleGuide: http://relaxed.ruby.style/#stylesymbolarray
131
+
132
+ Style/WhileUntilModifier:
133
+ Enabled: false
134
+ StyleGuide: https://relaxed.ruby.style/#stylewhileuntilmodifier
135
+
136
+ Style/WordArray:
137
+ Enabled: false
138
+ StyleGuide: https://relaxed.ruby.style/#stylewordarray
139
+
140
+ Lint/AmbiguousRegexpLiteral:
141
+ Enabled: false
142
+ StyleGuide: https://relaxed.ruby.style/#lintambiguousregexpliteral
143
+
144
+ Lint/AssignmentInCondition:
145
+ Enabled: false
146
+ StyleGuide: https://relaxed.ruby.style/#lintassignmentincondition
147
+
148
+ Layout/LineLength:
149
+ Enabled: false
150
+
151
+ Metrics:
152
+ Enabled: false
153
+
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.2
data/.secrets.baseline ADDED
@@ -0,0 +1,127 @@
1
+ {
2
+ "version": "1.5.0",
3
+ "plugins_used": [
4
+ {
5
+ "name": "ArtifactoryDetector"
6
+ },
7
+ {
8
+ "name": "AWSKeyDetector"
9
+ },
10
+ {
11
+ "name": "AzureStorageKeyDetector"
12
+ },
13
+ {
14
+ "name": "Base64HighEntropyString",
15
+ "limit": 4.5
16
+ },
17
+ {
18
+ "name": "BasicAuthDetector"
19
+ },
20
+ {
21
+ "name": "CloudantDetector"
22
+ },
23
+ {
24
+ "name": "DiscordBotTokenDetector"
25
+ },
26
+ {
27
+ "name": "GitHubTokenDetector"
28
+ },
29
+ {
30
+ "name": "GitLabTokenDetector"
31
+ },
32
+ {
33
+ "name": "HexHighEntropyString",
34
+ "limit": 3.0
35
+ },
36
+ {
37
+ "name": "IbmCloudIamDetector"
38
+ },
39
+ {
40
+ "name": "IbmCosHmacDetector"
41
+ },
42
+ {
43
+ "name": "IPPublicDetector"
44
+ },
45
+ {
46
+ "name": "JwtTokenDetector"
47
+ },
48
+ {
49
+ "name": "KeywordDetector",
50
+ "keyword_exclude": ""
51
+ },
52
+ {
53
+ "name": "MailchimpDetector"
54
+ },
55
+ {
56
+ "name": "NpmDetector"
57
+ },
58
+ {
59
+ "name": "OpenAIDetector"
60
+ },
61
+ {
62
+ "name": "PrivateKeyDetector"
63
+ },
64
+ {
65
+ "name": "PypiTokenDetector"
66
+ },
67
+ {
68
+ "name": "SendGridDetector"
69
+ },
70
+ {
71
+ "name": "SlackDetector"
72
+ },
73
+ {
74
+ "name": "SoftlayerDetector"
75
+ },
76
+ {
77
+ "name": "SquareOAuthDetector"
78
+ },
79
+ {
80
+ "name": "StripeDetector"
81
+ },
82
+ {
83
+ "name": "TelegramBotTokenDetector"
84
+ },
85
+ {
86
+ "name": "TwilioKeyDetector"
87
+ }
88
+ ],
89
+ "filters_used": [
90
+ {
91
+ "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
92
+ },
93
+ {
94
+ "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
95
+ "min_level": 2
96
+ },
97
+ {
98
+ "path": "detect_secrets.filters.heuristic.is_indirect_reference"
99
+ },
100
+ {
101
+ "path": "detect_secrets.filters.heuristic.is_likely_id_string"
102
+ },
103
+ {
104
+ "path": "detect_secrets.filters.heuristic.is_lock_file"
105
+ },
106
+ {
107
+ "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
108
+ },
109
+ {
110
+ "path": "detect_secrets.filters.heuristic.is_potential_uuid"
111
+ },
112
+ {
113
+ "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
114
+ },
115
+ {
116
+ "path": "detect_secrets.filters.heuristic.is_sequential_string"
117
+ },
118
+ {
119
+ "path": "detect_secrets.filters.heuristic.is_swagger_file"
120
+ },
121
+ {
122
+ "path": "detect_secrets.filters.heuristic.is_templated_secret"
123
+ }
124
+ ],
125
+ "results": {},
126
+ "generated_at": "2026-04-13T21:29:03Z"
127
+ }
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-04-13
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Konstantin Gredeskoul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,360 @@
1
+ # inquirex-ui
2
+
3
+ Widget rendering hints for [Inquirex](https://github.com/flowengine-rb/inquirex) flow
4
+ definitions.
5
+
6
+ `inquirex-ui` enriches an Inquirex `Definition` with **framework-agnostic rendering
7
+ metadata** — the `widget` DSL verb attaches a `WidgetHint` to each step node for each
8
+ rendering target (`:desktop`, `:mobile`, `:tty`, …), describing the preferred UI control.
9
+ Frontend adapters
10
+ (`inquirex-tty`, `inquirex-js`, `inquirex-rails`) consume these hints to pick the right
11
+ renderer. This gem produces enriched JSON — **no HTML or JavaScript is generated here**.
12
+
13
+ ## Installation
14
+
15
+ ```ruby
16
+ gem "inquirex-ui"
17
+ ```
18
+
19
+ `inquirex-ui` depends only on `inquirex` (the core gem). Both have zero required
20
+ runtime dependencies beyond Ruby itself.
21
+
22
+ ## `Inquirex.define` vs `Inquirex::UI.define`
23
+
24
+ The two entry points share the same DSL — the only difference is whether nodes carry
25
+ widget hints:
26
+
27
+ | | Entry point | Node class | `widget` verb |
28
+ |---|---|---|---|
29
+ | **`inquirex`** | `Inquirex.define` | `Inquirex::Node` | not available |
30
+ | **`inquirex-ui`** | `Inquirex::UI.define` | `Inquirex::UI::Node` | available |
31
+
32
+ Both return an `Inquirex::Definition` that works identically with `Inquirex::Engine`,
33
+ `to_json`/`from_json`, `MermaidExporter`, etc. `inquirex-ui` is a strict superset —
34
+ everything you can write in `Inquirex.define` works unchanged inside
35
+ `Inquirex::UI.define`.
36
+
37
+ **Use `Inquirex.define`** when you only need the flow logic: server-side processing,
38
+ pure Ruby scripts, tests that don't care about rendering.
39
+
40
+ **Use `Inquirex::UI.define`** when the definition will be consumed by a frontend adapter
41
+ (`inquirex-tty`, `inquirex-js`, Rails views) that needs to know *how* to render each
42
+ question.
43
+
44
+ ## Quick Start
45
+
46
+ ```ruby
47
+ require "inquirex/ui"
48
+
49
+ definition = Inquirex::UI.define id: "tax-intake", version: "1.0.0" do
50
+ meta title: "Tax Preparation Intake"
51
+
52
+ start :filing_status
53
+
54
+ ask :filing_status do
55
+ type :enum
56
+ question "What is your filing status?"
57
+ options single: "Single",
58
+ married_jointly: "Married Filing Jointly",
59
+ married_separately: "Married Filing Separately",
60
+ head_of_household: "Head of Household"
61
+ widget target: :desktop, type: :radio_group, columns: 2
62
+ widget target: :mobile, type: :dropdown
63
+ widget target: :tty, type: :select
64
+ transition to: :income
65
+ end
66
+
67
+ ask :income do
68
+ type :currency
69
+ question "Estimated annual income?"
70
+ # No widget call — WidgetRegistry provides :currency_input by default
71
+ transition to: :has_deductions
72
+ end
73
+
74
+ confirm :has_deductions do
75
+ question "Any deductions to claim?"
76
+ # WidgetRegistry default: :toggle (desktop) / :yes_no_buttons (mobile)
77
+ transition to: :deductions, if_rule: equals(:has_deductions, true)
78
+ transition to: :done
79
+ end
80
+
81
+ ask :deductions do
82
+ type :multi_enum
83
+ question "Select applicable deductions."
84
+ options %w[Mortgage Charitable Medical]
85
+ widget target: :desktop, type: :checkbox_group, layout: :vertical
86
+ widget target: :tty, type: :multi_select
87
+ transition to: :done
88
+ end
89
+
90
+ say :done do
91
+ text "Thank you for completing the intake."
92
+ end
93
+ end
94
+ ```
95
+
96
+ The definition object is a standard `Inquirex::Definition` — fully compatible with
97
+ `Inquirex::Engine`, `to_json`/`from_json`, and `Inquirex::Graph::MermaidExporter`.
98
+ Every step is an `Inquirex::UI::Node` (a subclass of `Inquirex::Node`) with widget hints
99
+ attached.
100
+
101
+ ______________________________________________________________________
102
+
103
+ ## The `widget` DSL Verb
104
+
105
+ `widget` is available inside any step block when using `Inquirex::UI.define`. Call it
106
+ once per target context. The `target:` keyword defaults to `:desktop`, and the design
107
+ is open-ended — any adapter may introduce its own targets (`:watch`, `:tv`, `:embed`, …).
108
+
109
+ ```ruby
110
+ ask :priority do
111
+ type :enum
112
+ question "How urgent is this?"
113
+ options low: "Low", medium: "Medium", high: "High"
114
+
115
+ widget target: :desktop, type: :radio_group, columns: 3
116
+ widget target: :mobile, type: :dropdown
117
+ widget target: :tty, type: :select
118
+
119
+ transition to: :next_step
120
+ end
121
+ ```
122
+
123
+ `type:` names the widget. All remaining keyword arguments become the hint's `options`
124
+ hash and are passed through to the adapter unchanged.
125
+
126
+ `target:` can be any symbol. The three built-in targets are `:desktop`, `:mobile`, and
127
+ `:tty`. Additional targets (`:watch`, `:embed`, …) are valid — adapters can define their
128
+ own without any changes to this gem.
129
+
130
+ ### Widget DSL Method
131
+
132
+ | Signature | Purpose |
133
+ |-----------|---------|
134
+ | `widget(target: :desktop, type:, **opts)` | Set rendering hint for the given target |
135
+
136
+ `target:` defaults to `:desktop` — you can omit it when only a single hint is needed:
137
+
138
+ ```ruby
139
+ widget type: :text_input, placeholder: "e.g. Alice"
140
+ ```
141
+
142
+ When no `widget` call is made for a target, `WidgetRegistry` fills in the default for
143
+ that data type. Adapters may also call `#effective_widget_hint_for(target:)` on any node
144
+ to get the explicit hint or the registry fallback in one call.
145
+
146
+ ______________________________________________________________________
147
+
148
+ ## Widget Registry (Auto-Defaults)
149
+
150
+ When no explicit `widget` call is made, `WidgetRegistry` provides a sensible default per
151
+ data type and rendering context:
152
+
153
+ | Data Type | `:desktop` default | `:mobile` default | `:tty` default |
154
+ |-----------|-------------------|------------------|----------------|
155
+ | `:string` | `text_input` | `text_input` | `text_input` |
156
+ | `:text` | `textarea` | `textarea` | `multiline` |
157
+ | `:integer` | `number_input` | `number_input` | `number_input` |
158
+ | `:decimal` | `number_input` | `number_input` | `number_input` |
159
+ | `:currency` | `currency_input` | `currency_input` | `number_input` |
160
+ | `:boolean` | `toggle` | `yes_no_buttons` | `yes_no` |
161
+ | `:enum` | `radio_group` | `dropdown` | `select` |
162
+ | `:multi_enum` | `checkbox_group` | `checkbox_group` | `multi_select` |
163
+ | `:date` | `date_picker` | `date_picker` | `text_input` |
164
+ | `:email` | `email_input` | `email_input` | `text_input` |
165
+ | `:phone` | `phone_input` | `phone_input` | `text_input` |
166
+
167
+ Display steps (`say`, `header`, `btw`, `warning`) have no type, so they return `nil`
168
+ widget hints by default.
169
+
170
+ ### All Recognized Widget Types
171
+
172
+ **Web / graphical (desktop + mobile):**
173
+
174
+ ```
175
+ text_input textarea number_input
176
+ currency_input toggle yes_no_buttons
177
+ radio_group dropdown checkbox_group
178
+ multi_select_dropdown date_picker email_input
179
+ phone_input
180
+ ```
181
+
182
+ **TTY — maps to [tty-prompt](https://github.com/piotrmurach/tty-prompt) methods:**
183
+
184
+ | Widget type | tty-prompt method | Notes |
185
+ |-------------|-------------------|-------|
186
+ | `text_input` | `prompt.ask` | Single-line text |
187
+ | `multiline` | `prompt.multiline` | Multi-line text (`:text` type) |
188
+ | `number_input` | `prompt.ask` | With numeric conversion |
189
+ | `yes_no` | `prompt.yes?` | Boolean gate |
190
+ | `select` | `prompt.select` | Single choice from list |
191
+ | `multi_select` | `prompt.multi_select` | Multiple choices from list |
192
+ | `enum_select` | `prompt.enum_select` | Numbered menu |
193
+ | `mask` | `prompt.mask` | Hidden/password input |
194
+ | `slider` | `prompt.slider` | Numeric range slider |
195
+
196
+ Adapters are not required to support every widget type and should fall back gracefully
197
+ (e.g. `radio_group` → `dropdown` in a TTY context).
198
+
199
+ ______________________________________________________________________
200
+
201
+ ## `WidgetHint`
202
+
203
+ `WidgetHint` is an immutable `Data` class (`Data.define`) that pairs a widget type with
204
+ an options hash:
205
+
206
+ ```ruby
207
+ hint = Inquirex::UI::WidgetHint.new(type: :radio_group, options: { columns: 2 })
208
+
209
+ hint.type # => :radio_group
210
+ hint.options # => { columns: 2 }
211
+ hint.to_h # => { "type" => "radio_group", "columns" => 2 }
212
+
213
+ Inquirex::UI::WidgetHint.from_h({ "type" => "radio_group", "columns" => 2 })
214
+ # => #<data Inquirex::UI::WidgetHint type=:radio_group, options={columns: 2}>
215
+ ```
216
+
217
+ The options hash is merged inline with `"type"` in the serialized form — no extra nesting.
218
+
219
+ ______________________________________________________________________
220
+
221
+ ## `UI::Node`
222
+
223
+ `Inquirex::UI::Node` extends `Inquirex::Node` with a `widget_hints` hash and two
224
+ accessor methods:
225
+
226
+ | Attribute / Method | Returns | Description |
227
+ |--------------------|---------|-------------|
228
+ | `widget_hints` | `Hash{Symbol => WidgetHint}?` | All explicit hints keyed by target, or `nil` for display nodes |
229
+ | `widget_hint_for(target:)` | `WidgetHint?` | Explicit hint for the given target, or `nil` |
230
+ | `effective_widget_hint_for(target:)` | `WidgetHint?` | Explicit hint or registry default for the given target |
231
+
232
+ ```ruby
233
+ step = definition.step(:filing_status)
234
+
235
+ step.widget_hints
236
+ # => { desktop: #<WidgetHint type=:radio_group, options={columns: 2}>,
237
+ # mobile: #<WidgetHint type=:dropdown, options={}> }
238
+
239
+ step.widget_hint_for(target: :desktop)
240
+ # => #<data WidgetHint type=:radio_group, options={columns: 2}>
241
+
242
+ step.effective_widget_hint_for(target: :mobile)
243
+ # => #<data WidgetHint type=:dropdown, options={}>
244
+ ```
245
+
246
+ Collecting steps produced by `Inquirex::UI.define` always return a non-nil value from
247
+ `effective_widget_hint_for` (registry fills in the gap). Display steps (`say`, `header`,
248
+ `btw`, `warning`) return `nil`.
249
+
250
+ ______________________________________________________________________
251
+
252
+ ## JSON Serialization
253
+
254
+ Widget hints round-trip through JSON:
255
+
256
+ ```ruby
257
+ definition.to_json
258
+ # => '{"id":"tax-intake","start":"filing_status","steps":{"filing_status":{"verb":"ask",
259
+ # "type":"enum","question":"What is your filing status?","options":[...],"transitions":[...],
260
+ # "widget":{"desktop":{"type":"radio_group","columns":2},"mobile":{"type":"dropdown"},
261
+ # "tty":{"type":"select"}}},...}}'
262
+ ```
263
+
264
+ ### Wire Format for a Step with Hints
265
+
266
+ ```json
267
+ {
268
+ "verb": "ask",
269
+ "type": "enum",
270
+ "question": "What is your filing status?",
271
+ "options": [
272
+ { "value": "single", "label": "Single" },
273
+ { "value": "married_jointly", "label": "Married Filing Jointly" }
274
+ ],
275
+ "transitions": [{ "to": "income" }],
276
+ "widget": {
277
+ "desktop": { "type": "radio_group", "columns": 2 },
278
+ "mobile": { "type": "dropdown" },
279
+ "tty": { "type": "select" }
280
+ }
281
+ }
282
+ ```
283
+
284
+ There is a single `"widget"` key whose value is an object keyed by target name. New
285
+ targets can be added without changing the schema. Display steps (`say`, `header`, `btw`,
286
+ `warning`) carry no `"widget"` key at all.
287
+
288
+ ### Deserializing UI Nodes
289
+
290
+ `Inquirex::Definition.from_json` restores the base `Inquirex::Node` class by default
291
+ (the core gem has no knowledge of `inquirex-ui`). To restore `UI::Node` instances:
292
+
293
+ ```ruby
294
+ json = definition.to_json
295
+ step_hash = JSON.parse(json)["steps"]["filing_status"]
296
+
297
+ restored = Inquirex::UI::Node.from_h(:filing_status, step_hash)
298
+ restored.widget_hints.type # => :radio_group
299
+ ```
300
+
301
+ ______________________________________________________________________
302
+
303
+ ## Engine Compatibility
304
+
305
+ `Inquirex::UI.define` returns a standard `Inquirex::Definition`. It works unchanged with
306
+ `Inquirex::Engine`:
307
+
308
+ ```ruby
309
+ engine = Inquirex::Engine.new(definition)
310
+ engine.answer("single") # :filing_status → :income
311
+ engine.answer(75_000.00) # :income → :has_deductions
312
+ engine.answer(true) # :has_deductions → :deductions
313
+ engine.answer(%w[Mortgage]) # :deductions → :done
314
+ engine.advance # past :done
315
+ engine.finished? # => true
316
+ engine.answers
317
+ # => { filing_status: "single", income: 75000.0, has_deductions: true, deductions: ["Mortgage"] }
318
+ ```
319
+
320
+ ______________________________________________________________________
321
+
322
+ ## For Adapter Authors
323
+
324
+ If you are building an adapter (TTY terminal, JS widget, Rails views):
325
+
326
+ 1. Parse the flow JSON to get step definitions.
327
+ 1. For each collecting step, read `step["widget"]` — it is an object keyed by target name.
328
+ 1. Look up your target (e.g. `step["widget"]["tty"]`). The `"type"` key names the widget; additional keys are options.
329
+ 1. If `"widget"` is absent, call `WidgetRegistry.default_hint_for(type)` (Ruby) or
330
+ implement the same lookup table in your target language.
331
+ 1. Fall back gracefully — not every adapter supports every widget type.
332
+
333
+ ```ruby
334
+ # Example adapter lookup
335
+ def widget_for(step_hash, target: :desktop)
336
+ widget_map = step_hash["widget"]
337
+ raw = widget_map&.fetch(target.to_s, nil) || widget_map&.fetch("desktop", nil)
338
+ return Inquirex::UI::WidgetRegistry.default_hint_for(step_hash["type"], context: target) unless raw
339
+
340
+ Inquirex::UI::WidgetHint.from_h(raw)
341
+ end
342
+ ```
343
+
344
+ ______________________________________________________________________
345
+
346
+ ## Development
347
+
348
+ ```bash
349
+ bundle install
350
+ bundle exec rspec # run specs (90 examples)
351
+ bundle exec rspec --format documentation
352
+ bundle exec rubocop # lint
353
+ ```
354
+
355
+ The core `inquirex` gem is loaded from a relative path (`../inquirex`) in development.
356
+ See `Gemfile` for details.
357
+
358
+ ## License
359
+
360
+ MIT. See [LICENSE.txt](LICENSE.txt).