polen 0.10.0-next.6 → 0.10.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/api/content/$$.d.ts +1 -0
  2. package/build/api/content/$$.d.ts.map +1 -1
  3. package/build/api/content/$$.js +1 -0
  4. package/build/api/content/$$.js.map +1 -1
  5. package/build/api/content/navbar.d.ts +10 -0
  6. package/build/api/content/navbar.d.ts.map +1 -0
  7. package/build/api/content/navbar.js +45 -0
  8. package/build/api/content/navbar.js.map +1 -0
  9. package/build/api/content/sidebar.d.ts +85 -5
  10. package/build/api/content/sidebar.d.ts.map +1 -1
  11. package/build/api/content/sidebar.js +151 -75
  12. package/build/api/content/sidebar.js.map +1 -1
  13. package/build/api/vite/plugins/pages.d.ts +1 -4
  14. package/build/api/vite/plugins/pages.d.ts.map +1 -1
  15. package/build/api/vite/plugins/pages.js +4 -42
  16. package/build/api/vite/plugins/pages.js.map +1 -1
  17. package/build/lib/file-router/scan.d.ts.map +1 -1
  18. package/build/lib/file-router/scan.js +6 -1
  19. package/build/lib/file-router/scan.js.map +1 -1
  20. package/build/sandbox.js +17 -2
  21. package/build/sandbox.js.map +1 -1
  22. package/build/template/components/HamburgerMenu.d.ts +9 -0
  23. package/build/template/components/HamburgerMenu.d.ts.map +1 -0
  24. package/build/template/components/HamburgerMenu.jsx +53 -0
  25. package/build/template/components/HamburgerMenu.jsx.map +1 -0
  26. package/build/template/components/NotFound.d.ts +2 -0
  27. package/build/template/components/NotFound.d.ts.map +1 -0
  28. package/build/template/components/NotFound.jsx +26 -0
  29. package/build/template/components/NotFound.jsx.map +1 -0
  30. package/build/template/components/ThemeToggle.jsx +2 -2
  31. package/build/template/routes/root.d.ts.map +1 -1
  32. package/build/template/routes/root.jsx +40 -30
  33. package/build/template/routes/root.jsx.map +1 -1
  34. package/package.json +1 -1
  35. package/src/api/content/$$.ts +1 -0
  36. package/src/api/content/navbar.test.ts +55 -0
  37. package/src/api/content/navbar.ts +61 -0
  38. package/src/api/content/sidebar.test.ts +297 -0
  39. package/src/api/content/sidebar.ts +235 -88
  40. package/src/api/vite/plugins/pages.ts +5 -51
  41. package/src/lib/file-router/scan.ts +7 -1
  42. package/src/sandbox.ts +20 -1
  43. package/src/template/components/HamburgerMenu.tsx +96 -0
  44. package/src/template/components/NotFound.tsx +28 -0
  45. package/src/template/components/ThemeToggle.tsx +5 -5
  46. package/src/template/contexts/ThemeContext.tsx +4 -4
  47. package/src/template/routes/root.tsx +59 -42
@@ -1,5 +1,6 @@
1
1
  import type { Config } from '#api/config/index'
2
2
  import { Content } from '#api/content/$'
3
+ import { createNavbar } from '#api/content/navbar'
3
4
  import type { NavbarDataRegistry } from '#api/vite/data/navbar'
4
5
  import { polenVirtual } from '#api/vite/vi'
5
6
  import type { Vite } from '#dep/vite/index'
@@ -9,7 +10,6 @@ import { debugPolen } from '#singletons/debug'
9
10
  import { superjson } from '#singletons/superjson'
10
11
  import mdx from '@mdx-js/rollup'
11
12
  import rehypeShiki from '@shikijs/rehype'
12
- import { Tree } from '@wollybeard/kit'
13
13
  import { Arr, Cache, Path, Str } from '@wollybeard/kit'
