Skip to content

Commit 4d8965a

Browse files
rsbhclaude
andauthored
feat: mobile responsive layout for default theme (#99)
* feat: add mobile responsive layout for default theme Sidebar becomes slide-out drawer on <768px with backdrop overlay. Desktop sub-nav replaced with mobile header (hamburger + logo + search + theme switcher). Sidebar closes on navigation. Content padding and spacing reduced for mobile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: match mobile layout to Figma design - Hamburger moved to right side with search + theme switcher - Menu is full-screen overlay below navbar (not side drawer) - Bottom prev/next navigation cards on mobile - Toggle between hamburger and X icon - Card 100% width on mobile - Content padding 40px top, 16px horizontal per Figma specs - Mobile nav gap 40px between prev/next cards Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review comments for mobile layout - Hide hamburger button when hideSidebar is true - Add type=button, aria-expanded, aria-controls for accessibility - Move mobileMenu data-open rule inside media query only - Add version switcher to mobile menu Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove min-width on API right block for mobile Reset min-width and max-width on stacked layout so code blocks don't overflow on mobile viewports. Add overflow-x: auto on code snippet body. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reduce API layout gap on mobile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reduce API layout gap only on mobile (768px) Keep original gap at 1100px tablet breakpoint, reduce to space-5 only at 768px mobile breakpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6bd54a7 commit 4d8965a

5 files changed

Lines changed: 234 additions & 1 deletion

File tree

packages/chronicle/src/components/api/api-code-snippet.module.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
border-radius: var(--rs-radius-2);
44
overflow: hidden;
55
width: 100%;
6+
max-width: 100%;
67
}
78

89
.header {
@@ -20,4 +21,5 @@
2021

2122
.body {
2223
background: var(--rs-color-background-base-primary);
24+
overflow-x: auto;
2325
}

packages/chronicle/src/components/api/api-overview.module.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@
6060

6161
.left,
6262
.right {
63+
min-width: 0;
64+
max-width: 100%;
6365
width: 100%;
6466
}
6567
}
68+
69+
@media (max-width: 768px) {
70+
.layout {
71+
gap: var(--rs-space-5);
72+
padding-left: var(--rs-space-5);
73+
padding-right: var(--rs-space-5);
74+
}
75+
}

packages/chronicle/src/themes/default/Layout.module.css

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,121 @@
283283
line-height: var(--rs-line-height-mini);
284284
flex-shrink: 0;
285285
}
286+
287+
.mobileMenuBtn {
288+
display: none;
289+
align-items: center;
290+
justify-content: center;
291+
background: none;
292+
border: none;
293+
cursor: pointer;
294+
padding: var(--rs-space-1);
295+
color: var(--rs-color-foreground-base-primary);
296+
}
297+
298+
.mobileHeader {
299+
display: none;
300+
align-items: center;
301+
justify-content: space-between;
302+
height: var(--navbar-height);
303+
padding: 0 var(--rs-space-5);
304+
background: var(--rs-color-background-base-primary);
305+
border-bottom: 0.5px solid var(--rs-color-border-base-primary);
306+
backdrop-filter: blur(1px);
307+
}
308+
309+
.mobileMenu {
310+
display: none;
311+
position: fixed;
312+
top: var(--navbar-height);
313+
left: 0;
314+
right: 0;
315+
bottom: 0;
316+
z-index: 100;
317+
background: var(--rs-color-background-base-primary);
318+
overflow-y: auto;
319+
padding: var(--rs-space-7) var(--rs-space-5);
320+
}
321+
322+
.mobileMenuFooter {
323+
margin-top: var(--rs-space-7);
324+
padding-top: var(--rs-space-5);
325+
border-top: 0.5px solid var(--rs-color-border-base-primary);
326+
}
327+
328+
.mobileNav {
329+
display: none;
330+
}
331+
332+
@media (max-width: 768px) {
333+
.sidebar {
334+
display: none;
335+
}
336+
337+
.mobileHeader {
338+
display: flex;
339+
}
340+
341+
.mobileMenuBtn {
342+
display: flex;
343+
}
344+
345+
.mobileMenu[data-open='true'] {
346+
display: block;
347+
}
348+
349+
.subNav {
350+
display: none;
351+
}
352+
353+
.content {
354+
padding: var(--rs-space-10) var(--rs-space-5);
355+
}
356+
357+
.card {
358+
width: 100%;
359+
border-left: none;
360+
box-shadow: none;
361+
}
362+
363+
.cardWrapper {
364+
padding: 0;
365+
}
366+
367+
.mobileNav {
368+
display: flex;
369+
gap: var(--rs-space-10);
370+
padding: var(--rs-space-3) var(--rs-space-5);
371+
background: var(--rs-color-background-base-primary);
372+
}
373+
374+
.mobileNavLink {
375+
flex: 1;
376+
display: flex;
377+
align-items: center;
378+
gap: var(--rs-space-3);
379+
padding: var(--rs-space-4) var(--rs-space-3);
380+
border: 0.5px solid var(--rs-color-border-base-primary);
381+
border-radius: var(--rs-radius-4);
382+
text-decoration: none;
383+
font-family: var(--rs-font-body);
384+
font-size: var(--rs-font-size-regular);
385+
font-weight: var(--rs-font-weight-medium);
386+
line-height: var(--rs-line-height-regular);
387+
letter-spacing: var(--rs-letter-spacing-regular);
388+
color: var(--rs-color-foreground-base-tertiary);
389+
min-width: 0;
390+
}
391+
392+
.mobileNavLink[data-direction='next'] {
393+
justify-content: flex-end;
394+
background: var(--rs-color-background-base-secondary);
395+
color: var(--rs-color-foreground-base-primary);
396+
}
397+
398+
.mobileNavLabel {
399+
overflow: hidden;
400+
text-overflow: ellipsis;
401+
white-space: nowrap;
402+
}
403+
}

