Add Basecamp-style boosts

This commit is contained in:
Andy Smith
2025-10-27 16:59:57 -05:00
parent bf44ad973a
commit b2a83f1625
17 changed files with 157 additions and 98 deletions
+1
View File
@@ -0,0 +1 @@
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m15.166 7.44824c.3049-.46037.9253-.58686 1.3858-.28222.4603.30484.5868.92529.2822 1.38574l-5.2979 8.00004c-.1657.2502-.4347.4133-.7334.4433-.2986.03-.5954-.0758-.80758-.2881l-2.70215-2.7041c-.39038-.3906-.39057-1.0236 0-1.414.39049-.3903 1.02361-.3903 1.41406 0l1.83787 1.8379z"/><path clip-rule="evenodd" d="m12 1c6.0751 0 11 4.92487 11 11 0 6.0751-4.9249 11-11 11-6.07513 0-11-4.9249-11-11 0-6.07513 4.92487-11 11-11zm0 2c-4.97056 0-9 4.02944-9 9 0 4.9706 4.02944 9 9 9 4.9706 0 9-4.0294 9-9 0-4.97056-4.0294-9-9-9z" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 669 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m15.293 7.29297c.3905-.39052 1.0235-.39052 1.414 0 .3906.39052.3905 1.02354 0 1.41406l-3.2929 3.29297 3.2929 3.293c.3905.3905.3906 1.0235 0 1.414-.3905.3905-1.0235.3905-1.414 0l-3.293-3.2929-3.29297 3.2929c-.39052.3906-1.02354.3906-1.41406 0-.39052-.3905-.39052-1.0235 0-1.414l3.29293-3.293-3.29293-3.29297c-.39052-.39052-.39052-1.02354 0-1.41406.39052-.39053 1.02354-.39053 1.41406 0l3.29297 3.29293z"/><path clip-rule="evenodd" d="m12 1c6.0751 0 11 4.92487 11 11 0 6.0751-4.9249 11-11 11-6.07513 0-11-4.9249-11-11 0-6.07513 4.92487-11 11-11zm0 2c-4.97056 0-9 4.02944-9 9 0 4.9706 4.02944 9 9 9 4.9706 0 9-4.0294 9-9 0-4.97056-4.0294-9-9-9z" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 793 B