14
14
  import remarkFrontmatter from 'remark-frontmatter'
15
15
  import remarkGfm from 'remark-gfm'
@@ -26,14 +26,10 @@ export interface Options {
26
26
  }
27
27
 
28
28
  export interface ProjectDataPages {
29
- sidebarIndex: SidebarIndex
29
+ sidebarIndex: Content.SidebarIndex
30
30
  pages: Content.Page[]
31
31
  }
32
32
 
33
- export interface SidebarIndex {
34
- [pathExpression: string]: Content.Sidebar
35
- }
36
-
37
33
  /**
38
34
  * Pages plugin with tree support
39
35
  */
@@ -204,57 +200,15 @@ export const Pages = ({
204
200
  const navbarPages = navbarData.get('pages')
205
201
  navbarPages.length = 0 // Clear existing
206
202
 
207
- // Process first-level children as navigation items
208
- if (scanResult.tree.root) {
209
- for (const child of scanResult.tree.root.children) {
210
- // Now we have Page objects in the tree
211
- const page = child.value
212
- const pathExp = FileRouter.routeToPathExpression(page.route)
213
-
214
- // Skip hidden pages and index files at root level
215
- if (page.metadata.hidden || page.route.logical.path.slice(-1)[0] === 'index') {
216
- continue
217
- }
218
-
219
- // Only include top-level pages (files directly in pages directory)
220
- if (page.route.logical.path.length === 1) {
221
- const title = Str.titlizeSlug(page.route.logical.path[0]!)
222
- navbarPages.push({
223
- // IMPORTANT: Always ensure paths start with '/' for React Router compatibility.
224
- pathExp: pathExp.startsWith('/') ? pathExp : '/' + pathExp,
225
- title,
226
- })
227
- }
228
- }
229
- }
203
+ const navbarItems = createNavbar(scanResult.list)
204
+ navbarPages.push(...navbarItems)
230
205
  }
231
206
 
232
207
  //
233
208
  // ━━ Build Sidebar
234
209
  //
235
210
 
236
- const sidebarIndex: SidebarIndex = {}
237
-
238
- // Build sidebar for each top-level directory using the page tree
239
- if (scanResult.tree.root) {
240
- Tree.visit(scanResult.tree, (node) => {
241
- if (!node.value) return
242
- const page = node.value as any
243
- // Only process top-level directories (pages with logical path length > 1 indicate nested structure)
244
- if (page.route.logical.path.length === 1 && node.children.length > 0) {
245
- const topLevelDir = page.route.logical.path[0]!
246
- const pathExp = `/${topLevelDir}`
247
-
248
- // Create a subtree for this directory
249
- const subtree = Tree.Tree(Tree.Node(page, node.children)) as Tree.Tree<any>
250
-
251
- // Build sidebar using the new page tree builder
252
- const sidebar = Content.buildFromPageTree(subtree, [topLevelDir])
253
- debug(`Built sidebar for ${pathExp}:`, sidebar)
254
- sidebarIndex[pathExp] = sidebar
255
- }
256
- })
257
- }
211
+ const sidebarIndex = Content.buildSidebarIndex(scanResult)
258
212
 
259
213
  //
260
214
  // ━━ Put It All together
@@ -87,7 +87,13 @@ export const filePathToRoute = (filePathExpression: string, rootDir: string): Ro
87
87
  }
88
88
 
