maquina-components 0.4.4 → 0.5.1

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -1
  3. data/app/assets/stylesheets/breadcrumbs.css +21 -0
  4. data/app/assets/stylesheets/combobox.css +36 -1
  5. data/app/assets/stylesheets/drawer.css +135 -0
  6. data/app/assets/stylesheets/menu_button.css +155 -0
  7. data/app/assets/stylesheets/separator.css +20 -0
  8. data/app/assets/stylesheets/sidebar.css +12 -0
  9. data/app/assets/stylesheets/stats.css +101 -0
  10. data/app/assets/tailwind/maquina_components_engine/engine.css +4 -0
  11. data/app/helpers/maquina_components/combobox_helper.rb +2 -1
  12. data/app/helpers/maquina_components/components_helper.rb +33 -0
  13. data/app/helpers/maquina_components/drawer_helper.rb +19 -0
  14. data/app/helpers/maquina_components/icons_helper.rb +68 -1
  15. data/app/javascript/controllers/breadcrumb_controller.js +39 -30
  16. data/app/javascript/controllers/combobox_controller.js +9 -0
  17. data/app/javascript/controllers/drawer_controller.js +287 -0
  18. data/app/javascript/controllers/drawer_trigger_controller.js +58 -0
  19. data/app/javascript/controllers/menu_button_controller.js +107 -29
  20. data/app/javascript/controllers/sidebar_controller.js +6 -8
  21. data/app/javascript/controllers/sidebar_trigger_controller.js +33 -4
  22. data/app/javascript/controllers/toaster_controller.js +12 -0
  23. data/app/views/components/_alert.html.erb +3 -1
  24. data/app/views/components/_badge.html.erb +7 -1
  25. data/app/views/components/_breadcrumbs.html.erb +1 -1
  26. data/app/views/components/_calendar.html.erb +1 -1
  27. data/app/views/components/_card.html.erb +1 -1
  28. data/app/views/components/_combobox.html.erb +3 -2
  29. data/app/views/components/_date_picker.html.erb +4 -3
  30. data/app/views/components/_drawer.html.erb +37 -0
  31. data/app/views/components/_dropdown.html.erb +21 -24
  32. data/app/views/components/_dropdown_menu.html.erb +1 -1
  33. data/app/views/components/_empty.html.erb +1 -1
  34. data/app/views/components/_header.html.erb +1 -1
  35. data/app/views/components/_menu_button.html.erb +29 -34
  36. data/app/views/components/_pagination.html.erb +1 -1
  37. data/app/views/components/_separator.html.erb +9 -10
  38. data/app/views/components/_sidebar.html.erb +1 -1
  39. data/app/views/components/_simple_table.html.erb +3 -1
  40. data/app/views/components/_table.html.erb +1 -1
  41. data/app/views/components/_toast.html.erb +6 -1
  42. data/app/views/components/_toaster.html.erb +1 -1
  43. data/app/views/components/_toggle_group.html.erb +1 -1
  44. data/app/views/components/alert/_description.html.erb +1 -1
  45. data/app/views/components/alert/_title.html.erb +1 -1
  46. data/app/views/components/breadcrumbs/_ellipsis.html.erb +1 -1
  47. data/app/views/components/breadcrumbs/_item.html.erb +1 -1
  48. data/app/views/components/breadcrumbs/_link.html.erb +1 -1
  49. data/app/views/components/breadcrumbs/_list.html.erb +1 -1
  50. data/app/views/components/breadcrumbs/_page.html.erb +1 -1
  51. data/app/views/components/breadcrumbs/_separator.html.erb +1 -1
  52. data/app/views/components/calendar/_header.html.erb +1 -1
  53. data/app/views/components/calendar/_week.html.erb +1 -1
  54. data/app/views/components/card/_action.html.erb +1 -1
  55. data/app/views/components/card/_content.html.erb +1 -1
  56. data/app/views/components/card/_description.html.erb +1 -1
  57. data/app/views/components/card/_footer.html.erb +1 -1
  58. data/app/views/components/card/_header.html.erb +1 -1
  59. data/app/views/components/card/_title.html.erb +1 -1
  60. data/app/views/components/combobox/_content.html.erb +1 -1
  61. data/app/views/components/combobox/_empty.html.erb +3 -3
  62. data/app/views/components/combobox/_group.html.erb +1 -1
  63. data/app/views/components/combobox/_input.html.erb +1 -1
  64. data/app/views/components/combobox/_label.html.erb +1 -1
  65. data/app/views/components/combobox/_list.html.erb +1 -1
  66. data/app/views/components/combobox/_option.html.erb +1 -1
  67. data/app/views/components/combobox/_separator.html.erb +1 -1
  68. data/app/views/components/combobox/_trigger.html.erb +1 -1
  69. data/app/views/components/drawer/_close.html.erb +14 -0
  70. data/app/views/components/drawer/_content.html.erb +7 -0
  71. data/app/views/components/drawer/_footer.html.erb +7 -0
  72. data/app/views/components/drawer/_header.html.erb +12 -0
  73. data/app/views/components/drawer/_provider.html.erb +14 -0
  74. data/app/views/components/drawer/_trigger.html.erb +14 -0
  75. data/app/views/components/dropdown_menu/_content.html.erb +1 -1
  76. data/app/views/components/dropdown_menu/_group.html.erb +1 -1
  77. data/app/views/components/dropdown_menu/_item.html.erb +1 -1
  78. data/app/views/components/dropdown_menu/_label.html.erb +3 -7
  79. data/app/views/components/dropdown_menu/_separator.html.erb +1 -1
  80. data/app/views/components/dropdown_menu/_shortcut.html.erb +3 -7
  81. data/app/views/components/dropdown_menu/_trigger.html.erb +1 -1
  82. data/app/views/components/empty/_content.html.erb +1 -1
  83. data/app/views/components/empty/_description.html.erb +3 -7
  84. data/app/views/components/empty/_header.html.erb +1 -1
  85. data/app/views/components/empty/_media.html.erb +5 -3
  86. data/app/views/components/empty/_title.html.erb +3 -7
  87. data/app/views/components/pagination/_content.html.erb +1 -1
  88. data/app/views/components/pagination/_item.html.erb +1 -1
  89. data/app/views/components/pagination/_link.html.erb +1 -1
  90. data/app/views/components/pagination/_next.html.erb +1 -1
  91. data/app/views/components/pagination/_previous.html.erb +1 -1
  92. data/app/views/components/sidebar/_content.html.erb +1 -1
  93. data/app/views/components/sidebar/_footer.html.erb +1 -1
  94. data/app/views/components/sidebar/_group.html.erb +1 -1
  95. data/app/views/components/sidebar/_header.html.erb +1 -1
  96. data/app/views/components/sidebar/_inset.html.erb +1 -1
  97. data/app/views/components/sidebar/_menu.html.erb +1 -1
  98. data/app/views/components/sidebar/_menu_button.html.erb +1 -1
  99. data/app/views/components/sidebar/_menu_item.html.erb +1 -1
  100. data/app/views/components/sidebar/_menu_link.html.erb +4 -7
  101. data/app/views/components/sidebar/_provider.html.erb +1 -1
  102. data/app/views/components/sidebar/_trigger.html.erb +1 -1
  103. data/app/views/components/stats/_stats_card.html.erb +32 -92
  104. data/app/views/components/stats/_stats_grid.html.erb +18 -24
  105. data/app/views/components/table/_body.html.erb +1 -1
  106. data/app/views/components/table/_caption.html.erb +3 -3
  107. data/app/views/components/table/_cell.html.erb +3 -3
  108. data/app/views/components/table/_footer.html.erb +1 -1
  109. data/app/views/components/table/_head.html.erb +3 -3
  110. data/app/views/components/table/_header.html.erb +1 -1
  111. data/app/views/components/table/_row.html.erb +1 -1
  112. data/app/views/components/toast/_action.html.erb +1 -1
  113. data/app/views/components/toast/_description.html.erb +1 -1
  114. data/app/views/components/toast/_title.html.erb +1 -1
  115. data/app/views/components/toggle_group/_item.html.erb +1 -1
  116. data/lib/generators/maquina_components/install/templates/maquina_components_helper.rb.tt +8 -0
  117. data/lib/generators/maquina_components/scaffold_templates/scaffold_templates_generator.rb +40 -0
  118. data/lib/generators/maquina_components/scaffold_templates/templates/_form.html.erb.tt +49 -0
  119. data/lib/generators/maquina_components/scaffold_templates/templates/edit.html.erb.tt +29 -0
  120. data/lib/generators/maquina_components/scaffold_templates/templates/index.html.erb.tt +58 -0
  121. data/lib/generators/maquina_components/scaffold_templates/templates/new.html.erb.tt +26 -0
  122. data/lib/generators/maquina_components/scaffold_templates/templates/partial.html.erb.tt +17 -0
  123. data/lib/generators/maquina_components/scaffold_templates/templates/show.html.erb.tt +53 -0
  124. data/lib/maquina-components.rb +5 -0
  125. data/lib/maquina_components/engine.rb +7 -3
  126. data/lib/maquina_components/version.rb +1 -1
  127. metadata +52 -3
  128. data/app/assets/stylesheets/maquina_components.css +0 -178
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5347ee9b9b49584bdb8549ecc263f2137ce2331b805c5539cab9db84babf9f14
4
- data.tar.gz: 18b6b97ab09e7f5506c0d539db940808e48c481893ce6ad933f5226fbbd1dae9
3
+ metadata.gz: 9159362a034d89b9abf1b6a5a636851942f69c32491eea74c438c9d8dc678b57
4
+ data.tar.gz: 9ab23823a833fdfb8d02727fea268539ea0dd26d974b3212aedf90c87d2c5963
5
5
  SHA512:
