magick-feature-flags 1.2.0 → 1.2.2

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.
@@ -1,67 +1,90 @@
1
+ <div class="breadcrumb">
2
+ <a href="<%= magick_admin_ui.features_path %>">Features</a>
3
+ <span class="breadcrumb-sep">/</span>
4
+ <a href="<%= magick_admin_ui.feature_path(@feature.name) %>"><%= @feature.display_name || @feature.name %></a>
5
+ <span class="breadcrumb-sep">/</span>
6
+ <span class="breadcrumb-current">Edit</span>
7
+ </div>
8
+
9
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;">
10
+ <h1 class="page-title">Edit Feature</h1>
11
+ <a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary btn-sm">Cancel</a>
12
+ </div>
13
+
1
14
  <div class="card">
2
15
  <div class="card-header">
3
- <h2>Edit Feature: <%= @feature.display_name || @feature.name %></h2>
4
- <div class="btn-group">
5
- <a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary btn-sm">← Back</a>
6
- </div>
16
+ <h2>Feature Settings</h2>
7
17
  </div>
18
+ <div class="card-body">
19
+ <%= form_with url: magick_admin_ui.feature_path(@feature.name), method: :put, local: true do |f| %>
20
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
21
+ <div class="form-group" style="margin-bottom: 0;">
22
+ <label class="form-label">Feature Key</label>
23
+ <input type="text" value="<%= @feature.name %>" disabled>
24
+ <p class="form-hint">Cannot be changed after creation</p>
25
+ </div>
8
26
 
9
- <%= form_with url: magick_admin_ui.feature_path(@feature.name), method: :put, local: true do |f| %>
10
- <div class="form-group">
11
- <label>Feature Name</label>
12
- <input type="text" value="<%= @feature.name %>" disabled>
13
- <small style="color: #999;">Feature name cannot be changed</small>
14
- </div>
27
+ <div class="form-group" style="margin-bottom: 0;">
28
+ <label class="form-label">Type</label>
29
+ <input type="text" value="<%= @feature.type.to_s.capitalize %>" disabled>
30
+ </div>
31
+ </div>
15
32
 
16
- <div class="form-group">
17
- <label>Type</label>
18
- <input type="text" value="<%= @feature.type.to_s.capitalize %>" disabled>
19
- </div>
33
+ <div style="margin-top: 20px;"></div>
20
34
 
21
- <div class="form-group">
22
- <label>Group</label>
23
- <%= text_field_tag 'group', @feature.group, placeholder: 'e.g., Authentication, Payment, UI', class: 'form-control' %>
24
- <small style="color: #999;">Optional: Group features together for easier organization and filtering</small>
25
- </div>
35
+ <div class="form-group">
36
+ <label class="form-label">Group</label>
37
+ <%= text_field_tag 'group', @feature.group, placeholder: 'e.g., Authentication, Payment, UI', class: 'form-control' %>
38
+ <p class="form-hint">Organize features into groups for easier filtering</p>
39
+ </div>
26
40
 
27
- <div class="form-group">
28
- <label>Current Value</label>
29
- <% if @feature.type == :boolean %>
30
- <div>
31
- <label style="display: inline-flex; align-items: center; cursor: pointer;">
41
+ <div class="form-group">
42
+ <label class="form-label">Value</label>
43
+ <% if @feature.type == :boolean %>
44
+ <label class="toggle-switch">
32
45
  <%= check_box_tag 'value', 'true', @feature.enabled?, id: 'feature_value' %>
33
- <span style="margin-left: 8px;">Enabled</span>
46
+ <span class="toggle-track"></span>
47
+ <span class="toggle-label-text"><%= @feature.enabled? ? 'Enabled' : 'Disabled' %></span>
34
48
  </label>
