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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fa9470ef968e4c5dff1e61b2c016056700d4bac56cb48601d86c7cd6eb0a0e1
4
- data.tar.gz: 1116eb1741c23532506854d7243c8785431a7ea9957a6ab097ec952ead59d345
3
+ metadata.gz: 1ec22802652367bcd30144e5e34a1df474e3d52b5943a35c21013d63284e46a1
4
+ data.tar.gz: 5aa8e17dc9669ff89072f3bb51ef8a4316410f448117406e5a64e18a96dafbfd
5
5
  SHA512:
6
- metadata.gz: 9ee345ffafaa76be5521b2fb9c9fade4f527517963acdb1f5cd50d283ff469e8704bde799d0b9c6070ffaa200f916e8d5ebb160416ef2466efabf653ef1c5f7d
7
- data.tar.gz: e50d6e88672d5f555466e4d12abe0f5dc712b6bb29054403a306964d8fa8ca0d99391a602ae39c3ae588dddb89f00cc96cf5b797de8deed1176178357035b500
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:authentication
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 theme toggle, can be moved to something else if you desire...)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecoat
4
- VERSION = "2.2.1"
4
+ VERSION = "2.3.0"
5
5
  end
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
- <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
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" data-controller="theme">
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
- <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
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>
@@ -7,7 +7,7 @@
7
7
  </div>
8
8
  <div class="grid flex-1 text-left text-sm leading-tight">
9
9
  <span class="truncate font-medium">Basecoat</span>
10
- <span class="truncate text-xs">v0.3.3</span>
10
+ <span class="truncate text-xs">v0.3.10</span>
11
11
  </div>
12
12
  </a>
13
13
  </header>
@@ -1,7 +1,5 @@
1
1
  <div class="alert-destructive max-w-2xl mx-auto">
2
- <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
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" data-controller="theme">
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
- <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"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M9 3v18"></path></svg>
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
- <svg aria-hidden="true" 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">
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" data-action="click->theme#toggle" class="btn-ghost size-8">
2
- <span class="hidden dark:block"><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"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></svg></span>
3
- <span class="block dark:hidden"><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="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></svg></span>
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
- <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">
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" data-controller="theme">
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
- <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="M10 10.5 8 13l2 2.5" /><path d="m14 10.5 2 2.5-2 2.5" /><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z" /></svg>
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">
@@ -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.lockb"))
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
- # Fallback: try bun first, then yarn, then npm
43
- system("bun add basecoat-css") || system("yarn add basecoat-css") || system("npm install basecoat-css")
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.3/dist/js/all.js\""
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 theme_controller.js
100
- theme_controller_source = File.expand_path("../generators/basecoat/templates/theme_controller.js", __dir__)
101
- theme_controller_destination = Rails.root.join("app/javascript/controllers/theme_controller.js")
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(theme_controller_destination))
104
- if prompt_overwrite(theme_controller_destination, overwrite_all)
105
- FileUtils.cp(theme_controller_source, theme_controller_destination)
106
- puts " Created: app/javascript/controllers/theme_controller.js"
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/theme_controller.js"
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
- font-size: var(--text-sm);
140
- dt {
141
- font-weight: var(--font-weight-bold);
142
- margin-top: calc(var(--spacing)*4);
143
- }
144
- }
145
-
146
- label:has(+ input:required):after {
147
- content: " *"
148
- }
149
-
150
- input:user-invalid, .field_with_errors input {
151
- border-color: var(--color-destructive);
152
- }
153
-
154
- label:has(+ input:user-invalid), .field_with_errors label {
155
- color: var(--color-destructive);
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
- <% if defined?(user_signed_in?) && user_signed_in? %>
300
- <div id="dropdown-user" class="dropdown-menu">
301
- <button type="button" id="dropdown-user-trigger" aria-haspopup="menu" aria-controls="dropdown-user-menu" aria-expanded="false" class="btn-ghost size-8">
302
- <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>
303
- </button>
304
- <div id="dropdown-user-popover" data-popover="" aria-hidden="true" data-align="end">
305
- <div role="menu" id="dropdown-user-menu" aria-labelledby="dropdown-user-trigger">
306
- <div class="px-1 py-1.5">
307
- <%= button_to destroy_user_session_path, method: :delete, class: "btn-link" do %>
308
- <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>
309
- Log out
310
- <% end %>
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
- <% end %>
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
- <% if defined?(Current) && defined?(Current.user) && Current.user %>
441
- <div id="dropdown-user" class="dropdown-menu">
442
- <button type="button" id="dropdown-user-trigger" aria-haspopup="menu" aria-controls="dropdown-user-menu" aria-expanded="false" class="btn-ghost size-8">
443
- <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>
444
- </button>
445
- <div id="dropdown-user-popover" data-popover="" aria-hidden="true" data-align="end">
446
- <div role="menu" id="dropdown-user-menu" aria-labelledby="dropdown-user-trigger">
447
- <div class="px-1 py-1.5">
448
- <%= button_to session_path, method: :delete, class: "btn-link" do %>
449
- <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>
450
- Log out
451
- <% end %>
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
- <% end %>
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.2.1
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
- }