diff --git a/app/assets/images/picture-remove.svg b/app/assets/images/picture-remove.svg index db868e362..ba80174fe 100644 --- a/app/assets/images/picture-remove.svg +++ b/app/assets/images/picture-remove.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index d6d7712fc..f737200eb 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -153,7 +153,6 @@ --color-selected-dark: oklch(var(--lch-blue-light)); --color-highlight: oklch(var(--lch-yellow-lighter)); --color-marker: oklch(var(--lch-red-medium)); - --color-container: oklch(var(--lch-aqua-light)); --color-terminal-bg: oklch(var(--lch-blue-darkest)); --color-terminal-text: oklch(var(--lch-green-medium)); @@ -261,8 +260,6 @@ --lch-pink-lighter: 30% 0.03 346; --lch-pink-lightest: 25% 0.011 348; - --color-container: oklch(var(--lch-aqua-lighter)); - --shadow: 0 0 0 1px oklch(var(--lch-black) / 0.42), 0 .2em 1.6em -0.8em oklch(var(--lch-black) / 0.6), 0 .4em 2.4em -1em oklch(var(--lch-black) / 0.7), diff --git a/app/assets/stylesheets/animation.css b/app/assets/stylesheets/animation.css index c99081856..656f1ae76 100644 --- a/app/assets/stylesheets/animation.css +++ b/app/assets/stylesheets/animation.css @@ -26,4 +26,13 @@ 50% { transform: translateX(-1rem); } 75% { transform: translateX(1rem); } } + + @keyframes wobble { + 0% { transform: rotate(calc(var(--bubble-rotate) + 30deg)); } + 15% { border-radius: 66% 34% 72% 28% / 39% 63% 37% 61%; } + 25% { border-radius: 55% 47% 62% 40% / 58% 50% 52% 44%; } + 33% { border-radius: 46% 54% 61% 39% / 50% 51% 49% 50%; } + 50% { border-radius: 54% 46% 61% 39% / 57% 49% 51% 43%; } + 75% { border-radius: 53% 45% 60% 38% / 56% 48% 50% 42%; } + } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 0e3e4a1ff..bc1f9ad6e 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -59,7 +59,7 @@ transition: opacity 0.2s ease-in-out; } - @media (hover: hover) { + @media (any-hover: hover) { .card:hover & { img:not([src=""]) { opacity: 1; @@ -212,6 +212,7 @@ .card__meta-grid { display: grid; grid-template-columns: auto auto; + margin-inline-start: var(--inline-space); } .card__meta-item { @@ -286,4 +287,61 @@ -2px 2px 0 var(--card-color), 2px 2px 0 var(--card-color); } + + .card__bubble { + --bubble-color: radial-gradient(oklch(var(--lch-blue-medium) / 10%) 50%, oklch(var(--lch-blue-medium) / 50%) 100%); + --bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%; + --bubble-rotate: 0deg; + --size: 5cqi; + + block-size: var(--size); + container-type: inline-size; + font-size: 1.75rem; + font-weight: bold; + inline-size: var(--size); + inset: 50% auto auto -0.5ch; + padding: 0.5cqi; + position: absolute; + translate: -100% -50%; + + &:before { + background: var(--bubble-color); + border-radius: var(--bubble-shape); + content: ""; + inset: 0; + position: absolute; + transform: rotate(var(--bubble-rotate)); + } + + @media (any-hover: hover) { + &:hover:before { + animation: wobble 1200ms; + } + } + + span { + display: grid; + font-size: 40cqi; + font-weight: 900; + inset: 0; + place-content: center; + position: absolute; + text-align: center; + } + + svg { + display: block; + letter-spacing: 0.125ch; + text-transform: uppercase; + } + + .card:nth-child(2n+1) & { --bubble-rotate: -90deg; } + .card:nth-child(3n+1) & { --bubble-rotate: 45deg; } + } + + .card__bubble--today { + span { + font-size: 30cqi; + } + } } diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index f4f280412..7770a1bd8 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -1,5 +1,8 @@ @layer components { .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); @@ -9,7 +12,7 @@ .events__container { --column-gap: clamp(var(--inline-space), 3cqmin, calc(var(--inline-space) * 3)); - --grid-lines: 0.1rem; + --grid-lines: 1px; margin: auto; max-inline-size: min(105ch, 100%); @@ -24,7 +27,8 @@ } .events__day-header-content { - background-color: var(--color-canvas); + 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; @@ -53,10 +57,10 @@ } .events--none { - background-color: var(--color-container); - border-block-start: var(--grid-lines) solid var(--color-canvas); + background-color: var(--color-canvas); + border-block-start: var(--grid-lines) solid var(--color-ink-lightest); padding-block: 3em; - margin: auto var(--grid-lines) calc(var(--grid-lines) * -1) auto; + margin: auto; } .events__popup { @@ -66,7 +70,7 @@ .event { --column-gap: 0.7ch; --panel-border-color: transparent; - --panel-border-radius: var(--grid-lines); + --panel-border-radius: 0.2em; --panel-padding: 0.6em 1.2em 0.6em 0.6em; --panel-size: auto; @@ -90,7 +94,7 @@ } .event__grid-item { - background-color: var(--color-container); + background-color: var(--color-canvas); block-size: 100%; border-radius: 0; display: flex; @@ -100,7 +104,7 @@ .event__grid-column-title { --z: 3; - background-color: var(--color-container); + background-color: var(--color-canvas); font-size: 0.9em; padding: 1.5em 0 1em; text-transform: uppercase; @@ -123,10 +127,10 @@ .event__wrapper { align-content: end; display: grid; - gap: calc(var(--grid-lines) * 2); + gap: var(--block-space-half); justify-items: center; margin: var(--grid-lines) var(--grid-lines) calc(var(--grid-lines) * 6) var(--grid-lines); - padding: 0 var(--grid-lines); + padding: var(--block-space) var(--inline-space-double); .event { grid-column-start: unset !important; diff --git a/app/assets/stylesheets/pins.css b/app/assets/stylesheets/pins.css index 8756e2391..8b77e95ee 100644 --- a/app/assets/stylesheets/pins.css +++ b/app/assets/stylesheets/pins.css @@ -25,6 +25,10 @@ display: none; } + .card__meta-grid { + margin-inline-start: 0; + } + .card { --block-space: 1em; --block-space-half: 0.5em; @@ -79,5 +83,9 @@ inset: 0 0 auto auto; position: absolute; } + + .card__bubble { + display: none; + } } } diff --git a/app/assets/stylesheets/terminals.css b/app/assets/stylesheets/terminals.css index 904ca078d..88a110dec 100644 --- a/app/assets/stylesheets/terminals.css +++ b/app/assets/stylesheets/terminals.css @@ -36,17 +36,22 @@ } .terminal__button { + --btn-background: var(--color-terminal-bg); --btn-color: var(--color-terminal-text); - --btn-border-radius: 1em; + --btn-border-radius: 0; --btn-border-color: var(--color-terminal-text); --btn-padding: 0.1em 0.7em; --btn-border-size: 1px; --hover-color: var(--color-terminal-text); - --hover-size: 2px; + --hover-size: 1px; font-family: var(--font-mono); font-size: var(--text-x-small); white-space: nowrap; + + &:where(:not(:active)):focus-visible { + outline: 2px solid var(--color-terminal-text); + } } .terminal__command { @@ -54,6 +59,7 @@ --btn-color: var(--color-terminal-text); font-weight: normal; + transition: unset; } .terminal__menu { @@ -113,16 +119,28 @@ } .terminal__item { + &:where(:not(:active)):focus-visible { + --btn-color: var(--color-terminal-bg); + + background: var(--color-terminal-text); + color: var(--color-terminal-bg); + outline: 2px solid var(--color-terminal-text); + + .terminal__command { + color: var(--color-terminal-bg); + } + } + @media (any-hover: hover) { - &:where(:not(:active):hover), - &:where(:not(:active)):focus-visible { - background: color-mix(in srgb, var(--color-terminal-text) 15%, var(--color-terminal-bg)); + &:where(:not(:active):hover) { + background: color-mix(in srgb, var(--color-terminal-text) 20%, var(--color-terminal-bg)); } } } .terminal--error .terminal__input { - outline: 2px solid var(--color-negative); - outline-offset: 2px; + accent-color: var(--color-negative); + caret-color: var(--color-negative); + color: var(--color-negative); } } diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 68e729f02..746d3cda5 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -59,6 +59,7 @@ module Authentication end def set_current_session(session) + logger.struct " Authorized User##{session.user.id}", authentication: { user: { id: session.user.id } } Current.session = session cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax } end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 3baaf7aab..19ca08745 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,9 +1,9 @@ module NotificationsHelper def event_notification_title(event) case event_notification_action(event) - when "comment_created" then "RE: " + event.eventable.card.title - when "card_assigned" then "Assigned to #{event.assignees.pluck(:name).to_sentence}: " + event.eventable.title - else event.eventable.title + when "comment_created" then "RE: #{card_notification_title(event.eventable.card)}" + when "card_assigned" then "Assigned to #{event.assignees.pluck(:name).to_sentence}: #{card_notification_title(event.eventable)}" + else card_notification_title(event.eventable) end end @@ -59,4 +59,8 @@ module NotificationsHelper comment = event.eventable "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" end + + def card_notification_title(card) + card.title.presence || "Card #{card.id}" + end end diff --git a/app/javascript/controllers/terminal_controller.js b/app/javascript/controllers/terminal_controller.js index e60b0c4e2..93c5dd590 100644 --- a/app/javascript/controllers/terminal_controller.js +++ b/app/javascript/controllers/terminal_controller.js @@ -45,6 +45,10 @@ export default class extends Controller { } } + hideError() { + this.element.classList.remove(this.errorClass) + } + get #hasHelpMenuCommand() { return this.inputTarget.value == "/help" || this.inputTarget.value == "/?" } diff --git a/app/models/card/closeable.rb b/app/models/card/closeable.rb index e0d15831b..0f7b88dd6 100644 --- a/app/models/card/closeable.rb +++ b/app/models/card/closeable.rb @@ -1,6 +1,9 @@ module Card::Closeable extend ActiveSupport::Concern + AUTO_CLOSE_AFTER = 30.days + AUTO_CLOSE_REMINDER_BEFORE = 7.days + included do has_one :closure, dependent: :destroy @@ -26,6 +29,14 @@ module Card::Closeable last_active_at + auto_close_period if auto_closing? && last_active_at end + def days_until_close + (auto_close_at.to_date - Date.current).to_i if auto_close_at + end + + def closing_soon? + considering? && Time.current >= auto_close_at - AUTO_CLOSE_REMINDER_BEFORE + end + def closed? closure.present? end diff --git a/app/models/card/engageable.rb b/app/models/card/engageable.rb index 6671bc5a1..f59ef2ab3 100644 --- a/app/models/card/engageable.rb +++ b/app/models/card/engageable.rb @@ -49,6 +49,7 @@ module Card::Engageable transaction do reopen engagement&.destroy + touch(:last_active_at) end end end diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 32b3d38da..d6b35c135 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -28,5 +28,9 @@ <%= render "cards/display/common/background", card: card %> + + <% if card.closing_soon? %> + <%= render "cards/display/preview/bubble", label: "Closes in", days: card.days_until_close %> + <% end %> <% end %> <% end %> diff --git a/app/views/cards/display/common/_meta.html.erb b/app/views/cards/display/common/_meta.html.erb index 12dca7c9e..acec071b1 100644 --- a/app/views/cards/display/common/_meta.html.erb +++ b/app/views/cards/display/common/_meta.html.erb @@ -1,7 +1,7 @@
<%= avatar_tag card.creator %> -
+
Added <%= local_datetime_tag(card.created_at, style: :daysago) %> <% if card.drafted? %> diff --git a/app/views/cards/display/preview/_bubble.html.erb b/app/views/cards/display/preview/_bubble.html.erb new file mode 100644 index 000000000..1b90ae83c --- /dev/null +++ b/app/views/cards/display/preview/_bubble.html.erb @@ -0,0 +1,23 @@ +
+ + + + + <%= label %> + + + + + + <%= days %> + + + + + + + <%= "Day".pluralize(days) %> + + + +
diff --git a/app/views/commands/_command.html.erb b/app/views/commands/_command.html.erb index 9fcecfac9..7f4fda5bb 100644 --- a/app/views/commands/_command.html.erb +++ b/app/views/commands/_command.html.erb @@ -2,7 +2,7 @@ action: "keydown.enter->terminal#restoreCommand:prevent keydown.enter->toggle-class#remove:prevent", navigable_list_target: "item" } do %> <%= button_tag command.title, type: "button", class: "btn btn--plain overflow-ellipsis terminal__command flex-item-grow justify-start", - data: { action: "toggle-class#remove terminal#restoreCommand", line: command.line } %> + data: { action: "toggle-class#remove terminal#restoreCommand", line: command.line }, tabindex: -1 %> <% if command.undoable? %> <%= button_to "Undo", command_undo_path(command), class: "btn btn--plain terminal__button flex-item-justify-end", diff --git a/app/views/commands/_form.html.erb b/app/views/commands/_form.html.erb index 4fa5fb7f2..a2f979c9c 100644 --- a/app/views/commands/_form.html.erb +++ b/app/views/commands/_form.html.erb @@ -15,10 +15,11 @@ class: "terminal__input input fill-transparent unpad", data: { terminal_target: "input", - action: "keydown.up->toggle-class#add:prevent keydown.up->navigable-list#selectCurrentOrLast", + action: "keydown.up->toggle-class#add:prevent keydown.up->navigable-list#selectCurrentOrLast terminal#hideError", turbo_permanent: true }, - placeholder: "Press ⌘+K to search or type /commands…" %> + placeholder: "Press ⌘+K to search or type commands…", + spellcheck: "false" %> <%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %> <% end %> diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb index 44e9e526a..067c3c2d6 100644 --- a/app/views/events/event/_layout.html.erb +++ b/app/views/events/event/_layout.html.erb @@ -1,7 +1,7 @@ <%= link_to event.notifiable_target, class: "event event--#{ event.action } panel center center-block flex-inline align-start justify-start gap position-relative", style: "--card-color: #{ card.color }; background-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas)); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));", + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); border: 1px solid color-mix(in srgb, var(--card-color) 20%, var(--color-canvas))", data: { related_element_target: "related", related_element_group_value: card.id, action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %> @@ -24,6 +24,6 @@ <%= yield %> - <%= event.collection.name %> + <%= card.id %> · <%= event.collection.name %>
<% end %> diff --git a/app/views/notifications/notification/_event.html.erb b/app/views/notifications/notification/_event.html.erb index 2dd8d16ee..8768d3d19 100644 --- a/app/views/notifications/notification/_event.html.erb +++ b/app/views/notifications/notification/_event.html.erb @@ -4,4 +4,4 @@
<%= event_notification_body(event) %>
-
<%= notification.source.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
+
<%= event.eventable.id %> · <%= notification.source.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
diff --git a/bin/dev b/bin/dev index e58bc97c3..316b6a0af 100755 --- a/bin/dev +++ b/bin/dev @@ -4,4 +4,5 @@ echo "Access with david@37signals.com / secret123456 on http://37signals.fizzy.l echo "Access with david@37signals.com / secret123456 on http://honcho.fizzy.localhost:3006" echo "Access first run on http://first-run.fizzy.localhost:3006" +export SOLID_QUEUE_IN_PUMA=1 exec ./bin/rails server -p 3006 diff --git a/config/environments/development.rb b/config/environments/development.rb index b929be5c4..74836019d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -77,4 +77,8 @@ Rails.application.configure do config.active_job.queue_adapter = :solid_queue config.solid_queue.connects_to = { database: { writing: :queue } } + + if Rails.root.join("tmp/structured-logging.txt").exist? + config.structured_logging.logger = ActiveSupport::Logger.new("log/structured-development.log") + end end diff --git a/test/models/card/engageable_test.rb b/test/models/card/engageable_test.rb index a0b041689..0db817a40 100644 --- a/test/models/card/engageable_test.rb +++ b/test/models/card/engageable_test.rb @@ -48,6 +48,8 @@ class Card::EngageableTest < ActiveSupport::TestCase end test "auto_reconsider_all_stagnated" do + travel_to Time.current + cards(:logo, :shipping).each(&:engage) cards(:logo).update!(last_active_at: 1.day.ago - Card::Engageable::STAGNATED_AFTER) @@ -59,5 +61,6 @@ class Card::EngageableTest < ActiveSupport::TestCase assert cards(:shipping).reload.doing? assert cards(:logo).reload.considering? + assert_equal Time.current, cards(:logo).last_active_at end end