35
- </div>
36
- <%= hidden_field_tag 'value', 'false' %>
37
- <small style="color: #999;">Check to enable, uncheck to disable</small>
38
- <% elsif @feature.type == :string %>
39
- <%= text_field_tag 'value', @feature.get_value, class: 'form-control' %>
40
- <% elsif @feature.type == :number %>
41
- <%= number_field_tag 'value', @feature.get_value, class: 'form-control' %>
42
- <% end %>
43
- </div>
49
+ <%= hidden_field_tag 'value', 'false' %>
50
+ <p class="form-hint">Toggle to enable or disable this feature globally</p>
51
+ <% elsif @feature.type == :string %>
52
+ <%= text_field_tag 'value', @feature.get_value, class: 'form-control' %>
53
+ <% elsif @feature.type == :number %>
54
+ <%= number_field_tag 'value', @feature.get_value, class: 'form-control' %>
55
+ <% end %>
56
+ </div>
44
57
 
45
- <div class="form-group">
46
- <div class="btn-group">
47
- <%= submit_tag 'Update Feature', class: 'btn btn-primary btn-sm' %>
48
- <a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary btn-sm">Cancel</a>
58
+ <div style="padding-top: 20px; border-top: 1px solid var(--color-border);">
59
+ <div class="btn-group">
60
+ <%= submit_tag 'Save Changes', class: 'btn btn-primary' %>
61
+ <a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary">Cancel</a>
62
+ </div>
49
63
  </div>
50
- </div>
51
- <% end %>
64
+ <% end %>
65
+ </div>
66
+ </div>
52
67
 
53
- <% if @feature.type == :boolean %>
54
- <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
55
- <h3 style="margin-bottom: 16px;">Quick Actions</h3>
68
+ <% if @feature.type == :boolean %>
69
+ <div class="card">
70
+ <div class="card-header">
71
+ <h2>Quick Actions</h2>
72
+ </div>
73
+ <div class="card-body">
74
+ <p style="font-size: 13px; color: var(--color-text-secondary); margin-bottom: 16px;">Override value and clear targeting rules</p>
56
75
  <div class="btn-group">
57
76
  <%= button_to 'Enable Globally', magick_admin_ui.enable_feature_path(@feature.name), method: :put, class: 'btn btn-success btn-sm' %>
58
77
  <%= button_to 'Disable Globally', magick_admin_ui.disable_feature_path(@feature.name), method: :put, class: 'btn btn-danger btn-sm' %>
59
78
  </div>
60
79
  </div>
61
- <% end %>
80
+ </div>
81
+ <% end %>
62
82
 
63
- <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
64
- <h3 style="margin-bottom: 16px;">Targeting Rules</h3>
83
+ <div class="card">
84
+ <div class="card-header">
85
+ <h2>Targeting Rules</h2>
86
+ </div>
87
+ <div class="card-body">
65
88
  <%= form_with url: magick_admin_ui.update_targeting_feature_path(@feature.name), method: :put, local: true do |f| %>
66
89
  <% targeting = @feature.instance_variable_get(:@targeting) || {} %>
67
90
  <% current_roles = targeting[:role].is_a?(Array) ? targeting[:role] : (targeting[:role] ? [targeting[:role]] : []) %>
@@ -69,15 +92,14 @@
69
92
 
70
93
  <% if roles_list.any? %>
71
94
  <div class="form-group">
72
- <label>Enable for Roles</label>
95
+ <label class="form-label">Roles</label>
73
96
  <div class="tags-search-container">
74
- <input type="text" id="roles_search" class="tags-search-input" placeholder="Search roles... (e.g., admin, manager)" autocomplete="off">
97
+ <input type="text" id="roles_search" class="tags-search-input" placeholder="Search roles..." autocomplete="off">
75
98
  <div class="tags-count" id="roles_count">
76
- <span id="roles_visible_count"><%= roles_list.length %></span> of <span id="roles_total_count"><%= roles_list.length %></span> roles
99
+ <span id="roles_visible_count"><%= roles_list.length %></span> of <span id="roles_total_count"><%= roles_list.length %></span>
77
100
  </div>
