active_storage_dashboard 0.1.4 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5aed0ae00966171b7c5708ae0231f2c4f84ca19ad7a22741e270f93ccf2502a7
4
- data.tar.gz: 95a0b2b47e44500724894f746364cf56d462b93a5170e4ec98abdc698b428878
3
+ metadata.gz: fc1b8fcb08322e17d53b4fa3e02ab6fa19a29db399e15ea3bd1f7374166c2ab8
4
+ data.tar.gz: b4d3e09639be0a9ccb241eb6a27e96647140c737f7142af24aee106ddfe37221
5
5
  SHA512:
6
- metadata.gz: 4b1e3afdab7ea4b241b87e2378aa01cee383449f817cc968cae35f5a0a30b1082656a78d0ee12b03c956af8fc4ca986e49e57009840893f881eada483794bf41
7
- data.tar.gz: 529016b918bd05c2eebea22372fd7affec5f784d21524d2d2df00b0bec6e8b21d5522c9c7caa40a06b38e36d9b5d5966ed24b871a156f861ebdef029a872a3df
6
+ metadata.gz: 0bcae21179ff33000e2fad468d3f29115c0405cfd8fe85a109346849b46bb7bda63d564029e419aa05d1de6ad17f0cc01e2a4da182ec9528d5a186e3e4262c94
7
+ data.tar.gz: ed485df1b97a0b6b309ec0efc2328c60f7a498471dcc547450567295acf7da268dba103a552434ddd673532351912834f4f5af39720a787866afee8bfd273985
@@ -3,8 +3,20 @@
3
3
  module ActiveStorageDashboard
4
4
  class AttachmentsController < ApplicationController
5
5
  def index
6
- @attachments = paginate(ActiveStorage::Attachment.order(created_at: :desc))
7
- @total_count = ActiveStorage::Attachment.count
6
+ @attachments = ActiveStorage::Attachment.order(created_at: :desc)
7
+
8
+ # Get record types for filter dropdown
9
+ @record_types = ActiveStorage::Attachment.distinct.pluck(:record_type).compact.sort
10
+
11
+ # Get content types for filter dropdown
12
+ @content_types = ActiveStorage::Blob.joins(:attachments).distinct.pluck(:content_type).compact.sort
13
+
14
+ # Apply filters
15
+ apply_filters
16
+
17
+ # Pagination after filters
18
+ @total_count = @attachments.count
19
+ @attachments = paginate(@attachments)
8
20
  end
9
21
 
10
22
  def show
@@ -23,5 +35,24 @@ module ActiveStorageDashboard
23
35
  redirect_to download_blob_path(@blob)
24
36
  end
25
37
  end
38
+
39
+ private
40
+
41
+ def apply_filters
42
+ # Filter by attachment name
43
+ if params[:name].present?
44
+ @attachments = @attachments.where('name LIKE ?', "%#{params[:name]}%")
45
+ end
46
+
47
+ # Filter by record type
48
+ if params[:record_type].present?
49
+ @attachments = @attachments.where(record_type: params[:record_type])
50
+ end
51
+
52
+ # Filter by content type
53
+ if params[:content_type].present?
54
+ @attachments = @attachments.joins(:blob).where(active_storage_blobs: { content_type: params[:content_type] })
55
+ end
56
+ end
26
57
  end
27
58
  end
@@ -3,8 +3,17 @@
3
3
  module ActiveStorageDashboard
4
4
  class BlobsController < ApplicationController
5
5
  def index
6
- @blobs = paginate(ActiveStorage::Blob.order(created_at: :desc))
7
- @total_count = ActiveStorage::Blob.count
6
+ @blobs = ActiveStorage::Blob.order(created_at: :desc)
7
+
8
+ # Get content types for filter dropdown
9
+ @content_types = ActiveStorage::Blob.distinct.pluck(:content_type).compact.sort
10
+
11
+ # Apply filters
12
+ apply_filters
13
+
14
+ # Pagination after filters
15
+ @total_count = @blobs.count
16
+ @blobs = paginate(@blobs)
8
17
  end
9
18
 
10
19
  def show
@@ -51,5 +60,36 @@ module ActiveStorageDashboard
51
60
  redirect_to main_app.rails_blob_path(@blob, disposition_param)
52
61
  end
53
62
  end
63
+
64
+ private
65
+
66
+ def apply_filters
67
+ # Filter by content type
68
+ if params[:content_type].present?
69
+ @blobs = @blobs.where(content_type: params[:content_type])
70
+ end
71
+
72
+ # Filter by size
73
+ if params[:size].present?
74
+ case params[:size]
75
+ when 'small'
76
+ @blobs = @blobs.where('byte_size < ?', 1.megabyte)
77
+ when 'medium'
78
+ @blobs = @blobs.where('byte_size >= ? AND byte_size <= ?', 1.megabyte, 10.megabytes)
79
+ when 'large'
80
+ @blobs = @blobs.where('byte_size > ?', 10.megabytes)
81
+ end
82
+ end
83
+
84
+ # Filter by status
85
+ if params[:status].present?
86
+ case params[:status]
87
+ when 'purgable'
88
+ @blobs = @blobs.left_outer_joins(:attachments).where(active_storage_attachments: { id: nil })
89
+ when 'attached'
90
+ @blobs = @blobs.joins(:attachments).distinct
91
+ end
92
+ end
93
+ end
54
94
  end
55
95
  end
@@ -11,6 +11,9 @@ module ActiveStorageDashboard
11
11
  @content_types = ActiveStorage::Blob.group(:content_type).count.sort_by { |_, count| -count }.first(5)
12
12
 
13
13
  @recent_blobs = ActiveStorage::Blob.order(created_at: :desc).limit(5)
