form_components 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +194 -0
  5. data/app/assets/stylesheets/form_components/application.css +9 -0
  6. data/app/assets/tailwind/form_components/engine.css +6 -0
  7. data/app/components/form_components/base/component.rb +76 -0
  8. data/app/components/form_components/check_box/component.html.slim +13 -0
  9. data/app/components/form_components/check_box/component.rb +26 -0
  10. data/app/components/form_components/collection/component.html.slim +10 -0
  11. data/app/components/form_components/collection/component.rb +76 -0
  12. data/app/components/form_components/collection_check_boxes/component.rb +12 -0
  13. data/app/components/form_components/collection_radio_buttons/component.rb +12 -0
  14. data/app/components/form_components/color_field/component.css +69 -0
  15. data/app/components/form_components/color_field/component.html.slim +8 -0
  16. data/app/components/form_components/color_field/component.rb +26 -0
  17. data/app/components/form_components/color_field/component_controller.js +25 -0
  18. data/app/components/form_components/combobox/component.html.slim +8 -0
  19. data/app/components/form_components/combobox/component.rb +28 -0
  20. data/app/components/form_components/field/component.rb +58 -0
  21. data/app/components/form_components/file_field/component.html.slim +10 -0
  22. data/app/components/form_components/file_field/component.rb +25 -0
  23. data/app/components/form_components/label/component.rb +31 -0
  24. data/app/components/form_components/nested_form/component.html.slim +20 -0
  25. data/app/components/form_components/nested_form/component.rb +71 -0
  26. data/app/components/form_components/nested_form/component_controller.js +36 -0
  27. data/app/components/form_components/radio_button/component.html.slim +12 -0
  28. data/app/components/form_components/radio_button/component.rb +24 -0
  29. data/app/components/form_components/rating_field/component.rb +39 -0
  30. data/app/components/form_components/scale_field/component.rb +44 -0
  31. data/app/components/form_components/select/component.html.slim +8 -0
  32. data/app/components/form_components/select/component.rb +35 -0
  33. data/app/components/form_components/submit/component.rb +21 -0
  34. data/app/components/form_components/text_area/component.html.slim +14 -0
  35. data/app/components/form_components/text_area/component.rb +22 -0
  36. data/app/components/form_components/text_field/component.html.slim +16 -0
  37. data/app/components/form_components/text_field/component.rb +33 -0
  38. data/app/components/form_components/validation.rb +41 -0
  39. data/app/form_builders/form_components/form_builder.rb +133 -0
  40. data/app/form_builders/form_components/validation_context.rb +17 -0
  41. data/app/javascript/form_components/application.js +12 -0
  42. data/config/importmap.rb +6 -0
  43. data/lib/form_components/engine.rb +23 -0
  44. data/lib/form_components/version.rb +5 -0
  45. data/lib/form_components.rb +13 -0
  46. data/vendor/javascript/form_components/jscolorpicker/LICENSE +21 -0
  47. data/vendor/javascript/form_components/jscolorpicker/colorpicker.css +409 -0
  48. data/vendor/javascript/form_components/jscolorpicker/colorpicker.js +2410 -0
  49. metadata +145 -0
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormComponents
4
+ # A form builder whose fields are components. Each helper takes the
5
+ # arguments Rails' takes, and its input is Rails' own (see
6
+ # Base::Component#rails), so names, ids, values and options are Rails'; the
7
+ # components add a label, a description, errors and the like around it,
8
+ # with options of their own (label:, description:, …) that Rails doesn't
9
+ # get. Helpers without a component here, such as hidden_field or date_select,
10
+ # are Rails'.
11
+ #
12
+ # = form_with model: @post, builder: FormComponents::FormBuilder do |f|
13
+ # = f.text_field :title
14
+ class FormBuilder < ActionView::Helpers::FormBuilder
15
+ include FormComponents::ValidationContext
16
+
17
+ # Rails' text fields of every kind, as TextField
18
+ TEXT_FIELDS = %i[text_field password_field search_field telephone_field phone_field date_field time_field
19
+ datetime_field datetime_local_field month_field week_field url_field email_field number_field].freeze
20
+
21
+ def label(method, text = nil, options = {}, &block)
22
+ component(:label, method, text, options, &block)
23
+ end
24
+
25
+ TEXT_FIELDS.each do |helper|
26
+ define_method(helper) do |method, options = {}|
27
+ component(:text_field, method, options, helper: helper)
28
+ end
29
+ end
30
+
31
+ def text_area(method, options = {})
32
+ component(:text_area, method, options)
33
+ end
34
+ alias_method :textarea, :text_area
35
+
36
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
37
+ component(:check_box, method, options, checked_value, unchecked_value)
38
+ end
39
+ alias_method :checkbox, :check_box
40
+
41
+ def radio_button(method, tag_value, options = {})
42
+ component(:radio_button, method, tag_value, options)
43
+ end
44
+ alias_method :radiobutton, :radio_button
45
+
46
+ def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
47
+ component(:collection_check_boxes, method, collection, value_method, text_method, options, html_options, &block)
48
+ end
49
+ alias_method :collection_checkboxes, :collection_check_boxes
50
+
51
+ def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
52
+ component(:collection_radio_buttons, method, collection, value_method, text_method, options, html_options, &block)
53
+ end
54
+ alias_method :collection_radiobuttons, :collection_radio_buttons
55
+
56
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
57
+ component(:select, method, [choices], options, html_options, &block)
58
+ end
59
+
60
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
61
+ component(:select, method, [collection, value_method, text_method], options, html_options, helper: :collection_select)
62
+ end
63
+
64
+ def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
65
+ component(:select, method, [collection, group_method, group_label_method, option_key_method, option_value_method], options, html_options, helper: :grouped_collection_select)
66
+ end
67
+
68
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
69
+ component(:select, method, [priority_zones], options, html_options, helper: :time_zone_select)
70
+ end
71
+
72
+ def weekday_select(method, options = {}, html_options = {})
73
+ component(:select, method, [], options, html_options, helper: :weekday_select)
74
+ end
75
+
76
+ # hotwire_combobox's combobox, when the host has that gem
77
+ def combobox(method, options_or_src = [], **options, &block)
78
+ component(:combobox, method, options_or_src, options, &block)
79
+ end
80
+
81
+ # Stars to rate with, from 1 to max: (5)
82
+ def rating_field(method, options = {})
83
+ component(:rating_field, method, options)
84
+ end
85
+
86
+ # A number to pick on a scale, in: (1..5), with min_label: and max_label:
87
+ # at its ends
88
+ def scale_field(method, options = {})
89
+ component(:scale_field, method, options)
90
+ end
91
+
92
+ # A text field for a color picker, see ColorField: with swatches: to pick
93
+ # from, and default: shown while it's empty
94
+ def color_field(method, options = {})
95
+ component(:color_field, method, options)
96
+ end
97
+
98
+ # With preview: an attachment to show above the field
99
+ def file_field(method, options = {})
100
+ component(:file_field, method, options)
101
+ end
102
+
103
+ def submit(value = nil, options = {})
104
+ FormComponents::Submit::Component.new(self, @object, value, options).render_in(@template)
105
+ end
106
+
107
+ # Renders a Stimulus-driven nested form for a has_many association.
108
+ # The block receives the component (nf) so you can declare slots:
109
+ #
110
+ # = f.nested_form :sections do |nf|
111
+ # - nf.with_item do |sf|
112
+ # .section-wrapper data-new-record=sf.object.new_record?.to_s
113
+ # = sf.hidden_field :_destroy
114
+ # = sf.text_field :title
115
+ # - nf.with_add_button
116
+ # button type="button" data-action="form-components--nested-form#add" Add section
117
+ #
118
+ # Options (keyword arguments):
119
+ # placeholder: – Stimulus token replaced on clone, default "NEW_<SINGULAR>"
120
+ # wrapper_selector: – CSS selector for the remove handler, default ".<singular>-wrapper"
121
+ # new_record: – template record object, default association_class.new
122
+ # add_label: – label for the default add button
123
+ def nested_form(method, **options, &block)
124
+ FormComponents::NestedForm::Component.new(self, @object, method, options).render_in(@template, &block)
125
+ end
126
+
127
+ private
128
+
129
+ def component(name, method, *, **, &block)
130
+ FormComponents.const_get("#{name.to_s.camelize}::Component").new(self, @object, method, *, **, &block).render_in(@template)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormComponents
4
+ module ValidationContext
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_reader :validation_context
9
+ alias_method :original_initialize, :initialize
10
+
11
+ def initialize(*args, &block)
12
+ original_initialize(*args, &block)
13
+ @validation_context = options[:validation_context]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ // Registers the components' Stimulus controllers with the host's
2
+ // application:
3
+ //
4
+ // import { registerFormComponents } from "form_components"
5
+ // registerFormComponents(window.Stimulus)
6
+ import NestedFormController from "form_components/components/nested_form/component_controller"
7
+ import ColorFieldController from "form_components/components/color_field/component_controller"
8
+
9
+ export function registerFormComponents(application) {
10
+ application.register("form-components--nested-form", NestedFormController)
11
+ application.register("form-components--color-field", ColorFieldController)
12
+ }
@@ -0,0 +1,6 @@
1
+ # Pins for form_components. The host app registers the controllers with:
2
+ # import { registerFormComponents } from "form_components"
3
+
4
+ pin "form_components", to: "form_components/application.js"
5
+ pin_all_from FormComponents::Engine.root.join("app/components/form_components"), under: "form_components/components", to: "form_components"
6
+ pin "form_components/jscolorpicker", to: "form_components/jscolorpicker/colorpicker.js"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormComponents
4
+ class Engine < ::Rails::Engine
5
+ engine_name "form_components"
6
+
7
+ initializer "form_components.assets" do |app|
8
+ if app.config.respond_to?(:assets)
9
+ app.config.assets.paths << root.join("app/javascript")
10
+ # The components' Stimulus controllers
11
+ app.config.assets.paths << root.join("app/components")
12
+ # JS Color Picker, for the color field
13
+ app.config.assets.paths << root.join("vendor/javascript")
14
+ end
15
+ end
16
+
17
+ initializer "form_components.importmap", before: "importmap" do |app|
18
+ app.config.importmap.paths << root.join("config/importmap.rb")
19
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
20
+ app.config.importmap.cache_sweepers << root.join("app/components")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormComponents
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component"
4
+ require "slim-rails"
5
+ require "importmap-rails"
6
+ require "form_components/version"
7
+ require "form_components/engine"
8
+
9
+ # A form builder whose fields are view components, each in
10
+ # app/components/form_components/<name>/ with its template and, for some, a
11
+ # Stimulus controller and styles. See the README.
12
+ module FormComponents
13
+ end
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 Zygomatic
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,409 @@
1
+ [data-bs-theme='dark'],
2
+ [data-cp-theme='dark'] {
3
+ --cp-body-bg: #212529 !important;
4
+ --cp-body-color: #dee2e6 !important;
5
+ --cp-body-color-rgb: 222, 226, 230 !important;
6
+
7
+ --cp-border-color: #666 !important;
8
+ --cp-hover-color: #777 !important;
9
+ --cp-button-color: #666 !important;
10
+ --cp-border-color-translucent: rgba(255, 255, 255, 0.15) !important;
11
+ --cp-tertiary-color: rgba(222, 226, 230, 0.5) !important;
12
+ }
13
+
14
+ :root,
15
+ [data-bs-theme='light'],
16
+ [data-cp-theme='light'] {
17
+ --cp-body-bg: #fff;
18
+ --cp-body-color: #212529;
19
+ --cp-body-color-rgb: 33, 37, 41;
20
+
21
+ --cp-border-color: #ccc;
22
+ --cp-hover-color: #999;
23
+ --cp-button-color: #ccc;
24
+ --cp-border-color-translucent: rgba(0, 0, 0, 0.175);
25
+ --cp-tertiary-color: rgba(33, 37, 41, 0.5);
26
+ }
27
+
28
+ :root {
29
+ --cp-size: 2.375rem;
30
+ --cp-delay: 150ms;
31
+
32
+ --cp-border-radius-sm: 0.25rem;
33
+ --cp-border-radius-lg: 0.5rem;
34
+ --cp-swatch-width: 1.5rem;
35
+
36
+ --cp-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
37
+ --cp-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
38
+
39
+ --cp-bg-checker: repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%) 50% / 0.5rem 0.5rem;
40
+ }
41
+
42
+ .color-picker, .cp_dialog {
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ /* Picker toggle button */
47
+ .color-picker {
48
+ display: inline-flex;
49
+ cursor: pointer;
50
+ padding: 0;
51
+
52
+ border: 1px solid var(--cp-border-color);
53
+
54
+ overflow: hidden;
55
+ border-radius: var(--cp-border-radius-sm);
56
+ background-color: transparent !important; /* CSS reset */
57
+ }
58
+
59
+ .color-picker:not(.cp_wide) {
60
+ padding: 1px;
61
+ height: var(--cp-size);
62
+ border: 2px solid var(--cp-border-color);
63
+ border-style: solid;
64
+ }
65
+
66
+ .color-picker:hover {
67
+ border-color: var(--cp-hover-color) !important;
68
+ }
69
+
70
+ .cp_wide {
71
+ border: 1px solid var(--cp-border-color);
72
+ height: var(--cp-size);
73
+ }
74
+
75
+ .cp_dialog input,
76
+ .cp_dialog button {
77
+ margin: 0; /* CSS reset */
78
+ border-radius: 0; /* CSS reset */
79
+ }
80
+ .cp_dialog button:hover {
81
+ background-color: var(--cp-border-color-translucent) !important;
82
+ }
83
+
84
+ .cp_input {
85
+ display: none !important;
86
+ pointer-events: none;
87
+ margin: 0;
88
+ border: 0;
89
+ }
90
+
91
+ .cp_wide .cp_input {
92
+ font-family: inherit;
93
+ display: block !important;
94
+ flex-grow: 1;
95
+ padding-left: 0.75rem;
96
+ height: inherit; /* or: var(--cp-size) */
97
+ }
98
+
99
+ .cp_wide .cp_input + .cp_button {
100
+ border-left: 1px solid var(--cp-border-color);
101
+ }
102
+
103
+ .cp_wrap {
104
+ display: flex;
105
+ cursor: pointer;
106
+ }
107
+
108
+ .cp_button {
109
+ height: 100%;
110
+ aspect-ratio: 1;
111
+
112
+ text-align: start;
113
+ overflow: hidden;
114
+ cursor: pointer;
115
+
116
+ display: flex;
117
+ justify-content: end;
118
+ align-items: end;
119
+
120
+ background: linear-gradient(var(--cp-current-color), var(--cp-current-color)), var(--cp-bg-checker);
121
+
122
+ border-radius: 3px;
123
+ }
124
+
125
+ .cp_button.cp_unset {
126
+ background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggZmlsbD0iIzM4M2E0YyIgZD0iTTIwOC40OSAxOTEuNTFhMTIgMTIgMCAwIDEtMTcgMTdMMTI4IDE0NWwtNjMuNTEgNjMuNDlhMTIgMTIgMCAwIDEtMTctMTdMMTExIDEyOEw0Ny41MSA2NC40OWExMiAxMiAwIDAgMSAxNy0xN0wxMjggMTExbDYzLjUxLTYzLjUyYTEyIDEyIDAgMCAxIDE3IDE3TDE0NSAxMjhaIi8+PC9zdmc+),
127
+ var(--cp-border-color);
128
+ background-size: 100% 100%;
129
+ }
130
+
131
+ .cp_caret {
132
+ background-color: var(--cp-border-color);
133
+ color: var(--cp-body);
134
+ font-size: calc(var(--cp-size) / 3);
135
+
136
+ padding: 0.1rem 0 0 0.1rem;
137
+ border-top-left-radius: var(--cp-border-radius-sm);
138
+ border-top: 1px solid var(--cp-border-color-translucent);
139
+ border-left: 1px solid var(--cp-border-color-translucent);
140
+
141
+ display: grid;
142
+ place-items: center;
143
+ color: var(--cp-body-color);
144
+ }
145
+ .cp_caret svg {
146
+ transition: transform var(--cp-delay) ease-in-out;
147
+ }
148
+ .cp_open .cp_caret svg {
149
+ transform: rotate(180deg);
150
+ }
151
+
152
+ /* Picker dialog */
153
+ .cp_dialog {
154
+ --cp-base-color: #000;
155
+ --cp-current-color: transparent;
156
+ --cp-color: transparent;
157
+ --cp-hue: 0;
158
+ --cp-alpha: 0;
159
+ --cp-primary: hsl(var(--cp-hue), 50%, 60%);
160
+
161
+ position: absolute;
162
+ z-index: 1055;
163
+ width: 17.2rem;
164
+
165
+ background-color: var(--cp-body-bg);
166
+
167
+ border: 1px solid var(--cp-border-color-translucent);
168
+ border-radius: var(--cp-border-radius-lg);
169
+ box-shadow: var(--cp-box-shadow);
170
+
171
+ opacity: 0;
172
+ pointer-events: none;
173
+
174
+ transition: opacity var(--cp-delay) ease-in-out;
175
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
176
+ Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
177
+ }
178
+
179
+ .cp_dialog-inner {
180
+ display: flex;
181
+ flex-direction: column;
182
+
183
+ gap: 0.75rem;
184
+ padding: 0.75rem;
185
+ }
186
+
187
+ .cp_dialog.cp_open {
188
+ opacity: 1;
189
+ pointer-events: all;
190
+ }
191
+
192
+ /* Preview */
193
+ .cp_sample {
194
+ width: 2rem;
195
+ position: relative;
196
+ background: linear-gradient(var(--cp-color), var(--cp-color)), var(--cp-bg-checker);
197
+ }
198
+
199
+ /* Areas & Sliders */
200
+ .cp_thumb {
201
+ position: absolute;
202
+ transform: translate(-50%, -50%);
203
+ pointer-events: none;
204
+
205
+ width: 1.65rem; /* For Safari */
206
+ height: 1.65rem;
207
+ aspect-ratio: 1;
208
+
209
+ border: 2px solid var(--cp-body-color);
210
+ border-radius: 9999px;
211
+
212
+ box-shadow: var(--cp-box-shadow-sm);
213
+ z-index: 1;
214
+ }
215
+
216
+ .cp_area {
217
+ position: relative;
218
+ height: 8rem;
219
+ }
220
+
221
+ .cp_slider {
222
+ position: relative;
223
+ height: 1.25rem;
224
+ border-radius: 9999px;
225
+ }
226
+ .cp_slider .cp_thumb {
227
+ top: 50%;
228
+ }
229
+
230
+ .cp_area,
231
+ .cp_slider,
232
+ .cp_thumb {
233
+ cursor: grab;
234
+ overscroll-behavior: none;
235
+ touch-action: none;
236
+ }
237
+ .cp_area:active,
238
+ .cp_slider:active,
239
+ .cp_thumb:active {
240
+ cursor: grabbing;
241
+ }
242
+
243
+ /* HSV Area */
244
+ .cp_area-hsv {
245
+ border-radius: var(--cp-border-radius-lg) var(--cp-border-radius-lg) 0px 0px;
246
+ background: linear-gradient(to top, rgba(0, 0, 0, var(--cp-alpha)), transparent),
247
+ linear-gradient(
248
+ to left,
249
+ hsla(var(--cp-hue), 100%, 50%, var(--cp-alpha)),
250
+ rgba(255, 255, 255, var(--cp-alpha))
251
+ ),
252
+ var(--cp-bg-checker);
253
+ }
254
+ .cp_area-hsv .cp_thumb {
255
+ background-color: var(--cp-color);
256
+ }
257
+
258
+ /* Hue Slider */
259
+ .cp_slider-hue {
260
+ background: linear-gradient(
261
+ to right,
262
+ hsl(0, 100%, 50%),
263
+ hsl(60, 100%, 50%),
264
+ hsl(120, 100%, 50%),
265
+ hsl(180, 100%, 50%),
266
+ hsl(240, 100%, 50%),
267
+ hsl(300, 100%, 50%),
268
+ hsl(360, 100%, 50%)
269
+ );
270
+ }
271
+ .cp_slider-hue .cp_thumb {
272
+ background-color: hsl(var(--cp-hue), 100%, 50%);
273
+ }
274
+
275
+ /* Alpha Slider */
276
+ .cp_slider-alpha {
277
+ background: linear-gradient(to right, transparent, var(--cp-base-color)), var(--cp-bg-checker);
278
+ }
279
+ .cp_slider-alpha .cp_thumb {
280
+ background-color: var(--cp-color);
281
+ }
282
+
283
+ /* Form styles */
284
+ .color-picker:focus-visible,
285
+ .cp_dialog *:focus-visible {
286
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
287
+ border-color: var(--cp-body-bg);
288
+ box-shadow: 0 0 0 2px var(--cp-tertiary-color);
289
+ outline: 0;
290
+ }
291
+
292
+ /* Swatches */
293
+ .cp_swatches {
294
+ display: flex;
295
+ flex-wrap: wrap;
296
+ gap: 0.25rem;
297
+ justify-content: center;
298
+ }
299
+ .cp_swatches:empty {
300
+ display: none;
301
+ }
302
+
303
+ .cp_swatch {
304
+ --cp-color: transparent;
305
+
306
+ width: var(--cp-swatch-width);
307
+ aspect-ratio: 1;
308
+ padding: 0;
309
+ border: 1px solid rgba(0, 0, 0, 0.2) !important;
310
+ border-radius: var(--cp-border-radius-sm) !important;
311
+ overflow: hidden;
312
+ cursor: pointer;
313
+ background: linear-gradient(var(--cp-color), var(--cp-color)), var(--cp-bg-checker);
314
+ transition: all 0.2s ease-in-out;
315
+ }
316
+
317
+ .cp_swatch:hover {
318
+ transform: scale(1.15);
319
+ }
320
+
321
+ /* Formats */
322
+ .cp_formats {
323
+ display: flex;
324
+ gap: 0.25rem;
325
+ }
326
+
327
+ .cp_formats:empty {
328
+ display: none;
329
+ }
330
+
331
+ .cp_format {
332
+ flex: 1 1 auto;
333
+ border: none;
334
+ border-radius: 9999px !important;
335
+ padding: 0.125rem 0.25rem;
336
+
337
+ background: none;
338
+ color: var(--cp-body-color);
339
+ font-size: 1rem;
340
+
341
+ cursor: pointer;
342
+ transition: color 100ms ease-out, background-color 100ms ease-out;
343
+ }
344
+ .cp_format:hover {
345
+ background-color: var(--cp-border-color-translucent) !important;
346
+ }
347
+ .cp_format[aria-checked='true'] {
348
+ color: var(--cp-body-color);
349
+ background-color: var(--cp-button-color);
350
+ }
351
+
352
+ /* Input */
353
+ .cp_input-group {
354
+ display: flex;
355
+ gap: 1px;
356
+ box-shadow: var(--cp-box-shadow-sm);
357
+ }
358
+ .cp_input-group > :first-child {
359
+ border-radius: var(--cp-border-radius-sm) 0 0 var(--cp-border-radius-sm);
360
+ }
361
+ .cp_input-group > :last-child {
362
+ border-radius: 0 var(--cp-border-radius-sm) var(--cp-border-radius-sm) 0;
363
+ }
364
+ .cp_input-group > :focus-visible {
365
+ z-index: 1;
366
+ }
367
+
368
+ .cp_value {
369
+ flex: 1 1 auto;
370
+ min-width: 0;
371
+ width: 0;
372
+ text-align: center;
373
+
374
+ border: none;
375
+ border-radius: 0;
376
+ padding: 0.25rem 0.375rem;
377
+ font-size: 0.9rem;
378
+ font-family: inherit;
379
+
380
+ color: var(--cp-body-color);
381
+ background-color: var(--cp-border-color-translucent);
382
+ }
383
+
384
+ /* Action Buttons */
385
+ .cp_action {
386
+ border: none;
387
+ padding: 0.35rem 0.5rem;
388
+
389
+ color: var(--cp-body-color);
390
+ background-color: var(--cp-button-color);
391
+
392
+ cursor: pointer;
393
+ transition: background-color 100ms ease-out;
394
+
395
+ display: flex;
396
+ place-items: center;
397
+ }
398
+ .cp_action:hover {
399
+ background-color: var(--cp-border-color-translucent);
400
+ }
401
+
402
+ /* Icons */
403
+ .cp_icon {
404
+ font-size: 1.15rem;
405
+ /* Use
406
+ color: var(--cp-primary);
407
+ to color buttons to picked color
408
+ */
409
+ }