@brillout/docpress 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Layout.tsx CHANGED
@@ -121,7 +121,7 @@ function LayoutDocsPage({ children }: { children: React.ReactNode }) {
121
121
  .page-content {
122
122
  margin: auto;
123
123
  }
124
- #menu-modal {
124
+ #menu-modal-wrapper {
125
125
  position: absolute !important;
126
126
  }
127
127
  `
@@ -7,5 +7,4 @@
7
7
  left: 0;
8
8
  height: 100%;
9
9
  width: 3px;
10
- z-index: 99;
11
10
  }
@@ -26,9 +26,14 @@ function NavigationWithColumnLayout(props: { navItems: NavItem[] }) {
26
26
  return (
27
27
  <>
28
28
  <Style>{getStyle()}</Style>
29
- <div className="navigation-content" style={{ paddingTop: 10 }}>
29
+ <div id="menu-navigation-container" className="navigation-content">
30
30
  {navItemsByColumnLayouts.map((columnLayout, i) => (
31
- <div id={`menu-navigation-${i}`} className="menu-navigation" key={i}>
31
+ <div
32
+ id={`menu-navigation-${i}`}
33
+ className="menu-navigation-content"
34
+ style={{ paddingTop: 10, transition: 'none 0.2s ease-in-out', transitionProperty: 'opacity, transform' }}
35
+ key={i}
36
+ >
32
37
  {columnLayout.isFullWidthCategory ? (
33
38
  <div style={{ marginTop: 0 }}>
34
39
  <ColumnsWrapper numberOfColumns={columnLayout.columns.length}>
@@ -85,17 +90,35 @@ function NavigationWithColumnLayout(props: { navItems: NavItem[] }) {
85
90
  function getStyle() {
86
91
  const style = css`
