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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +184 -0
- data/LICENSE.txt +6 -0
- data/README.md +97 -7
- data/app/assets/javascripts/silex_ui/controllers/clipboard_controller.js +10 -10
- data/app/assets/javascripts/silex_ui/controllers/dropdown_controller.js +24 -0
- data/app/assets/javascripts/silex_ui/controllers/inline_edit_controller.js +39 -0
- data/app/assets/javascripts/silex_ui/controllers/remote_modal_controller.js +22 -0
- data/app/assets/javascripts/silex_ui/controllers/sidebar_controller.js +34 -0
- data/app/assets/javascripts/silex_ui/controllers/theme_controller.js +43 -0
- data/app/assets/javascripts/silex_ui/controllers/tooltip_controller.js +65 -0
- data/app/assets/javascripts/silex_ui/index.js +22 -1
- data/app/assets/stylesheets/silex_ui/theme.css +528 -42
- data/app/components/silex_ui/action_list_component.rb +76 -0
- data/app/components/silex_ui/alert_component.html.erb +14 -6
- data/app/components/silex_ui/alert_component.rb +24 -12
- data/app/components/silex_ui/avatar_component.rb +56 -0
- data/app/components/silex_ui/badge_component.rb +21 -17
- data/app/components/silex_ui/base_component.rb +10 -3
- data/app/components/silex_ui/breakdown_component.html.erb +20 -6
- data/app/components/silex_ui/breakdown_component.rb +21 -4
- data/app/components/silex_ui/button_component.rb +31 -16
- data/app/components/silex_ui/card_component.html.erb +7 -4
- data/app/components/silex_ui/card_component.rb +9 -6
- data/app/components/silex_ui/copy_button_component.rb +17 -8
- data/app/components/silex_ui/dropdown_component.html.erb +13 -0
- data/app/components/silex_ui/dropdown_component.rb +82 -0
- data/app/components/silex_ui/empty_state_component.html.erb +13 -0
- data/app/components/silex_ui/empty_state_component.rb +6 -9
- data/app/components/silex_ui/field_component.html.erb +4 -4
- data/app/components/silex_ui/field_component.rb +8 -5
- data/app/components/silex_ui/filter_bar_component.html.erb +22 -0
- data/app/components/silex_ui/filter_bar_component.rb +54 -0
- data/app/components/silex_ui/filter_field_component.html.erb +10 -0
- data/app/components/silex_ui/filter_field_component.rb +69 -0
- data/app/components/silex_ui/flash_component.html.erb +1 -1
- data/app/components/silex_ui/icon_component.rb +211 -0
- data/app/components/silex_ui/inline_edit_component.html.erb +23 -0
- data/app/components/silex_ui/inline_edit_component.rb +47 -0
- data/app/components/silex_ui/modal_component.html.erb +4 -2
- data/app/components/silex_ui/modal_component.rb +3 -2
- data/app/components/silex_ui/nav_component.html.erb +5 -4
- data/app/components/silex_ui/nav_component.rb +22 -7
- data/app/components/silex_ui/page_header_component.html.erb +38 -10
- data/app/components/silex_ui/page_header_component.rb +97 -11
- data/app/components/silex_ui/pagination_component.html.erb +11 -10
- data/app/components/silex_ui/pagination_component.rb +1 -2
- data/app/components/silex_ui/panel_component.html.erb +27 -0
- data/app/components/silex_ui/panel_component.rb +60 -0
- data/app/components/silex_ui/progress_component.html.erb +21 -0
- data/app/components/silex_ui/progress_component.rb +42 -0
- data/app/components/silex_ui/remote_modal_component.html.erb +30 -0
- data/app/components/silex_ui/remote_modal_component.rb +54 -0
- data/app/components/silex_ui/section_component.html.erb +5 -2
- data/app/components/silex_ui/section_component.rb +8 -8
- data/app/components/silex_ui/shell_component.html.erb +40 -0
- data/app/components/silex_ui/shell_component.rb +106 -0
- data/app/components/silex_ui/stat_card_component.html.erb +31 -6
- data/app/components/silex_ui/stat_card_component.rb +48 -28
- data/app/components/silex_ui/stat_grid_component.rb +8 -18
- data/app/components/silex_ui/table_component.html.erb +10 -8
- data/app/components/silex_ui/table_component.rb +52 -20
- data/app/components/silex_ui/theme_toggle_component.html.erb +11 -0
- data/app/components/silex_ui/theme_toggle_component.rb +29 -0
- data/app/components/silex_ui/tones.rb +72 -0
- data/app/views/silex_ui/catalogue/_specimen.html.erb +4 -4
- data/app/views/silex_ui/catalogue/show.html.erb +332 -187
- data/lib/silex_ui/version.rb +1 -1
- data/lib/silex_ui.rb +8 -0
- 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
|
-
*
|
|
9
|
-
*
|
|
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: "
|
|
13
|
-
|
|
14
|
-
/*
|
|
15
|
-
--color-
|
|
16
|
-
--color-
|
|
17
|
-
--color-
|
|
18
|
-
--color-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
--color-ink-
|
|
23
|
-
--color-ink-
|
|
24
|
-
--color-ink-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
--color-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--color-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
--color-
|
|
34
|
-
--color-
|
|
35
|
-
--color-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
--color-ok-
|
|
40
|
-
--color-ok-
|
|
41
|
-
--color-ok-
|
|
42
|
-
--color-ok-700: #
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
--color-
|
|
46
|
-
--color-
|
|
47
|
-
--color-
|
|
48
|
-
--color-
|
|
18
|
+
--font-sans: "Atkinson Hyperlegible", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
19
|
+
|
|
20
|
+
/* Surfaces — du 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-
|
|
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
|
-
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
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>
|