Merge branch 'main' into unique-notifications

This commit is contained in:
Jason Zimdars
2025-11-12 10:06:47 -06:00
236 changed files with 1873 additions and 1260 deletions
+1 -3
View File
@@ -19,9 +19,7 @@ gem "solid_cache", "~> 1.0"
gem "solid_queue", "~> 1.2"
gem "sqlite3", ">= 2.0"
gem "thruster", require: false
source "https://e95ae463b12de3f204526a44650f6ae0@gems.stanko.io/private" do
gem "beamer-rails", "~> 0.1.0.beta4"
end
gem "beamer-rails", "~> 0.1.0.beta4"
# Features
gem "bcrypt", "~> 3.1.7"
+16 -20
View File
@@ -1,8 +1,8 @@
GIT
remote: https://github.com/basecamp/activerecord-tenanted
revision: aaa4a3473eab400c4c8a6ca05a7207578d3ade53
revision: e409f75dc2ea1c3355e98feb22b76b3cc0a01fe0
specs:
activerecord-tenanted (0.5.0)
activerecord-tenanted (0.6.0)
activerecord (>= 8.1.beta)
railties (>= 8.1.beta)
zeitwerk
@@ -36,7 +36,7 @@ GIT
GIT
remote: https://github.com/rails/rails.git
revision: d3d17c297a70d3e255a34693a1fb0074712e9930
revision: bc41ad696a76a151db56259938dccf64ef87b501
branch: main
specs:
actioncable (8.2.0.alpha)
@@ -143,20 +143,6 @@ PATH
rails (>= 8.1.0.beta1)
rails_structured_logging
GEM
remote: https://gems.stanko.io/private/
specs:
beamer-rails (0.1.0.beta4-arm64-darwin)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-arm64-linux-gnu)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-x86_64-darwin)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-x86_64-linux-gnu)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-x86_64-linux-musl)
sqlite3 (>= 2.0)
GEM
remote: https://rubygems.org/
specs:
@@ -198,6 +184,16 @@ GEM
base64 (0.3.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
beamer-rails (0.1.0.beta4-arm64-darwin)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-arm64-linux-gnu)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-x86_64-darwin)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-x86_64-linux-gnu)
sqlite3 (>= 2.0)
beamer-rails (0.1.0.beta4-x86_64-linux-musl)
sqlite3 (>= 2.0)
benchmark (0.5.0)
bigdecimal (3.3.1)
bindex (0.8.1)
@@ -294,7 +290,7 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.18.beta)
lexxy (0.1.20.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
@@ -537,7 +533,7 @@ GEM
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.1.0)
uri (1.1.1)
useragent (0.16.11)
vcr (6.3.1)
base64
@@ -604,7 +600,7 @@ DEPENDENCIES
autotuner
aws-sdk-s3
bcrypt (~> 3.1.7)
beamer-rails (~> 0.1.0.beta4)!
beamer-rails (~> 0.1.0.beta4)
benchmark
bootsnap
brakeman
+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="m10.0001 13.0002c.5523 0 1 .4477 1 1v6c0 .5522-.4478.9999-1 1-.55224 0-.99994-.4478-.99999-1v-3.586l-5.29297 5.293c-.3905.3905-1.02353.3904-1.41406 0-.39053-.3905-.39053-1.0236 0-1.4141l5.29297-5.2929h-3.58594c-.55225 0-.99995-.4478-1-1 0-.5523.44771-1 1-1z"/><path d="m20.2931 2.29313c.3905-.39051 1.0235-.39052 1.414 0s.3905 1.02356 0 1.41406l-5.2929 5.29297h3.5859c.5523.00004 1 .44774 1 1.00004 0 .5522-.4478.9999-1 1h-6c-.5522 0-.9999-.4478-1-1v-6.00004c0-.55229.4477-1 1-1 .5523.00004 1 .44774 1 1v3.58593z"/></g></svg>

After

Width:  |  Height:  |  Size: 644 B

+1
View File
@@ -0,0 +1 @@
<svg height="30" viewBox="0 0 30 30" width="30" xmlns="http://www.w3.org/2000/svg"><path d="m3 1c1.10457 0 2 .89543 2 2v17c0 .9763-.69986 1.7872-1.625 1.9629v6.6621c0 .2071-.16789.375-.375.375s-.375-.1679-.375-.375v-6.6621c-.92514-.1757-1.625-.9866-1.625-1.9629v-17c0-1.10457.89543-2 2-2zm6 0c1.1046 0 2 .89543 2 2v24c0 1.1046-.8954 2-2 2-1.10457 0-2-.8954-2-2v-24c0-1.10457.89543-2 2-2zm6 0c1.1046 0 2 .89543 2 2v20.5c0 .9763-.6999 1.7872-1.625 1.9629v3.1621c0 .2071-.1679.375-.375.375s-.375-.1679-.375-.375v-3.1621c-.9251-.1757-1.625-.9866-1.625-1.9629v-20.5c0-1.10457.8954-2 2-2zm6 0c1.1046 0 2 .89543 2 2v13.5c0 .9763-.6999 1.7872-1.625 1.9629v10.1621c0 .2071-.1679.375-.375.375s-.375-.1679-.375-.375v-10.1621c-.9251-.1757-1.625-.9866-1.625-1.9629v-13.5c0-1.10457.8954-2 2-2zm6 0c1.1046 0 2 .89543 2 2v17c0 .9763-.6999 1.7872-1.625 1.9629v6.6621c0 .2071-.1679.375-.375.375s-.375-.1679-.375-.375v-6.6621c-.9251-.1757-1.625-.9866-1.625-1.9629v-17c0-1.10457.8954-2 2-2z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1000 B

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill-rule="evenodd"><path d="m8 13c1.65685 0 3 1.3431 3 3v3c0 1.6569-1.34315 3-3 3h-4c-1.60511 0-2.91579-1.2606-2.99609-2.8457l-.00391-.1543v-3c0-1.6569 1.34315-3 3-3zm-4 2c-.55228 0-1 .4477-1 1v3l.00488.1025c.05133.5042.47744.8975.99512.8975h4c.55228 0 1-.4477 1-1v-3c0-.5523-.44772-1-1-1z"/><path d="m20 13c1.6569 0 3 1.3431 3 3v3c0 1.6569-1.3431 3-3 3h-4c-1.6051 0-2.9158-1.2606-2.9961-2.8457l-.0039-.1543v-3c0-1.6569 1.3431-3 3-3zm-4 2c-.5523 0-1 .4477-1 1v3l.0049.1025c.0513.5042.4774.8975.9951.8975h4c.5523 0 1-.4477 1-1v-3c0-.5523-.4477-1-1-1z"/><path d="m8 2c1.65685 0 3 1.34315 3 3v3c0 1.65685-1.34315 3-3 3h-4c-1.60511 0-2.91579-1.26055-2.99609-2.8457l-.00391-.1543v-3c0-1.65685 1.34315-3 3-3zm-4 2c-.55228 0-1 .44772-1 1v3l.00488.10254c.05133.50413.47744.89746.99512.89746h4c.55228 0 1-.44772 1-1v-3c0-.55228-.44772-1-1-1z"/><path d="m20 2c1.6569 0 3 1.34315 3 3v3c0 1.65685-1.3431 3-3 3h-4c-1.6051 0-2.9158-1.26055-2.9961-2.8457l-.0039-.1543v-3c0-1.65685 1.3431-3 3-3zm-4 2c-.5523 0-1 .44772-1 1v3l.0049.10254c.0513.50413.4774.89746.9951.89746h4c.5523 0 1-.44772 1-1v-3c0-.55228-.4477-1-1-1z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
<svg fill="none" height="275" viewBox="0 0 202 275" width="202" xmlns="http://www.w3.org/2000/svg"><path d="m201 258c0 11.159-44.772 16-100 16-55.2285 0-100-4.841-100-16s44.7715-16 100-16c55.228 0 100 4.841 100 16z" fill="#000" opacity=".08"/><path clip-rule="evenodd" d="m144.805 2.24222c2.294-1.843841 5.681-1.617368 7.696.56152 2.014 2.17904 1.917 5.51121-.174 7.57326l-.209.1953-.012.0117c-.009.0075-.022.0181-.039.0332-.035.0309-.087.078-.156.1397-.138.1235-.344.309-.614.5508-.54.484-1.336 1.1979-2.353 2.1152-2.033 1.835-4.952 4.4821-8.487 7.7256-7.073 6.4897-16.602 15.357-26.441 24.8759-2.543 2.4612-4.066 5.7749-4.269 9.2891-.383 6.654-.719 11.235-1.012 17.6885 27.89.2759 50.213 1.9831 62.831 3.1963l2.598.2558.018.002c7.816.8155 13.555 7.8838 12.741 15.4687l.001.001c-3.418 33.6092-7.351 63.0722-10.988 87.6142.005.178.001.359-.012.54-.03.417-.11.819-.228 1.203-3.37 24.477-6.469 42.647-8.652 56.28-2.231 13.914-13.656 24.673-28.256 25.45h-.011c-24.343 1.247-48.8273 1.346-73.2036.126l-2.3589-.122c-14.5946-.782-26.0187-11.539-28.2492-25.453-2.1836-13.632-5.2839-31.801-8.6543-56.276-.1189-.385-.1974-.789-.2278-1.208-.0133-.182-.0178-.363-.013-.542-3.6384-24.542-7.5712-54.004-10.9942-87.6112l.0009-.001c-.814-7.5854 4.9246-14.6542 12.7413-15.4697l.0179-.002c12.5608-1.2701 37.7453-3.3397 69.6834-3.4824.3004-6.6424.644-11.4215 1.0396-18.2793.3592-6.2258 3.0569-12.1147 7.5909-16.5019 9.93-9.6079 19.535-18.5451 26.654-25.0772 3.561-3.2672 6.504-5.93628 8.557-7.78905 1.027-.9262 1.831-1.64876 2.38-2.14063.274-.24594.485-.43526.628-.56249.07-.06313.124-.111.161-.14356.018-.01633.033-.02867.043-.03711.004-.00412.007-.00855.01-.01074l.003-.00195.001-.00098zm-43.801 180.81978c-19.4 0-36.6494.529-49.0474 1.058-5.7553.246-10.4622.491-13.8495.684 3.0876 22.131 5.9039 38.413 7.9281 51.05l.0726.422c1.6067 8.833 8.807 15.28 17.7176 15.757l2.319.12c22.4196 1.123 44.9546 1.111 67.4046.099l4.644-.225c9.05-.486 16.331-7.128 17.781-16.173 2.024-12.637 4.838-28.919 7.925-51.05-3.387-.193-8.094-.438-13.849-.684-12.398-.529-29.647-1.058-49.046-1.058zm-3.9481-99.0874c-31.2286.1604-55.8254 2.1836-68.0558 3.42-1.5771.1659-2.8714 1.698-2.7906 3.2392l.0119.1494.005.0489c3.2299 31.7109 6.9166 59.5089 10.3775 83.0379.0293-.002.0589-.003.0885-.005 3.4785-.201 8.5176-.468 14.7777-.735 11.3621-.485 26.7597-.968 44.1622-1.054-.0045-.387-.0095-.861-.015-1.42-.017-1.739-.0366-4.292-.0487-7.56-.0243-6.536-.0177-13.933.1084-25.385.1704-15.474.5585-32.713 1.3789-53.7364zm11.2131.0235c-.826 21.0519-1.215 38.3339-1.386 53.8319-.125 11.393-.133 18.735-.109 25.226.012 3.245.032 5.777.049 7.495.006.614.011 1.125.016 1.527 17.212.092 32.437.571 43.698 1.052 6.26.267 11.3.534 14.779.735.029.002.057.003.086.005 3.46-23.529 7.146-51.326 10.372-83.037l.005-.0498c.176-1.5891-1.149-3.2156-2.776-3.3886-11.793-1.1922-35.083-3.1168-64.734-3.3965z" fill="#000" fill-rule="evenodd"/><path d="m101.001 183.062c19.505 0 36.849.529 49.315 1.058 5.786.246 10.519.491 13.924.684-3.103 22.131-5.933 38.413-7.968 51.05-1.458 9.045-8.779 15.687-17.879 16.173l-4.669.225c-22.573 1.012-45.231 1.024-67.7731-.099l-2.3317-.12c-8.9591-.477-16.199-6.924-17.8145-15.757l-.073-.422c-2.0352-12.637-4.867-28.919-7.9714-51.05 3.4057-.193 8.1386-.438 13.9252-.684 12.4657-.529 29.8098-1.058 49.3155-1.058z" fill="#68abe9"/><path d="m56.6279 193.649c2.9632-.313 5.6326 1.728 6.0947 4.602l.0376.281.2601 2.344c1.2836 11.298 2.3362 16.481 4.1927 23.957.6454 2.598.9475 4.032 1.2977 5.222.0161.055.0326.106.0475.155.8538.102 2.113.162 4.2422.244 11.6052.444 23.1581.591 34.6066.001 3.073-.159 5.693 2.172 5.854 5.206.16 3.033-2.2 5.621-5.272 5.779-11.9181.615-23.8417.456-35.6199.005-2.1972-.084-4.2851-.159-5.9622-.427-1.7844-.284-3.9749-.903-5.7881-2.707-1.6614-1.653-2.3709-3.704-2.807-5.186-.4338-1.474-.8976-3.572-1.4193-5.673-1.9779-7.965-3.1089-13.606-4.441-25.321l-.269-2.427-.0238-.282c-.1652-2.905 2.0059-5.459 4.9692-5.773z" fill="#fff" opacity=".15"/></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

