voidable-hotwire 0.1.1 → 0.2.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/devise/confirmations/new.html.erb +22 -0
  3. data/app/views/devise/passwords/edit.html.erb +27 -0
  4. data/app/views/devise/passwords/new.html.erb +21 -0
  5. data/app/views/devise/registrations/edit.html.erb +50 -0
  6. data/app/views/devise/registrations/new.html.erb +29 -0
  7. data/app/views/devise/sessions/new.html.erb +35 -0
  8. data/app/views/devise/shared/_error_messages.html.erb +10 -0
  9. data/app/views/devise/shared/_links.html.erb +33 -0
  10. data/app/views/devise/unlocks/new.html.erb +22 -0
  11. data/lib/generators/voidable/devise_views/devise_views_generator.rb +15 -0
  12. data/lib/generators/voidable/install/install_generator.rb +112 -0
  13. data/lib/generators/voidable/install/templates/_settings_menu.html.erb +31 -0
  14. data/lib/generators/voidable/install/templates/application_sidebar.html.erb.tt +86 -0
  15. data/lib/generators/voidable/install/templates/application_topbar.html.erb.tt +69 -0
  16. data/lib/generators/voidable/install/templates/initializer.rb.tt +2 -0
  17. data/lib/generators/voidable/install/templates/settings_controller.rb.tt +7 -0
  18. data/lib/generators/voidable/install/templates/sidebar.html.erb.tt +86 -0
  19. data/lib/generators/voidable/install/templates/topbar.html.erb.tt +69 -0
  20. data/lib/generators/voidable/install/templates/voidable-devise.css +37 -0
  21. data/lib/generators/voidable/install/templates/voidable-layout-sidebar.css +460 -0
  22. data/lib/generators/voidable/install/templates/voidable-layout-topbar.css +361 -0
  23. data/lib/generators/voidable/install/templates/voidable-layout.js +41 -0
  24. data/lib/templates/erb/scaffold/_form.html.erb.tt +80 -0
  25. data/lib/templates/erb/scaffold/edit.html.erb.tt +18 -0
  26. data/lib/templates/erb/scaffold/index.html.erb.tt +65 -0
  27. data/lib/templates/erb/scaffold/new.html.erb.tt +3 -0
  28. data/lib/templates/erb/scaffold/partial.html.erb.tt +17 -0
  29. data/lib/templates/erb/scaffold/show.html.erb.tt +56 -0
  30. data/lib/voidable/hotwire/engine.rb +5 -0
  31. data/lib/voidable/hotwire/version.rb +1 -1
  32. metadata +29 -1
