stimulus_plumbers 0.4.9 → 0.4.11
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/CHANGELOG.md +34 -0
- data/README.md +2 -2
- data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +36 -3
- data/app/assets/javascripts/stimulus-plumbers/index.es.js +351 -173
- data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
- data/docs/component/form.md +27 -5
- data/docs/guide.md +9 -0
- data/lib/generators/stimulus_plumbers/install/install_generator.rb +7 -1
- data/lib/stimulus_plumbers/engine.rb +0 -4
- data/lib/stimulus_plumbers/form/builder.rb +4 -0
- data/lib/stimulus_plumbers/form/field.rb +1 -1
- data/lib/stimulus_plumbers/form/fields/inputs/code.rb +86 -0
- data/lib/stimulus_plumbers/form/fields/inputs/credit_card.rb +98 -0
- data/lib/stimulus_plumbers/form/fields/renderer.rb +2 -0
- data/lib/stimulus_plumbers/generators/css_entrypoint.rb +53 -0
- data/lib/stimulus_plumbers/generators/tokens_directive.rb +5 -5
- data/lib/stimulus_plumbers/themes/schema.rb +43 -22
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/vendor/ARIA.md +7 -0
- data/vendor/component/manifest.json +3 -0
- data/vendor/controller/docs/input-formatter.md +87 -21
- data/vendor/controller/manifest.json +36 -3
- metadata +3 -1
data/docs/component/form.md
CHANGED
|
@@ -61,11 +61,11 @@ Special options on native helpers:
|
|
|
61
61
|
|
|
62
62
|
Three methods render a complete, accessible field:
|
|
63
63
|
|
|
64
|
-
| Method | `as:` values
|
|
65
|
-
| --------------------------------------------------------------------------------- |
|
|
66
|
-
| `f.field(attr, as:, **opts)` | `:text` `:email` `:number` `:url` `:tel` `:color` `:month` `:week` `:range` `:datetime_local` `:text_area` `:file` `:password` `:date` `:time` `:select` `:search` |
|
|
67
|
-
| `f.collection_field(attr, as:, collection:, value_method:, text_method:, **opts)` | `:collection_select` `:grouped_collection_select`
|
|
68
|
-
| `f.choice(attr, as:, **opts)` | `:radio` `:check_box`
|
|
64
|
+
| Method | `as:` values |
|
|
65
|
+
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `f.field(attr, as:, **opts)` | `:text` `:email` `:number` `:url` `:tel` `:color` `:month` `:week` `:range` `:datetime_local` `:text_area` `:file` `:password` `:date` `:time` `:select` `:search` `:code` `:credit_card` |
|
|
67
|
+
| `f.collection_field(attr, as:, collection:, value_method:, text_method:, **opts)` | `:collection_select` `:grouped_collection_select` |
|
|
68
|
+
| `f.choice(attr, as:, **opts)` | `:radio` `:check_box` |
|
|
69
69
|
|
|
70
70
|
### Shared field options
|
|
71
71
|
|
|
@@ -89,6 +89,8 @@ Three methods render a complete, accessible field:
|
|
|
89
89
|
<%= f.field :bio, as: :text_area, hint: "Tell us about yourself." %>
|
|
90
90
|
<%= f.field :avatar, as: :file %>
|
|
91
91
|
<%= f.field :password, as: :password, revealable: true %>
|
|
92
|
+
<%= f.field :verification_code, as: :code, length: 6 %>
|
|
93
|
+
<%= f.field :card_number, as: :credit_card %>
|
|
92
94
|
<%= f.field :email, as: :email, floating: :filled %>
|
|
93
95
|
<%= f.field :country, as: :select,
|
|
94
96
|
choices: [["Australia", "au"], ["Canada", "ca"], ["United States", "us"]],
|
|
@@ -147,6 +149,26 @@ Use `f.search_field` for a native `<input type="search">`.
|
|
|
147
149
|
|
|
148
150
|
Use `f.password_field` for a plain `<input type="password">` (also accepts `revealable:`).
|
|
149
151
|
|
|
152
|
+
**Code** (`as: :code`) — character-cell entry backed by `input-formatter` and the `character-cells` plumber. The native input remains the accessible control; cells are decorative.
|
|
153
|
+
|
|
154
|
+
| Option | Values | Default | Description |
|
|
155
|
+
| -------------- | ---------------------------------------- | ---------------------- | ------------------------------------------- |
|
|
156
|
+
| `length` | positive Integer | required | Number of cells and input maximum length |
|
|
157
|
+
| `charset` | `:digits` / `:letters` / `:alphanumeric` | `:digits` | Allowed code characters |
|
|
158
|
+
| `groups` | Array of positive Integers | `[]` | Visual cell groups; must add up to `length` |
|
|
159
|
+
| `autocomplete` | String | `"one-time-code"` | Native autocomplete value |
|
|
160
|
+
| `inputmode` | String | `"numeric"` for digits | Native input mode |
|
|
161
|
+
|
|
162
|
+
**Credit card** (`as: :credit_card`) — grouped card-number entry backed by `input-formatter` and the `character-cells` plumber (grouped mode). Renders one cell per group with a literal dash separator between cells; their sum sets `maxlength`.
|
|
163
|
+
|
|
164
|
+
| Option | Values | Default | Description |
|
|
165
|
+
| -------------- | -------------------------- | -------------- | ---------------------------------------------------------------------------- |
|
|
166
|
+
| `groups` | Array of positive Integers | `[4, 4, 4, 4]` | Cell groups — one cell per group, dash-separated; their sum sets `maxlength` |
|
|
167
|
+
| `autocomplete` | String | `"cc-number"` | Native autocomplete value |
|
|
168
|
+
| `inputmode` | String | `"numeric"` | Native input mode |
|
|
169
|
+
|
|
170
|
+
Character-cell fields do not support `floating:` labels.
|
|
171
|
+
|
|
150
172
|
---
|
|
151
173
|
|
|
152
174
|
## f.collection_field
|
data/docs/guide.md
CHANGED
|
@@ -37,3 +37,12 @@ generator) inject their CSS directives into the first entry file found, in order
|
|
|
37
37
|
4. `app/javascript/entrypoints/application.css` (`jsbundling-rails` esbuild/webpack default)
|
|
38
38
|
|
|
39
39
|
Override with `STIMULUS_PLUMBERS_CSS_ENTRY=/path/to/entry.css` — used by both gems' generators.
|
|
40
|
+
|
|
41
|
+
## Installed CSS files
|
|
42
|
+
|
|
43
|
+
The core installer copies its token defaults to
|
|
44
|
+
`app/assets/stylesheets/stimulus_plumbers/tokens.css` and imports that app-local file.
|
|
45
|
+
|
|
46
|
+
This file is application-owned: later generator runs create a missing file but never overwrite an
|
|
47
|
+
existing one. They also replace legacy imports that pointed into an installed gem with the new
|
|
48
|
+
relative app-local import.
|
|
@@ -9,13 +9,19 @@ module StimulusPlumbers
|
|
|
9
9
|
class InstallGenerator < Rails::Generators::Base
|
|
10
10
|
include StimulusPlumbers::Generators::CssEntrypoint
|
|
11
11
|
|
|
12
|
+
TOKENS_CSS_SOURCE = File.join(
|
|
13
|
+
File.expand_path("../../../..", __dir__),
|
|
14
|
+
TokensDirective::TOKENS_CSS_PATH
|
|
15
|
+
)
|
|
16
|
+
|
|
12
17
|
def install
|
|
13
18
|
css_file = entry_css_file(**css_file_lookup_options)
|
|
14
19
|
return warn_entry_css_not_found(label: "CSS", **css_file_lookup_options) unless css_file
|
|
20
|
+
return unless copy_asset(TOKENS_CSS_SOURCE, TokensDirective::TOKENS_CSS_PATH)
|
|
15
21
|
|
|
16
22
|
apply_edit(
|
|
17
23
|
css_file,
|
|
18
|
-
TokensDirective.directive(from: File.dirname(css_file)),
|
|
24
|
+
TokensDirective.directive(from: File.dirname(css_file), destination_root: destination_root),
|
|
19
25
|
stale_pattern: TokensDirective.stale_pattern
|
|
20
26
|
)
|
|
21
27
|
end
|
|
@@ -9,10 +9,6 @@ module StimulusPlumbers
|
|
|
9
9
|
config.autoload_paths << File.expand_path("../stimulus-plumbers", __dir__)
|
|
10
10
|
config.i18n.load_path += Dir[File.expand_path("../../config/locales/*.{rb,yml}", __dir__)]
|
|
11
11
|
|
|
12
|
-
initializer "stimulus_plumbers.assets", after: :set_default_precompile do |app|
|
|
13
|
-
app.config.assets.precompile += %w[stimulus_plumbers/tokens.css] if app.config.respond_to?(:assets)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
12
|
initializer "stimulus_plumbers.helpers" do
|
|
17
13
|
ActiveSupport.on_load(:action_view) do
|
|
18
14
|
include StimulusPlumbers::Helpers
|
|
@@ -11,6 +11,8 @@ require_relative "fields/renderer"
|
|
|
11
11
|
require_relative "fields/fieldset"
|
|
12
12
|
require_relative "fields/inputs/checkbox"
|
|
13
13
|
require_relative "fields/inputs/combobox"
|
|
14
|
+
require_relative "fields/inputs/code"
|
|
15
|
+
require_relative "fields/inputs/credit_card"
|
|
14
16
|
require_relative "fields/inputs/datetime"
|
|
15
17
|
require_relative "fields/inputs/radio"
|
|
16
18
|
require_relative "fields/inputs/file"
|
|
@@ -30,6 +32,8 @@ module StimulusPlumbers
|
|
|
30
32
|
include Plumber::Options::Html
|
|
31
33
|
include Plumber::Options::Aria
|
|
32
34
|
include Fields::Inputs::Checkbox
|
|
35
|
+
include Fields::Inputs::Code
|
|
36
|
+
include Fields::Inputs::CreditCard
|
|
33
37
|
include Fields::Inputs::Datetime
|
|
34
38
|
include Fields::Inputs::Radio
|
|
35
39
|
include Fields::Inputs::File
|
|
@@ -8,7 +8,7 @@ module StimulusPlumbers
|
|
|
8
8
|
class Field < Base
|
|
9
9
|
TYPES = %i[
|
|
10
10
|
text email number url tel color month week range datetime_local
|
|
11
|
-
text_area file password date time select search
|
|
11
|
+
text_area file password date time select search code credit_card
|
|
12
12
|
].freeze
|
|
13
13
|
COLLECTION_TYPES = %i[radio check_box collection_select grouped_collection_select].freeze
|
|
14
14
|
OPTIONS = (Base::OPTIONS + %i[hide_label]).freeze
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Form
|
|
5
|
+
module Fields
|
|
6
|
+
module Inputs
|
|
7
|
+
module Code
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def render_code_input(attribute, html_opts, opts, error, length:, charset: :digits, groups: [], **kwargs)
|
|
11
|
+
floating = kwargs.delete(:floating)
|
|
12
|
+
raise ArgumentError, "floating labels are not supported for code fields" if floating.present?
|
|
13
|
+
|
|
14
|
+
validate_code_options!(length, groups)
|
|
15
|
+
inputmode = kwargs.delete(:inputmode) || (charset.to_sym == :digits ? "numeric" : nil)
|
|
16
|
+
autocomplete = kwargs.delete(:autocomplete) || "one-time-code"
|
|
17
|
+
config = { error: error, length: length, charset: charset, groups: groups,
|
|
18
|
+
autocomplete: autocomplete, inputmode: inputmode, kwargs: kwargs
|
|
19
|
+
}
|
|
20
|
+
render_code_field(attribute, html_opts, opts, config)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_code_field(attribute, html_opts, opts, config)
|
|
24
|
+
@template.content_tag(:div, **code_wrapper_options(config)) do
|
|
25
|
+
@template.safe_join(
|
|
26
|
+
[render_code_cells(config), code_input(attribute, html_opts, opts, config)]
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def code_wrapper_options(config)
|
|
32
|
+
merge_html_options(
|
|
33
|
+
theme.resolve(:form_field_input_code, error: config[:error]),
|
|
34
|
+
{ data: {
|
|
35
|
+
controller: "input-formatter",
|
|
36
|
+
input_formatter_format_value: "code",
|
|
37
|
+
input_formatter_options_value: { charset: config[:charset].to_s, length: config[:length] }.to_json,
|
|
38
|
+
input_formatter_groups_value: config[:groups].presence
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def code_input(attribute, html_opts, opts, config)
|
|
45
|
+
input_options = merge_html_options(
|
|
46
|
+
theme.resolve(:form_field_input_code_overlay, error: config[:error]),
|
|
47
|
+
opts,
|
|
48
|
+
html_opts,
|
|
49
|
+
config[:kwargs],
|
|
50
|
+
{ maxlength: config[:length], autocomplete: config[:autocomplete], inputmode: config[:inputmode],
|
|
51
|
+
data: { input_formatter_target: "input", action: formatter_actions }
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
@template.text_field(@object_name, attribute, objectify_options(input_options))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_code_cells(config)
|
|
58
|
+
count = config[:groups].present? ? config[:groups].sum : config[:length]
|
|
59
|
+
cell_options = merge_html_options(
|
|
60
|
+
theme.resolve(:form_field_input_code_cell, error: config[:error]),
|
|
61
|
+
{ data: { input_formatter_target: "cell" } }
|
|
62
|
+
)
|
|
63
|
+
@template.content_tag(:div, **merge_html_options(theme.resolve(:form_field_input_code_cells))) do
|
|
64
|
+
@template.safe_join(Array.new(count) { @template.content_tag(:span, nil, **cell_options) })
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_code_options!(length, groups)
|
|
69
|
+
raise ArgumentError, "length must be a positive integer" unless positive_integer?(length)
|
|
70
|
+
return if groups.blank?
|
|
71
|
+
|
|
72
|
+
raise ArgumentError, "groups must be an array of positive integers" unless valid_groups?(groups)
|
|
73
|
+
raise ArgumentError, "groups must add up to length" unless groups.sum == length
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def valid_groups?(groups) = groups.is_a?(Array) && groups.all? { |group| positive_integer?(group) }
|
|
77
|
+
def positive_integer?(value) = value.is_a?(Integer) && value.positive?
|
|
78
|
+
|
|
79
|
+
def formatter_actions
|
|
80
|
+
"input->input-formatter#onInput focus->input-formatter#onFocus blur->input-formatter#onBlur"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Form
|
|
5
|
+
module Fields
|
|
6
|
+
module Inputs
|
|
7
|
+
module CreditCard
|
|
8
|
+
DEFAULT_GROUPS = [4, 4, 4, 4].freeze
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def render_credit_card_input(attribute, html_opts, opts, error, groups: DEFAULT_GROUPS, floating: nil, **kwargs)
|
|
13
|
+
raise ArgumentError, "floating labels are not supported for credit card fields" if floating.present?
|
|
14
|
+
|
|
15
|
+
normalized_groups = groups.presence || DEFAULT_GROUPS
|
|
16
|
+
inputmode = kwargs.delete(:inputmode) || "numeric"
|
|
17
|
+
autocomplete = kwargs.delete(:autocomplete) || "cc-number"
|
|
18
|
+
validate_credit_card_groups!(normalized_groups)
|
|
19
|
+
length = normalized_groups.sum
|
|
20
|
+
config = { error: error, groups: normalized_groups, length: length,
|
|
21
|
+
autocomplete: autocomplete, inputmode: inputmode, kwargs: kwargs
|
|
22
|
+
}
|
|
23
|
+
render_credit_card_field(attribute, html_opts, opts, config)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_credit_card_field(attribute, html_opts, opts, config)
|
|
27
|
+
@template.content_tag(:div, **credit_card_wrapper_options(config)) do
|
|
28
|
+
@template.safe_join(
|
|
29
|
+
[render_credit_card_cells(config), credit_card_input(attribute, html_opts, opts, config)]
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def credit_card_wrapper_options(config)
|
|
35
|
+
merge_html_options(
|
|
36
|
+
theme.resolve(:form_field_input_credit_card, error: config[:error]),
|
|
37
|
+
{ data: {
|
|
38
|
+
controller: "input-formatter",
|
|
39
|
+
input_formatter_format_value: "creditCard",
|
|
40
|
+
input_formatter_groups_value: config[:groups]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def credit_card_input(attribute, html_opts, opts, config)
|
|
47
|
+
input_options = merge_html_options(
|
|
48
|
+
theme.resolve(:form_field_input_credit_card_overlay, error: config[:error]),
|
|
49
|
+
opts,
|
|
50
|
+
html_opts,
|
|
51
|
+
config[:kwargs],
|
|
52
|
+
{ maxlength: config[:length], autocomplete: config[:autocomplete], inputmode: config[:inputmode],
|
|
53
|
+
data: { input_formatter_target: "input", action: formatter_actions }
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
@template.text_field(@object_name, attribute, objectify_options(input_options))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_credit_card_cells(config)
|
|
60
|
+
cells = config[:groups].each_index.flat_map do |index|
|
|
61
|
+
index.zero? ? [render_credit_card_cell(config)] : [render_credit_card_separator, render_credit_card_cell(config)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@template.content_tag(:div, **merge_html_options(theme.resolve(:form_field_input_credit_card_cells))) do
|
|
65
|
+
@template.safe_join(cells)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_credit_card_cell(config)
|
|
70
|
+
cell_options = merge_html_options(
|
|
71
|
+
theme.resolve(:form_field_input_credit_card_cell, error: config[:error]),
|
|
72
|
+
{ data: { input_formatter_target: "cell" } }
|
|
73
|
+
)
|
|
74
|
+
@template.content_tag(:span, nil, **cell_options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_credit_card_separator
|
|
78
|
+
separator_options = merge_html_options(
|
|
79
|
+
theme.resolve(:form_field_input_credit_card_separator),
|
|
80
|
+
{ aria: { hidden: true } }
|
|
81
|
+
)
|
|
82
|
+
@template.content_tag(:span, "-", **separator_options)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_credit_card_groups!(groups)
|
|
86
|
+
return if groups.is_a?(Array) && groups.all? { |group| group.is_a?(Integer) && group.positive? }
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "groups must be an array of positive integers"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def formatter_actions
|
|
92
|
+
"input->input-formatter#onInput focus->input-formatter#onFocus blur->input-formatter#onBlur"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -18,6 +18,8 @@ module StimulusPlumbers
|
|
|
18
18
|
text_area: :render_text_area_input,
|
|
19
19
|
file: :render_file_input,
|
|
20
20
|
password: :render_password_input,
|
|
21
|
+
code: :render_code_input,
|
|
22
|
+
credit_card: :render_credit_card_input,
|
|
21
23
|
date: :render_combobox_date,
|
|
22
24
|
time: :render_combobox_time,
|
|
23
25
|
select: :render_combobox_dropdown,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
module StimulusPlumbers
|
|
4
6
|
module Generators
|
|
5
7
|
module CssEntrypoint
|
|
@@ -48,6 +50,57 @@ module StimulusPlumbers
|
|
|
48
50
|
say "Could not update #{relative_to_destination(css_file)}: #{e.message}. Skipping.", :yellow
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
def remove_lines(css_file, pattern)
|
|
54
|
+
content = File.read(css_file)
|
|
55
|
+
return unless content.match?(pattern)
|
|
56
|
+
|
|
57
|
+
File.write(css_file, content.lines.grep_v(pattern).join)
|
|
58
|
+
say_status :update, relative_to_destination(css_file), :green
|
|
59
|
+
rescue Errno::EROFS, Errno::EACCES => e
|
|
60
|
+
say "Could not update #{relative_to_destination(css_file)}: #{e.message}. Skipping.", :yellow
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_generated(path, contents)
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
65
|
+
File.write(path, contents)
|
|
66
|
+
say_status :create, relative_to_destination(path), :green
|
|
67
|
+
rescue Errno::EROFS, Errno::EACCES => e
|
|
68
|
+
say "Could not write #{relative_to_destination(path)}: #{e.message}. Skipping.", :yellow
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def append_to_gitignore(path)
|
|
72
|
+
gitignore = File.join(destination_root, ".gitignore")
|
|
73
|
+
return unless File.exist?(gitignore)
|
|
74
|
+
|
|
75
|
+
entry = "/#{relative_to_destination(path)}"
|
|
76
|
+
return if File.readlines(gitignore, chomp: true).include?(entry)
|
|
77
|
+
|
|
78
|
+
File.open(gitignore, "a") { |file| file.puts(entry) }
|
|
79
|
+
say_status :append, ".gitignore", :green
|
|
80
|
+
rescue Errno::EROFS, Errno::EACCES => e
|
|
81
|
+
say "Could not update .gitignore: #{e.message}. Skipping.", :yellow
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# CSS copied by an installer belongs to the application from this point on.
|
|
85
|
+
# Keep an existing file intact so a generator rerun never overwrites local
|
|
86
|
+
# customizations (the same policy used by tailwindcss-rails' installer).
|
|
87
|
+
def copy_asset(source, destination)
|
|
88
|
+
path = File.join(destination_root, destination)
|
|
89
|
+
|
|
90
|
+
if File.exist?(path)
|
|
91
|
+
say_status :identical, destination
|
|
92
|
+
return true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
96
|
+
FileUtils.cp(source, path)
|
|
97
|
+
say_status :create, destination, :green
|
|
98
|
+
true
|
|
99
|
+
rescue Errno::EROFS, Errno::EACCES => e
|
|
100
|
+
say "Could not copy #{destination}: #{e.message}. Skipping.", :yellow
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
51
104
|
# `stale_pattern:` is required — every call site needs stale-import detection,
|
|
52
105
|
# so making it optional would leave an untested "anchor without stale" state.
|
|
53
106
|
def content_edit(content, directive, stale_pattern:, anchor_pattern: nil)
|
|
@@ -5,19 +5,19 @@ require "pathname"
|
|
|
5
5
|
module StimulusPlumbers
|
|
6
6
|
module Generators
|
|
7
7
|
module TokensDirective
|
|
8
|
-
|
|
9
|
-
TOKENS_CSS_REL_PATH = "app/assets/stylesheets/stimulus_plumbers/tokens.css"
|
|
8
|
+
TOKENS_CSS_PATH = "app/assets/stylesheets/stimulus_plumbers/tokens.css"
|
|
10
9
|
|
|
11
10
|
module_function
|
|
12
11
|
|
|
13
|
-
def directive(from:)
|
|
14
|
-
path = Pathname.new(
|
|
12
|
+
def directive(from:, destination_root:)
|
|
13
|
+
path = Pathname.new(destination_root).join(TOKENS_CSS_PATH)
|
|
15
14
|
rel = path.relative_path_from(Pathname.new(from))
|
|
15
|
+
rel = "./#{rel}" unless rel.to_s.start_with?(".", "/")
|
|
16
16
|
%(@import "#{rel}";)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def stale_pattern
|
|
20
|
-
%r{@import "[^"]*#{Regexp.escape(
|
|
20
|
+
%r{@import "[^"]*#{Regexp.escape(TOKENS_CSS_PATH)}";}
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -119,69 +119,90 @@ module StimulusPlumbers
|
|
|
119
119
|
}.freeze
|
|
120
120
|
|
|
121
121
|
FORM = {
|
|
122
|
-
form_group:
|
|
122
|
+
form_group: {
|
|
123
123
|
layout: { default: :stacked, validate: Form::Ranges::LAYOUT },
|
|
124
124
|
error: { default: false, validate: Ranges::BOOL }
|
|
125
125
|
}.freeze,
|
|
126
|
-
form_field_label:
|
|
126
|
+
form_field_label: {
|
|
127
127
|
required: { default: false, validate: Ranges::BOOL },
|
|
128
128
|
hidden: { default: false, validate: Ranges::BOOL },
|
|
129
129
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] },
|
|
130
130
|
error: { default: false, validate: Ranges::BOOL }
|
|
131
131
|
}.freeze,
|
|
132
|
-
form_field_required_mark:
|
|
133
|
-
form_field_hint:
|
|
134
|
-
form_field_error:
|
|
135
|
-
form_field_choice_items:
|
|
132
|
+
form_field_required_mark: {}.freeze,
|
|
133
|
+
form_field_hint: {}.freeze,
|
|
134
|
+
form_field_error: {}.freeze,
|
|
135
|
+
form_field_choice_items: {
|
|
136
136
|
layout: { default: :stacked, validate: Form::Ranges::LAYOUT }
|
|
137
137
|
}.freeze,
|
|
138
|
-
form_field_checkbox_label:
|
|
138
|
+
form_field_checkbox_label: {
|
|
139
139
|
type: { default: :default, validate: Form::Checkbox::Ranges::TYPE },
|
|
140
140
|
variant: { default: :tertiary, validate: Form::Checkbox::Ranges::VARIANT }
|
|
141
141
|
}.freeze,
|
|
142
|
-
form_field_radio_label:
|
|
142
|
+
form_field_radio_label: {
|
|
143
143
|
type: { default: :default, validate: Form::Radio::Ranges::TYPE },
|
|
144
144
|
variant: { default: :tertiary, validate: Form::Radio::Ranges::VARIANT }
|
|
145
145
|
}.freeze,
|
|
146
|
-
form_field_radio_item_group:
|
|
147
|
-
form_field_input:
|
|
146
|
+
form_field_radio_item_group: {}.freeze,
|
|
147
|
+
form_field_input: {
|
|
148
148
|
error: { default: false, validate: Ranges::BOOL },
|
|
149
149
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
150
150
|
}.freeze,
|
|
151
|
-
form_field_input_group:
|
|
151
|
+
form_field_input_group: {
|
|
152
152
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
153
153
|
}.freeze,
|
|
154
|
-
form_field_input_textarea:
|
|
154
|
+
form_field_input_textarea: {
|
|
155
155
|
error: { default: false, validate: Ranges::BOOL },
|
|
156
156
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
157
157
|
}.freeze,
|
|
158
|
-
form_field_input_file:
|
|
158
|
+
form_field_input_file: {
|
|
159
159
|
error: { default: false, validate: Ranges::BOOL },
|
|
160
160
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
161
161
|
}.freeze,
|
|
162
|
-
form_field_input_select:
|
|
162
|
+
form_field_input_select: {
|
|
163
163
|
error: { default: false, validate: Ranges::BOOL },
|
|
164
164
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
165
165
|
}.freeze,
|
|
166
|
-
form_field_input_checkbox:
|
|
166
|
+
form_field_input_checkbox: {
|
|
167
167
|
error: { default: false, validate: Ranges::BOOL },
|
|
168
168
|
type: { default: :default, validate: Form::Checkbox::Ranges::TYPE },
|
|
169
169
|
variant: { default: :tertiary, validate: Form::Checkbox::Ranges::VARIANT }
|
|
170
170
|
}.freeze,
|
|
171
|
-
form_field_input_radio:
|
|
171
|
+
form_field_input_radio: {
|
|
172
172
|
error: { default: false, validate: Ranges::BOOL },
|
|
173
173
|
type: { default: :default, validate: Form::Radio::Ranges::TYPE },
|
|
174
174
|
variant: { default: :tertiary, validate: Form::Radio::Ranges::VARIANT }
|
|
175
175
|
}.freeze,
|
|
176
|
-
form_field_input_combobox:
|
|
176
|
+
form_field_input_combobox: {
|
|
177
177
|
error: { default: false, validate: Ranges::BOOL },
|
|
178
178
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
179
179
|
}.freeze,
|
|
180
|
-
form_field_input_reveal:
|
|
181
|
-
form_field_input_clearable:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
form_field_input_reveal: { error: { default: false, validate: Ranges::BOOL } }.freeze,
|
|
181
|
+
form_field_input_clearable: {}.freeze,
|
|
182
|
+
form_field_input_code: {
|
|
183
|
+
error: { default: false, validate: Ranges::BOOL }
|
|
184
|
+
}.freeze,
|
|
185
|
+
form_field_input_code_cells: {}.freeze,
|
|
186
|
+
form_field_input_code_cell: {
|
|
187
|
+
error: { default: false, validate: Ranges::BOOL }
|
|
188
|
+
}.freeze,
|
|
189
|
+
form_field_input_code_overlay: {
|
|
190
|
+
error: { default: false, validate: Ranges::BOOL }
|
|
191
|
+
}.freeze,
|
|
192
|
+
form_field_input_credit_card: {
|
|
193
|
+
error: { default: false, validate: Ranges::BOOL }
|
|
194
|
+
}.freeze,
|
|
195
|
+
form_field_input_credit_card_cells: {}.freeze,
|
|
196
|
+
form_field_input_credit_card_cell: {
|
|
197
|
+
error: { default: false, validate: Ranges::BOOL }
|
|
198
|
+
}.freeze,
|
|
199
|
+
form_field_input_credit_card_separator: {}.freeze,
|
|
200
|
+
form_field_input_credit_card_overlay: {
|
|
201
|
+
error: { default: false, validate: Ranges::BOOL }
|
|
202
|
+
}.freeze,
|
|
203
|
+
form_field_input_button_reveal: {}.freeze,
|
|
204
|
+
form_field_input_button_clear: {}.freeze,
|
|
205
|
+
form_submit: {
|
|
185
206
|
type: { default: :default, validate: Button::Ranges::TYPE },
|
|
186
207
|
variant: { default: :primary, validate: Button::Ranges::VARIANT }
|
|
187
208
|
}.freeze
|
data/vendor/ARIA.md
CHANGED
|
@@ -62,6 +62,13 @@ Two helper classes handle keyboard navigation in controllers — see [`stimulus-
|
|
|
62
62
|
- Toggle button: `aria-label` describes action ("Show password" / "Hide password")
|
|
63
63
|
- `aria-pressed` managed on toggle button, reflecting revealed state
|
|
64
64
|
|
|
65
|
+
#### Code Input (`input_formatter_controller` + `character_cells`)
|
|
66
|
+
|
|
67
|
+
- The real `<input>` is the entire accessible surface; cells are stamped `aria-hidden="true"` automatically.
|
|
68
|
+
- The input requires a `<label>` (visually hidden allowed), `autocomplete="one-time-code"` for OTP (WCAG 1.3.5), `inputmode` matching the charset, and `maxlength` matching the code length.
|
|
69
|
+
- Overlay the input with `opacity: 0` — never `display: none`/`visibility: hidden`, which remove it from the tab order (WCAG 2.1.1).
|
|
70
|
+
- Focus visibility (WCAG 2.4.7): the caret cell (`data-caret`) appears only while the input has focus; style the wrapper with `:focus-within` plus `[data-caret]`.
|
|
71
|
+
|
|
65
72
|
#### Input Clearable (`input_clearable_controller`)
|
|
66
73
|
- Clear button: `hidden` attribute while the input is empty, removing it from the keyboard/AT tab order until there's something to clear
|
|
67
74
|
- Escape inside the input clears it (keyboard equivalent to clicking the clear button, WCAG 2.1.1); default is prevented so it doesn't also close a parent overlay
|