+1 -1
View File
@@ -1 +1 @@
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m16.5 14.1c-.5-.3-1.1-.1-1.4.4-.6 1.2-1.8 1.9-3.1 2-1.3 0-2.5-.8-3.1-2-.3-.5-.9-.6-1.4-.3-.5.2-.6.8-.4 1.3 1 1.8 2.8 3 4.9 3s3.9-1.2 4.9-3c.2-.5.1-1.1-.4-1.4z"/><circle cx="8" cy="9" r="2"/><circle cx="16" cy="9" r="2"/><path d="m6.9 1.1c-6 2.9-8.4 10.1-5.5 16 2.3 4.6 7.2 7.3 12.3 6.6.1 0 .2 0 .3-.1.5-.2.7-.8.5-1.3-.2-.4-.6-.6-1-.6-5.5.7-10.5-3.1-11.2-8.6s3.1-10.4 8.5-11.1 10.5 3.1 11.2 8.6c.2 1.2.1 2.4-.2 3.6-.1.5.2 1.1.7 1.2s1.1-.2 1.2-.7c1.6-6.4-2.4-12.9-8.9-14.5-2.6-.6-5.4-.3-7.9.9z"/><path d="m18.1 22.8c0 .7.6 1.2 1.2 1.2s1.2-.6 1.2-1.2v-2c0-.1.1-.2.2-.2h2c.7 0 1.2-.6 1.2-1.2s-.3-1.4-1-1.4h-2c-.1 0-.2-.1-.2-.2v-2c0-.7-.6-1.2-1.2-1.2s-1.2.6-1.2 1.2v2c0 .1-.1.2-.2.2h-2c-.7 0-1.2.6-1.2 1.2s.6 1.2 1.2 1.2h2c.1 0 .2.1.2.2v2.2z"/></svg>
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m12 1c6.0751 0 11 4.92487 11 11 0 .7521-.0753 1.4879-.2197 2.1992-.11.5411-.6385.8911-1.1797.7813-.5411-.1101-.8901-.6386-.7803-1.1797.1179-.581.1797-1.1832.1797-1.8008 0-4.97056-4.0294-9-9-9-4.97056 0-9 4.02944-9 9 0 4.9706 4.02944 9 9 9 .6176 0 1.2198-.0618 1.8008-.1797.5411-.1098 1.0696.2392 1.1797.7803.1098.5412-.2402 1.0697-.7813 1.1797-.7113.1444-1.4471.2197-2.1992.2197-6.07513 0-11-4.9249-11-11 0-6.07513 4.92487-11 11-11z"/><path d="m19 15c.5523 0 1 .4477 1 1v2h2c.5523 0 1 .4477 1 1s-.4477 1-1 1h-2v2c0 .5523-.4477 1-1 1s-1-.4477-1-1v-2h-2c-.5523 0-1-.4477-1-1s.4477-1 1-1h2v-2c0-.5523.4477-1 1-1z"/><path d="m15.0342 13.7412c.1428-.5329.6904-.8492 1.2236-.707.5335.1424.8503.6901.708 1.2236l-.9658-.2578c.9662.2577.9658.2578.9658.2578v.002l-.001.0019s-.0004.0039-.0009.0059c-.0011.0039-.0025.0084-.0039.0136-.0031.011-.0077.0255-.0127.042-.01.033-.0238.0768-.042.1299-.0365.1064-.0903.2514-.166.4219-.1506.3387-.3928.7925-.7588 1.25-.7476.9342-2.0152 1.875-3.9805 1.875s-3.23294-.9408-3.98047-1.875c-.36603-.4575-.60824-.9113-.75879-1.25-.07576-.1705-.12956-.3155-.16601-.4219-.0182-.053-.03202-.0969-.042-.1299-.00499-.0165-.00958-.031-.01269-.042-.00149-.0052-.00284-.0097-.00391-.0136-.00055-.002-.00097-.0059-.00097-.0059l-.00098-.0019c-.0069-.035 0-.002 0-.002-.14228-.5335.17452-1.0812.70801-1.2236.53325-.1422 1.08085.1741 1.22363.707l.00098.0029c.0033.011.00967.0318.01953.0606.01978.0577.05335.1471.10254.2578.09944.2238.25827.5201.49218.8125.45255.5656 1.18455 1.125 2.41895 1.125s1.9664-.5594 2.4189-1.125c.234-.2924.3928-.5887.4922-.8125.0492-.1107.0828-.2001.1026-.2578.0098-.0288.0162-.0496.0195-.0606z"/><path d="m8.5 8c.82843 0 1.5.67157 1.5 1.5 0 .8284-.67157 1.5-1.5 1.5s-1.5-.6716-1.5-1.5c0-.82843.67157-1.5 1.5-1.5z"/><path d="m15.5 8c.8284 0 1.5.67157 1.5 1.5 0 .8284-.6716 1.5-1.5 1.5s-1.5-.6716-1.5-1.5c0-.82843.6716-1.5 1.5-1.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 2.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m3.5 20.5c.7.7 1.8.7 2.5 0l5.9-5.9 5.9 5.9c.7.7 1.8.7 2.5 0s.7-1.8 0-2.5l-5.9-5.9 5.9-5.9c.7-.7.7-1.8 0-2.5s-1.8-.7-2.5 0l-5.9 5.9-5.8-6.1c-.7-.7-1.8-.7-2.5 0s-.7 1.8 0 2.5l5.9 5.9-5.9 5.9c-.7.7-.7 1.8 0 2.5z"/></svg>

Before

Width:  |  Height:  |  Size: 286 B

