@xyd-js/framework 0.1.0-xyd.21 → 0.1.0-xyd.4

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.
@@ -0,0 +1,337 @@
1
+ import React, {isValidElement} from "react";
2
+ import {useLocation} from "react-router";
3
+
4
+ import {Toc, SubNav, UISidebar} from "@xyd-js/ui"
5
+ import type {ITOC} from "@xyd-js/ui";
6
+ import {Breadcrumbs, NavLinks} from "@xyd-js/components/writer";
7
+
8
+ import {useBreadcrumbs, useNavLinks, useSettings, useSidebarGroups, useToC} from "../contexts";
9
+ import {FwSidebarItemGroup, FwSidebarGroupContext, FwSidebarItemProps} from "./sidebar";
10
+ import {Nav} from "@xyd-js/ui";
11
+
12
+ import {manualHydration} from "../utils/manualHydration";
13
+ import {useMatchedSubNav} from "../hooks";
14
+
15
+ function FwNavLogo() {
16
+ const settings = useSettings()
17
+
18
+ const logo = isValidElement(settings?.styling?.logo) ? settings?.styling?.logo : manualHydration(settings?.styling?.logo)
19
+
20
+ // TODO: configurable url?
21
+ return <a href="/">
22
+ {logo}
23
+ </a>
24
+ }
25
+
26
+ function FwNav({kind}: { kind?: "middle" }) {
27
+ const matchedSubnav = useMatchedSubNav()
28
+ const location = useLocation()
29
+
30
+ const settings = useSettings()
31
+
32
+ const headers = matchedSubnav ? matchedSubnav?.items : settings?.structure?.header
33
+ const active = headers?.find(item => location.pathname.startsWith(item.url || ""))
34
+
35
+ return <Nav
36
+ value={active?.url || ""}
37
+ kind={kind}
38
+ logo={<FwNavLogo/>}
39
+ onChange={() => {
40
+ }}
41
+ >
42
+ {
43
+ settings?.structure?.header?.map((item, index) => {
44
+ return <Nav.Item
45
+ key={index + (item.url || "") + item.name}
46
+ href={item?.url || ""}
47
+ value={item.url}
48
+ >
49
+ {item.name}
50
+ </Nav.Item>
51
+ })
52
+ }
53
+ </Nav>
54
+ }
55
+
56
+ function FwSubNav() {
57
+ const matchedSubnav = useMatchedSubNav()
58
+ const location = useLocation()
59
+
60
+ if (!matchedSubnav) {
61
+ return null
62
+ }
63
+
64
+ // TODO: in the future routing props from settings like {match: "/docs/api/browser"}
65
+ const active = matchedSubnav?.items.findLast(item => location.pathname.startsWith(item.url || ""))
66
+
67
+ // TODO: value
68
+ return <SubNav
69
+ title={matchedSubnav?.name || ""}
70
+ value={active?.url || ""}
71
+ onChange={() => {
72
+ }}
73
+ >
74
+ {matchedSubnav?.items.map((item, index) => {
75
+ return <SubNav.Item value={item.url || ""} href={item.url}>
76
+ {item.name}
77
+ </SubNav.Item>
78
+ })}
79
+ </SubNav>
80
+
81
+ }
82
+
83
+ export interface FwSidebarGroupsProps {
84
+ onePathBehaviour?: boolean
85
+ clientSideRouting?: boolean
86
+ }
87
+
88
+ function recursiveSearch(items: FwSidebarItemProps[], href: string, levels: any[] = []) {
89
+ for (let i = 0; i < items.length; i++) {
90
+ const item = items[i]
91
+
92
+ if (item.href === href) {
93
+ return [...levels, i]
94
+ }
95
+
96
+ if (item.items) {
97
+ const result = recursiveSearch(item.items, href, [...levels, i])
98
+ if (result) {
99
+ return result
100
+ }
101
+ }
102
+ }
103
+ return null
104
+ }
105
+
106
+ function FwSidebarGroups(props: FwSidebarGroupsProps) {
107
+ const groups = useSidebarGroups()
108
+
109
+ const settings = useSettings()
110
+
111
+ const footerItems = settings.structure?.anchors?.bottom?.map(anchor => {
112
+ let icon
113
+
114
+ // TODO: refactor this !!!
115
+ if (typeof anchor.icon === "string") {
116
+ switch (anchor.icon) {
117
+ case "icon-cookbook": {
118
+ icon = <IconCookbook/>
119
+ break
120
+ }
121
+
122
+ case "icon-community": {
123
+ icon = <IconCommunity/>
124
+ break
125
+ }
126
+
127
+ case "icon-marketplace": {
128
+ icon = <IconMarketplace/>
129
+ break
130
+ }
131
+
132
+ case "icon-sdk": {
133
+ icon = <IconSDK/>
134
+ break
135
+ }
136
+
137
+ default: {
138
+ icon = null
139
+ }
140
+ }
141
+ } else {
142
+ icon = isValidElement(anchor.icon) ? anchor.icon : manualHydration(anchor.icon)
143
+ }
144
+
145
+ return <UISidebar.FooterItem href={anchor.url} icon={icon}>
146
+ {anchor.name}
147
+ </UISidebar.FooterItem>
148
+ })
149
+
150
+ const location = useLocation()
151
+ const initialActiveItems: any[] = []
152
+ groups.forEach(group => {
153
+ const activeLevels = recursiveSearch(group.items, location.pathname) || []
154
+
155
+ activeLevels.reduce((acc, index) => {
156
+ initialActiveItems.push(acc[index])
157
+ acc[index].active = true
158
+ return acc[index].items
159
+ }, group.items)
160
+
161
+ return group
162
+ })
163
+
164
+ return <FwSidebarGroupContext
165
+ onePathBehaviour={props.onePathBehaviour}
166
+ clientSideRouting={props.clientSideRouting}
167
+ initialActiveItems={initialActiveItems}
168
+ >
169
+ <UISidebar footerItems={footerItems && footerItems}>
170
+ {
171
+ groups?.map((group, index) => <FwSidebarItemGroup
172
+ key={index + group.group}
173
+ {...group}
174
+ />)
175
+ }
176
+ </UISidebar>
177
+ </FwSidebarGroupContext>
178
+ }
179
+
180
+ type FlatTOC = {
181
+ depth: number
182
+ value: string
183
+ }
184
+
185
+ function FwToc() {
186
+ const toc = useToC()
187
+
188
+ if (!toc) {
189
+ return null
190
+ }
191
+
192
+ const flatToc: FlatTOC[] = []
193
+
194
+ const flatten = (toc?: ITOC[]) => {
195
+ if (!toc) {
196
+ return
197
+ }
198
+
199
+ toc.forEach(item => {
200
+ flatToc.push({
201
+ depth: item.depth,
202
+ value: item.value
203
+ })
204
+
205
+ flatten(item.children)
206
+ })
207
+ }
208
+
209
+ flatten(toc)
210
+
211
+ // TODO: its temporary
212
+ const tocFinal = flatToc.filter(item => item.depth === 2)
213
+
214
+ const location = useLocation()
215
+
216
+ // TODO: better in the future
217
+ const defaultValue = location.hash ? location.hash.replace("#", "") : tocFinal[0]?.value
218
+
219
+ return <Toc defaultValue={defaultValue}>
220
+ {
221
+ tocFinal.map((item, index) => <Toc.Item
222
+ key={index + item.value + item.depth}
223
+ value={item.value}
224
+ >
225
+ {item.value}
226
+ </Toc.Item>)
227
+ }
228
+ </Toc>
229
+ }
230
+
231
+ // TODO: finish
232
+ function FwBreadcrumbs() {
233
+ const breadcrumbs = useBreadcrumbs()
234
+
235
+ return <Breadcrumbs
236
+ items={breadcrumbs || []}
237
+ />
238
+ }
239
+
240
+ // TDOO: finish
241
+ function FwNavLinks() {
242
+ const navlinks = useNavLinks()
243
+
244
+ // TODO: in the future - because of custom react frontmatter
245
+ if (typeof navlinks?.prev?.title !== "string" || typeof navlinks?.next?.title !== "string") {
246
+ return null
247
+ }
248
+
249
+ if (navlinks?.prev || navlinks?.next) {
250
+ return <NavLinks
251
+ prev={navlinks.prev}
252
+ next={navlinks.next}
253
+ />
254
+ }
255
+
256
+ return null
257
+ }
258
+
259
+ // TODO: issues with below svgs inside settigns - REFACTOR THIS
260
+ function IconCookbook() {
261
+ return <svg
262
+ xmlns="http://www.w3.org/2000/svg"
263
+ viewBox="0 0 24 24"
264
+ width="1em"
265
+ height="1em"
266
+ >
267
+ <path
268
+ fillRule="evenodd"
269
+ d="M14.447 7.106a1 1 0 0 1 .447 1.341l-4 8a1 1 0 1 1-1.788-.894l4-8a1 1 0 0 1 1.341-.447ZM6.6 7.2a1 1 0 0 1 .2 1.4L4.25 12l2.55 3.4a1 1 0 0 1-1.6 1.2l-3-4a1 1 0 0 1 0-1.2l3-4a1 1 0 0 1 1.4-.2Zm10.8 0a1 1 0 0 1 1.4.2l3 4a1 1 0 0 1 0 1.2l-3 4a1 1 0 0 1-1.6-1.2l2.55-3.4-2.55-3.4a1 1 0 0 1 .2-1.4Z"
270
+ clipRule="evenodd"
271
+ />
272
+ </svg>
273
+ }
274
+
275
+ function IconCommunity() {
276
+ return <svg
277
+ xmlns="http://www.w3.org/2000/svg"
278
+ fill="currentColor"
279
+ viewBox="0 0 24 24"
280
+ width="1em"
281
+ height="1em"
282
+ >
283
+ <path
284
+ fillRule="evenodd"
285
+ d="M10.5 8.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7ZM3 9.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm1-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Zm16 2a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0ZM8 18c0-.974.438-1.684 1.142-2.185C9.876 15.293 10.911 15 12 15c1.09 0 2.124.293 2.858.815.704.5 1.142 1.21 1.142 2.185a1 1 0 1 0 2 0c0-1.692-.812-2.982-1.983-3.815C14.876 13.373 13.411 13 12 13c-1.41 0-2.876.373-4.017 1.185C6.812 15.018 6 16.308 6 18a1 1 0 1 0 2 0Zm-3.016-3.675a1 1 0 0 1-.809 1.16C2.79 15.732 2 16.486 2 17.5a1 1 0 1 1-2 0c0-2.41 1.978-3.655 3.825-3.985a1 1 0 0 1 1.16.81Zm14.84 1.16a1 1 0 1 1 .351-1.97C22.022 13.845 24 15.09 24 17.5a1 1 0 1 1-2 0c0-1.014-.79-1.768-2.175-2.015Z"
286
+ clipRule="evenodd"
287
+ />
288
+ </svg>
289
+ }
290
+
291
+ // TODO: but this svg works on settings
292
+ function IconMarketplace() {
293
+ return <svg
294
+ xmlns="http://www.w3.org/2000/svg"
295
+ viewBox="0 0 24 24"
296
+ fill="none"
297
+ width="1em"
298
+ height="1em"
299
+ >
300
+ <path fill-rule="evenodd" clip-rule="evenodd"
301
+ d="M3.78163 3.28449C3.8768 2.96725 4.16879 2.75 4.5 2.75H19.5C19.8312 2.75 20.1232 2.96725 20.2184 3.28449L21.7184 8.28449C21.7393 8.3544 21.75 8.42701 21.75 8.5C21.75 10.5711 20.0711 12.25 18 12.25C16.7733 12.25 15.6842 11.661 15 10.7504C14.3158 11.661 13.2267 12.25 12 12.25C10.7733 12.25 9.68417 11.661 9 10.7504C8.31583 11.661 7.2267 12.25 6 12.25C3.92893 12.25 2.25 10.5711 2.25 8.5C2.25 8.42701 2.26066 8.3544 2.28163 8.28449L3.78163 3.28449ZM9.75 8.5C9.75 9.74264 10.7574 10.75 12 10.75C13.2426 10.75 14.25 9.74264 14.25 8.5C14.25 8.08579 14.5858 7.75 15 7.75C15.4142 7.75 15.75 8.08579 15.75 8.5C15.75 9.74264 16.7574 10.75 18 10.75C19.2083 10.75 20.1942 9.79754 20.2477 8.60244L18.942 4.25H5.05802L3.75229 8.60244C3.80584 9.79753 4.79169 10.75 6 10.75C7.24264 10.75 8.25 9.74264 8.25 8.5C8.25 8.08579 8.58579 7.75 9 7.75C9.41421 7.75 9.75 8.08579 9.75 8.5Z"
302
+ />
303
+ <path fill-rule="evenodd" clip-rule="evenodd"
304
+ d="M4 10.25C4.41421 10.25 4.75 10.5858 4.75 11V19.75H6.5C6.91421 19.75 7.25 20.0858 7.25 20.5C7.25 20.9142 6.91421 21.25 6.5 21.25H4C3.58579 21.25 3.25 20.9142 3.25 20.5V11C3.25 10.5858 3.58579 10.25 4 10.25ZM20 10.25C20.4142 10.25 20.75 10.5858 20.75 11V20.5C20.75 20.9142 20.4142 21.25 20 21.25H10.5C10.0858 21.25 9.75 20.9142 9.75 20.5C9.75 20.0858 10.0858 19.75 10.5 19.75H19.25V11C19.25 10.5858 19.5858 10.25 20 10.25Z"
305
+ />
306
+ <path
307
+ d="M12.003 19C11.31 18.9996 10.6384 18.7598 10.102 18.3213C9.56564 17.8829 9.19745 17.2726 9.05983 16.594C8.92222 15.9154 9.02364 15.2101 9.34693 14.5976C9.67022 13.9852 10.1955 13.5032 10.8337 13.2333C11.5673 12.9262 12.393 12.9221 13.1296 13.2222C13.8661 13.5222 14.4536 14.1018 14.7631 14.8338C15.0727 15.5659 15.0791 16.3907 14.7808 17.1274C14.4827 17.8642 13.9042 18.4527 13.1724 18.7641C12.8025 18.9205 12.4047 19.0007 12.003 19ZM11.1458 14.7215C11.1124 14.7215 11.0803 14.7348 11.0567 14.7584C11.0331 14.782 11.0198 14.8141 11.0198 14.8475V17.1923C11.0198 17.2258 11.0331 17.2578 11.0567 17.2814C11.0803 17.305 11.1124 17.3183 11.1458 17.3183C11.1671 17.3183 11.188 17.3128 11.2065 17.3024L13.3399 16.13C13.3597 16.1192 13.3761 16.1032 13.3876 16.0838C13.3991 16.0644 13.4052 16.0423 13.4052 16.0197C13.4052 15.9972 13.3991 15.9751 13.3876 15.9557C13.3761 15.9362 13.3597 15.9203 13.3399 15.9094L11.2063 14.7373C11.1879 14.727 11.1671 14.7215 11.1458 14.7215Z"
308
+ />
309
+ </svg>
310
+ }
311
+
312
+ function IconSDK() {
313
+ return <svg
314
+ viewBox="0 0 15 15"
315
+ xmlns="http://www.w3.org/2000/svg"
316
+ width="1em"
317
+ height="1em"
318
+ >
319
+ <path
320
+ d="M7.28856 0.796908C7.42258 0.734364 7.57742 0.734364 7.71144 0.796908L13.7114 3.59691C13.8875 3.67906 14 3.85574 14 4.05V10.95C14 11.1443 13.8875 11.3209 13.7114 11.4031L7.71144 14.2031C7.57742 14.2656 7.42258 14.2656 7.28856 14.2031L1.28856 11.4031C1.11252 11.3209 1 11.1443 1 10.95V4.05C1 3.85574 1.11252 3.67906 1.28856 3.59691L7.28856 0.796908ZM2 4.80578L7 6.93078V12.9649L2 10.6316V4.80578ZM8 12.9649L13 10.6316V4.80578L8 6.93078V12.9649ZM7.5 6.05672L12.2719 4.02866L7.5 1.80176L2.72809 4.02866L7.5 6.05672Z"
321
+ fill="currentColor"
322
+ fillRule="evenodd"
323
+ clipRule="evenodd"
324
+ />
325
+ </svg>
326
+ }
327
+
328
+ export {
329
+ FwNav,
330
+ FwSubNav,
331
+
332
+ FwBreadcrumbs,
333
+ FwToc,
334
+ FwNavLinks,
335
+
336
+ FwSidebarGroups,
337
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./sidebar-group"
2
+
3
+ export * from "./sidebar"
@@ -0,0 +1,240 @@
1
+ import React, {createContext, useContext, useEffect, useState} from "react";
2
+ import {useLocation, useNavigation, useNavigate} from "react-router";
3
+
4
+ import {FwSidebarGroupProps, FwSidebarItemProps} from "./sidebar";
5
+ import {UIContext} from "../../contexts/ui";
6
+ import {useSidebarGroups} from "../../contexts";
7
+
8
+
9
+ export interface FwGroupContext {
10
+ active: (item: FwSidebarItemProps) => [boolean, () => void],
11
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>, item: FwSidebarItemProps) => void,
12
+ }
13
+
14
+ type GroupBehaviour = (item: FwSidebarItemProps) => [boolean, () => void]
15
+
16
+ const groupContext = createContext<FwGroupContext>({
17
+ active: () => [false, () => {
18
+ }],
19
+ onClick: () => null // TODO: should be deprecated?
20
+ })
21
+
22
+ export function FwSidebarGroupContext({
23
+ children,
24
+ onePathBehaviour,
25
+ clientSideRouting,
26
+ initialActiveItems,
27
+ }:
28
+ {
29
+ children: React.ReactNode,
30
+ onePathBehaviour?: boolean,
31
+ clientSideRouting?: boolean // TODO: scrollRouting?,
32
+ initialActiveItems: any[]
33
+ }) {
34
+
35
+ let groupBehaviour: GroupBehaviour
36
+
37
+ if (onePathBehaviour) {
38
+ groupBehaviour = useOnePathBehaviour(initialActiveItems)
39
+ } else {
40
+ groupBehaviour = useDefaultBehaviour(initialActiveItems)
41
+ }
42
+ const location = useLocation()
43
+
44
+ const [href, setHref] = useState(location.pathname)
45
+
46
+ return <UIContext.Provider value={{
47
+ href: href,
48
+ setHref: (value) => {
49
+ setHref(value)
50
+ }
51
+ }}>
52
+ <groupContext.Provider value={{
53
+ active: groupBehaviour,
54
+ onClick: clientSideRouting ? (event, item) => {
55
+ setHref(item.href)
56
+ scrollToDataSlug(event, item)
57
+ // navigate(item.href)
58
+ } : undefined
59
+ }}>
60
+ {children}
61
+ </groupContext.Provider>
62
+ </UIContext.Provider>
63
+ }
64
+
65
+ export function useGroup() {
66
+ return useContext(groupContext)
67
+ }
68
+
69
+
70
+ // TODO: !!! better algorithm (JSON.stringify is not good) !!!!!
71
+ // TODO: !!!! use array structure instad! !!!
72
+
73
+ function getLastValue(set) {
74
+ let value;
75
+ for (value of set) ;
76
+ return value;
77
+ }
78
+
79
+ function stringify(item: FwSidebarItemProps) {
80
+ return JSON.stringify({
81
+ title: item.title,
82
+ href: item.href,
83
+ items: item.items?.map((item) => stringify(item)),
84
+ })
85
+ }
86
+
87
+ function recursiveSearch(items: FwSidebarItemProps[], href: string, levels: any[] = []) {
88
+ for (let i = 0; i < items.length; i++) {
89
+ const item = items[i]
90
+
91
+ if (item.href === href) {
92
+ return [...levels, i]
93
+ }
94
+
95
+ if (item.items) {
96
+ const result = recursiveSearch(item.items, href, [...levels, i])
97
+ if (result) {
98
+ return result
99
+ }
100
+ }
101
+ }
102
+ return null
103
+ }
104
+
105
+ function calcActive(groups: FwSidebarGroupProps[], url: any) {
106
+ const initialActiveItems: any[] = []
107
+
108
+ groups.forEach(group => {
109
+ const activeLevels = recursiveSearch(group.items, url) || []
110
+
111
+ activeLevels.reduce((acc, index) => {
112
+ initialActiveItems.push({
113
+ ...acc[index],
114
+ active: true
115
+ })
116
+ return acc[index].items
117
+ }, group.items)
118
+
119
+ return group
120
+ })
121
+
122
+ return initialActiveItems
123
+ }
124
+
125
+ function useDefaultBehaviour(initialActiveItems: any[]) {
126
+ const groups = useSidebarGroups()
127
+ const [weakSet] = useState(() => new Set<string>(initialActiveItems.map((item) => stringify(item))));
128
+ const [, setForceUpdate] = useState(0);
129
+
130
+ useEffect(() => {
131
+ window.addEventListener('xyd.history.pushState', (event: CustomEvent) => {
132
+ const url = event.detail?.url
133
+
134
+ if (!url) {
135
+ return
136
+ }
137
+ // TODO: better data structures
138
+ const active = calcActive(groups, url)
139
+ weakSet.clear()
140
+ active.forEach((item) => {
141
+ addItem(item)
142
+ })
143
+ });
144
+ }, [])
145
+
146
+ const forceUpdate = () => setForceUpdate((prev) => prev + 1);
147
+
148
+ const addItem = (item: FwSidebarItemProps) => {
149
+ weakSet.add(stringify(item));
150
+ forceUpdate();
151
+ };
152
+
153
+ const deleteItem = (item: FwSidebarItemProps) => {
154
+ weakSet.delete(stringify(item));
155
+ forceUpdate();
156
+ };
157
+
158
+ const hasItem = (item: FwSidebarItemProps) => {
159
+ return weakSet.has(stringify(item));
160
+ };
161
+
162
+ return (item: FwSidebarItemProps): [boolean, () => void] => [
163
+ hasItem(item) || false,
164
+ () => {
165
+ if (hasItem(item)) {
166
+ deleteItem(item);
167
+ } else {
168
+ addItem(item);
169
+ }
170
+ }
171
+ ]
172
+ }
173
+
174
+ function useOnePathBehaviour(initialActiveItems: any[]) {
175
+ const [weakSet] = useState(() => new Set<string>(initialActiveItems.map((item) => stringify(item))));
176
+ const [lastLevel, setLastLevel] = useState<false | number | undefined>(false);
177
+ const [, setForceUpdate] = useState(0);
178
+
179
+ const forceUpdate = () => setForceUpdate((prev) => prev + 1);
180
+
181
+ const addItem = (item: FwSidebarItemProps) => {
182
+ weakSet.add(stringify(item));
183
+ forceUpdate();
184
+ };
185
+
186
+ const deleteItem = (item: FwSidebarItemProps) => {
187
+ weakSet.delete(stringify(item));
188
+ forceUpdate();
189
+ };
190
+
191
+ const hasItem = (item: FwSidebarItemProps) => {
192
+ return weakSet.has(stringify(item));
193
+ };
194
+
195
+ const clear = () => {
196
+ weakSet.clear();
197
+ forceUpdate();
198
+ };
199
+
200
+ return (item: FwSidebarItemProps): [boolean, () => void] => [
201
+ hasItem(item),
202
+ () => {
203
+ setLastLevel(item.level)
204
+ if (hasItem(item) && item.level == 0) {
205
+ setLastLevel(false)
206
+ clear()
207
+ return
208
+ }
209
+
210
+ if (hasItem(item)) {
211
+ setLastLevel(false)
212
+ deleteItem(item);
213
+ return
214
+ }
215
+
216
+
217
+ if (((item.level || 0) > (lastLevel || 0)) || lastLevel == false) {
218
+ addItem(item)
219
+ } else {
220
+ const v = getLastValue(weakSet)
221
+ deleteItem(JSON.parse(v))
222
+ addItem(item)
223
+ }
224
+ }
225
+ ]
226
+ }
227
+
228
+ function scrollToDataSlug(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>, item: FwSidebarItemProps) {
229
+ event.preventDefault()
230
+
231
+ // TODO: find another solution because data-slug is added by 'atlas'
232
+
233
+ const dataSlug = document.querySelector(`[data-slug="${item.href}"]`)
234
+
235
+ if (dataSlug) {
236
+ dataSlug.scrollIntoView({block: "start", inline: "nearest"})
237
+ }
238
+ }
239
+
240
+
@@ -0,0 +1,127 @@
1
+ import React, {} from "react"
2
+
3
+ import {Badge} from "@xyd-js/components/writer"
4
+ import {UISidebar} from "@xyd-js/ui";
5
+
6
+ import {useGroup} from "./sidebar-group";
7
+
8
+ // TODO: custom hooks for active onclick handler etc?
9
+
10
+ export interface FwSidebarGroupProps {
11
+ group: string
12
+
13
+ items: FwSidebarItemProps[]
14
+ }
15
+
16
+ export function FwSidebarItemGroup(props: FwSidebarGroupProps) {
17
+ return <>
18
+ <UISidebar.ItemHeader>
19
+ {props.group}
20
+ </UISidebar.ItemHeader>
21
+
22
+ {props.items.map((item, index) => <FwSidebarItem
23
+ key={index + item.href}
24
+ title={item.title}
25
+ href={item.href}
26
+ items={item.items}
27
+ active={item.active}
28
+ level={0}
29
+ />)}
30
+ </>
31
+ }
32
+
33
+ export interface FwSidebarItemProps {
34
+ title: string | {
35
+ code: string
36
+ }
37
+
38
+ href: string
39
+
40
+ items?: FwSidebarItemProps[]
41
+
42
+ active?: boolean
43
+
44
+ // internal
45
+ readonly level?: number
46
+ // internal
47
+ }
48
+
49
+ // TODO: move to @xyd-js/components/content
50
+ const components = {
51
+ Frontmatter: {
52
+ // TODO: css
53
+ Title: ({children}) => <div style={{
54
+ display: "flex",
55
+ alignItems: "center",
56
+ justifyContent: "space-between",
57
+ width: "100%",
58
+ gap: "10px",
59
+ }}>
60
+ {children}
61
+ </div>,
62
+ },
63
+ Badge: ({children}) => <Badge>
64
+ {children}
65
+ </Badge>
66
+ }
67
+
68
+ function mdxExport(code: string, components: any) {
69
+ const scope = {
70
+ Fragment: React.Fragment,
71
+ jsxs: React.createElement,
72
+ jsx: React.createElement,
73
+ jsxDEV: React.createElement,
74
+ _jsxs: React.createElement,
75
+ _jsx: React.createElement,
76
+ ...components
77
+ }
78
+ const fn = new Function(...Object.keys(scope), `return ${code}`)
79
+ return fn(...Object.values(scope))
80
+ }
81
+
82
+ function FwSidebarItem(props: FwSidebarItemProps) {
83
+ const {active, onClick} = useGroup()
84
+ const [isActive, setActive] = active(props)
85
+
86
+ let Title: any
87
+
88
+ if (typeof props.title === "object" && "code" in props.title) {
89
+ const code = props.title.code
90
+
91
+ Title = () => mdxExport(
92
+ // TODO: in the future better mechanism + support props + better components (provider?) - similar to codehik
93
+ code.replace("() => ", ""),
94
+ components
95
+ )
96
+ } else {
97
+ Title = () => props.title
98
+ }
99
+
100
+ return <UISidebar.Item
101
+ button={!!props.items?.length}
102
+ href={props.href}
103
+ active={isActive}
104
+ onClick={() => {
105
+ setActive()
106
+ }}
107
+ >
108
+ <Title/>
109
+
110
+ {
111
+ props.items?.length && <UISidebar.SubTree isOpen={isActive}>
112
+ <>
113
+ {
114
+ props.items?.map((item, index) => <FwSidebarItem
115
+ key={index + item.href}
116
+ title={item.title}
117
+ href={item.href}
118
+ items={item.items}
119
+ active={active(item)[0]}
120
+ level={(props.level || 0) + 1}
121
+ />)
122
+ }
123
+ </>
124
+ </UISidebar.SubTree>
125
+ }
126
+ </UISidebar.Item>
127
+ }