+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="m3 15c.55228 0 1 .4477 1 1v3c0 .5523.44772 1 1 1h3c.55228 0 1 .4477 1 1s-.44772 1-1 1h-3c-1.65685 0-3-1.3431-3-3v-3c0-.5523.44772-1 1-1z"/><path d="m21 15c.5523 0 1 .4477 1 1v3c0 1.6569-1.3431 3-3 3h-3c-.5523 0-1-.4477-1-1s.4477-1 1-1h3c.5523 0 1-.4477 1-1v-3c0-.5523.4477-1 1-1z"/><path d="m8 2c.55228 0 1 .44772 1 1s-.44772 1-1 1h-3c-.55228 0-1 .44772-1 1v3c0 .55228-.44772 1-1 1s-1-.44772-1-1v-3c0-1.65685 1.34315-3 3-3z"/><path d="m19 2c1.6569 0 3 1.34315 3 3v3c0 .55228-.4477 1-1 1s-1-.44772-1-1v-3c0-.55228-.4477-1-1-1h-3c-.5523 0-1-.44772-1-1s.4477-1 1-1z"/></g></svg>

After

Width:  |  Height:  |  Size: 694 B

+1 -1
View File
@@ -89,7 +89,7 @@
--lch-ink-darkest: 26% 0.05 264;
--lch-ink-darker: 40% 0.026 262;
--lch-ink-dark: 57% 0.014 260;
--lch-ink-dark: 56% 0.014 260;
--lch-ink-medium: 66% 0.008 258;
--lch-ink-light: 84% 0.005 256;
--lch-ink-lighter: 92% 0.003 254;
+1 -1
View File
@@ -40,7 +40,7 @@
pointer-events: none;
}
[aria-busy] &:disabled {
form[aria-busy] &:disabled {
position: relative;
> * {
+159 -90
View File
@@ -3,6 +3,10 @@
/* ------------------------------------------------------------------------ */
#main:has(.card-columns) {
--main-padding: 0;
}
.card-columns {
--bubble-size: 3.5rem;
--cards-gap: min(1.2cqi, 1.7rem);
--column-gap: 8px;
@@ -14,10 +18,7 @@
--progress-max-cards: 20;
--progress-max-height: 50dvh;
padding-inline: var(--column-gap);
}
.card-columns {
-ms-overflow-style: none; /* Hide x-scrollbar on Edge */
container-type: inline-size;
display: grid;
gap: var(--column-gap);
@@ -26,7 +27,14 @@
max-inline-size: var(--main-width);
overflow-x: auto;
overflow-y: hidden;
padding-block-end: var(--column-width-collapsed);
position: relative;
scrollbar-width: none; /* Hide x-scrollbar on FF */
/* Hide x-scrollbar on Chrome/Safari/Opera */
&::-webkit-scrollbar {
display: none;
}
/* When it has something expanded */
&:has(.card-columns__left .cards:not(.is-collapsed), .card-columns__right .cards:not(.is-collapsed)) {
@@ -51,10 +59,12 @@
.card-columns__left {
justify-content: end;
padding-inline-start: var(--column-gap);
}
.card-columns__right {
justify-content: start;
padding-inline-end: var(--column-gap);
}
/* Column
@@ -63,20 +73,9 @@
.cards {
--column-color: color-mix(in srgb, var(--card-color) 15%, var(--color-canvas));
border-radius: calc(var(--column-width-collapsed) / 2);
inline-size: var(--column-width-expanded);
position: relative;
scroll-snap-align: start;
translate: 0;
transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle);
.no-transitions & {
transition: none;
}
&:not(.is-collapsed) {
padding-inline: var(--column-padding-expanded);
}
&.is-collapsed {
inline-size: var(--column-width-collapsed);
@@ -85,12 +84,41 @@
.card {
display: none;
}
}
&:not(.cards--considering) {
translate: 0 var(--column-width-collapsed);
&.drag-and-drop__hover-container {
--dnd-bg-color: transparent;
--dnd-border-color: transparent;
&.is-off-screen {
&:after {
content: attr(data-column-name);
font-size: var(--text-x-small);
font-weight: 500;
line-height: var(--column-width-collapsed);
padding-inline: 1ch;
position: fixed;
text-transform: uppercase;
top: 0;
translate: -50%;
}
&.is-collapsed {
&:after {
writing-mode: vertical-rl;
}
}
&:not(.is-collapsed) {
&:after {
background-color: var(--column-color);
inline-size: calc(var(--column-width-expanded) - 4px); /* make room for the dnd border */
}
}
}
}
@media (any-hover: hover) {
.card:has(.card__background img:not([src=""])):hover .card__background img:not([src=""]) {
filter: blur(3px) brightness(1.2);
@@ -99,6 +127,36 @@
}
}
.cards__transition-container {
block-size: 100%;
border-radius: calc(var(--column-width-collapsed) / 2);
translate: 0;
transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle);
.is-collapsed:not(.cards--considering) & {
translate: 0 var(--column-width-collapsed);
}
.cards:not(.is-collapsed) & {
padding-inline: var(--column-padding-expanded);
}
.drag-and-drop__hover-container & {
--dnd-bg-color: var(--column-color);
--dnd-border-color: var(--card-color);
background-color: var(--dnd-bg-color);
outline: 2px dashed var(--dnd-border-color);
outline-offset: -2px;
transition: background-color 200ms;
z-index: 1;
}
.no-transitions & {
transition: none;
}
}
/* The wrapper around the cards used to clip overflow while transitioning.
* Also, don't resize cards while transitioning to avoid reflow. */
.cards__list {
@@ -171,6 +229,46 @@
/* Column Elements
/* ------------------------------------------------------------------------ */
.cards__header {
.cards.is-collapsed & {
block-size: 100%;
}
.cards:not(.is-collapsed) & {
display: grid;
grid-template-areas: "menu expander maximize";
grid-template-columns: var(--column-width-collapsed) 1fr var(--column-width-collapsed);
margin-block-end: calc(0.5 * var(--cards-gap));
}
}
.cards__menu .btn--circle,
.cards__maximize-button {
--btn-background: transparent;
block-size: var(--column-width-collapsed);
inline-size: var(--column-width-collapsed);
opacity: 0;
outline-offset: -2px;
.cards:hover &,
&:focus-visible {
opacity: 1;
}
.cards.is-collapsed & {
display: none;
}
}
.cards__menu {
z-index: var(--z-popup);
}
.cards__maximize-button {
grid-area: maximize;
}
.cards__expander {
align-items: center;
border-radius: 99rem;
@@ -180,6 +278,8 @@
font-size: var(--text-x-small);
font-weight: 600;
gap: 0.5ch;
grid-area: expander;
outline-offset: -2px;
position: relative;
text-transform: uppercase;
@@ -239,7 +339,6 @@
.cards:not(.is-collapsed) & {
inline-size: 100%;
margin-block-end: calc(0.5 * var(--cards-gap));
justify-content: center;
}
@@ -247,39 +346,6 @@
cursor: unset;
filter: none;
}
@media (any-hover: hover) {
.btn--circle,
~ .btn--circle,
~ .cards__expander-menu .btn--circle:not(label) {
transition: opacity 300ms ease-in-out;
opacity: 0;
&:focus-visible,
.cards:hover & {
opacity: 1;
}
}
}
}
.btn.cards__expander-button {
--btn-background: transparent;
block-size: var(--column-width-collapsed);
inline-size: var(--column-width-collapsed);
inset: 0 0 auto auto;
position: absolute;
translate: calc(var(--column-gap) * -2);
z-index: var(--z-popup);
.cards--considering & {
translate: none;
}
.cards.is-collapsed & {
display: none;
}
}
.cards__expander-count {
@@ -305,25 +371,28 @@
}
.cards:not(.is-collapsed, .cards--considering) & {
align-items: center;
display: flex;
gap: 0.25ch;
max-inline-size: calc(100% - var(--column-width-collapsed) * 2);
}
}
.cards__expander-menu {
inset: 0 auto auto 0;
position: absolute;
translate: calc(var(--column-gap) * 2);
z-index: var(--z-popup);
.icon--collapse {
--icon-size: 1.15em;
.btn--circle {
--btn-background: transparent;
opacity: 0;
scale: 1.5;
transition: 150ms ease-out;
transition-property: opacity, scale;
block-size: var(--column-width-collapsed);
inline-size: var(--column-width-collapsed);
}
.cards.is-collapsed & {
display: none;
}
.cards.is-collapsed & {
display: none;
.cards:not(.is-collapsed) .cards__expander:hover & {
opacity: 0.66;
scale: 1;
}
}
}
@@ -331,6 +400,10 @@
box-shadow: none !important;
color: color-mix(in srgb, var(--card-color) 70%, var(--color-canvas));
.cards--considering & {
background-color: var(--card-bg-color) !important;
}
p {
font-size: var(--text-small);
overflow: hidden;
@@ -344,10 +417,6 @@
display: initial;
}
}
.cards--grid &:not(.blank-slate-for-grid) {
display: none;
}
}
}
@@ -450,8 +519,8 @@
@media (any-hover: hover) {
.card:hover & {
opacity: 1;
}
opacity: 1;
}
}
}
}
@@ -473,11 +542,9 @@
/* ------------------------------------------------------------------------ */
.cards--considering {
position: relative;
--card-color: oklch(var(--lch-blue-medium));
@media (min-width: 640px) {
padding-inline: var(--cards-gap);
}
position: relative;
.card {
--avatar-size: 2.75em;
@@ -604,24 +671,26 @@
/* -------------------------------------------------------------------------- */
/* Surface a mini bubble if there are cards with bubbles inside */
.cards--considering:has(.bubble:not([hidden])) .cards__expander-title,
.cards--considering:has(.bubble:not([hidden])),
.cards--doing.is-collapsed:has(.bubble:not([hidden])) {
--bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
.cards__transition-container {
--bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
&:before {
background: radial-gradient(
color-mix(in srgb, var(--bubble-color) 8%, var(--color-canvas)) 50%,
color-mix(in srgb, var(--bubble-color) 48%, var(--color-canvas)) 100%
);
block-size: 1em;
border-radius: var(--bubble-shape);
content: "";
inline-size: 1em;
inset: 0 0 auto auto;
position: absolute;
translate: 25% -25%;
z-index: 1;
&:before {
background: radial-gradient(
color-mix(in srgb, var(--bubble-color) 8%, var(--color-canvas)) 50%,
color-mix(in srgb, var(--bubble-color) 48%, var(--color-canvas)) 100%
);
block-size: 1em;
border-radius: var(--bubble-shape);
content: "";
inline-size: 1em;
inset: 0 0 auto auto;
position: absolute;
translate: 25% -25%;
z-index: 1;
}
}
}
+3 -2
View File
@@ -240,8 +240,9 @@
translate: 0 calc(-1 * var(--half-btn-height));
}
&:has([open]) {
z-index: 1;
form {
background-color: var(--color-canvas);
border-radius: 99rem;
}
.btn:not(.popup__btn, .btn--plain, .btn--reversed) {
+29 -7
View File
@@ -179,17 +179,20 @@
display: none;
}
& ~ .btn {
--btn-background: var(--card-color);
--btn-color: var(--color-ink-inverted);
}
}
.card-field__description {
lexxy-toolbar {
border-block-start: 1px solid var(--color-ink-light);
margin-block-start: var(--block-space-half) !important;
}
& ~ .btn.btn--reversed {
--btn-background: var(--card-color);
--btn-color: var(--color-ink-inverted);
}
& ~ .btn {
--btn-border-color: var(--card-color);
--btn-color: var(--card-color);
}
}
.card__stages {
@@ -253,6 +256,21 @@
}
}
.card__assignees-trigger {
background: transparent;
border: none;
padding: 0;
display: flex;
&:focus-visible {
outline: none;
.btn {
box-shadow: 0 0 0 var(--focus-ring-size) var(--focus-ring-color);
}
}
}
.card__meta-text {
line-height: 1;
white-space: nowrap;
@@ -498,5 +516,9 @@
margin-inline: auto;
text-align: start;
}
code {
text-align: left;
}
}
}
+5 -2
View File
@@ -7,8 +7,11 @@
}
.drag-and-drop__hover-container {
background-color: var(--color-selected-light);
outline: 2px dashed var(--color-selected-dark);
--dnd-bg-color: var(--color-selected-light);
--dnd-border-color: var(--color-selected-dark);
background-color: var(--dnd-bg-color);
outline: 2px dashed var(--dnd-border-color);
outline-offset: -2px;
transition: background-color 200ms;
z-index: 1;
+14 -1
View File
@@ -13,7 +13,8 @@
"menu menu menu"
"actions-start title actions-end";
max-inline-size: 100dvw;
padding: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--main-padding);
padding-block: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--block-space-half);
padding-inline: var(--main-padding);
/* Change the grid size depending on how many buttons are present */
&:has(.header__actions > *:nth-child(1)) { --header-button-count: 1; }
@@ -78,4 +79,16 @@
--left-offset: var(--inline-space);
}
}
.header__logo {
color: var(--color-ink);
inline-size: auto;
margin-block-start: 0.1em;
img {
block-size: auto;
inline-size: 1.15em;
margin-inline-end: 0.8ch;
}
}
}
+4
View File
@@ -42,6 +42,7 @@
.icon--clipboard { --svg: url("clipboard.svg "); }
.icon--close { --svg: url("close.svg "); }
.icon--close-circle { --svg: url("close-circle.svg "); }
.icon--collapse { --svg: url("collapse.svg "); }
.icon--board { --svg: url("board.svg "); }
.icon--board-add { --svg: url("board-add.svg "); }
.icon--column-left { --svg: url("column-left.svg "); }
@@ -53,7 +54,9 @@
.icon--everyone { --svg: url("everyone.svg "); }
.icon--expand { --svg: url("expand.svg "); }
.icon--gear { --svg: url("gear.svg "); }
.icon--grid { --svg: url("grid.svg "); }
.icon--filter { --svg: url("filter.svg "); }
.icon--fizzy { --svg: url("fizzy.svg"); }
.icon--globe { --svg: url("globe.svg "); }
.icon--golden-ticket { --svg: url("golden-ticket.svg "); }
.icon--home { --svg: url("home.svg "); }
@@ -64,6 +67,7 @@
.icon--lock { --svg: url("lock.svg "); }
.icon--logout { --svg: url("logout.svg "); }
.icon--marker { --svg: url("marker.svg "); }
.icon--maximize { --svg: url("maximize.svg "); }
.icon--menu { --svg: url("menu.svg "); }
.icon--menu-dots-horizontal { --svg: url("menu-dots-horizontal.svg "); }
.icon--menu-dots-vertical { --svg: url("menu-dots-vertical.svg "); }
@@ -1,17 +1,8 @@
@layer components {
.boxcar-header-logo {
color: var(--color-ink);
inline-size: auto;
margin-block-start: 0.1em;
/* Trigger
/* ------------------------------------------------------------------------ */
img {
block-size: auto;
inline-size: 1.15em;
margin-inline-end: 0.8ch;
}
}
.input:is(.boxcar-menu-trigger) {
.nav__trigger {
--input-background: var(--color-canvas);
--input-border-color: transparent;
--input-padding: 0.3em 2em 0.3em 0.75em;
@@ -58,37 +49,45 @@
}
}
.boxcar-menu:is(.panel) {
/* Dialog
/* ------------------------------------------------------------------------ */
.nav__menu.popup {
--panel-border-color: var(--color-selected-dark);
--panel-border-radius: 1.4em;
--panel-padding: var(--block-space) var(--block-space) 0 var(--block-space);
--panel-size: 45ch;
--popup-display: grid;
--nav-section-gap: 2px;
align-items: stretch;
block-size: auto;
box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%),
0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%),
0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%),
0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%);
flex-direction: column;
gap: 2px;
grid-template-rows: auto 1fr auto;
gap: var(--nav-section-gap);
overflow: hidden;
scrollbar-gutter: stable both-edges;
/* Safari doesn't position the sticky footer properly during the
* transition, so we need to delay the footer entrance. */
.popup__footer {
animation: safari-sticky-footer-hack 0s;
animation-delay: var(--dialog-duration);
animation-fill-mode: both;
inset-inline: var(--block-space);
}
}
.boxcar-menu__close {
.nav__scroll-container {
display: flex;
flex-direction: column;
gap: var(--nav-section-gap);
margin-inline: calc(-1 * var(--block-space)); /* space for scrollbar */
overflow: auto;
padding-block-end: var(--nav-section-gap);
padding-inline: var(--block-space);
}
.nav__close {
@media (any-hover: hover) {
display: none;
}
}
.boxcar-menu__hotkeys {
.nav__hotkeys {
--gap: 8px;
align-items: center;
@@ -152,4 +151,15 @@
}
}
}
.nav__footer {
background-color: var(--color-canvas);
border-block-start: 1px solid var(--color-ink-lighter);
font-size: var(--text-small);
line-height: 1.6;
margin-block-start: calc(-1 * var(--nav-section-gap));
padding: 1.5ch;
text-align: center;
z-index: 1;
}
}
+12 -2
View File
@@ -6,12 +6,22 @@
color: var(--color-ink);
inline-size: var(--panel-size, 60ch);
max-inline-size: 100%;
padding: var(--panel-padding, calc(var(--block-space) * 2));
padding: var(--panel-padding, var(--block-space));
@media (min-width: 640px) {
--panel-size: 100%;
padding: var(--panel-padding, var(--block-space-double));
}
}
.panel--centered {
--panel-border-size: 0;
--panel-size: 42ch;
--panel-size: 100%;
@media (min-width: 640px) {
--panel-size: 42ch;
}
#main:has(&) {
display: grid;
+39 -51
View File
@@ -6,6 +6,7 @@
--panel-size: auto;
--popup-icon-size: 24px;
--popup-item-padding-inline: 0.5rem;
--popup-display: flex;
inset: 0 auto auto 50%;
min-inline-size: min(25ch, calc(100dvw - var(--block-space-double)));
@@ -14,39 +15,9 @@
position: absolute;
transform: translateX(-50%);
z-index: var(--z-popup);
opacity: 0;
transform: scale(0.2) translateX(-50%);
transform-origin: top left;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay, transform;
&::backdrop {
background-color: var(--color-always-black);
opacity: 0;
transform: scale(1);
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
display: flex;
opacity: 1;
transform: scale(1) translateX(-50%);
&::backdrop {
opacity: 0.5;
}
}
@starting-style {
&[open] {
opacity: 0;
transform: scale(0.2) translateX(-50%);
}
&[open]::backdrop {
opacity: 0;
}
display: var(--popup-display);
}
&.orient-left {
@@ -59,10 +30,6 @@
transform: translateX(0);
}
&:has(.popup__footer) {
--panel-padding: var(--block-space) var(--block-space) 0 var(--block-space);
}
form {
display: contents;
}
@@ -86,11 +53,11 @@
background: var(--color-canvas);
font-size: var(--text-small);
font-weight: bold;
inset-block-start: 0;
list-style: none;
padding: var(--block-space-half) var(--inline-space-half);
position: sticky;
text-transform: uppercase;
top: calc(-1 * var(--block-space));
z-index: 1;
&:is(summary) {
@@ -196,21 +163,42 @@
}
}
.popup__footer {
background-color: var(--color-canvas);
border-block-start: 1px solid var(--color-ink-lighter);
font-size: var(--text-small);
inset: auto 0 0;
line-height: 1.6;
margin-block-start: var(--block-space-half);
padding: 1.5ch;
position: sticky;
text-align: center;
z-index: 1;
}
/* Animated
/* -------------------------------------------------------------------------- */
@keyframes safari-sticky-footer-hack {
0% { position: absolute; }
100% { position: sticky; }
.popup--animated {
opacity: 0;
transform: scale(0.2) translateX(-50%);
transform-origin: top left;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay, transform;
&::backdrop {
background-color: var(--color-always-black);
opacity: 0;
transform: scale(1);
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
opacity: 1;
transform: scale(1) translateX(-50%);
&::backdrop {
opacity: 0.5;
}
}
@starting-style {
&[open] {
opacity: 0;
transform: scale(0.2) translateX(-50%);
}
&[open]::backdrop {
opacity: 0;
}
}
}
}
+2 -2
View File
@@ -35,8 +35,8 @@
print-color-adjust: exact;
}
.boxcar-menu,
.boxcar-menu-trigger,
.nav__menu,
.nav__trigger,
.header__actions {
display: none;
}
+14
View File
@@ -29,6 +29,7 @@
}
.reactions__trigger {
--btn-border-color: var(--color-ink-lightest);
background-color: var(--color-ink-lightest);
&:not(:hover) {
@@ -38,10 +39,23 @@
}
}
.reactions__trigger {
--btn-border-color: var(--reaction-border-color);
img {
block-size: 65%;
inline-size: 65%;
}
}
.reactions__list {
display: inline-flex;
flex-wrap: wrap;
gap: var(--inline-space-half);
&:not(:has(.reaction)) {
display: none;
}
}
/* Single reaction
+14 -5
View File
@@ -1,13 +1,13 @@
@layer components {
.rich-text-content {
--block-margin: 0;
--block-margin: 0.5lh;
:where(h1, h2, h3, h4, h5, h6) {
display: block;
font-weight: 800;
letter-spacing: -0.02ch;
line-height: 1.1;
margin-block: var(--block-margin);
margin-block: 0 var(--block-margin);
overflow-wrap: break-word;
text-wrap: balance;
}
@@ -62,14 +62,17 @@
}
:where(hr) {
color: currentColor;
block-size: 0;
border-block-end: 1px solid currentColor;
border: none;
border-block-end: 2px solid currentColor;
inline-size: 20%;
margin: var(--block-margin) 0;
margin: calc(var(--block-margin) * 2) 0;
}
.horizontal-divider {
margin-inline: 0;
margin: var(--block-margin) 0;
padding-block: var(--block-margin);
}
.lexxy-content__strikethrough {
@@ -372,6 +375,12 @@
}
}
action-text-attachment[content-type^='application/vnd.actiontext.mention'] {
img {
object-fit: cover;
}
}
[data-lexical-cursor] {
animation: blink 1s step-end infinite;
block-size: 1lh;
+1 -1
View File
@@ -1,5 +1,5 @@
@layer components {
[data-controller^="tooltip"] {
[data-controller~="tooltip"] {
--tooltip-delay: 750ms;
--tooltip-duration: 150ms;
+1 -1
View File
@@ -16,7 +16,7 @@
.txt-ink { color: var(--color-ink); }
.txt-reversed { color: var(--color-ink-inverted); }
.txt-negative { color: var(--color-negative); }
.txt-subtle { color: var(--color-ink-medium); }
.txt-subtle { color: var(--color-ink-dark); }
.txt-alert { color: var(--color-marker); }
.txt-undecorated { text-decoration: none; }
.txt-underline { text-decoration: underline; }
@@ -1,4 +1,6 @@
class Account::EntropiesController < ApplicationController
before_action :ensure_admin
def update
Account.sole.entropy.update!(entropy_params)
redirect_to account_settings_path, notice: "Account updated"
@@ -1,5 +1,6 @@
class Account::JoinCodesController < ApplicationController
before_action :set_join_code
before_action :ensure_admin, only: %i[ update destroy ]
def show
end
@@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base
include Authorization
include CurrentRequest, CurrentTimezone, SetPlatform
include TurboFlash, ViewTransitions
include Saas
stale_when_importmap_changes
allow_browser versions: :modern
@@ -1,6 +1,8 @@
class Boards::EntropiesController < ApplicationController
include BoardScoped
before_action :ensure_permission_to_admin_board
def update
@board.entropy.update!(entropy_params)
end
@@ -1,6 +1,8 @@
class Boards::PublicationsController < ApplicationController
include BoardScoped
before_action :ensure_permission_to_admin_board
def create
@board.publish
end
+3 -11
View File
@@ -2,7 +2,7 @@ class BoardsController < ApplicationController
include FilterScoped
before_action :set_board, except: %i[ new create ]
before_action :ensure_permission_to_change_accesses, only: %i[ update ]
before_action :ensure_permission_to_admin_board, only: %i[ update ]
def show
if @filter.used?(ignore_boards: true)
@@ -48,20 +48,12 @@ class BoardsController < ApplicationController
@board = Current.user.boards.find params[:id]
end
def ensure_permission_to_change_accesses
if trying_to_change_accesses? && !Current.user.can_administer_board?(@board)
def ensure_permission_to_admin_board
unless Current.user.can_administer_board?(@board)
head :forbidden
end
end
def trying_to_change_accesses?
all_access_changed? || grantees_changed?
end
def all_access_changed?
params[:board]&.key?(:all_access)
end
def grantees_changed?
params.key?(:user_ids)
end
@@ -3,7 +3,7 @@ class Cards::AssignmentsController < ApplicationController
def new
@users = @board.users.active.alphabetically
fresh_when @users
fresh_when etag: [ @users, @card.assignees ]
end
def create
+1 -1
View File
@@ -1,7 +1,7 @@
class Cards::ColumnsController < ApplicationController
def edit
@card = Current.user.accessible_cards.find(params[:card_id])
@columns = @card.board.columns
@columns = @card.board.columns.sorted
fresh_when etag: [ @card, @columns ]
end
+1 -1
View File
@@ -3,7 +3,7 @@ class Cards::TaggingsController < ApplicationController
def new
@tags = Tag.all.alphabetically
fresh_when @tags
fresh_when etag: [ @tags, @card.tags ]
end
def create
+6
View File
@@ -9,4 +9,10 @@ module BoardScoped
def set_board
@board = Current.user.boards.find(params[:board_id])
end
def ensure_permission_to_admin_board
unless Current.user.can_administer_board?(@board)
head :forbidden
end
end
end
+11
View File
@@ -0,0 +1,11 @@
module Saas
extend ActiveSupport::Concern
included do
helper_method :signups_allowed?
end
def signups_allowed?
defined?(Signup) && defined?(saas)
end
end
+4 -1
View File
@@ -4,6 +4,8 @@ class JoinCodesController < ApplicationController
before_action :set_join_code
before_action :ensure_join_code_is_valid
layout "public"
def new
@account_name = ApplicationRecord.with_tenant(tenant) { Account.sole.name }
end
@@ -14,7 +16,8 @@ class JoinCodesController < ApplicationController
identity.memberships.find_or_create_by!(tenant: tenant) do |membership|
membership.join_code = code
end
identity.send_magic_link
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
end
session[:return_to_after_authenticating] = landing_url(script_name: "/#{tenant}")
@@ -1,6 +1,8 @@
class Memberships::EmailAddressesController < ApplicationController
require_untenanted_access
layout "public"
before_action :set_membership
rate_limit to: 5, within: 1.hour, only: :create
+6 -3
View File
@@ -1,7 +1,10 @@
class My::MenusController < ApplicationController
include FilterScoped
def show
fresh_when etag: [ @user_filtering, Current.session ]
@filters = Current.user.filters.all
@boards = Current.user.boards.ordered_by_recently_accessed
@tags = Tag.all.alphabetically
@users = User.active.alphabetically
# fresh_when etag: [ @user_filtering, Current.session ]
end
end
+14 -2
View File
@@ -1,4 +1,9 @@
class SessionsController < ApplicationController
# FIXME: Remove this before launch!
SIGNUP_USERNAME = Rails.env.local? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name
SIGNUP_PASSWORD = Rails.env.local? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password
http_basic_authenticate_with name: SIGNUP_USERNAME, password: SIGNUP_PASSWORD, realm: "Fizzy Signup", only: :create, unless: -> { Identity.exists?(email_address: email_address) }
require_untenanted_access
require_unauthenticated_access except: :destroy
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
@@ -9,8 +14,15 @@ class SessionsController < ApplicationController
end
def create
Identity.find_by_email_address(email_address)&.send_magic_link
redirect_to session_magic_link_path
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
elsif signups_allowed?
Signup.new(email_address: email_address).create_identity
session[:return_to_after_authenticating] = saas.new_signup_membership_path
redirect_to session_magic_link_path
end
end
def destroy
@@ -3,6 +3,8 @@ class Users::JoinsController < ApplicationController
before_action :set_join_code, :ensure_join_code_is_valid
layout "public"
def new
end
+4 -1
View File
@@ -1,6 +1,9 @@
module ApplicationHelper
def page_title_tag
tag.title @page_title || "Fizzy"
account_name = if ApplicationRecord.current_tenant && Current.session&.identity&.memberships&.many?
Account.sole&.name
end
tag.title [ @page_title, account_name, "Fizzy" ].compact.join(" | ")
end
def icon_tag(name, **options)
+10
View File
@@ -19,6 +19,16 @@ module AvatarsHelper
end
end
def mail_avatar_tag(user, size: 48, **options)
if user.avatar.attached?
image_tag user_avatar_url(user), alt: user.name, class: "avatar", size: size, **options
else
tag.span class: "avatar", style: "background-color: #{avatar_background_color(user)};" do
user.initials
end
end
end
def avatar_preview_tag(user, hidden_for_screen_reader: false, **options)
tag.span class: class_names("avatar", options.delete(:class)),
aria: { hidden: hidden_for_screen_reader, label: user.name },
+2 -2
View File
@@ -1,8 +1,8 @@
module BoardsHelper
def link_back_to_board(board)
link_to board, class: "btn borderless txt-medium",
data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click click->turbo-navigation#backIfSamePath" } do
tag.span ("&larr;" + tag.strong(board.name, class: "overflow-ellipsis")).html_safe
data: { controller: "hotkey", action: "keydown.left@document->hotkey#click click->turbo-navigation#backIfSamePath" } do
tag.span(tag.strong("Back to #{board.name}", class: "overflow-ellipsis") + tag.kbd("", class: "txt-x-small margin-inline-start hide-on-touch"), class: "flex align-center").html_safe
end
end
+1 -1
View File
@@ -1,7 +1,7 @@
module ClipboardHelper
def button_to_copy_to_clipboard(url, &)
tag.button class: "btn", data: {
controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
controller: "copy-to-clipboard tooltip", action: "copy-to-clipboard#copy",
copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url
}, &
end
+16 -132
View File
@@ -1,138 +1,22 @@
module EventsHelper
def event_columns(event_type, day_timeline)
case event_type
when "added"
events = day_timeline.events.where(action: [ "card_published", "card_reopened" ])
full_events_count = events.count
{
title: event_column_title("Added", full_events_count, day_timeline.day),
index: 1,
events: events.limit(100).load,
full_events_count: full_events_count
}
when "closed"
events = day_timeline.events.where(action: "card_closed")
full_events_count = events.count
{
title: event_column_title("Closed", full_events_count, day_timeline.day),
index: 3,
events: events.limit(100).load,
full_events_count: full_events_count
}
def event_action_icon(event)
case event.action
when "card_assigned"
"assigned"
when "card_unassigned"
"minus"
when "comment_created"
"comment"
when "card_title_changed"
"rename"
when "card_board_changed"
"move"
else
events = day_timeline.events.where.not(action: [ "card_published", "card_closed", "card_reopened" ])
full_events_count = events.count
{
title: event_column_title("Updated", full_events_count, day_timeline.day),
index: 2,
events: events.limit(100).load,
full_events_count: full_events_count
}
"person"
end
end
private
def event_column_title(base_title, count, day)
date_tag = local_datetime_tag day, style: :agoorweekday
parts = [ base_title, date_tag ]
parts << tag.span("(#{count})", class: "font-weight-normal") if count > 0
safe_join(parts, " ")
end
def event_column(event)
case event.action
when "card_closed"
3
when "card_published", "card_reopened"
1
else
2
end
end
def event_cluster_tag(hour, col, &)
row = 25 - hour
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
end
def event_action_sentence(event)
if event.action.comment_created?
comment_event_action_sentence(event)
else
card_event_action_sentence(event)
end
end
def comment_event_action_sentence(event)
"#{event_creator_name(event)} commented on #{event_card_title(event.eventable.card)}".html_safe
end
def event_creator_name(event)
tag.span data: { creator_id: event.creator.id } do
safe_join([
tag.span("You", data: { only_visible_to_you: true }),
tag.span(h(event.creator.name), data: { only_visible_to_others: true })
])
end
end
def card_event_action_sentence(event)
card = event.eventable
case event.action
when "card_assigned"
if event.assignees.include?(Current.user)
"#{event_creator_name(event)} will handle #{event_card_title(card)}"
else
"#{event_creator_name(event)} assigned #{h event.assignees.pluck(:name).to_sentence} to #{event_card_title(card)}"
end
when "card_unassigned"
"#{event_creator_name(event)} unassigned #{h(event.assignees.include?(Current.user) ? "yourself" : event.assignees.pluck(:name).to_sentence)} from #{event_card_title(card)}"
when "card_published"
"#{event_creator_name(event)} added #{event_card_title(card)}"
when "card_closed"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “Done”"
when "card_reopened"
"#{event_creator_name(event)} reopened #{event_card_title(card)}"
when "card_postponed"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “Not Now”"
when "card_auto_postponed"
"#{event_card_title(card)} was closed as “Not Now” due to inactivity"
when "card_resumed"
"#{event_creator_name(event)} resumed #{event_card_title(card)}"
when "card_title_changed"
"#{event_creator_name(event)} renamed #{event_card_title(card)} (was: “#{h event.particulars.dig('particulars', 'old_title')}”)"
when "card_board_changed", "card_collection_changed"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h (event.particulars.dig('particulars', 'new_board') || event.particulars.dig('particulars', 'new_collection'))}"
when "card_triaged"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'column')}"
when "card_sent_back_to_triage"
"#{event_creator_name(event)} moved #{event_card_title(card)} back to “Maybe?”"
end&.html_safe
end
def event_card_title(card)
tag.span card.title, class: "txt-underline"
end
def event_due_date(event)
event.particulars.dig("particulars", "due_date").to_date.strftime("%B %-d")
end
def event_action_icon(event)
case event.action
when "card_assigned"
"assigned"
when "card_unassigned"
"minus"
when "comment_created"
"comment"
when "card_title_changed"
"rename"
when "card_board_changed"
"move"
else
"person"
end
end
def events_at_hour_container(column, hour, &block)
tag.div class: "events__time-block", style: "grid-area: #{25 - hour}/#{column.index}", &block
end
end
+35
View File
@@ -16,4 +16,39 @@ module My::MenuHelper
navigable_list_target: "input",
action: "input->filter#filter" }
end
def my_menu_board_item(board)
my_menu_item("board", board) do
link_to(tag.span(board.name, class: "overflow-ellipsis"), board, class: "popup__btn btn")
end
end
def my_menu_tag_item(the_tag)
my_menu_item("tag", tag) do
link_to(tag.span("#{the_tag.title} (#{the_tag.cards_count})", class: "overflow-ellipsis"), cards_path(tag_ids: [ the_tag ]), class: "popup__btn btn")
end
end
def my_menu_user_item(user)
my_menu_item("person", user) do
link_to(tag.span(user.name, class: "overflow-ellipsis"), user, class: "popup__btn btn")
end
end
def my_menu_filter_item(filter)
my_menu_item("bookmark", filter) do
link_to(cards_path(filter_id: filter.id), class: "popup__btn btn") do
tag.div(class: "txt-tight-lines min-width txt-small overflow-ellipsis") do
tag.div(tag.strong(filter.boards_label)) +
tag.div(filter.summary, class: "txt-capitalize")
end
end
end
end
def my_menu_item(item, record)
tag.li(class: "popup__item", data: { filter_target: "item", navigable_list_target: "item", id: "filter-#{item}-#{record.id}" }) do
icon_tag(item, class: "popup__icon") + yield
end
end
end
+1 -1
View File
@@ -28,7 +28,7 @@ module NotificationsHelper
} do
link_to(notification,
class: [ "card card--notification", { "card--closed": notification.card.closed? }, { "unread": !notification.read? } ],
data: { turbo_frame: "_top", badge_target: "unread", action: "badge#update" },
data: { turbo_frame: "_top", badge_target: "unread", action: "badge#update dialog#close" },
style: { "--card-color:": notification.card.color },
&)
end
@@ -6,6 +6,10 @@ export default class extends Controller {
async click() {
await nextFrame()
this.clickableTarget.click()
this.#clickable.click()
}
get #clickable() {
return this.hasClickableTarget ? this.clickableTarget : this.element
}
}
@@ -2,8 +2,8 @@ import { Controller } from "@hotwired/stimulus"
import { nextFrame, debounce } from "helpers/timing_helpers";
export default class extends Controller {
static classes = [ "collapsed", "noTransitions" ]
static targets = [ "column", "button" ]
static classes = [ "collapsed", "noTransitions", "titleNotVisible" ]
static targets = [ "column", "button", "title" ]
static values = {
board: String
}
@@ -14,6 +14,14 @@ export default class extends Controller {
async connect() {
await this.#restoreColumnsDisablingTransitions()
this.#setupIntersectionObserver()
}
disconnect() {
if (this._intersectionObserver) {
this._intersectionObserver.disconnect()
this._intersectionObserver = null
}
}
toggle({ target }) {
@@ -106,4 +114,23 @@ export default class extends Controller {
#localStorageKeyFor(column) {
return `expand-${this.boardValue}-${column.getAttribute("id")}`
}
#setupIntersectionObserver() {
if (typeof IntersectionObserver === "undefined") return
if (this._intersectionObserver) this._intersectionObserver.disconnect()
this._intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
const title = entry.target
const column = title.closest(".cards")
if (!column) return
const offscreen = entry.intersectionRatio === 0
column.classList.toggle(this.titleNotVisibleClass, offscreen)
})
}, { threshold: [0] })
this.titleTargets.forEach(title => this._intersectionObserver.observe(title))
}
}
@@ -24,6 +24,7 @@ export default class extends Controller {
limitHeightToViewport(this.dialogTarget, true)
this.#loadLazyFrames()
this.dialogTarget.setAttribute("aria-hidden", "false")
this.dispatch("show")
}
@@ -54,4 +55,8 @@ export default class extends Controller {
event.stopPropagation()
}
}
#loadLazyFrames() {
Array.from(this.dialogTarget.querySelectorAll("turbo-frame")).forEach(frame => { frame.loading = "eager" })
}
}
@@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.addEventListener("dialog:show", this.handleDialogShow.bind(this))
}
disconnect() {
this.element.removeEventListener("dialog:show", this.handleDialogShow.bind(this))
}
handleDialogShow(event) {
this.#dialogControllers.forEach(dialogController => {
if (dialogController !== event.target) {
const dialog = dialogController.querySelector("dialog")
dialog.removeAttribute("open")
}
})
}
get #dialogControllers() {
return this.element.querySelectorAll('[data-controller~="dialog"]')
}
}
@@ -80,6 +80,10 @@ export default class extends Controller {
selectedItem.setAttribute(this.selectionAttributeValue, "true")
this.currentItem = selectedItem
await nextFrame()
try {
this.currentItem.scrollIntoView({ block: "nearest", inline: "nearest" })
} catch (e) {}
if (this.focusOnSelectionValue) { this.currentItem.focus() }
if (this.hasInputTarget && id) {
this.inputTarget.setAttribute("aria-activedescendant", id)
+1 -1
View File
@@ -1,5 +1,5 @@
class ApplicationMailer < ActionMailer::Base
default from: "The Fizzy team <support@37signals.com>"
default from: "Fizzy <support@fizzy.do>"
layout "mailer"
append_view_path Rails.root.join("app/views/mailers")
+1 -1
View File
@@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer
@magic_link = magic_link
@identity = @magic_link.identity
mail to: @identity.email_address, subject: "Sign in to Fizzy"
mail to: @identity.email_address, subject: "Your Fizzy verification code"
end
private
-8
View File
@@ -1,8 +0,0 @@
class UserMailer < ApplicationMailer
def email_change_confirmation(user:, email_address:, token:)
@user = user
@token = token
mail to: email_address, subject: "Confirm your new email address"
end
end
+11 -11
View File
@@ -31,49 +31,49 @@ class Account::Seeder
playground = Board.create! name: "Playground", creator: creator, all_access: true
# Cards
playground.cards.create! creator: creator, title: "Watch this: Fizzy orientation video", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML
<p>Theres a whole lot more you can do in Fizzy. In the video below 37signals founder and CEO, Jason Fried, will walk you through the basics in just 8 minutes.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/videos/fizzyorientation-4k.mp4" caption="Fizzy orientation" content-type="video/mp4" filename="fizzyorientation-4k.mp4"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Grab the invite link to invite someone else", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Now, grab the invite link to invite someone else", status: "published", description: <<~HTML
<p>Open Fizzy menu, select “<b><strong>+ Add people</b></strong>”, then copy the invite link. You can give this link to someone else so they can make an login for themselves in your account.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/invite-link.gif" alt="Demo of copying invite link" caption="Get a link to invite co-workers" content-type="image/*" filename="invite-link.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Head back home to check out activity", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Then, head back home to check out activity", status: "published", description: <<~HTML
<p>Hit “1” or pull down the Fizzy menu and select “Home”.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/back-to-home.gif" alt="Demo of visiting Home" caption="Go back to Home for Latest Activity" content-type="image/*" filename="back-to-home.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Check out all cards assigned to you", status: "published", description: <<~HTML
<p>Pull down the Fizzy menu at the top of the screen, and select “<b><strong>Assigned to me</b></strong>” or just hit “3” on your keyboard any time.</p>
playground.cards.create! creator: creator, title: "Now,check out all cards assigned to you", status: "published", description: <<~HTML
<p>Pull down the Fizzy menu at the top of the screen, and select “<b><strong>Assigned to me</b></strong>” or just hit “2” on your keyboard any time.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/all-assigned.gif" alt="Demo of navigating to 'Assigned to Me'" caption="See all cards assigned to me" content-type="image/*" filename="all-assigned.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Open the Fizzy menu", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Then, open the Fizzy menu", status: "published", description: <<~HTML
<p>The Fizzy menu is how you get around the app. Click “<b><strong>Fizzy</b></strong>” at the top of the screen or hit the “J” key on your keyboard to pop it open.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/open-menu.gif" alt="Demo of opening the main menu" caption="Open the Fizzy menu" content-type="image/*" filename="open-menu.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Assign this card to yourself", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Next, assign this card to yourself", status: "published", description: <<~HTML
<p>Click the little head with the + next to it, pick yourself.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/assign-to-self.gif" alt="Demo of assigning a card" caption="Assign this to yourself" content-type="image/*" filename="assign-to-self.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Tag this card Design then move it to YES", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Now, tag this card Design then move it to YES", status: "published", description: <<~HTML
<p>Click the little Tag icon, type “design”, then “<b><strong>Create tag</b></strong>”. Then, move the card to the new “YES” column you created in the previous step.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/tag-design.gif" alt="Demo of tagging a card" caption="Tag this #design" content-type="image/*" filename="tag-design.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Make two more columns", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Next, make two more columns", status: "published", description: <<~HTML
<ol>
<li>Make one called "Yes"</li>
<li>Make another called "Working on"</li>
@@ -86,13 +86,13 @@ class Account::Seeder
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/make-columns.gif" alt="Demo of adding columns" caption="Make two more columns" content-type="image/*" filename="make-columns.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Move this card to NOT NOW", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "Second, move this card to NOT NOW", status: "published", description: <<~HTML
<p>You can either select “NOT NOW” over in the sidebar, or you can go back out to the board view and drag this card into the “NOT NOW” column on the left side.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/not-now.gif" alt="Demo of moving a card to Not Now" caption="Move to Not Now" content-type="image/*" filename="not-now.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Rename this card", status: "published", description: <<~HTML
playground.cards.create! creator: creator, title: "First, rename this card", status: "published", description: <<~HTML
<ol>
<li>Click the title and you can rename the card, change the description, or add more information to the card.</li>
<li>Then, hit "Mark as Done" at the bottom of the card.</li>
+1 -1
View File
@@ -1,5 +1,5 @@
class Board < ApplicationRecord
include AutoPostponing, Accessible, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable
include Accessible, AutoPostponing, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable
belongs_to :creator, class_name: "User", default: -> { Current.user }
+5
View File
@@ -45,6 +45,7 @@ module Board::Accessible
mentions_for_user(user).destroy_all
notifications_for_user(user).destroy_all
watches_for(user).destroy_all
end
def watchers
@@ -89,4 +90,8 @@ module Board::Accessible
(notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.board_id = ?)",
id, id)
end
def watches_for(user)
Watch.where(card: cards, user: user)
end
end
+2 -2
View File
@@ -1,6 +1,6 @@
class Card < ApplicationRecord
include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic,
Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
include Assignable, Attachments, Closeable, Colored, Entropic, Eventable,
Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable
belongs_to :board, touch: true
-7
View File
@@ -1,7 +0,0 @@
module Card::Broadcastable
extend ActiveSupport::Concern
included do
broadcasts_refreshes
end
end
+4
View File
@@ -7,5 +7,9 @@ module Card::Mentions
def mentionable?
published?
end
def should_check_mentions?
saved_change_to_status? && published?
end
end
end
+5 -15
View File
@@ -19,20 +19,6 @@ module Mentions
private
def scan_mentionees
mentionees_from_plain_text | mentionees_from_rich_text
end
def mentionees_from_plain_text
scan_mentioned_handles.filter_map do |mention|
mentionable_users.find { |user| user.mentionable_handles.include?(mention) }
end
end
def scan_mentioned_handles
mentionable_content.scan(/(?<!\w)@(\w+)/).flatten.uniq(&:downcase)
end
def mentionees_from_rich_text
mentionees_from_attachments & mentionable_users
end
@@ -49,7 +35,7 @@ module Mentions
end
def should_create_mentions?
mentionable? && mentionable_content_changed?
mentionable? && (mentionable_content_changed? || should_check_mentions?)
end
def mentionable_content_changed?
@@ -64,4 +50,8 @@ module Mentions
def mentionable?
true
end
def should_check_mentions?
false
end
end
+4
View File
@@ -22,6 +22,10 @@ class Event < ApplicationRecord
eventable
end
def description_for(user)
Event::Description.new(self, user)
end
private
def dispatch_webhooks
Event::WebhookDispatchJob.perform_later(self)
+108
View File
@@ -0,0 +1,108 @@
class Event::Description
include ActionView::Helpers::TagHelper
include ERB::Util
attr_reader :event, :user
def initialize(event, user)
@event = event
@user = user
end
def to_html
to_sentence(creator_tag, card_title_tag).html_safe
end
def to_plain_text
to_sentence(creator_name, card.title)
end
private
def to_sentence(creator, card_title)
if event.action.comment_created?
comment_sentence(creator, card_title)
else
action_sentence(creator, card_title)
end
end
def creator_tag
tag.span data: { creator_id: event.creator.id } do
tag.span("You", data: { only_visible_to_you: true }) +
tag.span(event.creator.name, data: { only_visible_to_others: true })
end
end
def card_title_tag
tag.span card.title, class: "txt-underline"
end
def creator_name
h(event.creator.name)
end
def card
@card ||= event.action.comment_created? ? event.eventable.card : event.eventable
end
def comment_sentence(creator, card_title)
"#{creator} commented on #{card_title}"
end
def action_sentence(creator, card_title)
case event.action
when "card_assigned"
assigned_sentence(creator, card_title)
when "card_unassigned"
unassigned_sentence(creator, card_title)
when "card_published"
"#{creator} added #{card_title}"
when "card_closed"
%(#{creator} moved #{card_title} to "Done")
when "card_reopened"
"#{creator} reopened #{card_title}"
when "card_postponed"
%(#{creator} moved #{card_title} to "Not Now")
when "card_auto_postponed"
%(#{card_title} was closed as "Not Now" due to inactivity)
when "card_resumed"
"#{creator} resumed #{card_title}"
when "card_title_changed"
renamed_sentence(creator, card_title)
when "card_board_changed", "card_collection_changed"
moved_sentence(creator, card_title)
when "card_triaged"
triaged_sentence(creator, card_title)
when "card_sent_back_to_triage"
%(#{creator} moved #{card_title} back to "Maybe?")
end
end
def assigned_sentence(creator, card_title)
if event.assignees.include?(user)
"#{creator} will handle #{card_title}"
else
"#{creator} assigned #{h event.assignees.pluck(:name).to_sentence} to #{card_title}"
end
end
def unassigned_sentence(creator, card_title)
assignees_text = event.assignees.include?(user) ? "yourself" : event.assignees.pluck(:name).to_sentence
"#{creator} unassigned #{h(assignees_text)} from #{card_title}"
end
def renamed_sentence(creator, card_title)
old_title = event.particulars.dig("particulars", "old_title")
%(#{creator} renamed #{card_title} (was: "#{h old_title}"))
end
def moved_sentence(creator, card_title)
new_location = event.particulars.dig("particulars", "new_board") || event.particulars.dig("particulars", "new_collection")
%(#{creator} moved #{card_title} to "#{h new_location}")
end
def triaged_sentence(creator, card_title)
column = event.particulars.dig("particulars", "column")
%(#{creator} moved #{card_title} to "#{h column}")
end
end
+1 -1
View File
@@ -5,7 +5,7 @@ class Identity < UntenantedRecord
has_many :magic_links, dependent: :destroy
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(value) { value.strip.downcase }
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
def send_magic_link
magic_links.create!.tap do |magic_link|
+1 -1
View File
@@ -8,7 +8,7 @@ class Notifier::CardEventNotifier < Notifier
when "card_assigned"
source.assignees.excluding(creator)
when "card_published"
board.watchers.without(creator, *card.mentionees)
board.watchers.without(creator, *card.mentionees).including(*card.assignees).uniq
when "comment_created"
card.watchers.without(creator, *source.eventable.mentionees)
else
+16
View File
@@ -29,6 +29,18 @@ class User::DayTimeline
day.yesterday.beginning_of_day
end
def added_column
@added_column ||= build_column("Added", 1, events.where(action: %w[card_published card_reopened]))
end
def updated_column
@updated_column ||= build_column("Updated", 2, events.where.not(action: %w[card_published card_closed card_reopened]))
end
def closed_column
@closed_column ||= build_column("Closed", 3, events.where(action: "card_closed"))
end
def cache_key
ActiveSupport::Cache.expand_cache_key [ user, filter, day.to_date, events ], "day-timeline"
end
@@ -70,6 +82,10 @@ class User::DayTimeline
filtered_events.where(created_at: ...day.beginning_of_day).chronologically.last
end
def build_column(base_title, index, events)
Column.new(self, base_title, index, events)
end
def window
day.all_day
end
+42
View File
@@ -0,0 +1,42 @@
class User::DayTimeline::Column
include ActionView::Helpers::TagHelper, ActionView::Helpers::OutputSafetyHelper, TimeHelper
attr_reader :index
def initialize(day_timeline, base_title, index, events)
@day_timeline = day_timeline
@base_title = base_title
@index = index
@events = events
end
def title
date_tag = local_datetime_tag(day_timeline.day, style: :agoorweekday)
parts = [ base_title, date_tag ]
parts << tag.span("(#{full_events_count})", class: "font-weight-normal") if full_events_count > 0
safe_join(parts, " ")
end
def events_by_hour
limited_events.group_by { it.created_at.hour }
end
def has_more_events?
limited_events.count < full_events_count
end
def hidden_events_count
full_events_count - limited_events.count
end
private
attr_reader :base_title, :day_timeline, :events
def limited_events
@limited_events ||= events.limit(100).load
end
def full_events_count
@full_events_count ||= events.count
end
end
+1 -1
View File
@@ -18,7 +18,7 @@ module User::Role
admin? || other == self
end
def can_administer?(other = nil)
def can_administer?(other)
admin? && other != self
end
+5 -2
View File
@@ -2,8 +2,11 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to account_join_code_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">&larr; <strong>Invite link</strong></span>
<%= link_to account_join_code_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<strong>Back to Invite link</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
</div>
<% end %>
+16 -8
View File
@@ -2,8 +2,11 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to account_settings_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">&larr; <strong>Account Settings</strong></span>
<%= link_to account_settings_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<strong>Back to Account Settings</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
</div>
<% end %>
@@ -17,10 +20,13 @@
<% url = join_url(code: @join_code.code, tenant: @join_code.tenant, script_name: nil) %>
<div class="flex align-center gap-half">
<input type="text" class="input flex-item-grow" value="<%= url %>" readonly>
<%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", data: {
turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %>
<%= icon_tag "refresh" %>
<span class="for-screen-reader">Generate a new code</span>
<% if Current.user.admin? %>
<%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", data: {
turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %>
<%= icon_tag "refresh" %>
<span class="for-screen-reader">Generate a new code</span>
<% end %>
<% end %>
</div>
@@ -59,8 +65,10 @@
This code has been used <%= @join_code.usage_count %>/<%= @join_code.usage_limit %> times
(<%= @join_code.active? ? @join_code.usage_limit - @join_code.usage_count : "none" %> remaining)
<%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %>
<span>Change limit</span>
<% if Current.user.admin? %>
<%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %>
<span>Change limit</span>
<% end %>
<% end %>
</p>
</footer>
+1 -1
View File
@@ -4,5 +4,5 @@
<p class="margin-none-block-start">Fizzy doesnt let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. <em>This is the default, global setting — you can override it on each board.</em></p>
</header>
<%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %>
<%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path, disabled: !Current.user.admin? %>
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
<%= form_with model: account, url: account_settings_path, method: :put, scope: :account, data: { controller: "form" }, class: "flex gap-half" do |form| %>
<strong class="full-width"><%= form.text_field :name, required: true, class: "input input--transparent full-width txt-medium", placeholder: "Account name…", data: { action: "input->form#disableSubmitWhenInvalid" }, readonly: !Current.user.can_administer? %></strong>
<strong class="full-width"><%= form.text_field :name, required: true, class: "input input--transparent full-width txt-medium", placeholder: "Account name…", data: { action: "input->form#disableSubmitWhenInvalid" }, readonly: !Current.user.admin? %></strong>
<% if Current.user.can_administer? %>
<% if Current.user.admin? %>
<%= form.button class: "btn btn--circle btn--link txt-medium", data: { form_target: "submit" }, disabled: form.object do %>
<%= icon_tag "arrow-right" %>
<span class="for-screen-reader">Save changes</span>
+6 -1
View File
@@ -1,7 +1,12 @@
<% @page_title = "Account Settings" %>
<% content_for :header do %>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title">
<%= @page_title %>
<% unless Current.user.admin? %>
<div class="txt-normal font-weight-normal">Only admins can change these settings</div>
<% end %>
</h1>
<% end %>
<section class="settings margin-block-start-half">
@@ -2,12 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @board.name %></strong>
</span>
<% end %>
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
@@ -2,12 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @board.name %></strong>
</span>
<% end %>
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
+2 -7
View File
@@ -2,12 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to @column.board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @column.board.name %></strong>
</span>
<% end %>
<%= link_back_to_board(@column.board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
@@ -16,7 +11,7 @@
<% end %>
<section class="cards cards--grid">
<%= turbo_frame_tag dom_id(@column, :cards) do %>
<%= turbo_frame_tag @column, :cards do %>
<% if @page.used? %>
<%= with_automatic_pagination dom_id(@column, :cards), @page do %>
<%= render "boards/columns/list", cards: @page.records, draggable: true %>
@@ -2,12 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @board.name %></strong>
</span>
<% end %>
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
+10 -10
View File
@@ -2,15 +2,15 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @board.name %></strong>
</span>
<% end %>
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title">
<div><%= @page_title %></div>
<% unless Current.user.can_administer_board?(@board) %>
<div class="txt-normal font-weight-normal">Only admins can change these settings</div>
<% end %>
</h1>
<% end %>
<section class="settings">
@@ -19,10 +19,10 @@
controller: "form boards-form",
boards_form_self_removal_prompt_message_value: "Are you sure you want to remove yourself from this board? You wont be able to get back in unless someone invites you.",
action: "turbo:submit-start->boards-form#submitWithWarning" } do |form| %>
<%= render "boards/edit/name", form: form %>
<%= render "boards/edit/name", form: form, board: @board %>
<%= render "boards/edit/users", board: @board, selected_users: @selected_users, unselected_users: @unselected_users, form: form %>
<button type="submit" id="log_in" class="btn btn--link center txt-normal">
<button type="submit" id="log_in" class="btn btn--link center txt-normal" <%= "disabled" unless Current.user.can_administer_board?(@board) %>>
<span>Save changes</span>
</button>
@@ -33,6 +33,6 @@
<div class="settings__panel panel shadow center">
<%= render "boards/edit/auto_close", board: @board %>
<%= render "boards/edit/publication", board: @board %>
<%= render "boards/edit/delete", board: @board %>
<%= render "boards/edit/delete", board: @board if Current.user.can_administer_board?(@board) %>
</div>
</section>
+1 -1
View File
@@ -2,6 +2,6 @@
<div class="margin-block-end">
<h2 class="divider txt-large">Auto close</h2>
<p class="margin-none-block-start">Fizzy doesnt let stale cards stick around forever. Cards automatically close as “Not now” if no one updates, comments, or moves a card for…</p>
<%= render "entropy/auto_close", model: board, url: board_entropy_path(board) %>
<%= render "entropy/auto_close", model: board, url: board_entropy_path(board), disabled: !Current.user.can_administer_board?(@board) %>
</div>
<% end %>
-35
View File
@@ -1,35 +0,0 @@
<% name = label.parameterize %>
<fieldset class="knob" data-controller="knob" data-knob-target="field" style="--knob-options: <%= knob_options.length %>; --knob-index: <%= default_index %>;">
<div class="position-relative" role="radiogroup" aria-labelledby="<%= name %>">
<% knob_options.each_with_index do |value, i| %>
<label class="knob__option" style="--i: <%= i %>">
<input
<%= "checked" if value == knob_options[default_index] %>
data-action="change->knob#optionChanged"
data-index="<%= i %>"
data-knob-target="option"
name="<%= name %>"
type="radio"
value="<%= value %>"
/>
<span><%= value %></span>
</label>
<% end %>
<input
aria-hidden
class="knob__slider"
data-action="input->knob#sliderChanged"
data-knob-target="slider"
max="<%= knob_options.length - 1 %>"
min="0"
type="range"
value="<%= default_index %>"
/>
<div class="knob__knob" aria-hidden></div>
</div>
<div id="<%= name %>" class="knob__label">Days</div>
</fieldset>
+2 -1
View File
@@ -2,6 +2,7 @@
<label class="flex-item-grow">
<strong><%= form.text_field :name, name: "board[name]", class: "input full-width txt-medium",
required: true, autofocus: false, placeholder: "Board name…",
data: { action: "keydown.enter->form#submit:prevent, keydown.esc->form#cancel" } %></strong>
data: { action: "keydown.enter->form#submit:prevent, keydown.esc->form#cancel" },
readonly: !Current.user.can_administer_board?(board) %></strong>
</label>
</div>
+4 -3
View File
@@ -10,7 +10,7 @@
<span class="txt-large"><%= icon_tag "lock" %></span>
<label class="switch flex align-center justify-between">
<%= form_with url: board_publication_path(board), method: :delete, data: { controller: "form" } do |form| %>
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" } %>
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" }, disabled: !Current.user.can_administer_board?(@board) %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Turn off the public link</span>
<% end %>
@@ -32,7 +32,8 @@
<%= form_with model: board, class: "txt-align-start", data: { controller: "form", turbo_frame: "_top" } do |form| %>
<%= form.rich_textarea :public_description, class: "rich-text-content txt-small",
placeholder: "Add a public note about this board…",
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" },
readonly: !Current.user.can_administer_board?(@board) %>
<%= form.button "Save changes", type: :submit, class: "btn txt-small" %>
<% end %>
</div>
@@ -43,7 +44,7 @@
<span class="txt-large"><%= icon_tag "lock" %></span>
<label class="switch flex align-center justify-between">
<%= form_with url: board_publication_path(board), method: :post, data: { controller: "form" } do |form| %>
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" } %>
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" }, disabled: !Current.user.can_administer_board?(@board) %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Turn on the public link</span>
<% end %>
+8 -6
View File
@@ -2,13 +2,15 @@
data-drag-and-drop-target="container"
data-drag-and-strum-target="container"
data-collapsible-columns-target="column"
data-column-name="Done"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
data-drag-and-drop-url="<%= columns_card_drops_closure_path("__id__") %>"
>
<%= render "boards/show/expander", title: "Done", count: board.cards.closed.count, column_id: "closed-cards" do %>
<%= render "boards/show/menu/expand", column_path: board_columns_closed_path(board) %>
<% end %>
<%= column_frame_tag :closed_column, src: board_columns_closed_path(board) %>
<div class="cards__transition-container">
<header class="cards__header">
<%= render "boards/show/expander", title: "Done", count: board.cards.closed.count, column_id: "closed-cards" %>
<%= render "boards/show/menu/maximize", column_path: board_columns_closed_path(board) %>
</header>
<%= column_frame_tag :closed_column, src: board_columns_closed_path(board) %>
</div>
</section>
+12 -9
View File
@@ -4,17 +4,20 @@
data-drag-and-strum-target="container"
data-collapsible-columns-target="column"
data-controller="clicker"
data-column-name="<%= column.name %>"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle turbo:before-stream-render@document->collapsible-columns#restoreState"
data-drag-and-drop-url="<%= columns_card_drops_column_path("__id__", column_id: column.id) %>"
>
<%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) do %>
<%= render "boards/show/menu/column", column: column %>
<% end %>
<div class="cards__transition-container">
<header class="cards__header">
<%= render "boards/show/menu/column", column: column %>
<%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) %>
<%= link_to board_column_path(column.board, column), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "expand", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<% end %>
<%= column_frame_tag dom_id(column, :cards), src: board_column_path(column.board, column) %>
<%= link_to board_column_path(column.board, column), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "grid", class: "translucent" %>
<span class="for-screen-reader">Maximize column</span>
<% end %>
</header>
<%= column_frame_tag dom_id(column, :cards), src: board_column_path(column.board, column) %>
</div>
</section>
+14 -12
View File
@@ -1,26 +1,28 @@
<%= tag.div data: {
controller: "drag-and-drop drag-and-strum",
<%= tag.div class: "card-columns", data: {
controller: "collapsible-columns drag-and-drop drag-and-strum",
drag_and_drop_dragged_item_class: "drag-and-drop__dragged-item",
drag_and_drop_hover_container_class: "drag-and-drop__hover-container",
collapsible_columns_board_value: board.id,
collapsible_columns_collapsed_class: "is-collapsed",
collapsible_columns_no_transitions_class: "no-transitions",
collapsible_columns_title_not_visible_class: "is-off-screen",
action: "
dragstart->drag-and-drop#dragStart
dragover->drag-and-drop#dragOver
dragenter->drag-and-strum#dragEnter
drop->drag-and-drop#drop
dragend->drag-and-drop#dragEnd" } do %>
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-board-value="<%= board.id %>" data-collapsible-columns-collapsed-class="is-collapsed" data-collapsible-columns-no-transitions-class="no-transitions">
<div class="card-columns__left">
<%= render "boards/show/not_now", board: board %>
</div>
<div class="card-columns__left">
<%= render "boards/show/not_now", board: board %>
</div>
<%= render "boards/show/stream", board: board, page: page %>
<%= render "boards/show/stream", board: board, page: page %>
<div class="card-columns__right">
<%= render partial: "boards/show/column", collection: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %>
<%= render "boards/show/closed", board: board %>
<div class="card-columns__right">
<%= render partial: "boards/show/column", collection: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %>
<%= render "boards/show/closed", board: board %>
<%= render "boards/show/menu/columns", board: board %>
</div>
<%= render "boards/show/menu/columns", board: board %>
</div>
<% end %>
+4 -1
View File
@@ -1,6 +1,9 @@
<button class="cards__expander btn btn--plain" data-collapsible-columns-target="button" data-action="click->collapsible-columns#toggle"
style="--card-count: <%= [ count, 20 ].min %>" aria-controls="<%= column_id %>" aria-expanded="false">
<span class="cards__expander-count" data-drag-and-drop-counter="true"><%= count > 99 ? "99+" : count %></span>
<h2 class="cards__expander-title"><%= title %></h2>
<h2 class="cards__expander-title" data-collapsible-columns-target="title">
<%= title %>
<%= icon_tag "collapse" %>
</h2>
</button>
<%= yield if block_given? %>
+8 -6
View File
@@ -2,13 +2,15 @@
data-collapsible-columns-target="column"
data-drag-and-drop-target="container"
data-drag-and-strum-target="container"
data-column-name="Not Now"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
data-drag-and-drop-url="<%= columns_card_drops_not_now_path("__id__") %>"
>
<%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" do %>
<%= render "boards/show/menu/expand", column_path: board_columns_not_now_path(board) %>
<% end %>
<%= column_frame_tag :not_now_column, src: board_columns_not_now_path(board) %>
<div class="cards__transition-container">
<header class="cards__header">
<%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" %>
<%= render "boards/show/menu/maximize", column_path: board_columns_not_now_path(board) %>
</header>
<%= column_frame_tag :not_now_column, src: board_columns_not_now_path(board) %>
</div>
</section>
+17 -14
View File
@@ -1,20 +1,23 @@
<section id="the-stream" class="cards cards--considering"
data-drag-and-drop-target="container"
data-column-name="Maybe?"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
data-drag-and-drop-url="<%= columns_card_drops_stream_path("__id__") %>">
<header class="cards__expander">
<h2 class="cards__expander-title position-relative">Maybe?</h2>
<%= link_to board_columns_stream_path(board), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "expand", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<div class="cards__transition-container">
<header class="cards__header">
<div class="cards__expander">
<h2 class="cards__expander-title" data-collapsible-columns-target="title">Maybe?</h2>
</div>
<%= link_to board_columns_stream_path(board), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "grid", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<% end %>
</header>
<%= render "columns/show/add_card_button", board: board %>
<% if page.used? %>
<%= with_automatic_pagination "the-stream", @page do %>
<%= render "boards/columns/list", cards: page.records, draggable: true %>
<% end %>
<% end %>
</header>
<%= render "columns/show/add_card_button", board: board %>
<% if page.used? %>
<%= with_automatic_pagination "the-stream", @page do %>
<%= render "boards/columns/list", cards: page.records, draggable: true %>
<% end %>
<% end %>
</div>
</section>
+1 -1
View File
@@ -1,4 +1,4 @@
<div id="<%= dom_id(column, :menu) %>" class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<div id="<%= dom_id(column, :menu) %>" class="cards__menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open">
<%= icon_tag "menu-dots-horizontal", class: "translucent" %>
<span class="for-screen-reader">Column options</span>
+3 -2
View File
@@ -1,6 +1,7 @@
<div class="position-relative cards__new-column" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open:stop" aria-label="Add a new column, etc.">
<div class="cards__new-column position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<button class="btn btn--circle txt-x-small borderless" data-controller="tooltip" data-action="click->dialog#open:stop">
<%= icon_tag "add", class: "translucent" %>
<span class="for-screen-reader">Add a column</span>
</button>
<dialog class="popup panel flex-column gap-half fill-white shadow txt-small margin-block-double" data-dialog-target="dialog">
<%= render "boards/show/menu/column_form", board: board, column: Column.new, label: "Add column" %>
@@ -1,4 +1,4 @@
<%= link_to column_path, class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "expand", class: "translucent" %>
<%= link_to column_path, class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "grid", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<% end %>
+5 -1
View File
@@ -5,7 +5,11 @@
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title">Assign this to…</strong>
<div class="flex align-start justify-space-between">
<strong class="popup__title">Assign this to…</strong>
<kbd class="txt-xx-small hide-on-touch">a</kbd>
</div>
<% if @users.many? %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true,
+1 -1
View File
@@ -1,4 +1,4 @@
<%= turbo_frame_tag dom_id(@card, :columns) do %>
<%= turbo_frame_tag @card, :columns do %>
<div class="card__stages" role="radiogroup">
<legend class="for-screen-reader">Choose a column for this card</legend>
+10 -3
View File
@@ -13,9 +13,16 @@
data: { local_save_target: "input", action: "lexxy:change->form#disableSubmitWhenInvalid lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
<%= general_prompts(@card.board) %>
<% end %>
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start", data: { form_target: "submit" }, disabled: true do %>
<span>Post this comment</span>
<% end %>
<span class="flex-inline align-center gap">
<%= form.button class: "comment__submit btn btn--reversed",
data: { form_target: "submit" }, disabled: true do %>
<span>Post</span>
<% end %>
<span class="hide-on-touch txt-subtle">
<kbd class="txt-x-small"><%= hotkey_label([ "ctrl", "enter" ]) %></kbd>
</span>
</span>
<span data-form-target="cancel" hidden></span>
<% end %>
</div>
@@ -6,7 +6,7 @@
<%= turbo_frame_tag comment, :new_reaction do %>
<%= link_to new_card_comment_reaction_path(comment.card, comment), role: "button",
class: "reactions__trigger btn btn--circle borderless", action: "soft-keyboard#open",
class: "reactions__trigger btn btn--circle", action: "soft-keyboard#open",
data: { turbo_frame: dom_id(comment, :new_reaction), action: "dialog#close" } do %>
<%= image_tag "boost-color.svg", aria: { hidden: true } %>
<span class="for-screen-reader">Add your own reaction</span>
+4 -1
View File
@@ -7,8 +7,11 @@
<% end %>
</div>
<% else %>
<%= button_to card_closure_path(card), class: "btn borderless" do %>
<%= button_to card_closure_path(card), class: "btn borderless",
data: { controller: "hotkey", form_target: "submit", action: "keydown.d@document->hotkey#click" },
form: { data: { controller: "form" } } do %>
<span class="overflow-ellipsis">Mark as Done</span>
<kbd class="txt-x-small hide-on-touch">d</kbd>
<% end %>
<div id="<%= dom_id(card, :closure_notice) %>">
+11 -4
View File
@@ -4,9 +4,16 @@
<%= link_to card.title, edit_card_path(card), class: "card__title-link" %>
</h1>
<div class="card__description rich-text-content" data-controller="syntax-highlight retarget-links">
<%= card.description %>
</div>
<% unless card.description.blank? %>
<div class="card__description rich-text-content" data-controller="syntax-highlight retarget-links">
<%= card.description %>
</div>
<% end %>
<%= link_to edit_card_path(card), class: "btn fit-content txt-small", data: { controller: "hotkey", action: "keydown.e@document->hotkey#click" } do %>
<span>Edit</span>
<kbd class="txt-x-small hide-on-touch">e</kbd>
<% end %>
<% end %>
<% else %>
<%= form_with model: card, id: "card_form", data: { controller: "autoresize auto-save" } do |form| %>
@@ -19,7 +26,7 @@
<% end %>
</h1>
<%= form.rich_textarea :description, class: "card-field__description rich-text-content",
<%= form.rich_textarea :description, class: "card__description rich-text-content",
placeholder: "Add some notes, context, pictures, or video about this…",
data: { action: "lexxy:change->auto-save#change focusout->auto-save#submit" } do %>
<%= general_prompts(card.board) %>
@@ -1,7 +1,13 @@
<div class="card-perma__notch card-perma__notch--bottom">
<%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn",
form: { data: { controller: "form", action: "keydown.ctrl+enter@document->form#submit:prevent keydown.meta+enter@document->form#submit:prevent" } } %>
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
form: { data: { controller: "form" } },
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
<span>Create card</span>
<% end %>
<%= button_to "Create and add another", card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
title: "Create and add another (ctrl+shift+enter)", form: { data: { turbo: false } }, data: { controller: "hotkey", action: "keydown.ctrl+shift+enter@document->hotkey#click" } %>
<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
title: "Create and add another (ctrl+shift+enter)", form: { data: { controller: "form" } },
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
<span>Create and add another</span>
<% end %>
</div>

Some files were not shown because too many files have changed in this diff Show More