silex_ui 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/app/assets/javascripts/silex_ui/controllers/clipboard_controller.js +42 -0
- data/app/assets/javascripts/silex_ui/controllers/modal_controller.js +23 -0
- data/app/assets/javascripts/silex_ui/index.js +11 -0
- data/app/assets/stylesheets/silex_ui/theme.css +59 -0
- data/app/components/silex_ui/alert_component.html.erb +11 -0
- data/app/components/silex_ui/alert_component.rb +31 -0
- data/app/components/silex_ui/badge_component.rb +42 -0
- data/app/components/silex_ui/base_component.rb +32 -0
- data/app/components/silex_ui/breakdown_component.html.erb +17 -0
- data/app/components/silex_ui/breakdown_component.rb +27 -0
- data/app/components/silex_ui/button_component.rb +64 -0
- data/app/components/silex_ui/card_component.html.erb +22 -0
- data/app/components/silex_ui/card_component.rb +25 -0
- data/app/components/silex_ui/copy_button_component.rb +44 -0
- data/app/components/silex_ui/empty_state_component.rb +20 -0
- data/app/components/silex_ui/field_component.html.erb +18 -0
- data/app/components/silex_ui/field_component.rb +88 -0
- data/app/components/silex_ui/flash_component.html.erb +6 -0
- data/app/components/silex_ui/flash_component.rb +42 -0
- data/app/components/silex_ui/modal_component.html.erb +24 -0
- data/app/components/silex_ui/modal_component.rb +33 -0
- data/app/components/silex_ui/nav_component.html.erb +15 -0
- data/app/components/silex_ui/nav_component.rb +44 -0
- data/app/components/silex_ui/page_header_component.html.erb +17 -0
- data/app/components/silex_ui/page_header_component.rb +35 -0
- data/app/components/silex_ui/pagination_component.html.erb +30 -0
- data/app/components/silex_ui/pagination_component.rb +47 -0
- data/app/components/silex_ui/section_component.html.erb +12 -0
- data/app/components/silex_ui/section_component.rb +24 -0
- data/app/components/silex_ui/stat_card_component.html.erb +18 -0
- data/app/components/silex_ui/stat_card_component.rb +61 -0
- data/app/components/silex_ui/stat_grid_component.rb +41 -0
- data/app/components/silex_ui/table_component.html.erb +25 -0
- data/app/components/silex_ui/table_component.rb +98 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/silex_ui/install/install_generator.rb +68 -0
- data/lib/silex_ui/engine.rb +31 -0
- data/lib/silex_ui/sync.rb +43 -0
- data/lib/silex_ui/version.rb +5 -0
- data/lib/silex_ui.rb +37 -0
- data/lib/tasks/silex_ui.rake +19 -0
- metadata +131 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 84ca6a3d2f412fa7ff808b0d7a0513e87257123a852e26dbd098795085b76ff3
|
|
4
|
+
data.tar.gz: e237663991a4a318eb7190b48c9b84c11eedbd69e18f027abf491bd1ee71fa87
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ba83d0d1d6ae11d0ca91c0e6009a5c2970dd90c513714d4bae1c8cf22b363f7789df8c5fae2771d7029856a017dcef72bcde92a93d8be2ae3fcd202226f3ae53
|
|
7
|
+
data.tar.gz: e37f9d24f41677ba8d0d06db94624e32a759b1478845915af2d92aa7abbb2bbd34ded84507b71b4fb5f0351261bf13fe86fa9544588b05cfc0b7146c5f1df8fa
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Toutes les évolutions notables de silex_ui. Le format suit
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le versionnage
|
|
5
|
+
[SemVer](https://semver.org/lang/fr/).
|
|
6
|
+
|
|
7
|
+
## [Non publié]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-09-10
|
|
10
|
+
|
|
11
|
+
Première version.
|
|
12
|
+
|
|
13
|
+
### Ajouté
|
|
14
|
+
|
|
15
|
+
- Design tokens Tailwind v4 : familles `sand`, `ink`, `alert`, `warn`, `ok`, `info`.
|
|
16
|
+
- 16 ViewComponents : navigation, en-tête de page, section, carte, compteurs,
|
|
17
|
+
répartition, tableau, pagination, badge, alerte, flash, état vide, champ de
|
|
18
|
+
formulaire, bouton, bouton de copie, modale.
|
|
19
|
+
- Contrôleurs Stimulus `silex-ui--clipboard` et `silex-ui--modal`.
|
|
20
|
+
- Générateur `silex_ui:install` et tâche `silex_ui:sync`, branchée sur
|
|
21
|
+
`tailwindcss:build`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Théo Galais — Le Silex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# silex_ui
|
|
2
|
+
|
|
3
|
+
Design system partagé des applications Le Silex : **design tokens Tailwind v4**,
|
|
4
|
+
**ViewComponents** et **contrôleurs Stimulus**, dans un moteur Rails.
|
|
5
|
+
|
|
6
|
+
Une seule source de vérité pour la palette, la typographie et les composants —
|
|
7
|
+
au lieu d'un dossier de partials recopié d'une application à l'autre.
|
|
8
|
+
|
|
9
|
+
## Prérequis
|
|
10
|
+
|
|
11
|
+
- Rails ≥ 7.1
|
|
12
|
+
- Tailwind CSS **v4** (`tailwindcss-rails` ≥ 4.0)
|
|
13
|
+
- ViewComponent ≥ 3.0
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# Gemfile
|
|
19
|
+
gem "silex_ui"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bundle install
|
|
24
|
+
bin/rails generate silex_ui:install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Le générateur :
|
|
28
|
+
|
|
29
|
+
1. copie les tokens dans `app/assets/tailwind/silex_ui/theme.css` ;
|
|
30
|
+
2. copie une image plate des sources du gem dans `app/assets/tailwind/silex_ui/sources.html` ;
|
|
31
|
+
3. déclare les deux fichiers dans `app/assets/tailwind/application.css`.
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
@import "tailwindcss";
|
|
35
|
+
|
|
36
|
+
@import "./silex_ui/theme.css";
|
|
37
|
+
@source "./silex_ui/sources.html";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Les deux fichiers sont **versionnés avec l'application**. C'est délibéré : le
|
|
41
|
+
chemin d'installation d'un gem change entre le poste de dev, la CI et le
|
|
42
|
+
conteneur, alors qu'un `@source` doit pointer sur quelque chose de stable.
|
|
43
|
+
|
|
44
|
+
Après chaque montée de version du gem :
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bin/rails silex_ui:sync
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
La tâche est branchée sur `tailwindcss:build`, donc un build de CSS repart
|
|
51
|
+
toujours de sources à jour.
|
|
52
|
+
|
|
53
|
+
### Contrôleurs Stimulus
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
// app/javascript/controllers/index.js
|
|
57
|
+
import { registerSilexUiControllers } from "silex_ui"
|
|
58
|
+
registerSilexUiControllers(application)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Ils sont enregistrés sous le préfixe `silex-ui--`, hors de portée des
|
|
62
|
+
contrôleurs de l'application.
|
|
63
|
+
|
|
64
|
+
## Tokens
|
|
65
|
+
|
|
66
|
+
| Famille | Rôle | Nuances |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `sand` | le sol : fond de page, bordures | 50 · 100 · 200 · 300 |
|
|
69
|
+
| `ink` | le texte, du plus discret au plus plein | 300 · 400 · 600 · 900 |
|
|
70
|
+
| `alert` | ce qui a lapsé, ce qui est cassé | 50 · 100 · 300 · 700 |
|
|
71
|
+
| `warn` | ce qui est sur le point de basculer | 50 · 100 · 300 · 700 |
|
|
72
|
+
| `ok` | ce qui est en règle | 50 · 100 · 300 · 700 |
|
|
73
|
+
| `info` | neutre, factuel, sans urgence | 50 · 100 · 300 · 700 |
|
|
74
|
+
|
|
75
|
+
Les tons sont **sémantiques** : on écrit `tone: :alert`, jamais `tone: :red`.
|
|
76
|
+
Le jour où l'alerte devient violette, une seule ligne bouge.
|
|
77
|
+
|
|
78
|
+
## Composants
|
|
79
|
+
|
|
80
|
+
| Composant | Ce qu'il fait |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `NavComponent` | barre de navigation : marque, sections, zone utilisateur |
|
|
83
|
+
| `PageHeaderComponent` | titre de page, sous-titre, actions — alimente aussi `content_for :title` |
|
|
84
|
+
| `SectionComponent` | bloc précédé d'un titre en petites capitales |
|
|
85
|
+
| `CardComponent` | surface surélevée, avec en-tête et pied optionnels |
|
|
86
|
+
| `StatGridComponent` / `StatCardComponent` | rangée de compteurs, avec lien, ton, évolution |
|
|
87
|
+
| `BreakdownComponent` | répartition libellé / compte, chaque ligne cliquable |
|
|
88
|
+
| `TableComponent` | tableau à colonnes déclarées, tri, état vide |
|
|
89
|
+
| `PaginationComponent` | navigation de pages, l'URL restant à la charge de l'appelant |
|
|
90
|
+
| `BadgeComponent` | pastille de statut |
|
|
91
|
+
| `AlertComponent` | bandeau d'information persistant |
|
|
92
|
+
| `FlashComponent` | messages flash de Rails traduits en tons |
|
|
93
|
+
| `EmptyStateComponent` | ce qu'on voit quand il n'y a rien à voir |
|
|
94
|
+
| `FieldComponent` | champ de formulaire : libellé, champ, aide, erreurs |
|
|
95
|
+
| `ButtonComponent` | bouton ou lien, quatre variantes |
|
|
96
|
+
| `CopyButtonComponent` | copie dans le presse-papiers, avec confirmation |
|
|
97
|
+
| `ModalComponent` | fenêtre modale sur `<dialog>` |
|
|
98
|
+
|
|
99
|
+
Tous acceptent `class:` : les classes de l'appelant sont ajoutées après celles
|
|
100
|
+
du composant, donc elles gagnent à spécificité égale.
|
|
101
|
+
|
|
102
|
+
## Exemples
|
|
103
|
+
|
|
104
|
+
```erb
|
|
105
|
+
<%= render SilexUi::NavComponent.new(brand: "Network", brand_href: root_path) do |nav| %>
|
|
106
|
+
<% nav.with_item(label: "Sites", href: websites_path, active: controller_name == "websites") %>
|
|
107
|
+
<% nav.with_aside do %><%= current_user.display_name %><% end %>
|
|
108
|
+
<% end %>
|
|
109
|
+
|
|
110
|
+
<%= render SilexUi::PageHeaderComponent.new(title: "Sites", subtitle: "1 240 domaines") do |header| %>
|
|
111
|
+
<% header.with_actions do %>
|
|
112
|
+
<%= render SilexUi::ButtonComponent.new(label: "Ajouter un domaine", href: new_website_path) %>
|
|
113
|
+
<% end %>
|
|
114
|
+
<% end %>
|
|
115
|
+
|
|
116
|
+
<%= render SilexUi::StatGridComponent.new do |grid| %>
|
|
117
|
+
<% grid.with_stat(value: @total, label: "Domaines", href: websites_path) %>
|
|
118
|
+
<% grid.with_stat(value: @expired, label: "Expirés", tone: :alert, href: websites_path(filter: "expire")) %>
|
|
119
|
+
<% end %>
|
|
120
|
+
|
|
121
|
+
<%= render SilexUi::TableComponent.new(rows: @websites) do |table| %>
|
|
122
|
+
<% table.with_column(header: "Domaine") { |site| site.domain } %>
|
|
123
|
+
<% table.with_column(header: "Registrar") { |site| site.registrar.titleize } %>
|
|
124
|
+
<% table.with_column(header: "", align: :right) do |site| %>
|
|
125
|
+
<%= render SilexUi::ButtonComponent.new(label: "Éditer", href: edit_website_path(site), variant: :ghost, size: :sm) %>
|
|
126
|
+
<% end %>
|
|
127
|
+
<% end %>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Développement
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bundle install
|
|
134
|
+
bundle exec rspec
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Pour travailler sur le gem depuis une application, pointer le `Gemfile` de
|
|
138
|
+
celle-ci sur la copie locale :
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
gem "silex_ui", path: "../silex_ui"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Publication
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# 1. faire monter SilexUi::VERSION et compléter le CHANGELOG
|
|
148
|
+
# 2. puis :
|
|
149
|
+
bundle exec rake release
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`rake release` pose le tag git, pousse la branche et envoie le `.gem` sur
|
|
153
|
+
rubygems.org. La MFA est exigée sur ce gem.
|
|
154
|
+
|
|
155
|
+
## Licence
|
|
156
|
+
|
|
157
|
+
MIT — voir `LICENSE.txt`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Copie une valeur dans le presse-papiers et le dit — un bouton qui ne confirme
|
|
4
|
+
// rien laisse l'utilisateur cliquer trois fois.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
value: String,
|
|
8
|
+
label: { type: String, default: "Copier" },
|
|
9
|
+
copiedLabel: { type: String, default: "Copié" },
|
|
10
|
+
resetAfter: { type: Number, default: 1500 }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async copy(event) {
|
|
14
|
+
event.preventDefault()
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(this.valueValue)
|
|
18
|
+
this.#confirm()
|
|
19
|
+
} catch {
|
|
20
|
+
// Presse-papiers refusé (contexte non sécurisé, permission) : on laisse la
|
|
21
|
+
// valeur sélectionnable plutôt que de mentir sur le succès.
|
|
22
|
+
this.element.textContent = "Échec"
|
|
23
|
+
this.#restoreLater()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#confirm() {
|
|
28
|
+
this.element.textContent = this.copiedLabelValue
|
|
29
|
+
this.#restoreLater()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#restoreLater() {
|
|
33
|
+
clearTimeout(this.timeout)
|
|
34
|
+
this.timeout = setTimeout(() => {
|
|
35
|
+
this.element.textContent = this.labelValue
|
|
36
|
+
}, this.resetAfterValue)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
disconnect() {
|
|
40
|
+
clearTimeout(this.timeout)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Ouverture et fermeture d'un <dialog>. Le focus, la touche Échap et l'inertie
|
|
4
|
+
// du fond sont l'affaire du navigateur ; on ne réimplémente rien de tout ça.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["dialog"]
|
|
7
|
+
|
|
8
|
+
open(event) {
|
|
9
|
+
event?.preventDefault()
|
|
10
|
+
this.dialogTarget.showModal()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
close(event) {
|
|
14
|
+
event?.preventDefault()
|
|
15
|
+
this.dialogTarget.close()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Un clic hors du panneau ferme : dans un <dialog>, ce clic atteint le dialog
|
|
19
|
+
// lui-même, jamais son contenu.
|
|
20
|
+
closeOnBackdrop(event) {
|
|
21
|
+
if (event.target === this.dialogTarget) this.dialogTarget.close()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ClipboardController from "silex_ui/controllers/clipboard_controller"
|
|
2
|
+
import ModalController from "silex_ui/controllers/modal_controller"
|
|
3
|
+
|
|
4
|
+
// Enregistre les contrôleurs du design system sous le préfixe `silex-ui--`,
|
|
5
|
+
// pour qu'ils ne puissent jamais entrer en collision avec ceux de l'application.
|
|
6
|
+
export function registerSilexUiControllers(application) {
|
|
7
|
+
application.register("silex-ui--clipboard", ClipboardController)
|
|
8
|
+
application.register("silex-ui--modal", ModalController)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { ClipboardController, ModalController }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* Silex UI — design tokens.
|
|
2
|
+
*
|
|
3
|
+
* Une seule source de vérité pour la palette, la typographie et les rayons des
|
|
4
|
+
* applications Le Silex. Les noms suivent la convention Tailwind v4 : déclarer
|
|
5
|
+
* `--color-ink-900` suffit à rendre `text-ink-900`, `bg-ink-900`, `border-ink-900`
|
|
6
|
+
* disponibles dans l'application comme dans les composants du gem.
|
|
7
|
+
*
|
|
8
|
+
* Le ton : un fond sable chaud, une encre presque noire, et des accents qui se
|
|
9
|
+
* posent sur le sable au lieu de crier par-dessus. */
|
|
10
|
+
|
|
11
|
+
@theme {
|
|
12
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
13
|
+
|
|
14
|
+
/* Sable — le sol. Du plus clair (fond de page) au plus soutenu (bordures). */
|
|
15
|
+
--color-sand-50: #faf8f4;
|
|
16
|
+
--color-sand-100: #f4f0e8;
|
|
17
|
+
--color-sand-200: #e8e1d4;
|
|
18
|
+
--color-sand-300: #d6ccb9;
|
|
19
|
+
|
|
20
|
+
/* Encre — jamais un noir pur, du texte discret au texte plein. */
|
|
21
|
+
--color-ink-300: #bdb9b3;
|
|
22
|
+
--color-ink-400: #9a9691;
|
|
23
|
+
--color-ink-600: #6b6763;
|
|
24
|
+
--color-ink-900: #1c1a17;
|
|
25
|
+
|
|
26
|
+
/* Alerte — terracotta : ce qui a lapsé, ce qui est cassé. */
|
|
27
|
+
--color-alert-50: #fbf1ee;
|
|
28
|
+
--color-alert-100: #f2dcd5;
|
|
29
|
+
--color-alert-300: #d99e8c;
|
|
30
|
+
--color-alert-700: #9c3b26;
|
|
31
|
+
|
|
32
|
+
/* Attention — ocre : ce qui est sur le point de basculer. */
|
|
33
|
+
--color-warn-50: #fbf5e8;
|
|
34
|
+
--color-warn-100: #f0e5c8;
|
|
35
|
+
--color-warn-300: #d8bd7c;
|
|
36
|
+
--color-warn-700: #8a6318;
|
|
37
|
+
|
|
38
|
+
/* Validation — sauge : ce qui est en règle. */
|
|
39
|
+
--color-ok-50: #f1f5ef;
|
|
40
|
+
--color-ok-100: #dfe8da;
|
|
41
|
+
--color-ok-300: #a8bf9c;
|
|
42
|
+
--color-ok-700: #4a6b3d;
|
|
43
|
+
|
|
44
|
+
/* Information — bleu ardoise : neutre, factuel, sans urgence. */
|
|
45
|
+
--color-info-50: #eff2f6;
|
|
46
|
+
--color-info-100: #dde4ee;
|
|
47
|
+
--color-info-300: #9db0cc;
|
|
48
|
+
--color-info-700: #3c5578;
|
|
49
|
+
}
|
|
50
|
+
|
|
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
|
+
@layer base {
|
|
54
|
+
body {
|
|
55
|
+
background-color: var(--color-sand-50);
|
|
56
|
+
color: var(--color-ink-900);
|
|
57
|
+
-webkit-font-smoothing: antialiased;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="<%= css_classes %>" role="status">
|
|
2
|
+
<% if title.present? %>
|
|
3
|
+
<p class="text-xs font-light tracking-widest uppercase"><%= title %></p>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% if text.present? %>
|
|
7
|
+
<p class="<%= "mt-1" if title.present? %>"><%= text %></p>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%= content %>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Bandeau d'information persistant — l'AlertBannerComponent du Silex : un ton,
|
|
5
|
+
# un titre, un texte.
|
|
6
|
+
#
|
|
7
|
+
# <%= render SilexUi::AlertComponent.new(tone: :warn, title: "Synchronisation en retard",
|
|
8
|
+
# text: "Les registrars n'ont pas répondu depuis 3 jours.") %>
|
|
9
|
+
class AlertComponent < BaseComponent
|
|
10
|
+
TONE_CLASSES = {
|
|
11
|
+
neutral: "border-sand-300 bg-white text-ink-900",
|
|
12
|
+
ok: "border-ok-300 bg-ok-50 text-ok-700",
|
|
13
|
+
warn: "border-warn-300 bg-warn-50 text-warn-700",
|
|
14
|
+
alert: "border-alert-300 bg-alert-50 text-alert-700",
|
|
15
|
+
info: "border-info-300 bg-info-50 text-info-700"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(title: nil, text: nil, tone: :info, **options)
|
|
19
|
+
@title = title
|
|
20
|
+
@text = text
|
|
21
|
+
@tone = tone_of(tone)
|
|
22
|
+
super(**options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :title, :text
|
|
26
|
+
|
|
27
|
+
def css_classes
|
|
28
|
+
merge_class("rounded-sm border px-4 py-3 text-sm font-light", TONE_CLASSES[@tone])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Pastille de statut. Reprend l'API du BadgeComponent du Silex — un libellé, un
|
|
5
|
+
# ton, un lien et une infobulle facultatifs — en tons sémantiques.
|
|
6
|
+
#
|
|
7
|
+
# SilexUi::BadgeComponent.new(label: "expiré depuis 3 j", tone: :alert)
|
|
8
|
+
class BadgeComponent < BaseComponent
|
|
9
|
+
TONE_CLASSES = {
|
|
10
|
+
neutral: "border-sand-300 bg-white text-ink-600",
|
|
11
|
+
ok: "border-ok-300 bg-ok-50 text-ok-700",
|
|
12
|
+
warn: "border-warn-300 bg-warn-50 text-warn-700",
|
|
13
|
+
alert: "border-alert-300 bg-alert-50 text-alert-700",
|
|
14
|
+
info: "border-info-300 bg-info-50 text-info-700"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(label: nil, tone: :neutral, href: nil, title: nil, **options)
|
|
18
|
+
@label = label
|
|
19
|
+
@tone = tone_of(tone)
|
|
20
|
+
@href = href
|
|
21
|
+
@title = title.presence
|
|
22
|
+
super(**options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
text = @label || content
|
|
27
|
+
attributes = html_attributes.merge(class: css_classes, title: @title)
|
|
28
|
+
|
|
29
|
+
@href.present? ? link_to(text, @href, **attributes) : tag.span(text, **attributes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def css_classes
|
|
35
|
+
merge_class(
|
|
36
|
+
"inline-block rounded-sm border px-2 py-0.5 text-xs font-light whitespace-nowrap",
|
|
37
|
+
("transition hover:border-ink-900 hover:text-ink-900" if @href.present?),
|
|
38
|
+
TONE_CLASSES[@tone]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Socle commun : la palette de tons et la fusion de classes.
|
|
5
|
+
#
|
|
6
|
+
# Chaque composant accepte `class:` pour ajuster son rendu sans le forker —
|
|
7
|
+
# les classes de l'appelant sont ajoutées après celles du composant, donc
|
|
8
|
+
# elles gagnent à spécificité égale.
|
|
9
|
+
class BaseComponent < ViewComponent::Base
|
|
10
|
+
# Les tons sont sémantiques, jamais chromatiques : on écrit `tone: :alert`,
|
|
11
|
+
# pas `tone: :red`. Le jour où l'alerte devient violette, une seule ligne bouge.
|
|
12
|
+
TONES = %i[neutral ok warn alert info].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(class: nil, **html_attributes)
|
|
15
|
+
@extra_class = binding.local_variable_get(:class)
|
|
16
|
+
@html_attributes = html_attributes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :extra_class, :html_attributes
|
|
22
|
+
|
|
23
|
+
def tone_of(value)
|
|
24
|
+
TONES.include?(value&.to_sym) ? value.to_sym : :neutral
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `class_names` (alias de `token_list`) élimine les nil et les doublons.
|
|
28
|
+
def merge_class(*base)
|
|
29
|
+
class_names(*base, extra_class)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<% if any? %>
|
|
2
|
+
<%= render SilexUi::SectionComponent.new(title: title) do %>
|
|
3
|
+
<ul class="<%= css_classes %>">
|
|
4
|
+
<% items.each do |item| %>
|
|
5
|
+
<li class="flex justify-between gap-4 border-b border-sand-100 pb-2">
|
|
6
|
+
<% if item[:href].present? %>
|
|
7
|
+
<%= link_to item[:label], item[:href], class: "truncate text-ink-600 transition hover:text-ink-900" %>
|
|
8
|
+
<% else %>
|
|
9
|
+
<span class="truncate text-ink-600"><%= item[:label] %></span>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<span class="shrink-0 text-ink-400"><%= item[:count] %></span>
|
|
13
|
+
</li>
|
|
14
|
+
<% end %>
|
|
15
|
+
</ul>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Répartition : une liste libellé / compte, chaque ligne cliquable vers la
|
|
5
|
+
# liste filtrée correspondante. Le motif se répète sur tous les tableaux de
|
|
6
|
+
# bord du Silex (par hébergeur, par registrar, par tag).
|
|
7
|
+
#
|
|
8
|
+
# <%= render SilexUi::BreakdownComponent.new(title: "Par registrar",
|
|
9
|
+
# items: @by_registrar.map { |name, count| { label: name.titleize, count:, href: websites_path(registrar: name) } }) %>
|
|
10
|
+
class BreakdownComponent < BaseComponent
|
|
11
|
+
def initialize(items:, title: nil, **options)
|
|
12
|
+
@items = items
|
|
13
|
+
@title = title
|
|
14
|
+
super(**options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :items, :title
|
|
18
|
+
|
|
19
|
+
def any?
|
|
20
|
+
items.any?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def css_classes
|
|
24
|
+
merge_class("space-y-2 text-sm font-light")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Bouton ou lien : même costume, trois usages.
|
|
5
|
+
#
|
|
6
|
+
# SilexUi::ButtonComponent.new(label: "Ajouter", href: new_website_path)
|
|
7
|
+
# SilexUi::ButtonComponent.new(label: "Supprimer", href: website_path(w), method: :delete, variant: :danger)
|
|
8
|
+
# SilexUi::ButtonComponent.new(label: "Appliquer", type: :submit)
|
|
9
|
+
#
|
|
10
|
+
# `method:` bascule sur `button_to` (formulaire + jeton CSRF) ; sans `href`,
|
|
11
|
+
# on rend un `<button>`.
|
|
12
|
+
class ButtonComponent < BaseComponent
|
|
13
|
+
VARIANTS = {
|
|
14
|
+
primary: "bg-ink-900 text-sand-50 hover:bg-ink-600",
|
|
15
|
+
secondary: "border border-sand-300 bg-white text-ink-900 hover:border-ink-900",
|
|
16
|
+
ghost: "text-ink-400 hover:text-ink-900",
|
|
17
|
+
danger: "border border-alert-300 bg-alert-50 text-alert-700 hover:border-alert-700"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
SIZES = {
|
|
21
|
+
sm: "px-3 py-1 text-xs",
|
|
22
|
+
md: "px-4 py-2 text-xs",
|
|
23
|
+
lg: "px-6 py-3 text-sm"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(label: nil, href: nil, variant: :primary, size: :md, method: nil, type: :button, **options)
|
|
27
|
+
@label = label
|
|
28
|
+
@href = href
|
|
29
|
+
@variant = VARIANTS.key?(variant.to_sym) ? variant.to_sym : :primary
|
|
30
|
+
@size = SIZES.key?(size.to_sym) ? size.to_sym : :md
|
|
31
|
+
@method = method
|
|
32
|
+
@type = type
|
|
33
|
+
super(**options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call
|
|
37
|
+
if @method.present? && @href.present?
|
|
38
|
+
button_to(text, @href, **attributes.merge(method: @method))
|
|
39
|
+
elsif @href.present?
|
|
40
|
+
link_to(text, @href, **attributes)
|
|
41
|
+
else
|
|
42
|
+
button_tag(text, **attributes.merge(type: @type))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def text
|
|
49
|
+
@label || content
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def attributes
|
|
53
|
+
html_attributes.merge(class: css_classes)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def css_classes
|
|
57
|
+
merge_class(
|
|
58
|
+
"inline-block cursor-pointer rounded-sm font-light uppercase tracking-widest transition",
|
|
59
|
+
SIZES[@size],
|
|
60
|
+
VARIANTS[@variant]
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% body = capture do %>
|
|
2
|
+
<% if title.present? || header? %>
|
|
3
|
+
<div class="mb-3 flex items-baseline justify-between gap-4">
|
|
4
|
+
<% if title.present? %>
|
|
5
|
+
<h3 class="text-xs font-light tracking-widest text-ink-400 uppercase"><%= title %></h3>
|
|
6
|
+
<% end %>
|
|
7
|
+
<%= header %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<div class="text-sm font-light text-ink-600"><%= content %></div>
|
|
12
|
+
|
|
13
|
+
<% if footer? %>
|
|
14
|
+
<div class="mt-4 border-t border-sand-100 pt-3 text-xs font-light text-ink-400"><%= footer %></div>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<% if href.present? %>
|
|
19
|
+
<%= link_to href, class: css_classes do %><%= body %><% end %>
|
|
20
|
+
<% else %>
|
|
21
|
+
<div class="<%= css_classes %>"><%= body %></div>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Surface surélevée : un encadré blanc sur le sable. Sert de conteneur à tout
|
|
5
|
+
# ce qui doit se détacher du flux — panneau de synchronisation, fiche, aperçu.
|
|
6
|
+
class CardComponent < BaseComponent
|
|
7
|
+
renders_one :header
|
|
8
|
+
renders_one :footer
|
|
9
|
+
|
|
10
|
+
def initialize(title: nil, href: nil, **options)
|
|
11
|
+
@title = title
|
|
12
|
+
@href = href
|
|
13
|
+
super(**options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :title, :href
|
|
17
|
+
|
|
18
|
+
def css_classes
|
|
19
|
+
merge_class(
|
|
20
|
+
"block rounded-sm border border-sand-200 bg-white p-5",
|
|
21
|
+
("transition hover:border-ink-900" if href.present?)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Copier une valeur sans la sélectionner à la souris. Visuellement un badge,
|
|
5
|
+
# comme le CopyButtonComponent du Silex, mais branché sur le contrôleur
|
|
6
|
+
# Stimulus fourni par le gem.
|
|
7
|
+
#
|
|
8
|
+
# <%= render SilexUi::CopyButtonComponent.new(value: website.domain) %>
|
|
9
|
+
class CopyButtonComponent < BaseComponent
|
|
10
|
+
def initialize(value:, label: "Copier", copied_label: "Copié", tone: :neutral, **options)
|
|
11
|
+
@value = value.to_s
|
|
12
|
+
@label = label
|
|
13
|
+
@copied_label = copied_label
|
|
14
|
+
@tone = tone_of(tone)
|
|
15
|
+
super(**options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
button_tag(@label, type: "button", class: css_classes, **stimulus_attributes)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def css_classes
|
|
25
|
+
merge_class(
|
|
26
|
+
"inline-block cursor-pointer rounded-sm border px-2 py-0.5 text-xs font-light whitespace-nowrap",
|
|
27
|
+
"transition hover:border-ink-900 hover:text-ink-900",
|
|
28
|
+
SilexUi::BadgeComponent::TONE_CLASSES[@tone]
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def stimulus_attributes
|
|
33
|
+
html_attributes.deep_merge(
|
|
34
|
+
data: {
|
|
35
|
+
controller: "silex-ui--clipboard",
|
|
36
|
+
action: "silex-ui--clipboard#copy",
|
|
37
|
+
silex_ui__clipboard_value_value: @value,
|
|
38
|
+
silex_ui__clipboard_label_value: @label,
|
|
39
|
+
silex_ui__clipboard_copied_label_value: @copied_label
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|