14
+
15
+ @purgable_blobs = ActiveStorage::Blob.left_outer_joins(:attachments)
16
+ .where(active_storage_attachments: { id: nil })
14
17
 
15
18
  # Find largest blob
16
19
  @largest_blob = ActiveStorage::Blob.order(byte_size: :desc).first
@@ -1,5 +1,60 @@
1
1
  <h1 class="page-title">Active Storage Attachments</h1>
2
2
 
3
+ <div class="filter-panel">
4
+ <div class="filter-header">
5
+ <h3 class="filter-title">Filter Attachments</h3>
6
+ <button id="toggle-filters" class="filter-toggle">
7
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="filter-icon">
8
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
9
+ </svg>
10
+ </button>
11
+ </div>
12
+
13
+ <form action="<%= attachments_path %>" method="get" class="filters-form" id="filters-form">
14
+ <div class="filter-row">
15
+ <div class="filter-group">
16
+ <label for="filter_name">Attachment Name:</label>
17
+ <input type="text" name="name" id="filter_name" class="filter-input" value="<%= params[:name] %>" placeholder="Search by name">
18
+ </div>
19
+
20
+ <div class="filter-group">
21
+ <label for="filter_record_type">Record Type:</label>
22
+ <select name="record_type" id="filter_record_type" class="filter-select">
23
+ <option value="">All Record Types</option>
24
+ <% @record_types&.each do |record_type| %>
25
+ <option value="<%= record_type %>" <%= params[:record_type] == record_type ? 'selected' : '' %>><%= record_type %></option>
26
+ <% end %>
27
+ </select>
28
+ </div>
29
+
30
+ <div class="filter-group">
31
+ <label for="filter_content_type">Content Type:</label>
32
+ <select name="content_type" id="filter_content_type" class="filter-select">
33
+ <option value="">All Content Types</option>
34
+ <% @content_types&.each do |content_type| %>
35
+ <option value="<%= content_type %>" <%= params[:content_type] == content_type ? 'selected' : '' %>><%= content_type || 'Unknown' %></option>
36
+ <% end %>
37
+ </select>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="filter-actions">
42
+ <button type="submit" class="btn btn-primary">
43
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="action-icon">
44
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25" />
45
+ </svg>
46
+ Apply Filters
47
+ </button>
48
+ <a href="<%= attachments_path %>" class="btn btn-outline">
49
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="action-icon">
50
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
51
+ </svg>
52
+ Clear
53
+ </a>
54
+ </div>
55
+ </form>
56
+ </div>
57
+
3
58
  <div class="view-options">
4
59
  <button id="table-view-btn" class="view-toggle-btn active">Table View</button>
5
60
  <button id="card-view-btn" class="view-toggle-btn">Card View</button>
@@ -63,7 +118,7 @@
63
118
  <% end %>
64
119
  </div>
65
120
  <div class="attachment-info">
66
- <h3 class="filename" title="<%= attachment.blob.filename %>"><%= truncate(attachment.blob.filename.to_s, length: 100) %></h3>
121
+ <h3 class="filename" title="<%= attachment.blob.filename %>"><%= attachment.blob.filename %></h3>
67
122
  <p class="attachment-name"><strong><%= attachment.name %></strong></p>
68
123
  <p class="record-info"><%= attachment.record_type %> #<%= attachment.record_id %></p>
69
124
  <p class="file-info">
@@ -122,6 +177,7 @@
122
177
  background-color: white;
123
178
  display: flex;
124
179
  flex-direction: column;
180
+ width: 100%;
125
181
  }
126
182
 
