diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index f426a042e..13975ae62 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -16,7 +16,7 @@ --block-space-double: calc(var(--block-space) * 2); /* Text */ - --font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + --font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Fizzy", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --font-serif: ui-serif, serif; --font-mono: ui-monospace, monospace; @@ -213,7 +213,7 @@ --color-terminal-text: var(--color-ink); --color-terminal-text-light: var(--color-ink-lighter); --color-golden: oklch(89.1% 0.178 95.7); - --color-considering: oklch(var(--lch-blue-medium)); + --color-maybe: oklch(var(--lch-blue-medium)); /* Colors: Cards */ --color-card-default: oklch(var(--lch-blue-dark)); diff --git a/app/assets/stylesheets/base.css b/app/assets/stylesheets/base.css index 05d2b016e..689b80215 100644 --- a/app/assets/stylesheets/base.css +++ b/app/assets/stylesheets/base.css @@ -102,7 +102,8 @@ } /* Turbo */ - turbo-frame { + turbo-frame, + turbo-cable-stream-source { display: contents; } diff --git a/app/assets/stylesheets/blank-slates.css b/app/assets/stylesheets/blank-slates.css index 3e0e7615c..aa221c452 100644 --- a/app/assets/stylesheets/blank-slates.css +++ b/app/assets/stylesheets/blank-slates.css @@ -11,7 +11,7 @@ white-space: nowrap; } - .cards--considering & { + .cards--maybe & { background-color: var(--card-bg-color) !important; } diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 07021a330..6aaacaf8b 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -1,4 +1,33 @@ @layer components { + /* Layout adjustments for contained scrolling + /* ------------------------------------------------------------------------ */ + + /* Scroll columns individually on mobile */ + @media (max-width: 639px) { + body.contained-scrolling { + block-size: 100dvh; + grid-template-rows: 1fr var(--footer-height); + + #global-container { + display: grid; + grid-template-rows: auto 1fr; + overflow: hidden; + } + + #main { + display: grid; + grid-template-rows: auto auto 1fr; + overflow: auto; + padding: 0; + } + + /* Adapt the grid to public views (no filters or watchers sections) */ + &.public #main { + grid-template-rows: 1fr; + } + } + } + /* Column container /* ------------------------------------------------------------------------ */ @@ -10,7 +39,7 @@ --bubble-size: 3.5rem; --cards-gap: min(1.2cqi, 1.7rem); --column-gap: 8px; - --column-padding-expanded: calc(var(--column-gap) * 2); + --column-padding: calc(var(--column-gap) * 2); --column-transition-duration: 300ms; --column-width-collapsed: 40px; --column-width-expanded: 450px; @@ -21,17 +50,22 @@ container-type: inline-size; display: grid; gap: var(--column-gap); - grid-template-columns: 1fr var(--column-width-expanded) 1fr; + grid-template-columns: 1fr auto 1fr; + inline-size: 100%; margin-inline: auto; max-inline-size: var(--main-width); + outline: none; overflow-x: auto; overflow-y: hidden; - padding-block-end: var(--column-width-collapsed); position: relative; /* When it has something expanded */ - &:has(.card-columns__left .cards:not(.is-collapsed), .card-columns__right .cards:not(.is-collapsed)) { - grid-template-columns: auto var(--column-width-expanded) auto; + &:has(.card-columns__left .is-expanded, .card-columns__right .is-expanded) { + grid-template-columns: auto auto auto; + + @media (min-width: 640px) { + grid-template-columns: auto var(--column-width-expanded) auto; + } } &:has(.cards) { @@ -39,11 +73,19 @@ min-block-size: 20lh; } - body:not(.public) & { - @media (max-width: 519px) { - display: none; + @media (max-width: 639px) { + --column-width-expanded: calc(100vw - var(--column-gap) * 4); + + scroll-snap-type: inline mandatory; + + &:not(:has(.is-expanded)) { + grid-template-columns: auto var(--column-width-collapsed) auto; } } + + @media (min-width: 640px) { + padding-block-end: var(--column-width-collapsed); + } } .card-columns__left, @@ -51,16 +93,27 @@ align-items: stretch; display: flex; gap: var(--column-gap); + position: relative; + + @media (max-width: 639px) { + min-block-size: 0; + } } .card-columns__left { justify-content: end; + margin-inline-start: auto; padding-inline-start: var(--column-gap); + + @media (max-width: 639px) { + padding-inline-start: calc(var(--column-gap) * 2); + } } .card-columns__right { justify-content: start; padding-inline-end: var(--column-gap); + margin-inline-end: auto; } /* Column @@ -72,7 +125,13 @@ inline-size: var(--column-width-expanded); outline: none; position: relative; - scroll-snap-align: start; + scroll-snap-align: center; + + &.is-expanded { + @media (max-width: 639px) { + overflow: hidden; + } + } &.is-collapsed { inline-size: var(--column-width-collapsed); @@ -126,15 +185,17 @@ .cards__transition-container { block-size: 100%; border-radius: calc(var(--column-width-collapsed) / 2); - translate: 0; + translate: 0 0.5ch; /* Allow a little room for the mini bubble */ transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle); - .is-collapsed:not(.cards--considering) & { - translate: 0 var(--column-width-collapsed); - } + @media (min-width: 640px) { + .is-expanded & { + translate: 0 0.5ch; /* Allow a little room for the mini bubble */ + } - .cards:not(.is-collapsed) & { - padding-inline: var(--column-padding-expanded); + .is-collapsed & { + translate: 0 var(--column-width-collapsed); + } } .drag-and-drop__hover-container & { @@ -151,29 +212,33 @@ .no-transitions & { transition: none; } + + /* Use flex so the __list container can take up the remaining space for scrolling */ + @media (max-width: 639px) { + .is-expanded & { + display: flex; + flex-direction: column; + } + } } /* The wrapper around the cards used to clip overflow while transitioning. * Also, don't resize cards while transitioning to avoid reflow. */ .cards__list { - align-items: flex-end; /* use flex-start to wipe from left */ display: flex; flex-direction: column; gap: var(--cards-gap); - margin-block-start: -1ch; - margin-inline: -1ch; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; - .cards:not(.is-collapsed) & { - padding: 1ch; - } + .is-expanded & { + padding: var(--column-padding); - .card { - inline-size: calc(var(--column-width-expanded) - var(--column-padding-expanded) * 2); - } - - .cards--grid & { - display: contents; + /* Use the rest of the column height for scrolling */ + @media (max-width: 639px) { + flex: 1; + padding-inline: calc(var(--column-padding) / 4); + } } [aria-selected] & .card[aria-selected] { @@ -193,7 +258,17 @@ } .cards__new-column { - margin-block-start: var(--column-width-collapsed); + position: relative; + + @media (max-width: 639px) { + inset-inline-end: 0; + position: absolute; + translate: 100%; + } + + @media (min-width: 640px) { + margin-block-start: var(--column-width-collapsed); + } } /* Cards grid; used when filtering @@ -204,11 +279,7 @@ --card-grid-columns: 1; container-type: inline-size; - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: var(--cards-gap); - justify-content: center; + inline-size: 100%; margin-inline: auto; max-inline-size: var(--main-width); @@ -220,6 +291,15 @@ --card-grid-columns: 3; } + .cards__list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--cards-gap); + justify-content: center; + padding: 1ch; + } + .card { inline-size: calc((100% - var(--cards-gap) * (var(--card-grid-columns) - 1) ) / var(--card-grid-columns)); } @@ -245,11 +325,11 @@ block-size: 100%; } - .cards:not(.is-collapsed) & { + .cards.is-expanded & { 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)); + padding-inline: var(--column-padding); } } @@ -292,12 +372,18 @@ font-weight: 600; gap: 0.5ch; grid-area: expander; + justify-content: center; + outline: none; outline-offset: -2px; position: relative; text-transform: uppercase; + &[disabled] { + opacity: 1; + } + @media (any-hover: hover) { - &:hover { + .is-collapsed:hover { filter: brightness(0.9); } } @@ -338,7 +424,7 @@ block-size: 100%; content: ""; inline-size: 1px; - inset: 0 auto; + inset-block: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment)) 0; position: absolute; z-index: -2; } @@ -351,22 +437,17 @@ } } - .cards:not(.is-collapsed) & { + .cards.is-expanded & { inline-size: 100%; justify-content: center; } - - .cards:is(.cards--considering) &:hover { - cursor: unset; - filter: none; - } } .cards__expander-count { line-height: var(--column-width-collapsed); inline-size: var(--column-width-collapsed); - .cards:not(.is-collapsed) & { + .cards.is-expanded & { display: none; } } @@ -384,7 +465,7 @@ writing-mode: vertical-rl; } - .cards:not(.is-collapsed, .cards--considering) & { + .cards.is-expanded & { align-items: center; display: flex; gap: 0.25ch; @@ -394,74 +475,40 @@ .icon--collapse { --icon-size: 1.15em; - opacity: 0; - scale: 1.5; + opacity: 0.66; transition: 150ms ease-out; transition-property: opacity, scale; + @media (min-width: 640px) { + opacity: 0; + scale: 1.5; + } + .cards.is-collapsed & { display: none; } - .cards:not(.is-collapsed) .cards__expander:hover & { + .cards.is-expanded .cards__expander:hover & { opacity: 0.66; scale: 1; } } } - /* TODO: I think this is legacy now? */ - .cards__heading { - display: flex; - font-size: var(--text-medium); - inset-block-start: 0; - justify-content: center; - margin-block-start: 1px; - margin-inline: -8px; /* enough to cover card shadows, but avoid overlapping bubbles */ - padding-block: var(--cards-gap) 0; - position: sticky; - z-index: 3; - - /* Cover bubbles and cards on small viewports */ - @media (max-width: 639px) { - margin-inline: calc(-1 * var(--cards-gap) - var(--main-padding)); - } - - &, .cards__filter { - font-weight: bold; - text-transform: uppercase; - } - - .cards__filter { - --filter-block-padding: 0.5rem; - - background-color: var(--color-canvas); - border-radius: 99rem; - inline-size: auto; - margin-block: calc(-1 * var(--filter-block-padding)); - padding-block: var(--filter-block-padding); - } - - dialog { - font-weight: normal; - text-transform: none; - } - } - /* Override card styles within columns + /* Adding .board-tools here since it sits outside the cards container on mobile */ /* ------------------------------------------------------------------------ */ - .cards { - .card { - --block-space: 1em; - --block-space-half: 0.5em; - --card-padding-inline: 1em; - --text-xx-large: 1.6em; - --text-x-small: 1em; + .cards .card, + .board-tools { + --block-space: 1em; + --block-space-half: 0.5em; + --card-padding-inline: 1em; + --text-xx-large: 1.6em; + --text-x-small: 1em; - /* Set lower limit for font size */ - font-size: clamp(0.6rem, 0.85cqi, 100px); - } + /* Set lower limit for font size */ + font-size: clamp(0.6rem, 0.85cqi, 100px); .card__comments { --column-gap: 0.8ch; @@ -544,7 +591,7 @@ /* Considering /* ------------------------------------------------------------------------ */ - .cards--considering { + .cards--maybe { --card-color: oklch(var(--lch-blue-medium)); position: relative; @@ -598,15 +645,37 @@ --card-padding-block: var(--block-space); border: 1px solid var(--border-color); - margin-block-end: var(--cards-gap); + inline-size: auto; text-align: center; + @media (max-width: 639px) { + /* On mobile, hide the tool card inside the Maybe column */ + .cards & { + display: none; + } + + #cards_container > & { + margin: 0 3ch 1ch; + } + } + + @media (min-width: 640px) { + /* On desktop, hide the tool card above the columns */ + #cards_container > & { + display: none; + } + } + + @media (min-width: 800px) { + margin: var(--column-padding) var(--column-padding) 0; + } + &:has(dialog[open]) { z-index: 5; } - .separator--horizontal { - margin-block: var(--block-space); + .divider { + --divider-color: oklch(var(--lch-blue-light)); } .btn--link { @@ -620,7 +689,7 @@ footer { font-size: var(--text-x-small); - margin-block-end: calc(var(--block-space-half) * -1); + margin-block: 1ch calc(var(--block-space-half) * -1); } .overflow-count { @@ -671,18 +740,12 @@ } } - /* Closed (Done) - /* ------------------------------------------------------------------------ */ - - .cards--closed { - - } - /* Doing /* -------------------------------------------------------------------------- */ /* Surface a mini bubble if there are cards with bubbles inside */ - .cards--considering:has(.bubble:not([hidden])) .cards__expander-title, + .cards--maybe:has(.bubble:not([hidden])) .cards__expander-title, + .cards--maybe.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container, .cards--doing.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container { --bubble-color: var(--card-color, oklch(var(--lch-blue-medium))); --bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%; @@ -703,13 +766,14 @@ } /* Maybe column: position bubble relative to the title, not the container */ - .cards--considering & { + .cards--maybe.is-expanded & { overflow: visible; position: relative; &:before { inset-block-start: 50%; - translate: 125% -75%; + inset-inline-start: 0; + translate: -125% -75%; z-index: -1; } } @@ -749,54 +813,4 @@ } } } - - /* Mobile columns - /* -------------------------------------------------------------------------- */ - - .mobile-card-columns { - --column-gap: 8px; - --column-padding-expanded: calc(var(--column-gap) * 2); - --column-width-collapsed: 40px; - --progress-increment: var(--progress-max-width) / var(--progress-max-cards); - --progress-max-cards: 15; /* should match first geared pagination page size */ - --progress-max-width: 100%; - - padding-inline: 3vw; - - /* Hide on larger devices with cursors */ - @media (min-width: 520px) { - display: none; - } - - .cards.is-collapsed { - inline-size: auto; - display: block; - padding-block: 0.5ch; - - .cards__expander { - --gradient-direction: to right; - - flex-direction: row; - inline-size: auto; - - &:before { - block-size: 1px; - inline-size: 100%; - inset: 50% 0 auto; - translate: 0 -50%; - } - - &:after { - block-size: var(--column-width-collapsed); - inline-size: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment)); - margin-inline-start: 0; - max-inline-size: var(--progress-max-width); - } - } - - .cards__expander-title { - writing-mode: revert; - } - } - } } diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index c8453bc14..2ae664dcd 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -4,7 +4,7 @@ --comment-padding-block: var(--block-space-half); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; - --reaction-size: 1.6875rem; + --reaction-size: 2rem; display: flex; flex-direction: column; @@ -15,6 +15,10 @@ @media (min-width: 160ch) { padding-inline: var(--tray-size); } + + @media (min-width: 640px) { + --reaction-size: 1.6875rem; + } } .comments__subscribers { diff --git a/app/assets/stylesheets/drag_and_drop.css b/app/assets/stylesheets/drag_and_drop.css index 2ada036b4..03943be14 100644 --- a/app/assets/stylesheets/drag_and_drop.css +++ b/app/assets/stylesheets/drag_and_drop.css @@ -15,9 +15,5 @@ outline-offset: -2px; transition: background-color 200ms; z-index: 1; - - .cards__heading { - background-color: transparent; - } } } diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index b283fa447..efc04e21d 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -7,17 +7,19 @@ align-items: center; display: flex; flex-wrap: wrap; - gap: 1ch; gap: var(--inline-space-half); justify-content: center; + padding-block-start: 2px; /* prevents input focus-ring clipping on mobile */ position: relative; - view-transition-name: "filters"; + view-transition-name: filters; + z-index: 1; .btn { --btn-border-color: var(--color-ink-medium); --input-background: var(--color-canvas); } + &:has(dialog[open]), &:has([data-controller~="tooltip"]:hover) { z-index: calc(var(--z-nav) + 1); } diff --git a/app/assets/stylesheets/font-face.css b/app/assets/stylesheets/font-face.css new file mode 100644 index 000000000..086111ed4 --- /dev/null +++ b/app/assets/stylesheets/font-face.css @@ -0,0 +1,41 @@ +@layer base { + /* + Segoe UI Variable Fizzy font face configuration. + + 1. Segoe UI Variable (Weights 100-700): + Leverages variable font features to: + - Automatically adjust Weight (wght) dynamically within the 100-700 range. + - Automatically manage Optical Size (opsz) based on font-size. + + 2. Segoe UI Black (Weights 800-900): + Used as a fallback because Segoe UI Variable does not natively support 900 weight. + This ensures a consistent bold experience across all weights. + */ + @font-face { + font-family: "Segoe UI Variable Fizzy"; + src: local("Segoe UI Variable"); + font-weight: 100 700; + font-style: normal; + } + + @font-face { + font-family: "Segoe UI Variable Fizzy"; + src: local("Segoe UI Variable"); + font-weight: 100 700; + font-style: italic; + } + + @font-face { + font-family: "Segoe UI Variable Fizzy"; + src: local("Segoe UI Black"); + font-weight: 800 900; + font-style: normal; + } + + @font-face { + font-family: "Segoe UI Variable Fizzy"; + src: local("Segoe UI Black Italic"); + font-weight: 800 900; + font-style: italic; + } +} diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css index 5caaa56c0..ef65d7b6b 100644 --- a/app/assets/stylesheets/layout.css +++ b/app/assets/stylesheets/layout.css @@ -8,6 +8,16 @@ } } + /* Required for the card column page on mobile, but not needed otherwise */ + :where(#global-container) { + display: contents; + } + + :where(#header) { + position: relative; + z-index: var(--z-nav); + } + :where(#main) { inline-size: 100dvw; margin-inline: auto; diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index 0cf434da8..ada7324d2 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -108,7 +108,7 @@ } .notifications__status { + --panel-border-radius: 0.5em; --panel-padding: var(--block-space); } } - diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 189cfef5b..02ae8969e 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -155,6 +155,10 @@ max-inline-size: 100%; padding: var(--inline-space-half) var(--popup-item-padding-inline); text-align: start; + + &:focus-visible { + z-index: 1; + } } .popup__icon { diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index 8e1359e1b..5ed9c3fdf 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -126,7 +126,7 @@ .card--new, .cards__decoration, .card-columns:before, - .cards--considering:before { + .cards--maybe:before { display: none; } @@ -141,7 +141,7 @@ padding-inline: 0; } - .cards--considering { + .cards--maybe { background: none; margin: 0; padding-inline: 1ch; diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index 5dea448cc..d1cd0a03f 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -10,7 +10,7 @@ flex-wrap: wrap; gap: var(--inline-space-half); inline-size: 100%; - margin-block-start: calc(var(--block-space-half) / 2); + margin-block-start: var(--block-space-half); margin-inline: calc(var(--column-gap) / -1); &:has([open]) { @@ -44,6 +44,7 @@ } .reactions__trigger { + --btn-size: var(--reaction-size); --btn-border-color: var(--reaction-border-color); img { @@ -115,11 +116,13 @@ /* Make the avatar and delete buttons fit nicely within the reaction */ .reaction__avatar, + .reaction__avatar .avatar, .reaction__form-label, .reaction form { - block-size: var(--btn-size); + block-size: 100%; } + .reaction__delete { display: none; diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 91c04ff53..62304d020 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -96,12 +96,6 @@ .overflow-clip { text-overflow: clip; white-space: nowrap; overflow: hidden; } .overflow-ellipsis { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } - .overflow-hide-scrollbar::-webkit-scrollbar { - @media (pointer: course) { - display: none; - } - } - .overflow-line-clamp { -webkit-line-clamp: var(--lines, 2); -webkit-box-orient: vertical; diff --git a/app/controllers/cards/drafts_controller.rb b/app/controllers/cards/drafts_controller.rb new file mode 100644 index 000000000..a4e9388de --- /dev/null +++ b/app/controllers/cards/drafts_controller.rb @@ -0,0 +1,13 @@ +class Cards::DraftsController < ApplicationController + include CardScoped + + before_action :redirect_if_published + + def show + end + + private + def redirect_if_published + redirect_to @card unless @card.drafted? + end +end diff --git a/app/controllers/cards/engagements_controller.rb b/app/controllers/cards/engagements_controller.rb deleted file mode 100644 index 73b8e9c9d..000000000 --- a/app/controllers/cards/engagements_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -class Cards::EngagementsController < ApplicationController - include CardScoped - - def create - case params[:engagement] - when "doing" - @card.engage - when "on_deck" - @card.move_to_on_deck - end - render_card_replacement - end - - def destroy - @card.reconsider - render_card_replacement - end -end diff --git a/app/controllers/cards/publishes_controller.rb b/app/controllers/cards/publishes_controller.rb index 641f728cd..a0378eec3 100644 --- a/app/controllers/cards/publishes_controller.rb +++ b/app/controllers/cards/publishes_controller.rb @@ -5,7 +5,8 @@ class Cards::PublishesController < ApplicationController @card.publish if add_another_param? - redirect_to @board.cards.create!, notice: "Card added" + card = @board.cards.create!(status: :drafted) + redirect_to card_draft_path(card), notice: "Card added" else redirect_to @card.board end diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index e79eeb22b..e007527b8 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -3,6 +3,7 @@ class CardsController < ApplicationController before_action :set_board, only: %i[ create ] before_action :set_card, only: %i[ show edit update destroy ] + before_action :redirect_if_drafted, only: :show before_action :ensure_permission_to_administer_card, only: %i[ destroy ] def index @@ -13,7 +14,7 @@ class CardsController < ApplicationController respond_to do |format| format.html do card = Current.user.draft_new_card_in(@board) - redirect_to card + redirect_to card_draft_path(card) end format.json do @@ -56,6 +57,10 @@ class CardsController < ApplicationController @card = Current.user.accessible_cards.find_by!(number: params[:id]) end + def redirect_if_drafted + redirect_to card_draft_path(@card) if @card.drafted? + end + def ensure_permission_to_administer_card head :forbidden unless Current.user.can_administer_card?(@card) end diff --git a/app/controllers/users/roles_controller.rb b/app/controllers/users/roles_controller.rb index b881bdcfc..711308d13 100644 --- a/app/controllers/users/roles_controller.rb +++ b/app/controllers/users/roles_controller.rb @@ -4,7 +4,7 @@ class Users::RolesController < ApplicationController def update @user.update!(role_params) - redirect_to users_path + redirect_to account_settings_path end private diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 74deb686b..54c18704c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -30,7 +30,7 @@ class UsersController < ApplicationController @user.deactivate respond_to do |format| - format.html { redirect_to users_path } + format.html { redirect_to account_settings_path } format.json { head :no_content } end end diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index 69ef2efec..1ace03574 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -30,7 +30,9 @@ module AccessesHelper displayed_watchers = watchers.first(8) overflow_count = watchers.size - 8 - tag.strong(watchers.any? ? "Watching for new cards" : "No one is watching for new cards", class: "txt-uppercase") + + tag.div(class: "divider divider--fade") do + tag.strong(watchers.any? ? "Watching for new cards" : "No one is watching for new cards", class: "txt-uppercase") + end + tag.div(avatar_tags(displayed_watchers), class: "board-tools__watching") do tag.div(data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" }) do tag.button("+#{overflow_count}", class: "overflow-count btn btn--circle borderless", data: { action: "dialog#open" }, aria: { label: "Show #{overflow_count} more watchers" }) + diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb index d99c125cb..a92fb2824 100644 --- a/app/helpers/columns_helper.rb +++ b/app/helpers/columns_helper.rb @@ -11,7 +11,7 @@ module ColumnsHelper end def column_tag(id:, name:, drop_url:, collapsed: true, selected: nil, card_color: "var(--color-card-default)", data: {}, **properties, &block) - classes = token_list("cards", properties.delete(:class), "is-collapsed": collapsed) + classes = token_list("cards", properties.delete(:class), "is-collapsed": collapsed, "is-expanded": !collapsed) hotkeys_disabled = data[:card_hotkeys_disabled] data = { diff --git a/app/javascript/controllers/bar_controller.js b/app/javascript/controllers/bar_controller.js index 5e72e4e07..e83bce9a9 100644 --- a/app/javascript/controllers/bar_controller.js +++ b/app/javascript/controllers/bar_controller.js @@ -34,6 +34,7 @@ export default class extends Controller { showModalAndSubmit(event) { this.showModal() this.formTarget.requestSubmit() + this.#restoreFocusAfterTurboFrameLoads() } showModal() { @@ -51,6 +52,12 @@ export default class extends Controller { } } + #restoreFocusAfterTurboFrameLoads() { + this.turboFrameTarget.addEventListener("turbo:frame-load", () => { + this.searchInputTarget.focus() + }, { once: true }) + } + #loadTurboFrame() { this.turboFrameTarget.src = this.searchUrlValue } diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 2c97fcd09..1a9ba4174 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -2,10 +2,11 @@ import { Controller } from "@hotwired/stimulus" import { nextFrame, debounce } from "helpers/timing_helpers"; export default class extends Controller { - static classes = [ "collapsed", "noTransitions", "titleNotVisible" ] - static targets = [ "column", "button", "title" ] + static classes = [ "collapsed", "expanded", "noTransitions", "titleNotVisible" ] + static targets = [ "column", "button", "title", "maybeColumn" ] static values = { - board: String + board: String, + desktopBreakpoint: { type: String, default: "(min-width: 640px)" } } initialize() { @@ -15,6 +16,11 @@ export default class extends Controller { async connect() { await this.#restoreColumnsDisablingTransitions() this.#setupIntersectionObserver() + + this.mediaQuery = window.matchMedia(this.desktopBreakpointValue) + this.handleDesktop = this.#handleDesktop.bind(this) + this.mediaQuery.addEventListener("change", this.handleDesktop) + this.handleDesktop(this.mediaQuery) } disconnect() { @@ -22,10 +28,11 @@ export default class extends Controller { this._intersectionObserver.disconnect() this._intersectionObserver = null } + this.mediaQuery.removeEventListener("change", this.handleDesktop) } toggle({ target }) { - const column = target.closest('[data-collapsible-columns-target="column"]') + const column = target.closest('[data-collapsible-columns-target~="column"]') this.#toggleColumn(column); } @@ -41,7 +48,7 @@ export default class extends Controller { } focusOnColumn({ target }) { - if (this.#isCollapsed(target)) { + if (this.#isDesktop && this.#isCollapsed(target)) { this.#collapseAllExcept(target) this.#expand(target) } @@ -74,7 +81,9 @@ export default class extends Controller { } #collapseAllExcept(clickedColumn) { - this.columnTargets.forEach(column => { + const columns = this.#isDesktop ? this.columnTargets.filter(c => c !== this.maybeColumnTarget) : this.columnTargets + + columns.forEach(column => { if (column !== clickedColumn) { this.#collapse(column) } @@ -88,17 +97,25 @@ export default class extends Controller { #collapse(column) { const key = this.#localStorageKeyFor(column) - this.#buttonFor(column).setAttribute("aria-expanded", "false") + this.#buttonFor(column)?.setAttribute("aria-expanded", "false") + column.classList.remove(this.expandedClass) column.classList.add(this.collapsedClass) localStorage.removeItem(key) } - #expand(column) { - const key = this.#localStorageKeyFor(column) - - this.#buttonFor(column).setAttribute("aria-expanded", "true") + #expand(column, saveState = true) { + this.#buttonFor(column)?.setAttribute("aria-expanded", "true") column.classList.remove(this.collapsedClass) - localStorage.setItem(key, true) + column.classList.add(this.expandedClass) + + if (saveState) { + const key = this.#localStorageKeyFor(column) + localStorage.setItem(key, true) + } + + if (window.matchMedia('(max-width: 639px)').matches) { + column.scrollIntoView({ behavior: "smooth", inline: "center" }) + } } #buttonFor(column) { @@ -114,6 +131,7 @@ export default class extends Controller { #restoreColumn(column) { const key = this.#localStorageKeyFor(column) if (localStorage.getItem(key)) { + this.#collapseAllExcept(column) this.#expand(column) } } @@ -140,4 +158,33 @@ export default class extends Controller { this.titleTargets.forEach(title => this._intersectionObserver.observe(title)) } + + get #isDesktop() { + return this.mediaQuery?.matches + } + + #handleDesktop() { + this.#isDesktop ? this.#handleDesktopMode() : this.#handleMobileMode() + } + + async #handleDesktopMode() { + this.#expand(this.maybeColumnTarget, false) + this.#maybeButton.setAttribute("disabled", true) + } + + #handleMobileMode() { + this.#maybeButton.removeAttribute("disabled") + + const expandedColumn = this.columnTargets.find(column => column !== this.maybeColumnTarget && !this.#isCollapsed(column)) + + if (expandedColumn) { + this.#collapseAllExcept(expandedColumn) + } else { + this.#collapseAllExcept(this.maybeColumnTarget) + } + } + + get #maybeButton() { + return this.maybeColumnTarget.querySelector('[data-collapsible-columns-target="button"]') + } } diff --git a/app/models/card/closeable.rb b/app/models/card/closeable.rb index e4cea509f..ff947452b 100644 --- a/app/models/card/closeable.rb +++ b/app/models/card/closeable.rb @@ -31,6 +31,7 @@ module Card::Closeable def close(user: Current.user) unless closed? transaction do + not_now&.destroy create_closure! user: user track_event :closed, creator: user end diff --git a/app/models/card/engagement.rb b/app/models/card/engagement.rb deleted file mode 100644 index 2f45543dc..000000000 --- a/app/models/card/engagement.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Card::Engagement < ApplicationRecord - belongs_to :account, default: -> { card.account } - belongs_to :card, class_name: "::Card", touch: true - - validates :status, presence: true, inclusion: { in: %w[doing on_deck] } -end diff --git a/app/models/event/description.rb b/app/models/event/description.rb index f44233011..fd7a66ae1 100644 --- a/app/models/event/description.rb +++ b/app/models/event/description.rb @@ -14,7 +14,7 @@ class Event::Description end def to_plain_text - to_sentence(creator_name, h(card.title)) + to_sentence(creator_name, quoted(card.title)) end private @@ -41,6 +41,10 @@ class Event::Description h(event.creator.name) end + def quoted(text) + %("#{h text}") + end + def card @card ||= event.action.comment_created? ? event.eventable.card : event.eventable end diff --git a/app/models/webhook/delivery.rb b/app/models/webhook/delivery.rb index 3ebe49b06..50a0cd560 100644 --- a/app/models/webhook/delivery.rb +++ b/app/models/webhook/delivery.rb @@ -1,4 +1,6 @@ class Webhook::Delivery < ApplicationRecord + include Rails.application.routes.url_helpers + class ResponseTooLarge < StandardError; end STALE_TRESHOLD = 7.days @@ -130,8 +132,7 @@ class Webhook::Delivery < ApplicationRecord elsif webhook.for_campfire? render_payload(formats: :html) elsif webhook.for_slack? - html = render_payload(formats: :html) - { text: convert_html_to_mrkdwn(html) }.to_json + slack_payload else render_payload(formats: :json) end @@ -158,4 +159,16 @@ class Webhook::Delivery < ApplicationRecord document.text end + + def slack_payload + text = event.description_for(nil).to_plain_text + url = polymorphic_url(event.eventable, base_url_options.merge(script_name: account.slug)) + + { text: "#{text} <#{url}|Open in Fizzy>" }.to_json + end + + def base_url_options + Rails.application.routes.default_url_options.presence || + Rails.application.config.action_mailer.default_url_options + end end diff --git a/app/views/boards/columns/_empty_placeholder.html.erb b/app/views/boards/columns/_empty_placeholder.html.erb index 36507c1ff..d9da8a896 100644 --- a/app/views/boards/columns/_empty_placeholder.html.erb +++ b/app/views/boards/columns/_empty_placeholder.html.erb @@ -1,6 +1,4 @@ -
-
-

Drag cards here

-
-
No cards here
+
+

Drag cards here

+
No cards here
diff --git a/app/views/boards/columns/_list.html.erb b/app/views/boards/columns/_list.html.erb deleted file mode 100644 index 7adedf322..000000000 --- a/app/views/boards/columns/_list.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
- <%= render "cards/display/previews", cards: cards, draggable: draggable %> -
diff --git a/app/views/boards/columns/closeds/show.html.erb b/app/views/boards/columns/closeds/show.html.erb index 8a9d1f56c..110db1c8c 100644 --- a/app/views/boards/columns/closeds/show.html.erb +++ b/app/views/boards/columns/closeds/show.html.erb @@ -12,12 +12,14 @@
<%= turbo_frame_tag :closed_column do %> - <% if @page.used? %> - <%= with_automatic_pagination :closed_column, @page do %> - <%= render "boards/columns/list", cards: @page.records, draggable: true %> +
+ <% if @page.used? %> + <%= with_automatic_pagination :closed_column, @page do %> + <%= render "cards/display/previews", cards: @page.records, draggable: true %> + <% end %> + <% else %> + <%= render "boards/columns/empty_placeholder" %> <% end %> - <% else %> - <%= render "boards/columns/empty_placeholder" %> - <% end %> +
<% end %>
diff --git a/app/views/boards/columns/not_nows/show.html.erb b/app/views/boards/columns/not_nows/show.html.erb index 88ea33868..f744635ae 100644 --- a/app/views/boards/columns/not_nows/show.html.erb +++ b/app/views/boards/columns/not_nows/show.html.erb @@ -12,12 +12,14 @@
<%= turbo_frame_tag :not_now_column do %> - <% if @page.used? %> - <%= with_automatic_pagination :not_now_column, @page do %> - <%= render "boards/columns/list", cards: @page.records, draggable: true %> +
+ <% if @page.used? %> + <%= with_automatic_pagination :not_now_column, @page do %> + <%= render "cards/display/previews", cards: @page.records, draggable: true %> + <% end %> + <% else %> + <%= render "boards/columns/empty_placeholder" %> <% end %> - <% else %> - <%= render "boards/columns/empty_placeholder" %> - <% end %> +
<% end %>
diff --git a/app/views/boards/columns/show.html.erb b/app/views/boards/columns/show.html.erb index 4fef4eec7..cd6c396e7 100644 --- a/app/views/boards/columns/show.html.erb +++ b/app/views/boards/columns/show.html.erb @@ -12,12 +12,14 @@
<%= turbo_frame_tag @column, :cards do %> - <% if @page.used? %> - <%= with_automatic_pagination dom_id(@column, :cards), @page do %> - <%= render "boards/columns/list", cards: @page.records, draggable: true %> +
+ <% if @page.used? %> + <%= with_automatic_pagination dom_id(@column, :cards), @page do %> + <%= render "cards/display/previews", cards: @page.records, draggable: true %> + <% end %> + <% else %> + <%= render "boards/columns/empty_placeholder" %> <% end %> - <% else %> - <%= render "boards/columns/empty_placeholder" %> - <% end %> +
<% end %>
diff --git a/app/views/boards/columns/streams/show.html.erb b/app/views/boards/columns/streams/show.html.erb index 9f0477b39..6ec6ac339 100644 --- a/app/views/boards/columns/streams/show.html.erb +++ b/app/views/boards/columns/streams/show.html.erb @@ -12,12 +12,14 @@
<%= turbo_frame_tag :stream_column do %> - <% if @page.used? %> - <%= with_automatic_pagination :stream_column, @page do %> - <%= render "boards/columns/list", cards: @page.records, draggable: true %> +
+ <% if @page.used? %> + <%= with_automatic_pagination :stream_column, @page do %> + <%= render "cards/display/previews", cards: @page.records, draggable: true %> + <% end %> + <% else %> + <%= render "boards/columns/empty_placeholder" %> <% end %> - <% else %> - <%= render "boards/columns/empty_placeholder" %> - <% end %> +
<% end %>
diff --git a/app/views/boards/show.html.erb b/app/views/boards/show.html.erb index 9f0c2ebee..36f3cd552 100644 --- a/app/views/boards/show.html.erb +++ b/app/views/boards/show.html.erb @@ -1,4 +1,5 @@ <% @page_title = @board.name %> +<% @body_class = "contained-scrolling" %> <% turbo_exempts_page_from_cache %> <%= turbo_stream_from @board %> @@ -25,7 +26,7 @@ <% if @filter.used?(ignore_boards: true) %> <%= render "boards/show/filtered_cards", page: @page %> <% else %> + <%= render "columns/show/add_card_button", board: @board %> <%= render "boards/show/columns", page: @page, board: @board %> - <%= render "boards/show/mobile_columns", page: @page, board: @board %> <% end %> <% end %> diff --git a/app/views/boards/show/_columns.html.erb b/app/views/boards/show/_columns.html.erb index bf7a76fcc..191d18cbc 100644 --- a/app/views/boards/show/_columns.html.erb +++ b/app/views/boards/show/_columns.html.erb @@ -4,6 +4,7 @@ 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_expanded_class: "is-expanded", collapsible_columns_no_transitions_class: "no-transitions", collapsible_columns_title_not_visible_class: "is-off-screen", navigable_list_supports_vertical_navigation_value: false, diff --git a/app/views/boards/show/_filtered_cards.html.erb b/app/views/boards/show/_filtered_cards.html.erb index 5cfcd39b7..4e4d8f749 100644 --- a/app/views/boards/show/_filtered_cards.html.erb +++ b/app/views/boards/show/_filtered_cards.html.erb @@ -1,6 +1,8 @@
- <%= with_automatic_pagination :filtered_cards_paginated_container, page do %> - <%= render "cards/display/previews", cards: page.records, draggable: true %> - <% end %> -
No cards match this filter
+
+ <%= with_automatic_pagination :filtered_cards_paginated_container, page do %> + <%= render "cards/display/previews", cards: page.records, draggable: true %> + <% end %> +
No cards match this filter
+
diff --git a/app/views/boards/show/_mobile_columns.html.erb b/app/views/boards/show/_mobile_columns.html.erb deleted file mode 100644 index b52e14592..000000000 --- a/app/views/boards/show/_mobile_columns.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<% postponed_count = board.cards.postponed.size %> -<% awaiting_triage_count = board.cards.awaiting_triage.size %> -<% closed_count = board.cards.closed.size %> - -
-
- <%= render "columns/show/add_card_button", board: board %> -
- - <%= link_to board_columns_not_now_path(board), class: "cards cards--on-deck is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %> -
- <%= postponed_count %> -

- Not Now -

-
- <% end %> - - <%= link_to board_columns_stream_path(board), class: "cards cards--considering is-collapsed", data: { turbo_frame: "_top" } do %> -
- <%= awaiting_triage_count %> -

- Maybe? -

-
- <% end %> - - <% board.columns.sorted.each do |column| %> - <% active_count = column.cards.active.size %> - <%= link_to board_column_path(column.board, column), class: "cards cards--doing is-collapsed", style: "--card-color: #{column.color}", data: { turbo_frame: "_top" } do %> -
- <%= active_count %> -

- <%= column.name %> -

-
- <% end %> - <% end %> - - <%= link_to board_columns_closed_path(board), class: "cards cards--closed is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %> -
- <%= closed_count %> -

- Done -

-
- <% end %> -
diff --git a/app/views/boards/show/_stream.html.erb b/app/views/boards/show/_stream.html.erb index d4753eba5..7773fb292 100644 --- a/app/views/boards/show/_stream.html.erb +++ b/app/views/boards/show/_stream.html.erb @@ -1,20 +1,18 @@ -<%= column_tag id: "the-stream", name: "Maybe?", drop_url: columns_card_drops_stream_path("__id__"), collapsed: false, selected: "true", class: "cards--considering" do %> +<%= column_tag id: "maybe", name: "Maybe?", drop_url: columns_card_drops_stream_path("__id__"), collapsed: false, selected: "true", class: "cards--maybe", data: { + drag_and_strum_target: "container", + collapsible_columns_target: "column maybeColumn", + action: "focus->collapsible-columns#focusOnColumn" +} do %>
-
-

Maybe?

-
- <%= link_to board_columns_stream_path(board), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> - <%= icon_tag "grid", class: "translucent" %> - Expand column - <% end %> + <%= render "boards/show/expander", title: "Maybe?", count: board.cards.awaiting_triage.count, column_id: "maybe" %> + <%= render "boards/show/menu/maximize", column_path: board_columns_stream_path(board) %>
<%= 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 %> +
+ <% if page.used? %> + <%= with_automatic_pagination "maybe", @page do %> + <%= render "cards/display/previews", cards: page.records, draggable: true %> + <% end %> <% end %> - <% else %> -
-
- <% end %> +
<% end %> diff --git a/app/views/boards/show/menu/_columns.html.erb b/app/views/boards/show/menu/_columns.html.erb index 70aac7c90..017527a43 100644 --- a/app/views/boards/show/menu/_columns.html.erb +++ b/app/views/boards/show/menu/_columns.html.erb @@ -1,4 +1,4 @@ -
+