diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 25bc182772..561d78640e 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -207,7 +207,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 3e0e7615c2..aa221c4524 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 3408232f46..1bbed081ee 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,16 @@ --card-grid-columns: 3; } + .cards__list { + align-items: start; + 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 +326,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 +373,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 +425,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 +438,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 +466,7 @@ writing-mode: vertical-rl; } - .cards:not(.is-collapsed, .cards--considering) & { + .cards.is-expanded & { align-items: center; display: flex; gap: 0.25ch; @@ -394,16 +476,20 @@ .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; } @@ -413,7 +499,7 @@ /* Override card styles within columns /* ------------------------------------------------------------------------ */ - .cards { + .body--boards { .card { --block-space: 1em; --block-space-half: 0.5em; @@ -506,7 +592,7 @@ /* Considering /* ------------------------------------------------------------------------ */ - .cards--considering { + .cards--maybe { --card-color: oklch(var(--lch-blue-medium)); position: relative; @@ -560,15 +646,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 { @@ -582,7 +690,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 { @@ -633,18 +741,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%; @@ -665,13 +767,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; } } @@ -711,54 +814,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/drag_and_drop.css b/app/assets/stylesheets/drag_and_drop.css index 24bcaa7a2e..03943be14a 100644 --- a/app/assets/stylesheets/drag_and_drop.css +++ b/app/assets/stylesheets/drag_and_drop.css @@ -15,6 +15,5 @@ outline-offset: -2px; transition: background-color 200ms; z-index: 1; - } } diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index b283fa447b..082fcf2fc1 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -7,11 +7,12 @@ 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); diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css index b9f8d63675..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; diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index 8e1359e1be..5ed9c3fdf6 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/utilities.css b/app/assets/stylesheets/utilities.css index 91c04ff535..62304d020f 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/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index 69ef2efecb..1ace03574d 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -30,7 +30,9 @@ def board_watchers_list(board) 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 d99c125cb1..a92fb28245 100644 --- a/app/helpers/columns_helper.rb +++ b/app/helpers/columns_helper.rb @@ -11,7 +11,7 @@ def button_to_set_column(card, column) 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/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 2c97fcd097..1a9ba4174c 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/views/boards/columns/_empty_placeholder.html.erb b/app/views/boards/columns/_empty_placeholder.html.erb index 36507c1ff4..d9da8a8962 100644 --- a/app/views/boards/columns/_empty_placeholder.html.erb +++ b/app/views/boards/columns/_empty_placeholder.html.erb @@ -1,6 +1,4 @@ -