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 +7 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.ruby-version +1 -0
- data/.secrets.baseline +127 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +360 -0
- data/Rakefile +36 -0
- data/exe/inquirex-ui +3 -0
- data/justfile +38 -0
- data/lefthook.yml +35 -0
- data/lib/inquirex/ui/dsl/flow_builder.rb +21 -0
- data/lib/inquirex/ui/dsl/step_builder.rb +63 -0
- data/lib/inquirex/ui/node.rb +108 -0
- data/lib/inquirex/ui/version.rb +7 -0
- data/lib/inquirex/ui/widget_hint.rb +37 -0
- data/lib/inquirex/ui/widget_registry.rb +84 -0
- data/lib/inquirex/ui.rb +74 -0
- data/sig/inquirex/ui.rbs +6 -0
- metadata +165 -0
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
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).
|