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 @@ -
+

Drag cards here

diff --git a/app/views/boards/columns/_list.html.erb b/app/views/boards/columns/_list.html.erb index 7adedf3223..3c7ed50683 100644 --- a/app/views/boards/columns/_list.html.erb +++ b/app/views/boards/columns/_list.html.erb @@ -1,3 +1,4 @@ -
+
+ <%= yield %> <%= render "cards/display/previews", cards: cards, draggable: draggable %>
diff --git a/app/views/boards/show.html.erb b/app/views/boards/show.html.erb index 9f0c2ebeef..e1b0a3e960 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 %> @@ -26,6 +27,5 @@ <%= render "boards/show/filtered_cards", page: @page %> <% else %> <%= 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 a6001da79a..cb7d4c086e 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, @@ -23,7 +24,7 @@ <%= render "boards/show/not_now", board: board %>
- <%= render "boards/show/stream", board: board, page: page %> + <%= render "boards/show/maybe", board: board, page: page %>
<%= render partial: "boards/show/column", collection: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %> diff --git a/app/views/boards/show/_maybe.html.erb b/app/views/boards/show/_maybe.html.erb new file mode 100644 index 0000000000..552cef52c7 --- /dev/null +++ b/app/views/boards/show/_maybe.html.erb @@ -0,0 +1,20 @@ +<%= 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 %> +
+ <%= 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) %> +
+ <% if page.used? %> + <%= with_automatic_pagination "maybe", @page do %> + <%= render "boards/columns/list", cards: page.records, draggable: true do %> + <%= render "columns/show/add_card_button", board: board %> + <% end %> + <% end %> + <% else %> +
+
+ <% end %> +<% end %> 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 b52e145926..0000000000 --- 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 deleted file mode 100644 index d4753eba59..0000000000 --- a/app/views/boards/show/_stream.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= column_tag id: "the-stream", name: "Maybe?", drop_url: columns_card_drops_stream_path("__id__"), collapsed: false, selected: "true", class: "cards--considering" 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 "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 %> - <% else %> -
-
- <% end %> -<% end %> diff --git a/app/views/boards/show/menu/_columns.html.erb b/app/views/boards/show/menu/_columns.html.erb index 70aac7c902..017527a43c 100644 --- a/app/views/boards/show/menu/_columns.html.erb +++ b/app/views/boards/show/menu/_columns.html.erb @@ -1,4 +1,4 @@ -
+