89
89
  export const filePathToRouteLogical = (filePath: Path.Parsed): RouteLogical => {
90
- const dirPath = Str.split(Str.removeSurrounding(filePath.dir, Path.sep), Path.sep)
90
+ const dirSegments = Str.split(Str.removeSurrounding(filePath.dir, Path.sep), Path.sep)
91
+
92
+ // Parse numbered prefixes from directory segments
93
+ const dirPath = dirSegments.map(segment => {
94
+ const prefixMatch = Str.match(segment, conventions.numberedPrefix.pattern)
95
+ return prefixMatch?.groups.name ?? segment
96
+ })
91
97
 
92
98
  // Parse numbered prefix from filename
93
99
  const prefixMatch = Str.match(filePath.name, conventions.numberedPrefix.pattern)
package/src/sandbox.ts CHANGED
@@ -1 +1,20 @@
1
- // Sandbox file for temporary testing
1
+ import { filePathToRouteLogical } from '#lib/file-router/scan'
2
+ import { Path } from '@wollybeard/kit'
3
+
4
+ // Test parsing of numbered directory
5
+ const testPath1 = Path.parse('a/10_b/index.md')
6
+ const logical1 = filePathToRouteLogical(testPath1)
7
+ console.log('Path 1:', testPath1)
8
+ console.log('Logical 1:', logical1)
9
+
10
+ // Test parsing of numbered file
11
+ const testPath2 = Path.parse('a/10_b/g.md')
12
+ const logical2 = filePathToRouteLogical(testPath2)
13
+ console.log('\nPath 2:', testPath2)
14
+ console.log('Logical 2:', logical2)
15
+
16
+ // Test directory structure
17
+ const testPath3 = Path.parse('a/30_d/index.md')
18
+ const logical3 = filePathToRouteLogical(testPath3)
19
+ console.log('\nPath 3:', testPath3)
20
+ console.log('Logical 3:', logical3)
@@ -0,0 +1,96 @@
1
+ import type { Content } from '#api/content/$'
2
+ import { Cross2Icon, HamburgerMenuIcon } from '@radix-ui/react-icons'
3
+ import { Box, Flex, IconButton, Text } from '@radix-ui/themes'
4
+ import { useEffect } from 'react'
5
+ import { Sidebar } from '../components/sidebar/Sidebar.tsx'
6
+
7
+ export interface HamburgerMenuProps {
8
+ isOpen: boolean
9
+ onToggle: () => void
10
+ onClose: () => void
11
+ sidebarData: Content.Item[]
12
+ }
13
+
14
+ export const HamburgerMenu: React.FC<HamburgerMenuProps> = ({
15
+ isOpen,
16
+ onToggle,
17
+ onClose,
18
+ sidebarData,
19
+ }) => {
20
+ // Prevent body scroll when mobile menu is open
21
+ useEffect(() => {
22
+ if (isOpen) {
23
+ document.body.style.overflow = 'hidden'
24
+ } else {
25
+ document.body.style.overflow = ''
26
+ }
27
+
28
+ // Cleanup
29
+ return () => {
30
+ document.body.style.overflow = ''
31
+ }
32
+ }, [isOpen])
33
+
34
+ return (
35
+ <>
36
+ {/* Mobile menu button - show on mobile/tablet, hide on desktop */}
37
+ <Box display={{ initial: 'block', xs: 'block', sm: 'block', md: 'none', lg: 'none', xl: 'none' }}>
38
+ <IconButton
39
+ size='2'
40
+ variant='ghost'
41
+ onClick={onToggle}
42
+ aria-label='Toggle navigation menu'
43
+ >
44
+ {isOpen ? <Cross2Icon width='18' height='18' /> : <HamburgerMenuIcon width='18' height='18' />}
45
+ </IconButton>
46
+ </Box>
47
+
48
+ {/* Mobile Sidebar Drawer */}
49
+ {isOpen && (
50
+ <>
51
+ {/* Backdrop */}
52
+ <Box
53
+ position='fixed'
54
+ inset='0'
55
+ style={{
56
+ backgroundColor: 'var(--black-a9)',
57
+ zIndex: 50,
58
+ }}
59
+ onClick={onClose}
60
+ display={{ initial: 'block', xs: 'block', sm: 'block', md: 'none', lg: 'none', xl: 'none' }}
61
+ />
62
+
63
+ {/* Drawer */}
64
+ <Box
65
+ position='fixed'
66
+ top='0'
67
+ left='0'
68
+ bottom='0'
69
+ width='280px'
70
+ style={{
71
+ backgroundColor: 'var(--color-background)',
72
+ boxShadow: 'var(--shadow-6)',
73
+ zIndex: 100,
74
+ overflowY: 'auto',
75
+ }}
76
+ p='4'
77
+ display={{ initial: 'block', xs: 'block', sm: 'block', md: 'none', lg: 'none', xl: 'none' }}
78
+ >
79
+ <Flex justify='between' align='center' mb='4'>
80
+ <Text size='5' weight='bold'>Navigation</Text>
81
+ <IconButton
82
+ size='2'
83
+ variant='ghost'
84
+ onClick={onClose}
85
+ aria-label='Close navigation menu'
86
+ >
87
+ <Cross2Icon width='18' height='18' />
88
+ </IconButton>
89
+ </Flex>
90
+ <Sidebar data={sidebarData} />
91
+ </Box>
92
+ </>
93
+ )}
94
+ </>
95
+ )
96
+ }
@@ -0,0 +1,28 @@
1
+ import { Box, Button, Flex, Heading, Text } from '@radix-ui/themes'
2
+ import { Link as LinkReactRouter } from 'react-router'
3
+
4
+ export const NotFound: React.FC = () => {
5
+ return (
6
+ <Flex direction='column' align='center' gap='6' style={{ textAlign: `center`, paddingTop: `4rem` }}>
7
+ <Heading size='9' style={{ color: `var(--gray-12)` }}>404</Heading>
8
+ <Box>
9
+ <Heading size='5' mb='2'>Page Not Found</Heading>
10
+ <Text size='3' color='gray'>
11
+ The page you're looking for doesn't exist or has been moved.
12
+ </Text>
13
+ </Box>
14
+ <Flex gap='3'>
15
+ <LinkReactRouter to='/'>
16
+ <Button variant='soft' size='3'>
17
+ Go Home
18
+ </Button>
19
+ </LinkReactRouter>
20
+ <LinkReactRouter to='/reference'>
21
+ <Button variant='outline' size='3'>
22
+ View API Reference
23
+ </Button>
24
+ </LinkReactRouter>
25
+ </Flex>
26
+ </Flex>
27
+ )
28
+ }
@@ -8,14 +8,14 @@ export const ThemeToggle: React.FC = () => {
8
8
 
9
9
  return (
10
10
  <IconButton
11
- size="2"
12
- variant="ghost"
13
- color="gray"
11
+ size='2'
12
+ variant='ghost'
13
+ color='gray'
14
14
  onClick={toggleTheme}
15
15
  aria-label={`Switch to ${appearance === 'light' ? 'dark' : 'light'} theme`}
16
16
  style={{ cursor: 'pointer' }}
17
17
  >
18
- {appearance === 'light' ? <MoonIcon width="18" height="18" /> : <SunIcon width="18" height="18" />}
18
+ {appearance === 'light' ? <MoonIcon width='18' height='18' /> : <SunIcon width='18' height='18' />}
19
19
  </IconButton>
20
20
  )
21
- }
21
+ }
@@ -18,18 +18,18 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
18
18
  if (typeof window === 'undefined') {
19
19
  return 'light'
20
20
  }