127
183
  .attachment-preview {
@@ -171,11 +227,14 @@
171
227
  .attachment-info {
172
228
  padding: 16px;
173
229
  flex: 1;
230
+ min-width: 0;
231
+ width: 100%;
174
232
  }
175
233
 
176
234
  .attachment-info h3 {
177
235
  margin: 0 0 8px 0;
178
236
  font-size: 1.1rem;
237
+ width: 100%;
179
238
  }
180
239
 
181
240
  .attachment-info h3.filename {
@@ -184,12 +243,15 @@
184
243
  white-space: nowrap;
185
244
  overflow: hidden;
186
245
  text-overflow: ellipsis;
246
+ width: 100%;
247
+ display: block;
187
248
  }
188
249
 
189
250
  .attachment-info p {
190
251
  margin: 0 0 8px 0;
191
252
  font-size: 0.9rem;
192
253
  color: #6c757d;
254
+ width: 100%;
193
255
  }
194
256
 
195
257
  .attachment-name {
@@ -229,6 +291,129 @@
229
291
  .card-actions {
230
292
  margin-top: 8px;
231
293
  }
294
+
295
+ .filename-truncate {
296
+ white-space: nowrap;
297
+ overflow: hidden;
298
+ text-overflow: ellipsis;
299
+ max-width: 100%;
300
+ display: block;
301
+ }
302
+
303
+ /* Enhanced Filter Panel */
304
+ .filter-panel {
305
+ background-color: var(--card-bg);
306
+ padding: 0;
307
+ border-radius: var(--border-radius);
308
+ margin-bottom: 1.5rem;
309
+ box-shadow: var(--box-shadow);
310
+ overflow: hidden;
311
+ border: 1px solid rgba(0,0,0,0.05);
312
+ }
313
+
314
+ .filter-header {
315
+ display: flex;
316
+ justify-content: space-between;
317
+ align-items: center;
318
+ padding: 0.75rem 1.25rem;
319
+ background-color: rgba(0,0,0,0.02);
320
+ border-bottom: 1px solid rgba(0,0,0,0.05);
321
+ }
322
+
323
+ .filter-title {
324
+ margin: 0;
325
+ font-size: 1.1rem;
326
+ font-weight: 600;
327
+ color: var(--dark-color);
328
+ }
329
+
330
+ .filter-toggle {
331
+ background: transparent;
332
+ border: none;
333
+ cursor: pointer;
334
+ display: flex;
335
+ align-items: center;
336
+ color: var(--secondary-color);
337
+ padding: 0.25rem;
338
+ }
339
+
340
+ .filter-toggle:hover {
341
+ color: var(--primary-color);
342
+ }
343
+
344
+ .filter-icon, .action-icon {
345
+ width: 1.25rem;
346
+ height: 1.25rem;
347
+ margin-right: 0.25rem;
348
+ }
349
+
350
+ .filters-form {
351
+ padding: 1.25rem;
352
+ }
353
+
354
+ .filter-row {
355
+ display: flex;
356
+ flex-wrap: wrap;
357
+ gap: 1.25rem;
358
+ margin-bottom: 1.25rem;
359
+ }
360
+
361
+ .filter-group {
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 0.5rem;
365
+ flex: 1;
366
+ min-width: 180px;
367
+ }
368
+
369
+ .filter-group label {
370
+ font-weight: 500;
371
+ font-size: 0.875rem;
372
+ color: var(--dark-color);
373
+ }
374
+
375
+ .filter-select, .filter-input {
376
+ padding: 0.5rem 0.75rem;
377
+ border: 1px solid rgba(0,0,0,0.1);
378
+ border-radius: 0.375rem;
379
+ font-size: 0.875rem;
380
+ background-color: white;
381
+ transition: border-color 0.2s, box-shadow 0.2s;
382
+ }
383
+
384
+ .filter-select:focus, .filter-input:focus {
385
+ border-color: var(--primary-color);
386
+ box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.25);
387
+ outline: none;
388
+ }
389
+
390
+ .filter-actions {
391
+ display: flex;
392
+ gap: 0.75rem;
393
+ justify-content: flex-end;
394
+ }
395
+
396
+ .btn-primary {
397
+ background-color: var(--primary-color);
398
+ display: inline-flex;
399
+ align-items: center;
400
+ }
401
+
402
+ @media (max-width: 768px) {
403
+ .filter-row {
404
+ flex-direction: column;
405
+ }
406
+
407
+ .filter-actions {
408
+ flex-direction: column;
409
+ width: 100%;
410
+ }
411
+
412
+ .filter-actions .btn {
413
+ width: 100%;
414
+ justify-content: center;
415
+ }
416
+ }
232
417
  </style>
233
418
 
234
419
  <script>
@@ -265,5 +450,27 @@
265
450
  cardViewBtn.classList.add('active');
266
451
  localStorage.setItem('attachmentsViewMode', 'card');
267
452
  }
453
+
454
+ // Toggle filters visibility
455
+ const toggleFiltersBtn = document.getElementById('toggle-filters');
456
+ const filtersForm = document.getElementById('filters-form');
457
+
458
+ // Check localStorage for saved state
459
+ const filtersVisible = localStorage.getItem('attachmentFiltersVisible') !== 'false';
460
+
461
+ // Set initial state
462
+ if (!filtersVisible) {
463
+ filtersForm.style.display = 'none';
464
+ }
465
+
466
+ toggleFiltersBtn.addEventListener('click', function() {
467
+ if (filtersForm.style.display === 'none') {
468
+ filtersForm.style.display = 'block';
469
+ localStorage.setItem('attachmentFiltersVisible', 'true');
470
+ } else {
471
+ filtersForm.style.display = 'none';
472
+ localStorage.setItem('attachmentFiltersVisible', 'false');
473
+ }
474
+ });
268
475
  });
269
476
  </script>
@@ -91,7 +91,7 @@
91
91
  </tr>
92
92
  <tr>
93
93
  <th>Filename</th>
94
- <td title="<%= @blob.filename %>"><%= truncate(@blob.filename.to_s, length: 100) %></td>
94
+ <td title="<%= @blob.filename %>" class="filename-truncate"><%= @blob.filename %></td>
95
95
  </tr>
96
96
  <tr>
97
97
  <th>Content Type</th>
@@ -160,4 +160,12 @@
160
160
  max-height: 600px;
161
161
  border-radius: 4px;
162
162
  }
163
+
164
+ .filename-truncate {
165
+ white-space: nowrap;
166
+ overflow: hidden;
167
+ text-overflow: ellipsis;
168
+ max-width: 100%;
169
+ display: inline-block;
170
+ }
163
171
  </style>
@@ -1,5 +1,64 @@
1
1
  <h1 class="page-title">Active Storage Blobs</h1>
2
2
 
3
+ <div class="filter-panel">
4
+ <div class="filter-header">
5
+ <h3 class="filter-title">Filter Blobs</h3>
6
+ <button id="toggle-filters" class="filter-toggle">
7
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="filter-icon">
8
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
9
+ </svg>
10
+ </button>
11
+ </div>
12
+
13
+ <form action="<%= blobs_path %>" method="get" class="filters-form" id="filters-form">
14
+ <div class="filter-row">
15
+ <div class="filter-group">
16
+ <label for="filter_type">Content Type:</label>
17
+ <select name="content_type" id="filter_type" class="filter-select">
18
+ <option value="">All Types</option>
19
+ <% @content_types&.each do |content_type| %>
20
+ <option value="<%= content_type %>" <%= params[:content_type] == content_type ? 'selected' : '' %>><%= content_type || 'Unknown' %></option>
21
+ <% end %>
22
+ </select>
23
+ </div>
24
+
25
+ <div class="filter-group">
26
+ <label for="filter_size">Size:</label>
27
+ <select name="size" id="filter_size" class="filter-select">
28
+ <option value="">Any Size</option>
29
+ <option value="small" <%= params[:size] == 'small' ? 'selected' : '' %>>Small (< 1MB)</option>
30
+ <option value="medium" <%= params[:size] == 'medium' ? 'selected' : '' %>>Medium (1MB - 10MB)</option>
31
+ <option value="large" <%= params[:size] == 'large' ? 'selected' : '' %>>Large (> 10MB)</option>
32
+ </select>
33
+ </div>
34
+
35
+ <div class="filter-group">
36
+ <label for="filter_status">Status:</label>
37
+ <select name="status" id="filter_status" class="filter-select">
38
+ <option value="">All Status</option>
39
+ <option value="purgable" <%= params[:status] == 'purgable' ? 'selected' : '' %>>Purgable Blobs</option>
40
+ <option value="attached" <%= params[:status] == 'attached' ? 'selected' : '' %>>Attached Blobs</option>
41
+ </select>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="filter-actions">
46
+ <button type="submit" class="btn btn-primary">
47
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="action-icon">
48
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25" />
49
+ </svg>
50
+ Apply Filters
51
+ </button>
52
+ <a href="<%= blobs_path %>" class="btn btn-outline">
53
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="action-icon">
54
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
55
+ </svg>
56
+ Clear
57
+ </a>
58
+ </div>
59
+ </form>
60
+ </div>
61
+
3
62
  <div class="view-options">
4
63
  <button id="table-view-btn" class="view-toggle-btn active">Table View</button>
5
64
  <button id="card-view-btn" class="view-toggle-btn">Card View</button>
@@ -24,7 +83,7 @@
24
83
  <% @blobs.each do |blob| %>
25
84
  <tr>
26
85
  <td><%= blob.id %></td>
27
- <td title="<%= blob.filename %>"><%= truncate(blob.filename.to_s, length: 100) %></td>
86
+ <td title="<%= blob.filename %>" class="filename-truncate"><%= truncate(blob.filename.to_s, length: 40) %></td>
28
87
  <td><%= blob.content_type || 'Unknown' %></td>
29
88
  <td><%= format_bytes(blob.byte_size) %></td>
30
89
  <td><%= blob.created_at.strftime('%Y-%m-%d %H:%M') %></td>
@@ -63,7 +122,7 @@
63
122
  <% end %>
64
123
  </div>
65
124
  <div class="blob-info">
66
- <h3 class="filename" title="<%= blob.filename %>"><%= truncate(blob.filename.to_s, length: 100) %></h3>
125
+ <h3 class="filename" title="<%= blob.filename %>"><%= blob.filename %></h3>
67
126
  <p class="record-info">ID: <%= blob.id %></p>
68
127
  <p class="file-info">
69
128
  <%= format_bytes(blob.byte_size) %> •
@@ -124,6 +183,7 @@
124
183
  background-color: white;
125
184
  display: flex;
126
185
  flex-direction: column;
186
+ width: 100%;
127
187
  }
128
188
 
129
189
  .blob-preview {
@@ -173,6 +233,8 @@
173
233
  .blob-info {
174
234
  padding: 16px;
175
235
  flex: 1;
236
+ min-width: 0;
237
+ width: 100%;
176
238
  }
177
239
 
178
240
  .blob-info h3.filename {
@@ -181,12 +243,15 @@
181
243
  white-space: nowrap;
182
244
  overflow: hidden;
183
245
  text-overflow: ellipsis;
246
+ width: 100%;
247
+ display: block;
184
248
  }
185
249
 
186
250
  .blob-info p {
187
251
  margin: 0 0 8px 0;
188
252
  font-size: 0.9rem;
189
253
  color: #6c757d;
254
+ width: 100%;
190
255
  }
191
256
 
192
257
  .view-details-btn {
@@ -222,6 +287,129 @@
222
287
  .card-actions {
223
288
  margin-top: 8px;
224
289
  }
290
+
291
+ .filename-truncate {
292
+ white-space: nowrap;
293
+ overflow: hidden;
294
+ text-overflow: ellipsis;
295
+ max-width: 100%;
296
+ display: block;
297
+ }
298
+
299
+ /* Enhanced Filter Panel */
300
+ .filter-panel {
301
+ background-color: var(--card-bg);
302
+ padding: 0;
303
+ border-radius: var(--border-radius);
304
+ margin-bottom: 1.5rem;
305
+ box-shadow: var(--box-shadow);
306
+ overflow: hidden;
307
+ border: 1px solid rgba(0,0,0,0.05);
308
+ }
309
+
310
+ .filter-header {
311
+ display: flex;
312
+ justify-content: space-between;
313
+ align-items: center;
314
+ padding: 0.75rem 1.25rem;
315
+ background-color: rgba(0,0,0,0.02);
316
+ border-bottom: 1px solid rgba(0,0,0,0.05);
317
+ }
318
+
319
+ .filter-title {
320
+ margin: 0;
321
+ font-size: 1.1rem;
322
+ font-weight: 600;
323
+ color: var(--dark-color);
324
+ }
325
+
326
+ .filter-toggle {
327
+ background: transparent;
328
+ border: none;
329
+ cursor: pointer;
330
+ display: flex;
331
+ align-items: center;
332
+ color: var(--secondary-color);
333
+ padding: 0.25rem;
334
+ }
335
+
336
+ .filter-toggle:hover {
337
+ color: var(--primary-color);
338
+ }
339
+
340
+ .filter-icon, .action-icon {
341
+ width: 1.25rem;
342
+ height: 1.25rem;
343
+ margin-right: 0.25rem;
344
+ }
345
+
346
+ .filters-form {
347
+ padding: 1.25rem;
348
+ }
349
+
350
+ .filter-row {
351
+ display: flex;
352
+ flex-wrap: wrap;
353
+ gap: 1.25rem;
354
+ margin-bottom: 1.25rem;
355
+ }
356
+
357
+ .filter-group {
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 0.5rem;
361
+ flex: 1;
362
+ min-width: 180px;
363
+ }
364
+
365
+ .filter-group label {
366
+ font-weight: 500;
367
+ font-size: 0.875rem;
368
+ color: var(--dark-color);
369
+ }
370
+
371
+ .filter-select, .filter-input {
372
+ padding: 0.5rem 0.75rem;
373
+ border: 1px solid rgba(0,0,0,0.1);
374
+ border-radius: 0.375rem;
375
+ font-size: 0.875rem;
376
+ background-color: white;
377
+ transition: border-color 0.2s, box-shadow 0.2s;
378
+ }
379
+
380
+ .filter-select:focus, .filter-input:focus {
381
+ border-color: var(--primary-color);
382
+ box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.25);
383
+ outline: none;
384
+ }
385
+
386
+ .filter-actions {
387
+ display: flex;
388
+ gap: 0.75rem;
389
+ justify-content: flex-end;
390
+ }
391
+
392
+ .btn-primary {
393
+ background-color: var(--primary-color);
394
+ display: inline-flex;
395
+ align-items: center;
396
+ }
397
+
398
+ @media (max-width: 768px) {
399
+ .filter-row {
400
+ flex-direction: column;
401
+ }
402
+
403
+ .filter-actions {
404
+ flex-direction: column;
405
+ width: 100%;
406
+ }
407
+
408
+ .filter-actions .btn {
409
+ width: 100%;
410
+ justify-content: center;
411
+ }
412
+ }
225
413
  </style>
226
414
 
227
415
  <script>
@@ -258,5 +446,27 @@
258
446
  cardViewBtn.classList.add('active');
259
447
  localStorage.setItem('blobsViewMode', 'card');
260
448
  }