78
101
  </div>
79
102
  <div class="tags-checkbox-container" id="roles_checkbox_container">
80
- <%# Render checked roles first, then unchecked %>
81
103
  <% checked_roles, unchecked_roles = roles_list.partition { |role| current_roles.include?(role.to_s) } %>
82
104
  <% (checked_roles + unchecked_roles).each do |role| %>
83
105
  <% role_name = role.to_s %>
@@ -89,7 +111,7 @@
89
111
  </label>
90
112
  <% end %>
91
113
  </div>
92
- <small style="color: #999;">Select roles that should have access to this feature</small>
114
+ <p class="form-hint">Select roles that should have access to this feature</p>
93
115
  </div>
94
116
  <% else %>
95
117
  <div class="alert alert-info">
@@ -102,15 +124,14 @@
102
124
  <% tags_list = available_tags %>
103
125
  <% if tags_list.any? %>
104
126
  <div class="form-group">
105
- <label>Enable for Tags</label>
127
+ <label class="form-label">Tags</label>
106
128
  <div class="tags-search-container">
107
- <input type="text" id="tags_search" class="tags-search-input" placeholder="Search tags... (e.g., test, MONITOR)" autocomplete="off">
129
+ <input type="text" id="tags_search" class="tags-search-input" placeholder="Search tags..." autocomplete="off">
108
130
  <div class="tags-count" id="tags_count">
109
- <span id="tags_visible_count"><%= tags_list.length %></span> of <span id="tags_total_count"><%= tags_list.length %></span> tags
131
+ <span id="tags_visible_count"><%= tags_list.length %></span> of <span id="tags_total_count"><%= tags_list.length %></span>
110
132
  </div>
111
133
  </div>
112
134
  <div class="tags-checkbox-container" id="tags_checkbox_container">
113
- <%# Render checked tags first, then unchecked %>
114
135
  <% checked_tags, unchecked_tags = tags_list.partition do |tag|
115
136
  tag_id = tag.respond_to?(:id) ? tag.id.to_s : tag.to_s
116
137
  current_tags.include?(tag_id.to_s)
@@ -125,7 +146,7 @@
125
146
  </label>
126
147
  <% end %>
127
148
  </div>
128
- <small style="color: #999;">Select tags that should have access to this feature. Tags are loaded dynamically from your configuration.</small>
149
+ <p class="form-hint">Select tags that should have access to this feature</p>
129
150
  </div>
130
151
  <% else %>
131
152
  <div class="alert alert-info">
@@ -135,7 +156,7 @@
135
156
  <% end %>
136
157
 
137
158
  <div class="form-group">
138
- <label>Enable for User IDs</label>
159
+ <label class="form-label">User IDs</label>
139
160
  <div class="user-ids-input-container">
140
161
  <div class="user-ids-tags" id="user_ids_tags">
141
162
  <% current_user_ids = targeting[:user].is_a?(Array) ? targeting[:user] : (targeting[:user] ? [targeting[:user]] : []) %>
@@ -149,27 +170,109 @@
149
170
  <input type="text" id="user_ids_input" class="form-control user-ids-input" placeholder="Type user ID and press Enter" autocomplete="off">
150
171
  <%= hidden_field_tag 'targeting[user_ids]', current_user_ids.join(','), id: 'user_ids_hidden' %>
151
172
  </div>
152
- <small style="color: #999;">Type a user ID and press Enter to add. Click × to remove.</small>
173
+ <p class="form-hint">Type a user ID and press Enter to add. Supports comma-separated values.</p>
153
174
  </div>
154
175
 
