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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +157 -0
  5. data/app/assets/javascripts/silex_ui/controllers/clipboard_controller.js +42 -0
  6. data/app/assets/javascripts/silex_ui/controllers/modal_controller.js +23 -0
  7. data/app/assets/javascripts/silex_ui/index.js +11 -0
  8. data/app/assets/stylesheets/silex_ui/theme.css +59 -0
  9. data/app/components/silex_ui/alert_component.html.erb +11 -0
  10. data/app/components/silex_ui/alert_component.rb +31 -0
  11. data/app/components/silex_ui/badge_component.rb +42 -0
  12. data/app/components/silex_ui/base_component.rb +32 -0
  13. data/app/components/silex_ui/breakdown_component.html.erb +17 -0
  14. data/app/components/silex_ui/breakdown_component.rb +27 -0
  15. data/app/components/silex_ui/button_component.rb +64 -0
  16. data/app/components/silex_ui/card_component.html.erb +22 -0
  17. data/app/components/silex_ui/card_component.rb +25 -0
  18. data/app/components/silex_ui/copy_button_component.rb +44 -0
  19. data/app/components/silex_ui/empty_state_component.rb +20 -0
  20. data/app/components/silex_ui/field_component.html.erb +18 -0
  21. data/app/components/silex_ui/field_component.rb +88 -0
  22. data/app/components/silex_ui/flash_component.html.erb +6 -0
  23. data/app/components/silex_ui/flash_component.rb +42 -0
  24. data/app/components/silex_ui/modal_component.html.erb +24 -0
  25. data/app/components/silex_ui/modal_component.rb +33 -0
  26. data/app/components/silex_ui/nav_component.html.erb +15 -0
  27. data/app/components/silex_ui/nav_component.rb +44 -0
  28. data/app/components/silex_ui/page_header_component.html.erb +17 -0
  29. data/app/components/silex_ui/page_header_component.rb +35 -0
  30. data/app/components/silex_ui/pagination_component.html.erb +30 -0
  31. data/app/components/silex_ui/pagination_component.rb +47 -0
  32. data/app/components/silex_ui/section_component.html.erb +12 -0
  33. data/app/components/silex_ui/section_component.rb +24 -0
  34. data/app/components/silex_ui/stat_card_component.html.erb +18 -0
  35. data/app/components/silex_ui/stat_card_component.rb +61 -0
  36. data/app/components/silex_ui/stat_grid_component.rb +41 -0
  37. data/app/components/silex_ui/table_component.html.erb +25 -0
  38. data/app/components/silex_ui/table_component.rb +98 -0
  39. data/config/importmap.rb +5 -0
  40. data/lib/generators/silex_ui/install/install_generator.rb +68 -0
  41. data/lib/silex_ui/engine.rb +31 -0
  42. data/lib/silex_ui/sync.rb +43 -0
  43. data/lib/silex_ui/version.rb +5 -0
  44. data/lib/silex_ui.rb +37 -0
  45. data/lib/tasks/silex_ui.rake +19 -0
  46. metadata +131 -0
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SilexUi
4
+ # Tableau de données. On déclare les colonnes, le composant s'occupe du reste :
5
+ # en-tête, tri, lignes, état vide.
6
+ #
7
+ # <%= render SilexUi::TableComponent.new(rows: @websites, row_class: ->(w) { expiry_style(w, :row) }) do |table| %>
8
+ # <% table.with_column(header: "Domaine", sort: sort_url("domaine")) { |site| site.domain } %>
9
+ # <% table.with_column(header: "Registrar") { |site| site.registrar.titleize } %>
10
+ # <% end %>
11
+ #
12
+ # Comme le TableComponent du Silex, le tri est une URL fournie par l'appelant :
13
+ # le composant ne connaît ni les paramètres de la page, ni ses filtres.
14
+ class TableComponent < BaseComponent
15
+ def initialize(rows:, row_class: nil, empty_message: "Aucun élément à afficher", **options)
16
+ @rows = rows
17
+ @row_class = row_class
18
+ @empty_message = empty_message
19
+ @columns = []
20
+ super(**options)
21
+ end
22
+
23
+ attr_reader :rows, :columns, :empty_message
24
+
25
+ # Déclare une colonne. Le bloc reçoit la ligne et rend la cellule ; il peut
26
+ # renvoyer une chaîne comme écrire du balisage ERB, `capture` gère les deux.
27
+ def with_column(header: nil, sort: nil, direction: nil, align: :left, cell_class: nil, &cell)
28
+ @columns << Column.new(header: header, sort: sort, direction: direction,
29
+ align: align, cell_class: cell_class, cell: cell)
30
+ nil
31
+ end
32
+
33
+ # Le bloc passé au composant ne produit rien à afficher : il ne sert qu'à
34
+ # déclarer les colonnes. On le déroule avant le rendu du gabarit.
35
+ def before_render
36
+ content
37
+ end
38
+
39
+ def empty?
40
+ rows.none?
41
+ end
42
+
43
+ def row_classes(row)
44
+ class_names("border-b border-sand-100 align-top", @row_class&.call(row))
45
+ end
46
+
47
+ # Un en-tête tient sur une ligne : ce que le gabarit indente finit dans le
48
+ # texte de la cellule, et une assertion sur « Domaine » ne le retrouve plus.
49
+ def header_for(column)
50
+ return column.header unless column.sortable?
51
+
52
+ link_to(column.sort, class: "inline-flex items-center gap-1 transition hover:text-ink-900") do
53
+ safe_join([ column.header, tag.span(column.sort_marker, class: "text-ink-300") ], " ")
54
+ end
55
+ end
56
+
57
+ def cell_for(column, row)
58
+ view_context.capture(row, &column.cell)
59
+ end
60
+
61
+ def css_classes
62
+ merge_class("w-full border-collapse text-sm font-light")
63
+ end
64
+
65
+ Column = Struct.new(:header, :sort, :direction, :align, :cell_class, :cell, keyword_init: true) do
66
+ ALIGNMENTS = { left: "text-left", center: "text-center", right: "text-right" }.freeze
67
+
68
+ def header_classes
69
+ ActionView::Helpers::TagHelper.build_tag_values(
70
+ "py-3 pr-4 font-light whitespace-nowrap", alignment
71
+ ).join(" ")
72
+ end
73
+
74
+ def cell_classes
75
+ ActionView::Helpers::TagHelper.build_tag_values("py-3 pr-4", alignment, cell_class).join(" ")
76
+ end
77
+
78
+ def sortable?
79
+ sort.present?
80
+ end
81
+
82
+ # ↑ / ↓ quand la colonne porte le tri courant, ↕ quand elle peut le prendre.
83
+ def sort_marker
84
+ case direction&.to_sym
85
+ when :asc then "↑"
86
+ when :desc then "↓"
87
+ else "↕"
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def alignment
94
+ ALIGNMENTS.fetch(align&.to_sym, ALIGNMENTS[:left])
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "silex_ui", to: "silex_ui/index.js"
4
+ pin_all_from SilexUi::Engine.root.join("app/assets/javascripts/silex_ui/controllers"),
5
+ under: "silex_ui/controllers"
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "silex_ui/sync"
5
+
6
+ module SilexUi
7
+ module Generators
8
+ # `rails generate silex_ui:install`
9
+ #
10
+ # Branche le design system sur une application Rails + Tailwind v4 : copie
11
+ # les tokens, déclare les sources auprès du scanner Tailwind, épingle les
12
+ # contrôleurs Stimulus.
13
+ class InstallGenerator < Rails::Generators::Base
14
+ desc "Installe silex_ui dans l'application (tokens Tailwind, sources, importmap)"
15
+
16
+ TAILWIND_INPUT = "app/assets/tailwind/application.css"
17
+ IMPORT_LINE = %(@import "./silex_ui/theme.css";)
18
+ SOURCE_LINE = %(@source "./silex_ui/sources.html";)
19
+
20
+ def sync_design_system
21
+ say_status :sync, SilexUi::Sync::DESTINATION
22
+ SilexUi::Sync.call(destination_root)
23
+ end
24
+
25
+ def declare_tailwind_sources
26
+ path = File.join(destination_root, TAILWIND_INPUT)
27
+
28
+ unless File.exist?(path)
29
+ say_status :skip, "#{TAILWIND_INPUT} introuvable — ajoutez à la main :\n#{IMPORT_LINE}\n#{SOURCE_LINE}", :yellow
30
+ return
31
+ end
32
+
33
+ return say_status(:identical, TAILWIND_INPUT) if File.read(path).include?(IMPORT_LINE)
34
+
35
+ inject_into_file TAILWIND_INPUT, after: %r{@import "tailwindcss";\n} do
36
+ "\n#{IMPORT_LINE}\n#{SOURCE_LINE}\n"
37
+ end
38
+ end
39
+
40
+ def pin_javascript
41
+ return say_status(:skip, "importmap absent — contrôleurs Stimulus non épinglés", :yellow) unless importmap?
42
+
43
+ say_status :info, "Contrôleurs disponibles : import { registerSilexUiControllers } from \"silex_ui\""
44
+ end
45
+
46
+ def show_next_steps
47
+ say <<~TEXT
48
+
49
+ silex_ui #{SilexUi::VERSION} installé.
50
+
51
+ 1. Enregistrez les contrôleurs Stimulus dans app/javascript/controllers/index.js :
52
+
53
+ import { registerSilexUiControllers } from "silex_ui"
54
+ registerSilexUiControllers(application)
55
+
56
+ 2. Après chaque montée de version du gem : bin/rails silex_ui:sync
57
+
58
+ TEXT
59
+ end
60
+
61
+ private
62
+
63
+ def importmap?
64
+ File.exist?(File.join(destination_root, "config/importmap.rb"))
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+ require "view_component"
5
+
6
+ module SilexUi
7
+ # A plain engine (no isolate_namespace): the gem ships components, styles and
8
+ # Stimulus controllers, never routes or models. Its app/ directories are picked
9
+ # up by the host application's autoloader and asset pipeline as-is.
10
+ class Engine < ::Rails::Engine
11
+ config.silex_ui = ActiveSupport::OrderedOptions.new
12
+
13
+ initializer "silex_ui.assets" do |app|
14
+ next unless app.config.respond_to?(:assets)
15
+
16
+ app.config.assets.paths << root.join("app/assets/stylesheets")
17
+ app.config.assets.paths << root.join("app/assets/javascripts")
18
+ end
19
+
20
+ initializer "silex_ui.importmap", before: "importmap" do |app|
21
+ next unless app.respond_to?(:config) && app.config.respond_to?(:importmap)
22
+
23
+ app.config.importmap.paths << root.join("config/importmap.rb")
24
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
25
+ end
26
+
27
+ rake_tasks do
28
+ load SilexUi::Engine.root.join("lib/tasks/silex_ui.rake")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SilexUi
4
+ # Recopie dans l'application ce dont le build Tailwind a besoin : les tokens,
5
+ # et une image plate des sources du gem.
6
+ #
7
+ # Pourquoi une copie plutôt qu'un `@source` pointant vers le gem ? Parce que le
8
+ # chemin d'installation d'un gem change entre le poste de dev, la CI et le
9
+ # conteneur. Les deux fichiers produits ici vivent dans le dépôt de
10
+ # l'application, sont versionnés avec elle, et se régénèrent d'une commande
11
+ # après chaque montée de version du gem.
12
+ module Sync
13
+ DESTINATION = "app/assets/tailwind/silex_ui"
14
+ THEME_FILE = "theme.css"
15
+ SOURCES_FILE = "sources.html"
16
+
17
+ module_function
18
+
19
+ def call(app_root)
20
+ destination = Pathname.new(app_root).join(DESTINATION)
21
+ destination.mkpath
22
+
23
+ destination.join(THEME_FILE).write(SilexUi.theme_css)
24
+ destination.join(SOURCES_FILE).write(sources_document)
25
+
26
+ [ destination.join(THEME_FILE), destination.join(SOURCES_FILE) ]
27
+ end
28
+
29
+ # Tailwind n'a pas besoin de comprendre ce fichier, seulement d'y retrouver
30
+ # les mêmes chaînes de classes que dans les composants du gem.
31
+ def sources_document
32
+ <<~HTML
33
+ <!-- Généré par `rails silex_ui:sync` — silex_ui #{SilexUi::VERSION}.
34
+ Ne pas modifier à la main, ne pas servir : ce fichier n'existe que
35
+ pour donner au scanner Tailwind de l'application les classes
36
+ utilisées par les composants du design system. -->
37
+ <script type="text/plain">
38
+ #{SilexUi.source_digest}
39
+ </script>
40
+ HTML
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SilexUi
4
+ VERSION = "0.1.0"
5
+ end
data/lib/silex_ui.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "silex_ui/version"
5
+ require "silex_ui/engine" if defined?(Rails::Engine)
6
+
7
+ # Design system partagé des applications Le Silex.
8
+ #
9
+ # Le gem ne fournit que la couche de présentation : des design tokens Tailwind v4,
10
+ # un jeu de ViewComponents et les contrôleurs Stimulus qui vont avec.
11
+ module SilexUi
12
+ # Répertoires dont le contenu doit être visible du scanner Tailwind de
13
+ # l'application hôte. `rails silex_ui:sync` recopie ces sources dans l'app.
14
+ SOURCE_DIRECTORIES = %w[app/components app/assets/javascripts].freeze
15
+
16
+ class << self
17
+ def root
18
+ @root ||= Pathname.new(File.expand_path("..", __dir__))
19
+ end
20
+
21
+ # Concaténation brute des sources du gem. Tailwind y retrouve exactement les
22
+ # mêmes candidats que dans les fichiers d'origine — sans avoir à connaître le
23
+ # chemin d'installation du gem, qui change entre poste, CI et conteneur.
24
+ def source_digest
25
+ files = SOURCE_DIRECTORIES.flat_map do |dir|
26
+ Dir.glob(root.join(dir, "**", "*.{rb,erb,js}")).sort
27
+ end
28
+
29
+ files.map { |file| "/* #{Pathname.new(file).relative_path_from(root)} */\n#{File.read(file)}" }
30
+ .join("\n")
31
+ end
32
+
33
+ def theme_css
34
+ File.read(root.join("app/assets/stylesheets/silex_ui/theme.css"))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "silex_ui/sync"
4
+
5
+ namespace :silex_ui do
6
+ desc "Recopie les tokens et les sources du design system dans app/assets/tailwind/silex_ui"
7
+ task sync: :environment do
8
+ files = SilexUi::Sync.call(Rails.root)
9
+
10
+ puts "silex_ui #{SilexUi::VERSION} synchronisé :"
11
+ files.each { |file| puts " #{file.relative_path_from(Rails.root)}" }
12
+ end
13
+ end
14
+
15
+ # Le build Tailwind doit toujours partir de sources à jour : une montée de
16
+ # version du gem ne doit pas attendre qu'on pense à relancer la synchro.
17
+ if Rake::Task.task_defined?("tailwindcss:build")
18
+ Rake::Task["tailwindcss:build"].enhance([ "silex_ui:sync" ])
19
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: silex_ui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Théo Galais
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '10'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '10'
33
+ - !ruby/object:Gem::Dependency
34
+ name: view_component
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '6'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '6'
53
+ description: Design tokens Tailwind v4, ViewComponents et contrôleurs Stimulus pour
54
+ donner la même interface à toutes les applications Rails du Silex.
55
+ email:
56
+ - toolsilex@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - CHANGELOG.md
62
+ - LICENSE.txt
63
+ - README.md
64
+ - app/assets/javascripts/silex_ui/controllers/clipboard_controller.js
65
+ - app/assets/javascripts/silex_ui/controllers/modal_controller.js
66
+ - app/assets/javascripts/silex_ui/index.js
67
+ - app/assets/stylesheets/silex_ui/theme.css
68
+ - app/components/silex_ui/alert_component.html.erb
69
+ - app/components/silex_ui/alert_component.rb
70
+ - app/components/silex_ui/badge_component.rb
71
+ - app/components/silex_ui/base_component.rb
72
+ - app/components/silex_ui/breakdown_component.html.erb
73
+ - app/components/silex_ui/breakdown_component.rb
74
+ - app/components/silex_ui/button_component.rb
75
+ - app/components/silex_ui/card_component.html.erb
76
+ - app/components/silex_ui/card_component.rb
77
+ - app/components/silex_ui/copy_button_component.rb
78
+ - app/components/silex_ui/empty_state_component.rb
79
+ - app/components/silex_ui/field_component.html.erb
80
+ - app/components/silex_ui/field_component.rb
81
+ - app/components/silex_ui/flash_component.html.erb
82
+ - app/components/silex_ui/flash_component.rb
83
+ - app/components/silex_ui/modal_component.html.erb
84
+ - app/components/silex_ui/modal_component.rb
85
+ - app/components/silex_ui/nav_component.html.erb
86
+ - app/components/silex_ui/nav_component.rb
87
+ - app/components/silex_ui/page_header_component.html.erb
88
+ - app/components/silex_ui/page_header_component.rb
89
+ - app/components/silex_ui/pagination_component.html.erb
90
+ - app/components/silex_ui/pagination_component.rb
91
+ - app/components/silex_ui/section_component.html.erb
92
+ - app/components/silex_ui/section_component.rb
93
+ - app/components/silex_ui/stat_card_component.html.erb
94
+ - app/components/silex_ui/stat_card_component.rb
95
+ - app/components/silex_ui/stat_grid_component.rb
96
+ - app/components/silex_ui/table_component.html.erb
97
+ - app/components/silex_ui/table_component.rb
98
+ - config/importmap.rb
99
+ - lib/generators/silex_ui/install/install_generator.rb
100
+ - lib/silex_ui.rb
101
+ - lib/silex_ui/engine.rb
102
+ - lib/silex_ui/sync.rb
103
+ - lib/silex_ui/version.rb
104
+ - lib/tasks/silex_ui.rake
105
+ homepage: https://github.com/Le-Silex/silex_ui
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ source_code_uri: https://github.com/Le-Silex/silex_ui
110
+ changelog_uri: https://github.com/Le-Silex/silex_ui/blob/main/CHANGELOG.md
111
+ rubygems_mfa_required: 'true'
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 3.2.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.5.16
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Design system partagé des applications Le Silex (Rails + Tailwind v4)
131
+ test_files: []