maquina-components 0.5.1 → 0.6.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -2
  3. data/app/assets/stylesheets/alert.css +169 -135
  4. data/app/assets/stylesheets/badge.css +143 -138
  5. data/app/assets/stylesheets/breadcrumbs.css +183 -166
  6. data/app/assets/stylesheets/calendar.css +219 -206
  7. data/app/assets/stylesheets/card.css +126 -120
  8. data/app/assets/stylesheets/combobox.css +208 -191
  9. data/app/assets/stylesheets/date_picker.css +144 -134
  10. data/app/assets/stylesheets/drawer.css +158 -125
  11. data/app/assets/stylesheets/dropdown_menu.css +203 -190
  12. data/app/assets/stylesheets/empty.css +125 -117
  13. data/app/assets/stylesheets/form.css +625 -525
  14. data/app/assets/stylesheets/header.css +63 -47
  15. data/app/assets/stylesheets/menu_button.css +121 -98
  16. data/app/assets/stylesheets/pagination.css +130 -125
  17. data/app/assets/stylesheets/separator.css +23 -11
  18. data/app/assets/stylesheets/sidebar.css +447 -401
  19. data/app/assets/stylesheets/stats.css +77 -67
  20. data/app/assets/stylesheets/table.css +184 -166
  21. data/app/assets/stylesheets/toast.css +287 -278
  22. data/app/assets/stylesheets/toggle_group.css +132 -139
  23. data/app/assets/tailwind/maquina_components_engine/engine.css +54 -15
  24. data/app/assets/tailwind/maquina_components_engine/tokens.css +101 -0
  25. data/app/helpers/maquina_components/combobox_helper.rb +32 -24
  26. data/app/helpers/maquina_components/components_helper.rb +30 -6
  27. data/app/helpers/maquina_components/dropdown_menu_helper.rb +12 -12
  28. data/app/helpers/maquina_components/icons_helper.rb +26 -2
  29. data/app/javascript/controllers/drawer_trigger_controller.js +24 -3
  30. data/app/views/components/_label.html.erb +14 -0
  31. data/app/views/components/_table.html.erb +2 -2
  32. data/app/views/components/calendar/_week.html.erb +4 -12
  33. data/app/views/components/drawer/_close.html.erb +1 -1
  34. data/app/views/components/drawer/_description.html.erb +6 -0
  35. data/app/views/components/drawer/_header.html.erb +1 -1
  36. data/app/views/components/drawer/_provider.html.erb +13 -4
  37. data/app/views/components/drawer/_section.html.erb +7 -0
  38. data/app/views/components/drawer/_separator.html.erb +11 -0
  39. data/app/views/components/drawer/_title.html.erb +6 -0
  40. data/app/views/components/drawer/_trigger.html.erb +24 -7
  41. data/app/views/components/sidebar/_group_action.html.erb +26 -0
  42. data/app/views/components/sidebar/_menu_action.html.erb +28 -0
  43. data/app/views/components/sidebar/_menu_badge.html.erb +10 -0
  44. data/app/views/components/sidebar/_menu_button.html.erb +3 -2
  45. data/app/views/components/sidebar/_menu_link.html.erb +3 -2
  46. data/app/views/components/sidebar/_separator.html.erb +12 -0
  47. data/lib/generators/maquina_components/install/install_generator.rb +36 -1
  48. data/lib/generators/maquina_components/install/templates/maquina_components_helper.rb.tt +5 -0
  49. data/lib/generators/maquina_components/install/templates/shape_state_tokens.css.tt +76 -0
  50. data/lib/generators/maquina_components/install/templates/theme.css.tt +10 -3
  51. data/lib/maquina-components.rb +29 -1
  52. data/lib/maquina_components/doctor.rb +280 -0
  53. data/lib/maquina_components/version.rb +1 -1
  54. data/lib/tasks/maquina_components_tasks.rake +16 -4
  55. metadata +14 -2
@@ -1,10 +1,20 @@
1
1
  module MaquinaComponents