87
92
  @media(min-width: ${containerQueryMobileMenu + 1}px) {
93
+ .menu-navigation-content {
94
+ position: absolute;
95
+ width: 100%;
96
+ }
88
97
  ${navItemsByColumnLayouts
89
- .map(
90
- (_, i) =>
91
- css`
92
- html:not(.menu-modal-show-${i}) #menu-navigation-${i} {
93
- display: none;
98
+ .map((_, i) => {
99
+ const isFirst = i === 0
100
+ const isLast = i === navItemsByColumnLayouts.length - 1
101
+ return css`
102
+ #menu-navigation-${i} {
103
+ ${/* Fading animation */ ''}
104
+ html:not(.menu-modal-show-${i}) & {
105
+ opacity: 0;
106
+ pointer-events: none;
107
+ }
108
+ ${/* Sliding animation */ ''}
109
+ html:not(.menu-modal-show-${i}).menu-modal-show & {
110
+ ${!isFirst && !isLast ? '' : `transform: translate(${isFirst ? '-' : ''}50px, 0);`}
111
+ }
112
+ ${/* Performance optimization. */ ''}
113
+ ${/* - Using clip-path transition instead of height transition doesn't make a difference: https://github.com/brillout/docpress/commit/005cba0b4cba9c1b526e8e26901ee04129d79715 */ ''}
114
+ ${/* - Suprisingly, this is a performance regression when transitioning from one menu to another (the menu is kept open). Thus we apply this only when the menu is being closed/opened. */ ''}
115
+ html:not(.menu-modal-show-${i}).menu-modal-opening-or-closing & {
116
+ display: none;
117
+ }
94
118
  }
95
- html.menu-modal-show.menu-modal-show-${i} {
96
- .menu-toggle-${i} {
119
+ .menu-toggle-${i} {
120
+ html.menu-modal-show.menu-modal-show-${i} & {
97
121
  color: black !important;
98
- cursor: default !important;
99
122
  [class^='decolorize-'],
100
123
  [class*=' decolorize-'] {
101
124
  filter: grayscale(0) opacity(1) !important;
@@ -104,9 +127,12 @@ html.menu-modal-show.menu-modal-show-${i} {
104
127
  top: 0;
105
128
  }
106
129
  }
130
+ html.menu-modal-show & {
131
+ cursor: default !important;
132
+ }
107
133
  }
108
- `,
109
- )
134
+ `
135
+ })
110
136
  .join('')}
111
137
  }
112
138
  `
@@ -6,19 +6,19 @@ export { closeMenuOnMouseLeave }
6
6
  export { addListenerOpenMenuModal }
7
7
 
8
8
  import { containerQueryMobileLayout } from '../Layout'
9
+ import { getHydrationPromise } from '../renderer/getHydrationPromise'
9
10
  import { getViewportWidth } from '../utils/getViewportWidth'
10
11
  import { isBrowser } from '../utils/isBrowser'
11
12
 
12
13
  initScrollListener()
13
14
 
14
15
  function keepMenuModalOpen() {
15
- if (keepOpenIsDisabled) return
16
16
  open()
17
17
  }
18
18
  function openMenuModal(menuNavigationId: number) {
19
19
  open(menuNavigationId)
20
20
  }
21
- function open(menuNavigationId?: number) {
21
+ async function open(menuNavigationId?: number) {
22
22
  if (menuModalLock) {
23
23
  if (menuNavigationId === undefined) {
24
24
  clearTimeout(menuModalLock?.timeout)
@@ -29,7 +29,10 @@ function open(menuNavigationId?: number) {
29
29
  return
30
30
  }
31
31
  const { classList } = document.documentElement
32
- classList.add('menu-modal-show')
32
+ if (!classList.contains('menu-modal-show')) {
33
+ onBeforeOpeningOrClosing()
34
+ classList.add('menu-modal-show')
35
+ }
33
36
  if (menuNavigationId !== undefined) {
34
37
  const currentModalId = getCurrentMenuId()
35
38
  if (currentModalId === menuNavigationId) return
@@ -37,6 +40,10 @@ function open(menuNavigationId?: number) {
37
40
  classList.remove(`menu-modal-show-${currentModalId}`)
38
41
  }
39
42
  classList.add(`menu-modal-show-${menuNavigationId}`)
43
+ await getHydrationPromise()
44
+ // Because all `.menu-navigation-content` are `position: absolute` we have to propagate the content height ourselves.
45
+ const height = window.getComputedStyle(document.getElementById(`menu-navigation-${menuNavigationId}`)!).height
46
+ document.getElementById('menu-navigation-container')!.style.height = height
40
47
  }
41
48
  listener?.()
42
49
  }
@@ -45,16 +52,31 @@ function addListenerOpenMenuModal(cb: () => void) {
45
52
  listener = cb
46
53
  }
47
54
  function closeMenuModal() {
48
- document.documentElement.classList.remove('menu-modal-show')
55
+ const { classList } = document.documentElement
56
+ if (classList.contains('menu-modal-show')) {
57
+ onBeforeOpeningOrClosing(() => {
58
+ // Remove:
59
+ // menu-modal-show-0
60
+ // menu-modal-show-1
61
+ // ...
62
+ classList.forEach((className) => {
63
+ if (className.startsWith('menu-modal-show-')) {
64
+ classList.remove(className)
65
+ }
66
+ })
67
+ })
68
+ classList.remove('menu-modal-show')
69
+ }
49
70
  }
50
- let keepOpenIsDisabled: true | undefined
51
- function closeAndForbidKeepOpen() {
52
- if (!document.documentElement.classList.contains('menu-modal-show')) return
53
- keepOpenIsDisabled = true
54
- closeMenuModal()
55
- setTimeout(() => {
56
- keepOpenIsDisabled = undefined
57
- }, 500)
71
+ let timeoutModalAnimation: NodeJS.Timeout | undefined
72
+ function onBeforeOpeningOrClosing(cb?: () => void) {
73
+ const { classList } = document.documentElement
74
+ classList.add('menu-modal-opening-or-closing')
75
+ clearTimeout(timeoutModalAnimation)
76
+ timeoutModalAnimation = setTimeout(() => {
77
+ classList.remove('menu-modal-opening-or-closing')
78
+ cb?.()
79
+ }, 450)
58
80
  }
59
81
 
60
82
  let menuModalLock:
@@ -72,7 +94,7 @@ function closeMenuOnMouseLeave() {
72
94
  menuModalLock = undefined
73
95
  if (idNext === idCurrent) return
74
96
  if (idNext === undefined) {
75
- closeAndForbidKeepOpen()
97
+ closeMenuModal()
76
98
  } else {
77
99
  openMenuModal(idNext)
78
100
  }
@@ -94,11 +116,10 @@ function getCurrentMenuId(): null | number {
94
116
 
95
117
  function initScrollListener() {
96
118
  if (!isBrowser()) return
97
- window.addEventListener('scroll', closeAndForbidKeepOpen, { passive: true })
119
+ window.addEventListener('scroll', closeMenuModal, { passive: true })
98
120
  }
99
121
 
100
122
  function toggleMenuModal(menuId: number) {
101
- keepOpenIsDisabled = undefined
102
123
  const { classList } = document.documentElement
103
124
  if (classList.contains('menu-modal-show') && classList.contains(`menu-modal-show-${menuId}`)) {
104
125
  closeMenuModal()
@@ -109,7 +130,7 @@ function toggleMenuModal(menuId: number) {
109
130
  }
110
131
 
111
132
  function autoScroll() {
112
- const nav = document.querySelector('#menu-modal .navigation-content')!
133
+ const nav = document.querySelector('#menu-modal-wrapper .navigation-content')!
113
134
  const href = window.location.pathname
114
135
  const navLinks = Array.from(nav.querySelectorAll(`a[href="${href}"]`))
115
136
  const navLink = navLinks[0] as HTMLElement | undefined
package/MenuModal.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  export { MenuModal }
2
2
 
3
- import React, { useEffect, useRef, useState } from 'react'
3
+ import React, { useEffect, useState } from 'react'
4
4
  import { usePageContext } from './renderer/usePageContext'
5
5
  import { css } from './utils/css'
6
6
  import { blockMargin, containerQueryMobileLayout, containerQueryMobileMenu } from './Layout'
@@ -10,58 +10,48 @@ import { NavigationWithColumnLayout } from './MenuModal/NavigationWithColumnLayo
10
10
  import { addListenerOpenMenuModal, closeMenuModal, keepMenuModalOpen } from './MenuModal/toggleMenuModal'
11
11
 
12
12
  function MenuModal({ isTopNav }: { isTopNav: boolean }) {
13
- const ref = useRef<HTMLDivElement>(null)
14
- const [height, setHeight] = useState<number | undefined>(undefined)
13
+ // `transition: height` doesn't work on `height: auto` => we have to manually set and change `height` to a fixed size.
14
+ const [height, setHeight] = useState(0)
15
15
  useEffect(() => {
16
- const updateHeight = () => {
17
- const { scrollHeight } = ref!.current!.querySelector('.navigation-content')!
18
- if (height !== scrollHeight) setHeight(scrollHeight + blockMargin)
19
- }
20
- addListenerOpenMenuModal(updateHeight)
21
- updateHeight()
16
+ addListenerOpenMenuModal(() => {
17
+ const { scrollHeight } = document.getElementById('menu-modal-scroll-container')!
18
+ const heightNew = scrollHeight + blockMargin
19
+ if (height !== heightNew) setHeight(heightNew)
20
+ })
22
21
  })
23
22
  return (
24
23
  <>
25
24
  <Style>{getStyle()}</Style>
26
25
  <div
27
- id="menu-modal"
28
- className="link-hover-animation add-transition"
26
+ id="menu-modal-wrapper"
27
+ className="link-hover-animation add-transition show-on-nav-hover"
29
28
  style={{
30
29
  position: isTopNav ? 'absolute' : 'fixed',
31
30
  width: '100%',
32
31
  top: 'var(--nav-head-height)',
33
32
  left: 0,
34
- zIndex: 9999,
35
- overflowY: 'scroll',
33
+ zIndex: 199, // maximum value, because docsearch's modal has `z-index: 200`
36
34
  background: '#ededef',
37
- transitionProperty: 'height',
35
+ transitionProperty: 'height, opacity',
38
36
  transitionTimingFunction: 'ease',
39
- // https://github.com/brillout/docpress/issues/23
40
- // https://stackoverflow.com/questions/64514118/css-overscroll-behavior-contain-when-target-element-doesnt-overflow
41
- // https://stackoverflow.com/questions/9538868/prevent-body-from-scrolling-when-a-modal-is-opened
42
- overscrollBehavior: 'none',
43
37
  height,
38
+ overflow: 'hidden',
44
39
  }}
45
- ref={ref}
46
40
  onMouseOver={() => keepMenuModalOpen()}
47
41
  onMouseLeave={closeMenuModal}
48
42
  >
49
43
  <div
44
+ id="menu-modal-scroll-container"
50
45
  style={{
51
- // Place <NavSecondary /> to the bottom
52
- display: 'flex',
53
- flexDirection: 'column',
54
- justifyContent: 'space-between',
55
- minHeight: '100%',
56
- position: 'relative',
57
- // We don't set `container` to the parent #menu-modal beacuse of a Chrome bug (showing a blank <MenuModal>)
46
+ overflowY: 'scroll',
47
+ // We don't set `container` to the parent #menu-modal-wrapper beacuse of a Chrome bug (showing a blank <MenuModal>). Edit: IIRC because #menu-modal-wrapper has `position: fixed`.
58
48
  container: 'container-viewport / inline-size',
59
49
  }}
60
50
  >
61
51
  <Nav />
62
52
  <NavSecondary className="show-only-for-mobile" />
63
- <BorderBottom />
64
53
  </div>
54
+ <BorderBottom />
65
55
  <CloseButton className="show-only-for-mobile" />
66
56
  </div>
67
57
  </>
@@ -71,7 +61,13 @@ function BorderBottom() {
71
61
  return (
72
62
  <div
73
63
  id="border-bottom"
74
- style={{ position: 'absolute', background: '#fff', height: 'var(--block-margin)', width: '100%', bottom: 0 }}
64
+ style={{
65
+ position: 'absolute',
66
+ background: '#fff',
67
+ height: 'var(--block-margin)',
68
+ width: '100%',
69
+ bottom: 0,
70
+ }}
75
71
  />
76
72
  )
77
73
  }
@@ -98,30 +94,42 @@ function NavSecondary({ className }: { className: string }) {
98
94
  function getStyle() {
99
95
  return css`
100
96
  @media(min-width: ${containerQueryMobileMenu + 1}px) {
101
- #menu-modal {
102
- ${/* Firefox doesn't support `dvh` yet: https://caniuse.com/?search=dvh */ ''}
103
- ${/* Let's always use `dvh` instead of `vh` once Firefox supports it */ ''}
104
- max-height: calc(100vh - var(--nav-head-height));
105
- ${/* We use dvh because of mobile */ ''}
106
- ${/* https://stackoverflow.com/questions/37112218/css3-100vh-not-constant-in-mobile-browser/72245072#72245072 */ ''}
107
- max-height: calc(100dvh - var(--nav-head-height));
97
+ #menu-modal-scroll-container,
98
+ #menu-modal-wrapper {
99
+ max-height: calc(100vh - var(--nav-head-height));
108
100
  }
109
- html:not(.menu-modal-show) #menu-modal {
110
- height: 0 !important;
101
+ #menu-modal-scroll-container {
102
+ ${/* https://github.com/brillout/docpress/issues/23 */ ''}
103
+ ${/* https://stackoverflow.com/questions/64514118/css-overscroll-behavior-contain-when-target-element-doesnt-overflow */ ''}
104
+ ${/* https://stackoverflow.com/questions/9538868/prevent-body-from-scrolling-when-a-modal-is-opened */ ''}
105
+ overscroll-behavior: none;
106
+ }
107
+ html:not(.menu-modal-show) #menu-modal-wrapper {
108
+ ${/* 3px */ ''}
109
+ height: var(--block-margin) !important;
110
+ pointer-events: none;
111
111
  }
