11'use client'
22
3- import { memo , useEffect , useRef , useState } from 'react'
3+ import { memo , useCallback , useEffect , useRef , useState } from 'react'
44import { createLogger } from '@sim/logger'
55import { toError } from '@sim/utils/errors'
66import { cn } from '@/lib/core/utils/cn'
@@ -15,6 +15,42 @@ import {
1515
1616const logger = createLogger ( 'DocxPreview' )
1717
18+ /**
19+ * Fit the rendered docx pages to the host container width using a CSS scale.
20+ * The library renders `<section class="docx">` at the document's natural page
21+ * width (in cm), which overflows narrow panels.
22+ */
23+ function fitDocxToContainer ( host : HTMLElement ) {
24+ const wrapper = host . querySelector < HTMLElement > ( '.docx-wrapper' )
25+ if ( ! wrapper ) return
26+ const section = wrapper . querySelector < HTMLElement > ( 'section.docx' )
27+ if ( ! section ) return
28+
29+ wrapper . style . transform = ''
30+ wrapper . style . transformOrigin = 'top left'
31+ wrapper . style . width = ''
32+ wrapper . style . marginRight = ''
33+ wrapper . style . marginBottom = ''
34+
35+ const naturalPageWidth = section . offsetWidth
36+ if ( ! naturalPageWidth ) return
37+
38+ const wrapperStyle = window . getComputedStyle ( wrapper )
39+ const horizontalPadding =
40+ Number . parseFloat ( wrapperStyle . paddingLeft ) + Number . parseFloat ( wrapperStyle . paddingRight )
41+ const naturalWrapperWidth = naturalPageWidth + horizontalPadding
42+ const available = host . clientWidth
43+ const scale = Math . min ( 1 , available / naturalWrapperWidth )
44+
45+ if ( scale >= 1 ) return
46+
47+ wrapper . style . width = `${ naturalWrapperWidth } px`
48+ wrapper . style . transform = `scale(${ scale } )`
49+ const naturalHeight = wrapper . offsetHeight
50+ wrapper . style . marginRight = `${ ( scale - 1 ) * naturalWrapperWidth } px`
51+ wrapper . style . marginBottom = `${ ( scale - 1 ) * naturalHeight } px`
52+ }
53+
1854export const DocxPreview = memo ( function DocxPreview ( {
1955 file,
2056 workspaceId,
@@ -35,6 +71,25 @@ export const DocxPreview = memo(function DocxPreview({
3571 const [ rendering , setRendering ] = useState ( false )
3672 const [ hasRenderedPreview , setHasRenderedPreview ] = useState ( false )
3773
74+ const applyPostRenderStyling = useCallback ( ( ) => {
75+ const container = containerRef . current
76+ if ( ! container ) return
77+ const wrapper = container . querySelector < HTMLElement > ( '.docx-wrapper' )
78+ if ( wrapper ) wrapper . style . background = 'transparent'
79+ container . querySelectorAll < HTMLElement > ( 'section.docx' ) . forEach ( ( page ) => {
80+ page . style . boxShadow = 'var(--shadow-medium)'
81+ } )
82+ fitDocxToContainer ( container )
83+ } , [ ] )
84+
85+ useEffect ( ( ) => {
86+ const container = containerRef . current
87+ if ( ! container ) return
88+ const observer = new ResizeObserver ( ( ) => fitDocxToContainer ( container ) )
89+ observer . observe ( container )
90+ return ( ) => observer . disconnect ( )
91+ } , [ ] )
92+
3893 useEffect ( ( ) => {
3994 if ( ! containerRef . current || ! fileData || streamingContent !== undefined ) return
4095
@@ -53,11 +108,7 @@ export const DocxPreview = memo(function DocxPreview({
53108 ignoreHeight : false ,
54109 } )
55110 if ( ! cancelled && containerRef . current ) {
56- const wrapper = containerRef . current . querySelector < HTMLElement > ( '.docx-wrapper' )
57- if ( wrapper ) wrapper . style . background = 'transparent'
58- containerRef . current . querySelectorAll < HTMLElement > ( 'section.docx' ) . forEach ( ( page ) => {
59- page . style . boxShadow = 'var(--shadow-medium)'
60- } )
111+ applyPostRenderStyling ( )
61112 lastSuccessfulHtmlRef . current = containerRef . current . innerHTML
62113 setHasRenderedPreview ( true )
63114 }
@@ -78,7 +129,7 @@ export const DocxPreview = memo(function DocxPreview({
78129 return ( ) => {
79130 cancelled = true
80131 }
81- } , [ fileData , streamingContent ] )
132+ } , [ fileData , streamingContent , applyPostRenderStyling ] )
82133
83134 useEffect ( ( ) => {
84135 if ( streamingContent === undefined || ! containerRef . current ) return
@@ -121,18 +172,15 @@ export const DocxPreview = memo(function DocxPreview({
121172 } )
122173
123174 if ( ! cancelled && containerRef . current ) {
124- const wrapper = containerRef . current . querySelector < HTMLElement > ( '.docx-wrapper' )
125- if ( wrapper ) wrapper . style . background = 'transparent'
126- containerRef . current . querySelectorAll < HTMLElement > ( 'section.docx' ) . forEach ( ( page ) => {
127- page . style . boxShadow = 'var(--shadow-medium)'
128- } )
175+ applyPostRenderStyling ( )
129176 lastSuccessfulHtmlRef . current = containerRef . current . innerHTML
130177 setHasRenderedPreview ( true )
131178 }
132179 } catch ( err ) {
133180 if ( ! cancelled && ! ( err instanceof DOMException && err . name === 'AbortError' ) ) {
134181 if ( containerRef . current && previousHtml ) {
135182 containerRef . current . innerHTML = previousHtml
183+ applyPostRenderStyling ( )
136184 setHasRenderedPreview ( true )
137185 }
138186 const msg = toError ( err ) . message || 'Failed to render document'
@@ -155,7 +203,7 @@ export const DocxPreview = memo(function DocxPreview({
155203 clearTimeout ( debounceTimer )
156204 controller . abort ( )
157205 }
158- } , [ streamingContent , workspaceId ] )
206+ } , [ streamingContent , workspaceId , applyPostRenderStyling ] )
159207
160208 const error =
161209 hasRenderedPreview && streamingContent !== undefined
0 commit comments