diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 561d78640..24e5b8727 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -10,7 +10,7 @@ --block-space-double: calc(var(--block-space) * 2); /* 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-mono: ui-monospace, monospace; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index c8453bc14..2ae664dcd 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -4,7 +4,7 @@ --comment-padding-block: var(--block-space-half); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; - --reaction-size: 1.6875rem; + --reaction-size: 2rem; display: flex; flex-direction: column; @@ -15,6 +15,10 @@ @media (min-width: 160ch) { padding-inline: var(--tray-size); } + + @media (min-width: 640px) { + --reaction-size: 1.6875rem; + } } .comments__subscribers { diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index 082fcf2fc..efc04e21d 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -19,6 +19,7 @@ --input-background: var(--color-canvas); } + &:has(dialog[open]), &:has([data-controller~="tooltip"]:hover) { z-index: calc(var(--z-nav) + 1); } diff --git a/app/assets/stylesheets/font-face.css b/app/assets/stylesheets/font-face.css new file mode 100644 index 000000000..086111ed4 --- /dev/null +++ b/app/assets/stylesheets/font-face.css @@ -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; + } +} diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 189cfef5b..02ae8969e 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -155,6 +155,10 @@ max-inline-size: 100%; padding: var(--inline-space-half) var(--popup-item-padding-inline); text-align: start; + + &:focus-visible { + z-index: 1; + } } .popup__icon { diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index 5dea448cc..d1cd0a03f 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -10,7 +10,7 @@ flex-wrap: wrap; gap: var(--inline-space-half); 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); &:has([open]) { @@ -44,6 +44,7 @@ } .reactions__trigger { + --btn-size: var(--reaction-size); --btn-border-color: var(--reaction-border-color); img { @@ -115,11 +116,13 @@ /* Make the avatar and delete buttons fit nicely within the reaction */ .reaction__avatar, + .reaction__avatar .avatar, .reaction__form-label, .reaction form { - block-size: var(--btn-size); + block-size: 100%; } + .reaction__delete { display: none; diff --git a/app/controllers/cards/drafts_controller.rb b/app/controllers/cards/drafts_controller.rb new file mode 100644 index 000000000..a4e9388de --- /dev/null +++ b/app/controllers/cards/drafts_controller.rb @@ -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 diff --git a/app/controllers/cards/publishes_controller.rb b/app/controllers/cards/publishes_controller.rb index 641f728cd..a0378eec3 100644 --- a/app/controllers/cards/publishes_controller.rb +++ b/app/controllers/cards/publishes_controller.rb @@ -5,7 +5,8 @@ class Cards::PublishesController < ApplicationController @card.publish 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 redirect_to @card.board end diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index e79eeb22b..e007527b8 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -3,6 +3,7 @@ class CardsController < ApplicationController before_action :set_board, only: %i[ create ] 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 ] def index @@ -13,7 +14,7 @@ class CardsController < ApplicationController respond_to do |format| format.html do card = Current.user.draft_new_card_in(@board) - redirect_to card + redirect_to card_draft_path(card) end format.json do @@ -56,6 +57,10 @@ class CardsController < ApplicationController @card = Current.user.accessible_cards.find_by!(number: params[:id]) end + def redirect_if_drafted + redirect_to card_draft_path(@card) if @card.drafted? + end + def ensure_permission_to_administer_card head :forbidden unless Current.user.can_administer_card?(@card) end diff --git a/app/models/search.rb b/app/models/search.rb index 3b8127ef1..f81b9a9d8 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -1,6 +1,4 @@ module Search - CJK_PATTERN = /\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/ - def self.table_name_prefix "search_" end diff --git a/app/models/search/highlighter.rb b/app/models/search/highlighter.rb index 8672ed3c8..d2b53d772 100644 --- a/app/models/search/highlighter.rb +++ b/app/models/search/highlighter.rb @@ -13,14 +13,8 @@ class Search::Highlighter result = text.dup terms.each do |term| - if term.match?(Search::CJK_PATTERN) - result.gsub!(/(#{Regexp.escape(term)})/i) do |match| - "#{OPENING_MARK}#{match}#{CLOSING_MARK}" - end - else - result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match| - "#{OPENING_MARK}#{match}#{CLOSING_MARK}" - end + result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match| + "#{OPENING_MARK}#{match}#{CLOSING_MARK}" end end diff --git a/app/models/search/query.rb b/app/models/search/query.rb index 6bf06ef42..5fe0829e4 100644 --- a/app/models/search/query.rb +++ b/app/models/search/query.rb @@ -33,7 +33,7 @@ class Search::Query < ApplicationRecord end def remove_invalid_search_characters(terms) - terms.gsub(/[^\p{L}\p{N}_"]/, " ") + terms.gsub(/[^\w"]/, " ") end def remove_unbalanced_quotes(terms) diff --git a/app/models/search/record/sqlite.rb b/app/models/search/record/sqlite.rb index 73b3b675f..ae0d34281 100644 --- a/app/models/search/record/sqlite.rb +++ b/app/models/search/record/sqlite.rb @@ -8,10 +8,9 @@ module Search::Record::SQLite has_one :search_records_fts, -> { with_rowid }, class_name: "Search::Record::SQLite::Fts", foreign_key: :rowid, primary_key: :id, dependent: :destroy - before_save :stem_content 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 class_methods do @@ -43,11 +42,6 @@ module Search::Record::SQLite end 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) return nil unless html.present? diff --git a/app/models/search/stemmer.rb b/app/models/search/stemmer.rb index 2ae0e1bf1..f6a6c56d4 100644 --- a/app/models/search/stemmer.rb +++ b/app/models/search/stemmer.rb @@ -5,43 +5,9 @@ module Search::Stemmer def stem(value) if value.present? - tokenize(value).join(" ") + value.gsub(/[^\w\s]/, "").split(/\s+/).map { |word| STEMMER.stem(word.downcase) }.join(" ") else value 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 diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index c85236683..d66996f96 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -37,11 +37,5 @@ <% if card.published? %> <%= 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 %> diff --git a/app/views/cards/drafts/_container.html.erb b/app/views/cards/drafts/_container.html.erb new file mode 100644 index 000000000..c74326544 --- /dev/null +++ b/app/views/cards/drafts/_container.html.erb @@ -0,0 +1,34 @@ +
+ <% cache card do %> +
+ <%= render "cards/container/image", card: card %> +
+ +
+ <%= card_article_tag card, class: "card" do %> +
+ <%= render "cards/display/perma/board", card: card %> + <%= render "cards/display/perma/tags", card: card %> +
+ +
+
+ <%= render "cards/container/content", card: card %> + <%= render "cards/display/perma/steps", card: card %> +
+
+ + + <% end %> +
+ <% end %> + + <% if Fizzy.saas? %> + <%= render "cards/container/footer/saas/create", card: card %> + <% else %> + <%= render "cards/container/footer/create", card: card %> + <% end %> +
diff --git a/app/views/cards/drafts/show.html.erb b/app/views/cards/drafts/show.html.erb new file mode 100644 index 000000000..d670f4c2f --- /dev/null +++ b/app/views/cards/drafts/show.html.erb @@ -0,0 +1,16 @@ +<% @page_title = @card.title %> +<% @header_class = "header--card" %> + +<% content_for :header do %> +
+ <%= link_back_to_board(@card.board) %> +
+<% end %> + +
+ <%= render "cards/drafts/container", card: @card %> + + <%= render "layouts/lightbox" do %> + <%= button_to_remove_card_image(@card) if @card.image.attached? %> + <% end %> +
diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index 596006ce8..bfaa9c676 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -16,7 +16,7 @@
<%= render "cards/container", card: @card %> - <%= render "cards/messages", card: @card unless @card.drafted? %> + <%= render "cards/messages", card: @card %> <%= render "layouts/lightbox" do %> <%= button_to_remove_card_image(@card) if @card.image.attached? %> diff --git a/config/environments/development.rb b/config/environments/development.rb index ace7b503d..05dc335c2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -26,7 +26,7 @@ Rails.application.configure do config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else - config.action_controller.perform_caching = true + config.action_controller.perform_caching = false config.cache_store = :null_store end diff --git a/config/routes.rb b/config/routes.rb index d84fa06e3..0fd75fd98 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,6 +71,7 @@ Rails.application.routes.draw do resources :cards do scope module: :cards do + resource :draft, only: :show resource :board resource :closure resource :column diff --git a/saas/test/controllers/accounts/subscriptions/card_creation_test.rb b/saas/test/controllers/accounts/subscriptions/card_creation_test.rb index dd5e72826..1ddc4bc1c 100644 --- a/saas/test/controllers/accounts/subscriptions/card_creation_test.rb +++ b/saas/test/controllers/accounts/subscriptions/card_creation_test.rb @@ -10,7 +10,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest test "admin sees nearing card limit notice" do 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_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 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_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 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_match /you’ve 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 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_match /you’ve 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 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_no_match /upgrade/i, response.body diff --git a/test/controllers/cards/drafts_controller_test.rb b/test/controllers/cards/drafts_controller_test.rb new file mode 100644 index 000000000..8d14f3fff --- /dev/null +++ b/test/controllers/cards/drafts_controller_test.rb @@ -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 diff --git a/test/controllers/cards/publishes_controller_test.rb b/test/controllers/cards/publishes_controller_test.rb index 4de6d20bb..537a6f73b 100644 --- a/test/controllers/cards/publishes_controller_test.rb +++ b/test/controllers/cards/publishes_controller_test.rb @@ -28,6 +28,6 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest new_card = Card.last assert new_card.drafted? - assert_redirected_to new_card + assert_redirected_to card_draft_path(new_card) end end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index d13affa96..11c4b62a5 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -21,7 +21,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest end card = Card.last - assert_redirected_to card + assert_redirected_to card_draft_path(card) assert card.drafted? end @@ -31,10 +31,17 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_no_difference -> { Card.count } do post board_cards_path(boards(:writebook)) - assert_redirected_to draft + assert_redirected_to card_draft_path(draft) 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 get card_path(cards(:logo)) assert_response :success diff --git a/test/models/search/highlighter_test.rb b/test/models/search/highlighter_test.rb index 822e4913d..a065c4da1 100644 --- a/test/models/search/highlighter_test.rb +++ b/test/models/search/highlighter_test.rb @@ -82,34 +82,6 @@ class Search::HighlighterTest < ActiveSupport::TestCase assert_equal "<script>#{mark('test')}</script>", result 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 def mark(text) "#{Search::Highlighter::OPENING_MARK}#{text}#{Search::Highlighter::CLOSING_MARK}" diff --git a/test/models/search/query_test.rb b/test/models/search/query_test.rb deleted file mode 100644 index 628b9e69e..000000000 --- a/test/models/search/query_test.rb +++ /dev/null @@ -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 diff --git a/test/models/search/stemmer_test.rb b/test/models/search/stemmer_test.rb index e2963ee10..858ed2dca 100644 --- a/test/models/search/stemmer_test.rb +++ b/test/models/search/stemmer_test.rb @@ -12,40 +12,4 @@ class Search::StemmerTest < ActiveSupport::TestCase assert_equal "test run jump walk", result 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