112
112
  .show-only-for-mobile {
113
113
  display: none !important;
114
114
  }
115
115
  }
116
116
  @media(max-width: ${containerQueryMobileMenu}px) {
117
- #menu-modal {
117
+ #menu-modal-scroll-container {
118
+ ${/* Fallback for Firefox: it doesn't support `dvh` yet: https://caniuse.com/?search=dvh */ ''}
119
+ ${/* Let's always and systematically use `dvh` instead of `vh` once Firefox supports it */ ''}
118
120
  height: calc(100vh) !important;
121
+ ${/* We use dvh because of mobile */ ''}
122
+ ${/* https://stackoverflow.com/questions/37112218/css3-100vh-not-constant-in-mobile-browser/72245072#72245072 */ ''}
119
123
  height: calc(100dvh) !important;
124
+ ${/* Place <NavSecondary /> to the bottom */ ''}
125
+ display: flex;
126
+ flex-direction: column;
127
+ justify-content: space-between;
120
128
  }
121
129
  #border-bottom {
122
130
  display: none;
123
131
  }
124
- html:not(.menu-modal-show) #menu-modal {
132
+ html:not(.menu-modal-show) #menu-modal-wrapper {
125
133
  opacity: 0;
126
134
  pointer-events: none;
127
135
  }