155
- <div class="form-group">
156
- <label>Percentage of Users</label>
157
- <% current_percentage_users = targeting[:percentage_users] || '' %>
158
- <%= number_field_tag 'targeting[percentage_users]', current_percentage_users, min: 0, max: 100, step: 0.1, placeholder: '0-100', class: 'form-control' %>
159
- <small style="color: #999;">Enable for a consistent percentage of users (0-100). Leave empty to disable.</small>
176
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
177
+ <div class="form-group" style="margin-bottom: 0;">
178
+ <label class="form-label">Percentage of Users</label>
179
+ <% current_percentage_users = targeting[:percentage_users] || '' %>
180
+ <%= number_field_tag 'targeting[percentage_users]', current_percentage_users, min: 0, max: 100, step: 0.1, placeholder: '0-100', class: 'form-control' %>
181
+ <p class="form-hint">Consistent user-based rollout (0-100)</p>
182
+ </div>
183
+
184
+ <div class="form-group" style="margin-bottom: 0;">
185
+ <label class="form-label">Percentage of Requests</label>
186
+ <% current_percentage_requests = targeting[:percentage_requests] || '' %>
187
+ <%= number_field_tag 'targeting[percentage_requests]', current_percentage_requests, min: 0, max: 100, step: 0.1, placeholder: '0-100', class: 'form-control' %>
188
+ <p class="form-hint">Random request-based sampling (0-100)</p>
189
+ </div>
160
190
  </div>
161
191
 
