diff --git a/Gemfile b/Gemfile
index efd0166b4..17e33abd2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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"
diff --git a/Gemfile.lock b/Gemfile.lock
index cfbb22c7b..02c28a0e0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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
diff --git a/app/assets/images/collapse.svg b/app/assets/images/collapse.svg
new file mode 100644
index 000000000..90effb7cf
--- /dev/null
+++ b/app/assets/images/collapse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/fizzy.svg b/app/assets/images/fizzy.svg
new file mode 100644
index 000000000..fd9f5f8bf
--- /dev/null
+++ b/app/assets/images/fizzy.svg
@@ -0,0 +1 @@
+
diff --git a/app/assets/images/grid.svg b/app/assets/images/grid.svg
new file mode 100644
index 000000000..eca0897e6
--- /dev/null
+++ b/app/assets/images/grid.svg
@@ -0,0 +1 @@
+
diff --git a/app/assets/images/logo-color.svg b/app/assets/images/logo-color.svg
deleted file mode 100644
index 32d315b5f..000000000
--- a/app/assets/images/logo-color.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png
index 82632a317..92d4a8e74 100644
Binary files a/app/assets/images/logo.png and b/app/assets/images/logo.png differ
diff --git a/app/assets/images/logo.webp b/app/assets/images/logo.webp
new file mode 100644
index 000000000..20568de8b
Binary files /dev/null and b/app/assets/images/logo.webp differ
diff --git a/app/assets/images/maximize.svg b/app/assets/images/maximize.svg
new file mode 100644
index 000000000..425fa32d0
--- /dev/null
+++ b/app/assets/images/maximize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css
index c18cf4090..9f1092f82 100644
--- a/app/assets/stylesheets/_global.css
+++ b/app/assets/stylesheets/_global.css
@@ -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;
diff --git a/app/assets/stylesheets/buttons.css b/app/assets/stylesheets/buttons.css
index e2710006d..5a5c910d1 100644
--- a/app/assets/stylesheets/buttons.css
+++ b/app/assets/stylesheets/buttons.css
@@ -40,7 +40,7 @@
pointer-events: none;
}
- [aria-busy] &:disabled {
+ form[aria-busy] &:disabled {
position: relative;
> * {
diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css
index a74e8e3f8..675f3fe13 100644
--- a/app/assets/stylesheets/card-columns.css
+++ b/app/assets/stylesheets/card-columns.css
@@ -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;
+ }
}
}
diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css
index 0160fdaaf..fd506aa17 100644
--- a/app/assets/stylesheets/card-perma.css
+++ b/app/assets/stylesheets/card-perma.css
@@ -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) {
diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css
index 2b61ac4ce..2591332d9 100644
--- a/app/assets/stylesheets/cards.css
+++ b/app/assets/stylesheets/cards.css
@@ -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;
+ }
}
}
diff --git a/app/assets/stylesheets/drag_and_drop.css b/app/assets/stylesheets/drag_and_drop.css
index 99b3089eb..2ada036b4 100644
--- a/app/assets/stylesheets/drag_and_drop.css
+++ b/app/assets/stylesheets/drag_and_drop.css
@@ -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;
diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css
index d8b5a2a43..be5448991 100644
--- a/app/assets/stylesheets/header.css
+++ b/app/assets/stylesheets/header.css
@@ -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;
+ }
+ }
}
diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css
index 8575b72f2..8e9f19e89 100644
--- a/app/assets/stylesheets/icons.css
+++ b/app/assets/stylesheets/icons.css
@@ -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 "); }
diff --git a/app/assets/stylesheets/boxcar-menu.css b/app/assets/stylesheets/nav.css
similarity index 75%
rename from app/assets/stylesheets/boxcar-menu.css
rename to app/assets/stylesheets/nav.css
index e60a56bb9..2aa7be278 100644
--- a/app/assets/stylesheets/boxcar-menu.css
+++ b/app/assets/stylesheets/nav.css
@@ -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;
+ }
}
diff --git a/app/assets/stylesheets/panels.css b/app/assets/stylesheets/panels.css
index 9b0345096..a82dc1665 100644
--- a/app/assets/stylesheets/panels.css
+++ b/app/assets/stylesheets/panels.css
@@ -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;
diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css
index 825414807..c0803326d 100644
--- a/app/assets/stylesheets/popup.css
+++ b/app/assets/stylesheets/popup.css
@@ -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;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css
index df7f8d3a8..890e27ac3 100644
--- a/app/assets/stylesheets/print.css
+++ b/app/assets/stylesheets/print.css
@@ -35,8 +35,8 @@
print-color-adjust: exact;
}
- .boxcar-menu,
- .boxcar-menu-trigger,
+ .nav__menu,
+ .nav__trigger,
.header__actions {
display: none;
}
diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css
index 664aa7941..80bde798a 100644
--- a/app/assets/stylesheets/reactions.css
+++ b/app/assets/stylesheets/reactions.css
@@ -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
diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css
index 72cba909b..5e871260f 100644
--- a/app/assets/stylesheets/rich-text-content.css
+++ b/app/assets/stylesheets/rich-text-content.css
@@ -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;
diff --git a/app/assets/stylesheets/tooltips.css b/app/assets/stylesheets/tooltips.css
index 20a02985b..419664fc6 100644
--- a/app/assets/stylesheets/tooltips.css
+++ b/app/assets/stylesheets/tooltips.css
@@ -1,5 +1,5 @@
@layer components {
- [data-controller^="tooltip"] {
+ [data-controller~="tooltip"] {
--tooltip-delay: 750ms;
--tooltip-duration: 150ms;
diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css
index 99bac2252..6ad6b2089 100644
--- a/app/assets/stylesheets/utilities.css
+++ b/app/assets/stylesheets/utilities.css
@@ -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; }
diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb
index f45f3ef5f..00d195584 100644
--- a/app/controllers/account/entropies_controller.rb
+++ b/app/controllers/account/entropies_controller.rb
@@ -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"
diff --git a/app/controllers/account/join_codes_controller.rb b/app/controllers/account/join_codes_controller.rb
index 026c692f9..562ffaa0d 100644
--- a/app/controllers/account/join_codes_controller.rb
+++ b/app/controllers/account/join_codes_controller.rb
@@ -1,5 +1,6 @@
class Account::JoinCodesController < ApplicationController
before_action :set_join_code
+ before_action :ensure_admin, only: %i[ update destroy ]
def show
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index dc70236ef..235b9c2a0 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/boards/entropies_controller.rb b/app/controllers/boards/entropies_controller.rb
index 153dafe5f..e07272e9b 100644
--- a/app/controllers/boards/entropies_controller.rb
+++ b/app/controllers/boards/entropies_controller.rb
@@ -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
diff --git a/app/controllers/boards/publications_controller.rb b/app/controllers/boards/publications_controller.rb
index b7cda07ee..9435bd8bf 100644
--- a/app/controllers/boards/publications_controller.rb
+++ b/app/controllers/boards/publications_controller.rb
@@ -1,6 +1,8 @@
class Boards::PublicationsController < ApplicationController
include BoardScoped
+ before_action :ensure_permission_to_admin_board
+
def create
@board.publish
end
diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb
index 4bff8dde4..70383e139 100644
--- a/app/controllers/boards_controller.rb
+++ b/app/controllers/boards_controller.rb
@@ -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
diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb
index cac32efde..fdd2a86dc 100644
--- a/app/controllers/cards/assignments_controller.rb
+++ b/app/controllers/cards/assignments_controller.rb
@@ -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
diff --git a/app/controllers/cards/columns_controller.rb b/app/controllers/cards/columns_controller.rb
index f250f2dd6..3e32b1cb8 100644
--- a/app/controllers/cards/columns_controller.rb
+++ b/app/controllers/cards/columns_controller.rb
@@ -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
diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb
index 50f58e34b..23bac85e3 100644
--- a/app/controllers/cards/taggings_controller.rb
+++ b/app/controllers/cards/taggings_controller.rb
@@ -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
diff --git a/app/controllers/concerns/board_scoped.rb b/app/controllers/concerns/board_scoped.rb
index 2772c7d01..f4f3a3e2b 100644
--- a/app/controllers/concerns/board_scoped.rb
+++ b/app/controllers/concerns/board_scoped.rb
@@ -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
diff --git a/app/controllers/concerns/saas.rb b/app/controllers/concerns/saas.rb
new file mode 100644
index 000000000..fd4ca8491
--- /dev/null
+++ b/app/controllers/concerns/saas.rb
@@ -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
diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb
index f9980a8db..a4b6768b9 100644
--- a/app/controllers/join_codes_controller.rb
+++ b/app/controllers/join_codes_controller.rb
@@ -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}")
diff --git a/app/controllers/memberships/email_addresses_controller.rb b/app/controllers/memberships/email_addresses_controller.rb
index c75783ac1..0870410a9 100644
--- a/app/controllers/memberships/email_addresses_controller.rb
+++ b/app/controllers/memberships/email_addresses_controller.rb
@@ -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
diff --git a/app/controllers/my/menus_controller.rb b/app/controllers/my/menus_controller.rb
index 900cf7c0a..2b20d2188 100644
--- a/app/controllers/my/menus_controller.rb
+++ b/app/controllers/my/menus_controller.rb
@@ -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
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 10a3c9773..8f8222ed9 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -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
diff --git a/app/controllers/users/joins_controller.rb b/app/controllers/users/joins_controller.rb
index d811a6203..73a9ae296 100644
--- a/app/controllers/users/joins_controller.rb
+++ b/app/controllers/users/joins_controller.rb
@@ -3,6 +3,8 @@ class Users::JoinsController < ApplicationController
before_action :set_join_code, :ensure_join_code_is_valid
+ layout "public"
+
def new
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f6afd0ba4..fc37d1635 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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)
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 3dccac678..f572434ec 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -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 },
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index ae8739d05..3fde0d2ad 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -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 ("←" + 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
diff --git a/app/helpers/clipboard_helper.rb b/app/helpers/clipboard_helper.rb
index b079dc6a9..972856b15 100644
--- a/app/helpers/clipboard_helper.rb
+++ b/app/helpers/clipboard_helper.rb
@@ -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
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 992463740..501abf2dd 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -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
diff --git a/app/helpers/my/menu_helper.rb b/app/helpers/my/menu_helper.rb
index 2962a49ac..7924c4d97 100644
--- a/app/helpers/my/menu_helper.rb
+++ b/app/helpers/my/menu_helper.rb
@@ -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
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 51ff30da0..dda94684d 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -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
diff --git a/app/javascript/controllers/clicker_controller.js b/app/javascript/controllers/clicker_controller.js
index d09cb9ede..6d3571942 100644
--- a/app/javascript/controllers/clicker_controller.js
+++ b/app/javascript/controllers/clicker_controller.js
@@ -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
}
}
diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js
index 74dd1ff65..b11821f3f 100644
--- a/app/javascript/controllers/collapsible_columns_controller.js
+++ b/app/javascript/controllers/collapsible_columns_controller.js
@@ -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))
+ }
}
diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js
index 16204bd28..4fbf25dc5 100644
--- a/app/javascript/controllers/dialog_controller.js
+++ b/app/javascript/controllers/dialog_controller.js
@@ -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" })
+ }
}
diff --git a/app/javascript/controllers/dialog_manager_controller.js b/app/javascript/controllers/dialog_manager_controller.js
new file mode 100644
index 000000000..71552e2c8
--- /dev/null
+++ b/app/javascript/controllers/dialog_manager_controller.js
@@ -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"]')
+ }
+}
diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js
index a5aa1a94f..94acaf28b 100644
--- a/app/javascript/controllers/navigable_list_controller.js
+++ b/app/javascript/controllers/navigable_list_controller.js
@@ -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)
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 4e7878b62..abeb3ae09 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,5 +1,5 @@
class ApplicationMailer < ActionMailer::Base
- default from: "The Fizzy team "
+ default from: "Fizzy "
layout "mailer"
append_view_path Rails.root.join("app/views/mailers")
diff --git a/app/mailers/magic_link_mailer.rb b/app/mailers/magic_link_mailer.rb
index 391e64f6d..f23cc4fbf 100644
--- a/app/mailers/magic_link_mailer.rb
+++ b/app/mailers/magic_link_mailer.rb
@@ -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
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
deleted file mode 100644
index 09926c09b..000000000
--- a/app/mailers/user_mailer.rb
+++ /dev/null
@@ -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
diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb
index 991877833..28835041c 100644
--- a/app/models/account/seeder.rb
+++ b/app/models/account/seeder.rb
@@ -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
There’s 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.
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
Open Fizzy menu, select “+ Add people ”, then copy the invite link. You can give this link to someone else so they can make an login for themselves in your account.
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
Hit “1” or pull down the Fizzy menu and select “Home”.
HTML
- playground.cards.create! creator: creator, title: "Check out all cards assigned to you", status: "published", description: <<~HTML
- Pull down the Fizzy menu at the top of the screen, and select “Assigned to me ” or just hit “3” on your keyboard any time.
+ playground.cards.create! creator: creator, title: "Now,check out all cards assigned to you", status: "published", description: <<~HTML
+ Pull down the Fizzy menu at the top of the screen, and select “Assigned to me ” or just hit “2” on your keyboard any time.
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
The Fizzy menu is how you get around the app. Click “Fizzy ” at the top of the screen or hit the “J” key on your keyboard to pop it open.
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
Click the little head with the + next to it, pick yourself.
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
Click the little Tag icon, type “design”, then “Create tag ”. Then, move the card to the new “YES” column you created in the previous step.
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
Make one called "Yes"
Make another called "Working on"
@@ -86,13 +86,13 @@ class Account::Seeder
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
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.
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
Click the title and you can rename the card, change the description, or add more information to the card.
Then, hit "Mark as Done" at the bottom of the card.
diff --git a/app/models/board.rb b/app/models/board.rb
index 9e259d652..71ff693fb 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -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 }
diff --git a/app/models/board/accessible.rb b/app/models/board/accessible.rb
index 6d62c0b21..75b2e982e 100644
--- a/app/models/board/accessible.rb
+++ b/app/models/board/accessible.rb
@@ -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
diff --git a/app/models/card.rb b/app/models/card.rb
index 65c7cab18..859f4b1a7 100644
--- a/app/models/card.rb
+++ b/app/models/card.rb
@@ -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
diff --git a/app/models/card/broadcastable.rb b/app/models/card/broadcastable.rb
deleted file mode 100644
index aa24b776c..000000000
--- a/app/models/card/broadcastable.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Card::Broadcastable
- extend ActiveSupport::Concern
-
- included do
- broadcasts_refreshes
- end
-end
diff --git a/app/models/card/mentions.rb b/app/models/card/mentions.rb
index cb3a34f17..3ce74726b 100644
--- a/app/models/card/mentions.rb
+++ b/app/models/card/mentions.rb
@@ -7,5 +7,9 @@ module Card::Mentions
def mentionable?
published?
end
+
+ def should_check_mentions?
+ saved_change_to_status? && published?
+ end
end
end
diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb
index 055c114fc..df8563ba3 100644
--- a/app/models/concerns/mentions.rb
+++ b/app/models/concerns/mentions.rb
@@ -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(/(?(value) { value.strip.downcase }
+ normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
def send_magic_link
magic_links.create!.tap do |magic_link|
diff --git a/app/models/notifier/card_event_notifier.rb b/app/models/notifier/card_event_notifier.rb
index 570989566..6179a4404 100644
--- a/app/models/notifier/card_event_notifier.rb
+++ b/app/models/notifier/card_event_notifier.rb
@@ -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
diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb
index 367eccdcb..a5f98cb8a 100644
--- a/app/models/user/day_timeline.rb
+++ b/app/models/user/day_timeline.rb
@@ -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
diff --git a/app/models/user/day_timeline/column.rb b/app/models/user/day_timeline/column.rb
new file mode 100644
index 000000000..fe42c68dc
--- /dev/null
+++ b/app/models/user/day_timeline/column.rb
@@ -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
diff --git a/app/models/user/role.rb b/app/models/user/role.rb
index a327ffcf6..e12bf0191 100644
--- a/app/models/user/role.rb
+++ b/app/models/user/role.rb
@@ -18,7 +18,7 @@ module User::Role
admin? || other == self
end
- def can_administer?(other = nil)
+ def can_administer?(other)
admin? && other != self
end
diff --git a/app/views/account/join_codes/edit.html.erb b/app/views/account/join_codes/edit.html.erb
index de53bf8c8..14958a44c 100644
--- a/app/views/account/join_codes/edit.html.erb
+++ b/app/views/account/join_codes/edit.html.erb
@@ -2,8 +2,11 @@
<% content_for :header do %>
<% end %>
diff --git a/app/views/account/join_codes/show.html.erb b/app/views/account/join_codes/show.html.erb
index f3142b091..c528bb07d 100644
--- a/app/views/account/join_codes/show.html.erb
+++ b/app/views/account/join_codes/show.html.erb
@@ -2,8 +2,11 @@
<% content_for :header do %>
<% end %>
@@ -17,10 +20,13 @@
<% url = join_url(code: @join_code.code, tenant: @join_code.tenant, script_name: nil) %>
- <%= 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" %>
- Generate a new code
+
+ <% 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" %>
+ Generate a new code
+ <% end %>
<% end %>
@@ -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 %>
- Change limit
+ <% if Current.user.admin? %>
+ <%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %>
+ Change limit
+ <% end %>
<% end %>
diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb
index f6056ab37..11ff9b326 100644
--- a/app/views/account/settings/_entropy.html.erb
+++ b/app/views/account/settings/_entropy.html.erb
@@ -4,5 +4,5 @@
Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.
- <%= 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? %>
diff --git a/app/views/account/settings/_name.html.erb b/app/views/account/settings/_name.html.erb
index 3c4f68f03..e3b12f325 100644
--- a/app/views/account/settings/_name.html.erb
+++ b/app/views/account/settings/_name.html.erb
@@ -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| %>
- <%= 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? %>
+ <%= 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? %>
- <% 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" %>
Save changes
diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb
index 7e2dbbfbf..57f3913bf 100644
--- a/app/views/account/settings/show.html.erb
+++ b/app/views/account/settings/show.html.erb
@@ -1,7 +1,12 @@
<% @page_title = "Account Settings" %>
<% content_for :header do %>
-
+
<% end %>
diff --git a/app/views/boards/columns/closeds/show.html.erb b/app/views/boards/columns/closeds/show.html.erb
index 9aeb21044..a67c45c3b 100644
--- a/app/views/boards/columns/closeds/show.html.erb
+++ b/app/views/boards/columns/closeds/show.html.erb
@@ -2,12 +2,7 @@
<% content_for :header do %>
diff --git a/app/views/boards/show/_columns.html.erb b/app/views/boards/show/_columns.html.erb
index a2a406aff..5223b9254 100644
--- a/app/views/boards/show/_columns.html.erb
+++ b/app/views/boards/show/_columns.html.erb
@@ -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 %>
-
-
- <%= render "boards/show/not_now", board: board %>
-
+
+ <%= render "boards/show/not_now", board: board %>
+
- <%= render "boards/show/stream", board: board, page: page %>
+ <%= render "boards/show/stream", board: board, page: page %>
-
- <%= render partial: "boards/show/column", collection: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %>
- <%= render "boards/show/closed", board: board %>
+
+ <%= 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 %>
-
+ <%= render "boards/show/menu/columns", board: board %>
<% end %>
diff --git a/app/views/boards/show/_expander.html.erb b/app/views/boards/show/_expander.html.erb
index 91e6466da..d01debadf 100644
--- a/app/views/boards/show/_expander.html.erb
+++ b/app/views/boards/show/_expander.html.erb
@@ -1,6 +1,9 @@
<%= count > 99 ? "99+" : count %>
- <%= title %>
+
+ <%= title %>
+ <%= icon_tag "collapse" %>
+
<%= yield if block_given? %>
diff --git a/app/views/boards/show/_not_now.html.erb b/app/views/boards/show/_not_now.html.erb
index cdb40fe79..414b608c9 100644
--- a/app/views/boards/show/_not_now.html.erb
+++ b/app/views/boards/show/_not_now.html.erb
@@ -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) %>
+
+
+ <%= column_frame_tag :not_now_column, src: board_columns_not_now_path(board) %>
+
diff --git a/app/views/boards/show/_stream.html.erb b/app/views/boards/show/_stream.html.erb
index f8b9cd81f..f4ea59ea1 100644
--- a/app/views/boards/show/_stream.html.erb
+++ b/app/views/boards/show/_stream.html.erb
@@ -1,20 +1,23 @@
">
-
- Maybe?
- <%= 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" %>
- Expand column
+
+
+ <%= 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 %>
-
-
- <%= 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 %>
+
diff --git a/app/views/boards/show/menu/_column.html.erb b/app/views/boards/show/menu/_column.html.erb
index 52637cce4..fa3ab38ff 100644
--- a/app/views/boards/show/menu/_column.html.erb
+++ b/app/views/boards/show/menu/_column.html.erb
@@ -1,4 +1,4 @@
-
+
<%= icon_tag "menu-dots-horizontal", class: "translucent" %>
Column options
diff --git a/app/views/boards/show/menu/_columns.html.erb b/app/views/boards/show/menu/_columns.html.erb
index 6d0ab0c90..70aac7c90 100644
--- a/app/views/boards/show/menu/_columns.html.erb
+++ b/app/views/boards/show/menu/_columns.html.erb
@@ -1,6 +1,7 @@
-
-
+
+
<%= icon_tag "add", class: "translucent" %>
+ Add a column
<%= render "boards/show/menu/column_form", board: board, column: Column.new, label: "Add column" %>
diff --git a/app/views/boards/show/menu/_expand.html.erb b/app/views/boards/show/menu/_maximize.html.erb
similarity index 54%
rename from app/views/boards/show/menu/_expand.html.erb
rename to app/views/boards/show/menu/_maximize.html.erb
index 4413739d1..0194dbea5 100644
--- a/app/views/boards/show/menu/_expand.html.erb
+++ b/app/views/boards/show/menu/_maximize.html.erb
@@ -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" %>
Expand column
<% end %>
diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb
index c5b1c43ea..07ad6531b 100644
--- a/app/views/cards/assignments/new.html.erb
+++ b/app/views/cards/assignments/new.html.erb
@@ -5,7 +5,11 @@
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
-
+
+
+
+ a
+
<% if @users.many? %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true,
diff --git a/app/views/cards/columns/edit.html.erb b/app/views/cards/columns/edit.html.erb
index ae38fed17..e8061f9d6 100644
--- a/app/views/cards/columns/edit.html.erb
+++ b/app/views/cards/columns/edit.html.erb
@@ -1,4 +1,4 @@
-<%= turbo_frame_tag dom_id(@card, :columns) do %>
+<%= turbo_frame_tag @card, :columns do %>
Choose a column for this card
diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb
index ca5e731b8..cd46d7cb4 100644
--- a/app/views/cards/comments/_new.html.erb
+++ b/app/views/cards/comments/_new.html.erb
@@ -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 %>
- Post this comment
- <% end %>
+
+
+ <%= form.button class: "comment__submit btn btn--reversed",
+ data: { form_target: "submit" }, disabled: true do %>
+ Post
+ <% end %>
+
+ <%= hotkey_label([ "ctrl", "enter" ]) %>
+
+
<% end %>
diff --git a/app/views/cards/comments/reactions/_reactions.html.erb b/app/views/cards/comments/reactions/_reactions.html.erb
index bf3ff746a..6de217842 100644
--- a/app/views/cards/comments/reactions/_reactions.html.erb
+++ b/app/views/cards/comments/reactions/_reactions.html.erb
@@ -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 } %>
Add your own reaction
diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb
index c40da47b9..941122578 100644
--- a/app/views/cards/container/_closure.html.erb
+++ b/app/views/cards/container/_closure.html.erb
@@ -7,8 +7,11 @@
<% end %>
<% 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 %>
Mark as Done
+ d
<% end %>
diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb
index ee18dec9f..98b0b3bb6 100644
--- a/app/views/cards/container/_title.html.erb
+++ b/app/views/cards/container/_title.html.erb
@@ -4,9 +4,16 @@
<%= link_to card.title, edit_card_path(card), class: "card__title-link" %>
-
- <%= card.description %>
-
+ <% unless card.description.blank? %>
+
+ <%= card.description %>
+
+ <% end %>
+
+ <%= link_to edit_card_path(card), class: "btn fit-content txt-small", data: { controller: "hotkey", action: "keydown.e@document->hotkey#click" } do %>
+
Edit
+
e
+ <% end %>
<% end %>
<% else %>
<%= form_with model: card, id: "card_form", data: { controller: "autoresize auto-save" } do |form| %>
@@ -19,7 +26,7 @@
<% end %>
- <%= 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) %>
diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb
index a2317ce08..1c898c30d 100644
--- a/app/views/cards/container/footer/_draft.html.erb
+++ b/app/views/cards/container/footer/_draft.html.erb
@@ -1,7 +1,13 @@
- <%= 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 %>
+ Create card
+ <% 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 %>
+ Create and add another
+ <% end %>
diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb
index c07ec9e6d..d4455dcbe 100644
--- a/app/views/cards/display/common/_assignees.html.erb
+++ b/app/views/cards/display/common/_assignees.html.erb
@@ -1,16 +1,16 @@
-
- <% card.assignees.each do |assignee| %>
- <%= avatar_tag assignee, tabindex: (local_assigns.key?(:preview) && local_assigns[:preview]) ? -1 : 0 %>
- <% end %>
+
>
+
+ <% card.assignees.each do |assignee| %>
+ <%= avatar_preview_tag assignee, tabindex: (local_assigns.key?(:preview) && local_assigns[:preview]) ? -1 : 0 %>
+ <% end %>
- >
-
+
<%= icon_tag "person-add" %>
Assign
-
+
+
-
- <%= yield %>
-
-
+
+ <%= yield %>
+
diff --git a/app/views/cards/display/perma/_assignees.html.erb b/app/views/cards/display/perma/_assignees.html.erb
index a115f39b2..1292573d4 100644
--- a/app/views/cards/display/perma/_assignees.html.erb
+++ b/app/views/cards/display/perma/_assignees.html.erb
@@ -1,3 +1,3 @@
<%= render "cards/display/common/assignees", card: card do %>
- <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), refresh: "morph" %>
+ <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), loading: :lazy, refresh: "morph" %>
<% end %>
diff --git a/app/views/cards/display/perma/_board.html.erb b/app/views/cards/display/perma/_board.html.erb
index f816683b9..e78a33579 100644
--- a/app/views/cards/display/perma/_board.html.erb
+++ b/app/views/cards/display/perma/_board.html.erb
@@ -6,7 +6,7 @@
- <%= turbo_frame_tag "board_picker", src: edit_card_board_path(card), target: "_top", refresh: "morph" %>
+ <%= turbo_frame_tag "board_picker", src: edit_card_board_path(card), target: "_top", loading: :lazy, refresh: "morph" %>
<% end %>
diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb
index 015f7990a..0a5bbb1bf 100644
--- a/app/views/cards/display/perma/_tags.html.erb
+++ b/app/views/cards/display/perma/_tags.html.erb
@@ -1,12 +1,16 @@
-
>
-
+ >
+
<%= icon_tag "tag" %>
Add a tag
-
- <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), refresh: "morph" %>
+
+ <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), loading: :lazy, refresh: :morph %>
diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb
index 1a1eda843..58b2d7bde 100644
--- a/app/views/cards/edit.html.erb
+++ b/app/views/cards/edit.html.erb
@@ -9,13 +9,16 @@
<% end %>
- <%= 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: { local_save_target: "input", action: "lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do %>
<%= general_prompts(@card.board) %>
<% end %>
- <%= form.button "Save changes", type: :submit, class: "btn btn--reversed fit-content", style: "--btn-background: #{@card.color}" %>
+ <%= form.button type: :submit, class: "btn btn--reversed fit-content txt-small", data: { form_target: "submit" } do %>
+ Save changes
+ <%= hotkey_label([ "ctrl", "enter" ]) %>
+ <% end %>
<%= link_to "Close editor and discard changes", @card, data: { form_target: "cancel" }, hidden: true %>
<% end %>
<% end %>
diff --git a/app/views/cards/pins/show.html.erb b/app/views/cards/pins/show.html.erb
index c8ac69d7e..37f06232a 100644
--- a/app/views/cards/pins/show.html.erb
+++ b/app/views/cards/pins/show.html.erb
@@ -1,3 +1,3 @@
-<%= turbo_frame_tag dom_id(@card, :pin) do %>
+<%= turbo_frame_tag @card, :pin do %>
<%= render "cards/pins/pin_button", card: @card %>
<% end %>
diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb
index b96a3a7cb..596006ce8 100644
--- a/app/views/cards/show.html.erb
+++ b/app/views/cards/show.html.erb
@@ -11,6 +11,7 @@
<% end %>
+<%= turbo_stream_from @card %>
<%= turbo_stream_from @card, :activity %>
diff --git a/app/views/cards/taggings/new.html.erb b/app/views/cards/taggings/new.html.erb
index d629cfb88..e348f531f 100644
--- a/app/views/cards/taggings/new.html.erb
+++ b/app/views/cards/taggings/new.html.erb
@@ -5,24 +5,19 @@
data-navigable-list-actionable-items-value="true"
data-action="keydown->navigable-list#navigate filter:changed->navigable-list#reset dialog:show@document->navigable-list#reset"
>
-
+
+ t
+
<%= form_with url: card_taggings_path(@card),
id: dom_id(@card, :tags_form),
data: { controller: "form", action: "submit->form#preventEmptySubmit" },
class: "flex flex-column gap-half full-width margin-block-half" do |form| %>
<%= form.text_field :tag_title, placeholder: @tags.any? ? "Add a new tag or filter…" : "Name this tag…", class: "input txt-small full-width",
- autocomplete: "off", data: { filter_target: "input", form_target: "input", action: "input->filter#filter" } %>
+ autocomplete: "off", autofocus: true, data: { filter_target: "input", form_target: "input", action: "input->filter#filter" } %>
<% end %>
<% end %>
diff --git a/app/views/cards/triage/_columns.html.erb b/app/views/cards/triage/_columns.html.erb
index 591a8e256..5a92a0126 100644
--- a/app/views/cards/triage/_columns.html.erb
+++ b/app/views/cards/triage/_columns.html.erb
@@ -1 +1 @@
-<%= turbo_frame_tag dom_id(card, :columns), src: edit_card_column_path(card), target: "_top", refresh: "morph" %>
+<%= turbo_frame_tag card, :columns, src: edit_card_column_path(card), target: "_top", refresh: "morph" %>
diff --git a/app/views/cards/watches/show.html.erb b/app/views/cards/watches/show.html.erb
index 44892190d..5c62559bb 100644
--- a/app/views/cards/watches/show.html.erb
+++ b/app/views/cards/watches/show.html.erb
@@ -1,3 +1,3 @@
-<%= turbo_frame_tag dom_id(@card, :watch) do %>
+<%= turbo_frame_tag @card, :watch do %>
<%= render "cards/watches/watch_button", card: @card %>
<% end %>
diff --git a/app/views/entropy/_auto_close.html.erb b/app/views/entropy/_auto_close.html.erb
index 73655d0f6..6ccb5d205 100644
--- a/app/views/entropy/_auto_close.html.erb
+++ b/app/views/entropy/_auto_close.html.erb
@@ -1,4 +1,5 @@
<% url = local_assigns[:url] %>
+<% disabled = local_assigns[:disabled] %>
@@ -7,7 +8,8 @@
form: form,
name: :auto_postpone_period,
knob_options: entropy_auto_close_options,
- label: "Days until auto-close" %>
+ label: "Days until auto-close",
+ disabled: disabled %>
<% end %>
diff --git a/app/views/entropy/_knob.html.erb b/app/views/entropy/_knob.html.erb
index bfe92ed71..69bbb5fb0 100644
--- a/app/views/entropy/_knob.html.erb
+++ b/app/views/entropy/_knob.html.erb
@@ -13,7 +13,8 @@
action: "change->knob#optionChanged change->form#submit",
index: index,
knob_target: "option"
- } %>
+ },
+ disabled: disabled %>
<%= value %>
<% end %>
@@ -23,7 +24,8 @@
data: { action: "input->knob#sliderChanged change->form#submit", knob_target: "slider" },
"aria-hidden": true,
max: knob_options.length - 1,
- min: 0
+ min: 0,
+ disabled: disabled
%>
diff --git a/app/views/events/_day_timeline.html.erb b/app/views/events/_day_timeline.html.erb
deleted file mode 100644
index 244414d2a..000000000
--- a/app/views/events/_day_timeline.html.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-<% day_timeline.events.group_by { it.created_at.hour }.each do |hour, events_grouped_by_hour| %>
- <% events_grouped_by_hour.group_by { event_column(it) }.each do |column, events| %>
- <%= event_cluster_tag(hour, column) do %>
- <%= render partial: "events/event", collection: events %>
- <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %>
- <% end %>
- <% end %>
-<% end %>
diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb
index aefe9258e..6e036a412 100644
--- a/app/views/events/day_timeline/_column.html.erb
+++ b/app/views/events/day_timeline/_column.html.erb
@@ -1,17 +1,15 @@
-<% column = event_columns(event_type, day_timeline) %>
-
-
<%= column[:title] %>
- <% column[:events].group_by { it.created_at.hour }.each do |hour, events| %>
- <%= event_cluster_tag(hour, column[:index]) do %>
+
<%= column.title %>
+ <% column.events_by_hour.each do |hour, events| %>
+ <%= events_at_hour_container(column, hour) do %>
<%= render partial: "events/event", collection: events, cached: true %>
<%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %>
<% end %>
<% end %>
- <% if column[:events].count < column[:full_events_count] %>
-
diff --git a/app/views/events/day_timeline/_columns.html.erb b/app/views/events/day_timeline/_columns.html.erb
index 5391736e9..580c7312c 100644
--- a/app/views/events/day_timeline/_columns.html.erb
+++ b/app/views/events/day_timeline/_columns.html.erb
@@ -1,9 +1,9 @@
<% cache [ day_timeline.events ] do %>
<% if day_timeline.has_activity? %>
- <%= render "events/day_timeline/column", event_type: "added", day_timeline: day_timeline %>
- <%= render "events/day_timeline/column", event_type: "updated", day_timeline: day_timeline %>
- <%= render "events/day_timeline/column", event_type: "closed", day_timeline: day_timeline %>
+ <%= render "events/day_timeline/column", column: day_timeline.added_column %>
+ <%= render "events/day_timeline/column", column: day_timeline.updated_column %>
+ <%= render "events/day_timeline/column", column: day_timeline.closed_column %>
<% end %>
<% end %>
diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb
index 0fc767862..08c406002 100644
--- a/app/views/events/event/_layout.html.erb
+++ b/app/views/events/event/_layout.html.erb
@@ -18,7 +18,7 @@
- <% if event_column(event) == 2 %>
+ <% unless event.action.in?(%w[ card_closed card_published card_reopened ]) %>
<%= icon_tag event_action_icon(event) %>
@@ -32,7 +32,7 @@
- <%= event_action_sentence(event) %>
+ <%= event.description_for(Current.user).to_html %>
<%= yield %>
diff --git a/app/views/events/index/_add_card_button.html.erb b/app/views/events/index/_add_card_button.html.erb
index c22841cf5..028407b79 100644
--- a/app/views/events/index/_add_card_button.html.erb
+++ b/app/views/events/index/_add_card_button.html.erb
@@ -2,8 +2,16 @@
<% if board = user_filtering.single_board_or_first %>
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link btn--circle-mobile", data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %>
<%= icon_tag "add", class: "show-on-touch" %>
- Add a card
+ Add a card
C
<% end %>
<% end %>
+
+
diff --git a/app/views/filters/_filter_toggle.html.erb b/app/views/filters/_filter_toggle.html.erb
index 76f599384..09e65c583 100644
--- a/app/views/filters/_filter_toggle.html.erb
+++ b/app/views/filters/_filter_toggle.html.erb
@@ -1,13 +1,13 @@
<% if filter.persisted? %>
- <%= button_to filter_path(filter), method: :delete, class: "btn txt-x-small btn--reversed", form_class: "inline" do %>
+ <%= button_to filter_path(filter), method: :delete, class: "btn txt-x-small btn--reversed", data: { controller: "tooltip" }, form_class: "inline" do %>
<%= icon_tag "bookmark-outline" %>
- Remove saved filter
+ Delete custom view
<% end %>
<% else %>
- <%= button_to filters_path(filter.as_params), method: :post, class: "btn txt-x-small", form_class: "inline" do %>
+ <%= button_to filters_path(filter.as_params), method: :post, class: "btn txt-x-small", data: { controller: "tooltip" }, form_class: "inline" do %>
<%= icon_tag "bookmark-outline" %>
- Save filter
+ Save custom view
<% end %>
<% end %>
diff --git a/app/views/filters/_settings.html.erb b/app/views/filters/_settings.html.erb
index ae604011c..52137b5de 100644
--- a/app/views/filters/_settings.html.erb
+++ b/app/views/filters/_settings.html.erb
@@ -1,7 +1,7 @@
<%= tag.aside \
class: class_names("filters margin-block-end", { "filters--expanded": user_filtering.expanded? }),
data: {
- controller: "toggle-enable toggle-class filter-settings",
+ controller: "toggle-enable toggle-class filter-settings dialog-manager",
toggle_class_toggle_class: "filters--expanded",
filter_settings_filters_set_class: "filters--has-filters-set",
filter_settings_no_filtering_url_value: no_filtering_url,
diff --git a/app/views/filters/create.turbo_stream.erb b/app/views/filters/create.turbo_stream.erb
index 6bcd97ee4..c0fc91e7c 100644
--- a/app/views/filters/create.turbo_stream.erb
+++ b/app/views/filters/create.turbo_stream.erb
@@ -1,2 +1 @@
<%= turbo_stream.replace("filter-settings-save-toggle", partial: "filters/filter_toggle", locals: { filter: @filter }) %>
-<%= turbo_stream.replace("my-filters", partial: "my/menus/saved_filter", locals: { filters: @filters }) %>
diff --git a/app/views/filters/destroy.turbo_stream.erb b/app/views/filters/destroy.turbo_stream.erb
index 6bcd97ee4..c0fc91e7c 100644
--- a/app/views/filters/destroy.turbo_stream.erb
+++ b/app/views/filters/destroy.turbo_stream.erb
@@ -1,2 +1 @@
<%= turbo_stream.replace("filter-settings-save-toggle", partial: "filters/filter_toggle", locals: { filter: @filter }) %>
-<%= turbo_stream.replace("my-filters", partial: "my/menus/saved_filter", locals: { filters: @filters }) %>
diff --git a/app/views/filters/settings/_assignees.html.erb b/app/views/filters/settings/_assignees.html.erb
index 996116d63..0a60a4878 100644
--- a/app/views/filters/settings/_assignees.html.erb
+++ b/app/views/filters/settings/_assignees.html.erb
@@ -1,5 +1,5 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: "quick-filter position-relative",
+<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
diff --git a/app/views/filters/settings/_boards.html.erb b/app/views/filters/settings/_boards.html.erb
index 5caf96d65..97a21618d 100644
--- a/app/views/filters/settings/_boards.html.erb
+++ b/app/views/filters/settings/_boards.html.erb
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: "quick-filter position-relative",
+<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
diff --git a/app/views/filters/settings/_closers.html.erb b/app/views/filters/settings/_closers.html.erb
index 5da789245..24e542b1e 100644
--- a/app/views/filters/settings/_closers.html.erb
+++ b/app/views/filters/settings/_closers.html.erb
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: "quick-filter position-relative",
+<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
diff --git a/app/views/filters/settings/_creators.html.erb b/app/views/filters/settings/_creators.html.erb
index 92385f7a2..8453fe745 100644
--- a/app/views/filters/settings/_creators.html.erb
+++ b/app/views/filters/settings/_creators.html.erb
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: "quick-filter position-relative",
+<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
diff --git a/app/views/filters/settings/_indexed_by.html.erb b/app/views/filters/settings/_indexed_by.html.erb
index 48bf476da..8175fea9e 100644
--- a/app/views/filters/settings/_indexed_by.html.erb
+++ b/app/views/filters/settings/_indexed_by.html.erb
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: class_names("quick-filter position-relative"),
+<%= tag.div class: class_names("quick-filter"),
data: { controller: "dialog combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
filter_show: user_filtering.show_indexed_by?,
diff --git a/app/views/filters/settings/_manage.html.erb b/app/views/filters/settings/_manage.html.erb
index 8a13490ad..333e9973e 100644
--- a/app/views/filters/settings/_manage.html.erb
+++ b/app/views/filters/settings/_manage.html.erb
@@ -3,7 +3,7 @@
<%= render "filters/filter_toggle", filter: filter %>
- <%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click"} do %>
+ <%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey tooltip", action: "keydown.esc@document->hotkey#click"} do %>
<%= icon_tag "close" %>
Clear all
<% end %>
diff --git a/app/views/filters/settings/_sorted_by.html.erb b/app/views/filters/settings/_sorted_by.html.erb
index 5379cfd17..e5c0adc4e 100644
--- a/app/views/filters/settings/_sorted_by.html.erb
+++ b/app/views/filters/settings/_sorted_by.html.erb
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: class_names("quick-filter position-relative"),
+<%= tag.div class: class_names("quick-filter"),
data: {
controller: "dialog combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
diff --git a/app/views/filters/settings/_time_window.html.erb b/app/views/filters/settings/_time_window.html.erb
index cfc827ecc..b747384ac 100644
--- a/app/views/filters/settings/_time_window.html.erb
+++ b/app/views/filters/settings/_time_window.html.erb
@@ -1,5 +1,5 @@
<% filter = user_filtering.filter %>
-<%= tag.div class: "quick-filter position-relative",
+<%= tag.div class: "quick-filter",
data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", filter_show: filter.public_send("#{name}_window").present? } do %>
diff --git a/app/views/filters/settings/_toggle.html.erb b/app/views/filters/settings/_toggle.html.erb
index 1f6459e0a..d1e7d10bf 100644
--- a/app/views/filters/settings/_toggle.html.erb
+++ b/app/views/filters/settings/_toggle.html.erb
@@ -1,11 +1,13 @@
<% filter = user_filtering.filter %>
<% if user_filtering.expanded? %>
-
+
<%= icon_tag "filter" %>
+ Close filter options
<% else %>
-
+
<%= icon_tag "filter" %>
+ Expand filter options
<% end %>
diff --git a/app/views/join_codes/new.html.erb b/app/views/join_codes/new.html.erb
index 5974c16ba..c4d933b8b 100644
--- a/app/views/join_codes/new.html.erb
+++ b/app/views/join_codes/new.html.erb
@@ -1,12 +1,7 @@
-<% @page_title = "Join #{@account_name}" %>
+<% @page_title = "Join #{@account_name} in Fizzy" %>
-
+
<%= @page_title %>
<%= form_with url: join_path(code: params[:code], tenant: params[:tenant]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
@@ -16,7 +11,7 @@
- Join
+ Continue
<%= icon_tag "arrow-right" %>
<% end %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 31a162329..d2af3e959 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -5,7 +5,7 @@
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb
index 5e3c6e69f..cd7502ec1 100644
--- a/app/views/layouts/mailer.html.erb
+++ b/app/views/layouts/mailer.html.erb
@@ -43,27 +43,29 @@
width: 100% !important;
}
- .avatar {
- border-radius: 50%;
- height: 2.75em;
- margin-right: 0.2.75em;
- max-height: 2.75em;
- min-height: 2.75em;
- overflow: hidden;
- width: 2.75em;
- }
-
.avatar__container {
- padding-top: 1em;
vertical-align: top;
width: 3.25em;
}
+ .avatar {
+ border-radius: 50%;
+ color: white;
+ display: block;
+ font-weight: 600;
+ height: 2.75em;
+ line-height: 2.75em;
+ mso-line-height-rule: exactly;
+ overflow: hidden;
+ text-align: center;
+ width: 2.75em;
+ }
+
.btn {
background: #2d71e5;
border-color: #2d71e5;
border-radius: 3rem;
- color: #ffffff;
+ color: white !important;
font-weight: 500;
padding: 0.5em 1em;
text-decoration: none;
diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb
index c217f44bb..2f1e2be3d 100644
--- a/app/views/layouts/public.html.erb
+++ b/app/views/layouts/public.html.erb
@@ -1,16 +1,18 @@
-
+
<%= render "layouts/shared/head" %>
diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb
index 997bc48db..fd4b7cab7 100644
--- a/app/views/layouts/shared/_head.html.erb
+++ b/app/views/layouts/shared/_head.html.erb
@@ -1,8 +1,7 @@
<%= page_title_tag %>
-
-
+
<% unless @disable_view_transition %>
<% end %>
diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb
index 438dc18e9..e07fdedf1 100644
--- a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb
+++ b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb
@@ -1,9 +1,8 @@
-Confirm your email address change
-Hit the button below to use this email address in Fizzy.
+Confirm your email address change
<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %>
If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.
-
diff --git a/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb b/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb
index b966ecfa6..8ba613348 100644
--- a/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb
+++ b/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb
@@ -1,12 +1,14 @@
-Sign in to Fizzy
-Hit the button below to sign in to Fizzy on this device
-<%= link_to "Sign in to Fizzy", session_magic_link_url(code: @magic_link.code), class: "btn" %>
+<% if @identity.memberships.any? %>
+ Fizzy verification code
+ Please enter this 6-character verification code on the Fizzy sign-in page:
+<% else %>
+ Welcome to Fizzy!
+ Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
+<% end %>
-If you’re signing in on another device, enter this special code:
- <%= @magic_link.code %>
-
-
-
diff --git a/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb b/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb
index f44e1c908..2de3e5533 100644
--- a/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb
+++ b/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb
@@ -1,8 +1,7 @@
-Sign in to Fizzy
-<%= "=" * 80 %>
+<% if @identity.memberships.any? %>
+Please enter this 6-character verification code on the Fizzy sign-in page:
+<% else %>
+Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
+<% end %>
-Open this link in your browser to login on this device:
-<%= session_magic_link_url(code: @magic_link.code) %>
-
-If you're signing in on another device, enter this code:
<%= @magic_link.code %>
diff --git a/app/views/mailers/notification/bundle_mailer/_notification.html.erb b/app/views/mailers/notification/bundle_mailer/_notification.html.erb
index 5dfddcf31..543f1515c 100644
--- a/app/views/mailers/notification/bundle_mailer/_notification.html.erb
+++ b/app/views/mailers/notification/bundle_mailer/_notification.html.erb
@@ -1,8 +1,15 @@
- <%= image_tag user_avatar_url(notification.creator), alt: notification.creator.name, class: "avatar", size: 48 %>
+
<%= render "notification/bundle_mailer/header", notification: notification %>
+
+
+
+
+ <%= mail_avatar_tag(notification.creator) %>
+
+
<%= render "notification/bundle_mailer/#{notification.source_type.underscore}/body", notification: notification %>
diff --git a/app/views/memberships/email_addresses/create.html.erb b/app/views/memberships/email_addresses/create.html.erb
index 53851105d..44711b5cf 100644
--- a/app/views/memberships/email_addresses/create.html.erb
+++ b/app/views/memberships/email_addresses/create.html.erb
@@ -1,14 +1,17 @@
<% @page_title = "Confirm your new email address" %>
<% content_for :header do %>
-