+6 -17
View File
@@ -4,7 +4,7 @@
--comment-padding-block: var(--block-space-half);
--comment-padding-inline: var(--inline-space-double);
--comment-max: 70ch;
--reaction-size: 2rem;
--reaction-size: 1.6875rem;
display: flex;
flex-direction: column;
@@ -37,8 +37,6 @@
}
.comment__author {
margin-inline-end: calc(var(--comment-padding-inline) * -0.66);
.btn {
font-weight: inherit;
}
@@ -54,6 +52,7 @@
}
.comment__body {
padding-inline-end: var(--reaction-size);
text-align: start;
:first-child {
@@ -75,7 +74,7 @@
border-radius: 0.2em;
padding:
var(--comment-padding-block)
var(--comment-padding-inline)
calc(var(--comment-padding-inline) / 2)
calc(var(--comment-padding-block) * 1.5)
var(--comment-padding-inline);
word-wrap: break-word;
@@ -89,17 +88,8 @@
}
}
.comment__reaction {
background-color: var(--color-ink-lightest);
opacity: 0.66;
@media (any-hover: hover) {
.comment:not(:hover) & {
opacity: 0;
}
}
.comment--mine & {
.comment--mine {
.comment__reaction {
display: none;
}
}
@@ -147,8 +137,7 @@
);
}
.reactions,
.quick-reaction {
.reactions {
display: none !important;
}
}
+2 -1
View File
@@ -36,9 +36,11 @@
.icon--camera { --svg: url("camera.svg "); }
.icon--caret-down { --svg: url("caret-down.svg "); }
.icon--check { --svg: url("check.svg "); }
.icon--check-circle { --svg: url("check-circle.svg "); }
.icon--check-all { --svg: url("check-all.svg "); }
.icon--clipboard { --svg: url("clipboard.svg "); }
.icon--close { --svg: url("close.svg "); }
.icon--close-circle { --svg: url("close-circle.svg "); }
.icon--collection { --svg: url("collection.svg "); }
.icon--collection-add { --svg: url("collection-add.svg "); }
.icon--comment { --svg: url("comment.svg "); }
@@ -81,7 +83,6 @@
.icon--reaction { --svg: url("reaction.svg "); }
.icon--refresh { --svg: url("refresh.svg "); }
.icon--refresh--meta { --svg: url("refresh--meta.svg "); }
.icon--remove-med { --svg: url("remove-med.svg "); }
.icon--remove { --svg: url("remove.svg "); }
.icon--rename { --svg: url("rename.svg "); }
.icon--search { --svg: url("search.svg "); }
+59 -28
View File
@@ -12,18 +12,41 @@
inline-size: 100%;
margin-block-start: calc(var(--block-space-half) / 2);
margin-inline: calc(var(--column-gap) / -1);
&:has([open]) {
z-index: var(--z-popup);
}
&:not(:has(.reaction)) {
inline-size: auto;
inset-block-end: var(--comment-padding-block);
inset-inline-end: calc(var(--comment-padding-inline) / 2);
margin: 0;
position: absolute;
.reactions__list {
display: none;
}
.reactions__trigger {
background-color: var(--color-ink-lightest);
&:not(:hover) {
opacity: 0.66;
}
}
}
}
.reactions__list {
display: inline-flex;
flex-wrap: wrap;
gap: var(--column-gap);
&:not(:has(.reaction)) {
display: none;
}
gap: var(--inline-space-half);
}
/* Single reaction
/* -------------------------------------------------------------------------- */
.reaction {
--btn-size: 100%;
--reaction-hover-brightness: 0.9;
@@ -34,10 +57,10 @@
border: 1px solid var(--reaction-border-color);
border-radius: 4rem;
display: inline-flex;
gap: var(--column-gap);
gap: 0.25ch;
max-inline-size: 100%;
opacity: 1;
padding: 0.1em 0.25em 0.1em 0.12em;
padding: 0.1em 0.12em;
postion: relative;
transition: opacity 100ms ease-in-out, transform 150ms ease-in-out;
@@ -51,7 +74,7 @@
cursor: pointer;
@media (any-hover: hover) {
&:not(:active):hover {
&:not(.expanded):hover {
filter: brightness(var(--reaction-hover-brightness));
@media (prefers-color-scheme: dark) {
@@ -70,7 +93,6 @@
.reaction__delete {
display: none;
translate: 0.25em;
.expanded & {
display: grid;
@@ -88,7 +110,7 @@
&:has(.input:focus) {
outline: var(--focus-ring-size) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
outline-offset: -1px;
}
.reaction__form-label:focus {
@@ -112,25 +134,30 @@
animation: scale-fade-out 0.2s both;
}
.reaction__action {
--btn-border-color: var(--reaction-border-color);
--btn-size: var(--reaction-size);
font-size: 0.75em;
.reactions:not(:has(.reaction)) & {
display: none;
}
.reaction__menu-btn,
.reaction__submit-btn,
.reaction__cancel-btn {
--btn-size: 1.25rem;
--icon-size: var(--btn-size);
}
/* Quick reactions
.reaction__submit-btn {
color: oklch(var(--lch-green-dark));
}
.reaction__cancel-btn {
color: oklch(var(--lch-red-dark));
}
/* Menu
/* ------------------------------------------------------------------------ */
.quick-reaction {
.reaction__menu {
position: relative;
}
.quick-reaction__popup {
.reaction__popup {
--panel-border-radius: 1.2em;
--panel-padding: 0.5ch;
--offset: calc(-1 * var(--panel-padding));
@@ -140,22 +167,26 @@
transform: none;
}
.quick-reaction__emojis {
.reaction__emoji-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(10, 1fr);
.btn {
--btn-icon-size: 1.5ch;
--btn-size: var(--reaction-size);
--btn-border-color: var(--color-canvas);
--btn-size: 1.5rem;
font-size: 1rem;
position: relative;
/* Make sure the selection ring sits on top of adjacent buttons on hover */
/* Make sure the focus ring sits on top of adjacent buttons */
&:hover,
&:focus-visible {
filter: none;
z-index: 1;
}
&:hover {
scale: 1.5;
}
}
}
}
+47 -5
View File
@@ -1,12 +1,54 @@
module EmojiHelper
REACTIONS = {
"👍" => "Thumbs up",
"👏" => "Clapping",
"👋" => "Waving hand",
"💪" => "Muscle",
"👍" => "Thumbs up",
"🙌" => "Hands raised in celebration",
"💪" => "Flexed bicep",
"🤘" => "Sign of the horns",
"" => "Raised fist",
"" => "Sparkles",
"❤️" => "Red heart",
"😂" => "Face with tears of joy",
"💯" => "100 points",
"🎉" => "Party popper",
"🔥" => "Fire"
"🤩" => "Face with starry eyes",
"🥳" => "Partying face",
"😊" => "Smiling face with flush cheeks",
"😀" => "Grinning face",
"😂" => "Face with tears of joy",
"😅" => "Grinning face with sweat drop",
"😎" => "Smiling face with sunglasses",
"😉" => "Winking face",
"😜" => "Winking face with stuck out tongue",
"😬" => "Grimacing face",
"😮" => "Surprised face with open mouth",
"😳" => "Flushed face",
"🤔" => "Thinking face",
"😒" => "Unamused face",
"😢" => "Crying face",
"😭" => "Loudly crying face",
"😱" => "Face screaming in fear",
"👀" => "Eyes",
"🙏" => "Hands pressed together",
"💩" => "Pile of poop",
"👎" => "Thumbs down",
"✌️" => "Peace",
"👈" => "Finger pointing left",
"👆" => "Finger pointing Up",
"" => "Raised hand",
"👋" => "Waving hand",
"☀️" => "Sun",
"🌙" => "Moon",
"💥" => "Collision",
"🔥" => "Fire",
"🎂" => "Birthday cake",
"🍴" => "Fork and knife",
"💰" => "Money bag",
"🥇" => "Gold medal",
"🚨" => "Red flashing light",
"💡" => "Light bulb",
"🛠" => "Hammer and wrench",
"📈" => "Chart with upward trend",
"" => "Check mark",
"📢" => "Public address loudspeaker"
}
end
+1 -1
View File
@@ -48,7 +48,7 @@ module NotificationsHelper
title: "Mark as read",
data: { action: "form#submit:stop badge#update:stop", form_target: "submit" },
form: { data: { controller: "form" } } do
concat(icon_tag("remove-med"))
concat(icon_tag("remove"))
concat(tag.span("Mark as read", class: "for-screen-reader"))
end
end
@@ -0,0 +1,11 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "input" ]
insertEmoji(event) {
const emojiChar = event.target.getAttribute("data-emoji")
const value = this.inputTarget.value
this.inputTarget.value = `${value}${emojiChar}`
}
}
@@ -24,13 +24,6 @@
<%= icon_tag "menu-dots-horizontal" %>
<span class="for-screen-reader">Edit this comment</span>
<% end %>
<%= render "cards/comments/reactions/quick_reaction", comment: comment do %>
<button class="comment__reaction btn btn--circle borderless" data-action="click->dialog#open:stop">
<%= icon_tag "menu-dots-horizontal" %>
<span class="for-screen-reader">Add a reaction</span>
</button>
<% end %>
</div>
<div class="comment__body rich-text-content" data-controller="syntax-highlight retarget-links">
@@ -0,0 +1,13 @@
<div class="reaction__menu" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside">
<button class="reaction__menu-btn btn btn--circle borderless" data-action="click->dialog#open:stop" type="button">
<%= icon_tag "reaction" %>
</button>
<dialog class="reaction__popup popup panel fill-white shadow" data-dialog-target="dialog">
<div class="reaction__emoji-list">
<% EmojiHelper::REACTIONS.each do |character, title| %>
<%= tag.button character, title: title, class: "reaction__emoji-btn btn btn--circle borderless", type: "button", data: { action: "reaction-emoji#insertEmoji dialog#close", emoji: character } %>
<% end %>
</div>
</dialog>
</div>
@@ -1,25 +0,0 @@
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside">
<%= yield %>
<dialog class="quick-reaction__popup popup panel fill-white shadow" data-dialog-target="dialog">
<div class="quick-reaction__emojis">
<% EmojiHelper::REACTIONS.each do |character, title| %>
<%= form_with model: [ comment.card, comment, Reaction.new ], data: { turbo_frame: dom_id(comment, :new_reaction), action: "dialog#close"} do |form| %>
<%= hidden_field_tag "reaction[content]", character %>
<%= form.button type: "submit", title: title, class: "reaction__quick-emoji-btn btn btn--circle", data: { emoji: character } do %>
<span><%= character %></span>
<% end %>
<% end %>
<% end %>
<div class="flex-inline" data-controller="soft-keyboard">
<%= link_to new_card_comment_reaction_path(comment.card, comment), role: "button",
class: "btn btn--circle", action: "soft-keyboard#open",
data: { turbo_frame: dom_id(comment, :new_reaction), action: "dialog#close" } do %>
<%= icon_tag "reaction" %>
<span class="for-screen-reader">Add your own reaction</span>
<% end %>
</div>
</div>
</dialog>
</div>
@@ -19,7 +19,7 @@
method: :delete,
class: "reaction__delete btn btn--negative flex-item-justify-end",
data: { action: "reaction-delete#perform", reaction_delete_target: "button" } do %>
<%= icon_tag "minus" %>
<%= icon_tag "trash" %>
<span class="for-screen-reader">Delete this reaction</span>
<% end %>
</div>
@@ -5,10 +5,11 @@
</div>
<%= turbo_frame_tag comment, :new_reaction do %>
<%= render "cards/comments/reactions/quick_reaction", comment: comment do %>
<button class="reaction__action btn btn--circle" data-action="click->dialog#open:stop">
<%= icon_tag "reaction" %>
</button>
<%= link_to new_card_comment_reaction_path(comment.card, comment), role: "button",
class: "reactions__trigger btn btn--circle borderless", action: "soft-keyboard#open",
data: { turbo_frame: dom_id(comment, :new_reaction), action: "dialog#close" } do %>
<%= icon_tag "reaction" %>
<span class="for-screen-reader">Add your own reaction</span>
<% end %>
<% end %>
</div>
@@ -2,23 +2,25 @@
<%= form_with model: [ @comment.card, @comment, Reaction.new ],
class: "reaction reaction__form expanded",
html: { aria: { label: "New reaction" } },
data: { controller: "form", turbo_frame: dom_id(@comment, :reacting), action: "keydown.esc->form#cancel" } do |form| %>
data: { controller: "form reaction-emoji", turbo_frame: dom_id(@comment, :reacting), action: "keydown.esc->form#cancel submit->form#preventEmptySubmit" } do |form| %>
<label class="reaction__form-label flex gap" style="--column-gap: 0.4ch;">
<figure class="reaction__avatar margin-none flex-item-no-shrink">
<%= avatar_tag Current.user %>
</figure>
<%= form.text_field :content, autofocus: true, autocomplete: "off", autocorrect: "off", maxlength: 16,
required: true, pattern: /\S+.*/, class: "input reaction__input txt-small", aria: { label: "Add a reaction" } %>
pattern: /\S+.*/, class: "input reaction__input txt-small", data: { form_target: "input", reaction_emoji_target: "input" }, aria: { label: "Add a reaction" } %>
</label>
<%= form.button class: "btn btn--circle borderless btn--reversed", type: "submit" do %>
<%= icon_tag "check" %> <span class="for-screen-reader">Submit</span>
<%= render "cards/comments/reactions/menu", comment: @comment %>
<%= form.button class: "reaction__submit-btn btn btn--circle borderless", type: "submit", data: { form_target: "submit" } do %>
<%= icon_tag "check-circle" %> <span class="for-screen-reader">Submit</span>
<% end %>
<%= link_to card_comment_reactions_path(@comment.card, @comment), role: "button",
data: { turbo_frame: dom_id(@comment, :reacting), form_target: "cancel" }, class: "btn btn--circle borderless btn--negative" do %>
<%= icon_tag "minus" %> <span class="for-screen-reader">Cancel</span>
data: { turbo_frame: dom_id(@comment, :reacting), form_target: "cancel" }, class: "reaction__cancel-btn btn btn--circle borderless" do %>
<%= icon_tag "close-circle" %> <span class="for-screen-reader">Cancel</span>
<% end %>
<% end %>
<% end %>
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="search__header">
<button class="btn txt-x-small" data-action="bar#reset">
<%= icon_tag "remove-med" %>
<%= icon_tag "remove" %>
<span class="for-screen-reader">Close</span>
</button>
</div>