@@ -129,12 +137,16 @@ function getStyle() {
129
137
  html.menu-modal-show {
130
138
  overflow: hidden !important;
131
139
  }
132
- #menu-modal {
140
+ #menu-modal-wrapper {
133
141
  --nav-head-height: 0px !important;
134
142
  }
143
+ #menu-modal-wrapper,
144
+ #menu-navigation-container {
145
+ height: auto!important;
146
+ }
135
147
  }
136
148
  @container container-viewport (min-width: ${containerQueryMobileLayout}px) {
137
- #menu-modal .nav-item-level-3 {
149
+ #menu-modal-wrapper .nav-item-level-3 {
138
150
  display: none;
139
151
  }
140
152
  }
@@ -4,7 +4,7 @@
4
4
  overflow-x: hidden;
5
5
  --padding-left-global: 9px;
6
6
  }
7
- #menu-modal .nav-item {
7
+ #menu-modal-wrapper .nav-item {
8
8
  --padding-left-global: 15px;
9
9
  }
10
10
  .nav-item code {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",
@@ -55,15 +55,7 @@
55
55
  }
56
56
  },
57
57
  "devDependencies": {
58
- "@brillout/release-me": "^0.4.2",
59
- "@types/node": "link:../demo/node_modules/@types/node/",
60
- "@types/react": "link:../demo/node_modules/@types/react/",
61
- "@types/react-dom": "link:../demo/node_modules/@types/react-dom/",
62
- "react": "link:../demo/node_modules/react/",
63
- "react-dom": "link:../demo/node_modules/react-dom/",
64
- "typescript": "link:../demo/node_modules/typescript/",
65
- "vike": "link:../demo/node_modules/vike/",
66
- "vite": "link:../demo/node_modules/vite/"
58
+ "@brillout/release-me": "^0.4.2"
67
59
  },
68
60
  "repository": "https://github.com/brillout/docpress",
69
61
  "license": "MIT",
@@ -0,0 +1,31 @@
1
+ export { getHydrationPromise }
2
+ export { setHydrationIsFinished }
3
+
4
+ import { getGlobalObject } from '../utils/client'
5
+ import { genPromise } from '../utils/genPromise'
6
+
7
+ const globalObject = getGlobalObject<{
8
+ hydrationPromise: Promise<void>
9
+ hydrationPromiseResolve: () => void
10
+ }>(
11
+ 'onRenderClient.ts',
12
+ (() => {
13
+ const { promise: hydrationPromise, resolve: hydrationPromiseResolve } = genPromise()
14
+ return {
15
+ hydrationPromise,
16
+ hydrationPromiseResolve,
17
+ }
18
+ })(),
19
+ )
20
+
21
+ function getHydrationPromise() {
22
+ return globalObject.hydrationPromise
23
+ }
24
+
25
+ function setHydrationIsFinished() {
26
+ globalObject.hydrationPromiseResolve()
27
+ // Used by:
28
+ // - https://github.com/vikejs/vike/blob/9d67f3dd4bdfb38c835186b8147251e0e3b06657/docs/.testRun.ts#L22
29
+ // - https://github.com/brillout/telefunc/blob/57c942c15b7795cfda96b5106acc9e098aa509aa/docs/.testRun.ts#L26
30
+ ;(window as any).__docpress_hydrationFinished = true
31
+ }
@@ -12,10 +12,10 @@ import { installSectionUrlHashs } from '../installSectionUrlHashs'
12
12
  import { getGlobalObject } from '../utils/client'
