Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
b84da94
Add Hotwire Native Bridge wiring.
svara Jan 6, 2026
177afe1
Add bridge page controller and helpers.
svara Jan 6, 2026
ecf0921
Add bridge page targets.
svara Jan 6, 2026
37f0a2c
Add hotwire-native-bridge vendor js.
svara Jan 6, 2026
2d7b76f
Fix bridge viewport safe-area insets.
svara Jan 6, 2026
367a7ab
Use raw page_title in bridge page title attribute
svara Jan 6, 2026
69c17a7
Add bridge insets controller.
svara Jan 7, 2026
cdc4f2c
Wire bridge insets controller into the layout.
svara Jan 7, 2026
367edfc
Add bridge nav-button controller.
svara Jan 7, 2026
c61942d
Add bridge form controller.
svara Jan 7, 2026
4cbc2c6
Hide bridged submit button when native form component is active.
svara Jan 7, 2026
58664f8
Add `bridged_form_with` helper for bridge form controller.
svara Jan 7, 2026
de10717
Use `bridged_form_with` for board creation form.
svara Jan 7, 2026
ce0538d
Move bridge insets controller to bridge/controllers.
svara Jan 7, 2026
75431e0
Hide bridged nav-button elements when native nav component is active.
svara Jan 7, 2026
dc9c544
Use bridge nav-button for board settings action.
svara Jan 7, 2026
76eaf32
Add bridge buttons controller.
svara Jan 7, 2026
9331c37
Remove bridge nav-button controller.
svara Jan 7, 2026
e2f9989
Remove form related events from bridge page controller.
svara Jan 8, 2026
b8bb4cf
Remove option to toggle header visibility from mobile apps in bridge …
svara Jan 8, 2026
37f03fc
Use bridge buttons controller in boards.
svara Jan 8, 2026
0a57e39
Remove redundant url from bridge page change payload.
svara Jan 8, 2026
d38c0c9
Add bridge overflow menu controller.
svara Jan 8, 2026
1a46ed3
Hide bridged overflow-menu elements when native is active.
svara Jan 8, 2026
674509b
Update overflow menu controller to send an array of menu items.
svara Jan 8, 2026
eae17b8
Merge branch 'main' into mobile/bridge-components
Jan 8, 2026
3a43941
Move bridge javascript into existing folder structure
adjogima Jan 8, 2026
45f24d9
Move bridge buttons controller to <body>
adjogima Jan 8, 2026
1028eab
Remove useless shouldLoad
adjogima Jan 8, 2026
0d21efc
Use hidden overflow menu container for board bridge items.
svara Jan 8, 2026
3746774
Remove bridge button metadata from board settings link.
svara Jan 8, 2026
228f65d
Remove change event from bridge page controller.
svara Jan 8, 2026
70853ef
Drop label from bridge button.
svara Jan 8, 2026
e286539
Drop name from bridge icon.
svara Jan 8, 2026
8f07ad6
Explicitly send submit button data in bridge form controller.
svara Jan 8, 2026
33fa3a9
Remove unused BridgeElement extensions.
svara Jan 8, 2026
24e4335
Remove the <summary> distinction
adjogima Jan 8, 2026
c5bd48a
Remove wrapping div in board header
adjogima Jan 8, 2026
f848a49
Use already existing buttons to populate the top nav bar
adjogima Jan 8, 2026
4a798b7
Add bridge overflow menu controller to <body>
adjogima Jan 8, 2026
ff7162c
Fix overflow-menu css scope
jayohms Jan 8, 2026
19d16e6
Cleanup bridge components
jayohms Jan 8, 2026
e35d3c8
Break up excessively long lines with bridge attributes
jayohms Jan 8, 2026
ac45da2
Merge branch 'main' into mobile/bridge-components
jayohms Jan 8, 2026
b6aac0b
Introduce a bridge helper to get icon asset urls
jayohms Jan 8, 2026
b9dcc3c
Update bridged overflow menu items for settings links
jayohms Jan 8, 2026
2b0c4b4
Send icon urls with buttons and resolve duplicate bridge items on the…
jayohms Jan 9, 2026
8cff72e
Hook up the "Create card" button as a bridge form target
jayohms Jan 9, 2026
2e55f67
Extract filtering for enabled targets so that the index of the target…
jayohms Jan 9, 2026
9ac34f8
Only apply the "Don't/Watch this" bridge target for the text variant …
jayohms Jan 9, 2026
b074a11
Update the card perma to support bridge buttonsa and the edit form state
jayohms Jan 9, 2026
cf3efce
Move the rest of the visible card perma buttons to the overflow menu
jayohms Jan 9, 2026
d5fef48
Merge branch 'main' into mobile/bridge-components
jayohms Jan 9, 2026
05c0cbb
Prevent autofocusing the filter input in drop downs
adjogima Jan 12, 2026
caafebb
Extract title to helper
adjogima Jan 12, 2026
43314f7
Move the bridge form controller to the outer container so a cancel ta…
jayohms Jan 12, 2026
bd8feea
Fix the form submit css scope
jayohms Jan 12, 2026
82d4fa5
Tweak card footer on perma
adjogima Jan 12, 2026
3d09258
Move dialog focus handling to a stimulus controller
adjogima Jan 12, 2026
6fab121
Merge branch 'main' into mobile/bridge-components
adjogima Jan 12, 2026
450f57e
Fix missing underline on card closure message button
adjogima Jan 12, 2026
4f7cec3
Rename bride page controller to title controller and remove text size…
svara Jan 13, 2026
68d1966
Add bridge text size controller.
svara Jan 13, 2026
e5c7965
Fix bottom notch in new card form
adjogima Jan 13, 2026
9dc5151
Set footer height
adjogima Jan 13, 2026
8894382
Save a bit of space in "watch" card
adjogima Jan 13, 2026
3b97b2e
Merge branch 'main' into mobile/bridge-components
adjogima Jan 14, 2026
3cada3c
Merge branch 'main' into mobile/bridge-components
jayohms Jan 14, 2026
600ee87
Merge branch 'main' into mobile/bridge-components
jayohms Jan 16, 2026
44732a3
Move dialog focus handling into the dialog controller
adjogima Jan 19, 2026
5113961
Setup proper focus handling for mobile on the card perma's board picker
adjogima Jan 19, 2026
e87dc10
Setup focus handling on touch target's connect callback
adjogima Jan 19, 2026
b5985ef
Use private functions for bridge components
jayohms Jan 19, 2026
e9ebf8e
Fix bad formatting on bridge page title attr
adjogima Jan 19, 2026
a9b873f
Merge branch 'main' into mobile/bridge-components
jayohms Jan 19, 2026
cc4e19b
Remove reference to removed controller
jayohms Jan 19, 2026
cd6478d
Merge branch 'main' into mobile/bridge-components
jayohms Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions app/assets/stylesheets/card-perma.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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 changes: 40 additions & 0 deletions app/assets/stylesheets/native.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 changes: 4 additions & 0 deletions app/assets/stylesheets/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
3 changes: 2 additions & 1 deletion app/helpers/accesses_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ def involvement_button(board, access, show_watchers, icon_only)
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
Expand Down
3 changes: 2 additions & 1 deletion app/helpers/boards_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ def link_back_to_board(board)
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
Expand Down
5 changes: 5 additions & 0 deletions app/helpers/bridge_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module BridgeHelper
def bridge_icon(name)
asset_url("#{name}.svg")
end
end
4 changes: 4 additions & 0 deletions app/helpers/filters_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ def filter_dialog(label, &block)
}, &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
Expand Down
19 changes: 19 additions & 0 deletions app/helpers/forms_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,23 @@ def auto_submit_form_with(**attributes, &)
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) { }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes bridged_form_with behave differently than form_with in the case of no block -- it would make it render an empty form, rather than just an opening form tag. Is this intentional? If not, I think we could do without this conditional, and always pass & to get the matching behaviour.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svara do you recall why this conditional was added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the auto_submit_form_with pattern/behaviour.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I suspect that one has that because it's commonly used as an empty form. So it's probably there as a convenience.

I'd be tempted to remove it from this one, since bridged_form_with is more of a form wrapper, so you'd expect it to behave more like a regular form_with. It's a very minor thing, though!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the explanation @kevinmcconnell.

end
end
end
2 changes: 1 addition & 1 deletion app/helpers/webhooks_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def webhook_action_label(action)
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
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -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"

43 changes: 43 additions & 0 deletions app/javascript/controllers/bridge/buttons_controller.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
62 changes: 62 additions & 0 deletions app/javascript/controllers/bridge/form_controller.js
Original file line number Diff line number Diff line change
@@ -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" ]
})
}
}
31 changes: 31 additions & 0 deletions app/javascript/controllers/bridge/insets_controller.js
Original file line number Diff line number Diff line change
@@ -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`)
}
}
43 changes: 43 additions & 0 deletions app/javascript/controllers/bridge/overflow_menu_controller.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading