power_grid 0.2.16 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7029d0bebbab40ecdabfa5d050f403a57611bb43ffb9f9f0c79169d2c4aa7aad
4
- data.tar.gz: 5e2e7b62f5e4167b8bb7813fe047ba611446662cc20772e8a607ad73d57adca9
3
+ metadata.gz: b24d27601829a61578dd76eac387de4420fbbb2d2eb6ca2132133d132a33b2e1
4
+ data.tar.gz: 9ae6ba1fcd1d96eab8603c37f5650b83f3ba3d4652b03aeb69c3d730fc0e9354
5
5
  SHA512:
6
- metadata.gz: 67a99440174654e935086b375d7513e9347ce57cf5557fadc965c19351b426b7bc444b74217aea28ad174576b3f9fc8f212b0c0b1fe8fb23c5aa6f8f390eb248
7
- data.tar.gz: f4058ab216eefc43c1a35d10a2c14b2415a4273972866b4bee4409f6e2f53ceeebb8be852471d40dcdb509bf8af4bb6b4d7f2384dc83ee23f1015188a59088b0
6
+ metadata.gz: 3e31d48d6058e43e0dee373560a3106d318384704c63dd7d268223ada9d16168f5a67ddd48b3474fa4002d2f2a8efe711225725fa5a0e38b9fe10b00d936c547
7
+ data.tar.gz: 66165a1a44b17c8c460f7514a6fe4d940e0812a4cbfab1ee0df7574caff76f83dfda1330ad42822826653e73cba22c169e2814a1663a432b695be49b88d47374
data/.gitignore CHANGED
@@ -13,3 +13,9 @@
13
13
 
14
14
  *.gem
15
15
  Gemfile.lock
