chobble-forms 0.5.1 → 0.5.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e8c8cf30691cc89d9e89d2b92f0f447acb955d427bf49f5141568ba5ec2dcf1
4
- data.tar.gz: 739138d73c7d07ce4da208cfeebdf24ad8bc9c3525f487c85fc8a8c1a11f1097
3
+ metadata.gz: 291fbe89074db623fd71edea3063c4aa13584aed19e40e0f6b0d7a27cc28750e
4
+ data.tar.gz: 1f9dfdc73890607e8484362d121fddb89be6e128462d99aa847813f175678ef4
5
5
  SHA512:
6
- metadata.gz: f3a8dc1f1277b16f22f66e33df9b62f7edd5ba67651683c7796ce3e64d3cee6ced7d98ed1ae235ed1cb1b2f694374772b9b316a13a042ea473fab34d4c38d865
7
- data.tar.gz: fd275c3fb8d5417aa3a7ae23a1915c028d36291dac5b8909e45d4bf2fe674f909921f9316a6c4cb1f3b20e12ce45fae64979ff4a8d5e224e94efc60cf566068a
6
+ metadata.gz: 29cf3ec6da77e2bd6552cde85f95cfb46769f8e05dad35c851755109b603798aabb82a341c0495625afd34bfc021d9123c11cf9b7cb0ffe85b6a5584151416f9
7
+ data.tar.gz: c61849b91a1beba8ee445311e809071828cecb3c876acca9bbf5b9686e4c7f197106fc23e6187e38138237c7eba2fc7deae251f5c6697933d459763c68433958
data/README.md CHANGED
@@ -1,3 +1,75 @@
1
1
  # Chobble Forms
2
2
 
