basecoat 2.2.1 → 2.2.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 +138 -1
- data/lib/basecoat/form_builder.rb +22 -0
- data/lib/basecoat/form_helper.rb +239 -0
- data/lib/basecoat/railtie.rb +7 -0
- data/lib/basecoat/version.rb +1 -1
- data/lib/basecoat.rb +8 -0
- data/lib/generators/basecoat/templates/devise/shared/_error_messages.html.erb +1 -3
- data/lib/generators/basecoat/templates/layouts/_alert.html.erb +1 -3
- data/lib/generators/basecoat/templates/layouts/_aside.html.erb +1 -1
- data/lib/generators/basecoat/templates/layouts/_form_errors.html.erb +1 -3
- data/lib/generators/basecoat/templates/layouts/_header.html.erb +1 -1
- data/lib/generators/basecoat/templates/layouts/_notice.html.erb +1 -4
- data/lib/generators/basecoat/templates/layouts/_theme_toggle.html.erb +2 -2
- data/lib/generators/basecoat/templates/scaffold_hook.rb +1 -5
- data/lib/generators/basecoat/templates/search_controller.js +21 -0
- data/lib/generators/basecoat/templates/shared/_empty.html.erb +1 -1
- data/lib/tasks/basecoat.rake +81 -61
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 745c8eb3926d13292445d2d4fbd0b7550bfe5573ef3888ed27c19fc8018fe9c4
|
|
4
|
+
data.tar.gz: 321640c46d3f61ee0b87beb7214be8f4cd82ed31f10f4291a26d99a9cb455430
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a415461f36459cca8d34235c3939c51b8aa754bc68c5691ebc65f083bc8e2c6a086f600309c326127cb38e3a45f3b3a61dcc0c3684936b4641a2ba0ffc8a566
|
|
7
|
+
data.tar.gz: d021c20201711920476b6985174c60d27a8e4fa9d5faaaadb473196a15c9b7407000ad903e584b200df122ef9d69b2d0366302eef25b52078a4d6076d32eee47
|
data/README.md
CHANGED
|
@@ -30,6 +30,143 @@ Basecoat CSS combines tailwind with clean css classes. It creates the looks of s
|
|
|
30
30
|
|
|
31
31
|
If you need more complex components; enrich the views with https://railsblocks.com/ or https://shadcn.rails-components.com/ or just the shadcn React components themselves.
|
|
32
32
|
|
|
33
|
+
## Icons
|
|
34
|
+
|
|
35
|
+
Basecoat uses the [lucide-rails](https://github.com/heyvito/lucide-rails) gem for beautiful, consistent icons throughout the UI. The gem is automatically installed as a dependency.
|
|
36
|
+
|
|
37
|
+
You can use Lucide icons in your views with the `lucide_icon` helper:
|
|
38
|
+
|
|
39
|
+
```erb
|
|
40
|
+
<%= lucide_icon "home" %>
|
|
41
|
+
<%= lucide_icon "user", class: "w-5 h-5" %>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Browse available icons at [lucide.dev](https://lucide.dev/icons/).
|
|
45
|
+
|
|
46
|
+
### Using Different Icon Libraries
|
|
47
|
+
|
|
48
|
+
If you prefer to use a different icon library, you can switch to the [rails_icons](https://github.com/nejdetkadir/rails_icons) gem which supports multiple icon libraries including Lucide, Font Awesome, Heroicons, and more. Simply:
|
|
49
|
+
|
|
50
|
+
1. Add `rails_icons` to your Gemfile
|
|
51
|
+
2. Replace `lucide_icon` calls with `icon` calls from rails_icons in the generated views
|
|
52
|
+
3. Configure your preferred icon library in `config/initializers/rails_icons.rb`
|
|
53
|
+
|
|
54
|
+
## Form Helpers
|
|
55
|
+
|
|
56
|
+
Basecoat includes custom form helpers for enhanced UI components:
|
|
57
|
+
|
|
58
|
+
### Select Component
|
|
59
|
+
|
|
60
|
+
Use `basecoat_select` in forms or `basecoat_select_tag` outside of forms:
|
|
61
|
+
|
|
62
|
+
```erb
|
|
63
|
+
<%= form_for @user do |f| %>
|
|
64
|
+
<%= f.basecoat_select :fruit, [["Apple", 1], ["Pear", 2]] %>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<%# Or without a form: %>
|
|
68
|
+
<%= basecoat_select_tag "fruit", [["Apple", 1], ["Pear", 2]] %>
|
|
69
|
+
|
|
70
|
+
<%# With options: %>
|
|
71
|
+
<%= f.basecoat_select :fruit, [["Apple", 1], ["Pear", 2]],
|
|
72
|
+
group_label: "Fruits",
|
|
73
|
+
placeholder: "Search fruits..." %>
|
|
74
|
+
|
|
75
|
+
<%# You can add a remote url, which does a turbo call %>
|
|
76
|
+
<%= f.basecoat_select :fruit, [[]], url: "/fruits/search", turbo_frame: "custom_frame" %>
|
|
77
|
+
|
|
78
|
+
`fruits/search.turbo_stream.erb` should then have the following content:
|
|
79
|
+
|
|
80
|
+
<%= turbo_stream.update "custom_frame" do %>
|
|
81
|
+
<% @fruits.each do |fruit| %>
|
|
82
|
+
<%= tag.div fruit.name, role: "option", data: { value: fruit.id } %>
|
|
83
|
+
<% end %>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
# If you don't add the turbo_frame option there's a fallback to underscored URL (_fruits_search)
|
|
87
|
+
<%= f.basecoat_select :fruit, [[]], url: "/fruits/search" %>
|
|
88
|
+
|
|
89
|
+
Make sure this matches the frame in your partial!
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Remote Search Component
|
|
93
|
+
|
|
94
|
+
Use `basecoat_remote_search_tag` for a standalone search component with Turbo Stream support:
|
|
95
|
+
|
|
96
|
+
```erb
|
|
97
|
+
<%# Basic usage: %>
|
|
98
|
+
<%= basecoat_remote_search_tag("/posts/search") %>
|
|
99
|
+
|
|
100
|
+
<%# With custom turbo frame name: %>
|
|
101
|
+
<%= basecoat_remote_search_tag("/posts/search", turbo_frame: "custom_frame") %>
|
|
102
|
+
|
|
103
|
+
<%# With custom placeholder: %>
|
|
104
|
+
<%= basecoat_remote_search_tag("/posts/search", placeholder: "Search posts...") %>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Your controller should respond with a Turbo Stream that updates the frame:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# posts_controller.rb
|
|
111
|
+
def search
|
|
112
|
+
@posts = Post.where("title LIKE ?", "%#{params[:query]}%")
|
|
113
|
+
|
|
114
|
+
respond_to do |format|
|
|
115
|
+
format.turbo_stream
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```erb
|
|
121
|
+
<%# posts/search.turbo_stream.erb %>
|
|
122
|
+
<%= turbo_stream.update "_posts_search" do %>
|
|
123
|
+
<% @posts.each do |post| %>
|
|
124
|
+
<%= link_to post.title, post_path(post), class: "block p-2 hover:bg-muted" %>
|
|
125
|
+
<% end %>
|
|
126
|
+
<% end %>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Options:
|
|
130
|
+
- `url` (required): The URL to fetch search results from
|
|
131
|
+
- `turbo_frame`: Custom turbo frame name (defaults to underscored URL, e.g., `/posts/search` becomes `_posts_search`)
|
|
132
|
+
- `placeholder`: Custom placeholder text (defaults to "Type a command or search...")
|
|
133
|
+
|
|
134
|
+
### Country Select Component
|
|
135
|
+
|
|
136
|
+
Use `basecoat_country_select_tag` for a country picker with flag emojis:
|
|
137
|
+
|
|
138
|
+
```erb
|
|
139
|
+
<%# Basic usage (all countries): %>
|
|
140
|
+
<%= basecoat_country_select_tag :country %>
|
|
141
|
+
|
|
142
|
+
<%# With pre-selected country: %>
|
|
143
|
+
<%= basecoat_country_select_tag :country, selected: "US" %>
|
|
144
|
+
|
|
145
|
+
<%# With priority countries at the top: %>
|
|
146
|
+
<%= basecoat_country_select_tag :country, priority: ["US", "CA", "GB"] %>
|
|
147
|
+
|
|
148
|
+
<%# Only show specific countries: %>
|
|
149
|
+
<%= basecoat_country_select_tag :country, countries: ["US", "CA", "MX"] %>
|
|
150
|
+
|
|
151
|
+
<%# Exclude specific countries: %>
|
|
152
|
+
<%= basecoat_country_select_tag :country, except: ["KP", "IR"] %>
|
|
153
|
+
|
|
154
|
+
<%# In a form: %>
|
|
155
|
+
<%= form_for @user do |f| %>
|
|
156
|
+
<%= f.label :country %>
|
|
157
|
+
<%= f.basecoat_country_select :country %>
|
|
158
|
+
<% end %>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Options:
|
|
162
|
+
- `selected`: Pre-select a country by its ISO 3166-1 alpha-2 code (e.g., "US")
|
|
163
|
+
- `priority`: Array of country codes to show at the top of the list
|
|
164
|
+
- `countries`: Array of country codes to include (shows only these countries)
|
|
165
|
+
- `except`: Array of country codes to exclude
|
|
166
|
+
- All `basecoat_select_tag` options (placeholder, scrollable, etc.)
|
|
167
|
+
|
|
168
|
+
**Note:** This helper requires the `countries` gem, which is automatically installed with `rake basecoat:install`.
|
|
169
|
+
|
|
33
170
|
## Rake tasks
|
|
34
171
|
|
|
35
172
|
### Layout (required)
|
|
@@ -55,7 +192,7 @@ The scaffold templates are automatically available from the gem, so you can imme
|
|
|
55
192
|
|
|
56
193
|
Install the Basecoat-styled authentication views (for Rails built-in authentication):
|
|
57
194
|
|
|
58
|
-
rails generate
|
|
195
|
+
rails generate authentication
|
|
59
196
|
rails db:migrate
|
|
60
197
|
rake basecoat:install:authentication
|
|
61
198
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Basecoat
|
|
2
|
+
module FormBuilder
|
|
3
|
+
def basecoat_select(method, choices, options = {}, html_options = {})
|
|
4
|
+
value = @object.public_send(method) if @object
|
|
5
|
+
options = options.merge(selected: value) if value
|
|
6
|
+
|
|
7
|
+
name = "#{@object_name}[#{method}]"
|
|
8
|
+
options[:group_label] ||= method.to_s.titleize.pluralize
|
|
9
|
+
|
|
10
|
+
@template.basecoat_select_tag(name, choices, options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def basecoat_country_select(method, options = {})
|
|
14
|
+
value = @object.public_send(method) if @object
|
|
15
|
+
options = options.merge(selected: value) if value
|
|
16
|
+
|
|
17
|
+
name = "#{@object_name}[#{method}]"
|
|
18
|
+
|
|
19
|
+
@template.basecoat_country_select_tag(name, options)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
module Basecoat
|
|
2
|
+
module FormHelper
|
|
3
|
+
##
|
|
4
|
+
# Renders a remote search component with Turbo Stream support.
|
|
5
|
+
#
|
|
6
|
+
# ==== Parameters
|
|
7
|
+
# * +url+ - The URL to fetch search results from (required)
|
|
8
|
+
# * +options+ - Hash of optional parameters:
|
|
9
|
+
# * +:turbo_frame+ - Custom turbo frame name (defaults to underscored URL)
|
|
10
|
+
# * +:placeholder+ - Custom placeholder text (defaults to "Type a command or search...")
|
|
11
|
+
# * +:data_empty+ - Message shown when no results found (defaults to "No results found.")
|
|
12
|
+
# * +:classes+ - Additional CSS classes to apply to the component
|
|
13
|
+
#
|
|
14
|
+
# ==== Examples
|
|
15
|
+
# <%= basecoat_remote_search_tag("/posts/search") %>
|
|
16
|
+
# <%= basecoat_remote_search_tag("/posts/search", turbo_frame: "custom_frame") %>
|
|
17
|
+
# <%= basecoat_remote_search_tag("/posts/search", placeholder: "Search posts...", classes: "rounded-lg border shadow-md") %>
|
|
18
|
+
def basecoat_remote_search_tag(url, options = {})
|
|
19
|
+
turbo_frame = options[:turbo_frame] || url.gsub("/", "_")
|
|
20
|
+
placeholder = options[:placeholder] || "Type a command or search..."
|
|
21
|
+
data_empty = options[:data_empty] || "No results found."
|
|
22
|
+
classes = options[:classes] || ""
|
|
23
|
+
search_id = "search-#{SecureRandom.random_number(1000000)}"
|
|
24
|
+
|
|
25
|
+
content_tag(:div, id: search_id, data: { controller: "search", search_url_value: url }, class: "command #{classes}", "aria-label": "Command menu") do
|
|
26
|
+
content_tag(:header) do
|
|
27
|
+
lucide_icon("search").html_safe +
|
|
28
|
+
tag(:input,
|
|
29
|
+
type: "search",
|
|
30
|
+
id: "#{search_id}-input",
|
|
31
|
+
placeholder: placeholder,
|
|
32
|
+
autocomplete: "off",
|
|
33
|
+
autocorrect: "off",
|
|
34
|
+
spellcheck: "false",
|
|
35
|
+
"aria-autocomplete": "list",
|
|
36
|
+
role: "combobox",
|
|
37
|
+
"aria-expanded": "true",
|
|
38
|
+
"aria-controls": "#{search_id}-menu",
|
|
39
|
+
data: {
|
|
40
|
+
search_target: "input",
|
|
41
|
+
action: "input->search#search"
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end +
|
|
45
|
+
content_tag(:div, role: "menu", id: "#{search_id}-menu", "aria-orientation": "vertical", data: { empty: data_empty }, class: "scrollbar") do
|
|
46
|
+
turbo_frame_tag(turbo_frame)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Renders a select component outside of a form context.
|
|
53
|
+
#
|
|
54
|
+
# ==== Parameters
|
|
55
|
+
# * +name+ - The name attribute for the hidden input field (required)
|
|
56
|
+
# * +choices+ - Array of [label, value] pairs for select options (required)
|
|
57
|
+
# * +options+ - Hash of optional parameters:
|
|
58
|
+
# * +:selected+ - The initially selected value (defaults to first choice)
|
|
59
|
+
# * +:group_label+ - Label for the option group (defaults to titleized and pluralized name)
|
|
60
|
+
# * +:placeholder+ - Placeholder text for the search input (defaults to "Search entries...")
|
|
61
|
+
# * +:url+ - URL for remote search via Turbo Stream
|
|
62
|
+
# * +:turbo_frame+ - Custom turbo frame name when using +:url+ (defaults to underscored URL)
|
|
63
|
+
# * +:scrollable+ - Whether to make the listbox scrollable with max height (defaults to false)
|
|
64
|
+
#
|
|
65
|
+
# ==== Examples
|
|
66
|
+
# <%= basecoat_select_tag "fruit", [["Apple", 1], ["Pear", 2]] %>
|
|
67
|
+
# <%= basecoat_select_tag "fruit", [["Apple", 1], ["Pear", 2]], selected: 2, placeholder: "Search fruits..." %>
|
|
68
|
+
# <%= basecoat_select_tag "fruit", [], url: "/fruits/search", turbo_frame: "custom_frame", scrollable: true %>
|
|
69
|
+
def basecoat_select_tag(name, choices, options = {})
|
|
70
|
+
select_id = "select-#{SecureRandom.random_number(1000000)}"
|
|
71
|
+
url = options[:url]
|
|
72
|
+
|
|
73
|
+
if url || choices.empty?
|
|
74
|
+
selected_value = options[:selected]
|
|
75
|
+
selected_label = options[:selected_label] || "Select..."
|
|
76
|
+
else
|
|
77
|
+
selected_value = options[:selected] || choices.first[1]
|
|
78
|
+
selected_choice = choices.find { |label, val| val.to_s == selected_value.to_s }
|
|
79
|
+
selected_label = selected_choice ? selected_choice[0] : choices.first[0]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
group_label = options[:group_label] || name.to_s.titleize.pluralize
|
|
83
|
+
placeholder = options[:placeholder] || "Search entries..."
|
|
84
|
+
scrollable = options[:scrollable] || false
|
|
85
|
+
turbo_frame = options[:turbo_frame] || (url ? url.gsub("/", "_") : nil)
|
|
86
|
+
|
|
87
|
+
select_attrs = { id: select_id, class: "select" }
|
|
88
|
+
if url
|
|
89
|
+
select_attrs[:data] = {
|
|
90
|
+
controller: "search",
|
|
91
|
+
search_url_value: url
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
content_tag(:div, select_attrs) do
|
|
96
|
+
basecoat_select_button(select_id, selected_label) +
|
|
97
|
+
basecoat_select_popover(select_id, choices, group_label, placeholder, selected_value, url, scrollable, turbo_frame) +
|
|
98
|
+
tag(:input, type: "hidden", name: name, value: selected_value)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Renders a country select component with flag emojis.
|
|
104
|
+
#
|
|
105
|
+
# ==== Parameters
|
|
106
|
+
# * +name+ - The name attribute for the hidden input field (required)
|
|
107
|
+
# * +options+ - Hash of optional parameters:
|
|
108
|
+
# * +:selected+ - The initially selected country code (e.g., "US")
|
|
109
|
+
# * +:countries+ - Array of country codes to include (defaults to all countries)
|
|
110
|
+
# * +:priority+ - Array of priority country codes to show at the top
|
|
111
|
+
# * +:except+ - Array of country codes to exclude
|
|
112
|
+
# * All other basecoat_select_tag options (placeholder, scrollable, etc.)
|
|
113
|
+
#
|
|
114
|
+
# ==== Examples
|
|
115
|
+
# <%= basecoat_country_select_tag :country %>
|
|
116
|
+
# <%= basecoat_country_select_tag :country, selected: "US" %>
|
|
117
|
+
# <%= basecoat_country_select_tag :country, priority: ["US", "CA", "GB"] %>
|
|
118
|
+
# <%= basecoat_country_select_tag :country, countries: ["US", "CA", "MX"] %>
|
|
119
|
+
def basecoat_country_select_tag(name, options = {})
|
|
120
|
+
require 'countries'
|
|
121
|
+
|
|
122
|
+
# Extract country-specific options
|
|
123
|
+
country_codes = options.delete(:countries)
|
|
124
|
+
priority_codes = options.delete(:priority) || []
|
|
125
|
+
except_codes = options.delete(:except) || []
|
|
126
|
+
|
|
127
|
+
# Get all countries or filtered list
|
|
128
|
+
all_countries = if country_codes
|
|
129
|
+
country_codes.map { |code| ISO3166::Country[code] }.compact
|
|
130
|
+
else
|
|
131
|
+
ISO3166::Country.all
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Remove excluded countries
|
|
135
|
+
all_countries.reject! { |country| except_codes.include?(country.alpha2) } if except_codes.any?
|
|
136
|
+
|
|
137
|
+
# Build choices with flag emoji
|
|
138
|
+
choices = all_countries.map do |country|
|
|
139
|
+
["#{country.emoji_flag} #{country.iso_short_name}", country.alpha2]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Sort alphabetically by country name
|
|
143
|
+
choices.sort_by! { |label, _| label }
|
|
144
|
+
|
|
145
|
+
# Add priority countries at the top
|
|
146
|
+
if priority_codes.any?
|
|
147
|
+
priority_choices = priority_codes.map do |code|
|
|
148
|
+
country = ISO3166::Country[code]
|
|
149
|
+
next unless country
|
|
150
|
+
["#{country.emoji_flag} #{country.iso_short_name}", country.alpha2]
|
|
151
|
+
end.compact
|
|
152
|
+
|
|
153
|
+
choices.reject! { |_, code| priority_codes.include?(code) }
|
|
154
|
+
choices = priority_choices + [["---", nil]] + choices
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Update selected_label if a country is selected
|
|
158
|
+
if options[:selected]
|
|
159
|
+
country = ISO3166::Country[options[:selected]]
|
|
160
|
+
options[:selected_label] = "#{country.emoji_flag} #{country.iso_short_name}" if country
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Set defaults
|
|
164
|
+
options[:placeholder] ||= "Search countries..."
|
|
165
|
+
options[:group_label] ||= "Countries"
|
|
166
|
+
options[:scrollable] = true unless options.key?(:scrollable)
|
|
167
|
+
|
|
168
|
+
basecoat_select_tag(name, choices, options)
|
|
169
|
+
|
|
170
|
+
rescue LoadError
|
|
171
|
+
content_tag :div, class: "alert-destructive" do
|
|
172
|
+
lucide_icon("circle-alert") + tag.section("gem 'countries' required")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def basecoat_select_button(select_id, selected_label)
|
|
179
|
+
content_tag(:button, type: "button", class: "btn-outline justify-between font-normal w-[180px]", id: "#{select_id}-trigger", "aria-haspopup": "listbox", "aria-expanded": "false", "aria-controls": "#{select_id}-listbox") do
|
|
180
|
+
content_tag(:span, selected_label, class: "truncate") +
|
|
181
|
+
lucide_icon("chevrons-up-down", class: "text-muted-foreground opacity-50 shrink-0").html_safe
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def basecoat_select_popover(select_id, choices, group_label, placeholder, selected_value, url, scrollable, turbo_frame)
|
|
186
|
+
content_tag(:div, id: "#{select_id}-popover", data: { popover: true }, "aria-hidden": "true") do
|
|
187
|
+
basecoat_select_search_header(select_id, placeholder) +
|
|
188
|
+
basecoat_select_listbox(select_id, choices, group_label, selected_value, url, scrollable, turbo_frame)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def basecoat_select_search_header(select_id, placeholder)
|
|
193
|
+
content_tag(:header) do
|
|
194
|
+
lucide_icon("search").html_safe +
|
|
195
|
+
tag(:input, type: "text", value: "", placeholder: placeholder, autocomplete: "off", autocorrect: "off", spellcheck: "false", "aria-autocomplete": "list", role: "combobox", "aria-expanded": "false", "aria-controls": "#{select_id}-listbox", "aria-labelledby": "#{select_id}-trigger", data: { search_target: "input", action: "input->search#search" })
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def basecoat_select_listbox(select_id, choices, group_label, selected_value, url, scrollable, turbo_frame)
|
|
200
|
+
listbox_attrs = {
|
|
201
|
+
role: "listbox",
|
|
202
|
+
id: "#{select_id}-listbox",
|
|
203
|
+
"aria-orientation": "vertical",
|
|
204
|
+
"aria-labelledby": "#{select_id}-trigger"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
listbox_attrs[:class] = "scrollbar overflow-y-auto max-h-64" if scrollable
|
|
208
|
+
|
|
209
|
+
if url
|
|
210
|
+
listbox_attrs[:data] = {
|
|
211
|
+
controller: "search",
|
|
212
|
+
search_url_value: url
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
content_tag(:div, listbox_attrs) do
|
|
217
|
+
if url
|
|
218
|
+
turbo_frame_tag(turbo_frame)
|
|
219
|
+
else
|
|
220
|
+
content_tag(:div, role: "group", "aria-labelledby": "group-label-#{select_id}-items-1") do
|
|
221
|
+
content_tag(:div, group_label, role: "heading", id: "group-label-#{select_id}-items-1") +
|
|
222
|
+
choices.map.with_index do |(label, value), index|
|
|
223
|
+
basecoat_select_option(select_id, label, value, index + 1, value.to_s == selected_value.to_s)
|
|
224
|
+
end.join.html_safe
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def basecoat_select_option(select_id, label, value, index, selected)
|
|
231
|
+
content_tag(:div, label,
|
|
232
|
+
id: "#{select_id}-items-1-#{index}",
|
|
233
|
+
role: "option",
|
|
234
|
+
"data-value": value,
|
|
235
|
+
"aria-selected": selected
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
data/lib/basecoat/railtie.rb
CHANGED
|
@@ -7,5 +7,12 @@ module Basecoat
|
|
|
7
7
|
config.app_generators do |g|
|
|
8
8
|
g.templates.unshift File.expand_path("../templates", __dir__)
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
initializer "basecoat.form_builder" do
|
|
12
|
+
ActiveSupport.on_load(:action_view) do
|
|
13
|
+
ActionView::Helpers::FormBuilder.include Basecoat::FormBuilder
|
|
14
|
+
ActionView::Base.include Basecoat::FormHelper
|
|
15
|
+
end
|
|
16
|
+
end
|
|
10
17
|
end
|
|
11
18
|
end
|
data/lib/basecoat/version.rb
CHANGED
data/lib/basecoat.rb
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "basecoat/version"
|
|
4
4
|
require_relative "basecoat/railtie" if defined?(Rails)
|
|
5
|
+
require_relative "basecoat/form_builder" if defined?(Rails)
|
|
6
|
+
require_relative "basecoat/form_helper" if defined?(Rails)
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require "lucide-rails"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
# lucide-rails is optional but recommended
|
|
12
|
+
end
|
|
5
13
|
|
|
6
14
|
module Basecoat
|
|
7
15
|
class Error < StandardError; end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<% if resource.errors.any? %>
|
|
2
2
|
<div class="alert-destructive max-w-2xl mx-auto" data-turbo-cache="false">
|
|
3
|
-
|
|
4
|
-
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
5
|
-
</svg>
|
|
3
|
+
<%= lucide_icon "circle-x", class: "w-5 h-5" %>
|
|
6
4
|
<h2><%= pluralize(resource.errors.count, "error") %> prohibited this <%= resource.class.model_name.human.downcase %> from being saved</h2>
|
|
7
5
|
<section>
|
|
8
6
|
<ul>
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<div class="alert-destructive max-w-2xl mx-auto" data-turbo-cache="false">
|
|
2
|
-
|
|
3
|
-
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
4
|
-
</svg>
|
|
2
|
+
<%= lucide_icon "circle-x", class: "w-5 h-5" %>
|
|
5
3
|
<h2><%= alert %></h2>
|
|
6
4
|
</div>
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<div class="alert-destructive max-w-2xl mx-auto">
|
|
2
|
-
|
|
3
|
-
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
4
|
-
</svg>
|
|
2
|
+
<%= lucide_icon "circle-x", class: "w-5 h-5" %>
|
|
5
3
|
<h2><%= pluralize(object.errors.count, "error") %> prohibited this object from being saved</h2>
|
|
6
4
|
<section>
|
|
7
5
|
<ul>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<header class="bg-background sticky inset-x-0 top-0 isolate flex shrink-0 items-center gap-2 border-b z-10 px-4" data-controller="theme">
|
|
2
2
|
<div class="flex h-14 flex-1 items-center gap-2">
|
|
3
3
|
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'))" aria-label="Toggle sidebar" data-tooltip="Toggle sidebar" data-side="bottom" data-align="start" class="btn-sm-icon-ghost mr-auto size-7 -ml-1.5">
|
|
4
|
-
|
|
4
|
+
<%= lucide_icon "panel-left" %>
|
|
5
5
|
</button>
|
|
6
6
|
<%= render "layouts/theme_toggle" %>
|
|
7
7
|
<!-- AUTHENTICATION_DROPDOWN -->
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
<div id="toaster" class="toaster">
|
|
2
2
|
<div class="toast" role="status" aria-atomic="true" aria-hidden="false" data-category="success">
|
|
3
3
|
<div class="toast-content">
|
|
4
|
-
|
|
5
|
-
<circle cx="12" cy="12" r="10" />
|
|
6
|
-
<path d="m9 12 2 2 4-4" />
|
|
7
|
-
</svg>
|
|
4
|
+
<%= lucide_icon "circle-check" %>
|
|
8
5
|
|
|
9
6
|
<section>
|
|
10
7
|
<h2><%= notice %></h2>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<button type="button" aria-label="Toggle dark mode" data-tooltip="Toggle dark mode" data-side="left" data-action="click->theme#toggle" class="btn-ghost size-8">
|
|
2
|
-
<span class="hidden dark:block"
|
|
3
|
-
<span class="block dark:hidden"
|
|
2
|
+
<span class="hidden dark:block"><%= lucide_icon "sun" %></span>
|
|
3
|
+
<span class="block dark:hidden"><%= lucide_icon "moon" %></span>
|
|
4
4
|
</button>
|
|
@@ -14,11 +14,7 @@ begin
|
|
|
14
14
|
link_html = <<~HTML
|
|
15
15
|
<li>
|
|
16
16
|
<%= link_to #{plural_table_name}_path, data: { turbo_frame: "main_content", turbo_action: "advance" } do %>
|
|
17
|
-
|
|
18
|
-
<path d="m7 11 2-2-2-2" />
|
|
19
|
-
<path d="M11 13h4" />
|
|
20
|
-
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
|
21
|
-
</svg>
|
|
17
|
+
<%= lucide_icon "square-terminal" %>
|
|
22
18
|
<span>#{human_name.pluralize}</span>
|
|
23
19
|
<% end %>
|
|
24
20
|
</li>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input"]
|
|
5
|
+
static values = {
|
|
6
|
+
url: String
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
search(event) {
|
|
10
|
+
const query = event.target.value
|
|
11
|
+
const url = `${this.urlValue}?query=${encodeURIComponent(query)}`
|
|
12
|
+
|
|
13
|
+
fetch(url, {
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: "text/vnd.turbo-stream.html"
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.then(response => response.text())
|
|
19
|
+
.then(html => Turbo.renderStreamMessage(html))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="flex mt-10 flex-1 flex-col items-center justify-center gap-6">
|
|
2
2
|
<header class="flex max-w-sm flex-col items-center gap-2 text-center">
|
|
3
3
|
<div class="mb-2 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg">
|
|
4
|
-
|
|
4
|
+
<%= lucide_icon "folder-code" %>
|
|
5
5
|
</div>
|
|
6
6
|
<h3 class="text-lg font-medium tracking-tight">No <%= resource_name %> yet</h3>
|
|
7
7
|
<p class="text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4">
|
data/lib/tasks/basecoat.rake
CHANGED
|
@@ -21,12 +21,13 @@ namespace :basecoat do
|
|
|
21
21
|
|
|
22
22
|
desc "Install Basecoat application layout and partials"
|
|
23
23
|
task :install do
|
|
24
|
+
no_package_manager = false
|
|
24
25
|
overwrite_all = { value: false }
|
|
25
26
|
# Install basecoat-css (detect package manager)
|
|
26
27
|
puts "\n📦 Installing basecoat-css..."
|
|
27
28
|
|
|
28
29
|
# Detect package manager
|
|
29
|
-
if File.exist?(Rails.root.join("bun.
|
|
30
|
+
if File.exist?(Rails.root.join("bun.lock"))
|
|
30
31
|
system("bun add basecoat-css")
|
|
31
32
|
puts " Installed: basecoat-css via bun"
|
|
32
33
|
elsif File.exist?(Rails.root.join("yarn.lock"))
|
|
@@ -39,9 +40,8 @@ namespace :basecoat do
|
|
|
39
40
|
system("pnpm add basecoat-css")
|
|
40
41
|
puts " Installed: basecoat-css via pnpm"
|
|
41
42
|
else
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
puts " Installed: basecoat-css"
|
|
43
|
+
no_package_manager = true
|
|
44
|
+
puts " No package manager detected! We'll insert CDN links into _head.html.erb..."
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# If using importmap, also add to importmap.rb for JS
|
|
@@ -51,17 +51,10 @@ namespace :basecoat do
|
|
|
51
51
|
|
|
52
52
|
unless importmap_content.include?("basecoat-css")
|
|
53
53
|
File.open(importmap_path, "a") do |f|
|
|
54
|
-
f.puts "\npin \"basecoat-css/all\", to: \"https://cdn.jsdelivr.net/npm/basecoat-css@0.3.
|
|
54
|
+
f.puts "\npin \"basecoat-css/all\", to: \"https://cdn.jsdelivr.net/npm/basecoat-css@0.3.9/dist/js/all.js\""
|
|
55
55
|
end
|
|
56
56
|
puts " Added: basecoat-css to config/importmap.rb"
|
|
57
57
|
end
|
|
58
|
-
|
|
59
|
-
unless importmap_content.include?("basecoat-helper")
|
|
60
|
-
File.open(importmap_path, "a") do |f|
|
|
61
|
-
f.puts "pin \"basecoat-helper\""
|
|
62
|
-
end
|
|
63
|
-
puts " Added: basecoat-helper to config/importmap.rb"
|
|
64
|
-
end
|
|
65
58
|
end
|
|
66
59
|
|
|
67
60
|
# Add JavaScript imports and code
|
|
@@ -107,6 +100,18 @@ namespace :basecoat do
|
|
|
107
100
|
else
|
|
108
101
|
puts " Skipped: app/javascript/controllers/theme_controller.js"
|
|
109
102
|
end
|
|
103
|
+
|
|
104
|
+
# Copy search_controller.js
|
|
105
|
+
search_controller_source = File.expand_path("../generators/basecoat/templates/search_controller.js", __dir__)
|
|
106
|
+
search_controller_destination = Rails.root.join("app/javascript/controllers/search_controller.js")
|
|
107
|
+
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(search_controller_destination))
|
|
109
|
+
if prompt_overwrite(search_controller_destination, overwrite_all)
|
|
110
|
+
FileUtils.cp(search_controller_source, search_controller_destination)
|
|
111
|
+
puts " Created: app/javascript/controllers/search_controller.js"
|
|
112
|
+
else
|
|
113
|
+
puts " Skipped: app/javascript/controllers/search_controller.js"
|
|
114
|
+
end
|
|
110
115
|
end
|
|
111
116
|
|
|
112
117
|
# Add CSS imports and styles
|
|
@@ -135,25 +140,25 @@ namespace :basecoat do
|
|
|
135
140
|
css_content = File.read(css_path)
|
|
136
141
|
|
|
137
142
|
css_code = <<~CSS
|
|
138
|
-
dl {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
label:has(+ input:required):after {
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
input:user-invalid, .field_with_errors input {
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
label:has(+ input:user-invalid), .field_with_errors label {
|
|
155
|
-
|
|
156
|
-
}
|
|
143
|
+
dl {
|
|
144
|
+
font-size: var(--text-sm);
|
|
145
|
+
dt {
|
|
146
|
+
font-weight: var(--font-weight-bold);
|
|
147
|
+
margin-top: calc(var(--spacing)*4);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
label:has(+ input:required):after {
|
|
152
|
+
content: " *"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
input:user-invalid, .field_with_errors input {
|
|
156
|
+
border-color: var(--color-destructive);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
label:has(+ input:user-invalid), .field_with_errors label {
|
|
160
|
+
color: var(--color-destructive);
|
|
161
|
+
}
|
|
157
162
|
CSS
|
|
158
163
|
File.open(css_path, "a") { |f| f.write(css_code) }
|
|
159
164
|
puts " Added: basic styles to #{css_path.relative_path_from(Rails.root)}"
|
|
@@ -237,6 +242,21 @@ label:has(+ input:user-invalid), .field_with_errors label {
|
|
|
237
242
|
end
|
|
238
243
|
end
|
|
239
244
|
|
|
245
|
+
if no_package_manager
|
|
246
|
+
head_path = Rails.root.join("app/views/layouts/_head.html.erb")
|
|
247
|
+
if File.exist?(head_path)
|
|
248
|
+
head_content = File.read(head_path)
|
|
249
|
+
unless head_content.include?("basecoat.cdn.min.css")
|
|
250
|
+
cdn_link = ' <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
251
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@0.3.9/dist/basecoat.cdn.min.css">'
|
|
252
|
+
# Insert before the closing </head> tag
|
|
253
|
+
updated_content = head_content.sub(/(<\/head>)/, "#{cdn_link}\n\\1")
|
|
254
|
+
File.write(head_path, updated_content)
|
|
255
|
+
puts " Added: CDN link to app/views/layouts/_head.html.erb"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
240
260
|
# Copy scaffold hook initializer
|
|
241
261
|
initializer_source = File.expand_path("../generators/basecoat/templates/scaffold_hook.rb", __dir__)
|
|
242
262
|
initializer_destination = Rails.root.join("config/initializers/scaffold_hook.rb")
|
|
@@ -296,23 +316,23 @@ label:has(+ input:user-invalid), .field_with_errors label {
|
|
|
296
316
|
unless header_content.include?("dropdown-user")
|
|
297
317
|
user_dropdown = <<~HTML
|
|
298
318
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
319
|
+
<% if defined?(user_signed_in?) && user_signed_in? %>
|
|
320
|
+
<div id="dropdown-user" class="dropdown-menu">
|
|
321
|
+
<button type="button" id="dropdown-user-trigger" aria-haspopup="menu" aria-controls="dropdown-user-menu" aria-expanded="false" class="btn-ghost size-8">
|
|
322
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>
|
|
323
|
+
</button>
|
|
324
|
+
<div id="dropdown-user-popover" data-popover="" aria-hidden="true" data-align="end">
|
|
325
|
+
<div role="menu" id="dropdown-user-menu" aria-labelledby="dropdown-user-trigger">
|
|
326
|
+
<div class="px-1 py-1.5">
|
|
327
|
+
<%= button_to destroy_user_session_path, method: :delete, class: "btn-link" do %>
|
|
328
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
|
329
|
+
Log out
|
|
330
|
+
<% end %>
|
|
313
331
|
</div>
|
|
314
332
|
</div>
|
|
315
|
-
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<% end %>
|
|
316
336
|
HTML
|
|
317
337
|
updated_content = header_content.sub("<!-- AUTHENTICATION_DROPDOWN -->", user_dropdown)
|
|
318
338
|
File.write(header_path, updated_content)
|
|
@@ -437,23 +457,23 @@ label:has(+ input:user-invalid), .field_with_errors label {
|
|
|
437
457
|
unless header_content.include?("dropdown-user")
|
|
438
458
|
user_dropdown = <<~HTML
|
|
439
459
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
</div>
|
|
453
|
-
</div>
|
|
460
|
+
<% if defined?(Current) && defined?(Current.user) && Current.user %>
|
|
461
|
+
<div id="dropdown-user" class="dropdown-menu">
|
|
462
|
+
<button type="button" id="dropdown-user-trigger" aria-haspopup="menu" aria-controls="dropdown-user-menu" aria-expanded="false" class="btn-ghost size-8">
|
|
463
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>
|
|
464
|
+
</button>
|
|
465
|
+
<div id="dropdown-user-popover" data-popover="" aria-hidden="true" data-align="end">
|
|
466
|
+
<div role="menu" id="dropdown-user-menu" aria-labelledby="dropdown-user-trigger">
|
|
467
|
+
<div class="px-1 py-1.5">
|
|
468
|
+
<%= button_to session_path, method: :delete, class: "btn-link" do %>
|
|
469
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
|
470
|
+
Log out
|
|
471
|
+
<% end %>
|
|
454
472
|
</div>
|
|
455
473
|
</div>
|
|
456
|
-
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
<% end %>
|
|
457
477
|
HTML
|
|
458
478
|
updated_content = header_content.sub("<!-- AUTHENTICATION_DROPDOWN -->", user_dropdown)
|
|
459
479
|
File.write(header_path, updated_content)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: basecoat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martijn Lafeber
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: lucide-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
26
40
|
description: Provides beautiful, production-ready scaffold templates and Devise views
|
|
27
41
|
styled with Basecoat CSS framework
|
|
28
42
|
email:
|
|
@@ -43,6 +57,8 @@ files:
|
|
|
43
57
|
- basecoat-login.png
|
|
44
58
|
- basecoat-new.png
|
|
45
59
|
- lib/basecoat.rb
|
|
60
|
+
- lib/basecoat/form_builder.rb
|
|
61
|
+
- lib/basecoat/form_helper.rb
|
|
46
62
|
- lib/basecoat/railtie.rb
|
|
47
63
|
- lib/basecoat/version.rb
|
|
48
64
|
- lib/generators/basecoat/templates/application.html.erb
|
|
@@ -72,6 +88,7 @@ files:
|
|
|
72
88
|
- lib/generators/basecoat/templates/passwords/edit.html.erb
|
|
73
89
|
- lib/generators/basecoat/templates/passwords/new.html.erb
|
|
74
90
|
- lib/generators/basecoat/templates/scaffold_hook.rb
|
|
91
|
+
- lib/generators/basecoat/templates/search_controller.js
|
|
75
92
|
- lib/generators/basecoat/templates/sessions.html.erb
|
|
76
93
|
- lib/generators/basecoat/templates/sessions/new.html.erb
|
|
77
94
|
- lib/generators/basecoat/templates/shared/_empty.html.erb
|