Merge branch 'main' into mobile/bridge-components

* main: (65 commits)
  Repair scope for card styles within the columns view
  Add missing list to cards page
  Account for simpler public views
  Fix public layout
  Pull board-tools outside of the list for mobile
  saas: move log_level setting into an environment variable
  Restore log level configurability in production environment
  Remove engagements
  Use notch class so Save button is positioned correctly
  Go back to board after clearing filters
  Revert "Use existing no_filtering_url to direct back to the board when clearing filters"
  Use existing no_filtering_url to direct back to the board when clearing filters
  Ensure filters sit on top of cards
  Fix public boards
  Fix card grid layout
  Fix Chinese/Japanese characters missing from printed PDFs on macOS
  Add SQLite FTS5 support for CJK search
  Add CJK (Chinese, Japanese, Korean) search support
  Fix scrolling on mobile
  Remember expanded state on navigation
  ...
This commit is contained in:
Denis Švara
2026-01-08 14:34:10 +01:00
78 changed files with 714 additions and 440 deletions
@@ -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
}
@@ -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"]')
}
}