silex_ui 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +184 -0
  3. data/LICENSE.txt +6 -0
  4. data/README.md +97 -7
  5. data/app/assets/javascripts/silex_ui/controllers/clipboard_controller.js +10 -10
  6. data/app/assets/javascripts/silex_ui/controllers/dropdown_controller.js +24 -0
  7. data/app/assets/javascripts/silex_ui/controllers/inline_edit_controller.js +39 -0
  8. data/app/assets/javascripts/silex_ui/controllers/remote_modal_controller.js +22 -0
  9. data/app/assets/javascripts/silex_ui/controllers/sidebar_controller.js +34 -0
  10. data/app/assets/javascripts/silex_ui/controllers/theme_controller.js +43 -0
  11. data/app/assets/javascripts/silex_ui/controllers/tooltip_controller.js +65 -0
  12. data/app/assets/javascripts/silex_ui/index.js +22 -1
  13. data/app/assets/stylesheets/silex_ui/theme.css +528 -42
  14. data/app/components/silex_ui/action_list_component.rb +76 -0
  15. data/app/components/silex_ui/alert_component.html.erb +14 -6
  16. data/app/components/silex_ui/alert_component.rb +24 -12
  17. data/app/components/silex_ui/avatar_component.rb +56 -0
  18. data/app/components/silex_ui/badge_component.rb +21 -17
  19. data/app/components/silex_ui/base_component.rb +10 -3
  20. data/app/components/silex_ui/breakdown_component.html.erb +20 -6
  21. data/app/components/silex_ui/breakdown_component.rb +21 -4
  22. data/app/components/silex_ui/button_component.rb +31 -16
  23. data/app/components/silex_ui/card_component.html.erb +7 -4
  24. data/app/components/silex_ui/card_component.rb +9 -6
  25. data/app/components/silex_ui/copy_button_component.rb +17 -8
  26. data/app/components/silex_ui/dropdown_component.html.erb +13 -0
  27. data/app/components/silex_ui/dropdown_component.rb +82 -0
  28. data/app/components/silex_ui/empty_state_component.html.erb +13 -0
  29. data/app/components/silex_ui/empty_state_component.rb +6 -9
  30. data/app/components/silex_ui/field_component.html.erb +4 -4
  31. data/app/components/silex_ui/field_component.rb +8 -5
  32. data/app/components/silex_ui/filter_bar_component.html.erb +22 -0
  33. data/app/components/silex_ui/filter_bar_component.rb +54 -0
  34. data/app/components/silex_ui/filter_field_component.html.erb +10 -0
  35. data/app/components/silex_ui/filter_field_component.rb +69 -0
  36. data/app/components/silex_ui/flash_component.html.erb +1 -1
  37. data/app/components/silex_ui/icon_component.rb +211 -0
  38. data/app/components/silex_ui/inline_edit_component.html.erb +23 -0
  39. data/app/components/silex_ui/inline_edit_component.rb +47 -0
  40. data/app/components/silex_ui/modal_component.html.erb +4 -2
  41. data/app/components/silex_ui/modal_component.rb +3 -2
  42. data/app/components/silex_ui/nav_component.html.erb +5 -4
  43. data/app/components/silex_ui/nav_component.rb +22 -7
  44. data/app/components/silex_ui/page_header_component.html.erb +38 -10
  45. data/app/components/silex_ui/page_header_component.rb +97 -11
  46. data/app/components/silex_ui/pagination_component.html.erb +11 -10
  47. data/app/components/silex_ui/pagination_component.rb +1 -2
  48. data/app/components/silex_ui/panel_component.html.erb +27 -0
  49. data/app/components/silex_ui/panel_component.rb +60 -0
  50. data/app/components/silex_ui/progress_component.html.erb +21 -0
  51. data/app/components/silex_ui/progress_component.rb +42 -0
  52. data/app/components/silex_ui/remote_modal_component.html.erb +30 -0
  53. data/app/components/silex_ui/remote_modal_component.rb +54 -0
  54. data/app/components/silex_ui/section_component.html.erb +5 -2
  55. data/app/components/silex_ui/section_component.rb +8 -8
  56. data/app/components/silex_ui/shell_component.html.erb +40 -0
  57. data/app/components/silex_ui/shell_component.rb +106 -0
  58. data/app/components/silex_ui/stat_card_component.html.erb +31 -6
  59. data/app/components/silex_ui/stat_card_component.rb +48 -28
  60. data/app/components/silex_ui/stat_grid_component.rb +8 -18
  61. data/app/components/silex_ui/table_component.html.erb +10 -8
  62. data/app/components/silex_ui/table_component.rb +52 -20
  63. data/app/components/silex_ui/theme_toggle_component.html.erb +11 -0
  64. data/app/components/silex_ui/theme_toggle_component.rb +29 -0
  65. data/app/components/silex_ui/tones.rb +72 -0
  66. data/app/views/silex_ui/catalogue/_specimen.html.erb +4 -4
  67. data/app/views/silex_ui/catalogue/show.html.erb +332 -187
  68. data/lib/silex_ui/version.rb +1 -1
  69. data/lib/silex_ui.rb +8 -0
  70. metadata +31 -2
