diff --git a/client/src/app.scss b/client/src/app.scss index 619b971270cf..72f1a36aff69 100644 --- a/client/src/app.scss +++ b/client/src/app.scss @@ -102,8 +102,10 @@ --top-nav-height: 4rem; --article-actions-container-height: 2rem; --icon-size: 1rem; - --sticky-header-height: calc( - var(--top-nav-height) + var(--article-actions-container-height) + 2px + --sticky-header-without-actions-height: calc(var(--top-nav-height) + 1px); + --sticky-header-with-actions-height: calc( + var(--sticky-header-without-actions-height) + + var(--article-actions-container-height) + 1px ); } @@ -121,7 +123,11 @@ } :target { - scroll-margin-top: var(--sticky-header-height); + scroll-margin-top: var(--sticky-header-with-actions-height); +} + +.sticky-header-container.without-actions ~ * :target { + scroll-margin-top: var(--sticky-header-without-actions-height); } body { @@ -340,3 +346,9 @@ sup.new { background: var(--new-background-beta, var(--new-background)); } } + +.sticky-header-container { + position: sticky; + top: 0; + z-index: var(--z-index-main-header); +} diff --git a/client/src/app.tsx b/client/src/app.tsx index 656656cca46f..243208bed85e 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -55,7 +55,9 @@ function Layout({ pageType, children }) { > {pageType !== "document-page" && ( - +
+ +
)} {children} diff --git a/client/src/blog/index.scss b/client/src/blog/index.scss index 296f95164770..f84cf264d174 100644 --- a/client/src/blog/index.scss +++ b/client/src/blog/index.scss @@ -9,6 +9,8 @@ } .blog-container { + --author-gap: 1rem; + --avatar-size: 3rem; margin: 0 auto; max-width: min(calc(80vw + 4rem), var(--max-width)); padding: 2rem 1rem; @@ -30,6 +32,56 @@ } } + .date-author, + .author { + align-content: flex-start; + align-items: center; + display: flex; + flex-wrap: wrap; + } + + .date-author { + column-gap: 1.5rem; + padding-left: calc(var(--avatar-size) + var(--author-gap)); + } + + .author { + font-weight: var(--font-body-strong-weight); + gap: var(--author-gap); + margin-left: calc((var(--avatar-size) + var(--author-gap)) * -1); + + &::after { + margin-left: calc(4px - var(--author-gap)); + } + + img { + border: none !important; + border-radius: 3rem; + height: var(--avatar-size); + margin: 0; + object-fit: cover; + width: var(--avatar-size); + } + } + + figure.blog-image { + margin: 0 auto 2rem; + width: fit-content; + + img { + background: transparent; + border: none !important; + margin: 0 0 0.125rem; + width: 100%; + } + + figcaption { + font-size: smaller; + margin-left: auto; + width: fit-content; + } + } + h1 { margin-top: 1rem; } diff --git a/client/src/blog/index.tsx b/client/src/blog/index.tsx index 85d2ee68407a..6186bf32849b 100644 --- a/client/src/blog/index.tsx +++ b/client/src/blog/index.tsx @@ -9,7 +9,6 @@ import { BlogPost, AuthorDateReadTime } from "./post"; import { BlogImage, BlogPostMetadata } from "../../../libs/types/blog.js"; import "./index.scss"; -import "./post.scss"; import { Button } from "../ui/atoms/button"; import { SignUpSection as NewsletterSignUp } from "../newsletter"; diff --git a/client/src/blog/post.scss b/client/src/blog/post.scss index 23ad757e0e5f..91a12dec67ce 100644 --- a/client/src/blog/post.scss +++ b/client/src/blog/post.scss @@ -1,135 +1,127 @@ @use "../ui/vars" as *; -.blog-container.post { - max-width: calc(48rem + 4rem); - - + .section-newsletter h2 { - font: var(--type-heading-h3); - margin: 0; - } -} - -.blog-container { - --author-gap: 1rem; - --avatar-size: 3rem; - - .date-author, - .author { - align-content: flex-start; - align-items: center; - display: flex; - flex-wrap: wrap; +.blog-post-container { + display: grid; + gap: 3rem; + grid-template-areas: + "post" + "newsletter"; + width: 100%; + + @media screen and (min-width: $screen-lg) { + grid-template-areas: + "post toc" + "newsletter toc"; + grid-template-columns: minmax(auto, 100%) minmax(0, 12rem); } - .date-author { - column-gap: 1.5rem; - padding-left: calc(var(--avatar-size) + var(--author-gap)); + @media screen and (min-width: $screen-xl) { + grid-template-areas: + "nothing post toc" + "nothing newsletter toc"; + grid-template-columns: minmax(auto, 1fr) minmax(0, 52rem) minmax(15rem, 1fr); } - .author { - font-weight: var(--font-body-strong-weight); - gap: var(--author-gap); - margin-left: calc((var(--avatar-size) + var(--author-gap)) * -1); + > .sidebar-container { + --offset: var(--top-nav-height); + display: none; - &::after { - margin-left: calc(4px - var(--author-gap)); + .toc-container { + position: unset; + top: auto; } - img { - border: none !important; - border-radius: 3rem; - height: var(--avatar-size); - margin: 0; - object-fit: cover; - width: var(--avatar-size); - } - } - - figure.blog-image { - margin: 0 auto 2rem; - width: fit-content; + @media screen and (min-width: $screen-lg) { + display: flex; + grid-area: toc; - img { - background: transparent; - border: none !important; - margin: 0 0 0.125rem; - width: 100%; + .toc > nav { + margin-top: 2rem; + } } + } - figcaption { - font-size: smaller; - margin-left: auto; - width: fit-content; - } + > .section-newsletter { + grid-area: newsletter; } - .previous-next { - display: flex; - gap: 1rem; + > .blog-post { + grid-area: post; + max-width: 52rem; - @media screen and (max-width: $screen-md) { - flex-direction: column-reverse; + + .section-newsletter h2 { + font: var(--type-heading-h3); + margin: 0; } - a { - color: var(--text-primary); + .previous-next { display: flex; gap: 1rem; - text-decoration: none; - width: 100%; - &:hover h2 { - text-decoration: underline; + @media screen and (max-width: $screen-md) { + flex-direction: column-reverse; } - &:active { - background: none; - } + a { + color: var(--text-primary); + display: flex; + gap: 1rem; + text-decoration: none; + width: 100%; - @media screen and (min-width: $screen-md) { - &.previous, - &.next { - &::before, - &::after { - align-self: center; - background-color: var(--text-primary); - flex-shrink: 0; - height: 1rem; - mask-position: center; - mask-repeat: no-repeat; - vertical-align: middle; - width: 1rem; - } + &:hover h2 { + text-decoration: underline; } - &.previous::before { - content: ""; - mask-image: url("../assets/icons/previous.svg"); + &:active { + background: none; } - &.next::after { - content: ""; - mask-image: url("../assets/icons/next.svg"); + @media screen and (min-width: $screen-md) { + &.previous, + &.next { + &::before, + &::after { + align-self: center; + background-color: var(--text-primary); + flex-shrink: 0; + height: 1rem; + mask-position: center; + mask-repeat: no-repeat; + vertical-align: middle; + width: 1rem; + } + } + + &.previous::before { + content: ""; + mask-image: url("../assets/icons/previous.svg"); + } + + &.next::after { + content: ""; + mask-image: url("../assets/icons/next.svg"); + } } } - } - - article { - margin: 0 auto; - } - h2:first-of-type { - color: var(--text-link); - font-size: 1rem; - margin: 0; - text-align: center; + article { + margin: 0 auto; + } - strong { - color: var(--text-primary); - display: block; - font-size: 0.8em; - font-weight: normal; - line-height: 1.2rem; + h2:first-of-type { + color: var(--text-link); + font-size: 1rem; + margin: 0; + text-align: center; + + strong { + color: var(--text-primary); + display: block; + font-size: 0.8em; + font-weight: normal; + line-height: 1.2rem; + } } } } diff --git a/client/src/blog/post.tsx b/client/src/blog/post.tsx index 7dac2dac23d1..2062a19417ea 100644 --- a/client/src/blog/post.tsx +++ b/client/src/blog/post.tsx @@ -2,7 +2,7 @@ import useSWR from "swr"; import { HydrationData } from "../../../libs/types/hydration"; import { HTTPError, RenderDocumentBody } from "../document"; -import { WRITER_MODE } from "../env"; +import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; import "./index.scss"; import "./post.scss"; @@ -20,6 +20,8 @@ import { } from "../document/hooks"; import { DEFAULT_LOCALE } from "../../../libs/constants"; import { SignUpSection as NewsletterSignUp } from "../newsletter"; +import { TOC } from "../document/organisms/toc"; +import { SidePlacement } from "../ui/organisms/placement"; function MaybeLink({ className = "", link, children }) { return link ? ( @@ -191,9 +193,19 @@ export function BlogPost(props: HydrationData) { return ( <> {doc && blogMeta && ( - <> +
+
+
+ + {PLACEMENT_ENABLED && !blogMeta?.sponsored && } +
+
@@ -206,7 +218,7 @@ export function BlogPost(props: HydrationData) { {blogMeta.links && }
- +
)} ); diff --git a/client/src/document/hooks.ts b/client/src/document/hooks.ts index f2175a9cfbcf..e88bb5868572 100644 --- a/client/src/document/hooks.ts +++ b/client/src/document/hooks.ts @@ -103,64 +103,31 @@ export function useCopyExamplesToClipboardAndAIExplain(doc: Doc | undefined) { * Provides the height of the sticky header. */ export function useStickyHeaderHeight() { - function determineStickyHeaderHeight(): number { - if (typeof getComputedStyle !== "function") { - // SSR. - return 0; - } - const sidebar = document.querySelector(".sidebar-container"); - - if (sidebar) { - return parseFloat(getComputedStyle(sidebar).top); - } - - const styles = getComputedStyle(document.documentElement); - const stickyHeaderHeight = styles - .getPropertyValue("--sticky-header-height") - .trim(); - - if (stickyHeaderHeight.endsWith("rem")) { - const fontSize = styles.fontSize.trim(); - if (fontSize.endsWith("px")) { - return parseFloat(stickyHeaderHeight) * parseFloat(fontSize); - } else { - console.warn( - `[useStickyHeaderHeight] fontSize has unexpected unit: ${fontSize}` - ); - return 0; - } - } else if (stickyHeaderHeight.endsWith("px")) { - return parseFloat(stickyHeaderHeight); - } else { - console.warn( - `[useStickyHeaderHeight] --sticky-header-height has unexpected unit: ${stickyHeaderHeight}` - ); - return 0; - } - } - - const [height, setHeight] = useState(determineStickyHeaderHeight()); + const [height, setHeight] = useState(0); const timeout = useRef | null>(null); useEffect(() => { - // Unfortunately we cannot observe the CSS variable using MutationObserver, - // but we know that it may change when the width of the window changes. - - const debouncedListener = () => { - if (timeout.current) { - window.clearTimeout(timeout.current); + const header = document.getElementsByClassName( + "sticky-header-container" + )?.[0]; + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { height } = entry.contentRect; + if (timeout.current) { + window.clearTimeout(timeout.current); + } + timeout.current = setTimeout(() => { + setHeight(height); + timeout.current = null; + }, 250); } - timeout.current = setTimeout(() => { - setHeight(determineStickyHeaderHeight()); - timeout.current = null; - }, 250); - }; + }); - window.addEventListener("resize", debouncedListener); + resizeObserver.observe(header); - return () => window.removeEventListener("resize", debouncedListener); - }, []); + return () => resizeObserver.disconnect(); + }, [setHeight]); return height; } diff --git a/client/src/document/index.scss b/client/src/document/index.scss index ba15f224bd63..16bba5732b4d 100644 --- a/client/src/document/index.scss +++ b/client/src/document/index.scss @@ -4,12 +4,6 @@ @use "../ui/molecules/grids/grids.scss" as *; @use "../ui/base/typography" as *; -.main-document-header-container { - position: sticky; - top: 0; - z-index: var(--z-index-main-header); -} - .main-page-content { overflow-wrap: break-word; padding: 3rem 1rem 1rem; @@ -769,7 +763,7 @@ kbd { } .sidebar-container { - --offset: var(--sticky-header-height); + --offset: var(--sticky-header-with-actions-height); --max-height: calc(100vh - var(--offset)); @media screen and (min-width: $screen-md) and (min-height: $screen-height-place-limit) { @@ -807,7 +801,7 @@ kbd { display: flex; flex-direction: column; gap: 0; - height: calc(100vh - var(--sticky-header-height)); + height: calc(100vh - var(--sticky-header-with-actions-height)); mask-image: linear-gradient( to bottom, rgba(0, 0, 0, 0) 0%, @@ -816,7 +810,7 @@ kbd { ); overflow: auto; position: sticky; - top: var(--sticky-header-height); + top: var(--sticky-header-with-actions-height); .place { margin: 1rem 0; diff --git a/client/src/document/index.tsx b/client/src/document/index.tsx index 8c22edf06fd9..6fd1d0ed35b7 100644 --- a/client/src/document/index.tsx +++ b/client/src/document/index.tsx @@ -189,7 +189,7 @@ export function Document(props /* TODO: define a TS interface for this */) { if (error) { return ( <> -
+
@@ -211,7 +211,7 @@ export function Document(props /* TODO: define a TS interface for this */) { return ( <> -
+
diff --git a/client/src/document/organisms/toc/index.tsx b/client/src/document/organisms/toc/index.tsx index c4e2a8518130..5065bbc589e1 100644 --- a/client/src/document/organisms/toc/index.tsx +++ b/client/src/document/organisms/toc/index.tsx @@ -10,7 +10,7 @@ export function TOC({ toc }: { toc: Toc[] }) { const observedElements = React.useCallback(() => { const mainElement = document.querySelector("main") ?? document; const elements = mainElement.querySelectorAll( - "h1, h1 ~ *:not(section), h2, h2 ~ *:not(section), h3, h3 ~ *:not(section)" + "h1, h1 ~ *:not(section), h2:not(.document-toc-heading), h2:not(.document-toc-heading) ~ *:not(section), h3, h3 ~ *:not(section)" ); return Array.from(elements); }, []); diff --git a/client/src/ui/molecules/grids/_document-page.scss b/client/src/ui/molecules/grids/_document-page.scss index 24ab1d718587..97623596ae4c 100644 --- a/client/src/ui/molecules/grids/_document-page.scss +++ b/client/src/ui/molecules/grids/_document-page.scss @@ -62,7 +62,7 @@ padding-right: 1rem; .toc { - --offset: var(--sticky-header-height); + --offset: var(--sticky-header-with-actions-height); display: block; grid-area: toc; diff --git a/client/src/ui/organisms/article-actions/index.scss b/client/src/ui/organisms/article-actions/index.scss index 23e39fb2c691..df709e2d6123 100644 --- a/client/src/ui/organisms/article-actions/index.scss +++ b/client/src/ui/organisms/article-actions/index.scss @@ -158,7 +158,7 @@ bottom: auto; box-shadow: var(--shadow-02); left: var(--article-actions-position-left, initial); - max-height: calc(100vh - 12px - var(--sticky-header-height)); + max-height: calc(100vh - 12px - var(--sticky-header-with-actions-height)); padding: 0; position: absolute; right: 0; diff --git a/client/src/ui/organisms/top-navigation/index.tsx b/client/src/ui/organisms/top-navigation/index.tsx index cde7f2e8c3e3..6c3592f22d60 100644 --- a/client/src/ui/organisms/top-navigation/index.tsx +++ b/client/src/ui/organisms/top-navigation/index.tsx @@ -11,7 +11,7 @@ import { useLocation } from "react-router-dom"; const DARK_NAV_ROUTES = [/\/plus\/?$/i, "_homepage", /^\/?$/]; const TRANSPARENT_NAV_ROUTES = []; //["_homepage", /\/?$/]; -export function TopNavigation({ extraClasses }: { extraClasses?: string }) { +export function TopNavigation() { const location = useLocation(); const [showMainMenu, setShowMainMenu] = useState(false); @@ -30,9 +30,7 @@ export function TopNavigation({ extraClasses }: { extraClasses?: string }) { return (