449
+
450
+ // Toggle filters visibility
451
+ const toggleFiltersBtn = document.getElementById('toggle-filters');
452
+ const filtersForm = document.getElementById('filters-form');
453
+
454
+ // Check localStorage for saved state
455
+ const filtersVisible = localStorage.getItem('filtersVisible') !== 'false';
456
+
457
+ // Set initial state
458
+ if (!filtersVisible) {
459
+ filtersForm.style.display = 'none';
460
+ }
461
+
462
+ toggleFiltersBtn.addEventListener('click', function() {
463
+ if (filtersForm.style.display === 'none') {
464
+ filtersForm.style.display = 'block';
465
+ localStorage.setItem('filtersVisible', 'true');
466
+ } else {
467
+ filtersForm.style.display = 'none';
468
+ localStorage.setItem('filtersVisible', 'false');
469
+ }
470
+ });
261
471
  });
262
472
  </script>
@@ -56,7 +56,7 @@
56
56
  </tr>
57
57
  <tr>
58
58
  <th>Filename</th>
59
- <td title="<%= @blob.filename %>"><%= truncate(@blob.filename.to_s, length: 100) %></td>
59
+ <td title="<%= @blob.filename %>" class="filename-truncate"><%= @blob.filename %></td>
60
60
  </tr>
61
61
  <tr>
62
62
  <th>Content Type</th>
@@ -200,4 +200,12 @@
200
200
  max-height: 600px;
201
201
  border-radius: 4px;
202
202
  }
203
+
204
+ .filename-truncate {
205
+ white-space: nowrap;
206
+ overflow: hidden;
207
+ text-overflow: ellipsis;
208
+ max-width: 100%;
209
+ display: inline-block;
210
+ }
203
211
  </style>
@@ -35,6 +35,17 @@
35
35
  <div class="stat-card-value count-up" data-value="<%= @variant_records_count %>">0</div>
36
36
  <div class="stat-card-label">Variant Records</div>
37
37
  </div>
38
+
39
+ <div class="stat-card">
40
+ <div class="stat-card-icon purge-icon">
41
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
42
+ <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
43
+ </svg>
44
+
45
+ </div>
46
+ <div class="stat-card-value count-up" data-value="<%= @purgable_blobs.count %>">0</div>
47
+ <div class="stat-card-label">Purgable Blobs</div>
48
+ </div>
38
49
 
39
50
  <div class="stat-card">
40
51
  <div class="stat-card-icon storage-icon">
@@ -104,7 +115,7 @@
104
115
  <% end %>
105
116
  </div>
106
117
  <div class="activity-content">
107
- <div class="activity-title" title="<%= blob.filename %>"><%= truncate(blob.filename.to_s, length: 30) %></div>
118
+ <div class="activity-title filename-truncate" title="<%= blob.filename %>"><%= truncate(blob.filename.to_s, length: 40) %></div>
108
119
  <div class="activity-meta">
109
120
  <span class="activity-time"><%= time_ago_in_words(blob.created_at, locale: :en) %> ago</span>
110
121
  <span class="activity-size"><%= format_bytes(blob.byte_size) %></span>
@@ -179,7 +190,7 @@
179
190
  <% end %>
180
191
  </div>
181
192
  <div class="file-info">
182
- <h3 class="file-name" title="<%= blob.filename %>"><%= truncate(blob.filename.to_s, length: 30) %></h3>
193
+ <h3 class="file-name filename-truncate" title="<%= blob.filename %>"><%= blob.filename %></h3>
183
194
  <div class="file-meta">
184
195
  <span class="file-size"><%= format_bytes(blob.byte_size) %></span>
185
196
  <span class="file-date"><%= blob.created_at.strftime('%Y-%m-%d %H:%M') %></span>
