Merge branch 'main' into modal-scroll-block

This commit is contained in:
Alexander Zaytsev
2026-01-19 22:19:51 +01:00
73 changed files with 521 additions and 102 deletions
+1 -1
View File
@@ -449,7 +449,7 @@ GEM
timeout (0.6.0)
trilogy (2.9.0)
tsort (0.2.0)
turbo-rails (2.0.20)
turbo-rails (2.0.21)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
+1 -1
View File
@@ -568,7 +568,7 @@ GEM
timeout (0.6.0)
trilogy (2.9.0)
tsort (0.2.0)
turbo-rails (2.0.20)
turbo-rails (2.0.21)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
+18 -13
View File
@@ -281,10 +281,7 @@
/* Overlap the card BG by half the button height */
&:has(.btn) {
&,
~ .card-perma__closure-message {
translate: 0 calc(-1 * var(--half-btn-height));
}
translate: 0 calc(-1 * var(--half-btn-height));
}
form {
@@ -297,12 +294,6 @@
--btn-color: var(--color-ink-inverted);
}
.btn--plain {
--btn-color: var(--card-color);
text-decoration: underline;
}
.btn--reversed {
--btn-background: var(--color-canvas);
--btn-color: var(--card-color);
@@ -330,11 +321,25 @@
.card-perma__closure-message {
color: var(--card-color);
grid-area: closure-message;
margin-block-start: 0.5ch;
margin-block: var(--block-space) var(--block-space-double);
padding-inline: 1ch;
form {
display: inline;
.btn--plain {
--btn-color: var(--card-color);
text-decoration: underline;
}
@media (max-width: 799px) {
margin-block: var(--block-space-half);
translate: 0 calc(-0.5 * var(--half-btn-height));
}
@media (min-width: 800px) {
.card-perma__notch--bottom:has(.btn) ~ & {
margin-block: var(--block-space-half) var(--block-space);
translate: 0 calc(-0.5 * var(--half-btn-height));
}
}
}
}
+40
View File
@@ -6,6 +6,8 @@
--custom-safe-inset-bottom: var(--injected-safe-inset-bottom, env(safe-area-inset-bottom, 0px));
--custom-safe-inset-left: var(--injected-safe-inset-left, env(safe-area-inset-left, 0px));
--footer-height: 0;
-webkit-tap-highlight-color: transparent;
.hide-on-native {
@@ -28,11 +30,27 @@
}
}
/* Card columns
/* ------------------------------------------------------------------------ */
.board-tools.card {
padding-block-start: 0;
}
/* Card perma
/* ------------------------------------------------------------------------ */
.card-perma {
margin-block-start: 0;
&:not(:has(.card-perma__notch-new-card-buttons)) .card-perma__bg {
padding-block: clamp(0.25rem, 2vw, var(--padding-block));
}
}
.card-perma__closure-message {
margin-block: var(--block-space);
translate: unset;
}
/* Search
@@ -43,3 +61,25 @@
}
}
}
[data-bridge-components~=form] {
[data-controller~=bridge--form] {
[data-bridge--form-target~=submit] {
display: none;
}
}
}
[data-bridge-components~=overflow-menu] {
[data-controller~=bridge--overflow-menu] {
[data-bridge--overflow-menu-target~=item] {
display: none;
}
}
}
[data-bridge-components~=buttons] {
[data-bridge--buttons-target~=button] {
display: none;
}
}
+4
View File
@@ -53,6 +53,10 @@
.popup__title {
font-weight: 800;
white-space: nowrap;
&[tabindex="-1"]:focus-visible {
outline: unset;
}
}
/* Hide lists when all the items within are hidden */
+2 -1
View File
@@ -48,7 +48,8 @@ module AccessesHelper
params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement), icon_only: icon_only },
aria: { labelledby: dom_id(board, :involvement_label) },
title: (label_text if icon_only),
class: class_names("btn", { "btn--reversed": access.watching? && icon_only })) do
class: class_names("btn", { "btn--reversed": access.watching? && icon_only }),
data: !icon_only && { bridge__overflow_menu_target: "item", bridge_title: label_text }) do
icon_tag("notification-bell-#{icon_only ? 'reverse-' : nil}#{access.involvement.dasherize}") +
tag.span(label_text, class: class_names("txt-nowrap txt-uppercase", "for-screen-reader": icon_only), id: dom_id(board, :involvement_label))
end
+2 -1
View File
@@ -4,7 +4,8 @@ module BoardsHelper
end
def link_to_edit_board(board)
link_to edit_board_path(board), class: "btn", data: { controller: "tooltip" } do
link_to edit_board_path(board), class: "btn",
data: { controller: "tooltip", bridge__overflow_menu_target: "item", bridge_title: "Board settings" } do
icon_tag("settings") + tag.span("Settings for #{board.name}", class: "for-screen-reader")
end
end
+5
View File
@@ -0,0 +1,5 @@
module BridgeHelper
def bridge_icon(name)
asset_url("#{name}.svg")
end
end
+4
View File
@@ -40,6 +40,10 @@ module FiltersHelper
}, &block
end
def filter_title(title)
tag.strong title, class: "popup__title pad-inline-half", tabindex: "-1", data: { dialog_target: "focusTouch" }
end
def collapsible_nav_section(title, **properties, &block)
tag.details class: "nav__section popup__section", data: { action: "toggle->nav-section-expander#toggle", nav_section_expander_target: "section", nav_section_expander_key_value: title.parameterize }, open: true, **properties do
concat(tag.summary(class: "popup__section-title") do
+19
View File
@@ -9,4 +9,23 @@ module FormsHelper
form_with(**attributes, data: data) { }
end
end
def bridged_form_with(**attributes, &)
data = attributes.delete(:data) || {}
controllers = [ data[:controller], "bridge--form" ].compact.join(" ").strip
actions = [
data[:action],
"turbo:submit-start->bridge--form#submitStart",
"turbo:submit-end->bridge--form#submitEnd"
].compact.join(" ").strip
data[:controller] = controllers
data[:action] = actions
if block_given?
form_with **attributes, data: data, &
else
form_with(**attributes, data: data) { }
end
end
end
+1 -1
View File
@@ -25,7 +25,7 @@ module WebhooksHelper
def link_to_webhooks(board, &)
link_to board_webhooks_path(board_id: board),
class: [ "btn", { "btn--reversed": board.webhooks.any? } ],
data: { controller: "tooltip" } do
data: { controller: "tooltip", bridge__overflow_menu_target: "item", bridge_title: "Webhooks" } do
icon_tag("world") + tag.span("Webhooks", class: "for-screen-reader")
end
end
+1 -1
View File
@@ -1,8 +1,8 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "@hotwired/hotwire-native-bridge"
import "initializers"
import "controllers"
import "lexxy"
import "@rails/actiontext"
@@ -0,0 +1,43 @@
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
import { BridgeElement } from "@hotwired/hotwire-native-bridge"
export default class extends BridgeComponent {
static component = "buttons"
static targets = [ "button" ]
buttonTargetConnected() {
this.notifyBridgeOfConnect()
}
buttonTargetDisconnected() {
if (!this.#isControllerTearingDown()) {
this.notifyBridgeOfConnect()
}
}
notifyBridgeOfConnect() {
const buttons = this.#enabledButtonTargets
.map((target, index) => {
const element = new BridgeElement(target)
return { ...element.getButton(), index }
})
this.send("connect", { buttons }, message => {
this.#clickButton(message)
})
}
#clickButton(message) {
const selectedIndex = message.data.selectedIndex
this.#enabledButtonTargets[selectedIndex].click()
}
get #enabledButtonTargets() {
return this.buttonTargets
.filter(target => !target.closest("[data-bridge-disabled]"))
}
#isControllerTearingDown() {
return !document.body.contains(this.element)
}
}
@@ -0,0 +1,62 @@
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
import { BridgeElement } from "@hotwired/hotwire-native-bridge"
export default class extends BridgeComponent {
static component = "form"
static targets = [ "submit", "cancel" ]
static values = { submitTitle: String }
submitTargetConnected() {
this.notifyBridgeOfConnect()
this.#observeSubmitTarget()
}
submitTargetDisconnected() {
this.notifyBridgeOfDisonnect()
this.submitObserver?.disconnect()
}
notifyBridgeOfConnect() {
const submitElement = new BridgeElement(this.submitTarget)
const cancelElement = this.hasCancelTarget ? new BridgeElement(this.cancelTarget) : null
const submitButton = { title: submitElement.title }
const cancelButton = cancelElement ? { title: cancelElement.title } : null
this.send("connect", { submitButton, cancelButton }, message => this.receive(message))
}
receive(message) {
switch (message.event) {
case "submit":
this.submitTarget.click()
break
case "cancel":
this.cancelTarget.click()
break
}
}
notifyBridgeOfDisonnect() {
this.send("disconnect")
}
submitStart() {
this.send("submitStart")
}
submitEnd() {
this.send("submitEnd")
}
#observeSubmitTarget() {
this.submitObserver = new MutationObserver(() => {
this.send(this.submitTarget.disabled ? "submitDisabled" : "submitEnabled")
})
this.submitObserver.observe(this.submitTarget, {
attributes: true,
attributeFilter: [ "disabled" ]
})
}
}
@@ -0,0 +1,31 @@
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
// Bridge component to control custom safe-area insets from native apps.
// Sets CSS variables --injected-safe-inset-(top|right|bottom|left).
export default class extends BridgeComponent {
static component = "insets"
connect() {
super.connect()
this.notifyBridgeOfConnect()
}
disconnect() {
super.disconnect()
this.send("disconnect")
}
notifyBridgeOfConnect() {
this.send("connect", {}, message => {
this.#setInsets(message.data)
})
}
#setInsets({ top, right, bottom, left }) {
const root = document.documentElement.style
root.setProperty("--injected-safe-inset-top", `${top}px`)
root.setProperty("--injected-safe-inset-right", `${right}px`)
root.setProperty("--injected-safe-inset-bottom", `${bottom}px`)
root.setProperty("--injected-safe-inset-left", `${left}px`)
}
}
@@ -0,0 +1,43 @@
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
import { BridgeElement } from "@hotwired/hotwire-native-bridge"
export default class extends BridgeComponent {
static component = "overflow-menu"
static targets = [ "item" ]
itemTargetConnected() {
this.notifyBridgeOfConnect()
}
itemTargetDisconnected() {
if (!this.#isControllerTearingDown) {
this.notifyBridgeOfConnect()
}
}
notifyBridgeOfConnect() {
const items = this.#enabledItemTargets
.map((target, index) => {
const element = new BridgeElement(target)
return { title: element.title, index }
})
this.send("connect", { items }, message => {
this.#clickItem(message)
})
}
#clickItem(message) {
const selectedIndex = message.data.selectedIndex
this.#enabledItemTargets[selectedIndex].click()
}
get #enabledItemTargets() {
return this.itemTargets
.filter(target => !target.closest("[data-bridge-disabled]"))
}
#isControllerTearingDown() {
return !document.body.contains(this.element)
}
}
@@ -0,0 +1,25 @@
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
export default class extends BridgeComponent {
static component = "text-size"
connect() {
super.connect()
this.notifyBridgeOfConnect()
}
disconnect() {
super.disconnect()
this.send("disconnect")
}
notifyBridgeOfConnect() {
this.send("connect", {}, message => {
this.#setTextSize(message.data)
})
}
#setTextSize(data) {
document.documentElement.dataset.textSize = data.textSize
}
}
@@ -0,0 +1,63 @@
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
import { viewport } from "helpers/bridge/viewport_helpers"
import { nextFrame } from "helpers/timing_helpers"
export default class extends BridgeComponent {
static component = "title"
static targets = [ "header" ]
static values = { title: String }
async connect() {
super.connect()
await nextFrame()
this.#startObserver()
window.addEventListener("resize", this.#windowResized)
}
disconnect() {
super.disconnect()
this.#stopObserver()
window.removeEventListener("resize", this.#windowResized)
}
notifyBridgeOfVisibilityChange(visible) {
this.send("visibility", { title: this.#title, elementVisible: visible })
}
// Intersection Observer
#startObserver() {
if (!this.hasHeaderTarget) return
this.observer = new IntersectionObserver(([ entry ]) =>
this.notifyBridgeOfVisibilityChange(entry.isIntersecting),
{ rootMargin: `-${this.#topOffset}px 0px 0px 0px` }
)
this.observer.observe(this.headerTarget)
this.previousTopOffset = this.#topOffset
}
#stopObserver() {
this.observer?.disconnect()
}
#updateObserverIfNeeded() {
if (this.#topOffset === this.previousTopOffset) return
this.#stopObserver()
this.#startObserver()
}
#windowResized = () => {
this.#updateObserverIfNeeded()
}
get #title() {
return this.titleValue ? this.titleValue : document.title
}
get #topOffset() {
return viewport.top
}
}
@@ -1,8 +1,9 @@
import { Controller } from "@hotwired/stimulus"
import { orient } from "helpers/orientation_helpers"
import { isTouchDevice } from "helpers/platform_helpers"
export default class extends Controller {
static targets = [ "dialog" ]
static targets = [ "dialog", "focusMouse", "focusTouch" ]
static values = {
modal: { type: Boolean, default: false },
sizing: { type: Boolean, default: true },
@@ -14,6 +15,10 @@ export default class extends Controller {
if (this.autoOpenValue) this.open()
}
focusTouchTargetConnected() {
this.#setupFocus()
}
open() {
const modal = this.modalValue
@@ -63,4 +68,10 @@ export default class extends Controller {
captureKey(event) {
if (event.key !== "Escape") { event.stopPropagation() }
}
#setupFocus() {
const touch = isTouchDevice()
if (this.hasFocusMouseTarget) this.focusMouseTarget.autofocus = !touch
if (this.hasFocusTouchTarget) this.focusTouchTarget.autofocus = touch
}
}
@@ -0,0 +1,24 @@
let top = 0
const viewportTarget = window.visualViewport || window
export const viewport = {
get top() {
return top
},
get height() {
return viewportTarget.height || window.innerHeight
}
}
function update() {
requestAnimationFrame(() => {
const styles = getComputedStyle(document.documentElement)
const customInset = styles.getPropertyValue("--custom-safe-inset-top")
const fallbackInset = styles.getPropertyValue("--safe-area-inset-top")
const insetValue = (customInset || fallbackInset).trim()
top = parseInt(insetValue || "0", 10) || 0
})
}
viewportTarget.addEventListener("resize", update)
update()
@@ -0,0 +1,18 @@
import { BridgeElement } from "@hotwired/hotwire-native-bridge"
BridgeElement.prototype.getButton = function() {
return {
title: this.title,
icon: this.getIcon()
}
}
BridgeElement.prototype.getIcon = function() {
const url = this.bridgeAttribute(`icon-url`)
if (url) {
return { url }
}
return null
}
+1
View File
@@ -1 +1,2 @@
import "initializers/current"
import "initializers/bridge/bridge_element"
+1 -1
View File
@@ -1,7 +1,7 @@
<% @page_title = "Account Settings" %>
<% content_for :header do %>
<h1 class="header__title">
<h1 class="header__title" data-bridge--title-target="header">
<%= @page_title %>
<% unless Current.user.admin? %>
<div class="txt-normal font-weight-normal">Only admins can change these settings</div>
@@ -5,7 +5,7 @@
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
@@ -5,7 +5,7 @@
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= link_back_to_board(@column.board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
@@ -5,7 +5,7 @@
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title">
<h1 class="header__title" data-bridge--title-target="header">
<div><%= @page_title %></div>
<% unless Current.user.can_administer_board?(@board) %>
<div class="txt-normal font-weight-normal">Only admins can change these settings</div>
+2 -2
View File
@@ -1,11 +1,11 @@
<% @page_title = "Create a new board" %>
<div class="panel panel--centered">
<%= form_with model: @board, class: "flex flex-column gap", data: { controller: "form", action: "submit->form#preventEmptySubmit" } do |form| %>
<%= bridged_form_with model: @board, class: "flex flex-column gap", data: { controller: "form", action: "submit->form#preventEmptySubmit" } do |form| %>
<h1 class="txt-x-large margin-none font-weight-black"><%= @page_title %></h1>
<%= form.text_field :name, required: true, class: "input full-width", autofocus: true, autocomplete: "off", placeholder: "Name it…", data: { form_target: "input", action: "keydown.esc@document->form#cancel", validation_message: "Board names cant be blank" } %>
<button type="submit" class="btn btn--link center">
<button type="submit" class="btn btn--link center" data-bridge--form-target="submit">
<span>Create board</span>
<%= icon_tag "arrow-right" %>
</button>
+3 -3
View File
@@ -5,15 +5,15 @@
<%= turbo_stream_from @board %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<div class="header__actions header__actions--start hide-on-native">
<%= link_to_webhooks(@board) if Current.user.admin? %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @board.name %></span>
</h1>
<div class="header__actions header__actions--end">
<div class="header__actions header__actions--end hide-on-native">
<%= link_to_edit_board @board %>
</div>
<% end %>
+3 -1
View File
@@ -7,7 +7,9 @@
<%= render "boards/show/expander", title: "Maybe?", count: board.cards.awaiting_triage.count, column_id: "maybe" %>
<%= render "boards/show/menu/maximize", column_path: board_columns_stream_path(board) %>
</header>
<%= render "columns/show/add_card_button", board: board %>
<div data-bridge-disabled="true">
<%= render "columns/show/add_card_button", board: board %>
</div>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if page.used? %>
<%= with_automatic_pagination "maybe", @page do %>
+1 -1
View File
@@ -1,4 +1,4 @@
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;" data-controller="dialog-manager">
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;" data-controller="dialog-manager bridge--form">
<% cache card do %>
<div class="card-perma__actions card-perma__actions--left">
<%= render "cards/container/gild", card: card if card.published? && !card.closed? %>
+2 -2
View File
@@ -5,10 +5,10 @@
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title pad-inline-half">Move this card to…</strong>
<%= filter_title "Move this card to…" %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half font-weight-normal", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<ul class="popup__list margin-block-start-half margin-none-block-end" data-filter-target="list">
<% @boards.each do |board| %>
+6 -10
View File
@@ -1,17 +1,13 @@
<div class="display-contents" id="<%= dom_id(card, :card_closure_toggle) %>">
<% if card.closed? %>
<div class="card-perma__notch card-perma__notch--bottom flex-column">
<div class="flex gap-half margin-block-start-double margin-block-end">
<span class="card-perma__closure-message">
Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>.
<%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %>
<span class="pad-inline-end">Undo</span>
<% end %>
</span>
</div>
<div class="card-perma__closure-message flex gap-half justify-center">
<span>Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>.</span>
<%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %>
<span class="pad-inline-end">Undo</span>
<% end %>
</div>
<% else %>
<div class="card-perma__notch card-perma__notch--bottom flex-column">
<div class="card-perma__notch card-perma__notch--bottom">
<%= render "cards/container/closure_buttons", card: card %>
</div>
<% if card.entropic? && card.open? && !card.postponed? %>
@@ -1,12 +1,13 @@
<div class="flex gap-half">
<%= link_to edit_card_path(card), class: "btn btn--circle-mobile borderless", data: { controller: "hotkey", action: "keydown.e@document->hotkey#click", turbo_frame: dom_id(card, :edit) } do %>
<%= link_to edit_card_path(card), class: "btn btn--circle-mobile borderless",
data: { controller: "hotkey", action: "keydown.e@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Edit", bridge_icon_url: bridge_icon("pencil"), turbo_frame: dom_id(card, :edit) } do %>
<%= icon_tag "pencil", class: "icon--mobile-only" %>
<span>Edit</span>
<kbd class="txt-x-small hide-on-touch">e</kbd>
<% end %>
<%= button_to card_closure_path(card), class: "btn btn--circle-mobile borderless",
data: { controller: "hotkey", form_target: "submit", action: "keydown.d@document->hotkey#click" },
data: { controller: "hotkey", form_target: "submit", bridge__buttons_target: "button", bridge_title: "Mark as Done", bridge_icon_url: bridge_icon("check"), action: "keydown.d@document->hotkey#click" },
form: { data: { controller: "form" } } do %>
<%= icon_tag "check", class: "icon--mobile-only" %>
<span class="overflow-ellipsis">Mark as Done</span>
+4 -2
View File
@@ -1,10 +1,12 @@
<% if card.golden? %>
<%= button_to card_goldness_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip hotkey", action: "keydown.shift+g@document->hotkey#click" } do %>
<%= button_to card_goldness_path(card), method: :delete, class: "btn btn--reversed",
data: { controller: "tooltip hotkey", action: "keydown.shift+g@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Demote to normal" } do %>
<%= icon_tag "golden-ticket" %>
<span class="for-screen-reader">Demote to normal (shift+g)</span>
<% end %>
<% else %>
<%= button_to card_goldness_path(card), class: "btn", data: { controller: "tooltip hotkey", action: "keydown.shift+g@document->hotkey#click" } do %>
<%= button_to card_goldness_path(card), class: "btn",
data: { controller: "tooltip hotkey", action: "keydown.shift+g@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Promote to Golden Ticket" } do %>
<%= icon_tag "golden-ticket" %>
<span class="for-screen-reader">Promote to Golden Ticket (shift+g)</span>
<% end %>
+3 -2
View File
@@ -1,11 +1,12 @@
<% if card.image.attached? %>
<%= button_to card_image_path(card), method: :delete, class: "btn", data: { controller: "tooltip" } do %>
<%= button_to card_image_path(card), method: :delete, class: "btn",
data: { controller: "tooltip", bridge__overflow_menu_target: "item", bridge_title: "Remove background image" } do %>
<%= icon_tag "picture-remove" %>
<span class="for-screen-reader">Remove background image</span>
<% end %>
<% elsif !card.closed? %>
<%= form_with model: card, data: { controller: "form" } do |form| %>
<label class="card-perma__image-btn btn input--file" data-controller="tooltip">
<label class="card-perma__image-btn btn input--file" data-controller="tooltip" data-bridge--overflow-menu-target="item" data-bridge-title="Add background image">
<%= icon_tag "picture-add" %>
<span class="for-screen-reader">Add background image</span>
@@ -1,7 +1,7 @@
<div class="card-perma__notch card-perma__notch--bottom flex-column">
<%= button_tag type: :submit, form: dom_id(card, :edit_form), class: "btn borderless",
title: "Save changes (#{ hotkey_label(["ctrl", "enter"]) })",
data: { controller: "hotkey", action: "keydown.ctrl+enter@document->hotkey#click keydown.meta+enter@document->hotkey#click" } do %>
data: { controller: "hotkey", bridge__form_target: "submit", action: "keydown.ctrl+enter@document->hotkey#click keydown.meta+enter@document->hotkey#click" } do %>
<span>Save changes</span>
<% end %>
</div>
@@ -2,8 +2,8 @@
<div class="card-perma__notch-new-card-buttons">
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
form: { data: { controller: "form" } },
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
form: { data: { controller: "form bridge--form" } },
data: { form_target: "submit", bridge__form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
<span>Create card</span>
<% end %>
+2 -1
View File
@@ -22,6 +22,7 @@
<%= general_prompts(@card.board) %>
<% end %>
<%= link_to "Close editor and discard changes", @card, data: { form_target: "cancel" }, hidden: true %>
<%= link_to "Close editor and discard changes", @card,
data: { form_target: "cancel", bridge__form_target: "cancel", bridge_title: "Cancel" }, hidden: true %>
<% end %>
<% end %>
+1 -1
View File
@@ -4,7 +4,7 @@
<%= render "cards/broadcasts", filter: @filter %>
<% content_for :header do %>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis txt-capitalize-first-letter"><%= @user_filtering.selected_boards_label %></span>
</h1>
+4 -2
View File
@@ -1,10 +1,12 @@
<div id="<%= dom_id(card, :pin_button) %>">
<% if card.pinned_by? Current.user %>
<%= button_to card_pin_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click" } do %>
<%= button_to card_pin_path(card), method: :delete, class: "btn btn--reversed",
data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Un-pin this card" } do %>
<%= icon_tag "pinned" %> <span class="for-screen-reader">Un-pin this card (shift+p)</span>
<% end %>
<% else %>
<%= button_to card_pin_path(card), class: "btn", data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click" } do %>
<%= button_to card_pin_path(card), class: "btn",
data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Pin this card" } do %>
<%= icon_tag "unpinned" %> <span class="for-screen-reader">Pin this card (shift+p)</span>
<% end %>
<% end %>
@@ -1,11 +1,13 @@
<div id="<%= dom_id(card, :watch_button) %>">
<% if card.watched_by? Current.user %>
<%= button_to card_watch_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip hotkey", action: "keydown.shift+n@document->hotkey#click" } do %>
<%= button_to card_watch_path(card), method: :delete, class: "btn btn--reversed",
data: { controller: "tooltip hotkey", action: "keydown.shift+n@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Stop watching" } do %>
<%= icon_tag "bell" %>
<span class="for-screen-reader">Stop watching (shift+n)</span>
<% end %>
<% else %>
<%= button_to card_watch_path(card), class: "btn", data: { controller: "tooltip hotkey", action: "keydown.shift+n@document->hotkey#click" } do %>
<%= button_to card_watch_path(card), class: "btn",
data: { controller: "tooltip hotkey", action: "keydown.shift+n@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Watch this" } do %>
<%= icon_tag "bell-off" %>
<span class="for-screen-reader">Watch this (shift+n)</span>
<% end %>
@@ -1,5 +1,7 @@
<div class="board-tools card">
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link", form: { data: { turbo_frame: "_top" } }, data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %>
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link",
form: { data: { turbo_frame: "_top" } },
data: { controller: "hotkey", action: "keydown.c@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add card", bridge_icon_url: bridge_icon("add") } do %>
<%= icon_tag "add", class: "show-on-touch" %>
<span>Add a card</span>
<kbd class="hide-on-touch">C</kbd>
@@ -5,7 +5,7 @@
<%= back_link_to "Activity", root_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title">
<h1 class="header__title" data-bridge--title-target="header">
<%= @column.title %>
</h1>
<% end %>
+1 -1
View File
@@ -6,7 +6,7 @@
<% content_for :header do %>
<%= render "events/index/add_card_button", user_filtering: @user_filtering %>
<h1 class="header__title" data-controller="dialog-manager">
<h1 class="header__title" data-controller="dialog-manager" data-bridge--title-target="header">
<% if @user_filtering.boards.many? %>
<span>Activity <%= @user_filtering.filter.boards.any? ? "in" : "across" %></span>
<% else %>
@@ -1,6 +1,7 @@
<div class="header__actions header__actions--end">
<div>
<%= link_to new_board_path, class: "btn btn--link btn--circle-mobile", data: { controller: "hotkey", action: "keydown.b@document->hotkey#click" } do %>
<%= link_to new_board_path, class: "btn btn--link btn--circle-mobile",
data: { controller: "hotkey", action: "keydown.b@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add a board", bridge_icon_url: bridge_icon("board") } do %>
<%= icon_tag "board" %>
<span class="overflow-ellipsis">Add a board</span>
<kbd class="hide-on-touch">B</kbd>
@@ -1,6 +1,7 @@
<div class="header__actions header__actions--start">
<% if board = user_filtering.single_board_or_first %>
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link btn--circle-mobile", data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %>
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link btn--circle-mobile",
data: { controller: "hotkey", action: "keydown.c@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add a card", bridge_icon_url: bridge_icon("add") } do %>
<%= icon_tag "add" %>
<span class="overflow-ellipsis">Add a card</span>
<kbd class="hide-on-touch">C</kbd>
@@ -16,9 +16,9 @@
</template>
<%= filter_dialog "Board…" do %>
<strong class="popup__title">Board…</strong>
<%= filter_title "Board…" %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<ul class="popup__list" data-filter-target="list" role="listbox">
<% user_filtering.boards.each do |board| %>
+2 -2
View File
@@ -16,9 +16,9 @@
</template>
<%= filter_dialog "Person…" do %>
<strong class="popup__title">Person…</strong>
<%= filter_title "Person…" %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<ul class="popup__list" data-filter-target="list" role="listbox">
<% user_filtering.users.each do |user| %>
@@ -16,11 +16,10 @@
</template>
<%= filter_dialog "Assigned to…" do %>
<strong class="popup__title">Assigned to…</strong>
<%= filter_title "Assigned to…" %>
<% if Current.account.users.active.many? %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<% end %>
<ul class="popup__list" data-filter-target="list" role="listbox">
+2 -3
View File
@@ -17,11 +17,10 @@
</template>
<%= filter_dialog "Board…" do %>
<strong class="popup__title">Board…</strong>
<%= filter_title "Board…" %>
<% if user_filtering.boards.many? %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<% end %>
<ul class="popup__list" data-filter-target="list" role="listbox">
+2 -3
View File
@@ -17,11 +17,10 @@
</template>
<%= filter_dialog "Closed by…" do %>
<strong class="popup__title">Closed by…</strong>
<%= filter_title "Closed by…" %>
<% if user_filtering.users.many? %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<% end %>
<ul class="popup__list" data-filter-target="list" role="listbox">
@@ -17,11 +17,10 @@
</template>
<%= filter_dialog "Added by…" do %>
<strong class="popup__title">Added by…</strong>
<%= filter_title "Added by…" %>
<% if user_filtering.users.many? %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<% end %>
<ul class="popup__list" data-filter-target="list" role="listbox">
+2 -3
View File
@@ -15,11 +15,10 @@
</template>
<%= filter_dialog "Tagged…" do %>
<strong class="popup__title">Tagged…</strong>
<%= filter_title "Tagged…" %>
<% if user_filtering.tags.many? %>
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", dialog_target: "focusMouse", action: "input->filter#filter" } %>
<% end %>
<ul class="popup__list" data-filter-target="list" role="listbox">
+5 -2
View File
@@ -2,8 +2,11 @@
<html lang="en">
<%= render "layouts/shared/head" %>
<body class="<%= @body_class %>" data-controller="local-time timezone-cookie turbo-navigation theme" data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation" data-platform="<%= platform.type %>">
<div id="global-container">
<body class="<%= @body_class %>"
data-controller="local-time timezone-cookie turbo-navigation theme bridge--title bridge--text-size bridge--insets"
data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation"
data-platform="<%= platform.type %>" data-bridge--title-title-value="<%= @page_title %>">
<div id="global-container" data-controller="bridge--buttons bridge--overflow-menu">
<header class="header header--mobile-actions-stack <%= @header_class %>" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<%= render "my/menu" if Current.user %>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= back_link_to "My profile", user_path(Current.user), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<section class="panel panel--wide shadow center webhooks">
+1 -1
View File
@@ -5,7 +5,7 @@
<%= back_link_to "tokens", my_access_tokens_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<article class="panel panel--wide shadow center txt-align-start" style="view-transition-name: <%= dom_id(@access_token) %>">
+1 -1
View File
@@ -5,7 +5,7 @@
<%= back_link_to "Tokens", my_access_tokens_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<article class="panel panel--wide shadow center txt-align-start" style="view-transition-name: <%= dom_id(@access_token) %>">
+1 -1
View File
@@ -5,7 +5,7 @@
<%= back_link_to "Home", root_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<div class="pins-list panel panel--wide flex center borderless unpad flex-column gap-half margin-block-start">
+2 -2
View File
@@ -6,10 +6,10 @@
<%= back_link_to "Home", root_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<div class="header__actions header__actions--end">
<%= link_to notifications_settings_path, class: "btn" do %>
<%= link_to notifications_settings_path, class: "btn", data: { bridge__overflow_menu_target: "item", bridge_title: "Notification settings" } do %>
<%= icon_tag "settings" %> <span class="for-screen-reader">Notification settings</span>
<% end %>
</div>
@@ -1,7 +1,7 @@
<% @page_title = "Notification Settings" %>
<% content_for :header do %>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<section class="settings margin-block-start-half">
@@ -5,7 +5,7 @@
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
@@ -5,7 +5,7 @@
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
@@ -5,7 +5,7 @@
<%= back_link_to @column.board.name, published_board_url(@column.board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
@@ -5,7 +5,7 @@
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
+1 -1
View File
@@ -14,7 +14,7 @@
<% end %>
<% content_for :header do %>
<h1 class="header__title divider divider--fade full-width">
<h1 class="header__title divider divider--fade full-width" data-bridge--title-target="header">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= back_link_to "Home", root_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<div class="search-perma margin-block-start">
+1 -1
View File
@@ -5,7 +5,7 @@
<%= link_back_to_board(@board) %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<%= tag.section class: "panel panel--wide shadow center webhooks" do %>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= back_link_to "Webhooks", board_webhooks_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<h1 class="header__title" data-bridge--title-target="header"><%= @page_title %></h1>
<% end %>
<article class="panel panel--wide center txt-align-start" style="view-transition-name: <%= dom_id(@webhook) %>">
+4
View File
@@ -4,11 +4,15 @@ pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin "@hotwired/hotwire-native-bridge", to: "@hotwired--hotwire-native-bridge.js"
pin "@rails/request.js", to: "@rails--request.js" # @0.0.13
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/helpers", under: "helpers"
pin_all_from "app/javascript/initializers", under: "initializers"
pin_all_from "app/javascript/bridge/initializers", under: "bridge/initializers"
pin_all_from "app/javascript/bridge/helpers", under: "bridge/helpers"
pin_all_from "app/javascript/bridge/controllers/bridge", under: "controllers/bridge", to: "bridge/controllers/bridge"
pin "marked" # @15.0.11
pin "lexxy"
pin "@rails/activestorage", to: "activestorage.esm.js"
+3 -1
View File
@@ -15,7 +15,9 @@ class NotificationTest < ActiveSupport::TestCase
notification.read # Mark as read first
assert_turbo_stream_broadcasts([ notification.user, :notifications ], count: 1) do
notification.unread
perform_enqueued_jobs do
notification.unread
end
end
end
+4
View File
@@ -0,0 +1,4 @@
// @hotwired/hotwire-native-bridge@1.2.2 downloaded from https://ga.jspm.io/npm:@hotwired/hotwire-native-bridge@1.2.2/dist/hotwire-native-bridge.js
import{Controller as e}from"@hotwired/stimulus";var t=class{#e;#t;#s;#n;constructor(){this.#e=null;this.#t=0;this.#s=[];this.#n=new Map}start(){this.notifyApplicationAfterStart()}notifyApplicationAfterStart(){document.dispatchEvent(new Event("web-bridge:ready"))}supportsComponent(e){return!!this.#e&&this.#e.supportsComponent(e)}send({component:e,event:t,data:s,callback:n}){if(!this.#e){this.#i({component:e,event:t,data:s,callback:n});return null}if(!this.supportsComponent(e))return null;const i=this.generateMessageId();const r={id:i,component:e,event:t,data:s||{}};this.#e.receive(r);n&&this.#n.set(i,n);return i}receive(e){this.executeCallbackFor(e)}executeCallbackFor(e){const t=this.#n.get(e.id);t&&t(e)}removeCallbackFor(e){this.#n.has(e)&&this.#n.delete(e)}removePendingMessagesFor(e){this.#s=this.#s.filter((t=>t.component!=e))}generateMessageId(){const e=++this.#t;return e.toString()}setAdapter(e){this.#e=e;document.documentElement.dataset.bridgePlatform=this.#e.platform;this.adapterDidUpdateSupportedComponents();this.#r()}adapterDidUpdateSupportedComponents(){this.#e&&(document.documentElement.dataset.bridgeComponents=this.#e.supportedComponents.join(" "))}#i(e){this.#s.push(e)}#r(){this.#s.forEach((e=>this.send(e)));this.#s=[]}};var s=class{constructor(e){this.element=e}get title(){return(this.bridgeAttribute("title")||this.attribute("aria-label")||this.element.textContent||this.element.value).trim()}get enabled(){return!this.disabled}get disabled(){const e=this.bridgeAttribute("disabled");return e==="true"||e===this.platform}enableForComponent(e){e.enabled&&this.removeBridgeAttribute("disabled")}hasClass(e){return this.element.classList.contains(e)}attribute(e){return this.element.getAttribute(e)}bridgeAttribute(e){return this.attribute(`data-bridge-${e}`)}setBridgeAttribute(e,t){this.element.setAttribute(`data-bridge-${e}`,t)}removeBridgeAttribute(e){this.element.removeAttribute(`data-bridge-${e}`)}click(){this.platform=="android"&&this.element.removeAttribute("target");this.element.click()}get platform(){return document.documentElement.dataset.bridgePlatform}};var{userAgent:n}=window.navigator;function i(e){const t=n.match(/bridge-components: \[(.*?)\]/);return!!t&&t[1].split(" ").includes(e)}var r=class extends e{static component="";static get shouldLoad(){return i(this.component)}pendingMessageCallbacks=[];initialize(){this.pendingMessageCallbacks=[]}connect(){this.removeRestoreEventListener();this.addRestoreEventListener()}disconnect(){this.removePendingCallbacks();this.removePendingMessages();this.removeRestoreEventListener()}addRestoreEventListener(){this.restore=this.restore.bind(this);document.addEventListener("native:restore",this.restore)}removeRestoreEventListener(){document.removeEventListener("native:restore",this.restore)}restore(){this.connect()}get component(){return this.constructor.component}get platformOptingOut(){const{bridgePlatform:e}=document.documentElement.dataset;return this.identifier==this.element.getAttribute(`data-controller-optout-${e}`)}get enabled(){return!this.platformOptingOut&&this.bridge.supportsComponent(this.component)}send(e,t={},s){t.metadata={url:window.location.href};const n={component:this.component,event:e,data:t,callback:s};const i=this.bridge.send(n);s&&this.pendingMessageCallbacks.push(i)}removePendingCallbacks(){this.pendingMessageCallbacks.forEach((e=>this.bridge.removeCallbackFor(e)))}removePendingMessages(){this.bridge.removePendingMessagesFor(this.component)}get bridgeElement(){return new s(this.element)}get bridge(){return window.HotwireNative.web}};if(!window.HotwireNative){const e=new t;window.HotwireNative={web:e};a(e);e.start()}function a(e){window.Strada||(window.Strada={web:e});window.webBridge||(window.webBridge=e)}export{r as BridgeComponent,s as BridgeElement};