Merge pull request #9442 from logseq/feat/integrated-title-bar

Feat: Integrated title bar for Windows and Linux [minor change]
pull/9536/head
Tienson Qin 2023-05-30 16:10:47 +08:00 committed by GitHub
commit 7a3f849d3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 299 additions and 49 deletions

View File

@ -204,8 +204,19 @@ jobs:
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
- name: Install Fluxbox
run: sudo apt-get update && sudo apt-get install -y fluxbox
# Emulate a virtual framebuffer on machines with no display hardware
- name: Run XVFB
run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
# Start a lightweight window manager to simulate window actions (maximize,restore etc)
- name: Start Fluxbox
run: DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
- name: Run Playwright test
run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3
env:
LOGSEQ_CI: true
DEBUG: "pw:api"

View File

@ -182,15 +182,26 @@ jobs:
- name: Ensure static yarn.lock is up to date
run: git diff --exit-code static/yarn.lock
- name: Install Fluxbox
run: sudo apt-get update && sudo apt-get install -y fluxbox
# Emulate a virtual framebuffer on machines with no display hardware
- name: Run XVFB
run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
# Start a lightweight window manager to simulate window actions (maximize,restore etc)
- name: Start Fluxbox
run: DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
- name: Run Playwright test - 1/2
run: xvfb-run -- npx playwright test --reporter github --shard=1/2
run: DISPLAY=:1.0 npx playwright test --reporter github --shard=1/2
env:
LOGSEQ_CI: true
DEBUG: "pw:api"
RELEASE: true # skip dev only test
- name: Run Playwright test - 2/2
run: xvfb-run -- npx playwright test --reporter github --shard=2/2
run: DISPLAY=:1.0 npx playwright test --reporter github --shard=2/2
env:
LOGSEQ_CI: true
DEBUG: "pw:api"

View File

@ -137,8 +137,19 @@ jobs:
- name: Ensure static yarn.lock is up to date
run: git diff --exit-code static/yarn.lock
- name: Install Fluxbox
run: sudo apt-get update && sudo apt-get install -y fluxbox
# Emulate a virtual framebuffer on machines with no display hardware
- name: Run XVFB
run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
# Start a lightweight window manager to simulate window actions (maximize,restore etc)
- name: Start Fluxbox
run: DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
- name: Run Playwright test
run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3
env:
LOGSEQ_CI: true
DEBUG: "pw:api"

34
e2e-tests/window.spec.ts Normal file
View File

@ -0,0 +1,34 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import { IsMac } from './utils';
if (!IsMac) {
test('window should not be maximized on first launch', async ({ page, app }) => {
await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
})
test('window should be maximized and icon should change on maximize-toggle click', async ({ page }) => {
await page.click('.window-controls .maximize-toggle.maximize')
await expect(page.locator('.window-controls .maximize-toggle.restore')).toHaveCount(1)
})
test('window should be restored and icon should change on maximize-toggle click', async ({ page }) => {
await page.click('.window-controls .maximize-toggle.restore')
await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
})
test('window controls should be hidden on fullscreen mode', async ({ page }) => {
// Keyboard press F11 won't work, probably because it's a chromium shortcut (not a document event)
await page.evaluate(`window.document.body.requestFullscreen()`)
await expect(page.locator('.window-controls .maximize-toggle')).toHaveCount(0)
})
test('window controls should be visible when we exit fullscreen mode', async ({ page }) => {
await page.click('.window-controls .fullscreen-toggle')
await expect(page.locator('.window-controls')).toHaveCount(1)
})
}

View File

