brutalist_rails_ui 0.1.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 +7 -0
- data/README.md +240 -0
- data/app/views/brutalist_rails_ui/_flash.html.erb +7 -0
- data/app/views/brutalist_rails_ui/_form_buttons.html.erb +6 -0
- data/app/views/brutalist_rails_ui/_modal.html.erb +15 -0
- data/app/views/brutalist_rails_ui/_pagination.html.erb +13 -0
- data/lib/brutalist_rails_ui/engine.rb +4 -0
- data/lib/brutalist_rails_ui/helpers.rb +138 -0
- data/lib/brutalist_rails_ui/version.rb +3 -0
- data/lib/brutalist_rails_ui.rb +3 -0
- data/lib/generators/brutalist_rails_ui/install/install_generator.rb +34 -0
- data/lib/generators/brutalist_rails_ui/install/templates/brutalist_rails_ui.css +88 -0
- data/lib/generators/brutalist_rails_ui/install/templates/modal_controller.js +16 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 926531173316cb0b387b1797fbdd6c0d6dab22ef199c62fb72ec3dd888cb1c63
|
|
4
|
+
data.tar.gz: 8d53b13cda298e3b3a2b883f65085a7c32c267a86033461169fd03faf79414b7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3140365ce4b09dc1bf643f5783c6ebec46fa2cf8da2de1e896d9be59b58d79e1f7c062814dab893dbb5079ab67c91b26f282ee2e7f2c49c0c3431db544ed98f7
|
|
7
|
+
data.tar.gz: c7a80dd90bf304791176303e660cc6b6106e79668f1bfbc2e852c46af3b192018d745c390213b8b00ad20325a8d6da13fcba73b561ea4d9a45f6b55a25c211e3
|
data/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# brutalist_rails_ui
|
|
2
|
+
|
|
3
|
+
Neobrutalist UI component library for Rails. Thick black borders, yellow accents, uppercase type, no rounded corners.
|
|
4
|
+
|
|
5
|
+
Built with Tailwind CSS v4 + Hotwire.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "brutalist_rails_ui", path: "path/to/brutalist_rails_ui"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run the installer:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
rails g brutalist_rails_ui:install
|
|
20
|
+
bin/rails tailwindcss:build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Add to `app/helpers/application_helper.rb`:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
module ApplicationHelper
|
|
27
|
+
include BrutalistRailsUi::Helpers
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The generator:
|
|
32
|
+
- Copies `brutalist_rails_ui.css` → `app/assets/tailwind/`
|
|
33
|
+
- Injects `@import "./brutalist_rails_ui"` into `application.css`
|
|
34
|
+
- Copies `modal_controller.js` → `app/javascript/controllers/`
|
|
35
|
+
|
|
36
|
+
Add `modal_controller` to your importmap in `application.html.erb`:
|
|
37
|
+
|
|
38
|
+
```erb
|
|
39
|
+
"controllers/modal_controller": "<%= asset_path('controllers/modal_controller.js') %>"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## CSS Directives
|
|
43
|
+
|
|
44
|
+
| Class | Use |
|
|
45
|
+
|-------|-----|
|
|
46
|
+
| `.btn-primary` | Black button, yellow on hover |
|
|
47
|
+
| `.btn-secondary` | White button, yellow on hover |
|
|
48
|
+
| `.btn-danger` | Red button |
|
|
49
|
+
| `.input` | Form input field |
|
|
50
|
+
| `.label` | Form label |
|
|
51
|
+
| `.card` | White box with black border |
|
|
52
|
+
| `.card-header` | Black bar with white text (inside `.card`) |
|
|
53
|
+
| `.table-brutal` | Table with black header and dividers |
|
|
54
|
+
| `.badge` | Inline status pill |
|
|
55
|
+
| `.money-input` | Wrapper for `$`-prefixed number inputs |
|
|
56
|
+
| `.progress-bar` | Gray track with `.progress-bar-fill` inside |
|
|
57
|
+
|
|
58
|
+
## View Helpers
|
|
59
|
+
|
|
60
|
+
### `page_header`
|
|
61
|
+
|
|
62
|
+
```erb
|
|
63
|
+
<%= page_header "Transactions" do %>
|
|
64
|
+
<%= link_to "+ Add", new_transaction_path, class: "btn-primary text-sm" %>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<%# With subtitle %>
|
|
68
|
+
<%= page_header "Unassigned", subtitle: "42 transactions need a category" %>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `card`
|
|
72
|
+
|
|
73
|
+
```erb
|
|
74
|
+
<%= card "Recent Activity" do %>
|
|
75
|
+
<p class="p-6">Content here</p>
|
|
76
|
+
<% end %>
|
|
77
|
+
|
|
78
|
+
<%# No header %>
|
|
79
|
+
<%= card do %>
|
|
80
|
+
<p class="p-6">Content</p>
|
|
81
|
+
<% end %>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `card_header`
|
|
85
|
+
|
|
86
|
+
```erb
|
|
87
|
+
<div class="card">
|
|
88
|
+
<%= card_header "Accounts", action_text: "View all", action_path: accounts_path %>
|
|
89
|
+
...
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `empty_state`
|
|
94
|
+
|
|
95
|
+
```erb
|
|
96
|
+
<%= empty_state icon: "✓", message: "All transactions assigned" %>
|
|
97
|
+
|
|
98
|
+
<%= empty_state icon: "📭", message: "No accounts yet" do %>
|
|
99
|
+
<%= link_to "Connect a bank", banks_link_path, class: "btn-primary mt-4" %>
|
|
100
|
+
<% end %>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `kpi_box`
|
|
104
|
+
|
|
105
|
+
```erb
|
|
106
|
+
<div class="grid grid-cols-2 gap-4">
|
|
107
|
+
<%= kpi_box label: "Total Spent", value: "$12.40" %>
|
|
108
|
+
<%= kpi_box label: "Total Calls", value: "42", dark: true %>
|
|
109
|
+
</div>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `cta_banner`
|
|
113
|
+
|
|
114
|
+
```erb
|
|
115
|
+
<%= cta_banner headline: "Connect a bank",
|
|
116
|
+
subtext: "Sync transactions automatically",
|
|
117
|
+
link_text: "Get Started",
|
|
118
|
+
path: banks_link_path %>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `status_badge`
|
|
122
|
+
|
|
123
|
+
```erb
|
|
124
|
+
<%= status_badge "pending" %>
|
|
125
|
+
<%= status_badge "over", "over" => "bg-red-600 text-white" %>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `nav_link`
|
|
129
|
+
|
|
130
|
+
```erb
|
|
131
|
+
<%= nav_link "Dashboard", root_path %>
|
|
132
|
+
<%= nav_link "Transactions", transactions_path %>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `money` / `money_class`
|
|
136
|
+
|
|
137
|
+
```erb
|
|
138
|
+
<span class="<%= money_class(transaction.amount) %>">
|
|
139
|
+
<%= money(transaction.amount) %>
|
|
140
|
+
</span>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Partials
|
|
144
|
+
|
|
145
|
+
### Modal
|
|
146
|
+
|
|
147
|
+
Wraps content in a Turbo Frame modal overlay with backdrop-click-to-close.
|
|
148
|
+
|
|
149
|
+
```erb
|
|
150
|
+
<%# In your view (e.g. transactions/new.html.erb) %>
|
|
151
|
+
<%= render "brutalist_rails_ui/modal", title: "Add Transaction", width: "max-w-md" do %>
|
|
152
|
+
<%= render "form", transaction: @transaction %>
|
|
153
|
+
<% end %>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Options:
|
|
157
|
+
- `title` — header text (required)
|
|
158
|
+
- `width` — Tailwind max-width class (default: `max-w-sm`)
|
|
159
|
+
- `frame_id` — turbo-frame id (default: `modal`)
|
|
160
|
+
- `body_class` — padding class for body wrapper (default: `p-6`)
|
|
161
|
+
|
|
162
|
+
Add `<turbo-frame id="modal"></turbo-frame>` to your layout.
|
|
163
|
+
|
|
164
|
+
### Flash
|
|
165
|
+
|
|
166
|
+
```erb
|
|
167
|
+
<%= render "brutalist_rails_ui/flash" %>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Pagination
|
|
171
|
+
|
|
172
|
+
Requires [Pagy](https://github.com/ddnexus/pagy).
|
|
173
|
+
|
|
174
|
+
```erb
|
|
175
|
+
<%= render "brutalist_rails_ui/pagination", pagy: @pagy %>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Form buttons
|
|
179
|
+
|
|
180
|
+
```erb
|
|
181
|
+
<%= render "brutalist_rails_ui/form_buttons",
|
|
182
|
+
f: f,
|
|
183
|
+
cancel_path: transactions_path,
|
|
184
|
+
submit_text: "Add Transaction" %>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Options:
|
|
188
|
+
- `f` — form builder (required)
|
|
189
|
+
- `cancel_path` — cancel link href (required)
|
|
190
|
+
- `submit_text` — submit button label (default: `Save`)
|
|
191
|
+
- `cancel_text` — cancel button label (default: `Cancel`)
|
|
192
|
+
- `cancel_frame` — turbo frame for cancel link (default: `_top`)
|
|
193
|
+
|
|
194
|
+
## Usage with WASM (wasm-rails)
|
|
195
|
+
|
|
196
|
+
When running inside [wasm-rails](https://github.com/emerson-argueta/wasm-rails), three extra steps are required because Bundler auto-require and engine view path registration don't work in the WASM environment.
|
|
197
|
+
|
|
198
|
+
### 1. Explicit require in `config/application.rb`
|
|
199
|
+
|
|
200
|
+
`Bundler.require` is skipped in WASM, so add an explicit require alongside your other gems:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
require "wasm_rails"
|
|
204
|
+
require "brutalist_rails_ui"
|
|
205
|
+
require "turbo-rails"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 2. Include helpers in `ApplicationHelper`
|
|
209
|
+
|
|
210
|
+
Rails hooks like `ActiveSupport.on_load` and `config.to_prepare` don't fire reliably in WASM. Include helpers directly instead:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
module ApplicationHelper
|
|
214
|
+
include BrutalistRailsUi::Helpers
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 3. Bundle the gem's ERB partials
|
|
219
|
+
|
|
220
|
+
Add `brutalist_rails_ui` to `GEM_EXTRA_PATHS` in `bin/build_app_bundle.mjs` so the gem's `app/views` are included in the WASM bundle:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const GEM_EXTRA_PATHS = {
|
|
224
|
+
'turbo-rails': ['app/controllers', 'app/controllers/concerns', 'app/helpers', 'app/models', 'app/models/concerns', 'app/views'],
|
|
225
|
+
'brutalist_rails_ui': ['app/views'],
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Then rebuild:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm run build:app
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Requirements
|
|
236
|
+
|
|
237
|
+
- Ruby 3.1+
|
|
238
|
+
- Rails 7.1+
|
|
239
|
+
- Tailwind CSS v4 (via tailwindcss-rails)
|
|
240
|
+
- Hotwire / Stimulus (for modal controller)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% if notice.present? %>
|
|
2
|
+
<div id="flash" class="border-2 border-black bg-yellow-400 p-3 text-sm font-bold mb-4"><%= notice %></div>
|
|
3
|
+
<% elsif alert.present? %>
|
|
4
|
+
<div id="flash" class="border-2 border-black bg-red-500 text-white p-3 text-sm font-bold mb-4"><%= alert %></div>
|
|
5
|
+
<% else %>
|
|
6
|
+
<div id="flash"></div>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="flex justify-end gap-2 pt-2">
|
|
2
|
+
<%= link_to local_assigns.fetch(:cancel_text, "Cancel"), cancel_path,
|
|
3
|
+
class: "btn-secondary",
|
|
4
|
+
data: { turbo_frame: local_assigns.fetch(:cancel_frame, "_top") } %>
|
|
5
|
+
<%= f.submit local_assigns.fetch(:submit_text, "Save"), class: "btn-primary" %>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<turbo-frame id="<%= local_assigns.fetch(:frame_id, "modal") %>">
|
|
2
|
+
<div class="fixed inset-0 bg-black/60 flex items-center justify-content z-50"
|
|
3
|
+
data-controller="modal"
|
|
4
|
+
data-action="click->modal#closeOnBackdrop">
|
|
5
|
+
<div class="bg-white border-2 border-black w-full <%= local_assigns.fetch(:width, "max-w-sm") %> overflow-hidden mx-auto"
|
|
6
|
+
data-modal-target="panel">
|
|
7
|
+
<div class="px-6 py-3 bg-black">
|
|
8
|
+
<h2 class="text-sm font-black text-white uppercase tracking-wider"><%= title %></h2>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="<%= local_assigns.fetch(:body_class, "p-6") %>">
|
|
11
|
+
<%= yield %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</turbo-frame>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% if pagy.last > 1 %>
|
|
2
|
+
<div class="flex items-center justify-between mt-4 text-sm font-bold border-2 border-black px-4 py-3 bg-white">
|
|
3
|
+
<span class="text-xs uppercase tracking-wide">Showing <%= pagy.from %>–<%= pagy.to %> of <%= pagy.count %></span>
|
|
4
|
+
<div class="flex gap-0">
|
|
5
|
+
<% if pagy.previous %>
|
|
6
|
+
<%= link_to "← Prev", url_for(page: pagy.previous), class: "px-3 py-1 border-2 border-black hover:bg-yellow-400 font-bold text-xs" %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% if pagy.next %>
|
|
9
|
+
<%= link_to "Next →", url_for(page: pagy.next), class: "px-3 py-1 border-2 border-black hover:bg-yellow-400 font-bold text-xs" %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module BrutalistRailsUi
|
|
2
|
+
module Helpers
|
|
3
|
+
# Nav link that highlights when active.
|
|
4
|
+
# Pass mobile: true for the vertical mobile menu variant.
|
|
5
|
+
def nav_link(label, path, mobile: false, **options)
|
|
6
|
+
base = path.split("?").first
|
|
7
|
+
active = current_page?(path) || (base != "/" && request.path.start_with?(base))
|
|
8
|
+
if mobile
|
|
9
|
+
classes = active \
|
|
10
|
+
? "block bg-yellow-400 text-black px-4 py-3 text-sm font-bold border-b border-gray-700" \
|
|
11
|
+
: "block text-white hover:bg-gray-800 px-4 py-3 text-sm font-bold border-b border-gray-700"
|
|
12
|
+
else
|
|
13
|
+
classes = active \
|
|
14
|
+
? "bg-yellow-400 text-black px-3 py-4 text-sm font-bold border-r border-l border-yellow-400" \
|
|
15
|
+
: "text-white hover:bg-white hover:text-black px-3 py-4 text-sm font-bold border-r border-gray-700 last:border-r-0"
|
|
16
|
+
end
|
|
17
|
+
link_to label, path, class: classes, **options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Page header row: h1 title + optional action buttons block.
|
|
21
|
+
#
|
|
22
|
+
# <%= page_header "Transactions" do %>
|
|
23
|
+
# <%= link_to "+ Add", new_transaction_path, class: "btn-primary" %>
|
|
24
|
+
# <% end %>
|
|
25
|
+
def page_header(title, subtitle: nil, &block)
|
|
26
|
+
content_tag :div, class: "flex flex-wrap items-center justify-between gap-3 mb-6" do
|
|
27
|
+
concat content_tag(:div) {
|
|
28
|
+
concat content_tag(:h1, title, class: "text-2xl font-black text-black uppercase tracking-tight")
|
|
29
|
+
concat content_tag(:p, subtitle, class: "text-sm font-bold text-gray-500 mt-1 uppercase tracking-wide") if subtitle
|
|
30
|
+
}
|
|
31
|
+
concat content_tag(:div, capture(&block), class: "flex flex-wrap items-center gap-2") if block
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# White card with optional black header bar.
|
|
36
|
+
#
|
|
37
|
+
# <%= card "Recent Transactions" do %>
|
|
38
|
+
# <p class="p-6">Content here</p>
|
|
39
|
+
# <% end %>
|
|
40
|
+
def card(title = nil, &block)
|
|
41
|
+
content_tag :div, class: "bg-white border-2 border-black overflow-hidden" do
|
|
42
|
+
if title
|
|
43
|
+
concat content_tag(:div, class: "px-6 py-3 bg-black") {
|
|
44
|
+
content_tag(:h2, title, class: "text-xs font-bold text-white uppercase tracking-wider")
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
concat capture(&block)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Card header bar with optional right-side action.
|
|
52
|
+
#
|
|
53
|
+
# <%= card_header "Accounts", action_text: "View all", action_path: accounts_path %>
|
|
54
|
+
def card_header(title, action_text: nil, action_path: nil)
|
|
55
|
+
content_tag :div, class: "px-6 py-3 bg-black flex items-center justify-between" do
|
|
56
|
+
concat content_tag(:h2, title, class: "text-xs font-bold text-white uppercase tracking-wider")
|
|
57
|
+
if action_text && action_path
|
|
58
|
+
concat link_to(action_text, action_path, class: "text-xs text-yellow-400 hover:text-white font-bold")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Centered empty state with icon and message.
|
|
64
|
+
#
|
|
65
|
+
# <%= empty_state icon: "✓", message: "All transactions assigned" %>
|
|
66
|
+
# <%= empty_state icon: "📭", message: "No accounts yet" do %>
|
|
67
|
+
# <%= link_to "Connect a bank", banks_link_path, class: "btn-primary mt-4" %>
|
|
68
|
+
# <% end %>
|
|
69
|
+
def empty_state(icon: nil, message:, &block)
|
|
70
|
+
content_tag :div, class: "px-6 py-12 text-center" do
|
|
71
|
+
concat content_tag(:p, icon, class: "text-3xl mb-3") if icon
|
|
72
|
+
concat content_tag(:p, message, class: "font-black text-black uppercase tracking-wide")
|
|
73
|
+
concat capture(&block) if block
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# KPI stat box for summary grids.
|
|
78
|
+
#
|
|
79
|
+
# <div class="grid grid-cols-2 gap-4">
|
|
80
|
+
# <%= kpi_box label: "Total Spent", value: "$12.40" %>
|
|
81
|
+
# <%= kpi_box label: "Total Calls", value: "42", dark: true %>
|
|
82
|
+
# </div>
|
|
83
|
+
def kpi_box(label:, value:, dark: false)
|
|
84
|
+
bg = dark ? "bg-black text-white" : "bg-white border-2 border-black text-black"
|
|
85
|
+
label_class = dark ? "text-xs font-bold uppercase tracking-widest text-gray-400 mb-1" \
|
|
86
|
+
: "text-xs font-bold uppercase tracking-widest text-gray-500 mb-1"
|
|
87
|
+
content_tag :div, class: "#{bg} p-5" do
|
|
88
|
+
concat content_tag(:p, label, class: label_class)
|
|
89
|
+
concat content_tag(:p, value, class: "text-2xl font-black")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Yellow CTA banner with headline and action link.
|
|
94
|
+
#
|
|
95
|
+
# <%= cta_banner headline: "Connect a bank",
|
|
96
|
+
# subtext: "Sync transactions automatically",
|
|
97
|
+
# link_text: "Get Started",
|
|
98
|
+
# path: banks_link_path %>
|
|
99
|
+
def cta_banner(headline:, subtext: nil, link_text:, path:, **link_options)
|
|
100
|
+
content_tag :div, class: "border-2 border-black bg-yellow-400 p-6 flex items-center justify-between gap-4" do
|
|
101
|
+
concat content_tag(:div) {
|
|
102
|
+
concat content_tag(:p, headline, class: "font-black text-black uppercase")
|
|
103
|
+
concat content_tag(:p, subtext, class: "text-sm text-black mt-1 font-bold") if subtext
|
|
104
|
+
}
|
|
105
|
+
concat link_to(link_text, path, class: "btn-primary flex-shrink-0", **link_options)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Status badge pill.
|
|
110
|
+
#
|
|
111
|
+
# <%= status_badge "pending" %>
|
|
112
|
+
# <%= status_badge "funded", "funded" => "bg-green-600 text-white" %>
|
|
113
|
+
def status_badge(status, color_map = {})
|
|
114
|
+
defaults = {
|
|
115
|
+
"active" => "bg-black text-white",
|
|
116
|
+
"funded" => "bg-black text-white",
|
|
117
|
+
"over" => "bg-red-600 text-white",
|
|
118
|
+
"pending" => "bg-yellow-400 text-black",
|
|
119
|
+
"income" => "bg-black text-white",
|
|
120
|
+
"transfer" => "bg-white text-black",
|
|
121
|
+
"untracked" => "bg-white text-black"
|
|
122
|
+
}.merge(color_map)
|
|
123
|
+
color = defaults[status.to_s] || "bg-white text-black"
|
|
124
|
+
tag.span status.to_s.humanize,
|
|
125
|
+
class: "inline-flex items-center border-2 border-black px-2 py-0.5 text-xs font-bold uppercase tracking-wide #{color}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Format a number as currency.
|
|
129
|
+
def money(amount)
|
|
130
|
+
number_to_currency(amount, unit: "$", precision: 2)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# CSS class for positive/negative amounts.
|
|
134
|
+
def money_class(amount)
|
|
135
|
+
amount.to_f.negative? ? "text-red-600" : "text-black"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module BrutalistRailsUi
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Installs BrutalistRailsUi CSS and Stimulus modal controller"
|
|
9
|
+
|
|
10
|
+
def copy_css
|
|
11
|
+
copy_file "brutalist_rails_ui.css", "app/assets/tailwind/brutalist_rails_ui.css"
|
|
12
|
+
inject_into_file "app/assets/tailwind/application.css",
|
|
13
|
+
%(@import "./brutalist_rails_ui";\n),
|
|
14
|
+
after: %(@import "tailwindcss";\n)
|
|
15
|
+
rescue Thor::Error
|
|
16
|
+
say_status :skip, "Could not inject import — add '@import \"./brutalist_rails_ui\";' to app/assets/tailwind/application.css manually", :yellow
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_modal_controller
|
|
20
|
+
copy_file "modal_controller.js", "app/javascript/controllers/modal_controller.js"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def done
|
|
24
|
+
say ""
|
|
25
|
+
say "BrutalistRailsUi installed!", :green
|
|
26
|
+
say ""
|
|
27
|
+
say " 1. Rebuild Tailwind: bin/rails tailwindcss:build"
|
|
28
|
+
say " 2. Add modal to your layout's importmap (if using importmap-rails):"
|
|
29
|
+
say " \"controllers/modal_controller\": asset_path('controllers/modal_controller.js')"
|
|
30
|
+
say ""
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* Buttons */
|
|
3
|
+
.btn-primary {
|
|
4
|
+
@apply inline-flex items-center px-4 py-2 text-sm font-bold bg-black text-white hover:bg-yellow-400 hover:text-black border-2 border-black cursor-pointer;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.btn-secondary {
|
|
8
|
+
@apply inline-flex items-center px-4 py-2 text-sm font-bold bg-white text-black hover:bg-yellow-400 border-2 border-black cursor-pointer;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.btn-danger {
|
|
12
|
+
@apply inline-flex items-center px-4 py-2 text-sm font-bold bg-red-600 text-white hover:bg-red-700 border-2 border-black cursor-pointer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Form controls */
|
|
16
|
+
.input {
|
|
17
|
+
@apply w-full border-2 border-black px-3 py-2 text-sm font-mono focus:outline-none focus:border-black bg-white;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.label {
|
|
21
|
+
@apply block text-xs font-bold uppercase tracking-wide mb-1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Layout */
|
|
25
|
+
.card {
|
|
26
|
+
@apply bg-white border-2 border-black overflow-hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.card-header {
|
|
30
|
+
@apply px-6 py-3 bg-black;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Table */
|
|
34
|
+
.table-brutal {
|
|
35
|
+
@apply w-full;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.table-brutal thead {
|
|
39
|
+
@apply bg-black;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.table-brutal thead th {
|
|
43
|
+
@apply px-6 py-3 text-left text-xs font-bold text-gray-400 uppercase tracking-wider;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.table-brutal tbody {
|
|
47
|
+
@apply divide-y-2 divide-black bg-white;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.table-brutal tbody tr {
|
|
51
|
+
@apply hover:bg-gray-50;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.table-brutal tbody td {
|
|
55
|
+
@apply px-6 py-3 text-sm;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Badge */
|
|
59
|
+
.badge {
|
|
60
|
+
@apply inline-flex items-center border-2 border-black px-2 py-0.5 text-xs font-bold uppercase tracking-wide;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Money input wrapper */
|
|
64
|
+
.money-input {
|
|
65
|
+
@apply relative;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.money-input .money-prefix {
|
|
69
|
+
@apply absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 text-sm font-bold pointer-events-none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.money-input .input {
|
|
73
|
+
@apply pl-7;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Progress bar */
|
|
77
|
+
.progress-bar {
|
|
78
|
+
@apply h-2 bg-gray-200 overflow-hidden;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.progress-bar-fill {
|
|
82
|
+
@apply h-full bg-black transition-all;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.progress-bar-fill.over {
|
|
86
|
+
@apply bg-red-600;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["panel"]
|
|
5
|
+
|
|
6
|
+
closeOnBackdrop(event) {
|
|
7
|
+
if (this.hasPanelTarget && !this.panelTarget.contains(event.target)) {
|
|
8
|
+
this.close()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
close() {
|
|
13
|
+
const frame = this.element.closest("turbo-frame")
|
|
14
|
+
if (frame) frame.innerHTML = ""
|
|
15
|
+
}
|
|
16
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: brutalist_rails_ui
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Emerson Argueta
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: actionview
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.1'
|
|
41
|
+
description:
|
|
42
|
+
email:
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- README.md
|
|
48
|
+
- app/views/brutalist_rails_ui/_flash.html.erb
|
|
49
|
+
- app/views/brutalist_rails_ui/_form_buttons.html.erb
|
|
50
|
+
- app/views/brutalist_rails_ui/_modal.html.erb
|
|
51
|
+
- app/views/brutalist_rails_ui/_pagination.html.erb
|
|
52
|
+
- lib/brutalist_rails_ui.rb
|
|
53
|
+
- lib/brutalist_rails_ui/engine.rb
|
|
54
|
+
- lib/brutalist_rails_ui/helpers.rb
|
|
55
|
+
- lib/brutalist_rails_ui/version.rb
|
|
56
|
+
- lib/generators/brutalist_rails_ui/install/install_generator.rb
|
|
57
|
+
- lib/generators/brutalist_rails_ui/install/templates/brutalist_rails_ui.css
|
|
58
|
+
- lib/generators/brutalist_rails_ui/install/templates/modal_controller.js
|
|
59
|
+
homepage:
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata: {}
|
|
63
|
+
post_install_message:
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.5.9
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Neobrutalist UI components for Rails — Tailwind directives, view helpers,
|
|
82
|
+
and partials.
|
|
83
|
+
test_files: []
|