3
- Semantic Rails forms with strict i18n enforcement.
3
+ Semantic Rails forms with strict i18n enforcement.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'chobble-forms'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Type Safety with Sorbet
20
+
21
+ ChobbleForms uses Sorbet for static type checking, providing improved type safety and better IDE support. All library files use `typed: strict` with comprehensive type signatures.
22
+
23
+ ### Type Checking
24
+
25
+ Run type checking with:
26
+
27
+ ```bash
28
+ bundle exec srb tc
29
+ ```
30
+
31
+ Or use the provided Rake task:
32
+
33
+ ```bash
34
+ bundle exec rake typecheck
35
+ ```
36
+
37
+ ### Development Setup
38
+
39
+ For contributors working on this gem:
40
+
41
+ 1. Install dependencies: `bundle install`
42
+ 2. Generate RBI files: `bundle exec rake sorbet_rbi`
43
+ 3. Run type checking: `bundle exec rake typecheck`
44
+
45
+ All methods have full type signatures, providing:
46
+ - Static type checking at development time
47
+ - Runtime type validation
48
+ - Better IDE autocomplete and documentation
49
+ - Early detection of type mismatches
50
+
51
+ ## CSS Styles
52
+
53
+ ChobbleForms includes CSS for styling the form components. To use the included styles, add this to your application.css:
54
+
55
+ ```css
56
+ /*
57
+ *= require chobble_forms
58
+ */
59
+ ```
60
+
61
+ The CSS includes:
62
+
63
+ - **Form Grids**: Responsive grid layouts for various form field combinations
64
+ - **Form Fields**: Styling for inputs, textareas, and labels
65
+ - **Radio Buttons**: Custom radio button appearance with pass/fail color coding
66
+ - **Flash Messages**: Styled success, error, notice, and alert messages
67
+
68
+ ### CSS Variables
69
+
70
+ The CSS uses the following CSS variables that you can override in your application:
71
+
72
+ - `--color-primary`: Primary color for hover states (default: #118bee)
73
+ - `--color-pass`: Pass/success color (default: #00a94f)
74
+ - `--color-fail`: Fail/error color (default: #d32f2f)
75
+ - `--color-disabled`: Disabled state color (default: #959495)
@@ -0,0 +1,73 @@
1
+ /* Flash Messages
2
+ * Styles for success, error, notice, and alert messages
3
+ */
4
+
5
+ .success,
6
+ .error,
7
+ .notice,
8
+ .alert {
9
+ width: 30rem;
10
+ max-width: 90%;
11
+ margin: 2rem auto;
12
+ text-align: left;
13
+ padding: 1rem;
14
+ border-radius: 8px;
15
+ font-weight: 500;
16
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
17
+ animation: fadeIn 0.5s ease-in;
18
+ }
19
+
20
+ .success {
21
+ background: linear-gradient(135deg, #e8f5e8 0%, #f0fff0 100%);
22
+ border: 1px solid #90ee90;
23
+ color: #2d5016;
24
+ }
25
+
26
+ .notice {
27
+ background: linear-gradient(135deg, #e8f8ff 0%, #b6f5ff 100%);
28
+ border: 1px solid #b8e4ff;
29
+ color: #505f84;
30
+ }
31
+
32
+ .error {
33
+ background: linear-gradient(135deg, #ffe8e8 0%, #fff0f0 100%);
34
+ border: 1px solid #ffb3b3;
35
+ color: #cc0000;
36
+ }
37
+
38
+ .alert {
39
+ background: linear-gradient(135deg, #fff3cd 0%, #fffae5 100%);
40
+ border: 1px solid #ffd700;
41
+ color: #856404;
42
+ }
43
+
44
+ .error ul {
45
+ margin: 0.5rem 0 0 0;
46
+ padding: 0;
47
+ list-style: none;
48
+ }
49
+
50
+ .error li {
51
+ margin: 0.25rem 0;
52
+ padding-left: 1rem;
53
+ position: relative;
54
+ }
55
+
56
+ .error li:before {
57
+ content: "•";
58
+ position: absolute;
59
+ left: 0;
60
+ color: #cc0000;
61
+ font-weight: bold;
62
+ }
63
+
64
+ @keyframes fadeIn {
65
+ from {
66
+ opacity: 0;
67
+ transform: translateY(-10px);
68
+ }
69
+ to {
70
+ opacity: 1;
71
+ transform: translateY(0);
72
+ }
73
+ }
@@ -0,0 +1,126 @@
1
+ /* Form Field Styling
2
+ * Styles for form inputs, textareas, and related elements within form grids
3
+ */
4
+
5
+ /* Highlight animation for targeted form elements */
6
+ form *:target {
7
+ animation: highlight-fade 5s ease-out forwards;
8
+ border-radius: 5px;
9
+ }
10
+
11
+ @keyframes highlight-fade {
12
+ 0% {
13
+ background: #ffff3766;
14
+ outline: 20px solid #ffff3766;
15
+ }
16
+ 80% {
17
+ background: #ffff3766;
18
+ outline: 20px solid #ffff3766;
19
+ }
20
+ 100% {
21
+ background: transparent;
22
+ outline: 20px solid transparent;
23
+ }
24
+ }
25
+
26
+ /* Form grid field styling */
27
+ .form-grid input,
28
+ .form-grid label {
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ .form-grid input[type="text"],
34
+ .form-grid input[type="number"] {
35
+ padding: 0.4rem 0.8rem;
36
+ }
37
+
38
+ .form-grid input[type="number"],
39
+ .form-grid input[type="text"][inputmode="decimal"] {
40
+ width: 5rem;
41
+ text-align: left;
42
+ padding: 0.4rem;
43
+ font-variant-numeric: tabular-nums;
44
+ }
45
+
46
+ .form-grid textarea {
47
+ margin: 0 0 0.5rem;
48
+ padding: 0.5rem 1rem;
49
+ }
50
+
51
+ .form-grid label {
52
+ display: flex;
53
+ flex-direction: row;
54
+ gap: 0.5rem;
55
+ }
56
+
57
+ .form-grid .label {
58
+ line-height: 1.1rem;
59
+ text-wrap: balance;
60
+ }
61
+
62
+ .form-grid .pass-fail {
63
+ display: flex;
64
+ gap: 0.5rem;
65
+ }
66
+
67
+ .form-grid .pass-fail label {
68
+ display: flex;
69
+ }
70
+
71
+ .form-grid input[type="radio"] {
72
+ /* Prevent the radio from affecting grid layout */
73
+ vertical-align: middle;
74
+ }
75
+
76
+ /* Comment field container */
77
+ .comment-field-container {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 0.5rem;
81
+ }
82
+
83
+ .comment-field-container label {
84
+ cursor: pointer;
85
+ font-weight: normal;
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 0.5rem;
89
+ }
90
+
91
+ .comment-field-container input[type="checkbox"] {
92
+ margin: 0;
93
+ }
94
+
95
+ /* Field with link styling */
96
+ .field.field-with-link {
97
+ position: relative;
98
+ }
99
+
100
+ .field.field-with-link a {
101
+ position: absolute;
102
+ right: 10px;
103
+ top: 35px;
104
+ font-size: 0.9em;
105
+ }
106
+
107
+ /* Muted state for N/A toggled fields */
108
+ .muted {
109
+ opacity: 0.4;
110
+ }
111
+
112
+ /* Form actions (submit button + secondary link) */
113
+ .form-actions {
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: center;
117
+ gap: 1rem;
118
+ flex-wrap: wrap;
119
+ }
120
+
121
+ /* Secondary button styling */
122
+ .secondary[role="button"],
123
+ a.secondary[role="button"] {
124
+ background: var(--color-bg);
125
+ color: var(--color-link);
126
+ }
@@ -0,0 +1,208 @@
1
+ /* Form Grid Layouts
2
+ * Provides flexible grid layouts for various form field combinations
3
+ */
4
+
5
+ .form-grid {
6
+ display: grid;
7
+ gap: 1rem;
8
+ align-items: end;
9
+ padding-top: 1rem;
10
+ }
11
+
12
+ legend + .form-grid {
13
+ border-top: none;
14
+ }
15
+
16
+ /* Radio + Comment grid layout */
17
+ .radio-comment {
18
+ grid-template-areas:
19
+ "label label"
20
+ "pass-fail comment-label"
21
+ "comment comment";
22
+ grid-template-columns: auto 1fr;
23
+ align-items: center;
24
+ }
25
+
26
+ @media (min-width: 768px) {
27
+ .radio-comment {
28
+ grid-template-areas:
29
+ "label pass-fail comment-space comment-label"
30
+ "comment comment comment comment";
31
+ grid-template-columns: max-content auto 1fr auto;
32
+ }
33
+ }
34
+
35
+ .radio-comment > .label {
36
+ grid-area: label;
37
+ }
38
+
39
+ .radio-comment > .label label {
40
+ flex-direction: column;
41
+ }
42
+
43
+ .radio-comment > .pass-fail {
44
+ grid-area: pass-fail;
45
+ }
46
+
47
+ .radio-comment > .comment-checkbox {
48
+ grid-area: comment-label;
49
+ }
50
+
51
+ .radio-comment > textarea {
52
+ grid-area: comment;
53
+ }
54
+
55
+ /* Number + Pass/Fail + Comment grid layout */
56
+ .number-radio-comment {
57
+ grid-template-areas:
58
+ "label label"
59
+ "number pass-fail"
60
+ "comment-label comment-space"
61
+ "comment comment";
62
+ grid-template-columns: auto 1fr;
63
+ }
64
+
65
+ @media (min-width: 768px) {
66
+ .number-radio-comment {
67
+ grid-template-areas:
68
+ "label label label label"
69
+ "number pass-fail comment-space comment-label"
70
+ "comment comment comment comment";
71
+ grid-template-columns: auto auto 1fr auto;
72
+ }
73
+ }
74
+
75
+ .number-radio-comment > .label {
76
+ grid-area: label;
77
+ }
78
+
79
+ .number-radio-comment > .label label {
80
+ flex-direction: column;
81
+ }
82
+
83
+ .number-radio-comment > .number {
84
+ grid-area: number;
85
+ }
86
+
87
+ .number-radio-comment > .pass-fail {
88
+ grid-area: pass-fail;
89
+ }
90
+
91
+ .number-radio-comment > .comment-checkbox {
92
+ grid-area: comment-label;
93
+ }
94
+
95
+ .number-radio-comment > textarea {
96
+ grid-area: comment;
97
+ }
98
+
99
+ /* Number + Comment grid layout */
100
+ .number-comment {
101
+ grid-template-areas:
102
+ "label label"
103
+ "number comment-label"
104
+ "comment comment";
105
+ grid-template-columns: min-content auto;
106
+ align-items: center;
107
+ }
108
+
109
+ @media (min-width: 768px) {
110
+ .number-comment {
111
+ grid-template-areas:
112
+ "label number comment-space comment-label"
113
+ "comment comment comment comment";
114
+ grid-template-columns: max-content 6rem 1fr auto;
115
+ }
116
+ }
117
+
118
+ .number-comment > .label {
119
+ grid-area: label;
120
+ width: 14rem;
121
+ }
122
+
123
+ .number-comment > .number {
124
+ grid-area: number;
125
+ }
126
+
127
+ .number-comment > .comment-checkbox {
128
+ grid-area: comment-label;
129
+ }
130
+
131
+ .number-comment > textarea {
132
+ grid-area: comment;
133
+ }
134
+
135
+ /* Checkbox + Comment grid layout */
136
+ .checkbox-comment {
137
+ display: grid;
138
+ gap: 0.5rem;
139
+ align-items: center;
140
+ margin-bottom: 1rem;
141
+ grid-template-areas:
142
+ "label label label label"
143
+ "check1 label2 comment-space comment-label";
144
+ grid-template-columns: auto auto 1fr auto;
145
+ }
146
+
147
+ @media (min-width: 768px) {
148
+ .checkbox-comment {
149
+ grid-template-areas: "label check1 label2 comment-label";
150
+ grid-template-columns: max-content auto auto auto;
151
+ }
152
+ }
153
+
154
+ .checkbox-comment > .label {
155
+ grid-area: label;
156
+ }
157
+
158
+ .checkbox-comment > .checkbox {
159
+ grid-area: check1;
160
+ }
161
+
162
+ .checkbox-comment > .checkbox-label {
163
+ grid-area: label2;
164
+ }
165
+
166
+ .checkbox-comment > .comment-checkbox {
167
+ grid-area: comment-label;
168
+ }
169
+
170
+ .checkbox-comment > textarea {
171
+ grid-column: 1 / -1;
172
+ margin-top: 0.5rem;
173
+ }
174
+
175
+ /* Simple number layout */
176
+ .number {
177
+ grid-template-area: "label number";
178
+ grid-template-columns: auto min-content;
179
+ align-items: center;
180
+ }
181
+
182
+ .number-comment > .label {
183
+ grid-area: label;
184
+ }
185
+
186
+ .number-comment > .number {
187
+ grid-area: number;
188
+ width: 14rem;
189
+ }
190
+
191
+ /* User capacity flexbox layout */
192
+ fieldset#user_capacity {
193
+ display: flex;
194
+ flex-wrap: wrap;
195
+ gap: 1rem;
196
+ }
197
+
198
+ /* Mobile: 2 columns (2 rows of 2) */
199
+ fieldset#user_capacity > * {
200
+ flex: 1 1 calc(50% - 0.5rem);
201
+ }
202
+
203
+ /* Desktop: 4 columns (1 row of 4) */
204
+ @media (min-width: 768px) {
205
+ fieldset#user_capacity > * {
206
+ flex: 1 1 calc(25% - 0.75rem);
207
+ }
208
+ }
@@ -0,0 +1,116 @@
1
+ /* Custom Radio Button Styles
2
+ * Modern, accessible radio button styling with pass/fail color coding
3
+ * Based on https://moderncss.dev/pure-css-custom-styled-radio-buttons/
4
+ */
5
+
6
+ input[type="radio"] {
7
+ /* Remove default appearance */
8
+ -webkit-appearance: none;
9
+ appearance: none;
10
+ /* For iOS < 15 */
11
+ background-color: transparent;
12
+ /* Not removed via appearance */
13
+ margin: 0;
14
+
15
+ /* Custom styling */
16
+ font: inherit;
17
+ color: currentColor;
18
+ width: 1.15em;
19
+ height: 1.15em;
20
+ border: 0.15em solid currentColor;
21
+ border-radius: 50%;
22
+ transform: translateY(-0.075em);
23
+
24
+ /* Create the dot in the center */
25
+ display: inline-grid;
26
+ place-content: center;
27
+
28
+ /* Ensure the radio is clickable */
29
+ cursor: pointer;
30
+
31
+ /* Smooth transitions */
32
+ transition: border-color 120ms ease-in-out;
33
+ }
34
+
35
+ input[type="radio"]::before {
36
+ content: "";
37
+ width: 0.65em;
38
+ height: 0.65em;
39
+ border-radius: 50%;
40
+ transform: scale(0);
41
+ transition: 120ms transform ease-in-out;
42
+ /* Use currentColor for IE fallback */
43
+ background-color: currentColor;
44
+ }
45
+
46
+ input[type="radio"]:checked::before {
47
+ transform: scale(1);
48
+ }
49
+
50
+ input[type="radio"]:focus {
51
+ outline: 2px solid currentColor;
52
+ outline-offset: 2px;
53
+ }
54
+
55
+ input[type="radio"]:disabled {
56
+ color: var(--color-disabled, #959495);
57
+ cursor: not-allowed;
58
+ }
59
+
60
+ /* Label styling for radio buttons */
61
+ label:has(input[type="radio"]) {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ gap: 0.5em;
65
+ cursor: pointer;
66
+ line-height: 1.1;
67
+ /* Ensure label remains clickable */
68
+ position: relative;
69
+ display: flex;
70
+ }
71
+
72
+ label > input[type="radio"] {
73
+ margin-left: 0;
74
+ margin-right: 0;
75
+ }
76
+
77
+ /* Pass/Fail color coding */
78
+ .pass-fail input[value="true"]::before,
79
+ .pass-fail input[value="pass"]::before {
80
+ box-shadow: inset 1em 1em var(--color-pass, #00a94f);
81
+ }
82
+
83
+ .pass-fail input[value="false"]::before,
84
+ .pass-fail input[value="fail"]::before {
85
+ box-shadow: inset 1em 1em var(--color-fail, #d32f2f);
86
+ }
87
+
88
+ /* Hover state */
89
+ label:hover input[type="radio"]:not(:disabled) {
90
+ border-color: var(--color-primary, #118bee);
91
+ }
92
+
93
+ /* High contrast support */
94
+ @media (prefers-contrast: high) {
95
+ input[type="radio"] {
96
+ border-width: 0.2em;
97
+ }
98
+
99
+ input[type="radio"]:checked::before {
100
+ background-color: CanvasText;
101
+ box-shadow: none;
102
+ }
103
+ }
104
+
105
+ /* Print styles */
106
+ @media print {
107
+ input[type="radio"]:checked {
108
+ border-width: 0.1em;
109
+ background-color: black;
110
+ }
111
+
112
+ input[type="radio"]:checked::before {
113
+ background-color: black;
114
+ box-shadow: none;
115
+ }
116
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * ChobbleForms CSS Manifest
3
+ * This file includes all the CSS components for the ChobbleForms gem
4
+ *
5
+ *= require chobble_forms/form_grids
6
+ *= require chobble_forms/form_fields
7
+ *= require chobble_forms/radio_buttons
8
+ *= require chobble_forms/flash_messages
9
+ */
data/lib/chobble-forms.rb CHANGED
@@ -1,6 +1,12 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
1
5
  require_relative "chobble_forms/version"
2
6
  require_relative "chobble_forms/engine"
3
7
  require_relative "chobble_forms/helpers"
8
+ require_relative "chobble_forms/field_utils"
4
9
 
5
10
  module ChobbleForms
11
+ extend T::Sig
6
12
  end
@@ -1,10 +1,16 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
1
6
  module ChobbleForms
2
7
  class Engine < ::Rails::Engine
8
+ extend T::Sig
3
9
  isolate_namespace ChobbleForms
4
10
 
5
11
  initializer "chobble_forms.add_view_paths" do |app|
6
12
  ActiveSupport.on_load(:action_controller) do
7
- prepend_view_path ChobbleForms::Engine.root.join("views")
13
+ T.unsafe(self).prepend_view_path ChobbleForms::Engine.root.join("views")
8
14
  end
9
15
  end
10
16
 
@@ -17,5 +23,13 @@ module ChobbleForms
17
23
  include ChobbleForms::Helpers
18
24
  end
19
25
  end
26
+
27
+ # Asset pipeline configuration
28
+ initializer "chobble_forms.assets" do |app|
29
+ if app.config.respond_to?(:assets) && app.config.assets
30
+ app.config.assets.paths << root.join("app/assets/stylesheets")
31
+ app.config.assets.precompile += %w[chobble_forms.css]
32
+ end
33
+ end
20
34
  end
21
35
  end
@@ -0,0 +1,45 @@
1
+ # typed: false
2
+
3
+ module ChobbleForms
4
+ module FieldUtils
5
+ def self.strip_field_suffix(field)
6
+ field.to_s.gsub(/_pass$|_comment$/, "")
7
+ end
8
+
9
+ def self.get_composite_fields(field, partial)
10
+ fields = []
11
+ partial_str = partial.to_s
12
+
13
+ if partial_str.include?("pass_fail") && !field.to_s.end_with?("_pass")
14
+ fields << "#{field}_pass"
15
+ end
16
+
17
+ if partial_str.include?("comment")
18
+ base = field.to_s.end_with?("_pass") ? strip_field_suffix(field) : field
19
+ fields << "#{base}_comment"
20
+ end
21
+
22
+ fields
23
+ end
24
+
25
+ def self.is_pass_field?(field)
26
+ field.to_s.end_with?("_pass")
27
+ end
28
+
29
+ def self.is_comment_field?(field)
30
+ field.to_s.end_with?("_comment")
31
+ end
32
+
33
+ def self.is_composite_field?(field)
34
+ is_pass_field?(field) || is_comment_field?(field)
35
+ end
36
+
37
+ def self.base_field_name(field)
38
+ strip_field_suffix(field)
39
+ end
40
+
41
+ def self.form_field_label(form, field)
42
+ I18n.t("forms.#{form}.fields.#{field}")
43
+ end
44
+ end
45
+ end
@@ -1,19 +1,25 @@
1
- require "action_view"
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
2
5
 
3
6
  module ChobbleForms
4
7
  module Helpers
5
- include ActionView::Helpers::NumberHelper
8
+ extend T::Sig
9
+
10
+ sig { params(field: T.any(Symbol, String), local_assigns: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
6
11
  def form_field_setup(field, local_assigns)
7
12
  validate_local_assigns(local_assigns)
8
13
  validate_form_context
9
14
 
10
15
  field_translations = build_field_translations(field)
11
- value, prefilled = get_field_value_and_prefilled_status(@_current_form,
12
- field)
16
+ form_obj = T.unsafe(instance_variable_get(:@_current_form))
17
+ value, prefilled = get_field_value_and_prefilled_status(form_obj, field)
13
18
 
14
19
  build_field_setup_result(field_translations, value, prefilled)
15
20
  end
16
21
 
22
+ sig { params(form_object: T.untyped, field: T.any(Symbol, String)).returns([T.untyped, T::Boolean]) }
17
23
  def get_field_value_and_prefilled_status(form_object, field)
18
24
  return [nil, false] unless form_object&.object
19
25
  model = form_object.object
@@ -21,6 +27,7 @@ module ChobbleForms
21
27
  [resolved[:value], resolved[:prefilled]]
22
28
  end
23
29
 
30
+ sig { params(form: T.untyped, comment_field: T.any(Symbol, String), base_field_label: String).returns(T::Hash[Symbol, T.untyped]) }
24
31
  def comment_field_options(form, comment_field, base_field_label)
25
32
  raise ArgumentError, "form_object required" unless form
26
33
  model = form.object
@@ -54,13 +61,14 @@ module ChobbleForms
54
61
  }
55
62
  end
56
63
 
64
+ sig { params(prefilled: T::Boolean, checked_value: T.untyped, expected_value: T.untyped).returns(T::Hash[Symbol, T::Boolean]) }
57
65
  def radio_button_options(prefilled, checked_value, expected_value)
58
66
  (prefilled && checked_value == expected_value) ? {checked: true} : {}
59
67
  end
60
68
 
61
69
  private
62
70
 
63
- ALLOWED_LOCAL_ASSIGNS = %i[
71
+ ALLOWED_LOCAL_ASSIGNS = T.let(%i[
64
72
  accept
65
73
  field
66
74
  max
@@ -72,15 +80,16 @@ module ChobbleForms
72
80
  rows
73
81
  step
74
82
  type
75
- ]
83
+ ], T::Array[Symbol])
76
84
 
85
+ sig { params(local_assigns: T::Hash[Symbol, T.untyped]).void }
77
86
  def validate_local_assigns(local_assigns)
78
- if local_assigns[:field].present? &&
87
+ if local_assigns[:field]&.respond_to?(:to_s) &&
79
88
  local_assigns[:field].to_s.match?(/^[A-Z]/)
80
89
  raise ArgumentError, "Field names must be snake_case symbols, not class names. Use :field, not Field."
81
90
  end
82
91
 
83
- locally_assigned_keys = (local_assigns || {}).keys
92
+ locally_assigned_keys = local_assigns.keys
84
93
  disallowed_keys = locally_assigned_keys - ALLOWED_LOCAL_ASSIGNS
85
94
 
86
95
  if disallowed_keys.any?
@@ -88,16 +97,21 @@ module ChobbleForms
88
97
  end
89
98
  end
90
99
 
100
+ sig { void }
91
101
  def validate_form_context
92
- raise ArgumentError, "missing i18n_base" unless @_current_i18n_base
93
- raise ArgumentError, "missing form_object" unless @_current_form
102
+ i18n_base = T.unsafe(instance_variable_get(:@_current_i18n_base))
103
+ form_obj = T.unsafe(instance_variable_get(:@_current_form))
104
+ raise ArgumentError, "missing i18n_base" unless i18n_base
105
+ raise ArgumentError, "missing form_object" unless form_obj
94
106
  end
95
107
 
108
+ sig { params(field: T.any(Symbol, String)).returns(T::Hash[Symbol, T.nilable(String)]) }
96
109
  def build_field_translations(field)
97
- fields_key = "#{@_current_i18n_base}.fields.#{field}"
110
+ i18n_base = T.unsafe(instance_variable_get(:@_current_i18n_base))
111
+ fields_key = "#{i18n_base}.fields.#{field}"
98
112
  field_label = t(fields_key, raise: true)
99
113
 
100
- base_parts = @_current_i18n_base.split(".")
114
+ base_parts = i18n_base.split(".")
101
115
  root = base_parts[0..-2]
102
116
  hint_key = (root + ["hints", field]).join(".")
103
117
  placeholder_key = (root + ["placeholders", field]).join(".")
@@ -109,15 +123,20 @@ module ChobbleForms
109
123
  }
110
124
  end
111
125
 
126
+ sig { params(field_translations: T::Hash[Symbol, T.nilable(String)], value: T.untyped, prefilled: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
112
127
  def build_field_setup_result(field_translations, value, prefilled)
128
+ form_obj = T.unsafe(instance_variable_get(:@_current_form))
129
+ i18n_base = T.unsafe(instance_variable_get(:@_current_i18n_base))
130
+
113
131
  {
114
- form_object: @_current_form,
115
- i18n_base: @_current_i18n_base,
132
+ form_object: form_obj,
133
+ i18n_base: i18n_base,
116
134
  value:,
117
135
  prefilled:
118
136
  }.merge(field_translations)
119
137
  end
120
138
 
139
+ sig { params(model: T.untyped, field: T.any(Symbol, String)).returns(T::Hash[Symbol, T.untyped]) }
121
140
  def resolve_field_value(model, field)
122
141
  field_str = field.to_s
123
142
 
@@ -136,7 +155,8 @@ module ChobbleForms
136
155
  end
137
156
 
138
157
  # Extract previous value if available
139
- previous_value = extract_previous_value(@previous_inspection, model, field)
158
+ prev_inspection = T.unsafe(instance_variable_get(:@previous_inspection))
159
+ previous_value = extract_previous_value(prev_inspection, model, field)
140
160
 
141
161
  # Return previous value if current is nil and previous exists
142
162
  if current_value.nil? && !previous_value.nil?
@@ -154,6 +174,7 @@ module ChobbleForms
154
174
  end
155
175
  end
156
176
 
177
+ sig { params(previous_inspection: T.untyped, current_model: T.untyped, field: T.any(Symbol, String)).returns(T.untyped) }
157
178
  def extract_previous_value(previous_inspection, current_model, field)
158
179
  if !previous_inspection
159
180
  nil
@@ -166,6 +187,7 @@ module ChobbleForms
166
187
  end
167
188
  end
168
189
 
190
+ sig { params(value: T.untyped).returns(T.untyped) }
169
191
  def format_numeric_value(value)
170
192
  if value.is_a?(String) &&
171
193
  value.match?(/\A-?\d*\.?\d+\z/) &&
@@ -182,10 +204,12 @@ module ChobbleForms
182
204
  )
183
205
  end
184
206
 
207
+ sig { params(value: T.untyped).returns(T.nilable(String)) }
185
208
  def strip_trailing_zeros(value)
186
209
  value&.to_s&.sub(/\.0+$/, "")
187
210
  end
188
211
 
212
+ sig { params(model: T.untyped, field_str: String).returns(T::Hash[Symbol, T.untyped]) }
189
213
  def resolve_association_value(model, field_str)
190
214
  base_name = field_str.chomp("_id")
191
215
  association_name = base_name.to_sym
@@ -1,3 +1,6 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
1
4
  module ChobbleForms
2
- VERSION = "0.5.1"
5
+ VERSION = "0.5.3"
3
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chobble-forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chobble.com
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-01 00:00:00.000000000 Z
11
+ date: 2025-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 7.0.0
19
+ version: 8.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 7.0.0
26
+ version: 8.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sorbet-runtime
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,34 @@ dependencies:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sorbet
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tapioca
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.16'
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: rake
43
85
  requirement: !ruby/object:Gem::Requirement
@@ -118,8 +160,14 @@ extensions: []
118
160
  extra_rdoc_files: []
119
161
  files:
120
162
  - README.md
163
+ - app/assets/stylesheets/chobble_forms.css
164
+ - app/assets/stylesheets/chobble_forms/flash_messages.css
165
+ - app/assets/stylesheets/chobble_forms/form_fields.css
166
+ - app/assets/stylesheets/chobble_forms/form_grids.css
167
+ - app/assets/stylesheets/chobble_forms/radio_buttons.css
121
168
  - lib/chobble-forms.rb
122
169
  - lib/chobble_forms/engine.rb
170
+ - lib/chobble_forms/field_utils.rb
123
171
  - lib/chobble_forms/helpers.rb
124
172
  - lib/chobble_forms/version.rb
125
173
  - views/chobble_forms/_auto_submit_select.html.erb