silex_ui 0.1.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 +201 -0
- data/LICENSE.txt +6 -0
- data/README.md +121 -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/controllers/silex_ui/catalogue_controller.rb +47 -0
- data/app/views/silex_ui/catalogue/_specimen.html.erb +15 -0
- data/app/views/silex_ui/catalogue/show.html.erb +396 -0
- data/config/routes.rb +7 -0
- data/lib/silex_ui/version.rb +1 -1
- data/lib/silex_ui.rb +38 -1
- metadata +35 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17dc9638b6a4ef5db846b596b181cf7a4517e38b13cb990fd2b2fcc2bb042297
|
|
4
|
+
data.tar.gz: d36863c0f23bc76ae13ef5d2130634681677e068d9c41000508cd4099888d0c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a21e4670e28539f69a9b05461c3c82045168fec106360f7b39385a66f7670b5956db27c714329bdc67cdd53c08a9147e1c288b8d2704594d1ddf1376a6fb2a03
|
|
7
|
+
data.tar.gz: 9437eec4ce261ff973d1995b14bc5dac2e8aba59764f0a53d02df41978d8720ed9d64f358dbe0d6334882310e50b0ff8a5348f3c84db72b6a5f32189122ab584
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,207 @@ Toutes les évolutions notables de silex_ui. Le format suit
|
|
|
6
6
|
|
|
7
7
|
## [Non publié]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] — 2026-09-10
|
|
10
|
+
|
|
11
|
+
De quoi corriger sans quitter la liste.
|
|
12
|
+
|
|
13
|
+
### Ajouté
|
|
14
|
+
|
|
15
|
+
- `SilexUi::RemoteModalComponent` — le `Modal::RemoteComponent` du Silex, porté
|
|
16
|
+
sur `<dialog>` : une coquille posée une fois, dont le corps est un Turbo Frame
|
|
17
|
+
vide. `RemoteModalComponent.trigger(modal:, frame:)` rend les attributs du
|
|
18
|
+
déclencheur — les écrire à la main sur chaque lien, c'est se tromper une fois
|
|
19
|
+
sur trois d'identifiant de frame. Rouvrir le même lien recharge le frame,
|
|
20
|
+
sinon un formulaire rouvert garderait la saisie abandonnée du tour d'avant.
|
|
21
|
+
- `SilexUi::InlineEditComponent` — l'`inline_edit` du Silex : la valeur cède la
|
|
22
|
+
place au champ au clic, Échap annule, Entrée envoie, et quitter le champ
|
|
23
|
+
envoie aussi. Un champ ouvert qu'on abandonne est un champ qu'on croit avoir
|
|
24
|
+
enregistré.
|
|
25
|
+
- `SilexUi::FilterBarComponent` et `SilexUi::FilterFieldComponent` — le
|
|
26
|
+
`floating-filter` : le libellé vit dans le champ et remonte en violet dès
|
|
27
|
+
qu'une valeur est choisie. Tout se joue en CSS (`:has(option:checked)`,
|
|
28
|
+
`:placeholder-shown`), donc aucun contrôleur n'a à savoir si un filtre est
|
|
29
|
+
rempli. Deux densités, `:md` et `:sm`.
|
|
30
|
+
- Contrôleurs `silex-ui--remote-modal`, `silex-ui--inline-edit` et
|
|
31
|
+
`silex-ui--tooltip`.
|
|
32
|
+
|
|
33
|
+
### Modifié
|
|
34
|
+
|
|
35
|
+
- `TableComponent` accepte `framed:` — un tableau posé dans un panneau qui a
|
|
36
|
+
déjà bordure et rayon laisse tomber les siens, plutôt que de dessiner un
|
|
37
|
+
second cadre. Ses colonnes acceptent `icon:`, et ses lignes respirent.
|
|
38
|
+
- Les modales plafonnent à `85vh` et font défiler leur corps : un formulaire
|
|
39
|
+
long ne doit pas sortir de l'écran par le bas.
|
|
40
|
+
- `PageHeaderComponent` rend les segments du fil d'Ariane et pose lui-même les
|
|
41
|
+
points de séparation.
|
|
42
|
+
|
|
43
|
+
## [0.7.0] — 2026-09-10
|
|
44
|
+
|
|
45
|
+
Le rail, repris ligne à ligne sur le CSS du Silex plutôt qu'à l'œil.
|
|
46
|
+
|
|
47
|
+
### Modifié
|
|
48
|
+
|
|
49
|
+
- **Le rail suit `.admin-nav-icon`, pas `.campaign-icon-wrapper`.** Le Silex a
|
|
50
|
+
deux styles de vignette : celui de ses campagnes — 52 px, bordure de 2 px,
|
|
51
|
+
anneau — et celui de sa navigation, bien plus léger : 42 px, un fond violet
|
|
52
|
+
dilué, aucune bordure, l'icône en 16 px. C'est le second qu'il fallait.
|
|
53
|
+
- **Les icônes du rail sont indigo, jamais grises.** Une seule règle du Silex
|
|
54
|
+
(`.admin-nav-icon i { color: #667eea }`) expliquait l'essentiel de l'écart :
|
|
55
|
+
c'est elle qui donne au rail sa couleur avant même qu'on lise une forme.
|
|
56
|
+
- **La section courante ne prend ni bordure ni anneau** — sa barre contre le
|
|
57
|
+
bord suffit, et une ombre portée la décolle. Le fond violet plein est commenté
|
|
58
|
+
dans le CSS du Silex ; le reprendre alourdissait tout le rail.
|
|
59
|
+
- La barre de la section courante reprend ses proportions (3 × 24 px), et les
|
|
60
|
+
entrées leur respiration (`py-2.5`).
|
|
61
|
+
- Le survol suit le Silex : la pastille s'éclaircit et grandit d'un cheveu.
|
|
62
|
+
- **Le hamburger mobile a ses trois barres**, qui se croisent en X à
|
|
63
|
+
l'ouverture. Une icône de grille ne dit pas « menu ».
|
|
64
|
+
|
|
65
|
+
### Corrigé
|
|
66
|
+
|
|
67
|
+
- **Sur mobile, le libellé du rail est dans le lien.** Il était posé à côté :
|
|
68
|
+
seul le carré de 42 px conduisait quelque part, appuyer sur le texte ne
|
|
69
|
+
faisait rien. Il n'apparaît plus qu'une fois dans le DOM.
|
|
70
|
+
- La largeur du rail et la hauteur de la barre mobile passent par leurs jetons
|
|
71
|
+
(`w-rail`, `pl-rail`, `h-15`) au lieu d'être réécrites à trois endroits.
|
|
72
|
+
|
|
73
|
+
### Ajouté
|
|
74
|
+
|
|
75
|
+
- Le sélecteur de thème à **trois positions** — clair, automatique, sombre —
|
|
76
|
+
dans sa pilule. « Automatique » n'est plus un défaut caché mais un choix qu'on
|
|
77
|
+
peut reprendre, et il suit le système en cours de route : un thème qui ne
|
|
78
|
+
bascule qu'au rechargement n'est pas automatique.
|
|
79
|
+
- **Une infobulle qui ne se fait pas rogner.** Celle du rail vivait dans son
|
|
80
|
+
lien, donc dans une boîte qui défile : elle était coupée et ne s'est jamais
|
|
81
|
+
affichée — ce qu'on voyait était l'infobulle native du navigateur. Le
|
|
82
|
+
contrôleur `silex-ui--tooltip` la pose désormais sur `<body>`, en position
|
|
83
|
+
fixe. Il lit le `title`, le retire, et le remet en partant : sans JavaScript
|
|
84
|
+
le navigateur fait le travail, avec lui l'infobulle est stylée. Jamais les
|
|
85
|
+
deux.
|
|
86
|
+
- `BreakdownComponent` accepte `limit:` et `more_href:` — au-delà de la limite
|
|
87
|
+
il annonce le reste (« + 34 autres ») au lieu de l'étaler. Une répartition qui
|
|
88
|
+
déroule quarante lignes n'est plus une répartition.
|
|
89
|
+
- `ShellComponent` gagne un slot `utilities` : ce qui se pose au-dessus du menu
|
|
90
|
+
utilisateur, sous un filet — cloche de notifications, sélecteur de thème.
|
|
91
|
+
|
|
92
|
+
## [0.6.0] — 2026-09-10
|
|
93
|
+
|
|
94
|
+
### Modifié
|
|
95
|
+
|
|
96
|
+
- **Les icônes sont celles du Silex.** Le jeu passe de Heroicons à Font Awesome,
|
|
97
|
+
avec le vocabulaire exact de l'app (68 icônes) : mêmes formes pleines, mêmes
|
|
98
|
+
noms. Les noms logiques du gem ne changent pas — `icon: "globe"` continue de
|
|
99
|
+
marcher.
|
|
100
|
+
|
|
101
|
+
### Ajouté
|
|
102
|
+
|
|
103
|
+
- `config.icon_renderer` — `:svg` (défaut) embarque le tracé dans le gem,
|
|
104
|
+
`:font` rend `<i class="fa-solid fa-globe">` et laisse l'application fournir
|
|
105
|
+
la police.
|
|
106
|
+
|
|
107
|
+
Ce second mode existe pour Font Awesome **Pro** : ses icônes ne sont pas
|
|
108
|
+
redistribuables, un gem public ne peut donc pas les transporter. Une
|
|
109
|
+
application sous licence Pro charge son kit et bascule le rendu ; le gem se
|
|
110
|
+
contente alors de nommer l'icône.
|
|
111
|
+
|
|
112
|
+
## [0.5.0] — 2026-09-10
|
|
113
|
+
|
|
114
|
+
Ce qui manquait pour que les pages ressemblent vraiment au Silex : ses deux
|
|
115
|
+
en-têtes, et la surface blanche dans laquelle tout son contenu vit.
|
|
116
|
+
|
|
117
|
+
### Ajouté
|
|
118
|
+
|
|
119
|
+
- `SilexUi::PanelComponent` — la `.content-section` du Silex : surface blanche
|
|
120
|
+
arrondie, en-tête à pastille dégradée, titre franc et lien vers le détail.
|
|
121
|
+
C'est lui qui structure les pages ; un bloc de contenu ne vit pas à même le
|
|
122
|
+
fond.
|
|
123
|
+
- `SilexUi::ProgressComponent` — objectif, part réalisée, et les deux bornes
|
|
124
|
+
lisibles de part et d'autre.
|
|
125
|
+
- `SilexUi::ActionListComponent` — la liste « Actions requises » : une pastille
|
|
126
|
+
colorée, ce qu'il faut faire, pourquoi, et un chevron qui n'apparaît que si la
|
|
127
|
+
ligne mène quelque part.
|
|
128
|
+
|
|
129
|
+
### Modifié
|
|
130
|
+
|
|
131
|
+
- `PageHeaderComponent` prend une `variant:`. `:hero` — la bande pâle pleine
|
|
132
|
+
largeur des pages de liste, avec ses halos et son fil d'Ariane ; `:banner` —
|
|
133
|
+
la carte à dégradé violet plein des tableaux de bord, avec sa plaque
|
|
134
|
+
translucide à droite. Le Silex a les deux ; n'en porter qu'une revenait à ne
|
|
135
|
+
ressembler qu'à la moitié de l'app.
|
|
136
|
+
- Les compteurs respirent : chiffre en 3xl extra-gras, carte plus généreuse.
|
|
137
|
+
- `TableComponent` n'affiche plus de bande d'en-tête quand aucune colonne n'est
|
|
138
|
+
nommée — elle se lisait comme une ligne vide.
|
|
139
|
+
|
|
140
|
+
## [0.4.0] — 2026-09-10
|
|
141
|
+
|
|
142
|
+
L'ossature du Silex : son rail d'icônes, son menu utilisateur, ses avatars.
|
|
143
|
+
|
|
144
|
+
### Ajouté
|
|
145
|
+
|
|
146
|
+
- `SilexUi::ShellComponent` — le cadre de l'application : rail fixe de 80px à
|
|
147
|
+
gauche, page à droite. Chaque icône porte son libellé, qui apparaît au survol
|
|
148
|
+
sur desktop et se pose à côté d'elle sur mobile : un rail d'icônes sans texte
|
|
149
|
+
se devine au lieu de se lire. La section courante prend l'anneau violet et la
|
|
150
|
+
barre d'accent contre le bord. Sous 768px, le rail cède à une barre supérieure
|
|
151
|
+
et un panneau qui coulisse derrière un fond sombre.
|
|
152
|
+
- `SilexUi::AvatarComponent` — une image quand il y en a une, les initiales sur
|
|
153
|
+
le dégradé de marque sinon. Deux lettres, jamais une : un avatar à une lettre
|
|
154
|
+
se confond trop vite.
|
|
155
|
+
- `SilexUi::DropdownComponent` — le menu utilisateur du Silex, qui se ferme au
|
|
156
|
+
clic dehors, à Échap et à la navigation. Les entrées à `method:` passent par
|
|
157
|
+
un formulaire, donc par le jeton CSRF.
|
|
158
|
+
- Contrôleurs `silex-ui--sidebar` et `silex-ui--dropdown`.
|
|
159
|
+
- Jetons d'espacement `rail`, `15` et `70` pour le rail et son panneau mobile.
|
|
160
|
+
|
|
161
|
+
## [0.3.0] — 2026-09-10
|
|
162
|
+
|
|
163
|
+
Le design system parle désormais le dialecte du Silex. Les composants gardent
|
|
164
|
+
leur API : c'est leur habillage qui change, et les applications qui passent par
|
|
165
|
+
eux suivent sans rien modifier.
|
|
166
|
+
|
|
167
|
+
### Modifié
|
|
168
|
+
|
|
169
|
+
- **Palette entièrement remplacée.** Le sable et l'encre légère cèdent la place
|
|
170
|
+
à l'ardoise froide et à l'indigo du Silex. `sand-*` devient `surface-*`,
|
|
171
|
+
`ink-*` change de valeurs et gagne les nuances 500 et 700.
|
|
172
|
+
- **Formes du Silex** : cartes arrondies et surélevées, barre d'accent qui
|
|
173
|
+
s'épaissit au survol, icône dans un carré arrondi, pastilles pleines,
|
|
174
|
+
en-tête de page en dégradé pastel avec deux halos, tableaux à en-tête
|
|
175
|
+
collant et lignes qui réagissent au survol.
|
|
176
|
+
- **Atkinson Hyperlegible** remplace la pile système, comme au Silex.
|
|
177
|
+
- `silex_ui:sync` recopie aussi `app/views`.
|
|
178
|
+
|
|
179
|
+
### Ajouté
|
|
180
|
+
|
|
181
|
+
- `SilexUi::IconComponent` — 44 icônes Heroicons 2.2 (MIT) en SVG inline, sans
|
|
182
|
+
dépendance réseau. Les composants acceptent tous un `icon:`.
|
|
183
|
+
- **Thème sombre** : toute l'échelle est redéfinie, pas seulement les surfaces.
|
|
184
|
+
Un pastel posé sur du sombre reste un pastel — les fonds d'état deviennent
|
|
185
|
+
des teintes sourdes et c'est le texte qui s'éclaircit.
|
|
186
|
+
- `SilexUi::ThemeToggleComponent` et le contrôleur `silex-ui--theme`, qui
|
|
187
|
+
retient le choix d'une visite à l'autre et suit le système par défaut.
|
|
188
|
+
- `SilexUi::Tones` — une seule table de correspondance ton → classes, pour
|
|
189
|
+
qu'une pastille verte et un compteur vert tirent du même vert.
|
|
190
|
+
- Quatre tons de catégorisation en plus des cinq états : `purple`, `pink`,
|
|
191
|
+
`teal`, `cyan`.
|
|
192
|
+
|
|
193
|
+
## [0.2.0] — 2026-09-10
|
|
194
|
+
|
|
195
|
+
### Ajouté
|
|
196
|
+
|
|
197
|
+
- Page catalogue : tous les composants dans toutes leurs variantes, montée par
|
|
198
|
+
l'application hôte (`mount SilexUi::Engine => "/design"`), en développement
|
|
199
|
+
seulement. Un composant qu'on ne peut voir qu'en trouvant la page qui l'utilise
|
|
200
|
+
est un composant qu'on finit par recopier à la main.
|
|
201
|
+
- `SilexUi.configure` : `catalogue_layout` pour les applications dont le layout
|
|
202
|
+
exige une session, `catalogue_enabled` pour ouvrir ou fermer la page ailleurs
|
|
203
|
+
qu'en développement.
|
|
204
|
+
|
|
205
|
+
### Modifié
|
|
206
|
+
|
|
207
|
+
- `silex_ui:sync` recopie aussi `app/views`, sans quoi les nuanciers du catalogue
|
|
208
|
+
se rendraient sans leurs couleurs.
|
|
209
|
+
|
|
9
210
|
## [0.1.0] — 2026-09-10
|
|
10
211
|
|
|
11
212
|
Première version.
|
data/LICENSE.txt
CHANGED
|
@@ -19,3 +19,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
--------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
Les tracés d'icônes embarqués dans app/components/silex_ui/icon_component.rb
|
|
26
|
+
proviennent de Font Awesome Free 6.7 (https://fontawesome.com), publiés sous
|
|
27
|
+
licence CC BY 4.0 — https://fontawesome.com/license/free
|
data/README.md
CHANGED
|
@@ -61,26 +61,110 @@ registerSilexUiControllers(application)
|
|
|
61
61
|
Ils sont enregistrés sous le préfixe `silex-ui--`, hors de portée des
|
|
62
62
|
contrôleurs de l'application.
|
|
63
63
|
|
|
64
|
+
## Le catalogue
|
|
65
|
+
|
|
66
|
+
Tous les composants, dans toutes leurs variantes, sur une page. À monter dans
|
|
67
|
+
l'application hôte — en développement seulement :
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# config/routes.rb
|
|
71
|
+
mount SilexUi::Engine => "/design" if Rails.env.development?
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Puis http://localhost:3000/design.
|
|
75
|
+
|
|
76
|
+
La page emprunte le layout de l'application, parce que c'est lui qui sait où sont
|
|
77
|
+
sa feuille de style et son importmap. Si ce layout exige une session, désignez-en
|
|
78
|
+
un autre :
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# config/initializers/silex_ui.rb
|
|
82
|
+
SilexUi.configure do |config|
|
|
83
|
+
config.catalogue_layout = "silex_ui/bare" # ou false
|
|
84
|
+
config.catalogue_enabled = false # pour le fermer même en dev
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
64
88
|
## Tokens
|
|
65
89
|
|
|
66
90
|
| Famille | Rôle | Nuances |
|
|
67
91
|
|---|---|---|
|
|
68
|
-
| `
|
|
69
|
-
| `ink` | le texte, du plus
|
|
70
|
-
| `
|
|
71
|
-
| `warn`
|
|
72
|
-
| `
|
|
73
|
-
|
|
92
|
+
| `surface` | les fonds, de la carte à la bordure | 0 · 50 · 100 · 200 · 300 |
|
|
93
|
+
| `ink` | le texte, du plus plein au plus discret | 900 · 700 · 500 · 400 · 300 |
|
|
94
|
+
| `brand` | l'accent — l'indigo du Silex et son violet | 50 · 100 · 300 · 500 · 600 · 700 |
|
|
95
|
+
| `ok` `warn` `alert` `info` | les quatre états | 50 · 100 · 300 · 500 · 700 |
|
|
96
|
+
| `purple` `pink` `teal` `cyan` | les familles, quand il ne s'agit plus d'un état | 50 · 300 · 500 · 700 |
|
|
97
|
+
|
|
98
|
+
Rayons : `rounded-badge` (8px), `rounded-surface` (12px), `rounded-card` (14px).
|
|
99
|
+
Ombres : `shadow-card` et `shadow-card-hover`.
|
|
74
100
|
|
|
75
101
|
Les tons sont **sémantiques** : on écrit `tone: :alert`, jamais `tone: :red`.
|
|
76
102
|
Le jour où l'alerte devient violette, une seule ligne bouge.
|
|
77
103
|
|
|
104
|
+
## Thème sombre
|
|
105
|
+
|
|
106
|
+
Le thème se pose sur `<html data-theme="dark">`, comme le fait le Silex sur son
|
|
107
|
+
`<body>`. Toute l'échelle est redéfinie, pas seulement les surfaces : un pastel
|
|
108
|
+
posé sur du sombre reste un pastel, donc les fonds d'état deviennent des teintes
|
|
109
|
+
sourdes et c'est le texte qui s'éclaircit.
|
|
110
|
+
|
|
111
|
+
`SilexUi::ThemeToggleComponent` fournit l'interrupteur ; le contrôleur retient le
|
|
112
|
+
choix et suit le système par défaut. Pour éviter que la page clignote en blanc au
|
|
113
|
+
chargement, posez le thème avant le premier rendu, dans le `<head>` :
|
|
114
|
+
|
|
115
|
+
```erb
|
|
116
|
+
<%= javascript_tag nonce: true do %>
|
|
117
|
+
(function () {
|
|
118
|
+
var stored = localStorage.getItem("silex-ui-theme")
|
|
119
|
+
var dark = stored ? stored === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
120
|
+
document.documentElement.dataset.theme = dark ? "dark" : "light"
|
|
121
|
+
})()
|
|
122
|
+
<% end %>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Icônes
|
|
126
|
+
|
|
127
|
+
68 icônes **Font Awesome** — le vocabulaire exact du Silex, mêmes formes pleines,
|
|
128
|
+
mêmes noms.
|
|
129
|
+
|
|
130
|
+
```erb
|
|
131
|
+
<%= render SilexUi::IconComponent.new(name: :globe, size: :lg) %>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Tous les composants qui peuvent en porter une acceptent `icon:` — bouton,
|
|
135
|
+
pastille, compteur, en-tête, panneau, section, carte, bandeau, état vide, rail.
|
|
136
|
+
`SilexUi::IconComponent::NAMES` donne la liste ; le catalogue les affiche toutes.
|
|
137
|
+
|
|
138
|
+
Deux rendus, selon ce dont dispose l'application :
|
|
139
|
+
|
|
140
|
+
| `config.icon_renderer` | Rendu | Pour qui |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `:svg` (défaut) | tracé embarqué dans le gem | toute application, sans rien installer |
|
|
143
|
+
| `:font` | `<i class="fa-solid fa-globe">` | une application qui charge son propre kit |
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# config/initializers/silex_ui.rb — application sous licence Font Awesome Pro
|
|
147
|
+
SilexUi.configure do |config|
|
|
148
|
+
config.icon_renderer = :font
|
|
149
|
+
config.icon_font_style = "fa-solid" # ou "fa-regular", "fa-light"…
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Le mode `:font` existe pour **Font Awesome Pro** : ses icônes ne sont pas
|
|
154
|
+
redistribuables, un gem public ne peut donc pas les embarquer. L'application
|
|
155
|
+
charge son kit sous sa licence, le gem se contente de nommer l'icône — et les
|
|
156
|
+
tracés Pro ne quittent jamais l'app.
|
|
157
|
+
|
|
158
|
+
Les tracés embarqués viennent de Font Awesome **Free** 6.7
|
|
159
|
+
([CC BY 4.0](https://fontawesome.com/license/free)), seule édition qu'un gem
|
|
160
|
+
public a le droit de redistribuer.
|
|
161
|
+
|
|
78
162
|
## Composants
|
|
79
163
|
|
|
80
164
|
| Composant | Ce qu'il fait |
|
|
81
165
|
|---|---|
|
|
82
166
|
| `NavComponent` | barre de navigation : marque, sections, zone utilisateur |
|
|
83
|
-
| `PageHeaderComponent` |
|
|
167
|
+
| `PageHeaderComponent` | en-tête de page, en bande pâle (`:hero`) ou en carte violette (`:banner`) |
|
|
84
168
|
| `SectionComponent` | bloc précédé d'un titre en petites capitales |
|
|
85
169
|
| `CardComponent` | surface surélevée, avec en-tête et pied optionnels |
|
|
86
170
|
| `StatGridComponent` / `StatCardComponent` | rangée de compteurs, avec lien, ton, évolution |
|
|
@@ -95,10 +179,40 @@ Le jour où l'alerte devient violette, une seule ligne bouge.
|
|
|
95
179
|
| `ButtonComponent` | bouton ou lien, quatre variantes |
|
|
96
180
|
| `CopyButtonComponent` | copie dans le presse-papiers, avec confirmation |
|
|
97
181
|
| `ModalComponent` | fenêtre modale sur `<dialog>` |
|
|
182
|
+
| `ShellComponent` | l'ossature : rail d'icônes à gauche, page à droite |
|
|
183
|
+
| `PanelComponent` | la surface blanche dans laquelle vit tout contenu |
|
|
184
|
+
| `ProgressComponent` | un objectif et ce qui en est fait |
|
|
185
|
+
| `ActionListComponent` | lignes à pastille, titre, sous-titre et chevron |
|
|
186
|
+
| `AvatarComponent` | image ou initiales sur le dégradé de marque |
|
|
187
|
+
| `DropdownComponent` | menu qui se ferme dès qu'on regarde ailleurs |
|
|
188
|
+
| `RemoteModalComponent` | modale dont le contenu arrive du serveur à l'ouverture |
|
|
189
|
+
| `InlineEditComponent` | une valeur qu'on corrige sur place |
|
|
190
|
+
| `FilterBarComponent` / `FilterFieldComponent` | barre de filtres à libellé flottant |
|
|
191
|
+
| `IconComponent` | une icône en SVG inline |
|
|
192
|
+
| `ThemeToggleComponent` | l'interrupteur clair / sombre |
|
|
98
193
|
|
|
99
194
|
Tous acceptent `class:` : les classes de l'appelant sont ajoutées après celles
|
|
100
195
|
du composant, donc elles gagnent à spécificité égale.
|
|
101
196
|
|
|
197
|
+
## L'ossature
|
|
198
|
+
|
|
199
|
+
Le rail du Silex, monté dans le layout — et seulement une fois la session
|
|
200
|
+
ouverte : les pages de connexion n'ont rien à quoi naviguer.
|
|
201
|
+
|
|
202
|
+
```erb
|
|
203
|
+
<%= render SilexUi::ShellComponent.new do |shell| %>
|
|
204
|
+
<% shell.with_brand do %><%= link_to "N", root_path %><% end %>
|
|
205
|
+
<% shell.with_item icon: "home", href: root_path, label: "Tableau de bord", active: current_page?(root_path) %>
|
|
206
|
+
<% shell.with_item icon: "globe", href: websites_path, label: "Sites" %>
|
|
207
|
+
<% shell.with_footer do %><%= render SilexUi::AvatarComponent.new(name: current_user.display_name) %><% end %>
|
|
208
|
+
<% shell.with_page do %><%= yield %><% end %>
|
|
209
|
+
<% end %>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
L'en-tête de page (`PageHeaderComponent`) est prévu pour occuper toute la largeur
|
|
213
|
+
du contenu : posez-le hors de la gouttière, et le reste de la page dans un
|
|
214
|
+
conteneur.
|
|
215
|
+
|
|
102
216
|
## Exemples
|
|
103
217
|
|
|
104
218
|
```erb
|
|
@@ -3,9 +3,10 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
// Copie une valeur dans le presse-papiers et le dit — un bouton qui ne confirme
|
|
4
4
|
// rien laisse l'utilisateur cliquer trois fois.
|
|
5
5
|
export default class extends Controller {
|
|
6
|
+
static targets = ["label"]
|
|
6
7
|
static values = {
|
|
7
8
|
value: String,
|
|
8
|
-
label:
|
|
9
|
+
label: String,
|
|
9
10
|
copiedLabel: { type: String, default: "Copié" },
|
|
10
11
|
resetAfter: { type: Number, default: 1500 }
|
|
11
12
|
}
|
|
@@ -15,24 +16,23 @@ export default class extends Controller {
|
|
|
15
16
|
|
|
16
17
|
try {
|
|
17
18
|
await navigator.clipboard.writeText(this.valueValue)
|
|
18
|
-
this.#
|
|
19
|
+
this.#say(this.copiedLabelValue)
|
|
19
20
|
} catch {
|
|
20
21
|
// Presse-papiers refusé (contexte non sécurisé, permission) : on laisse la
|
|
21
22
|
// valeur sélectionnable plutôt que de mentir sur le succès.
|
|
22
|
-
this
|
|
23
|
-
this.#restoreLater()
|
|
23
|
+
this.#say("Échec")
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this
|
|
30
|
-
|
|
27
|
+
// Seul le libellé change ; l'icône du bouton n'est pas à réécrire.
|
|
28
|
+
#say(text) {
|
|
29
|
+
if (this.hasLabelTarget) this.labelTarget.textContent = text
|
|
30
|
+
this.element.classList.add("silex-ui-copied")
|
|
31
31
|
|
|
32
|
-
#restoreLater() {
|
|
33
32
|
clearTimeout(this.timeout)
|
|
34
33
|
this.timeout = setTimeout(() => {
|
|
35
|
-
this.
|
|
34
|
+
if (this.hasLabelTarget) this.labelTarget.textContent = this.labelValue
|
|
35
|
+
this.element.classList.remove("silex-ui-copied")
|
|
36
36
|
}, this.resetAfterValue)
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Un menu qui s'ouvre au clic et se ferme dès qu'on regarde ailleurs : clic
|
|
4
|
+
// dehors, touche Échap, ou navigation Turbo.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["menu"]
|
|
7
|
+
|
|
8
|
+
toggle(event) {
|
|
9
|
+
event.preventDefault()
|
|
10
|
+
this.menuTarget.classList.toggle("hidden")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
close() {
|
|
14
|
+
this.menuTarget.classList.add("hidden")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
closeOnOutsideClick(event) {
|
|
18
|
+
if (!this.element.contains(event.target)) this.close()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnect() {
|
|
22
|
+
this.close()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Bascule affichage ↔ formulaire pour l'édition en place.
|
|
4
|
+
//
|
|
5
|
+
// Le champ garde le focus et le curseur en fin de valeur : on vient pour
|
|
6
|
+
// corriger, pas pour tout retaper. Échap annule, Entrée envoie, et perdre le
|
|
7
|
+
// focus envoie aussi — un champ ouvert qu'on quitte est un champ qu'on croit
|
|
8
|
+
// avoir enregistré.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static targets = ["display", "form", "field"]
|
|
11
|
+
static values = { submitOnBlur: { type: Boolean, default: true } }
|
|
12
|
+
|
|
13
|
+
edit(event) {
|
|
14
|
+
event?.preventDefault()
|
|
15
|
+
this.displayTarget.hidden = true
|
|
16
|
+
this.formTarget.hidden = false
|
|
17
|
+
|
|
18
|
+
const field = this.hasFieldTarget ? this.fieldTarget : this.formTarget.querySelector("select, input, textarea")
|
|
19
|
+
if (!field) return
|
|
20
|
+
|
|
21
|
+
field.focus()
|
|
22
|
+
if (field.setSelectionRange && field.type === "text") field.setSelectionRange(field.value.length, field.value.length)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
submit() {
|
|
26
|
+
const form = this.formTarget.querySelector("form") ?? this.formTarget
|
|
27
|
+
form.requestSubmit?.()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
submitOnBlur() {
|
|
31
|
+
if (this.submitOnBlurValue) this.submit()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cancel(event) {
|
|
35
|
+
event?.preventDefault()
|
|
36
|
+
this.formTarget.hidden = true
|
|
37
|
+
this.displayTarget.hidden = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Ouvre une modale dont le contenu est chargé à la demande.
|
|
4
|
+
//
|
|
5
|
+
// Se pose sur le déclencheur — un lien vers la page à charger. Le Turbo Frame
|
|
6
|
+
// de la modale prend l'href du lien, la modale s'ouvre, le serveur fournit le
|
|
7
|
+
// reste. Rouvrir le même lien recharge le frame : sans ça, un formulaire
|
|
8
|
+
// rouvert garderait la saisie abandonnée du tour précédent.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static values = { modal: String, frame: String }
|
|
11
|
+
|
|
12
|
+
open(event) {
|
|
13
|
+
event.preventDefault()
|
|
14
|
+
|
|
15
|
+
const frame = document.getElementById(this.frameValue)
|
|
16
|
+
if (frame) {
|
|
17
|
+
frame.getAttribute("src") === this.element.href ? frame.reload() : (frame.src = this.element.href)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
document.getElementById(this.modalValue)?.showModal()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Le rail, sur mobile : un panneau qui coulisse et un fond qui le protège.
|
|
4
|
+
// Sur desktop le rail est toujours là — ce contrôleur ne fait rien.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["panel", "overlay", "burger"]
|
|
7
|
+
|
|
8
|
+
toggle() {
|
|
9
|
+
this.#open ? this.close() : this.open()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
open() {
|
|
13
|
+
this.panelTarget.dataset.open = ""
|
|
14
|
+
if (this.hasBurgerTarget) this.burgerTarget.dataset.open = ""
|
|
15
|
+
this.overlayTarget.hidden = false
|
|
16
|
+
document.body.style.overflow = "hidden"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close() {
|
|
20
|
+
delete this.panelTarget.dataset.open
|
|
21
|
+
if (this.hasBurgerTarget) delete this.burgerTarget.dataset.open
|
|
22
|
+
this.overlayTarget.hidden = true
|
|
23
|
+
document.body.style.overflow = ""
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Une page qui change sous un panneau ouvert laisserait le fond bloqué.
|
|
27
|
+
disconnect() {
|
|
28
|
+
document.body.style.overflow = ""
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get #open() {
|
|
32
|
+
return this.panelTarget.dataset.open !== undefined
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Clair, automatique ou sombre — et on s'en souvient. Le thème est posé sur
|
|
4
|
+
// <html> par un attribut : le CSS n'a qu'une variable à relire, pas une feuille
|
|
5
|
+
// entière à recharger.
|
|
6
|
+
//
|
|
7
|
+
// « Automatique » suit le système, y compris quand il change en cours de route :
|
|
8
|
+
// un thème qui ne bascule qu'au rechargement n'est pas automatique.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static targets = ["option"]
|
|
11
|
+
static values = { storageKey: { type: String, default: "silex-ui-theme" } }
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
this.media = window.matchMedia("(prefers-color-scheme: dark)")
|
|
15
|
+
this.follow = () => { if (this.#mode === "auto") this.#apply("auto") }
|
|
16
|
+
this.media.addEventListener("change", this.follow)
|
|
17
|
+
|
|
18
|
+
this.#apply(this.#mode)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnect() {
|
|
22
|
+
this.media?.removeEventListener("change", this.follow)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
select(event) {
|
|
26
|
+
const mode = event.currentTarget.dataset.mode
|
|
27
|
+
localStorage.setItem(this.storageKeyValue, mode)
|
|
28
|
+
this.#apply(mode)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#apply(mode) {
|
|
32
|
+
const dark = mode === "dark" || (mode === "auto" && this.media.matches)
|
|
33
|
+
document.documentElement.dataset.theme = dark ? "dark" : "light"
|
|
34
|
+
|
|
35
|
+
this.optionTargets.forEach((option) => {
|
|
36
|
+
option.dataset.mode === mode ? option.setAttribute("data-active", "") : option.removeAttribute("data-active")
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get #mode() {
|
|
41
|
+
return localStorage.getItem(this.storageKeyValue) ?? "auto"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Une infobulle qui ne se fait pas rogner.
|
|
4
|
+
//
|
|
5
|
+
// Elle ne peut pas vivre dans son élément : le rail défile, et tout ce qui
|
|
6
|
+
// dépasse de sa boîte est coupé. Elle est donc posée sur <body>, en position
|
|
7
|
+
// fixe, et suivie à la main.
|
|
8
|
+
//
|
|
9
|
+
// Le libellé vient de l'attribut `title`, que le contrôleur retire en
|
|
10
|
+
// s'installant : sans JavaScript, le navigateur affiche sa propre infobulle ;
|
|
11
|
+
// avec, la nôtre la remplace. Jamais les deux.
|
|
12
|
+
export default class extends Controller {
|
|
13
|
+
static values = { placement: { type: String, default: "right" }, offset: { type: Number, default: 12 } }
|
|
14
|
+
|
|
15
|
+
connect() {
|
|
16
|
+
this.label = this.element.getAttribute("title")
|
|
17
|
+
if (!this.label) return
|
|
18
|
+
|
|
19
|
+
this.element.removeAttribute("title")
|
|
20
|
+
this.show = this.show.bind(this)
|
|
21
|
+
this.hide = this.hide.bind(this)
|
|
22
|
+
|
|
23
|
+
this.element.addEventListener("mouseenter", this.show)
|
|
24
|
+
this.element.addEventListener("focus", this.show)
|
|
25
|
+
this.element.addEventListener("mouseleave", this.hide)
|
|
26
|
+
this.element.addEventListener("blur", this.hide)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnect() {
|
|
30
|
+
this.element.removeEventListener("mouseenter", this.show)
|
|
31
|
+
this.element.removeEventListener("focus", this.show)
|
|
32
|
+
this.element.removeEventListener("mouseleave", this.hide)
|
|
33
|
+
this.element.removeEventListener("blur", this.hide)
|
|
34
|
+
this.hide()
|
|
35
|
+
|
|
36
|
+
if (this.label) this.element.setAttribute("title", this.label)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
show() {
|
|
40
|
+
this.bubble ??= this.#build()
|
|
41
|
+
document.body.appendChild(this.bubble)
|
|
42
|
+
|
|
43
|
+
const anchor = this.element.getBoundingClientRect()
|
|
44
|
+
const bubble = this.bubble.getBoundingClientRect()
|
|
45
|
+
|
|
46
|
+
this.bubble.style.top = `${anchor.top + anchor.height / 2 - bubble.height / 2}px`
|
|
47
|
+
this.bubble.style.left = this.placementValue === "left"
|
|
48
|
+
? `${anchor.left - bubble.width - this.offsetValue}px`
|
|
49
|
+
: `${anchor.right + this.offsetValue}px`
|
|
50
|
+
|
|
51
|
+
this.bubble.dataset.visible = ""
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
hide() {
|
|
55
|
+
this.bubble?.remove()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#build() {
|
|
59
|
+
const bubble = document.createElement("div")
|
|
60
|
+
bubble.className = "silex-ui-tooltip"
|
|
61
|
+
bubble.setAttribute("role", "tooltip")
|
|
62
|
+
bubble.textContent = this.label
|
|
63
|
+
return bubble
|
|
64
|
+
}
|
|
65
|
+
}
|