21
-
21
+
22
22
  // Check localStorage first
23
23
  const stored = localStorage.getItem(THEME_STORAGE_KEY)
24
24
  if (stored === 'light' || stored === 'dark') {
25
25
  return stored
26
26
  }
27
-
27
+
28
28
  // Check system preference
29
29
  if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
30
30
  return 'dark'
31
31
  }
32
-
32
+
33
33
  return 'light'
34
34
  })
35
35
 
@@ -57,4 +57,4 @@ export const useTheme = () => {
57
57
  throw new Error('useTheme must be used within a ThemeProvider')
58
58
  }
59
59
  return context
60
- }
60
+ }
@@ -1,10 +1,10 @@
1
- import { assetUrl } from '#api/utils/asset-url/index'
2
1
  import type { ReactRouter } from '#dep/react-router/index'
3
2
  import { createRoute } from '#lib/react-router-aid/react-router-aid'
4
- import { Box, Button, Grid, Heading, Text } from '@radix-ui/themes'
3
+ import { Box, Grid } from '@radix-ui/themes'
5
4
  import { Flex, Theme } from '@radix-ui/themes'
6
5
  import radixStylesUrl from '@radix-ui/themes/styles.css?url'
7
6
  import { Arr } from '@wollybeard/kit'
7
+ import { useEffect, useState } from 'react'
8
8
  import { Link as LinkReactRouter } from 'react-router'