@@ -5,55 +5,541 @@
5
5
  * `--color-ink-900` suffit à rendre `text-ink-900`, `bg-ink-900`, `border-ink-900`
6
6
  * disponibles dans l'application comme dans les composants du gem.
7
7
  *
8
- * Le ton : un fond sable chaud, une encre presque noire, et des accents qui se
9
- * posent sur le sable au lieu de crier par-dessus. */
8
+ * Les valeurs sont celles du Silex : une base ardoise froide, un indigo qui vire
9
+ * au violet pour l'accent, et des pastels pour les états. */
10
+
11
+ @import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap");
12
+
13
+ /* Tailwind v4 pilote le thème sombre par une classe ou un attribut : on le
14
+ * déclenche sur `data-theme="dark"`, comme le fait le Silex sur son <body>. */
15
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
10
16
 
11
17
  @theme {
12
- --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
13
-
14
- /* Sablele sol. Du plus clair (fond de page) au plus soutenu (bordures). */
15
- --color-sand-50: #faf8f4;
16
- --color-sand-100: #f4f0e8;
17
- --color-sand-200: #e8e1d4;
18
- --color-sand-300: #d6ccb9;
19
-
20
- /* Encre — jamais un noir pur, du texte discret au texte plein. */
21
- --color-ink-300: #bdb9b3;
22
- --color-ink-400: #9a9691;
23
- --color-ink-600: #6b6763;
24
- --color-ink-900: #1c1a17;
25
-
26
- /* Alerte — terracotta : ce qui a lapsé, ce qui est cassé. */
27
- --color-alert-50: #fbf1ee;
28
- --color-alert-100: #f2dcd5;
29
- --color-alert-300: #d99e8c;
30
- --color-alert-700: #9c3b26;
31
-
32
- /* Attention — ocre : ce qui est sur le point de basculer. */
33
- --color-warn-50: #fbf5e8;
34
- --color-warn-100: #f0e5c8;
35
- --color-warn-300: #d8bd7c;
36
- --color-warn-700: #8a6318;
37
-
38
- /* Validation — sauge : ce qui est en règle. */
39
- --color-ok-50: #f1f5ef;
40
- --color-ok-100: #dfe8da;
41
- --color-ok-300: #a8bf9c;
42
- --color-ok-700: #4a6b3d;
43
-
44
- /* Information — bleu ardoise : neutre, factuel, sans urgence. */
45
- --color-info-50: #eff2f6;
46
- --color-info-100: #dde4ee;
47
- --color-info-300: #9db0cc;
48
- --color-info-700: #3c5578;
18
+ --font-sans: "Atkinson Hyperlegible", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
19
+
20
+ /* Surfacesdu fond de page à la bordure. 0 est le blanc des cartes. */
21
+ --color-surface-0: #ffffff;
22
+ --color-surface-50: #f8fafc;
23
+ --color-surface-100: #f1f5f9;
24
+ --color-surface-200: #e2e8f0;
25
+ --color-surface-300: #cbd5e1;
26
+
27
+ /* Encre — du texte plein au texte que l'on ne lit qu'en le cherchant. */
28
+ --color-ink-950: #0f172a;
29
+ --color-ink-900: #1e293b;
30
+ --color-ink-700: #475569;
31
+ --color-ink-500: #64748b;
32
+ --color-ink-400: #94a3b8;
33
+ --color-ink-300: #cbd5e1;
34
+
35
+ /* Marque — l'indigo du Silex, et le violet vers lequel ses dégradés glissent. */
36
+ --color-brand-50: #eef2ff;
37
+ --color-brand-100: #e0e7ff;
38
+ --color-brand-300: #a5b4fc;
39
+ --color-brand-500: #667eea;
40
+ --color-brand-600: #5a67d8;
41
+ --color-brand-700: #764ba2;
42
+
43
+ /* États. Chaque famille : un fond pastel, une bordure, un texte soutenu. */
44
+ --color-ok-50: #dcfce7;
45
+ --color-ok-100: #bbf7d0;
46
+ --color-ok-300: #86efac;
47
+ --color-ok-500: #10b981;
48
+ --color-ok-700: #15803d;
49
+
50
+ --color-warn-50: #fef3c7;
51
+ --color-warn-100: #fde68a;
52
+ --color-warn-300: #fcd34d;
53
+ --color-warn-500: #f59e0b;
54
+ --color-warn-700: #b45309;
55
+
56
+ --color-alert-50: #fee2e2;
57
+ --color-alert-100: #fecaca;
58
+ --color-alert-300: #fca5a5;
59
+ --color-alert-500: #ef4444;
60
+ --color-alert-700: #b91c1c;
61
+
62
+ --color-info-50: #dbeafe;
63
+ --color-info-100: #bfdbfe;
64
+ --color-info-300: #93c5fd;
65
+ --color-info-500: #3b82f6;
66
+ --color-info-700: #1d4ed8;
67
+
68
+ /* Teintes de catégorisation — celles des pastilles et des compteurs du Silex,
69
+ * quand il ne s'agit plus d'un état mais d'une famille de choses. */
70
+ --color-purple-50: #ede9fe;
71
+ --color-purple-300: #c4b5fd;
72
+ --color-purple-500: #8b5cf6;
73
+ --color-purple-700: #6d28d9;
74
+
75
+ --color-pink-50: #fce7f3;
76
+ --color-pink-300: #f9a8d4;
77
+ --color-pink-500: #ec4899;
78
+ --color-pink-700: #be185d;
79
+
80
+ --color-teal-50: #ccfbf1;
81
+ --color-teal-300: #5eead4;
82
+ --color-teal-500: #14b8a6;
83
+ --color-teal-700: #0f766e;
84
+
85
+ --color-cyan-50: #cffafe;
86
+ --color-cyan-300: #67e8f9;
87
+ --color-cyan-500: #06b6d4;
88
+ --color-cyan-700: #0e7490;
89
+
90
+ /* Rayons — le Silex arrondit franchement : 12px pour les surfaces,
91
+ * 8px pour les pastilles, 14px pour les cartes qui se soulèvent. */
92
+ --radius-badge: 0.5rem;
93
+ --radius-surface: 0.75rem;
94
+ --radius-card: 0.875rem;
95
+ --radius-panel: 1.125rem;
96
+ --radius-hero: 1.25rem;
97
+
98
+ /* Ombres — posées, jamais lourdes ; la seconde est celle du survol. */
99
+ --shadow-card: 0 1px 3px rgb(0 0 0 / 0.05);
100
+ --shadow-card-hover: 0 4px 16px rgb(0 0 0 / 0.1);
101
+ --shadow-panel: 0 1px 3px rgb(0 0 0 / 0.05);
102
+ --shadow-panel-hover: 0 4px 12px rgb(0 0 0 / 0.08);
103
+
104
+ /* Le carré arrondi qui porte une icône — compteur, en-tête, rail. */
105
+ --spacing-icon-tile: 3.25rem;
106
+
107
+ /* Le rail du Silex, et la barre supérieure qui le remplace sur mobile. */
108
+ --spacing-rail: 5rem;
109
+ --spacing-rail-tile: 2.625rem;
110
+ --spacing-15: 3.75rem;
111
+ --spacing-70: 17.5rem;
112
+ }
113
+
114
+ /* Thème sombre — les valeurs du Silex, redéclarées plutôt que dérivées : un
115
+ * gris sombre n'est pas un gris clair inversé. */
116
+ [data-theme="dark"] {
117
+ --color-surface-0: #161b22;
118
+ --color-surface-50: #0d1117;
119
+ --color-surface-100: #161b22;
120
+ --color-surface-200: #30363d;
121
+ --color-surface-300: #3d444d;
122
+
123
+ --color-ink-950: #f8fafc;
124
+ --color-ink-900: #e6edf3;
125
+ --color-ink-700: #c9d1d9;
126
+ --color-ink-500: #8b949e;
127
+ --color-ink-400: #6e7681;
128
+ --color-ink-300: #484f58;
129
+
130
+ --shadow-card: 0 1px 3px rgb(0 0 0 / 0.4);
131
+ --shadow-card-hover: 0 4px 16px rgb(0 0 0 / 0.6);
132
+
133
+ /* Les états aussi changent d'échelle. Un pastel posé sur du sombre reste un
134
+ * pastel : la ligne expirée deviendrait un rectangle rose illisible. On
135
+ * inverse donc le rapport — le fond devient une teinte sourde, et c'est le
136
+ * texte qui s'éclaircit. */
137
+ --color-brand-50: #1e2140;
138
+ --color-brand-100: #2a2f5c;
139
+ --color-brand-300: #4c5490;
140
+ --color-brand-600: #a5b4fc;
141
+
142
+ --color-ok-50: #0f2a1a;
143
+ --color-ok-100: #17402a;
144
+ --color-ok-300: #2c6b45;
145
+ --color-ok-700: #86efac;
146
+
147
+ --color-warn-50: #33260a;
148
+ --color-warn-100: #4a3710;
149
+ --color-warn-300: #7a5c1c;
150
+ --color-warn-700: #fcd34d;
151
+
152
+ --color-alert-50: #3a1416;
153
+ --color-alert-100: #521c20;
154
+ --color-alert-300: #8a2f34;
155
+ --color-alert-700: #fca5a5;
156
+
157
+ --color-info-50: #0f2340;
158
+ --color-info-100: #17335c;
159
+ --color-info-300: #2b5590;
160
+ --color-info-700: #93c5fd;
161
+
162
+ --color-purple-50: #251b40;
163
+ --color-purple-300: #5b45a0;
164
+ --color-purple-700: #c4b5fd;
165
+
166
+ --color-pink-50: #3a1428;
167
+ --color-pink-300: #8a3663;
168
+ --color-pink-700: #f9a8d4;
169
+
170
+ --color-teal-50: #0c2e2b;
171
+ --color-teal-300: #1f6b64;
172
+ --color-teal-700: #5eead4;
173
+
174
+ --color-cyan-50: #0c2b33;
175
+ --color-cyan-300: #1c6070;
176
+ --color-cyan-700: #67e8f9;
177
+ }
178
+
179
+ /* Le panneau du rail sur mobile. Deux utilitaires Tailwind qui écrivent la même
180
+ * propriété se départagent par leur ordre dans la feuille, pas par l'ordre des
181
+ * classes dans le HTML : pour un état qui bascule, une règle nommée se lit et se
182
+ * débogue mieux qu'un empilement d'utilitaires. */
183
+ @layer components {
184
+ .silex-ui-tooltip {
185
+ position: fixed;
186
+ z-index: 60;
187
+ padding: 0.375rem 0.625rem;
188
+ border-radius: var(--radius-badge);
189
+ background: var(--color-ink-900);
190
+ color: var(--color-surface-0);
191
+ font-size: 0.75rem;
192
+ font-weight: 600;
193
+ white-space: nowrap;
194
+ pointer-events: none;
195
+ box-shadow: var(--shadow-card-hover);
196
+ opacity: 0;
197
+ transition: opacity 0.15s ease;
198
+ }
199
+
200
+ .silex-ui-tooltip[data-visible] {
201
+ opacity: 1;
202
+ }
203
+
204
+ /* Le filtre flottant du Silex : le libellé vit dans le champ et remonte dès
205
+ * qu'une valeur est choisie. Tout se joue en CSS — `:has(option:checked)` pour
206
+ * un select, `:placeholder-shown` pour un champ texte — donc aucun contrôleur
207
+ * n'a besoin de savoir si un filtre est rempli. */
208
+ .silex-ui-filter {
209
+ position: relative;
210
+ min-width: 11.25rem;
211
+ background: var(--color-surface-50);
212
+ border: 1.5px solid var(--color-surface-200);
213
+ border-radius: 0.625rem;
214
+ transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
215
+ }
216
+
217
+ .silex-ui-filter:hover {
218
+ border-color: var(--color-surface-300);
219
+ }
220
+
221
+ .silex-ui-filter:focus-within {
222
+ border-color: var(--color-brand-500);
223
+ background: var(--color-surface-0);
224
+ box-shadow: 0 0 0 3px rgb(102 126 234 / 0.1);
225
+ }
226
+
227
+ .silex-ui-filter__icon {
228
+ position: absolute;
229
+ top: 50%;
230
+ left: 0.875rem;
231
+ z-index: 2;
232
+ transform: translateY(-50%);
233
+ color: var(--color-ink-400);
234
+ pointer-events: none;
235
+ transition: color 0.2s ease;
236
+ }
237
+
238
+ .silex-ui-filter:focus-within .silex-ui-filter__icon {
239
+ color: var(--color-brand-500);
240
+ }
241
+
242
+ .silex-ui-filter__label {
243
+ position: absolute;
244
+ top: 50%;
245
+ left: 2.5rem;
246
+ transform: translateY(-50%);
247
+ font-size: 0.875rem;
248
+ font-weight: 500;
249
+ color: var(--color-ink-500);
250
+ pointer-events: none;
251
+ transition: all 0.2s ease;
252
+ }
253
+
254
+ .silex-ui-filter:focus-within .silex-ui-filter__label,
255
+ .silex-ui-filter select:not(:has(option[value=""]:checked)) ~ .silex-ui-filter__label,
256
+ .silex-ui-filter input:not(:placeholder-shown) ~ .silex-ui-filter__label {
257
+ top: 0.5rem;
258
+ transform: none;
259
+ font-size: 0.6875rem;
260
+ font-weight: 600;
261
+ color: var(--color-brand-500);
262
+ }
263
+
264
+ .silex-ui-filter__control {
265
+ width: 100%;
266
+ padding: 1.625rem 1rem 0.375rem 2.5rem;
267
+ border: none;
268
+ background: transparent;
269
+ font-size: 0.875rem;
270
+ color: var(--color-ink-900);
271
+ }
272
+
273
+ .silex-ui-filter__control:focus {
274
+ outline: none;
275
+ }
276
+
277
+ select.silex-ui-filter__control {
278
+ padding-right: 2.5rem;
279
+ appearance: none;
280
+ cursor: pointer;
281
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
282
+ background-repeat: no-repeat;
283
+ background-position: right 1rem center;
284
+ }
285
+
286
+ input.silex-ui-filter__control::placeholder {
287
+ color: var(--color-ink-400);
288
+ }
289
+
290
+ /* Variante compacte : même mécanique, 42 px de haut au lieu de 52. Sur un
291
+ * écran de liste, sept filtres à pleine taille prennent plus de place que la
292
+ * liste qu'ils filtrent. */
293
+ .silex-ui-filter--sm {
294
+ min-width: 9rem;
295
+ }
296
+
297
+ .silex-ui-filter--sm .silex-ui-filter__control {
298
+ padding: 1.25rem 0.75rem 0.25rem 2.125rem;
299
+ font-size: 0.8125rem;
300
+ }
301
+
302
+ .silex-ui-filter--sm select.silex-ui-filter__control {
303
+ padding-right: 2rem;
304
+ background-position: right 0.75rem center;
305
+ }
306
+
307
+ .silex-ui-filter--sm .silex-ui-filter__icon {
308
+ left: 0.75rem;
309
+ }
310
+
311
+ .silex-ui-filter--sm .silex-ui-filter__label {
312
+ left: 2.125rem;
313
+ font-size: 0.8125rem;
314
+ }
315
+
316
+ .silex-ui-filter--sm:focus-within .silex-ui-filter__label,
317
+ .silex-ui-filter--sm select:not(:has(option[value=""]:checked)) ~ .silex-ui-filter__label,
318
+ .silex-ui-filter--sm input:not(:placeholder-shown) ~ .silex-ui-filter__label {
319
+ top: 0.3125rem;
320
+ font-size: 0.625rem;
321
+ }
322
+
323
+ .silex-ui-burger span {
324
+ display: block;
325
+ width: 100%;
326
+ height: 3px;
327
+ border-radius: 10px;
328
+ background: var(--color-ink-700);
329
+ transition: transform 0.3s ease, opacity 0.3s ease;
330
+ transform-origin: center;
331
+ }
332
+
333
+ .silex-ui-burger:hover span {
334
+ background: var(--color-purple-500);
335
+ }
336
+
337
+ .silex-ui-burger[data-open] span:nth-child(1) {
338
+ transform: translateY(7px) rotate(45deg);
339
+ }
340
+
341
+ .silex-ui-burger[data-open] span:nth-child(2) {
342
+ opacity: 0;
343
+ }
344
+
345
+ .silex-ui-burger[data-open] span:nth-child(3) {
346
+ transform: translateY(-7px) rotate(-45deg);
347
+ }
348
+
349
+ .silex-ui-panel {
350
+ translate: -100% 0;
351
+ transition: translate 0.3s ease;
352
+ }
353
+
354
+ .silex-ui-panel[data-open] {
355
+ translate: 0 0;
356
+ }
357
+
358
+ @media (min-width: 48rem) {
359
+ .silex-ui-tooltip {
360
+ position: fixed;
361
+ z-index: 60;
362
+ padding: 0.375rem 0.625rem;
363
+ border-radius: var(--radius-badge);
364
+ background: var(--color-ink-900);
365
+ color: var(--color-surface-0);
366
+ font-size: 0.75rem;
367
+ font-weight: 600;
368
+ white-space: nowrap;
369
+ pointer-events: none;
370
+ box-shadow: var(--shadow-card-hover);
371
+ opacity: 0;
372
+ transition: opacity 0.15s ease;
373
+ }
374
+
375
+ .silex-ui-tooltip[data-visible] {
376
+ opacity: 1;
377
+ }
378
+
379
+ /* Le filtre flottant du Silex : le libellé vit dans le champ et remonte dès
380
+ * qu'une valeur est choisie. Tout se joue en CSS — `:has(option:checked)` pour
381
+ * un select, `:placeholder-shown` pour un champ texte — donc aucun contrôleur
382
+ * n'a besoin de savoir si un filtre est rempli. */
383
+ .silex-ui-filter {
384
+ position: relative;
385
+ min-width: 11.25rem;
386
+ background: var(--color-surface-50);
387
+ border: 1.5px solid var(--color-surface-200);
388
+ border-radius: 0.625rem;
389
+ transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
390
+ }
391
+
392
+ .silex-ui-filter:hover {
393
+ border-color: var(--color-surface-300);
394
+ }
395
+
396
+ .silex-ui-filter:focus-within {
397
+ border-color: var(--color-brand-500);
398
+ background: var(--color-surface-0);
399
+ box-shadow: 0 0 0 3px rgb(102 126 234 / 0.1);
400
+ }
401
+
402
+ .silex-ui-filter__icon {
403
+ position: absolute;
404
+ top: 50%;
405
+ left: 0.875rem;
406
+ z-index: 2;
407
+ transform: translateY(-50%);
408
+ color: var(--color-ink-400);
409
+ pointer-events: none;
410
+ transition: color 0.2s ease;
411
+ }
412
+
413
+ .silex-ui-filter:focus-within .silex-ui-filter__icon {
414
+ color: var(--color-brand-500);
415
+ }
416
+
417
+ .silex-ui-filter__label {
418
+ position: absolute;
419
+ top: 50%;
420
+ left: 2.5rem;
421
+ transform: translateY(-50%);
422
+ font-size: 0.875rem;
423
+ font-weight: 500;
424
+ color: var(--color-ink-500);
425
+ pointer-events: none;
426
+ transition: all 0.2s ease;
427
+ }
428
+
429
+ .silex-ui-filter:focus-within .silex-ui-filter__label,
430
+ .silex-ui-filter select:not(:has(option[value=""]:checked)) ~ .silex-ui-filter__label,
431
+ .silex-ui-filter input:not(:placeholder-shown) ~ .silex-ui-filter__label {
432
+ top: 0.5rem;
433
+ transform: none;
434
+ font-size: 0.6875rem;
435
+ font-weight: 600;
436
+ color: var(--color-brand-500);
437
+ }
438
+
439
+ .silex-ui-filter__control {
440
+ width: 100%;
441
+ padding: 1.625rem 1rem 0.375rem 2.5rem;
442
+ border: none;
443
+ background: transparent;
444
+ font-size: 0.875rem;
445
+ color: var(--color-ink-900);
446
+ }
447
+
448
+ .silex-ui-filter__control:focus {
449
+ outline: none;
450
+ }
451
+
452
+ select.silex-ui-filter__control {
453
+ padding-right: 2.5rem;
454
+ appearance: none;
455
+ cursor: pointer;
456
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
457
+ background-repeat: no-repeat;
458
+ background-position: right 1rem center;
459
+ }
460
+
461
+ input.silex-ui-filter__control::placeholder {
462
+ color: var(--color-ink-400);
463
+ }
464
+
465
+ /* Variante compacte : même mécanique, 42 px de haut au lieu de 52. Sur un
466
+ * écran de liste, sept filtres à pleine taille prennent plus de place que la
467
+ * liste qu'ils filtrent. */
468
+ .silex-ui-filter--sm {
469
+ min-width: 9rem;
470
+ }
471
+
472
+ .silex-ui-filter--sm .silex-ui-filter__control {
473
+ padding: 1.25rem 0.75rem 0.25rem 2.125rem;
474
+ font-size: 0.8125rem;
475
+ }
476
+
477
+ .silex-ui-filter--sm select.silex-ui-filter__control {
478
+ padding-right: 2rem;
479
+ background-position: right 0.75rem center;
480
+ }
481
+
482
+ .silex-ui-filter--sm .silex-ui-filter__icon {
483
+ left: 0.75rem;
484
+ }
485
+
486
+ .silex-ui-filter--sm .silex-ui-filter__label {
487
+ left: 2.125rem;
488
+ font-size: 0.8125rem;
489
+ }
490
+
491
+ .silex-ui-filter--sm:focus-within .silex-ui-filter__label,
492
+ .silex-ui-filter--sm select:not(:has(option[value=""]:checked)) ~ .silex-ui-filter__label,
493
+ .silex-ui-filter--sm input:not(:placeholder-shown) ~ .silex-ui-filter__label {
494
+ top: 0.3125rem;
495
+ font-size: 0.625rem;
496
+ }
497
+
498
+ .silex-ui-burger span {
499
+ display: block;
500
+ width: 100%;
501
+ height: 3px;
502
+ border-radius: 10px;
503
+ background: var(--color-ink-700);
504
+ transition: transform 0.3s ease, opacity 0.3s ease;
505
+ transform-origin: center;
506
+ }
507
+
508
+ .silex-ui-burger:hover span {
509
+ background: var(--color-purple-500);
510
+ }
511
+
512
+ .silex-ui-burger[data-open] span:nth-child(1) {
513
+ transform: translateY(7px) rotate(45deg);
514
+ }
515
+
516
+ .silex-ui-burger[data-open] span:nth-child(2) {
517
+ opacity: 0;
518
+ }
519
+
520
+ .silex-ui-burger[data-open] span:nth-child(3) {
521
+ transform: translateY(-7px) rotate(-45deg);
522
+ }
523
+
524
+ .silex-ui-panel {
525
+ translate: 0 0;
526
+ }
527
+ }
49
528
  }
