diff --git a/app/assets/images/bubbles.svg b/app/assets/images/bubbles.svg
new file mode 100644
index 0000000000..bddf292cad
--- /dev/null
+++ b/app/assets/images/bubbles.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/stylesheets/bubble.css b/app/assets/stylesheets/bubble.css
index 33dd732c24..fc875571fe 100644
--- a/app/assets/stylesheets/bubble.css
+++ b/app/assets/stylesheets/bubble.css
@@ -43,6 +43,18 @@
}
}
+ .bubble--golden {
+ &:before {
+ box-shadow:
+ 0 0 0 1px color-mix(in oklch, var(--color-golden) 100%, transparent),
+ 0 0 0.2em 0.2em color-mix(in oklch, var(--color-golden) 25%, transparent);
+ background: radial-gradient(
+ color-mix(in srgb, var(--color-golden) 8%, var(--color-canvas)) 50%,
+ color-mix(in srgb, var(--color-golden) 48%, var(--color-canvas)) 100%
+ );
+ }
+ }
+
.bubble__number {
display: grid;
font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.fizzy.do/5986089/boards/2/cards/1373 */
diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css
index 1083c2ffc9..31ec902b0f 100644
--- a/app/assets/stylesheets/card-columns.css
+++ b/app/assets/stylesheets/card-columns.css
@@ -789,7 +789,7 @@
--card-color: var(--color-card-complete) !important;
}
- .bubble {
+ .bubble:not(.bubble--golden) {
display: none !important;
}
}
@@ -799,17 +799,13 @@
/* Surface a mini bubble if there are cards with bubbles inside */
.cards--maybe:has(.bubble:not([hidden])) .cards__expander-title,
- .cards--maybe.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container,
+ .cards--on-deck:has(.bubble--golden:not([hidden])) .cards__expander-title,
.cards--doing.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container {
- --bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
--bubble-opacity: 75%;
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
- &:before {
- background: radial-gradient(
- color-mix(in srgb, var(--bubble-color) calc(var(--bubble-opacity) / 5), var(--color-canvas)) 50%,
- color-mix(in srgb, var(--bubble-color) var(--bubble-opacity), var(--color-canvas)) 100%
- );
+ &:before,
+ &:after {
block-size: 1em;
border-radius: var(--bubble-shape);
content: "";
@@ -820,21 +816,53 @@
z-index: 1;
}
+ /* Regular mini bubble */
+ .cards:has(.bubble:not(.bubble--golden):not([hidden])) &::before {
+ --bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
+ background: radial-gradient(
+ color-mix(in srgb, var(--bubble-color) calc(var(--bubble-opacity) / 5), var(--color-canvas)) 50%,
+ color-mix(in srgb, var(--bubble-color) var(--bubble-opacity), var(--color-canvas)) 100%
+ );
+ }
+
+ /* Golden mini bubble */
+ .cards:has(.bubble--golden:not([hidden])) &::after {
+ --bubble-color: var(--color-golden);
+ background: radial-gradient(
+ color-mix(in srgb, var(--bubble-color) calc(var(--bubble-opacity) / 5), var(--color-canvas)) 50%,
+ color-mix(in srgb, var(--bubble-color) var(--bubble-opacity), var(--color-canvas)) 100%
+ );
+ }
+
+ /* Offset if both bubbles present */
+ .cards:has(.bubble:not(.bubble--golden):not([hidden])):has(.bubble--golden:not([hidden])) & {
+ &::before { translate: -15% -25%; }
+ &::after { translate: 35% -15%; }
+ }
+
/* Maybe column: position bubble relative to the title, not the container */
- .cards--maybe.is-expanded & {
+ .cards--maybe.is-expanded &,
+ .cards--on-deck.is-expanded & {
overflow: visible;
position: relative;
- &:before {
+ &:before,
+ &:after {
inset-block-start: 50%;
inset-inline-start: 0;
- translate: -125% -75%;
+ translate: -135% -50%;
z-index: -1;
}
+
+ .cards:has(.bubble:not(.bubble--golden):not([hidden])):has(.bubble--golden:not([hidden])) & {
+ &::before { translate: -160% -50%; }
+ &::after { translate: -110% -50%; }
+ }
}
@media (max-width: 639px) {
- &.cards__expander-title:before {
+ &.cards__expander-title:before,
+ &.cards__expander-title:after {
display: none;
}
}
diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css
index b3d37d68ac..cd6b90b4ad 100644
--- a/app/assets/stylesheets/card-perma.css
+++ b/app/assets/stylesheets/card-perma.css
@@ -248,7 +248,7 @@
&:has([open]) {
position: relative;
- z-index: 1;
+ z-index: 2;
}
&:has([data-controller~="tooltip"]:hover) {
diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css
index cbaa66c914..e2392dce01 100644
--- a/app/assets/stylesheets/cards.css
+++ b/app/assets/stylesheets/cards.css
@@ -407,7 +407,7 @@
.card-perma:has(.card--postponed) {
--card-color: var(--color-card-complete) !important;
- .bubble {
+ .bubble:not(.bubble--golden) {
display: none;
}
}
diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css
index 42a3a21ece..353f0e8a24 100644
--- a/app/assets/stylesheets/icons.css
+++ b/app/assets/stylesheets/icons.css
@@ -34,6 +34,7 @@
.icon--bookmark-outline { --svg: url("bookmark-outline.svg "); }
.icon--bookmark { --svg: url("bookmark.svg "); }
.icon--boost { --svg: url("boost.svg "); }
+ .icon--bubbles { --svg: url("bubbles.svg "); }
.icon--camera { --svg: url("camera.svg "); }
.icon--caret-down { --svg: url("caret-down.svg "); }
.icon--check { --svg: url("check.svg "); }
diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css
index aa0b8c1daf..228d6886e7 100644
--- a/app/assets/stylesheets/popup.css
+++ b/app/assets/stylesheets/popup.css
@@ -170,6 +170,7 @@
text-align: start;
&:focus-visible {
+ outline: none;
z-index: 1;
}
}
diff --git a/app/controllers/cards/bubble_ups_controller.rb b/app/controllers/cards/bubble_ups_controller.rb
new file mode 100644
index 0000000000..a969603822
--- /dev/null
+++ b/app/controllers/cards/bubble_ups_controller.rb
@@ -0,0 +1,21 @@
+class Cards::BubbleUpsController < ApplicationController
+ include CardScoped
+
+ def create
+ @card.bubble_up_at TimeSlot.for(params[:slot])
+
+ respond_to do |format|
+ format.turbo_stream { render_card_replacement }
+ format.json { head :no_content }
+ end
+ end
+
+ def destroy
+ @card.pop
+
+ respond_to do |format|
+ format.turbo_stream { render_card_replacement }
+ format.json { head :no_content }
+ end
+ end
+end
diff --git a/app/helpers/bubble_up_helper.rb b/app/helpers/bubble_up_helper.rb
new file mode 100644
index 0000000000..0f08281462
--- /dev/null
+++ b/app/helpers/bubble_up_helper.rb
@@ -0,0 +1,14 @@
+module BubbleUpHelper
+ def bubble_up_options_for(card)
+ if card.bubble_up?
+ {
+ isPostponed: card.postponed?,
+ resurfaceAt: card.bubble_up.resurface_at.iso8601
+ }
+ end
+ end
+
+ def slot_too_soon(slot)
+ slot == "latertoday" && Time.current.hour > 16 ? true : false
+ end
+end
diff --git a/app/javascript/controllers/bubble_controller.js b/app/javascript/controllers/bubble_controller.js
index b8b7f65454..965ffb44a9 100644
--- a/app/javascript/controllers/bubble_controller.js
+++ b/app/javascript/controllers/bubble_controller.js
@@ -1,11 +1,11 @@
import { Controller } from "@hotwired/stimulus"
-import { signedDifferenceInDays } from "helpers/date_helpers"
+import { signedDifferenceInDays, differenceInDays } from "helpers/date_helpers"
const REFRESH_INTERVAL = 3_600_000 // 1 hour (in milliseconds)
export default class extends Controller {
static targets = [ "entropy", "stalled", "top", "center", "bottom" ]
- static values = { entropy: Object, stalled: Object }
+ static values = { entropy: Object, stalled: Object, up: Object }
#timer
@@ -23,7 +23,9 @@ export default class extends Controller {
this.#showEntropy()
} else if (this.#isStalled) {
this.#showStalled()
- } else {
+ } else if (this.#isBubbling) {
+ this.#showBubbling()
+ }else {
this.#hide()
}
}
@@ -67,6 +69,28 @@ export default class extends Controller {
})
}
+ get #isBubbling() {
+ if (!this.upValue.resurfaceAt) return false
+ return this.upValue.isPostponed || this.#hasBubbled
+ }
+
+ #showBubbling() {
+ const dayLabel = this.#resurfaceDayCount === 1 ? "day" : "days"
+ this.#render({
+ top: this.#hasBubbled ? "Bubbled Up" : this.#resurfaceDayCount < 1 ? "Bubbles Up" : "Bubbles Up in",
+ center: this.#resurfaceDayCount < 1 ? "!" : this.#resurfaceDayCount,
+ bottom: this.#resurfaceDayCount < 1 ? "Today" : this.#hasBubbled ? `${dayLabel} ago` : dayLabel
+ })
+ }
+
+ get #hasBubbled() {
+ return new Date() > new Date(this.upValue.resurfaceAt)
+ }
+
+ get #resurfaceDayCount() {
+ return differenceInDays(new Date(), new Date(this.upValue.resurfaceAt))
+ }
+
#hide() {
this.element.toggleAttribute("hidden", true)
}
diff --git a/app/models/card.rb b/app/models/card.rb
index d422afa854..ffe1b41e78 100644
--- a/app/models/card.rb
+++ b/app/models/card.rb
@@ -1,5 +1,5 @@
class Card < ApplicationRecord
- include Accessible, Assignable, Attachments, Broadcastable, Closeable, Colored, Commentable,
+ include Accessible, Assignable, Attachments, Broadcastable, BubblesUp, Closeable, Colored, Commentable,
Entropic, Eventable, Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Readable, Searchable, Stallable, Statuses, Storage::Tracked, Taggable, Triageable, Watchable
diff --git a/app/models/card/bubble_up.rb b/app/models/card/bubble_up.rb
new file mode 100644
index 0000000000..7c9c147633
--- /dev/null
+++ b/app/models/card/bubble_up.rb
@@ -0,0 +1,12 @@
+class Card::BubbleUp < ApplicationRecord
+ belongs_to :account, default: -> { card.account }
+ belongs_to :card, touch: true
+
+ scope :due_to_resurface, -> { joins(card: :not_now).where(resurface_at: ..Time.now) }
+
+ def self.resurface_all_due
+ due_to_resurface.find_each do |bubble_up|
+ bubble_up.card.send_back_to_triage(skip_event: true)
+ end
+ end
+end
diff --git a/app/models/card/bubbles_up.rb b/app/models/card/bubbles_up.rb
new file mode 100644
index 0000000000..9659e72787
--- /dev/null
+++ b/app/models/card/bubbles_up.rb
@@ -0,0 +1,29 @@
+module Card::BubblesUp
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :bubble_up, dependent: :destroy, class_name: "Card::BubbleUp"
+ end
+
+ def bubble_up?
+ bubble_up.present?
+ end
+
+ def bubbling?
+ bubble_up? && Time.current.before?(bubble_up.resurface_at)
+ end
+
+ def bubbled?
+ bubble_up? && Time.current.after?(bubble_up.resurface_at)
+ end
+
+ def bubble_up_at(time)
+ postpone unless postponed?
+ bubble_up ||= association(:bubble_up).reader || self.build_bubble_up
+ bubble_up.update resurface_at: time
+ end
+
+ def pop
+ bubble_up&.destroy
+ end
+end
diff --git a/app/models/card/closeable.rb b/app/models/card/closeable.rb
index ff947452bb..dac3aa3055 100644
--- a/app/models/card/closeable.rb
+++ b/app/models/card/closeable.rb
@@ -32,6 +32,7 @@ def close(user: Current.user)
unless closed?
transaction do
not_now&.destroy
+ bubble_up&.destroy
create_closure! user: user
track_event :closed, creator: user
end
diff --git a/app/models/card/postponable.rb b/app/models/card/postponable.rb
index ab58960750..d6ac7794d3 100644
--- a/app/models/card/postponable.rb
+++ b/app/models/card/postponable.rb
@@ -34,6 +34,7 @@ def postpone(user: Current.user, event_name: :postponed)
reopen
activity_spike&.destroy
create_not_now!(user: user) unless postponed?
+ pop if bubble_up?
track_event event_name, creator: user
end
end
diff --git a/app/models/card/triageable.rb b/app/models/card/triageable.rb
index d156639022..ae9c45c5b1 100644
--- a/app/models/card/triageable.rb
+++ b/app/models/card/triageable.rb
@@ -21,6 +21,7 @@ def triage_into(column)
transaction do
resume
+ pop if bubble_up?
update! column: column
track_event "triaged", particulars: { column: column.name }
end
diff --git a/app/models/time_slot.rb b/app/models/time_slot.rb
new file mode 100644
index 0000000000..7e7f55dd34
--- /dev/null
+++ b/app/models/time_slot.rb
@@ -0,0 +1,36 @@
+class TimeSlot
+ attr_reader :slot
+
+ HUMAN_NAMES_BY_VALUE = {
+ "latertoday" => "Later Today",
+ "tomorrow" => "Tomorrow",
+ "thisweekend" => "This Weekend",
+ "nextweek" => "Next Week",
+ "surprise" => "Surprise Me"
+ }
+
+ class << self
+ def for(slot)
+ new.for(slot)
+ end
+
+ def initialize(slot)
+ @slot = slot
+ end
+ end
+
+ def for(slot)
+ case slot
+ when "latertoday"
+ Time.current.change(hour: 18)
+ when "tomorrow"
+ 1.day.from_now.change(hour: 8)
+ when "thisweekend"
+ Date.current.next_occurring(:saturday).in_time_zone.change(hour: 8)
+ when "nextweek"
+ Date.current.next_occurring(:monday).in_time_zone.change(hour: 8)
+ when "surprise"
+ rand(2..14).days.from_now.change(hour: 8)
+ end
+ end
+end
diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb
index b6823bd768..1f26c64ba2 100644
--- a/app/views/cards/_container.html.erb
+++ b/app/views/cards/_container.html.erb
@@ -2,6 +2,7 @@
<% cache card do %>
<%= render "cards/container/gild", card: card if card.published? && !card.closed? %>
+ <%= render "cards/container/bubble_up", card: card if card.published? && !card.closed? %>
<%= render "cards/container/image", card: card %>
@@ -32,7 +33,7 @@
<% end %>
- <% if card.entropic? %>
+ <% if card.entropic? || card.bubble_up? %>
<%= render "cards/display/preview/bubble", card: card %>
<% end %>
diff --git a/app/views/cards/container/_bubble_up.html.erb b/app/views/cards/container/_bubble_up.html.erb
new file mode 100644
index 0000000000..69779bd563
--- /dev/null
+++ b/app/views/cards/container/_bubble_up.html.erb
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb
index fe8b7c6138..dd4ab2c56f 100644
--- a/app/views/cards/display/_preview.html.erb
+++ b/app/views/cards/display/_preview.html.erb
@@ -54,7 +54,7 @@
<%= render "cards/display/common/background", card: card %>
- <% if card.entropic? %>
+ <% if card.entropic? || card.bubble_up? %>
<%= render "cards/display/preview/bubble", card: card %>
<% end %>
<% end %>
diff --git a/app/views/cards/display/_public_preview.html.erb b/app/views/cards/display/_public_preview.html.erb
index 7aa53fed75..fe02a69c53 100644
--- a/app/views/cards/display/_public_preview.html.erb
+++ b/app/views/cards/display/_public_preview.html.erb
@@ -36,7 +36,7 @@
<%= card.title %>
<% end %>
- <% if card.entropic? %>
+ <% if card.entropic? || card.bubble_up? %>
<%= render "cards/display/preview/bubble", card: card %>
<% end %>
diff --git a/app/views/cards/display/preview/_bubble.html.erb b/app/views/cards/display/preview/_bubble.html.erb
index 40d8e4311f..f74664a72a 100644
--- a/app/views/cards/display/preview/_bubble.html.erb
+++ b/app/views/cards/display/preview/_bubble.html.erb
@@ -1,12 +1,13 @@
<%= tag.div \
id: dom_id(card, "bubble"),
hidden: true,
- class: "bubble",
+ class: class_names("bubble", "bubble--golden" => card.bubble_up?),
data: {
controller: "bubble",
action: "turbo:morph-element->bubble#update:self",
bubble_entropy_value: entropy_bubble_options_for(card).to_json,
- bubble_stalled_value: stalled_bubble_options_for(card)&.to_json
+ bubble_stalled_value: stalled_bubble_options_for(card)&.to_json,
+ bubble_up_value: bubble_up_options_for(card)&.to_json
} do %>