Skip to content

Commit c9ee53e

Browse files
UI 4 - States (#4324)
Co-authored-by: Adrian Marin <adrian@adrianthedev.com>
1 parent ca4f02f commit c9ee53e

12 files changed

Lines changed: 343 additions & 18 deletions

File tree

app/assets/stylesheets/application.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
@import "./css/components/ui/description_list.css";
136136
@import "./css/components/ui/tabs.css";
137137
@import "./css/components/ui/badge.css";
138+
@import "./css/components/ui/state.css";
138139
@import "./css/components/ui/file_upload_input.css";
139140
@import "./css/components/ui/file_upload_item.css";
140141
@import "./css/components/ui/dropdown.css";
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* State Component - BEM: .state (block), .state__* (elements), .state--* (modifiers) */
2+
3+
/* Empty State Component */
4+
5+
.state {
6+
@apply mx-auto flex w-full max-w-72 flex-col items-center justify-center gap-3 rounded-lg bg-background p-6 text-center;
7+
}
8+
9+
.state__illustration {
10+
@apply relative h-36 w-60 flex items-center justify-center;
11+
}
12+
13+
.state__card {
14+
@apply absolute flex h-11 w-44 items-center gap-1.5 rounded-lg border border-secondary p-2.5;
15+
16+
background: linear-gradient(194deg, var(--color-primary) 27%, var(--color-background) 65%);
17+
box-shadow: var(--box-shadow-card);
18+
}
19+
20+
.state__card--top {
21+
@apply start-0 top-0;
22+
}
23+
24+
.state__card--middle {
25+
@apply start-14 top-8;
26+
}
27+
28+
.state__card--bottom {
29+
@apply start-5 top-20;
30+
}
31+
32+
.state__badge {
33+
@apply flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium leading-none text-content;
34+
35+
background: color-mix(in oklab, var(--color-tertiary) 88%, var(--color-primary));
36+
}
37+
38+
.state__body {
39+
@apply flex min-w-0 flex-1 flex-col gap-1;
40+
}
41+
42+
.state__line {
43+
@apply block h-1.5 rounded-full;
44+
45+
background: color-mix(in oklab, var(--color-tertiary) 78%, var(--color-primary));
46+
}
47+
48+
.state__line--title {
49+
@apply opacity-75;
50+
}
51+
52+
.state__line--large {
53+
@apply w-14;
54+
}
55+
56+
.state__line--medium {
57+
@apply w-12;
58+
}
59+
60+
.state__line--small {
61+
@apply w-11;
62+
}
63+
64+
.state__line--content {
65+
@apply w-full;
66+
}
67+
68+
.state__message {
69+
@apply text-base font-normal leading-6 text-content-secondary;
70+
}
71+
72+
/* Frame Load Failed Component */
73+
.state--frame-load-failed {
74+
@apply max-w-96 gap-1;
75+
}
76+
77+
.state__document {
78+
@apply absolute flex h-28 w-20 flex-col rounded-lg border border-secondary p-0.5;
79+
80+
background: linear-gradient(235deg, var(--color-primary) 27%, var(--color-background) 65%);
81+
}
82+
83+
.state__document--start {
84+
@apply start-4 rotate-[-21deg];
85+
}
86+
87+
.state__document--center {
88+
@apply start-16 z-10;
89+
}
90+
91+
.state__document--end {
92+
@apply start-24 rotate-[21deg] z-0;
93+
}
94+
95+
.state__document-body {
96+
@apply flex h-full w-full items-center rounded-lg p-2.5;
97+
98+
box-shadow: var(--box-shadow-card);
99+
}
100+
101+
.state__document-lines {
102+
@apply flex h-full w-full flex-col justify-between
103+
}
104+
105+
.state__document-line {
106+
@apply block h-1 w-full rounded-full;
107+
108+
background: color-mix(in oklab, var(--color-tertiary) 78%, var(--color-primary));
109+
}
110+
111+
.state__document-line--short {
112+
@apply w-10;
113+
}
114+
115+
.state__magnifier {
116+
@apply absolute start-[30%] top-[30%] z-20 bg-none;
117+
}
118+
119+
.state__magnifier-icon {
120+
@apply size-24 text-content-secondary opacity-20;
121+
}
122+
123+
.state__note {
124+
@apply text-center text-sm font-normal leading-5 text-content;
125+
}
126+
127+
.state__link {
128+
color: color-mix(in oklab, var(--color-info-content), var(--color-content) 10%);
129+
}
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
<div class="relative flex-1 flex flex-col items-center justify-center space-y-2 py-6 border border-dashed border-content-secondary rounded-lg">
2-
<div class="text-content-secondary text-center">
3-
<%= text %>
1+
<%= content_tag :div, class: class_names("state", @classes) do %>
2+
<div class="state__illustration" aria-hidden="true">
3+
<% cards.each do |card| %>
4+
<div class="<%= class_names("state__card", "state__card--#{card[:position]}") %>">
5+
<div class="state__badge"><%= card[:number] %></div>
6+
7+
<div class="state__body">
8+
<div class="<%= class_names("state__line", "state__line--title", "state__line--#{card[:title]}") %>"></div>
9+
<div class="state__line state__line--content"></div>
10+
</div>
11+
</div>
12+
<% end %>
413
</div>
5-
</div>
14+
15+
<p class="state__message"><%= text %></p>
16+
<% end %>

app/components/avo/empty_state_component.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@
33
class Avo::EmptyStateComponent < Avo::BaseComponent
44
prop :message
55
prop :by_association, default: false
6+
prop :classes
67

78
def text
89
@message || locale_message
910
end
1011

12+
def cards
13+
[
14+
{number: 1, position: :top, title: :large},
15+
{number: 2, position: :middle, title: :large},
16+
{number: 3, position: :bottom, title: :large}
17+
]
18+
end
19+
1120
private
1221

1322
def locale_message
Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,45 @@
1-
<%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2-
<%
3-
classes = 'absolute inset-auto start-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2'
4-
label = t 'avo.failed_to_load'
5-
src_url = params[:src].present? && !params[:src].starts_with?('http://') ? CGI.escapeHTML(params[:src]) : nil
1+
<%
2+
src_url = params[:src].present? && !params[:src].starts_with?("http://") ? CGI.escapeHTML(params[:src]) : nil
3+
frame_label = if params[:turbo_frame].present?
4+
params[:turbo_frame].to_s.humanize.downcase
5+
else
6+
"this frame"
7+
end
68
%>
7-
<div class="relative flex-1 py-4">
8-
<div class="relative block text-gray-300 h-64 w-full">
9-
<%= svg "avo/failed_to_load", class: "#{classes} h-52 text-gray-400" %>
10-
</div>
11-
<div class="relative block text-center text-lg text-gray-400 font-semibold pb-6"><%= label %> <span class="border-b-2 border-gray-200 border-dashed"><%= params[:turbo_frame].to_s.humanize.downcase if params[:turbo_frame].present? %></span> frame</div>
12-
<% if Rails.env.development? && src_url %>
13-
<div class="text-center text-sm w-full pb-3">
14-
This is not an issue with Avo. Use <%= link_to 'this page', src_url, target: :_blank %> to see why this frame failed to load.
9+
10+
<%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
11+
<div class="state state--frame-load-failed">
12+
<div class="state__illustration" aria-hidden="true">
13+
<% %i[start center end].each do |position| %>
14+
<div class="state__document state__document--<%= position %>">
15+
<div class="state__document-body">
16+
<div class="state__document-lines">
17+
<span class="state__document-line"></span>
18+
<span class="state__document-line"></span>
19+
<span class="state__document-line"></span>
20+
<span class="state__document-line"></span>
21+
<span class="state__document-line state__document-line--short"></span>
22+
</div>
23+
</div>
24+
</div>
25+
<% end %>
26+
27+
<div class="state__magnifier">
28+
<%= svg "tabler/outline/zoom", class: "state__magnifier-icon" %>
1529
</div>
30+
</div>
31+
32+
<p class="state__message text-center font-normal">
33+
Failed to load:
34+
<span class="font-bold"><%= frame_label %></span>
35+
</p>
36+
37+
<% if Rails.env.development? && src_url.present? %>
38+
<p class="state__note text-center font-normal">
39+
Follow
40+
<%= link_to "this link", src_url, target: "_blank", rel: "noopener", class: "state__link font-normal" %>
41+
for more details about the issue and how to fix it.
42+
</p>
1643
<% end %>
1744
</div>
1845
<% end %>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require "rails_helper"
2+
3+
RSpec.describe EmptyStateComponentPreview, type: :component do
4+
it "renders the default preview" do
5+
render_preview(:default, from: described_class, params: {
6+
message: "No record found"
7+
})
8+
9+
expect(page).to have_css(".state")
10+
expect(page).to have_text("No record found")
11+
expect(page).to have_css(".state__illustration")
12+
end
13+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require "rails_helper"
2+
3+
RSpec.describe FrameLoadFailedComponentPreview, type: :component do
4+
it "renders the default preview" do
5+
render_preview(:default, from: described_class, params: {
6+
frame: "filter",
7+
src: "/admin/resources/comments"
8+
})
9+
10+
expect(page).to have_css(".state--frame-load-failed")
11+
expect(page).to have_text("Failed to load:")
12+
expect(page).to have_text("filter")
13+
# Dev note with link is only shown when Rails.env.development? — test env skips it
14+
end
15+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class EmptyStateComponentPreview < ViewComponent::Preview
2+
# @param message text "No record found"
3+
def default(message: "No record found")
4+
render_with_template(
5+
template: "empty_state_component_preview/default",
6+
locals: {message: message}
7+
)
8+
end
9+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div style="padding: 2rem;">
2+
<div>
3+
<div class="mb-2 font-semibold">Interactive</div>
4+
<%= render Avo::EmptyStateComponent.new(message: message) %>
5+
</div>
6+
</div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class FrameLoadFailedComponentPreview < ViewComponent::Preview
2+
# @param frame text "filter"
3+
# @param src text "/admin/resources/comments"
4+
def default(frame: "filter", src: "/admin/resources/comments")
5+
render_with_template(
6+
template: "frame_load_failed_component_preview/default",
7+
locals: {frame: frame, src: src}
8+
)
9+
end
10+
end

0 commit comments

Comments
 (0)