diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 3409b83de..1083c2ffc 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -536,16 +536,33 @@ /* Set lower limit for font size */ font-size: clamp(0.6rem, 0.85cqi, 100px); - .card__comments { - --column-gap: 0.8ch; + .card__counts { + --gap: 0.5ch; + align-items: flex-end; display: flex; - margin-block-end: calc(var(--block-space) * -1.7); - margin-inline-end: calc(var(--card-padding-inline) * -0.5); + flex-shrink: 0; + gap: calc(2 * var(--gap)); + margin-inline: auto calc(var(--card-padding-inline) * -0.5); + padding-inline-start: var(--gap); + } - .icon { - --icon-size: 1.6em; + .card__boosts, + .card__comments { + --icon-size: 1.6em; + align-items: center; + display: flex; + flex-shrink: 0; + font-weight: 600; + gap: var(--gap); + + img { + block-size: var(--icon-size); + inline-size: var(--icon-size); + } + + .icon--comment { color: var(--card-color); } } @@ -584,11 +601,16 @@ .local-time-value { font-weight: inherit; } + + @media (max-width: 639px) { + inline-size: auto; + } } &:has(.card__background img:not([src=""])) { .card__content, .card__meta, + .card__boosts, .card__comments, .card__column-name:not(.card__column-name--current) { opacity: 0; @@ -599,6 +621,7 @@ &:hover { .card__content, .card__footer, + .card__boosts, .card__comments, .card__column-name:not(.card__column-name--current) { opacity: 1; diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index e68706765..b3d37d68a 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -34,6 +34,23 @@ } } + @media (max-width: 799px) { + --half-btn-height: 1.25rem; + --padding-inline: 1.5ch; + + column-gap: 0; + grid-template-areas: + "notch-top notch-top notch-top" + "card card card" + "actions-left notch-bottom actions-right" + "closure-message closure-message closure-message"; + grid-template-columns: 1fr auto 1fr; + inline-size: calc(100% + 2 * var(--padding-inline)); + margin-inline: calc(-1 * var(--padding-inline)); + max-inline-size: none; + position: relative; + } + .card { --card-aspect-ratio: 2 / 0.95; --lexxy-bg-color: var(--card-bg-color); @@ -47,6 +64,8 @@ } .card__body { + position: relative; + @media (max-width: 639px) { flex-direction: column; padding-block-end: calc(var(--card-padding-block) * 1.5); @@ -86,6 +105,11 @@ } } + .card__meta { + grid-area: meta; + margin-inline-end: auto; + } + .card__stages { max-inline-size: 32ch; @@ -119,6 +143,71 @@ } } + .card__footer { + --btn-size: 2.5rem; + + display: flex; + gap: 0.5ch; + inline-size: 100%; + text-align: start; + + /* Switch to grid layout so that the bg zoom button can stay next to the + * meta element, and the reactions can sit below */ + &:has(.reaction) { + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + "meta bg-zoom" + "reactions reactions"; + } + + @media (max-width: 639px) { + font-size: var(--text-x-small); + } + } + + .reactions { + --reaction-size: var(--btn-size); + + align-self: flex-end; + display: flex; + gap: 0.5ch; + grid-area: reactions; + margin-inline-start: auto; + + &:has(.reaction) { + --padding: calc(var(--card-padding-block) / 2); + --reaction-size: 1.6875rem; + + margin-block: var(--padding) calc(-1 * var(--padding)); + padding-block-start: var(--padding); + position: relative; + + &:before { + border-block-start: 1px dashed color-mix(in srgb, transparent, var(--card-color) 33%); + content: ""; + inset: 0 calc(-1 * var(--card-padding-inline)) auto; + position: absolute; + } + } + + &:not(:has(.reaction)) { + position: static; + + .reactions__trigger { + --btn-border-color: var(--color-ink-light); + } + } + } + + .reaction__popup.popup { + inline-size: max-content; + } + + .card__zoom-bg-btn { + grid-area: bg-zoom; + } + .bubble { --bubble-number-max: 42px; --bubble-size: 6rem; @@ -132,23 +221,6 @@ inset: calc(var(--bubble-size) / 1.5) 0 auto auto; } } - - @media (max-width: 799px) { - --half-btn-height: 1.5rem; - --padding-inline: 1.5ch; - - column-gap: 0; - grid-template-areas: - "notch-top notch-top notch-top" - "card card card" - "actions-left notch-bottom actions-right" - "closure-message closure-message closure-message"; - grid-template-columns: 1fr auto 1fr; - inline-size: calc(100% + 2 * var(--padding-inline)); - margin-inline: calc(-1 * var(--padding-inline)); - max-inline-size: none; - position: relative; - } } /* Child items diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 3aec6e3b0..0baec71e6 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -252,9 +252,10 @@ padding-block: var(--block-space-half); } - /* Meta + /* Footer /* ------------------------------------------------------------------------ */ + /* Card metadata */ .card__meta { --meta-spacer-block: 0.5ch; --meta-spacer-inline: 0.75ch; @@ -376,11 +377,12 @@ display: flex; flex-direction: column; font-weight: bold; - inset: auto 1ch 1ch auto; + inset: auto 0 -1lh auto; justify-content: center; max-inline-size: 25ch; min-inline-size: 16ch; padding: 1ch; + pointer-events: none; position: absolute; rotate: 5deg; transform-origin: top right; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 006cbb985..a8605503b 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -80,6 +80,11 @@ display: none !important; } } + + .reactions { + margin-block-start: var(--block-space-half); + margin-inline: calc(var(--column-gap) / -1); + } } .comment__author { diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index d1cd0a03f..64f2a5164 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -10,8 +10,6 @@ flex-wrap: wrap; gap: var(--inline-space-half); inline-size: 100%; - margin-block-start: var(--block-space-half); - margin-inline: calc(var(--column-gap) / -1); &:has([open]) { z-index: var(--z-popup); @@ -34,11 +32,8 @@ .reactions__trigger { --btn-border-color: var(--color-ink-lightest); - background-color: var(--color-ink-lightest); - &:not(:hover) { - opacity: 0.66; - } + background-color: var(--color-ink-lightest); } } } diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 7793b6f2d..0e47857ab 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -440,6 +440,7 @@ .card__meta-text:not(.card__meta-text--updated), .card__stages, .card__steps, + .card__boosts, .card__comments, .card__closed { display: none; diff --git a/app/controllers/cards/comments/reactions_controller.rb b/app/controllers/cards/comments/reactions_controller.rb index 0f1277a3c..0c822360e 100644 --- a/app/controllers/cards/comments/reactions_controller.rb +++ b/app/controllers/cards/comments/reactions_controller.rb @@ -2,20 +2,26 @@ class Cards::Comments::ReactionsController < ApplicationController include CardScoped before_action :set_comment - before_action :set_reaction, only: %i[ destroy ] - before_action :ensure_permision_to_administer_reaction, only: %i[ destroy ] + before_action :set_reactable + + with_options only: :destroy do + before_action :set_reaction + before_action :ensure_permission_to_administer_reaction + end def index + render "reactions/index" end def new + render "reactions/new" end def create - @reaction = @comment.reactions.create!(params.expect(reaction: :content)) + @reaction = @reactable.reactions.create!(params.expect(reaction: :content)) respond_to do |format| - format.turbo_stream + format.turbo_stream { render "reactions/create" } format.json { head :created } end end @@ -24,7 +30,7 @@ class Cards::Comments::ReactionsController < ApplicationController @reaction.destroy respond_to do |format| - format.turbo_stream + format.turbo_stream { render "reactions/destroy" } format.json { head :no_content } end end @@ -34,11 +40,15 @@ class Cards::Comments::ReactionsController < ApplicationController @comment = @card.comments.find(params[:comment_id]) end - def set_reaction - @reaction = @comment.reactions.find(params[:id]) + def set_reactable + @reactable = @comment end - def ensure_permision_to_administer_reaction + def set_reaction + @reaction = @reactable.reactions.find(params[:id]) + end + + def ensure_permission_to_administer_reaction head :forbidden if Current.user != @reaction.reacter end end diff --git a/app/controllers/cards/reactions_controller.rb b/app/controllers/cards/reactions_controller.rb new file mode 100644 index 000000000..4105a4121 --- /dev/null +++ b/app/controllers/cards/reactions_controller.rb @@ -0,0 +1,49 @@ +class Cards::ReactionsController < ApplicationController + include CardScoped + + before_action :set_reactable + + with_options only: :destroy do + before_action :set_reaction + before_action :ensure_permission_to_administer_reaction + end + + def index + render "reactions/index" + end + + def new + render "reactions/new" + end + + def create + @reaction = @reactable.reactions.create!(params.expect(reaction: :content)) + + respond_to do |format| + format.turbo_stream { render "reactions/create" } + format.json { head :created } + end + end + + def destroy + @reaction.destroy + + respond_to do |format| + format.turbo_stream { render "reactions/destroy" } + format.json { head :no_content } + end + end + + private + def set_reactable + @reactable = @card + end + + def set_reaction + @reaction = @reactable.reactions.find(params[:id]) + end + + def ensure_permission_to_administer_reaction + head :forbidden if Current.user != @reaction.reacter + end +end diff --git a/app/helpers/reactions_helper.rb b/app/helpers/reactions_helper.rb new file mode 100644 index 000000000..5c47781f6 --- /dev/null +++ b/app/helpers/reactions_helper.rb @@ -0,0 +1,10 @@ +module ReactionsHelper + def reaction_path_prefix_for(reactable) + case reactable + when Card then [ reactable ] + when Comment then [ reactable.card, reactable ] + else + raise ArgumentError, "Unknown reactable type: #{reactable.class}" + end + end +end diff --git a/app/models/card.rb b/app/models/card.rb index 43e2d9361..d422afa85 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -7,6 +7,7 @@ class Card < ApplicationRecord belongs_to :board belongs_to :creator, class_name: "User", default: -> { Current.user } + has_many :reactions, -> { order(:created_at) }, as: :reactable, dependent: :delete_all has_one_attached :image, dependent: :purge_later has_rich_text :description @@ -22,7 +23,7 @@ class Card < ApplicationRecord scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order last_active_at: :desc, id: :desc } scope :with_users, -> { preload(creator: [ :avatar_attachment, :account ], assignees: [ :avatar_attachment, :account ]) } - scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, board: [ :entropy, :columns ], not_now: [ :user ]).with_rich_text_description_and_embeds } + scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, reactions: :reacter, board: [ :entropy, :columns ], not_now: [ :user ]).with_rich_text_description_and_embeds } scope :indexed_by, ->(index) do case index diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index de3bc4b27..b6823bd76 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -21,11 +21,14 @@ <% if card.published? %> <%= render "cards/triage/columns", card: card %> <% end %> + + <%= render "cards/display/common/stamp", card: card %> -