162
- <div class="form-group">
163
- <label>Percentage of Requests</label>
164
- <% current_percentage_requests = targeting[:percentage_requests] || '' %>
165
- <%= number_field_tag 'targeting[percentage_requests]', current_percentage_requests, min: 0, max: 100, step: 0.1, placeholder: '0-100', class: 'form-control' %>
166
- <small style="color: #999;">Enable for a random percentage of requests (0-100). Leave empty to disable.</small>
192
+ <hr class="section-divider">
193
+
194
+ <div class="exclusion-border">
195
+ <h3 class="section-title section-title-danger">Exclusions</h3>
196
+ <p style="font-size: 13px; color: var(--color-text-secondary); margin-bottom: 20px;">Exclusions override all inclusion rules. Users matching any exclusion will be denied access.</p>
197
+
198
+ <% current_excluded_user_ids = targeting[:excluded_users].is_a?(Array) ? targeting[:excluded_users] : (targeting[:excluded_users] ? [targeting[:excluded_users]] : []) %>
199
+ <div class="form-group">
200
+ <label class="form-label">Excluded User IDs</label>
201
+ <div class="user-ids-input-container">
202
+ <div class="user-ids-tags" id="excluded_user_ids_tags">
203
+ <% current_excluded_user_ids.each do |user_id| %>
204
+ <span class="user-id-tag excluded">
205
+ <%= user_id %>
206
+ <span class="tag-remove" data-user-id="<%= user_id %>">×</span>
207
+ </span>
208
+ <% end %>
209
+ </div>
210
+ <input type="text" id="excluded_user_ids_input" class="form-control user-ids-input" placeholder="Type user ID and press Enter" autocomplete="off">
211
+ <%= hidden_field_tag 'targeting[excluded_user_ids]', current_excluded_user_ids.join(','), id: 'excluded_user_ids_hidden' %>
212
+ </div>
213
+ <p class="form-hint">These users will be blocked even if they match inclusion rules.</p>
214
+ </div>
215
+
216
+ <% if roles_list.any? %>
217
+ <% current_excluded_roles = targeting[:excluded_roles].is_a?(Array) ? targeting[:excluded_roles] : (targeting[:excluded_roles] ? [targeting[:excluded_roles]] : []) %>
218
+ <div class="form-group">
219
+ <label class="form-label">Excluded Roles</label>
220
+ <div class="tags-search-container">
221
+ <input type="text" id="excluded_roles_search" class="tags-search-input" placeholder="Search roles to exclude..." autocomplete="off">
222
+ <div class="tags-count" id="excluded_roles_count">
223
+ <span id="excluded_roles_visible_count"><%= roles_list.length %></span> of <span id="excluded_roles_total_count"><%= roles_list.length %></span>
224
+ </div>
225
+ </div>
226
+ <div class="tags-checkbox-container" id="excluded_roles_checkbox_container">
227
+ <% checked_excluded_roles, unchecked_excluded_roles = roles_list.partition { |role| current_excluded_roles.include?(role.to_s) } %>
228
+ <% (checked_excluded_roles + unchecked_excluded_roles).each do |role| %>
229
+ <% role_name = role.to_s %>
230
+ <% role_display = role.to_s.humanize %>
231
+ <% is_checked = current_excluded_roles.include?(role.to_s) %>
232
+ <label class="tag-checkbox-label <%= 'checked' if is_checked %>" data-role-name="<%= role_name.downcase %>" data-checked="<%= is_checked %>">
233
+ <%= check_box_tag 'targeting[excluded_roles][]', role, is_checked, id: "excluded_role_#{role}", class: 'tag-checkbox' %>
234
+ <span class="tag-checkbox-text"><%= role_display %></span>
235
+ </label>
236
+ <% end %>
237
+ </div>
238
+ <p class="form-hint">Users with these roles will be blocked from this feature.</p>
239
+ </div>
240
+ <% end %>
241
+
242
+ <% if tags_list.any? %>
243
+ <% current_excluded_tags = targeting[:excluded_tags].is_a?(Array) ? targeting[:excluded_tags] : (targeting[:excluded_tags] ? [targeting[:excluded_tags]] : []) %>
244
+ <div class="form-group">
245
+ <label class="form-label">Excluded Tags</label>
246
+ <div class="tags-search-container">
247
+ <input type="text" id="excluded_tags_search" class="tags-search-input" placeholder="Search tags to exclude..." autocomplete="off">
248
+ <div class="tags-count" id="excluded_tags_count">
249
+ <span id="excluded_tags_visible_count"><%= tags_list.length %></span> of <span id="excluded_tags_total_count"><%= tags_list.length %></span>
250
+ </div>
251
+ </div>
252
+ <div class="tags-checkbox-container" id="excluded_tags_checkbox_container">
253
+ <% checked_excluded_tags, unchecked_excluded_tags = tags_list.partition do |tag|
254
+ tag_id = tag.respond_to?(:id) ? tag.id.to_s : tag.to_s
255
+ current_excluded_tags.include?(tag_id.to_s)
256
+ end %>
257
+ <% (checked_excluded_tags + unchecked_excluded_tags).each do |tag| %>
258
+ <% tag_id = tag.respond_to?(:id) ? tag.id.to_s : tag.to_s %>
259
+ <% tag_name = tag.respond_to?(:name) ? tag.name : (tag.respond_to?(:to_s) ? tag.to_s : tag_id) %>
260
+ <% is_checked = current_excluded_tags.include?(tag_id.to_s) %>
261
+ <label class="tag-checkbox-label <%= 'checked' if is_checked %>" data-tag-name="<%= tag_name.downcase %>" data-tag-id="<%= tag_id.downcase %>" data-checked="<%= is_checked %>">
262
+ <%= check_box_tag 'targeting[excluded_tags][]', tag_id, is_checked, id: "excluded_tag_#{tag_id}", class: 'tag-checkbox' %>
263
+ <span class="tag-checkbox-text"><%= tag_name %></span>
264
+ </label>
265
+ <% end %>
266
+ </div>
267
+ <p class="form-hint">Users with these tags will be blocked from this feature.</p>
268
+ </div>
269
+ <% end %>
167
270
  </div>
168
271
 
169
- <div class="form-group">
272
+ <div style="padding-top: 24px; border-top: 1px solid var(--color-border); margin-top: 8px;">
170
273
  <div class="btn-group">
171
- <%= submit_tag 'Update Targeting', class: 'btn btn-primary btn-sm' %>
172
- <a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary btn-sm">Cancel</a>
274
+ <%= submit_tag 'Save Targeting Rules', class: 'btn btn-primary' %>
275
+ <a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary">Cancel</a>
173
276
  </div>