@@ -299,6 +310,10 @@
299
310
  .storage-icon {
300
311
  background-color: var(--info-color);
301
312
  }
313
+
314
+ .purge-icon {
315
+ background-color: var(--danger-color);
316
+ }
302
317
 
303
318
  .stat-card-value {
304
319
  font-size: 2.5rem;
@@ -426,12 +441,15 @@
426
441
  .activity-timeline {
427
442
  display: flex;
428
443
  flex-direction: column;
444
+ width: 100%;
429
445
  }
430
446
 
431
447
  .activity-item {
432
448
  display: flex;
433
449
  gap: 1rem;
434
450
  padding: 0.75rem 0;
451
+ width: 100%;
452
+ max-width: 100%;
435
453
  }
436
454
 
437
455
  .activity-divider {
@@ -446,15 +464,27 @@
446
464
  }
447
465
 
448
466
  .activity-content {
449
- flex-grow: 1;
467
+ flex: 1;
468
+ min-width: 0; /* Critical for text truncation */
469
+ max-width: calc(100% - 60px); /* Account for icon width + gap */
470
+ width: 100%;
471
+ overflow: hidden;
450
472
  }
451
473
 
452
474
  .activity-title {
453
475
  font-weight: 500;
454
476
  margin-bottom: 0.25rem;
477
+ width: 100%;
478
+ max-width: 100%;
479
+ }
480
+
481
+ .filename-truncate {
455
482
  white-space: nowrap;
456
483
  overflow: hidden;
457
484
  text-overflow: ellipsis;
485
+ max-width: 100%;
486
+ display: block;
487
+ width: 100%;
458
488
  }
459
489
 
460
490
  .activity-meta {
@@ -463,11 +493,15 @@
463
493
  display: flex;
464
494
  gap: 1rem;
465
495
  margin-bottom: 0.5rem;
496
+ max-width: 100%;
497
+ flex-wrap: wrap;
466
498
  }
467
499
 
468
500
  .activity-actions {
469
501
  display: flex;
470
502
  gap: 0.5rem;
503
+ flex-wrap: wrap;
504
+ max-width: 100%;
471
505
  }
472
506
 
473
507
  /* Storage summary */
@@ -535,6 +569,7 @@
535
569
  display: flex;
536
570
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
537
571
  transition: transform 0.2s ease, box-shadow 0.2s ease;
572
+ width: 100%;
538
573
  }
539
574
 
540
575
  .recent-file-card:hover {
@@ -589,6 +624,8 @@
589
624
  .file-info {
590
625
  padding: 0.75rem;
591
626
  flex: 1;
627
+ min-width: 0; /* Important for flexbox to respect text-overflow */
628
+ width: 100%;
592
629
  }
593
630
 
594
631
  .file-name {
@@ -597,6 +634,8 @@
597
634
  white-space: nowrap;
598
635
  overflow: hidden;
599
636
  text-overflow: ellipsis;
637
+ width: 100%;
638
+ display: block;
600
639
  }
601
640
 
602
641
  .file-meta {
@@ -605,6 +644,7 @@
605
644
  color: var(--secondary-color);
606
645
  font-size: 0.75rem;
607
646
  margin-bottom: 0.5rem;
647
+ width: 100%;
608
648
  }
609
649
 
610
650
  .view-details-btn {
@@ -664,6 +704,142 @@
664
704
  font-style: italic;
665
705
  padding: 2rem 0;
666
706
  }
707
+
708
+ .filename-truncate {
709
+ white-space: nowrap;
710
+ overflow: hidden;
711
+ text-overflow: ellipsis;
712
+ max-width: 100%;
713
+ display: block;
714
+ }
715
+ </style>
716
+
717
+ <footer class="dashboard-footer">
718
+ <div class="footer-content">
719
+ <div class="love-message">
720
+ Made with <span class="heart">❤</span> by Giovanni Panasiti
721
+ </div>
722
+ <div class="opensource-message">
723
+ Embracing the open source spirit; because great software, like great ideas, deserves to be shared freely.
724
+ </div>
725
+ <div class="github-link">
726
+ <a href="https://github.com/giovapanasiti/active_storage_dashboard" target="_blank" class="github-button">
727
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="github-icon">
728
+ <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
729
+ </svg>
730
+ Star on GitHub
731
+ </a>
732
+ </div>
733
+ </div>
734
+ </footer>
735
+
736
+ <style>
737
+ /* Base styles */
738
+ :root {
739
+ --primary-color: #4361ee;
740
+ --primary-dark-color: #3a0ca3;
741
+ --secondary-color: #6c757d;
742
+ --success-color: #2ecc71;
743
+ --info-color: #3498db;
744
+ --warning-color: #f39c12;
745
+ --danger-color: #e74c3c;
746
+ --light-color: #f8f9fa;
747
+ --dark-color: #343a40;
748
+ --card-bg: #ffffff;
749
+ --border-radius: 0.5rem;
750
+ --transition-speed: 0.3s;
751
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
752
+ }
753
+
754
+ /* Footer styles */
755
+ .dashboard-footer {
756
+ margin-top: 4rem;
757
+ padding: 2rem 0;
758
+ text-align: center;
759
+ border-top: 1px solid var(--border-color);
760
+ background: linear-gradient(180deg, transparent, rgba(67, 97, 238, 0.05));
761
+ }
762
+
763
+ .footer-content {
764
+ max-width: 800px;
765
+ margin: 0 auto;
766
+ display: flex;
767
+ flex-direction: column;
768
+ gap: 1rem;
769
+ align-items: center;
770
+ justify-content: center;
771
+ }
772
+
773
+ .love-message {
774
+ font-size: 1.1rem;
775
+ font-weight: 500;
776
+ color: var(--dark-color);
777
+ }
778
+
779
+ .heart {
780
+ color: var(--danger-color);
781
+ display: inline-block;
782
+ animation: heartbeat 1.5s ease infinite;
783
+ }
784
+
785
+ @keyframes heartbeat {
786
+ 0%, 100% { transform: scale(1); }
787
+ 50% { transform: scale(1.2); }
788
+ }
789
+
790
+ .opensource-message {
791
+ color: var(--secondary-color);
792
+ font-style: italic;
793
+ max-width: 600px;
794
+ line-height: 1.6;
795
+ }
796
+
797
+ .github-link {
798
+ margin-top: 1rem;
799
+ }
800
+
801
+ .github-button {
802
+ display: inline-flex;
803
+ align-items: center;
804
+ gap: 0.5rem;
805
+ padding: 0.5rem 1rem;
806
+ background-color: var(--dark-color);
807
+ color: white;
808
+ border-radius: 2rem;
809
+ text-decoration: none;
810
+ font-size: 0.9rem;
811
+ transition: all 0.3s ease;
812
+ }
813
+
814
+ .github-button:hover {
815
+ background-color: #000;
816
+ transform: translateY(-2px);
817
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
818
+ text-decoration: none;
819
+ color: white;
820
+ }
821
+
822
+ .github-icon {
823
+ transition: transform 0.3s ease;
824
+ }
825
+
826
+ .github-button:hover .github-icon {
827
+ transform: rotate(360deg);
828
+ }
829
+
830
+ @media (max-width: 576px) {
831
+ .dashboard-footer {
832
+ padding: 1.5rem 1rem;
833
+ }
834
+
835
+ .love-message {
836
+ font-size: 1rem;
837
+ }
838
+
839
+ .opensource-message {
840
+ font-size: 0.9rem;
841
+ }
842
+ }
667
843
  </style>
668
844
 
669
845
  <script>
@@ -30,7 +30,7 @@
30
30
  </tr>
31
31
  <tr>
32
32
  <th>Filename</th>
33
- <td title="<%= @blob.filename %>"><%= truncate(@blob.filename.to_s, length: 100) %></td>
33
+ <td title="<%= @blob.filename %>" class="filename-truncate"><%= @blob.filename %></td>
34
34
  </tr>
35
35
  <tr>
36
36
  <th>Content Type</th>
@@ -56,4 +56,14 @@
56
56
 
57
57
  <div class="card-body">
58
58
  <a href="<%= variant_records_path %>" class="back-link">← Back to Variant Records</a>
59
- </div>
59
+ </div>
60
+
61
+ <style>
62
+ .filename-truncate {
63
+ white-space: nowrap;
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ max-width: 100%;
67
+ display: inline-block;
68
+ }
69
+ </style>
@@ -118,6 +118,14 @@
118
118
  gap: 1.5rem;
119
119
  }
