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]