Merge pull request #493 from basecamp/activity-dom

Activity dom
This commit is contained in:
Andy Smith
2025-05-14 13:10:46 -05:00
committed by GitHub
15 changed files with 191 additions and 116 deletions
+8
View File
@@ -33,6 +33,14 @@
/* Components */
--btn-size: 2.65em;
/* Z-index */
--z-events-column-header: 1;
--z-events-day-header: 2;
--z-popup: 10;
--z-terminal: 20;
--z-tray: 21;
/* OKLCH colors: Fixed */
--lch-black: 0% 0 0;
--lch-white: 100% 0 0;
+1 -1
View File
@@ -10,7 +10,7 @@
max-inline-size: var(--panel-size) !important;
position: absolute;
translate: 0 -50%;
z-index: 2;
z-index: var(--z-popup);
&[open] {
display: flex;
+108 -62
View File
@@ -1,43 +1,113 @@
@layer components {
/* Event column layout
/* ------------------------------------------------------------------------ */
.events {
background-color: var(--color-ink-lighter);
border-block-start: var(--grid-lines) solid var(--color-ink-lighter);
border-inline-start: var(--grid-lines) solid var(--color-ink-lighter);
display: grid;
gap: 0 var(--grid-lines);
grid-template-columns: repeat(3, 1fr);
position: relative;
z-index: 0;
--events-gap: 1ch;
--events-border: 1px solid var(--color-ink-lighter);
}
.events__container {
--column-gap: clamp(var(--inline-space), 3cqmin, calc(var(--inline-space) * 3));
--grid-lines: 1px;
.events__day {
position: relative;
margin: auto;
max-inline-size: min(105ch, 100%);
@media (max-width: 639px) {
margin-block-end: calc(var(--events-gap) * 2);
}
}
.events__day-header,
.events__column-header {
font-size: var(--text-small);
text-align: center;
text-transform: uppercase;
}
.events__day-header {
background-color: var(--color-canvas);
block-size: var(--grid-lines);
display: block;
position: relative;
z-index: 1;
}
.events__day-header-content {
background-color: var(--color-ink-lighter);
border-radius: 0.2em;
display: inline-flex;
font-size: 0.8em;
inset: calc(var(--grid-lines) - 1em) auto -1em;
display: block;
inline-size: fit-content;
line-height: 1.5;
padding: 0.2em 1.3em;
margin-inline: auto;
padding: 0.5ch 1.3em;
position: absolute;
text-transform: uppercase;
transform: translateX(calc(-50% - var(--grid-lines) / 2));
z-index: 2;
inset: 0 auto auto 50%;
translate: -50% -50%;
z-index: var(--z-events-day-header);
}
.events__columns {
border-block-start: var(--events-border);
position: relative;
@media (min-width: 640px) {
align-items: end;
border-inline: var(--events-border);
display: grid;
grid-template-columns: repeat(3, 1fr);
/* Pseudo column borders since .events__column is display: contents */
&:before,
&:after {
border-inline-start: var(--events-border);
content: "";
inset-block: 0;
position: absolute;
z-index: var(--z-events-day-header);
}
&:before { inset-inline-start: calc(100% / 3); }
&:after { inset-inline-end: calc(100% / 3); }
}
}
.events__column {
@media (max-width: 639px) {
&:not(:has(.event)) {
display: none;
}
}
@media (min-width: 640px) {
display: contents;
}
}
.events__column-header {
background-color: var(--color-canvas);
grid-row-start: 2;
inset-block-start: calc(var(--events-gap) * -2);
margin-block-end: var(--events-gap);
padding-block: calc(var(--events-gap) * 3) var(--events-gap);
position: sticky;
z-index: var(--z-events-column-header);
}
.events__time-block {
align-content: end;
display: grid;
gap: var(--events-gap);
justify-items: center;
margin: 0;
padding: 0;
@media (min-width: 640px) {
padding-block-end: calc(var(--events-gap) * 3);
padding-inline: calc(var(--events-gap) * 2);
}
.event {
grid-column-start: unset !important;
grid-row-start: unset !important;
}
}
.events__none {
background-color: var(--color-canvas);
border-block-start: var(--events-border);
grid-column: 1 / -1;
padding-block: 3em;
text-align: center;
}
.events__filter-btn {
@@ -46,27 +116,13 @@
margin: auto;
}
.events__index {
--row-gap: var(--block-space);
}
.events__index-header {
h2 {
font-weight: 900;
}
}
.events--none {
background-color: var(--color-canvas);
border-block-start: var(--grid-lines) solid var(--color-ink-lightest);
padding-block: 3em;
margin: auto;
}
.events__popup {
margin-block-start: var(--block-space-double);
}
/* Event
/* ------------------------------------------------------------------------ */
.event {
--column-gap: 0.7ch;
--panel-border-color: transparent;
@@ -76,7 +132,11 @@
margin: var(--inline-space);
position: relative;
z-index: 1;
/* z-index: 1; */
@media (max-width: 639px) {
--panel-size: 100%;
}
}
.event--related {
@@ -84,7 +144,7 @@
--hover-size: 0.15rem;
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
z-index: 2;
/* z-index: 2; */
}
.event__collection {
@@ -119,25 +179,11 @@
.event__title {
--lines: 4;
font-size: 1.1em;
line-height: 1.2;
}
.event__wrapper {
align-content: end;
display: grid;
gap: var(--block-space-half);
justify-items: center;
margin: var(--grid-lines) var(--grid-lines) calc(var(--grid-lines) * 6) var(--grid-lines);
padding: var(--block-space) var(--inline-space-double);
.event {
grid-column-start: unset !important;
grid-row-start: unset !important;
}
}
.event__icon {
aspect-ratio: 1;
background-color: var(--card-color);
+4 -4
View File
@@ -10,14 +10,14 @@
max-inline-size: min(50ch, 90vw);
position: absolute;
transform: translateX(-50%);
z-index: 2;
z-index: var(--z-popup);
&[open] {
display: flex;
}
#header:has(&) {
max-inline-size: 100%;
max-inline-size: 100vw;
position: relative;
z-index: 5;
@@ -41,7 +41,7 @@
.btn {
--btn-background: transparent;
}
@media (any-hover: hover) {
&:hover {
background: var(--color-selected);
@@ -110,7 +110,7 @@
.icon {
margin-inline-end: 0.5em;
}
padding-inline-start: var(--inline-space-half);
}
+1 -1
View File
@@ -9,7 +9,7 @@
inset: auto 0 0 0;
padding: var(--block-space) calc(var(--tray-size) + var(--inline-space) * 3);
position: fixed;
z-index: 1;
z-index: var(--z-terminal);
@media (prefers-color-scheme: dark) {
background-color: var(--color-black);
+1 -1
View File
@@ -15,7 +15,7 @@
justify-content: end;
padding: var(--block-space-half) var(--inline-space) calc(var(--block-space-half) / 2);
position: fixed;
z-index: 2;
z-index: var(--z-tray);
.tray__item {
--offset: calc((var(--position) - 1) * (var(--block-space) * -1));
+2
View File
@@ -26,6 +26,8 @@
.txt-capitalize { text-transform: capitalize; }
.txt-link { color: var(--color-link); text-decoration: underline; }
.font-black { font-weight: 900; }
/* Flexbox and Grid */
.justify-end { justify-content: end; }
.justify-start { justify-content: start; }
+26 -22
View File
@@ -1,4 +1,29 @@
module EventsHelper
def event_columns(event_type, day_timeline)
case event_type
when "added"
events = day_timeline.events.where(action: "card_published")
{
title: events.count > 0 ? "Added (#{events.count})" : "Added",
index: 1,
events: events
}
when "closed"
events = day_timeline.events.where(action: "card_closed")
{
title: events.count > 0 ? "Closed (#{events.count})" : "Closed",
index: 3,
events: events
}
else
{
title: "Updated",
index: 2,
events: day_timeline.events.where.not(action: [ "card_published", "card_closed" ])
}
end
end
def event_column(event)
case event.action
when "card_closed"
@@ -12,7 +37,7 @@ module EventsHelper
def event_cluster_tag(hour, col, &)
row = 25 - hour
tag.div class: "event__wrapper", style: "grid-area: #{row}/#{col}", &
tag.ul class: "events__time-block", style: "grid-area: #{row}/#{col}", &
end
def event_next_page_link(next_day)
@@ -22,27 +47,6 @@ module EventsHelper
end
end
def render_event_grid_cells(columns: 4, rows: 24)
safe_join((2..rows + 1).map do |row|
(1..columns).map do |col|
tag.div class: class_names("event__grid-item"), style: "grid-area: #{row}/#{col};"
end
end.flatten)
end
def render_column_headers(day_timeline)
headers = {
"Added" => day_timeline.events.where(action: "card_published").count,
"Updated" => nil,
"Closed" => day_timeline.events.where(action: "card_closed").count
}
headers.map do |header, count|
title = count&.positive? ? "#{header} (#{count})" : header
content_tag(:h3, title, class: "event__grid-column-title position-sticky")
end.join.html_safe
end
def event_action_sentence(event)
if event.action.comment_created?
comment_event_action_sentence(event)
+8 -10
View File
@@ -1,20 +1,18 @@
<div class="events--day" data-controller="related-element"
<section class="events__day" data-controller="related-element"
data-related-element-highlight-class="event--related">
<% if day_timeline.has_activity? %>
<h2 class="txt-medium events__day-header center">
<span class="events__day-header-content">
<%= local_datetime_tag day_timeline.day, style: :agoorweekday, class: "txt-ink" %>
</span>
<h2 class="events__day-header">
<%= local_datetime_tag day_timeline.day, style: :agoorweekday, class: "txt-ink" %>
</h2>
<div class="center events">
<%= render_column_headers(day_timeline) %>
<%= render_event_grid_cells %>
<%= render "events/day_timeline", day_timeline: day_timeline %>
<div class="events__columns">
<%= render "events/day_timeline_column", event_type: "added", day_timeline: day_timeline %>
<%= render "events/day_timeline_column", event_type: "updated", day_timeline: day_timeline %>
<%= render "events/day_timeline_column", event_type: "closed", day_timeline: day_timeline %>
</div>
<%= render "events/empty_days", day_timeline: day_timeline %>
<% else %>
<%= render "events/empty_days", day_timeline: day_timeline %>
<% end %>
</div>
</section>
-1
View File
@@ -2,7 +2,6 @@
<% events_grouped_by_hour.group_by { event_column(it) }.each do |column, events| %>
<%= event_cluster_tag(hour, column) do %>
<%= render partial: "events/event", collection: events %>
<%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %>
<% end %>
<% end %>
@@ -0,0 +1,11 @@
<% column = event_columns(event_type, day_timeline) %>
<div class="events__column">
<h3 class="events__column-header"><%= column[:title] %></h3>
<% column[:events].group_by { it.created_at.hour }.each do |hour, events| %>
<%= event_cluster_tag(hour, column[:index]) do %>
<%= render partial: "events/event", collection: events %>
<%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %>
<% end %>
<% end %>
</div>
@@ -0,0 +1,11 @@
<% day_timeline.events.group_by { event_column(it) }.each do |column, events_grouped_by_column| %>
<div class="events__column">
<h2 class="events__column-header"><%= column %></h2>
<% events_grouped_by_column.group_by { it.created_at.hour }.each do |hour, events| %>
<%= event_cluster_tag(hour, column) do %>
<%= render partial: "events/event", collection: events %>
<%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %>
<% end %>
<% end %>
</div>
<% end %>
+3 -3
View File
@@ -2,18 +2,18 @@
<% if earliest.present? %>
<% if earliest == latest %>
<div class="events--none txt-align-center">
<div class="events__none">
<h2 class="txt-medium margin-none"><%= local_datetime_tag earliest, style: :agoorweekday, class: "txt-ink txt-capitalize" %></h2>
<p class="margin-none">No activity</p>
</div>
<% elsif earliest < latest %>
<div class="events--none txt-align-center">
<div class="events__none">
<h2 class="txt-medium margin-none"><%= local_datetime_tag earliest, style: :agoorweekday, class: "txt-ink txt-capitalize" %> <%= local_datetime_tag latest, style: :agoorweekday, class: "txt-ink txt-capitalize" %></h2>
<p class="margin-none">No activity for <%= (latest - earliest).seconds.in_days.round + 1 %> days</p>
</div>
<% end %>
<% else %>
<div class="events--none txt-align-center">
<div class="events__none">
<p class="margin-none">No more activity.</p>
</div>
<% end %>
+5 -9
View File
@@ -4,8 +4,8 @@
<nav class="align-start">
<div class="btn btn--circle btn--placeholder flex-item-justify-start"></div>
<div class="events__index-header flex-inline flex-column center gap-half margin-block-end-double txt-align-center">
<h2 class="txt-large txt-nowrap center"><%= Account.sole.name %> / Latest activity</h2>
<div class="flex-inline flex-column center gap-half margin-block-end-double txt-align-center min-width">
<h2 class="font-black txt-large center overflow-ellipsis max-width"><%= Account.sole.name %> / Latest activity</h2>
<%= render "events/filter", filter: @filter %>
</div>
@@ -16,11 +16,7 @@
</nav>
<% end %>
<div class="events__container align-start gap">
<div class="flex flex-column">
<div id="activity" class="events__index flex flex-column">
<%= render "events/day", day_timeline: @day_timeline %>
</div>
<%= event_next_page_link(@day_timeline.next_day) %>
</div>
<div class="events" id="activity">
<%= render "events/day", day_timeline: @day_timeline %>
<%= event_next_page_link(@day_timeline.next_day) %>
</div>
+2 -2
View File
@@ -11,7 +11,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest
test "index" do
get events_path
assert_select "div.event__wrapper[style='grid-area: 17/2']" do
assert_select "ul.events__time-block[style='grid-area: 17/2']" do
assert_select "strong", text: "David assigned JZ to Layout is broken"
end
end
@@ -21,7 +21,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest
get events_path
assert_select "div.event__wrapper[style='grid-area: 22/2']" do
assert_select "ul.events__time-block[style='grid-area: 22/2']" do
assert_select "strong", text: "David assigned JZ to Layout is broken"
end
end