2
2
  module IconsHelper
3
+ # Renders an icon by name, preferring the host app's main_icon_svg_for
4
+ # override and falling back to the engine's built-in set.
5
+ #
6
+ # An unknown name used to render nothing at all, which is invisible in
7
+ # review and in production. When MaquinaComponents.strict_icons is on
8
+ # (development and test by default) it raises instead; production keeps
9
+ # the nil so a typo can never take a page down.
3
10
  def icon_for(name, options = {})
4
11
  return nil unless name
5
12
 
6
13
  svg = main_icon_svg_for(name.to_sym) || icon_svg_for(name.to_sym)
7
- return nil unless svg
14
+ unless svg
15
+ raise MaquinaComponents::UnknownIconError, unknown_icon_message(name) if MaquinaComponents.strict_icons?
16
+ return nil
17
+ end
8
18
 
9
19
  apply_icon_options(svg, options)
10
20
  end
@@ -25,6 +35,13 @@ module MaquinaComponents
25
35
 
26
36
  private
27
37
 
38
+ def unknown_icon_message(name)
39
+ "Unknown icon #{name.to_sym.inspect}. Define it in your app's " \
40
+ "main_icon_svg_for override, or use one of the engine's built-in icons " \
41
+ "(see app/helpers/maquina_components/icons_helper.rb). Set " \
42
+ "MaquinaComponents.strict_icons = false to fall back to rendering nothing."
43
+ end
44
+
28
45
  def apply_icon_options(svg, options)
29
46
  return svg.html_safe unless options&.any?
30
47
 
@@ -204,7 +221,7 @@ module MaquinaComponents
204
221
  <path d="M12 8h.01"/>
205
222
  </svg>
206
223
  SVG
207
- when :triangle_alert
224
+ when :triangle_alert, :alert_triangle # :alert_triangle is a common lucide-era alias
208
225
  <<~SVG.freeze
209
226
  <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="">
210
227
  <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
@@ -212,6 +229,13 @@ module MaquinaComponents
212
229
  <path d="M12 17h.01"/>
213
230
  </svg>
214
231
  SVG
232
+ when :briefcase
233
+ <<~SVG.freeze
234
+ <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="">
235
+ <path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
236
+ <rect width="20" height="14" x="2" y="6" rx="2"/>
237
+ </svg>
238
+ SVG
215
239
  when :check_circle
216
240
  <<~SVG.freeze
217
241
  <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="">
@@ -7,11 +7,16 @@ import { Controller } from "@hotwired/stimulus";
7
7
  * state on the trigger (aria-expanded, aria-controls).
8
8
  * Can be placed anywhere on the page - finds drawer via outlet selector.
9
9
  *
10
+ * The outlet selector decides which drawer(s) a trigger controls. The default
11
+ * document-wide marker targets every drawer on the page; scope it to one
12
+ * provider's id when a page has more than one drawer (the drawer/trigger
13
+ * partial does this via its for_id: local).
14
+ *
10
15
  * @example
11
16
  * <button
12
17
  * data-controller="drawer-trigger"
13
18
  * data-action="click->drawer-trigger#triggerClick"
14
- * data-drawer-trigger-drawer-outlet="[data-outlet='drawer']"
19
+ * data-drawer-trigger-drawer-outlet="#drawer-provider-filters"
15
20
  * >
16
21
  * Toggle
17
22
  * </button>