9
9
  import { Outlet, ScrollRestoration, useLocation } from 'react-router'
10
10
  import logoSrc from 'virtual:polen/project/assets/logo.svg'
@@ -13,8 +13,10 @@ import projectDataNavbar from 'virtual:polen/project/data/navbar.jsonsuper'
13
13
  import projectDataPages from 'virtual:polen/project/data/pages.jsonsuper'
14
14
  import { pages } from 'virtual:polen/project/pages.jsx'
15
15
  import { templateVariables } from 'virtual:polen/template/variables'
16
+ import { HamburgerMenu } from '../components/HamburgerMenu.tsx'
16
17
  import { Link } from '../components/Link.tsx'
17
18
  import { Logo } from '../components/Logo.tsx'
19
+ import { NotFound } from '../components/NotFound.tsx'
18
20
  import { Sidebar } from '../components/sidebar/Sidebar.tsx'
19
21
  import { ThemeToggle } from '../components/ThemeToggle.tsx'
20
22
  import { ThemeProvider, useTheme } from '../contexts/ThemeContext.tsx'
@@ -60,6 +62,12 @@ export const Component = () => {
60
62
  const Layout = () => {
61
63
  const location = useLocation()
62
64
  const { appearance } = useTheme()
65
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
66
+
67
+ // Close mobile menu on route change
68
+ useEffect(() => {
69
+ setMobileMenuOpen(false)
70
+ }, [location.pathname])
63
71
 
64
72
  // Determine if we should show sidebar based on current path
65
73
  const getCurrentNavPathExp = (): string | null => {
@@ -80,18 +88,30 @@ const Layout = () => {
80
88
  <Flex
81
89
  gridArea={'header'}
82
90
  align='center'
83
- gap='8'
91
+ gap={{ initial: '4', md: '8' }}
84
92
  pb='4'
85
- mb='8'
93
+ mb={{ initial: '4', md: '8' }}
86
94
  style={{
87
95
  borderBottom: `1px solid var(--gray-3)`,
88
96
  }}
89
97
  >
98
+ {/* Mobile menu - only show when sidebar exists */}
99
+ {isShowSidebar && (
100
+ <HamburgerMenu
101
+ isOpen={mobileMenuOpen}
102
+ onToggle={() => setMobileMenuOpen(!mobileMenuOpen)}
103
+ onClose={() => setMobileMenuOpen(false)}
104
+ sidebarData={sidebar.items}
105
+ />
106
+ )}
107
+
90
108
  <LinkReactRouter
91
109
  to='/'
92
110
  style={{ color: `inherit`, textDecoration: `none` }}
93
111
  >
94
- <Logo src={logoSrc} title={templateVariables.title} height={30} showTitle={true} />
112
+ <Box display={{ initial: 'block', md: 'block' }}>
113
+ <Logo src={logoSrc} title={templateVariables.title} height={30} showTitle={true} />
114
+ </Box>
95
115
  </LinkReactRouter>
96
116
  <Flex direction='row' gap='4' style={{ flex: 1 }}>
97
117
  {projectDataNavbar.map((item, key) => (
@@ -107,16 +127,36 @@ const Layout = () => {
107
127
  return (
108
128
  <Theme asChild appearance={appearance}>
109
129
  <Grid
110
- width={{ initial: 'var(--container-4)' }}
111
- areas="'header header header header header header header header' 'sidebar sidebar . content content content content content'"
130
+ width={{ initial: '100%', sm: '100%', md: 'var(--container-4)' }}
131
+ maxWidth='100vw'
132
+ areas={{
133
+ initial: "'header' 'content'",
134
+ sm: "'header' 'content'",
135
+ md:
136
+ "'header header header header header header header header' 'sidebar sidebar . content content content content content'",
137
+ }}
112
138
  rows='min-content auto'
113
- columns='repeat(8, 1fr)'
114
- gapX='2'
115
- my='8'
139
+ columns={{ initial: '1fr', sm: '1fr', md: 'repeat(8, 1fr)' }}
140
+ gapX={{ initial: '0', sm: '0', md: '2' }}
141
+ my={{ initial: '0', sm: '0', md: '8' }}
116
142
  mx='auto'
143
+ px={{ initial: '4', sm: '4', md: '0' }}
144
+ py={{ initial: '4', sm: '4', md: '0' }}
117
145
  >
118
146
  <style>
119
147
  {`
148
+ /* Responsive container fixes */
149
+ @media (max-width: 768px) {
150
+ body {
151
+ overflow-x: hidden;
152
+ }
153
+ }
154
+
155
+ /* Ensure proper centering on all screen sizes */
156
+ .rt-Grid {
157
+ box-sizing: border-box;
158
+ }
159
+
120
160
  /* Shiki code blocks */
121
161
  pre.shiki {
122
162
  margin: 1rem 0;
@@ -150,15 +190,18 @@ const Layout = () => {
150
190
  `}
151
191
  </style>
152
192
  {header}
193
+
194
+ {/* Desktop Sidebar */}
153
195
  {isShowSidebar && (
154
- <Sidebar
196
+ <Box
197
+ display={{ initial: 'none', xs: 'none', sm: 'none', md: 'block' }}
155
198
  gridColumn='1 / 3'
156
199
  gridRow='2 / auto'
157
- data={sidebar.items}
158
- // ml='-100px'
159
- // style={{ transform: 'translate(calc(-100% - var(--space-8)))' }}
160
- />
200
+ >
201
+ <Sidebar data={sidebar.items} />
202
+ </Box>
161
203
  )}
204
+
162
205
  <Box gridArea='content / content / auto / 8'>
163
206
  <Outlet />
164
207
  </Box>
@@ -195,36 +238,10 @@ if (PROJECT_DATA.schema) {
195
238
  //
196
239
  //
197
240
 
198
- const NotFoundComponent = () => {
199
- return (
200
- <Flex direction='column' align='center' gap='6' style={{ textAlign: `center`, paddingTop: `4rem` }}>
201
- <Heading size='9' style={{ color: `var(--gray-12)` }}>404</Heading>
202
- <Box>
203
- <Heading size='5' mb='2'>Page Not Found</Heading>
204
- <Text size='3' color='gray'>
205
- The page you're looking for doesn't exist or has been moved.
206
- </Text>
207
- </Box>
208
- <Flex gap='3'>
209
- <LinkReactRouter to='/'>
210
- <Button variant='soft' size='3'>
211
- Go Home
212
- </Button>
213
- </LinkReactRouter>
214
- <LinkReactRouter to='/reference'>
215
- <Button variant='outline' size='3'>
216
- View API Reference
217
- </Button>
218
- </LinkReactRouter>
219
- </Flex>
220
- </Flex>
221
- )
222
- }
223
-
224
241
  const notFoundRoute = createRoute({
225
242
  id: `*_not_found`,
226
243
  path: `*`,
227
- Component: NotFoundComponent,
244
+ Component: NotFound,
228
245
  handle: {
229
246
  statusCode: 404,
230
247
  },