@ -422,7 +422,8 @@
js/__dirname)
(defmethod handle :getAppBaseInfo [^js win [_ _opts]]
{:isFullScreen (.isFullScreen win)})
{:isFullScreen (.isFullScreen win)
:isMaximized (.isMaximized win)})
(defmethod handle :getAssetsFiles [^js win [_ {:keys [exts]}]]
(when-let [graph-path (state/get-window-graph-path win)]
@ -599,6 +600,20 @@
(when-let [web-content (.-webContents win)]
(.reload web-content)))
(defmethod handle :window-minimize [^js win]
(.minimize win))
(defmethod handle :window-toggle-maximized [^js win]
(if (.isMaximized win)
(.unmaximize win)
(.maximize win)))
(defmethod handle :window-toggle-fullscreen [^js win]
(.setFullScreen win (not (.isFullScreen win))))
(defmethod handle :window-close [^js win]
(.close win))
;;;;;;;;;;;;;;;;;;;;;;;
;; file-sync-rs-apis ;;
;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -26,10 +26,11 @@
(create-main-window! url nil))
([url opts]
(let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
native-titlebar? (cfgs/get-item :window/native-titlebar?)
win-opts (cond->
{:width (.-width win-state)
:height (.-height win-state)
:frame true
:frame (or mac? native-titlebar?)
:titleBarStyle "hiddenInset"
:trafficLightPosition {:x 16 :y 16}
:autoHideMenuBar (not mac?)
@ -187,7 +188,9 @@
(doto win
(.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
(.on "leave-full-screen" #(.send web-contents "full-screen" "leave")))
(.on "leave-full-screen" #(.send web-contents "full-screen" "leave"))
(.on "maximize" #(.send web-contents "maximize" true))
(.on "unmaximize" #(.send web-contents "maximize" false)))
;; clear
(fn []

View File

@ -35,12 +35,13 @@
[frontend.ui :as ui]
[frontend.util :as util]
[frontend.util.cursor :as cursor]
[frontend.components.window-controls :as window-controls]
[goog.dom :as gdom]
[goog.object :as gobj]
[logseq.common.path :as path]
[react-draggable]
[reitit.frontend.easy :as rfe]
[rum.core :as rum]
[logseq.common.path :as path]))
[rum.core :as rum]))
(rum/defc nav-content-item < rum/reactive
[name {:keys [class]} child]
@ -284,8 +285,8 @@
(when (< touching-x-offset 0)
(max touching-x-offset (- 0 (:width el-rect))))))
offset-ratio (and (number? touching-x-offset)
(some->> (:width el-rect)
(/ touching-x-offset)))]
(some->> (:width el-rect)
(/ touching-x-offset)))]
(rum/use-effect!
#(js/setTimeout
@ -734,6 +735,8 @@
indexeddb-support? (state/sub :indexeddb/support?)
page? (= :page route-name)
home? (= :home route-name)
native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
edit? (:editor/editing? @state/state)
default-home (get-default-home-if-valid)
logged? (user-handler/logged-in?)
@ -763,6 +766,7 @@
{:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
:ls-right-sidebar-open sidebar-open?
:ls-wide-mode wide-mode?
:ls-window-controls window-controls?
:ls-fold-button-on-right fold-button-on-right?
:ls-hl-colored ls-block-hl-colored?}])}
@ -799,6 +803,9 @@
:show-action-bar? show-action-bar?
:show-recording-bar? show-recording-bar?})]
(when window-controls?
(window-controls/container))
(right-sidebar/sidebar)
[:div#app-single-container]]

View File

@ -93,6 +93,7 @@
height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
margin-top: 30px;
width: 100%;
padding-top: var(--ls-win32-title-bar-height);
> .fake-bar {
@apply w-full px-5 pt-1 sm:hidden;
@ -450,6 +451,28 @@
}
}
.ls-window-controls {
&.ls-right-sidebar-open {
.cp__right-sidebar-topbar {
margin-right: 144px;
.is-fullscreen & {
margin-right: 48px;
}
}
}
&:not(.ls-right-sidebar-open) {
.cp__header > .r {
margin-right: 144px;
.is-fullscreen & {
margin-right: 48px;
}
}
}
}
.ls-wide-mode {
.cp__sidebar-main-content {
max-width: var(--ls-main-content-max-width-wide);
@ -537,7 +560,6 @@ html[data-theme='dark'] {
}
&.open {
width: var(--ls-right-sidebar-width);
max-width: 60vw;
}
@ -573,7 +595,9 @@ html[data-theme='dark'] {
user-select: none;
-webkit-app-region: drag;
a, svg {
a,
svg,
button {
-webkit-app-region: no-drag;
}
}

View File

@ -29,7 +29,7 @@
[]
(ui/with-shortcut :go/home "left"
[:button.button.icon.inline
{:title "Home"
{:title (t :home)
:on-click #(do
(when (mobile-util/native-iphone?)
(state/set-left-sidebar-open! false))
@ -269,7 +269,6 @@
:current-repo current-repo
:default-home default-home})
(when (not (state/sub :ui/sidebar-open?))
(sidebar/toggle))
(sidebar/toggle)
(updater-tips-new-version t)]]))

View File

@ -1,7 +1,9 @@
.cp__header {
@apply z-10;
padding-top: var(--ls-headbar-inner-top-padding);
@apply shadow z-10;
-webkit-app-region: drag;
padding-top: calc(var(--ls-headbar-inner-top-padding));
margin-top: var(--ls-win32-title-bar-height);
height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
display: flex;
align-items: center;

View File

@ -73,6 +73,9 @@
(when v [:.ml-4 (ui/foldable
[:div (str k)]
[:.ml-4 (case k
:tx-id
[:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
:blocks
(map (fn [block]
[:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
@ -220,6 +223,7 @@
(rum/defc sidebar-resizer
[sidebar-open? sidebar-id handler-position]
(let [el-ref (rum/use-ref nil)
min-px-width 144 ; Custom window controls width
min-ratio 0.1
max-ratio 0.7
keyboard-step 5
@ -227,12 +231,12 @@
remove-resizing-class (fn []
(.. js/document.documentElement -classList (remove "is-resizing-buf"))
(reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
set-width! (fn [ratio element]
(when (and el-ref element)
(let [width (str (* ratio 100) "%")]
(#(.setProperty (.-style element) "width" width)
(.setAttribute (rum/deref el-ref) "aria-valuenow" ratio)
(ui-handler/persist-right-sidebar-width!)))))]
set-width! (fn [ratio]
(when el-ref
(let [value (* ratio 100)
width (str value "%")]
(.setAttribute (rum/deref el-ref) "aria-valuenow" value)
(ui-handler/persist-right-sidebar-width! width))))]
(rum/use-effect!
(fn []
(when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
@ -243,6 +247,7 @@
{:move
(fn [^js/MouseEvent e]
(let [width js/document.documentElement.clientWidth
min-ratio (max min-ratio (/ min-px-width width))
sidebar-el (js/document.getElementById sidebar-id)
offset (.-pageX e)
ratio (.toFixed (/ offset width) 6)
@ -259,7 +264,7 @@
(and (< ratio max-ratio) sidebar-el)
(when sidebar-el
(#(.. js/document.documentElement -classList (remove cursor-class))
(set-width! ratio sidebar-el)))
(set-width! ratio)))
:else
#(.. js/document.documentElement -classList (remove cursor-class)))
(when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
@ -269,6 +274,7 @@
(.on "keydown" (fn [e]
(when-let [sidebar-el (js/document.getElementById sidebar-id)]
(let [width js/document.documentElement.clientWidth
min-ratio (max min-ratio (/ min-px-width width))
keyboard-step (case (.-code e)
"ArrowLeft" (- keyboard-step)
"ArrowRight" keyboard-step
@ -278,7 +284,7 @@
ratio (if (= handler-position :west) (- 1 ratio) ratio)]
(when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
((add-resizing-class)
(set-width! ratio sidebar-el)))))))
(set-width! ratio)))))))
(.on "keyup" remove-resizing-class)))
#())
[])
@ -333,9 +339,7 @@
(when config/dev? [:div.text-sm
[:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
(state/sidebar-add-block! repo "history" :history))}
(t :right-side-bar/history)]])]
(toggle)]
(t :right-side-bar/history)]])]]
[:.sidebar-item-list.flex-1.scrollbar-spacing.flex.flex-col.gap-2
(if @*anim-finished?
@ -353,9 +357,11 @@
[[(state/get-current-repo) "contents" :contents nil]]
blocks)
sidebar-open? (state/sub :ui/sidebar-open?)
width (state/sub :ui/sidebar-width)
repo (state/sub :git/current-repo)]
[:div#right-sidebar.cp__right-sidebar.h-screen
{:class (if sidebar-open? "open" "closed")}
{:class (if sidebar-open? "open" "closed")
:style {:width width}}
(sidebar-resizer sidebar-open? "right-sidebar" :west)
(when sidebar-open?
(sidebar-inner repo t blocks))]))

View File

@ -610,6 +610,19 @@
(fn [_] (conversion-component/files-breaking-changed))
{:id :filename-format-panel :center? true})}))
(rum/defcs native-titlebar-row < rum/reactive
[state t]
(let [enabled? (state/sub [:electron/user-cfgs :window/native-titlebar?])]
(toggle
"native-titlebar"
(t :settings-page/native-titlebar)
enabled?
#(when (js/confirm (t :relaunch-confirm-to-work))
(state/set-state! [:electron/user-cfgs :window/native-titlebar?] (not enabled?))
(ipc/ipc :userAppCfgs :window/native-titlebar? (not enabled?))
(js/logseq.api.relaunch))
[:span.text-sm.opacity-50 (t :settings-page/native-titlebar-desc)])))
(rum/defcs settings-general < rum/reactive
[_state current-repo]
(let [preferred-language (state/sub [:preferred-language])
@ -621,6 +634,7 @@
(version-row t version)
(language-row t preferred-language)
(theme-modes-row t switch-theme system-theme? dark?)
(when (and (util/electron?) (not util/mac?)) (native-titlebar-row t))
(when (config/global-config-enabled?) (edit-global-config-edn))
(when current-repo (edit-config-edn))
(when current-repo (edit-custom-css))

View File

@ -386,3 +386,29 @@
[:path
{:d
"M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM352 328c0 13.2-10.8 24-24 24h-144C170.8 352 160 341.2 160 328v-144C160 170.8 170.8 160 184 160h144C341.2 160 352 170.8 352 184V328z"}]])
;; Titlebar icons from https://github.com/microsoft/vscode-codicons
(defn window-minimize
([] (window-minimize 16))
([size]
[:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
[:path {:d "M14 8v1H3V8h11z"}]]))
(defn window-maximize
([] (window-maximize 16))
([size]
[:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
[:path {:d "M3 3v10h10V3H3zm9 9H4V4h8v8z"}]]))
(defn window-restore
([] (window-restore 16))
([size]
[:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
[:path {:d "M3 5v9h9V5H3zm8 8H4V6h7v7z"}]
[:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M5 5h1V4h7v7h-1v1h2V3H5v2z"}]]))
(defn window-close
([] (window-close 16))
([size]
[:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
[:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M7.116 8l-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"}]]))

View File

@ -8,8 +8,6 @@
--ls-z-index-level-3: 999;
--ls-z-index-level-4: 9999;
--ls-z-index-level-5: 99999;
--ls-right-sidebar-width: 40%;
}
html {

View File

@ -0,0 +1,52 @@
(ns frontend.components.window-controls
(:require [electron.ipc :as ipc]
[frontend.components.svg :as svg]
[frontend.context.i18n :refer [t]]
[frontend.state :as state]
[frontend.ui :as ui]
[rum.core :as rum]))
(defn minimize
[]
(ipc/ipc "window-minimize"))
(defn toggle-maximized
[]
(ipc/ipc "window-toggle-maximized"))
(defn close
[]
(ipc/ipc "window-close"))
(defn toggle-fullscreen
[]
(ipc/ipc "window-toggle-fullscreen"))
(rum/defc container < rum/reactive
[]
(let [maximized? (state/sub :electron/window-maximized?)
fullscreen? (state/sub :electron/window-fullscreen?)]
[:div.window-controls.flex
(if fullscreen?
[:button.button.icon.fullscreen-toggle
{:title (t :window/exit-fullscreen)
:on-click toggle-fullscreen}
(ui/icon "arrows-minimize")]
[:<>
[:button.button.icon.minimize
{:title (t :window/minimize)
:on-click minimize}
(svg/window-minimize)]
[:button.button.icon.maximize-toggle
{:title (if maximized? (t :window/restore) (t :window/maximize))
:class (if maximized? "restore" "maximize")
:on-click toggle-maximized}
(if maximized?
(svg/window-restore)
(svg/window-maximize))]
[:button.button.icon.close
{:title (t :window/close)
:on-click close}
(svg/window-close)]])]))

View File

@ -0,0 +1,14 @@
.window-controls {
position: fixed;
top: 0;
right: 0;
z-index: 10;
.button {
-webkit-app-region: no-drag;
background: transparent;
border-radius: 0;
width: 48px;
height: 48px;
}
}

View File

@ -99,6 +99,8 @@
(case name
:home
"Logseq"
:whiteboards
(t :whiteboards)
:repos
"Repos"
:repo-add
@ -129,6 +131,15 @@
(let [page (db/pull [:block/name (util/page-name-sanity-lc name)])]
(or (util/get-page-original-name page)
"Logseq"))))
:whiteboard
(let [name (:name path-params)
block? (util/uuid-string? name)]
(str
(if block?
(t :untitled)
(let [page (db/pull [:block/name (util/page-name-sanity-lc name)])]
(or (util/get-page-original-name page)
"Logseq"))) " - " (t :whiteboard)))
:tag
(str "#" (:name path-params))
:diff

View File

@ -17,26 +17,18 @@
[promesa.core :as p]
[logseq.common.path :as path]))
(defn- get-css-var-value
[var-name]
(.getPropertyValue (js/getComputedStyle (.-documentElement js/document)) var-name))
;; sidebars
(def *right-sidebar-resized-at (atom (js/Date.now)))
(defn- get-right-sidebar-width
[]
(or (.. (js/document.getElementById "right-sidebar") -style -width)
(get-css-var-value "--right-sidebar-width")))
(defn persist-right-sidebar-width!
[]
(storage/set "ls-right-sidebar-width" (get-right-sidebar-width)))
[width]
(state/set-state! :ui/sidebar-width width)
(storage/set "ls-right-sidebar-width" width))
(defn restore-right-sidebar-width!
[]
(when-let [width (storage/get "ls-right-sidebar-width")]
(.setProperty (.-style (js/document.getElementById "right-sidebar")) "width" width)))
(state/set-state! :ui/sidebar-width width)))
(defn close-left-sidebar!
[]

View File

@ -88,6 +88,7 @@
{:did-mount (fn [state]
(state/set-root-component! (:rum/react-component state))
(state/setup-electron-updater!)
(state/load-app-user-cfgs)
(ui/inject-document-devices-envs!)
(ui/inject-dynamic-style-node!)
(quick-tour/init)

View File

@ -73,9 +73,9 @@
:ui/navigation-item-collapsed? {}
;; right sidebar
:ui/fullscreen? false
:ui/settings-open? false
:ui/sidebar-open? false
:ui/sidebar-width "40%"
:ui/left-sidebar-open? (boolean (storage/get "ls-left-sidebar-open?"))
:ui/theme (or (storage/get :ui/theme) "light")
:ui/system-theme? ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
@ -165,6 +165,8 @@
:electron/updater {}
:electron/user-cfgs nil
:electron/server nil
:electron/window-maximized? false
:electron/window-fullscreen? false
;; assets
:assets/alias-enabled? (or (storage/get :assets/alias-enabled?) false)

View File

@ -367,11 +367,15 @@
(doseq [[event function]
[["persist-zoom-level" #(storage/set :zoom-level %)]
["restore-zoom-level" #(when-let [zoom-level (storage/get :zoom-level)] (js/window.apis.setZoomLevel zoom-level))]
["full-screen" #(js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")]]]
["full-screen" #(do (js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")
(state/set-state! :electron/window-fullscreen? (= % "enter")))]
["maximize" #(state/set-state! :electron/window-maximized? %)]]]
(.on js/window.apis event function))
(p/then (ipc/ipc :getAppBaseInfo) #(let [{:keys [isFullScreen]} (js->clj % :keywordize-keys true)]
(and isFullScreen (.add cl "is-fullscreen")))))))
(p/then (ipc/ipc :getAppBaseInfo) #(let [{:keys [isFullScreen isMaximized]} (js->clj % :keywordize-keys true)]
(when isFullScreen ((.add cl "is-fullscreen")
(state/set-state! :electron/window-fullscreen? true)))
(when isMaximized (state/set-state! :electron/window-maximized? true)))))))
(defn inject-dynamic-style-node!
[]

View File

@ -225,6 +225,8 @@
:settings-page/login-prompt "To access new features before anyone else you must be an Open Collective Sponsor or Backer of Logseq and therefore log in first."
:settings-page/sync "Sync"
:settings-page/enable-whiteboards "Whiteboards"
:settings-page/native-titlebar "Native title bar"
:settings-page/native-titlebar-desc "Enables the native window title bar on Windows and Linux."
:yes "Yes"
:submit "Submit"
@ -249,7 +251,10 @@
:whiteboard/link-whiteboard-or-block "Link whiteboard/page/block"
:page-search "Search in the current page"
:graph-search "Search graph"
:home "Home"
:new-page "New page"
:whiteboard "Whiteboard"
:whiteboards "Whiteboards"
:new-whiteboard "New whiteboard"
:new-graph "Add new graph"
:graph "Graph"
@ -377,6 +382,11 @@
:shortcut.category/toggle "Toggle"
:shortcut.category/whiteboard "Whiteboard"
:shortcut.category/others "Others"
:window/minimize "Minimize"
:window/maximize "Maximize"
:window/restore "Restore"
:window/close "Close"
:window/exit-fullscreen "Exit full screen"
;; Commands are nested for now to stay in sync with the shortcuts system.
;; Other languages should not nest keys under :commands

View File

@ -171,7 +171,10 @@
:search "Buscar o Crear Página"
:page-search "Buscar en la página actual"
:graph-search "Buscar grafo"
:home "Inicio"
:new-page "Nueva página"
:whiteboard "Pizarra"
:whiteboards "Pizarras"
:new-graph "Añadir nuevo grafo"
:graph "Grafo"
:graph/persist "Logseq está sincronizando su estado interno, por favor espere unos segundos."