diff --git a/app/assets/stylesheets/buttons.css b/app/assets/stylesheets/buttons.css index 4ee5f199ea..64ea77611a 100644 --- a/app/assets/stylesheets/buttons.css +++ b/app/assets/stylesheets/buttons.css @@ -107,13 +107,22 @@ > * { grid-area: 1/1; } + + @media (max-width: 639px) { + --btn-size: 3em; + --icon-size: 70%; + } } /* Make a normal button circular on mobile */ @media (max-width: 639px) { .btn--circle-mobile { + --btn-size: 3em; + --btn-padding: 0; + --icon-size: 70%; + aspect-ratio: 1; - padding: 0.5em; + inline-size: var(--btn-size); kbd, span:last-of-type { @@ -270,4 +279,3 @@ } } } - diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index 59a80a32a4..240dadb4b7 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -5,6 +5,7 @@ --actions-block-inset: 1.5rem; --actions-inline-inset: 4rem; --color-container: color-mix(in srgb, var(--card-color) 33%, var(--color-canvas)); + --half-btn-height: 1.25rem; --padding-inline: calc(var(--block-space-double) + var(--block-space)); --padding-block: calc(var(--block-space-double) + var(--block-space-half)); @@ -14,7 +15,8 @@ grid-template-areas: "notch-top notch-top notch-top" "actions-left card actions-right" - "notch-bottom notch-bottom notch-bottom"; + "notch-bottom notch-bottom notch-bottom" + "closure-message closure-message closure-message"; grid-template-columns: 48px minmax(0, 1120px) 48px; inline-size: fit-content; margin-block-start: var(--block-space); @@ -47,6 +49,7 @@ .card__body { @media (max-width: 639px) { flex-direction: column; + padding-block-end: calc(var(--card-padding-block) * 1.5); } } @@ -89,15 +92,30 @@ @media (max-width: 639px) { border: 1px solid var(--card-color); border-radius: 0.25em; - overflow: hidden; + flex-direction: row; + gap: 0; + overflow: auto; max-inline-size: 100%; padding: 0; + position: relative; + white-space: nowrap; + + & > form { + flex-grow: 1; + max-inline-size: 25ch; + min-block-size: 2.5em; + } + + & > form + form:not(:has(.card__column-name--current)) { + box-shadow: -1px 0 0 0 var(--color-container); + } } } .card__column-name { @media (max-width: 639px) { --btn-border-radius: 0; + justify-content: center; } } @@ -127,13 +145,15 @@ } @media (max-width: 799px) { + --half-btn-height: 1.5rem; --padding-inline: 1.5ch; column-gap: 0; grid-template-areas: "notch-top notch-top notch-top" "card card card" - "actions-left notch-bottom actions-right"; + "actions-left notch-bottom actions-right" + "closure-message closure-message closure-message"; grid-template-columns: 1fr auto 1fr; inline-size: calc(100% + 2 * var(--padding-inline)); margin-inline: calc(-1 * var(--padding-inline)); @@ -151,7 +171,12 @@ grid-area: card; padding: clamp(2rem, 4vw, var(--padding-block)); - @media (max-width: 799px) { + @media (max-width: 639px) { + padding: clamp(0.25rem, 2vw, var(--padding-block)); + padding-block-end: clamp(2.5rem, 4vw, var(--padding-block)); + } + + @media (min-width: 640px) and (max-width: 799px) { padding-inline: var(--padding-inline); } } @@ -263,13 +288,14 @@ } .card-perma__notch--bottom { - --half-btn-height: 1.25rem; - grid-area: notch-bottom; /* Overlap the card BG by half the button height */ &:has(.btn) { - translate: 0 calc(-1 * var(--half-btn-height)); + &, + ~ .card-perma__closure-message { + translate: 0 calc(-1 * var(--half-btn-height)); + } } form { @@ -314,5 +340,8 @@ .card-perma__closure-message { color: var(--card-color); + grid-area: closure-message; + margin-block-start: 0.5ch; + padding-inline: 1ch; } } diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 1dbdf09971..73e446185f 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -225,6 +225,11 @@ padding-block: calc(var(--events-gap) * 3) var(--events-gap); position: sticky; z-index: var(--z-events-column-header); + + @media (max-width: 639px) { + margin-inline: calc(var(--main-padding) * -1); + padding-inline: var(--main-padding); + } } .events__column-footer { diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css index 21553e358d..a3b90ac4f3 100644 --- a/app/assets/stylesheets/header.css +++ b/app/assets/stylesheets/header.css @@ -116,17 +116,15 @@ /* Optional class to stack header actions on small screens /* ------------------------------------------------------------------------ */ - /* .header--mobile-actions-stack { + .header--mobile-actions-stack { @media (max-width: 639px) { - grid-template-columns: 1fr 1fr; grid-template-areas: - "menu menu" - "actions-start actions-end" - "title title"; + "actions-start menu actions-end" + "title title title"; .header__title { margin-block-start: 0.25rem; } } - } */ + } } diff --git a/app/assets/stylesheets/native.css b/app/assets/stylesheets/native.css index f7b059c0c6..96df0e00ba 100644 --- a/app/assets/stylesheets/native.css +++ b/app/assets/stylesheets/native.css @@ -6,8 +6,40 @@ --custom-safe-inset-bottom: var(--injected-safe-inset-bottom, env(safe-area-inset-bottom, 0px)); --custom-safe-inset-left: var(--injected-safe-inset-left, env(safe-area-inset-left, 0px)); + -webkit-tap-highlight-color: transparent; + .hide-on-native { display: none; } + + /* Header + /* ------------------------------------------------------------------------ */ + + .header:is( + :not(:has(.header__title, .header__actions)), + :not(:has(.header__title, .header__actions--end)):has(.header__actions--start .btn--back:only-child) + ) { + display: none; + } + + .header__actions { + .btn--back { + display: none; + } + } + + /* Card perma + /* ------------------------------------------------------------------------ */ + + .card-perma { + margin-block-start: 0; + } + + /* Search + /* ------------------------------------------------------------------------ */ + + .search__title { + text-decoration: none; + } } } diff --git a/app/assets/stylesheets/pins.css b/app/assets/stylesheets/pins.css new file mode 100644 index 0000000000..fbecaf912d --- /dev/null +++ b/app/assets/stylesheets/pins.css @@ -0,0 +1,5 @@ +@layer components { + .pins-list { + --panel-size: 45ch; + } +} diff --git a/app/assets/stylesheets/search.css b/app/assets/stylesheets/search.css index 96368841e1..fcd4563ca8 100644 --- a/app/assets/stylesheets/search.css +++ b/app/assets/stylesheets/search.css @@ -26,7 +26,7 @@ summary { max-block-size: 100%; overflow-y: auto; overscroll-behavior: contain; - padding: var(--block-space) calc(var(--block-space-double) + var(--btn-size)); + padding: var(--block-space); } /* Form @@ -34,18 +34,22 @@ summary { .search__input { --clear-icon-size: 1em; - --focus-ring-size: 0; - --input-border-color: var(--color-ink-light); - --input-border-radius: 0; - --input-padding: 0.1em; - border-width: 0 0 1px; max-inline-size: 50ch; position: relative; &::-webkit-search-cancel-button { display: none; } + + .bar__input & { + --focus-ring-size: 0; + --input-border-color: var(--color-ink-light); + --input-border-radius: 0; + --input-padding: 0.1em; + + border-width: 0 0 1px; + } } .search__reset { @@ -107,4 +111,22 @@ summary { .search__title { text-decoration: underline; } + + /* Perma + /* ------------------------------------------------------------------------ */ + + .search-perma { + .search__form > label, + .search__reset { + display: none; + } + + .search__input { + max-inline-size: min(80ch, 100%); + } + + .search { + padding-inline: 0; + } + } } diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 7cc90f92d8..7793b6f2da 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -156,11 +156,11 @@ --tray-item-scale: calc(1 - (var(--tray-item-index) - 1) / 30); --tray-item-z: calc(6 - var(--tray-item-index)); + font-size: 10px; + margin-block-end: var(--tray-item-margin); position: relative; .tray__dialog & { - font-size: 10px; - margin-block-end: var(--tray-item-margin); transition: var(--tray-duration) var(--ease-out-overshoot-subtle); transition-delay: var(--tray-item-delay); transition-property: margin, opacity, scale; @@ -285,7 +285,7 @@ /* Tray cards /* ------------------------------------------------------------------------ */ - .tray { + .tray__item { .card { --card-padding-block: 1.5ch; --card-padding-inline: 1.5ch; @@ -345,52 +345,69 @@ inset-inline-start: -100%; } - .tray__item { - --tray-item-z: calc(10 - var(--tray-item-index)); - - position: relative; - - [open] &[aria-selected] { - outline: 0; - z-index: calc(var(--tray-item-z) + 2); + /* Disable the expander if there aren't items to show */ + .tray__dialog:not(:has(.tray__item)) ~ .tray__toggle { + opacity: 0.5; - .card__link { - border-radius: 0.25ch; - outline: var(--focus-ring-size) solid var(--focus-ring-color); - outline-offset: var(--focus-ring-offset); - z-index: 1; - } + &, .tray__toggle-btn { + pointer-events: none; } + } - /* Show 6 max items on smallest devices */ - @media (max-height: 578px) { - &:nth-child(1n + 7) { display: none; } + /* Add a border on mobile */ + @media (max-width: 799px) { + .tray__dialog:not([open]) ~ .tray__toggle { + border-inline-end: 1px dashed var(--color-ink-light); + translate: calc(-1 * var(--tray-margin)) 0; } + } + } - /* 7 max */ - @media (min-height: 578px) and (max-height: 656px) { - &:nth-child(1n + 8) { display: none; } - } + .tray__item--pin { + --tray-item-z: calc(10 - var(--tray-item-index)); - /* 8 max */ - @media (min-height: 656px) and (max-height: 734px) { - &:nth-child(1n + 9) { display: none; } - } + position: relative; - /* 9 max */ - @media (min-height: 734px) and (max-height: 812px) { - &:nth-child(1n + 10) { display: none; } - } + [open] &[aria-selected] { + outline: 0; + z-index: calc(var(--tray-item-z) + 2); - /* 10 max on larger screens */ - @media (min-height: 812px) { - &:nth-child(1n + 11) { display: none; } + .card__link { + border-radius: 0.25ch; + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + z-index: 1; } + } - &:not([aria-selected]) .card__link:focus-visible, - .tray__dialog:not([open]) & .card__link:focus-visible { - --focus-ring-size: 0; - } + /* Show 6 max items on smallest devices */ + @media (max-height: 578px) { + &:nth-child(1n + 7) { display: none; } + } + + /* 7 max */ + @media (min-height: 578px) and (max-height: 656px) { + &:nth-child(1n + 8) { display: none; } + } + + /* 8 max */ + @media (min-height: 656px) and (max-height: 734px) { + &:nth-child(1n + 9) { display: none; } + } + + /* 9 max */ + @media (min-height: 734px) and (max-height: 812px) { + &:nth-child(1n + 10) { display: none; } + } + + /* 10 max on larger screens */ + @media (min-height: 812px) { + &:nth-child(1n + 11) { display: none; } + } + + &:not([aria-selected]) .card__link:focus-visible, + .tray__dialog:not([open]) & .card__link:focus-visible { + --focus-ring-size: 0; } .tray__remove-pin-btn { @@ -399,18 +416,17 @@ background-color: var(--card-bg-color); inset: 0 0 auto auto; - opacity: 0; - pointer-events: none; + opacity: 0.66; position: absolute; z-index: 1; - .tray__dialog[open] & { - opacity: 0.66; - pointer-events: unset; + &:hover { + opacity: 1; + } - &:hover { - opacity: 1; - } + .tray__dialog:not([open]) & { + opacity: 0; + pointer-events: none; } [aria-busy] & { @@ -451,9 +467,10 @@ inline-size: fit-content; margin: 0 0 0 auto; transition: translate 150ms ease-out; + translate: -2em; - [open] & { - translate: -2em; + .tray__dialog:not([open]) & { + translate: 0; } } @@ -491,24 +508,6 @@ .card__bubble { display: none; } - - /* Disable the expander if there aren't items to show */ - .tray__dialog:not(:has(.tray__item)) ~ .tray__toggle { - opacity: 0.5; - - &, .tray__toggle-btn { - pointer-events: none; - } - } - - /* Add a border on mobile */ - @media (max-width: 799px) { - .tray__dialog:not([open]) ~ .tray__toggle { - border-inline-end: 1px dashed var(--color-ink-light); - translate: calc(-1 * var(--tray-margin)) 0; - } - - } } ::view-transition-group(tray-pins) { diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb index f48b5d5571..b2dc747278 100644 --- a/app/helpers/columns_helper.rb +++ b/app/helpers/columns_helper.rb @@ -6,8 +6,8 @@ def button_to_set_column(card, column) method: :post, class: [ "card__column-name btn", { "card__column-name--current": column == card.column && card.open? } ], style: "--column-color: #{column.color}", - form_class: "flex align-center gap-half", - data: { turbo_frame: "_top" } + form_class: "flex gap-half", + data: { turbo_frame: "_top", scroll_to_target: column == card.column && card.open? ? "target" : nil } end def column_tag(id:, name:, drop_url:, collapsed: true, selected: nil, card_color: "var(--color-card-default)", data: {}, **properties, &block) diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js index bd5e876659..dfe8d60873 100644 --- a/app/javascript/controllers/navigable_list_controller.js +++ b/app/javascript/controllers/navigable_list_controller.js @@ -1,5 +1,6 @@ import { Controller } from "@hotwired/stimulus" import { nextFrame } from "helpers/timing_helpers" +import { isMobile } from "helpers/platform_helpers" export default class extends Controller { static targets = [ "item", "input" ] @@ -18,6 +19,11 @@ export default class extends Controller { onlyActOnFocusedItems: { type: Boolean, default: false } } + // Don't load for mobile devices + static get shouldLoad() { + return !isMobile() + } + connect() { if (this.autoSelectValue) { this.reset() diff --git a/app/javascript/controllers/scroll_to_controller.js b/app/javascript/controllers/scroll_to_controller.js new file mode 100644 index 0000000000..aad761fd81 --- /dev/null +++ b/app/javascript/controllers/scroll_to_controller.js @@ -0,0 +1,19 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ "target" ] + + connect() { + this.#scrollTargetIntoView() + } + + #scrollTargetIntoView() { + if(this.hasTargetTarget) { + this.element.scrollTo({ + top: this.targetTarget.offsetTop - this.element.offsetHeight / 2 + this.targetTarget.offsetHeight / 2, + left: this.targetTarget.offsetLeft - this.element.offsetWidth / 2 + this.targetTarget.offsetWidth / 2, + behavior: "instant" + }) + } + } +} diff --git a/app/javascript/controllers/soft_keyboard_controller.js b/app/javascript/controllers/soft_keyboard_controller.js index 75fb4f2eef..25dc15bf7e 100644 --- a/app/javascript/controllers/soft_keyboard_controller.js +++ b/app/javascript/controllers/soft_keyboard_controller.js @@ -1,10 +1,11 @@ import { Controller } from "@hotwired/stimulus" import { nextEventNamed } from "helpers/timing_helpers" +import { isTouchDevice } from "helpers/platform_helpers" export default class extends Controller { // Only load for touch devices static get shouldLoad() { - return "ontouchstart" in window && navigator.maxTouchPoints > 0 + return isTouchDevice() } // Use a fake input to trigger the soft keyboard on actions that load async content diff --git a/app/javascript/helpers/platform_helpers.js b/app/javascript/helpers/platform_helpers.js new file mode 100644 index 0000000000..636e4b9681 --- /dev/null +++ b/app/javascript/helpers/platform_helpers.js @@ -0,0 +1,15 @@ +export function isTouchDevice() { + return "ontouchstart" in window && navigator.maxTouchPoints > 0 +} + +export function isIos() { + return /iPhone|iPad/.test(navigator.userAgent) +} + +export function isAndroid() { + return /Android/.test(navigator.userAgent) +} + +export function isMobile() { + return isIos() || isAndroid() +} diff --git a/app/views/bar/_bar.html.erb b/app/views/bar/_bar.html.erb index 6d2014e125..3eb4495981 100644 --- a/app/views/bar/_bar.html.erb +++ b/app/views/bar/_bar.html.erb @@ -8,7 +8,7 @@
<%= tag.dialog id: "bar-modal", class: "bar__modal", data: { diff --git a/app/views/cards/columns/edit.html.erb b/app/views/cards/columns/edit.html.erb index a2ad2c819a..586fac20ff 100644 --- a/app/views/cards/columns/edit.html.erb +++ b/app/views/cards/columns/edit.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag @card, :columns do %> -