polen 0.8.0-next.4 → 0.8.0-next.6

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 (42) hide show
  1. package/build/api/vite/plugins/core.d.ts.map +1 -1
  2. package/build/api/vite/plugins/core.js +146 -52
  3. package/build/api/vite/plugins/core.js.map +1 -1
  4. package/build/lib/file-router/linter.d.ts.map +1 -1
  5. package/build/lib/file-router/linter.js +3 -3
  6. package/build/lib/file-router/linter.js.map +1 -1
  7. package/build/lib/file-router/route.d.ts +48 -10
  8. package/build/lib/file-router/route.d.ts.map +1 -1
  9. package/build/lib/file-router/route.js +68 -3
  10. package/build/lib/file-router/route.js.map +1 -1
  11. package/build/lib/file-router/scan.d.ts +2 -2
  12. package/build/lib/file-router/scan.d.ts.map +1 -1
  13. package/build/lib/file-router/scan.js +8 -8
  14. package/build/lib/file-router/scan.js.map +1 -1
  15. package/build/lib/file-router/sidebar.d.ts +2 -0
  16. package/build/lib/file-router/sidebar.d.ts.map +1 -0
  17. package/build/lib/file-router/sidebar.js +2 -0
  18. package/build/lib/file-router/sidebar.js.map +1 -0
  19. package/build/lib/kit-temp.d.ts +2 -0
  20. package/build/lib/kit-temp.d.ts.map +1 -0
  21. package/build/lib/kit-temp.js +23 -0
  22. package/build/lib/kit-temp.js.map +1 -0
  23. package/build/project-data.d.ts +21 -1
  24. package/build/project-data.d.ts.map +1 -1
  25. package/build/template/components/Sidebar.d.ts +7 -0
  26. package/build/template/components/Sidebar.d.ts.map +1 -0
  27. package/build/template/components/Sidebar.jsx +108 -0
  28. package/build/template/components/Sidebar.jsx.map +1 -0
  29. package/build/template/routes/root.d.ts.map +1 -1
  30. package/build/template/routes/root.jsx +28 -5
  31. package/build/template/routes/root.jsx.map +1 -1
  32. package/package.json +1 -1
  33. package/src/api/vite/plugins/core.ts +178 -54
  34. package/src/lib/file-router/index.test.ts +5 -5
  35. package/src/lib/file-router/linter.ts +3 -3
  36. package/src/lib/file-router/route.ts +147 -11
  37. package/src/lib/file-router/scan.ts +9 -9
  38. package/src/lib/file-router/sidebar.ts +0 -0
  39. package/src/lib/kit-temp.ts +21 -0
  40. package/src/project-data.ts +26 -1
  41. package/src/template/components/Sidebar.tsx +185 -0
  42. package/src/template/routes/root.tsx +35 -5
@@ -1,9 +1,45 @@
1
- import type { Path } from '@wollybeard/kit'
1
+ import { arrayEquals } from '#lib/kit-temp.js'
2
+ import { type Path } from '@wollybeard/kit'
2
3
 
3
- export type RoutePathSegment = string
4
+ //
5
+ //
6
+ //
7
+ //
8
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Path
9
+ //
10
+ //
4
11
 
