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.
@@ -4,561 +4,880 @@
4
4
  <title>Magick Feature Flags</title>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <style>
7
- * {
8
- margin: 0;
9
- padding: 0;
10
- box-sizing: border-box;
7
+ :root {
8
+ --color-bg: #f8fafc;
9
+ --color-surface: #ffffff;
10
+ --color-nav: #0f172a;
11
+ --color-nav-text: #e2e8f0;
12
+ --color-text: #1e293b;
13
+ --color-text-secondary: #64748b;
14
+ --color-text-muted: #94a3b8;
15
+ --color-border: #e2e8f0;
16
+ --color-border-subtle: #f1f5f9;
17
+ --color-primary: #6366f1;
18
+ --color-primary-hover: #4f46e5;
19
+ --color-primary-light: #eef2ff;
20
+ --color-primary-text: #4338ca;
21
+ --color-success: #10b981;
22
+ --color-success-hover: #059669;
23
+ --color-success-light: #ecfdf5;
24
+ --color-success-text: #065f46;
25
+ --color-danger: #f43f5e;
26
+ --color-danger-hover: #e11d48;
27
+ --color-danger-light: #fff1f2;
28
+ --color-danger-text: #9f1239;
29
+ --color-warning: #f59e0b;
30
+ --color-warning-light: #fffbeb;
31
+ --color-warning-text: #92400e;
32
+ --color-info-light: #eff6ff;
33
+ --color-info-text: #1e40af;
34
+ --font-stack: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, 'Helvetica Neue', Arial, sans-serif;
35
+ --font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
36
+ --radius-sm: 6px;
37
+ --radius-md: 8px;
38
+ --radius-lg: 12px;
39
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
11
40
  }
41
+
42
+ * { margin: 0; padding: 0; box-sizing: border-box; }
43
+
12
44
  body {
13
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
14
- background: #f5f5f5;
15
- color: #333;
16
- line-height: 1.6;
45
+ font-family: var(--font-stack);
46
+ background: var(--color-bg);
47
+ color: var(--color-text);
48
+ line-height: 1.5;
49
+ font-size: 14px;
50
+ -webkit-font-smoothing: antialiased;
51
+ -moz-osx-font-smoothing: grayscale;
52
+ }
53
+
54
+ .nav {
55
+ background: var(--color-nav);
56
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
57
+ position: sticky;
58
+ top: 0;
59
+ z-index: 100;
60
+ }
61
+ .nav-inner {
62
+ max-width: 1200px;
63
+ margin: 0 auto;
64
+ padding: 0 24px;
65
+ height: 56px;
66
+ display: flex;
67
+ align-items: center;
17
68
  }
69
+ .nav-brand {
70
+ font-size: 15px;
71
+ font-weight: 600;
72
+ color: #ffffff;
73
+ text-decoration: none;
74
+ letter-spacing: -0.01em;
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
+ }
79
+ .nav-brand:hover { color: #ffffff; }
80
+ .nav-brand-icon {
81
+ width: 28px;
82
+ height: 28px;
83
+ background: var(--color-primary);
84
+ border-radius: 6px;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ font-weight: 700;
89
+ font-size: 14px;
90
+ color: #fff;
91
+ }
92
+
18
93
  .container {
19
94
  max-width: 1200px;
20
95
  margin: 0 auto;
21
- padding: 20px;
96
+ padding: 24px;
22
97
  }
23
- header {
24
- background: #fff;
25
- border-bottom: 1px solid #e0e0e0;
26
- padding: 20px 0;
27
- margin-bottom: 30px;
98
+
99
+ /* Flash messages */
100
+ .flash-banner {
101
+ padding: 12px 24px;
102
+ font-size: 13px;
103
+ font-weight: 500;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ animation: flash-in 0.3s ease-out;
28
108
  }
29
- header h1 {
30
- font-size: 24px;
31
- font-weight: 600;
32
- color: #2c3e50;
109
+ .flash-banner-success {
110
+ background: var(--color-success-light);
111
+ color: var(--color-success-text);
112
+ border-bottom: 1px solid #a7f3d0;
33
113
  }
34
- header h1 a {
35
- text-decoration: none;
36
- color: #2c3e50;
37
- transition: color 0.2s;
114
+ .flash-banner-danger {
115
+ background: var(--color-danger-light);
116
+ color: var(--color-danger-text);
117
+ border-bottom: 1px solid #fecdd3;
118
+ }
119
+ .flash-dismiss {
120
+ background: none;
121
+ border: none;
122
+ cursor: pointer;
123
+ font-size: 18px;
124
+ line-height: 1;
125
+ opacity: 0.6;
126
+ color: inherit;
127
+ padding: 0 4px;
38
128
  }
39
- header h1 a:hover {
40
- color: #3498db;
129
+ .flash-dismiss:hover { opacity: 1; }
130
+ @keyframes flash-in {
131
+ from { opacity: 0; transform: translateY(-8px); }
132
+ to { opacity: 1; transform: translateY(0); }
41
133
  }
134
+
135
+ /* Cards */
42
136
  .card {
43
- background: #fff;
44
- border-radius: 8px;
45
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
46
- padding: 24px;
137
+ background: var(--color-surface);
138
+ border: 1px solid var(--color-border);
139
+ border-radius: var(--radius-lg);
47
140
  margin-bottom: 20px;
48
141
  }
142
+ .card-body { padding: 24px; }
49
143
  .card-header {
50
144
  display: flex;
51
145
  justify-content: space-between;
52
146
  align-items: center;
53
- margin-bottom: 20px;
54
- padding-bottom: 16px;
55
- border-bottom: 1px solid #e0e0e0;
147
+ padding: 20px 24px;
148
+ border-bottom: 1px solid var(--color-border);
56
149
  }
57
150
  .card-header h2 {
58
- font-size: 20px;
151
+ font-size: 15px;
59
152
  font-weight: 600;
60
- color: #2c3e50;
61
- }
62
- table {
63
- width: 100%;
64
- border-collapse: collapse;
65
- }
66
- th, td {
67
- padding: 12px;
68
- text-align: left;
69
- border-bottom: 1px solid #e0e0e0;
70
- }
71
- th {
72
- font-weight: 600;
73
- color: #666;
74
- font-size: 14px;
75
- text-transform: uppercase;
76
- letter-spacing: 0.5px;
77
- }
78
- tr:hover {
79
- background: #f9f9f9;
153
+ color: var(--color-text);
80
154
  }
155
+
156
+ /* Buttons */
81
157
  .btn {
82
- display: inline-block;
83
- padding: 8px 16px;
84
- border-radius: 4px;
158
+ display: inline-flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ gap: 6px;
162
+ padding: 8px 14px;
163
+ border-radius: var(--radius-sm);
85
164
  text-decoration: none;
86
- font-size: 14px;
165
+ font-size: 13px;
87
166
  font-weight: 500;
88
167
  cursor: pointer;
89
- border: none;
90
- transition: all 0.2s;
168
+ border: 1px solid transparent;
169
+ transition: all 0.15s ease;
170
+ line-height: 1.4;
171
+ white-space: nowrap;
91
172
  }
92
173
  .btn-primary {
93
- background: #3498db;
174
+ background: var(--color-primary);
94
175
  color: #fff;
176
+ border-color: var(--color-primary);
95
177
  }
96
178
  .btn-primary:hover {
97
- background: #2980b9;
179
+ background: var(--color-primary-hover);
180
+ border-color: var(--color-primary-hover);
98
181
  }
99
182
  .btn-success {
100
- background: #27ae60;
183
+ background: var(--color-success);
101
184
  color: #fff;
185
+ border-color: var(--color-success);
102
186
  }
103
187
  .btn-success:hover {
104
- background: #229954;
188
+ background: var(--color-success-hover);
189
+ border-color: var(--color-success-hover);
105
190
  }
106
191
  .btn-danger {
107
- background: #e74c3c;
192
+ background: var(--color-danger);
108
193
  color: #fff;
194
+ border-color: var(--color-danger);
109
195
  }
110
196
  .btn-danger:hover {
111
- background: #c0392b;
197
+ background: var(--color-danger-hover);
198
+ border-color: var(--color-danger-hover);
112
199
  }
113
200
  .btn-secondary {
114
- background: #95a5a6;
115
- color: #fff;
201
+ background: var(--color-surface);
202
+ color: var(--color-text);
203
+ border-color: var(--color-border);
116
204
  }
117
205
  .btn-secondary:hover {
118
- background: #7f8c8d;
206
+ background: var(--color-bg);
207
+ border-color: #cbd5e1;
208
+ }
209
+ .btn-ghost {
210
+ background: transparent;
211
+ color: var(--color-text-secondary);
212
+ border-color: transparent;
213
+ padding: 6px 10px;
214
+ }
215
+ .btn-ghost:hover {
216
+ background: var(--color-bg);
217
+ color: var(--color-text);
119
218
  }
120
219
  .btn-sm {
121
- padding: 8px 16px;
122
- font-size: 14px;
123
- min-width: 80px;
124
- text-align: center;
220
+ padding: 6px 12px;
221
+ font-size: 13px;
125
222
  }
126
223
  .btn-group {
127
224
  display: flex;
128
225
  gap: 8px;
129
226
  flex-wrap: wrap;
227
+ align-items: center;
130
228
  }
131
- .btn-group .btn {
132
- flex: 0 0 auto;
133
- }
134
- form.button_to {
135
- display: inline-block;
136
- margin: 0;
137
- }
229
+
230
+ form.button_to { display: inline-block; margin: 0; }
138
231
  form.button_to input[type="submit"] {
139
- padding: 8px 16px;
140
- font-size: 14px;
141
- min-width: 80px;
142
- text-align: center;
232
+ display: inline-flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ padding: 8px 14px;
236
+ border-radius: var(--radius-sm);
237
+ font-size: 13px;
238
+ font-weight: 500;
239
+ cursor: pointer;
240
+ border: 1px solid transparent;
241
+ transition: all 0.15s ease;
242
+ line-height: 1.4;
143
243
  }
244
+
245
+ /* Table */
246
+ table { width: 100%; border-collapse: collapse; }
247
+ th {
248
+ padding: 10px 16px;
249
+ text-align: left;
250
+ font-weight: 500;
251
+ color: var(--color-text-secondary);
252
+ font-size: 12px;
253
+ text-transform: uppercase;
254
+ letter-spacing: 0.05em;
255
+ border-bottom: 1px solid var(--color-border);
256
+ background: var(--color-bg);
257
+ }
258
+ td {
259
+ padding: 14px 16px;
260
+ border-bottom: 1px solid var(--color-border-subtle);
261
+ vertical-align: middle;
262
+ }
263
+ tbody tr { transition: background-color 0.1s ease; }
264
+ tbody tr:hover { background: var(--color-bg); }
265
+ tbody tr:last-child td { border-bottom: none; }
266
+
267
+ /* Badges / Pills */
144
268
  .badge {
145
- display: inline-block;
146
- padding: 4px 8px;
147
- border-radius: 4px;
269
+ display: inline-flex;
270
+ align-items: center;
271
+ padding: 2px 8px;
272
+ border-radius: 100px;
148
273
  font-size: 12px;
149
274
  font-weight: 500;
275
+ line-height: 1.6;
150
276
  }
151
277
  .badge-success {
152
- background: #d4edda;
153
- color: #155724;
278
+ background: var(--color-success-light);
279
+ color: var(--color-success-text);
154
280
  }
155
281
  .badge-warning {
156
- background: #fff3cd;
157
- color: #856404;
282
+ background: var(--color-warning-light);
283
+ color: var(--color-warning-text);
158
284
  }
159
285
  .badge-danger {
160
- background: #f8d7da;
161
- color: #721c24;
286
+ background: var(--color-danger-light);
287
+ color: var(--color-danger-text);
162
288
  }
163
289
  .badge-info {
164
- background: #d1ecf1;
165
- color: #0c5460;
290
+ background: var(--color-primary-light);
291
+ color: var(--color-primary-text);
166
292
  }
167
- .form-group {
168
- margin-bottom: 20px;
293
+ .badge-neutral {
294
+ background: var(--color-bg);
295
+ color: var(--color-text-secondary);
296
+ border: 1px solid var(--color-border);
297
+ }
298
+
299
+ /* Status dot */
300
+ .status-indicator {
301
+ display: inline-flex;
302
+ align-items: center;
303
+ gap: 6px;
304
+ font-size: 13px;
305
+ }
306
+ .status-dot {
307
+ width: 8px;
308
+ height: 8px;
309
+ border-radius: 50%;
310
+ flex-shrink: 0;
311
+ }
312
+ .status-dot-green { background: var(--color-success); }
313
+ .status-dot-yellow { background: var(--color-warning); }
314
+ .status-dot-red { background: var(--color-danger); }
315
+
316
+ /* Toggle indicator */
317
+ .toggle-indicator {
318
+ display: inline-flex;
319
+ align-items: center;
320
+ padding: 2px 10px;
321
+ border-radius: 100px;
322
+ font-size: 12px;
323
+ font-weight: 600;
324
+ letter-spacing: 0.02em;
325
+ }
326
+ .toggle-on {
327
+ background: var(--color-success);
328
+ color: #fff;
329
+ }
330
+ .toggle-off {
331
+ background: #e2e8f0;
332
+ color: var(--color-text-secondary);
333
+ }
334
+ .toggle-partial {
335
+ background: var(--color-warning);
336
+ color: #fff;
169
337
  }
170
- label {
338
+
339
+ /* Forms */
340
+ .form-group { margin-bottom: 24px; }
341
+ .form-label {
171
342
  display: block;
172
- margin-bottom: 8px;
343
+ margin-bottom: 6px;
173
344
  font-weight: 500;
174
- color: #333;
345
+ font-size: 13px;
346
+ color: var(--color-text);
347
+ }
348
+ .form-hint {
349
+ font-size: 12px;
350
+ color: var(--color-text-muted);
351
+ margin-top: 6px;
175
352
  }
176
353
  input[type="text"],
177
354
  input[type="number"],
178
355
  select {
179
356
  width: 100%;
180
- padding: 10px;
181
- border: 1px solid #ddd;
182
- border-radius: 4px;
357
+ padding: 8px 12px;
358
+ border: 1px solid var(--color-border);
359
+ border-radius: var(--radius-sm);
183
360
  font-size: 14px;
361
+ font-family: var(--font-stack);
362
+ color: var(--color-text);
363
+ background: var(--color-surface);
364
+ transition: border-color 0.15s, box-shadow 0.15s;
365
+ outline: none;
184
366
  }
185
- input[type="checkbox"] {
186
- width: auto;
367
+ input[type="text"]:focus,
368
+ input[type="number"]:focus,
369
+ select:focus {
370
+ border-color: var(--color-primary);
371
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
187
372
  }
188
- .alert {
189
- padding: 12px 16px;
190
- border-radius: 4px;
191
- margin-bottom: 20px;
373
+ input[type="text"]:disabled,
374
+ input[type="number"]:disabled {
375
+ background: var(--color-bg);
376
+ color: var(--color-text-secondary);
377
+ cursor: not-allowed;
192
378
  }
193
- .alert-success {
194
- background: #d4edda;
195
- color: #155724;
196
- border: 1px solid #c3e6cb;
379
+ input[type="checkbox"] { width: auto; }
380
+
381
+ code {
382
+ font-family: var(--font-mono);
383
+ font-size: 12px;
384
+ background: var(--color-bg);
385
+ border: 1px solid var(--color-border);
386
+ padding: 2px 6px;
387
+ border-radius: 4px;
388
+ color: var(--color-text);
197
389
  }
198
- .alert-danger {
199
- background: #f8d7da;
200
- color: #721c24;
201
- border: 1px solid #f5c6cb;
390
+
391
+ /* Feature detail grid */
392
+ .detail-row {
393
+ display: flex;
394
+ gap: 12px;
395
+ flex-wrap: wrap;
396
+ margin-bottom: 20px;
202
397
  }
203
- .alert-info {
204
- background: #d1ecf1;
205
- color: #0c5460;
206
- border: 1px solid #bee5eb;
398
+ .detail-chip {
399
+ display: flex;
400
+ flex-direction: column;
401
+ gap: 4px;
402
+ padding: 12px 16px;
403
+ background: var(--color-bg);
404
+ border: 1px solid var(--color-border);
405
+ border-radius: var(--radius-md);
406
+ min-width: 120px;
207
407
  }
208
- .feature-details {
209
- display: grid;
210
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
211
- gap: 20px;
212
- margin-bottom: 20px;
408
+ .detail-chip-label {
409
+ font-size: 11px;
410
+ font-weight: 500;
411
+ color: var(--color-text-muted);
412
+ text-transform: uppercase;
413
+ letter-spacing: 0.05em;
213
414
  }
214
- .detail-item {
215
- padding: 16px;
216
- background: #f9f9f9;
217
- border-radius: 4px;
415
+ .detail-chip-value {
416
+ font-size: 14px;
417
+ font-weight: 500;
418
+ color: var(--color-text);
218
419
  }
219
- .detail-label {
420
+
421
+ /* Targeting rules */
422
+ .targeting-section { margin-top: 4px; }
423
+ .targeting-section h4 {
220
424
  font-size: 12px;
221
- color: #666;
425
+ font-weight: 600;
222
426
  text-transform: uppercase;
223
- letter-spacing: 0.5px;
224
- margin-bottom: 4px;
427
+ letter-spacing: 0.05em;
428
+ margin-bottom: 12px;
429
+ color: var(--color-text-secondary);
430
+ }
431
+ .targeting-section-inclusion h4 { color: var(--color-primary-text); }
432
+ .targeting-section-exclusion h4 { color: var(--color-danger-text); }
433
+ .targeting-rule {
434
+ display: flex;
435
+ align-items: baseline;
436
+ gap: 8px;
437
+ padding: 10px 14px;
438
+ background: var(--color-bg);
439
+ border: 1px solid var(--color-border);
440
+ border-radius: var(--radius-sm);
441
+ margin-bottom: 8px;
442
+ }
443
+ .targeting-rule-exclusion {
444
+ background: var(--color-danger-light);
445
+ border-color: #fecdd3;
225
446
  }
226
- .detail-value {
227
- font-size: 16px;
447
+ .targeting-rule-label {
448
+ font-size: 13px;
228
449
  font-weight: 500;
229
- color: #333;
450
+ color: var(--color-text-secondary);
451
+ white-space: nowrap;
230
452
  }
231
- .targeting-rules {
232
- margin-top: 20px;
453
+ .targeting-rule-values {
454
+ display: flex;
455
+ flex-wrap: wrap;
456
+ gap: 4px;
233
457
  }
234
- .targeting-rules ul {
235
- list-style: none;
236
- padding: 0;
458
+ .pill {
459
+ display: inline-flex;
460
+ align-items: center;
461
+ padding: 2px 10px;
462
+ border-radius: 100px;
463
+ font-size: 12px;
464
+ font-weight: 500;
237
465
  }
238
- .targeting-rules li {
239
- padding: 8px 12px;
240
- background: #f9f9f9;
241
- margin-bottom: 8px;
242
- border-radius: 4px;
243
- border-left: 3px solid #3498db;
466
+ .pill-inclusion {
467
+ background: var(--color-primary-light);
468
+ color: var(--color-primary-text);
469
+ }
470
+ .pill-exclusion {
471
+ background: var(--color-danger-light);
472
+ color: var(--color-danger-text);
473
+ }
474
+
475
+ /* Breadcrumb */
476
+ .breadcrumb {
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 8px;
480
+ margin-bottom: 20px;
481
+ font-size: 13px;
482
+ }
483
+ .breadcrumb a {
484
+ color: var(--color-text-secondary);
485
+ text-decoration: none;
486
+ }
487
+ .breadcrumb a:hover { color: var(--color-primary); }
488
+ .breadcrumb-sep { color: var(--color-text-muted); }
489
+ .breadcrumb-current {
490
+ color: var(--color-text);
491
+ font-weight: 500;
244
492
  }
493
+
494
+ /* Stats */
245
495
  .stats-grid {
246
496
  display: grid;
247
497
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
248
- gap: 20px;
249
- margin-top: 20px;
498
+ gap: 16px;
250
499
  }
251
500
  .stat-card {
252
- background: #f9f9f9;
501
+ background: var(--color-surface);
502
+ border: 1px solid var(--color-border);
503
+ border-radius: var(--radius-md);
253
504
  padding: 20px;
254
- border-radius: 4px;
255
- text-align: center;
256
505
  }
257
506
  .stat-value {
258
- font-size: 32px;
259
- font-weight: 600;
260
- color: #3498db;
261
- margin-bottom: 8px;
507
+ font-size: 28px;
508
+ font-weight: 700;
509
+ color: var(--color-text);
510
+ letter-spacing: -0.02em;
511
+ margin-bottom: 4px;
262
512
  }
263
513
  .stat-label {
264
- font-size: 14px;
265
- color: #666;
514
+ font-size: 12px;
515
+ font-weight: 500;
516
+ color: var(--color-text-muted);
266
517
  text-transform: uppercase;
267
- letter-spacing: 0.5px;
518
+ letter-spacing: 0.05em;
268
519
  }
520
+
521
+ /* Empty state */
269
522
  .empty-state {
270
523
  text-align: center;
271
524
  padding: 60px 20px;
272
- color: #999;
525
+ color: var(--color-text-muted);
273
526
  }
274
527
  .empty-state h3 {
275
- margin-bottom: 10px;
276
- color: #666;
528
+ margin-bottom: 8px;
529
+ color: var(--color-text-secondary);
530
+ font-size: 15px;
531
+ font-weight: 600;
532
+ }
533
+ .empty-state p { font-size: 13px; }
534
+
535
+ /* Section divider */
536
+ .section-divider {
537
+ border: none;
538
+ border-top: 1px solid var(--color-border);
539
+ margin: 28px 0;
540
+ }
541
+ .section-title {
542
+ font-size: 15px;
543
+ font-weight: 600;
544
+ color: var(--color-text);
545
+ margin-bottom: 20px;
546
+ }
547
+ .section-title-danger {
548
+ color: var(--color-danger-text);
549
+ }
550
+
551
+ /* Alert */
552
+ .alert {
553
+ padding: 12px 16px;
554
+ border-radius: var(--radius-sm);
555
+ margin-bottom: 16px;
556
+ font-size: 13px;
557
+ }
558
+ .alert-info {
559
+ background: var(--color-info-light);
560
+ color: var(--color-info-text);
561
+ border: 1px solid #bfdbfe;
562
+ }
563
+ .alert code {
564
+ background: rgba(255,255,255,0.5);
565
+ border-color: rgba(0,0,0,0.08);
566
+ }
567
+
568
+ /* Toggle switch (CSS-only) */
569
+ .toggle-switch {
570
+ position: relative;
571
+ display: inline-flex;
572
+ align-items: center;
573
+ gap: 10px;
574
+ cursor: pointer;
575
+ }
576
+ .toggle-switch input[type="checkbox"] {
577
+ position: absolute;
578
+ opacity: 0;
579
+ width: 0;
580
+ height: 0;
581
+ }
582
+ .toggle-track {
583
+ width: 44px;
584
+ height: 24px;
585
+ background: #cbd5e1;
586
+ border-radius: 12px;
587
+ position: relative;
588
+ transition: background 0.2s ease;
589
+ flex-shrink: 0;
590
+ }
591
+ .toggle-track::after {
592
+ content: '';
593
+ position: absolute;
594
+ top: 2px;
595
+ left: 2px;
596
+ width: 20px;
597
+ height: 20px;
598
+ background: #fff;
599
+ border-radius: 50%;
600
+ transition: transform 0.2s ease;
601
+ box-shadow: 0 1px 3px rgba(0,0,0,0.15);
602
+ }
603
+ .toggle-switch input[type="checkbox"]:checked + .toggle-track {
604
+ background: var(--color-success);
605
+ }
606
+ .toggle-switch input[type="checkbox"]:checked + .toggle-track::after {
607
+ transform: translateX(20px);
608
+ }
609
+ .toggle-label-text {
610
+ font-size: 14px;
611
+ font-weight: 500;
612
+ color: var(--color-text);
613
+ user-select: none;
277
614
  }
278
615
 
279
616
  /* User IDs Tag Input */
280
617
  .user-ids-input-container {
281
- border: 1px solid #ddd;
282
- border-radius: 4px;
283
- padding: 8px;
284
- background: #fff;
285
- min-height: 44px;
618
+ border: 1px solid var(--color-border);
619
+ border-radius: var(--radius-sm);
620
+ padding: 6px 8px;
621
+ background: var(--color-surface);
622
+ min-height: 42px;
286
623
  display: flex;
287
624
  flex-wrap: wrap;
288
625
  align-items: center;
289
626
  gap: 6px;
627
+ transition: border-color 0.15s, box-shadow 0.15s;
628
+ }
629
+ .user-ids-input-container:focus-within {
630
+ border-color: var(--color-primary);
631
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
290
632
  }
291
633
  .user-ids-tags {
292
634
  display: flex;
293
635
  flex-wrap: wrap;
294
- gap: 6px;
636
+ gap: 4px;
295
637
  flex: 1;
296
638
  }
297
639
  .user-id-tag {
298
640
  display: inline-flex;
299
641
  align-items: center;
300
- gap: 6px;
301
- background: #3498db;
642
+ gap: 4px;
643
+ background: var(--color-primary);
302
644
  color: #fff;
303
- padding: 4px 8px;
304
- border-radius: 4px;
305
- font-size: 13px;
645
+ padding: 2px 8px;
646
+ border-radius: 100px;
647
+ font-size: 12px;
306
648
  font-weight: 500;
307
649
  }
650
+ .user-id-tag.excluded {
651
+ background: var(--color-danger);
652
+ }
308
653
  .tag-remove {
309
654
  cursor: pointer;
310
- font-size: 18px;
655
+ font-size: 14px;
311
656
  line-height: 1;
312
657
  font-weight: bold;
313
- opacity: 0.8;
314
- transition: opacity 0.2s;
315
- }
316
- .tag-remove:hover {
317
- opacity: 1;
658
+ opacity: 0.7;
659
+ transition: opacity 0.15s;
660
+ margin-left: 2px;
318
661
  }
662
+ .tag-remove:hover { opacity: 1; }
319
663
  .user-ids-input {
320
664
  flex: 1;
321
665
  min-width: 150px;
322
666
  border: none;
323
667
  outline: none;
324
- padding: 6px 8px;
668
+ padding: 4px 6px;
325
669
  font-size: 14px;
670
+ font-family: var(--font-stack);
671
+ color: var(--color-text);
672
+ background: transparent;
326
673
  }
327
- .user-ids-input:focus {
328
- outline: none;
329
- }
330
- .user-ids-input-container:focus-within {
331
- border-color: #3498db;
332
- box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
333
- }
674
+ .user-ids-input::placeholder { color: var(--color-text-muted); }
334
675
 
335
- /* Tags Search Interface */
676
+ /* Checkbox search interface */
336
677
  .tags-search-container {
337
678
  display: flex;
338
679
  align-items: center;
339
680
  gap: 12px;
340
- margin-bottom: 12px;
681
+ margin-bottom: 8px;
341
682
  }
342
683
  .tags-search-input {
343
684
  flex: 1;
344
- padding: 10px 12px;
345
- border: 1px solid #ddd;
346
- border-radius: 4px;
347
- font-size: 14px;
348
- transition: border-color 0.2s, box-shadow 0.2s;
685
+ padding: 8px 12px;
686
+ border: 1px solid var(--color-border);
687
+ border-radius: var(--radius-sm);
688
+ font-size: 13px;
689
+ font-family: var(--font-stack);
690
+ color: var(--color-text);
691
+ background: var(--color-surface);
692
+ outline: none;
693
+ transition: border-color 0.15s, box-shadow 0.15s;
349
694
  }
350
695
  .tags-search-input:focus {
351
- outline: none;
352
- border-color: #3498db;
353
- box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
696
+ border-color: var(--color-primary);
697
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
354
698
  }
699
+ .tags-search-input::placeholder { color: var(--color-text-muted); }
355
700
  .tags-count {
356
- font-size: 13px;
357
- color: #666;
701
+ font-size: 12px;
702
+ color: var(--color-text-muted);
358
703
  white-space: nowrap;
359
704
  }
360
705
  .tags-checkbox-container {
361
- max-height: 300px;
706
+ max-height: 240px;
362
707
  overflow-y: auto;
363
- border: 1px solid #e0e0e0;
364
- border-radius: 4px;
365
- padding: 12px;
366
- background: #fafafa;
708
+ border: 1px solid var(--color-border);
709
+ border-radius: var(--radius-sm);
710
+ padding: 8px;
711
+ background: var(--color-bg);
367
712
  display: flex;
368
713
  flex-direction: column;
369
- gap: 8px;
370
- }
371
- .tags-checkbox-container::-webkit-scrollbar {
372
- width: 8px;
373
- }
374
- .tags-checkbox-container::-webkit-scrollbar-track {
375
- background: #f1f1f1;
376
- border-radius: 4px;
714
+ gap: 2px;
377
715
  }
716
+ .tags-checkbox-container::-webkit-scrollbar { width: 6px; }
717
+ .tags-checkbox-container::-webkit-scrollbar-track { background: transparent; }
378
718
  .tags-checkbox-container::-webkit-scrollbar-thumb {
379
- background: #ccc;
380
- border-radius: 4px;
381
- }
382
- .tags-checkbox-container::-webkit-scrollbar-thumb:hover {
383
- background: #999;
719
+ background: #cbd5e1;
720
+ border-radius: 3px;
384
721
  }
722
+ .tags-checkbox-container::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
385
723
  .tag-checkbox-label {
386
724
  display: inline-flex;
387
725
  align-items: center;
388
726
  cursor: pointer;
389
727
  padding: 6px 8px;
390
- border-radius: 4px;
391
- transition: background-color 0.2s;
392
- }
393
- .tag-checkbox-label:hover {
394
- background-color: #f0f0f0;
395
- }
396
- .tag-checkbox-label.hidden {
397
- display: none;
398
- }
399
- .tag-checkbox {
400
- margin-right: 8px;
401
- cursor: pointer;
728
+ border-radius: var(--radius-sm);
729
+ transition: background-color 0.1s;
402
730
  }
731
+ .tag-checkbox-label:hover { background-color: var(--color-surface); }
732
+ .tag-checkbox-label.hidden { display: none; }
733
+ .tag-checkbox { margin-right: 8px; cursor: pointer; accent-color: var(--color-primary); }
403
734
  .tag-checkbox-text {
404
- font-size: 14px;
405
- color: #333;
735
+ font-size: 13px;
736
+ color: var(--color-text);
406
737
  user-select: none;
407
738
  }
408
- .tag-checkbox:checked + .tag-checkbox-text {
409
- font-weight: 500;
410
- color: #1976d2;
411
- }
739
+ .tag-checkbox:checked + .tag-checkbox-text,
412
740
  .tag-checkbox:checked ~ .tag-checkbox-text {
413
741
  font-weight: 500;
414
- color: #1976d2;
742
+ color: var(--color-primary);
415
743
  }
416
- /* Highlight checked labels */
417
744
  .tag-checkbox-label:has(.tag-checkbox:checked) {
418
- background-color: #e3f2fd;
745
+ background-color: var(--color-primary-light);
419
746
  }
420
- /* Fallback for browsers without :has() support */
421
747
  .tag-checkbox-label.checked {
422
- background-color: #e3f2fd;
748
+ background-color: var(--color-primary-light);
423
749
  }
424
750
  .tag-checkbox-label.checked .tag-checkbox-text {
425
751
  font-weight: 500;
426
- color: #1976d2;
752
+ color: var(--color-primary);
753
+ }
754
+
755
+ /* Exclusion accent */
756
+ .exclusion-border {
757
+ border-left: 3px solid var(--color-danger);
758
+ padding-left: 20px;
759
+ }
760
+
761
+ /* Page title */
762
+ .page-title {
763
+ font-size: 22px;
764
+ font-weight: 700;
765
+ color: var(--color-text);
766
+ letter-spacing: -0.02em;
767
+ }
768
+ .page-subtitle {
769
+ font-size: 14px;
770
+ color: var(--color-text-secondary);
771
+ margin-top: 4px;
772
+ }
773
+
774
+ /* Link */
775
+ .link-subtle {
776
+ color: var(--color-primary);
777
+ text-decoration: none;
778
+ font-size: 13px;
779
+ font-weight: 500;
427
780
  }
781
+ .link-subtle:hover { text-decoration: underline; }
428
782
 
429
783
  /* Mobile Responsive */
430
784
  @media (max-width: 768px) {
431
- .container {
432
- padding: 10px;
433
- }
434
- header {
435
- padding: 15px 0;
436
- }
437
- header h1 {
438
- font-size: 20px;
439
- }
440
- .card {
441
- padding: 16px;
442
- }
785
+ .container { padding: 16px; }
786
+ .nav-inner { padding: 0 16px; }
787
+ .card-body { padding: 16px; }
443
788
  .card-header {
444
789
  flex-direction: column;
445
790
  align-items: flex-start;
446
791
  gap: 12px;
792
+ padding: 16px;
447
793
  }
448
- .card-header h2 {
449
- font-size: 18px;
450
- }
451
- table {
452
- display: block;
453
- overflow-x: auto;
454
- -webkit-overflow-scrolling: touch;
455
- }
456
- thead {
457
- display: none;
458
- }
459
- tr {
794
+ table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
795
+ thead { display: none; }
796
+ tbody tr {
460
797
  display: block;
461
- margin-bottom: 16px;
462
- border: 1px solid #e0e0e0;
463
- border-radius: 8px;
464
- padding: 12px;
465
- background: #fff;
798
+ margin-bottom: 12px;
799
+ border: 1px solid var(--color-border);
800
+ border-radius: var(--radius-md);
801
+ padding: 12px 16px;
802
+ background: var(--color-surface);
466
803
  }
467
804
  td {
468
805
  display: block;
469
- padding: 8px 0;
806
+ padding: 6px 0;
470
807
  border: none;
471
- text-align: left;
472
808
  }
473
809
  td:before {
474
810
  content: attr(data-label);
475
- font-weight: 600;
476
- color: #666;
811
+ font-weight: 500;
812
+ color: var(--color-text-muted);
477
813
  display: block;
478
- margin-bottom: 4px;
479
- font-size: 12px;
814
+ margin-bottom: 2px;
815
+ font-size: 11px;
480
816
  text-transform: uppercase;
481
- letter-spacing: 0.5px;
817
+ letter-spacing: 0.05em;
482
818
  }
483
819
  td:last-child {
484
- border-top: 1px solid #e0e0e0;
820
+ border-top: 1px solid var(--color-border);
485
821
  margin-top: 8px;
486
- padding-top: 12px;
487
- }
488
- .btn-group {
489
- flex-direction: column;
490
- width: 100%;
491
- }
492
- .btn-group .btn {
493
- width: 100%;
494
- }
495
- .feature-details {
496
- grid-template-columns: 1fr;
497
- }
498
- .stats-grid {
499
- grid-template-columns: 1fr;
500
- }
501
- .card-header > div {
502
- width: 100%;
503
- }
504
- .card-header > div .btn {
505
- width: 100%;
506
- margin-bottom: 8px;
507
- }
508
- .user-ids-input-container {
509
- min-height: 40px;
510
- padding: 6px;
511
- }
512
- .user-ids-input {
513
- min-width: 100px;
514
- font-size: 16px; /* Prevents zoom on iOS */
515
- }
516
- .tags-search-container {
517
- flex-direction: column;
518
- align-items: stretch;
519
- }
520
- .tags-count {
521
- text-align: right;
522
- margin-top: 4px;
523
- }
524
- .tags-checkbox-container {
525
- max-height: 200px;
822
+ padding-top: 10px;
526
823
  }
824
+ .btn-group { flex-direction: column; width: 100%; }
825
+ .btn-group .btn { width: 100%; }
826
+ .detail-row { flex-direction: column; }
827
+ .stats-grid { grid-template-columns: 1fr; }
828
+ .filter-bar { flex-direction: column !important; gap: 10px !important; }
829
+ .filter-bar > * { flex: 1 1 100% !important; min-width: 0 !important; }
830
+ .user-ids-input { font-size: 16px; }
831
+ .tags-search-container { flex-direction: column; align-items: stretch; }
832
+ .tags-count { text-align: right; }
833
+ .tags-checkbox-container { max-height: 200px; }
834
+ .exclusion-border { padding-left: 16px; }
527
835
  }
528
836
 
529
- /* Tablet */
530
837
  @media (min-width: 769px) and (max-width: 1024px) {
531
- .container {
532
- padding: 15px;
533
- }
534
- table {
535
- font-size: 14px;
536
- }
537
- th, td {
538
- padding: 10px 8px;
539
- }
838
+ .container { padding: 20px; }
839
+ th, td { padding: 10px 12px; }
540
840
  }
541
841
  </style>
542
842
  </head>
543
843
  <body>
544
- <header>
545
- <div class="container">
546
- <h1><a href="<%= magick_admin_ui.root_path %>">🎩 Magick Feature Flags</a></h1>
844
+ <nav class="nav">
845
+ <div class="nav-inner">
846
+ <a href="<%= magick_admin_ui.root_path %>" class="nav-brand">
847
+ <span class="nav-brand-icon">M</span>
848
+ Magick
849
+ </a>
547
850
  </div>
548
- </header>
851
+ </nav>
549
852
 
550
- <div class="container">
551
- <% if notice %>
552
- <div class="alert alert-success"><%= notice %></div>
553
- <% end %>
554
- <% if alert %>
555
- <div class="alert alert-danger"><%= alert %></div>
556
- <% end %>
853
+ <% if notice %>
854
+ <div class="flash-banner flash-banner-success" data-flash>
855
+ <span><%= notice %></span>
856
+ <button class="flash-dismiss" onclick="this.parentElement.remove()" aria-label="Dismiss">&times;</button>
857
+ </div>
858
+ <% end %>
859
+ <% if alert %>
860
+ <div class="flash-banner flash-banner-danger" data-flash>
861
+ <span><%= alert %></span>
862
+ <button class="flash-dismiss" onclick="this.parentElement.remove()" aria-label="Dismiss">&times;</button>
863
+ </div>
864
+ <% end %>
557
865
 
866
+ <div class="container">
558
867
  <%= yield %>
559
868
  </div>
560
869
 
561
870
  <script>
871
+ // Auto-dismiss flash messages
872
+ document.querySelectorAll('[data-flash]').forEach(function(el) {
873
+ setTimeout(function() {
874
+ el.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
875
+ el.style.opacity = '0';
876
+ el.style.transform = 'translateY(-8px)';
877
+ setTimeout(function() { el.remove(); }, 300);
878
+ }, 5000);
879
+ });
880
+
562
881
  // User IDs Tag Input Handler
563
882
  (function() {
564
883
  const input = document.getElementById('user_ids_input');
@@ -569,7 +888,7 @@
569
888
 
570
889
  function updateHiddenInput() {
571
890
  const tags = Array.from(tagsContainer.querySelectorAll('.user-id-tag'));
572
- const userIds = tags.map(tag => tag.textContent.trim().replace('×', '').trim()).filter(id => id);
891
+ const userIds = tags.map(tag => tag.textContent.trim().replace('\u00d7', '').trim()).filter(id => id);
573
892
  hiddenInput.value = userIds.join(',');
574
893
  }
575
894
 
@@ -577,14 +896,13 @@
577
896
  userId = userId.trim();
578
897
  if (!userId) return;
579
898
 
580
- // Check if already exists
581
899
  const existing = Array.from(tagsContainer.querySelectorAll('.user-id-tag'))
582
- .some(tag => tag.textContent.trim().replace('×', '').trim() === userId);
900
+ .some(tag => tag.textContent.trim().replace('\u00d7', '').trim() === userId);
583
901
  if (existing) return;
584
902
 
585
903
  const tag = document.createElement('span');
586
904
  tag.className = 'user-id-tag';
587
- tag.innerHTML = userId + ' <span class="tag-remove" data-user-id="' + userId + '">×</span>';
905
+ tag.innerHTML = userId + ' <span class="tag-remove" data-user-id="' + userId + '">\u00d7</span>';
588
906
 
589
907
  const removeBtn = tag.querySelector('.tag-remove');
590
908
  removeBtn.addEventListener('click', function() {
@@ -601,7 +919,6 @@
601
919
  e.preventDefault();
602
920
  const value = input.value.trim();
603
921
  if (value) {
604
- // Support comma-separated values
605
922
  value.split(',').forEach(id => {
606
923
  const trimmed = id.trim();
607
924
  if (trimmed) addTag(trimmed);
@@ -611,7 +928,6 @@
611
928
  }
612
929
  });
613
930
 
614
- // Handle paste with comma-separated values
615
931
  input.addEventListener('paste', function(e) {
616
932
  setTimeout(function() {
617
933
  const value = input.value.trim();
@@ -625,7 +941,6 @@
625
941
  }, 10);
626
942
  });
627
943
 
628
- // Remove existing tags when clicked
629
944
  tagsContainer.querySelectorAll('.tag-remove').forEach(btn => {
630
945
  btn.addEventListener('click', function() {
631
946
  this.parentElement.remove();
@@ -656,7 +971,6 @@
656
971
  const checkbox = label.querySelector('.tag-checkbox');
657
972
  const isChecked = checkbox && checkbox.checked;
658
973
 
659
- // Match against tag name, id, or display text
660
974
  const matches = !searchTerm ||
661
975
  tagName.includes(searchTerm) ||
662
976
  tagId.includes(searchTerm) ||
@@ -671,14 +985,12 @@
671
985
  }
672
986
  });
673
987
 
674
- // Sort visible labels: checked first, then unchecked
675
988
  visibleLabels.sort((a, b) => {
676
989
  if (a.checked && !b.checked) return -1;
677
990
  if (!a.checked && b.checked) return 1;
678
991
  return 0;
679
992
  });
680
993
 
681
- // Reorder DOM: checked items first, then unchecked, then hidden
682
994
  visibleLabels.forEach(({ label }) => {
683
995
  checkboxContainer.appendChild(label);
684
996
  });
@@ -689,10 +1001,8 @@
689
1001
  visibleCount.textContent = visibleLabels.length;
690
1002
  }
691
1003
 
692
- // Filter on input
693
1004
  searchInput.addEventListener('input', filterTags);
694
1005
 
695
- // Clear search on Escape key
696
1006
  searchInput.addEventListener('keydown', function(e) {
697
1007
  if (e.key === 'Escape') {
698
1008
  searchInput.value = '';
@@ -701,7 +1011,6 @@
701
1011
  }
702
1012
  });
703
1013
 
704
- // Update checked state classes and reorder on change
705
1014
  checkboxContainer.querySelectorAll('.tag-checkbox').forEach(checkbox => {
706
1015
  function updateCheckedClass() {
707
1016
  const label = checkbox.closest('.tag-checkbox-label');
@@ -712,14 +1021,155 @@
712
1021
  label.classList.remove('checked');
713
1022
  label.setAttribute('data-checked', 'false');
714
1023
  }
715
- // Reorder after change to keep checked items on top
716
1024
  filterTags();
717
1025
  }
718
1026
  checkbox.addEventListener('change', updateCheckedClass);
719
- updateCheckedClass(); // Initial state
1027
+ updateCheckedClass();
720
1028
  });
721
1029
  })();
722
1030
 
1031
+ // Excluded User IDs Tag Input Handler
1032
+ (function() {
1033
+ const input = document.getElementById('excluded_user_ids_input');
1034
+ const tagsContainer = document.getElementById('excluded_user_ids_tags');
1035
+ const hiddenInput = document.getElementById('excluded_user_ids_hidden');
1036
+
1037
+ if (!input || !tagsContainer || !hiddenInput) return;
1038
+
1039
+ function updateHiddenInput() {
1040
+ const tags = Array.from(tagsContainer.querySelectorAll('.user-id-tag'));
1041
+ const userIds = tags.map(tag => tag.textContent.trim().replace('\u00d7', '').trim()).filter(id => id);
1042
+ hiddenInput.value = userIds.join(',');
1043
+ }
1044
+
1045
+ function addTag(userId) {
1046
+ userId = userId.trim();
1047
+ if (!userId) return;
1048
+
1049
+ const existing = Array.from(tagsContainer.querySelectorAll('.user-id-tag'))
1050
+ .some(tag => tag.textContent.trim().replace('\u00d7', '').trim() === userId);
1051
+ if (existing) return;
1052
+
1053
+ const tag = document.createElement('span');
1054
+ tag.className = 'user-id-tag excluded';
1055
+ tag.innerHTML = userId + ' <span class="tag-remove" data-user-id="' + userId + '">\u00d7</span>';
1056
+
1057
+ const removeBtn = tag.querySelector('.tag-remove');
1058
+ removeBtn.addEventListener('click', function() {
1059
+ tag.remove();
1060
+ updateHiddenInput();
1061
+ });
1062
+
1063
+ tagsContainer.appendChild(tag);
1064
+ updateHiddenInput();
1065
+ }
1066
+
1067
+ input.addEventListener('keydown', function(e) {
1068
+ if (e.key === 'Enter' || e.keyCode === 13) {
1069
+ e.preventDefault();
1070
+ const value = input.value.trim();
1071
+ if (value) {
1072
+ value.split(',').forEach(id => {
1073
+ const trimmed = id.trim();
1074
+ if (trimmed) addTag(trimmed);
1075
+ });
1076
+ input.value = '';
1077
+ }
1078
+ }
1079
+ });
1080
+
1081
+ input.addEventListener('paste', function(e) {
1082
+ setTimeout(function() {
1083
+ const value = input.value.trim();
1084
+ if (value.includes(',')) {
1085
+ value.split(',').forEach(id => {
1086
+ const trimmed = id.trim();
1087
+ if (trimmed) addTag(trimmed);
1088
+ });
1089
+ input.value = '';
1090
+ }
1091
+ }, 10);
1092
+ });
1093
+
1094
+ tagsContainer.querySelectorAll('.tag-remove').forEach(btn => {
1095
+ btn.addEventListener('click', function() {
1096
+ this.parentElement.remove();
1097
+ updateHiddenInput();
1098
+ });
1099
+ });
1100
+ })();
1101
+
1102
+ // Generic checkbox search filter (reusable for roles, tags, excluded roles, excluded tags)
1103
+ function initCheckboxSearchFilter(searchId, containerId, visibleCountId, totalCountId) {
1104
+ const searchInput = document.getElementById(searchId);
1105
+ const checkboxContainer = document.getElementById(containerId);
1106
+ const visibleCount = document.getElementById(visibleCountId);
1107
+ const totalCount = document.getElementById(totalCountId);
1108
+
1109
+ if (!searchInput || !checkboxContainer || !visibleCount || !totalCount) return;
1110
+
1111
+ function filterItems() {
1112
+ const searchTerm = searchInput.value.toLowerCase().trim();
1113
+ const labels = Array.from(checkboxContainer.querySelectorAll('.tag-checkbox-label'));
1114
+ let visibleLabels = [];
1115
+ let hiddenLabels = [];
1116
+
1117
+ labels.forEach(label => {
1118
+ const name = label.getAttribute('data-tag-name') || label.getAttribute('data-role-name') || '';
1119
+ const id = label.getAttribute('data-tag-id') || '';
1120
+ const text = (label.querySelector('.tag-checkbox-text')?.textContent || '').toLowerCase();
1121
+ const checkbox = label.querySelector('.tag-checkbox');
1122
+ const isChecked = checkbox && checkbox.checked;
1123
+
1124
+ const matches = !searchTerm || name.includes(searchTerm) || id.includes(searchTerm) || text.includes(searchTerm);
1125
+
1126
+ if (matches) {
1127
+ label.classList.remove('hidden');
1128
+ visibleLabels.push({ label: label, checked: isChecked });
1129
+ } else {
1130
+ label.classList.add('hidden');
1131
+ hiddenLabels.push(label);
1132
+ }
1133
+ });
1134
+
1135
+ visibleLabels.sort((a, b) => (a.checked === b.checked) ? 0 : a.checked ? -1 : 1);
1136
+
1137
+ visibleLabels.forEach(({ label }) => checkboxContainer.appendChild(label));
1138
+ hiddenLabels.forEach(label => checkboxContainer.appendChild(label));
1139
+
1140
+ visibleCount.textContent = visibleLabels.length;
1141
+ }
1142
+
1143
+ searchInput.addEventListener('input', filterItems);
1144
+ searchInput.addEventListener('keydown', function(e) {
1145
+ if (e.key === 'Escape') {
1146
+ searchInput.value = '';
1147
+ filterItems();
1148
+ searchInput.blur();
1149
+ }
1150
+ });
1151
+
1152
+ checkboxContainer.querySelectorAll('.tag-checkbox').forEach(checkbox => {
1153
+ function updateCheckedClass() {
1154
+ const label = checkbox.closest('.tag-checkbox-label');
1155
+ if (checkbox.checked) {
1156
+ label.classList.add('checked');
1157
+ label.setAttribute('data-checked', 'true');
1158
+ } else {
1159
+ label.classList.remove('checked');
1160
+ label.setAttribute('data-checked', 'false');
1161
+ }
1162
+ filterItems();
1163
+ }
1164
+ checkbox.addEventListener('change', updateCheckedClass);
1165
+ updateCheckedClass();
1166
+ });
1167
+ }
1168
+
1169
+ // Initialize excluded roles and tags search filters
1170
+ initCheckboxSearchFilter('excluded_roles_search', 'excluded_roles_checkbox_container', 'excluded_roles_visible_count', 'excluded_roles_total_count');
1171
+ initCheckboxSearchFilter('excluded_tags_search', 'excluded_tags_checkbox_container', 'excluded_tags_visible_count', 'excluded_tags_total_count');
1172
+
723
1173
  // Roles Search Filter
724
1174
  (function() {
725
1175
  const searchInput = document.getElementById('roles_search');
@@ -741,7 +1191,6 @@
741
1191
  const checkbox = label.querySelector('.tag-checkbox');
742
1192
  const isChecked = checkbox && checkbox.checked;
743
1193
 
744
- // Match against role name or display text
745
1194
  const matches = !searchTerm ||
746
1195
  roleName.includes(searchTerm) ||
747
1196
  roleText.includes(searchTerm);
@@ -755,14 +1204,12 @@
755
1204
  }
756
1205
  });
757
1206
 
758
- // Sort visible labels: checked first, then unchecked
759
1207
  visibleLabels.sort((a, b) => {
760
1208
  if (a.checked && !b.checked) return -1;
761
1209
  if (!a.checked && b.checked) return 1;
762
1210
  return 0;
763
1211
  });
764
1212
 
765
- // Reorder DOM: checked items first, then unchecked, then hidden
766
1213
  visibleLabels.forEach(({ label }) => {
767
1214
  checkboxContainer.appendChild(label);
768
1215
  });
@@ -773,10 +1220,8 @@
773
1220
  visibleCount.textContent = visibleLabels.length;
774
1221
  }
775
1222
 
776
- // Filter on input
777
1223
  searchInput.addEventListener('input', filterRoles);
778
1224
 
779
- // Clear search on Escape key
780
1225
  searchInput.addEventListener('keydown', function(e) {
781
1226
  if (e.key === 'Escape') {
782
1227
  searchInput.value = '';
@@ -785,7 +1230,6 @@
785
1230
  }
786
1231
  });
787
1232
 
788
- // Update checked state classes and reorder on change
789
1233
  checkboxContainer.querySelectorAll('.tag-checkbox').forEach(checkbox => {
790
1234
  function updateCheckedClass() {
791
1235
  const label = checkbox.closest('.tag-checkbox-label');
@@ -796,13 +1240,57 @@
796
1240
  label.classList.remove('checked');
797
1241
  label.setAttribute('data-checked', 'false');
798
1242
  }
799
- // Reorder after change to keep checked items on top
800
1243
  filterRoles();
801
1244
  }
802
1245
  checkbox.addEventListener('change', updateCheckedClass);
803
- updateCheckedClass(); // Initial state
1246
+ updateCheckedClass();
804
1247
  });
805
1248
  })();
1249
+
1250
+ // Index page: instant search filter
1251
+ (function() {
1252
+ const searchInput = document.getElementById('feature_search_input');
1253
+ const groupSelect = document.getElementById('feature_group_select');
1254
+ const tableBody = document.getElementById('features_table_body');
1255
+ const emptyState = document.getElementById('features_empty_filter');
1256
+ const featureCount = document.getElementById('feature_count_text');
1257
+
1258
+ if (!searchInput || !tableBody) return;
1259
+
1260
+ function filterFeatures() {
1261
+ const searchTerm = searchInput.value.toLowerCase().trim();
1262
+ const groupTerm = groupSelect ? groupSelect.value : '';
1263
+ const rows = Array.from(tableBody.querySelectorAll('tr[data-feature]'));
1264
+ let visibleCount = 0;
1265
+
1266
+ rows.forEach(row => {
1267
+ const name = (row.getAttribute('data-feature-name') || '').toLowerCase();
1268
+ const displayName = (row.getAttribute('data-feature-display') || '').toLowerCase();
1269
+ const desc = (row.getAttribute('data-feature-desc') || '').toLowerCase();
1270
+ const group = row.getAttribute('data-feature-group') || '';
1271
+
1272
+ const matchesSearch = !searchTerm || name.includes(searchTerm) || displayName.includes(searchTerm) || desc.includes(searchTerm);
1273
+ const matchesGroup = !groupTerm || group === groupTerm;
1274
+
1275
+ if (matchesSearch && matchesGroup) {
1276
+ row.style.display = '';
1277
+ visibleCount++;
1278
+ } else {
1279
+ row.style.display = 'none';
1280
+ }
1281
+ });
1282
+
1283
+ if (emptyState) {
1284
+ emptyState.style.display = visibleCount === 0 ? '' : 'none';
1285
+ }
1286
+ if (featureCount) {
1287
+ featureCount.textContent = visibleCount + ' feature' + (visibleCount !== 1 ? 's' : '');
1288
+ }
1289
+ }
1290
+
1291
+ searchInput.addEventListener('input', filterFeatures);
1292
+ if (groupSelect) groupSelect.addEventListener('change', filterFeatures);
1293
+ })();
806
1294
  </script>
807
1295
  </body>
808
1296
  </html>