diff --git a/Dockerfile b/Dockerfile index 25ee37e68..2e7a9b823 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile # Fetch beamer library -FROM basecamp/beamer:vfs AS beamer +FROM registry.37signals.com/basecamp/beamer:vfs AS beamer # Final stage for app image diff --git a/Gemfile.lock b/Gemfile.lock index ae7553c8d..bcd197099 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical - revision: 70ba099446c8a033c06c2bde9c5e70b7f983e60b + revision: 109735daa044233236cf7a72f30043e5c790e853 specs: actiontext-lexical (0.1.0) rails (>= 8.0.2) diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 59a5b9357..1431f23e8 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -10,6 +10,10 @@ --block-space-double: calc(var(--block-space) * 2); /* Text */ + --font-sans: system-ui; + --font-serif: ui-serif, serif; + --font-mono: ui-monospace, monospace; + --text-xx-small: 0.55rem; --text-x-small: 0.75rem; --text-small: 0.85rem; @@ -33,6 +37,11 @@ --footer-height: 3.5rem; --tray-size: clamp(12rem, 25dvw, 24rem); + /* Focus rings for keyboard navigation */ + --focus-ring-color: var(--color-link); + --focus-ring-offset: 1px; + --focus-ring-size: 2px; + @media (max-width: 799px) { --tray-size: var(--footer-height); } diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css index 2ad1ab090..aefcb864b 100644 --- a/app/assets/stylesheets/actiontext-lexical.css +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -6,12 +6,12 @@ figure.node--selected { &:not(:has(img)) { - box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark); + box-shadow: 0 0 0 var(--focus-ring-size) var(--focus-ring-color); } &:has(img) { img { - box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark); + box-shadow: 0 0 0 var(--focus-ring-size) var(--focus-ring-color); } } } @@ -45,7 +45,7 @@ &:is([type="submit"]) { --btn-background: var(--card-color, var(--color-link)); --btn-color: var(--color-ink-inverted); - --outline-color: var(--card-color, var(--color-link)); + --focus-ring-color: var(--card-color, var(--color-link)); } } @@ -55,8 +55,6 @@ } .lexical-editor__content { - --outline-size: max(2px, 0.08em); - margin: var(--block-space-half) 0; min-block-size: calc(7lh + var(--block-space)); outline: 0; @@ -177,15 +175,11 @@ padding: var(--lexical-prompt-padding); &:hover { - background-color: var(--color-selected); + background-color: var(--color-ink-lightest); } &[aria-selected] { - box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset; - - &:hover { - --hover-size: 0; - } + background-color: var(--color-selected); } img { @@ -196,6 +190,10 @@ margin: 0; } + + & { + margin-top: 2px; + } + code { background-color: var(--color-terminal-text-light); border-radius: 0.25ch; diff --git a/app/assets/stylesheets/avatars.css b/app/assets/stylesheets/avatars.css index 214ac26f1..e3f15d4ab 100644 --- a/app/assets/stylesheets/avatars.css +++ b/app/assets/stylesheets/avatars.css @@ -2,8 +2,7 @@ .avatar { --avatar-border-radius: 50%; --avatar-size-default: 5ch; - --btn-border-size: 0; - --hover-size: 0; + --btn-border-size: 0; aspect-ratio: 1; block-size: var(--avatar-size, var(--avatar-size-default)); @@ -16,19 +15,13 @@ :is(img, .icon) { aspect-ratio: 1; - block-size: auto; + block-size: 100%; border-radius: var(--avatar-border-radius); grid-area: 1/1; - inline-size: var(--avatar-size, var(--avatar-size-default)); + inline-size: 100%; max-inline-size: 100%; object-fit: cover; } - - &:where(a):not(:active):hover { - @media (any-hover: hover) { - filter: brightness(0.7); - } - } } .avatar__form { diff --git a/app/assets/stylesheets/base.css b/app/assets/stylesheets/base.css index 8ec590969..3971e9a95 100644 --- a/app/assets/stylesheets/base.css +++ b/app/assets/stylesheets/base.css @@ -8,13 +8,6 @@ } body { - --font-sans: system-ui; - --font-serif: ui-serif, serif; - --font-mono: ui-monospace, monospace; - --hover-color: var(--color-ink-medium); - --hover-size: 0.15rem; - --hover-filter: brightness(1); - -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; @@ -28,55 +21,33 @@ text-size-adjust: none; } - a:not([class]) { - --hover-size: 0; + a { + text-decoration: none; - color: var(--color-link); - text-decoration: underline; - text-decoration-skip-ink: auto; + &:not([class]) { + color: var(--color-link); + text-decoration: underline; + text-decoration-skip-ink: auto; + } } :is(a, button, input, textarea, .switch, .btn) { - --outline-size: 2px; - - caret-color: var(--color-link); - text-decoration: none; + transition: 100ms ease-out; + transition-property: background-color, border-color, box-shadow, filter, outline; touch-action: manipulation; - transition: - box-shadow 150ms ease, - outline-offset 150ms ease, - background-color 150ms ease, - opacity 150ms ease, - filter 150ms ease; - - /* Hover */ - @media (any-hover: hover) { - &:where(:not(:active):hover) { - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); - } - } /* Keyboard navigation */ - &:where(:not(:active)):focus-visible { - outline-color: var(--outline-color, currentColor); - outline-width: var(--outline-size); - outline-offset: var(--outline-offset, calc(var(--outline-size) * 2)); + &:where(:focus-visible) { + border-radius: 0.25ch; + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); } - &:where(:focus-visible):focus, - &:where(:focus-visible):active { - outline: 0; - } - - /* Pressing */ - &:focus:not(:focus-visible) { - --outline-offset: 0; - } - - /* Disabled */ - &:where([disabled]):not(:hover):not(:active) { + /* Default disabled styles */ + &:where([disabled]) { cursor: not-allowed; - filter: brightness(0.75); + opacity: 0.5; + pointer-events: none; } } diff --git a/app/assets/stylesheets/buttons.css b/app/assets/stylesheets/buttons.css index 34ceab7f4..6e92fb98c 100644 --- a/app/assets/stylesheets/buttons.css +++ b/app/assets/stylesheets/buttons.css @@ -1,7 +1,8 @@ @layer components { .btn { - --transition: 150ms ease; + --icon-size: var(--btn-icon-size, 1.3em); --btn-border-radius: 99rem; + --btn-hover-brightness: 0.9; align-items: center; background-color: var(--btn-background, var(--color-canvas)); @@ -16,57 +17,18 @@ justify-content: center; padding: var(--btn-padding, 0.5em 1.1em); pointer-events: auto; - transition: - background-color var( --transition), - border var( --transition), - color var( --transition), - filter var( --transition), - opacity var( --transition), - scale var( --transition); + position: relative; + transition: 100ms ease-out; + transition-property: background-color, border, box-shadow, color, filter, opacity, scale; - /* Default icon styles */ - &:where(:has(.icon)) { - --icon-size: var(--btn-icon-size, 1.3em); - - text-align: start; - } - - /* With radios and checkboxes */ - &:has(input[type=radio]), - &:has(input[type=checkbox]) { - position: relative; - - :is(input[type=radio]), - :is(input[type=checkbox]) { - --hover-size: 0; - - appearance: none; - border-radius: var(--btn-border-radius); - cursor: pointer; - display: flex; - inset: 0; - margin: 0; - padding: 0; - position: absolute; - - &:focus-visible { - outline: none; - } - } - - .checked { - display: none; + @media (any-hover: hover) { + &:hover { + filter: brightness(var(--btn-hover-brightness)); } } - &:has(input:checked) { - --btn-background: var(--color-ink); - --btn-color: var(--color-ink-inverted); - --outline-color: var(--color-ink); - - .checked { - display: block; - } + @media (prefers-color-scheme: dark) { + --btn-hover-brightness: 1.25; } &[disabled], @@ -93,15 +55,13 @@ --btn-color: inherit; --btn-icon-size: 100%; --btn-padding: 0; - --hover-size: 0; - --outline-size: 0; } .btn--link { --btn-background: var(--color-link); --btn-border-color: var(--color-canvas); --btn-color: var(--color-ink-inverted); - --outline-color: var(--color-link); + --focus-ring-color: var(--color-link); } .btn--circle, @@ -122,16 +82,16 @@ .btn--negative { --btn-background: var(--color-negative); - --btn-border-color: var(--color-canvas); + --btn-border-color: var(--color-negative); --btn-color: var(--color-ink-inverted); - --outline-color: var(--color-negative); + --focus-ring-color: var(--color-negative); } .btn--positive { --btn-background: var(--color-positive); --btn-border-color: var(--color-canvas); --btn-color: var(--color-ink-inverted); - --outline-color: var(--color-positive); + --focus-ring-color: var(--color-positive); } .btn--success { @@ -157,7 +117,51 @@ --btn-background: var(--color-ink); --btn-border-color: var(--color-canvas); --btn-color: var(--color-canvas); - --outline-color: var(--color-ink); + --focus-ring-color: var(--color-ink); + } + + /* Toggleable buttons + /* ------------------------------------------------------------------------ */ + + .btn { + &:has(input[type=radio], input[type=checkbox]) { + position: relative; + + :is(input[type=radio], input[type=checkbox]) { + appearance: none; + border-radius: var(--btn-border-radius); + cursor: pointer; + display: flex; + inset: 0; + margin: 0; + padding: 0; + position: absolute; + + &:focus-visible { + outline: none; + } + } + + .checked { + display: none; + } + } + + &:has(input:checked) { + --btn-background: var(--color-ink); + --btn-border-color: var(--color-ink); + --btn-color: var(--color-ink-inverted); + --focus-ring-color: var(--color-ink); + + .checked { + display: block; + } + } + + &:has(input:focus-visible) { + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + } } /* Button groups diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 4e028663f..ec8a848e0 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -118,6 +118,7 @@ /* ------------------------------------------------------------------------ */ .cards__heading { + background-color: var(--color-canvas); display: flex; font-size: var(--text-medium); @@ -129,17 +130,19 @@ position: sticky; z-index: 2; - &, button { + &, .cards__filter { font-weight: bold; text-transform: uppercase; } - button { - --hover-size: 0; + .cards__filter { + --filter-block-padding: 0.5rem; + background-color: var(--color-canvas); + border-radius: 99rem; inline-size: auto; - margin-inline-start: 2em; - padding: 0 2em 0 0; + margin-block: calc(-1 * var(--filter-block-padding)); + padding-block: var(--filter-block-padding); } dialog { diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index 9a4de11ed..13933d9da 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -48,10 +48,10 @@ .collection-picker__button { --btn-border-size: 0; - + font-size: 0.5em; margin-inline-start: 1ch; - + + .panel { --panel-size: 18ch; } @@ -152,6 +152,7 @@ &:has(input:checked) { --btn-background: var(--card-color); + --btn-border-color: var(--card-color); --btn-color: var(--color-ink-inverted); } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 4462dfb6e..710c8ae05 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -4,6 +4,7 @@ .card { --avatar-size: 2.75em; + --card-bg-color: color-mix(in srgb, var(--card-color) 4%, var(--color-canvas)); --card-padding-inline: var(--inline-space-double); --card-padding-block: var(--block-space); --border-color: transparent; @@ -11,7 +12,7 @@ --border-size: 0; aspect-ratio: var(--card-aspect-ratio, auto); - background-color: color-mix(in srgb, var(--card-color) 4%, var(--color-canvas)); + background-color: var(--card-bg-color); border-radius: var(--border-radius); box-shadow: var(--shadow); color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); @@ -64,6 +65,22 @@ max-inline-size: 100%; min-inline-size: 0; padding: var(--block-space-half) var(--inline-space) var(--block-space-half) var(--card-padding-inline); + + &:has(.collection-picker__button) { + cursor: pointer; + transition: background-color: 100ms ease-out; + + @media (any-hover: hover) { + &:hover { + background-color: color-mix(in srgb, var(--card-color) 90%, var(--color-ink)); + } + } + + &:has(.btn:focus-visible) { + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + } + } } .card__collection-name { @@ -139,8 +156,6 @@ } .card__link { - --hover-size: 0; - content: ""; inset: 0; position: absolute; @@ -168,7 +183,6 @@ } .card__title { - --hover-size: 0; --input-border-radius: 0; --input-color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); --lines: 3; @@ -204,10 +218,17 @@ --btn-icon-size: 1em; --card-padding-block: calc(var(--block-space) * 1.25); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-link) 20%, var(--color-canvas)); + box-shadow: 0 0 0 1px oklch(var(--lch-blue-lighter)); color: var(--btn-color); font-weight: 700; - transition: none; + transition: box-shadow 100ms ease-out; + + @media (any-hover: hover) { + &:hover { + filter: none; + box-shadow: 0 0 0 1px oklch(var(--lch-blue-lighter)), var(--shadow); + } + } } .card__move-button { @@ -324,7 +345,7 @@ /* Variants /* ------------------------------------------------------------------------ */ - .card--golden.card--doing { + .card--golden:is(.card--doing, .card--considering) { --lch-golden: 89.1% 0.178 95.7; background-color: var(--color-canvas); @@ -342,6 +363,17 @@ @media (prefers-color-scheme: dark) { --lch-golden: var(--lch-blue-medium); } + + &.card--considering { + .card__collection { + background-color: transparent; + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + } + + .card__collection-name { + border-color: color-mix(in srgb, var(--color-ink) 33%, oklch(var(--lch-golden))); + } + } } .card--notification { @@ -362,6 +394,7 @@ .card__collection { font-size: var(--text-xx-small); padding-block: 0.5ch; + padding-inline-start: var(--inline-space-double); } .card__header { @@ -393,7 +426,6 @@ --btn-icon-size: 0.75em; --btn-padding: 0; --btn-size: 1.25em; - --hover-size: 0; position: relative; z-index: 1; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 6b1953d27..5ac852793 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -98,12 +98,15 @@ } .comment__edit { + background-color: var(--color-ink-lightest); + .comment:not(.comment--mine) & { display: none; } } .comment__reaction { + background-color: var(--color-ink-lightest); opacity: 0.66; @media (any-hover: hover) { diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index e3c7d5d98..fa1674f76 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -205,10 +205,7 @@ } .event--related { - --hover-color: var(--card-color); - --hover-size: 0.15rem; - - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + box-shadow: 0 0 0 0.15rem var(--card-color); } .event__content { diff --git a/app/assets/stylesheets/expander.css b/app/assets/stylesheets/expander.css index 291851b34..0182f6497 100644 --- a/app/assets/stylesheets/expander.css +++ b/app/assets/stylesheets/expander.css @@ -30,7 +30,7 @@ } [open] & { - box-shadow: none; + filter: none; } .icon { @@ -44,6 +44,7 @@ align-items: stretch; border-radius: var(--expander-radius); flex-direction: column; + filter: none; gap: 0; padding-block-start: var(--expander-height); @@ -92,21 +93,19 @@ } .expander__item { - --btn-background: transparent; + --btn-background: var(--card-color); --btn-border-radius: 0.3em; --btn-border-size: 0; --btn-font-weight: 500; --btn-padding: var(--inline-space-half) var(--inline-space); - --hover-size: 0; - --outline-color: transparent; inline-size: calc(100% + var(--inline-space-double)); margin-inline-start: calc(-1 * var(--inline-space)); justify-content: flex-start; white-space: nowrap; - &:hover, + /* &:hover, &:focus-visible { background-color: oklch(var(--lch-canvas) / 15%); - } + } */ } diff --git a/app/assets/stylesheets/fizzy-menu.css b/app/assets/stylesheets/fizzy-menu.css index bead9d38a..fa29fe8b6 100644 --- a/app/assets/stylesheets/fizzy-menu.css +++ b/app/assets/stylesheets/fizzy-menu.css @@ -14,12 +14,19 @@ --input-border-color: var(--color-selected-dark); --input-padding: 0.2em 2em 0.2em 0.9em; + background-color: var(--color-canvas); box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%), 0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%), 0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%), 0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%); margin-block-start: 0.1em; + @media (any-hover: hover) { + &:hover { + filter: brightness(0.9); + } + } + svg { block-size: auto; inline-size: 1.1em; @@ -69,4 +76,4 @@ } } } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css new file mode 100644 index 000000000..acbb3eb59 --- /dev/null +++ b/app/assets/stylesheets/header.css @@ -0,0 +1,44 @@ +@layer components { + /* Centered title with space for two buttons on either side */ + .header { + --header-gap: 0.5ch; + --btn-icon-size: 1rem; + --header-btn-size: 2rem; + --header-button-count: 0; + --header-actions-width: calc((var(--header-btn-size) + var(--header-gap)) * var(--header-button-count)); + + align-items: center; + display: flex; + padding: var(--block-space-half) var(--inline-space); + + /* Change the grid size depending on how many buttons are present */ + &:has(.header__actions > *:nth-child(1)) { --header-button-count: 1; } + &:has(.header__actions > *:nth-child(2)) { --header-button-count: 2; } + &:has(.header__actions > *:nth-child(3)) { --header-button-count: 3; } + } + + .header__actions { + display: flex; + font-size: var(--text-x-small); + gap: var(--header-gap); + inline-size: var(--header-actions-width); + min-inline-size: fit-content; + } + + .header__actions--start { + margin-inline-end: auto; + } + + .header__actions--end { + margin-inline-start: auto; + justify-content: flex-end; + } + + .header__title { + color: inherit; + font-size: var(--text-large); + font-weight: 900; + margin: 0 auto; + text-align: center; + } +} diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index ed4a51072..ecd175e10 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -21,18 +21,12 @@ -webkit-box-shadow: 0 0 0px 1000px var(--color-selected) inset; } - &:where(:not(:active)):focus { - --input-border-color: var(--color-selected-dark); - --hover-color: var(--color-selected-dark); - --outline-size: 0; - --outline-color: transparent; - - filter: var(--hover-filter); - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + &:where(:focus) { + --focus-ring-offset: -1px; } - &:where([disabled]):not(:hover):not(:active) { - filter: none; + &[readonly] { + --focus-ring-size: 0; } } @@ -134,6 +128,9 @@ /* Switches */ .switch { + --switch-color: var(--color-ink-medium); + --switch-hover-brightness: 0.9; + block-size: 1.75em; border-radius: 2em; display: inline-flex; @@ -142,9 +139,7 @@ &:has(:focus-visible) { .switch__btn { - box-shadow: - 0 0 0 var(--outline-size) var(--color-canvas), - 0 0 0 calc(var(--outline-size) * 2) var(--color-ink); + outline: var(--focus-ring-size) solid var(--focus-ring-color); } } } @@ -156,10 +151,11 @@ } .switch__btn { - background-color: var(--color-ink-medium); + background-color: var(--switch-color); border-radius: 2em; cursor: pointer; inset: 0; + outline-offset: var(--focus-ring-offset); position: absolute; transition: 150ms ease; @@ -175,13 +171,20 @@ transition: 150ms ease; } + @media (any-hover: hover) { + &:hover { + background-color: color-mix(in srgb, var(--switch-color) 80%, var(--color-ink)); + } + } + .switch__input:disabled + & { - background-color: var(--color-ink-medium) !important; + --switch-color: var(--color-ink-medium) !important; + cursor: not-allowed; } .switch__input:checked + & { - background-color: var(--color-link); + --switch-color: var(--color-link); &::before { transform: translateX(1.2em); @@ -191,15 +194,13 @@ /* Containers that act like (and contain) inputs */ .input--actor { + outline-offset: -1px; transition: box-shadow 150ms ease, outline-offset 150ms ease; &:focus-within { --input-border-color: var(--color-selected-dark); - --hover-color: var(--color-selected-dark); - --outline-size: 0; - filter: var(--hover-filter); - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + outline: var(--focus-ring-size) solid var(--focus-ring-color); } .input { @@ -207,9 +208,6 @@ --input-border-radius: 0; --input-background: transparent; --input-border-size: 0; - --hover-size: 0; - --outline-size: 0; - --outline-color: transparent; inline-size: 100%; outline: 0; @@ -233,11 +231,9 @@ } .input.boost__input { - --hover-size: 0; --input-border-radius: 0; --input-border-size: 0; --input-padding: 0; - --outline-size: 0; color: inherit; font-size: inherit; diff --git a/app/assets/stylesheets/knobs.css b/app/assets/stylesheets/knobs.css index 390fa6d5a..cf73d217b 100644 --- a/app/assets/stylesheets/knobs.css +++ b/app/assets/stylesheets/knobs.css @@ -19,8 +19,6 @@ } .knob__slider { - --hover-size: 0; - appearance: none; background-color: transparent; block-size: var(--knob-size); @@ -75,7 +73,8 @@ } &:has(:focus-visible) { - box-shadow: 0 0 0 2px var(--knob-color); + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: 0; } /* Tick marks */ diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css index 9bf4d4953..40de97e09 100644 --- a/app/assets/stylesheets/layout.css +++ b/app/assets/stylesheets/layout.css @@ -92,7 +92,9 @@ display: none; } } +} +@layer components { .skip-navigation { --left-offset: -999em; diff --git a/app/assets/stylesheets/markdown.css b/app/assets/stylesheets/markdown.css index 3e91fb41e..5dfac832c 100644 --- a/app/assets/stylesheets/markdown.css +++ b/app/assets/stylesheets/markdown.css @@ -1,6 +1,5 @@ @layer components { .heading__link { - --hover-size: 0; --opacity: 0.5; --size: 0.8em; diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 8e9edcea5..1b158f9e6 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -59,14 +59,18 @@ @media (any-hover: hover) { &:hover { - --hover-size: 0; - - background: var(--color-selected); + background: var(--color-ink-lightest); } } &[aria-selected] { - box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset; + background-color: var(--color-selected); + + @media (any-hover: hover) { + &:hover { + background-color: var(--color-selected); + } + } } } @@ -76,8 +80,6 @@ --btn-border-size: 0; --btn-font-weight: 500; --btn-padding: var(--inline-space-half) 0 var(--inline-space-half) var(--inline-space); - --hover-size: 0; - --outline-color: transparent; flex: 1 1 auto; justify-content: start; @@ -90,11 +92,6 @@ } } - &[aria-selected] { - border-radius: 0.3em; - box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset; - } - &:has(input:checked) { --btn-background: transparent; --btn-color: var(--color-ink); @@ -110,7 +107,6 @@ @media (any-hover: hover) { &:where(:not(:active):hover) { - --hover-size: 0; --btn-background: var(--color-selected); } } diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index 22454ca51..f232c925f 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -26,6 +26,7 @@ .reaction { --btn-size: 100%; + --reaction-hover-brightness: 0.9; align-items: center; background-color: var(--color-canvas); @@ -51,18 +52,13 @@ @media (any-hover: hover) { &:not(:active):hover { - --reaction-border-color: var(--color-ink-medium); + filter: brightness(var(--reaction-hover-brightness)); - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + @media (prefers-color-scheme: dark) { + --reaction-hover-brightness: 1.25; + } } } - - &:has(.reaction__input):focus-within { - --hover-color: var(--color-selected-dark); - --reaction-border-color: var(--color-selected-dark); - - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); - } } /* Make the avatar and delete buttons fit nicely within the reaction */ @@ -74,6 +70,7 @@ .reaction__delete { display: none; + translate: 0.25em; .expanded & { display: flex; @@ -89,17 +86,20 @@ transform-origin: left center; } + &:has(.input:focus) { + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + } + .reaction__form-label:focus { outline: none; } } .reaction__input { - --hover-size: 0; --input-background: transparent; --input-border-size: 0; --input-padding: 0; - --outline-size: 0; box-shadow: none; display: inherit; diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index a24c1845d..396e56db1 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -210,10 +210,6 @@ } } - .attachment__figure { - --hover-size: 0; - } - .attachment--file { align-items: center; display: inline-flex; @@ -329,8 +325,6 @@ } .attachment__link { - --hover-size: 0; - color: var(--color-link); text-decoration: underline; } diff --git a/app/assets/stylesheets/terminals.css b/app/assets/stylesheets/terminals.css index 116a92e1c..c2f543fb6 100644 --- a/app/assets/stylesheets/terminals.css +++ b/app/assets/stylesheets/terminals.css @@ -23,8 +23,6 @@ --btn-border-color: var(--color-terminal-text); --btn-padding: 0.1em 0.7em; --btn-border-size: 1px; - --hover-color: var(--color-terminal-text); - --hover-size: 1px; font-family: var(--font-mono); font-size: var(--text-x-small); @@ -49,14 +47,21 @@ } .terminal__help { + background: var(--color-terminal-bg); + border-radius: 1em; display: none; + inline-size: auto; + inset: 50% auto auto 50%; + max-inline-size: 90vw; + + padding: calc(var(--block-space) * 1.5) var(--block-space-double); + position: fixed; + transform: translate(-50%, -50%); + .terminal--showing-help &, .terminal--open .terminal__menu:empty ~ & { - border-block-end: 1px dashed; display: block; - margin-block-end: 0.5lh; - padding-block-end: 0.5lh; } h2 { @@ -76,7 +81,7 @@ dl { display: grid; grid-template-columns: min-content 1fr; - gap: 0.5em 1em; + gap: var(--inline-space); font-size: var(--text-small); margin: 0; } @@ -89,16 +94,14 @@ dd { grid-column: 2; + white-space: nowrap; } } .terminal__input:is(.input) { --input-border-color: transparent; --input-border-radius: 0.3em; - --hover-color: transparent; --input-color: var(--color-terminal-text); - --outline-size: 0; - --outline-color: transparent; accent-color: var(--color-terminal-text); caret-color: var(--color-terminal-text); diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 48ae50b31..8ea7aea54 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -63,9 +63,6 @@ /* ------------------------------------------------------------------------ */ .tray__expander { - --hover-size: 0; - --outline-size: 0; - align-self: stretch; background: transparent; border: 0; @@ -116,8 +113,6 @@ /* ------------------------------------------------------------------------ */ .tray__actions { - --hover-size: 0; - align-self: center; align-items: stretch; border: 1px dashed oklch(var(--lch-white) / 50%); diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 741e417d7..45cfd7d56 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -26,7 +26,7 @@ .txt-capitalize { text-transform: capitalize; } .txt-link { color: var(--color-link); text-decoration: underline; } - .font-black { font-weight: 900; } + .font-weight-black { font-weight: 900; } .font-weight-normal { font-weight: normal; } /* Flexbox and Grid */ @@ -218,4 +218,8 @@ /* Visibility */ [hidden] { display: none !important; } [contents] { display: contents; } + + .hide-focus-ring { + --focus-ring-size: 0; + } } diff --git a/app/assets/stylesheets/workflows.css b/app/assets/stylesheets/workflows.css index 2486b99fa..2dc043584 100644 --- a/app/assets/stylesheets/workflows.css +++ b/app/assets/stylesheets/workflows.css @@ -4,14 +4,13 @@ --btn-padding: 0.2em 0.5em; --btn-border-size: 0; --btn-border-radius: 0.2em; - --hover-size: 0; color: inherit; inline-size: 100%; @media (hover: hover) { &:hover { - background-color: color(from var(--card-color) srgb r g b / 0.15); + --btn-background: color(from var(--card-color) srgb r g b / 0.15); } } } @@ -23,21 +22,29 @@ @media (hover: hover) { &:hover { - background-color: color(from var(--card-color) srgb r g b / 0.65); + --btn-background: var(--card-color); } } } + /* Workflow settings + /* ------------------------------------------------------------------------ */ + .workflow-preview { --border-size: 0.2em; aspect-ratio: 1; + border: 0.2em solid var(--color-ink-lighter); flex: 1 1 30%; max-inline-size: 30%; - button { - --hover-size: 0; + @media (any-hover: hover) { + &:hover:not(.workflow-preview--selected) { + border-color: var(--color-ink-light); + } + } + button { appearance: none; aspect-ratio: 1; background: transparent; @@ -58,15 +65,23 @@ } .workflow-preview--selected { - --border-color: var(--color-link); + border-color: var(--color-link); } .workflow-preview__swatch { --btn-size: 1em; --btn-border-radius: 50%; --btn-border-size: 0; - --hover-size: 0; cursor: auto; } + + .workflow__swatch-btn { + + @media (any-hover: hover) { + &:hover { + filter: brightness(0.9); + } + } + } } diff --git a/app/controllers/cards/collections_controller.rb b/app/controllers/cards/collections_controller.rb index 669200940..31d48b8a5 100644 --- a/app/controllers/cards/collections_controller.rb +++ b/app/controllers/cards/collections_controller.rb @@ -1,9 +1,9 @@ -module Cards - class CollectionsController < ApplicationController - def update - @card = Card.find(params[:card_id]) - @card.update!(collection_id: params[:collection_id]) - redirect_to @card - end +class Cards::CollectionsController < ApplicationController + include CollectionScoped + + def update + @card = Current.user.accessible_cards.find(params[:card_id]) + @card.move_to(@collection) + redirect_to @card end end diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index cb72bffd2..985f5a553 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -35,6 +35,7 @@ module CardsHelper classes = [ options.delete(:class), ("card--golden" if card.golden?), + ("card--considering" if card.considering?), ("card--doing" if card.doing?), ("card--drafted" if card.drafted?) ].compact.join(" ") diff --git a/app/javascript/controllers/local_time_controller.js b/app/javascript/controllers/local_time_controller.js index 7e38772c9..ee7dfd3a3 100644 --- a/app/javascript/controllers/local_time_controller.js +++ b/app/javascript/controllers/local_time_controller.js @@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus" import { differenceInDays } from "helpers/date_helpers" export default class extends Controller { - static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays", "daysago", "agoorweekday" ] + static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays", "daysago", "agoorweekday", "timeordate" ] static values = { refreshInterval: Number } static classes = [ "local-time-value"] @@ -19,6 +19,7 @@ export default class extends Controller { this.datewithweekdayFormatter = new Intl.DateTimeFormat(undefined, { weekday: "long", month: "long", day: "numeric" }) this.indaysFormatter = new InDaysFormatter() this.agoorweekdayFormatter = new DaysAgoOrWeekdayFormatter() + this.timeordateFormatter = new TimeOrDateFormatter() } connect() { @@ -75,6 +76,10 @@ export default class extends Controller { this.#formatTime(this.agoorweekdayFormatter, target) } + timeordateTargetConnected(target) { + this.#formatTime(this.timeordateFormatter, target) + } + #refreshRelativeTimes() { this.agoTargets.forEach(target => { this.#formatTime(this.agoFormatter, target) @@ -148,6 +153,18 @@ class InDaysFormatter { } } +class TimeOrDateFormatter { + format(date) { + const days = differenceInDays(date, new Date()) + + if (days >= 1) { + return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(date) + } else { + return new Intl.DateTimeFormat(undefined, { timeStyle: "short" }).format(date) + } + } +} + function styleableValue(value) { return `${value}` } diff --git a/app/jobs/collection/clean_inaccessible_data_job.rb b/app/jobs/collection/clean_inaccessible_data_job.rb new file mode 100644 index 000000000..16ff1b90f --- /dev/null +++ b/app/jobs/collection/clean_inaccessible_data_job.rb @@ -0,0 +1,5 @@ +class Collection::CleanInaccessibleDataJob < ApplicationJob + def perform(user, collection) + collection.clean_inaccessible_data_for(user) + end +end diff --git a/app/models/access.rb b/app/models/access.rb index 32595ff9f..88303bd15 100644 --- a/app/models/access.rb +++ b/app/models/access.rb @@ -6,6 +6,8 @@ class Access < ApplicationRecord scope :ordered_by_recently_accessed, -> { order(accessed_at: :desc) } + after_destroy_commit :clean_inaccessible_data_later + def accessed touch :accessed_at unless recently_accessed? end @@ -14,4 +16,8 @@ class Access < ApplicationRecord def recently_accessed? accessed_at&.> 5.minutes.ago end + + def clean_inaccessible_data_later + Collection::CleanInaccessibleDataJob.perform_later(user, collection) + end end diff --git a/app/models/card.rb b/app/models/card.rb index 0c38c51bf..c33715bd8 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -38,6 +38,13 @@ class Card < ApplicationRecord self end + def move_to(new_collection) + transaction do + card.update!(collection: new_collection) + card.events.update_all(collection_id: new_collection.id) + end + end + private def set_default_title self.title = "Untitled" if title.blank? diff --git a/app/models/card/engageable.rb b/app/models/card/engageable.rb index cf8142934..7d26308a2 100644 --- a/app/models/card/engageable.rb +++ b/app/models/card/engageable.rb @@ -9,7 +9,7 @@ module Card::Engageable scope :by_engagement_status, ->(status) do case status.to_s - when "considering" then considering + when "considering" then considering.with_golden_first when "doing" then doing.with_golden_first end end diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index ad0f310d0..ebbcce1ce 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -26,13 +26,4 @@ module Card::Searchable def title_and_description [ title, description.to_plain_text ].join(" ") end - - def search_embedding_content - <<~CONTENT - Title: #{title} - Description: #{description.to_plain_text} - Created by: #{creator.name}} - Assigned to: #{assignees.map(&:name).join(", ")}} - CONTENT - end end diff --git a/app/models/collection/accessible.rb b/app/models/collection/accessible.rb index b4daae86f..264d03af8 100644 --- a/app/models/collection/accessible.rb +++ b/app/models/collection/accessible.rb @@ -36,8 +36,27 @@ module Collection::Accessible accesses.find_by(user: user) end + def accessible_to?(user) + access_for(user).present? + end + + def clean_inaccessible_data_for(user) + return if accessible_to?(user) + + clean_inaccessible_records user.notifications + clean_inaccessible_records user.mentions + end + private def grant_access_to_everyone accesses.grant_to(User.all) if all_access_previously_changed?(to: true) end + + def clean_inaccessible_records(records) + records.find_each do |record| + if record.card&.collection == self + record.destroy + end + end + end end diff --git a/app/models/command/ai/translator.rb b/app/models/command/ai/translator.rb index 9459f6d50..1ae707e58 100644 --- a/app/models/command/ai/translator.rb +++ b/app/models/command/ai/translator.rb @@ -234,13 +234,13 @@ class Command::Ai::Translator "commands": ["/assign andy", "/tag #v2"] } - User: assign to jz* + User: assign to jz Output: { "commands": ["/assign jz"] } - User: cards assigned to jz* + User: cards assigned to jz Output: { "context": { "assignee_ids": ["jz"] } @@ -259,74 +259,74 @@ class Command::Ai::Translator "commands": ["/reopen"] } - User: tag with #design* + User: tag with #design Output: { "commands": ["/tag #design"] } - User: completed cards* + User: completed cards Output: { "context": { "indexed_by": "closed" } } - User: completed cards yesterday* + User: completed cards yesterday Output: { "context": { "indexed_by": "closed", "closure": "yesterday" } } - User: "cards tagged with #design"* + User: "cards tagged with #design" Output: { "context": { "tag_ids": ["design"] } } - User: Unassigned cards* + User: Unassigned cards Output: { "context": { "assignment_status": "unassigned" } } - User: Close Andy’s cards, then assign them to Kevin* + User: Close Andy’s cards, then assign them to Kevin Output: { "context": { "assignee_ids": ["andy"] }, "commands": ["/close", "/assign kevin"] } - User: cards created yesterday* + User: cards created yesterday Output: { "context": { "creation": "yesterday" } } - User: cards completed last week* + User: cards completed last week Output: { "context": { "closure": "lastweek", "indexed_by": "closed" } } - User: my cards that are going to be auto closed* + User: my cards that are going to be auto closed Output: { "context": { "assignee_ids": [""], "indexed_by": "closing_soon" } } - User: visit user kevin* + User: visit user kevin Output: { "commands": ["/user kevin"] } - User: visit /users/kevin* + User: visit /users/kevin Output: { "commands": ["/visit /users/kevin"] } - Fallback search example (when nothing matches):* + Fallback search example (when nothing matches): { "commands": ["/search what's blocking deploy"] } ---------------------------- END OF PROMPT --------------------------- diff --git a/app/models/comment.rb b/app/models/comment.rb index 971603ae7..15099c09b 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -22,12 +22,4 @@ class Comment < ApplicationRecord def watch_card_by_creator card.watch_by creator end - - def search_embedding_content - <<~CONTENT - Card title: #{card.title} - Content: #{body.to_plain_text} - Created by: #{creator.name}} - CONTENT - end end diff --git a/app/models/tag.rb b/app/models/tag.rb index 15bab718e..0247a0fa5 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -13,4 +13,8 @@ class Tag < ApplicationRecord def hashtag "#" + title end + + def cards_count + cards.count + end end diff --git a/app/views/accounts/settings/_users.html.erb b/app/views/accounts/settings/_users.html.erb index 7ffd3c5a5..60eecef33 100644 --- a/app/views/accounts/settings/_users.html.erb +++ b/app/views/accounts/settings/_users.html.erb @@ -7,7 +7,7 @@ -
+
<%= render partial: "accounts/user", collection: users %>
diff --git a/app/views/accounts/settings/show.html.erb b/app/views/accounts/settings/show.html.erb index 7df94999c..9614946e0 100644 --- a/app/views/accounts/settings/show.html.erb +++ b/app/views/accounts/settings/show.html.erb @@ -1,17 +1,19 @@ <% @page_title = "Account Settings" %> <% content_for :header do %> -