Merge branch 'main' into mobile-app/prepare-webviews

* main: (107 commits)
  Document the new sign in method
  Replace handle_ naming
  Use same constant for fake magic links
  Replace FakeMagicLink with a temporary object
  Tidy up session_token
  Clean up interfaces
  Split tests by controller or responsibility
  Simplify auth logic
  Fix due to unit test when creating with invalid emails
  Restore sessions_controller test on creating invalid email address
  Move magic link api tests to their own files
  Rename test to clarify what they're about
  Cleanup session creation
  Update to always return a pending auth token for JSON responses.
  Update API test for cross code
  Change test expectation on single tenant mode account creation
  Add unit tests for the new endpoints
  Pass a server token when creating a magic link via API
  Simplify code a bit
  Simplify session create logic for both html and json
  ...
This commit is contained in:
Jay Ohms
2025-12-19 13:52:04 -05:00
120 changed files with 1551 additions and 636 deletions
+16
View File
@@ -461,7 +461,23 @@
font-size: clamp(0.6rem, 0.85cqi, 100px);
}
.card__comments {
--column-gap: 0.8ch;
display: flex;
margin-block-end: calc(var(--block-space) * -1.7);
margin-inline-end: calc(var(--card-padding-inline) * -0.5);
.icon {
--icon-size: 1.6em;
color: var(--card-color);
}
}
.card__steps {
--column-gap: 0.8ch;
display: flex;
}
+13
View File
@@ -325,6 +325,19 @@
}
}
.card-perma__notch-new-card-buttons {
display: flex;
gap: var(--inline-space-half);
@media (max-width: 479px) {
flex-direction: column;
.btn {
inline-size: 100%;
}
}
}
.card-perma__closure-message {
color: var(--card-color);
grid-area: closure-message;
+62 -69
View File
@@ -35,6 +35,56 @@
.house-md-content {
padding: var(--comment-padding-block) 0 0;
}
.comment--system & {
--comment-padding-block: var(--block-space-half);
text-align: center;
&::before {
/* Make up space for lack of avatar */
content: "";
display: flex;
inline-size: calc(var(--comment-padding-inline) * 0.9);
}
.comment__avatar {
display: none;
}
.comment__author {
a { margin: 0 auto; }
h3 { margin-inline: auto; }
strong { display: none; }
}
.comment__body {
padding: 0;
text-align: center;
}
.comment__content {
--stripe-color: var(--color-ink-lightest);
background-image: repeating-linear-gradient(
45deg in srgb,
var(--color-canvas) 0 1px,
var(--stripe-color) 1px 10px);
padding-inline: var(--comment-padding-inline);
.comments--system-expanded .comment--system & {
--stripe-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas));
}
.comment__history {
background-color: var(--stripe-color);
}
}
.reactions {
display: none !important;
}
}
}
.comment__author {
@@ -74,6 +124,7 @@
background-color: var(--comment-bg-color);
border-radius: 0.2em;
max-inline-size: calc(100% - calc(var(--comment-padding-inline) * 0.75));
padding:
var(--comment-padding-block)
calc(var(--comment-padding-inline) / 2)
@@ -94,86 +145,28 @@
}
.comment--system {
--comment-padding-block: var(--block-space-half);
display: none;
max-inline-size: var(--comment-max);
text-align: center;
transition: var(--dialog-duration) allow-discrete;
transition-property: display;
.comments--system-expanded & {
display: flex;
display: contents;
}
}
&:nth-last-child(1 of &):not(:has(~ * &) *):not(:has(~ &) *):not(:has(~ * &)):not(:has(&)) {
/* This unholy selector targets the last system comment in the document */
/* Hat-tip, https://css-tip.com/last-element-dom/ */
display: flex;
/* Show the last system comment */
:nth-last-child(1 of .comment--system) {
display: contents;
.comment__history {
display: grid;
}
.comment__history {
display: grid;
}
}
&:nth-child(1 of &):not(:has(&) ~ * *):not(:has(&) ~ *):not(& ~ * *):not(& *) {
/* Targets the first system comment in the document to effectively */
/* hide the "Show history" button if there's only one entry */
.comment__history {
display: none;
}
}
&::before {
/* Make up space for lack of avatar */
content: "";
display: flex;
inline-size: calc(var(--comment-padding-inline) * 0.75);
}
.comment__avatar {
/* Hide the "Show history" button if there's only one system comment */
:nth-child(1 of .comment--system) {
.comment__history {
display: none;
}
.comment__author {
a {
margin: 0 auto;
}
h3 {
margin-inline: auto;
}
strong {
display: none;
}
}
.comment__body {
padding: 0;
text-align: center;
}
.comment__content {
--stripe-color: var(--color-ink-lightest);
background-image: repeating-linear-gradient(
45deg in srgb,
var(--color-canvas) 0 1px,
var(--stripe-color) 1px 10px);
padding-inline: var(--comment-padding-inline);
.comments--system-expanded .comment--system & {
--stripe-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas));
}
.comment__history {
background-color: var(--stripe-color);
}
}
.reactions {
display: none !important;
}
}
}
+9
View File
@@ -36,6 +36,15 @@
}
}
.popup__footer {
border-block-start: 1px solid var(--color-ink-lightest);
color: var(--card-color);
margin-block-start: var(--popup-item-padding-inline);
padding: var(--popup-item-padding-inline) var(--popup-item-padding-inline) 0;
text-align: center;
text-transform: initial;
}
.popup__title {
font-weight: 800;
white-space: nowrap;
+4
View File
@@ -24,6 +24,10 @@
margin: 0;
position: absolute;
@media (max-width: 640px) {
inset-inline-end: calc(var(--comment-padding-inline) / 3);
}
.reactions__list {
display: none;
}
@@ -28,6 +28,10 @@
}
}
p:has(+ p) {
margin: 0;
}
ol, ul {
padding-inline-start: 3ch;
}
+4 -7
View File
@@ -93,17 +93,14 @@
}
.steps__icon {
--icon-size: 0.875em;
background-color: var(--card-color);
block-size: 1.3em;
border-radius: 50%;
color: var(--color-ink-inverted);
display: grid;
inline-size: 1.3em;
place-content: center;
.icon {
background-color: var(--color-ink-inverted);
block-size: 0.8em;
inline-size: 0.8em;
}
}
}
}
+1
View File
@@ -440,6 +440,7 @@
.card__meta-text:not(.card__meta-text--updated),
.card__stages,
.card__steps,
.card__comments,
.card__closed {
display: none;
}
+1 -1
View File
@@ -83,7 +83,7 @@ class BoardsController < ApplicationController
def show_columns
cards = @board.cards.awaiting_triage.latest.with_golden_first.preloaded
set_page_and_extract_portion_from cards
fresh_when etag: [ @board, @page.records, @user_filtering ]
fresh_when etag: [ @board, @page.records, @user_filtering, Current.account ]
end
def board_params
@@ -8,11 +8,16 @@ class Cards::AssignmentsController < ApplicationController
end
def create
@card.toggle_assignment @board.users.active.find(params[:assignee_id])
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
if @card.toggle_assignment @board.users.active.find(params[:assignee_id])
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
else
respond_to do |format|
format.turbo_stream
format.json { head :unprocessable_entity }
end
end
end
end
+1 -1
View File
@@ -61,6 +61,6 @@ class CardsController < ApplicationController
end
def card_params
params.expect(card: [ :status, :title, :description, :image, :created_at, :last_active_at, tag_ids: [] ])
params.expect(card: [ :status, :title, :description, :image, :created_at, :last_active_at ])
end
end
+3 -32
View File
@@ -4,13 +4,12 @@ module Authentication
included do
before_action :require_account # Checking and setting account must happen first
before_action :require_authentication
after_action :ensure_development_magic_link_not_leaked
helper_method :authenticated?
helper_method :email_address_pending_authentication
etag { Current.identity.id if authenticated? }
include LoginHelper
include Authentication::ViaMagicLink, LoginHelper
end
class_methods do
@@ -102,35 +101,7 @@ module Authentication
cookies.delete(:session_token)
end
def ensure_development_magic_link_not_leaked
unless Rails.env.development?
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
end
end
def email_address_pending_authentication_matches?(email_address)
if ActiveSupport::SecurityUtils.secure_compare(email_address, email_address_pending_authentication || "")
session.delete(:email_address_pending_authentication)
true
else
false
end
end
def email_address_pending_authentication
session[:email_address_pending_authentication]
end
def redirect_to_session_magic_link(magic_link, return_to: nil)
serve_development_magic_link(magic_link)
session[:email_address_pending_authentication] = magic_link.identity.email_address if magic_link
session[:return_to_after_authenticating] = return_to if return_to
redirect_to main_app.session_magic_link_path(script_name: nil)
end
def serve_development_magic_link(magic_link)
if Rails.env.development?
flash[:magic_link_code] = magic_link&.code
end
def session_token
cookies[:session_token]
end
end
@@ -0,0 +1,67 @@
module Authentication::ViaMagicLink
extend ActiveSupport::Concern
included do
after_action :ensure_development_magic_link_not_leaked
end
private
def ensure_development_magic_link_not_leaked
unless Rails.env.development?
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
end
end
def redirect_to_fake_session_magic_link(email_address, **options)
fake_magic_link = MagicLink.new(
identity: Identity.new(email_address: email_address),
code: SecureRandom.base32(6),
expires_at: MagicLink::EXPIRATION_TIME.from_now
)
redirect_to_session_magic_link fake_magic_link, **options
end
def redirect_to_session_magic_link(magic_link, return_to: nil)
serve_development_magic_link(magic_link)
set_pending_authentication_token(magic_link)
session[:return_to_after_authenticating] = return_to if return_to
respond_to do |format|
format.html { redirect_to main_app.session_magic_link_url(script_name: nil) }
format.json { render json: { pending_authentication_token: pending_authentication_token }, status: :created }
end
end
def serve_development_magic_link(magic_link)
if Rails.env.development? && magic_link.present?
flash[:magic_link_code] = magic_link.code
response.set_header("X-Magic-Link-Code", magic_link.code)
end
end
def set_pending_authentication_token(magic_link)
cookies[:pending_authentication_token] = {
value: pending_authentication_token_verifier.generate(magic_link.identity.email_address, expires_at: magic_link.expires_at),
httponly: true,
same_site: :lax,
expires: magic_link.expires_at
}
end
def email_address_pending_authentication
pending_authentication_token_verifier.verified(pending_authentication_token)
end
def pending_authentication_token_verifier
Rails.application.message_verifier(:pending_authentication)
end
def pending_authentication_token
cookies[:pending_authentication_token]
end
def clear_pending_authentication_token
cookies.delete(:pending_authentication_token)
end
end
+1 -1
View File
@@ -16,7 +16,7 @@ module FilterScoped
end
def filter_params
params.reverse_merge(**Filter.default_values).permit(*Filter::PERMITTED_PARAMS)
params.with_defaults(**Filter.default_values).permit(*Filter::PERMITTED_PARAMS)
end
def set_user_filtering
+2 -1
View File
@@ -4,7 +4,8 @@ class My::MenusController < ApplicationController
@boards = Current.user.boards.ordered_by_recently_accessed
@tags = Current.account.tags.all.alphabetically
@users = Current.account.users.active.alphabetically
@accounts = Current.identity.accounts
fresh_when etag: [ @filters, @boards, @tags, @users ]
fresh_when etag: [ @filters, @boards, @tags, @users, @accounts ]
end
end
@@ -1,7 +1,7 @@
class Sessions::MagicLinksController < ApplicationController
disallow_account_scope
require_unauthenticated_access
rate_limit to: 10, within: 15.minutes, only: :create, with: -> { redirect_to session_magic_link_path, alert: "Wait 15 minutes, then try again" }
rate_limit to: 10, within: 15.minutes, only: :create, with: :rate_limit_exceeded
before_action :ensure_that_email_address_pending_authentication_exists
layout "public"
@@ -11,25 +11,20 @@ class Sessions::MagicLinksController < ApplicationController
def create
if magic_link = MagicLink.consume(code)
authenticate_with magic_link
authenticate magic_link
else
redirect_to session_magic_link_path, flash: { shake: true }
invalid_code
end
end
private
def ensure_that_email_address_pending_authentication_exists
unless email_address_pending_authentication.present?
redirect_to new_session_path, alert: "Enter your email address to sign in."
end
end
def authenticate_with(magic_link)
if email_address_pending_authentication_matches?(magic_link.identity.email_address)
start_new_session_for magic_link.identity
redirect_to after_sign_in_url(magic_link)
else
redirect_to new_session_path, alert: "Authentication failed. Please try again."
alert_message = "Enter your email address to sign in."
respond_to do |format|
format.html { redirect_to new_session_path, alert: alert_message }
format.json { render json: { message: alert_message }, status: :unauthorized }
end
end
end
@@ -37,6 +32,41 @@ class Sessions::MagicLinksController < ApplicationController
params.expect(:code)
end
def authenticate(magic_link)
if ActiveSupport::SecurityUtils.secure_compare(email_address_pending_authentication || "", magic_link.identity.email_address)
sign_in magic_link
else
email_address_mismatch
end
end
def sign_in(magic_link)
clear_pending_authentication_token
start_new_session_for magic_link.identity
respond_to do |format|
format.html { redirect_to after_sign_in_url(magic_link) }
format.json { render json: { session_token: session_token } }
end
end
def email_address_mismatch
clear_pending_authentication_token
alert_message = "Something went wrong. Please try again."
respond_to do |format|
format.html { redirect_to new_session_path, alert: alert_message }
format.json { render json: { message: alert_message }, status: :unauthorized }
end
end
def invalid_code
respond_to do |format|
format.html { redirect_to session_magic_link_path, flash: { shake: true } }
format.json { render json: { message: "Try another code." }, status: :unauthorized }
end
end
def after_sign_in_url(magic_link)
if magic_link.for_sign_up?
new_signup_completion_path
@@ -44,4 +74,12 @@ class Sessions::MagicLinksController < ApplicationController
after_authentication_url
end
end
def rate_limit_exceeded
rate_limit_exceeded_message = "Try again in 15 minutes."
respond_to do |format|
format.html { redirect_to session_magic_link_path, alert: rate_limit_exceeded_message }
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
end
end
end
+42 -10
View File
@@ -1,7 +1,7 @@
class SessionsController < ApplicationController
disallow_account_scope
require_unauthenticated_access except: :destroy
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
rate_limit to: 10, within: 3.minutes, only: :create, with: :rate_limit_exceeded
layout "public"
@@ -9,16 +9,12 @@ class SessionsController < ApplicationController
end
def create
if identity = Identity.find_by_email_address(email_address)
redirect_to_session_magic_link identity.send_magic_link
if identity = Identity.find_by(email_address: email_address)
sign_in identity
elsif Account.accepting_signups?
sign_up
else
signup = Signup.new(email_address: email_address)
if signup.valid?(:identity_creation)
magic_link = signup.create_identity if Account.accepting_signups?
redirect_to_session_magic_link magic_link
else
head :unprocessable_entity
end
redirect_to_fake_session_magic_link email_address
end
end
@@ -28,7 +24,43 @@ class SessionsController < ApplicationController
end
private
def magic_link_from_sign_in_or_sign_up
if identity = Identity.find_by_email_address(email_address)
identity.send_magic_link
else
signup = Signup.new(email_address: email_address)
signup.create_identity if signup.valid?(:identity_creation) && Account.accepting_signups?
end
end
def email_address
params.expect(:email_address)
end
def rate_limit_exceeded
rate_limit_exceeded_message = "Try again later."
respond_to do |format|
format.html { redirect_to new_session_path, alert: rate_limit_exceeded_message }
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
end
end
def sign_in(identity)
redirect_to_session_magic_link identity.send_magic_link
end
def sign_up
signup = Signup.new(email_address: email_address)
if signup.valid?(:identity_creation)
magic_link = signup.create_identity
redirect_to_session_magic_link magic_link
else
respond_to do |format|
format.html { redirect_to new_session_path, alert: "Something went wrong" }
format.json { render json: { message: "Something went wrong" }, status: :unprocessable_entity }
end
end
end
end
-6
View File
@@ -10,12 +10,6 @@ module ApplicationHelper
tag.span class: class_names("icon icon--#{name}", options.delete(:class)), "aria-hidden": true, **options
end
def inline_svg(name)
file_path = "#{Rails.root}/app/assets/images/#{name}.svg"
return File.read(file_path).html_safe if File.exist?(file_path)
"(not found)"
end
def back_link_to(label, url, action, **options)
link_to url, class: "btn btn--back", data: { controller: "hotkey", action: action }, **options do
icon_tag("arrow-left") + tag.strong("Back to #{label}", class: "overflow-ellipsis") + tag.kbd("ESC", class: "txt-x-small hide-on-touch").html_safe
+1 -1
View File
@@ -43,7 +43,7 @@ module ColumnsHelper
end
def column_frame_tag(id, src: nil, data: {}, **options, &block)
data = data.reverse_merge \
data = data.with_defaults \
drag_and_drop_refresh: true,
controller: "frame",
action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload"
@@ -0,0 +1,26 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { limit: Number, count: Number }
static targets = ["unassigned", "limitMessage"]
connect() {
this.updateState()
}
countValueChanged() {
this.updateState()
}
updateState() {
const atLimit = this.countValue >= this.limitValue
this.unassignedTargets.forEach(el => {
el.hidden = atLimit
})
if (this.hasLimitMessageTarget) {
this.limitMessageTarget.hidden = !atLimit
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ class Account < ApplicationRecord
def create_with_owner(account:, owner:)
create!(**account).tap do |account|
account.users.create!(role: :system, name: "System")
account.users.create!(**owner.reverse_merge(role: "owner", verified_at: Time.current))
account.users.create!(**owner.with_defaults(role: :owner, verified_at: Time.current))
end
end
end
+1
View File
@@ -69,6 +69,7 @@ class Account::Seeder
<li>Make another called "Working on"</li>
</ol>
<p>Go back to the Board view, click the little + to the right of the DONE column, name the column, pick a color, then do it again.</p>
<p><br></p>
<p>After that, drag this card to DONE or select DONE in the sidebar.</p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/make-columns.gif" alt="Demo of adding columns" caption="Make two more columns" content-type="image/*" filename="make-columns.gif" presentation="gallery"></action-text-attachment>
HTML
+11
View File
@@ -1,7 +1,18 @@
class Assignment < ApplicationRecord
LIMIT = 100
belongs_to :account, default: -> { card.account }
belongs_to :card, touch: true
belongs_to :assignee, class_name: "User"
belongs_to :assigner, class_name: "User"
validate :within_limit, on: :create
private
def within_limit
if card.assignments.count >= LIMIT
errors.add(:base, "Card already has the maximum of #{LIMIT} assignees")
end
end
end
+5 -3
View File
@@ -24,10 +24,12 @@ module Card::Assignable
private
def assign(user)
assignments.create! assignee: user, assigner: Current.user
watch_by user
assignment = assignments.create assignee: user, assigner: Current.user
track_event :assigned, assignee_ids: [ user.id ]
if assignment.persisted?
watch_by user
track_event :assigned, assignee_ids: [ user.id ]
end
rescue ActiveRecord::RecordNotUnique
# Already assigned
end
-2
View File
@@ -6,8 +6,6 @@ module Card::Statuses
before_save :mark_if_just_published
after_create -> { track_event :published }, if: :published?
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
end
attr_accessor :was_just_published
+1 -1
View File
@@ -39,7 +39,7 @@ module User::EmailAddressChangeable
private
def generate_email_address_change_token(from: identity.email_address, to:, **options)
options = options.reverse_merge(
options = options.with_defaults(
for: EMAIL_CHANGE_TOKEN_PURPOSE,
old_email_address: from,
new_email_address: to,
+1 -1
View File
@@ -1,4 +1,4 @@
<li class="flex align-center gap-half" data-filter-target="item" data-navigable-list-target="item">
<li class="flex align-center gap-half" data-filter-target="item" data-navigable-list-target="item" role="option">
<%= link_to user, class: "txt-ink flex gap-half align-center min-width" do %>
<%= avatar_preview_tag user, hidden_for_screen_reader: true %>
<div class="txt-align-start overflow-ellipsis">
+1 -1
View File
@@ -12,7 +12,7 @@
<div class="settings__user-filter">
<input placeholder="Filter…" class="input input--transparent full-width txt-small" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-filter-target="input" data-action="input->filter#filter">
<ul class="settings__user-list margin-block-half" data-filter-target="list">
<ul class="settings__user-list margin-block-half" data-filter-target="list" role="listbox">
<%= render partial: "account/settings/user", collection: users %>
</ul>
</div>
+13 -8
View File
@@ -1,11 +1,15 @@
<% postponed_count = board.cards.postponed.size %>
<% awaiting_triage_count = board.cards.awaiting_triage.size %>
<% closed_count = board.cards.closed.size %>
<section class="mobile-card-columns">
<div class="cards margin-block-end-half">
<%= render "columns/show/add_card_button", board: board %>
</div>
<%= link_to board_columns_not_now_path(board), class: "cards cards--on-deck is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= board.cards.postponed.count %>">
<span class="cards__expander-count"><%= board.cards.postponed.count %></span>
<div class="cards__expander btn btn--plain" style="--card-count: <%= postponed_count %>">
<span class="cards__expander-count"><%= postponed_count %></span>
<h2 class="cards__expander-title">
Not Now
</h2>
@@ -13,8 +17,8 @@
<% end %>
<%= link_to board_columns_stream_path(board), class: "cards cards--considering is-collapsed", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= board.cards.awaiting_triage.count %>">
<span class="cards__expander-count"><%= board.cards.awaiting_triage.count %></span>
<div class="cards__expander btn btn--plain" style="--card-count: <%= awaiting_triage_count %>">
<span class="cards__expander-count"><%= awaiting_triage_count %></span>
<h2 class="cards__expander-title">
Maybe?
</h2>
@@ -22,9 +26,10 @@
<% end %>
<% board.columns.sorted.each do |column| %>
<% active_count = column.cards.active.size %>
<%= link_to board_column_path(column.board, column), class: "cards cards--doing is-collapsed", style: "--card-color: #{column.color}", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= column.cards.active.count %>">
<span class="cards__expander-count"><%= column.cards.active.count %></span>
<div class="cards__expander btn btn--plain" style="--card-count: <%= active_count %>">
<span class="cards__expander-count"><%= active_count %></span>
<h2 class="cards__expander-title">
<%= column.name %>
</h2>
@@ -33,8 +38,8 @@
<% end %>
<%= link_to board_columns_closed_path(board), class: "cards cards--closed is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= board.cards.closed.count %>">
<span class="cards__expander-count"><%= board.cards.closed.count %></span>
<div class="cards__expander btn btn--plain" style="--card-count: <%= closed_count %>">
<span class="cards__expander-count"><%= closed_count %></span>
<h2 class="cards__expander-title">
Done
</h2>
+14 -3
View File
@@ -1,10 +1,12 @@
<%= turbo_frame_tag @card, :assignment do %>
<%= tag.div class: "max-width full-width", data: {
action: "turbo:before-cache@document->dialog#close dialog:show@document->navigable-list#reset keydown->navigable-list#navigate filter:changed->navigable-list#reset",
controller: "filter navigable-list",
controller: "filter navigable-list assignment-limit",
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
navigable_list_actionable_items_value: true,
assignment_limit_limit_value: Assignment::LIMIT,
assignment_limit_count_value: @card.assignments.count } do %>
<div class="flex align-start justify-space-between">
<strong class="popup__title">Assign this to…</strong>
@@ -17,9 +19,18 @@
<ul class="popup__list" data-filter-target="list">
<%= render "user", card: @card, user: Current.user, user_label: "Me" do %>
<span class="visually-hidden"><%= Current.user.name %></span>
<kbd class="txt-xx-small hide-on-touch">m</kbd>
<% end %>
<%= render collection: @assigned_to, partial: "user", locals: { card: @card } %>
<%= render collection: @users, partial: "user", locals: { card: @card } %>
<% @users.each do |user| %>
<span data-assignment-limit-target="unassigned">
<%= render "user", card: @card, user: user %>
</span>
<% end %>
</ul>
<div class="popup__footer" hidden data-assignment-limit-target="limitMessage">
Maximum <%= Assignment::LIMIT %> assignees
</div>
<% end %>
<% end %>
+2 -2
View File
@@ -1,8 +1,8 @@
<% cache comment do %>
<%# Helper Dependency Updated: avatar_image_tag 2025-12-15 %>
<%= turbo_frame_tag comment, :container do %>
<%= turbo_frame_tag comment, :container, class: ["comment--system": comment.creator.system?] do %>
<%# Cache bump 2025-12-14: action text attachment rendering changed for lightbox -%>
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment align-start full-width <%= "comment--system" if comment.creator.system? %>">
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment align-start full-width">
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag comment.creator, hidden_for_screen_reader: true %>
</figure>
@@ -1,5 +1,5 @@
<div class="card-perma__notch card-perma__notch--bottom flex-column">
<div class="flex gap-half">
<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" } },
+2 -1
View File
@@ -30,8 +30,9 @@
</div>
</div>
<footer class="card__footer">
<footer class="card__footer flex gap-half">
<%= render "cards/display/preview/meta", card: card, preview: true %>
<%= render "cards/display/preview/comments", card: card %>
<%= render "cards/display/common/background", card: card %>
</footer>
@@ -12,6 +12,10 @@
</span>
</button>
<% if Current.user %>
<%= button_to "Assign to me", card_assignments_path(card, params: { assignee_id: Current.user.id }), method: :post, data: {controller: "hotkey", action: "keydown.m@document->hotkey#click" }, hidden: true %>
<% end %>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= yield %>
</dialog>
@@ -0,0 +1,7 @@
<% comments = card.comments.by_user %>
<% if comments.any? %>
<div class="card__comments align-center gap-half flex-item-justify-end flex-item-no-shrink">
<%= icon_tag "comment" %>
<strong><%= comments.count %></strong>
</div>
<% end %>
+2 -2
View File
@@ -7,8 +7,8 @@
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<nav>
<%= link_to "https://fizzy.do", class: "header__logo center flex-inline align-center" do %>
<span><%= image_tag "logo.png" %></span>
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
<span><%= image_tag "logo.png", alt: "" %></span>
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
<% end %>
</nav>
+2 -2
View File
@@ -4,8 +4,8 @@
<%= tag.button class:"nav__trigger input input--select center flex-inline align-center txt-normal", data: {
action: "click->dialog#open keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
<span><%= image_tag "logo.png" %></span>
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
<span><%= image_tag "logo.png", alt: "" %></span>
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
<kbd class="kbd txt-xx-small hide-on-touch">J</kbd>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= render "my/menus/people", users: @users %>
<%= render "my/menus/settings" %>
<%= render "my/menus/shortcuts" %>
<%= render "my/menus/accounts", accounts: Current.identity.accounts %>
<%= render "my/menus/accounts", accounts: @accounts %>
<% end %>
<footer class="nav__footer">
+1 -1
View File
@@ -9,7 +9,7 @@
</header>
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large",
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large txt-uppercase",
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
data: { magic_link_target: "input", action: "keydown.enter->magic-link#submitOnEnter paste->magic-link#submitOnPaste" } %>