@@ -31,9 +36,25 @@ export default class extends Controller {
31
36
  drawerOutletConnected(outlet) {
32
37
  this.element.setAttribute("aria-haspopup", "dialog")
33
38
  this.syncExpanded(outlet.openValue)
39
+ this.syncControls()
40
+ }
41
+
42
+ drawerOutletDisconnected() {
43
+ this.syncControls()
44
+ }
45
+
46
+ // aria-controls takes a space-separated id list, so a trigger wired to more
47
+ // than one drawer stays accurate instead of pointing at whichever outlet
48
+ // connected last.
49
+ syncControls() {
50
+ const ids = this.drawerOutlets
51
+ .filter(outlet => outlet.hasPanelTarget && outlet.panelTarget.id)
52
+ .map(outlet => outlet.panelTarget.id)
34
53
 
35
- if (outlet.hasPanelTarget && outlet.panelTarget.id) {
36
- this.element.setAttribute("aria-controls", outlet.panelTarget.id)
54
+ if (ids.length) {
55
+ this.element.setAttribute("aria-controls", ids.join(" "))
56
+ } else {
57
+ this.element.removeAttribute("aria-controls")
37
58
  }
38
59
  }
39
60
 
@@ -0,0 +1,14 @@
1
+ <%# locals: (text: nil, content: nil, for_id: nil, required: false, css_classes: "", **html_options) %>
2
+ <%
3
+ # `for` is a Ruby keyword and cannot be a strict-local name, so the local is
4
+ # for_id: — a caller-supplied for: is honored as well.
5
+ label_for = for_id.presence || html_options.delete(:for).presence
6
+
7
+ merged_data = merge_component_data(html_options,
8
+ component: :label,
9
+ required: (required ? true : nil)
10
+ )
11
+ %>
12
+ <%= content_tag :label, for: label_for, class: css_classes.presence, data: merged_data, **html_options do %>
13
+ <%= text.presence || content || yield %>
14
+ <% end %>
@@ -9,11 +9,11 @@
9
9
  container_data[:variant] = variant if variant
10
10
  %>
11
11
  <% if container %>
12
- <div data-table-part="container"<%= " data-variant=\"#{variant}\"" if variant %>>
12
+ <%= tag.div data: container_data do %>
13
13
  <%= content_tag :table, class: css_classes.presence, data: table_data, **html_options do %>
14
14
  <%= yield %>
15
15
  <% end %>
16
- </div>
16
+ <% end %>
17
17
  <% else %>
18
18
  <%= content_tag :table, class: css_classes.presence, data: table_data, **html_options do %>
19
19
  <%= yield %>
@@ -37,17 +37,9 @@
37
37
  action: "click->calendar#selectDay keydown->calendar#handleKeydown"
38
38
  }.compact
39
39
  %>
40
- <button type="button"
41
- data-calendar-part="day"
42
- data-calendar-target="day"
43
- data-date="<%= day.iso8601 %>"
44
- <%= "data-state=\"#{day_state}\"" if day_state %>
45
- <%= "data-outside" if is_outside %>
46
- <%= "data-today" if is_today %>
47
- data-action="click->calendar#selectDay keydown->calendar#handleKeydown"
48
- <%= "disabled" if is_disabled %>
49
- tabindex="<%= is_today ? '0' : '-1' %>"
50
- <%= "aria-selected=\"true\"" if day_state&.include?("selected") || day_state == "range-start" || day_state == "range-end" %>
51
- <%= "aria-current=\"date\"" if is_today %>><%= day.day %></button>
40
+ <%= tag.button day.day, type: "button", disabled: is_disabled,
41
+ tabindex: (is_today ? 0 : -1),
42
+ aria: {selected: (day_state ? true : nil), current: (is_today ? "date" : nil)}.compact,
43
+ data: day_data %>
52
44
  <% end %>
53
45
  <% end %>
@@ -1,4 +1,4 @@
1
- <%# locals: (show_icon: true, css_classes: "btn btn-ghost", **html_options) %>
1
+ <%# locals: (show_icon: true, css_classes: "", **html_options) %>
2
2
  <% merged_data = merge_component_data(html_options,
3
3
  drawer_part: :close,
4
4
  action: "click->drawer#close"
@@ -0,0 +1,6 @@
1
+ <%# locals: (text: nil, content: nil, tag: :p, css_classes: "", **html_options) %>
2
+ <% merged_data = merge_component_data(html_options, drawer_part: :description) %>
3
+
4
+ <%= content_tag tag, class: css_classes.presence, data: merged_data, **html_options do %>
5
+ <%= text || content %>
6
+ <% end %>
@@ -7,6 +7,6 @@
7
7
  <%= yield %>
8
8
  </div>
9
9
  <% if show_close %>
10
- <%= render "components/drawer/close", css_classes: "btn btn-ghost btn-icon-sm" %>
10
+ <%= render "components/drawer/close" %>
11
11
  <% end %>
12
12
  <% end %>
@@ -1,14 +1,23 @@
1
- <%# locals: (id: nil, default_open: false, css_classes: "", cookie_name: "drawer_state", keyboard_shortcut: "d", **html_options) %>
2
- <% merged_data = merge_component_data(html_options,
1
+ <%# locals: (id: nil, name: nil, default_open: false, css_classes: "", cookie_name: "drawer_state", keyboard_shortcut: "d", **html_options) %>
2
+ <%
3
+ # Deterministic ids only — random ids break idiomorph matching across morphs.
4
+ # Pass name: (or id:) when a page has more than one drawer, and point each
5
+ # trigger at it with for_id:.
6
+ provider_id = id.presence ||
7
+ ["drawer-provider", name.presence && name.to_s.parameterize.presence].compact.join("-")
8
+
9
+ merged_data = merge_component_data(html_options,
3
10
  component: :drawer,
4
11
  controller: "drawer",
5
12
  outlet: "drawer",
13
+ drawer_name: name.presence,
6
14
  drawer_default_open_value: default_open,
7
15
  drawer_open_value: default_open,
8
16
  drawer_cookie_name_value: cookie_name,
9
17
  drawer_keyboard_shortcut_value: keyboard_shortcut,
10
18
  action: "keydown.meta+#{keyboard_shortcut}@window->drawer#toggleWithKeyboard keydown.ctrl+#{keyboard_shortcut}@window->drawer#toggleWithKeyboard keydown.esc@window->drawer#closeOnEscape"
11
- ) %>
12
- <%= content_tag :div, id: id || "drawer-provider", class: css_classes, data: merged_data, **html_options do %>
19
+ )
20
+ %>
21
+ <%= content_tag :div, id: provider_id, class: css_classes, data: merged_data, **html_options do %>
13
22
  <%= yield %>
14
23
  <% end %>
@@ -0,0 +1,7 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = merge_component_data(html_options,
3
+ drawer_part: :section
4
+ ) %>
5
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
6
+ <%= yield %>
7
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%# locals: (orientation: :horizontal, css_classes: "", **html_options) %>
2
+ <%# Renders the separator primitive so the divider keeps its 1px track, then
3
+ tags it with the drawer part that re-spaces it for the panel. %>
4
+ <% merged_data = merge_component_data(html_options,
5
+ drawer_part: :separator
6
+ ) %>
7
+ <%= render "components/separator",
8
+ orientation: orientation,
9
+ css_classes: css_classes,
10
+ data: merged_data,
11
+ **html_options %>
@@ -0,0 +1,6 @@
1
+ <%# locals: (text: nil, content: nil, tag: :h2, css_classes: "", **html_options) %>
2
+ <% merged_data = merge_component_data(html_options, drawer_part: :title) %>
3
+
4
+ <%= content_tag tag, class: css_classes.presence, data: merged_data, **html_options do %>
5
+ <%= text || content %>
6
+ <% end %>
@@ -1,13 +1,30 @@
1
- <%# locals: (variant: :default, size: :default, icon_name: nil, css_classes: "", **html_options) %>
2
- <% merged_data = merge_component_data(html_options,
1
+ <%# locals: (variant: :default, size: :default, icon_name: nil, for_id: nil, css_classes: "", **html_options) %>
2
+ <%
3
+ # `for` is a Ruby keyword and cannot be a strict-local name, so the local is
4
+ # for_id: — but a caller-supplied for: is accepted too, popped out of
5
+ # html_options before it can leak onto the button as a stray attribute.
6
+ target = for_id.presence || html_options.delete(:for).presence
7
+
8
+ # Default keeps the document-wide selector, so single-drawer pages behave as
9
+ # before. A bare value is treated as an element id (the drawer provider's);
10
+ # anything starting with #, [ or . is used verbatim as a selector.
11
+ drawer_selector =
12
+ if target.blank?
13
+ "[data-outlet='drawer']"
14
+ elsif target.to_s.start_with?("#", "[", ".")
15
+ target.to_s
16
+ else
17
+ "##{target}"
18
+ end
19
+
20
+ merged_data = merge_component_data(html_options,
3
21
  drawer_part: :trigger,
4
22
  controller: "drawer-trigger",
5
23
  action: "click->drawer-trigger#triggerClick",
6
- drawer_trigger_drawer_outlet: "[data-outlet='drawer']"
7
- ) %>
8
- <% button_data = merged_data.merge(
9
- component: "button"
10
- ) %>
24
+ drawer_trigger_drawer_outlet: drawer_selector
25
+ )
26
+ button_data = merged_data.merge(component: "button")
27
+ %>
11
28
  <%= content_tag :button, type: :button, class: css_classes, data: button_data, **html_options do %>
12
29
  <%= icon_for(icon_name) if icon_name %>
13
30
  <%= yield %>
@@ -0,0 +1,26 @@
1
+ <%# locals: (label:, url: nil, icon_name: :ellipsis, css_classes: "", **html_options) %>
2
+ <%#
3
+ Render this inside a sidebar/group, alongside the group label — `ml-auto`
4
+ pushes it to the right edge of the group and the group is the positioning
5
+ context its expanded touch target resolves against.
6
+
7
+ label: is required: the control is icon-only, so without it there is
8
+ nothing for a screen reader to announce.
9
+ %>
10
+ <% merged_data = merge_component_data(html_options,
11
+ sidebar_part: "group-action"
12
+ ) %>
13
+ <% action_content = capture do %>
14
+ <%= icon_for(icon_name) if icon_name.present? %>
15
+ <span class="sr-only"><%= label %></span>
16
+ <% end %>
17
+ <% if url.present? %>
18
+ <%= link_to url, class: css_classes.presence, data: merged_data,
19
+ "aria-label": label, **html_options do %>
20
+ <%= action_content %>
21
+ <% end %>
22
+ <% else %>
23
+ <%= content_tag :button, action_content, type: :button,
24
+ class: css_classes.presence, data: merged_data,
25
+ "aria-label": label, **html_options %>
26
+ <% end %>
@@ -0,0 +1,28 @@
1
+ <%# locals: (label:, url: nil, icon_name: :ellipsis, show_on_hover: false, css_classes: "", **html_options) %>
2
+ <%#
3
+ Render this INSIDE a menu_item, as a sibling of the menu_button or
4
+ menu_link — the item is the positioning context this absolute action
5
+ resolves against, and `menu-item:has(menu-action)` is what reserves the
6
+ right-hand padding on the button so the two never overlap.
7
+
8
+ label: is required: the control is icon-only, so without it there is
9
+ nothing for a screen reader to announce.
10
+ %>
11
+ <% merged_data = merge_component_data(html_options,
12
+ sidebar_part: "menu-action",
13
+ show_on_hover: (show_on_hover ? true : nil)
14
+ ) %>
15
+ <% action_content = capture do %>
16
+ <%= icon_for(icon_name) if icon_name.present? %>
17
+ <span class="sr-only"><%= label %></span>
18
+ <% end %>
19
+ <% if url.present? %>
20
+ <%= link_to url, class: css_classes.presence, data: merged_data,
21
+ "aria-label": label, **html_options do %>
22
+ <%= action_content %>
23
+ <% end %>
24
+ <% else %>
25
+ <%= content_tag :button, action_content, type: :button,
26
+ class: css_classes.presence, data: merged_data,
27
+ "aria-label": label, **html_options %>
28
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
+ <%# Pinned inside a menu_item, next to the button or link. It is decorative
3
+ for pointer users — the CSS makes it pointer-events-none — so keep the
4
+ count meaningful in the button's own label when it matters. %>
5
+ <% merged_data = merge_component_data(html_options,
6
+ sidebar_part: "menu-badge"
7
+ ) %>
8
+ <%= content_tag :span, class: css_classes.presence, data: merged_data, **html_options do %>
9
+ <%= text || content || yield %>
10
+ <% end %>
@@ -2,10 +2,11 @@
2
2
  <% merged_data = merge_component_data(html_options,
3
3
  sidebar_part: "menu-button",
4
4
  size: size,
5
- active: active
5
+ active: (active ? true : nil)
6
6
  ) %>
7
7
 
8
- <%= link_to url, class: css_classes, data: merged_data, **html_options do %>
8
+ <%= link_to url, class: css_classes, data: merged_data,
9
+ "aria-current": (active ? "page" : nil), **html_options do %>
9
10
  <% if icon_name.present? %>
10
11
  <%= icon_for(icon_name) %>
11
12
  <% end %>
@@ -1,10 +1,11 @@
1
1
  <%# locals: (url: "#", active: false, title: nil, subtitle: nil, icon: nil, text_icon: nil, icon_classes: "", css_classes: "", **html_options) %>
2
2
  <% merged_data = merge_component_data(html_options,
3
3
  sidebar_part: "menu-link",
4
- active: active
4
+ active: (active ? true : nil)
5
5
  ) %>
6
6
 
7
- <%= link_to url, class: css_classes, data: merged_data, **html_options do %>
7
+ <%= link_to url, class: css_classes, data: merged_data,
8
+ "aria-current": (active ? "page" : nil), **html_options do %>
8
9
  <% if icon.present? || text_icon.present? %>
9
10
  <div data-sidebar-part="menu-avatar">
10
11
  <% if icon.present? %>
@@ -0,0 +1,12 @@
1
+ <%# locals: (orientation: :horizontal, css_classes: "", **html_options) %>
2
+ <%# Renders the separator primitive so the divider keeps its 1px track, then
3
+ tags it with the sidebar part that re-spaces it and swaps in the sidebar's
4
+ own border token. %>
5
+ <% merged_data = merge_component_data(html_options,
6
+ sidebar_part: :separator
7
+ ) %>
8
+ <%= render "components/separator",
9
+ orientation: orientation,
10
+ css_classes: css_classes,
11
+ data: merged_data,
12
+ **html_options %>
@@ -9,6 +9,12 @@ module MaquinaComponents
9
9
 
10
10
  desc "Install maquina_components into your Rails application"
11
11
 
12
+ # Marker comment carried by the shape/state token block. Its presence is
13
+ # what makes a second `rails g maquina_components:install` a no-op for
14
+ # that block instead of a second copy.
15
+ TOKENS_MARKER = "maquina_components:shape-state-tokens"
16
+ APP_CSS_PATH = "app/assets/tailwind/application.css"
17
+
12
18
  class_option :skip_theme, type: :boolean, default: false,
13
19
  desc: "Skip adding theme variables to application.css"
14
20
  class_option :skip_helper, type: :boolean, default: false,
@@ -73,6 +79,30 @@ module MaquinaComponents
73
79
  say_status :append, "Added theme variables to application.css", :green
74
80
  end
75
81
 
82
+ # Safe to re-run against an app that installed an earlier version: the
83
+ # existing palette is left exactly as it is and only the new shape/state
84
+ # token block is appended, guarded by TOKENS_MARKER.
85
+ def add_shape_and_state_tokens
86
+ return if options[:skip_theme]
87
+
88
+ full_path = File.join(destination_root, APP_CSS_PATH)
89
+
90
+ unless File.exist?(full_path)
91
+ say_status :skip, "#{APP_CSS_PATH} not found", :yellow
92
+ return
93
+ end
94
+
95
+ if File.read(full_path).include?(TOKENS_MARKER)
96
+ say_status :skip, "Shape/state tokens already present", :blue
97
+ return
98
+ end
99
+
100
+ tokens = File.read(File.expand_path("templates/shape_state_tokens.css.tt", __dir__))
101
+ append_to_file APP_CSS_PATH, "\n#{tokens}"
102
+
103
+ say_status :append, "Added shape/state token block to #{APP_CSS_PATH}", :green
104
+ end
105
+
76
106
  def create_helper
77
107
  return if options[:skip_helper]
78
108
 
@@ -97,7 +127,12 @@ module MaquinaComponents
97
127
  say "Next steps:"
98
128
  say ""
99
129
  say "1. Customize theme variables in app/assets/tailwind/application.css"
100
- say " to match your design system."
130
+ say " to match your design system. Shape, focus-ring, elevation and"
131
+ say " control-mark tokens are listed there too, commented out at their"
132
+ say " 0.5.1 values - uncomment a line to pin the old look."
133
+ say ""
134
+ say " Upgrading an existing app? Run `bin/rails maquina:doctor` for a"
135
+ say " report of app CSS the 0.6 token layer makes redundant or breaks."
101
136
  say ""
102
137
  say "2. Override the icon helper in app/helpers/maquina_components_helper.rb"
103
138
  say " to use your own icon system (Heroicons, Lucide, etc.)."
@@ -26,6 +26,11 @@ module MaquinaComponentsHelper
26
26
  # @param options [Hash] Options hash with :class, :stroke_width, etc.
27
27
  # @return [String, nil] SVG string or nil to fall back to engine's icons
28
28
  #
29
+ # A name neither this method nor the engine can resolve raises
30
+ # MaquinaComponents::UnknownIconError in development and test, and renders
31
+ # nothing in production. Set MaquinaComponents.strict_icons = false in an
32
+ # initializer to opt out of the raise.
33
+ #
29
34
  # @example Using Heroicons
30
35
  # def main_icon_svg_for(name)
31
36
  # heroicon_tag(name, variant: :outline)
@@ -0,0 +1,76 @@
1
+ /* maquina_components:shape-state-tokens ===================================
2
+ Added by `rails generate maquina_components:install`. The marker comment on
3
+ the line above is how the generator knows not to add this block twice, and
4
+ re-running the generator never touches your palette above - keep the marker.
5
+
6
+ 0.6.0 moves shape, elevation, focus rings, hover fills and control marks out
7
+ of per-component CSS and into tokens. Everything has a built-in default, so
8
+ you only declare what you want to change.
9
+ ========================================================================== */
10
+
11
+ :root {
12
+ /* Hover fills - 0.5.1 hardcoded these color-mix() calls per button variant.
13
+ Same values, now in one place. */
14
+ --primary-hover: color-mix(in oklch, var(--primary) 90%, black);
15
+ --secondary-hover: color-mix(in oklch, var(--secondary) 80%, black);
16
+ --destructive-hover: color-mix(in oklch, var(--destructive) 90%, black);
17
+ --accent-hover: var(--accent);
18
+
19
+ /* Info - 0.5.1 had no --info at all, so info toasts fell back to the brand
20
+ accent. Set them to your own blue (or to var(--primary)) if you prefer. */
21
+ --info: oklch(0.93 0.04 240);
22
+ --info-foreground: oklch(0.4 0.12 250);
23
+
24
+ .dark {
25
+ --info: oklch(0.34 0.07 250);
26
+ --info-foreground: oklch(0.88 0.06 240);
27
+ }
28
+ }
29
+
30
+ /* -------------------------------------------------------------------------
31
+ The tokens below are listed at their 0.5.1 values. 0.6.0 ships its own
32
+ defaults for all of them; uncomment a line to pin the old look instead.
33
+ ------------------------------------------------------------------------- */
34
+
35
+ :root {
36
+ /* --- Shape -------------------------------------------------------------
37
+ 0.5.1 hardcoded: rounded-md on inputs/buttons/selects, rounded-xl on
38
+ cards, rounded-[4px] on checkbox marks, rounded-full on radios/switches. */
39
+ /* --control-radius: 0.375rem; */
40
+ /* --surface-radius: 0.75rem; */
41
+ /* --mark-radius: 4px; */
42
+ /* --pill-radius: 9999px; */
43
+
44
+ /* --- Focus ring -------------------------------------------------------
45
+ 0.5.1 hardcoded: box-shadow 0 0 0 2px var(--background),
46
+ 0 0 0 4px var(--ring). */
47
+ /* --focus-ring-width: 2px; */
48
+ /* --focus-ring-offset: 2px; */
49
+ /* --focus-ring-style: solid; */
50
+ /* --focus-ring-color: var(--ring); */
51
+
52
+ /* --- Elevation --------------------------------------------------------
53
+ 0.5.1 hardcoded: shadow-xs/shadow-sm on controls, shadow on cards,
54
+ shadow-md on dropdowns, popovers and toasts. */
55
+ /* --elevation-control: 0 1px 2px 0 rgb(0 0 0 / 0.05); */
56
+ /* --elevation-raised: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); */
57
+ /* --elevation-overlay: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); */
58
+
59
+ /* --- Typography weights ----------------------------------------------
60
+ 0.5.1 hardcoded: font-medium on labels, font-bold on stat values. */
61
+ /* --label-weight: 500; */
62
+ /* --value-weight: 700; */
63
+
64
+ /* --- Control fill -----------------------------------------------------
65
+ 0.5.1 hardcoded: background-color: transparent on inputs. */
66
+ /* --control-fill: transparent; */
67
+
68
+ /* --- Control marks ---------------------------------------------------
69
+ 0.5.1 baked these SVGs into the engine's own rules. Uncomment to keep
70
+ the 0.5.1 artwork, or point them at your own. */
71
+ /* --checkbox-mark-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); */
72
+ /* --checkbox-indeterminate-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3crect x='3' y='7' width='10' height='2' rx='1'/%3e%3c/svg%3e"); */
73
+ /* --radio-mark-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); */
74
+ /* --switch-thumb-image: url("data:image/svg+xml,%3csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='10' cy='10' r='8' fill='white'/%3e%3c/svg%3e"); */
75
+ /* --select-chevron-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); */
76
+ }
@@ -173,7 +173,14 @@
173
173
  --color-sidebar-ring: var(--sidebar-ring);
174
174
  }
175
175
 
176
- /* Global border color */
177
- * {
178
- border-color: var(--color-border);
176
+ /* Global border color.
177
+ *
178
+ * This must stay inside @layer base. Unlayered CSS beats every layer at any
179
+ * specificity, so an unlayered `*` here would override the border-color of
180
+ * every component in @layer components — the tinted borders on the alert and
181
+ * toast variants would silently fall back to --border. */
182
+ @layer base {
183
+ * {
184
+ border-color: var(--color-border);
185
+ }
179
186
  }
@@ -7,5 +7,33 @@ require "maquina_components/version"
7
7
  require "maquina_components/engine"
8
8
 
9
9
  module MaquinaComponents
10
- # Your code goes here...
10
+ # Raised by icon_for when a name resolves to no SVG and strict_icons is on.
11
+ class UnknownIconError < ArgumentError; end
12
+
13
+ class << self
14
+ # When true, icon_for raises UnknownIconError for a name it can't resolve
15
+ # instead of silently rendering nothing. Defaults to Rails.env.local?
16
+ # (development and test), so typos surface while you work but never break
17
+ # a production page.
18
+ #
19
+ # # config/initializers/maquina_components.rb
20
+ # MaquinaComponents.strict_icons = false
21
+ attr_writer :strict_icons
22
+
23
+ def strict_icons
24
+ return @strict_icons if defined?(@strict_icons)
25
+
26
+ default_strict_icons
27
+ end
28
+
29
+ def strict_icons?
30
+ !!strict_icons
31
+ end
32
+
33
+ private
34
+
35
+ def default_strict_icons
36
+ defined?(Rails) && Rails.respond_to?(:env) && Rails.env.respond_to?(:local?) && Rails.env.local?
37
+ end
38
+ end
11
39
  end