silex_ui 0.2.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +184 -0
- data/LICENSE.txt +6 -0
- data/README.md +97 -7
- data/app/assets/javascripts/silex_ui/controllers/clipboard_controller.js +10 -10
- data/app/assets/javascripts/silex_ui/controllers/dropdown_controller.js +24 -0
- data/app/assets/javascripts/silex_ui/controllers/inline_edit_controller.js +39 -0
- data/app/assets/javascripts/silex_ui/controllers/remote_modal_controller.js +22 -0
- data/app/assets/javascripts/silex_ui/controllers/sidebar_controller.js +34 -0
- data/app/assets/javascripts/silex_ui/controllers/theme_controller.js +43 -0
- data/app/assets/javascripts/silex_ui/controllers/tooltip_controller.js +65 -0
- data/app/assets/javascripts/silex_ui/index.js +22 -1
- data/app/assets/stylesheets/silex_ui/theme.css +528 -42
- data/app/components/silex_ui/action_list_component.rb +76 -0
- data/app/components/silex_ui/alert_component.html.erb +14 -6
- data/app/components/silex_ui/alert_component.rb +24 -12
- data/app/components/silex_ui/avatar_component.rb +56 -0
- data/app/components/silex_ui/badge_component.rb +21 -17
- data/app/components/silex_ui/base_component.rb +10 -3
- data/app/components/silex_ui/breakdown_component.html.erb +20 -6
- data/app/components/silex_ui/breakdown_component.rb +21 -4
- data/app/components/silex_ui/button_component.rb +31 -16
- data/app/components/silex_ui/card_component.html.erb +7 -4
- data/app/components/silex_ui/card_component.rb +9 -6
- data/app/components/silex_ui/copy_button_component.rb +17 -8
- data/app/components/silex_ui/dropdown_component.html.erb +13 -0
- data/app/components/silex_ui/dropdown_component.rb +82 -0
- data/app/components/silex_ui/empty_state_component.html.erb +13 -0
- data/app/components/silex_ui/empty_state_component.rb +6 -9
- data/app/components/silex_ui/field_component.html.erb +4 -4
- data/app/components/silex_ui/field_component.rb +8 -5
- data/app/components/silex_ui/filter_bar_component.html.erb +22 -0
- data/app/components/silex_ui/filter_bar_component.rb +54 -0
- data/app/components/silex_ui/filter_field_component.html.erb +10 -0
- data/app/components/silex_ui/filter_field_component.rb +69 -0
- data/app/components/silex_ui/flash_component.html.erb +1 -1
- data/app/components/silex_ui/icon_component.rb +211 -0
- data/app/components/silex_ui/inline_edit_component.html.erb +23 -0
- data/app/components/silex_ui/inline_edit_component.rb +47 -0
- data/app/components/silex_ui/modal_component.html.erb +4 -2
- data/app/components/silex_ui/modal_component.rb +3 -2
- data/app/components/silex_ui/nav_component.html.erb +5 -4
- data/app/components/silex_ui/nav_component.rb +22 -7
- data/app/components/silex_ui/page_header_component.html.erb +38 -10
- data/app/components/silex_ui/page_header_component.rb +97 -11
- data/app/components/silex_ui/pagination_component.html.erb +11 -10
- data/app/components/silex_ui/pagination_component.rb +1 -2
- data/app/components/silex_ui/panel_component.html.erb +27 -0
- data/app/components/silex_ui/panel_component.rb +60 -0
- data/app/components/silex_ui/progress_component.html.erb +21 -0
- data/app/components/silex_ui/progress_component.rb +42 -0
- data/app/components/silex_ui/remote_modal_component.html.erb +30 -0
- data/app/components/silex_ui/remote_modal_component.rb +54 -0
- data/app/components/silex_ui/section_component.html.erb +5 -2
- data/app/components/silex_ui/section_component.rb +8 -8
- data/app/components/silex_ui/shell_component.html.erb +40 -0
- data/app/components/silex_ui/shell_component.rb +106 -0
- data/app/components/silex_ui/stat_card_component.html.erb +31 -6
- data/app/components/silex_ui/stat_card_component.rb +48 -28
- data/app/components/silex_ui/stat_grid_component.rb +8 -18
- data/app/components/silex_ui/table_component.html.erb +10 -8
- data/app/components/silex_ui/table_component.rb +52 -20
- data/app/components/silex_ui/theme_toggle_component.html.erb +11 -0
- data/app/components/silex_ui/theme_toggle_component.rb +29 -0
- data/app/components/silex_ui/tones.rb +72 -0
- data/app/views/silex_ui/catalogue/_specimen.html.erb +4 -4
- data/app/views/silex_ui/catalogue/show.html.erb +332 -187
- data/lib/silex_ui/version.rb +1 -1
- data/lib/silex_ui.rb +8 -0
- metadata +31 -2
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SilexUi
|
|
4
|
-
# Bandeau d'information
|
|
5
|
-
# un titre, un texte.
|
|
4
|
+
# Bandeau d'information — l'AlertBannerComponent du Silex : un trait de
|
|
5
|
+
# couleur à gauche, une icône, un titre, un texte.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# SilexUi::AlertComponent.new(tone: :warn, title: "Synchronisation en retard",
|
|
8
|
+
# text: "Les registrars n'ont pas répondu depuis 3 jours.")
|
|
9
9
|
class AlertComponent < BaseComponent
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
alert: "
|
|
15
|
-
|
|
10
|
+
# Chaque ton porte son icône par défaut : un bandeau rouge sans signe est un
|
|
11
|
+
# bandeau qu'on lit trop tard.
|
|
12
|
+
DEFAULT_ICONS = {
|
|
13
|
+
neutral: "info", brand: "sparkles", ok: "success", warn: "alert",
|
|
14
|
+
alert: "error", info: "info", purple: "sparkles", pink: "sparkles",
|
|
15
|
+
teal: "info", cyan: "info"
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
|
-
def initialize(title: nil, text: nil, tone: :info, **options)
|
|
18
|
+
def initialize(title: nil, text: nil, tone: :info, icon: nil, **options)
|
|
19
19
|
@title = title
|
|
20
20
|
@text = text
|
|
21
21
|
@tone = tone_of(tone)
|
|
22
|
+
@icon = icon
|
|
22
23
|
super(**options)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
attr_reader :title, :text
|
|
26
27
|
|
|
28
|
+
def icon_name
|
|
29
|
+
@icon.nil? ? DEFAULT_ICONS[@tone] : @icon.presence
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def accent_class
|
|
33
|
+
SilexUi::Tones.accent(@tone)
|
|
34
|
+
end
|
|
35
|
+
|
|
27
36
|
def css_classes
|
|
28
|
-
merge_class(
|
|
37
|
+
merge_class(
|
|
38
|
+
"relative flex gap-3 overflow-hidden rounded-surface border py-3 pr-4 pl-5 text-sm",
|
|
39
|
+
SilexUi::Tones.filled(@tone)
|
|
40
|
+
)
|
|
29
41
|
end
|
|
30
42
|
end
|
|
31
43
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Un avatar : une image quand il y en a une, les initiales sinon, posées sur
|
|
5
|
+
# le dégradé de marque — celui du rail du Silex.
|
|
6
|
+
#
|
|
7
|
+
# SilexUi::AvatarComponent.new(name: current_user.display_name)
|
|
8
|
+
# SilexUi::AvatarComponent.new(name: "Théo", src: user.avatar_url, size: :lg)
|
|
9
|
+
class AvatarComponent < BaseComponent
|
|
10
|
+
SIZES = {
|
|
11
|
+
sm: "size-8 text-xs",
|
|
12
|
+
md: "size-10 text-sm",
|
|
13
|
+
lg: "size-12 text-base"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(name: nil, src: nil, size: :md, square: false, **options)
|
|
17
|
+
@name = name.to_s
|
|
18
|
+
@src = src
|
|
19
|
+
@size = SIZES.key?(size.to_sym) ? size.to_sym : :md
|
|
20
|
+
@square = square
|
|
21
|
+
super(**options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
return image_tag(@src, alt: @name, class: css_classes, **html_attributes) if @src.present?
|
|
26
|
+
|
|
27
|
+
tag.span(initials, title: @name.presence, class: class_names(css_classes, gradient), **html_attributes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Deux lettres : la première de chaque mot quand le nom en a plusieurs, les
|
|
33
|
+
# deux premières sinon. Un avatar à une lettre se confond trop vite.
|
|
34
|
+
def initials
|
|
35
|
+
words = @name.split(/[\s._-]+/).reject(&:empty?)
|
|
36
|
+
|
|
37
|
+
case words.size
|
|
38
|
+
when 0 then "?"
|
|
39
|
+
when 1 then words.first[0, 2].upcase
|
|
40
|
+
else words.first(2).map { |word| word[0] }.join.upcase
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def gradient
|
|
45
|
+
"bg-gradient-to-br from-brand-500 to-brand-700 text-white"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def css_classes
|
|
49
|
+
merge_class(
|
|
50
|
+
"inline-flex shrink-0 items-center justify-center overflow-hidden font-semibold",
|
|
51
|
+
@square ? "rounded-surface" : "rounded-full",
|
|
52
|
+
SIZES[@size]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SilexUi
|
|
4
|
-
# Pastille de statut
|
|
5
|
-
#
|
|
4
|
+
# Pastille de statut : un libellé, une icône, un ton. La forme est celle du
|
|
5
|
+
# Silex — hauteur fixe, coins nets, fond pastel et texte soutenu.
|
|
6
6
|
#
|
|
7
|
-
# SilexUi::BadgeComponent.new(label: "expiré depuis 3 j", tone: :alert)
|
|
7
|
+
# SilexUi::BadgeComponent.new(label: "expiré depuis 3 j", tone: :alert, icon: "alert")
|
|
8
8
|
class BadgeComponent < BaseComponent
|
|
9
|
-
|
|
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)
|
|
9
|
+
def initialize(label: nil, tone: :neutral, icon: nil, href: nil, title: nil, **options)
|
|
18
10
|
@label = label
|
|
19
11
|
@tone = tone_of(tone)
|
|
12
|
+
@icon = icon
|
|
20
13
|
@href = href
|
|
21
14
|
@title = title.presence
|
|
22
15
|
super(**options)
|
|
23
16
|
end
|
|
24
17
|
|
|
25
18
|
def call
|
|
26
|
-
text = @label || content
|
|
27
19
|
attributes = html_attributes.merge(class: css_classes, title: @title)
|
|
28
20
|
|
|
29
|
-
@href.present? ? link_to(
|
|
21
|
+
@href.present? ? link_to(body, @href, **attributes) : tag.span(body, **attributes)
|
|
30
22
|
end
|
|
31
23
|
|
|
32
24
|
private
|
|
33
25
|
|
|
26
|
+
def body
|
|
27
|
+
safe_join([ icon(@icon, size: :xs), (@label || content) ].compact)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Sans libellé, la pastille se referme sur son icône : un carré, pas un
|
|
31
|
+
# rectangle à moitié vide.
|
|
32
|
+
def icon_only?
|
|
33
|
+
@label.blank? && !content? && @icon.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
34
36
|
def css_classes
|
|
35
37
|
merge_class(
|
|
36
|
-
"inline-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
"inline-flex h-7 items-center justify-center gap-1.5 rounded-badge border",
|
|
39
|
+
"text-[13px] leading-none font-semibold whitespace-nowrap",
|
|
40
|
+
icon_only? ? "w-7 px-0" : "px-3",
|
|
41
|
+
("transition hover:brightness-95" if @href.present?),
|
|
42
|
+
SilexUi::Tones.filled(@tone)
|
|
39
43
|
)
|
|
40
44
|
end
|
|
41
45
|
end
|
|
@@ -7,9 +7,10 @@ module SilexUi
|
|
|
7
7
|
# les classes de l'appelant sont ajoutées après celles du composant, donc
|
|
8
8
|
# elles gagnent à spécificité égale.
|
|
9
9
|
class BaseComponent < ViewComponent::Base
|
|
10
|
-
# Les tons sont sémantiques
|
|
11
|
-
#
|
|
12
|
-
|
|
10
|
+
# Les cinq premiers tons sont sémantiques : on écrit `tone: :alert`, pas
|
|
11
|
+
# `tone: :red`. Les quatre suivants ne disent rien d'un état — ils servent à
|
|
12
|
+
# distinguer des familles de choses, comme les pastilles du Silex.
|
|
13
|
+
TONES = %i[neutral brand ok warn alert info purple pink teal cyan].freeze
|
|
13
14
|
|
|
14
15
|
def initialize(class: nil, **html_attributes)
|
|
15
16
|
@extra_class = binding.local_variable_get(:class)
|
|
@@ -28,5 +29,11 @@ module SilexUi
|
|
|
28
29
|
def merge_class(*base)
|
|
29
30
|
class_names(*base, extra_class)
|
|
30
31
|
end
|
|
32
|
+
|
|
33
|
+
def icon(name, **options)
|
|
34
|
+
return nil if name.blank?
|
|
35
|
+
|
|
36
|
+
render SilexUi::IconComponent.new(name: name, **options)
|
|
37
|
+
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
<% if any? %>
|
|
2
|
-
<%= render SilexUi::SectionComponent.new(title: title) do %>
|
|
2
|
+
<%= render SilexUi::SectionComponent.new(title: title, icon: icon_name) do %>
|
|
3
3
|
<ul class="<%= css_classes %>">
|
|
4
4
|
<% items.each do |item| %>
|
|
5
|
-
<li
|
|
5
|
+
<li>
|
|
6
6
|
<% if item[:href].present? %>
|
|
7
|
-
<%= link_to item[:
|
|
7
|
+
<%= link_to item[:href], class: "flex items-center justify-between gap-4 rounded-badge px-3 py-2 transition hover:bg-surface-100" do %>
|
|
8
|
+
<span class="truncate text-ink-700"><%= item[:label] %></span>
|
|
9
|
+
<span class="shrink-0 font-semibold text-ink-500"><%= item[:count] %></span>
|
|
10
|
+
<% end %>
|
|
8
11
|
<% else %>
|
|
9
|
-
<
|
|
12
|
+
<div class="flex items-center justify-between gap-4 px-3 py-2">
|
|
13
|
+
<span class="truncate text-ink-700"><%= item[:label] %></span>
|
|
14
|
+
<span class="shrink-0 font-semibold text-ink-500"><%= item[:count] %></span>
|
|
15
|
+
</div>
|
|
10
16
|
<% end %>
|
|
11
|
-
|
|
12
|
-
<span class="shrink-0 text-ink-400"><%= item[:count] %></span>
|
|
13
17
|
</li>
|
|
14
18
|
<% end %>
|
|
15
19
|
</ul>
|
|
20
|
+
|
|
21
|
+
<% if hidden_count.positive? %>
|
|
22
|
+
<p class="mt-2 px-3 text-[13px] text-ink-400">
|
|
23
|
+
<% if more_href.present? %>
|
|
24
|
+
<%= link_to "+ #{hidden_count} autres", more_href, class: "font-semibold text-brand-600 transition hover:text-brand-700" %>
|
|
25
|
+
<% else %>
|
|
26
|
+
+ <%= hidden_count %> autres
|
|
27
|
+
<% end %>
|
|
28
|
+
</p>
|
|
29
|
+
<% end %>
|
|
16
30
|
<% end %>
|
|
17
31
|
<% end %>
|
|
@@ -8,20 +8,37 @@ module SilexUi
|
|
|
8
8
|
# <%= render SilexUi::BreakdownComponent.new(title: "Par registrar",
|
|
9
9
|
# items: @by_registrar.map { |name, count| { label: name.titleize, count:, href: websites_path(registrar: name) } }) %>
|
|
10
10
|
class BreakdownComponent < BaseComponent
|
|
11
|
-
def initialize(items:, title: nil, **options)
|
|
11
|
+
def initialize(items:, title: nil, icon: nil, limit: nil, more_href: nil, **options)
|
|
12
12
|
@items = items
|
|
13
13
|
@title = title
|
|
14
|
+
@icon = icon
|
|
15
|
+
@limit = limit
|
|
16
|
+
@more_href = more_href
|
|
14
17
|
super(**options)
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
attr_reader :
|
|
20
|
+
attr_reader :title, :icon_name, :more_href
|
|
21
|
+
|
|
22
|
+
def icon_name = @icon
|
|
23
|
+
|
|
24
|
+
# Une répartition qui déroule quarante lignes n'est plus une répartition.
|
|
25
|
+
# Au-delà de la limite, on annonce le reste au lieu de l'étaler.
|
|
26
|
+
def items
|
|
27
|
+
@limit ? @items.first(@limit) : @items
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def hidden_count
|
|
31
|
+
return 0 if @limit.nil?
|
|
32
|
+
|
|
33
|
+
[ @items.size - @limit, 0 ].max
|
|
34
|
+
end
|
|
18
35
|
|
|
19
36
|
def any?
|
|
20
|
-
items.any?
|
|
37
|
+
@items.any?
|
|
21
38
|
end
|
|
22
39
|
|
|
23
40
|
def css_classes
|
|
24
|
-
merge_class("
|
|
41
|
+
merge_class("-mx-3 divide-y divide-surface-200 text-sm")
|
|
25
42
|
end
|
|
26
43
|
end
|
|
27
44
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SilexUi
|
|
4
4
|
# Bouton ou lien : même costume, trois usages.
|
|
5
5
|
#
|
|
6
|
-
# SilexUi::ButtonComponent.new(label: "Ajouter", href: new_website_path)
|
|
6
|
+
# SilexUi::ButtonComponent.new(label: "Ajouter", href: new_website_path, icon: "plus")
|
|
7
7
|
# SilexUi::ButtonComponent.new(label: "Supprimer", href: website_path(w), method: :delete, variant: :danger)
|
|
8
8
|
# SilexUi::ButtonComponent.new(label: "Appliquer", type: :submit)
|
|
9
9
|
#
|
|
@@ -11,23 +11,33 @@ module SilexUi
|
|
|
11
11
|
# on rend un `<button>`.
|
|
12
12
|
class ButtonComponent < BaseComponent
|
|
13
13
|
VARIANTS = {
|
|
14
|
-
primary: "bg-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
primary: "border-transparent bg-gradient-to-br from-brand-500 to-brand-700 text-white " \
|
|
15
|
+
"shadow-card hover:shadow-card-hover hover:-translate-y-px",
|
|
16
|
+
secondary: "border-surface-200 bg-surface-0 text-ink-700 shadow-card " \
|
|
17
|
+
"hover:border-brand-500 hover:text-brand-600",
|
|
18
|
+
ghost: "border-transparent bg-transparent text-ink-500 hover:bg-surface-100 hover:text-brand-600",
|
|
19
|
+
danger: "border-alert-300 bg-alert-50 text-alert-700 hover:border-alert-500 hover:bg-alert-100"
|
|
18
20
|
}.freeze
|
|
19
21
|
|
|
20
22
|
SIZES = {
|
|
21
|
-
sm: "
|
|
22
|
-
md: "
|
|
23
|
-
lg: "px-6
|
|
23
|
+
sm: "h-8 gap-1.5 px-3 text-xs",
|
|
24
|
+
md: "h-10 gap-2 px-4 text-sm",
|
|
25
|
+
lg: "h-12 gap-2.5 px-6 text-base"
|
|
24
26
|
}.freeze
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
# Sans libellé, le bouton se referme sur son icône : un carré, pas un
|
|
29
|
+
# rectangle à moitié vide.
|
|
30
|
+
SQUARE_SIZES = { sm: "size-8", md: "size-11", lg: "size-12" }.freeze
|
|
31
|
+
|
|
32
|
+
ICON_SIZES = { sm: :sm, md: :sm, lg: :md }.freeze
|
|
33
|
+
|
|
34
|
+
def initialize(label: nil, href: nil, variant: :primary, size: :md, icon: nil, method: nil,
|
|
35
|
+
type: :button, **options)
|
|
27
36
|
@label = label
|
|
28
37
|
@href = href
|
|
29
38
|
@variant = VARIANTS.key?(variant.to_sym) ? variant.to_sym : :primary
|
|
30
39
|
@size = SIZES.key?(size.to_sym) ? size.to_sym : :md
|
|
40
|
+
@icon = icon
|
|
31
41
|
@method = method
|
|
32
42
|
@type = type
|
|
33
43
|
super(**options)
|
|
@@ -35,28 +45,33 @@ module SilexUi
|
|
|
35
45
|
|
|
36
46
|
def call
|
|
37
47
|
if @method.present? && @href.present?
|
|
38
|
-
button_to(
|
|
48
|
+
button_to(body, @href, **attributes.merge(method: @method))
|
|
39
49
|
elsif @href.present?
|
|
40
|
-
link_to(
|
|
50
|
+
link_to(body, @href, **attributes)
|
|
41
51
|
else
|
|
42
|
-
button_tag(
|
|
52
|
+
button_tag(body, **attributes.merge(type: @type))
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
private
|
|
47
57
|
|
|
48
|
-
def
|
|
49
|
-
@label || content
|
|
58
|
+
def body
|
|
59
|
+
safe_join([ icon(@icon, size: square? ? :md : ICON_SIZES[@size]), (@label || content) ].compact)
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
def attributes
|
|
53
63
|
html_attributes.merge(class: css_classes)
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def square?
|
|
67
|
+
@label.blank? && !content? && @icon.present?
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def css_classes
|
|
57
71
|
merge_class(
|
|
58
|
-
"inline-
|
|
59
|
-
|
|
72
|
+
"inline-flex cursor-pointer items-center justify-center rounded-surface border font-semibold",
|
|
73
|
+
"transition duration-200 disabled:cursor-default disabled:opacity-50",
|
|
74
|
+
square? ? SQUARE_SIZES[@size] : SIZES[@size],
|
|
60
75
|
VARIANTS[@variant]
|
|
61
76
|
)
|
|
62
77
|
end
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
<% body = capture do %>
|
|
2
2
|
<% if title.present? || header? %>
|
|
3
|
-
<div class="mb-3 flex items-
|
|
3
|
+
<div class="mb-3 flex flex-wrap items-center justify-between gap-4">
|
|
4
4
|
<% if title.present? %>
|
|
5
|
-
<h3 class="text-
|
|
5
|
+
<h3 class="flex items-center gap-2 text-[13px] font-bold tracking-wider text-ink-700 uppercase">
|
|
6
|
+
<%= render SilexUi::IconComponent.new(name: icon_name, size: :sm, class: "text-brand-500") if icon_name.present? %>
|
|
7
|
+
<%= title %>
|
|
8
|
+
</h3>
|
|
6
9
|
<% end %>
|
|
7
10
|
<%= header %>
|
|
8
11
|
</div>
|
|
9
12
|
<% end %>
|
|
10
13
|
|
|
11
|
-
<div class="text-sm
|
|
14
|
+
<div class="text-sm text-ink-700"><%= content %></div>
|
|
12
15
|
|
|
13
16
|
<% if footer? %>
|
|
14
|
-
<div class="mt-4 border-t border-
|
|
17
|
+
<div class="mt-4 border-t border-surface-200 pt-3 text-xs text-ink-500"><%= footer %></div>
|
|
15
18
|
<% end %>
|
|
16
19
|
<% end %>
|
|
17
20
|
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SilexUi
|
|
4
|
-
# Surface surélevée :
|
|
5
|
-
# ce qui doit se détacher du
|
|
4
|
+
# Surface surélevée : la carte blanche du Silex, coins arrondis et ombre
|
|
5
|
+
# posée. Sert de conteneur à tout ce qui doit se détacher du fond.
|
|
6
6
|
class CardComponent < BaseComponent
|
|
7
7
|
renders_one :header
|
|
8
8
|
renders_one :footer
|
|
9
9
|
|
|
10
|
-
def initialize(title: nil, href: nil, **options)
|
|
10
|
+
def initialize(title: nil, icon: nil, href: nil, **options)
|
|
11
11
|
@title = title
|
|
12
|
+
@icon = icon
|
|
12
13
|
@href = href
|
|
13
14
|
super(**options)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
attr_reader :title, :href
|
|
17
|
+
attr_reader :title, :href, :icon_name
|
|
18
|
+
|
|
19
|
+
def icon_name = @icon
|
|
17
20
|
|
|
18
21
|
def css_classes
|
|
19
22
|
merge_class(
|
|
20
|
-
"block rounded-
|
|
21
|
-
("
|
|
23
|
+
"block rounded-surface border border-surface-200 bg-surface-0 p-5 shadow-card transition duration-300",
|
|
24
|
+
("hover:-translate-y-0.5 hover:border-brand-300 hover:shadow-card-hover" if href.present?)
|
|
22
25
|
)
|
|
23
26
|
end
|
|
24
27
|
end
|
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SilexUi
|
|
4
|
-
# Copier une valeur sans la sélectionner à la souris. Visuellement
|
|
5
|
-
# comme le CopyButtonComponent du Silex,
|
|
4
|
+
# Copier une valeur sans la sélectionner à la souris. Visuellement une
|
|
5
|
+
# pastille, comme le CopyButtonComponent du Silex, branchée sur le contrôleur
|
|
6
6
|
# Stimulus fourni par le gem.
|
|
7
7
|
#
|
|
8
8
|
# <%= render SilexUi::CopyButtonComponent.new(value: website.domain) %>
|
|
9
9
|
class CopyButtonComponent < BaseComponent
|
|
10
|
-
def initialize(value:, label: "Copier", copied_label: "Copié", tone: :neutral, **options)
|
|
10
|
+
def initialize(value:, label: nil, title: "Copier", copied_label: "Copié", tone: :neutral, **options)
|
|
11
11
|
@value = value.to_s
|
|
12
12
|
@label = label
|
|
13
|
+
@title = title
|
|
13
14
|
@copied_label = copied_label
|
|
14
15
|
@tone = tone_of(tone)
|
|
15
16
|
super(**options)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def call
|
|
19
|
-
button_tag(
|
|
20
|
+
button_tag(body, type: "button", title: @title, class: css_classes, **stimulus_attributes)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
private
|
|
23
24
|
|
|
25
|
+
def body
|
|
26
|
+
safe_join([
|
|
27
|
+
icon("copy", size: :xs, class: "silex-ui-copy-icon"),
|
|
28
|
+
(tag.span(@label, data: { silex_ui__clipboard_target: "label" }) if @label.present?)
|
|
29
|
+
].compact)
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
def css_classes
|
|
25
33
|
merge_class(
|
|
26
|
-
"inline-
|
|
27
|
-
"
|
|
28
|
-
|
|
34
|
+
"inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-badge border",
|
|
35
|
+
"text-[13px] leading-none font-semibold whitespace-nowrap transition hover:brightness-95",
|
|
36
|
+
@label.present? ? "px-3" : "w-7 px-0",
|
|
37
|
+
SilexUi::Tones.filled(@tone)
|
|
29
38
|
)
|
|
30
39
|
end
|
|
31
40
|
|
|
@@ -35,7 +44,7 @@ module SilexUi
|
|
|
35
44
|
controller: "silex-ui--clipboard",
|
|
36
45
|
action: "silex-ui--clipboard#copy",
|
|
37
46
|
silex_ui__clipboard_value_value: @value,
|
|
38
|
-
silex_ui__clipboard_label_value: @label,
|
|
47
|
+
silex_ui__clipboard_label_value: @label.to_s,
|
|
39
48
|
silex_ui__clipboard_copied_label_value: @copied_label
|
|
40
49
|
}
|
|
41
50
|
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="<%= css_classes %>" data-controller="silex-ui--dropdown"
|
|
2
|
+
data-action="click@window->silex-ui--dropdown#closeOnOutsideClick keydown.esc@window->silex-ui--dropdown#close">
|
|
3
|
+
<div data-action="click->silex-ui--dropdown#toggle"><%= trigger %></div>
|
|
4
|
+
|
|
5
|
+
<div class="<%= menu_classes %>" data-silex-ui--dropdown-target="menu" role="menu">
|
|
6
|
+
<% if header? %>
|
|
7
|
+
<div class="border-b border-surface-200 px-3 py-2.5"><%= header %></div>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%= safe_join(items) %>
|
|
11
|
+
<%= content %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilexUi
|
|
4
|
+
# Un menu qui s'ouvre au clic et se ferme dès qu'on regarde ailleurs.
|
|
5
|
+
#
|
|
6
|
+
# <%= render SilexUi::DropdownComponent.new(placement: :up) do |menu| %>
|
|
7
|
+
# <% menu.with_trigger do %><%= render SilexUi::AvatarComponent.new(name: "Théo") %><% end %>
|
|
8
|
+
# <% menu.with_header do %>…<% end %>
|
|
9
|
+
# <% menu.with_item(label: "Profil", href: profile_path, icon: "users") %>
|
|
10
|
+
# <% menu.with_item(label: "Déconnexion", href: logout_path, method: :delete, icon: "close", tone: :alert) %>
|
|
11
|
+
# <% end %>
|
|
12
|
+
class DropdownComponent < BaseComponent
|
|
13
|
+
renders_one :trigger
|
|
14
|
+
renders_one :header
|
|
15
|
+
renders_many :items, "ItemComponent"
|
|
16
|
+
|
|
17
|
+
PLACEMENTS = {
|
|
18
|
+
down: "top-full left-0 mt-2",
|
|
19
|
+
down_end: "top-full right-0 mt-2",
|
|
20
|
+
up: "bottom-full left-0 mb-2",
|
|
21
|
+
up_end: "bottom-full right-0 mb-2",
|
|
22
|
+
right: "bottom-0 left-full ml-2"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def initialize(placement: :down, width: "w-70", **options)
|
|
26
|
+
@placement = PLACEMENTS.key?(placement.to_sym) ? placement.to_sym : :down
|
|
27
|
+
@width = width
|
|
28
|
+
super(**options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def menu_classes
|
|
32
|
+
class_names(
|
|
33
|
+
"absolute z-50 hidden overflow-hidden rounded-surface border border-surface-200",
|
|
34
|
+
"bg-surface-0 py-1 shadow-card-hover", @width, PLACEMENTS[@placement]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def css_classes
|
|
39
|
+
merge_class("relative")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Une entrée : une icône, un libellé, et un ton pour celles qui coupent.
|
|
43
|
+
class ItemComponent < ViewComponent::Base
|
|
44
|
+
def initialize(label:, href: nil, icon: nil, method: nil, tone: :neutral)
|
|
45
|
+
@label = label
|
|
46
|
+
@href = href
|
|
47
|
+
@icon = icon
|
|
48
|
+
@method = method
|
|
49
|
+
@tone = tone
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def call
|
|
53
|
+
body = safe_join([ icon_tag, tag.span(@label) ].compact)
|
|
54
|
+
|
|
55
|
+
if @method.present?
|
|
56
|
+
# Forme à bloc : `button_to` avec une chaîne rend un `<input>` dont la
|
|
57
|
+
# valeur est échappée, et l'icône y finirait en texte.
|
|
58
|
+
button_to(@href, method: @method, class: css_classes, form_class: "block w-full") { body }
|
|
59
|
+
elsif @href.present?
|
|
60
|
+
link_to(body, @href, class: css_classes)
|
|
61
|
+
else
|
|
62
|
+
tag.span(body, class: css_classes)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def icon_tag
|
|
69
|
+
return nil if @icon.blank?
|
|
70
|
+
|
|
71
|
+
render SilexUi::IconComponent.new(name: @icon, size: :sm)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def css_classes
|
|
75
|
+
class_names(
|
|
76
|
+
"flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left text-sm transition",
|
|
77
|
+
@tone.to_sym == :alert ? "text-alert-700 hover:bg-alert-50" : "text-ink-700 hover:bg-surface-100"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="<%= merge_class("flex flex-col items-center justify-center gap-3 px-6 py-16 text-center") %>">
|
|
2
|
+
<% if icon_name.present? %>
|
|
3
|
+
<span class="flex size-14 items-center justify-center rounded-full bg-surface-100 text-ink-400">
|
|
4
|
+
<%= render SilexUi::IconComponent.new(name: icon_name, size: :xl) %>
|
|
5
|
+
</span>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<p class="text-sm text-ink-500"><%= message %></p>
|
|
9
|
+
|
|
10
|
+
<% if content? %>
|
|
11
|
+
<div class="mt-1"><%= content %></div>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SilexUi
|
|
4
|
-
# Ce qu'on voit quand il n'y a rien à voir
|
|
5
|
-
# action qui remplit l'écran.
|
|
4
|
+
# Ce qu'on voit quand il n'y a rien à voir : une icône pâle, un message, et si
|
|
5
|
+
# possible la seule action qui remplit l'écran.
|
|
6
6
|
class EmptyStateComponent < BaseComponent
|
|
7
|
-
def initialize(message:, **options)
|
|
7
|
+
def initialize(message:, icon: "folder", **options)
|
|
8
8
|
@message = message
|
|
9
|
+
@icon = icon
|
|
9
10
|
super(**options)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
attr_reader :message
|
|
13
|
+
attr_reader :message, :icon_name
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
-
tag.div(class: merge_class("py-20 text-center text-sm font-light text-ink-400")) do
|
|
16
|
-
safe_join([ tag.p(message), (tag.div(content, class: "mt-4") if content?) ].compact)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
15
|
+
def icon_name = @icon
|
|
19
16
|
end
|
|
20
17
|
end
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<div class="<%= css_classes %>">
|
|
2
2
|
<% if boolean? %>
|
|
3
3
|
<%= input %>
|
|
4
|
-
<%= form.label attribute, label_text, class: "text-sm
|
|
4
|
+
<%= form.label attribute, label_text, class: "cursor-pointer text-sm text-ink-700" %>
|
|
5
5
|
<% else %>
|
|
6
6
|
<%= form.label attribute, label_text,
|
|
7
|
-
class: "mb-
|
|
7
|
+
class: "mb-1.5 block text-[13px] font-semibold text-ink-700" %>
|
|
8
8
|
<%= input %>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
11
|
<% if hint.present? %>
|
|
12
|
-
<p class="mt-1 text-xs
|
|
12
|
+
<p class="mt-1 text-xs text-ink-400"><%= hint %></p>
|
|
13
13
|
<% end %>
|
|
14
14
|
|
|
15
15
|
<% if errors? %>
|
|
16
|
-
<p class="mt-1 text-xs font-
|
|
16
|
+
<p class="mt-1 text-xs font-semibold text-alert-700"><%= errors.to_sentence %></p>
|
|
17
17
|
<% end %>
|
|
18
18
|
</div>
|