13
13
  import { initKeyBindings } from '../initKeyBindings'
14
14
  import { initOnNavigation } from './initOnNavigation'
15
+ import { setHydrationIsFinished } from './getHydrationPromise'
15
16
 
16
17
  const globalObject = getGlobalObject<{
17
18
  root?: ReactDOM.Root
18
- renderPromiseResolve?: () => void
19
19
  }>('onRenderClient.ts', {})
20
20
 
21
21
  addEcosystemStamp()
@@ -27,11 +27,12 @@ async function onRenderClient(pageContext: PageContextClient) {
27
27
 
28
28
  // TODO: stop using any
29
29
  const pageContextResolved: PageContextResolved = (pageContext as any).pageContextResolved
30
+ let renderPromiseResolve!: () => void
30
31
  const renderPromise = new Promise<void>((r) => {
31
- globalObject.renderPromiseResolve = r
32
+ renderPromiseResolve = r
32
33
  })
33
34
  let page = getPageElement(pageContext, pageContextResolved)
34
- page = <OnRenderDoneHook>{page}</OnRenderDoneHook>
35
+ page = <OnRenderDoneHook renderPromiseResolve={renderPromiseResolve}>{page}</OnRenderDoneHook>
35
36
  const container = document.getElementById('page-view')!
36
37
  if (pageContext.isHydration) {
37
38
  globalObject.root = ReactDOM.hydrateRoot(container, page)
@@ -58,26 +59,22 @@ function onRenderStart() {
58
59
  closeMenuModal()
59
60
  }
60
61
 
61
- function onRenderDone() {
62
+ function onRenderDone(renderPromiseResolve: () => void) {
62
63
  autoScrollNav()
63
64
  // TODO/refactor: use React?
64
65
  installSectionUrlHashs()
65
66
  setHydrationIsFinished()
66
- globalObject.renderPromiseResolve!()
67
+ renderPromiseResolve()
67
68
  }
68
69
 
69
- function OnRenderDoneHook({ children }: { children: React.ReactNode }) {
70
- useEffect(onRenderDone)
70
+ function OnRenderDoneHook({
71
+ renderPromiseResolve,
72
+ children,
73
+ }: { renderPromiseResolve: () => void; children: React.ReactNode }) {
74
+ useEffect(() => onRenderDone(renderPromiseResolve))
71
75
  return children
72
76
  }
73
77
 
74
- function setHydrationIsFinished() {
75
- // Used by:
76
- // - https://github.com/vikejs/vike/blob/9d67f3dd4bdfb38c835186b8147251e0e3b06657/docs/.testRun.ts#L22
77
- // - https://github.com/brillout/telefunc/blob/57c942c15b7795cfda96b5106acc9e098aa509aa/docs/.testRun.ts#L26
78
- ;(window as any).__docpress_hydrationFinished = true
79
- }
80
-
81
78
  // Used by:
82
79
  // - https://github.com/vikejs/vike/blob/87cca54f30b3c7e71867763d5723493d7eef37ab/vike/client/client-routing-runtime/prefetch.ts#L309-L312
83
80
  function addEcosystemStamp() {
package/utils/css.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export function css(strings: TemplateStringsArray | string[], ...values: (string | number)[]): string {
2
- // The boring part
2
+ // The boring part: just concatenate
3
3
  let result = strings
4
4
  .map((str, i) => {
5
5
  let s = str
@@ -0,0 +1,5 @@
1
+ export function genPromise<T = void>(): { promise: Promise<T>; resolve: (val: T) => void } {
2
+ let resolve!: (val: T) => void
3
+ const promise = new Promise<T>((r) => (resolve = r))
4
+ return { promise, resolve }
5
+ }