diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 30e5da88a9..d475d06285 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -205,7 +205,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 05d2b016eb..689b802153 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 0725dd3362..3c36b30da5 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 aed15d90f3..b0710a81f5 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -1,4 +1,30 @@ @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, + #main { + display: grid; + grid-template-rows: auto 1fr; + } + + #global-container { + overflow: hidden; + } + + #main { + overflow: auto; + padding: 0; + } + } + } + /* Column container /* ------------------------------------------------------------------------ */ @@ -10,7 +36,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,28 +47,41 @@ 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) { 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, @@ -50,16 +89,23 @@ align-items: stretch; display: flex; gap: var(--column-gap); + position: relative; } .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 @@ -73,6 +119,13 @@ position: relative; scroll-snap-align: start; + &.is-expanded { + @media (max-width: 639px) { + overflow: hidden; + scroll-snap-align: center; + } + } + &.is-collapsed { inline-size: var(--column-width-collapsed); @@ -128,12 +181,10 @@ 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); + @media (min-width: 640px) { + .is-collapsed & { + translate: 0 var(--column-width-collapsed); + } } .drag-and-drop__hover-container & { @@ -150,25 +201,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); + /* Use the rest of the column height for scrolling */ + @media (max-width: 639px) { + flex: 1; + padding-inline: calc(var(--column-padding) / 4); + } } .cards--grid & { @@ -192,7 +251,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 @@ -244,11 +313,12 @@ 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); } } @@ -295,8 +365,12 @@ position: relative; text-transform: uppercase; + &[disabled] { + opacity: 1; + } + @media (any-hover: hover) { - &:hover { + .is-collapsed:hover { filter: brightness(0.9); } } @@ -349,22 +423,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; } } @@ -382,7 +451,7 @@ writing-mode: vertical-rl; } - .cards:not(.is-collapsed, .cards--considering) & { + .cards.is-expanded & { align-items: center; display: flex; gap: 0.25ch; @@ -401,51 +470,13 @@ 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 /* ------------------------------------------------------------------------ */ @@ -542,7 +573,7 @@ /* Considering /* ------------------------------------------------------------------------ */ - .cards--considering { + .cards--maybe { --card-color: oklch(var(--lch-blue-medium)); position: relative; @@ -596,7 +627,7 @@ --card-padding-block: var(--block-space); border: 1px solid var(--border-color); - margin-block-end: var(--cards-gap); + inline-size: auto; text-align: center; &:has(dialog[open]) { @@ -669,18 +700,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%; @@ -701,13 +726,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; } } @@ -747,54 +773,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: 20; - --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/drag_and_drop.css b/app/assets/stylesheets/drag_and_drop.css index 2ada036b4d..03943be14a 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/layout.css b/app/assets/stylesheets/layout.css index e82992dfcc..6ea479eb64 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; @@ -22,11 +32,6 @@ max-inline-size: 100dvw; } - :where(#header) { - position: relative; - z-index: var(--z-nav); - } - :is(#header, #footer) { @media print { display: none; diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index c794934588..f700b9332f 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -125,7 +125,7 @@ .card--new, .cards__decoration, .card-columns:before, - .cards--considering:before { + .cards--maybe:before { display: none; } @@ -140,7 +140,7 @@ padding-inline: 0; } - .cards--considering { + .cards--maybe { background: none; margin: 0; padding-inline: 1ch; diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index e252d89813..0d00d99778 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -95,12 +95,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/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 2c97fcd097..806e70c760 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); } @@ -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) } @@ -89,6 +98,7 @@ export default class extends Controller { const key = this.#localStorageKeyFor(column) this.#buttonFor(column).setAttribute("aria-expanded", "false") + column.classList.remove(this.expandedClass) column.classList.add(this.collapsedClass) localStorage.removeItem(key) } @@ -98,7 +108,12 @@ export default class extends Controller { this.#buttonFor(column).setAttribute("aria-expanded", "true") column.classList.remove(this.collapsedClass) + column.classList.add(this.expandedClass) localStorage.setItem(key, true) + + if (window.matchMedia('(max-width: 639px)').matches) { + column.scrollIntoView({ behavior: "smooth", inline: "nearest" }) + } } #buttonFor(column) { @@ -140,4 +155,26 @@ 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) + this.#maybeButton.setAttribute("disabled", true) + } + + #handleMobileMode() { + this.columnTargets.forEach(column => this.#collapse(column)) + this.#maybeButton.removeAttribute("disabled") + } + + get #maybeButton() { + return this.maybeColumnTarget.querySelector('[data-collapsible-columns-target="button"]') + } } diff --git a/app/views/boards/columns/_empty_placeholder.html.erb b/app/views/boards/columns/_empty_placeholder.html.erb index 36507c1ff4..51a5b79e62 100644 --- a/app/views/boards/columns/_empty_placeholder.html.erb +++ b/app/views/boards/columns/_empty_placeholder.html.erb @@ -1,4 +1,4 @@ -