120
120
 
121
+ .filename-truncate {
122
+ white-space: nowrap;
123
+ overflow: hidden;
124
+ text-overflow: ellipsis;
125
+ max-width: 100%;
126
+ display: inline-block;
127
+ }
128
+
121
129
  .stat-card {
122
130
  padding: 1.25rem;
123
131
  text-align: center;
@@ -228,6 +236,211 @@
228
236
  color: white;
229
237
  }
230
238
 
239
+ /* Enhanced Button Styles */
240
+ .btn {
241
+ display: inline-flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ padding: 0.375rem 0.75rem;
245
+ font-size: 0.875rem;
246
+ font-weight: 500;
247
+ line-height: 1.5;
248
+ text-align: center;
249
+ white-space: nowrap;
250
+ vertical-align: middle;
251
+ cursor: pointer;
252
+ user-select: none;
253
+ border: 1px solid transparent;
254
+ border-radius: 0.25rem;
255
+ transition: color 0.15s ease-in-out,
256
+ background-color 0.15s ease-in-out,
257
+ border-color 0.15s ease-in-out,
258
+ box-shadow 0.15s ease-in-out;
259
+ }
260
+
261
+ .btn:hover {
262
+ text-decoration: none;
263
+ }
264
+
265
+ .btn:focus {
266
+ outline: 0;
267
+ box-shadow: 0 0 0 0.2rem rgba(74, 107, 239, 0.25);
268
+ }
269
+
270
+ .btn-sm {
271
+ padding: 0.25rem 0.5rem;
272
+ font-size: 0.765625rem;
273
+ line-height: 1.5;
274
+ border-radius: 0.2rem;
275
+ }
276
+
277
+ .btn-primary {
278
+ color: #fff;
279
+ background-color: var(--primary-color);
280
+ border-color: var(--primary-color);
281
+ }
282
+
283
+ .btn-primary:hover {
284
+ color: #fff;
285
+ background-color: #3b5bd9;
286
+ border-color: #2e4ecf;
287
+ }
288
+
289
+ .btn-primary:focus {
290
+ box-shadow: 0 0 0 0.2rem rgba(74, 107, 239, 0.5);
291
+ }
292
+
293
+ .btn-outline {
294
+ color: var(--primary-color);
295
+ background-color: transparent;
296
+ border-color: var(--primary-color);
297
+ }
298
+
299
+ .btn-outline:hover {
300
+ color: #fff;
301
+ background-color: var(--primary-color);
302
+ border-color: var(--primary-color);
303
+ }
304
+
305
+ .btn-outline:focus {
306
+ box-shadow: 0 0 0 0.2rem rgba(74, 107, 239, 0.25);
307
+ }
308
+
309
+ .btn-success {
310
+ color: #fff;
311
+ background-color: var(--success-color);
312
+ border-color: var(--success-color);
313
+ }
314
+
315
+ .btn-success:hover {
316
+ color: #fff;
317
+ background-color: #218838;
318
+ border-color: #1e7e34;
319
+ }
320
+
321
+ .btn-danger {
322
+ color: #fff;
323
+ background-color: var(--danger-color);
324
+ border-color: var(--danger-color);
325
+ }
326
+
327
+ .btn-danger:hover {
328
+ color: #fff;
329
+ background-color: #c82333;
330
+ border-color: #bd2130;
331
+ }
332
+
333
+ .action-icon {
334
+ width: 1rem;
335
+ height: 1rem;
336
+ margin-right: 0.25rem;
337
+ }
338
+
339
+ .filter-panel {
340
+ background-color: var(--card-bg);
341
+ padding: 0;
342
+ border-radius: 0.25rem;
343
+ margin-bottom: 1.5rem;
344
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
345
+ border: 1px solid var(--border-color);
346
+ }
347
+
348
+ .filter-header {
349
+ display: flex;
350
+ justify-content: space-between;
351
+ align-items: center;
352
+ padding: 0.75rem 1.25rem;
353
+ background-color: rgba(0, 0, 0, 0.03);
354
+ border-bottom: 1px solid var(--border-color);
355
+ }
356
+
357
+ .filter-title {
358
+ margin: 0;
359
+ font-size: 1rem;
360
+ font-weight: 500;
361
+ }
362
+
363
+ .filter-toggle {
364
+ background: transparent;
365
+ border: none;
366
+ cursor: pointer;
367
+ display: flex;
368
+ align-items: center;
369
+ color: var(--secondary-color);
370
+ padding: 0.25rem;
371
+ }
372
+
373
+ .filter-toggle:hover {
374
+ color: var(--primary-color);
375
+ }
376
+
377
+ .filter-icon {
378
+ width: 1.25rem;
379
+ height: 1.25rem;
380
+ }
381
+
382
+ .filters-form {
383
+ padding: 1.25rem;
384
+ }
385
+
386
+ .filter-row {
387
+ display: flex;
388
+ flex-wrap: wrap;
389
+ gap: 1rem;
390
+ margin-bottom: 1rem;
391
+ }
392
+
393
+ .filter-group {
394
+ display: flex;
395
+ flex-direction: column;
396
+ gap: 0.375rem;
397
+ flex: 1;
398
+ min-width: 180px;
399
+ }
400
+
401
+ .filter-group label {
402
+ font-weight: 500;
403
+ font-size: 0.875rem;
404
+ }
405
+
406
+ .filter-select,
407
+ .filter-input {
408
+ padding: 0.375rem 0.75rem;
409
+ border: 1px solid var(--border-color);
410
+ border-radius: 0.25rem;
411
+ font-size: 0.875rem;
412
+ background-color: #fff;
413
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
414
+ }
415
+
416
+ .filter-select:focus,
417
+ .filter-input:focus {
418
+ border-color: var(--primary-color);
419
+ outline: 0;
420
+ box-shadow: 0 0 0 0.2rem rgba(74, 107, 239, 0.25);
421
+ }
422
+
423
+ .filter-actions {
424
+ display: flex;
425
+ gap: 0.75rem;
426
+ justify-content: flex-end;
427
+ }
428
+
429
+ @media (max-width: 768px) {
430
+ .filter-row {
431
+ flex-direction: column;
432
+ }
433
+
434
+ .filter-actions {
435
+ flex-direction: column;
436
+ width: 100%;
437
+ }
438
+
439
+ .filter-actions .btn {
440
+ width: 100%;
441
+ }
442
+ }
443
+
231
444
  .flash {
232
445
  padding: 0.75rem 1.25rem;
233
446
  margin-bottom: 1rem;
@@ -251,18 +464,54 @@
251
464
  }
