This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git


The following commit(s) were added to refs/heads/master by this push:
     new 4692719a861 [course] add a collapsible module rail to Doris 101 
lessons (#4032)
4692719a861 is described below

commit 4692719a86101d27e03bde1e6ccdf29797e820a0
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Aug 3 20:34:41 2026 +0800

    [course] add a collapsible module rail to Doris 101 lessons (#4032)
    
    ## What this PR does
    
    The Doris 101 lesson layout gave the module rail a fixed 255px column,
    so readers who wanted more room for the lesson stage had no way to
    reclaim it.
    
    This adds a toggle to the rail header that collapses it to a 74px strip
    showing only the 01–06 module badges, widening the lesson stage to
    match. Clicking again restores it. The choice is remembered in
    `localStorage` so it survives navigation between modules.
    
    ## Implementation notes
    
    - The collapsed state is driven by `<html data-course-rail>` rather than
    by React state. A `COURSE_RAIL_BOOTSTRAP` script in `headTags` applies
    the stored choice before first paint, mirroring the existing
    `BRAND_THEME_BOOTSTRAP`. Reading it from an effect instead would paint
    the rail expanded and then animate it shut on every lesson navigation,
    since the player remounts per module page.
    - The rail width moved into a `--course-rail-width` custom property so
    the shell grid and the collapsed override share one source of truth.
    - While collapsed, the hover tooltip carries the module title alongside
    its description, and it now also covers the active module, which renders
    as a `div` rather than a `Link`.
    - The toggle carries `aria-expanded` plus a state-dependent
    `aria-label`/`title`, and the two new transitions are disabled under
    `prefers-reduced-motion`.
    - The rail is already hidden below 1100px, so mobile layout is
    unaffected.
    
    ## Testing
    
    - `yarn typecheck` — no new errors in `src/components/course/` (the
    pre-existing failures under `src/utils/` are untouched).
    - `yarn start` — verified collapse/expand, persistence across module
    navigation, and that a collapsed rail no longer flashes open on
    navigation or hard reload.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: morningman <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 docusaurus.config.js                    | 16 ++++++
 src/components/course/CourseLesson.scss | 96 ++++++++++++++++++++++++++++++---
 src/components/course/CourseLesson.tsx  | 68 +++++++++++++++++++++--
 3 files changed, 169 insertions(+), 11 deletions(-)

diff --git a/docusaurus.config.js b/docusaurus.config.js
index 69114bbc891..b20f553884a 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -49,6 +49,17 @@ const BRAND_THEME_BOOTSTRAP = `(function () {
     syncKapaScript(document.documentElement);
 }());`;
 
+// Doris 101 remembers whether its module rail is collapsed. Applying the 
stored
+// choice before first paint keeps the rail from rendering expanded and then
+// snapping shut once React mounts on every lesson navigation.
+const COURSE_RAIL_BOOTSTRAP = `(function () {
+    try {
+        if (localStorage.getItem('doris-101-rail-collapsed') === 'true') {
+            document.documentElement.setAttribute('data-course-rail', 
'collapsed');
+        }
+    } catch (error) {}
+}());`;
+
 // Per-document last-update timestamps, generated from git history by
 // scripts/last-update/generate.js and refreshed on demand via the
 // "Refresh Docs Last-Update Map" workflow. markdown.parseFrontMatter (below)
@@ -239,6 +250,11 @@ const config = {
             attributes: {},
             innerHTML: BRAND_THEME_BOOTSTRAP,
         },
+        {
+            tagName: 'script',
+            attributes: {},
+            innerHTML: COURSE_RAIL_BOOTSTRAP,
+        },
     ],
     scripts: ['/js/custom-script.js',
         {
diff --git a/src/components/course/CourseLesson.scss 
b/src/components/course/CourseLesson.scss
index 3138b24888a..528758e8606 100644
--- a/src/components/course/CourseLesson.scss
+++ b/src/components/course/CourseLesson.scss
@@ -10,6 +10,8 @@ body:has(.course-player) {
 }
 
 .course-player {
+    --course-rail-width: 255px;
+
     min-width: 0;
     min-height: calc(100vh - var(--ifm-navbar-height));
     background: var(--brand-cream-light);
@@ -105,7 +107,8 @@ body:has(.course-player) {
     &__shell {
         display: grid;
         min-width: 0;
-        grid-template-columns: 255px minmax(0, 1fr);
+        grid-template-columns: var(--course-rail-width) minmax(0, 1fr);
+        transition: grid-template-columns 0.24s ease;
     }
 
     &__rail {
@@ -137,13 +140,14 @@ body:has(.course-player) {
             border-radius: 12px;
         }
 
-        li > a {
-            position: relative;
+        li > a:hover {
+            border-color: var(--brand-border-soft);
+            background: var(--brand-paper);
+        }
 
-            &:hover {
-                border-color: var(--brand-border-soft);
-                background: var(--brand-paper);
-            }
+        li > a,
+        :root[data-course-rail='collapsed'] & li > div {
+            position: relative;
 
             &::before,
             &::after {
@@ -191,12 +195,86 @@ body:has(.course-player) {
         }
     }
 
+    &__rail-head {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 0.5rem;
+        margin-bottom: 1rem;
+    }
+
     &__rail-label {
-        margin: 0 0 1rem;
+        min-width: 0;
+        margin: 0;
+        overflow: hidden;
         color: var(--brand-primary-deep);
         font: 800 0.68rem/1 var(--font-mono);
         letter-spacing: 0.12em;
         text-transform: uppercase;
+        white-space: nowrap;
+    }
+
+    &__rail-toggle {
+        display: grid;
+        width: 28px;
+        height: 28px;
+        flex-shrink: 0;
+        place-items: center;
+        padding: 0;
+        border: 1px solid var(--brand-border-soft);
+        border-radius: 8px;
+        background: var(--brand-paper);
+        color: var(--brand-ink-soft);
+        cursor: pointer;
+
+        svg {
+            width: 15px;
+            height: 15px;
+            transition: transform 0.24s ease;
+        }
+
+        &:hover {
+            border-color: var(--brand-primary);
+            color: var(--brand-primary-deep);
+        }
+    }
+
+    :root[data-course-rail='collapsed'] & {
+        --course-rail-width: 74px;
+    }
+
+    :root[data-course-rail='collapsed'] &__rail {
+        padding: 1.75rem 0.7rem;
+    }
+
+    :root[data-course-rail='collapsed'] &__rail-head {
+        justify-content: center;
+    }
+
+    :root[data-course-rail='collapsed'] &__rail-label {
+        display: none;
+    }
+
+    :root[data-course-rail='collapsed'] &__rail-toggle svg {
+        transform: rotate(180deg);
+    }
+
+    :root[data-course-rail='collapsed'] &__rail li > a,
+    :root[data-course-rail='collapsed'] &__rail li > div {
+        grid-template-columns: minmax(0, 1fr);
+        justify-items: center;
+        padding: 0.5rem 0.35rem;
+    }
+
+    :root[data-course-rail='collapsed'] &__rail li > a::after,
+    :root[data-course-rail='collapsed'] &__rail li > div::after {
+        width: 250px;
+        content: attr(data-title) '\a' attr(data-description);
+        white-space: pre-line;
+    }
+
+    :root[data-course-rail='collapsed'] &__module-copy {
+        display: none;
     }
 
     &__module-number {
@@ -928,6 +1006,8 @@ body:has(.course-player) {
 @media (prefers-reduced-motion: reduce) {
     .course-player__rail li > a::before,
     .course-player__rail li > a::after,
+    .course-player__shell,
+    .course-player__rail-toggle svg,
     .course-player__progress b,
     .course-player__dots button {
         transition: none;
diff --git a/src/components/course/CourseLesson.tsx 
b/src/components/course/CourseLesson.tsx
index e6f85f13d18..9d64f1ab45e 100644
--- a/src/components/course/CourseLesson.tsx
+++ b/src/components/course/CourseLesson.tsx
@@ -79,6 +79,19 @@ interface CourseLayerStackProps {
     items: CourseConceptItem[];
 }
 
+const RAIL_COLLAPSED_STORAGE_KEY = 'doris-101-rail-collapsed';
+
+// The rail is styled off `<html data-course-rail>` rather than off React 
state so the
+// COURSE_RAIL_BOOTSTRAP script in docusaurus.config.js can apply the stored 
choice
+// before first paint. React state only mirrors it for the toggle's accessible 
state.
+function applyRailCollapsed(collapsed: boolean): void {
+    if (collapsed) {
+        document.documentElement.setAttribute('data-course-rail', 'collapsed');
+    } else {
+        document.documentElement.removeAttribute('data-course-rail');
+    }
+}
+
 export function CourseLessonUnit({ children }: CourseLessonUnitProps): 
JSX.Element {
     return <>{children}</>;
 }
@@ -191,6 +204,7 @@ export function CourseLessonPlayer({
     const units = Children.toArray(children).filter(isValidElement) as 
ReactElement<CourseLessonUnitProps>[];
     const unitIds = useMemo(() => units.map(unit => unit.props.id), 
[children]);
     const [activeUnit, setActiveUnit] = useState(0);
+    const [railCollapsed, setRailCollapsed] = useState(false);
     const [answers, setAnswers] = useState<Record<string, number>>({});
     const didMount = useRef(false);
     const activeHeadingRef = useRef<HTMLHeadingElement>(null);
@@ -218,6 +232,23 @@ export function CourseLessonPlayer({
         }
     }, [activeUnit]);
 
+    useEffect(() => {
+        setRailCollapsed(document.documentElement.dataset.courseRail === 
'collapsed');
+    }, []);
+
+    const toggleRail = () => {
+        setRailCollapsed(current => {
+            const next = !current;
+            applyRailCollapsed(next);
+            try {
+                window.localStorage.setItem(RAIL_COLLAPSED_STORAGE_KEY, 
String(next));
+            } catch {
+                // Storage can be unavailable in hardened/private browsing.
+            }
+            return next;
+        });
+    };
+
     const selectUnit = (index: number) => {
         if (index < 0 || index >= units.length || index === activeUnit) return;
         setActiveUnit(index);
@@ -244,7 +275,28 @@ export function CourseLessonPlayer({
 
             <div className="course-player__shell">
                 <aside className="course-player__rail" aria-label="Doris 101 
modules">
-                    <p className="course-player__rail-label">Course modules</p>
+                    <div className="course-player__rail-head">
+                        <p className="course-player__rail-label">Course 
modules</p>
+                        <button
+                            className="course-player__rail-toggle"
+                            type="button"
+                            aria-expanded={!railCollapsed}
+                            aria-label={railCollapsed ? 'Expand course 
modules' : 'Collapse course modules'}
+                            title={railCollapsed ? 'Expand course modules' : 
'Collapse course modules'}
+                            onClick={toggleRail}
+                        >
+                            <svg viewBox="0 0 16 16" aria-hidden="true" 
focusable="false">
+                                <path
+                                    d="M10.5 3 5.5 8l5 5"
+                                    fill="none"
+                                    stroke="currentColor"
+                                    strokeWidth="2"
+                                    strokeLinecap="round"
+                                    strokeLinejoin="round"
+                                />
+                            </svg>
+                        </button>
+                    </div>
                     <ol>
                         {COURSE_101_MODULES.map((module, index) => {
                             const isCurrent = index === moduleNumber - 1;
@@ -264,9 +316,19 @@ export function CourseLessonPlayer({
                             return (
                                 <li className={isCurrent ? 
'course-player__module--active' : undefined} key={module.number}>
                                     {isCurrent ? (
-                                        <div 
aria-current="step">{content}</div>
+                                        <div
+                                            aria-current="step"
+                                            data-title={module.shortTitle}
+                                            
data-description={module.description}
+                                        >
+                                            {content}
+                                        </div>
                                     ) : (
-                                        <Link to={module.href} 
data-description={module.description}>
+                                        <Link
+                                            to={module.href}
+                                            data-title={module.shortTitle}
+                                            
data-description={module.description}
+                                        >
                                             {content}
                                         </Link>
                                     )}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to