diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 25bc18277..561d78640 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 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/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..082fcf2fc 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 b9f8d6367..6ea479eb6 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/print.css b/app/assets/stylesheets/print.css index c79493458..5ed9c3fdf 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -7,6 +7,7 @@ --color-canvas: white; --border-dark: 1px solid var(--color-ink); --border-light: 1px solid color-mix(in oklch, var(--color-ink), transparent 75%); + --font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Hiragino Sans GB", "Hiragino Sans", "Apple SD Gothic Neo", "Microsoft YaHei", "Meiryo", "Malgun Gothic"; } @page { @@ -125,7 +126,7 @@ .card--new, .cards__decoration, .card-columns:before, - .cards--considering:before { + .cards--maybe:before { display: none; } @@ -140,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 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/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_controller.rb b/app/controllers/cards_controller.rb index daf22a5bc..e79eeb22b 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -12,7 +12,7 @@ class CardsController < ApplicationController def create respond_to do |format| format.html do - card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted") + card = Current.user.draft_new_card_in(@board) redirect_to 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/search.rb b/app/models/search.rb index f81b9a9d8..3b8127ef1 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -1,4 +1,6 @@ module Search + CJK_PATTERN = /\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/ + def self.table_name_prefix "search_" end diff --git a/app/models/search/highlighter.rb b/app/models/search/highlighter.rb index d2b53d772..8672ed3c8 100644 --- a/app/models/search/highlighter.rb +++ b/app/models/search/highlighter.rb @@ -13,8 +13,14 @@ class Search::Highlighter result = text.dup terms.each do |term| - result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match| - "#{OPENING_MARK}#{match}#{CLOSING_MARK}" + if term.match?(Search::CJK_PATTERN) + result.gsub!(/(#{Regexp.escape(term)})/i) do |match| + "#{OPENING_MARK}#{match}#{CLOSING_MARK}" + end + else + result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match| + "#{OPENING_MARK}#{match}#{CLOSING_MARK}" + end end end diff --git a/app/models/search/query.rb b/app/models/search/query.rb index 5fe0829e4..6bf06ef42 100644 --- a/app/models/search/query.rb +++ b/app/models/search/query.rb @@ -33,7 +33,7 @@ class Search::Query < ApplicationRecord end def remove_invalid_search_characters(terms) - terms.gsub(/[^\w"]/, " ") + terms.gsub(/[^\p{L}\p{N}_"]/, " ") end def remove_unbalanced_quotes(terms) diff --git a/app/models/search/record/sqlite.rb b/app/models/search/record/sqlite.rb index ae0d34281..73b3b675f 100644 --- a/app/models/search/record/sqlite.rb +++ b/app/models/search/record/sqlite.rb @@ -8,9 +8,10 @@ module Search::Record::SQLite has_one :search_records_fts, -> { with_rowid }, class_name: "Search::Record::SQLite::Fts", foreign_key: :rowid, primary_key: :id, dependent: :destroy + before_save :stem_content after_save :upsert_to_fts5_table - scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", query) } + scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", Search::Stemmer.stem(query.to_s)) } end class_methods do @@ -42,6 +43,11 @@ module Search::Record::SQLite end private + def stem_content + self.title = Search::Stemmer.stem(title) if title_changed? + self.content = Search::Stemmer.stem(content) if content_changed? + end + def escape_fts_highlight(html) return nil unless html.present? diff --git a/app/models/search/stemmer.rb b/app/models/search/stemmer.rb index f6a6c56d4..2ae0e1bf1 100644 --- a/app/models/search/stemmer.rb +++ b/app/models/search/stemmer.rb @@ -5,9 +5,43 @@ module Search::Stemmer def stem(value) if value.present? - value.gsub(/[^\w\s]/, "").split(/\s+/).map { |word| STEMMER.stem(word.downcase) }.join(" ") + tokenize(value).join(" ") else value end end + + private + def tokenize(value) + tokens = [] + current_word = +"" + + value.each_char do |char| + if cjk_character?(char) + if current_word.present? + tokens << stem_word(current_word) + current_word = +"" + end + tokens << char + elsif char =~ /[\p{L}\p{N}_]/ + current_word << char + else + if current_word.present? + tokens << stem_word(current_word) + current_word = +"" + end + end + end + + tokens << stem_word(current_word) if current_word.present? + tokens + end + + def cjk_character?(char) + char.match?(Search::CJK_PATTERN) + end + + def stem_word(word) + STEMMER.stem(word.downcase) + end end diff --git a/app/models/user/accessor.rb b/app/models/user/accessor.rb index e8a24830f..d383f8929 100644 --- a/app/models/user/accessor.rb +++ b/app/models/user/accessor.rb @@ -11,6 +11,12 @@ module User::Accessor after_create_commit :grant_access_to_boards, unless: :system? end + def draft_new_card_in(board) + board.cards.find_or_initialize_by(creator: self, status: "drafted").tap do |card| + card.update!(created_at: Time.current, updated_at: Time.current, last_active_at: Time.current) + end + end + private def grant_access_to_boards Access.insert_all account.boards.all_access.ids.collect { |board_id| { id: ActiveRecord::Type::Uuid.generate, board_id: board_id, user_id: id, account_id: account.id } } 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 cfa56d721..1397bb4bc 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 3fb62213c..fdb8d6dcc 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 aa730b179..5e527826a 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 c98bd28b1..ae1d7e5a6 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 ff418996b..09974106f 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 %> @@ -27,7 +28,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 @@ -
+