This commit is contained in:
Jason Zimdars
2025-03-03 16:17:54 -08:00
parent 9a6d73b4d0
commit 28a569c49b
98 changed files with 1688 additions and 829 deletions
+1
View File
@@ -0,0 +1 @@
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m15.9 19.5-11-11c-.3.8-.4 1.7-.4 2.6v4.9c0 .8-.7 1.5-1.5 1.5-.6 0-1 .4-1 1s.4 1 1 1z"/><path d="m9.5 21.2v.3c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5c0-.1 0-.2 0-.3s-.1-.2-.2-.2h-4.5c-.2 0-.3.1-.3.2z"/><path d="m23.7 23.7c.4-.4.4-1 0-1.4l-2.8-2.8h.1c.6 0 1-.4 1-1s-.4-1-1-1c-.8 0-1.5-.7-1.5-1.5v-4.9c.1-3.9-2.6-7.4-6.5-8.1v-2c0-.6-.4-1-1-1s-1 .4-1 1v2.1c-1.6.3-3.1 1-4.3 2.3l-5-5c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4l22 22c.2.2.4.3.7.3.3-.1.5-.2.7-.4z"/></svg>

After

Width:  |  Height:  |  Size: 557 B

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 310.61 281.75" xmlns="http://www.w3.org/2000/svg"><path d="m20.47 271.92c-1.6-6.97 2.82-15.44 9.97-17.4 12.8-3.61 24.89 6.78 37.67 6.31 15.88-.37 21.55-17.47 30.42-27.85 24.86-29.76 43.09-15.99 55.66-29.43 6.11-6.89 6.02-17.7 11.34-25.03 3.56-5.2 10.53-8.05 16.55-5.64 4.98 1.84 8.18 6.54 11.3 10.68 1.82 2.38 3.85 4.63 6.2 5.91 4.23 2.49 9.06.6 10.31-3.89 3.96-14.02-15.89-22.54-12.55-34.08 1.29-3.43 4.96-4.99 9.06-4.91 6.05.12 11.44 3.32 17.61 4.46 28.76 4.66 13.73-34.72 14.62-50.46.19-27.14 15.28-45.38 34.38-62.66 5.49-5.16 13.69-10.19 21.22-7.22 5.82 2.17 8.96 9.87 10.6 16.13 5.62 22.06 9.09 44.93 1.11 66.37-20.23 50.47-56.78 95.13-102.65 123.97-46.23 27.85-101.79 42.33-155.42 44.54-9.31.01-24.05 1.27-27.35-9.61l-.04-.17z" fill="#fff"/><g stroke="#000" stroke-miterlimit="10" stroke-width="6"><circle cx="109.95" cy="235.27" fill="#fff" r="15.98"/><circle cx="159.65" cy="148.41" fill="none" r="15.53"/><circle cx="205.67" cy="113.68" fill="none" r="11.29"/><circle cx="200.45" cy="216.88" fill="#fff" r="12.28"/><circle cx="69.15" cy="187.8" fill="none" r="11.44"/><circle cx="103.6" cy="205.59" fill="#fff" r="6.35"/><circle cx="64.07" cy="232.27" fill="#fff" r="6.35"/><circle cx="153.29" cy="219.28" fill="#fff" r="6.35"/><circle cx="261.43" cy="128.79" fill="#fff" r="10.02"/><circle cx="222.19" cy="140.93" fill="#fff" r="5.22"/><circle cx="236.73" cy="187.8" fill="#fff" r="5.22"/><circle cx="292.7" cy="61.47" fill="#fff" r="5.22"/><circle cx="252.54" cy="61.47" fill="none" r="15.81"/><circle cx="292.7" cy="88.64" fill="#fff" r="11.36"/><path d="m13.03 265.59c-10.1-3.27-13.54-18.51-5.82-25.79 5.26-4.97 13.31-5.68 19.21-9.86s8.92-11.25 11.38-18.04c4.51-12.46 8.08-25.25 10.69-38.24.84-4.17 1.59-8.42 3.48-12.24 4.88-9.87 17.2-14.91 27.98-12.64s19.6 10.92 23.56 21.2c2.26 5.87 3.12 12.28 6.03 17.87s8.87 10.38 15.03 9.11c6.49-1.34 10.06-8.97 9.26-15.55s-4.75-12.3-8.56-17.73-7.72-11.18-8.43-17.78c-.99-9.28 4.93-18.37 12.98-23.09s17.82-5.68 27.12-4.9c6.21.52 13.48 1.4 17.73-3.15 5.15-5.52 1.88-14.38 2.65-21.89 1.32-12.95 15.25-21.16 28.21-22.37 2.29-.21 4.65-.29 6.78-1.16 6.09-2.48 8.01-10.2 7.84-16.78s-1.52-13.51 1.23-19.49c4.42-9.63 17.06-12.01 27.62-11.13s21.63 3.68 31.42-.37" fill="none"/><circle cx="288.05" cy="19.59" fill="#fff" r="16.59"/><circle cx="18.98" cy="262.62" fill="#fff" r="15.98"/><circle cx="205.67" cy="67.25" fill="#fff" r="10.02"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

+1 -1
View File
@@ -1 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m10 20v-6h4v6h5v-8h3l-10-9-10 9h3v8z"/></svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m12 3 8 6v12h-5v-7h-6v7h-5v-12z"/></svg>

Before

Width:  |  Height:  |  Size: 114 B

After

Width:  |  Height:  |  Size: 109 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m1.50197 7.275 5.764-5.765c.57744-.579297 1.51515-.580803 2.09445-.00337.07782.07757.14676.16355.20555.25637l4.10003 6.443c1.8579-.44233 3.8147-.15604 5.468.8.7059.4121.944 1.3184.5319 2.0243-.0641.1098-.142.2108-.2319.3007l-8.105 8.1c-.3257.3218-.7805.4772-1.235.422-.45519-.0584-.85766-.3243-1.09003-.72-.96021-1.6515-1.24675-3.6096-.8-5.467l-6.443-4.1c-.68979-.43852-.893482-1.3532-.45496-2.04299.05793-.09112.12565-.17562.20196-.25201z"/><path d="m16.54 14.428 5.94 5.94c.5856.5857.5856 1.5352 0 2.121-.5896.5764-1.5315.5764-2.121 0l-5.94-5.94c-.5755-.5959-.559-1.5455.0368-2.121.5813-.5614 1.5029-.5614 2.0842 0z"/></g></svg>

After

Width:  |  Height:  |  Size: 749 B

+1 -1
View File
@@ -1 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m23 10c0-1.11-.9-2-2-2h-6.32l.96-4.57c.02-.1.03-.21.03-.32 0-.41-.17-.79-.44-1.06l-1.06-1.05-6.58 6.58c-.37.37-.59.87-.59 1.42v10a2 2 0 0 0 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73zm-22 11h4v-12h-4z"/></svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m10.2 11.2h-2.1c-.2 0-.4-.2-.4-.4v-2.1c0-.6-.5-1.1-1.1-1.1s-1.1.5-1.1 1.1v2.1c0 .2-.2.4-.4.4h-2.1c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1h2.1c.2 0 .4.2.4.4v2.1c0 .6.5 1.1 1.1 1.1s1.1-.5 1.1-1.1v-2.1c0-.2.2-.4.4-.4h2.1c.6 0 1.1-.5 1.1-1.1s-.5-1.1-1.1-1.1z"/><rect height="16.6" rx="1.4" width="2.7" x="16.2" y="3.4"/><rect height="8.3" rx="1.4" transform="matrix(0 1 -1 0 36.3 .9)" width="2.7" x="16.3" y="14.4"/><rect height="6.6" rx="1.4" transform="matrix(.70710678 .70710678 -.70710678 .70710678 9 -9.5)" width="2.7" x="14.6" y="2.8"/></svg>

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 608 B

+1
View File
@@ -0,0 +1 @@
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m22.5 20.4-5.4-5.4 3-3c.2-.1.3-.3.4-.5.7-1.2.3-2.7-.9-3.4-1.7-1-3.6-1.3-5.5-1l-3.7-5.9c-.1-.2-.2-.3-.3-.4-.5-.5-1.1-.7-1.8-.7s-1.3.2-1.7.7l-5.8 5.8c-.1.1-.2.2-.3.4-.7 1.2-.4 2.7.8 3.4l5.9 3.7c-.3 1.9.1 3.8 1 5.5.4.7 1.1 1.1 1.8 1.2h.3c.7 0 1.3-.3 1.8-.7l3-3 5.3 5.3c.6.6 1.5.6 2.1 0 .6-.5.6-1.4 0-2zm-11.9-1.7c-.1.1-.3.2-.4.1-.1 0-.3-.1-.4-.2-.8-1.4-1.1-3.1-.7-4.7l.2-.7-7-4.5c-.2-.1-.3-.4-.1-.6 0 0 0-.1.1-.1l5.7-5.8c.1-.1.3-.1.3-.1.1 0 .2 0 .3.1 0 0 0 0 .1.1l4.5 7 .7-.2c1.6-.4 3.3-.1 4.7.7.1.1.2.2.2.3v.4s0 .1-.1.1z"/></svg>

After

Width:  |  Height:  |  Size: 630 B