252
465
 
253
466
  .download-btn {
254
- display: inline-block;
255
- padding: 6px 12px;
467
+ display: inline-flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ padding: 0.375rem 0.75rem;
256
471
  background-color: var(--success-color);
257
472
  color: white;
258
473
  text-decoration: none;
259
- border-radius: 4px;
260
- font-size: 0.9rem;
474
+ border-radius: 0.25rem;
475
+ font-size: 0.875rem;
476
+ font-weight: 500;
477
+ border: 1px solid var(--success-color);
478
+ transition: color 0.15s ease-in-out,
479
+ background-color 0.15s ease-in-out,
480
+ border-color 0.15s ease-in-out,
481
+ box-shadow 0.15s ease-in-out;
261
482
  }
262
483
 
263
484
  .download-btn:hover {
264
485
  text-decoration: none;
265
- opacity: 0.9;
486
+ background-color: #218838;
487
+ border-color: #1e7e34;
488
+ color: white;
489
+ }
490
+
491
+ .download-btn:focus {
492
+ outline: 0;
493
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);
494
+ }
495
+
496
+ .view-details-btn {
497
+ display: inline-flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ padding: 0.375rem 0.75rem;
501
+ background-color: var(--primary-color);
502
+ color: white;
503
+ text-decoration: none;
504
+ border-radius: 0.25rem;
505
+ font-size: 0.875rem;
506
+ font-weight: 500;
507
+ border: 1px solid var(--primary-color);
508
+ transition: all 0.15s ease-in-out;
509
+ }
510
+
511
+ .view-details-btn:hover {
512
+ text-decoration: none;
513
+ background-color: #3b5bd9;
514
+ border-color: #2e4ecf;
266
515
  color: white;
267
516
  }
268
517
  </style>
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giovanni Panasiti
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-16 00:00:00.000000000 Z
10
+ date: 2025-05-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails