basecoat 2.2.1 → 2.3.0
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 +140 -2
- 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/devise.html.erb +1 -1
- 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/_head.html.erb +23 -0
- data/lib/generators/basecoat/templates/layouts/_header.html.erb +2 -2
- data/lib/generators/basecoat/templates/layouts/_notice.html.erb +1 -4
- data/lib/generators/basecoat/templates/layouts/_theme_toggle.html.erb +3 -3
- 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/sessions.html.erb +1 -1
- data/lib/generators/basecoat/templates/shared/_empty.html.erb +1 -1
- data/lib/tasks/basecoat.rake +77 -69
- metadata +18 -2
- data/lib/generators/basecoat/templates/theme_controller.js +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ec22802652367bcd30144e5e34a1df474e3d52b5943a35c21013d63284e46a1
|
|
4
|
+
data.tar.gz: 5aa8e17dc9669ff89072f3bb51ef8a4316410f448117406e5a64e18a96dafbfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89b9cabbb20c51b45aefdea591f85ccdbe55dcb73c08363f24a8907b0be88886a7c99c19c68a73eef27e47270415e8756ad4a03318963a450f2013e7c422c8bc
|
|
7
|
+
data.tar.gz: 1aa2c47cb3737cd0e7bd9a4143d4cd8fbc7943e2c9d52e065c7f71042ad6a147963a9891408ca56037f8aa14740807d8818b115f6677fa92ca494e7a3ca5f234
|
data/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Try it from scratch:
|
|
|
13
13
|
|
|
14
14
|
rails new myproject -c tailwind
|
|
15
15
|
cd myproject
|
|
16
|
+
yarn
|
|
16
17
|
bundle add basecoat
|
|
17
18
|
rails basecoat:install
|
|
18
19
|
rails g scaffold Post title:string! description:text posted_at:datetime active:boolean rating:integer
|
|
@@ -30,6 +31,143 @@ Basecoat CSS combines tailwind with clean css classes. It creates the looks of s
|
|
|
30
31
|
|
|
31
32
|
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
33
|
|
|
34
|
+
## Icons
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
You can use Lucide icons in your views with the `lucide_icon` helper:
|
|
39
|
+
|
|
40
|
+
```erb
|
|
41
|
+
<%= lucide_icon "home" %>
|
|
42
|
+
<%= lucide_icon "user", class: "w-5 h-5" %>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Browse available icons at [lucide.dev](https://lucide.dev/icons/).
|
|
46
|
+
|
|
47
|
+
### Using Different Icon Libraries
|
|
48
|
+
|
|
49
|
+
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:
|
|
50
|
+
|
|
51
|
+
1. Add `rails_icons` to your Gemfile
|
|
52
|
+
2. Replace `lucide_icon` calls with `icon` calls from rails_icons in the generated views
|
|
53
|
+
3. Configure your preferred icon library in `config/initializers/rails_icons.rb`
|
|
54
|
+
|
|
55
|
+
## Form Helpers
|
|
56
|
+
|
|
57
|
+
Basecoat includes custom form helpers for enhanced UI components:
|
|
58
|
+
|
|
59
|
+
### Select Component
|
|
60
|
+
|
|
61
|
+
Use `basecoat_select` in forms or `basecoat_select_tag` outside of forms:
|
|
62
|
+
|
|
63
|
+
```erb
|
|
64
|
+
<%= form_for @user do |f| %>
|
|
65
|
+
<%= f.basecoat_select :fruit, [["Apple", 1], ["Pear", 2]] %>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<%# Or without a form: %>
|
|
69
|
+
<%= basecoat_select_tag "fruit", [["Apple", 1], ["Pear", 2]] %>
|
|
70
|
+
|
|
71
|
+
<%# With options: %>
|
|
72
|
+
<%= f.basecoat_select :fruit, [["Apple", 1], ["Pear", 2]],
|
|
73
|
+
group_label: "Fruits",
|
|
74
|
+
placeholder: "Search fruits..." %>
|
|
75
|
+
|
|
76
|
+
<%# You can add a remote url, which does a turbo call %>
|
|
77
|
+
<%= f.basecoat_select :fruit, [[]], url: "/fruits/search", turbo_frame: "custom_frame" %>
|
|
78
|
+
|
|
79
|
+
`fruits/search.turbo_stream.erb` should then have the following content:
|
|
80
|
+
|
|
81
|
+
<%= turbo_stream.update "custom_frame" do %>
|
|
82
|
+
<% @fruits.each do |fruit| %>
|
|
83
|
+
<%= tag.div fruit.name, role: "option", data: { value: fruit.id } %>
|
|
84
|
+
<% end %>
|
|
85
|
+
<% end %>
|
|
86
|
+
|
|
87
|
+
# If you don't add the turbo_frame option there's a fallback to underscored URL (_fruits_search)
|
|
88
|
+
<%= f.basecoat_select :fruit, [[]], url: "/fruits/search" %>
|
|
89
|
+
|
|
90
|
+
Make sure this matches the frame in your partial!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Remote Search Component
|
|
94
|
+
|
|
95
|
+
Use `basecoat_remote_search_tag` for a standalone search component with Turbo Stream support:
|
|
96
|
+
|
|
97
|
+
```erb
|
|
98
|
+
<%# Basic usage: %>
|
|
99
|
+
<%= basecoat_remote_search_tag("/posts/search") %>
|
|
100
|
+
|
|
101
|
+
<%# With custom turbo frame name: %>
|
|
102
|
+
<%= basecoat_remote_search_tag("/posts/search", turbo_frame: "custom_frame") %>
|
|
103
|
+
|
|
104
|
+
<%# With custom placeholder: %>
|
|
105
|
+
<%= basecoat_remote_search_tag("/posts/search", placeholder: "Search posts...") %>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Your controller should respond with a Turbo Stream that updates the frame:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# posts_controller.rb
|
|
112
|
+
def search
|
|
113
|
+
@posts = Post.where("title LIKE ?", "%#{params[:query]}%")
|
|
114
|
+
|
|
115
|
+
respond_to do |format|
|
|
116
|
+
format.turbo_stream
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<%# posts/search.turbo_stream.erb %>
|
|
123
|
+
<%= turbo_stream.update "_posts_search" do %>
|
|
124
|
+
<% @posts.each do |post| %>
|
|
125
|
+
<%= link_to post.title, post_path(post), class: "block p-2 hover:bg-muted" %>
|
|
126
|
+
<% end %>
|
|
127
|
+
<% end %>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
- `url` (required): The URL to fetch search results from
|
|
132
|
+
- `turbo_frame`: Custom turbo frame name (defaults to underscored URL, e.g., `/posts/search` becomes `_posts_search`)
|
|
133
|
+
- `placeholder`: Custom placeholder text (defaults to "Type a command or search...")
|
|
134
|
+
|
|
135
|
+
### Country Select Component
|
|
136
|
+
|
|
137
|
+
Use `basecoat_country_select_tag` for a country picker with flag emojis:
|
|
138
|
+
|
|
139
|
+
```erb
|
|
140
|
+
<%# Basic usage (all countries): %>
|
|
141
|
+
<%= basecoat_country_select_tag :country %>
|
|
142
|
+
|
|
143
|
+
<%# With pre-selected country: %>
|
|
144
|
+
<%= basecoat_country_select_tag :country, selected: "US" %>
|
|
145
|
+
|
|
146
|
+
<%# With priority countries at the top: %>
|
|
147
|
+
<%= basecoat_country_select_tag :country, priority: ["US", "CA", "GB"] %>
|
|
148
|
+
|
|
149
|
+
<%# Only show specific countries: %>
|
|
150
|
+
<%= basecoat_country_select_tag :country, countries: ["US", "CA", "MX"] %>
|
|
151
|
+
|
|
152
|
+
<%# Exclude specific countries: %>
|
|
153
|
+
<%= basecoat_country_select_tag :country, except: ["KP", "IR"] %>
|
|
154
|
+
|
|
155
|
+
<%# In a form: %>
|
|
156
|
+
<%= form_for @user do |f| %>
|
|
157
|
+
<%= f.label :country %>
|
|
158
|
+
<%= f.basecoat_country_select :country %>
|
|
159
|
+
<% end %>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Options:
|
|
163
|
+
- `selected`: Pre-select a country by its ISO 3166-1 alpha-2 code (e.g., "US")
|
|
164
|
+
- `priority`: Array of country codes to show at the top of the list
|
|
165
|
+
- `countries`: Array of country codes to include (shows only these countries)
|
|
166
|
+
- `except`: Array of country codes to exclude
|
|
167
|
+
- All `basecoat_select_tag` options (placeholder, scrollable, etc.)
|
|
168
|
+
|
|
169
|
+
**Note:** This helper requires the `countries` gem.
|
|
170
|
+
|
|
33
171
|
## Rake tasks
|
|
34
172
|
|
|
35
173
|
### Layout (required)
|
|
@@ -55,7 +193,7 @@ The scaffold templates are automatically available from the gem, so you can imme
|
|
|
55
193
|
|
|
56
194
|
Install the Basecoat-styled authentication views (for Rails built-in authentication):
|
|
57
195
|
|
|
58
|
-
rails generate
|
|
196
|
+
rails generate authentication
|
|
59
197
|
rails db:migrate
|
|
60
198
|
rake basecoat:install:authentication
|
|
61
199
|
|
|
@@ -86,7 +224,7 @@ rake basecoat:install:pagy
|
|
|
86
224
|
- Rails 8.0+
|
|
87
225
|
- Tailwind CSS ([installation instructions](https://github.com/rails/tailwindcss-rails))
|
|
88
226
|
- Basecoat CSS
|
|
89
|
-
- Stimulus (for the
|
|
227
|
+
- Stimulus (for the search helper)
|
|
90
228
|
|
|
91
229
|
## Issues
|
|
92
230
|
|
|
@@ -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>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<%= render 'layouts/notice', notice: notice if notice %>
|
|
6
6
|
<div class="grid min-h-svh lg:grid-cols-2">
|
|
7
7
|
<div class="flex flex-col gap-4 p-6 md:p-10">
|
|
8
|
-
<div class="flex justify-between items-center gap-2"
|
|
8
|
+
<div class="flex justify-between items-center gap-2">
|
|
9
9
|
<a href="#" class="flex items-center gap-2 font-medium">
|
|
10
10
|
<div class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
|
11
11
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>
|
|
@@ -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>
|
|
@@ -18,4 +18,27 @@
|
|
|
18
18
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
|
19
19
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
|
20
20
|
<%= javascript_importmap_tags %>
|
|
21
|
+
<script>
|
|
22
|
+
(() => {
|
|
23
|
+
try {
|
|
24
|
+
const stored = localStorage.getItem('themeMode');
|
|
25
|
+
if (stored ? stored === 'dark'
|
|
26
|
+
: matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
27
|
+
document.documentElement.classList.add('dark');
|
|
28
|
+
}
|
|
29
|
+
} catch (_) {}
|
|
30
|
+
|
|
31
|
+
const apply = dark => {
|
|
32
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
33
|
+
try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
document.addEventListener('basecoat:theme', (event) => {
|
|
37
|
+
const mode = event.detail?.mode;
|
|
38
|
+
apply(mode === 'dark' ? true
|
|
39
|
+
: mode === 'light' ? false
|
|
40
|
+
: !document.documentElement.classList.contains('dark'));
|
|
41
|
+
});
|
|
42
|
+
})();
|
|
43
|
+
</script>
|
|
21
44
|
</head>
|
|
@@ -1,7 +1,7 @@
|
|
|
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"
|
|
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">
|
|
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
|
-
<button type="button" aria-label="Toggle dark mode" data-tooltip="Toggle dark mode" data-side="left"
|
|
2
|
-
<span class="hidden dark:block"
|
|
3
|
-
<span class="block dark:hidden"
|
|
1
|
+
<button type="button" aria-label="Toggle dark mode" data-tooltip="Toggle dark mode" data-side="left" onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))" class="btn-ghost size-8">
|
|
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
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<%= render 'layouts/notice', notice: notice if notice %>
|
|
6
6
|
<div class="grid min-h-svh lg:grid-cols-2">
|
|
7
7
|
<div class="flex flex-col gap-4 p-6 md:p-10">
|
|
8
|
-
<div class="flex justify-between items-center gap-2"
|
|
8
|
+
<div class="flex justify-between items-center gap-2">
|
|
9
9
|
<a href="#" class="flex items-center gap-2 font-medium">
|
|
10
10
|
<div class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
|
11
11
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>
|
|
@@ -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.10/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
|
|
@@ -96,16 +89,16 @@ namespace :basecoat do
|
|
|
96
89
|
puts " Added: cool view transition to app/javascript/application.js"
|
|
97
90
|
end
|
|
98
91
|
|
|
99
|
-
# Copy
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
# Copy search_controller.js
|
|
93
|
+
search_controller_source = File.expand_path("../generators/basecoat/templates/search_controller.js", __dir__)
|
|
94
|
+
search_controller_destination = Rails.root.join("app/javascript/controllers/search_controller.js")
|
|
102
95
|
|
|
103
|
-
FileUtils.mkdir_p(File.dirname(
|
|
104
|
-
if prompt_overwrite(
|
|
105
|
-
FileUtils.cp(
|
|
106
|
-
puts " Created: app/javascript/controllers/
|
|
96
|
+
FileUtils.mkdir_p(File.dirname(search_controller_destination))
|
|
97
|
+
if prompt_overwrite(search_controller_destination, overwrite_all)
|
|
98
|
+
FileUtils.cp(search_controller_source, search_controller_destination)
|
|
99
|
+
puts " Created: app/javascript/controllers/search_controller.js"
|
|
107
100
|
else
|
|
108
|
-
puts " Skipped: app/javascript/controllers/
|
|
101
|
+
puts " Skipped: app/javascript/controllers/search_controller.js"
|
|
109
102
|
end
|
|
110
103
|
end
|
|
111
104
|
|
|
@@ -135,25 +128,25 @@ namespace :basecoat do
|
|
|
135
128
|
css_content = File.read(css_path)
|
|
136
129
|
|
|
137
130
|
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
|
-
}
|
|
131
|
+
dl {
|
|
132
|
+
font-size: var(--text-sm);
|
|
133
|
+
dt {
|
|
134
|
+
font-weight: var(--font-weight-bold);
|
|
135
|
+
margin-top: calc(var(--spacing)*4);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
label:has(+ input:required):after {
|
|
140
|
+
content: " *"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
input:user-invalid, .field_with_errors input {
|
|
144
|
+
border-color: var(--color-destructive);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
label:has(+ input:user-invalid), .field_with_errors label {
|
|
148
|
+
color: var(--color-destructive);
|
|
149
|
+
}
|
|
157
150
|
CSS
|
|
158
151
|
File.open(css_path, "a") { |f| f.write(css_code) }
|
|
159
152
|
puts " Added: basic styles to #{css_path.relative_path_from(Rails.root)}"
|
|
@@ -237,6 +230,21 @@ label:has(+ input:user-invalid), .field_with_errors label {
|
|
|
237
230
|
end
|
|
238
231
|
end
|
|
239
232
|
|
|
233
|
+
if no_package_manager
|
|
234
|
+
head_path = Rails.root.join("app/views/layouts/_head.html.erb")
|
|
235
|
+
if File.exist?(head_path)
|
|
236
|
+
head_content = File.read(head_path)
|
|
237
|
+
unless head_content.include?("basecoat.cdn.min.css")
|
|
238
|
+
cdn_link = ' <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
239
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@0.3.10/dist/basecoat.cdn.min.css">'
|
|
240
|
+
# Insert before the closing </head> tag
|
|
241
|
+
updated_content = head_content.sub(/(<\/head>)/, "#{cdn_link}\n\\1")
|
|
242
|
+
File.write(head_path, updated_content)
|
|
243
|
+
puts " Added: CDN link to app/views/layouts/_head.html.erb"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
240
248
|
# Copy scaffold hook initializer
|
|
241
249
|
initializer_source = File.expand_path("../generators/basecoat/templates/scaffold_hook.rb", __dir__)
|
|
242
250
|
initializer_destination = Rails.root.join("config/initializers/scaffold_hook.rb")
|
|
@@ -296,23 +304,23 @@ label:has(+ input:user-invalid), .field_with_errors label {
|
|
|
296
304
|
unless header_content.include?("dropdown-user")
|
|
297
305
|
user_dropdown = <<~HTML
|
|
298
306
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
307
|
+
<% if defined?(user_signed_in?) && user_signed_in? %>
|
|
308
|
+
<div id="dropdown-user" class="dropdown-menu">
|
|
309
|
+
<button type="button" id="dropdown-user-trigger" aria-haspopup="menu" aria-controls="dropdown-user-menu" aria-expanded="false" class="btn-ghost size-8">
|
|
310
|
+
<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>
|
|
311
|
+
</button>
|
|
312
|
+
<div id="dropdown-user-popover" data-popover="" aria-hidden="true" data-align="end">
|
|
313
|
+
<div role="menu" id="dropdown-user-menu" aria-labelledby="dropdown-user-trigger">
|
|
314
|
+
<div class="px-1 py-1.5">
|
|
315
|
+
<%= button_to destroy_user_session_path, method: :delete, class: "btn-link" do %>
|
|
316
|
+
<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>
|
|
317
|
+
Log out
|
|
318
|
+
<% end %>
|
|
313
319
|
</div>
|
|
314
320
|
</div>
|
|
315
|
-
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<% end %>
|
|
316
324
|
HTML
|
|
317
325
|
updated_content = header_content.sub("<!-- AUTHENTICATION_DROPDOWN -->", user_dropdown)
|
|
318
326
|
File.write(header_path, updated_content)
|
|
@@ -437,23 +445,23 @@ label:has(+ input:user-invalid), .field_with_errors label {
|
|
|
437
445
|
unless header_content.include?("dropdown-user")
|
|
438
446
|
user_dropdown = <<~HTML
|
|
439
447
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
</div>
|
|
453
|
-
</div>
|
|
448
|
+
<% if defined?(Current) && defined?(Current.user) && Current.user %>
|
|
449
|
+
<div id="dropdown-user" class="dropdown-menu">
|
|
450
|
+
<button type="button" id="dropdown-user-trigger" aria-haspopup="menu" aria-controls="dropdown-user-menu" aria-expanded="false" class="btn-ghost size-8">
|
|
451
|
+
<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>
|
|
452
|
+
</button>
|
|
453
|
+
<div id="dropdown-user-popover" data-popover="" aria-hidden="true" data-align="end">
|
|
454
|
+
<div role="menu" id="dropdown-user-menu" aria-labelledby="dropdown-user-trigger">
|
|
455
|
+
<div class="px-1 py-1.5">
|
|
456
|
+
<%= button_to session_path, method: :delete, class: "btn-link" do %>
|
|
457
|
+
<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>
|
|
458
|
+
Log out
|
|
459
|
+
<% end %>
|
|
454
460
|
</div>
|
|
455
461
|
</div>
|
|
456
|
-
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
<% end %>
|
|
457
465
|
HTML
|
|
458
466
|
updated_content = header_content.sub("<!-- AUTHENTICATION_DROPDOWN -->", user_dropdown)
|
|
459
467
|
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.
|
|
4
|
+
version: 2.3.0
|
|
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,10 +88,10 @@ 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
|
|
78
|
-
- lib/generators/basecoat/templates/theme_controller.js
|
|
79
95
|
- lib/tasks/basecoat.rake
|
|
80
96
|
- lib/templates/erb/scaffold/_form.html.erb.tt
|
|
81
97
|
- lib/templates/erb/scaffold/edit.html.erb.tt
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
4
|
-
connect() {
|
|
5
|
-
// Apply theme on initial load (runs immediately to prevent flash)
|
|
6
|
-
this.applyStoredTheme()
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
toggle() {
|
|
10
|
-
const isDark = !document.documentElement.classList.contains('dark')
|
|
11
|
-
this.apply(isDark)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
apply(dark) {
|
|
15
|
-
document.documentElement.classList.toggle('dark', dark)
|
|
16
|
-
try {
|
|
17
|
-
localStorage.setItem('themeMode', dark ? 'dark' : 'light')
|
|
18
|
-
} catch (_) {}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
applyStoredTheme() {
|
|
22
|
-
try {
|
|
23
|
-
const stored = localStorage.getItem('themeMode')
|
|
24
|
-
if (stored ? stored === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
25
|
-
document.documentElement.classList.add('dark')
|
|
26
|
-
}
|
|
27
|
-
} catch (_) {}
|
|
28
|
-
}
|
|
29
|
-
}
|