Skip to content

Commit f1d7b67

Browse files
committed
add fancy theme switcher + sidebar subcategories
1 parent 659dd15 commit f1d7b67

5 files changed

Lines changed: 260 additions & 71 deletions

File tree

components/DarkModeToggle.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useState } from 'react';
2+
import Icon from './Icon';
23

34
const STORAGE_KEY = 'miniwiki-theme';
45

@@ -35,10 +36,26 @@ export default function DarkModeToggle() {
3536
<button
3637
type="button"
3738
onClick={toggleTheme}
38-
className="rounded-md border border-slate-300 px-2 py-1 text-xs text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
39+
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition-colors duration-300 hover:text-blue-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 dark:text-slate-300 dark:hover:text-blue-300"
3940
aria-label="Toggle color mode"
41+
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
4042
>
41-
{theme === 'dark' ? 'Light' : 'Dark'}
43+
<span className="relative h-5 w-5">
44+
<Icon
45+
name="sun"
46+
size={18}
47+
className={`absolute inset-0 transition-all duration-300 ease-out ${
48+
theme === 'dark' ? 'scale-0 rotate-90 opacity-0' : 'scale-100 rotate-0 opacity-100'
49+
}`}
50+
/>
51+
<Icon
52+
name="moon"
53+
size={18}
54+
className={`absolute inset-0 transition-all duration-300 ease-out ${
55+
theme === 'dark' ? 'scale-100 rotate-0 opacity-100' : 'scale-0 -rotate-90 opacity-0'
56+
}`}
57+
/>
58+
</span>
4259
</button>
4360
);
4461
}

components/Sidebar.js

Lines changed: 130 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,134 @@
11
import Link from 'next/link';
22
import { useEffect, useMemo, useState } from 'react';
3+
import Icon from './Icon';
4+
5+
function normalizeRoutePath(routePath = '/') {
6+
if (!routePath) {
7+
return '/';
8+
}
9+
10+
const normalized = `/${String(routePath).replace(/^\/+|\/+$/g, '')}`;
11+
return normalized === '/' ? '/' : normalized.replace(/\/+$/, '');
12+
}
13+
14+
function getNestedItems(item) {
15+
if (Array.isArray(item?.items)) {
16+
return item.items;
17+
}
18+
19+
if (Array.isArray(item?.children)) {
20+
return item.children;
21+
}
22+
23+
return [];
24+
}
25+
26+
function itemContainsCurrentPath(item, currentPath) {
27+
if (item?.path && normalizeRoutePath(item.path) === normalizeRoutePath(currentPath)) {
28+
return true;
29+
}
30+
31+
const nested = getNestedItems(item);
32+
return nested.some((child) => itemContainsCurrentPath(child, currentPath));
33+
}
34+
35+
function SidebarItems({ items, currentPath, depth = 0 }) {
36+
return (
37+
<ul className={`${depth === 0 ? 'space-y-1' : 'mt-1 space-y-1 border-l border-slate-200 pl-3 dark:border-slate-700/70'}`}>
38+
{items.map((item) => {
39+
const nestedItems = getNestedItems(item);
40+
41+
if (nestedItems.length > 0) {
42+
return (
43+
<SidebarGroup
44+
key={`${item.title}-${depth}`}
45+
item={item}
46+
currentPath={currentPath}
47+
depth={depth}
48+
/>
49+
);
50+
}
51+
52+
const active = normalizeRoutePath(item.path) === normalizeRoutePath(currentPath);
53+
54+
return (
55+
<li key={`${item.title}-${item.path}`}>
56+
<Link
57+
href={item.path}
58+
className={`block rounded-md px-2 py-1.5 ${
59+
depth === 0 ? 'text-sm' : 'text-[13px]'
60+
} ${
61+
active
62+
? 'bg-blue-100 text-blue-800 dark:bg-blue-950/50 dark:text-blue-200'
63+
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
64+
}`}
65+
>
66+
{item.title}
67+
</Link>
68+
</li>
69+
);
70+
})}
71+
</ul>
72+
);
73+
}
74+
75+
function SidebarGroup({ item, currentPath, depth }) {
76+
const nestedItems = getNestedItems(item);
77+
const initialOpen = useMemo(() => {
78+
if (!nestedItems.length) {
79+
return true;
80+
}
81+
82+
if (item.defaultOpen === true) {
83+
return true;
84+
}
85+
86+
return nestedItems.some((child) => itemContainsCurrentPath(child, currentPath));
87+
}, [nestedItems, item.defaultOpen, currentPath]);
88+
89+
const [isOpen, setIsOpen] = useState(initialOpen);
90+
91+
useEffect(() => {
92+
if (initialOpen) {
93+
setIsOpen(true);
94+
}
95+
}, [initialOpen]);
96+
97+
const collapsible = item.collapsible !== false;
98+
99+
return (
100+
<li>
101+
<button
102+
type="button"
103+
onClick={() => collapsible && setIsOpen((value) => !value)}
104+
className={`group flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left ${
105+
depth === 0
106+
? 'text-sm font-medium text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800'
107+
: 'text-[13px] font-medium text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
108+
}`}
109+
>
110+
<span>{item.title}</span>
111+
{collapsible ? (
112+
<Icon
113+
name="chevrondown"
114+
size={14}
115+
className={`transition-transform duration-200 ${isOpen ? 'rotate-0' : '-rotate-90'}`}
116+
/>
117+
) : null}
118+
</button>
119+
120+
{isOpen ? <SidebarItems items={nestedItems} currentPath={currentPath} depth={depth + 1} /> : null}
121+
</li>
122+
);
123+
}
3124