+4
View File
@@ -17,6 +17,10 @@
max-inline-size: 100%;
object-fit: cover;
}
+ .avatar {
margin-inline-start: -0.8em;
}
}
.avatar__group {
+11 -65
View File
@@ -62,16 +62,6 @@
z-index: 1;
}
}
.bubble__container {
inline-size: min(67ch, 75vw);
max-inline-size: 100dvw;
@media (max-width: 70ch) {
inline-size: min(67ch, 100dvw);
}
}
.bubble__detail {
--bubble-border-color: color-mix(in srgb, var(--bubble-color) 30%, var(--color-bg));
--bubble-border-width: 0.15em;
@@ -101,51 +91,6 @@
color: var(--bubble-color);
}
&.bubble__boosts {
--rotation: 45deg;
aspect-ratio: 5/7;
border-radius: var(--bubble-shape);
inline-size: min-content;
inset: 70cqi 5cqi auto auto;
min-inline-size: 4.5ch;
padding: 0;
transform: rotate(var(--rotation));
.boost__form {
transform: rotate(calc(var(--rotation) * -1));
}
.boost__btn {
--btn-background: var(--bubble-color);
--btn-border-radius: 50%;
--btn-color: var(--color-ink-reversed);
--btn-size: 0.5em;
--btn-padding: 0.3em 0.5em 0.4em;
font-weight: 800;
}
.boost__input {
color: var(--bubble-color);
font-size: inherit;
font-weight: 800;
padding: 0;
text-align: center;
@supports (field-sizing: content) {
field-sizing: content;
}
}
@media (hover: hover) {
.bubbles .bubble:not(.popped):hover & {
transform: rotate(calc(var(--rotation) + var(--bubble-shift) * 1)) translate(0rem, 1rem);
}
}
}
&.bubble__assignee {
font-size: 4.5cqi;
inset: 8em auto auto -1.7em;
@@ -321,8 +266,14 @@
--divider-offset: 1.2em;
--hover-size: 0;
font-size: var(--text-medium);
max-inline-size: 80ch;
container-type: inline-size;
font-size: 1.4cqi;
inline-size: min(90ch, 100dvw);
max-inline-size: 100dvw;
@media (max-width: 70ch) {
inline-size: 100%;
}
.bubble {
order: 0;
@@ -337,7 +288,7 @@
margin-inline: var(--divider-offset);
li {
border-radius: 0.6em;
border-radius: 0;
list-style: none;
padding: 0.5em var(--inline-space);
position: relative;
@@ -350,7 +301,6 @@
&:hover {
background-color: color(from var(--bubble-color) srgb r g b / 0.15);
border: 0;
border-radius: 0.6em;
}
}
}
@@ -408,8 +358,9 @@
}
.bubble__title-text {
font-weight: 600;
font-weight: 700;
flex-grow: 1;
letter-spacing: -0.015em;
white-space: nowrap;
.drafted & {
@@ -550,11 +501,6 @@
position: relative;
z-index: 1;
.btn {
--btn-background: var(--bubble-color);
--btn-color: var(--color-ink-reversed);
}
.btn--plain {
--btn-color: var(--bubble-color);
+317
View File
@@ -0,0 +1,317 @@
.cards {
--column-gap: 2cqi;
--row-gap: 2cqi;
font-size: 0.9cqi;
display: grid;
background-color: var(--color-container);
grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(100%/2 - var(--column-gap) * 2)), 1fr));
justify-items: center;
margin-block: 0;
padding: 3cqi;
view-transition-name: cards-container;
> * {
flex-shrink: 1;
}
.card {
max-inline-size: 50cqi;
}
@media (min-width: 70ch) {
grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(100%/3 - var(--column-gap) * 2)), 1fr));
font-size: 0.7cqi;
.card {
max-inline-size: 33cqi;
}
}
}
.card {
--block-space: 1em;
--column-gap: 0.5em;
--inline-space: 0.5em;
--row-gap: 0.5em;
aspect-ratio: 15/7;
background-color: color-mix(in srgb, var(--bubble-color) 4%, var(--color-bg));
color: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink));
font-size: 1em;
max-inline-size: 100ch;
z-index: 1;
.card__container & {
inline-size: 60cqi;
@media (max-width: 70ch) {
inline-size: 100dvw;
}
}
}
.card__actions {
position: absolute;
inset: 1.4em auto auto -4em;
z-index: 1;
}
.card__actions--right {
inset: 1.4em -3.5em auto auto;
}
.card__actions-container {
background-color: var(--color-bg);
margin-block: -1em 1em;
padding: 1em 2em;
position: relative;
z-index: 0;
.btn {
--btn-background: var(--bubble-color);
--btn-color: var(--color-ink-reversed);
}
.btn--plain {
--btn-color: var(--bubble-color);
text-decoration: underline;
}
.bubble__pop-message {
color: var(--bubble-color);
}
}
.card__actions-wrapper {
font-size: 1.33cqi;
}
.cards .card__assignees:not(:has(.avatar)) {
.txt-uppercase {
display: none;
}
}
.card__background {
inset: 0;
position: absolute;
z-index: -1;
img {
block-size: 100%;
inline-size: 100%;
object-fit: cover;
object-position: center;
opacity: 0.2;
transition: opacity 0.2s ease-in-out;
}
@media (hover: hover) {
.card:hover & {
img {
opacity: 1;
}
}
}
}
.card__body {
padding-inline: 1em;
}
.card__body-header {
flex: 2 1 75%;
padding-block: 1em;
}
.card__bucket {
background-color: var(--bubble-color);
font-weight: 800;
letter-spacing: 0.5ch;
padding: 0.5em 1em;
}
.card__container {
background-color: var(--color-container);
display: inline-block;
font-size: 1.33cqi;
padding: 2cqi 3cqi 3cqi;
}
.card__container--pointing {
--arrow-size: 20em;
--aspect-ratio: 7;
position: relative;
margin-block-end: calc(var(--arrow-size) / var(--aspect-ratio));
&::after {
aspect-ratio: var(--aspect-ratio);
background: var(--color-container);
clip-path: polygon(50% 100%, 100% 0, 0 0);
inline-size: var(--arrow-size);
content: "";
display: block;
inset-block-start: 99%;
inset-inline-start: 50%;
position: absolute;
transform: translateX(-50%);
}
}
.card__delete-btn {
.cards & {
display: none;
}
}
.card__footer {
margin-block-end: 1em;
padding-inline-end: 1em;
}
.card__header {
padding-inline-end: 0.7em;
}
.card__hide-on-index {
.cards & {
display: none;
}
}
.card__link {
--hover-size: 0;
content: "";
inset: 0;
position: absolute;
z-index: -1;
.cards & {
z-index: 1;
}
}
.card__meta {
--border-color: var(--bubble-color);
--row-gap: 0.25em;
font-size: 0.9em;
margin-inline-start: 1em;
strong {
font-size: 1.1em;
font-weight: 900;
}
.separator {
height: calc(2lh + 2ex);
}
}
.card__people {
margin-inline-start: 1em;
span {
font-size: 0.9em;
}
}
.card__stages {
flex: 1 1 25%;
font-size: 0.8em;
padding-block: 1em;
}
.card__tags {
color: var(--bubble-color);
}
.card__tag {
color: var(--bubble-color);
font-weight: 700;
}
.card__title {
--hover-size: 0;
--input-border-radius: 0;
font-size: 2.5em;
font-weight: 900;
line-height: 1.1;
min-block-size: 3lh;
text-wrap: balance;
.cards & {
pointer-events: none;
}
.card__title-field {
&:is(textarea) {
&::placeholder {
color: inherit;
opacity: 0.5;
}
}
}
.card__title-link {
--lines: 3;
color: inherit;
}
}
.card-count__selector {
font-size: 1cqi;
inset: 0.5cqi 0.5cqi auto auto;
position: absolute;
text-align: start;
.popup {
inset: 1em 1em auto auto;
}
}
.input.boost__input {
--input-border-radius: 0;
--input-border-size: 0;
--input-padding: 0;
color: inherit;
font-size: 1.1em !important;
font-weight: inherit;
inline-size: min-content;
max-inline-size: 3ch;
min-inline-size: 1ch;
@supports (field-sizing: content) {
field-sizing: content;
}
}
.card__popped {
align-items: center;
aspect-ratio: 7/4;
border: 0.4rem solid var(--bubble-color);
position: absolute;
inset: auto 2em 2em auto;
justify-content: space-between;
padding: 0.5em 1em;
transform: rotate(-10deg);
z-index: 1;
}
.card__popped-title {
color: var(--color-ink-reversed);
font-size: 1.5em;
font-weight: 900;
text-shadow:
-2px -2px 0 var(--bubble-color),
2px -2px 0 var(--bubble-color),
-2px 2px 0 var(--bubble-color),
2px 2px 0 var(--bubble-color);
}
+20 -3
View File
@@ -1,6 +1,6 @@
:root {
/* Named color values */
--lch-black: 0% 0 0;
--lch-black: 27.13% 0.1035 284.77;
--lch-white: 100% 0 0;
--lch-gray-light: 96% 0.005 96;
--lch-gray: 92% 0.005 96;
@@ -12,9 +12,10 @@
--lch-red: 51% 0.2 31;
--lch-green: 65.59% 0.234 142.49;
--lch-green-light: 95% 0.03 142.49;
--lch-yellow: 96.01% 0.0348 81.29;
--lch-yellow: 92.62% 0.1 91.5;
--lch-always-black: 0% 0 0;
--lch-always-white: 100% 0 0;
--lch-light-blue: 84.94% 0.048 216.62;
/* Abstractions */
--color-negative: oklch(var(--lch-red));
@@ -33,6 +34,7 @@
--color-marker: oklch(var(--lch-orange));
--color-always-black: oklch(var(--lch-always-black));
--color-always-white: oklch(var(--lch-always-white));
--color-container: oklch(var(--lch-light-blue));
/* Redefine named color values for dark mode */
@media (prefers-color-scheme: dark) {
@@ -47,7 +49,8 @@
--lch-red: 73.8% 0.184 29.18;
--lch-green: 75% 0.21 141.89;
--lch-green-light: 28.11% 0.02 142.49;
--lch-yellow: 35.29% 0.0493 84.59;
--lch-yellow: 40.9% 0.06 88.9;
--lch-light-blue: 20.62% 0.048 216.62;
}
}
@@ -88,3 +91,17 @@
--btn-size: 2em;
}
}
.color-picker__button {
font-size: 0.75em;
.cards & {
display: none;
}
+ .panel {
--panel-size: auto;
inset: 0 0 auto auto;
}
}
+22 -19
View File
@@ -1,13 +1,19 @@
.comments {
--avatar-size: 2.33em;
--comment-padding-block: 0.65lh;
--comment-padding-block: 0.25lh;
--comment-padding-inline: 3ch;
max-inline-size: 120ch;
font-size: 1.33cqi;
inline-size: min(67ch, 100dvw);
max-inline-size: 100dvw;
row-gap: var(--comment-padding-block);
@media (max-width: 70ch) {
inline-size: 100%;
}
> :first-child {
margin-block-start: var(--comment-padding-block);
/* margin-block-start: var(--comment-padding-block); */
}
}
@@ -15,14 +21,10 @@
margin-block: calc(var(--comment-padding-block) * 0.3);
margin-inline: 0 auto;
position: relative;
&:not(.comment--new) {
max-inline-size: 66ch;
}
}
.comment--new {
background-color: var(--color-subtle-light);
border-block: 1px solid var(--color-subtle-dark);
}
.comment__avatar {
@@ -30,7 +32,7 @@
margin-inline: auto calc(var(--comment-padding-inline) * -0.75);
z-index: 0;
.comment--mine & {
.comment--mine_ & {
margin-inline: calc(var(--comment-padding-inline) * -0.75) auto;
}
}
@@ -56,11 +58,10 @@
}
.comment__content {
background-color: var(--color-subtle-light);
margin-inline: auto var(--avatar-size);
margin-inline: auto 0 auto var(--avatar-size);
padding: var(--comment-padding-block) var(--comment-padding-inline);
.comment--mine & {
.comment--mine_ & {
margin-inline: var(--avatar-size) auto;
}
@@ -71,12 +72,11 @@
.comment__input {
--input-border-size: 0;
--input-padding: 0;
line-height: inherit;
min-block-size: calc(9lh + (2 * var(--comment-padding-block)));
min-inline-size: 100%;
padding-block: var(--comment-padding-block);
padding-inline: calc(var(--comment-padding-inline) + calc((1lh - 1ex) / 2));
@supports (field-sizing: content) {
field-sizing: content;
@@ -84,7 +84,7 @@
}
}
.comment--mine {
.comment--mine_ {
flex-direction: row-reverse;
margin-inline: auto 0;
@@ -93,6 +93,11 @@
}
}
.comment__author {
border-block-end: 1px solid var(--color-subtle-dark);
margin-block-end: 0.3lh;
}
.comment__submit {
inset: auto 0.5em 0.5em auto;
position: absolute;
@@ -100,13 +105,11 @@
}
.comment__timestamp {
color: var(--color-ink);
opacity: 0.5;
color: var(--color-subtle-dark);
}
.event-summary {
color: var(--bubble-color);
font-size: 0.875em;
font-weight: normal;
gap: 0.5ch;
opacity: 0.7;
+43 -40
View File
@@ -1,28 +1,46 @@
.events {
--grid-lines: 0.3em;
border-block-start: 1px solid var(--color-subtle);
display: grid;
gap: 0 var(--grid-lines);
grid-template-columns: repeat(4, 1fr);
padding-block-start: var(--block-space);
grid-template-columns: repeat(3, 1fr);
position: relative;
z-index: 0;
}
&:has(.event--related) {
.event:not(.event--related) {
box-shadow: none;
opacity: 0.75;
}
}
.events__container {
display: grid;
grid-template-columns: 5fr 2fr;
--column-gap: clamp(var(--inline-space), 3cqmin, calc(var(--inline-space) * 3));
}
.events__day-header {
background-color: var(--color-bg);
block-size: var(--grid-lines);
display: block;
position: relative;
z-index: 1;
}
.events__day-header-content {
background-color: var(--color-bg);
border-radius: 2em;
display: inline-flex;
inset: calc(var(--grid-lines) - 1em) auto -1em;
padding: 0.3em 1.3em 0.3em;
position: absolute;
transform: translateX(-50%);
z-index: 2;
}
.events__index {
--row-gap: calc(4 * var(--block-space));
--grid-lines: 0.2em;
--row-gap: var(--block-space);
}
.events--none {
.events + & {
padding-block-start: calc(4 * var(--block-space));
}
background-color: var(--color-container);
border-block-start: var(--grid-lines) solid var(--color-bg);
padding-block: 3em;
margin: auto var(--grid-lines) calc(var(--grid-lines) * -1) auto;
}
.events__popup {
@@ -40,6 +58,7 @@
.event {
--column-gap: 0.7ch;
--panel-border-color: transparent;
--panel-border-radius: 0.5em;
--panel-padding: 0.6em 1.2em 0.6em 0.6em;
--panel-size: auto;
@@ -50,31 +69,13 @@
}
.event--related {
--hover-color: var(--bubble-color);
z-index: 2;
box-shadow:
0 0 0 1px oklch(var(--lch-always-black) / 0.02),
0 .2em 1.6em -0.8em oklch(var(--lch-always-black) / 0.2),
0 .4em 2.4em -1em oklch(var(--lch-always-black) / 0.3),
0 .4em .8em -1.2em oklch(var(--lch-always-black) / 0.4),
0 .8em 1.2em -1.6em oklch(var(--lch-always-black) / 0.5),
0 1.2em 1.6em -2em oklch(var(--lch-always-black) / 0.6),
0 0 0.3em 0.3em var(--bubble-color) !important;
@media (prefers-color-scheme: dark) {
box-shadow:
0 0 0 1px oklch(var(--lch-always-black) / 0.42),
0 .2em 1.6em -0.8em oklch(var(--lch-always-black) / 0.6),
0 .4em 2.4em -1em oklch(var(--lch-always-black) / 0.7),
0 .4em .8em -1.2em oklch(var(--lch-always-black) / 0.8),
0 .8em 1.2em -1.6em oklch(var(--lch-always-black) / 0.9),
0 1.2em 1.6em -2em oklch(var(--lch-always-black) / 1),
0 0 0.3em 0.3em var(--bubble-color) !important;
}
}
.event__grid-item {
background-color: var(--color-selected);
background-color: var(--color-container);
block-size: 100%;
border-radius: 0;
display: flex;
@@ -84,8 +85,10 @@
.event__grid-column-title {
--z: 3;
background-color: var(--color-bg);
padding-block: var(--grid-lines);
background-color: var(--color-container);
font-size: 0.9em;
padding: 2em 0 1em;
text-transform: uppercase;
}
.event__timestamp {
@@ -103,10 +106,10 @@
.event__wrapper {
align-content: end;
display: grid;
gap: var(--grid-lines);
gap: calc(var(--grid-lines) * 2);
justify-items: center;
margin: var(--grid-lines);
padding: calc(var(--grid-lines) / 2);
padding: var(--grid-lines);
.event {
grid-column-start: unset !important;
+1 -1
View File
@@ -13,7 +13,7 @@ house-md {
/* Toolbar */
house-md-toolbar {
background-color: inherit;
border-block-end: 1px solid var(--color-subtle);
border-block-end: 1px solid var(--color-subtle-dark);
border-radius: 0;
color: currentColor;
display: inline-flex;
+3 -3
View File
@@ -95,18 +95,18 @@
}
}
.input--textara {
.input--textarea {
--input-padding: 0;
line-height: inherit;
min-block-size: calc(4lh + (2 * var(--input-padding)));
min-block-size: calc(3lh + (2 * var(--input-padding)));
min-inline-size: 100%;
padding-block: var(--input-padding);
padding-inline: calc(var(--input-padding) + calc((1lh - 1ex) / 2));
@supports (field-sizing: content) {
field-sizing: content;
max-block-size: calc(5lh + (2 * var(--input-padding)));
max-block-size: calc(3lh + (2 * var(--input-padding)));
min-block-size: calc(1lh + (2 * var(--input-padding)));
}
}
-6
View File
@@ -23,12 +23,6 @@ body {
text-justify: distribute;
widows: 2;
}
&:has(.bubble__perma) {
display: grid;
grid-template-columns: 1fr minmax(min-content, auto) 1fr;
gap: var(--inline-space);
}
}
:where(#footer) {
+158
View File
@@ -0,0 +1,158 @@
.pin-tray {
--size: 38ch;
--height: calc(5ch + 1lh + var(--block-space));
block-size: var(--height);
inline-size: var(--size);
inset: auto auto var(--block-space-half) var(--inline-space);
padding: var(--block-space-half) var(--inline-space);
position: fixed;
z-index: 1;
.pin {
--offset: calc((var(--position) - 1) * (var(--block-space) * -1));
--position: 1;
--scale: calc(1 - var(--position) / 30);
--z: calc(6 - var(--position));
inset: auto auto 0;
inline-size: var(--size);
outline: 0;
pointer-events: none;
position: absolute;
scale: var(--scale);
transform: translateY(var(--offset));
transform-origin: center;
transition: scale 0.2s ease-out, transform 0.2s ease-out;
z-index: var(--z);
&:nth-child(1) { --position: 1; }
&:nth-child(2) { --position: 2; }
&:nth-child(3) { --position: 3; }
&:nth-child(4) { --position: 4; }
&:nth-child(5) { --position: 5; }
&:nth-child(6) { --position: 6; }
&:nth-child(1n + 7) { display: none; }
.pin-tray[open] & {
--scale: 1;
--offset: calc((100% + var(--block-space-half)) * (var(--position) * -1));
margin-block-end: calc(var(--block-space) * -1);
pointer-events: unset;
}
.pin-tray:has(.pin:only-child) & {
pointer-events: unset;
}
}
}
.pin-tray__actions {
opacity: 0;
scale: calc(1 - 1 / 30);
transform: translateY(100%);
transition: opacity 0.2s ease-out, scale 0.2s ease-out, transform 0.2s ease-out;
.pin-tray[open] & {
opacity: 1;
scale: 1;
transform: translateY(0);
}
.pin-tray:has(.pin) & {
.btn img {
display: none;
}
}
.pin-tray:not(:has(.pin)) & {
opacity: 1;
scale: 1;
transform: none;
transition: none;
.pin-tray__all_action {
--btn-border-radius: 50%;
--btn-padding: 0;
aspect-ratio: 1;
block-size: var(--btn-size);
display: grid;
inline-size: var(--btn-size);
place-items: center;
> * {
grid-area: 1/1;
}
span {
display: none;
}
}
.pin-tray__read_action {
display: none;
}
}
}
.pin__content {
color: var(--color-ink);
border: 1px solid;
.notificiations-list--read & {
border: 1px solid var(--color-subtle);
box-shadow: none;
}
}
.pin__title {
padding-inline-end: calc(var(--inline-space-half) + 1.2em);
}
.pin-tray__expander {
--hover-size: 0;
--outline-size: 0;
cursor: pointer;
inset: 0;
position: absolute;
z-index: 6;
.pin-tray[open] &,
.pin-tray:not(:has(.pin)) &,
.pin-tray:has(.pin:only-child) & {
display: none;
}
}
.pin-tray__overflow {
--offset: calc((var(--position) - 1) * (var(--block-space) * -1));
--position: 7;
--scale: calc(1 - var(--position) / 30);
--z: calc(6 - var(--position));
block-size: var(--height);
display: none;
inset: auto auto 0;
inline-size: var(--size);
pointer-events: none;
position: absolute;
scale: var(--scale);
transform: translateY(var(--offset));
transition: scale 0.2s ease-out, transform 0.2s ease-out;
z-index: var(--z);
.pin-tray:has(.pin:nth-child(7)) & {
display: flex;
}
.pin-tray[open] & {
--offset: calc((100% + var(--block-space-half)) * (var(--position) * -1));
--scale: 1;
pointer-events: unset;
}
}
+4 -2
View File
@@ -2,9 +2,11 @@
--panel-border-radius: 0.5em;
--panel-padding: 0.4em 0.5em 0.5em;
--panel-size: auto;
inline-size: auto;
inset: 0 auto auto 0;
max-inline-size: 30ch;
min-inline-size: 20ch;
max-inline-size: 40ch;
position: absolute;
z-index: 2;
+3
View File
@@ -0,0 +1,3 @@
.search {
min-inline-size: 38ch;
}
+9
View File
@@ -19,3 +19,12 @@
inline-size: 18ch;
}
}
.tag-picker__button {
font-size: 0.7em;
margin-inline-start: 0.5em;
+ .panel {
--panel-size: 18ch;
}
}
+3
View File
@@ -51,6 +51,7 @@
.align-start { align-items: start; }
.align-end { align-items: end; }
.align-self-end { align-self: end; }
.align-self-start { align-self: start; }
.contain { contain: inline-size; }
@@ -166,6 +167,7 @@
.fill-white { background-color: var(--color-ink-reversed); }
.fill-shade { background-color: var(--color-subtle-light); }
.fill-selected { background-color: var(--color-selected); }
.fill-highlight { background-color: var(--color-highlight); }
.fill-transparent { background-color: transparent; }
.translucent { opacity: var(--opacity, 0.5); }
@@ -219,6 +221,7 @@
/* Separators */
.separator {
block-size: 100%;
border-block: 0;
border-inline-end: 0;
border-inline-start: var(--border-size, 1px) var(--border-style, solid) var(--border-color, currentColor);
+28 -1
View File
@@ -1,12 +1,22 @@
.workflow-stage {
--btn-border-radius: 0.5em;
--btn-background: transparent;
--btn-padding: 0.2em 0.5em;
--btn-border-size: 0;
--btn-border-radius: 0.2em;
--hover-size: 0;
color: inherit;
text-transform: uppercase;
@media (hover: hover) {
&:hover {
background-color: color(from var(--bubble-color) srgb r g b / 0.15);
}
}
.bubbles-list & {
display: none;
}
}
.workflow-stage--current {
@@ -19,6 +29,23 @@
background-color: color(from var(--bubble-color) srgb r g b / 0.65);
}
}
.ticket & {
--btn-background: var(--color-ink);
background: var(--bubble-color);
border-radius: 0.2em;
color: var(--color-ink-reversed);
}
.bubbles-list & {
display: flex;
background: transparent;
border-radius: 0.2em;
color: var(--color-ink);
padding: 0;
border: 0;
}
}
.workflow__popup {
@@ -0,0 +1,5 @@
class Bubbles::Pins::TraysController < ApplicationController
def show
@pins = Current.user.pins.includes(:bubble).ordered.limit(20)
end
end
@@ -0,0 +1,16 @@
class Bubbles::PinsController < ApplicationController
include BubbleScoped, BucketScoped
def show
end
def create
@bubble.set_pinned(Current.user, true)
redirect_to bucket_bubble_pin_path(@bucket, @bubble)
end
def destroy
@bubble.set_pinned(Current.user, false)
redirect_to bucket_bubble_pin_path(@bucket, @bubble)
end
end
+18
View File
@@ -5,9 +5,14 @@ class BubblesController < ApplicationController
before_action :set_filter, only: :index
before_action :set_bubble, only: %i[ show edit update destroy ]
before_action :handle_display_count, only: :index
DISPLAY_COUNT_OPTIONS = [ 6, 12, 18, 24 ].freeze
DEFAULT_DISPLAY_COUNT = 6
def index
@bubbles = @filter.bubbles.published_or_drafted_by(Current.user)
@display_count = display_count
end
def create
@@ -46,4 +51,17 @@ class BubblesController < ApplicationController
def deleted_notice
"Bubble deleted" unless @bubble.creating?
end
def handle_display_count
if params[:set_display_count].present?
cookies[:display_count] = params[:set_display_count]
redirect_to bubbles_path(
params.permit(*Filter::PERMITTED_PARAMS, :bucket_ids).except(:set_display_count)
)
end
end
def display_count
(cookies[:display_count] || DEFAULT_DISPLAY_COUNT).to_i
end
end
+1
View File
@@ -6,6 +6,7 @@ class EventsController < ApplicationController
update_bucket_filter
@buckets = Current.user.buckets.alphabetically
@events = events_by_hour_and_column
@filters = Current.user.filters.all
@next_day = latest_event_before_today&.created_at
end
+1 -1
View File
@@ -12,7 +12,7 @@ class WorkflowsController < ApplicationController
def create
@workflow = Current.account.workflows.create! workflow_params
# FIXME: this should definitely change.
%w[ Triage WIP On-hold ].each { |name| @workflow.stages.create! name: name }
[ "Maybe?", "Not now", "Done" ].each { |name| @workflow.stages.create! name: name }
redirect_to workflows_path
end
+11
View File
@@ -10,4 +10,15 @@ module BubblesHelper
"--bubble-rotate: #{value}deg;"
end
def display_count_options
BubblesController::DISPLAY_COUNT_OPTIONS.map do |count|
{
value: count,
label: count,
selected: @display_count == count,
id: "display-count-#{count}"
}
end
end
end
+1 -1
View File
@@ -6,7 +6,7 @@ module CommentsHelper
def new_comment_placeholder(bubble)
if bubble.creator == Current.user && bubble.messages.comments.empty?
"Add some notes…"
"Next, add some notes, context, pictures, or video about this"
else
"Type your comment…"
end
+15 -18
View File
@@ -13,10 +13,8 @@ module EventsHelper
def event_column(event)
case event.action
when "popped"
4
when "published"
3
when "commented"
when "published"
2
else
1
@@ -53,15 +51,14 @@ module EventsHelper
.where(bubbles: { bucket_id: params[:bucket_ids].presence || Current.user.bucket_ids })
headers = {
"Touched" => nil,
"Discussed" => nil,
"Updated" => nil,
"Added" => accessible_events.where(action: "published").count,
"Popped" => accessible_events.where(action: "popped").joins(:creator).merge(User.without_system).count
}
headers.map do |header, count|
title = count&.positive? ? "#{header} (#{count})" : header
content_tag(:h3, title, class: "event__grid-column-title margin-block-end-half position-sticky")
content_tag(:h3, title, class: "event__grid-column-title position-sticky")
end.join.html_safe
end
@@ -69,30 +66,30 @@ module EventsHelper
case event.action
when "assigned"
if event.assignees.include?(Current.user)
"#{ event.creator.name } will handle <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } will handle <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
else
"#{ event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
end
when "unassigned"
"#{ event.creator.name } unassigned #{ event.assignees.pluck(:name).to_sentence } from <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } unassigned #{ event.assignees.pluck(:name).to_sentence } from <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "boosted"
"#{ event.creator.name } boosted <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } boosted <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "commented"
"#{ event.creator.name } commented on <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } commented on <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "published"
"#{ event.creator.name } added <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } added <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "popped"
"#{ event.creator.name } popped <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{ event.creator.name } popped <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "staged"
"#{event.creator.name} changed the stage to #{event.stage_name} on <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{event.creator.name} changed the stage to #{event.stage_name} on <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "due_date_added"
"#{event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "due_date_changed"
"#{event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span class='txt-link'>#{ event.bubble.title }</span>".html_safe
"#{event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>".html_safe
when "due_date_removed"
"#{event.creator.name} removed the date on <span class='txt-link'>#{ event.bubble.title }</span>"
"#{event.creator.name} removed the date on <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span>"
when "title_changed"
"#{event.creator.name} renamed on <span class='txt-link'>#{ event.bubble.title }</span> (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe
"#{event.creator.name} renamed on <span style='color: var(--bubble-color)'>#{ event.bubble.title }</span> (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe
end
end
end
+1 -1
View File
@@ -5,7 +5,7 @@ module WorkflowsHelper
def button_to_set_stage(bubble, stage)
button_to bucket_bubble_stagings_path(bubble.bucket, bubble, stage_id: stage),
method: :post, class: [ "btn full-width justify-start borderless workflow-stage", { "workflow-stage--current": stage == bubble.stage } ],
method: :post, class: [ "btn full-width justify-start workflow-stage txt-uppercase", { "workflow-stage--current": stage == bubble.stage } ],
form_class: "flex align-center gap-half",
data: { turbo_frame: "_top" } do
tag.span class: "overflow-ellipsis" do
+9 -1
View File
@@ -1,6 +1,6 @@
class Bubble < ApplicationRecord
include Assignable, Boostable, Colored, Commentable, Eventable,
Messages, Notifiable, Poppable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable
Messages, Notifiable, Pinnable, Poppable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable
belongs_to :bucket, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
@@ -11,6 +11,7 @@ class Bubble < ApplicationRecord
after_save :track_due_date_change, if: :saved_change_to_due_on?
after_save :track_title_change, if: :saved_change_to_title?
after_create :assign_initial_stage
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
scope :chronologically, -> { order created_at: :asc, id: :asc }
@@ -48,4 +49,11 @@ class Bubble < ApplicationRecord
})
end
end
def assign_initial_stage
if workflow_stage = bucket.account.workflows.first&.stages&.first
update! stage: workflow_stage
track_event :staged, stage_id: workflow_stage.id, stage_name: workflow_stage.name
end
end
end
+2 -2
View File
@@ -1,9 +1,9 @@
module Bubble::Colored
extend ActiveSupport::Concern
COLORS = %w[ #BF1B1B #ED3F1C #ED8008 #7C956B #266ec3 #3B3633 ]
COLORS = %w[ #b7462b #ff63a8 #eb7a32 #6ac967 #2c6da8 #663251 ]
included do
attribute :color, default: "#266ec3"
attribute :color, default: "#2c6da8"
end
end
+19
View File
@@ -0,0 +1,19 @@
module Bubble::Pinnable
extend ActiveSupport::Concern
included do
has_many :pins, dependent: :destroy
end
def pinned_by?(user)
pins.exists?(user: user)
end
def set_pinned(user, pinned)
if pinned
pins.find_or_create_by!(user: user)
else
pins.find_by(user: user)&.destroy
end
end
end
+10
View File
@@ -9,5 +9,15 @@ class Bucket < ApplicationRecord
validates_presence_of :name
after_create :ensure_workflow_exists
scope :alphabetically, -> { order(name: :asc) }
private
def ensure_workflow_exists
unless account.workflows.exists?
workflow = account.workflows.create!(name: "Default Workflow")
[ "Maybe?", "Not now", "Done" ].each { |name| workflow.stages.create!(name: name) }
end
end
end
+22
View File
@@ -0,0 +1,22 @@
module Filter::Params
extend ActiveSupport::Concern
PERMITTED_PARAMS = %i[
indexed_by assignment_status bucket_ids creator_ids
assignee_ids stage_ids tag_ids terms display_count
].freeze
def as_params
self.class.normalize_params(
indexed_by: indexed_by,
assignment_status: assignment_status,
bucket_ids: buckets.ids,
creator_ids: creators.ids,
assignee_ids: assignees.ids,
stage_ids: stages.ids,
tag_ids: tags.ids,
terms: terms,
display_count: display_count
)
end
end
+12
View File
@@ -0,0 +1,12 @@
class Pin < ApplicationRecord
belongs_to :bubble
belongs_to :user
scope :ordered, -> { order(created_at: :desc) }
after_create_commit -> { broadcast_prepend_later_to [ user, :pins ],
target: "pins",
partial: "bubbles/pins/pin"
}
after_destroy_commit -> { broadcast_remove_to [ user, :pins ] }
end
+3
View File
@@ -23,6 +23,9 @@ class User < ApplicationRecord
has_one_attached :avatar
has_many :pins, dependent: :destroy
has_many :pinned_bubbles, through: :pins, source: :bubble
normalizes :email_address, with: ->(value) { value.strip.downcase }
after_create_commit :grant_access_to_buckets
+2 -6
View File
@@ -2,20 +2,16 @@
<% content_for :header do %>
<nav class="align-start">
<%= link_to root_path, class: "btn borderless flex-item-justify-start" do %>
<%= link_to root_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Go back</span>
<% end %>
<header class="flex flex-column center">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
</div>
<h1 class="txt-x-large"><%= @page_title %></h1>
</header>
<%= link_to workflows_path, class: "btn flex-item-justify-end borderless" do %>
<%= link_to workflows_path, class: "btn flex-item-justify-end" do %>
<%= image_tag "bolt.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Workflows</span>
<% end %>
+8 -13
View File
@@ -1,15 +1,10 @@
<%= tag.div class: "bubble__detail bubble__boosts",
role: "group", aria: { label: "Boosts" },
data: {
controller: "animation toggle-class",
animation_play_class: "boosting",
toggle_class_toggle_class: "boosting",
action: "animationend->toggle-class#toggle" },
hidden: !bubble.boosts_count.positive? do %>
<%= form_with url: bucket_bubble_boosts_path(bubble.bucket, bubble),
class: "boost__form",
data: { action: "toggle-class#toggle" } do %>
<%= text_field_tag :boost_count, bubble.boosts_count, min: 1, class: "input borderless boost__input", autocomplete: "off", aria: { label: "Boost count" } %>
<%= tag.button "+", class: "btn btn--plain boost__btn", type: :submit, aria: { label: "Boost" } %>
<%= form_with url: bucket_bubble_boosts_path(bubble.bucket, bubble), class: "pad-inline flex align-center gap" do %>
<span class="overflow-ellipsis">
<strong><%= text_field_tag :boost_count, bubble.boosts_count, min: 1, class: "boost__input input", autocomplete: "off" %></strong>
<span><%= bubble.boosts_count == 1 ? "boost" : "boosts" %></span>
</span>
<%= tag.button class: "btn", type: :submit, style: "font-size: 0.5em" do %>
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Boost</span>
<% end %>
<% end %>
-9
View File
@@ -1,9 +0,0 @@
<% bubble.assignees.each do |assignee| %>
<article class="bubble__detail bubble__assignee">
<div class="btn btn--plain position-relative fill-transparent">
<span class="assignee__name" aria-hidden="true"><span><%= assignee.name %></span></span>
<%= avatar_image_tag assignee, loading: :lazy, class: "avatar flex-item-no-shrink", aria: { hidden: true } %>
<span class="for-screen-reader">Assigned to <%= assignee.name %></span>
</div>
</article>
<% end %>
+1 -1
View File
@@ -20,7 +20,7 @@
<% end %>
<% else %>
<%= form_with model: bubble, url: bucket_bubble_path(bubble.bucket, bubble), id: "bubble_form", data: { controller: "auto-save" } do |form| %>
<%= form.text_area :title, placeholder: "Name it…", class: "input input--textara txt-align-center full-width borderless bubble__title-rendered", autofocus: bubble.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
<%= form.text_area :title, placeholder: "Name it…", class: "input full-width borderless txt-align-start card__title-field", autofocus: bubble.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
<% end %>
<% end %>
+52
View File
@@ -0,0 +1,52 @@
<% cache bubble do %>
<article class="card flex flex-column position-relative txt-align-start full-width"
style="--bubble-color: <%= bubble.color %>; view-transition-name: <%= dom_id(bubble, :ticket) %>;" id="<%= dom_id(bubble, :ticket) %>">
<header class="card__header flex align-center gap min-width">
<%= link_to bubble.bucket.name, bubbles_path(bucket_ids: [ bubble.bucket ]),
class: "card__bucket txt-uppercase overflow-ellipsis txt-reversed" %>
<%= render "bubbles/tags", bubble: bubble %>
</header>
<div class="card__body flex gap full-width flex-item-grow">
<div class="card__body-header">
<h1 class="card__title">
<% if bubble.published? %>
<%= turbo_frame_tag bubble, :edit do %>
<%= link_to bubble_title(bubble), edit_bucket_bubble_path(bubble.bucket, bubble), class: "card__title-link overflow-line-clamp" %>
<% end %>
<% else %>
<%= form_with model: bubble, url: bucket_bubble_path(bubble.bucket, bubble), id: "bubble_form", data: { controller: "auto-save" } do |form| %>
<%= form.text_area :title, placeholder: "Name it…",
class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if bubble.creating? }",
autofocus: bubble.title.blank?,
data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
<% end %>
<% end %>
</h1>
<%= link_to bucket_bubble_path(bubble.bucket, bubble), class: "card__link" do %>
<span class="for-screen-reader"><%= bubble.title %></span>
<% end %>
</div>
<%= turbo_frame_tag dom_id(bubble, :stage_picker), src: new_bucket_bubble_stage_picker_path(bubble.bucket, bubble) %>
</div>
<footer class="card__footer full-width flex align-center gap">
<%= render "bubbles/people", bubble: bubble %>
<%= render "bubbles/meta", bubble: bubble %>
</footer>
<div class="card__background">
<%= image_tag bubble.image.presence || "", size: 120, data: { upload_preview_target: "image" } %>
</div>
<% if bubble.popped? %>
<div class="card__popped flex flex-column">
<span class="card__popped-title txt-uppercase">Completed</span>
<strong><%= bubble.popped_at.strftime("%b %d, %Y") %></strong>
<span>by <span style="border-block-end: 1px dashed var(--color-ink);"><%= bubble.popped_by.name %></span></span>
</div>
<% end %>
</article>
<% end %>
+2 -2
View File
@@ -1,7 +1,7 @@
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close">
<button class="btn full-width justify-start borderless" data-action="dialog#open:stop">
<button class="btn" data-action="dialog#open:stop">
<%= image_tag "art.svg", aria: { hidden: true }, size: 24 %>
<span>Color</span>
<span class="for-screen-reader">Change color</span>
</button>
<dialog class="color-picker panel fill-white shadow gap-half" data-dialog-target="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
@@ -0,0 +1,37 @@
<div class="card-count__selector" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn borderless btn--circle"
data-action="click->dialog#open:stop"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="display-count-dialog">
<span><%= display_count %></span>
</button>
<dialog id="display-count-dialog"
class="popup panel flex-column align-start justify-start fill-white shadow"
data-dialog-target="dialog"
aria-label="Select number of tickets to display">
<% display_count_options.each do |option| %>
<div class="btn fill-transparent popup__item full-width">
<%= form_tag request.path, method: :get, class: "full-width" do %>
<% filter.as_params.each do |key, value| %>
<% if value.is_a?(Array) %>
<% value.each do |v| %>
<%= filter_hidden_field_tag key, v %>
<% end %>
<% else %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% end %>
<%= hidden_field_tag :set_display_count, option[:value] %>
<input type="checkbox"
id="<%= option[:id] %>"
<%= "checked" if option[:selected] %>
onChange="this.form.submit()"
aria-label="<%= option[:label] %>">
<label for="<%= option[:id] %>" class="overflow-ellipsis"><%= option[:label] %></label>
<% end %>
</div>
<% end %>
</dialog>
</div>
-9
View File
@@ -1,14 +1,5 @@
<div class="flex flex-column gap-half align-center" data-controller="dialog filter-form"
data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside turbo:before-cache@document->dialog#close">
<h1 class="txt-x-large margin-none-block-end txt-tight-lines">
<% if filter.buckets.none? %>
All collections
<% elsif filter.buckets.one? %>
<%= filter.buckets.first.name %>
<% else %>
<%= filter.buckets.map(&:name).to_sentence %>
<% end %>
</h1>
<%= render "filters/settings", filter: filter %>
<%= render "filters/dialog", filter: filter %>
+17 -3
View File
@@ -1,3 +1,17 @@
<div class="bubble__image">
<%= image_tag bubble.image.presence || "", data: { upload_preview_target: "image" }, aria: { hidden: true } %>
</div>
<% if bubble.image.attached? %>
<%= button_to bucket_bubble_image_path(bubble.bucket, bubble), method: :delete, class: "btn" do %>
<%= image_tag "picture-remove.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Clear background image</span>
<% end %>
<% else %>
<%= form_with model: bubble, url: bucket_bubble_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %>
<label class="btn">
<%= image_tag "picture-add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Add background image</span>
<%= form.file_field :image, class: "input",
accept: "image/png, image/jpeg, image/jpg, image/webp",
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" },
hidden: true %>
</label>
<% end %>
<% end %>
+7
View File
@@ -3,4 +3,11 @@
<%# Template Dependency: event_summaries/event_summary %>
<%= render bubble.messages, cached: true %>
<%= render "comments/new", bubble: bubble %>
<div class="flex flex-column gap-half justify-start full-width txt-align-start">
<span>Watching this…</span>
<div class="flex align-center flex-wrap gap-half max-width">
<%= render partial: "bubbles/watches/watcher", collection: bubble.watchers_and_subscribers.sorted_with_user_first(Current.user) %>
</div>
</div>
<% end %>
+45
View File
@@ -0,0 +1,45 @@
<div class="card__meta flex txt-tight-lines txt-uppercase">
<div class="flex flex-column gap txt-align-end">
<span class="pad-inline overflow-ellipsis"><%= bubble.creating? ? "Added" : "Updated" %>
<%= days = (Date.current - bubble.updated_at.to_date).to_i
case days
when 0 then "<strong>Today</strong>".html_safe
when 1 then "<strong>Yesterday</strong>".html_safe
else "<strong>#{days}</strong> days ago".html_safe
end %>
</span>
<hr class="full-width separator--horizontal unpad margin-none card__hide-on-index">
<% if bubble.creating? %>
<span class="pad-inline overflow-ellipsis card__hide-on-index">Expires
<%= days = (bubble.auto_pop_at.to_date - Date.current).to_i
case days
when 0 then "<strong>Today</strong>".html_safe
else "in <strong>#{days}</strong> days".html_safe
end %>
</span>
<% else %>
<span class="pad-inline overflow-ellipsis card__hide-on-index">Added
<%= days = (Date.current - bubble.created_at.to_date).to_i
case days
when 0 then "<strong>Today</strong>".html_safe
when 1 then "<strong>Yesterday</strong>".html_safe
else "<strong>#{days}</strong> days ago".html_safe
end %>
</span>
<% end %>
</div>
<hr class="separator margin-none unpad card__hide-on-index">
<div class="flex flex-column gap txt-align-start card__hide-on-index">
<span class="pad-inline overflow-ellipsis">
<strong><%= bubble.comments_count %></strong> <%= bubble.comments_count == 1 ? "comment" : "comments" %>
</span>
<hr class="full-width separator--horizontal unpad margin-none">
<%= render "bubbles/boosts", bubble: bubble %>
</div>
</div>
+17
View File
@@ -0,0 +1,17 @@
<div class="card__people flex gap">
<div class="flex flex-column align-center">
<span class="txt-uppercase">Reported</span>
<%= avatar_tag bubble.creator, loading: :lazy, class: "avatar" %>
</div>
<div class="card__assignees flex flex-column align-center">
<span class="txt-uppercase">Assigned</span>
<div class="flex">
<% bubble.assignees.each do |assignee| %>
<%= avatar_tag assignee, loading: :lazy, class: "avatar" %>
<% end %>
<%= render "bubbles/sidebar/assignment", bubble: bubble, users: bubble.bucket.users.active %>
</div>
</div>
</div>
+6 -7
View File
@@ -1,16 +1,15 @@
<div class="bubble__actions-container center border-radius pad flex-inline align-center justify-center gap pad-inline-double" style="--bubble-color: <%= @bubble.color %>">
<div class="card__actions-container center border-radius flex-inline align-center justify-center gap" style="--bubble-color: <%= @bubble.color %>">
<% if bubble.popped? %>
<span class="bubble__pop-message">Popped by <%= bubble.popped_by.name %> on <%= local_datetime_tag(bubble.popped_at, style: :shortdate) %>.</span>
<span class="card__pop-message">Completed by <%= bubble.popped_by.name %> on <%= local_datetime_tag(bubble.popped_at, style: :shortdate) %>.</span>
<%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), method: :delete, class: "btn btn--plain borderless fill-transparent" do %>
<span>Un-pop</span>
<span>Un-do</span>
<% end %>
<% else %>
<%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), class: "btn borderless" do %>
<%= image_tag "pop.svg", aria: { hidden: true }, size: 24, class: "colorize--white" %>
<span>Pop</span>
<span>Mark as done</span>
<% end %>
<span class="bubble__pop-message">
Auto-pops <%= local_datetime_tag(bubble.auto_pop_at, style: :indays) %>.
<span class="card__pop-message">
Auto-closes <%= local_datetime_tag(bubble.auto_pop_at, style: :indays) %>.
</span>
<% end %>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
<%= button_to bucket_bubble_publish_path(bubble.bucket, bubble), class: "btn txt-small btn--link" do %>
<%= button_to bucket_bubble_publish_path(bubble.bucket, bubble), class: "btn txt-small btn--link", style: "--btn-background: #{ bubble.color }" do %>
<span>Post to project</span>
<% end %>
+12 -5
View File
@@ -1,6 +1,13 @@
<% bubble.tags.each do |tag| %>
<%= link_to bubbles_path(bucket_ids: [ bubble.bucket ], tag_ids: [ tag.id ]),
class: "bubble__tag btn btn--plain fill-transparent min-width", style: "color: #{bubble.color}", data: { turbo_frame: "_top" } do %>
<span class="overflow-ellipsis"><%= tag.hashtag %></span>
<div class="card__tags flex align-center flex-item-grow">
<% if bubble.tags.any? || bubble.creating? %>
<span># </span>
<% bubble.tags.each_with_index do |tag, index| %>
<%= link_to bubbles_path(bucket_ids: [ bubble.bucket ], tag_ids: [ tag.id ]),
class: "card__tag btn btn--plain min-width txt-uppercase fill-transparent", data: { turbo_frame: "_top" } do %>
<span class="overflow-ellipsis"><%= tag.title %></span>
<% end %><%= ", " unless index == bubble.tags.size - 1 %>
<% end %>
<% end %>
<% end %>
<%= render "bubbles/sidebar/tag", bubble: bubble, tags: Current.account.tags %>
</div>
+5 -7
View File
@@ -1,15 +1,13 @@
<%= turbo_frame_tag @bubble, :edit do %>
<%= form_with model: @bubble, url: bucket_bubble_path(@bubble.bucket, @bubble), class: "flex flex-column justify-center gap full-width", data: { controller: "form" } do |form| %>
<%= form.label :title, class: "flex flex-column justify-center align-center" do %>
<%= form.text_area :title, class: "input input--textara txt-align-center full-width borderless bubble__title-rendered",
<%= form_with model: @bubble, url: bucket_bubble_path(@bubble.bucket, @bubble), class: "flex flex-column gap full-width", data: { controller: "form" } do |form| %>
<%= form.label :title, class: "flex flex-column align-center" do %>
<%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card__title-field",
required: true, autofocus: true, placeholder: "Name it…",
rows: 3,
data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" },
style: "color: var(--spat-color); --input-border-radius: 0" %>
<% end %>
<%= form.button "Save", class: "btn btn--reversed txt-small center", type: :submit, hidden: true do %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Save</span>
<% end %>
<%= form.button "Save", type: :submit, hidden: true %>
<%= link_to "Close editor and discard changes", bucket_bubble_path(@bubble.bucket, @bubble), data: { form_target: "cancel" }, hidden: true %>
<% end %>
<% end %>
+22 -16
View File
@@ -5,35 +5,41 @@
<% content_for :header do %>
<nav class="align-start">
<% if bucket = @filter.buckets.first %>
<%= link_to edit_bucket_path(bucket), class: "btn borderless flex-item-justify-start" do %>
<%= image_tag "settings.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Settings for <%= bucket.name %></span>
<% end %>
<%= link_to root_path, class: "btn", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= image_tag "home.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Home</span>
<% end %>
<header class="flex flex-column center">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
</div>
<div class="btn btn--placeholder flex-item-justify-start"></div>
<header class="search flex flex-inline flex-column center flex-shrink txt--large">
<input type="search" placeholder="Type names, tags, phrases to find something…" class="input input--search">
<%= render "bubbles/filters", filter: @filter %>
</header>
<% if bucket = @filter.buckets.first %>
<%= button_to bucket_bubbles_path(bucket), method: :post, class: "btn borderless", form_class: "flex-item-justify-end" do %>
<%= link_to edit_bucket_path(bucket), class: "btn flex-item-justify-end" do %>
<%= image_tag "settings.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Settings for <%= bucket.name %></span>
<% end %>
<%= button_to bucket_bubbles_path(bucket), method: :post, class: "btn" do %>
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Create a new bubble</span>
<% end %>
<% else %>
<div class="btn btn--placeholder flex-item-justify-end"></div>
<div class="btn btn--placeholder"></div>
<% end %>
</nav>
<% end %>
<section class="bubbles flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= @filter.id %>" data-controller="bubble-size" data-action="turbo:morph@window->bubble-size#resize">
<section class="cards gap position-relative" style="view-transition-name: cards-container; data-controller="bubble-size" data-action="turbo:morph@window->bubble-size#resize">
<% if @bubbles.any? %>
<%= render partial: "bubbles/bubble", collection: @bubbles.limit(10), cached: true %>
<%= render "display_count_selector", display_count: @display_count, filter: @filter %>
<%= render partial: "bubbles/card", collection: @bubbles.limit(@display_count), as: :bubble, cached: true %>
<% else %>
<p><strong>Nothing here.</strong></p>
<p class="txt-medium"><strong>Nothing here.</strong></p>
<% end %>
</section>
@@ -45,9 +51,9 @@
data-divider-installed-class="bubbles-list__divider--installed"
data-divider-dragging-class="dragging"
data-action="turbo:morph@document->divider#install dragstart->divider#configureDrag dragenter->divider#acceptDrop dragover->divider#acceptDrop dragover->divider#moveDivider drop->divider#drop">
<%= render partial: "bubbles/list/bubble", collection: @bubbles, cached: true %>
<%= render "bubbles/list/divider", filter: @filter %>
<%= render partial: "bubbles/list/bubble", collection: @bubbles.offset(@display_count), as: :bubble, cached: true %>
</ul>
</section>
<%= render "notifications/tray" %>
<%= render "bubbles/pins/tray" %>
+34 -10
View File
@@ -34,18 +34,42 @@
</div>
<hr class="separator--horizontal flex-item-grow" aria-hidden="true" style="--border-color: var(--color-subtle-dark); --border-style: dotted" />
<% if bubble.due_on.present? %>
<strong class="txt-nowrap txt-small" style="color: <%= bubble.color %>">
<div class="for-screen-reader">Due on <%= local_datetime_tag bubble.due_on, style: :shortdate %></div>
<div aria-hidden="true"><%= local_datetime_tag bubble.due_on, style: :shortdate %></div>
</strong>
<% end %>
</div>
<div class="flex gap-half txt-small full-width min-width">
<span class="overflow-ellipsis"><%= "Added by #{bubble.creator.name} on" %> <%= local_datetime_tag bubble.created_at, style: :shortdate %></span>
<span class="flex-item-no-shrink"><%= render "bubbles/tags", bubble: bubble %></span>
<div class="flex gap-half full-width min-width align-center" style="font-size: 0.8em;">
<span class="overflow-ellipsis"><%= "Added to #{bubble.bucket.name} by #{bubble.creator.name}" %> <%=
days = (Date.current - bubble.created_at.to_date).to_i
case days
when 0 then "today"
when 1 then "yesterday"
else "#{days} days ago".html_safe
end
%></span>
<% if bubble.tags.any? %>
<div class="flex flex-item-no-shrink">
<div>
<span>#</span>
<% bubble.tags.each_with_index do |tag, index| %>
<%= link_to bubbles_path(bucket_ids: [ bubble.bucket ], tag_ids: [ tag.id ]),
class: "bubble__tag btn btn--plain fill-transparent min-width", style: "font-weight: 700;", data: { turbo_frame: "_top" } do %>
<span class="overflow-ellipsis"><%= tag.title %></span>
<% end %><%= ", " unless index == bubble.tags.size - 1 %>
<% end %>
</div>
</div>
<% end %>
<% if bubble.stage %>
<div class="flex flex-column max-width min-width flex-item-no-shrink">
<% workflow = Current.account.workflows.first %>
<div class="flex flex-column txt-uppercase border" style="font-size: 0.8em; --border-color: currentColor; padding: 0.1em 0.4em;">
<% workflow.stages.each do |stage| %>
<%= button_to_set_stage bubble, stage %>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
</li>
+17
View File
@@ -0,0 +1,17 @@
<% cache pin do %>
<%= tag.div id: dom_id(pin), class: "pin flex position-relative" do %>
<%= link_to bucket_bubble_path(pin.bubble.bucket, pin.bubble),
class: "pin__content pad pad-inline-double shadow fill-white flex align-start txt-align-start gap flex-item-grow max-width",
style: "border-color: #{ pin.bubble.color }",
data: { action: "click->dialog#close", turbo_frame: "_top" } do %>
<div class="flex flex-column txt-tight-lines min-width flex-item-grow">
<strong class="overflow-ellipsis pin__title"><%= pin.bubble.title %></strong>
<div class="overflow-ellipsis" style="color: <%= pin.bubble.color %>">
<%= pin.bubble.bucket.name %>
<%= "· #" if pin.bubble.tags.any? %>
<%= pin.bubble.tags.map(&:title).join(", ") %>
</div>
</div>
<% end %>
<% end %>
<% end %>
+24
View File
@@ -0,0 +1,24 @@
<%= turbo_stream_from Current.user, :pins %>
<%= tag.dialog id: "pin-tray", class: "pin-tray flex flex-column gap unpad borderless justify-end fill-transparent",
data: { controller: "dialog", turbo_permanent: true, dialog_modal_value: false, dialog_target: "dialog", action: "keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" } do %>
<%= turbo_frame_tag "pins", src: bubbles_pins_tray_path %>
<%= link_to bubbles_pins_path, class: "pin-tray__overflow border-radius" do %>
<div class="pin__content border-radius pad shadow fill-white flex align-center txt-align-center gap full-width">
<div class="flex flex-column txt-tight-lines min-width flex-item-grow">
<strong>+ More…</strong>
</div>
</div>
<% end %>
<div class="pin-tray__actions flex align-center gap-half">
<%= tag.button type: "button", class: "pin-tray__all_action btn flex-item-justify-start shadow", data: { action: "click->dialog#close"} do %>
<%= image_tag "pinned.svg", aria: { hidden: true }, size: 24 %>
<span>See all pins</span>
<% end %>
</div>
<button class="pin-tray__expander borderless unpad fill-transparent" data-action="dialog#toggle" aria-label="Expand pins stack">
</button>
<% end %>
+13
View File
@@ -0,0 +1,13 @@
<%= turbo_frame_tag dom_id(@bubble, :pin) do %>
<% if @bubble.pinned_by? Current.user %>
<%= button_to bucket_bubble_pin_path(@bubble.bucket, @bubble), method: :delete, class: "btn btn--reversed" do %>
<%= image_tag "pinned.svg", size: 16, aria: { hidden: true } %>
<span class="for-screen-reader">Un-pin this card</span>
<% end %>
<% else %>
<%= button_to bucket_bubble_pin_path(@bubble.bucket, @bubble), class: "btn" do %>
<%= image_tag "unpinned.svg", size: 16, aria: { hidden: true } %>
<span class="for-screen-reader">Pin this card</span>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,3 @@
<%= turbo_frame_tag "pins" do %>
<%= render collection: @pins, partial: "bubbles/pins/pin" %>
<% end %>
@@ -0,0 +1,3 @@
<%= turbo_frame_tag "pins" do %>
<%= render collection: @pins, partial: "bubbles/pins/pin" %>
<% end %>
+57 -92
View File
@@ -2,130 +2,95 @@
<% content_for :header do %>
<% if @bubble.creating? && @bubble.can_recover_abandoned_creation? %>
<div class="position-sticky flex align-center gap-half fill-selected justify-center border-block margin-block-end"
style="--border-color: var(--color-selected-dark); --border-style: dashed; view-transition-name: draft-banner;">
<div class="fill-selected position-sticky flex align-center gap-half justify-center border-block margin-block-end"
style="view-transition-name: draft-banner; --bubble-color: <%= @bubble.color %>;">
You have an unsaved work. Would you like to continue where you left off?
<%= button_to bucket_bubble_recover_path(@bubble.bucket, @bubble), class: "btn txt-small btn--link", data: { turbo_action: "replace" } do %>
<%= button_to bucket_bubble_recover_path(@bubble.bucket, @bubble), class: "btn btn--reversed", data: { turbo_action: "replace" } do %>
<span>Restore</span>
<% end %>
</div>
<% end %>
<% if @bubble.drafted? %>
<div class="position-sticky flex align-center gap-half fill-selected justify-center border-block margin-block-end"
style="--border-color: var(--color-selected-dark); --border-style: dashed; view-transition-name: draft-banner;">
<div class="fill-selected position-sticky flex align-center gap-half fill-highlight justify-center border-block margin-block-end"
style="view-transition-name: draft-banner;">
This is a draft, its only visible to you.
<%= render "bubbles/publish", bubble: @bubble %>
</div>
<% end %>
<nav class="align-start max-width">
<header class="flex flex-column center max-width">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
</div>
<h1 class="txt-large txt-align-center min-width flex align-center gap-half margin-block-end justify-center">
<%= link_to @bucket.name, bubbles_path(bucket_ids: [ @bucket ]), data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } %>
</h1>
<%= link_to root_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= image_tag "home.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Home</span>
<% end %>
<header class="search flex flex-inline flex-column center flex-shrink txt--large">
<input type="search" placeholder="Type names, tags, phrases to find something…" class="input input--search">
</header>
<div class="btn btn--placeholder flex-item-justify-end"></div>
</nav>
<% end %>
<aside class="flex align-start justify-start max-width" style="--bubble-color: <%= @bubble.color %>">
<div class="flex flex-column" style="row-gap: 3px;">
<%= render "bubbles/color", bubble: @bubble %>
<div data-controller="beacon" data-beacon-url-value="<%= bucket_bubble_readings_url(@bubble.bucket, @bubble) %>">
<div class="card__container position-relative margin-block-start <%= "card__container--pointing" unless @bubble.published? %>">
<%= render "bubbles/card", bubble: @bubble %>
<%= form_with model: @bubble, url: bucket_bubble_path(@bubble.bucket, @bubble), data: { controller: "form" } do |form| %>
<label class="btn full-width justify-start borderless">
<%= image_tag "calendar-add.svg", aria: { hidden: true }, size: 24 %>
<span>Date</span>
<%= form.date_field :due_on, class: "input input--hidden", data: { action: "change->form#submit click->form#showPicker" } %>
</label>
<% end %>
<%= render "bubbles/sidebar/assignment", bubble: @bubble, users: @bucket.users.active %>
<%= render "bubbles/sidebar/tag", bubble: @bubble, tags: Current.account.tags %>
<% if @bubble.image.attached? %>
<%= button_to bucket_bubble_image_path(@bubble.bucket, @bubble), method: :delete, class: "btn full-width justify-start borderless" do %>
<%= image_tag "picture-add.svg", aria: { hidden: true }, size: 24 %>
<span>Clear background image</span>
<div class="card__actions flex flex-column gap-half">
<%= render "bubbles/color", bubble: @bubble %>
<%= render "bubbles/image", bubble: @bubble %>
<%= button_to bucket_bubble_path(@bubble.bucket, @bubble),
method: :delete,
class: "btn",
data: { turbo_confirm: "Are you sure you want to delete this?" } do %>
<%= image_tag "trash.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Delete</span>
<% end %>
<% else %>
<%= form_with model: @bubble, url: bucket_bubble_path(@bubble.bucket, @bubble), data: { controller: "form" } do |form| %>
<label class="btn full-width justify-start borderless min-width">
<%= image_tag "picture-add.svg", aria: { hidden: true }, size: 24 %>
<span class="overflow-ellipsis" aria-hidden="true">Background</span>
<%= form.file_field :image, class: "input visually-hidden",
accept: "image/png, image/jpeg, image/jpg, image/webp",
aria: { label: "Upload background image" },
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" } %>
</label>
<% end %>
<% end %>
</div>
<%= button_to bucket_bubble_boosts_path(@bubble.bucket, @bubble), class: "btn full-width justify-start borderless", data: { turbo_frame: dom_id(@bubble, :boosts) } do %>
<%= image_tag "thumb-up.svg", aria: { hidden: true }, size: 24 %>
<span class="overflow-ellipsis">Boost</span>
<% end %>
<% unless @bubble.drafted? %>
<button class="btn full-width justify-start borderless min-width">
<%= image_tag "picture-double.svg", aria: { hidden: true }, size: 24 %>
<span class="overflow-ellipsis">Ive seen this</span>
</button>
<% end %>
<%= button_to bucket_bubble_path(@bubble.bucket, @bubble),
method: :delete,
class: "btn full-width justify-start borderless min-width",
data: { turbo_confirm: "Are you sure you want to delete this bubble?" } do %>
<%= image_tag "trash.svg", aria: { hidden: true }, size: 24 %>
<span class="overflow-ellipsis">Delete</span>
<% end %>
</div>
</aside>
<div class="bubble__container" data-controller="beacon" data-beacon-url-value="<%= bucket_bubble_readings_url(@bubble.bucket, @bubble) %>">
<div class="bubble__perma flex justify-center center">
<%= render "bubbles/bubble", bubble: @bubble %>
</div>
<% unless @bubble.drafted? || @bubble.creating? %>
<%= render "bubbles/pop_toggle", bubble: @bubble %>
<% end %>
<div style="margin-block-start: calc(var(--block-space) * -1);">
<% if @bubble.published? %>
<%= render "bubbles/messages", bubble: @bubble %>
<% else %>
<div class="comments">
<div class="comment comment--new border-radius flex align-start full-width margin-block-double">
<form data-controller="outlet-auto-save paste" data-outlet-auto-save-auto-save-outlet="#bubble_form" class="flex flex-column gap full-width" data-action="paste->paste#pasteFiles">
<%= tag.house_md @bubble.draft_comment, name: "bubble[draft_comment]", class: "input comment__input", form: "bubble_form", placeholder: new_comment_placeholder(@bubble),
data: { action: "house-md:change->outlet-auto-save#change focusout->outlet-auto-save#submit", uploads_url: uploads_url(format: "json") } %>
</form>
</div>
<div class="card__actions card__actions--right flex flex-column gap-half">
<%= turbo_frame_tag dom_id(@bubble, :watch), src: bucket_bubble_watch_path(@bubble.bucket, @bubble) %>
<%= turbo_frame_tag dom_id(@bubble, :pin), src: bucket_bubble_pin_path(@bubble.bucket, @bubble) %>
</div>
<% end %>
</div>
<% unless @bubble.drafted? || @bubble.creating? %>
<div class="card__actions-wrapper">
<%= render "bubbles/pop_toggle", bubble: @bubble %>
</div>
<% end %>
<% if @bubble.published? %>
<%= turbo_frame_tag dom_id(@bubble, :watch), src: bucket_bubble_watch_path(@bubble.bucket, @bubble) %>
<%= render "bubbles/messages", bubble: @bubble %>
<% else %>
<div class="comments align-center center borderless margin-block-start flex flex-column gap-half">
<div class="comment comment--new flex align-start full-width">
<form data-controller="outlet-auto-save paste"
class="flex flex-column gap full-width"
data-outlet-auto-save-auto-save-outlet="#bubble_form"
data-action="paste->paste#pasteFiles">
<%= tag.house_md @bubble.draft_comment, name: "bubble[draft_comment]", class: "input comment__input",
form: "bubble_form",
placeholder: new_comment_placeholder(@bubble),
data: { action: "house-md:change->outlet-auto-save#change focusout->outlet-auto-save#submit", uploads_url: uploads_url(format: "json") } %>
</form>
</div>
</div>
<% end %>
<% if @bubble.creating? %>
<div class="bubble__actions-container center border-radius pad flex align-center justify-space-between gap pad-inline-double" style="--bubble-color: <%= @bubble.color %>;">
<%= button_to "Save as draft", bucket_bubble_path(@bubble.bucket, @bubble), name: "bubble[status]", value: "drafted", method: :put, class: "btn btn--plain borderless fill-transparent" %>
<%= button_to "Create bubble", bucket_bubble_publish_path(@bubble.bucket, @bubble), class: "btn btn--reversed" %>
<div class="comments center pad-block flex align-center gap">
<%= button_to "Create card", bucket_bubble_publish_path(@bubble.bucket, @bubble), class: "btn btn--reversed" %>
<%= button_to "Save as draft", bucket_bubble_path(@bubble.bucket, @bubble), name: "bubble[status]", value: "drafted", method: :put, class: "btn" %>
</div>
<% end %>
</div>
<div class="flex align-start justify-end">
<%= turbo_frame_tag dom_id(@bubble, :stage_picker), src: new_bucket_bubble_stage_picker_path(@bubble.bucket, @bubble) %>
</div>
<%= link_to "Go back", bubbles_path(bucket_ids: [ @bubble.bucket ]), class: "btn", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" }, hidden: true %>
<%= render "notifications/tray" %>
<%= render "bubbles/pins/tray" %>
@@ -1,11 +1,11 @@
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn full-width justify-start borderless" data-action="click->dialog#open:stop">
<%= image_tag "person.svg", aria: { hidden: true }, size: 24 %>
<span>Assign</span>
<button class="btn card__hide-on-index <%= bubble.creating? || bubble.drafted? ? "fill-highlight" : "fill-transparent" %>" data-action="click->dialog#open:stop">
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Assign</span>
</button>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog">
<strong class="popup__title margin-block-start-half pad-inline-half">Assign this to…</strong>
<strong class="popup__title margin-block-start-half pad-inline-half txt-nowrap">Assign this to…</strong>
<%= form_with url: bucket_bubble_assignment_toggles_path(bubble.bucket, bubble), class: "flex flex-column full-width popup__list", data: { controller: "form" } do |form| %>
<% users.sort_by(&:name).each do |user| %>
+2 -2
View File
@@ -1,7 +1,7 @@
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn full-width justify-start borderless" data-action="click->dialog#open:stop">
<button class="tag-picker__button btn card__hide-on-index <%= bubble.creating? || bubble.drafted? ? "fill-highlight" : "fill-transparent" %>" data-action="click->dialog#open:stop">
<%= image_tag "tag.svg", aria: { hidden: true }, size: 24 %>
<span>Tag</span>
<span class="for-screen-reader">Add a tag</span>
</button>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow" data-dialog-target="dialog">
+5 -28
View File
@@ -1,32 +1,9 @@
<%= turbo_frame_tag dom_id(@bubble, :stage_picker) do %>
<% if @selected_workflow %>
<aside class="position-relative" style="--bubble-color: <%= @bubble.color %>"
data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside">
<div class="flex align-center justify-start gap-half">
<button class="btn txt-small borderless" style="margin-inline-start: calc((var(--btn-size) + var(--inline-space)) * -0.5);" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= image_tag "bolt.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Choose a workflow</span>
</button>
<strong class="txt-medium"><%= @selected_workflow.name %></strong>
<span class="btn btn--placeholder" aria-hidden="true"></span>
</div>
<dialog class="workflow__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="list-style-none unpad margin-none">
<% @workflows.each do |workflow| %>
<li><%= link_to_stage_picker @bubble, workflow %></li>
<% end %>
</menu>
</dialog>
<div class="workflow">
<hr class="separator--horizontal" aria-hidden="true" style="--border-color: var(--color-subtle)" />
<div class="flex flex-column">
<% @selected_workflow.stages.each do |stage| %>
<%= button_to_set_stage @bubble, stage %>
<% end %>
</div>
</div>
</aside>
<div class="card__stages flex flex-column max-width min-width">
<% @selected_workflow.stages.each do |stage| %>
<%= button_to_set_stage @bubble, stage %>
<% end %>
</div>
<% end %>
<% end %>
+10 -29
View File
@@ -1,32 +1,13 @@
<%= turbo_frame_tag dom_id(@bubble, :watch) do %>
<div class="flex flex-column align-start gap-half">
<div class="flex flex-column align-start">
<h3 class="txt-medium txt-tight-lines">Watchers</h3>
<div><%= pluralize @watchers.count, "person" %> are being notified when someone comments on this bubble.</div>
</div>
<div class="flex flex-wrap gap-half max-width txt-small">
<%= render partial: "bubbles/watches/watcher", collection: @watchers %>
</div>
<div class="flex flex-column align-start margin-block-start">
<% if @bubble.watched_by? Current.user %>
<strong>You are watching this bubble</strong>
<div>You will be notified when someone comments.</div>
<div class="margin-block-half">
<%= button_to bucket_bubble_watch_path(@bubble.bucket, @bubble), method: :delete, class: "btn" do %>
<span>Stop watching</span>
<% end %>
</div>
<% else %>
<strong>Youre not watching this bubble</strong>
<div>You wont be notified when someone comments.</div>
<div class="margin-block-half">
<%= button_to bucket_bubble_watch_path(@bubble.bucket, @bubble), class: "btn" do %>
<span">Watch this</span>
<% end %>
</div>
<% if @bubble.watched_by? Current.user %>
<%= button_to bucket_bubble_watch_path(@bubble.bucket, @bubble), method: :delete, class: "btn btn--reversed" do %>
<%= image_tag "bell-off.svg", size: 16, aria: { hidden: true } %>
<span class="for-screen-reader">Stop watching</span>
<% end %>
</div>
</div>
<% else %>
<%= button_to bucket_bubble_watch_path(@bubble.bucket, @bubble), class: "btn" do %>
<%= image_tag "bell.svg", size: 16, aria: { hidden: true } %>
<span class="for-screen-reader">Watch this</span>
<% end %>
<% end %>
<% end %>
+2 -2
View File
@@ -2,7 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to buckets_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= link_to bubbles_path(bucket_ids: [ @bbucket ]), class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Go back</span>
<% end %>
@@ -72,6 +72,6 @@
<span class="for-screen-reader">Save</span>
</button>
<%= link_to "Cancel and go back", buckets_path, data: { form_target: "cancel", turbo_frame: "_top" }, hidden: true %>
<%= link_to "Cancel and go back", bubbles_path(bucket_ids: [ @bucket ]), data: { form_target: "cancel", turbo_frame: "_top" }, hidden: true %>
<% end %>
</div>
+2 -1
View File
@@ -10,7 +10,7 @@
<header class="flex flex-column center">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
<%= link_to "Activity", root_path %>
</div>
<h1 class="txt-x-large"><%= @page_title %></h1>
</header>
@@ -30,3 +30,4 @@
</ul>
<%= render "notifications/tray" %>
<%= render "bubbles/pins/tray" %>
+3 -3
View File
@@ -2,7 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to buckets_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= link_to root_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Go back</span>
<% end %>
@@ -11,7 +11,7 @@
<div class="panel shadow center" style="--panel-size: 55ch; view-transition-name: new-bucket">
<%= form_with model: @bucket, class: "flex flex-column gap txt-large", data: { controller: "form" } do |form| %>
<%= image_tag "bubbles.svg", aria: { hidden: "true" }, size: 150, class: "colorize--black center" %>
<h2 class="txt-x-large">Create a new collection</h2>
<label class="flex align-center gap">
<%= form.text_field :name, required: true, class: "input full-width", autofocus: true, placeholder: "Give it a name…", data: { action: "keydown.esc@document->form#cancel" } %>
</label>
@@ -20,6 +20,6 @@
<%= image_tag "check.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Save</span>
</button>
<%= link_to "Cancel and go back", buckets_path, data: { form_target: "cancel", turbo_frame: "_top" }, hidden: true %>
<%= link_to "Cancel and go back", root_path, data: { form_target: "cancel", turbo_frame: "_top" }, hidden: true %>
<% end %>
</div>
+7 -11
View File
@@ -1,17 +1,13 @@
<% cache comment do %>
<%= turbo_frame_tag dom_id(comment) do %>
<div class="comment__content flex-inline flex-column full-width border-radius">
<div class="comment__content flex-inline flex-column full-width">
<div class="comment__author flex align-center gap-half">
<h3 contents>
<strong><%= link_to comment.creator.name, user_path(comment.creator), class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %></strong>
<%= link_to bucket_bubble_path(comment.bubble.bucket, comment.bubble, anchor: "comment_#{comment.id}"), class: "txt-undecorated" do %>
<%= local_datetime_tag comment.created_at, style: :shortdate, class: "comment__timestamp" %>
<% end %>
</h3>
<strong><%= link_to comment.creator.name, user_path(comment.creator), class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %></strong>
<%= link_to bucket_bubble_path(comment.bubble.bucket, comment.bubble, anchor: "comment_#{comment.id}"), class: "txt-undecorated" do %>
<%= local_datetime_tag comment.created_at, style: :shortdate, class: "comment__timestamp" %>
<% end %>
<%= link_to edit_bucket_bubble_comment_path(comment.bubble.bucket, comment.bubble, comment), role: "button",
class: "btn txt-small btn--plain fill-transparent comment__edit",
style: "font-size: 0.4em; opacity: 0.5;" do %>
<%= link_to edit_bucket_bubble_comment_path(comment.bubble.bucket, comment.bubble, comment), class: "btn txt-small btn--plain fill-transparent comment__edit", style: "font-size: 0.4em; opacity: 0.5;" do %>
<%= image_tag "menu-dots-horizontal.svg", class: "icon colorize--black" %>
<span class="for-screen-reader">Edit</span>
<% end %>
@@ -23,4 +19,4 @@
<%= render "comments/reactions/reactions", comment: comment %>
</div>
<% end %>
<% end %>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="comment comment--new border-radius flex align-start full-width margin-block-end-double">
<div class="comment comment--new flex align-start full-width margin-block-end-double">
<div class="flex flex-column full-width">
<div class="position-relative">
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width",
@@ -5,16 +5,14 @@
data-reaction-delete-reacter-id-value="<%= reaction.reacter.id %>"
data-controller="reaction-delete" data-reaction-delete-perform-class="reaction--deleting"
data-reaction-delete-reveal-class="expanded" data-reaction-delete-reacter-id-value="<%= reaction.reacter.id %>">
<figure class="reaction__avatar margin-none flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag reaction.reacter, hidden_for_screen_reader: true, loading: :lazy %>
<figure class="reaction__avatar margin-none flex-item-no-shrink">
<%= avatar_tag reaction.reacter, aria: { label: "#{reaction.reacter.name} reacted #{reaction.content}" }, loading: :lazy %>
</figure>
<%= tag.span reaction.content, class: [ "txt-small", { "txt-medium": reaction.all_emoji? } ],
data: {
action: "click->reaction-delete#reveal keydown.enter->reaction-delete#reveal:prevent",
reaction_delete_target: "content" } do %>
<span class="for-screen-reader"><%= "#{reaction.reacter.name} reacted #{reaction.content}" %></span>
<% end %>
<%= tag.span reaction.content, role: "button",
class: [ "txt-small", { "txt-medium": reaction.all_emoji? } ],
data: { action: "click->reaction-delete#reveal keydown.enter->reaction-delete#reveal:prevent", reaction_delete_target: "content" } %>
<%= button_to bucket_bubble_comment_reaction_path(comment.bubble.bucket, comment.bubble, comment, reaction),
method: :delete,
class: "btn btn--negative flex-item-justify-end reaction__delete",
@@ -23,4 +21,5 @@
<span class="for-screen-reader">Delete this reaction</span>
<% end %>
</div>
<% end %>
<span id="delete_reaction_accessible_label" class="for-screen-reader">Press enter to delete this reaction</span>
<% end %>
+3 -1
View File
@@ -1,7 +1,9 @@
<div class="events--day" data-controller="related-element"
data-related-element-highlight-class="event--related">
<% if events.any? %>
<h2 class="txt-large margin-block-end-half"><%= event_day_title(activity_day) %></h2>
<h2 class="txt-medium events__day-header center">
<span class="events__day-header-content"><%= event_day_title(activity_day) %><span>
</h2>
<div class="center events">
<%= render_column_headers(activity_day) %>
+5 -3
View File
@@ -1,15 +1,17 @@
<% if earliest.present? %>
<% if earliest == latest %>
<div class="events--none txt-align-center translucent">
<div class="events--none txt-align-center">
<h2 class="txt-medium margin-none"><%= event_day_title(earliest) %></h2>
<p class="margin-none">No activity</p>
</div>
<% elsif earliest < latest %>
<div class="events--none txt-align-center translucent">
<div class="events--none txt-align-center">
<h2 class="txt-medium margin-none"><%= event_day_title(earliest) %> <%= event_day_title(latest) %></h2>
<p class="margin-none">No activity for <%= (latest - earliest).seconds.in_days.round + 1 %> days</p>
</div>
<% end %>
<% else %>
<p class="events--none txt-align-center translucent">No more activity.</p>
<div class="events--none txt-align-center">
<p class="margin-none">No more activity.</p>
</div>
<% end %>
+7 -9
View File
@@ -2,19 +2,17 @@
<% link = event.action == "commented" ? bucket_bubble_path(bubble.bucket, bubble, anchor: dom_id(event.comment)) : bucket_bubble_path(bubble.bucket, bubble) %>
<%= link_to link,
class: "event panel shadow center center-block flex-inline align-start justify-start gap position-relative",
style: "--bubble-color: #{ bubble.color }; #{ bubble_rotation(bubble) }",
data: { controller: "animation",
related_element_target: "related",
class: "event panel center center-block flex-inline align-start justify-start gap position-relative",
style: "--bubble-color: #{ bubble.color }; background-color: color-mix(in srgb, var(--bubble-color) 10%, var(--color-bg));
color: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink));",
data: { related_element_target: "related",
related_element_group_value: bubble.id,
animation_play_class: "bubble--wobble",
animation_play_on_load_value: "true",
action: "mouseover->animation#play mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
<div class="avatar txt-x-small flex-item-no-shrink">
<%= avatar_image_tag(event.creator) %>
</div>
<div class="flex flex-column min-width txt-small align-start txt-align-start">
<div class="flex flex-column min-width txt-small align-start txt-align-start" style="color: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink));">
<strong class="event__title txt-break overflow-line-clamp txt-tight-lines">
<%= event_action_sentence(event) %>
</strong>
@@ -25,6 +23,6 @@
</span>
<% end %>
<span class="event__bucket translucent"><%= bubble.bucket.name %></span>
<span class="event__bucket"><%= bubble.bucket.name %></span>
</div>
<% end %>
+6 -5
View File
@@ -1,17 +1,18 @@
<% presenter = BucketFilterPresenter.new(@buckets, params, cookies) %>
<div class="position-relative margin-block-start-half margin-block-end center max-width"
<div class="position-relative max-width"
data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="input input--select flex-inline min-width max-width" data-action="click->dialog#open:stop">
<button class="input input--select flex-inline min-width max-width txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<%= presenter.filter_text %>
</span>
</button>
<dialog class="events__popup popup panel flex-column full-width align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">Filter by projects…</strong>
<dialog class="events__popup popup panel flex-column full-width align-start gap-half fill-white shadow"
style="z-index: 5; inset: 0 0 auto auto;" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half txt-nowrap">Filter by Collection…</strong>
<%= form_with url: events_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<%= link_to "Show everything", events_path(clear_filter: true), class: "btn popup__item" %>
<%= link_to "Show everything", events_path(clear_filter: true), class: "btn popup__item txt-nowrap" %>
<% @buckets.each do |bucket| %>
<div class="btn popup__item">
<%= form.check_box "bucket_ids[]", {
+41 -11
View File
@@ -1,20 +1,50 @@
<% @page_title = "Latest Activity" %>
<% @page_title = "Home" %>
<% content_for :header do %>
<nav class="align-start">
<header class="flex-inline flex-column center max-width">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
</div>
<h1 class="txt-x-large"><%= @page_title %></h1>
<%= render "events/filter" %>
</header>
<div class="btn btn--placeholder flex-item-justify-start"></div>
<%= link_to account_users_path, class: "btn flex-item-justify-end" do %>
<%= image_tag "settings.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Account settings</span>
<% end %>
</nav>
<% end %>
<div id="activity" class="events__index flex flex-column gap">
<%= render "events/day", activity_day: @activity_day, next_day: @next_day, events: @events %>
<div class="events__container align-start gap">
<div class="flex flex-column">
<div class="flex align-center gap justify-space-between margin-block-end-half" style="padding-inline: calc(0.3em + 0.6em);">
<h2 class="txt-large txt-nowrap">Latest activity</h2>
<%= render "events/filter" %>
</div>
<div id="activity" class="events__index flex flex-column">
<%= render "events/day", activity_day: @activity_day, next_day: @next_day, events: @events %>
</div>
</div>
<div class="txt-align-start">
<h2 class="txt-large txt-nowrap" style="margin-block-end: 0.5em;">Custom views</h2>
<div style="border-block-start: 1px solid var(--color-subtle); padding-block-start: var(--block-space-half);">
<% @filters.each do |filter| %>
<h3 class="txt-medium margin-block-end-half txt-tight-lines">
<%= link_to filter.summary,bubbles_path(**filter.as_params) %>
</h3>
<% end %>
</div>
<div class="flex align-center gap min-width margin-block-start-double margin-block-end-half" >
<h2 class="txt-large overflow-ellipsis">Collections</h2>
<%= link_to new_bucket_path, class: "btn txt-small flex-item-justify-end flex-item-no-shrink", style: "view-transition-name: new-bucket" do %>
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">New</span>
<% end %>
</div>
<div style="border-block-start: 1px solid var(--color-subtle); padding-block-start: var(--block-space-half);">
<% @buckets.each do |bucket| %>
<h3 class="txt-medium margin-block-end-half txt-tight-lines"><%= link_to bucket.name, bubbles_path(bucket_ids: [ bucket ]) %></h3>
<% end %>
</div>
</div>
</div>
<%= event_next_page_link(@next_day) %>
+44 -45
View File
@@ -1,54 +1,53 @@
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.assignment_status.unassigned? %>
No one
<% elsif filter.assignees.any? %>
<%= filter.assignees.map(&:name).to_sentence %>
<% else %>
Assigned to…
<% end %>
</span>
</button>
<% if filter.assignees.any? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.assignment_status.unassigned? %>
No one
<% elsif filter.assignees.any? %>
<%= filter.assignees.map(&:name).to_sentence %>
<% else %>
Assigned to…
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Assigned to…" aria-description="Assigned to…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Assigned to…</strong>
<%= form_with url: bubbles_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:assignee_ids, :assignment_status).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Assigned to…</strong>
<%= form_with url: bubbles_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:assignee_ids, :assignment_status).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= link_to bubbles_path(filter.as_params.except(:assignee_ids, :assignment_status)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<%= link_to bubbles_path(filter.as_params.except(:assignee_ids, :assignment_status)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<div class="btn popup__item">
<%= form.check_box :assignment_status, {
checked: filter.assignment_status.unassigned?,
data: { action: "change->form#submit" },
include_hidden: false,
}, "unassigned" %>
<%= form.label :assignment_status, "No one", class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% Current.account.users.active.order(:name).each do |user| %>
<div class="btn popup__item">
<%= form.check_box :assignee_ids, {
multiple: true,
checked: filter.assignees.include?(user),
<%= form.check_box "assignment_status", {
checked: filter.assignment_status.unassigned?,
data: { action: "change->form#submit" },
include_hidden: false,
}, user.id %>
}, "unassigned" %>
<%= form.label :assignee_ids, user.name, for: form.field_id(:assignee_ids, user.id), class: "overflow-ellipsis" %>
<%= form.label :assignment_status, "No one", class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% Current.account.users.active.order(:name).each do |user| %>
<div class="btn popup__item">
<%= form.check_box "assignee_ids[]", {
checked: filter.assignees.include?(user),
data: { action: "change->form#submit" },
include_hidden: false,
}, user.id %>
<%= form.label "assignee_ids[]", user.name, for: dom_id(user, :filter), class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
<% end %>
</dialog>
</div>
</dialog>
</div>
<% end %>
+33 -42
View File
@@ -1,43 +1,34 @@
<% if filter.buckets.many? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.buckets.any? %>
<%= filter.buckets.map(&:name).to_sentence %>
<% else %>
In collection
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="In collection" aria-description="In collection"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">In collection</strong>
<%= form_with url: bubbles_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:bucket_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= link_to bubbles_path(filter.as_params.except(:bucket_ids)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<% filter.buckets.order(:name).each do |bucket| %>
<div class="btn popup__item">
<%= form.check_box :bucket_ids, {
multiple: true,
checked: filter.buckets.include?(bucket),
data: { action: "change->form#submit" },
include_hidden: false,
}, bucket.id %>
<%= form.label :bucket_ids, bucket.name, for: form.field_id(:bucket_ids, bucket.id), class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.buckets.any? %>
<%= filter.buckets.map(&:name).to_sentence %>
<% else %>
In Collection…
<% end %>
</dialog>
</div>
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">In Collection</strong>
<%= form_with url: bubbles_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:bucket_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% Current.user.buckets.order(:name).each do |bucket| %>
<div class="btn popup__item">
<%= form.check_box "bucket_ids[]", {
checked: filter.buckets.include?(bucket),
data: { action: "change->form#submit" },
include_hidden: false,
}, bucket.id %>
<%= form.label "bucket_ids[]", bucket.name, for: dom_id(bucket, :filter), class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
</dialog>
</div>
+41 -44
View File
@@ -37,45 +37,59 @@
<% end %>
<div class="filter__columns flex gap">
<div class="filter__menu" role="group" aria-label="Sort by">
<div class="filter__label"><strong>Sort by</strong></div>
<div>
<label class="btn filter__button">
<%= radio_button_tag "indexed_by", filter.default_indexed_by, filter.default_indexed_by?, class: "visually-hidden" %>
<menu class="filter__menu">
<li class="filter__label"><strong>In Collection</strong></li>
<li>
<%= button_tag type: :button, class: "btn filter__button", data: { action: "filter-form#clearCategory", filter_form_name_param: "bucket_ids[]" } do %>
<span>All Collections</span>
<% end %>
</li>
<% Current.user.buckets.order(:name).each do |bucket| %>
<li>
<%= label_tag "bucket_ids_#{bucket.id}", class: "btn filter__button" do %>
<%= check_box_tag "bucket_ids[]", bucket.id, filter.buckets.include?(bucket), id: "bucket_ids_#{bucket.id}", hidden: true %>
<span><%= bucket.name %></span>
<%= image_tag "close.svg", aria: { hidden: true }, size: 24 %>
<% end %>
</li>
<% end %>
</menu>
<menu class="filter__menu">
<li class="filter__label"><strong>Sort by</strong></li>
<li>
<%= label_tag "indexed_by_#{filter.default_indexed_by}", class: "btn filter__button" do %>
<%= radio_button_tag "indexed_by", filter.default_indexed_by, filter.default_indexed_by?, hidden: true %>
<span>Most active</span>
<%= image_tag "close.svg", aria: { hidden: true }, size: 24 %>
</label>
</div>
<% end %>
</li>
<% Filter::INDEXES.each do |index| %>
<div>
<li>
<label class="btn filter__button">
<%= radio_button_tag "indexed_by", index, filter.indexed_by == index, class: "visually-hidden" %>
<span><%= index.humanize %></span>
<%= image_tag "close.svg", aria: { hidden: true }, size: 24 %>
</label>
</div>
</li>
<% end %>
</div>
</menu>
<div class="filter__menu" role="group" aria-label="In Collection">
<div class="filter__label"><strong>In Collection</strong></div>
<div>
<%= button_tag type: :button, class: "btn filter__button", data: { action: "filter-form#clearCategory", filter_form_name_param: "bucket_ids[]" } do %>
<span>All Collections</span>
<% end %>
</div>
<% Current.user.buckets.order(:name).each do |bucket| %>
<div>
<label class="btn filter__button">
<%= check_box_tag "bucket_ids[]", bucket.id, filter.buckets.include?(bucket), class: "visually-hidden" %>
<span><%= bucket.name %></span>
<menu class="filter__menu">
<li class="filter__label"><strong>In Stage</strong></li>
<% workflow = Current.account.workflows.first %>
<% workflow.stages.each do |stage| %>
<li>
<%= label_tag "stage_ids_#{stage.id}", class: "btn filter__button" do %>
<%= check_box_tag "stage_ids[]", stage.id, filter.stages.include?(stage), id: "stage_ids_#{stage.id}", hidden: true %>
<span><%= stage.name %></span>
<%= image_tag "close.svg", aria: { hidden: true }, size: 24 %>
</label>
</div>
<% end %>
</li>
<% end %>
</div>
</menu>
<div class="filter__menu" role="group" aria-label="Tagged">
<div class="filter__label"><strong>Tagged</strong></div>
@@ -143,24 +157,7 @@
</label>
</div>
<% end %>
</div>
<div class="filter__menu" role="group" aria-label="In Stage">
<div class="filter__label"><strong>In Stage</strong></div>
<% Current.account.workflows.order(:name).each do |workflow| %>
<div class="filter__label"><strong><%= workflow.name %></strong></div>
<% workflow.stages.order(:name).each do |stage| %>
<div>
<label class="btn filter__button">
<%= check_box_tag "stage_ids[]", stage.id, filter.stages.include?(stage), class: "visually-hidden" %>
<span><%= stage.name %></span>
<%= image_tag "close.svg", aria: { hidden: true }, size: 24 %>
</label>
</div>
<% end %>
<% end %>
</div>
</menu>
</div>
<% end %>
+3 -3
View File
@@ -1,16 +1,16 @@
<div class="filters flex-inline align-center gap-half">
<div class="filters flex-inline align-center gap-half margin-block">
<button class="btn btn--plain txt-small" data-action="click->dialog#toggle">
<%= image_tag "filter.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Filter</span>
</button>
<div class="flex-inline center align-center gap-half">
<%= render "filters/indexed_by", filter: filter %>
<%= render "filters/buckets", filter: filter %>
<%= render "filters/indexed_by", filter: filter %>
<%= render "filters/stages", filter: filter %>
<%= render "filters/tags", filter: filter %>
<%= render "filters/assignees", filter: filter %>
<%= render "filters/creators", filter: filter %>
<%= render "filters/stages", filter: filter %>
<% filter.terms.each do |term| %>
<%= filter_chip_tag %Q("#{term}"), filter.as_params_without(:terms, term) %>
+4 -11
View File
@@ -1,4 +1,4 @@
<% if Current.account.workflows.any? && filter.stages.any? %>
<% if Current.account.workflows.any? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
@@ -19,15 +19,9 @@
<% filter.as_params.except(:stage_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= link_to bubbles_path(filter.as_params.except(:stage_ids)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<% Current.account.workflows.order(:name).each do |workflow| %>
<strong class="popup__title margin-block-start-half pad-inline-half flex overflow-ellipsis"><%= workflow.name %></strong>
<% workflow.stages.order(:name).each do |stage| %>
<% workflow = Current.account.workflows.first %>
<% workflow.stages.each do |stage| %>
<div class="btn popup__item">
<%= form.check_box :stage_ids, {
multiple: true,
@@ -39,7 +33,6 @@
<%= form.label :stage_ids, stage.name, for: form.field_id(:stage_ids, stage.id), class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
<% end %>
</dialog>
+37 -38
View File
@@ -1,41 +1,40 @@
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.tags.any? %>
<%= filter.tags.map(&:hashtag).to_sentence %>
<% else %>
Tagged…
<% end %>
</span>
</button>
<% if filter.tags.any? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.tags.any? %>
<%= filter.tags.map(&:hashtag).to_sentence %>
<% else %>
Tagged…
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Tagged…" aria-description="Tagged…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Tagged…</strong>
<%= form_with url: bubbles_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:tag_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Tagged…</strong>
<%= form_with url: bubbles_path, method: :get, class: "flex flex-column full-width popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:tag_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= link_to bubbles_path(filter.as_params.except(:tag_ids)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<% Current.account.tags.order(:title).each do |tag| %>
<div class="btn popup__item">
<%= form.check_box "tag_ids[]", {
checked: filter.tags.include?(tag),
data: { action: "change->form#submit" },
include_hidden: false,
}, tag.id %>
<%= link_to bubbles_path(filter.as_params.except(:tag_ids)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<%= form.label "tag_ids[]", tag.hashtag, for: dom_id(tag, :filter), class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
<% Current.account.tags.order(:title).each do |tag| %>
<div class="btn popup__item">
<%= form.check_box :tag_ids, {
multiple: true,
checked: filter.tags.include?(tag),
data: { action: "change->form#submit" },
include_hidden: false,
}, tag.id %>
<%= form.label :tag_ids, tag.hashtag, for: form.field_id(:tag_ids, tag.id), class: "overflow-ellipsis" %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
</dialog>
</div>
</dialog>
</div>
<% end %>
+1 -8
View File
@@ -1,14 +1,7 @@
<% @page_title = "Set up Fizzy" %>
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 55ch;">
<div class="bubble center" style="--bubble-size: 12cqi; --bubble-color: var(--color-link);">
<span class="bubble__shape"></span>
<span class="bubble__detail bubble__date">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</span>
<span class="bubble__detail bubble__boosts">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</span>
<span class="bubble__detail bubble__assignee">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</span>
</div>
<h1 class="margin-block-start-half margin-block-end-double">Fizzy</h1>
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
<%= form_with model: User.new, url: first_run_path, class: "flex flex-column gap txt-large" do |form| %>
<div class="flex align-center gap">
+7 -9
View File
@@ -2,16 +2,14 @@
<% content_for :header do %>
<nav class="align-start">
<div class="btn btn--placeholder flex-item-justify-start"></div>
<header class="flex flex-column center margin-block-end">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
</div>
<h1 class="txt-x-large"><%= @page_title %></h1>
</header>
<%= link_to root_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= image_tag "home.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Home</span>
<% end %>
<%= link_to notifications_settings_path, class: "btn borderless flex-item-justify-end" do %>
<h1 class="txt-x-large"><%= @page_title %></h1>
<%= link_to notifications_settings_path, class: "btn flex-item-justify-end" do %>
<%= image_tag "settings.svg", size: 20 %>
<span class="for-screen-reader">Notification settings</span>
<% end %>
@@ -7,13 +7,7 @@
<span class="for-screen-reader">Go back</span>
<% end %>
<header class="flex flex-column center margin-block-end">
<div class="flex align-center gap-half justify-center">
<%= link_to "Collections", root_path %>
<%= link_to "Activity", events_path %>
</div>
<h1 class="txt-x-large"><%= @page_title %></h1>
</header>
<h1 class="txt-x-large"><%= @page_title %></h1>
<div class="btn btn--placeholder flex-item-justify-end"></div>
</nav>
+2 -9
View File
@@ -1,16 +1,9 @@
<% @page_title = "Sign in" %>
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 55ch;">
<div class="bubble center" style="--bubble-size: 12cqi; --bubble-color: var(--color-link);">
<span class="bubble__shape"></span>
<span class="bubble__detail bubble__date">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</span>
<span class="bubble__detail bubble__boosts">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</span>
<span class="bubble__detail bubble__assignee">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</span>
</div>
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
<h1 class="margin-block-start-half margin-block-end-double">Fizzy</h1>
<%= form_with url: session_path, class: "flex flex-column gap txt-large" do |form| %>
<%= form_with url: session_path, class: "flex flex-column gap txt-large", data: { turbo: false } do |form| %>
<div class="flex align-center gap">
<%= translation_button :email_address %>
<label class="flex align-center gap input input--actor">
+9 -1
View File
@@ -1,5 +1,5 @@
Rails.application.routes.draw do
root "buckets#index"
root "events#index"
resource :account do
scope module: :accounts do
@@ -52,6 +52,7 @@ Rails.application.routes.draw do
resource :stage_picker
resources :stagings
resource :watch
resource :pin, only: [ :show, :create, :destroy ]
end
namespace :assignments, as: :assignment do
@@ -92,4 +93,11 @@ Rails.application.routes.draw do
post "join/:join_code", to: "users#create"
get "up", to: "rails/health#show", as: :rails_health_check
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
namespace :bubbles do
namespace :pins do
resource :tray, only: :show
end
resources :pins, only: :index
end
end
+2 -2
View File
@@ -13,9 +13,9 @@
ActiveRecord::Schema[8.1].define(version: 1) do
create_table "solid_cable_messages", force: :cascade do |t|
t.binary "channel", limit: 1024, null: false
t.binary "payload", limit: 536870912, null: false
t.datetime "created_at", null: false
t.integer "channel_hash", limit: 8, null: false
t.datetime "created_at", null: false
t.binary "payload", limit: 536870912, null: false
t.index ["channel"], name: "index_solid_cable_messages_on_channel"
t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
+4 -4
View File
@@ -12,11 +12,11 @@
ActiveRecord::Schema[8.1].define(version: 1) do
create_table "solid_cache_entries", force: :cascade do |t|
t.binary "key", limit: 1024, null: false
t.binary "value", limit: 536870912, null: false
t.datetime "created_at", null: false
t.integer "key_hash", limit: 8, null: false
t.integer "byte_size", limit: 4, null: false
t.datetime "created_at", null: false
t.binary "key", limit: 1024, null: false
t.integer "key_hash", limit: 8, null: false
t.binary "value", limit: 536870912, null: false
t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
+11
View File
@@ -0,0 +1,11 @@
class CreatePins < ActiveRecord::Migration[7.0]
def change
create_table :pins do |t|
t.references :bubble, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps
end
add_index :pins, [ :bubble_id, :user_id ], unique: true
end
end
+39 -39
View File
@@ -12,42 +12,42 @@
ActiveRecord::Schema[8.1].define(version: 1) do
create_table "solid_queue_blocked_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.string "concurrency_key", null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.datetime "expires_at", null: false
t.bigint "job_id", null: false
t.integer "priority", default: 0, null: false
t.string "queue_name", null: false
t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
end
create_table "solid_queue_claimed_executions", force: :cascade do |t|
t.datetime "created_at", null: false
t.bigint "job_id", null: false
t.bigint "process_id"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
end
create_table "solid_queue_failed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.text "error"
t.datetime "created_at", null: false
t.text "error"
t.bigint "job_id", null: false
t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
end
create_table "solid_queue_jobs", force: :cascade do |t|
t.string "queue_name", null: false
t.string "class_name", null: false
t.text "arguments"
t.integer "priority", default: 0, null: false
t.string "active_job_id"
t.datetime "scheduled_at"
t.datetime "finished_at"
t.text "arguments"
t.string "class_name", null: false
t.string "concurrency_key"
t.datetime "created_at", null: false
t.datetime "finished_at"
t.integer "priority", default: 0, null: false
t.string "queue_name", null: false
t.datetime "scheduled_at"
t.datetime "updated_at", null: false
t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
@@ -57,76 +57,76 @@ ActiveRecord::Schema[8.1].define(version: 1) do
end
create_table "solid_queue_pauses", force: :cascade do |t|
t.string "queue_name", null: false
t.datetime "created_at", null: false
t.string "queue_name", null: false
t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
end
create_table "solid_queue_processes", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "hostname"
t.string "kind", null: false
t.datetime "last_heartbeat_at", null: false
t.bigint "supervisor_id"
t.integer "pid", null: false
t.string "hostname"
t.text "metadata"
t.datetime "created_at", null: false
t.string "name", null: false
t.integer "pid", null: false
t.bigint "supervisor_id"
t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
end
create_table "solid_queue_ready_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "created_at", null: false
t.bigint "job_id", null: false
t.integer "priority", default: 0, null: false
t.string "queue_name", null: false
t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
end
create_table "solid_queue_recurring_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "task_key", null: false
t.datetime "run_at", null: false
t.datetime "created_at", null: false
t.bigint "job_id", null: false
t.datetime "run_at", null: false
t.string "task_key", null: false
t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
end
create_table "solid_queue_recurring_tasks", force: :cascade do |t|
t.string "key", null: false
t.string "schedule", null: false
t.string "command", limit: 2048
t.string "class_name"
t.text "arguments"
t.string "queue_name"
t.integer "priority", default: 0
t.boolean "static", default: true, null: false
t.text "description"
t.string "class_name"
t.string "command", limit: 2048
t.datetime "created_at", null: false
t.text "description"
t.string "key", null: false
t.integer "priority", default: 0
t.string "queue_name"
t.string "schedule", null: false
t.boolean "static", default: true, null: false
t.datetime "updated_at", null: false
t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
end
create_table "solid_queue_scheduled_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "scheduled_at", null: false
t.datetime "created_at", null: false
t.bigint "job_id", null: false
t.integer "priority", default: 0, null: false
t.string "queue_name", null: false
t.datetime "scheduled_at", null: false
t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
end
create_table "solid_queue_semaphores", force: :cascade do |t|
t.string "key", null: false
t.integer "value", default: 1, null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.datetime "expires_at", null: false
t.string "key", null: false
t.datetime "updated_at", null: false
t.integer "value", default: 1, null: false
t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
Generated
+83 -72
View File
@@ -10,42 +10,42 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
ActiveRecord::Schema[8.1].define(version: 2025_03_18_005138) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.index ["bucket_id", "user_id"], name: "index_accesses_on_bucket_id_and_user_id", unique: true
t.index ["bucket_id"], name: "index_accesses_on_bucket_id"
t.index ["user_id"], name: "index_accesses_on_user_id"
end
create_table "accounts", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "join_code"
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["join_code"], name: "index_accounts_on_join_code", unique: true
t.index ["name"], name: "index_accounts_on_name", unique: true
end
create_table "action_text_markdowns", force: :cascade do |t|
t.string "record_type", null: false
t.integer "record_id", null: false
t.string "name", null: false
t.text "content", default: "", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.integer "record_id", null: false
t.string "record_type", null: false
t.datetime "updated_at", null: false
t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record"
end
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.bigint "record_id", null: false
t.string "record_type", null: false
t.string "slug"
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
@@ -53,14 +53,14 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.string "content_type"
t.datetime "created_at", null: false
t.string "filename", null: false
t.string "key", null: false
t.text "metadata"
t.string "service_name", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
@@ -71,45 +71,45 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
end
create_table "assignees_filters", id: false, force: :cascade do |t|
t.integer "filter_id", null: false
t.integer "assignee_id", null: false
t.integer "filter_id", null: false
t.index ["assignee_id"], name: "index_assignees_filters_on_assignee_id"
t.index ["filter_id"], name: "index_assignees_filters_on_filter_id"
end
create_table "assigners_filters", id: false, force: :cascade do |t|
t.integer "filter_id", null: false
t.integer "assigner_id", null: false
t.integer "filter_id", null: false
t.index ["assigner_id"], name: "index_assigners_filters_on_assigner_id"
t.index ["filter_id"], name: "index_assigners_filters_on_filter_id"
end
create_table "assignments", force: :cascade do |t|
t.integer "assignee_id", null: false
t.integer "assigner_id", null: false
t.integer "bubble_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "assigner_id", null: false
t.index ["assignee_id", "bubble_id"], name: "index_assignments_on_assignee_id_and_bubble_id", unique: true
t.index ["bubble_id"], name: "index_assignments_on_bubble_id"
end
create_table "bubbles", force: :cascade do |t|
t.string "title"
t.string "color"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "creator_id", null: false
t.date "due_on"
t.integer "bucket_id", null: false
t.integer "boosts_count", default: 0, null: false
t.integer "stage_id"
t.integer "comments_count", default: 0, null: false
t.float "activity_score", default: 0.0, null: false
t.text "status", default: "creating", null: false
t.datetime "auto_pop_at", null: false
t.datetime "activity_score_at"
t.float "activity_score_order", default: 0.0, null: false
t.datetime "auto_pop_at", null: false
t.integer "boosts_count", default: 0, null: false
t.integer "bucket_id", null: false
t.string "color"
t.integer "comments_count", default: 0, null: false
t.datetime "created_at", null: false
t.integer "creator_id", null: false
t.date "due_on"
t.integer "stage_id"
t.text "status", default: "creating", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["activity_score_order"], name: "index_bubbles_on_activity_score_order", order: :desc
t.index ["auto_pop_at"], name: "index_bubbles_on_auto_pop_at"
t.index ["bucket_id"], name: "index_bubbles_on_bucket_id"
@@ -118,31 +118,31 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
create_table "buckets", force: :cascade do |t|
t.integer "account_id", null: false
t.boolean "all_access", default: false, null: false
t.datetime "created_at", null: false
t.integer "creator_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "all_access", default: false, null: false
t.index ["account_id"], name: "index_buckets_on_account_id"
t.index ["creator_id"], name: "index_buckets_on_creator_id"
end
create_table "buckets_filters", id: false, force: :cascade do |t|
t.integer "filter_id", null: false
t.integer "bucket_id", null: false
t.integer "filter_id", null: false
t.index ["bucket_id"], name: "index_buckets_filters_on_bucket_id"
t.index ["filter_id"], name: "index_buckets_filters_on_filter_id"
end
create_table "comments", force: :cascade do |t|
t.integer "creator_id", null: false
t.datetime "created_at", null: false
t.integer "creator_id", null: false
t.datetime "updated_at", null: false
end
create_table "creators_filters", id: false, force: :cascade do |t|
t.integer "filter_id", null: false
t.integer "creator_id", null: false
t.integer "filter_id", null: false
t.index ["creator_id"], name: "index_creators_filters_on_creator_id"
t.index ["filter_id"], name: "index_creators_filters_on_filter_id"
end
@@ -153,25 +153,25 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
end
create_table "events", force: :cascade do |t|
t.integer "creator_id", null: false
t.json "particulars", default: {}
t.string "action", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "summary_id", null: false
t.integer "bubble_id", null: false
t.datetime "created_at", null: false
t.integer "creator_id", null: false
t.date "due_date"
t.json "particulars", default: {}
t.integer "summary_id", null: false
t.datetime "updated_at", null: false
t.index ["bubble_id"], name: "index_events_on_bubble_id"
t.index ["creator_id"], name: "index_events_on_creator_id"
t.index ["summary_id", "action"], name: "index_events_on_summary_id_and_action"
end
create_table "filters", force: :cascade do |t|
t.integer "creator_id", null: false
t.string "params_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "creator_id", null: false
t.json "fields", default: {}, null: false
t.string "params_digest", null: false
t.datetime "updated_at", null: false
t.index ["creator_id", "params_digest"], name: "index_filters_on_creator_id_and_params_digest", unique: true
end
@@ -191,23 +191,23 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
create_table "messages", force: :cascade do |t|
t.integer "bubble_id", null: false
t.string "messageable_type", null: false
t.integer "messageable_id", null: false
t.datetime "created_at", null: false
t.integer "messageable_id", null: false
t.string "messageable_type", null: false
t.datetime "updated_at", null: false
t.index ["bubble_id"], name: "index_messages_on_bubble_id"
t.index ["messageable_type", "messageable_id"], name: "index_messages_on_messageable", unique: true
end
create_table "notifications", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "event_id", null: false
t.integer "bubble_id", null: false
t.string "resource_type", null: false
t.integer "resource_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "event_id", null: false
t.datetime "read_at"
t.integer "resource_id", null: false
t.string "resource_type", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.index ["bubble_id"], name: "index_notifications_on_bubble_id"
t.index ["event_id"], name: "index_notifications_on_event_id"
t.index ["resource_type", "resource_id"], name: "index_notifications_on_resource"
@@ -215,40 +215,49 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
t.index ["user_id"], name: "index_notifications_on_user_id"
end
create_table "pops", force: :cascade do |t|
create_table "pins", force: :cascade do |t|
t.integer "bubble_id", null: false
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.index ["bubble_id"], name: "index_pins_on_bubble_id"
t.index ["user_id"], name: "index_pins_on_user_id"
end
create_table "pops", force: :cascade do |t|
t.integer "bubble_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["bubble_id"], name: "index_pops_on_bubble_id", unique: true
t.index ["user_id"], name: "index_pops_on_user_id"
end
create_table "reactions", force: :cascade do |t|
t.integer "comment_id", null: false
t.integer "reacter_id", null: false
t.string "content", limit: 16, null: false
t.datetime "created_at", null: false
t.integer "reacter_id", null: false
t.datetime "updated_at", null: false
t.index ["comment_id"], name: "index_reactions_on_comment_id"
t.index ["reacter_id"], name: "index_reactions_on_reacter_id"
end
create_table "sessions", force: :cascade do |t|
t.integer "user_id", null: false
t.string "ip_address"
t.string "user_agent"
t.datetime "created_at", null: false
t.string "ip_address"
t.datetime "updated_at", null: false
t.string "user_agent"
t.integer "user_id", null: false
t.index ["user_id"], name: "index_sessions_on_user_id"
end
create_table "subscriptions", force: :cascade do |t|
t.string "subscribable_type", null: false
t.integer "subscribable_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.integer "subscribable_id", null: false
t.string "subscribable_type", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.index ["subscribable_type", "subscribable_id", "user_id"], name: "idx_on_subscribable_type_subscribable_id_user_id_81936d569b", unique: true
t.index ["subscribable_type", "subscribable_id"], name: "index_subscriptions_on_subscribable"
t.index ["user_id"], name: "index_subscriptions_on_user_id"
@@ -256,57 +265,57 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
create_table "taggings", force: :cascade do |t|
t.integer "bubble_id", null: false
t.integer "tag_id", null: false
t.datetime "created_at", null: false
t.integer "tag_id", null: false
t.datetime "updated_at", null: false
t.index ["bubble_id", "tag_id"], name: "index_taggings_on_bubble_id_and_tag_id", unique: true
t.index ["tag_id"], name: "index_taggings_on_tag_id"
end
create_table "tags", force: :cascade do |t|
t.string "title"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "account_id", null: false
t.datetime "created_at", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_tags_on_account_id"
end
create_table "users", force: :cascade do |t|
t.integer "account_id", null: false
t.string "name", null: false
t.string "email_address"
t.string "password_digest"
t.boolean "active", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "email_address"
t.string "name", null: false
t.string "password_digest"
t.string "role", default: "member", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["email_address"], name: "index_users_on_email_address", unique: true
t.index ["role"], name: "index_users_on_role"
end
create_table "watches", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "bubble_id", null: false
t.boolean "watching", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.boolean "watching", default: true, null: false
t.index ["bubble_id"], name: "index_watches_on_bubble_id"
t.index ["user_id"], name: "index_watches_on_user_id"
end
create_table "workflow_stages", force: :cascade do |t|
t.integer "workflow_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.integer "workflow_id", null: false
t.index ["workflow_id"], name: "index_workflow_stages_on_workflow_id"
end
create_table "workflows", force: :cascade do |t|
t.integer "account_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_workflows_on_account_id"
end
@@ -320,6 +329,8 @@ ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
add_foreign_key "notifications", "bubbles"
add_foreign_key "notifications", "events"
add_foreign_key "notifications", "users"
add_foreign_key "pins", "bubbles"
add_foreign_key "pins", "users"
add_foreign_key "pops", "bubbles"
add_foreign_key "pops", "users"
add_foreign_key "sessions", "users"
+58 -17
View File
@@ -708,6 +708,12 @@ columns:
default_function:
collation:
comment:
pins:
- *5
- *25
- *30
- *6
- *7
pops:
- *5
- *25
@@ -946,6 +952,7 @@ primary_keys:
filters_tags:
messages: id
notifications: id
pins: id
pops: id
reactions: id
schema_migrations: version
@@ -980,6 +987,7 @@ data_sources:
filters_tags: true
messages: true
notifications: true
pins: true
pops: true
reactions: true
schema_migrations: true
@@ -1282,22 +1290,6 @@ indexes:
comment:
valid: true
bubbles:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: bubbles
name: index_bubbles_on_activity_score_order
unique: false
columns:
- activity_score_order
lengths: {}
orders: :desc
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: bubbles
name: index_bubbles_on_stage_id
@@ -1346,6 +1338,22 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: bubbles
name: index_bubbles_on_activity_score_order
unique: false
columns:
- activity_score_order
lengths: {}
orders: :desc
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
buckets:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: buckets
@@ -1701,6 +1709,39 @@ indexes:
nulls_not_distinct:
comment:
valid: true
pins:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: pins
name: index_pins_on_user_id
unique: false
columns:
- user_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: pins
name: index_pins_on_bubble_id
unique: false
columns:
- bubble_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
pops:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: pops
@@ -2004,4 +2045,4 @@ indexes:
nulls_not_distinct:
comment:
valid: true
version: 20250304140641
version: 20250318005138
@@ -0,0 +1,23 @@
require "test_helper"
class Bubbles::PinsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
end
test "create" do
assert_changes -> { bubbles(:layout).pinned_by?(users(:kevin)) }, from: false, to: true do
post bucket_bubble_pin_url(buckets(:writebook), bubbles(:layout))
end
assert_redirected_to bucket_bubble_pin_url(buckets(:writebook), bubbles(:layout))
end
test "destroy" do
assert_changes -> { bubbles(:shipping).pinned_by?(users(:kevin)) }, from: true, to: false do
delete bucket_bubble_pin_url(buckets(:writebook), bubbles(:shipping))
end
assert_redirected_to bucket_bubble_pin_url(buckets(:writebook), bubbles(:shipping))
end
end
+7
View File
@@ -0,0 +1,7 @@
logo_kevin:
bubble: logo
user: kevin
shipping_kevin:
bubble: shipping
user: kevin
+6 -6
View File
@@ -1,11 +1,11 @@
qa_triage:
name: Triage
qa_maybe:
name: Maybe?
workflow: qa
qa_in_progress:
name: In Progress
qa_not_now:
name: Not now
workflow: qa
qa_ready_for_review:
name: Ready for Review
qa_done:
name: Done
workflow: qa
+2 -2
View File
@@ -34,14 +34,14 @@ class BubbleMessagesTest < ActionDispatch::IntegrationTest
assert_equal "assigned", bubble.messages.last.messageable.events.last.action
# Stage it
post bucket_bubble_stagings_url(buckets(:writebook), bubble), params: { stage_id: workflow_stages(:qa_triage).id }
post bucket_bubble_stagings_url(buckets(:writebook), bubble), params: { stage_id: workflow_stages(:qa_maybe).id }
assert_equal 3, bubble.messages.count
assert_predicate bubble.messages.last, :event_summary?
assert_equal 2, bubble.messages.last.event_summary.events.count
assert_equal "staged", bubble.messages.last.messageable.events.last.action
# Unstage it
post bucket_bubble_stagings_url(buckets(:writebook), bubble), params: { stage_id: workflow_stages(:qa_triage).id }
post bucket_bubble_stagings_url(buckets(:writebook), bubble), params: { stage_id: workflow_stages(:qa_maybe).id }
assert_equal 3, bubble.messages.count
assert_predicate bubble.messages.last, :event_summary?
assert_equal 3, bubble.messages.last.event_summary.events.count
+3 -3
View File
@@ -8,7 +8,7 @@ class FilterTest < ActiveSupport::TestCase
bubbles(:layout).capture Comment.new(body: "I hate haggis")
bubbles(:logo).capture Comment.new(body: "I love haggis")
bubbles(:logo).update(stage: workflow_stages(:qa_triage))
bubbles(:logo).update(stage: workflow_stages(:qa_maybe))
end
assert_not_includes users(:kevin).filters.new.bubbles, @new_bubble
@@ -19,7 +19,7 @@ class FilterTest < ActiveSupport::TestCase
filter = users(:david).filters.new creator_ids: [ users(:david).id ], tag_ids: [ tags(:mobile).id ]
assert_equal [ bubbles(:layout) ], filter.bubbles
filter = users(:david).filters.new stage_ids: [ workflow_stages(:qa_triage).id ]
filter = users(:david).filters.new stage_ids: [ workflow_stages(:qa_maybe).id ]
assert_equal [ bubbles(:logo) ], filter.bubbles
filter = users(:david).filters.new assignment_status: "unassigned", bucket_ids: [ @new_bucket.id ]
@@ -105,7 +105,7 @@ class FilterTest < ActiveSupport::TestCase
test "summary" do
assert_equal "Most discussed, tagged #Mobile, and assigned to JZ ", filters(:jz_assignments).summary
filters(:jz_assignments).update!(stages: workflow_stages(:qa_triage, :qa_in_progress))
filters(:jz_assignments).update!(stages: workflow_stages(:qa_maybe, :qa_not_now))
assert_equal "Most discussed, tagged #Mobile, assigned to JZ, and staged in Triage or In Progress ", filters(:jz_assignments).summary
filters(:jz_assignments).update!(stages: [], assignees: [], tags: [], buckets: [ buckets(:writebook) ])