16
+
17
+ # Ignore dummy app logs and storage
18
+ spec/dummy/log/*
19
+ !spec/dummy/log/.keep
20
+ spec/dummy/storage/*
21
+ !spec/dummy/storage/.keep
data/README.md CHANGED
@@ -17,6 +17,10 @@ Built with **ViewComponent**, **Turbo Frames**, **Stimulus**, and **TailwindCSS*
17
17
  - Multi-column search support.
18
18
  - Search on **joined tables** via `sql_expression`.
19
19
  - Automatic input debouncing.
20
+ - 🎛 **Advanced Filtering**:
21
+ - Support for **Search**, **Select**, **Checkboxes**, and **Radio Buttons**.
22
+ - Dynamic collections using Lambdas (e.g., `-> { User.all }`).
23
+ - Custom filtering logic via blocks.
20
24
  - 📄 **Smart Pagination**:
21
25
  - Numbered pagination window (e.g., `1 2 ... 5 6 7 ... 10`).
22
26
  - Dynamic **Per Page** limits.
@@ -24,6 +28,9 @@ Built with **ViewComponent**, **Turbo Frames**, **Stimulus**, and **TailwindCSS*
24
28
  - 🎨 **Visuals**:
25
29
  - Professional "Slate" color palette.
26
30
  - Fully responsive and Dark Mode compatible.
31
+ - **Customizable CSS** classes.
32
+ - 🛠 **Modular Toolbar**:
33
+ - Render the toolbar (search/filters) separately from the table for flexible layouts.
27
34
  - 🚀 **Optimization**: Built-in support for `includes` to automatically prevent N+1 queries.
28
35
 
29
36
  ## Installation
@@ -44,7 +51,7 @@ $ bundle install
44
51
 
45
52
  ### 1. Define your Grid
46
53
 
47
- Create a class that inherits from `PowerGrid::Base`. This class defines your data source and columns.
54
+ Create a class that inherits from `PowerGrid::Base`. This class defines your data source, columns, and filters.
48
55
 
49
56
  ```ruby
50
57
  # app/grids/users_grid.rb
@@ -59,6 +66,14 @@ class UsersGrid < PowerGrid::Base
59
66
  column :status do |user|
60
67
  tag.span(user.status.humanize, class: "badge badge-#{user.status}")
61
68
  end
69
+
70
+ # Filters
71
+ filter :role, collection: ["Admin", "User"], include_blank: "All Roles"
72
+
73
+ # Advanced Filter (Checkbox with custom logic)
74
+ filter :department_ids, type: :checkbox, collection: -> { Department.pluck(:name, :id) } do |scope, value|
75
+ scope.where(department_id: value)
76
+ end
62
77
  end
63
78
  ```
64
79
 
@@ -84,36 +99,85 @@ Render the `PowerGrid::TableComponent`, passing the grid instance.
84
99
  <%= render PowerGrid::TableComponent.new(@grid) %>
85
100
  ```
86
101
 
87
- ## detailed API Reference
102
+ ## Advanced Filtering
88
103
 
89
- ### `column(name, options = {}, &block)`
104
+ PowerGrid supports rich filtering options beyond simple text search.
90
105
 
91
- Defines a column in the table.
106
+ ### Checkboxes (Multi-select)
107
+ Use `type: :checkbox` to render a list of checkboxes.
92
108
 
93
- | Option | Type | Description |
94
- | :--- | :--- | :--- |
95
- | `sortable` | `boolean` | If true, the column header will be clickable to sort. |
96
- | `searchable` | `boolean` | If true, the global search input will check this column using `LIKE`. |
97
- | `sql_expression` | `string` | The raw SQL column name to use for sorting/searching. Essential for joined tables (e.g., `"posts.title"`). |
98
- | `includes` | `symbol/array` | Association(s) to eager load when rendering this column to prevent N+1 queries. |
109
+ ```ruby
110
+ filter :category_ids, header: "Categories", type: :checkbox, collection: -> { Category.pluck(:name, :id) }
111
+ ```
112
+
113
+ ### Radio Buttons (Single-select)
114
+ Use `type: :radio` to render radio buttons.
99
115
 
100
- **Example: Joined Column**
101
116
  ```ruby
102
- column :"posts.title",
103
- searchable: true,
104
- sortable: true,
105
- sql_expression: "posts.title",
106
- includes: :posts
117
+ filter :active, type: :radio, collection: [["Yes", true], ["No", false]]
107
118
  ```
108
119
 
109
- ### `scope { ... }`
120
+ ### Dynamic Collections
121
+ Pass a `lambda` or `Proc` to `collection:` to load options dynamically at request time.
122
+
123
+ ```ruby
124
+ filter :manager_id, collection: -> { User.where(role: 'manager').pluck(:name, :id) }
125
+ ```
110
126
 
111
- Defines the default Active Record scope. This is used if no `initial_scope` is passed to the initializer.
127
+ ### Custom Filtering Logic
128
+ Pass a block to `filter` to customize how the query is modified.
112
129
 
113
130
  ```ruby
114
- scope { User.active.order(created_at: :desc) }
131
+ filter :query_date, type: :text do |scope, value|
132
+ # Parse date string and filter range
133
+ date = Date.parse(value) rescue nil
134
+ date ? scope.where(created_at: date.all_day) : scope
135
+ end
115
136
  ```
116
137
 
138
+ ## CSS Customization
139
+
140
+ PowerGrid uses TailwindCSS by default but allows you to override classes for deep customization. Pass a `css:` hash to the component.
141
+
142
+ ```erb
143
+ <%= render PowerGrid::TableComponent.new(@grid, css: {
144
+ table: "min-w-full divide-y divide-gray-200 border border-gray-300",
145
+ th: "px-6 py-3 bg-blue-50 text-left text-xs font-medium text-blue-500 uppercase tracking-wider",
146
+ tr: "bg-white hover:bg-blue-50 transition-colors duration-150"
147
+ }) %>
148
+ ```
149
+
150
+ Available keys in `DEFAULT_CSS`: `container`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `pagination`, `page_link`, `page_link_active`, `page_prev`, `page_next`, `page_gap`, `search_input`, `filter_select`, `filter_input`.
151
+
152
+ ## Modular Toolbar
153
+
154
+ You can render the toolbar (Search, Filters, Per Page) separately from the table. This is useful for placing filters in a sidebar or sticky header.
155
+
156
+ 1. Disable the built-in toolbar in `TableComponent`:
157
+ ```erb
158
+ <%= render PowerGrid::TableComponent.new(@grid, toolbar: false) %>
159
+ ```
160
+
161
+ 2. Render `ToolbarComponent` where you want it:
162
+ ```erb
163
+ <div class="sticky top-0 bg-white z-10 shadow p-4">
164
+ <%= render PowerGrid::ToolbarComponent.new(@grid) %>
165
+ </div>
166
+ ```
167
+
168
+ ## Detailed API Reference
169
+
170
+ ### `column(name, options = {}, &block)`
171
+
172
+ Defines a column in the table.
173
+
174
+ | Option | Type | Description |
175
+ | :--- | :--- | :--- |
176
+ | `sortable` | `boolean` | If true, the column header will be clickable to sort. |
177
+ | `searchable` | `boolean` | If true, the global search input will check this column using `LIKE`. |
178
+ | `sql_expression` | `string` | The raw SQL column name/expression to use for sorting/searching. Essential for joined tables (e.g., `"posts.title"`). |
179
+ | `includes` | `symbol/array` | Association(s) to eager load when rendering this column to prevent N+1 queries. |
180
+
117
181
  ### `initialize(params, initial_scope: nil)`
118
182
 
119
183
  - `params`: The Rails `params` hash (required for sorting/filtering state).
@@ -123,18 +187,13 @@ scope { User.active.order(created_at: :desc) }
123
187
 
124
188
  ### TailwindCSS
125
189
 
126
- PowerGrid creates HTML with standard Tailwind utility classes (using the `slate` color palette). Ensure your Tailwind configuration scans your gem paths or includes utilities for:
127
- - Colors: `slate-50` to `slate-900`.
128
- - Spacing, Borders, Flexbox, Typography.
190
+ PowerGrid creates HTML with standard Tailwind utility classes (using the `slate` color palette). Ensure your Tailwind configuration scans your gem paths.
129
191
 
130
192
  ### Hotwire & Stimulus
131
193
 
132
194
  Ensure your application has `turbo-rails` and `stimulus-rails` installed.
133
195
  The gem includes a Stimulus controller `power_grid--table_controller` which handles search input debouncing and auto-submission.
134
196
 
135
- If using **Importmap** (default in Rails 7+), this is configured automatically.
136
- If using **esbuild/webpack**, manually register the controller if needed, or ensure the gem's assets are in your load path.
137
-
138
197
  ## Contributing
139
198
 
140
199
  Bug reports and pull requests are welcome on GitHub at https://github.com/bhavinNandani/power_grid.
@@ -1,77 +1,22 @@
1
- <div class="power-grid-container w-full bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800" data-controller="power-grid-table">
1
+ <div class="<%= css_class(:container) %>" data-controller="power-grid-table">
2
2
 
3
3
  <!-- Clean Toolbar -->
4
- <% unless @grid.try(:hide_controls) %>
5
- <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 p-4 border-b border-slate-200 dark:border-slate-800">
6
-
7
- <!-- Search -->
8
- <div class="w-full md:w-72">
9
- <% if @grid.searchable? %>
10
- <div class="relative">
11
- <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
12
- <svg class="h-5 w-5 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
14
- </svg>
15
- </div>
16
- <%= text_field_tag :q, params[:q], placeholder: "Search...",
17
- class: "block w-full pl-12 pr-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg leading-5 bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
18
- data: { action: "input->power-grid-table#search" },
19
- form: "power_grid_form_#{request.path.parameterize}"
20
- %>
21
- </div>
22
- <% end %>
23
- </div>
24
-
25
- <!-- Filters & Actions -->
26
- <div class="flex items-center gap-3">
27
- <%= form_with url: request.path, method: :get, data: { turbo_frame: "power_grid_table", turbo_action: "advance", power_grid_table_target: "form" }, class: "flex items-center gap-3", id: "power_grid_form_#{request.path.parameterize}" do |f| %>
28
- <% params.except(:per_page, :page, :q, *(@grid.class.defined_filters.keys)).each do |key, value| %>
29
- <%= hidden_field_tag key, value %>
30
- <% end %>
31
-
32
- <!-- Filters -->
33
- <% @grid.class.defined_filters.each do |name, options| %>
34
- <div class="relative">
35
- <% if options[:collection] %>
36
- <%= select_tag name, options_for_select(options[:collection], params[name]),
37
- include_blank: "All #{name.to_s.humanize.pluralize}",
38
- class: "block w-full pl-3 pr-10 py-2 text-base border-slate-300 dark:border-slate-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-lg bg-white dark:bg-slate-800",
39
- onchange: "this.form.requestSubmit()"
40
- %>
41
- <% else %>
42
- <%= text_field_tag name, params[name], placeholder: name.to_s.humanize,
43
- class: "block w-full pl-3 pr-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg leading-5 bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
44
- data: { action: "input->power-grid-table#search" }
45
- %>
46
- <% end %>
47
- </div>
48
- <% end %>
49
-
50
- <!-- Per Page -->
51
- <div>
52
- <%= select_tag :per_page, options_for_select([10, 25, 50, 100], @grid.per_page),
53
- class: "block w-full pl-3 pr-10 py-2 text-base border-slate-300 dark:border-slate-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-lg bg-white dark:bg-slate-800",
54
- onchange: "this.form.requestSubmit()"
55
- %>
56
- </div>
57
-
58
- <% end %>
59
- </div>
60
- </div>
4
+ <% if show_toolbar? %>
5
+ <%= render PowerGrid::ToolbarComponent.new(@grid, css: @css, headless: @headless) %>
61
6
  <% end %>
62
7
 
63
8
  <%= turbo_frame_tag "power_grid_table", class: "w-full block" do %>
64
9
  <div class="overflow-x-auto">
65
- <table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
66
- <thead class="bg-slate-50 dark:bg-slate-800">
10
+ <table class="<%= css_class(:table) %>">
11
+ <thead class="<%= css_class(:thead) %>">
67
12
  <tr>
68
13
  <% columns.each_with_index do |(key, options), idx| %>
69
- <th scope="col" class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">
14
+ <th scope="col" class="<%= css_class(:th) %>">
70
15
  <% label = options[:header] || key.to_s.humanize %>
71
16
  <% if options[:sortable] %>
72
- <%= link_to request.params.merge(order: key, dir: (params[:order] == key.to_s && params[:dir] == "asc" ? "desc" : "asc")), class: "group inline-flex items-center space-x-1 hover:text-slate-700 dark:hover:text-slate-200", data: { turbo_action: "advance" } do %>
17
+ <%= link_to request.params.merge(order: key, dir: (params[:order] == key.to_s && params[:dir] == "asc" ? "desc" : "asc")), class: "group inline-flex items-center space-x-1 hover:text-gray-700 dark:hover:text-gray-200", data: { turbo_action: "advance" } do %>
73
18
  <span><%= label %></span>
74
- <span class="flex-none rounded text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300">
19
+ <span class="flex-none rounded text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300">
75
20
  <% if params[:order] == key.to_s %>
76
21
  <% if params[:dir] == "asc" %>
77
22
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" clip-rule="evenodd" /></svg>
@@ -90,11 +35,11 @@
90
35
  <% end %>
91
36
  </tr>
92
37
  </thead>
93
- <tbody class="divide-y divide-slate-200 dark:divide-slate-700 bg-white dark:bg-slate-900">
38
+ <tbody class="<%= css_class(:tbody) %>">
94
39
  <% records.each_with_index do |record, index| %>
95
- <tr class="hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
40
+ <tr class="<%= css_class(:tr) %>">
96
41
  <% columns.each do |key, options| %>
97
- <td class="px-6 py-5 whitespace-nowrap text-sm text-slate-700 dark:text-slate-300 font-medium <%= options[:class] %>">
42
+ <td class="<%= css_class(:td) %> <%= options[:class] %>">
98
43
  <% if options[:block] %>
99
44
  <%= options[:block].call(record) %>
100
45
  <% else %>
@@ -106,15 +51,15 @@
106
51
  <% end %>
107
52
  <% if records.empty? %>
108
53
  <tr>
109
- <td colspan="<%= columns.size %>" class="px-6 py-12 text-center text-sm text-slate-500 dark:text-slate-400">
54
+ <td colspan="<%= columns.size %>" class="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400">
110
55
  <div class="flex flex-col items-center justify-center space-y-3">
111
- <div style="height: 48px; width: 48px;" class="mx-auto text-slate-300">
56
+ <div style="height: 48px; width: 48px;" class="mx-auto text-gray-300">
112
57
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="48" height="48" style="min-width: 48px; min-height: 48px;">
113
58
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
114
59
  </svg>
115
60
  </div>
116
- <p class="text-base font-medium text-slate-900 dark:text-white">No records found</p>
117
- <p class="text-slate-400">Try adjusting your search or filters</p>
61
+ <p class="text-base font-medium text-gray-900 dark:text-white">No records found</p>
62
+ <p class="text-gray-400">Try adjusting your search or filters</p>
118
63
  </div>
119
64
  </td>
120
65
  </tr>
@@ -125,9 +70,9 @@
125
70
 
126
71
  <!-- Clean Pagination Footer -->
127
72
  <% if @grid.total_pages > 1 %>
128
- <div class="flex items-center justify-between px-6 py-4 border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 rounded-b-xl">
73
+ <div class="<%= css_class(:pagination) %>">
129
74
  <!-- Mobile Pagination help text -->
130
- <div class="text-sm text-slate-700 dark:text-slate-400 hidden sm:block">
75
+ <div class="text-sm text-gray-700 dark:text-gray-400 hidden sm:block">
131
76
  Showing <span class="font-medium"><%= @grid.current_page %></span> of <span class="font-medium"><%= @grid.total_pages %></span> pages
132
77
  </div>
133
78
 
@@ -136,7 +81,7 @@
136
81
  <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
137
82
  <!-- Prev -->
138
83
  <%= link_to (@grid.current_page > 1 ? request.params.merge(page: @grid.current_page - 1) : "#"),
139
- class: "relative inline-flex items-center px-2 py-2 rounded-l-md border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm font-medium #{@grid.current_page > 1 ? 'text-slate-500 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-700' : 'text-slate-300 cursor-not-allowed'}",
84
+ class: css_class(@grid.current_page > 1 ? :page_prev : :page_prev_disabled),
140
85
  disabled: @grid.current_page <= 1,
141
86
  data: { turbo_action: "advance" } do %>
142
87
  <span class="sr-only">Previous</span>
@@ -147,17 +92,17 @@
147
92
 
148
93
  <% @grid.pagination_window.each do |page| %>
149
94
  <% if page == :gap %>
150
- <span class="relative inline-flex items-center px-4 py-2 border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm font-medium text-slate-700 dark:text-slate-300">...</span>
95
+ <span class="<%= css_class(:page_gap) %>">...</span>
151
96
  <% elsif page == @grid.current_page %>
152
- <span class="z-10 bg-indigo-50 dark:bg-indigo-900 border-indigo-500 text-indigo-600 dark:text-indigo-200 relative inline-flex items-center px-4 py-2 border text-sm font-medium"><%= page %></span>
97
+ <span class="<%= css_class(:page_link_active) %>"><%= page %></span>
153
98
  <% else %>
154
- <%= link_to page, request.params.merge(page: page), class: "bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-700 text-slate-500 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-700 relative inline-flex items-center px-4 py-2 border text-sm font-medium", data: { turbo_action: "advance" } %>
99
+ <%= link_to page, request.params.merge(page: page), class: css_class(:page_link), data: { turbo_action: "advance" } %>
155
100
  <% end %>
156
101
  <% end %>
157
102
 
158
103
  <!-- Next -->
159
104
  <%= link_to (@grid.current_page < @grid.total_pages ? request.params.merge(page: @grid.current_page + 1) : "#"),
160
- class: "relative inline-flex items-center px-2 py-2 rounded-r-md border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm font-medium #{@grid.current_page < @grid.total_pages ? 'text-slate-500 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-700' : 'text-slate-300 cursor-not-allowed'}",
105
+ class: css_class(@grid.current_page < @grid.total_pages ? :page_next : :page_next_disabled),
161
106
  disabled: @grid.current_page >= @grid.total_pages,
162
107
  data: { turbo_action: "advance" } do %>
163
108
  <span class="sr-only">Next</span>
@@ -4,13 +4,57 @@ module PowerGrid
4
4
  class TableComponent < ViewComponent::Base
5
5
  include Turbo::FramesHelper
6
6
 
7
- def initialize(grid_or_class, params: nil)
8
- if grid_or_class.is_a?(Class)
9
- @grid = grid_or_class.new(params)
10
- else
11
- @grid = grid_or_class
7
+ DEFAULT_CSS = {
8
+ container: "w-full bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden ring-1 ring-black/5",
9
+ toolbar: "flex flex-col md:flex-row md:items-center justify-between gap-4 p-4 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900",
10
+ search_input: "block w-full rounded-lg border-0 py-1.5 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 bg-white dark:bg-gray-950 transition-shadow",
11
+ filter_select: "block w-full rounded-lg border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 bg-white dark:bg-gray-950 transition-shadow",
12
+ filter_input: "block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 bg-white dark:bg-gray-950 transition-shadow",
13
+ table: "min-w-full divide-y divide-gray-200 dark:divide-gray-800",
14
+ thead: "bg-gray-50 dark:bg-gray-800/50",
15
+ th: "px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider",
16
+ tbody: "divide-y divide-gray-200 dark:divide-gray-800 bg-white dark:bg-gray-900",
17
+ tr: "hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors duration-150 ease-in-out group",
18
+ td: "px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300",
19
+ pagination: "flex items-center justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900",
20
+ pagination_summary: "text-sm text-gray-700 dark:text-gray-400 hidden sm:block",
21
+ page_link: "relative inline-flex items-center px-3 py-1.5 text-sm font-semibold text-gray-900 dark:text-gray-200 ring-1 ring-inset ring-gray-300 dark:ring-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 focus:z-20 focus:outline-offset-0 rounded-lg mx-0.5 transition-all",
22
+ page_link_active: "relative z-10 inline-flex items-center px-3 py-1.5 text-sm font-semibold text-white bg-blue-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 rounded-lg mx-0.5 shadow-sm",
23
+ page_prev: "relative inline-flex items-center rounded-l-lg px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 dark:ring-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 focus:z-20 focus:outline-offset-0 mr-1 transition-all",
24
+ page_prev_disabled: "relative inline-flex items-center rounded-l-lg px-2 py-2 text-gray-300 ring-1 ring-inset ring-gray-200 dark:ring-gray-800 cursor-not-allowed mr-1 bg-gray-50 dark:bg-gray-900/50",
25
+ page_next: "relative inline-flex items-center rounded-r-lg px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 dark:ring-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 focus:z-20 focus:outline-offset-0 ml-1 transition-all",
26
+ page_next_disabled: "relative inline-flex items-center rounded-r-lg px-2 py-2 text-gray-300 ring-1 ring-inset ring-gray-200 dark:ring-gray-800 cursor-not-allowed ml-1 bg-gray-50 dark:bg-gray-900/50",
27
+ page_gap: "relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-700 rounded-lg mx-0.5"
28
+ }.freeze
29
+
30
+ attr_reader :headless
31
+
32
+ def initialize(grid_or_class, params: nil, css: {}, toolbar: true, headless: false)
33
+ if grid_or_class.is_a?(Class)
34
+ @grid = grid_or_class.new(params)
35
+ else
36
+ @grid = grid_or_class
37
+ end
38
+ @show_toolbar = toolbar
39
+ @headless = headless
40
+ if @headless
41
+ @css = css
42
+ else
43
+ @css = DEFAULT_CSS.merge(css)
44
+ end
45
+ end
46
+
47
+ def show_toolbar?
48
+ @show_toolbar
49
+ end
50
+
51
+ def css_class(key)
52
+ if headless
53
+ @css[key]
54
+ else
55
+ @css[key] || DEFAULT_CSS[key]
56
+ end
12
57
  end
13
- end
14
58
 
15
59
  def columns
16
60
  @grid.class.defined_columns
@@ -20,6 +64,78 @@ module PowerGrid
20
64
  @grid.records
21
65
  end
22
66
 
67
+ def render_filter(name, options)
68
+ label = options[:header] || options[:label] || name.to_s.humanize
69
+ value = @grid.params[name]
70
+
71
+ collection = options[:collection]
72
+ collection = collection.call if collection.respond_to?(:call)
73
+
74
+ if options[:type].to_s == "checkbox" && collection
75
+ # Checkbox Logic
76
+ # ...
77
+ current_values = Array(value).map(&:to_s)
78
+
79
+ content_tag(:div, class: "flex flex-col gap-1") do
80
+ concat content_tag(:span, label, class: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1")
81
+ content_tag(:div, class: "flex flex-wrap gap-2") do
82
+ collection.each do |item|
83
+ # Handle collection item being [label, value] or just value
84
+ display, val = item.is_a?(Array) ? item : [item.to_s.humanize, item]
85
+ checked = current_values.include?(val.to_s)
86
+
87
+ concat(
88
+ content_tag(:label, class: "inline-flex items-center space-x-2 cursor-pointer") do
89
+ concat check_box_tag("#{name}[]", val, checked, class: "form-checkbox rounded text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-700 dark:bg-gray-800", onchange: "this.form.requestSubmit()")
90
+ concat content_tag(:span, display, class: "text-sm text-gray-700 dark:text-gray-300")
91
+ end
92
+ )
93
+ end
94
+ end
95
+ end
96
+
97
+ elsif options[:type].to_s == "radio" && collection
98
+ # Radio Logic
99
+ content_tag(:div, class: "flex flex-col gap-1") do
100
+ concat content_tag(:span, label, class: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1")
101
+ content_tag(:div, class: "flex flex-wrap gap-2") do
102
+ # Add "All" option for Radio
103
+ all_checked = value.blank?
104
+ concat(
105
+ content_tag(:label, class: "inline-flex items-center space-x-2 cursor-pointer") do
106
+ concat radio_button_tag(name, "", all_checked, class: "form-radio text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-700 dark:bg-gray-800", onchange: "this.form.requestSubmit()")
107
+ concat content_tag(:span, "All", class: "text-sm text-gray-700 dark:text-gray-300")
108
+ end
109
+ )
110
+
111
+ collection.each do |item|
112
+ display, val = item.is_a?(Array) ? item : [item.to_s.humanize, item]
113
+ checked = value.to_s == val.to_s
114
+
115
+ concat(
116
+ content_tag(:label, class: "inline-flex items-center space-x-2 cursor-pointer") do
117
+ concat radio_button_tag(name, val, checked, class: "form-radio text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-700 dark:bg-gray-800", onchange: "this.form.requestSubmit()")
118
+ concat content_tag(:span, display, class: "text-sm text-gray-700 dark:text-gray-300")
119
+ end
120
+ )
121
+ end
122
+ end
123
+ end
124
+
125
+ elsif collection
126
+ # Select Logic (Default for collections)
127
+ select_tag name, options_for_select(collection, value),
128
+ include_blank: "All #{name.to_s.humanize.pluralize}",
129
+ class: css_class(:filter_select),
130
+ onchange: "this.form.requestSubmit()"
131
+ else
132
+ # Text Input Logic
133
+ text_field_tag name, value, placeholder: label,
134
+ class: css_class(:filter_input),
135
+ data: { action: "input->power-grid-table#search" }
136
+ end
137
+ end
138
+
23
139
  def sort_url(column, direction)
24
140
  # TODO: Helper to generate URL with updated params
25
141
  # For now just return hash or similar, we might need a helper that merges params
@@ -0,0 +1,47 @@
1
+
2
+ <% unless @grid.try(:hide_controls) %>
3
+ <div class="<%= css_class(:container) %>">
4
+
5
+ <!-- Search -->
6
+ <div class="w-full md:w-72">
7
+ <% if @grid.searchable? %>
8
+ <div class="relative">
9
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
10
+ <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
11
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
12
+ </svg>
13
+ </div>
14
+ <%= text_field_tag :q, @grid.params[:q], placeholder: "Search...",
15
+ class: css_class(:search_input),
16
+ data: { action: "input->power-grid-table#search" },
17
+ form: "power_grid_form_#{request.path.parameterize}"
18
+ %>
19
+ </div>
20
+ <% end %>
21
+ </div>
22
+
23
+ <!-- Filters & Actions -->
24
+ <div class="flex items-center gap-3">
25
+ <%= form_with url: request.path, method: :get, data: { turbo_frame: "power_grid_table", turbo_action: "advance", power_grid_table_target: "form" }, class: "flex items-center gap-3", id: "power_grid_form_#{request.path.parameterize}" do |f| %>
26
+ <% @grid.params.except(:per_page, :page, :q, *(@grid.class.defined_filters.keys)).each do |key, value| %>
27
+ <%= hidden_field_tag key, value %>
28
+ <% end %>
29
+
30
+ <!-- Filters -->
31
+ <% @grid.class.defined_filters.each do |name, options| %>
32
+ <div class="relative">
33
+ <%= render_filter(name, options) %>
34
+ </div>
35
+ <% end %>
36
+
37
+ <!-- Per Page -->
38
+ <div>
39
+ <%= select_tag :per_page, options_for_select([10, 25, 50, 100], @grid.per_page),
40
+ class: css_class(:filter_select),
41
+ onchange: "this.form.requestSubmit()"
42
+ %>
43
+ </div>
44
+ <% end %>
45
+ </div>
46
+ </div>
47
+ <% end %>
@@ -0,0 +1,114 @@
1
+ require "view_component"
2
+
3
+ module PowerGrid
4
+ class ToolbarComponent < ViewComponent::Base
5
+ include Turbo::FramesHelper
6
+
7
+ DEFAULT_CSS = {
8
+ container: "flex flex-col md:flex-row md:items-center justify-between gap-4 p-4",
9
+ search_input: "block w-full pl-12 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
10
+ filter_select: "block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-lg bg-white dark:bg-gray-800",
11
+ filter_input: "block w-full pl-3 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
12
+ }.freeze
13
+
14
+ def initialize(grid, css: {}, headless: false)
15
+ @grid = grid
16
+ @css = css
17
+ @headless = headless
18
+ end
19
+
20
+ def css_class(key)
21
+ if @headless
22
+ @css[key]
23
+ else
24
+ @css[key] || DEFAULT_CSS[key]
25
+ end
26
+ end
27
+
28
+ def render_filter(name, options)
29
+ label = options[:header] || options[:label] || name.to_s.humanize
30
+ value = @grid.params[name] || @grid.params[name.to_s]
31
+
32
+ collection = options[:collection]
33
+ collection = collection.call if collection.respond_to?(:call)
34
+
35
+ filter_type = options[:type].to_s.downcase
36
+
37
+ if filter_type == "checkbox" && collection
38
+ current_values = Array(value).map(&:to_s)
39
+ content_tag(:div, class: "flex flex-col gap-1") do
40
+ concat content_tag(:span, label, class: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1")
41
+ concat(content_tag(:div, class: "flex flex-wrap gap-2") do
42
+ collection.each do |item|
43
+ # Handle collection item being [label, value] or just value
44
+ display, val = item.is_a?(Array) ? item : [item.to_s.humanize, item]
45
+ checked = current_values.include?(val.to_s)
46
+ concat(
47
+ content_tag(:label, class: "inline-flex items-center space-x-2 cursor-pointer") do
48
+ concat check_box_tag("#{name}[]", val, checked, class: "form-checkbox rounded text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-700 dark:bg-gray-800", onchange: "this.form.requestSubmit()")
49
+ concat content_tag(:span, display, class: "text-sm text-gray-700 dark:text-gray-300")
50
+ end
51
+ )
52
+ end
53
+ nil
54
+ end)
55
+ end
56
+ elsif filter_type == "radio" && collection
57
+ content_tag(:div, class: "flex flex-col gap-1") do
58
+ concat content_tag(:span, label, class: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1")
59
+ concat(content_tag(:div, class: "flex flex-wrap gap-2") do
60
+ all_checked = value.blank?
61
+ concat(
62
+ content_tag(:label, class: "inline-flex items-center space-x-2 cursor-pointer") do
63
+ concat radio_button_tag(name, "", all_checked, class: "form-radio text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-700 dark:bg-gray-800", onchange: "this.form.requestSubmit()")
64
+ concat content_tag(:span, "All", class: "text-sm text-gray-700 dark:text-gray-300")
65
+ end
66
+ )
67
+ collection.each do |item|
68
+ display, val = item.is_a?(Array) ? item : [item.to_s.humanize, item]
69
+ checked = value.to_s == val.to_s
70
+ concat(
71
+ content_tag(:label, class: "inline-flex items-center space-x-2 cursor-pointer") do
72
+ concat radio_button_tag(name, val, checked, class: "form-radio text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-700 dark:bg-gray-800", onchange: "this.form.requestSubmit()")
73
+ concat content_tag(:span, display, class: "text-sm text-gray-700 dark:text-gray-300")
74
+ end
75
+ )
76
+ end
77
+ nil
78
+ end)
79
+ end
80
+ elsif filter_type == "number_range"
81
+ min_value = @grid.params["#{name}_min"]
82
+ max_value = @grid.params["#{name}_max"]
83
+ content_tag(:div, class: "flex flex-col gap-1") do
84
+ concat content_tag(:span, label, class: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1")
85
+ concat(content_tag(:div, class: "flex items-center space-x-2") do
86
+ concat text_field_tag("#{name}_min", min_value, placeholder: "Min", class: css_class(:filter_input), data: { action: "input->power-grid-table#search" })
87
+ concat content_tag(:span, "-", class: "text-gray-500")
88
+ concat text_field_tag("#{name}_max", max_value, placeholder: "Max", class: css_class(:filter_input), data: { action: "input->power-grid-table#search" })
89
+ end)
90
+ end
91
+ elsif filter_type == "boolean"
92
+ boolean_options = [["Yes", "true"], ["No", "false"]]
93
+ select_tag name, options_for_select(boolean_options, value),
94
+ include_blank: "All #{name.to_s.humanize.pluralize}",
95
+ class: css_class(:filter_select),
96
+ onchange: "this.form.requestSubmit()"
97
+ elsif filter_type == "date_range"
98
+ text_field_tag name, value,
99
+ placeholder: label,
100
+ class: css_class(:filter_input),
101
+ data: { controller: "power-grid--flatpickr" }
102
+ elsif collection
103
+ select_tag name, options_for_select(collection, value),
104
+ include_blank: "All #{name.to_s.humanize.pluralize}",
105
+ class: css_class(:filter_select),
106
+ onchange: "this.form.requestSubmit()"
107
+ else
108
+ text_field_tag name, value, placeholder: label,
109
+ class: css_class(:filter_input),
110
+ data: { action: "input->power-grid-table#search" }
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import flatpickr from "flatpickr"
3
+
4
+ // Connects to data-controller="power-grid--flatpickr"
5
+ export default class extends Controller {
6
+ connect() {
7
+ this.fp = flatpickr(this.element, {
8
+ mode: "range",
9
+ dateFormat: "Y-m-d",
10
+ altInput: true,
11
+ altFormat: "F j, Y",
12
+ onChange: (selectedDates, dateStr, instance) => {
13
+ if (selectedDates.length === 2 || selectedDates.length === 0) {
14
+ this.element.form.requestSubmit()
15
+ }
16
+ }
17
+ })
18
+ }
19
+
20
+ disconnect() {
21
+ this.fp.destroy()
22
+ }
23
+ }
@@ -1,4 +1,5 @@
1
1
  module PowerGrid
2
+ require_relative "exporter"
2
3
  class Base
3
4
  class << self
4
5
  def scope(&block)
@@ -10,9 +11,9 @@ module PowerGrid
10
11
  @columns[name] = options.merge(block: block)
11
12
  end
12
13
 
13
- def filter(name, **options)
14
+ def filter(name, **options, &block)
14
15
  @filters ||= {}
15
- @filters[name] = options
16
+ @filters[name] = options.merge(block: block)
16
17
  end
17
18
 
18
19
  def defined_scope
@@ -32,25 +33,36 @@ module PowerGrid
32
33
  attr_accessor :hide_controls
33
34
 
34
35
  def initialize(params = {}, initial_scope: nil, **options)
36
+ params ||= {}
35
37
  params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
38
+ params = params.with_indifferent_access
39
+
40
+ # Handle namespaced params (common pattern for custom forms: grid[field])
41
+ if params[:grid].present? && params[:grid].is_a?(Hash)
42
+ params = params.merge(params[:grid])
43
+ end
44
+
36
45
  @params = params.merge(options)
37
46
  @initial_scope = initial_scope
38
47
  @hide_controls = false # Initialize to false by default
39
48
  end
40
49
 
41
50
  def records
42
- @records ||= begin
43
- scope = @initial_scope || instance_eval(&self.class.defined_scope)
44
- scope = apply_includes(scope)
45
- scope = apply_filters(scope)
46
- scope = apply_search(scope)
47
- scope = apply_sort(scope)
51
+ @records ||= apply_pagination(scope)
52
+ end
53
+
54
+ def scope
55
+ @scope ||= begin
56
+
57
+ s = @initial_scope || instance_eval(&self.class.defined_scope)
58
+ s = apply_includes(s)
59
+ s = apply_filters(s)
60
+ s = apply_search(s)
61
+ s = apply_sort(s)
48
62
 
49
63
  # Capture total count after filtering but before pagination
50
- @total_count = scope.count
51
-
52
- scope = apply_pagination(scope)
53
- scope
64
+ @total_count = s.count
65
+ s
54
66
  end
55
67
  end
56
68
 
@@ -112,6 +124,11 @@ module PowerGrid
112
124
  range
113
125
  end
114
126
 
127
+
128
+ def to_csv
129
+ Exporter.new(self).to_csv
130
+ end
131
+
115
132
  private
116
133
 
117
134
  def apply_includes(scope)
@@ -158,20 +175,49 @@ module PowerGrid
158
175
  end
159
176
 
160
177
  def apply_filters(scope)
161
- self.class.defined_filters.each do |name, options|
162
- value = params[name]
163
- next if value.blank?
164
178
 
165
- # Basic equality filter for now.
166
- # TODO: Support blocks/procs for custom filtering
167
-
168
- # If sql_expression option exists, use that
169
- col_name = options[:sql_expression] || name
179
+ self.class.defined_filters.each do |name, options|
180
+ value = @params[name] || @params[name.to_s]
170
181
 
171
- scope = scope.where(col_name => value)
182
+ is_number_range = options[:type].to_s == "number_range"
183
+ has_min_max = @params["#{name}_min"].present? || @params["#{name}_max"].present?
184
+
185
+ next if value.blank? && !(is_number_range && has_min_max)
186
+
187
+ if options[:block]
188
+ scope = options[:block].call(scope, value)
189
+ elsif options[:type].to_s == "date_range" && value.is_a?(String) && value.include?(" to ")
190
+ start_date, end_date = value.split(" to ")
191
+ col_name = options[:sql_expression] || name
192
+ scope = scope.where(col_name => start_date..end_date)
193
+ elsif options[:type].to_s == "number_range"
194
+ min = @params["#{name}_min"]
195
+ max = @params["#{name}_max"]
196
+ col_name = options[:sql_expression] || name
197
+
198
+
199
+
200
+ if min.present? && max.present?
201
+ scope = scope.where(col_name => min..max)
202
+ elsif min.present?
203
+ scope = scope.where("#{col_name} >= ?", min)
204
+ elsif max.present?
205
+ scope = scope.where("#{col_name} <= ?", max)
206
+ end
207
+ elsif options[:type].to_s == "boolean"
208
+ col_name = options[:sql_expression] || name
209
+ bool_value = ActiveRecord::Type::Boolean.new.cast(value)
210
+ scope = scope.where(col_name => bool_value)
211
+ else
212
+ # Basic equality filter
213
+ # If sql_expression option exists, use that
214
+ col_name = options[:sql_expression] || name
215
+ scope = scope.where(col_name => value)
216
+ end
172
217
  end
173
218
  scope
174
219
  end
175
220
 
221
+
176
222
  end
177
223
  end
@@ -13,6 +13,12 @@ module PowerGrid
13
13
  end
14
14
  end
15
15
 
16
+ initializer "power_grid.helper" do
17
+ ActiveSupport.on_load(:action_view) do
18
+ include PowerGrid::Helper
19
+ end
20
+ end
21
+
16
22
  # Removed importmap auto-configuration to prevent "Is a directory" errors.
17
23
  # Users can pin the controller manually if needed.
18
24
  end
@@ -0,0 +1,32 @@
1
+ require 'csv'
2
+
3
+ module PowerGrid
4
+ class Exporter
5
+ def initialize(grid)
6
+ @grid = grid
7
+ end
8
+
9
+ def to_csv
10
+ collection = @grid.records
11
+ columns = @grid.class.defined_columns
12
+
13
+ CSV.generate(headers: true) do |csv|
14
+ # Header Row
15
+ csv << columns.map { |name, options| options[:header] || name.to_s.humanize }
16
+
17
+ # Data Rows
18
+ collection.each do |record|
19
+ csv << columns.map do |name, options|
20
+ # Try to fetch value from record
21
+ if record.respond_to?(name)
22
+ record.public_send(name)
23
+ else
24
+ # Fallback for virtual columns not on the model
25
+ nil
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module PowerGrid
2
+ module Helper
3
+ def render_grid(grid, **options)
4
+ render PowerGrid::TableComponent.new(grid, **options)
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module PowerGrid
2
- VERSION = "0.2.16"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/power_grid.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "power_grid/version"
2
2
  require_relative "power_grid/base"
3
+ require_relative "power_grid/helper"
3
4
  require_relative "power_grid/engine"
4
5
  require "view_component"
5
6
 
data/power_grid.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
  spec.name = "power_grid"
5
5
  spec.version = PowerGrid::VERSION
6
6
  spec.authors = ["Bhavin Nandani"]
7
- spec.email = ["bhavin.nandani@gmail.com"]
7
+ spec.email = ["nandanibhavin@gmail.com"]
8
8
 
9
9
  spec.summary = "A powerful, server-side processed table component for Rails."
10
10
  spec.description = "PowerGrid provides a view component based table with server-side sorting, filtering, and pagination."
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: power_grid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.16
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bhavin Nandani
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-22 00:00:00.000000000 Z
11
+ date: 2025-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -141,7 +141,7 @@ dependencies:
141
141
  description: PowerGrid provides a view component based table with server-side sorting,
142
142
  filtering, and pagination.
143
143
  email:
144
- - bhavin.nandani@gmail.com
144
+ - nandanibhavin@gmail.com
145
145
  executables: []
146
146
  extensions: []
147
147
  extra_rdoc_files: []
@@ -157,6 +157,9 @@ files:
157
157
  - Rakefile
158
158
  - app/components/power_grid/table_component.html.erb
159
159
  - app/components/power_grid/table_component.rb
160
+ - app/components/power_grid/toolbar_component.html.erb
161
+ - app/components/power_grid/toolbar_component.rb
162
+ - app/javascript/controllers/power_grid/flatpickr_controller.js
160
163
  - app/javascript/controllers/power_grid/table_controller.js
161
164
  - bin/console
162
165
  - bin/setup
@@ -164,6 +167,8 @@ files:
164
167
  - lib/power_grid.rb
165
168
  - lib/power_grid/base.rb
166
169
  - lib/power_grid/engine.rb
170
+ - lib/power_grid/exporter.rb
171
+ - lib/power_grid/helper.rb
167
172
  - lib/power_grid/version.rb
168
173
  - power_grid.gemspec
169
174
  homepage: https://github.com/bhavinNandani/power_grid
@@ -189,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
194
  - !ruby/object:Gem::Version
190
195
  version: '0'
191
196
  requirements: []
192
- rubygems_version: 3.1.6
197
+ rubygems_version: 3.4.22
193
198
  signing_key:
194
199
  specification_version: 4
195
200
  summary: A powerful, server-side processed table component for Rails.