4125
function SidebarSection({ section, currentPath }) {
5126
const initialOpen = useMemo(() => {
6127
if (!section?.items?.length) {
7128
return true;
8129
}
9130

10-
return section.items.some((item) => item.path === currentPath);
131+
return section.items.some((item) => itemContainsCurrentPath(item, currentPath));
11132
}, [section, currentPath]);
12133

13134
const [isOpen, setIsOpen] = useState(initialOpen);
@@ -28,31 +149,16 @@ function SidebarSection({ section, currentPath }) {
28149
className="mb-2 flex w-full items-center justify-between rounded-md px-2 py-1 text-left text-sm font-semibold text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
29150
>
30151
<span>{section.title}</span>
31-
{collapsible ? <span className="text-xs">{isOpen ? '−' : '+'}</span> : null}
152+
{collapsible ? (
153+
<Icon
154+
name="chevrondown"
155+
size={14}
156+
className={`transition-transform duration-200 ${isOpen ? 'rotate-0' : '-rotate-90'}`}
157+
/>
158+
) : null}
32159
</button>
33160

34-
{isOpen ? (
35-
<ul className="space-y-1">
36-
{(section.items || []).map((item) => {
37-
const active = item.path === currentPath;
38-
39-
return (
40-
<li key={`${item.title}-${item.path}`}>
41-
<Link
42-
href={item.path}
43-
className={`block rounded-md px-2 py-1.5 text-sm ${
44-
active
45-
? 'bg-blue-100 text-blue-800 dark:bg-blue-950/50 dark:text-blue-200'
46-
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
47-
}`}
48-
>
49-
{item.title}
50-
</Link>
51-
</li>
52-
);
53-
})}
54-
</ul>
55-
) : null}
161+
{isOpen ? <SidebarItems items={section.items || []} currentPath={currentPath} depth={0} /> : null}
56162
</section>
57163
);
58164
}

content/guides/json-reference/sidebar-json.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ File: `content/sidebar.json`
1111

1212
- Add a new sidebar section
1313
- Add links under an existing section
14+
- Add nested subcategories under a section
1415
- Reorder sections and links
1516
- Toggle section collapse behavior
1617

@@ -33,12 +34,17 @@ File: `content/sidebar.json`
3334
| Key path | Type | Required | Description |
3435
| --- | --- | --- | --- |
3536
| `sections[].items[].title` | `string` | Yes | Navigation label for page. |
36-
| `sections[].items[].path` | `string` | Yes | URL/path target for that item. |
37+
| `sections[].items[].path` | `string` | Conditionally | URL/path target for that leaf item. Required when `items`/`children` is not present. |
38+
| `sections[].items[].collapsible` | `boolean` | No | Whether this nested group can be collapsed (group items only). |
39+
| `sections[].items[].defaultOpen` | `boolean` | No | Forces nested group open by default on first render. |
40+
| `sections[].items[].items` | `array` | Conditionally | Nested subcategory list (group items only). |
41+
| `sections[].items[].children` | `array` | Conditionally | Alias for `items` for nested subcategory list. |
3742

3843
## Notes
3944

4045
- Order of `sections` controls sidebar section order.
4146
- Order of `items` controls link order within each section.
47+
- Nested groups auto-open when they contain the current page.
4248

4349
## Add a new section (copy-ready)
4450

