mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 10:59:20 +02:00
add compact document view for larger screens
This commit is contained in:
parent
3b7ef3c5c5
commit
929552afef
|
|
@ -9,10 +9,12 @@ import {
|
|||
import DocumentHeader from "./DocumentHeader.vue"
|
||||
import {
|
||||
at,
|
||||
clearTeleportedOverlays,
|
||||
findButtonByText,
|
||||
menuItem,
|
||||
mountUnderSidebarProvider,
|
||||
seedCapabilities,
|
||||
seedPersistentState,
|
||||
settleMutations,
|
||||
t,
|
||||
} from "./test-helpers"
|
||||
|
|
@ -72,6 +74,43 @@ async function openOptionsMenu(
|
|||
await at(triggers, triggers.length - 1).trigger("click")
|
||||
}
|
||||
|
||||
function menuItemTexts() {
|
||||
return Array.from(
|
||||
document.body.querySelectorAll<HTMLElement>("[role^='menuitem']"),
|
||||
).map((el) => el.textContent.trim())
|
||||
}
|
||||
|
||||
// happy-dom lays nothing out, so useElementSize reports 0 until a resize
|
||||
// observer hands it a size; this one answers every observe with the given
|
||||
// width. Restored by hand: vi.unstubAllGlobals would also undo the globals
|
||||
// the nuxt runtime stubbed for the whole suite
|
||||
const realResizeObserver = window.ResizeObserver
|
||||
|
||||
function stubMainWidth(width: number) {
|
||||
window.ResizeObserver = class {
|
||||
constructor(private callback: ResizeObserverCallback) {}
|
||||
|
||||
observe(target: Element) {
|
||||
const size = { inlineSize: width, blockSize: 40 }
|
||||
this.callback(
|
||||
[
|
||||
{
|
||||
target,
|
||||
contentRect: { width, height: 40 },
|
||||
contentBoxSize: [size],
|
||||
borderBoxSize: [size],
|
||||
} as unknown as ResizeObserverEntry,
|
||||
],
|
||||
this,
|
||||
)
|
||||
}
|
||||
|
||||
unobserve = vi.fn()
|
||||
|
||||
disconnect = vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
// the editor store, the query cache and the vue-sonner module mock are all
|
||||
// app-wide singletons every mount in the file shares, and the menus are
|
||||
// teleported into the shared <body>
|
||||
|
|
@ -88,10 +127,17 @@ describe("<DocumentHeader>", { concurrent: false }, () => {
|
|||
editorStore.mappedDefaultBranchId = MAIN_BRANCH
|
||||
editorStore.aiAssistantOpen = false
|
||||
useEditorMeta().setEditable(true)
|
||||
seedPersistentState("editor-compact-view", true)
|
||||
editorStore.setActiveBranchProtected(false)
|
||||
})
|
||||
|
||||
afterEach(disposeMockEndpoints)
|
||||
afterEach(() => {
|
||||
disposeMockEndpoints()
|
||||
window.ResizeObserver = realResizeObserver
|
||||
// the options menus teleport into the shared <body> and their ids
|
||||
// repeat across mounts, so a leftover would answer the next lookup
|
||||
clearTeleportedOverlays()
|
||||
})
|
||||
|
||||
it("stays blank until the initial sections have loaded", async ({
|
||||
expect,
|
||||
|
|
@ -273,6 +319,63 @@ describe("<DocumentHeader>", { concurrent: false }, () => {
|
|||
expect(editorStore.targetBranchId).toBeNull()
|
||||
})
|
||||
|
||||
it("hides the view toggle while the main area is no wider than the compact column", async ({
|
||||
expect,
|
||||
}) => {
|
||||
mainOnly()
|
||||
const wrapper = await mountHeader()
|
||||
|
||||
await openOptionsMenu(wrapper)
|
||||
|
||||
expect(menuItemTexts()).not.toContain(
|
||||
t("editor.navbar.document-options.view.full-title"),
|
||||
)
|
||||
expect(menuItemTexts()).not.toContain(
|
||||
t("editor.navbar.document-options.view.compact-title"),
|
||||
)
|
||||
})
|
||||
|
||||
it("offers the full view first once the main area is wider than the compact column", async ({
|
||||
expect,
|
||||
}) => {
|
||||
stubMainWidth(1400)
|
||||
mainOnly()
|
||||
const wrapper = await mountHeader()
|
||||
|
||||
await openOptionsMenu(wrapper)
|
||||
|
||||
expect(at(menuItemTexts(), 0)).toBe(
|
||||
t("editor.navbar.document-options.view.full-title"),
|
||||
)
|
||||
})
|
||||
|
||||
it("switches to the full view from the options menu", async ({ expect }) => {
|
||||
stubMainWidth(1400)
|
||||
mainOnly()
|
||||
const wrapper = await mountHeader()
|
||||
await openOptionsMenu(wrapper)
|
||||
|
||||
menuItem(t("editor.navbar.document-options.view.full-title")).click()
|
||||
await nextTick()
|
||||
|
||||
expect(useEditorMeta().isCompactView.value).toBe(false)
|
||||
})
|
||||
|
||||
it("offers the compact view while the full view is active", async ({
|
||||
expect,
|
||||
}) => {
|
||||
stubMainWidth(1400)
|
||||
mainOnly()
|
||||
useEditorMeta().toggleCompactView()
|
||||
const wrapper = await mountHeader()
|
||||
|
||||
await openOptionsMenu(wrapper)
|
||||
|
||||
expect(at(menuItemTexts(), 0)).toBe(
|
||||
t("editor.navbar.document-options.view.compact-title"),
|
||||
)
|
||||
})
|
||||
|
||||
it("asks to duplicate the document from the options menu", async ({
|
||||
expect,
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@ const emit = defineEmits<{
|
|||
(e: "delete-document" | "duplicate-document"): void
|
||||
}>()
|
||||
|
||||
const { isEditable, isBranchProtected, toggleIsEditable } = useEditorMeta()
|
||||
const {
|
||||
isEditable,
|
||||
isBranchProtected,
|
||||
toggleIsEditable,
|
||||
isCompactView,
|
||||
toggleCompactView,
|
||||
} = useEditorMeta()
|
||||
const editorStore = useEditorStore()
|
||||
const { isAssistantEnabled } = useCapabilitiesAPI()
|
||||
const { useFetchDocumentBranchesByDocId } = useDocumentAPI()
|
||||
|
|
@ -20,7 +26,10 @@ const fetchBranches = useFetchDocumentBranchesByDocId(
|
|||
() => editorStore.activeDocumentId,
|
||||
)
|
||||
const header = useTemplateRef("header")
|
||||
const { height } = useElementSize(header)
|
||||
const { height, width } = useElementSize(header)
|
||||
const viewToggleVisible = computed(
|
||||
() => width.value > EDITOR_COMPACT_MAX_WIDTH_PX,
|
||||
)
|
||||
const { t } = useI18n({ useScope: "global" })
|
||||
const { updateDocumentBranch, createDocumentBranch, deleteDocumentBranch } =
|
||||
useDocumentAPI()
|
||||
|
|
@ -335,6 +344,25 @@ function activateBranch(branch: "default" | "draft") {
|
|||
</ShadcnUiButton>
|
||||
</ShadcnUiDropdownMenuTrigger>
|
||||
<ShadcnUiDropdownMenuContent side="bottom" align="end" loop>
|
||||
<ShadcnUiDropdownMenuItem
|
||||
v-if="viewToggleVisible"
|
||||
@click="toggleCompactView"
|
||||
>
|
||||
<Icon
|
||||
:name="
|
||||
isCompactView ? 'lucide:maximize-2' : 'lucide:minimize-2'
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
{{
|
||||
isCompactView
|
||||
? $t("editor.navbar.document-options.view.full-title")
|
||||
: $t(
|
||||
"editor.navbar.document-options.view.compact-title",
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</ShadcnUiDropdownMenuItem>
|
||||
<ShadcnUiDropdownMenuItem @click="toggleReviewability">
|
||||
<Icon
|
||||
:name="
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
at,
|
||||
emitFrom,
|
||||
seedAuthSession,
|
||||
seedPersistentState,
|
||||
settleMutations,
|
||||
WAIT_FOR_OPTIONS,
|
||||
} from "~/components/test-helpers"
|
||||
|
|
@ -185,6 +186,7 @@ describe("<DocumentContainer>", { concurrent: false }, () => {
|
|||
redirectToLogin.mockReset()
|
||||
clearQueryCache()
|
||||
useEditorMeta().setEditable(true)
|
||||
seedPersistentState("editor-compact-view", true)
|
||||
useEditorStore().updateActiveDocumentId(DOCUMENT_ID)
|
||||
useEditorStore().updateActiveBranchId(BRANCH_ID)
|
||||
useEditorStore().updateTargetBranchId(null)
|
||||
|
|
@ -518,6 +520,33 @@ describe("<DocumentContainer>", { concurrent: false }, () => {
|
|||
expect(useEditorMeta().isEditable.value).toBe(false)
|
||||
})
|
||||
|
||||
it("caps and centers the editor column in compact view", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const wrapper = await mountContainer()
|
||||
|
||||
await sync(BRANCH_ID)
|
||||
|
||||
const column = wrapper.find(".editor-scroll-highlight-container")
|
||||
expect(column.classes()).toContain("max-w-320")
|
||||
expect(column.classes()).toContain("mx-auto")
|
||||
expect(column.attributes("data-compact-view")).toBe("")
|
||||
})
|
||||
|
||||
it("lets the editor column fill the page in full view", async ({
|
||||
expect,
|
||||
}) => {
|
||||
useEditorMeta().toggleCompactView()
|
||||
const wrapper = await mountContainer()
|
||||
|
||||
await sync(BRANCH_ID)
|
||||
|
||||
const column = wrapper.find(".editor-scroll-highlight-container")
|
||||
expect(column.classes()).not.toContain("max-w-320")
|
||||
expect(column.classes()).not.toContain("mx-auto")
|
||||
expect(column.attributes("data-compact-view")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("passes the content editor on once it is ready", async ({ expect }) => {
|
||||
const wrapper = await mountContainer()
|
||||
await sync(BRANCH_ID)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const { useFetchDocumentBranchesByDocId } = useDocumentAPI()
|
|||
const fetchBranches = useFetchDocumentBranchesByDocId(
|
||||
() => editorStore.activeDocumentId,
|
||||
)
|
||||
const { isEditable } = useEditorMeta()
|
||||
const { isEditable, isCompactView } = useEditorMeta()
|
||||
const route = useRoute()
|
||||
const nuxtApp = useNuxtApp()
|
||||
const fetchDocumentHooks = documentHookAPI.useFetchDocumentHooksByDocID(
|
||||
|
|
@ -370,9 +370,11 @@ function showSettings(target: "github") {
|
|||
-->
|
||||
<div
|
||||
:key="editorStore.activeBranchId"
|
||||
:data-compact-view="isCompactView ? '' : undefined"
|
||||
:class="[
|
||||
SCROLL_HIGHLIGHT_EDITOR_CLASS,
|
||||
'relative flex h-full w-full flex-col items-center transition-opacity duration-300',
|
||||
isCompactView && 'mx-auto max-w-320',
|
||||
readyToReveal ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
]"
|
||||
@transitionend.self="emit('fade-in-complete')"
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ async function executeReviewableAction() {
|
|||
>
|
||||
<div
|
||||
v-show="hookStatus === 'stale'"
|
||||
class="pointer-events-none absolute top-1/2 -left-5 h-full w-1.25 -translate-y-1/2 rounded-r-lg bg-hook-decoration lg:-left-12.5 lg:h-[calc(100%+2rem)]"
|
||||
class="pointer-events-none absolute top-1/2 -left-5 h-full w-1.25 -translate-y-1/2 rounded-r-lg bg-hook-decoration in-data-compact-view:rounded-full lg:-left-12.5 lg:h-[calc(100%+2rem)]"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col-reverse items-start gap-2 sm:flex-row sm:justify-between"
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ function buildDecorations(
|
|||
el.className = cn(
|
||||
"pointer-events-none absolute top-0",
|
||||
"w-1.25 bg-hook-decoration block rounded-r-lg",
|
||||
// the bar hugs the page edge in full view; in compact view the
|
||||
// column floats, so it becomes a pill
|
||||
"in-data-compact-view:rounded-full",
|
||||
placeOutside && "m-0!",
|
||||
)
|
||||
el.ignoreMutation = true
|
||||
|
|
|
|||
|
|
@ -74,4 +74,20 @@ describe("useEditorMeta", { concurrent: false }, () => {
|
|||
|
||||
expect(isReadonly(meta.isEditable)).toBe(true)
|
||||
})
|
||||
|
||||
it("starts in compact view", ({ expect }) => {
|
||||
const meta = useEditorMeta()
|
||||
|
||||
expect(meta.isCompactView.value).toBe(true)
|
||||
})
|
||||
|
||||
it("switches to full view and back", ({ expect }) => {
|
||||
const meta = useEditorMeta()
|
||||
|
||||
meta.toggleCompactView()
|
||||
expect(meta.isCompactView.value).toBe(false)
|
||||
|
||||
meta.toggleCompactView()
|
||||
expect(meta.isCompactView.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const EDITOR_COMPACT_MAX_WIDTH_PX = 1280
|
||||
|
||||
export default function () {
|
||||
// the reader's own read/edit choice. Whether the page can actually be
|
||||
// edited also depends on the branch, so consumers read isEditable
|
||||
|
|
@ -6,6 +8,12 @@ export default function () {
|
|||
defaultValue: true,
|
||||
})
|
||||
|
||||
// compact view caps and centers the editor column on wide screens
|
||||
const compactViewPreference = usePersistentState<boolean>({
|
||||
key: "editor-compact-view",
|
||||
defaultValue: true,
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
const isEditable = computed(
|
||||
|
|
@ -20,6 +28,10 @@ export default function () {
|
|||
editablePreference.value = v
|
||||
}
|
||||
|
||||
function toggleCompactView() {
|
||||
compactViewPreference.value = !compactViewPreference.value
|
||||
}
|
||||
|
||||
function updateLock(v: boolean) {
|
||||
editorStore.updateLock(v)
|
||||
}
|
||||
|
|
@ -28,6 +40,8 @@ export default function () {
|
|||
isEditable,
|
||||
toggleIsEditable,
|
||||
setEditable,
|
||||
isCompactView: computed(() => compactViewPreference.value),
|
||||
toggleCompactView,
|
||||
isBranchProtected: computed(() => editorStore.activeBranchProtected),
|
||||
isLocked: computed(() => editorStore.locked), // locks are used by drag handle menus
|
||||
updateLock,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@
|
|||
}
|
||||
},
|
||||
"document-options": {
|
||||
"view": {
|
||||
"full-title": "Full View",
|
||||
"compact-title": "Compact View"
|
||||
},
|
||||
"review-workflow": {
|
||||
"enable-title": "Make Reviewable",
|
||||
"disable-title": "Make Non Reviewable"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user