174
277
  </div>
175
278
  <% end %>
@@ -1,29 +1,26 @@
1
- <div class="card">
2
- <div class="card-header">
3
- <h2>Feature Flags</h2>
4
- <div>
5
- <span class="badge badge-info"><%= @features.size %> features</span>
6
- </div>
1
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
2
+ <div>
3
+ <h1 class="page-title">Features</h1>
4
+ <p class="page-subtitle" id="feature_count_text"><%= @features.size %> feature<%= @features.size != 1 ? 's' : '' %></p>
7
5
  </div>
6
+ </div>
8
7
 
9
- <!-- Filtering UI -->
10
- <div style="padding: 16px; border-bottom: 1px solid #e0e0e0; background: #f9f9f9;">
11
- <%= form_with url: magick_admin_ui.features_path, method: :get, local: true, style: "display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end;" do |f| %>
12
- <div style="flex: 1; min-width: 200px;">
13
- <label for="search" style="display: block; margin-bottom: 4px; font-weight: 500;">Search</label>
14
- <%= text_field_tag :search, params[:search], placeholder: "Search by name or description...", class: 'form-control', style: "width: 100%;" %>
8
+ <div class="card">
9
+ <div style="padding: 12px 16px; border-bottom: 1px solid var(--color-border);">
10
+ <div class="filter-bar" style="display: flex; gap: 12px; align-items: center;">
11
+ <div style="flex: 1; min-width: 200px; position: relative;">
12
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--color-text-muted); pointer-events: none;"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
13
+ <input type="text" id="feature_search_input" placeholder="Search features..." style="padding-left: 34px;" autocomplete="off" value="<%= params[:search] %>">
15
14
  </div>
16
15
  <div style="flex: 0 0 180px;">
17
- <label for="group" style="display: block; margin-bottom: 4px; font-weight: 500;">Group</label>
18
- <%= select_tag :group, options_for_select([['All Groups', '']] + @available_groups.map { |g| [g, g] }, params[:group]), class: 'form-control', style: "width: 100%;" %>
16
+ <select id="feature_group_select">
17
+ <option value="">All groups</option>
18
+ <% @available_groups.each do |g| %>
19
+ <option value="<%= g %>" <%= 'selected' if params[:group] == g %>><%= g %></option>
20
+ <% end %>
21
+ </select>
19
22
  </div>
20
- <div style="flex: 0 0 auto;">
21
- <%= submit_tag 'Filter', class: 'btn btn-primary btn-sm', style: "height: 38px;" %>
22
- <% if params[:search].present? || params[:group].present? %>
23
- <%= link_to 'Clear', magick_admin_ui.features_path, class: 'btn btn-secondary btn-sm', style: "height: 38px;" %>
24
- <% end %>
25
- </div>
26
- <% end %>
23
+ </div>
27
24
  </div>
28
25
 
29
26
  <% if @features.empty? %>
@@ -40,68 +37,70 @@
40
37
  <th>Type</th>
41
38
  <th>Status</th>
42
39
  <th>Value</th>
43
- <th>Description</th>
44
- <th>Actions</th>
40
+ <th style="text-align: right;">Actions</th>
45
41
  </tr>
46
42
  </thead>
47
- <tbody>
43
+ <tbody id="features_table_body">
48
44
  <% @features.each do |feature| %>
49
- <tr>
45
+ <tr data-feature
46
+ data-feature-name="<%= feature.name %>"
47
+ data-feature-display="<%= feature.display_name %>"
48
+ data-feature-desc="<%= feature.description %>"
49
+ data-feature-group="<%= feature.group %>">
50
50
  <td data-label="Name">