50
529
 
51
- /* Rendu par défaut du document. L'application peut l'écraser, mais partir de là
52
- * évite que chaque app redécouvre son fond et son antialiasing. */
53
530
  @layer base {
531
+ html {
532
+ color-scheme: light;
533
+ }
534
+
535
+ html[data-theme="dark"] {
536
+ color-scheme: dark;
537
+ }
538
+
54
539
  body {
55
- background-color: var(--color-sand-50);
540
+ background-color: var(--color-surface-50);
56
541
  color: var(--color-ink-900);
57
542
  -webkit-font-smoothing: antialiased;
543
+ transition: background-color 0.3s ease, color 0.3s ease;
58
544
  }
59
545
  }
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SilexUi
4
+ # La liste « Actions requises » du Silex : une pastille colorée, ce qu'il faut
5
+ # faire, pourquoi, et un chevron qui mène là où on le fait.
6
+ #
7
+ # <%= render SilexUi::ActionListComponent.new do |list| %>
8
+ # <% list.with_item(title: "130 campagnes sans budget", subtitle: "Budget non défini",
9
+ # icon: "alert", tone: :alert, href: path) %>
10
+ # <% end %>
11
+ class ActionListComponent < BaseComponent
12
+ renders_many :items, "ItemComponent"
13
+
14
+ def call
15
+ tag.ul(safe_join(items), class: merge_class("divide-y divide-surface-200"))
16
+ end
17
+
18
+ class ItemComponent < ViewComponent::Base
19
+ def initialize(title:, subtitle: nil, icon: nil, tone: :neutral, href: nil, meta: nil)
20
+ @title = title
21
+ @subtitle = subtitle
22
+ @icon = icon
23
+ @tone = tone
24
+ @href = href
25
+ @meta = meta
26
+ end
27
+
28
+ def call
29
+ tag.li(@href.present? ? link_to(body, @href, class: row_classes) : tag.div(body, class: row_classes))
30
+ end
31
+
32
+ private
33
+
34
+ def body
35
+ safe_join([ tile, text, meta_tag, chevron ].compact)
36
+ end
37
+
38
+ def tile
39
+ return nil if @icon.blank?
40
+
41
+ tag.span(render(SilexUi::IconComponent.new(name: @icon, size: :md)),
42
+ class: class_names("flex size-11 shrink-0 items-center justify-center rounded-surface",
43
+ SilexUi::Tones.tile(@tone)))
44
+ end
45
+
46
+ def text
47
+ tag.span(class: "min-w-0 flex-1") do
48
+ safe_join([
49
+ tag.span(@title, class: "block truncate text-sm font-bold text-ink-900"),
50
+ (tag.span(@subtitle, class: "mt-0.5 block truncate text-[13px] text-ink-500") if @subtitle.present?)
51
+ ].compact)
52
+ end
53
+ end
54
+
55
+ def meta_tag
56
+ return nil if @meta.blank?
57
+
58
+ tag.span(@meta, class: "shrink-0 text-[13px] whitespace-nowrap text-ink-400")
59
+ end
60
+
61
+ # Le chevron ne s'affiche que si la ligne mène quelque part : une flèche
62
+ # qui ne conduit à rien se clique quand même.
63
+ def chevron
64
+ return nil if @href.blank?
65
+
66
+ tag.span(render(SilexUi::IconComponent.new(name: "chevron-right", size: :sm)),
67
+ class: "shrink-0 text-ink-300 transition group-hover/action:translate-x-0.5 group-hover/action:text-brand-500")
68
+ end
69
+
70
+ def row_classes
71
+ class_names("group/action -mx-3 flex items-center gap-4 rounded-surface px-3 py-3.5 transition",
72
+ ("hover:bg-surface-50" if @href.present?))
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,11 +1,19 @@
1
1
  <div class="<%= css_classes %>" role="status">
2
- <% if title.present? %>
3
- <p class="text-xs font-light tracking-widest uppercase"><%= title %></p>
4
- <% end %>
2
+ <span class="absolute inset-y-0 left-0 w-1 <%= accent_class %>"></span>
5
3
 
6
- <% if text.present? %>
7
- <p class="<%= "mt-1" if title.present? %>"><%= text %></p>
4
+ <% if icon_name.present? %>
5
+ <%= render SilexUi::IconComponent.new(name: icon_name, size: :md, class: "mt-0.5") %>
8
6
  <% end %>
9
7
 
10
- <%= content %>
8
+ <div class="min-w-0">
9
+ <% if title.present? %>
10
+ <p class="font-semibold"><%= title %></p>
11
+ <% end %>
12
+
13
+ <% if text.present? %>
14
+ <p class="<%= "mt-0.5" if title.present? %>"><%= text %></p>
15
+ <% end %>
16
+
17
+ <%= content %>
18
+ </div>
11
19
  </div>