packages/chronicle/src/themes/default/Layout.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
CodeBracketSquareIcon,
55
RectangleStackIcon,
66
DocumentTextIcon,
7-
Squares2X2Icon
7+
Squares2X2Icon,
8+
Bars3Icon,
9+
XMarkIcon
810
} from '@heroicons/react/24/outline';
911
import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara';
1012
import { PlayIcon } from '@radix-ui/react-icons';
@@ -73,6 +75,7 @@ export function Layout({
7375
const navigate = useNavigate();
7476
const { page, version } = usePageContext();
7577
const scrollRef = useRef<HTMLDivElement>(null);
78+
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
7679
const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/');
7780
const isApiBase = (basePath: string) =>
7881
pathname === basePath || pathname.startsWith(`${basePath}/`);
@@ -109,10 +112,82 @@ export function Layout({
109112
requestAnimationFrame(() => {
110113
el.scrollTop = savedScrollTop;
111114
});
115+
setMobileSidebarOpen(false);
112116
}, [pathname]);
113117

114118
return (
115119
<Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
120+
<div className={styles.mobileHeader}>
121+
<SidebarLogo config={config} />
122+
<Flex align='center' gap={3}>
123+
{config.search?.enabled && <Search />}
124+
<ClientThemeSwitcher size={16} />
125+
{!hideSidebar && (
126+
<button
127+
type='button'
128+
className={styles.mobileMenuBtn}
129+
onClick={() => setMobileSidebarOpen(o => !o)}
130+
aria-label={mobileSidebarOpen ? 'Close menu' : 'Open menu'}
131+
aria-expanded={mobileSidebarOpen}
132+
aria-controls='mobile-menu'
133+
>
134+
{mobileSidebarOpen
135+
? <XMarkIcon width={16} height={16} />
136+
: <Bars3Icon width={16} height={16} />}
137+
</button>
138+
)}
139+
</Flex>
140+
</div>
141+
<div id='mobile-menu' className={styles.mobileMenu} data-open={!hideSidebar && mobileSidebarOpen}>
142+
{showTopLinks ? (
143+
<div className={styles.topLinks}>
144+
{contentEntries.map(entry => (
145+
<Sidebar.Item
146+
key={entry.href}
147+
href={entry.href}
148+
active={activeContentDir === entry.contentDir}
149+
leadingIcon={renderConfigIcon(entry.icon, entry.label, <DocumentTextIcon width={16} height={16} />)}
150+
classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
151+
render={<RouterLink to={entry.href} />}
152+
>
153+
{entry.label}
154+
</Sidebar.Item>
155+
))}
156+
{apiEntries.map(api => (
157+
<Sidebar.Item
158+
key={`${api.basePath}-${api.name}`}
159+
href={api.basePath}
160+
active={isApiBase(api.basePath)}
161+
leadingIcon={renderConfigIcon(api.icon, api.name, <CodeBracketSquareIcon width={16} height={16} />)}
162+
classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
163+
render={<RouterLink to={api.basePath} />}
164+
>
165+
{api.name} API
166+
</Sidebar.Item>
167+
))}
168+
</div>
169+
) : null}
170+
{tree.children.map((item, i) => (
171+
isApiRoute ? (
172+
<ApiSidebarNode
173+
key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
174+
item={item}
175+
pathname={pathname}
176+
/>
177+
) : (
178+
<SidebarNode
179+
key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
180+
item={item}
181+
pathname={pathname}
182+
/>
183+
)
184+
))}
185+
{config.versions?.length ? (
186+
<div className={styles.mobileMenuFooter}>
187+
<VersionSwitcher />
188+
</div>
189+
) : null}
190+
</div>
116191
<Flex className={cx(styles.body, classNames?.body)}>
117192
{hideSidebar ? null : (
118193
<Sidebar
@@ -221,6 +296,20 @@ export function Layout({
221296
<main className={cx(styles.content, classNames?.content)}>
222297
{children}
223298
</main>
299+
<div className={styles.mobileNav}>
300+
{prev ? (
301+
<RouterLink to={prev.url} className={styles.mobileNavLink}>
302+
<ArrowLeftIcon width={16} height={16} />
303+
<span className={styles.mobileNavLabel}>{prev.title}</span>
304+
</RouterLink>
305+
) : <div />}
306+
{next ? (
307+
<RouterLink to={next.url} className={styles.mobileNavLink} data-direction='next'>
308+
<span className={styles.mobileNavLabel}>{next.title}</span>
309+
<ArrowRightIcon width={16} height={16} />
310+
</RouterLink>
311+
) : <div />}
312+
</div>
224313
</div>
225314
</div>
226315
</Flex>

packages/chronicle/src/themes/default/Page.module.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,17 @@
184184
.headerLoader {
185185
margin-bottom: var(--rs-space-5);
186186
}
187+
188+
@media (max-width: 768px) {
189+
.page {
190+
gap: var(--rs-space-5);
191+
}
192+
193+
.article {
194+
max-width: 100%;
195+
}
196+
197+
.title {
198+
margin-bottom: var(--rs-space-5);
199+
}
200+
}

0 commit comments

Comments
 (0)