feat(drive): refactor document and drawing editors with new metadata handling
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Replaced suite page metadata with drive-specific metadata for document and drawing editors. - Introduced new `driveEditorPageMetadata` function to manage titles and favicons based on editor type. - Updated layout components for document and drawing editors to utilize the new metadata structure. - Enhanced document title handling in various editor components to reflect the current editing context. - Added new SVG icons for UltiDocs, UltiSheets, UltiSlides, and UltiDraw to improve visual consistency across editors. - Improved print styles and layout handling for better document rendering in print and PDF formats.
This commit is contained in:
parent
8f81d7aba1
commit
82ca9a27db
@ -1,11 +1,8 @@
|
|||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { suitePageMetadata } from "@/lib/suite/page-metadata"
|
import { driveEditorPageMetadata } from "@/lib/drive/drive-editor-metadata"
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
return suitePageMetadata({
|
return driveEditorPageMetadata("docs", "Document")
|
||||||
app: "drive",
|
|
||||||
titleSegment: "Document",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DriveDocsEditLayout({
|
export default function DriveDocsEditLayout({
|
||||||
|
|||||||
14
app/drive/draw/[fileId]/edit/layout.tsx
Normal file
14
app/drive/draw/[fileId]/edit/layout.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import { driveEditorPageMetadata } from "@/lib/drive/drive-editor-metadata"
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
return driveEditorPageMetadata("draw", "Dessin")
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DriveDrawEditLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { displayFileBaseName } from "@/lib/drive/display-file-name"
|
import { displayFileBaseName } from "@/lib/drive/display-file-name"
|
||||||
import { suitePageMetadata } from "@/lib/suite/page-metadata"
|
import { driveEditorPageMetadata } from "@/lib/drive/drive-editor-metadata"
|
||||||
|
import { resolveOnlyOfficeEditorKind } from "@/lib/drive/onlyoffice-formats"
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -12,10 +13,8 @@ export async function generateMetadata({ params }: LayoutProps): Promise<Metadat
|
|||||||
const raw = decodeURIComponent(fileId)
|
const raw = decodeURIComponent(fileId)
|
||||||
const baseName = raw.split("/").filter(Boolean).pop() ?? raw
|
const baseName = raw.split("/").filter(Boolean).pop() ?? raw
|
||||||
const name = displayFileBaseName(baseName)
|
const name = displayFileBaseName(baseName)
|
||||||
return suitePageMetadata({
|
const kind = resolveOnlyOfficeEditorKind({ name: baseName })
|
||||||
app: "drive",
|
return driveEditorPageMetadata(kind, name)
|
||||||
titleSegment: name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditLayout({ children }: LayoutProps) {
|
export default function EditLayout({ children }: LayoutProps) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { displayFileBaseName } from "@/lib/drive/display-file-name"
|
|||||||
import { resolveRenameName } from "@/lib/drive/drive-default-name"
|
import { resolveRenameName } from "@/lib/drive/drive-default-name"
|
||||||
import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree"
|
import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree"
|
||||||
import { buildDriveEditHref, resolveDriveEditReturnTo } from "@/lib/drive/drive-url"
|
import { buildDriveEditHref, resolveDriveEditReturnTo } from "@/lib/drive/drive-url"
|
||||||
|
import { resolveOnlyOfficeEditorKind } from "@/lib/drive/onlyoffice-formats"
|
||||||
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
|
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
|
||||||
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
|
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
|
||||||
|
|
||||||
@ -48,7 +49,8 @@ export function OfficeEditor({
|
|||||||
|
|
||||||
const fileName = fileNameFromPath(displayPath)
|
const fileName = fileNameFromPath(displayPath)
|
||||||
const title = displayFileBaseName(fileName)
|
const title = displayFileBaseName(fileName)
|
||||||
useDriveDocumentTitle(title)
|
const editorKind = resolveOnlyOfficeEditorKind({ name: fileName })
|
||||||
|
useDriveDocumentTitle(title, editorKind)
|
||||||
|
|
||||||
const backHref = useMemo(
|
const backHref = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { displayFileBaseName } from "@/lib/drive/display-file-name"
|
|||||||
import { getGuestEditorIdentity } from "@/lib/drive/guest-editor-identity"
|
import { getGuestEditorIdentity } from "@/lib/drive/guest-editor-identity"
|
||||||
import { resolvePublicShareEditReturnTo, shouldShowPublicShareEditorBack } from "@/lib/drive/public-share-url"
|
import { resolvePublicShareEditReturnTo, shouldShowPublicShareEditorBack } from "@/lib/drive/public-share-url"
|
||||||
import type { PublicShareRootType } from "@/lib/drive/public-share-url"
|
import type { PublicShareRootType } from "@/lib/drive/public-share-url"
|
||||||
|
import { resolveOnlyOfficeEditorKind } from "@/lib/drive/onlyoffice-formats"
|
||||||
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
|
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
|
||||||
|
|
||||||
function fileNameFromPath(filePath: string, fallback?: string): string {
|
function fileNameFromPath(filePath: string, fallback?: string): string {
|
||||||
@ -44,7 +45,8 @@ export function PublicOfficeEditor({
|
|||||||
|
|
||||||
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
||||||
const title = displayFileBaseName(fileName)
|
const title = displayFileBaseName(fileName)
|
||||||
useDriveDocumentTitle(title)
|
const editorKind = resolveOnlyOfficeEditorKind({ name: fileName })
|
||||||
|
useDriveDocumentTitle(title, editorKind)
|
||||||
|
|
||||||
const backHref = useMemo(
|
const backHref = useMemo(
|
||||||
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export function PublicRichTextEditor({
|
|||||||
|
|
||||||
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
||||||
const title = displayFileBaseName(fileName)
|
const title = displayFileBaseName(fileName)
|
||||||
useDriveDocumentTitle(title)
|
useDriveDocumentTitle(title, "docs")
|
||||||
|
|
||||||
const backHref = useMemo(
|
const backHref = useMemo(
|
||||||
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export function PublicUltidrawEditor({
|
|||||||
|
|
||||||
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
||||||
const title = displayFileBaseName(fileName)
|
const title = displayFileBaseName(fileName)
|
||||||
useDriveDocumentTitle(title)
|
useDriveDocumentTitle(title, "draw")
|
||||||
|
|
||||||
const backHref = useMemo(
|
const backHref = useMemo(
|
||||||
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
||||||
|
|||||||
@ -918,7 +918,7 @@ export function RichTextDocumentEditor({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{chrome ? (
|
{chrome ? (
|
||||||
<div className="flex min-h-0 flex-1 flex-row">
|
<div className="flex min-h-0 min-w-0 flex-1 flex-row">
|
||||||
<DocsEditorWorkspace
|
<DocsEditorWorkspace
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pageLayout={pageLayout}
|
pageLayout={pageLayout}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export function RichTextEditor({ fileId }: { fileId: string }) {
|
|||||||
|
|
||||||
const fileName = file?.name ?? fileNameFromPath(displayPath)
|
const fileName = file?.name ?? fileNameFromPath(displayPath)
|
||||||
const title = displayFileBaseName(fileName)
|
const title = displayFileBaseName(fileName)
|
||||||
useDriveDocumentTitle(title)
|
useDriveDocumentTitle(title, "docs")
|
||||||
|
|
||||||
const [backHref, setBackHref] = useState("/drive")
|
const [backHref, setBackHref] = useState("/drive")
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function DocsBodyMarginMasks({
|
|||||||
return (
|
return (
|
||||||
<div key={`body-mask-${index}`} aria-hidden>
|
<div key={`body-mask-${index}`} aria-hidden>
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute z-[15]"
|
className="docs-body-margin-mask pointer-events-none absolute z-[15] box-border border-l border-r border-[#dadce0]"
|
||||||
style={{
|
style={{
|
||||||
top: pageTop,
|
top: pageTop,
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -48,7 +48,7 @@ export function DocsBodyMarginMasks({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute z-[15]"
|
className="docs-body-margin-mask pointer-events-none absolute z-[15] box-border border-l border-r border-[#dadce0]"
|
||||||
style={{
|
style={{
|
||||||
top: footerTop,
|
top: footerTop,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|||||||
@ -178,7 +178,12 @@ export function DocsChrome({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="-mt-1 flex min-w-0 items-center overflow-x-auto overflow-y-visible">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"-mt-1 flex min-w-0 items-center overflow-x-auto overflow-y-visible",
|
||||||
|
"[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<DocsMenubar
|
<DocsMenubar
|
||||||
className="docs-menubar shrink-0"
|
className="docs-menubar shrink-0"
|
||||||
viewMenuActions={viewMenuActions}
|
viewMenuActions={viewMenuActions}
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export function DocsEditorWorkspace({
|
|||||||
}, [onPageStackReady, pageCount, showLayout, zoom])
|
}, [onPageStackReady, pageCount, showLayout, zoom])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="docs-editor-workspace flex min-h-0 flex-1 flex-col">
|
<div className="docs-editor-workspace flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
<DocsRulerMarginDragTooltip tooltip={dragTooltip} />
|
<DocsRulerMarginDragTooltip tooltip={dragTooltip} />
|
||||||
<DocsGraphicFloatingToolbar
|
<DocsGraphicFloatingToolbar
|
||||||
editor={editor}
|
editor={editor}
|
||||||
@ -159,7 +159,7 @@ export function DocsEditorWorkspace({
|
|||||||
{showToolbarShell ? (
|
{showToolbarShell ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"docs-toolbar-shell shrink-0",
|
"docs-toolbar-shell min-w-0 shrink-0",
|
||||||
toolbarShellClassName
|
toolbarShellClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -218,6 +218,9 @@ export function DocsHeaderFooterBand({
|
|||||||
footerGeom.zoneBottom - contentHeight - DOCS_HF_CHROME_BAR_PX
|
footerGeom.zoneBottom - contentHeight - DOCS_HF_CHROME_BAR_PX
|
||||||
const chromeBarTop = isHeader ? headerChromeTop : footerChromeTop
|
const chromeBarTop = isHeader ? headerChromeTop : footerChromeTop
|
||||||
|
|
||||||
|
const editLateralTop = isHeader ? zoneTop : footerChromeTop
|
||||||
|
const editLateralHeight = contentHeight + DOCS_HF_CHROME_BAR_PX
|
||||||
|
|
||||||
const [formatOpen, setFormatOpen] = useState(false)
|
const [formatOpen, setFormatOpen] = useState(false)
|
||||||
const [pageNumOpen, setPageNumOpen] = useState(false)
|
const [pageNumOpen, setPageNumOpen] = useState(false)
|
||||||
const contentPersistTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const contentPersistTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
@ -348,6 +351,26 @@ export function DocsHeaderFooterBand({
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="docs-hf-lateral-border pointer-events-none absolute border-l border-[#dadce0]"
|
||||||
|
style={{
|
||||||
|
top: editLateralTop,
|
||||||
|
left: 0,
|
||||||
|
height: editLateralHeight,
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="docs-hf-lateral-border pointer-events-none absolute border-r border-[#dadce0]"
|
||||||
|
style={{
|
||||||
|
top: editLateralTop,
|
||||||
|
right: 0,
|
||||||
|
width: 0,
|
||||||
|
height: editLateralHeight,
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
{isHeader ? (
|
{isHeader ? (
|
||||||
<div
|
<div
|
||||||
className="docs-hf-separator pointer-events-none absolute border-t border-[#dadce0]"
|
className="docs-hf-separator pointer-events-none absolute border-t border-[#dadce0]"
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
|
import {
|
||||||
|
DOCS_LOGO_BODY,
|
||||||
|
DOCS_LOGO_COLOR,
|
||||||
|
DOCS_LOGO_FOLD,
|
||||||
|
DOCS_LOGO_FOLD_Y_OFFSET,
|
||||||
|
DOCS_LOGO_LINE_1,
|
||||||
|
DOCS_LOGO_LINE_2,
|
||||||
|
} from "@/lib/drive/docs-logo-paths"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
/** material-symbols:description (Iconify MCP) — corps sans le pli. */
|
|
||||||
const BODY =
|
|
||||||
"M8 18h8v-2H8zm0-4h8v-2H8zm-2 8q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22z"
|
|
||||||
const FOLD = "M13 7h5l-5-5z"
|
|
||||||
const LINE_1 = "M8 18h8v-2H8z"
|
|
||||||
const LINE_2 = "M8 14h8v-2H8z"
|
|
||||||
|
|
||||||
/** Décalage vertical du pli blanc (viewBox 24). */
|
|
||||||
const FOLD_Y_OFFSET = 1
|
|
||||||
|
|
||||||
export function DocsLogoIcon({ className }: { className?: string }) {
|
export function DocsLogoIcon({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -18,13 +16,13 @@ export function DocsLogoIcon({ className }: { className?: string }) {
|
|||||||
className={cn("shrink-0", className)}
|
className={cn("shrink-0", className)}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<path fill="#4285F4" d={BODY} />
|
<path fill={DOCS_LOGO_COLOR} d={DOCS_LOGO_BODY} />
|
||||||
<path fill="#ffffff" d={LINE_1} />
|
<path fill="#ffffff" d={DOCS_LOGO_LINE_1} />
|
||||||
<path fill="#ffffff" d={LINE_2} />
|
<path fill="#ffffff" d={DOCS_LOGO_LINE_2} />
|
||||||
<path
|
<path
|
||||||
fill="#ffffff"
|
fill="#ffffff"
|
||||||
d={FOLD}
|
d={DOCS_LOGO_FOLD}
|
||||||
transform={`translate(0 ${FOLD_Y_OFFSET})`}
|
transform={`translate(0 ${DOCS_LOGO_FOLD_Y_OFFSET})`}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -557,8 +557,10 @@ function DocsToolbarInner({
|
|||||||
fonts,
|
fonts,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const reservedTrailingPx = showChromeToggle ? 44 : 0
|
||||||
const { containerRef, measureRef, visibleCount, hasOverflow } = useToolbarOverflow(
|
const { containerRef, measureRef, visibleCount, hasOverflow } = useToolbarOverflow(
|
||||||
segments.length
|
segments.length,
|
||||||
|
reservedTrailingPx
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!editor) return null
|
if (!editor) return null
|
||||||
@ -569,7 +571,7 @@ function DocsToolbarInner({
|
|||||||
const toolbarRow = (
|
const toolbarRow = (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="docs-toolbar relative flex items-center gap-0 overflow-hidden px-1.5 py-0.5"
|
className="docs-toolbar relative flex min-w-0 w-full max-w-full items-center gap-0 overflow-hidden px-1.5 py-0.5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={measureRef}
|
ref={measureRef}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export function UltidrawEditor({ fileId }: { fileId: string }) {
|
|||||||
|
|
||||||
const fileName = file?.name ?? fileNameFromPath(displayPath)
|
const fileName = file?.name ?? fileNameFromPath(displayPath)
|
||||||
const title = displayFileBaseName(fileName)
|
const title = displayFileBaseName(fileName)
|
||||||
useDriveDocumentTitle(title)
|
useDriveDocumentTitle(title, "draw")
|
||||||
|
|
||||||
const [backHref, setBackHref] = useState("/drive")
|
const [backHref, setBackHref] = useState("/drive")
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../ulti-backend"
|
"path": "../ulti-backend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../ultisuite-deploy"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {}
|
||||||
|
|||||||
8
lib/drive/docs-logo-paths.ts
Normal file
8
lib/drive/docs-logo-paths.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** Shared UltiDocs logo paths (material-symbols:description). Used by chrome + favicon. */
|
||||||
|
export const DOCS_LOGO_BODY =
|
||||||
|
"M8 18h8v-2H8zm0-4h8v-2H8zm-2 8q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22z"
|
||||||
|
export const DOCS_LOGO_FOLD = "M13 7h5l-5-5z"
|
||||||
|
export const DOCS_LOGO_LINE_1 = "M8 18h8v-2H8z"
|
||||||
|
export const DOCS_LOGO_LINE_2 = "M8 14h8v-2H8z"
|
||||||
|
export const DOCS_LOGO_FOLD_Y_OFFSET = 1
|
||||||
|
export const DOCS_LOGO_COLOR = "#4285F4"
|
||||||
@ -2,19 +2,179 @@ import type { DocPageLayout } from "@/lib/drive/doc-page-setup"
|
|||||||
import { buildParagraphStylesCss } from "@/lib/drive/docs-paragraph-styles-css"
|
import { buildParagraphStylesCss } from "@/lib/drive/docs-paragraph-styles-css"
|
||||||
import type { DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles"
|
import type { DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles"
|
||||||
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
|
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
|
||||||
|
import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants"
|
||||||
|
|
||||||
const PRINT_STYLE_ID = "docs-print-dynamic-styles"
|
const PRINT_STYLE_ID = "docs-print-dynamic-styles"
|
||||||
|
const PRINT_IFRAME_ID = "docs-print-iframe"
|
||||||
|
|
||||||
function buildPageRule(pageLayout: DocPageLayout): string {
|
function buildPageRule(pageLayout: DocPageLayout): string {
|
||||||
const wMm = pageLayout.format.widthMm
|
const wMm = pageLayout.format.widthMm
|
||||||
const hMm = pageLayout.format.heightMm
|
const hMm = pageLayout.format.heightMm
|
||||||
const landscape =
|
const landscape = pageLayout.format.widthMm > pageLayout.format.heightMm
|
||||||
pageLayout.format.widthMm > pageLayout.format.heightMm ||
|
|
||||||
(pageLayout.format.widthMm === pageLayout.format.heightMm && false)
|
|
||||||
const size = landscape ? `${hMm}mm ${wMm}mm` : `${wMm}mm ${hMm}mm`
|
const size = landscape ? `${hMm}mm ${wMm}mm` : `${wMm}mm ${hMm}mm`
|
||||||
return `@page { size: ${size}; margin: 0; }`
|
return `@page { size: ${size}; margin: 0; }`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPrintLayoutRules(pageLayout: DocPageLayout): string {
|
||||||
|
const pageWidth = pageLayout.widthPx
|
||||||
|
const pageHeight = pageLayout.heightPx
|
||||||
|
return `
|
||||||
|
html, body {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: white !important;
|
||||||
|
width: ${pageWidth}px !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-print-root {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: ${pageWidth}px !important;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-docs-page-stack] {
|
||||||
|
position: relative !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
transform: none !important;
|
||||||
|
transform-origin: top left !important;
|
||||||
|
width: ${pageWidth}px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ultidrive-docs-page {
|
||||||
|
width: ${pageWidth}px !important;
|
||||||
|
height: ${pageHeight}px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: none !important;
|
||||||
|
break-after: page;
|
||||||
|
page-break-after: always;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ultidrive-docs-page:last-child {
|
||||||
|
break-after: auto;
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-body-margin-mask {
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-hf-chrome,
|
||||||
|
.docs-graphic-handle,
|
||||||
|
.docs-graphic-rotate-handle,
|
||||||
|
.docs-graphic-outline,
|
||||||
|
.docs-graphic-snap-guides {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-hf-band .docs-region-editor-root {
|
||||||
|
border: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StyleRestore = () => void
|
||||||
|
|
||||||
|
function patchInlineStyle(el: HTMLElement, property: string, value: string): StyleRestore {
|
||||||
|
const previous = el.style.getPropertyValue(property)
|
||||||
|
const priority = el.style.getPropertyPriority(property)
|
||||||
|
el.style.setProperty(property, value)
|
||||||
|
return () => {
|
||||||
|
if (previous) {
|
||||||
|
el.style.setProperty(property, previous, priority)
|
||||||
|
} else {
|
||||||
|
el.style.removeProperty(property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse on-screen page gaps and zoom offsets so print matches page size. */
|
||||||
|
function adjustPageStackForPrint(
|
||||||
|
stack: HTMLElement,
|
||||||
|
pageLayout: DocPageLayout,
|
||||||
|
pageCount: number
|
||||||
|
): StyleRestore {
|
||||||
|
const restores: StyleRestore[] = []
|
||||||
|
const pageWidth = pageLayout.widthPx
|
||||||
|
const pageHeight = pageLayout.heightPx
|
||||||
|
const pageStep = pageHeight + DOCS_PAGE_GAP_PX
|
||||||
|
const gapTotal = Math.max(0, pageCount - 1) * DOCS_PAGE_GAP_PX
|
||||||
|
|
||||||
|
restores.push(patchInlineStyle(stack, "transform", "none"))
|
||||||
|
restores.push(patchInlineStyle(stack, "left", "0"))
|
||||||
|
restores.push(patchInlineStyle(stack, "width", `${pageWidth}px`))
|
||||||
|
|
||||||
|
let wrapper = stack.parentElement
|
||||||
|
while (wrapper && !wrapper.classList.contains("ultidrive-docs-canvas")) {
|
||||||
|
restores.push(patchInlineStyle(wrapper, "width", `${pageWidth}px`))
|
||||||
|
restores.push(patchInlineStyle(wrapper, "height", "auto"))
|
||||||
|
restores.push(patchInlineStyle(wrapper, "min-height", "0"))
|
||||||
|
restores.push(patchInlineStyle(wrapper, "padding-top", "0"))
|
||||||
|
restores.push(patchInlineStyle(wrapper, "padding-bottom", "0"))
|
||||||
|
restores.push(patchInlineStyle(wrapper, "margin-left", "0"))
|
||||||
|
restores.push(patchInlineStyle(wrapper, "margin-right", "0"))
|
||||||
|
wrapper = wrapper.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackHeight = parseFloat(stack.style.height)
|
||||||
|
if (Number.isFinite(stackHeight) && gapTotal > 0) {
|
||||||
|
restores.push(patchInlineStyle(stack, "height", `${stackHeight - gapTotal}px`))
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.querySelectorAll<HTMLElement>("[style]").forEach((el) => {
|
||||||
|
const top = parseFloat(el.style.top)
|
||||||
|
if (!Number.isFinite(top) || top <= 0) return
|
||||||
|
const pageIndex = Math.floor(top / pageStep)
|
||||||
|
const nextTop = top - pageIndex * DOCS_PAGE_GAP_PX
|
||||||
|
if (nextTop !== top) {
|
||||||
|
restores.push(patchInlineStyle(el, "top", `${nextTop}px`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stack.querySelectorAll<HTMLElement>(".docs-page-flow-spacer").forEach((spacer) => {
|
||||||
|
const height = parseFloat(spacer.style.height)
|
||||||
|
if (Number.isFinite(height) && height >= DOCS_PAGE_GAP_PX) {
|
||||||
|
restores.push(
|
||||||
|
patchInlineStyle(spacer, "height", `${height - DOCS_PAGE_GAP_PX}px`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const surface = stack.querySelector<HTMLElement>(".ultidrive-docs-editor-surface")
|
||||||
|
if (surface) {
|
||||||
|
const surfaceHeight = parseFloat(surface.style.height)
|
||||||
|
if (Number.isFinite(surfaceHeight) && gapTotal > 0) {
|
||||||
|
restores.push(
|
||||||
|
patchInlineStyle(surface, "height", `${surfaceHeight - gapTotal}px`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (let i = restores.length - 1; i >= 0; i -= 1) {
|
||||||
|
restores[i]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDynamicPrintStyles(
|
||||||
|
pageLayout: DocPageLayout,
|
||||||
|
paragraphStyles: DocParagraphStylesCatalog
|
||||||
|
): string {
|
||||||
|
return [
|
||||||
|
buildPageRule(pageLayout),
|
||||||
|
buildPrintLayoutRules(pageLayout),
|
||||||
|
buildParagraphStylesCss(paragraphStyles, ".ultidrive-richtext-editor, .ultidrive-richtext-region-editor"),
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
function injectPrintStyles(
|
function injectPrintStyles(
|
||||||
pageLayout: DocPageLayout,
|
pageLayout: DocPageLayout,
|
||||||
paragraphStyles: DocParagraphStylesCatalog
|
paragraphStyles: DocParagraphStylesCatalog
|
||||||
@ -24,10 +184,7 @@ function injectPrintStyles(
|
|||||||
|
|
||||||
const style = document.createElement("style")
|
const style = document.createElement("style")
|
||||||
style.id = PRINT_STYLE_ID
|
style.id = PRINT_STYLE_ID
|
||||||
style.textContent = [
|
style.textContent = buildDynamicPrintStyles(pageLayout, paragraphStyles)
|
||||||
buildPageRule(pageLayout),
|
|
||||||
buildParagraphStylesCss(paragraphStyles, ".ultidrive-richtext-editor, .ultidrive-richtext-region-editor"),
|
|
||||||
].join("\n")
|
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -35,6 +192,108 @@ function injectPrintStyles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyDocumentStyles(targetDoc: Document): void {
|
||||||
|
document.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => {
|
||||||
|
targetDoc.head.appendChild(node.cloneNode(true))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForDocumentReady(doc: Document): Promise<void> {
|
||||||
|
const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]'))
|
||||||
|
await Promise.all(
|
||||||
|
links.map(
|
||||||
|
(link) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
const sheet = link as HTMLLinkElement
|
||||||
|
if (sheet.sheet) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
link.addEventListener("load", () => resolve(), { once: true })
|
||||||
|
link.addEventListener("error", () => resolve(), { once: true })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (doc.fonts?.ready) {
|
||||||
|
await doc.fonts.ready
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePrintIframe(): void {
|
||||||
|
document.getElementById(PRINT_IFRAME_ID)?.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printStackInIframe(
|
||||||
|
stack: HTMLElement,
|
||||||
|
snapshot: DocsExportSnapshot
|
||||||
|
): Promise<void> {
|
||||||
|
removePrintIframe()
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe")
|
||||||
|
iframe.id = PRINT_IFRAME_ID
|
||||||
|
iframe.setAttribute("aria-hidden", "true")
|
||||||
|
iframe.style.cssText =
|
||||||
|
"position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0;pointer-events:none"
|
||||||
|
document.body.appendChild(iframe)
|
||||||
|
|
||||||
|
const doc = iframe.contentDocument
|
||||||
|
const win = iframe.contentWindow
|
||||||
|
if (!doc || !win) {
|
||||||
|
removePrintIframe()
|
||||||
|
throw new Error("Impossible de préparer la fenêtre d'impression")
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDocumentStyles(doc)
|
||||||
|
|
||||||
|
const printStyle = doc.createElement("style")
|
||||||
|
printStyle.textContent = buildDynamicPrintStyles(
|
||||||
|
snapshot.pageLayout,
|
||||||
|
snapshot.paragraphStyles
|
||||||
|
)
|
||||||
|
doc.head.appendChild(printStyle)
|
||||||
|
|
||||||
|
doc.documentElement.classList.add("docs-printing")
|
||||||
|
|
||||||
|
const root = doc.createElement("div")
|
||||||
|
root.className = "docs-print-root"
|
||||||
|
root.appendChild(stack.cloneNode(true))
|
||||||
|
doc.body.replaceChildren(root)
|
||||||
|
|
||||||
|
await waitForDocumentReady(doc)
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const cleanup = () => {
|
||||||
|
win.removeEventListener("afterprint", onAfterPrint)
|
||||||
|
removePrintIframe()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAfterPrint = () => {
|
||||||
|
cleanup()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
win.addEventListener("afterprint", onAfterPrint)
|
||||||
|
|
||||||
|
try {
|
||||||
|
win.focus()
|
||||||
|
win.print()
|
||||||
|
} catch (error) {
|
||||||
|
cleanup()
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (document.getElementById(PRINT_IFRAME_ID)) {
|
||||||
|
cleanup()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForPrintLayout(): Promise<void> {
|
async function waitForPrintLayout(): Promise<void> {
|
||||||
if (typeof document !== "undefined" && document.fonts?.ready) {
|
if (typeof document !== "undefined" && document.fonts?.ready) {
|
||||||
await document.fonts.ready
|
await document.fonts.ready
|
||||||
@ -56,9 +315,17 @@ export async function prepareDocsPrintEnvironment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeStyles = injectPrintStyles(snapshot.pageLayout, snapshot.paragraphStyles)
|
const removeStyles = injectPrintStyles(snapshot.pageLayout, snapshot.paragraphStyles)
|
||||||
|
|
||||||
|
const stack = snapshot.getPageStackElement()
|
||||||
|
const restoreStackLayout =
|
||||||
|
stack != null
|
||||||
|
? adjustPageStackForPrint(stack, snapshot.pageLayout, snapshot.pageCount)
|
||||||
|
: () => {}
|
||||||
|
|
||||||
await waitForPrintLayout()
|
await waitForPrintLayout()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
restoreStackLayout()
|
||||||
root.classList.remove("docs-printing", "docs-pdf-capture")
|
root.classList.remove("docs-printing", "docs-pdf-capture")
|
||||||
removeStyles()
|
removeStyles()
|
||||||
}
|
}
|
||||||
@ -67,18 +334,15 @@ export async function prepareDocsPrintEnvironment(
|
|||||||
export async function printDocsDocument(snapshot: DocsExportSnapshot): Promise<void> {
|
export async function printDocsDocument(snapshot: DocsExportSnapshot): Promise<void> {
|
||||||
const cleanup = await prepareDocsPrintEnvironment(snapshot, "print")
|
const cleanup = await prepareDocsPrintEnvironment(snapshot, "print")
|
||||||
|
|
||||||
const onAfterPrint = () => {
|
try {
|
||||||
cleanup()
|
const stack = snapshot.getPageStackElement()
|
||||||
window.removeEventListener("afterprint", onAfterPrint)
|
if (!stack) {
|
||||||
}
|
|
||||||
window.addEventListener("afterprint", onAfterPrint)
|
|
||||||
|
|
||||||
window.print()
|
window.print()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback cleanup if afterprint never fires (some browsers)
|
await printStackInIframe(stack, snapshot)
|
||||||
window.setTimeout(() => {
|
} finally {
|
||||||
if (document.documentElement.classList.contains("docs-printing")) {
|
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|||||||
97
lib/drive/drive-editor-metadata.ts
Normal file
97
lib/drive/drive-editor-metadata.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import { SUITE_TITLE_SEP } from "@/lib/suite/page-metadata"
|
||||||
|
|
||||||
|
export type DriveEditorKind = "docs" | "sheets" | "presentation" | "draw"
|
||||||
|
|
||||||
|
export const DRIVE_EDITOR_LABELS: Record<DriveEditorKind, string> = {
|
||||||
|
docs: "UltiDocs",
|
||||||
|
sheets: "Sheets",
|
||||||
|
presentation: "Presentation",
|
||||||
|
draw: "Draw",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DRIVE_EDITOR_FAVICONS: Record<DriveEditorKind, string> = {
|
||||||
|
docs: "/drive/ultidocs-mark.svg",
|
||||||
|
sheets: "/drive/ultisheets-mark.svg",
|
||||||
|
presentation: "/drive/ultislides-mark.svg",
|
||||||
|
draw: "/drive/ultidraw-mark.svg",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function driveEditorDocumentTitle(
|
||||||
|
titleSegment: string,
|
||||||
|
kind: DriveEditorKind
|
||||||
|
): string {
|
||||||
|
const trimmed = titleSegment.trim()
|
||||||
|
const label = DRIVE_EDITOR_LABELS[kind]
|
||||||
|
if (!trimmed) return label
|
||||||
|
return `${trimmed}${SUITE_TITLE_SEP}${label}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function driveEditorPageMetadata(
|
||||||
|
kind: DriveEditorKind,
|
||||||
|
titleSegment?: string
|
||||||
|
): Metadata {
|
||||||
|
const label = DRIVE_EDITOR_LABELS[kind]
|
||||||
|
const favicon = DRIVE_EDITOR_FAVICONS[kind]
|
||||||
|
|
||||||
|
return {
|
||||||
|
title:
|
||||||
|
titleSegment !== undefined
|
||||||
|
? {
|
||||||
|
default: titleSegment,
|
||||||
|
template: `%s${SUITE_TITLE_SEP}${label}`,
|
||||||
|
}
|
||||||
|
: label,
|
||||||
|
icons: {
|
||||||
|
icon: [{ url: favicon, type: "image/svg+xml" }],
|
||||||
|
apple: [{ url: favicon, type: "image/svg+xml" }],
|
||||||
|
shortcut: favicon,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconLinks(): HTMLLinkElement[] {
|
||||||
|
return Array.from(
|
||||||
|
document.querySelectorAll<HTMLLinkElement>(
|
||||||
|
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyDriveEditorFavicon(kind: DriveEditorKind): () => void {
|
||||||
|
const href = DRIVE_EDITOR_FAVICONS[kind]
|
||||||
|
const links = iconLinks()
|
||||||
|
const previous = links.map((link) => ({
|
||||||
|
link,
|
||||||
|
href: link.getAttribute("href"),
|
||||||
|
type: link.getAttribute("type"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (links.length === 0) {
|
||||||
|
const link = document.createElement("link")
|
||||||
|
link.rel = "icon"
|
||||||
|
link.type = "image/svg+xml"
|
||||||
|
link.href = href
|
||||||
|
document.head.appendChild(link)
|
||||||
|
return () => {
|
||||||
|
link.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
link.href = href
|
||||||
|
link.type = "image/svg+xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const { link, href: prevHref, type } of previous) {
|
||||||
|
if (prevHref == null) {
|
||||||
|
link.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
link.href = prevHref
|
||||||
|
if (type) link.type = type
|
||||||
|
else link.removeAttribute("type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -120,3 +120,33 @@ export function isOnlyOfficeFile(file: {
|
|||||||
if (mime && isOnlyOfficeMime(mime)) return true
|
if (mime && isOnlyOfficeMime(mime)) return true
|
||||||
return isOnlyOfficeExtension(fileExtension(file.name))
|
return isOnlyOfficeExtension(fileExtension(file.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ONLYOFFICE_CELL_SET = new Set<string>(ONLYOFFICE_CELL)
|
||||||
|
const ONLYOFFICE_SLIDE_SET = new Set<string>([...ONLYOFFICE_SLIDE, ...ONLYOFFICE_DIAGRAM])
|
||||||
|
|
||||||
|
export function resolveOnlyOfficeEditorKind(file: {
|
||||||
|
name: string
|
||||||
|
mime_type?: string
|
||||||
|
}): "sheets" | "presentation" {
|
||||||
|
const mime = (file.mime_type ?? "").toLowerCase()
|
||||||
|
const ext = fileExtension(file.name)
|
||||||
|
|
||||||
|
if (
|
||||||
|
mime.includes("spreadsheet") ||
|
||||||
|
mime.includes("excel") ||
|
||||||
|
ONLYOFFICE_CELL_SET.has(ext)
|
||||||
|
) {
|
||||||
|
return "sheets"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
mime.includes("presentation") ||
|
||||||
|
mime.includes("powerpoint") ||
|
||||||
|
mime.includes("visio") ||
|
||||||
|
ONLYOFFICE_SLIDE_SET.has(ext)
|
||||||
|
) {
|
||||||
|
return "presentation"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "sheets"
|
||||||
|
}
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { SUITE_TITLE_SEP } from "@/lib/suite/page-metadata"
|
import {
|
||||||
|
applyDriveEditorFavicon,
|
||||||
|
driveEditorDocumentTitle,
|
||||||
|
type DriveEditorKind,
|
||||||
|
} from "@/lib/drive/drive-editor-metadata"
|
||||||
|
|
||||||
export function useDriveDocumentTitle(titleSegment: string) {
|
export function useDriveDocumentTitle(titleSegment: string, kind: DriveEditorKind) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const trimmed = titleSegment.trim()
|
const previousTitle = document.title
|
||||||
if (!trimmed) return
|
document.title = driveEditorDocumentTitle(titleSegment, kind)
|
||||||
const previous = document.title
|
const restoreFavicon = applyDriveEditorFavicon(kind)
|
||||||
document.title = `${trimmed}${SUITE_TITLE_SEP}UltiDrive`
|
|
||||||
return () => {
|
return () => {
|
||||||
document.title = previous
|
document.title = previousTitle
|
||||||
|
restoreFavicon()
|
||||||
}
|
}
|
||||||
}, [titleSegment])
|
}, [titleSegment, kind])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,15 @@ import { useLayoutEffect, useRef, useState } from "react"
|
|||||||
|
|
||||||
const OVERFLOW_BUTTON_WIDTH = 36
|
const OVERFLOW_BUTTON_WIDTH = 36
|
||||||
|
|
||||||
export function useToolbarOverflow(itemCount: number) {
|
export function useToolbarOverflow(itemCount: number, reservedTrailingPx = 0) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const measureRef = useRef<HTMLDivElement>(null)
|
const measureRef = useRef<HTMLDivElement>(null)
|
||||||
const [visibleCount, setVisibleCount] = useState(itemCount)
|
const [visibleCount, setVisibleCount] = useState(itemCount)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setVisibleCount(itemCount)
|
||||||
|
}, [itemCount])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
const measure = measureRef.current
|
const measure = measureRef.current
|
||||||
@ -16,15 +20,24 @@ export function useToolbarOverflow(itemCount: number) {
|
|||||||
|
|
||||||
const recalculate = () => {
|
const recalculate = () => {
|
||||||
const children = Array.from(measure.children) as HTMLElement[]
|
const children = Array.from(measure.children) as HTMLElement[]
|
||||||
if (children.length === 0) return
|
if (children.length === 0) {
|
||||||
|
setVisibleCount(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerWidth = container.clientWidth
|
||||||
|
if (containerWidth <= 0) return
|
||||||
|
|
||||||
const totalWidth = children.reduce((sum, child) => sum + child.offsetWidth, 0)
|
const totalWidth = children.reduce((sum, child) => sum + child.offsetWidth, 0)
|
||||||
if (totalWidth <= container.clientWidth) {
|
const trailingReserve = reservedTrailingPx
|
||||||
|
const maxWithoutOverflow = containerWidth - trailingReserve
|
||||||
|
|
||||||
|
if (totalWidth <= maxWithoutOverflow) {
|
||||||
setVisibleCount(children.length)
|
setVisibleCount(children.length)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = container.clientWidth - OVERFLOW_BUTTON_WIDTH
|
const available = containerWidth - OVERFLOW_BUTTON_WIDTH - trailingReserve
|
||||||
let used = 0
|
let used = 0
|
||||||
let fit = 0
|
let fit = 0
|
||||||
|
|
||||||
@ -39,10 +52,22 @@ export function useToolbarOverflow(itemCount: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recalculate()
|
recalculate()
|
||||||
|
|
||||||
const ro = new ResizeObserver(recalculate)
|
const ro = new ResizeObserver(recalculate)
|
||||||
ro.observe(container)
|
ro.observe(container)
|
||||||
return () => ro.disconnect()
|
ro.observe(measure)
|
||||||
}, [itemCount])
|
for (const child of measure.children) {
|
||||||
|
ro.observe(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mo = new MutationObserver(recalculate)
|
||||||
|
mo.observe(measure, { childList: true })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ro.disconnect()
|
||||||
|
mo.disconnect()
|
||||||
|
}
|
||||||
|
}, [itemCount, reservedTrailingPx])
|
||||||
|
|
||||||
const hasOverflow = visibleCount < itemCount
|
const hasOverflow = visibleCount < itemCount
|
||||||
|
|
||||||
|
|||||||
7
public/drive/ultidocs-mark.svg
Normal file
7
public/drive/ultidocs-mark.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!-- Keep in sync with lib/drive/docs-logo-paths.ts + DocsLogoIcon -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="UltiDocs">
|
||||||
|
<path fill="#4285F4" d="M8 18h8v-2H8zm0-4h8v-2H8zm-2 8q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22z"/>
|
||||||
|
<path fill="#ffffff" d="M8 18h8v-2H8z"/>
|
||||||
|
<path fill="#ffffff" d="M8 14h8v-2H8z"/>
|
||||||
|
<path fill="#ffffff" d="M13 7h5l-5-5z" transform="translate(0 1)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
5
public/drive/ultidraw-mark.svg
Normal file
5
public/drive/ultidraw-mark.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="UltiDraw">
|
||||||
|
<rect width="40" height="40" rx="8" fill="#6965DB"/>
|
||||||
|
<path fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" d="M11 27c4-8 8-12 12-14 2 4 2 8 0 12-4 2-8 2-12 2z"/>
|
||||||
|
<path fill="none" stroke="#FFC107" stroke-width="2.5" stroke-linecap="round" d="M24 13l5-4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 410 B |
5
public/drive/ultisheets-mark.svg
Normal file
5
public/drive/ultisheets-mark.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="UltiSheets">
|
||||||
|
<path fill="#0F9D58" d="M8 6h18l8 8v22q0 1.65-1.18 2.83T30 40H10q-1.65 0-2.83-1.17T6 36V8q0-1.65 1.17-2.83T10 6z"/>
|
||||||
|
<path fill="#ffffff" d="M26 6v8h8z" opacity="0.95"/>
|
||||||
|
<path fill="#ffffff" d="M12 18h16v2H12zm0 6h16v2H12zm0 6h11v2H12z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
6
public/drive/ultislides-mark.svg
Normal file
6
public/drive/ultislides-mark.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="UltiPresentation">
|
||||||
|
<path fill="#F4B400" d="M8 6h18l8 8v22q0 1.65-1.18 2.83T30 40H10q-1.65 0-2.83-1.17T6 36V8q0-1.65 1.17-2.83T10 6z"/>
|
||||||
|
<path fill="#ffffff" d="M26 6v8h8z" opacity="0.95"/>
|
||||||
|
<rect x="12" y="17" width="16" height="11" rx="1.5" fill="#ffffff"/>
|
||||||
|
<path fill="#F4B400" d="M20 31 12 36h16z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
@ -1,66 +1,5 @@
|
|||||||
/* UltiDocs print / PDF capture styles */
|
/* UltiDocs PDF capture styles (print uses a dedicated iframe) */
|
||||||
|
|
||||||
@media print {
|
|
||||||
.docs-printing header,
|
|
||||||
.docs-printing .docs-toolbar-shell,
|
|
||||||
.docs-printing .docs-rulers-left-rail,
|
|
||||||
.docs-printing .docs-status-bar,
|
|
||||||
.docs-printing .docs-hf-chrome,
|
|
||||||
.docs-printing .docs-graphic-floating-toolbar,
|
|
||||||
.docs-printing .docs-table-floating-toolbar,
|
|
||||||
.docs-printing .docs-graphic-options-sidebar,
|
|
||||||
.docs-printing .docs-loading-splash,
|
|
||||||
.docs-printing .docs-graphic-handle,
|
|
||||||
.docs-printing .docs-graphic-rotate-handle,
|
|
||||||
.docs-printing .docs-graphic-outline,
|
|
||||||
.docs-printing .docs-graphic-snap-guides,
|
|
||||||
.docs-printing .docs-ruler-margin-drag-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing .ultidrive-docs-canvas {
|
|
||||||
overflow: visible !important;
|
|
||||||
background: white !important;
|
|
||||||
height: auto !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing [data-docs-page-stack] {
|
|
||||||
transform: none !important;
|
|
||||||
position: relative !important;
|
|
||||||
left: auto !important;
|
|
||||||
top: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing .ultidrive-docs-page {
|
|
||||||
box-shadow: none !important;
|
|
||||||
break-after: page;
|
|
||||||
page-break-after: always;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing .ultidrive-docs-page:last-child {
|
|
||||||
break-after: auto;
|
|
||||||
page-break-after: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing .docs-hf-band--editing {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing .docs-hf-band .docs-region-editor-root {
|
|
||||||
border: none !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs-printing #docs-page-graphic-layer-behind .docs-graphic-handle,
|
|
||||||
.docs-printing #docs-page-graphic-layer-front .docs-graphic-handle,
|
|
||||||
.docs-printing #docs-page-graphic-layer-behind .docs-graphic-outline,
|
|
||||||
.docs-printing #docs-page-graphic-layer-front .docs-graphic-outline {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* PDF capture uses the same layout rules without @media print */
|
|
||||||
.docs-printing.docs-pdf-capture header,
|
.docs-printing.docs-pdf-capture header,
|
||||||
.docs-printing.docs-pdf-capture .docs-toolbar-shell,
|
.docs-printing.docs-pdf-capture .docs-toolbar-shell,
|
||||||
.docs-printing.docs-pdf-capture .docs-rulers-left-rail,
|
.docs-printing.docs-pdf-capture .docs-rulers-left-rail,
|
||||||
@ -72,19 +11,53 @@
|
|||||||
.docs-printing.docs-pdf-capture .docs-graphic-handle,
|
.docs-printing.docs-pdf-capture .docs-graphic-handle,
|
||||||
.docs-printing.docs-pdf-capture .docs-graphic-rotate-handle,
|
.docs-printing.docs-pdf-capture .docs-graphic-rotate-handle,
|
||||||
.docs-printing.docs-pdf-capture .docs-graphic-outline,
|
.docs-printing.docs-pdf-capture .docs-graphic-outline,
|
||||||
.docs-printing.docs-pdf-capture .docs-graphic-snap-guides {
|
.docs-printing.docs-pdf-capture .docs-graphic-snap-guides,
|
||||||
|
.docs-printing.docs-pdf-capture .docs-ruler-margin-drag-tooltip,
|
||||||
|
.docs-printing.docs-pdf-capture .docs-editor-workspace > div > .absolute {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-printing.docs-pdf-capture .docs-editor-workspace,
|
||||||
|
.docs-printing.docs-pdf-capture .docs-editor-workspace > div,
|
||||||
|
.docs-printing.docs-pdf-capture .docs-editor-workspace .flex-row {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-printing.docs-pdf-capture .ultidrive-docs-canvas {
|
.docs-printing.docs-pdf-capture .ultidrive-docs-canvas {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
background: white !important;
|
background: white !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-printing.docs-pdf-capture .ultidrive-docs-canvas > div,
|
||||||
|
.docs-printing.docs-pdf-capture .ultidrive-docs-canvas > div > div {
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-printing.docs-pdf-capture [data-docs-page-stack] {
|
.docs-printing.docs-pdf-capture [data-docs-page-stack] {
|
||||||
|
position: relative !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
|
transform-origin: top left !important;
|
||||||
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-printing.docs-pdf-capture .ultidrive-docs-page {
|
.docs-printing.docs-pdf-capture .ultidrive-docs-page {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-printing.docs-pdf-capture .docs-body-margin-mask {
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-printing.docs-pdf-capture .ultidrive-docs-editor-surface--dimmed .ProseMirror {
|
||||||
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -476,6 +476,10 @@ html.dark .ultidrive-richtext-region-editor table th {
|
|||||||
z-index: 24;
|
z-index: 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-hf-lateral-border {
|
||||||
|
z-index: 25;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-hf-chrome {
|
.docs-hf-chrome {
|
||||||
z-index: 27;
|
z-index: 27;
|
||||||
}
|
}
|
||||||
@ -965,6 +969,10 @@ html.dark .docs-menu-badge {
|
|||||||
|
|
||||||
.docs-toolbar-shell {
|
.docs-toolbar-shell {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
padding-inline: 8px 16px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-toolbar-shell--collapsed {
|
.docs-toolbar-shell--collapsed {
|
||||||
@ -973,13 +981,13 @@ html.dark .docs-menu-badge {
|
|||||||
|
|
||||||
.docs-toolbar-shell > .docs-toolbar {
|
.docs-toolbar-shell > .docs-toolbar {
|
||||||
padding-inline: 12px;
|
padding-inline: 12px;
|
||||||
margin-inline-start: 8px;
|
|
||||||
margin-inline-end: 16px;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-toolbar {
|
.docs-toolbar {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
color: #202124;
|
color: #202124;
|
||||||
background: #edf2fa;
|
background: #edf2fa;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user