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.
- checksums.yaml +4 -4
- data/README.md +76 -2
- data/app/assets/stylesheets/alert.css +169 -135
- data/app/assets/stylesheets/badge.css +143 -138
- data/app/assets/stylesheets/breadcrumbs.css +183 -166
- data/app/assets/stylesheets/calendar.css +219 -206
- data/app/assets/stylesheets/card.css +126 -120
- data/app/assets/stylesheets/combobox.css +208 -191
- data/app/assets/stylesheets/date_picker.css +144 -134
- data/app/assets/stylesheets/drawer.css +158 -125
- data/app/assets/stylesheets/dropdown_menu.css +203 -190
- data/app/assets/stylesheets/empty.css +125 -117
- data/app/assets/stylesheets/form.css +625 -525
- data/app/assets/stylesheets/header.css +63 -47
- data/app/assets/stylesheets/menu_button.css +121 -98
- data/app/assets/stylesheets/pagination.css +130 -125
- data/app/assets/stylesheets/separator.css +23 -11
- data/app/assets/stylesheets/sidebar.css +447 -401
- data/app/assets/stylesheets/stats.css +77 -67
- data/app/assets/stylesheets/table.css +184 -166
- data/app/assets/stylesheets/toast.css +287 -278
- data/app/assets/stylesheets/toggle_group.css +132 -139
- data/app/assets/tailwind/maquina_components_engine/engine.css +54 -15
- data/app/assets/tailwind/maquina_components_engine/tokens.css +101 -0
- data/app/helpers/maquina_components/combobox_helper.rb +32 -24
- data/app/helpers/maquina_components/components_helper.rb +30 -6
- data/app/helpers/maquina_components/dropdown_menu_helper.rb +12 -12
- data/app/helpers/maquina_components/icons_helper.rb +26 -2
- data/app/javascript/controllers/drawer_trigger_controller.js +24 -3
- data/app/views/components/_label.html.erb +14 -0
- data/app/views/components/_table.html.erb +2 -2
- data/app/views/components/calendar/_week.html.erb +4 -12
- data/app/views/components/drawer/_close.html.erb +1 -1
- data/app/views/components/drawer/_description.html.erb +6 -0
- data/app/views/components/drawer/_header.html.erb +1 -1
- data/app/views/components/drawer/_provider.html.erb +13 -4
- data/app/views/components/drawer/_section.html.erb +7 -0
- data/app/views/components/drawer/_separator.html.erb +11 -0
- data/app/views/components/drawer/_title.html.erb +6 -0
- data/app/views/components/drawer/_trigger.html.erb +24 -7
- data/app/views/components/sidebar/_group_action.html.erb +26 -0
- data/app/views/components/sidebar/_menu_action.html.erb +28 -0
- data/app/views/components/sidebar/_menu_badge.html.erb +10 -0
- data/app/views/components/sidebar/_menu_button.html.erb +3 -2
- data/app/views/components/sidebar/_menu_link.html.erb +3 -2
- data/app/views/components/sidebar/_separator.html.erb +12 -0
- data/lib/generators/maquina_components/install/install_generator.rb +36 -1
- data/lib/generators/maquina_components/install/templates/maquina_components_helper.rb.tt +5 -0
- data/lib/generators/maquina_components/install/templates/shape_state_tokens.css.tt +76 -0
- data/lib/generators/maquina_components/install/templates/theme.css.tt +10 -3
- data/lib/maquina-components.rb +29 -1
- data/lib/maquina_components/doctor.rb +280 -0
- data/lib/maquina_components/version.rb +1 -1
- data/lib/tasks/maquina_components_tasks.rake +16 -4
- 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
|
-
|
|
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="
|
|
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 (
|
|
36
|
-
this.element.setAttribute("aria-controls",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 %>
|
|
@@ -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 %>
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
<%# locals: (id: nil, default_open: false, css_classes: "", cookie_name: "drawer_state", keyboard_shortcut: "d", **html_options) %>
|
|
2
|
-
<%
|
|
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
|
-
|
|
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,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
|
-
<%
|
|
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:
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
}
|
data/lib/maquina-components.rb
CHANGED
|
@@ -7,5 +7,33 @@ require "maquina_components/version"
|
|
|
7
7
|
require "maquina_components/engine"
|
|
8
8
|
|
|
9
9
|
module MaquinaComponents
|
|
10
|
-
#
|
|
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
|