6
- metadata.gz: bcbde28da68d8b05b3d97d47f2f373cb7313f37265d26b78ed309827011c1cf7e1d1871d68ec6b9670240b73b1464979a0af37faa4d495e15c5b7f40ecc1a3ca
7
- data.tar.gz: 47eb6037d5e98fc4b913750239d2e420c28c69b128ea4c5b9b17a450a5a2b5163f36e16cf7892b514e0c02622773c67e6620e90eea3e25d1f6350525b1a234d3
6
+ metadata.gz: 2b7d4230c490e4a98db049a2a527300b060873b6befb6925c2825455923e2179f353ffadbca0f90ebda98779ef711083b819c60a13763a5d982be3d22b73ab4e
7
+ data.tar.gz: c0671e469d43f1fbc265638755f453365748f2cf3d39e435ffa5218d81f9453a55f87cd1babe8acdfb2ccfdfb21a4e2415366a82388ddabb74f9a127a3bdc821
data/README.md CHANGED
@@ -76,6 +76,8 @@ This will:
76
76
  - Add theme variables (light + dark mode)
77
77
  - Create a helper file for icon customization
78
78
 
79
+ All engine helpers (`icon_for`, `sidebar_open?`, `toast_flash_messages`, etc.) are automatically available in your views without any additional configuration.
80
+
79
81
  ### 3. Use Components