@@ -0,0 +1,361 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ html {
4
+ font-family: var(--void-font-sans);
5
+ color: var(--void-color-text);
6
+ background: var(--void-color-bg);
7
+ }
8
+
9
+ body {
10
+ min-height: 100dvh;
11
+ line-height: var(--void-leading-normal);
12
+ display: flex;
13
+ flex-direction: column;
14
+ }
15
+
16
+ a { color: inherit; text-decoration: none; }
17
+
18
+ /* Nav bar */
19
+ .app-nav {
20
+ position: sticky;
21
+ top: 0;
22
+ z-index: 100;
23
+ background: color-mix(in srgb, var(--void-color-bg) 88%, transparent);
24
+ backdrop-filter: saturate(140%) blur(6px);
25
+ -webkit-backdrop-filter: saturate(140%) blur(6px);
26
+ border-bottom: 1px solid var(--void-color-border);
27
+ flex-shrink: 0;
28
+ }
29
+
30
+ .app-nav-inner {
31
+ max-width: 72rem;
32
+ margin: 0 auto;
33
+ padding: 0 var(--void-space-6);
34
+ display: flex;
35
+ align-items: center;
36
+ gap: var(--void-space-6);
37
+ height: var(--void-space-14);
38
+ }
39
+
40
+ .app-nav-brand {
41
+ font-family: var(--void-font-mono);
42
+ font-weight: var(--void-weight-bold);
43
+ font-size: var(--void-text-base);
44
+ letter-spacing: var(--void-tracking-tightest);
45
+ color: var(--void-color-text);
46
+ }
47
+
48
+ .app-nav-links {
49
+ display: flex;
50
+ gap: var(--void-space-1);
51
+ list-style: none;
52
+ }
53
+
54
+ .app-nav-links a {
55
+ display: block;
56
+ padding: var(--void-space-1) var(--void-space-2);
57
+ font-size: var(--void-text-sm);
58
+ color: var(--void-color-text-secondary);
59
+ border-radius: var(--void-radius-sm);
60
+ transition: color var(--void-duration-fast);
61
+ }
62
+
63
+ .app-nav-links a:hover { color: var(--void-color-text); }
64
+
65
+ .app-nav-right {
66
+ margin-left: auto;
67
+ display: flex;
68
+ align-items: center;
69
+ gap: var(--void-space-3);
70
+ }
71
+
72
+ .app-nav-auth {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: var(--void-space-3);
76
+ font-size: var(--void-text-sm);
77
+ color: var(--void-color-text-secondary);
78
+ }
79
+
80
+ .app-nav-auth form { display: inline; }
81
+ .app-nav-auth a { color: var(--void-color-text-secondary); }
82
+ .app-nav-auth a:hover { color: var(--void-color-text); }
83
+
84
+ /* Settings popover */
85
+ .settings-btn {
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ width: 28px;
90
+ height: 28px;
91
+ padding: 0;
92
+ border: 1px solid var(--void-color-border);
93
+ border-radius: var(--void-radius-sm);
94
+ background: transparent;
95
+ color: var(--void-color-text-muted);
96
+ cursor: pointer;
97
+ flex-shrink: 0;
98
+ transition: color var(--void-duration-fast) var(--void-ease-in-out),
99
+ background var(--void-duration-fast) var(--void-ease-in-out);
100
+ }
101
+
102
+ .settings-btn:hover {
103
+ color: var(--void-color-text);
104
+ background: var(--void-color-bg-hover);
105
+ }
106
+
107
+ .settings-btn i {
108
+ font-size: 14px;
109
+ }
110
+
111
+ .app-nav-right void-popover .void-popover-body {
112
+ left: auto;
113
+ right: 0;
114
+ }
115
+
116
+ .settings-menu {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: var(--void-space-1);
120
+ min-width: 160px;
121
+ }
122
+
123
+ .settings-menu-item {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: var(--void-space-2);
127
+ padding: var(--void-space-2);
128
+ border: none;
129
+ border-radius: var(--void-radius-sm);
130
+ background: transparent;
131
+ font-family: var(--void-font-sans);
132
+ font-size: var(--void-text-sm);
133
+ color: var(--void-color-text);
134
+ cursor: pointer;
135
+ transition: background var(--void-duration-fast) var(--void-ease-in-out);
136
+ }
137
+
138
+ .settings-menu-item:hover {
139
+ background: var(--void-color-bg-hover);
140
+ }
141
+
142
+ .settings-menu-item void-switch {
143
+ margin-left: auto;
144
+ }
145
+
146
+ .settings-menu-item i {
147
+ font-size: 16px;
148
+ color: var(--void-color-text-secondary);
149
+ }
150
+
151
+ .settings-menu-item:has(.settings-menu-action) {
152
+ padding: 0;
153
+ }
154
+
155
+ .settings-menu-action {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: var(--void-space-2);
159
+ width: 100%;
160
+ padding: var(--void-space-2);
161
+ border: none;
162
+ border-radius: var(--void-radius-sm);
163
+ background: transparent;
164
+ font-family: var(--void-font-sans);
165
+ font-size: var(--void-text-sm);
166
+ color: var(--void-color-text);
167
+ cursor: pointer;
168
+ }
169
+
170
+ .settings-menu-action i {
171
+ font-size: 16px;
172
+ color: var(--void-color-text-secondary);
173
+ }
174
+
175
+ .settings-menu-form {
176
+ display: contents;
177
+ }
178
+
179
+ /* Main content */
180
+ .app-main {
181
+ flex: 1;
182
+ max-width: 72rem;
183
+ width: 100%;
184
+ margin: 0 auto;
185
+ display: flex;
186
+ flex-direction: column;
187
+ }
188
+
189
+ .app-content-body {
190
+ flex: 1;
191
+ padding: var(--void-space-8) var(--void-space-6);
192
+ overflow-y: auto;
193
+ }
194
+
195
+ .app-content-body:has(.table-section) {
196
+ overflow: hidden;
197
+ }
198
+
199
+ /* Content header */
200
+ .app-content-header {
201
+ height: var(--void-space-14);
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: space-between;
205
+ padding: 0 var(--void-space-4);
206
+ border-bottom: 1px solid var(--void-color-border);
207
+ background: var(--void-color-bg-secondary);
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ .app-content-header-left {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: var(--void-space-3);
215
+ min-width: 0;
216
+ }
217
+
218
+ .app-content-header-right {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: var(--void-space-3);
222
+ }
223
+
224
+ .app-content-header-breadcrumb {
225
+ font-family: var(--void-font-mono);
226
+ font-size: var(--void-text-xs);
227
+ color: var(--void-color-text-muted);
228
+ }
229
+
230
+ .app-content-header-subtitle {
231
+ font-family: var(--void-font-sans);
232
+ font-size: var(--void-text-xs);
233
+ color: var(--void-color-text-muted);
234
+ }
235
+
236
+ .app-content-header-search { width: 220px; height: var(--void-space-8); }
237
+ .app-content-header-right void-button { height: var(--void-space-8); }
238
+
239
+ /* Flash messages */
240
+ .flash-messages {
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: var(--void-space-2);
244
+ margin-bottom: var(--void-space-4);
245
+ }
246
+
247
+ /* Scrollbar styling */
248
+ .app-content-body::-webkit-scrollbar,
249
+ .table-section void-table::-webkit-scrollbar {
250
+ width: 6px;
251
+ height: 6px;
252
+ }
253
+
254
+ .app-content-body::-webkit-scrollbar-track,
255
+ .table-section void-table::-webkit-scrollbar-track {
256
+ background: transparent;
257
+ }
258
+
259
+ .app-content-body::-webkit-scrollbar-thumb,
260
+ .table-section void-table::-webkit-scrollbar-thumb {
261
+ background: color-mix(in srgb, var(--void-color-text-muted) 30%, transparent);
262
+ border-radius: var(--void-radius-full);
263
+ }
264
+
265
+ .app-content-body::-webkit-scrollbar-thumb:hover,
266
+ .table-section void-table::-webkit-scrollbar-thumb:hover {
267
+ background: color-mix(in srgb, var(--void-color-text-muted) 60%, transparent);
268
+ }
269
+
270
+ .app-content-body,
271
+ .table-section void-table {
272
+ scrollbar-width: thin;
273
+ scrollbar-color: color-mix(in srgb, var(--void-color-text-muted) 30%, transparent) transparent;
274
+ }
275
+
276
+ /* Page components */
277
+ .page-header { margin-bottom: var(--void-space-5); }
278
+ .page-breadcrumb { font-family: var(--void-font-mono); font-size: var(--void-text-xs); color: var(--void-color-text-muted); margin: 0 0 var(--void-space-2) 0; }
279
+ .page-title { margin: 0; font-family: var(--void-font-sans); color: var(--void-color-text); }
280
+ .page-title-row { display: flex; justify-content: space-between; align-items: center; }
281
+ .page-subtitle { margin: var(--void-space-1) 0 0 0; font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text-secondary); }
282
+
283
+ /* Detail views */
284
+ .detail-card-body { padding: var(--void-space-5); }
285
+ .detail-section-label { font-family: var(--void-font-sans); font-size: var(--void-text-xs); font-weight: var(--void-weight-semibold); color: var(--void-color-text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 var(--void-space-3) 0; }
286
+ .detail-grid { display: grid; grid-template-columns: 10rem 1fr; gap: var(--void-space-3) var(--void-space-4); margin: 0; }
287
+ .detail-label { font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text-secondary); margin: 0; padding-top: 2px; }
288
+ .detail-value { font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0; }
289
+ .detail-value-mono { font-family: var(--void-font-mono); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0; }
290
+
291
+ /* Danger zone */
292
+ .danger-zone { margin-top: var(--void-space-5); padding: var(--void-space-4); border: 1px solid var(--void-color-border); border-radius: var(--void-radius-md); }
293
+ .danger-zone-description { font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0 0 var(--void-space-4) 0; }
294
+ .action-bar { display: flex; gap: var(--void-space-2); justify-content: flex-end; }
295
+
296
+ /* Forms */
297
+ .form-body { padding: var(--void-space-5); display: flex; flex-direction: column; gap: var(--void-space-4); }
298
+ .form-actions { display: flex; gap: var(--void-space-2); align-items: center; padding-top: var(--void-space-2); }
299
+ .form-errors { margin-bottom: var(--void-space-4); }
300
+ .form-error-list { margin: var(--void-space-2) 0 0 var(--void-space-4); padding: 0; }
301
+ .flash-alert { margin-bottom: var(--void-space-4); }
302
+
303
+ /* Table section */
304
+ .table-section {
305
+ flex: 1;
306
+ min-height: 0;
307
+ display: flex;
308
+ flex-direction: column;
309
+ border: 1px solid var(--void-color-border);
310
+ border-radius: var(--void-radius-lg);
311
+ overflow: hidden;
312
+ }
313
+
314
+ .table-section-header {
315
+ padding: var(--void-space-3) var(--void-space-5);
316
+ border-bottom: 1px solid var(--void-color-border);
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: space-between;
320
+ background: var(--void-color-bg-secondary);
321
+ flex-shrink: 0;
322
+ }
323
+
324
+ .table-section-title {
325
+ font-size: var(--void-text-sm);
326
+ font-weight: var(--void-weight-medium);
327
+ color: var(--void-color-text);
328
+ letter-spacing: var(--void-tracking-snug);
329
+ }
330
+
331
+ .table-section-count {
332
+ font-size: var(--void-text-xs);
333
+ color: var(--void-color-text-muted);
334
+ font-family: var(--void-font-mono);
335
+ }
336
+
337
+ .table-section void-table {
338
+ flex: 1;
339
+ overflow: auto;
340
+ min-height: 0;
341
+ }
342
+
343
+ .table-section void-table thead th {
344
+ position: sticky;
345
+ top: 0;
346
+ background: var(--void-color-bg);
347
+ z-index: 1;
348
+ }
349
+
350
+ .table-section .pagination-wrapper {
351
+ flex-shrink: 0;
352
+ margin-top: 0;
353
+ padding: var(--void-space-3) var(--void-space-5);
354
+ border-top: 1px solid var(--void-color-border);
355
+ }
356
+
357
+ /* Tables */
358
+ .table-cell-right { text-align: right; }
359
+ .table-empty { text-align: center; padding: var(--void-space-6); font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text-muted); }
360
+ .pagination-wrapper { display: flex; justify-content: center; margin-top: var(--void-space-4); }
361
+ .back-link-wrapper { margin-top: var(--void-space-4); }
@@ -0,0 +1,41 @@
1
+ // Password visibility toggle for void-action-input
2
+ document.addEventListener('void-action', (e) => {
3
+ const el = e.target;
4
+ if (el.tagName === 'VOID-ACTION-INPUT' && (el.type === 'password' || el.type === 'text')) {
5
+ const visible = el.type === 'text';
6
+ el.type = visible ? 'password' : 'text';
7
+ el.icon = visible ? 'eye' : 'eye-off';
8
+ }
9
+ });
10
+
11
+ // Theme toggle
12
+ (function() {
13
+ const html = document.documentElement;
14
+
15
+ function applyTheme() {
16
+ if (!localStorage.getItem('theme')) {
17
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
18
+ html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
19
+ } else {
20
+ html.setAttribute('data-theme', localStorage.getItem('theme'));
21
+ }
22
+ }
23
+
24
+ function syncToggle() {
25
+ const toggle = document.getElementById('theme-toggle');
26
+ if (toggle) {
27
+ toggle.toggleAttribute('checked', html.getAttribute('data-theme') === 'dark');
28
+ }
29
+ }
30
+
31
+ applyTheme();
32
+ syncToggle();
33
+ document.addEventListener('turbo:load', syncToggle);
34
+
35
+ document.addEventListener('void-change', (e) => {
36
+ if (e.target.id !== 'theme-toggle') return;
37
+ const next = e.detail.checked ? 'dark' : 'light';
38
+ html.setAttribute('data-theme', next);
39
+ localStorage.setItem('theme', next);
40
+ });
41
+ })();
@@ -0,0 +1,80 @@
1
+ <%%= form_with(model: <%= model_resource_name %>) do |form| %>
2
+ <%% if <%= singular_table_name %>.errors.any? %>
3
+ <void-alert color="error" style="margin-bottom: var(--void-space-4);">
4
+ <strong><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</strong>
5
+ <ul style="margin: var(--void-space-2) 0 0 var(--void-space-4); padding: 0;">
6
+ <%% <%= singular_table_name %>.errors.each do |error| %>
7
+ <li><%%= error.full_message %></li>
8
+ <%% end %>
9
+ </ul>
10
+ </void-alert>
11
+ <%% end %>
12
+
13
+ <void-card>
14
+ <div style="padding: var(--void-space-5); display: flex; flex-direction: column; gap: var(--void-space-4);">
15
+ <% attributes.each do |attribute| -%>
16
+ <% if attribute.password_digest? -%>
17
+ <void-field label="Password" required>
18
+ <void-input type="password" name="<%= singular_table_name %>[password]" autocomplete="new-password"></void-input>
19
+ </void-field>
20
+
21
+ <void-field label="Password confirmation" required>
22
+ <void-input type="password" name="<%= singular_table_name %>[password_confirmation]" autocomplete="new-password"></void-input>
23
+ </void-field>
24
+ <% elsif attribute.attachments? -%>
25
+ <void-field label="<%= attribute.human_name %>">
26
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true %>
27
+ </void-field>
28
+ <% elsif attribute.attachment? -%>
29
+ <void-field label="<%= attribute.human_name %>">
30
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
31
+ </void-field>
32
+ <% elsif attribute.type == :text -%>
33
+ <void-field label="<%= attribute.human_name %>">
34
+ <void-textarea name="<%= singular_table_name %>[<%= attribute.column_name %>]"><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></void-textarea>
35
+ </void-field>
36
+ <% elsif attribute.type == :integer -%>
37
+ <void-field label="<%= attribute.human_name %>">
38
+ <void-number-input name="<%= singular_table_name %>[<%= attribute.column_name %>]" value="<%%= <%= singular_table_name %>.<%= attribute.column_name %> %>"></void-number-input>
39
+ </void-field>
40
+ <% elsif attribute.type == :float || attribute.type == :decimal -%>
41
+ <void-field label="<%= attribute.human_name %>">
42
+ <void-number-input name="<%= singular_table_name %>[<%= attribute.column_name %>]" value="<%%= <%= singular_table_name %>.<%= attribute.column_name %> %>" step="any"></void-number-input>
43
+ </void-field>
44
+ <% elsif attribute.type == :boolean -%>
45
+ <void-field label="<%= attribute.human_name %>">
46
+ <void-switch name="<%= singular_table_name %>[<%= attribute.column_name %>]" <%%= "checked" if <%= singular_table_name %>.<%= attribute.column_name %> %>></void-switch>
47
+ </void-field>
48
+ <% elsif attribute.type == :date -%>
49
+ <void-field label="<%= attribute.human_name %>">
50
+ <void-date-picker name="<%= singular_table_name %>[<%= attribute.column_name %>]" value="<%%= <%= singular_table_name %>.<%= attribute.column_name %> %>"></void-date-picker>
51
+ </void-field>
52
+ <% elsif attribute.type == :datetime -%>
53
+ <void-field label="<%= attribute.human_name %>">
54
+ <void-input type="datetime-local" name="<%= singular_table_name %>[<%= attribute.column_name %>]" value="<%%= <%= singular_table_name %>.<%= attribute.column_name %>&.strftime('%Y-%m-%dT%H:%M') %>"></void-input>
55
+ </void-field>
56
+ <% elsif attribute.type == :time -%>
57
+ <void-field label="<%= attribute.human_name %>">
58
+ <void-input type="time" name="<%= singular_table_name %>[<%= attribute.column_name %>]" value="<%%= <%= singular_table_name %>.<%= attribute.column_name %>&.strftime('%H:%M') %>"></void-input>
59
+ </void-field>
60
+ <% elsif attribute.reference? -%>
61
+ <void-field label="<%= attribute.human_name %>">
62
+ <void-select name="<%= singular_table_name %>[<%= attribute.column_name %>]">
63
+ <option value="">Select...</option>
64
+ <%%= options_from_collection_for_select(<%= attribute.column_name.chomp("_id").classify %>.all, :id, :name, <%= singular_table_name %>.<%= attribute.column_name %>) %>
65
+ </void-select>
66
+ </void-field>
67
+ <% else -%>
68
+ <void-field label="<%= attribute.human_name %>">
69
+ <void-input name="<%= singular_table_name %>[<%= attribute.column_name %>]" value="<%%= <%= singular_table_name %>.<%= attribute.column_name %> %>"></void-input>
70
+ </void-field>
71
+ <% end -%>
72
+ <% end -%>
73
+
74
+ <div style="display: flex; gap: var(--void-space-2); align-items: center; padding-top: var(--void-space-2);">
75
+ <void-button type="submit" color="success"><%%= <%= singular_table_name %>.persisted? ? "Update <%= human_name.downcase %>" : "Create <%= human_name.downcase %>" %></void-button>
76
+ <void-button variant="ghost"><a href="<%%= <%= index_helper(type: :path) %> %>">Back</a></void-button>
77
+ </div>
78
+ </div>
79
+ </void-card>
80
+ <%% end %>
@@ -0,0 +1,18 @@
1
+ <%% content_for :title, "Editing <%= human_name.downcase %>" %>
2
+
3
+ <%%= render "form", <%= singular_table_name %>: @<%= singular_table_name %> %>
4
+
5
+ <div style="margin-top: var(--void-space-5); padding: var(--void-space-4); border: 1px solid var(--void-color-border); border-radius: var(--void-radius-md);">
6
+ <p style="font-family: var(--void-font-sans); font-size: var(--void-text-xs); font-weight: var(--void-weight-semibold); color: var(--void-color-text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 var(--void-space-3) 0;">Danger zone</p>
7
+ <void-button variant="outline" color="error" size="sm" onclick="document.getElementById('delete-dialog').open = true">Delete this <%= human_name.downcase %></void-button>
8
+ </div>
9
+
10
+ <void-dialog id="delete-dialog" heading="Delete <%= human_name.downcase %>" size="sm">
11
+ <p style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0 0 var(--void-space-4) 0;">
12
+ Are you sure you want to delete <strong><%%= @<%= singular_table_name %>.<%= display_column %> %></strong>? This action cannot be undone.
13
+ </p>
14
+ <div style="display: flex; gap: var(--void-space-2); justify-content: flex-end;">
15
+ <void-button variant="ghost" size="sm" onclick="document.getElementById('delete-dialog').open = false">Cancel</void-button>
16
+ <void-button color="error" size="sm"><a href="<%%= url_for(<%= model_resource_name(prefix: "@") %>) %>" data-turbo-method="delete">Delete</a></void-button>
17
+ </div>
18
+ </void-dialog>
@@ -0,0 +1,65 @@
1
+ <%% content_for :title, "<%= human_name.pluralize %>" %>
2
+ <%% content_for :subtitle, "Manage your <%= human_name.pluralize.downcase %>" %>
3
+ <%% content_for :header_actions do %>
4
+ <void-button color="success" size="sm"><a href="<%%= <%= new_helper(type: :path) %> %>">New <%= human_name.downcase %></a></void-button>
5
+ <%% end %>
6
+
7
+ <%% if notice.present? %>
8
+ <void-alert color="success" dismissible class="flash-alert"><%%= notice %></void-alert>
9
+ <%% end %>
10
+
11
+ <div class="table-section">
12
+ <div class="table-section-header">
13
+ <span class="table-section-title">All <%= human_name.pluralize %></span>
14
+ <span class="table-section-count"><%%= @<%= plural_table_name %>.size %> <%= human_name.pluralize.downcase %></span>
15
+ </div>
16
+
17
+ <void-table hoverable>
18
+ <table>
19
+ <thead>
20
+ <tr>
21
+ <% attributes.reject(&:password_digest?).each do |attribute| -%>
22
+ <th><%= attribute.human_name %></th>
23
+ <% end -%>
24
+ <th></th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <%% if @<%= plural_table_name %>.any? %>
29
+ <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
30
+ <tr>
31
+ <% attributes.reject(&:password_digest?).each do |attribute| -%>
32
+ <% if attribute.type == :boolean -%>
33
+ <td>
34
+ <void-badge color="<%%= <%= singular_table_name %>.<%= attribute.column_name %> ? 'success' : 'default' %>" variant="outline"><%%= <%= singular_table_name %>.<%= attribute.column_name %> ? 'Yes' : 'No' %></void-badge>
35
+ </td>
36
+ <% elsif attribute.attachment? -%>
37
+ <td><%%= link_to <%= singular_table_name %>.<%= attribute.column_name %>.filename, <%= singular_table_name %>.<%= attribute.column_name %> if <%= singular_table_name %>.<%= attribute.column_name %>.attached? %></td>
38
+ <% elsif attribute.attachments? -%>
39
+ <td><%%= <%= singular_table_name %>.<%= attribute.column_name %>.count %> file(s)</td>
40
+ <% else -%>
41
+ <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td>
42
+ <% end -%>
43
+ <% end -%>
44
+ <td class="table-cell-right">
45
+ <void-dropdown-menu position="bottom-end">
46
+ <void-button variant="ghost" size="sm">&#x22EF;</void-button>
47
+ <void-dropdown-menu-item><a href="<%%= url_for(<%= model_resource_name(singular_table_name) %>) %>">Show</a></void-dropdown-menu-item>
48
+ <void-dropdown-menu-item><a href="<%%= <%= edit_helper(singular_table_name, type: :path) %> %>">Edit</a></void-dropdown-menu-item>
49
+ <hr>
50
+ <void-dropdown-menu-item destructive><a href="<%%= url_for(<%= model_resource_name(singular_table_name) %>) %>" data-turbo-method="delete" data-turbo-confirm="Are you sure?">Delete</a></void-dropdown-menu-item>
51
+ </void-dropdown-menu>
52
+ </td>
53
+ </tr>
54
+ <%% end %>
55
+ <%% else %>
56
+ <tr>
57
+ <td colspan="<%= attributes.reject(&:password_digest?).size + 1 %>" class="table-empty">
58
+ No <%= human_name.pluralize.downcase %> found. Create your first one to get started.
59
+ </td>
60
+ </tr>
61
+ <%% end %>
62
+ </tbody>
63
+ </table>
64
+ </void-table>
65
+ </div>
@@ -0,0 +1,3 @@
1
+ <%% content_for :title, "New <%= human_name.downcase %>" %>
2
+
3
+ <%%= render "form", <%= singular_table_name %>: @<%= singular_table_name %> %>
@@ -0,0 +1,17 @@
1
+ <div id="<%%= dom_id <%= singular_name %> %>">
2
+ <% attributes.reject(&:password_digest?).each do |attribute| -%>
3
+ <div>
4
+ <strong><%= attribute.human_name %>:</strong>
5
+ <% if attribute.attachment? -%>
6
+ <%%= link_to <%= singular_name %>.<%= attribute.column_name %>.filename, <%= singular_name %>.<%= attribute.column_name %> if <%= singular_name %>.<%= attribute.column_name %>.attached? %>
7
+ <% elsif attribute.attachments? -%>
8
+ <%% <%= singular_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %>
9
+ <div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div>
10
+ <%% end %>
11
+ <% else -%>
12
+ <%%= <%= singular_name %>.<%= attribute.column_name %> %>
13
+ <% end -%>
14
+ </div>
15
+
16
+ <% end -%>
17
+ </div>
@@ -0,0 +1,56 @@
1
+ <% display_column = attributes.reject(&:password_digest?).first&.column_name || 'id' -%>
2
+ <%% content_for :title, @<%= singular_table_name %>.<%= display_column %>.to_s %>
3
+ <%% content_for :header_actions do %>
4
+ <void-dropdown-menu position="bottom-end">
5
+ <void-button color="caution" variant="outline" size="sm">Actions</void-button>
6
+ <void-dropdown-menu-item><a href="<%%= <%= edit_helper(type: :path) %> %>">Edit</a></void-dropdown-menu-item>
7
+ <hr>
8
+ <void-dropdown-menu-item destructive onclick="document.getElementById('delete-dialog').open = true">Delete</void-dropdown-menu-item>
9
+ </void-dropdown-menu>
10
+ <%% end %>
11
+
12
+ <%% if notice.present? %>
13
+ <void-alert color="success" dismissible style="margin-bottom: var(--void-space-4);"><%%= notice %></void-alert>
14
+ <%% end %>
15
+
16
+ <void-card>
17
+ <div style="padding: var(--void-space-5);">
18
+ <p style="font-family: var(--void-font-sans); font-size: var(--void-text-xs); font-weight: var(--void-weight-semibold); color: var(--void-color-text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 var(--void-space-3) 0;">Details</p>
19
+ <dl style="display: grid; grid-template-columns: 10rem 1fr; gap: var(--void-space-3) var(--void-space-4); margin: 0;">
20
+ <% attributes.reject(&:password_digest?).each do |attribute| -%>
21
+ <dt style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text-secondary); margin: 0; padding-top: 2px;"><%= attribute.human_name %></dt>
22
+ <% if attribute.type == :boolean -%>
23
+ <dd style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0;">
24
+ <void-badge color="<%%= @<%= singular_table_name %>.<%= attribute.column_name %> ? 'success' : 'default' %>" variant="outline"><%%= @<%= singular_table_name %>.<%= attribute.column_name %> ? 'Yes' : 'No' %></void-badge>
25
+ </dd>
26
+ <% elsif [:integer, :float, :decimal].include?(attribute.type) -%>
27
+ <dd style="font-family: var(--void-font-mono); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0;"><%%= @<%= singular_table_name %>.<%= attribute.column_name %> %></dd>
28
+ <% elsif attribute.attachment? -%>
29
+ <dd style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0;"><%%= link_to @<%= singular_table_name %>.<%= attribute.column_name %>.filename, @<%= singular_table_name %>.<%= attribute.column_name %> if @<%= singular_table_name %>.<%= attribute.column_name %>.attached? %></dd>
30
+ <% elsif attribute.attachments? -%>
31
+ <dd style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0;">
32
+ <%% @<%= singular_table_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %>
33
+ <div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div>
34
+ <%% end %>
35
+ </dd>
36
+ <% else -%>
37
+ <dd style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0;"><%%= @<%= singular_table_name %>.<%= attribute.column_name %> %></dd>
38
+ <% end -%>
39
+ <% end -%>
40
+ </dl>
41
+ </div>
42
+ </void-card>
43
+
44
+ <div style="margin-top: var(--void-space-4);">
45
+ <void-button variant="ghost" size="sm"><a href="<%%= <%= index_helper(type: :path) %> %>">&#x2190; Back to <%= human_name.pluralize.downcase %></a></void-button>
46
+ </div>
47
+
48
+ <void-dialog id="delete-dialog" heading="Delete <%= human_name.downcase %>" size="sm">
49
+ <p style="font-family: var(--void-font-sans); font-size: var(--void-text-sm); color: var(--void-color-text); margin: 0 0 var(--void-space-4) 0;">
50
+ Are you sure you want to delete this <%= human_name.downcase %>? This action cannot be undone.
51
+ </p>
52
+ <div style="display: flex; gap: var(--void-space-2); justify-content: flex-end;">
53
+ <void-button variant="ghost" size="sm" onclick="document.getElementById('delete-dialog').open = false">Cancel</void-button>
54
+ <void-button color="error" size="sm"><a href="<%%= url_for(<%= model_resource_name(prefix: "@") %>) %>" data-turbo-method="delete">Delete</a></void-button>
55
+ </div>
56
+ </void-dialog>
@@ -14,6 +14,11 @@ module Voidable
14
14
  include Voidable::Hotwire::Helpers
15
15
  end
16
16
  end
17
+
18
+ initializer "voidable-hotwire.generator_templates" do |app|
19
+ templates_dir = root.join("lib/templates").to_s
20
+ app.config.generators.templates.unshift(templates_dir) if File.directory?(templates_dir)
21
+ end
17
22
  end
18
23
  end
19
24
  end
@@ -1,5 +1,5 @@
1
1
  module Voidable
2
2
  module Hotwire
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end