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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8c471e84ca4f41561bb7daa42d18669287be88e7d278d82ea79566f0baeaf46
4
- data.tar.gz: 6af9d732ec1b5188374e38ff7d06d771540886e7ea64f826dea89b0a876bd248
3
+ metadata.gz: 17dc9638b6a4ef5db846b596b181cf7a4517e38b13cb990fd2b2fcc2bb042297
4
+ data.tar.gz: d36863c0f23bc76ae13ef5d2130634681677e068d9c41000508cd4099888d0c1
5
5
  SHA512:
6
- metadata.gz: 1ef386b477752142f8e2d2ec94a89e5256c03aab117693f3e945e6b3cfc223d1402ba29defb027ae5b3a4fa05ba17336c00006a159994f8a29e48327a134d2e7
7
- data.tar.gz: 4a4fef0ccf67d57b6f4efbddd7338ad9655a0c1ded3a1e2649820b5f640a926b4c5c892f336e72fb00fe70a55af5f149d1352a8d6f7312aa8a4ea649e49de1be
6
+ metadata.gz: a21e4670e28539f69a9b05461c3c82045168fec106360f7b39385a66f7670b5956db27c714329bdc67cdd53c08a9147e1c288b8d2704594d1ddf1376a6fb2a03
7
+ data.tar.gz: 9437eec4ce261ff973d1995b14bc5dac2e8aba59764f0a53d02df41978d8720ed9d64f358dbe0d6334882310e50b0ff8a5348f3c84db72b6a5f32189122ab584
data/CHANGELOG.md CHANGED
@@ -6,6 +6,190 @@ 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
+
9
193
  ## [0.2.0] — 2026-09-10
10
194
 
11
195
  ### Ajouté
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
@@ -89,22 +89,82 @@ end
89
89
 
90
90
  | Famille | Rôle | Nuances |
91
91
  |---|---|---|
92
- | `sand` | le sol : fond de page, bordures | 50 · 100 · 200 · 300 |
93
- | `ink` | le texte, du plus discret au plus plein | 300 · 400 · 600 · 900 |
94
- | `alert` | ce qui a lapsé, ce qui est cassé | 50 · 100 · 300 · 700 |
95
- | `warn` | ce qui est sur le point de basculer | 50 · 100 · 300 · 700 |
96
- | `ok` | ce qui est en règle | 50 · 100 · 300 · 700 |
97
- | `info` | neutre, factuel, sans urgence | 50 · 100 · 300 · 700 |
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`.
98
100
 
99
101
  Les tons sont **sémantiques** : on écrit `tone: :alert`, jamais `tone: :red`.
100
102
  Le jour où l'alerte devient violette, une seule ligne bouge.
101
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
+
102
162
  ## Composants
103
163
 
104
164
  | Composant | Ce qu'il fait |
105
165
  |---|---|
106
166
  | `NavComponent` | barre de navigation : marque, sections, zone utilisateur |
107
- | `PageHeaderComponent` | titre de page, sous-titre, actions alimente aussi `content_for :title` |
167
+ | `PageHeaderComponent` | en-tête de page, en bande pâle (`:hero`) ou en carte violette (`:banner`) |
108
168
  | `SectionComponent` | bloc précédé d'un titre en petites capitales |
109
169
  | `CardComponent` | surface surélevée, avec en-tête et pied optionnels |
110
170
  | `StatGridComponent` / `StatCardComponent` | rangée de compteurs, avec lien, ton, évolution |
@@ -119,10 +179,40 @@ Le jour où l'alerte devient violette, une seule ligne bouge.
119
179
  | `ButtonComponent` | bouton ou lien, quatre variantes |
120
180
  | `CopyButtonComponent` | copie dans le presse-papiers, avec confirmation |
121
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 |
122
193
 
123
194
  Tous acceptent `class:` : les classes de l'appelant sont ajoutées après celles
124
195
  du composant, donc elles gagnent à spécificité égale.
125
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
+
126
216
  ## Exemples
127
217
 
128
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: { type: String, default: "Copier" },
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.#confirm()
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.element.textContent = "Échec"
23
- this.#restoreLater()
23
+ this.#say("Échec")
24
24
  }
25
25
  }
26
26
 
27
- #confirm() {
28
- this.element.textContent = this.copiedLabelValue
29
- this.#restoreLater()
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.element.textContent = this.labelValue
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
+ }
@@ -1,11 +1,32 @@
1
1
  import ClipboardController from "silex_ui/controllers/clipboard_controller"
2
+ import DropdownController from "silex_ui/controllers/dropdown_controller"
3
+ import InlineEditController from "silex_ui/controllers/inline_edit_controller"
2
4
  import ModalController from "silex_ui/controllers/modal_controller"
5
+ import RemoteModalController from "silex_ui/controllers/remote_modal_controller"
6
+ import SidebarController from "silex_ui/controllers/sidebar_controller"
7
+ import ThemeController from "silex_ui/controllers/theme_controller"
8
+ import TooltipController from "silex_ui/controllers/tooltip_controller"
3
9
 
4
10
  // Enregistre les contrôleurs du design system sous le préfixe `silex-ui--`,
5
11
  // pour qu'ils ne puissent jamais entrer en collision avec ceux de l'application.
6
12
  export function registerSilexUiControllers(application) {
7
13
  application.register("silex-ui--clipboard", ClipboardController)
14
+ application.register("silex-ui--dropdown", DropdownController)
15
+ application.register("silex-ui--inline-edit", InlineEditController)
8
16
  application.register("silex-ui--modal", ModalController)
17
+ application.register("silex-ui--remote-modal", RemoteModalController)
18
+ application.register("silex-ui--sidebar", SidebarController)
19
+ application.register("silex-ui--theme", ThemeController)
20
+ application.register("silex-ui--tooltip", TooltipController)
9
21
  }
10
22
 
11
- export { ClipboardController, ModalController }
23
+ export {
24
+ ClipboardController,
25
+ DropdownController,
26
+ InlineEditController,
27
+ ModalController,
28
+ RemoteModalController,
29
+ SidebarController,
30
+ ThemeController,
31
+ TooltipController
32
+ }