@@ -52,3 +58,24 @@ File: `content/sidebar.json`
5258
]
5359
}
5460
```
61+
62+
## Nested subcategories (copy-ready)
63+
64+
```json
65+
{
66+
"title": "Components",
67+
"collapsible": true,
68+
"items": [
69+
{ "title": "Components Index", "path": "/guides/components/index" },
70+
{
71+
"title": "Layout",
72+
"collapsible": true,
73+
"items": [
74+
{ "title": "Header", "path": "/guides/components/header" },
75+
{ "title": "Sidebar", "path": "/guides/components/sidebar" },
76+
{ "title": "Footer", "path": "/guides/components/footer" }
77+
]
78+
}
79+
]
80+
}
81+
```

content/sidebar.json

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,49 @@
3434
"collapsible": true,
3535
"items": [
3636
{ "title": "Components Index", "path": "/guides/components/index" },
37-
{ "title": "Accordion", "path": "/guides/components/accordion" },
38-
{ "title": "Alert", "path": "/guides/components/alert" },
39-
{ "title": "Badge", "path": "/guides/components/badge" },
40-
{ "title": "ButtonLink", "path": "/guides/components/button-link" },
41-
{ "title": "Callout", "path": "/guides/components/callout" },
42-
{ "title": "Card", "path": "/guides/components/card" },
43-
{ "title": "CardGrid", "path": "/guides/components/card-grid" },
44-
{ "title": "CodeBlock", "path": "/guides/components/code-block" },
45-
{ "title": "DarkModeToggle", "path": "/guides/components/dark-mode-toggle" },
46-
{ "title": "DefinitionList", "path": "/guides/components/definition-list" },
47-
{ "title": "Divider", "path": "/guides/components/divider" },
48-
{ "title": "FeatureList", "path": "/guides/components/feature-list" },
49-
{ "title": "Figure", "path": "/guides/components/figure" },
50-
{ "title": "Footer", "path": "/guides/components/footer" },
51-
{ "title": "Header", "path": "/guides/components/header" },
52-
{ "title": "Icon", "path": "/guides/components/icon" },
53-
{ "title": "Infobox", "path": "/guides/components/infobox" },
54-
{ "title": "Kbd", "path": "/guides/components/kbd" },
55-
{ "title": "PresetSearch", "path": "/guides/components/preset-search" },
56-
{ "title": "mdx-components", "path": "/guides/components/mdx-components" },
57-
{ "title": "SearchBar", "path": "/guides/components/search-bar" },
58-
{ "title": "Sidebar", "path": "/guides/components/sidebar" },
59-
{ "title": "Steps", "path": "/guides/components/steps" },
60-
{ "title": "Tabs", "path": "/guides/components/tabs" },
61-
{ "title": "WikiTable", "path": "/guides/components/wiki-table" },
37+
{
38+
"title": "Layout & Navigation",
39+
"collapsible": true,
40+
"items": [
41+
{ "title": "Header", "path": "/guides/components/header" },
42+
{ "title": "Sidebar", "path": "/guides/components/sidebar" },
43+
{ "title": "Footer", "path": "/guides/components/footer" },
44+
{ "title": "Tabs", "path": "/guides/components/tabs" },
45+
{ "title": "Steps", "path": "/guides/components/steps" }
46+
]
47+
},
48+
{
49+
"title": "Content Blocks",
50+
"collapsible": true,
51+
"items": [
52+
{ "title": "Accordion", "path": "/guides/components/accordion" },
53+
{ "title": "Alert", "path": "/guides/components/alert" },
54+
{ "title": "Callout", "path": "/guides/components/callout" },
55+
{ "title": "Card", "path": "/guides/components/card" },
56+
{ "title": "CardGrid", "path": "/guides/components/card-grid" },
57+
{ "title": "CodeBlock", "path": "/guides/components/code-block" },
58+
{ "title": "DefinitionList", "path": "/guides/components/definition-list" },
59+
{ "title": "Divider", "path": "/guides/components/divider" },
60+
{ "title": "FeatureList", "path": "/guides/components/feature-list" },
61+
{ "title": "Figure", "path": "/guides/components/figure" },
62+
{ "title": "Infobox", "path": "/guides/components/infobox" },
63+
{ "title": "WikiTable", "path": "/guides/components/wiki-table" }
64+
]
65+
},
66+
{
67+
"title": "UI & Utilities",
68+
"collapsible": true,
69+
"items": [
70+
{ "title": "Badge", "path": "/guides/components/badge" },
71+
{ "title": "ButtonLink", "path": "/guides/components/button-link" },
72+
{ "title": "DarkModeToggle", "path": "/guides/components/dark-mode-toggle" },
73+
{ "title": "Icon", "path": "/guides/components/icon" },
74+
{ "title": "Kbd", "path": "/guides/components/kbd" },
75+
{ "title": "mdx-components", "path": "/guides/components/mdx-components" },
76+
{ "title": "PresetSearch", "path": "/guides/components/preset-search" },
77+
{ "title": "SearchBar", "path": "/guides/components/search-bar" }
78+
]
79+
},
6280
{ "title": "Writing Guidelines", "path": "/guides/writing-guidelines" }
6381
]
6482
},

0 commit comments

Comments
 (0)