Merge branch 'main' into mobile/bridge-components

* main:
  Fix that caching should be disabled in dev by default. References 3cf841d463.
  Revert "Add CJK (Chinese, Japanese, Korean) search support"
  Account for new button sizes on mobile
  Bump filter z-index when filters are open
  Use a dedicated resource for showing cards in draft mode
  Refactor popup.css styles for focus state
  Add Segoe UI Variable Fizzy font face configuration comments
  Update @layer name to `base` in font-face definition for "Segoe UI Variable Fizzy"
  Fix font-face `src` reference for "Segoe UI Variable Fizzy" to correct local font lookup
  Adjust font-weight range for "Segoe UI Variable Fizzy" to include 800-900
  Update --font-sans and related font-face references to "Segoe UI Variable Fizzy" for consistency
  Add "Segoe UI fizzy" to --font-sans and define custom font faces
  Update --font-sans to include "Segoe UI Variable" for improved text rendering
  Add margin to details inside popup styles
This commit is contained in:
Jay Ohms
2026-01-08 15:46:29 -05:00
27 changed files with 172 additions and 196 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
--block-space-double: calc(var(--block-space) * 2); --block-space-double: calc(var(--block-space) * 2);
/* Text */ /* Text */
--font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Fizzy", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--font-serif: ui-serif, serif; --font-serif: ui-serif, serif;
--font-mono: ui-monospace, monospace; --font-mono: ui-monospace, monospace;
+5 -1
View File
@@ -4,7 +4,7 @@
--comment-padding-block: var(--block-space-half); --comment-padding-block: var(--block-space-half);
--comment-padding-inline: var(--inline-space-double); --comment-padding-inline: var(--inline-space-double);
--comment-max: 70ch; --comment-max: 70ch;
--reaction-size: 1.6875rem; --reaction-size: 2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -15,6 +15,10 @@
@media (min-width: 160ch) { @media (min-width: 160ch) {
padding-inline: var(--tray-size); padding-inline: var(--tray-size);
} }
@media (min-width: 640px) {
--reaction-size: 1.6875rem;
}
} }
.comments__subscribers { .comments__subscribers {
+1
View File
@@ -19,6 +19,7 @@
--input-background: var(--color-canvas); --input-background: var(--color-canvas);
} }
&:has(dialog[open]),
&:has([data-controller~="tooltip"]:hover) { &:has([data-controller~="tooltip"]:hover) {
z-index: calc(var(--z-nav) + 1); z-index: calc(var(--z-nav) + 1);
} }
+41
View File
@@ -0,0 +1,41 @@
@layer base {
/*
Segoe UI Variable Fizzy font face configuration.
1. Segoe UI Variable (Weights 100-700):
Leverages variable font features to:
- Automatically adjust Weight (wght) dynamically within the 100-700 range.
- Automatically manage Optical Size (opsz) based on font-size.
2. Segoe UI Black (Weights 800-900):
Used as a fallback because Segoe UI Variable does not natively support 900 weight.
This ensures a consistent bold experience across all weights.
*/
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Variable");
font-weight: 100 700;
font-style: normal;
}
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Variable");
font-weight: 100 700;
font-style: italic;
}
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Black");
font-weight: 800 900;
font-style: normal;
}
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Black Italic");
font-weight: 800 900;
font-style: italic;
}
}
+4
View File
@@ -155,6 +155,10 @@
max-inline-size: 100%; max-inline-size: 100%;
padding: var(--inline-space-half) var(--popup-item-padding-inline); padding: var(--inline-space-half) var(--popup-item-padding-inline);
text-align: start; text-align: start;
&:focus-visible {
z-index: 1;
}
} }
.popup__icon { .popup__icon {
+5 -2
View File
@@ -10,7 +10,7 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--inline-space-half); gap: var(--inline-space-half);
inline-size: 100%; inline-size: 100%;
margin-block-start: calc(var(--block-space-half) / 2); margin-block-start: var(--block-space-half);
margin-inline: calc(var(--column-gap) / -1); margin-inline: calc(var(--column-gap) / -1);
&:has([open]) { &:has([open]) {
@@ -44,6 +44,7 @@
} }
.reactions__trigger { .reactions__trigger {
--btn-size: var(--reaction-size);
--btn-border-color: var(--reaction-border-color); --btn-border-color: var(--reaction-border-color);
img { img {
@@ -115,11 +116,13 @@
/* Make the avatar and delete buttons fit nicely within the reaction */ /* Make the avatar and delete buttons fit nicely within the reaction */
.reaction__avatar, .reaction__avatar,
.reaction__avatar .avatar,
.reaction__form-label, .reaction__form-label,
.reaction form { .reaction form {
block-size: var(--btn-size); block-size: 100%;
} }
.reaction__delete { .reaction__delete {
display: none; display: none;
@@ -0,0 +1,13 @@
class Cards::DraftsController < ApplicationController
include CardScoped
before_action :redirect_if_published
def show
end
private
def redirect_if_published
redirect_to @card unless @card.drafted?
end
end
@@ -5,7 +5,8 @@ class Cards::PublishesController < ApplicationController
@card.publish @card.publish
if add_another_param? if add_another_param?
redirect_to @board.cards.create!, notice: "Card added" card = @board.cards.create!(status: :drafted)
redirect_to card_draft_path(card), notice: "Card added"
else else
redirect_to @card.board redirect_to @card.board
end end
+6 -1
View File
@@ -3,6 +3,7 @@ class CardsController < ApplicationController
before_action :set_board, only: %i[ create ] before_action :set_board, only: %i[ create ]
before_action :set_card, only: %i[ show edit update destroy ] before_action :set_card, only: %i[ show edit update destroy ]
before_action :redirect_if_drafted, only: :show
before_action :ensure_permission_to_administer_card, only: %i[ destroy ] before_action :ensure_permission_to_administer_card, only: %i[ destroy ]
def index def index
@@ -13,7 +14,7 @@ class CardsController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
card = Current.user.draft_new_card_in(@board) card = Current.user.draft_new_card_in(@board)
redirect_to card redirect_to card_draft_path(card)
end end
format.json do format.json do
@@ -56,6 +57,10 @@ class CardsController < ApplicationController
@card = Current.user.accessible_cards.find_by!(number: params[:id]) @card = Current.user.accessible_cards.find_by!(number: params[:id])
end end
def redirect_if_drafted
redirect_to card_draft_path(@card) if @card.drafted?
end
def ensure_permission_to_administer_card def ensure_permission_to_administer_card
head :forbidden unless Current.user.can_administer_card?(@card) head :forbidden unless Current.user.can_administer_card?(@card)
end end
-2
View File
@@ -1,6 +1,4 @@
module Search module Search
CJK_PATTERN = /\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/
def self.table_name_prefix def self.table_name_prefix
"search_" "search_"
end end
+2 -8
View File
@@ -13,14 +13,8 @@ class Search::Highlighter
result = text.dup result = text.dup
terms.each do |term| terms.each do |term|
if term.match?(Search::CJK_PATTERN) result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match|
result.gsub!(/(#{Regexp.escape(term)})/i) do |match| "#{OPENING_MARK}#{match}#{CLOSING_MARK}"
"#{OPENING_MARK}#{match}#{CLOSING_MARK}"
end
else
result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match|
"#{OPENING_MARK}#{match}#{CLOSING_MARK}"
end
end end
end end
+1 -1
View File
@@ -33,7 +33,7 @@ class Search::Query < ApplicationRecord
end end
def remove_invalid_search_characters(terms) def remove_invalid_search_characters(terms)
terms.gsub(/[^\p{L}\p{N}_"]/, " ") terms.gsub(/[^\w"]/, " ")
end end
def remove_unbalanced_quotes(terms) def remove_unbalanced_quotes(terms)
+1 -7
View File
@@ -8,10 +8,9 @@ module Search::Record::SQLite
has_one :search_records_fts, -> { with_rowid }, has_one :search_records_fts, -> { with_rowid },
class_name: "Search::Record::SQLite::Fts", foreign_key: :rowid, primary_key: :id, dependent: :destroy class_name: "Search::Record::SQLite::Fts", foreign_key: :rowid, primary_key: :id, dependent: :destroy
before_save :stem_content
after_save :upsert_to_fts5_table after_save :upsert_to_fts5_table
scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", Search::Stemmer.stem(query.to_s)) } scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", query) }
end end
class_methods do class_methods do
@@ -43,11 +42,6 @@ module Search::Record::SQLite
end end
private private
def stem_content
self.title = Search::Stemmer.stem(title) if title_changed?
self.content = Search::Stemmer.stem(content) if content_changed?
end
def escape_fts_highlight(html) def escape_fts_highlight(html)
return nil unless html.present? return nil unless html.present?
+1 -35
View File
@@ -5,43 +5,9 @@ module Search::Stemmer
def stem(value) def stem(value)
if value.present? if value.present?
tokenize(value).join(" ") value.gsub(/[^\w\s]/, "").split(/\s+/).map { |word| STEMMER.stem(word.downcase) }.join(" ")
else else
value value
end end
end end
private
def tokenize(value)
tokens = []
current_word = +""
value.each_char do |char|
if cjk_character?(char)
if current_word.present?
tokens << stem_word(current_word)
current_word = +""
end
tokens << char
elsif char =~ /[\p{L}\p{N}_]/
current_word << char
else
if current_word.present?
tokens << stem_word(current_word)
current_word = +""
end
end
end
tokens << stem_word(current_word) if current_word.present?
tokens
end
def cjk_character?(char)
char.match?(Search::CJK_PATTERN)
end
def stem_word(word)
STEMMER.stem(word.downcase)
end
end end
-6
View File
@@ -37,11 +37,5 @@
<% if card.published? %> <% if card.published? %>
<%= render "cards/container/footer/published", card: card %> <%= render "cards/container/footer/published", card: card %>
<% elsif card.drafted? %>
<% if Fizzy.saas? %>
<%= render "cards/container/footer/saas/create", card: card %>
<% else %>
<%= render "cards/container/footer/create", card: card %>
<% end %>
<% end %> <% end %>
</section> </section>
@@ -0,0 +1,34 @@
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;">
<% cache card do %>
<div class="card-perma__actions card-perma__actions--left">
<%= render "cards/container/image", card: card %>
</div>
<div class="card-perma__bg">
<%= card_article_tag card, class: "card" do %>
<header class="card__header">
<%= render "cards/display/perma/board", card: card %>
<%= render "cards/display/perma/tags", card: card %>
</header>
<div class="card__body justify-space-between">
<div class="card__content">
<%= render "cards/container/content", card: card %>
<%= render "cards/display/perma/steps", card: card %>
</div>
</div>
<footer class="card__footer full-width flex align-start gap">
<%= render "cards/display/perma/meta", card: card %>
<%= render "cards/display/perma/background", card: card %>
</footer>
<% end %>
</div>
<% end %>
<% if Fizzy.saas? %>
<%= render "cards/container/footer/saas/create", card: card %>
<% else %>
<%= render "cards/container/footer/create", card: card %>
<% end %>
</section>
+16
View File
@@ -0,0 +1,16 @@
<% @page_title = @card.title %>
<% @header_class = "header--card" %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_back_to_board(@card.board) %>
</div>
<% end %>
<div data-controller="beacon lightbox" data-beacon-url-value="<%= card_reading_path(@card) %>">
<%= render "cards/drafts/container", card: @card %>
<%= render "layouts/lightbox" do %>
<%= button_to_remove_card_image(@card) if @card.image.attached? %>
<% end %>
</div>
+1 -1
View File
@@ -16,7 +16,7 @@
<div data-controller="beacon lightbox" data-beacon-url-value="<%= card_reading_path(@card) %>"> <div data-controller="beacon lightbox" data-beacon-url-value="<%= card_reading_path(@card) %>">
<%= render "cards/container", card: @card %> <%= render "cards/container", card: @card %>
<%= render "cards/messages", card: @card unless @card.drafted? %> <%= render "cards/messages", card: @card %>
<%= render "layouts/lightbox" do %> <%= render "layouts/lightbox" do %>
<%= button_to_remove_card_image(@card) if @card.image.attached? %> <%= button_to_remove_card_image(@card) if @card.image.attached? %>
+1 -1
View File
@@ -26,7 +26,7 @@ Rails.application.configure do
config.cache_store = :memory_store config.cache_store = :memory_store
config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" }
else else
config.action_controller.perform_caching = true config.action_controller.perform_caching = false
config.cache_store = :null_store config.cache_store = :null_store
end end
+1
View File
@@ -71,6 +71,7 @@ Rails.application.routes.draw do
resources :cards do resources :cards do
scope module: :cards do scope module: :cards do
resource :draft, only: :show
resource :board resource :board
resource :closure resource :closure
resource :column resource :column
@@ -10,7 +10,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees nearing card limit notice" do test "admin sees nearing card limit notice" do
accounts(:initech).update_column(:cards_count, 950) accounts(:initech).update_column(:cards_count, 950)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug) get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success assert_response :success
assert_match /upgrade to unlimited/i, response.body assert_match /upgrade to unlimited/i, response.body
@@ -19,7 +19,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees nearing storage limit notice" do test "admin sees nearing storage limit notice" do
Account.any_instance.stubs(:bytes_used).returns(600.megabytes) Account.any_instance.stubs(:bytes_used).returns(600.megabytes)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug) get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success assert_response :success
assert_match /upgrade to get more/i, response.body assert_match /upgrade to get more/i, response.body
@@ -30,7 +30,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees exceeding card limit notice" do test "admin sees exceeding card limit notice" do
accounts(:initech).update_column(:cards_count, 1001) accounts(:initech).update_column(:cards_count, 1001)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug) get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success assert_response :success
assert_match /youve used your.*free cards/i, response.body assert_match /youve used your.*free cards/i, response.body
@@ -39,7 +39,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees exceeding storage limit notice" do test "admin sees exceeding storage limit notice" do
Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes) Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug) get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success assert_response :success
assert_match /youve run out of.*free storage/i, response.body assert_match /youve run out of.*free storage/i, response.body
@@ -64,7 +64,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "comped account under limits sees no notices" do test "comped account under limits sees no notices" do
accounts(:initech).comp accounts(:initech).comp
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug) get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success assert_response :success
assert_no_match /upgrade/i, response.body assert_no_match /upgrade/i, response.body
@@ -0,0 +1,21 @@
require "test_helper"
class Cards::DraftsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
end
test "show" do
card = boards(:writebook).cards.create!(creator: users(:kevin), status: :drafted)
get card_draft_path(card)
assert_response :success
end
test "show redirects to card when published" do
card = cards(:logo)
get card_draft_path(card)
assert_redirected_to card
end
end
@@ -28,6 +28,6 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest
new_card = Card.last new_card = Card.last
assert new_card.drafted? assert new_card.drafted?
assert_redirected_to new_card assert_redirected_to card_draft_path(new_card)
end end
end end
+9 -2
View File
@@ -21,7 +21,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
end end
card = Card.last card = Card.last
assert_redirected_to card assert_redirected_to card_draft_path(card)
assert card.drafted? assert card.drafted?
end end
@@ -31,10 +31,17 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
assert_no_difference -> { Card.count } do assert_no_difference -> { Card.count } do
post board_cards_path(boards(:writebook)) post board_cards_path(boards(:writebook))
assert_redirected_to draft assert_redirected_to card_draft_path(draft)
end end
end end
test "show redirects to draft when card is drafted" do
card = boards(:writebook).cards.create!(creator: users(:kevin), status: :drafted)
get card_path(card)
assert_redirected_to card_draft_path(card)
end
test "show" do test "show" do
get card_path(cards(:logo)) get card_path(cards(:logo))
assert_response :success assert_response :success
-28
View File
@@ -82,34 +82,6 @@ class Search::HighlighterTest < ActiveSupport::TestCase
assert_equal "&lt;script&gt;#{mark('test')}&lt;/script&gt;", result assert_equal "&lt;script&gt;#{mark('test')}&lt;/script&gt;", result
end end
test "highlight Chinese characters" do
highlighter = Search::Highlighter.new("测试")
result = highlighter.highlight("这是一个测试文本")
assert_equal "这是一个#{mark('测试')}文本", result
end
test "highlight Japanese characters" do
highlighter = Search::Highlighter.new("テスト")
result = highlighter.highlight("これはテストです")
assert_equal "これは#{mark('テスト')}です", result
end
test "highlight Korean characters" do
highlighter = Search::Highlighter.new("테스트")
result = highlighter.highlight("이것은 테스트입니다")
assert_equal "이것은 #{mark('테스트')}입니다", result
end
test "highlight mixed CJK and English" do
highlighter = Search::Highlighter.new("world 世界")
result = highlighter.highlight("hello world 你好世界")
assert_equal "hello #{mark('world')} 你好#{mark('世界')}", result
end
private private
def mark(text) def mark(text)
"#{Search::Highlighter::OPENING_MARK}#{text}#{Search::Highlighter::CLOSING_MARK}" "#{Search::Highlighter::OPENING_MARK}#{text}#{Search::Highlighter::CLOSING_MARK}"
-57
View File
@@ -1,57 +0,0 @@
require "test_helper"
class Search::QueryTest < ActiveSupport::TestCase
setup do
@account = accounts(:"37s")
Current.account = @account
end
test "sanitize preserves ASCII words" do
query = build_query("hello world")
assert_equal "hello world", query.terms
end
test "sanitize preserves Chinese characters" do
query = build_query("测试文本")
assert_equal "测试文本", query.terms
end
test "sanitize preserves Japanese characters" do
query = build_query("テスト")
assert_equal "テスト", query.terms
end
test "sanitize preserves Korean characters" do
query = build_query("테스트")
assert_equal "테스트", query.terms
end
test "sanitize preserves mixed CJK and English" do
query = build_query("hello 世界 test")
assert_equal "hello 世界 test", query.terms
end
test "sanitize removes special characters but preserves CJK" do
query = build_query("测试@文本")
assert_equal "测试 文本", query.terms
end
test "sanitize preserves quoted phrases with CJK" do
query = build_query('"你好世界"')
assert_equal '"你好世界"', query.terms
end
private
def build_query(terms)
query = Search::Query.wrap(terms)
query.validate
query
end
end
-36
View File
@@ -12,40 +12,4 @@ class Search::StemmerTest < ActiveSupport::TestCase
assert_equal "test run jump walk", result assert_equal "test run jump walk", result
end end
test "split Chinese characters for FTS indexing" do
result = Search::Stemmer.stem("测试")
assert_equal "测 试", result
end
test "split Japanese characters for FTS indexing" do
result = Search::Stemmer.stem("テスト")
assert_equal "テ ス ト", result
end
test "split Korean characters for FTS indexing" do
result = Search::Stemmer.stem("테스트")
assert_equal "테 스 트", result
end
test "mixed CJK and English" do
result = Search::Stemmer.stem("running 测试 test")
assert_equal "run 测 试 test", result
end
test "mixed CJK and English without spaces" do
result = Search::Stemmer.stem("hello世界test")
assert_equal "hello 世 界 test", result
end
test "CJK punctuation is treated as separator" do
result = Search::Stemmer.stem("你好。世界")
assert_equal "你 好 世 界", result
end
end end