diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj
index cefd382b72..0ab7d9ae75 100644
--- a/backend/src/app/rpc/commands/profile.clj
+++ b/backend/src/app/rpc/commands/profile.clj
@@ -62,6 +62,7 @@
[:map {:title "ProfileProps" :closed true}
[:plugins {:optional true} ctp/schema:plugin-registry]
[:renderer {:optional true} [::sm/one-of #{:svg :wasm}]]
+ [:japanese-layout-all-files {:optional true} ::sm/boolean]
[:mcp-enabled {:optional true} ::sm/boolean]
[:newsletter-updates {:optional true} ::sm/boolean]
[:newsletter-news {:optional true} ::sm/boolean]
diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj
index b0cda30dd8..7475e389e2 100644
--- a/backend/test/backend_tests/rpc_profile_test.clj
+++ b/backend/test/backend_tests/rpc_profile_test.clj
@@ -164,6 +164,21 @@
(t/is (nil? (:error out)))
(t/is (nil? (:photo-id (:result out))))))))
+(t/deftest update-profile-japanese-layout-setting
+ (let [profile (th/create-profile* 1)
+ params {::th/type :update-profile-props
+ ::rpc/profile-id (:id profile)
+ :props {:japanese-layout-all-files true}}
+ out (th/command! params)]
+ (t/is (nil? (:error out)))
+ (t/is (true? (get-in out [:result :japanese-layout-all-files])))
+
+ (let [params {::th/type :get-profile
+ ::rpc/profile-id (:id profile)}
+ out (th/command! params)]
+ (t/is (nil? (:error out)))
+ (t/is (true? (get-in out [:result :props :japanese-layout-all-files]))))))
+
(t/deftest profile-deletion-1
(let [prof (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id prof)
diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc
index ce45edd485..baffa8d91d 100644
--- a/common/src/app/common/files/changes.cljc
+++ b/common/src/app/common/files/changes.cljc
@@ -438,7 +438,12 @@
[:map {:title "SetTokensSource"}
[:type [:= :set-tokens-source]]
[:file-id ::sm/uuid]
- [:library-id [:maybe ::sm/uuid]]]]])
+ [:library-id [:maybe ::sm/uuid]]]]
+
+ [:set-japanese-layout
+ [:map {:title "SetJapaneseLayout"}
+ [:type [:= :set-japanese-layout]]
+ [:enabled ::sm/boolean]]]])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
@@ -1092,6 +1097,10 @@
[data {:keys [library-id]}]
(cfo/set-tokens-source data library-id))
+(defmethod process-change :set-japanese-layout
+ [data {:keys [enabled]}]
+ (ctf/set-japanese-layout-enabled data enabled))
+
;; === Operations
(def decode-shape-attrs
diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc
index 3317d91653..d948196886 100644
--- a/common/src/app/common/files/changes_builder.cljc
+++ b/common/src/app/common/files/changes_builder.cljc
@@ -1150,6 +1150,18 @@
:library-id prev-val})
(apply-changes-local))))
+(defn set-japanese-layout
+ [changes enabled?]
+ (assert-file-data! changes)
+ (let [file-data (::file-data (meta changes))
+ previous-enabled (ctf/japanese-layout-enabled? file-data)]
+ (-> changes
+ (update :redo-changes conj {:type :set-japanese-layout
+ :enabled enabled?})
+ (update :undo-changes conj {:type :set-japanese-layout
+ :enabled previous-enabled})
+ (apply-changes-local))))
+
;; Misc changes
(defn reorder-children
diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc
index df4733c25a..6d4914ee06 100644
--- a/common/src/app/common/types/file.cljc
+++ b/common/src/app/common/types/file.cljc
@@ -77,7 +77,8 @@
(def schema:options
[:map {:title "FileOptions"}
[:components-v2 {:optional true} ::sm/boolean]
- [:base-font-size {:optional true} :string]])
+ [:base-font-size {:optional true} :string]
+ [:japanese-layout {:optional true} ::sm/boolean]])
(def schema:data
[:map {:title "FileData"}
@@ -1255,6 +1256,19 @@
(assoc-in file-data [:options :base-font-size] base-font-size))
+;; Japanese text layout
+
+(defn japanese-layout-enabled?
+ [file-data]
+ (true? (get-in file-data [:options :japanese-layout])))
+
+(defn set-japanese-layout-enabled
+ [file-data enabled?]
+ (if enabled?
+ (assoc-in file-data [:options :japanese-layout] true)
+ (d/update-in-when file-data [:options] dissoc :japanese-layout)))
+
+
;; Ref Chains
(defn get-ref-chain-until-target-ref
"Returns a vector with the shape ref chain until target-ref, including itself"
diff --git a/common/test/common_tests/files_changes_test.cljc b/common/test/common_tests/files_changes_test.cljc
index 935ff092fb..0d6dbc86f6 100644
--- a/common/test/common_tests/files_changes_test.cljc
+++ b/common/test/common_tests/files_changes_test.cljc
@@ -251,6 +251,26 @@
"The parent must use the updated bounds of both children")
(assert-idempotent data changes))))))
+(t/deftest set-japanese-layout-file-option
+ (let [file-id (uuid/custom 2 2)
+ page-id (uuid/custom 1 1)
+ data (make-file-data file-id page-id)
+ enabled (ch/process-changes data [{:type :set-japanese-layout
+ :enabled true}])
+ disabled (ch/process-changes enabled [{:type :set-japanese-layout
+ :enabled false}])
+ changes (-> (pcb/empty-changes)
+ (pcb/with-file-data data)
+ (pcb/set-japanese-layout true))]
+ (t/is (false? (ctf/japanese-layout-enabled? data)))
+ (t/is (true? (ctf/japanese-layout-enabled? enabled)))
+ (t/is (false? (ctf/japanese-layout-enabled? disabled)))
+ (t/is (not (contains? (:options disabled) :japanese-layout)))
+ (t/is (= [{:type :set-japanese-layout :enabled true}]
+ (:redo-changes changes)))
+ (t/is (= '({:type :set-japanese-layout :enabled false})
+ (:undo-changes changes)))))
+
(t/deftest add-obj
(let [file-id (uuid/custom 2 2)
page-id (uuid/custom 1 1)
diff --git a/frontend/resources/images/icons/text-combine-upright-all.svg b/frontend/resources/images/icons/text-combine-upright-all.svg
new file mode 100644
index 0000000000..594b7f49fd
--- /dev/null
+++ b/frontend/resources/images/icons/text-combine-upright-all.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/text-combine-upright-digits.svg b/frontend/resources/images/icons/text-combine-upright-digits.svg
new file mode 100644
index 0000000000..3e367a3fd3
--- /dev/null
+++ b/frontend/resources/images/icons/text-combine-upright-digits.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/text-combine-upright-none.svg b/frontend/resources/images/icons/text-combine-upright-none.svg
new file mode 100644
index 0000000000..f54cf6f1e3
--- /dev/null
+++ b/frontend/resources/images/icons/text-combine-upright-none.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/text-orientation-mixed.svg b/frontend/resources/images/icons/text-orientation-mixed.svg
new file mode 100644
index 0000000000..72cd592478
--- /dev/null
+++ b/frontend/resources/images/icons/text-orientation-mixed.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/text-orientation-upright.svg b/frontend/resources/images/icons/text-orientation-upright.svg
new file mode 100644
index 0000000000..67d241b2bb
--- /dev/null
+++ b/frontend/resources/images/icons/text-orientation-upright.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/warichu-none.svg b/frontend/resources/images/icons/warichu-none.svg
new file mode 100644
index 0000000000..7ede0d3d0a
--- /dev/null
+++ b/frontend/resources/images/icons/warichu-none.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/warichu.svg b/frontend/resources/images/icons/warichu.svg
new file mode 100644
index 0000000000..3348550253
--- /dev/null
+++ b/frontend/resources/images/icons/warichu.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/writing-mode-horizontal.svg b/frontend/resources/images/icons/writing-mode-horizontal.svg
new file mode 100644
index 0000000000..204e9190e7
--- /dev/null
+++ b/frontend/resources/images/icons/writing-mode-horizontal.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/writing-mode-vertical.svg b/frontend/resources/images/icons/writing-mode-vertical.svg
new file mode 100644
index 0000000000..e89a00a562
--- /dev/null
+++ b/frontend/resources/images/icons/writing-mode-vertical.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index c8e538560f..ccd7c1ecae 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -679,6 +679,17 @@
(dm/export layout/toggle-layout-flag)
(dm/export layout/remove-layout-flag)
+(defn set-japanese-layout
+ [enabled?]
+ (ptk/reify ::set-japanese-layout
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [file-data (dsh/lookup-file-data state)
+ changes (-> (pcb/empty-changes it)
+ (pcb/with-file-data file-data)
+ (pcb/set-japanese-layout enabled?))]
+ (rx/of (dch/commit-changes changes))))))
+
;; --- Profile
(defn update-nudge
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 84f6bd72f2..ddf143cae0 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -11,6 +11,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cph]
[app.common.files.tokens :as cfo]
+ [app.common.types.file :as ctf]
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
@@ -289,6 +290,14 @@
"Currently working file data on workspace"
(l/derived dsh/lookup-file-data st/state))
+(def japanese-layout-config
+ "Japanese layout switches: `:file?` for the workspace file and
+ `:all-files?` from the profile."
+ (l/derived (fn [state]
+ {:file? (ctf/japanese-layout-enabled? (dsh/lookup-file-data state))
+ :all-files? (true? (dm/get-in state [:profile :props :japanese-layout-all-files]))})
+ st/state =))
+
(def workspace-file-colors
(l/derived (fn [{:keys [id] :as data}]
(some-> (:colors data) (update-vals #(assoc % :file-id id))))
diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
index 991532c963..1278e80fd3 100644
--- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
+++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
@@ -276,6 +276,9 @@
(def ^:icon-id text-auto-height "text-auto-height")
(def ^:icon-id text-auto-width "text-auto-width")
(def ^:icon-id text-bottom "text-bottom")
+(def ^:icon-id text-combine-upright-all "text-combine-upright-all")
+(def ^:icon-id text-combine-upright-digits "text-combine-upright-digits")
+(def ^:icon-id text-combine-upright-none "text-combine-upright-none")
(def ^:icon-id text-fixed "text-fixed")
(def ^:icon-id text-font-family "text-font-family")
(def ^:icon-id text-font-size "text-font-size")
@@ -287,6 +290,8 @@
(def ^:icon-id text-ltr "text-ltr")
(def ^:icon-id text-middle "text-middle")
(def ^:icon-id text-mixed "text-mixed")
+(def ^:icon-id text-orientation-mixed "text-orientation-mixed")
+(def ^:icon-id text-orientation-upright "text-orientation-upright")
(def ^:icon-id text-palette "text-palette")
(def ^:icon-id text-paragraph "text-paragraph")
(def ^:icon-id text-rtl "text-rtl")
@@ -309,7 +314,11 @@
(def ^:icon-id vertical-align-items-start "vertical-align-items-start")
(def ^:icon-id view-as-icons "view-as-icons")
(def ^:icon-id view-as-list "view-as-list")
+(def ^:icon-id warichu "warichu")
+(def ^:icon-id warichu-none "warichu-none")
(def ^:icon-id wrap "wrap")
+(def ^:icon-id writing-mode-horizontal "writing-mode-horizontal")
+(def ^:icon-id writing-mode-vertical "writing-mode-vertical")
(def icon-list
"A collection of all icons"
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index 7dec276e5d..9d285841ad 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -247,6 +247,9 @@
(def ^:icon text-auto-height (icon-xref :text-auto-height))
(def ^:icon text-auto-width (icon-xref :text-auto-width))
(def ^:icon text-bottom (icon-xref :text-bottom))
+(def ^:icon text-combine-upright-all (icon-xref :text-combine-upright-all))
+(def ^:icon text-combine-upright-digits (icon-xref :text-combine-upright-digits))
+(def ^:icon text-combine-upright-none (icon-xref :text-combine-upright-none))
(def ^:icon text-fixed (icon-xref :text-fixed))
(def ^:icon text-font-family (icon-xref :text-font-family))
(def ^:icon text-font-size (icon-xref :text-font-size))
@@ -258,6 +261,8 @@
(def ^:icon text-ltr (icon-xref :text-ltr))
(def ^:icon text-middle (icon-xref :text-middle))
(def ^:icon text-mixed (icon-xref :text-mixed))
+(def ^:icon text-orientation-mixed (icon-xref :text-orientation-mixed))
+(def ^:icon text-orientation-upright (icon-xref :text-orientation-upright))
(def ^:icon text-palette (icon-xref :text-palette))
(def ^:icon text-paragraph (icon-xref :text-paragraph))
(def ^:icon text-rtl (icon-xref :text-rtl))
@@ -284,7 +289,11 @@
(def ^:icon vertical-align-items-start (icon-xref :vertical-align-items-start))
(def ^:icon view-as-icons (icon-xref :view-as-icons))
(def ^:icon view-as-list (icon-xref :view-as-list))
+(def ^:icon warichu (icon-xref :warichu))
+(def ^:icon warichu-none (icon-xref :warichu-none))
(def ^:icon wrap (icon-xref :wrap))
+(def ^:icon writing-mode-horizontal (icon-xref :writing-mode-horizontal))
+(def ^:icon writing-mode-vertical (icon-xref :writing-mode-vertical))
(def default
"A collection of all icons"
diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs
index c4d711ba79..dee7ba1d0a 100644
--- a/frontend/src/app/main/ui/settings/options.cljs
+++ b/frontend/src/app/main/ui/settings/options.cljs
@@ -115,10 +115,35 @@
:on-change handle-render-change}]]
[:> text* {:typography t/body-medium :class (stl/css :feedback)} [:a {:href "#" :on-click go-settings-feedback :class (stl/css :link)} (tr "dashboard.webgl-switch.feedback") [:> icon* {:icon-id "arrow-up-right" :size "s"}]]]]))
+(mf/defc japanese-layout-settings*
+ [{:keys [enabled]}]
+ (let [handle-change
+ (mf/use-fn
+ (fn [enabled?]
+ (st/emit! (ev/event {::ev/name (if enabled?
+ "enable-japanese-layout-all-files"
+ "disable-japanese-layout-all-files")
+ ::ev/origin "settings"})
+ (du/update-profile-props
+ {:japanese-layout-all-files enabled?})
+ (ntf/success (tr "notifications.profile-saved")))))]
+ [:section {:class (stl/css :japanese-layout-container)}
+ [:> heading* {:class (stl/css :title)
+ :level 2
+ :typography t/title-large}
+ (tr "dashboard.japanese-layout.title")]
+ [:> text* {:class (stl/css :description) :typography t/body-medium}
+ (tr "dashboard.japanese-layout.description")]
+ [:form {:class (stl/css :japanese-layout-form)}
+ [:> switch* {:label (tr "dashboard.japanese-layout.enable-all-files")
+ :default-checked enabled
+ :on-change handle-change}]]]))
+
(mf/defc options-page*
[]
(let [profile (mf/deref refs/profile)
- renderer (or (-> profile :props :renderer) :svg)]
+ renderer (or (-> profile :props :renderer) :svg)
+ japanese-layout-enabled (:all-files? (mf/deref refs/japanese-layout-config))]
(mf/use-effect
#(dom/set-html-title (tr "title.settings.options")))
@@ -132,5 +157,6 @@
:id "options-section-title"}
(tr "labels.settings")]
[:> options-form*]]
+ [:> japanese-layout-settings* {:enabled japanese-layout-enabled}]
(when (contains? cf/flags :render-switch)
[:> webgl-settings* {:renderer renderer}])]]))
diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss
index cdae1e185b..af13b89b95 100644
--- a/frontend/src/app/main/ui/settings/options.scss
+++ b/frontend/src/app/main/ui/settings/options.scss
@@ -21,9 +21,9 @@
}
}
-/* Copied from profile.scss .form-container, but without included nested
- rules, since we want custom styles for it */
-.webgl-container {
+/* Settings sections with standalone controls. */
+.webgl-container,
+.japanese-layout-container {
display: grid;
grid-auto-rows: auto;
gap: var(--sp-s);
@@ -34,6 +34,10 @@
color: var(--color-foreground-primary);
}
+.japanese-layout-form {
+ padding-block-start: var(--sp-xs);
+}
+
.webgl-header {
display: flex;
flex-direction: row;
diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs
index 79d2b80a20..93c5ffd753 100644
--- a/frontend/src/app/main/ui/workspace/main_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/main_menu.cljs
@@ -45,6 +45,7 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
+ [app.util.text.writing-mode :as wm]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@@ -233,8 +234,10 @@
(mf/defc preferences-menu*
{::mf/private true
::mf/wrap [mf/memo]}
- [{:keys [layout profile toggle-flag on-close toggle-theme toggle-render]}]
- (let [renderer (or (-> profile :props :renderer) :svg)
+ [{:keys [japanese-layout-enabled japanese-layout-all-files layout profile toggle-flag
+ on-close toggle-theme toggle-render toggle-japanese-layout]}]
+ (let [renderer (or (-> profile :props :renderer) :svg)
+ read-only? (mf/use-ctx ctx/workspace-read-only?)
show-nudge-options
(mf/use-fn
@@ -317,6 +320,21 @@
:id "file-menu-nudge"}
[:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]]
+ (when (and (not read-only?)
+ (not japanese-layout-all-files)
+ (wm/vertical-layout-active?))
+ [:> dropdown-menu-item* {:on-click toggle-japanese-layout
+ :class (stl/css :base-menu-item :submenu-item)
+ :on-key-down (fn [event]
+ (when (kbd/enter? event)
+ (toggle-japanese-layout event)))
+ :data-testid "japanese-layout"
+ :id "file-menu-japanese-layout"}
+ [:span {:class (stl/css :item-name)}
+ (if japanese-layout-enabled
+ (tr "workspace.header.menu.disable-japanese-layout")
+ (tr "workspace.header.menu.enable-japanese-layout"))]])
+
[:> dropdown-menu-item* {:on-click toggle-theme
:class (stl/css :base-menu-item :submenu-item)
:on-key-down (fn [event]
@@ -886,6 +904,8 @@
(mf/defc menu*
[{:keys [layout file]}]
(let [profile (mf/deref refs/profile)
+ japanese-layout (mf/deref refs/japanese-layout-config)
+ japanese-layout-enabled (:file? japanese-layout)
show-menu* (mf/use-state false)
show-menu? (deref show-menu*)
@@ -947,6 +967,15 @@
(dom/stop-propagation event)
(st/emit! (du/toggle-theme))))
+ toggle-japanese-layout
+ (mf/use-fn
+ (mf/deps japanese-layout-enabled)
+ (fn [event]
+ (dom/stop-propagation event)
+ (st/emit! (dw/set-japanese-layout (not japanese-layout-enabled)))
+ (reset! show-menu* false)
+ (reset! selected-sub-menu* nil)))
+
toggle-render
(mf/use-fn
(mf/deps profile)
@@ -1146,10 +1175,13 @@
:preferences
[:> preferences-menu* {:layout layout
:profile profile
+ :japanese-layout-enabled japanese-layout-enabled
+ :japanese-layout-all-files (:all-files? japanese-layout)
:toggle-flag toggle-flag
:toggle-theme toggle-theme
:toggle-render toggle-render
:show-shortcuts show-shortcuts
+ :toggle-japanese-layout toggle-japanese-layout
:on-close close-sub-menu}]
:plugins
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
index ad8bcc2a9c..10b430c7a3 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.sidebar.options.common
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st]
[app.util.dom :as dom]
@@ -26,6 +27,20 @@
:ref ref}
children])))
+(defn radio-selected
+ "Value to select in a radio group for an attribute value: its name, or
+ `default` when unset. Mixed (`:multiple`) values select nothing."
+ ([value]
+ (radio-selected value ""))
+ ([value default]
+ (cond
+ (= value :multiple) ""
+ (or (nil? value)
+ (and (string? value) (empty? value))) default
+ (keyword? value) (d/name value)
+ (string? value) value
+ :else (str value))))
+
(defn emit-value-or-token [value emit-value-fn ids attrs]
(if (or (string? value)
(number? value)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
index 61ee90bf07..b571baed15 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
@@ -30,6 +30,8 @@
[app.main.ui.ds.controls.shared.searchable-options-dropdown :refer [searchable-options-dropdown*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.hooks :as hooks]
+ [app.main.ui.workspace.sidebar.options.common :refer [radio-selected]]
+ [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl]
[app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry*]]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
@@ -52,10 +54,6 @@
Evaluated once at module load time; cf/flags is immutable after startup."
(contains? cf/flags :token-typography-row))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Sub-components
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
(mf/defc text-align-options*
[{:keys [values on-change on-blur]}]
(let [options
@@ -85,14 +83,14 @@
(when (some? on-blur) (on-blur))))]
[:div {:class (stl/css :align-options)}
- [:> radio-buttons* {:selected (:text-align values)
+ [:> radio-buttons* {:selected (radio-selected (:text-align values))
:on-change handle-change
:name "align-text-options"
:options options}]]))
(mf/defc text-direction-options*
[{:keys [values on-change on-blur]}]
- (let [direction (:text-direction values)
+ (let [direction (radio-selected (:text-direction values))
options
(mf/with-memo []
[{:value "ltr"
@@ -119,7 +117,7 @@
(mf/defc vertical-align*
[{:keys [values on-change on-blur]}]
- (let [vertical-align (or (:vertical-align values) "top")
+ (let [vertical-align (radio-selected (:vertical-align values) "top")
options
(mf/with-memo []
[{:value "top"
@@ -151,6 +149,7 @@
(mf/defc grow-options*
[{:keys [ids values on-blur]}]
(let [grow-type (:grow-type values)
+ selected (radio-selected grow-type)
editor-instance (mf/deref refs/workspace-editor)
options
(mf/with-memo []
@@ -191,14 +190,14 @@
(when (some? on-blur) (on-blur))))]
[:div {:class (stl/css :grow-options)}
- [:> radio-buttons* {:selected (d/name grow-type)
+ [:> radio-buttons* {:selected selected
:on-change handle-change
:name "grow-text-options"
:options options}]]))
(mf/defc text-decoration-options*
[{:keys [values on-change on-blur token-applied]}]
- (let [text-decoration (some-> (:text-decoration values) d/name)
+ (let [text-decoration (radio-selected (:text-decoration values))
options
(mf/with-memo [token-applied]
[{:value "underline"
@@ -222,8 +221,8 @@
[:div {:class (stl/css :text-decoration-options)}
[:> radio-buttons* {:selected (if (= text-decoration "none")
- nil
- text-decoration)
+ ""
+ (radio-selected text-decoration))
:on-change handle-change
:name "text-decoration-options"
:disabled (and token-typography-row-enabled? (some? token-applied))
@@ -246,8 +245,10 @@
(d/seek #(= (:id %) (uuid/uuid id)))))
(defn- check-props [n-props o-props]
- (let [o-values (unchecked-get o-props "values")
- n-values (unchecked-get n-props "values")]
+ (let [o-values (unchecked-get o-props "values")
+ n-values (unchecked-get n-props "values")
+ o-ruby-values (unchecked-get o-props "rubyValues")
+ n-ruby-values (unchecked-get n-props "rubyValues")]
(and (identical? (unchecked-get n-props "ids")
(unchecked-get o-props "ids"))
(identical? (unchecked-get n-props "type")
@@ -258,6 +259,9 @@
(unchecked-get o-props "fileId"))
(identical? (unchecked-get n-props "typographies")
(unchecked-get o-props "typographies"))
+ (identical? (unchecked-get n-props "textSelectionActive")
+ (unchecked-get o-props "textSelectionActive"))
+ (= n-ruby-values o-ruby-values)
(identical? (get o-values :fills)
(get n-values :fills))
(identical? (get o-values :font-family)
@@ -284,6 +288,32 @@
(get n-values :text-decoration))
(identical? (get o-values :text-direction)
(get n-values :text-direction))
+ (identical? (get o-values :writing-mode)
+ (get n-values :writing-mode))
+ (identical? (get o-values :text-orientation)
+ (get n-values :text-orientation))
+ (identical? (get o-values :text-combine-upright)
+ (get n-values :text-combine-upright))
+ (identical? (get o-values :warichu)
+ (get n-values :warichu))
+ (identical? (get o-values :font-features)
+ (get n-values :font-features))
+ (identical? (get o-values :annotation-clearance)
+ (get n-values :annotation-clearance))
+ (identical? (get o-values :text-emphasis)
+ (get n-values :text-emphasis))
+ (identical? (get o-values :ruby)
+ (get n-values :ruby))
+ (identical? (get o-values :ruby-hidden)
+ (get n-values :ruby-hidden))
+ (identical? (get o-values :ruby-size)
+ (get n-values :ruby-size))
+ (identical? (get o-values :ruby-align)
+ (get n-values :ruby-align))
+ (identical? (get o-values :ruby-overhang)
+ (get n-values :ruby-overhang))
+ (identical? (get o-values :ruby-side)
+ (get n-values :ruby-side))
(identical? (get o-values :text-transform)
(get n-values :text-transform))
(identical? (get o-values :typography-ref-file)
@@ -291,7 +321,9 @@
(identical? (get o-values :typography-ref-id)
(get n-values :typography-ref-id))
(identical? (get o-values :vertical-align)
- (get n-values :vertical-align)))))
+ (get n-values :vertical-align))
+ (identical? (get o-values :line-adjustment)
+ (get n-values :line-adjustment)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Main component
@@ -299,14 +331,20 @@
(mf/defc text-menu*
{::mf/wrap [#(mf/memo' % check-props)]}
- [{:keys [ids type values applied-tokens libraries file-id typographies]}]
+ [{:keys [ids type values ruby-values text-selection-active
+ applied-tokens libraries file-id typographies]}]
(let [;; --- UI state
menu-state* (mf/use-state {:main-menu true
- :more-options false})
+ :more-options false
+ :japanese-layout true})
menu-state (deref menu-state*)
main-menu-open? (:main-menu menu-state)
more-options-open? (:more-options menu-state)
+ japanese-layout-open? (:japanese-layout menu-state)
+
+ japanese-layout-config-enabled?
+ (tjl/japanese-layout-config-enabled? (mf/deref refs/japanese-layout-config))
font-id (or (:font-id values) (:font-id txt/default-typography))
@@ -397,6 +435,10 @@
(mf/use-fn
#(swap! menu-state* update :more-options not))
+ toggle-japanese-layout
+ (mf/use-fn
+ #(swap! menu-state* update :japanese-layout not))
+
toggle-token-dropdown
(mf/use-fn
#(swap! token-dropdown-open* not))
@@ -427,6 +469,14 @@
(fn [attrs]
(emit-update! ids attrs)))
+ on-ruby-presentation-change
+ (mf/use-fn
+ (mf/deps ids on-change text-selection-active)
+ (fn [attrs]
+ (if text-selection-active
+ (on-change attrs)
+ (st/emit! (dwt/update-all-ruby-presentation ids attrs)))))
+
on-convert-to-typography
(mf/use-fn
(mf/deps values ids file-id emit-update!)
@@ -485,7 +535,17 @@
:values values
:on-change on-change
:show-recent true
- :on-blur on-text-blur})]
+ :on-blur on-text-blur})
+
+ japanese-layout-props
+ (mf/props
+ {:ids ids
+ :values values
+ :ruby-values ruby-values
+ :text-selection-active text-selection-active
+ :on-change on-change
+ :on-ruby-presentation-change on-ruby-presentation-change
+ :on-blur on-text-blur})]
(hooks/use-stream
expand-stream
@@ -503,97 +563,114 @@
(when token-dropdown-open?
(ts/schedule 0 #(some-> (mf/ref-val dropdown-ref) dom/focus!))))
- [:section {:class (stl/css :element-set)
- ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`).
- :data-keep-editing-on-blur true
- :aria-label (tr "workspace.options.text-options.text-section")}
- [:div {:class (stl/css :element-title)}
- [:> title-bar* {:collapsable true
- :collapsed (not main-menu-open?)
- :on-collapsed toggle-main-menu
- :title label
- :class (stl/css :title-spacing-text)}
- [:*
- (when (and token-typography-row-enabled? (some? (resolve-delay typography-tokens)) (not typography))
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
- :on-click toggle-token-dropdown
- :tooltip-placement "top-left"
- :icon i/tokens}])
- (when (and (not typography) (not multiple?) (not applied-token-name))
- [:> icon-button* {:variant "ghost"
- :aria-label (if (not font)
- (tr "workspace.options.font-not-available" (:font-family values))
- (tr "workspace.options.convert-to-typography"))
- :on-click on-convert-to-typography
- :tooltip-placement "top-left"
- :disabled (not font)
- :icon i/add}])]]
- (when (and token-typography-row-enabled? token-dropdown-open?)
- [:> searchable-options-dropdown* {:on-click on-option-click
- :id listbox-id
- :options (resolve-delay dropdown-options)
- :selected selected-token-id
- :align "right"
- :placeholder (tr "workspace.tokens.search-by-token")
- :ref set-option-ref}])]
-
- (when main-menu-open?
- [:div {:class (stl/css :element-content)}
- (cond
- (and token-typography-row-enabled? (= :multiple current-token-name) (= typography-id :multiple))
- [:div {:class (stl/css :multiple-typography)}
- [:span {:class (stl/css :multiple-text)}
- (tr "workspace.libraries.text.mixed-tokens-and-assets")]]
-
- (and token-typography-row-enabled? (= :multiple current-token-name))
- [:div {:class (stl/css :multiple-typography)}
- [:span {:class (stl/css :multiple-text)}
- (tr "workspace.libraries.text.mixed-tokens")]
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "workspace.libraries.text.multiple-token-tooltip")
+ [:*
+ [:section {:class (stl/css :element-set)
+ ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`).
+ :data-keep-editing-on-blur true
+ :aria-label (tr "workspace.options.text-options.text-section")}
+ [:div {:class (stl/css :element-title)}
+ [:> title-bar* {:collapsable true
+ :collapsed (not main-menu-open?)
+ :on-collapsed toggle-main-menu
+ :title label
+ :class (stl/css :title-spacing-text)}
+ [:*
+ (when (and token-typography-row-enabled? (some? (resolve-delay typography-tokens)) (not typography))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
+ :on-click toggle-token-dropdown
:tooltip-placement "top-left"
- :on-click handle-detach-all-tokens
- :icon i/detach}]]
-
- (and token-typography-row-enabled? current-token-name)
- [:> token-typography-row* {:token-name current-token-name
- :detach-token detach-token
- :active-tokens (resolve-delay typography-tokens)}]
-
- (= typography-id :multiple)
- [:div {:class (stl/css :multiple-typography)}
- [:span {:class (stl/css :multiple-text)}
- (tr "workspace.libraries.text.mixed-typography")]
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "workspace.libraries.text.multiple-assets-tooltip")
- :on-click handle-detach-typography
+ :icon i/tokens}])
+ (when (and (not typography) (not multiple?) (not applied-token-name))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (if (not font)
+ (tr "workspace.options.font-not-available" (:font-family values))
+ (tr "workspace.options.convert-to-typography"))
+ :on-click on-convert-to-typography
:tooltip-placement "top-left"
- :icon i/detach}]]
+ :disabled (not font)
+ :icon i/add}])]]
+ (when (and token-typography-row-enabled? token-dropdown-open?)
+ [:> searchable-options-dropdown* {:on-click on-option-click
+ :id listbox-id
+ :options (resolve-delay dropdown-options)
+ :selected selected-token-id
+ :align "right"
+ :placeholder (tr "workspace.tokens.search-by-token")
+ :ref set-option-ref}])]
- typography
- [:> typography-entry* {:file-id typography-file-id
- :typography typography
- :is-local (= typography-file-id file-id)
- :on-detach handle-detach-typography
- :on-change handle-change-typography}]
+ (when main-menu-open?
+ [:div {:class (stl/css :element-content)}
+ (cond
+ (and token-typography-row-enabled? (= :multiple current-token-name) (= typography-id :multiple))
+ [:div {:class (stl/css :multiple-typography)}
+ [:span {:class (stl/css :multiple-text)}
+ (tr "workspace.libraries.text.mixed-tokens-and-assets")]]
+ (and token-typography-row-enabled? (= :multiple current-token-name))
+ [:div {:class (stl/css :multiple-typography)}
+ [:span {:class (stl/css :multiple-text)}
+ (tr "workspace.libraries.text.mixed-tokens")]
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.libraries.text.multiple-token-tooltip")
+ :tooltip-placement "top-left"
+ :on-click handle-detach-all-tokens
+ :icon i/detach}]]
+ (and token-typography-row-enabled? current-token-name)
+ [:> token-typography-row* {:token-name current-token-name
+ :detach-token detach-token
+ :active-tokens (resolve-delay typography-tokens)}]
- :else
- [:> text-options* common-props])
+ (= typography-id :multiple)
+ [:div {:class (stl/css :multiple-typography)}
+ [:span {:class (stl/css :multiple-text)}
+ (tr "workspace.libraries.text.mixed-typography")]
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.libraries.text.multiple-assets-tooltip")
+ :on-click handle-detach-typography
+ :tooltip-placement "top-left"
+ :icon i/detach}]]
- [:div {:class (stl/css :text-align-options)}
- [:> text-align-options* common-props]
- [:> grow-options* common-props]
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "labels.options")
- :data-testid "text-align-options-button"
- :on-click toggle-more-options
- :icon i/menu}]]
+ typography
+ [:> typography-entry* {:file-id typography-file-id
+ :typography typography
+ :is-local (= typography-file-id file-id)
+ :on-detach handle-detach-typography
+ :on-change handle-change-typography}]
- (when more-options-open?
- [:div {:class (stl/css :text-decoration-options)}
- [:> vertical-align* common-props]
- [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})]
- [:> text-direction-options* common-props]])])]))
+ :else
+ [:> text-options* common-props])
+
+ [:div {:class (stl/css :text-align-options)}
+ [:> text-align-options* common-props]
+ [:> grow-options* common-props]
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "labels.options")
+ :data-testid "text-align-options-button"
+ :on-click toggle-more-options
+ :icon i/menu}]]
+
+ (when japanese-layout-config-enabled?
+ [:> tjl/line-adjustment-option* common-props])
+
+ (when more-options-open?
+ [:div {:class (stl/css :text-decoration-options)}
+ [:> vertical-align* common-props]
+ [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})]
+ [:> text-direction-options* common-props]])])]
+
+ (when japanese-layout-config-enabled?
+ [:section {:class (stl/css :element-set)
+ ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`).
+ :data-keep-editing-on-blur true
+ :aria-label (tr "workspace.options.text-options.japanese-layout")}
+ [:div {:class (stl/css :element-title)}
+ [:> title-bar* {:collapsable true
+ :collapsed (not japanese-layout-open?)
+ :on-collapsed toggle-japanese-layout
+ :title (tr "workspace.options.text-options.japanese-layout")
+ :class (stl/css :title-spacing-text)}]]
+ (when japanese-layout-open?
+ [:div {:class (stl/css :element-content)}
+ [:> tjl/japanese-layout-options* japanese-layout-props]])])]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
new file mode 100644
index 0000000000..8714f09158
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
@@ -0,0 +1,523 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC Sucursal en España SL
+
+(ns app.main.ui.workspace.sidebar.options.menus.text-japanese-layout
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.types.text.cjk-layout :as cjk]
+ [app.main.ui.components.title-bar :refer [title-bar*]]
+ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.controls.input :refer [input*]]
+ [app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
+ [app.main.ui.ds.controls.select :refer [select*]]
+ [app.main.ui.ds.controls.switch :refer [switch*]]
+ [app.main.ui.ds.foundations.assets.icon :as i]
+ [app.main.ui.workspace.sidebar.options.common :refer [advanced-options* radio-selected]]
+ [app.util.dom :as dom]
+ [app.util.i18n :refer [tr]]
+ [app.util.text.writing-mode :as wm]
+ [rumext.v2 :as mf]))
+
+(def ^:private mixed-span-values
+ #{:mixed :multiple "mixed" "multiple"})
+
+(defn- mixed-span-value?
+ [value]
+ (contains? mixed-span-values value))
+
+(defn- span-input-value
+ [value]
+ (if (mixed-span-value? value)
+ "mixed"
+ (radio-selected value)))
+
+(defn span-select-value
+ [value default]
+ (if (mixed-span-value? value)
+ "mixed"
+ (radio-selected value default)))
+
+(defn with-mixed-span-option
+ [options value]
+ (cond-> options
+ (mixed-span-value? value)
+ (conj {:id "mixed"
+ :label (tr "labels.mixed-values")
+ :disabled true
+ :dimmed true})))
+
+(defn japanese-layout-config-enabled?
+ "True under the WASM renderer when Japanese layout is enabled for the file
+ or in the user's profile. `config` is a `refs/japanese-layout-config`
+ value."
+ [{:keys [file? all-files?]}]
+ (and ^boolean (wm/vertical-layout-active?)
+ (boolean (or file? all-files?))))
+
+(defn vertical-japanese-layout?
+ "True when the Japanese layout controls are editing vertical text."
+ [values]
+ (= "vertical-rl" (:writing-mode values)))
+
+(defn proportional-metrics-feature
+ "Proportional metrics feature for the writing mode: `vpal` or `palt`."
+ [writing-mode]
+ (if (= writing-mode "vertical-rl") "vpal" "palt"))
+
+(defn text-combine-upright-options
+ [text-selection-active translate]
+ (cond-> [{:value "none"
+ :id "none-text-combine-upright"
+ :label (translate "workspace.options.text-options.text-combine-upright-none")
+ :icon i/text-combine-upright-none}]
+ text-selection-active
+ (conj {:value "all"
+ :id "all-text-combine-upright"
+ :label (translate "workspace.options.text-options.text-combine-upright-all")
+ :icon i/text-combine-upright-all})
+
+ true
+ (conj {:value "digits"
+ :id "digits-text-combine-upright"
+ :label (translate "workspace.options.text-options.text-combine-upright-digits")
+ :icon i/text-combine-upright-digits})))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Sub-components
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(mf/defc attr-radio-options*
+ "Radio group bound to one text attribute."
+ [{:keys [values on-change on-blur attr default-value name class options]}]
+ (let [selected (radio-selected (get values attr) default-value)
+ handle-change
+ (mf/use-fn
+ (mf/deps attr on-change on-blur)
+ (fn [value]
+ (on-change {attr value})
+ (when (some? on-blur) (on-blur))))]
+ [:div {:class class}
+ [:> radio-buttons* {:selected selected
+ :on-change handle-change
+ :name name
+ :options options}]]))
+
+(defn writing-mode-options
+ [translate]
+ [{:value "horizontal-tb"
+ :id "horizontal-tb-writing-mode"
+ :label (translate "workspace.options.text-options.writing-mode-horizontal")
+ :icon i/writing-mode-horizontal}
+ {:value "vertical-rl"
+ :id "vertical-rl-writing-mode"
+ :label (translate "workspace.options.text-options.writing-mode-vertical")
+ :icon i/writing-mode-vertical}])
+
+(defn text-orientation-options
+ [translate]
+ [{:value "mixed"
+ :id "mixed-text-orientation"
+ :label (translate "workspace.options.text-options.text-orientation-mixed")
+ :icon i/text-orientation-mixed}
+ {:value "upright"
+ :id "upright-text-orientation"
+ :label (translate "workspace.options.text-options.text-orientation-upright")
+ :icon i/text-orientation-upright}])
+
+;; Warichu (割注): renders the selection as two half-size lines in one inline
+;; position (top/bottom horizontally, right/left vertically).
+(defn warichu-options
+ [translate]
+ [{:value "none"
+ :id "none-warichu"
+ :label (translate "workspace.options.text-options.warichu-none")
+ :icon i/warichu-none}
+ {:value "warichu"
+ :id "warichu-warichu"
+ :label (translate "workspace.options.text-options.warichu")
+ :icon i/warichu}])
+
+;; select* matches options by :id, so each id is the stored value.
+(defn text-combine-upright-count-options
+ [translate]
+ [{:id "digits2"
+ :label (translate "workspace.options.text-options.text-combine-upright-digits-2")}
+ {:id "digits3"
+ :label (translate "workspace.options.text-options.text-combine-upright-digits-3")}
+ {:id "digits"
+ :label (translate "workspace.options.text-options.text-combine-upright-digits-4")}])
+
+(defn ruby-size-options
+ [translate]
+ [{:id "half" :label (translate "workspace.options.text-options.ruby-size-half")}
+ {:id "third" :label (translate "workspace.options.text-options.ruby-size-third")}
+ {:id "quarter" :label (translate "workspace.options.text-options.ruby-size-quarter")}])
+
+(defn ruby-align-options
+ [translate]
+ [{:id "space-around" :label (translate "workspace.options.text-options.ruby-align-space-around")}
+ {:id "center" :label (translate "workspace.options.text-options.ruby-align-center")}
+ {:id "start" :label (translate "workspace.options.text-options.ruby-align-start")}
+ {:id "space-between" :label (translate "workspace.options.text-options.ruby-align-space-between")}])
+
+(defn ruby-overhang-options
+ [translate]
+ [{:id "auto" :label (translate "workspace.options.text-options.ruby-overhang-auto")}
+ {:id "none" :label (translate "workspace.options.text-options.ruby-overhang-none")}])
+
+(defn ruby-side-options
+ [translate]
+ [{:id "over" :label (translate "workspace.options.text-options.ruby-side-over")}
+ {:id "under" :label (translate "workspace.options.text-options.ruby-side-under")}])
+
+(mf/defc text-combine-upright-options*
+ ;; Digit TCY finds eligible runs itself, so it applies to a whole shape;
+ ;; `all` is offered only for a text selection.
+ [{:keys [values on-change on-blur text-selection-active]}]
+ (let [text-combine-upright (radio-selected (:text-combine-upright values)
+ (cjk/enum-default :text-combine-upright))
+ selected (if (cjk/digit-combine? text-combine-upright) "digits" text-combine-upright)
+ options
+ (mf/with-memo [text-selection-active]
+ (text-combine-upright-options text-selection-active tr))
+
+ handle-change
+ (mf/use-fn
+ (mf/deps on-change on-blur)
+ (fn [value]
+ (on-change {:text-combine-upright value})
+ (when (some? on-blur) (on-blur))))]
+
+ [:div {:class (stl/css :text-combine-upright-options)}
+ [:> radio-buttons* {:selected selected
+ :on-change handle-change
+ :name "text-combine-upright-options"
+ :options options}]]))
+
+(mf/defc text-combine-upright-count-options*
+ [{:keys [values on-change on-blur]}]
+ (let [text-combine-upright (radio-selected (:text-combine-upright values)
+ (cjk/enum-default :text-combine-upright))
+ digits? (cjk/digit-combine? text-combine-upright)
+ options (mf/with-memo [] (text-combine-upright-count-options tr))
+
+ handle-change
+ (mf/use-fn
+ (mf/deps on-change on-blur)
+ (fn [value]
+ (on-change {:text-combine-upright value})
+ (when (some? on-blur) (on-blur))))]
+
+ (when digits?
+ [:div {:class (stl/css :japanese-select-option)}
+ [:span {:class (stl/css :japanese-option-label)}
+ (tr "workspace.options.text-options.text-combine-upright-digits-count")]
+ [:> select* {:default-selected text-combine-upright
+ :aria-label (tr "workspace.options.text-options.text-combine-upright-digits-count")
+ :options options
+ :on-change handle-change}]])))
+
+(mf/defc ruby-options*
+ [{:keys [values on-change on-blur advanced-open on-toggle-advanced]}]
+ (let [ruby (span-input-value (:ruby values))
+ ruby* (mf/use-state ruby)
+ dirty* (mf/use-state false)
+ value (deref ruby*)
+ dirty? (deref dirty*)
+
+ commit!
+ (mf/use-fn
+ (mf/deps dirty? value on-change on-blur)
+ (fn []
+ (when dirty?
+ (on-change {:ruby value})
+ (reset! dirty* false))
+ (when (some? on-blur) (on-blur))))
+
+ handle-change
+ (mf/use-fn
+ (fn [event]
+ (reset! dirty* true)
+ (reset! ruby* (dom/get-target-val event))))
+
+ handle-key-down
+ (mf/use-fn
+ (mf/deps commit!)
+ (fn [event]
+ (when (= "Enter" (.-key event))
+ (dom/blur! (dom/get-target event)))))]
+
+ (mf/with-effect [ruby]
+ (reset! ruby* ruby)
+ (reset! dirty* false))
+
+ [:div {:class (stl/css :ruby-options)}
+ [:> input* {:class (stl/css :ruby-input)
+ :label (tr "workspace.options.text-options.ruby")
+ :placeholder (tr "workspace.options.text-options.ruby-placeholder")
+ :value value
+ :on-change handle-change
+ :on-blur commit!
+ :on-key-down handle-key-down}]
+ [:> icon-button* {:variant "ghost"
+ :selected advanced-open
+ :aria-label (tr "workspace.options.text-options.ruby-advanced-options")
+ :aria-expanded advanced-open
+ :data-testid "ruby-advanced-options-toggle"
+ :on-click on-toggle-advanced
+ :icon i/menu}]]))
+
+(mf/defc span-select-option*
+ "Select bound to one span attribute, with a disabled entry for mixed values."
+ [{:keys [values on-change on-blur attr default-value label options]}]
+ (let [value (get values attr)
+ selected (span-select-value value default-value)
+ options (with-mixed-span-option options value)
+ handle-change
+ (mf/use-fn
+ (mf/deps attr on-change on-blur)
+ (fn [value]
+ (on-change {attr value})
+ (when (some? on-blur) (on-blur))))]
+ [:div {:class (stl/css :japanese-select-option)}
+ [:span {:class (stl/css :japanese-option-label)} label]
+ [:> select* {:default-selected selected
+ :aria-label label
+ :options options
+ :on-change handle-change}]]))
+
+(defn- ruby-common-props
+ [values on-change on-blur]
+ (mf/props
+ {:values values
+ :on-change on-change
+ :on-blur on-blur}))
+
+(mf/defc ruby-hidden-option*
+ [{:keys [values on-change on-blur]}]
+ (let [value (:ruby-hidden values)
+ enabled? (cond
+ (mixed-span-value? value) nil
+ (true? value) true
+ :else false)
+ label (tr "workspace.options.text-options.ruby-hidden")
+ handle-change
+ (mf/use-fn
+ (mf/deps on-change on-blur)
+ (fn [checked?]
+ (on-change {:ruby-hidden checked?})
+ (when (some? on-blur) (on-blur))))]
+ [:div {:class (stl/css :ruby-hidden-option :japanese-select-option)}
+ [:span {:class (stl/css :japanese-option-label)} label]
+ [:> switch* {:default-checked enabled?
+ :aria-label label
+ :on-change handle-change}]]))
+
+(mf/defc ruby-customization-options*
+ [{:keys [values on-change on-blur]}]
+ (let [common-props (ruby-common-props values on-change on-blur)
+ options (mf/with-memo []
+ {:ruby-size (ruby-size-options tr)
+ :ruby-align (ruby-align-options tr)
+ :ruby-overhang (ruby-overhang-options tr)
+ :ruby-side (ruby-side-options tr)})]
+ [:div {:class (stl/css :japanese-layout-controls)}
+ [:> ruby-hidden-option* common-props]
+ [:> span-select-option* (mf/spread-props common-props
+ {:attr :ruby-size
+ :default-value (cjk/enum-default :ruby-size)
+ :label (tr "workspace.options.text-options.ruby-size")
+ :options (:ruby-size options)})]
+ [:> span-select-option* (mf/spread-props common-props
+ {:attr :ruby-align
+ :default-value (cjk/enum-default :ruby-align)
+ :label (tr "workspace.options.text-options.ruby-align")
+ :options (:ruby-align options)})]
+ [:> span-select-option* (mf/spread-props common-props
+ {:attr :ruby-overhang
+ :default-value (cjk/enum-default :ruby-overhang)
+ :label (tr "workspace.options.text-options.ruby-overhang")
+ :options (:ruby-overhang options)})]
+ [:> span-select-option* (mf/spread-props common-props
+ {:attr :ruby-side
+ :default-value (cjk/enum-default :ruby-side)
+ :label (tr "workspace.options.text-options.ruby-side")
+ :options (:ruby-side options)})]]))
+
+(mf/defc ruby-advanced-options*
+ [{:keys [values on-change on-blur]}]
+ (let [open* (mf/use-state false)
+ open? (deref open*)
+ toggle-open (mf/use-fn #(swap! open* not))
+ common-props (ruby-common-props values on-change on-blur)]
+ [:div {:class (stl/css :ruby-advanced-options)}
+ [:> ruby-options* (mf/spread-props common-props
+ {:advanced-open open?
+ :on-toggle-advanced toggle-open})]
+ [:> advanced-options* {:class (stl/css :ruby-advanced-content)
+ :is-visible open?}
+ [:> ruby-customization-options* common-props]]]))
+
+(mf/defc ruby-presentation-options*
+ [{:keys [values on-change on-blur]}]
+ (let [open* (mf/use-state false)
+ open? (deref open*)
+ toggle-open (mf/use-fn #(swap! open* not))
+ common-props (ruby-common-props values on-change on-blur)]
+ [:div {:class (stl/css :ruby-advanced-options)}
+ [:> title-bar* {:collapsable true
+ :collapsed (not open?)
+ :title (tr "workspace.options.text-options.ruby-advanced-options")
+ :on-collapsed toggle-open
+ :aria-expanded open?
+ :data-testid "ruby-presentation-options-toggle"}]
+ [:> advanced-options* {:class (stl/css :ruby-advanced-content)
+ :is-visible open?}
+ [:> ruby-customization-options* common-props]]]))
+
+(mf/defc font-features-options*
+ ;; Japanese proportional metric alternates, per span: `palt` for
+ ;; horizontal text, `vpal` for vertical.
+ [{:keys [values on-change on-blur]}]
+ (let [writing-mode (:writing-mode values)
+ feature (proportional-metrics-feature writing-mode)
+ value (:font-features values)
+ enabled? (cond
+ (mixed-span-value? value) nil
+ (= feature value) true
+ :else false)
+
+ handle-change
+ (mf/use-fn
+ (mf/deps feature on-change on-blur)
+ (fn [checked?]
+ (on-change {:font-features (if checked? feature "none")})
+ (when (some? on-blur) (on-blur))))]
+
+ [:div {:class (stl/css :japanese-select-option)}
+ [:span {:class (stl/css :japanese-option-label)}
+ (tr "workspace.options.text-options.font-features")]
+ [:> switch* {:default-checked enabled?
+ :aria-label (tr "workspace.options.text-options.font-features")
+ :on-change handle-change}]]))
+
+(defn text-emphasis-options
+ ([]
+ (text-emphasis-options tr))
+ ([translate]
+ [{:id "none"
+ :label (translate "workspace.options.text-options.text-emphasis-none")}
+ {:id "filled-dot"
+ :label (translate "workspace.options.text-options.text-emphasis-filled-dot")}
+ {:id "open-dot"
+ :label (translate "workspace.options.text-options.text-emphasis-open-dot")}
+ {:id "filled-circle"
+ :label (translate "workspace.options.text-options.text-emphasis-filled-circle")}
+ {:id "open-circle"
+ :label (translate "workspace.options.text-options.text-emphasis-open-circle")}
+ {:id "filled-sesame"
+ :label (translate "workspace.options.text-options.text-emphasis-filled-sesame")}
+ {:id "open-sesame"
+ :label (translate "workspace.options.text-options.text-emphasis-open-sesame")}]))
+
+(defn annotation-clearance-options
+ ([]
+ (annotation-clearance-options tr))
+ ([translate]
+ [{:id "none"
+ :label (translate "workspace.options.text-options.annotation-clearance-none")}
+ {:id "auto"
+ :label (translate "workspace.options.text-options.annotation-clearance-auto")}]))
+
+(defn line-adjustment-options
+ ([]
+ (line-adjustment-options tr))
+ ([translate]
+ [{:id "push-in-first"
+ :label (translate "workspace.options.text-options.line-adjustment-push-in-first")}
+ {:id "push-out-first"
+ :label (translate "workspace.options.text-options.line-adjustment-push-out-first")}
+ {:id "push-out-only"
+ :label (translate "workspace.options.text-options.line-adjustment-push-out-only")}]))
+
+(mf/defc line-adjustment-option*
+ "Whole-shape setting: how lines slightly too long are fitted."
+ [{:keys [values on-change on-blur]}]
+ (let [options (mf/with-memo [] (line-adjustment-options))]
+ [:> span-select-option* {:values values
+ :on-change on-change
+ :on-blur on-blur
+ :attr :line-adjustment
+ :default-value (cjk/enum-default :line-adjustment)
+ :label (tr "workspace.options.text-options.line-adjustment")
+ :options options}]))
+
+(mf/defc japanese-layout-options*
+ [{:keys [values ruby-values text-selection-active
+ on-change on-ruby-presentation-change on-blur]}]
+ (let [vertical? (vertical-japanese-layout? values)
+ common-props (mf/props
+ {:values values
+ :on-change on-change
+ :on-blur on-blur})
+ ruby-presentation-props (mf/props
+ {:values ruby-values
+ :on-change on-ruby-presentation-change
+ :on-blur on-blur})
+ options (mf/with-memo []
+ {:writing-mode (writing-mode-options tr)
+ :text-orientation (text-orientation-options tr)
+ :warichu (warichu-options tr)
+ :text-emphasis (text-emphasis-options)
+ :annotation-clearance (annotation-clearance-options)})]
+
+ [:div {:class (stl/css :japanese-layout-options)}
+ [:div {:class (stl/css :japanese-layout-controls)}
+ [:div {:class (stl/css :japanese-icon-options)}
+ [:> attr-radio-options* (mf/spread-props common-props
+ {:attr :writing-mode
+ :default-value (cjk/enum-default :writing-mode)
+ :name "writing-mode-options"
+ :class (stl/css :writing-mode-options)
+ :options (:writing-mode options)})]
+ (when ^boolean vertical?
+ [:*
+ ;; An unset orientation reads as nil, or "" in the v2 editor; both
+ ;; select "mixed".
+ [:> attr-radio-options* (mf/spread-props common-props
+ {:attr :text-orientation
+ :default-value (cjk/enum-default :text-orientation)
+ :name "text-orientation-options"
+ :class (stl/css :text-orientation-options)
+ :options (:text-orientation options)})]
+ [:> text-combine-upright-options*
+ (mf/spread-props common-props
+ {:text-selection-active text-selection-active})]])
+ (when ^boolean text-selection-active
+ [:> attr-radio-options* (mf/spread-props common-props
+ {:attr :warichu
+ :default-value (cjk/enum-default :warichu)
+ :name "warichu-options"
+ :class (stl/css :warichu-options)
+ :options (:warichu options)})])]
+ (when ^boolean vertical?
+ [:> text-combine-upright-count-options* common-props])
+ [:> font-features-options* common-props]
+ ;; Emphasis marks (圏点 / bouten) apply to the selected characters.
+ (when ^boolean text-selection-active
+ [:> span-select-option* (mf/spread-props common-props
+ {:attr :text-emphasis
+ :default-value (cjk/enum-default :text-emphasis)
+ :label (tr "workspace.options.text-options.text-emphasis")
+ :options (:text-emphasis options)})])
+ [:> span-select-option* (mf/spread-props common-props
+ {:attr :annotation-clearance
+ :default-value (cjk/enum-default :annotation-clearance)
+ :label (tr "workspace.options.text-options.annotation-clearance")
+ :options (:annotation-clearance options)})]
+ (if text-selection-active
+ [:> ruby-advanced-options* common-props]
+ [:> ruby-presentation-options* ruby-presentation-props])]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss
new file mode 100644
index 0000000000..b84d9971a5
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss
@@ -0,0 +1,78 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC Sucursal en España SL
+
+@use "ds/typography.scss" as t;
+@use "ds/_sizes.scss" as *;
+@use "ds/_borders.scss" as *;
+@use "ds/spacing.scss" as *;
+
+.writing-mode-options,
+.text-orientation-options,
+.text-combine-upright-options,
+.warichu-options {
+ block-size: $sz-32;
+}
+
+.ruby-advanced-options {
+ display: flex;
+ flex-direction: column;
+ overflow: visible;
+}
+
+.ruby-options {
+ display: flex;
+ align-items: flex-end;
+ gap: var(--sp-xs);
+}
+
+.ruby-input {
+ flex: 1;
+}
+
+.ruby-advanced-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+ padding: var(--sp-xs);
+ border: $b-1 solid var(--color-background-quaternary);
+ border-radius: $br-8;
+}
+
+.japanese-layout-options,
+.japanese-layout-controls {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+}
+
+.japanese-layout-options {
+ padding-block: var(--sp-xs);
+}
+
+.ruby-hidden-option {
+ min-block-size: $sz-32;
+}
+
+.japanese-icon-options {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--sp-xs);
+}
+
+.japanese-select-option {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
+ align-items: center;
+ gap: var(--sp-s);
+ inline-size: 100%;
+}
+
+.japanese-option-label {
+ @include t.use-typography("body-small");
+
+ color: var(--color-foreground-secondary);
+}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs
index dd2c9dc102..fe0fa3050d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs
@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
+ [app.common.types.text.cjk-layout :as cjk]
[app.main.data.workspace.texts :as dwt]
[app.main.features :as features]
[app.main.refs :as refs]
@@ -27,8 +28,30 @@
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu*]]
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu*]]
[app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu*]]
+ [app.render-wasm.api :as wasm.api]
[rumext.v2 :as mf]))
+(defn- dom-text-range-selected?
+ [editor]
+ (when editor
+ (let [selection (.getSelection js/window)
+ element (.-element editor)
+ anchor (some-> selection .-anchorNode)
+ focus (some-> selection .-focusNode)]
+ (and selection
+ (not (.-isCollapsed selection))
+ element
+ anchor
+ focus
+ (.contains element anchor)
+ (.contains element focus)))))
+
+(defn- draft-text-range-selected?
+ [editor-state]
+ (when editor-state
+ (let [selection (.getSelection editor-state)]
+ (and selection (not (.isCollapsed selection))))))
+
(mf/defc options*
[{:keys [shape libraries file-id page-id]}]
(let [id (dm/get-prop shape :id)
@@ -128,6 +151,19 @@
(when text-editor-v2?
editor)
+ text-selection-active
+ (boolean
+ (cond
+ text-editor-wasm?
+ (and (= id (wasm.api/text-editor-get-active-shape-id))
+ (wasm.api/text-editor-has-selection?))
+
+ text-editor-v2?
+ (dom-text-range-selected? editor-instance)
+
+ :else
+ (draft-text-range-selected? editor-state)))
+
fill-values
(dwt/current-text-values
{:editor-styles editor-styles
@@ -154,7 +190,14 @@
:editor-state editor-state
:editor-instance editor-instance
:shape shape
- :attrs txt/text-node-attrs}))]
+ :attrs txt/text-node-attrs}))
+
+ ruby-values
+ (if text-selection-active
+ (select-keys text-values cjk/ruby-presentation-attrs)
+ (dwt/current-ruby-values
+ {:shape shape
+ :attrs cjk/ruby-presentation-attrs}))]
[:*
[:> layer-menu* {:ids ids
@@ -202,6 +245,8 @@
:type type
:applied-tokens applied-tokens
:values text-values
+ :ruby-values ruby-values
+ :text-selection-active text-selection-active
:libraries libraries
:file-id file-id
:typographies typographies}]
@@ -237,4 +282,3 @@
:values (select-keys shape exports-attrs)
:page-id page-id
:file-id file-id}]]))
-
diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs
index f68303e117..19ee5bfd54 100644
--- a/frontend/test/frontend_tests/runner.cljs
+++ b/frontend/test/frontend_tests/runner.cljs
@@ -143,6 +143,7 @@
[frontend-tests.ui.static-test]
[frontend-tests.ui.stroke-menu-test]
[frontend-tests.ui.text-attrs-multiple-test]
+ [frontend-tests.ui.text-options-test]
[frontend-tests.ui.workspace-libraries-test]
[frontend-tests.util-clipboard-test]
[frontend-tests.util-object-test]
@@ -311,6 +312,7 @@
'frontend-tests.ui.static-test
'frontend-tests.ui.stroke-menu-test
'frontend-tests.ui.text-attrs-multiple-test
+ 'frontend-tests.ui.text-options-test
'frontend-tests.ui.workspace-libraries-test
'frontend-tests.util-clipboard-test
'frontend-tests.util-object-test
diff --git a/frontend/test/frontend_tests/ui/text_options_test.cljs b/frontend/test/frontend_tests/ui/text_options_test.cljs
new file mode 100644
index 0000000000..e0999e870b
--- /dev/null
+++ b/frontend/test/frontend_tests/ui/text_options_test.cljs
@@ -0,0 +1,248 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC Sucursal en España SL
+
+(ns frontend-tests.ui.text-options-test
+ (:require
+ ["react-dom/server" :as rds]
+ [app.common.types.text.cjk-layout :as cjk]
+ [app.main.data.workspace.texts :as dwt]
+ [app.main.ui.ds.controls.select :as select]
+ [app.main.ui.shapes.text.html-text :as html-text]
+ [app.main.ui.shapes.text.styles :as text-styles]
+ [app.main.ui.workspace.sidebar.options.common :refer [radio-selected]]
+ [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl]
+ [app.util.text.writing-mode :as wm]
+ [cljs.test :as t :include-macros true]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(def ^:private japanese-span-attrs
+ [:text-combine-upright
+ :text-emphasis
+ :ruby
+ :ruby-hidden
+ :ruby-size
+ :ruby-align
+ :ruby-overhang
+ :ruby-side
+ :warichu
+ :font-features
+ :annotation-clearance])
+
+
+(t/deftest japanese-layout-controls-follow-file-or-profile-configuration
+ (with-redefs [wm/vertical-layout-active? (constantly true)]
+ (t/is (false? (tjl/japanese-layout-config-enabled? {})))
+ (t/is (false? (tjl/japanese-layout-config-enabled? {:file? false :all-files? false})))
+ (t/is (true? (tjl/japanese-layout-config-enabled? {:file? true :all-files? false})))
+ (t/is (true? (tjl/japanese-layout-config-enabled? {:file? false :all-files? true}))))
+ (with-redefs [wm/vertical-layout-active? (constantly false)]
+ (t/is (false? (tjl/japanese-layout-config-enabled? {:file? true :all-files? true})))))
+
+(t/deftest text-emphasis-select-reports-and-restores-canonical-values
+ (let [options (tjl/text-emphasis-options identity)
+ reported-id (:id (select/get-option options "filled-dot"))]
+ (t/is (= "filled-dot" reported-id))
+ (t/is (= "filled-dot"
+ (:id (select/get-option options reported-id))))))
+
+(t/deftest annotation-clearance-select-reports-canonical-values
+ (let [options (tjl/annotation-clearance-options identity)]
+ (t/is (= ["none" "auto"] (mapv :id options)))
+ (t/is (= "auto" (:id (select/get-option options "auto"))))))
+
+(t/deftest unrestricted-tcy-is-only-offered-for-a-text-selection
+ (t/is (= ["none" "digits"]
+ (mapv :value (tjl/text-combine-upright-options false identity))))
+ (t/is (= ["none" "all" "digits"]
+ (mapv :value (tjl/text-combine-upright-options true identity)))))
+
+(t/deftest ruby-container-styles-preserve-customization
+ (let [style (text-styles/generate-ruby-container-styles
+ {:ruby-align "space-between"
+ :ruby-overhang "none"
+ :ruby-side "under"})]
+ (t/is (= "space-between" (.-rubyAlign style)))
+ (t/is (= "none" (.-rubyOverhang style)))
+ (t/is (= "under" (.-rubyPosition style)))))
+
+(t/deftest advanced-furigana-options-are-collapsed-by-default
+ (let [markup (rds/renderToStaticMarkup
+ (mf/element tjl/ruby-presentation-options*
+ #js {:values {}
+ :on-change identity
+ :on-blur identity}))]
+ (t/is (str/includes? markup "data-testid=\"ruby-presentation-options-toggle\""))
+ (t/is (str/includes? markup "aria-expanded=\"false\""))
+ (t/is (str/includes? markup "workspace.options.text-options.ruby-advanced-options"))))
+
+(t/deftest advanced-furigana-options-include-hide-toggle
+ (let [markup (rds/renderToStaticMarkup
+ (mf/element tjl/ruby-hidden-option*
+ #js {:values {:ruby-hidden true}
+ :on-change identity
+ :on-blur identity}))]
+ (t/is (str/includes? markup "workspace.options.text-options.ruby-hidden"))
+ (t/is (str/includes? markup "role=\"switch\""))
+ (t/is (str/includes? markup "aria-checked=\"true\""))))
+
+(t/deftest hidden-furigana-renders-base-text-without-ruby-layout
+ (let [markup (rds/renderToStaticMarkup
+ (mf/element html-text/render-text*
+ #js {:node {:text "日"
+ :ruby "にち"
+ :ruby-hidden true}
+ :parent {}
+ :shape {}}))]
+ (t/is (str/includes? markup ">日"))
+ (t/is (not (str/includes? markup "