5
- export interface RoutePath {
6
- segments: RoutePathSegment[]
12
+ export type Path = PathSegment[]
13
+
14
+ export type PathRoot = []
15
+
16
+ export type PathTop = [PathSegment]
17
+
18
+ export type PathSub = [PathSegment, PathSegment, ...PathSegment[]]
19
+
20
+ export type PathSegment = string
21
+
22
+ export const sep = `/`
23
+
24
+ export const pathToExpression = (path: Path) => {
25
+ return sep + path.join(sep)
26
+ }
27
+
28
+ //
29
+ //
30
+ //
31
+ //
32
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Route (Generic)
33
+ //
34
+ //
35
+
36
+ export interface Route {
37
+ logical: RouteLogical
38
+ file: RouteFile
39
+ }
40
+
41
+ export interface RouteLogical {
42
+ path: Path
7
43
  }
8
44
 
9
45
  export interface RouteFile {
@@ -13,21 +49,121 @@ export interface RouteFile {
13
49
  }
14
50
  }
15
51
 
16
- export interface Route {
17
- path: RoutePath
18
- file: RouteFile
52
+ //
53
+ //
54
+ //
55
+ //
56
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Top Level Route
57
+ //
58
+ //
59
+
60
+ export interface TopLevelRoute extends Route {
61
+ logical: TopLevelRouteLogical
19
62
  }
20
63
 
21
- export const sep = `/`
64
+ export interface TopLevelRouteLogical {
65
+ path: PathTop
66
+ }
22
67
 
23
- export const routeToString = (route: Route) => {
24
- return sep + route.path.segments.join(sep)
68
+ /**
69
+ * Route is top level meaning exists directly under the root.
70
+ *
71
+ * It excludes the root level route.
72
+ */
73
+ export const routeIsTopLevel = (route: Route): route is TopLevelRoute => {
74
+ return route.logical.path.length === 1
25
75
  }
26
76
 
27
- export const routeIsIndex = (route: Route) => {
77
+ //
78
+ // ━━ Sub Level
79
+ //
80
+
81
+ //
82
+ //
83
+ //
84
+ //
85
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Sub Level Route
86
+ //
87
+ //
88
+
89
+ export interface RouteSubLevel extends Route {
90
+ logical: RoutePathSubLevel
91
+ }
92
+
93
+ export interface RoutePathSubLevel {
94
+ path: PathSub
95
+ }
96
+
97
+ /**
98
+ * Route is not top or root level
99
+ */
100
+ export const routeIsSubLevel = (route: Route): route is RouteSubLevel => {
101
+ return route.logical.path.length > 1
102
+ }
103
+
104
+ //
105
+ // ━━ Root Level
106
+ //
107
+
108
+ //
109
+ //
110
+ //
111
+ //
112
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Root Level Route
113
+ //
114
+ //
115
+
116
+ /**
117
+ * Route is the singular root route.
118
+
119
+ * This is the case of index under root.
120
+ */
121
+ export const routeIsRootLevel = (route: Route): route is TopLevelRoute => {
122
+ // No need to check for name "index"
123
+ // Segments is uniquely empty for <root>/index
124
+ return route.logical.path.length === 0
125
+ }
126
+
127
+ //
128
+ //
129
+ //
130
+ // ━━━━━━━━━━━━━━ • Route Functions
131
+ //
132
+ //
133
+
134
+ export const routeIsFromIndexFile = (route: Route): boolean => {
28
135
  return route.file.path.relative.name === conventions.index.name
29
136
  }
30
137
 
138
+ export const routeIsSubOf = (route: Route, potentialAncestorPath: PathSegment[]): boolean => {
139
+ if (route.logical.path.length <= potentialAncestorPath.length) {
140
+ return false
141
+ }
142
+ return arrayEquals(
143
+ route.logical.path.slice(0, potentialAncestorPath.length),
144
+ potentialAncestorPath,
145
+ )
146
+ }
147
+
148
+ /**
149
+ * You are responsible for ensuring given ancestor path is really an ancestor of given route's path.
150
+ */
151
+ export const makeRelativeUnsafe = (route: Route, assumedAncestorPath: PathSegment[]): Route => {
152
+ // We assume that we're working with paths where index is elided per our FileRouter system.
153
+ const newPath = route.logical.path.slice(assumedAncestorPath.length)
154
+ return {
155
+ ...route,
156
+ logical: {
157
+ ...route.logical,
158
+ path: newPath,
159
+ },
160
+ }
161
+ }
162
+
163
+ export const routeToPathExpression = (route: Route) => {
164
+ return pathToExpression(route.logical.path)
165
+ }
166
+
31
167
  const conventions = {
32
168
  index: {
33
169
  name: `index`,
@@ -1,7 +1,7 @@
1
1
  import { TinyGlobby } from '#dep/tiny-globby/index.js'
2
2
  import { Path, Str } from '@wollybeard/kit'
3
3
  import { type Diagnostic, lint } from './linter.js'
4
- import type { Route, RouteFile, RoutePath } from './route.js'
4
+ import type { Route, RouteFile, RouteLogical } from './route.js'
5
5
 
6
6
  //
7
7
  //
@@ -64,26 +64,26 @@ export const filePathToRoute = (filePathExpression: string, rootDir: string): Ro
64
64
  relative: Path.parse(Path.relative(rootDir, filePathExpression)),
65
65
  },
66
66
  }
67
- const path = filePathToRoutePath(file.path.relative)
67
+ const logical = filePathToRouteLogical(file.path.relative)
68
68
 
69
69
  return {
70
- path,
70
+ logical,
71
71
  file,
72
72
  }
73
73
  }
74
74
 
75
- export const filePathToRoutePath = (filePath: Path.Parsed): RoutePath => {
76
- const dirSegments = Str.split(Str.removeSurrounding(filePath.dir, Path.sep), Path.sep)
75
+ export const filePathToRouteLogical = (filePath: Path.Parsed): RouteLogical => {
76
+ const dirPath = Str.split(Str.removeSurrounding(filePath.dir, Path.sep), Path.sep)
77
77
 
78
78
  if (Str.isMatch(filePath.name, conventions.index.name)) {
79
- const segments = dirSegments
79
+ const path = dirPath
80
80
  return {
81
- segments,
81
+ path,
82
82
  }
83
83
  }
84
84
 
85
- const segments = dirSegments.concat(filePath.name)
85
+ const path = dirPath.concat(filePath.name)
86
86
  return {
87
- segments,
87
+ path,
88
88
  }
89
89
  }
File without changes
@@ -0,0 +1,21 @@
1
+ //
2
+ //
3
+ //
4
+ //
5
+ //
6
+ // Holding Module for Missing @wollybeard/kit Functionality
7
+ //
8
+ // ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
9
+ //
10
+ // Code here is meant to be migrated eventually to @wollybeard/kit.
11
+ //
12
+ //
13
+ //
14
+
15
+ export const arrayEquals = (a: any[], b: any[]) => {
16
+ if (a.length !== b.length) return false
17
+ for (let i = 0; i < a.length; i++) {
18
+ if (a[i] !== b[i]) return false
19
+ }
20
+ return true
21
+ }
@@ -5,6 +5,7 @@ import type { Schema } from './api/schema/index.js'
5
5
  export interface ProjectData {
6
6
  schema: null | Schema.Schema
7
7
  siteNavigationItems: SiteNavigationItem[]
8
+ sidebarIndex: SidebarIndex
8
9
  faviconPath: string
9
10
  paths: Configurator.Config[`paths`][`project`]
10
11
  pagesScanResult: FileRouter.ScanResult
@@ -18,5 +19,29 @@ export interface ProjectData {
18
19
 
19
20
  export interface SiteNavigationItem {
20
21
  title: string
21
- path: string
22
+ pathExp: string
23
+ }
24
+
25
+ export interface SidebarIndex {
26
+ [pathExpression: string]: Sidebar
27
+ }
28
+
29
+ export interface Sidebar {
30
+ items: SidebarItem[]
31
+ }
32
+
33
+ export type SidebarItem = SidebarNav | SidebarSection
34
+
35
+ export interface SidebarNav {
36
+ type: `SidebarItem`
37
+ title: string
38
+ pathExp: string
39
+ }
40
+
41
+ export interface SidebarSection {
42
+ type: `SidebarSection`
43
+ title: string
44
+ pathExp: string
45
+ isNavToo: boolean
46
+ navs: SidebarNav[]
22
47
  }
@@ -0,0 +1,185 @@
1
+ import { ChevronDownIcon, ChevronRightIcon } from '@radix-ui/react-icons'
2
+ import { Box, Flex, Text } from '@radix-ui/themes'
3
+ import { useState } from 'react'
4
+ import { Link, useLocation } from 'react-router'
5
+ import type { SidebarItem, SidebarNav, SidebarSection } from '../../project-data.js'
6
+
7
+ interface SidebarProps {
8
+ items: SidebarItem[]
9
+ }
10
+
11
+ export const Sidebar = ({ items }: SidebarProps) => {
12
+ const location = useLocation()
13
+
14
+ return (
15
+ <Box
16
+ style={{
17
+ width: `240px`,
18
+ minWidth: `240px`,
19
+ flexShrink: 0,
20
+ borderRight: `1px solid var(--gray-3)`,
21
+ height: `100%`,
22
+ paddingRight: `var(--space-4)`,
23
+ }}
24
+ >
25
+ <Flex direction='column' gap='1'>
26
+ {items.map((item) => (
27
+ <SidebarItemComponent
28
+ key={item.pathExp}
29
+ item={item}
30
+ currentPathExp={location.pathname}
31
+ />
32
+ ))}
33
+ </Flex>
34
+ </Box>
35
+ )
36
+ }
37
+
38
+ interface SidebarItemComponentProps {
39
+ item: SidebarItem
40
+ currentPathExp: string
41
+ level?: number
42
+ }
43
+
44
+ const SidebarItemComponent = ({ item, currentPathExp, level = 0 }: SidebarItemComponentProps) => {
45
+ if (item.type === `SidebarItem`) {
46
+ return <SidebarNavItem nav={item} currentPathExp={currentPathExp} level={level} />
47
+ }
48
+
49
+ return <SidebarSectionItem section={item} currentPathExp={currentPathExp} level={level} />
50
+ }
51
+
52
+ interface SidebarNavItemProps {
53
+ nav: SidebarNav
54
+ currentPathExp: string
55
+ level: number
56
+ }
57
+
58
+ const SidebarNavItem = ({ nav, currentPathExp, level }: SidebarNavItemProps) => {
59
+ const isActive = currentPathExp === nav.pathExp
60
+
61
+ return (
62
+ <Link
63
+ to={nav.pathExp}
64
+ style={{
65
+ textDecoration: `none`,
66
+ color: isActive ? `var(--accent-11)` : `var(--gray-12)`,
67
+ padding: `var(--space-2) var(--space-3)`,
68
+ paddingLeft: `calc(var(--space-3) + ${(level * 16).toString()}px)`,
69
+ borderRadius: `var(--radius-2)`,
70
+ display: `block`,
71
+ backgroundColor: isActive ? `var(--accent-3)` : `transparent`,
72
+ transition: `background-color 0.2s ease, color 0.2s ease`,
73
+ }}
74
+ onMouseEnter={(e) => {
75
+ if (!isActive) {
76
+ e.currentTarget.style.backgroundColor = `var(--gray-2)`
77
+ }
78
+ }}
79
+ onMouseLeave={(e) => {
80
+ if (!isActive) {
81
+ e.currentTarget.style.backgroundColor = `transparent`
82
+ }
83
+ }}
84
+ >
85
+ <Text size='2' weight={isActive ? `medium` : `regular`}>
86
+ {nav.title}
87
+ </Text>
88
+ </Link>
89
+ )
90
+ }
91
+
92
+ interface SidebarSectionItemProps {
93
+ section: SidebarSection
94
+ currentPathExp: string
95
+ level: number
96
+ }
97
+
98
+ const SidebarSectionItem = ({ section, currentPathExp, level }: SidebarSectionItemProps) => {
99
+ const [isExpanded, setIsExpanded] = useState(true)
100
+ const isDirectlyActive = currentPathExp === section.pathExp
101
+ const hasActiveChild = section.navs.some(nav => currentPathExp === nav.pathExp)
102
+ const isActiveGroup = isDirectlyActive || hasActiveChild
103
+
104
+ return (
105
+ <>
106
+ <Flex
107
+ align='center'
108
+ style={{
109
+ padding: `var(--space-2) var(--space-3)`,
110
+ paddingLeft: `calc(var(--space-3) + ${(level * 16).toString()}px)`,
111
+ borderRadius: `var(--radius-2)`,
112
+ backgroundColor: isDirectlyActive ? `var(--accent-3)` : hasActiveChild ? `var(--accent-2)` : `transparent`,
113
+ transition: `background-color 0.2s ease`,
114
+ }}
115
+ onMouseEnter={(e) => {
116
+ if (!isActiveGroup) {
117
+ e.currentTarget.style.backgroundColor = `var(--gray-2)`
118
+ }
119
+ }}
120
+ onMouseLeave={(e) => {
121
+ if (!isActiveGroup) {
122
+ e.currentTarget.style.backgroundColor = `transparent`
123
+ }
124
+ }}
125
+ >
126
+ <Box
127
+ onClick={(e) => {
128
+ e.stopPropagation()
129
+ console.log(`Chevron clicked!`)
130
+ setIsExpanded(!isExpanded)
131
+ }}
132
+ style={{
133
+ display: `flex`,
134
+ alignItems: `center`,
135
+ cursor: `pointer`,
136
+ padding: `4px`,
137
+ marginRight: `4px`,
138
+ marginLeft: `-4px`,
139
+ }}
140
+ >
141
+ {isExpanded ? <ChevronDownIcon /> : <ChevronRightIcon />}
142
+ </Box>
143
+ {section.isNavToo
144
+ ? (
145
+ <Link
146
+ to={section.pathExp}
147
+ style={{
148
+ textDecoration: `none`,
149
+ color: isDirectlyActive ? `var(--accent-11)` : `var(--gray-12)`,
150
+ flex: 1,
151
+ }}
152
+ >
153
+ <Text size='2' weight={isDirectlyActive ? `bold` : `medium`}>
154
+ {section.title}
155
+ </Text>
156
+ </Link>
157
+ )
158
+ : (
159
+ <Text
160
+ size='2'
161
+ weight={isDirectlyActive ? `bold` : `medium`}
162
+ style={{
163
+ flex: 1,
164
+ color: isDirectlyActive ? `var(--accent-11)` : `var(--gray-12)`,
165
+ }}
166
+ >
167
+ {section.title}
168
+ </Text>
169
+ )}
170
+ </Flex>
171
+ {isExpanded && (
172
+ <Flex direction='column' gap='1'>
173
+ {section.navs.map((nav) => (
174
+ <SidebarNavItem
175
+ key={nav.pathExp}
176
+ nav={nav}
177
+ currentPathExp={currentPathExp}
178
+ level={level + 1}
179
+ />
180
+ ))}
181
+ </Flex>
182
+ )}
183
+ </>
184
+ )
185
+ }
@@ -5,10 +5,11 @@ import { Box, Button, Heading, Text } from '@radix-ui/themes'
5
5
  import { Flex, Theme } from '@radix-ui/themes'
6
6
  import radixStylesUrl from '@radix-ui/themes/styles.css?url'
7
7
  import { Link as LinkReactRouter } from 'react-router'
8
- import { Outlet, ScrollRestoration } from 'react-router'
8
+ import { Outlet, ScrollRestoration, useLocation } from 'react-router'
9
9
  import { PROJECT_DATA } from 'virtual:polen/project/data'
10
10
  import { templateVariables } from 'virtual:polen/template/variables'
11
11
  import { Link } from '../components/Link.jsx'
12
+ import { Sidebar } from '../components/Sidebar.jsx'
12
13
  import entryClientUrl from '../entry.client.jsx?url'
13
14
  import { changelog } from './changelog.jsx'
14
15
  import { index } from './index.jsx'
@@ -55,6 +56,23 @@ export const Component = () => {
55
56
  }
56
57
 
57
58
  const Layout = () => {
59
+ const location = useLocation()
60
+
61
+ // Determine if we should show sidebar based on current path
62
+ const getCurrentNavPathExp = (): string | null => {
63
+ // todo: general path manipulation lib because we are duplicating logic here found in FileRouter
64
+ // todo: kit: try a Str.split that returns [] | string[] so that our predicates can refine on it?
65
+ const segments = location.pathname.split(`/`).filter(Boolean)
66
+ if (Arr.isntEmpty(segments)) {
67
+ return `/${segments[0]}`
68
+ }
69
+ return null
70
+ }
71
+
72
+ const currentNavPathExp = getCurrentNavPathExp()
73
+ const sidebar = currentNavPathExp && PROJECT_DATA.sidebarIndex[currentNavPathExp]
74
+ const showSidebar = sidebar && sidebar.items.length > 0
75
+
58
76
  return (
59
77
  <Theme asChild>
60
78
  <Box m='8'>
@@ -80,15 +98,26 @@ const Layout = () => {
80
98
  </LinkReactRouter>
81
99
  <Flex direction='row' gap='4'>
82
100
  {PROJECT_DATA.siteNavigationItems.map((item, key) => (
83
- <Link key={key} color='gray' to={item.path}>
101
+ <Link key={key} color='gray' to={item.pathExp}>
84
102
  {item.title}
85
103
  </Link>
86
104
  ))}
87
105
  </Flex>
88
106
  </Flex>
89
- <Box>
90
- <Outlet />
91
- </Box>
107
+ {showSidebar
108
+ ? (
109
+ <Flex gap='8'>
110
+ <Sidebar items={sidebar.items} />
111
+ <Box style={{ flex: 1 }}>
112
+ <Outlet />
113
+ </Box>
114
+ </Flex>
115
+ )
116
+ : (
117
+ <Box>
118
+ <Outlet />
119
+ </Box>
120
+ )}
92
121
  </Box>
93
122
  </Theme>
94
123
  )
@@ -165,6 +194,7 @@ children.push(notFoundRoute)
165
194
  //
166
195
  //
167
196
 
197
+ import { Arr } from '@wollybeard/kit'
168
198
  import { pages } from 'virtual:polen/project/pages.jsx'
169
199
 
170
200
  export const root = createRoute({