chobble-forms 0.5.0 → 0.5.2
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 +4 -4
- data/README.md +73 -1
- data/app/assets/stylesheets/chobble_forms/flash_messages.css +73 -0
- data/app/assets/stylesheets/chobble_forms/form_fields.css +126 -0
- data/app/assets/stylesheets/chobble_forms/form_grids.css +208 -0
- data/app/assets/stylesheets/chobble_forms/radio_buttons.css +116 -0
- data/app/assets/stylesheets/chobble_forms.css +9 -0
- data/lib/chobble-forms.rb +6 -0
- data/lib/chobble_forms/engine.rb +15 -1
- data/lib/chobble_forms/field_utils.rb +45 -0
- data/lib/chobble_forms/helpers.rb +39 -15
- data/lib/chobble_forms/version.rb +4 -1
- data/views/chobble_forms/_file_field.html.erb +1 -1
- metadata +52 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dad95e563979147c11cad0fcfe4dbdd1865957f4d46f49755bdd65f9b1275814
|
4
|
+
data.tar.gz: 69603954ce9e91cd635b527bfa837b4066da0a87ece343d571d2e28caf31aa5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4f4402e89b595c12d2fdc9173c98e2a288358b9ae4a0afdf2e979ec2e127f34ff58c8b1d4a0bae48fb6a979cf197d4a0c59192d552f54c02e78414fcfb35d51
|
7
|
+
data.tar.gz: e07e61dfe9e117a973d4311740fe82d81fe91cb075d6e8f2a6e9687616e6eea1bba7cc638e3475e8961ed0370303e9a3efc18b8debbb8325d34a134a4e90e59c
|
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: max(2px, 0.15em) solid currentColor;
|
52
|
+
outline-offset: max(2px, 0.15em);
|
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
|
data/lib/chobble_forms/engine.rb
CHANGED
@@ -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
|
-
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
2
5
|
|
3
6
|
module ChobbleForms
|
4
7
|
module Helpers
|
5
|
-
|
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
|
-
|
12
|
-
|
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]
|
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 =
|
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
|
-
|
93
|
-
|
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
|
-
|
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 =
|
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:
|
115
|
-
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
|
-
|
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
|
@@ -12,7 +12,7 @@
|
|
12
12
|
<%= setup[:form_object].label field, setup[:field_label] %>
|
13
13
|
<%= setup[:form_object].file_field field, accept: accept %>
|
14
14
|
|
15
|
-
<% if current_file&.attached? %>
|
15
|
+
<% if current_file&.attached? && model.persisted? %>
|
16
16
|
<div class="file-preview" style="margin-top: 10px;">
|
17
17
|
<% if current_file.image? %>
|
18
18
|
<%= image_tag current_file.variant(resize_to_limit: [preview_size, preview_size]),
|
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.
|
4
|
+
version: 0.5.2
|
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-07
|
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:
|
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:
|
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
|