51
- <strong><%= feature.display_name || feature.name %></strong>
52
- <br>
53
- <small style="color: #999;"><%= feature.name %></small>
51
+ <div style="font-weight: 600; color: var(--color-text);"><%= feature.display_name || feature.name %></div>
52
+ <div style="font-size: 12px; color: var(--color-text-muted); font-family: var(--font-mono);"><%= feature.name %></div>
54
53
  </td>
55
54
  <td data-label="Group">
56
55
  <% if feature.group.present? %>
57
- <span class="badge badge-info"><%= feature.group %></span>
56
+ <span class="badge badge-neutral"><%= feature.group %></span>
58
57
  <% else %>
59
- <em style="color: #999;">—</em>
58
+ <span style="color: var(--color-text-muted);">&mdash;</span>
60
59
  <% end %>
61
60
  </td>
62
61
  <td data-label="Type">
63
- <span class="badge badge-info">
64
- <%= feature.type.to_s.capitalize %>
65
- </span>
62
+ <span style="font-size: 13px; color: var(--color-text-secondary);"><%= feature.type.to_s.capitalize %></span>
66
63
  </td>
67
64
  <td data-label="Status">
68
65
  <% case feature.status.to_sym %>
69
66
  <% when :active %>
70
- <span class="badge badge-success">Active</span>
67
+ <span class="status-indicator"><span class="status-dot status-dot-green"></span> Active</span>
71
68
  <% when :deprecated %>
72
- <span class="badge badge-warning">Deprecated</span>
69
+ <span class="status-indicator"><span class="status-dot status-dot-yellow"></span> Deprecated</span>
73
70
  <% when :inactive %>
74
- <span class="badge badge-danger">Inactive</span>
71
+ <span class="status-indicator"><span class="status-dot status-dot-red"></span> Inactive</span>
75
72
  <% else %>
76
- <span class="badge"><%= feature.status.to_s.capitalize %></span>
73
+ <span class="status-indicator"><%= feature.status.to_s.capitalize %></span>
77
74
  <% end %>
78
75
  </td>
79
76
  <td data-label="Value">
80
77
  <% if feature.type == :boolean %>
81
78
  <% targeting = feature.instance_variable_get(:@targeting) || {} %>
82
79
  <% if targeting.any? %>
83
- <span class="badge badge-warning">Partially Enabled</span>
80
+ <span class="toggle-indicator toggle-partial">PARTIAL</span>
84
81
  <% elsif feature.enabled? %>
85
- <span class="badge badge-success">Enabled</span>
82
+ <span class="toggle-indicator toggle-on">ON</span>
86
83
  <% else %>
87
- <span class="badge badge-danger">Disabled</span>
84
+ <span class="toggle-indicator toggle-off">OFF</span>
88
85
  <% end %>
89
86
  <% else %>
90
87
  <code><%= feature.get_value.inspect %></code>
91
88
  <% end %>
92
89
  </td>
93
- <td data-label="Description">
94
- <%= feature.description || '<em>No description</em>'.html_safe %>
95
- </td>
96
- <td data-label="Actions">
97
- <div class="btn-group">
98
- <a href="<%= magick_admin_ui.feature_path(feature.name) %>" class="btn btn-primary btn-sm">View</a>
99
- <a href="<%= magick_admin_ui.edit_feature_path(feature.name) %>" class="btn btn-secondary btn-sm">Edit</a>
100
- </div>
90
+ <td data-label="Actions" style="text-align: right;">
91
+ <a href="<%= magick_admin_ui.feature_path(feature.name) %>" class="link-subtle">View</a>
101
92
  </td>
102
93
  </tr>
103
94
  <% end %>
104
95
  </tbody>
105
96
  </table>
97
+ <tr id="features_empty_filter" style="display: none;">
98
+ <td colspan="6">
99
+ <div class="empty-state" style="padding: 40px 20px;">
100
+ <h3>No matching features</h3>
101
+ <p>Try adjusting your search or filter.</p>
102
+ </div>
103
+ </td>
104
+ </tr>
106
105
  <% end %>
107
106
  </div>