80
82
 
81
83
  ```erb
@@ -117,6 +119,19 @@ bin/rails generate maquina_components:install --skip-theme
117
119
  bin/rails generate maquina_components:install --skip-helper
118
120
  ```
119
121
 
122
+ ### Scaffold Templates
123
+
124
+ Copy ERB scaffold templates into your application so Rails' built-in `scaffold`
125
+ generator produces maquina_components-styled views:
126
+
127
+ ```bash
128
+ bin/rails generate maquina_components:scaffold_templates
129
+ ```
130
+
131
+ This installs `index`, `show`, `new`, `edit`, `_form`, and partial templates to
132
+ `lib/templates/erb/scaffold/`. After running it, `rails generate scaffold ModelName field:type`
133
+ generates views built with maquina_components. Customize the templates as needed.
134
+
120
135
  **Prerequisite:** [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) must be installed first.
121
136
 
122
137
  ---
@@ -137,8 +152,10 @@ bin/rails generate maquina_components:install --skip-helper
137
152
  | **Card** | Content container with header, content, footer | [Card](https://maquina.app/documentation/components/card/) |
138
153
  | **Alert** | Callout messages (info, warning, error) | [Alert](https://maquina.app/documentation/components/alert/) |
139
154
  | **Badge** | Status indicators and labels | [Badge](https://maquina.app/documentation/components/badge/) |
140
- | **Table** | Data tables with sorting support | [Table](https://maquina.app/documentation/components/table/) |
155
+ | **Table** | Data tables with striped and bordered variants | [Table](https://maquina.app/documentation/components/table/) |
156
+ | **Separator** | Horizontal or vertical divider | [Separator](https://maquina.app/documentation/components/separator/) |
141
157
  | **Empty State** | Placeholder for empty lists | [Empty State](https://maquina.app/documentation/components/empty/) |
158
+ | **Stats** | Metric cards in a responsive grid | [Stats](https://maquina.app/documentation/components/stats/) |
142
159
 
143
160
  ### Navigation Components
144
161
 
@@ -155,6 +172,7 @@ bin/rails generate maquina_components:install --skip-helper
155
172
  | **Calendar** | Inline date picker with single/range selection | [Calendar](https://maquina.app/documentation/components/calendar/) |
156
173
  | **Combobox** | Searchable dropdown with keyboard navigation | [Combobox](https://maquina.app/documentation/components/combobox/) |
157
174
  | **Date Picker** | Popover-based date selection | [Date Picker](https://maquina.app/documentation/components/date-picker/) |
175
+ | **Drawer** | Slide-out panel with overlay and keyboard shortcut | [Drawer](https://maquina.app/documentation/components/drawer/) |
158
176
  | **Toggle Group** | Single/multiple selection button group | [Toggle Group](https://maquina.app/documentation/components/toggle-group/) |
159
177
 
160
178
  ### Feedback Components
@@ -94,6 +94,7 @@
94
94
  [data-breadcrumb-part="ellipsis"] {
95
95
  @apply inline-flex size-9 items-center justify-center;
96
96
  color: var(--muted-foreground);
97
+ cursor: pointer;
97
98
  }
98
99
 
99
100
  [data-breadcrumb-part="ellipsis"] svg {
@@ -106,6 +107,26 @@
106
107
  color: var(--foreground);
107
108
  }
108
109
 
110
+ /* ===== Overflow Dropdown (top-layer popover) ===== */
111
+ /* Visual styling comes from dropdown_menu.css via data-dropdown-menu-part.
112
+ Anchor positioning places the popover in modern browsers;
113
+ breadcrumb_controller falls back to measured coordinates elsewhere.
114
+ The doubled attribute selector outranks dropdown_menu.css's
115
+ position: absolute regardless of import order. */
116
+ @supports (anchor-name: --a) and (position-area: block-end) {
117
+ [data-breadcrumb-part="ellipsis"] {
118
+ anchor-name: --maquina-breadcrumbs;
119
+ }
120
+
121
+ [data-breadcrumb-part="dropdown"][data-dropdown-menu-part="content"] {
122
+ position: fixed;
123
+ position-anchor: --maquina-breadcrumbs;
124
+ position-area: block-end span-inline-end;
125
+ margin-block-start: 4px;
126
+ position-try-fallbacks: flip-block;
127
+ }
128
+ }
129
+
109
130
  /* ===== Responsive Behavior ===== */
110
131
  /* Force nowrap when responsive controller is active so scrollWidth detects overflow */
111
132
  [data-controller="breadcrumb"] [data-breadcrumb-part="list"] {
@@ -53,12 +53,47 @@
53
53
  @apply rounded-md border p-0 shadow-md outline-none;
54
54
  @apply min-w-[200px];
55
55
 
56
- /* Reset popover defaults - positioning handled by JS */
56
+ /* Reset popover defaults - positioning below (anchor CSS) or in JS fallback */
57
57
  margin: 0;
58
58
  padding: 0;
59
59
  inset: unset;
60
60
  }
61
61
 
62
+ /* ===== Anchor Positioning (modern browsers) ===== */
63
+ /* Places the top-layer popover declaratively; combobox_controller skips its
64
+ measured fallback when these features are supported. flip-block flips the
65
+ popover above the trigger when there is no room below — something the JS
66
+ fallback never did. */
67
+ @supports (anchor-name: --a) and (position-area: block-end) {
68
+ [data-combobox-part="trigger"] {
69
+ anchor-name: --maquina-combobox;
70
+ }
71
+
72
+ [data-combobox-part="content"] {
73
+ position: fixed;
74
+ position-anchor: --maquina-combobox;
75
+ margin-block-start: 4px;
76
+ position-try-fallbacks: flip-block;
77
+ }
78
+
79
+ [data-combobox-part="content"][data-align="start"] {
80
+ position-area: block-end span-inline-end;
81
+ }
82
+
83
+ [data-combobox-part="content"][data-align="center"] {
84
+ position-area: block-end;
85
+ }
86
+
87
+ [data-combobox-part="content"][data-align="end"] {
88
+ position-area: block-end span-inline-start;
89
+ }
90
+
91
+ [data-combobox-part="content"][data-width="default"] {
92
+ width: auto;
93
+ min-width: max(200px, anchor-size(inline));
94
+ }
95
+ }
96
+
62
97
  /* Width variants */
63
98
  [data-combobox-part="content"][data-width="sm"] {
64
99
  @apply w-40;
@@ -0,0 +1,135 @@
1
+ /* ===== Drawer Component Styles ===== */
2
+
3
+ /* ===== Provider (Wrapper) ===== */
4
+ [data-component="drawer"] {
5
+ @apply flex w-full;
6
+ }
7
+
8
+ /* ===== Drawer Root ===== */
9
+ [data-drawer-part="root"] {
10
+ @apply block text-foreground;
11
+ }
12
+
13
+ /* Prevent transitions on initial load */
14
+ [data-drawer-part="root"].drawer-loading,
15
+ [data-drawer-part="root"].drawer-loading * {
16
+ transition: none !important;
17
+ }
18
+
19
+ /* ===== Drawer Backdrop (overlay) ===== */
20
+ [data-drawer-part="backdrop"] {
21
+ @apply fixed inset-0 z-40 hidden bg-black opacity-0;
22
+ transition: opacity 300ms;
23
+ }
24
+
25
+ [data-drawer-part="backdrop"][data-state="visible"] {
26
+ @apply block opacity-60;
27
+ }
28
+
29
+ /* ===== Drawer Panel (fixed positioned) ===== */
30
+ [data-drawer-part="panel"] {
31
+ @apply fixed inset-y-0 z-50 flex h-full w-80 flex-col;
32
+ @apply bg-background shadow-lg;
33
+ transition: right 300ms ease-in-out, left 300ms ease-in-out;
34
+ }
35
+
36
+ [data-drawer-part="root"][data-side="left"] [data-drawer-part="panel"] {
37
+ @apply left-0;
38
+ right: auto;
39
+ }
40
+
41
+ [data-drawer-part="root"][data-side="right"] [data-drawer-part="panel"] {
42
+ @apply right-0;
43
+ left: auto;
44
+ }
45
+
46
+ /* Closed state - slide out */
47
+ [data-drawer-part="root"][data-side="left"][data-state="closed"] [data-drawer-part="panel"] {
48
+ @apply -translate-x-full;
49
+ }
50
+
51
+ [data-drawer-part="root"][data-side="right"][data-state="closed"] [data-drawer-part="panel"] {
52
+ @apply translate-x-full;
53
+ }
54
+
55
+ /* ===== Drawer Inner Content Wrapper ===== */
56
+ [data-drawer-part="inner"] {
57
+ @apply flex h-full w-full flex-col;
58
+ }
59
+
60
+ /* ===== Body Scroll Lock ===== */
61
+ /* Shared by drawer_controller and sidebar_controller: the controllers set
62
+ the data attribute and the --maquina-scroll-lock-top offset on <body>;
63
+ all styling lives here. */
64
+ body[data-maquina-scroll-locked] {
65
+ position: fixed;
66
+ top: var(--maquina-scroll-lock-top, 0px);
67
+ width: 100%;
68
+ overflow: hidden;
69
+ }
70
+
71
+ /* ===== Drawer Header ===== */
72
+ [data-drawer-part="header"] {
73
+ @apply flex shrink-0 items-center justify-between gap-2 border-b px-4 py-3;
74
+ border-color: var(--border);
75
+ }
76
+
77
+ [data-drawer-part="header-content"] {
78
+ @apply flex-1;
79
+ }
80
+
81
+ /* ===== Drawer Content (scrollable area) ===== */
82
+ [data-drawer-part="content"] {
83
+ @apply flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-4;
84
+ }
85
+
86
+ /* ===== Drawer Footer ===== */
87
+ [data-drawer-part="footer"] {
88
+ @apply flex shrink-0 flex-col gap-2 border-t p-4;
89
+ border-color: var(--border);
90
+ }
91
+
92
+ /* ===== Drawer Trigger ===== */
93
+
94
+ [data-drawer-part="trigger"]:hover {
95
+ background-color: var(--accent);
96
+ color: var(--accent-foreground);
97
+ }
98
+
99
+ /* ===== Drawer Title ===== */
100
+ [data-drawer-part="title"] {
101
+ @apply text-lg font-semibold;
102
+ }
103
+
104
+ /* ===== Drawer Description ===== */
105
+ [data-drawer-part="description"] {
106
+ @apply text-sm text-muted-foreground;
107
+ }
108
+
109
+ /* ===== Drawer Close Button ===== */
110
+ [data-drawer-part="close"] {
111
+ @apply inline-flex items-center justify-center rounded-md p-1;
112
+ @apply outline-hidden ring-ring focus-visible:ring-2;
113
+ @apply transition-colors;
114
+ color: var(--foreground);
115
+ }
116
+
117
+ [data-drawer-part="close"]:hover {
118
+ background-color: var(--accent);
119
+ color: var(--accent-foreground);
120
+ }
121
+
122
+ [data-drawer-part="close"] svg {
123
+ @apply size-4;
124
+ }
125
+
126
+ /* ===== Drawer Section ===== */
127
+ [data-drawer-part="section"] {
128
+ @apply flex flex-col gap-2;
129
+ }
130
+
131
+ /* ===== Drawer Separator ===== */
132
+ [data-drawer-part="separator"] {
133
+ @apply mx-4 my-2 w-auto;
134
+ background-color: var(--border);
135
+ }
@@ -0,0 +1,155 @@
1
+ /* ===== Menu Button Component Styles ===== */
2
+ /*
3
+ * Sidebar-flavored button that opens a dropdown panel.
4
+ * Pairs _menu_button.html.erb (trigger) with _dropdown.html.erb (content)
5
+ * and menu_button_controller.js.
6
+ *
7
+ * Structure:
8
+ * - menu-button (root ul)
9
+ * - item (li with Stimulus controller)
10
+ * - trigger (button)
11
+ * - avatar (text icon square)
12
+ * - text (title + subtitle)
13
+ * - content (positioned dropdown panel)
14
+ */
15
+
16
+ /* ===== Root Container ===== */
17
+ [data-component="menu-button"] {
18
+ @apply flex w-full min-w-0 flex-col gap-1;
19
+ margin: 0;
20
+ padding: 0;
21
+ list-style: none;
22
+ }
23
+
24
+ [data-menu-button-part="item"] {
25
+ position: relative;
26
+ list-style: none;
27
+ }
28
+
29
+ /* ===== Trigger Button ===== */
30
+ [data-menu-button-part="trigger"] {
31
+ @apply flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm;
32
+ @apply transition-[width,height,padding] duration-150;
33
+ height: 3rem;
34
+ background-color: transparent;
35
+ color: inherit;
36
+ border: none;
37
+ outline: none;
38
+ cursor: pointer;
39
+ }
40
+
41
+ [data-menu-button-part="trigger"]:hover,
42
+ [data-menu-button-part="trigger"]:active,
43
+ [data-menu-button-part="trigger"][data-state="open"] {
44
+ background-color: var(--sidebar-accent);
45
+ color: var(--sidebar-accent-foreground);
46
+ }
47
+
48
+ [data-menu-button-part="trigger"]:focus-visible {
49
+ box-shadow: 0 0 0 2px var(--sidebar-ring);
50
+ }
51
+
52
+ [data-menu-button-part="trigger"]:disabled,
53
+ [data-menu-button-part="trigger"][aria-disabled="true"] {
54
+ pointer-events: none;
55
+ opacity: 0.5;
56
+ }
57
+
58
+ [data-menu-button-part="trigger"] svg {
59
+ @apply size-4 shrink-0;
60
+ }
61
+
62
+ [data-menu-button-part="trigger"] img {
63
+ @apply shrink-0;
64
+ }
65
+
66
+ /* Icon-collapsed sidebar squeezes the trigger to an icon square */
67
+ [data-sidebar-part="root"][data-collapsible="icon"] [data-menu-button-part="trigger"] {
68
+ width: 2rem;
69
+ height: 2rem;
70
+ padding: 0;
71
+ }
72
+
73
+ /* ===== Avatar (text icon) ===== */
74
+ [data-menu-button-part="avatar"] {
75
+ @apply flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg;
76
+ background-color: var(--sidebar-primary);
77
+ color: var(--sidebar-primary-foreground);
78
+ }
79
+
80
+ /* ===== Text (title + subtitle) ===== */
81
+ [data-menu-button-part="text"] {
82
+ @apply grid flex-1 text-left text-sm leading-tight;
83
+ }
84
+
85
+ [data-menu-button-part="title"] {
86
+ @apply truncate font-semibold;
87
+ }
88
+
89
+ [data-menu-button-part="subtitle"] {
90
+ @apply truncate text-xs;
91
+ }
92
+
93
+ /* ===== Content Panel ===== */
94
+ [data-menu-button-part="content"] {
95
+ position: absolute;
96
+ z-index: 50;
97
+ display: none;
98
+ flex-direction: column;
99
+ overflow: hidden;
100
+ min-width: 14rem;
101
+ max-width: 14rem;
102
+ background-color: var(--popover, var(--background));
103
+ color: var(--popover-foreground, var(--foreground));
104
+ @apply rounded-lg border p-1 shadow-md;
105
+ border-color: var(--border);
106
+ }
107
+
108
+ [data-menu-button-part="content"][data-side="top"] {
109
+ bottom: calc(100% + 4px);
110
+ }
111
+
112
+ [data-menu-button-part="content"][data-side="bottom"] {
113
+ top: calc(100% + 4px);
114
+ }
115
+
116
+ [data-menu-button-part="content"][data-align="start"] {
117
+ left: 0;
118
+ }
119
+
120
+ [data-menu-button-part="content"][data-align="end"] {
121
+ right: 0;
122
+ }
123
+
124
+ /* Animation states */
125
+ [data-menu-button-part="content"][data-state="open"] {
126
+ display: flex;
127
+ animation: menu-button-in 150ms ease-out;
128
+ }
129
+
130
+ [data-menu-button-part="content"][data-state="closing"] {
131
+ display: flex;
132
+ animation: menu-button-out 100ms ease-in forwards;
133
+ }
134
+
135
+ @keyframes menu-button-in {
136
+ from {
137
+ opacity: 0;
138
+ transform: scale(0.95) translateY(4px);
139
+ }
140
+ to {
141
+ opacity: 1;
142
+ transform: scale(1) translateY(0);
143
+ }
144
+ }
145
+
146
+ @keyframes menu-button-out {
147
+ from {
148
+ opacity: 1;
149
+ transform: scale(1) translateY(0);
150
+ }
151
+ to {
152
+ opacity: 0;
153
+ transform: scale(0.95) translateY(4px);
154
+ }
155
+ }
@@ -0,0 +1,20 @@
1
+ /* ===== Separator Component Styles ===== */
2
+ /*
3
+ * Decorative divider, horizontal or vertical.
4
+ * Styled via data attributes; colors come from theme variables.
5
+ */
6
+
7
+ [data-component="separator"] {
8
+ @apply mx-2 shrink-0;
9
+ background-color: var(--border);
10
+ }
11
+
12
+ [data-component="separator"][data-orientation="horizontal"] {
13
+ height: 1px;
14
+ width: 100%;
15
+ }
16
+
17
+ [data-component="separator"][data-orientation="vertical"] {
18
+ width: 1px;
19
+ height: 1rem;
20
+ }
@@ -290,6 +290,18 @@
290
290
  @apply truncate;
291
291
  }
292
292
 
293
+ [data-sidebar-part="menu-link-text"] {
294
+ @apply flex flex-col gap-0.5 leading-none;
295
+ }
296
+
297
+ [data-sidebar-part="menu-link-title"] {
298
+ @apply font-medium;
299
+ }
300
+
301
+ [data-sidebar-part="menu-link-subtitle"] {
302
+ @apply text-xs text-sidebar-foreground/70;
303
+ }
304
+
293
305
  /* Has menu action - add padding */
294
306
  .group-has-data-\[sidebar-part\=menu-action\]\/menu-item [data-sidebar-part="menu-link"],
295
307
  [data-sidebar-part="menu-item"]:has([data-sidebar-part="menu-action"]) [data-sidebar-part="menu-link"] {
@@ -0,0 +1,101 @@
1
+ /* ===== Stats Component Styles ===== */
2
+ /*
3
+ * Dashboard stat cards and their responsive grid.
4
+ *
5
+ * Structure:
6
+ * - stats-grid (root, optional action slot)
7
+ * - action (leading or trailing action area)
8
+ * - grid (responsive card grid, columns via data-columns)
9
+ * - stats-card
10
+ * - body
11
+ * - text (title, value, subtitle)
12
+ * - icon
13
+ */
14
+
15
+ /* ===== Card ===== */
16
+ [data-component="stats-card"] {
17
+ @apply rounded-lg border p-4 shadow-sm;
18
+ background-color: var(--card);
19
+ border-color: var(--border);
20
+ }
21
+
22
+ [data-component="stats-card"] [data-stats-part="body"] {
23
+ @apply flex items-center justify-between;
24
+ }
25
+
26
+ [data-component="stats-card"] [data-stats-part="title"] {
27
+ @apply text-sm;
28
+ color: var(--muted-foreground);
29
+ }
30
+
31
+ [data-component="stats-card"] [data-stats-part="value"] {
32
+ @apply text-2xl font-bold;
33
+ }
34
+
35
+ [data-component="stats-card"] [data-stats-part="subtitle"] {
36
+ @apply mt-1 text-xs;
37
+ color: var(--muted-foreground);
38
+ }
39
+
40
+ [data-component="stats-card"] [data-stats-part="icon"] svg {
41
+ @apply size-8 shrink-0;
42
+ }
43
+
44
+ /* Color defaults live in the components layer so caller utility classes
45
+ (value_classes / icon_classes, e.g. text-green-600) always win — the
46
+ utilities layer is defined to beat the components layer, and unlayered
47
+ rules would beat utilities regardless of specificity */
48
+ @layer components {
49
+ [data-component="stats-card"] [data-stats-part="value"] {
50
+ color: var(--foreground);
51
+ }
52
+
53
+ [data-component="stats-card"] [data-stats-part="icon"] {
54
+ color: var(--muted-foreground);
55
+ }
56
+ }
57
+
58
+ /* ===== Grid ===== */
59
+ [data-component="stats-grid"][data-with-action="true"] {
60
+ @apply flex flex-col gap-4 lg:flex-row lg:items-center;
61
+ }
62
+
63
+ [data-component="stats-grid"] [data-stats-part="grid"] {
64
+ @apply grid grid-cols-1 gap-4;
65
+ }
66
+
67
+ [data-component="stats-grid"][data-with-action="true"] [data-stats-part="grid"] {
68
+ @apply flex-1;
69
+ }
70
+
71
+ [data-component="stats-grid"][data-columns="2"] [data-stats-part="grid"] {
72
+ @apply sm:grid-cols-2;
73
+ }
74
+
75
+ [data-component="stats-grid"][data-columns="3"] [data-stats-part="grid"] {
76
+ @apply sm:grid-cols-3;
77
+ }
78
+
79
+ [data-component="stats-grid"][data-columns="4"] [data-stats-part="grid"] {
80
+ @apply sm:grid-cols-4;
81
+ }
82
+
83
+ [data-component="stats-grid"][data-columns="5"] [data-stats-part="grid"] {
84
+ @apply sm:grid-cols-5;
85
+ }
86
+
87
+ [data-component="stats-grid"][data-columns="6"] [data-stats-part="grid"] {
88
+ @apply sm:grid-cols-6;
89
+ }
90
+
91
+ [data-component="stats-grid"] [data-stats-part="action"] {
92
+ @apply flex justify-center;
93
+ }
94
+
95
+ [data-component="stats-grid"] [data-stats-part="action"][data-position="start"] {
96
+ @apply lg:justify-start;
97
+ }
98
+
99
+ [data-component="stats-grid"] [data-stats-part="action"][data-position="end"] {
100
+ @apply lg:justify-end;
101
+ }
@@ -7,12 +7,16 @@
7
7
  @import "../../stylesheets/card.css";
8
8
  @import "../../stylesheets/combobox.css";
9
9
  @import "../../stylesheets/date_picker.css";
10
+ @import "../../stylesheets/drawer.css";
10
11
  @import "../../stylesheets/dropdown_menu.css";
11
12
  @import "../../stylesheets/empty.css";
12
13
  @import "../../stylesheets/form.css";
13
14
  @import "../../stylesheets/header.css";
15
+ @import "../../stylesheets/menu_button.css";
14
16
  @import "../../stylesheets/pagination.css";
17
+ @import "../../stylesheets/separator.css";
15
18
  @import "../../stylesheets/sidebar.css";
19
+ @import "../../stylesheets/stats.css";
16
20
  @import "../../stylesheets/table.css";
17
21
  @import "../../stylesheets/toast.css";
18
22
  @import "../../stylesheets/toggle_group.css";
@@ -59,7 +59,8 @@ module MaquinaComponents
59
59
  # @return [String] Rendered HTML
60
60
  def combobox(id: nil, name: nil, value: nil, placeholder: "Select...", css_classes: "", **html_options, &block)
61
61
  builder = ComboboxBuilder.new(self, placeholder: placeholder)
62
- combobox_id = id || "combobox-#{SecureRandom.hex(4)}"
62
+ # Deterministic fallback id: random ids break idiomorph matching across Turbo morphs
63
+ combobox_id = id || "combobox-#{name.to_s.parameterize.presence || "default"}"
63
64
  builder.combobox_id = combobox_id
64
65
 
65
66
  capture(builder, &block)
@@ -0,0 +1,33 @@
1
+ module MaquinaComponents
2
+ # Shared plumbing for component partials.
3
+ module ComponentsHelper
4
+ # Merges a caller's data: hash with a component's own data attributes.
5
+ #
6
+ # Component values win for identity keys (component, variant, size, ...)
7
+ # so callers can't accidentally break a component's styling hooks. The
8
+ # Stimulus token keys :controller and :action concatenate instead, so a
9
+ # caller can attach extra behavior without losing the component's:
10
+ #
11
+ # <%= render "components/combobox", name: "country",
12
+ # data: { controller: "analytics", action: "change->analytics#track" } %>
13
+ # # => data-controller="combobox analytics"
14
+ #
15
+ # @param html_options [Hash] the partial's **html_options (data: is consumed)
16
+ # @param component_data [Hash] the component's own data attributes
17
+ # @return [Hash] the merged data hash
18
+ def merge_component_data(html_options, **component_data)
19
+ user_data = html_options.delete(:data) || {}
20
+ merged = user_data.merge(component_data)
21
+
22
+ [:controller, :action].each do |key|
23
+ user_value = user_data[key] || user_data[key.to_s]
24
+ next if user_value.blank? || component_data[key].blank?
25
+
26
+ merged.delete(key.to_s)
27
+ merged[key] = "#{component_data[key]} #{user_value}"
28
+ end
29
+
30
+ merged
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ module MaquinaComponents
2
+ module DrawerHelper
3
+ def drawer_state(cookie_name = "drawer_state")
4
+ cookie_value = cookies[cookie_name]
5
+
6
+ return :open if cookie_value.nil?
7
+
8
+ (cookie_value == "true") ? :open : :closed
9
+ end
10
+
11
+ def drawer_open?(cookie_name = "drawer_state")
12
+ drawer_state(cookie_name) == :open
13
+ end
14
+
15
+ def drawer_closed?(cookie_name = "drawer_state")
16
+ drawer_state(cookie_name) == :closed
17
+ end
18
+ end
19
+ end