Files
fizzy/app/assets/stylesheets/card-columns.css
T
Adrien Maston fefb28848b Merge branch 'main' into mobile/prepare-webviews-2
* main: (26 commits)
  Add a new Pins section to docs/API.md covering pin/unpin and the pinned cards list response.
  Align board name start so it doesn't get too big
  Clean up card/events header layout
  Sync email to Stripe when user changes email address (#2432)
  Fix IDOR in webhook activation endpoint (#2431)
  Add card reactions to API docs and reactions_url to card JSON (#2427)
  Remove unnecessary claude plan
  Allow boosts on cards (#2411)
  Revert "Fix notification click URL by using correct data property"
  Add migration to remove draft cards from search index
  Guard search indexing with searchable? check
  Forbid comments on draft cards
  prefactor: update search to use published cards
  Fix notification click URL by using correct data property
  Wait for service worker to be active before subscribing
  Fix stuck state when permission granted but no subscription
  Extract Card::Commentable
  Include arm64 build in Docker workflow
  Remove unnecessary `await` in push handler
  Correctly initialise WebPush connection (#2417)
  ...
2026-01-27 17:17:21 +01:00

889 lines
20 KiB
CSS

@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
/* ------------------------------------------------------------------------ */
#main:has(.card-columns) {
--main-padding: 0;
}
.card-columns {
--bubble-size: 3.5rem;
--cards-gap: min(1.2cqi, 1.7rem);
--column-gap: 8px;
--column-padding: calc(var(--column-gap) * 2);
--column-transition-duration: 300ms;
--column-width-collapsed: 40px;
--column-width-expanded: 450px;
--progress-increment: var(--progress-max-height) / var(--progress-max-cards);
--progress-max-cards: 15; /* should match first geared pagination page size */
--progress-max-height: 50dvh;
container-type: inline-size;
display: grid;
gap: var(--column-gap);
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;
position: relative;
/* When it has something expanded */
&: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) {
block-size: 100%;
min-block-size: 20lh;
}
@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,
.card-columns__right {
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
/* ------------------------------------------------------------------------ */
.cards {
--column-color: color-mix(in srgb, var(--card-color) 15%, var(--color-canvas));
inline-size: var(--column-width-expanded);
outline: none;
position: relative;
scroll-snap-align: center;
&.is-expanded {
@media (max-width: 639px) {
overflow: hidden;
}
}
&.is-collapsed {
inline-size: var(--column-width-collapsed);
.pagination-link.pagination-link--active-when-observed,
.card {
display: none;
}
}
&.drag-and-drop__hover-container {
--dnd-bg-color: transparent;
--dnd-border-color: transparent;
&.is-off-screen {
&:after {
content: attr(data-column-name);
font-size: var(--text-x-small);
font-weight: 500;
line-height: var(--column-width-collapsed);
padding-inline: 1ch;
position: fixed;
text-transform: uppercase;
top: 0;
translate: -50%;
}
&.is-collapsed {
&:after {
writing-mode: vertical-rl;
}
}
&:not(.is-collapsed) {
&:after {
background-color: var(--column-color);
inline-size: calc(var(--column-width-expanded) - 4px); /* make room for the dnd border */
}
}
}
}
@media (any-hover: hover) {
.card:has(.card__background img:not([src=""])):hover .card__background img:not([src=""]) {
filter: blur(3px) brightness(1.2);
opacity: 0.2;
}
}
}
.cards__transition-container {
block-size: 100%;
border-radius: calc(var(--column-width-collapsed) / 2);
translate: 0 0.5ch; /* Allow a little room for the mini bubble */
transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle);
@media (min-width: 640px) {
.is-expanded & {
translate: 0 0.5ch; /* Allow a little room for the mini bubble */
}
.is-collapsed & {
translate: 0 var(--column-width-collapsed);
}
}
.drag-and-drop__hover-container & {
--dnd-bg-color: var(--column-color);
--dnd-border-color: var(--card-color);
background-color: var(--dnd-bg-color);
outline: 2px dashed var(--dnd-border-color);
outline-offset: -2px;
transition: background-color 200ms;
z-index: 1;
}
.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 {
display: flex;
flex-direction: column;
gap: var(--cards-gap);
overflow-x: hidden;
overflow-y: auto;
.is-expanded & {
padding: var(--column-padding) var(--column-padding) calc(var(--column-padding) + var(--custom-safe-inset-bottom));
/* 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] {
outline: var(--focus-ring-size) solid var(--color-selected-dark);
outline-offset: var(--focus-ring-offset);
html[data-theme="dark"] & {
outline-color: oklch(var(--lch-blue-medium));
}
@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
outline-color: oklch(var(--lch-blue-medium));
}
}
}
&:has(.card) {
.blank-slate {
display: none;
}
}
/* Use the default blank-slate on small viewports since drag-and-drop isn't available */
[data-controller~="drag-and-drop"] & {
@media (max-width: 639px) {
.blank-slate--drag {
display: none;
}
}
@media (min-width: 640px) {
.blank-slate--default {
display: none;
}
}
}
}
.cards__new-column {
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
/* -------------------------------------------------------------------------- */
.cards--grid {
--cards-gap: 1rem;
--card-grid-columns: 1;
container-type: inline-size;
inline-size: 100%;
margin-inline: auto;
max-inline-size: var(--main-width);
@media (min-width: 640px) {
--card-grid-columns: 2;
}
@media (min-width: 960px) {
--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));
}
.card__header .card__column-name--current {
--btn-padding: 0.1em 0.5em;
background: none;
border: 1px solid currentColor;
color: var(--card-color);
display: inline-flex;
flex: 0 1 auto;
inline-size: fit-content;
margin: 0 0 0 auto;
}
.blank-slate--drag {
display: none;
}
}
/* Column Elements
/* ------------------------------------------------------------------------ */
.cards__header {
.cards.is-collapsed & {
block-size: 100%;
}
.cards.is-expanded & {
display: grid;
grid-template-areas: "menu expander maximize";
grid-template-columns: var(--column-width-collapsed) 1fr var(--column-width-collapsed);
padding-inline: var(--column-padding);
}
}
.cards__menu .btn--circle,
.cards__maximize-button {
--btn-background: transparent;
block-size: var(--column-width-collapsed);
inline-size: var(--column-width-collapsed);
opacity: 0;
outline-offset: -2px;
.cards:hover &,
&:focus-visible {
opacity: 1;
}
.cards.is-collapsed & {
display: none;
}
}
.cards__menu {
position: relative;
z-index: var(--z-popup);
}
.cards__maximize-button {
grid-area: maximize;
}
.cards__expander {
--gradient-direction: to bottom;
align-items: center;
border-radius: 99rem;
cursor: pointer;
display: flex;
flex-direction: row-reverse;
font-size: var(--text-x-small);
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) {
.is-collapsed:hover {
filter: brightness(0.9);
}
}
/* Progress */
&:after {
background: linear-gradient(var(--gradient-direction), var(--card-color), var(--column-color) 80%);
block-size: var(--column-width-collapsed);
border-radius: 99rem;
content: "";
inset: 0 0 auto;
margin-inline: auto;
max-block-size: var(--progress-max-height);
min-block-size: var(--column-width-collapsed);
opacity: 0;
position: absolute;
transition:
block-size 500ms var(--ease-out-overshoot),
inline-size var(--column-transition-duration) ease-out,
opacity var(--column-transition-duration) ease-out;
z-index: -1;
}
.no-transitions &:after {
transition: none;
}
.cards.is-collapsed & {
block-size: 100%;
flex-direction: column;
inline-size: var(--column-width-collapsed);
justify-content: start;
letter-spacing: 0.05em;
/* Guitar string */
&:before {
background-color: var(--column-color);
block-size: 100%;
content: "";
inline-size: 1px;
inset-block: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment)) 0;
position: absolute;
z-index: -2;
}
&:after {
block-size: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment));
max-block-size: none;
opacity: 1;
inline-size: var(--column-width-collapsed);
}
}
.cards.is-expanded & {
inline-size: 100%;
justify-content: center;
}
}
.cards__expander-count {
line-height: var(--column-width-collapsed);
inline-size: var(--column-width-collapsed);
.cards.is-expanded & {
display: none;
}
}
.cards__expander-title {
font-weight: inherit;
font-size: inherit;
line-height: var(--column-width-collapsed);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.cards.is-collapsed & {
max-inline-size: 50vh;
writing-mode: vertical-rl;
}
.cards.is-expanded & {
align-items: center;
display: flex;
gap: 0.25ch;
max-inline-size: calc(100% - var(--column-width-collapsed) * 2);
}
.icon--collapse {
--icon-size: 1.15em;
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.is-expanded .cards__expander:hover & {
opacity: 0.66;
scale: 1;
}
}
}
/* Override card styles within columns
/* Adding .board-tools here since it sits outside the cards container on mobile */
/* ------------------------------------------------------------------------ */
.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);
.card__counts {
--gap: 0.5ch;
align-items: flex-end;
display: flex;
flex-shrink: 0;
gap: calc(2 * var(--gap));
margin-inline: auto calc(var(--card-padding-inline) * -0.5);
padding-inline-start: var(--gap);
}
.card__boosts,
.card__comments {
--icon-size: 1.6em;
align-items: center;
display: flex;
flex-shrink: 0;
font-weight: 600;
gap: var(--gap);
img {
block-size: var(--icon-size);
inline-size: var(--icon-size);
}
.icon--comment {
color: var(--card-color);
}
}
.card__steps {
--column-gap: 0.8ch;
display: flex;
}
.card__tags {
gap: calc(var(--card-header-space) / 2);
}
.card__title {
pointer-events: none;
}
.card__link {
z-index: 1;
}
.card__stages,
.card__hide-on-index {
display: none;
}
.card__body {
padding-block: calc(var(--card-padding-block) * 0.75) var(--card-padding-block);
}
.card__meta {
font-weight: 600;
strong,
.local-time-value {
font-weight: inherit;
}
@media (max-width: 639px) {
inline-size: auto;
}
}
&:has(.card__background img:not([src=""])) {
.card__content,
.card__meta,
.card__boosts,
.card__comments,
.card__column-name:not(.card__column-name--current) {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
@media (any-hover: hover) {
&:hover {
.card__content,
.card__footer,
.card__boosts,
.card__comments,
.card__column-name:not(.card__column-name--current) {
opacity: 1;
}
.card__background img {
filter: blur(3px) brightness(1.2);
opacity: 0.2;
}
}
}
}
.bubble {
inset-inline-start: 100%;
translate: -90% -40%;
}
}
/* Considering
/* ------------------------------------------------------------------------ */
.cards--maybe {
--card-color: oklch(var(--lch-blue-medium));
position: relative;
.card {
--avatar-size: 2.75em;
--text-small: 1.1em;
background-color: var(--color-canvas);
line-height: 1.2;
z-index: 2;
@media (min-width: 640px) {
--text-xx-large: 1.6em;
}
}
.card__board {
background-color: transparent;
color: var(--card-content-color);
}
.card__header {
color: var(--color-ink);
padding-block-start: calc(var(--card-padding-block) / 2);
}
.card__tags {
color: inherit;
}
.card__body {
padding-block: 0 var(--card-padding-block);
}
.card__people-label {
display: none;
}
.card__title {
min-block-size: 0;
}
}
/* Board tools
/* -------------------------------------------------------------------------- */
.board-tools.card {
--border-color: var(--color-selected-dark);
--border-size: 1px;
--card-padding-block: var(--block-space);
border: 1px solid var(--border-color);
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;
}
.divider {
--divider-color: oklch(var(--lch-blue-light));
}
.btn--link {
font-size: 1.2em;
}
.btn:not(.btn--link, .btn--circle) {
border: 0;
color: var(--color-link);
}
footer {
font-size: var(--text-x-small);
margin-block: 1ch calc(var(--block-space-half) * -1);
}
.overflow-count {
font-size: 1.2em;
font-weight: 500;
padding: 0.5em 0.3em;
}
}
.board-tools__watching {
--btn-size: 32px;
--gap: 0.5ch;
display: flex;
gap: var(--gap);
inline-size: 100%;
margin-block: var(--block-space-half);
place-content: center;
position: relative;
}
.board-tools__watching-dialog {
--panel-padding: 2ch;
--panel-size: 100%;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
inset-block-start: 0;
justify-content: center;
position: absolute;
z-index: 1;
}
/* On Deck (Not Now)
/* ------------------------------------------------------------------------ */
.cards--closed,
.cards--on-deck {
--card-color: var(--color-ink-light) !important;
.card,
.blank-slate {
--card-color: var(--color-card-complete) !important;
}
.bubble {
display: none !important;
}
}
/* Doing
/* -------------------------------------------------------------------------- */
/* Surface a mini bubble if there are cards with bubbles inside */
.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-opacity: 75%;
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
&:before {
background: radial-gradient(
color-mix(in srgb, var(--bubble-color) calc(var(--bubble-opacity) / 5), var(--color-canvas)) 50%,
color-mix(in srgb, var(--bubble-color) var(--bubble-opacity), var(--color-canvas)) 100%
);
block-size: 1em;
border-radius: var(--bubble-shape);
content: "";
inline-size: 1em;
inset: 0 0 auto auto;
position: absolute;
translate: 20% -20%;
z-index: 1;
}
/* Maybe column: position bubble relative to the title, not the container */
.cards--maybe.is-expanded & {
overflow: visible;
position: relative;
&:before {
inset-block-start: 50%;
inset-inline-start: 0;
translate: -125% -75%;
z-index: -1;
}
}
@media (max-width: 639px) {
&.cards__expander-title:before {
display: none;
}
}
html[data-theme="dark"] & {
--bubble-opacity: 100%;
}
@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
--bubble-opacity: 100%;
}
}
}
/* Card column indicators
/* -------------------------------------------------------------------------- */
.card__column-name {
--btn-background: transparent;
--btn-padding: 0.2em 0.5em;
--btn-border-size: 0;
--btn-border-radius: 0.2em;
color: inherit;
inline-size: 100%;
justify-content: flex-start;
text-transform: uppercase;
@media (hover: hover) {
&:not(.card__column-name--current):hover {
--btn-background: color(from var(--column-color) srgb r g b / 0.15);
color: var(--column-color);
}
}
}
.card__column-name--current {
--btn-background: var(--card-color);
color: var(--color-ink-inverted);
opacity: 1 !important;
@media (hover: hover) {
&:hover {
--btn-background: var(--card-color);
}
}
}
}