@xyd-js/ui 0.1.0-xyd.0
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.
- package/README.md +3 -0
- package/index.ts +3 -0
- package/package.json +29 -0
- package/rollup.config.js +79 -0
- package/src/components/Anchor/Anchor.styles.ts +7 -0
- package/src/components/Anchor/Anchor.tsx +58 -0
- package/src/components/Anchor/index.ts +7 -0
- package/src/components/Nav/Nav.styles.tsx +87 -0
- package/src/components/Nav/Nav.tsx +56 -0
- package/src/components/Nav/index.ts +8 -0
- package/src/components/Sidebar/Collapse.styles.tsx +24 -0
- package/src/components/Sidebar/Collapse.tsx +84 -0
- package/src/components/Sidebar/Sidebar.styles.tsx +117 -0
- package/src/components/Sidebar/Sidebar.tsx +121 -0
- package/src/components/Sidebar/index.ts +11 -0
- package/src/components/SubNav/SubNav.styles.tsx +81 -0
- package/src/components/SubNav/SubNav.tsx +42 -0
- package/src/components/SubNav/index.ts +7 -0
- package/src/components/Toc/Toc.styles.tsx +56 -0
- package/src/components/Toc/Toc.tsx +153 -0
- package/src/components/Toc/index.ts +1 -0
- package/src/components/index.ts +13 -0
- package/src/types/index.ts +23 -0
- package/tsconfig.json +36 -0
package/README.md
ADDED
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xyd-js/ui",
|
|
3
|
+
"version": "0.1.0-xyd.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
"./index.css": "./dist/index.css",
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"react": "^18.3.1",
|
|
17
|
+
"scroll-into-view-if-needed": "^3.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"autoprefixer": "^10.4.20",
|
|
21
|
+
"postcss": "^8.4.47",
|
|
22
|
+
"postcss-import": "^16.1.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "rimraf build",
|
|
26
|
+
"prebuild": "pnpm clean",
|
|
27
|
+
"build": "rollup -c rollup.config.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {fileURLToPath} from 'url';
|
|
2
|
+
import {dirname} from 'path';
|
|
3
|
+
|
|
4
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
5
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
6
|
+
import typescript from '@rollup/plugin-typescript';
|
|
7
|
+
import dts from 'rollup-plugin-dts';
|
|
8
|
+
import {terser} from 'rollup-plugin-terser';
|
|
9
|
+
import babel from '@rollup/plugin-babel';
|
|
10
|
+
import postcss from 'rollup-plugin-postcss';
|
|
11
|
+
import wyw from '@wyw-in-js/rollup';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
import {createRequire} from 'module';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const {dependencies} = require('./package.json', {assert: {type: 'json'}});
|
|
20
|
+
|
|
21
|
+
const external = Object.keys(dependencies);
|
|
22
|
+
|
|
23
|
+
export default [
|
|
24
|
+
{
|
|
25
|
+
input: {
|
|
26
|
+
index: 'index.ts'
|
|
27
|
+
},
|
|
28
|
+
output: [
|
|
29
|
+
{
|
|
30
|
+
dir: 'dist',
|
|
31
|
+
format: 'esm',
|
|
32
|
+
sourcemap: false,
|
|
33
|
+
entryFileNames: '[name].js'
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
plugins: [
|
|
37
|
+
wyw({
|
|
38
|
+
include: ['**/*.{ts,tsx}'],
|
|
39
|
+
babelOptions: {
|
|
40
|
+
presets: [
|
|
41
|
+
'@babel/preset-typescript',
|
|
42
|
+
'@babel/preset-react'
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
postcss({
|
|
47
|
+
extract: true,
|
|
48
|
+
plugins: [
|
|
49
|
+
require('postcss-import'),
|
|
50
|
+
require('autoprefixer')
|
|
51
|
+
]
|
|
52
|
+
}),
|
|
53
|
+
resolve(),
|
|
54
|
+
commonjs(),
|
|
55
|
+
typescript({
|
|
56
|
+
tsconfig: './tsconfig.json',
|
|
57
|
+
}),
|
|
58
|
+
babel({
|
|
59
|
+
babelHelpers: 'bundled',
|
|
60
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
61
|
+
presets: [
|
|
62
|
+
'@babel/preset-env',
|
|
63
|
+
'@babel/preset-react'
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
terser(),
|
|
67
|
+
],
|
|
68
|
+
external
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
input: 'index.ts',
|
|
72
|
+
output: {
|
|
73
|
+
file: 'dist/index.d.ts',
|
|
74
|
+
format: 'es',
|
|
75
|
+
},
|
|
76
|
+
plugins: [dts()],
|
|
77
|
+
external
|
|
78
|
+
}
|
|
79
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, {forwardRef} from 'react'
|
|
2
|
+
import type {ComponentProps, ReactElement} from 'react'
|
|
3
|
+
// import {Link} from "react-router";
|
|
4
|
+
|
|
5
|
+
import {$anchor} from "./Anchor.styles";
|
|
6
|
+
|
|
7
|
+
export type UIAnchorProps = Omit<ComponentProps<'a'>, 'ref'> & {
|
|
8
|
+
newWindow?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Link(props: any) {
|
|
12
|
+
return <div>Link</div>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const UIAnchor = forwardRef<HTMLAnchorElement, UIAnchorProps>(function (
|
|
16
|
+
{href = '', children, newWindow},
|
|
17
|
+
// ref is used in <NavbarMenu />
|
|
18
|
+
forwardedRef
|
|
19
|
+
): ReactElement {
|
|
20
|
+
if (newWindow) {
|
|
21
|
+
return (
|
|
22
|
+
<Link
|
|
23
|
+
ref={forwardedRef}
|
|
24
|
+
to={href}
|
|
25
|
+
target="_blank"
|
|
26
|
+
rel="noreferrer"
|
|
27
|
+
className={$anchor.host}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
<span> (opens in a new tab)</span>
|
|
31
|
+
</Link>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!href) {
|
|
36
|
+
return (
|
|
37
|
+
<Link
|
|
38
|
+
ref={forwardedRef}
|
|
39
|
+
to={href}
|
|
40
|
+
className={$anchor.host}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</Link>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Link
|
|
49
|
+
ref={forwardedRef}
|
|
50
|
+
to={href}
|
|
51
|
+
className={$anchor.host}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</Link>
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
UIAnchor.displayName = 'UIAnchor'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
|
|
3
|
+
export const $nav = {
|
|
4
|
+
host: css`
|
|
5
|
+
position: sticky;
|
|
6
|
+
top: 0;
|
|
7
|
+
z-index: 20;
|
|
8
|
+
width: 100%;
|
|
9
|
+
background: transparent;
|
|
10
|
+
display: flex;
|
|
11
|
+
`,
|
|
12
|
+
shadow: css`
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
position: absolute;
|
|
15
|
+
z-index: -1;
|
|
16
|
+
height: 100%;
|
|
17
|
+
width: 100%;
|
|
18
|
+
background-color: white;
|
|
19
|
+
`,
|
|
20
|
+
nav: css`
|
|
21
|
+
display: flex;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: var(--xyd-navbar-height);
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: flex-end;
|
|
26
|
+
gap: 8px;
|
|
27
|
+
padding-left: calc(max(env(safe-area-inset-left), 16px));
|
|
28
|
+
padding-right: calc(max(env(safe-area-inset-right), 16px));
|
|
29
|
+
`,
|
|
30
|
+
nav$$middle: css`
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
33
|
+
align-items: center;
|
|
34
|
+
`
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const $list = {
|
|
38
|
+
host: css`
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
`,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const $item = {
|
|
47
|
+
host: css`
|
|
48
|
+
font-size: 14px; /* 0.875rem */
|
|
49
|
+
position: relative;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
color: #4b5563; /* Gray-600 */
|
|
52
|
+
padding: 6px 16px;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
|
|
57
|
+
&:hover {
|
|
58
|
+
color: #1f2937; /* Gray-800 */
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&[data-state="active"] {
|
|
62
|
+
font-weight: bold;
|
|
63
|
+
background: #f9f9f9;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
title1: css`
|
|
68
|
+
position: absolute;
|
|
69
|
+
inset: 0;
|
|
70
|
+
text-align: center;
|
|
71
|
+
align-items: center;
|
|
72
|
+
display: flex;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
`,
|
|
75
|
+
title2: css`
|
|
76
|
+
visibility: hidden;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const $logo = {
|
|
82
|
+
host: css`
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
margin-right: auto;
|
|
86
|
+
`
|
|
87
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as RadixTabs from "@radix-ui/react-tabs";
|
|
3
|
+
|
|
4
|
+
import {$nav, $list, $item, $logo} from "./Nav.styles";
|
|
5
|
+
|
|
6
|
+
export interface NavProps {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
value: string
|
|
9
|
+
onChange: (value: string) => void
|
|
10
|
+
logo?: React.ReactNode;
|
|
11
|
+
kind?: "middle"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Nav({children, value, onChange, logo, kind}: NavProps) {
|
|
15
|
+
return <RadixTabs.Root asChild value={value} onValueChange={onChange}>
|
|
16
|
+
<div className={`${$nav.host}`}>
|
|
17
|
+
<div className={$nav.shadow}/>
|
|
18
|
+
<nav className={`
|
|
19
|
+
${$nav.nav}
|
|
20
|
+
${kind === "middle" && $nav.nav$$middle}
|
|
21
|
+
`}>
|
|
22
|
+
<div className={`
|
|
23
|
+
${$logo.host}
|
|
24
|
+
xyd_ui-comp-nav-logo
|
|
25
|
+
`}>
|
|
26
|
+
{logo}
|
|
27
|
+
</div>
|
|
28
|
+
<RadixTabs.List asChild>
|
|
29
|
+
<div className={$list.host}>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
</RadixTabs.List>
|
|
33
|
+
{kind === "middle" && <div/>}
|
|
34
|
+
</nav>
|
|
35
|
+
</div>
|
|
36
|
+
</RadixTabs.Root>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface NavItemProps {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
href: string;
|
|
42
|
+
value: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Nav.Item = function NavItem({children, value, href}) {
|
|
46
|
+
return <RadixTabs.Trigger asChild value={value}>
|
|
47
|
+
<a
|
|
48
|
+
href={href}
|
|
49
|
+
className={`${$item.host}`}
|
|
50
|
+
>
|
|
51
|
+
<span className={$item.title1}>{children}</span>
|
|
52
|
+
<span className={$item.title2}>{children}</span>
|
|
53
|
+
</a>
|
|
54
|
+
</RadixTabs.Trigger>
|
|
55
|
+
};
|
|
56
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { css } from "@linaria/core";
|
|
2
|
+
|
|
3
|
+
export const $collapse = {
|
|
4
|
+
container: css`
|
|
5
|
+
transform: translateZ(0);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
transition: all 300ms ease-in-out;
|
|
8
|
+
|
|
9
|
+
//@media (prefers-reduced-motion: reduce) {
|
|
10
|
+
// transition: none;
|
|
11
|
+
//}
|
|
12
|
+
`,
|
|
13
|
+
base: css`
|
|
14
|
+
opacity: 0;
|
|
15
|
+
transition: opacity 500ms ease-in-out;
|
|
16
|
+
|
|
17
|
+
//@media (prefers-reduced-motion: reduce) {
|
|
18
|
+
// transition: none;
|
|
19
|
+
//}
|
|
20
|
+
`,
|
|
21
|
+
open: css`
|
|
22
|
+
opacity: 1;
|
|
23
|
+
`,
|
|
24
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from "react";
|
|
2
|
+
import type {ReactElement, ReactNode} from "react";
|
|
3
|
+
import {$collapse} from "./Collapse.styles";
|
|
4
|
+
|
|
5
|
+
export interface UICollapseProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
horizontal?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function UICollapse({
|
|
12
|
+
children,
|
|
13
|
+
isOpen,
|
|
14
|
+
horizontal = false,
|
|
15
|
+
}: UICollapseProps): ReactElement {
|
|
16
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const animationRef = useRef<number | null>(null);
|
|
19
|
+
const initialOpen = useRef(isOpen);
|
|
20
|
+
const initialRender = useRef(true);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const container = containerRef.current;
|
|
24
|
+
const inner = innerRef.current;
|
|
25
|
+
|
|
26
|
+
if (animationRef.current) {
|
|
27
|
+
clearTimeout(animationRef.current);
|
|
28
|
+
}
|
|
29
|
+
if (initialRender.current || !container || !inner) return;
|
|
30
|
+
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
// Opening animation
|
|
33
|
+
if (horizontal) {
|
|
34
|
+
inner.style.width = `${inner.scrollWidth}px`;
|
|
35
|
+
container.style.width = `${inner.scrollWidth}px`;
|
|
36
|
+
} else {
|
|
37
|
+
inner.style.height = `${inner.scrollHeight}px`;
|
|
38
|
+
container.style.height = `${inner.scrollHeight}px`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
animationRef.current = window.setTimeout(() => {
|
|
42
|
+
container.style.removeProperty(horizontal ? "width" : "height");
|
|
43
|
+
}, 300);
|
|
44
|
+
} else {
|
|
45
|
+
// Closing animation
|
|
46
|
+
if (horizontal) {
|
|
47
|
+
const width = container.scrollWidth; // Cache current width
|
|
48
|
+
container.style.width = `${width}px`; // Set to fixed width first
|
|
49
|
+
|
|
50
|
+
// Force reflow for Firefox
|
|
51
|
+
container.offsetWidth;
|
|
52
|
+
|
|
53
|
+
container.style.width = "0px";
|
|
54
|
+
} else {
|
|
55
|
+
const height = container.scrollHeight; // Cache current height
|
|
56
|
+
container.style.height = `${height}px`; // Set to fixed height first
|
|
57
|
+
|
|
58
|
+
// Force reflow for Firefox
|
|
59
|
+
container.offsetHeight;
|
|
60
|
+
|
|
61
|
+
container.style.height = "0px";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [horizontal, isOpen]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
initialRender.current = false;
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
ref={containerRef}
|
|
73
|
+
className={`${$collapse.container}`}
|
|
74
|
+
style={initialOpen.current || horizontal ? undefined : {height: 0}}
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
ref={innerRef}
|
|
78
|
+
className={`${$collapse.base} ${isOpen ? $collapse.open : ""}`}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
|
|
3
|
+
export const $sidebar = {
|
|
4
|
+
host: css`
|
|
5
|
+
background: #f9f9f9;
|
|
6
|
+
height: 100%;
|
|
7
|
+
border-radius: 0.5rem;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
`,
|
|
11
|
+
ul: css`
|
|
12
|
+
overflow-y: auto;
|
|
13
|
+
overflow-x: hidden;
|
|
14
|
+
height: 100%;
|
|
15
|
+
padding: 1rem;
|
|
16
|
+
|
|
17
|
+
// TODO: get height of top
|
|
18
|
+
//height: calc(100vh - 54px);
|
|
19
|
+
`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const $footer = {
|
|
23
|
+
host: css`
|
|
24
|
+
padding: 1rem;
|
|
25
|
+
//box-shadow: 0 -2px 10px rgba(0, 0, 0, .06);
|
|
26
|
+
box-shadow: 0 -2px 10px rgba(237, 237, 237, .1);
|
|
27
|
+
//border: 1px solid rgb(227, 227, 235);
|
|
28
|
+
//border-top: 1px solid rgb(227, 227, 235);
|
|
29
|
+
border-top: 1px solid #ededed;
|
|
30
|
+
`,
|
|
31
|
+
item$host: css`
|
|
32
|
+
display: flex;
|
|
33
|
+
width: 100%;
|
|
34
|
+
padding: 2px;
|
|
35
|
+
color: #6e6e80;
|
|
36
|
+
`,
|
|
37
|
+
item: css`
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
width: 100%;
|
|
41
|
+
gap: 7px;
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
padding: 4px 8px;
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
background: #ececf1;
|
|
47
|
+
color: #111827;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
|
|
50
|
+
svg {
|
|
51
|
+
fill: #111827;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
svg {
|
|
56
|
+
fill: #6e6e80;
|
|
57
|
+
font-size: 18px;
|
|
58
|
+
width: 18px;
|
|
59
|
+
height: 18px;
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const $item = {
|
|
65
|
+
host: css`
|
|
66
|
+
color: #6e6e80;
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
`,
|
|
69
|
+
link: css`
|
|
70
|
+
display: flex;
|
|
71
|
+
width: 100%;
|
|
72
|
+
padding: 2px;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
`,
|
|
75
|
+
link$$active: css`
|
|
76
|
+
background: #fff;
|
|
77
|
+
color: #7051d4;
|
|
78
|
+
border-radius: 4px;
|
|
79
|
+
`,
|
|
80
|
+
link$$activeSecondary: css`
|
|
81
|
+
background: unset;
|
|
82
|
+
color: #111827;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
`,
|
|
85
|
+
link$item: css`
|
|
86
|
+
display: flex;
|
|
87
|
+
width: 100%;
|
|
88
|
+
padding: 4px 8px;
|
|
89
|
+
|
|
90
|
+
&:hover {
|
|
91
|
+
background: #ececf1;
|
|
92
|
+
color: #111827;
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const $tree = {
|
|
99
|
+
host: css`
|
|
100
|
+
margin-left: 8px;
|
|
101
|
+
`,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const $itemHeader = {
|
|
105
|
+
host: css`
|
|
106
|
+
// TODO: calc based on items?
|
|
107
|
+
padding-left: 10px;
|
|
108
|
+
margin-bottom: 6px;
|
|
109
|
+
margin-top: 16px;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
line-height: 16px;
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
letter-spacing: 1px;
|
|
114
|
+
color: #111827;
|
|
115
|
+
`
|
|
116
|
+
}
|
|
117
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
import {$sidebar, $footer, $item, $tree, $itemHeader} from "./Sidebar.styles";
|
|
4
|
+
import {UICollapse} from "./Collapse";
|
|
5
|
+
|
|
6
|
+
export interface UISidebarProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
footerItems?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function UISidebar({children, footerItems}: UISidebarProps) {
|
|
12
|
+
// TODO: in the future theming api?
|
|
13
|
+
return <div className={`
|
|
14
|
+
${$sidebar.host}
|
|
15
|
+
xyd_ui-comp-sidebar
|
|
16
|
+
`}>
|
|
17
|
+
<ul className={$sidebar.ul}>
|
|
18
|
+
{children}
|
|
19
|
+
</ul>
|
|
20
|
+
{footerItems && <SidebarFooter>
|
|
21
|
+
{footerItems}
|
|
22
|
+
</SidebarFooter>}
|
|
23
|
+
</div>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UISidebarItemProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
button?: boolean;
|
|
29
|
+
href?: string;
|
|
30
|
+
active?: boolean;
|
|
31
|
+
activeTheme?: "secondary";
|
|
32
|
+
onClick?: (v: any) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// TODO: move to ui
|
|
36
|
+
function Link({children, ...props}) {
|
|
37
|
+
return <a {...props}>
|
|
38
|
+
{children}
|
|
39
|
+
</a>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
UISidebar.Item = function SidebarItem({
|
|
43
|
+
children,
|
|
44
|
+
button,
|
|
45
|
+
href,
|
|
46
|
+
active,
|
|
47
|
+
activeTheme,
|
|
48
|
+
onClick
|
|
49
|
+
}: UISidebarItemProps) {
|
|
50
|
+
const [firstChild, ...restChilds] = React.Children.toArray(children)
|
|
51
|
+
|
|
52
|
+
const ButtonOrAnchor = button ? 'button' : Link
|
|
53
|
+
|
|
54
|
+
return <li
|
|
55
|
+
className={$item.host}
|
|
56
|
+
>
|
|
57
|
+
<ButtonOrAnchor
|
|
58
|
+
href={button ? undefined : href}
|
|
59
|
+
onClick={button ? onClick : undefined}
|
|
60
|
+
className={`
|
|
61
|
+
${$item.link}
|
|
62
|
+
`}
|
|
63
|
+
>
|
|
64
|
+
<div className={`
|
|
65
|
+
${$item.link$item}
|
|
66
|
+
${active && $item.link$$active}
|
|
67
|
+
${active && activeTheme === "secondary" && $item.link$$activeSecondary}
|
|
68
|
+
`}>
|
|
69
|
+
{firstChild}
|
|
70
|
+
</div>
|
|
71
|
+
</ButtonOrAnchor>
|
|
72
|
+
{restChilds}
|
|
73
|
+
</li>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface UISidebarItemHeaderProps {
|
|
77
|
+
children: React.ReactNode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
UISidebar.ItemHeader = function SidebarItemHeader({children}: UISidebarItemHeaderProps) {
|
|
81
|
+
return <li className={$itemHeader.host}>
|
|
82
|
+
{children}
|
|
83
|
+
</li>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface UISidebarSubTreeProps {
|
|
87
|
+
children: React.ReactNode;
|
|
88
|
+
isOpen?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
UISidebar.SubTree = function SidebarSubItem({children, isOpen}: UISidebarSubTreeProps) {
|
|
92
|
+
return <ul className={$tree.host}>
|
|
93
|
+
<UICollapse isOpen={isOpen || false}>
|
|
94
|
+
{children}
|
|
95
|
+
</UICollapse>
|
|
96
|
+
</ul>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function SidebarFooter({children}: { children: React.ReactNode }) {
|
|
100
|
+
return <div className={$footer.host}>
|
|
101
|
+
<ul>
|
|
102
|
+
{children}
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SidebarFooterItemProps {
|
|
108
|
+
children: React.ReactNode;
|
|
109
|
+
href?: string;
|
|
110
|
+
icon?: React.ReactNode;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
UISidebar.FooterItem = function SidebarFooter({children, href, icon}: SidebarFooterItemProps) {
|
|
114
|
+
return <li className={$footer.item$host}>
|
|
115
|
+
<a className={$footer.item} href={href}>
|
|
116
|
+
{icon}
|
|
117
|
+
{children}
|
|
118
|
+
</a>
|
|
119
|
+
</li>
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
|
|
3
|
+
export const $subNav = {
|
|
4
|
+
host: css`
|
|
5
|
+
align-items: center;
|
|
6
|
+
background-color: #f6f6f7;
|
|
7
|
+
border-radius: 0.50rem;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 44px;
|
|
13
|
+
margin-top: 3px;
|
|
14
|
+
padding: 0 0.25rem;
|
|
15
|
+
`,
|
|
16
|
+
prefix: css`
|
|
17
|
+
color: #44474a;
|
|
18
|
+
//font: var(--font-sans-font-nav-category-base);
|
|
19
|
+
font-size: 12px;
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
padding-left: 0.50rem;
|
|
22
|
+
padding-right: 1.50rem;
|
|
23
|
+
position: relative;
|
|
24
|
+
text-transform: uppercase;
|
|
25
|
+
|
|
26
|
+
&:after {
|
|
27
|
+
background: #d2d5d8;
|
|
28
|
+
border-radius: 1px;
|
|
29
|
+
content: " ";
|
|
30
|
+
height: 0.75rem;
|
|
31
|
+
position: absolute;
|
|
32
|
+
right: 0.50rem;
|
|
33
|
+
top: 50%;
|
|
34
|
+
transform: translateY(-50%);
|
|
35
|
+
width: 2px;
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
ul: css`
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
height: 100%;
|
|
42
|
+
`,
|
|
43
|
+
li: css`
|
|
44
|
+
display: flex;
|
|
45
|
+
height: 100%;
|
|
46
|
+
|
|
47
|
+
align-items: center;
|
|
48
|
+
position: relative;
|
|
49
|
+
|
|
50
|
+
&[data-state="active"] {
|
|
51
|
+
font-weight: 500;
|
|
52
|
+
|
|
53
|
+
a {
|
|
54
|
+
color: #202223;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
a:after {
|
|
58
|
+
background-color: #7051d4;
|
|
59
|
+
border-radius: 1px;
|
|
60
|
+
bottom: 0;
|
|
61
|
+
content: " ";
|
|
62
|
+
height: 2px;
|
|
63
|
+
left: 0;
|
|
64
|
+
position: absolute;
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
link: css`
|
|
70
|
+
color: #4b5563;
|
|
71
|
+
//font: var(--font-sans-font-nav-item-active-base);
|
|
72
|
+
line-height: 2.75rem;
|
|
73
|
+
display: block;
|
|
74
|
+
height: 100%;
|
|
75
|
+
padding: 0 0.50rem;
|
|
76
|
+
|
|
77
|
+
&:hover {
|
|
78
|
+
color: #202223;
|
|
79
|
+
}
|
|
80
|
+
`
|
|
81
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import * as RadixTabs from "@radix-ui/react-tabs";
|
|
3
|
+
|
|
4
|
+
import {$subNav} from "./SubNav.styles";
|
|
5
|
+
|
|
6
|
+
export interface SubNavProps {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
title: string
|
|
9
|
+
value: string
|
|
10
|
+
onChange: (value: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SubNav({children, title, value, onChange}: SubNavProps) {
|
|
14
|
+
return <RadixTabs.Root asChild value={value} onValueChange={onChange}>
|
|
15
|
+
<nav className={$subNav.host}>
|
|
16
|
+
<div className={$subNav.prefix}>
|
|
17
|
+
{title}
|
|
18
|
+
</div>
|
|
19
|
+
<RadixTabs.List asChild>
|
|
20
|
+
<ul className={$subNav.ul}>
|
|
21
|
+
{children}
|
|
22
|
+
</ul>
|
|
23
|
+
</RadixTabs.List>
|
|
24
|
+
</nav>
|
|
25
|
+
</RadixTabs.Root>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SubNavItemProps {
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
value: string
|
|
31
|
+
href?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
SubNav.Item = function SubNavItem({children, value, href}: SubNavItemProps) {
|
|
35
|
+
return <RadixTabs.Trigger asChild value={value}>
|
|
36
|
+
<li className={$subNav.li}>
|
|
37
|
+
<a href={href} className={`${$subNav.link}`}>
|
|
38
|
+
{children}
|
|
39
|
+
</a>
|
|
40
|
+
</li>
|
|
41
|
+
</RadixTabs.Trigger>
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {css} from '@linaria/core';
|
|
2
|
+
|
|
3
|
+
export const $toc = {
|
|
4
|
+
host: css`
|
|
5
|
+
position: relative;
|
|
6
|
+
padding-left: 16px;
|
|
7
|
+
`,
|
|
8
|
+
ul: css`
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
list-style: none;
|
|
12
|
+
`,
|
|
13
|
+
li: css`
|
|
14
|
+
position: relative;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
|
|
17
|
+
margin: 0 0 10px;
|
|
18
|
+
padding: 0;
|
|
19
|
+
`,
|
|
20
|
+
link: css`
|
|
21
|
+
display: inline-block;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
color: #6e6e80;
|
|
24
|
+
line-height: 1.4;
|
|
25
|
+
text-wrap: pretty;
|
|
26
|
+
transition: color .15s ease;
|
|
27
|
+
`,
|
|
28
|
+
link$$active: css`
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
color: #353740;
|
|
31
|
+
`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cubizEnter = 'cubic-bezier(.19, 1, .22, 1)';
|
|
35
|
+
|
|
36
|
+
export const $scroller = {
|
|
37
|
+
host: css`
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
bottom: 0;
|
|
41
|
+
left: 0;
|
|
42
|
+
width: 2px;
|
|
43
|
+
|
|
44
|
+
background-color: #ececf1;
|
|
45
|
+
`,
|
|
46
|
+
scroll: css`
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: 0;
|
|
49
|
+
left: 0;
|
|
50
|
+
width: 2px;
|
|
51
|
+
height: var(--active-track-height); // TODO: this must be dynamic
|
|
52
|
+
transform: translateY(var(--active-track-top)); // TODO: this must be dynamic
|
|
53
|
+
background-color: #353740;
|
|
54
|
+
transition: height .4s ${cubizEnter}, transform .4s ${cubizEnter};
|
|
55
|
+
`
|
|
56
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, {useState, useEffect, useContext} from "react"
|
|
2
|
+
import {Link} from "react-router";
|
|
3
|
+
|
|
4
|
+
import {$toc, $scroller} from "./Toc.styles";
|
|
5
|
+
|
|
6
|
+
export interface TocProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
defaultValue?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Context = React.createContext({
|
|
12
|
+
value: "",
|
|
13
|
+
onChange: (v: string) => {
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// TODO: based on scroller?
|
|
18
|
+
export function Toc({children, defaultValue}: TocProps) {
|
|
19
|
+
const [activeTrackHeight, setActiveTrackHeight] = useState(0)
|
|
20
|
+
const [activeTrackTop, setActiveTrackTop] = useState(0)
|
|
21
|
+
|
|
22
|
+
const [value, setValue] = useState(defaultValue || "")
|
|
23
|
+
|
|
24
|
+
// TODO: more reactish implt?
|
|
25
|
+
function handleScroll() {
|
|
26
|
+
const activeElement = document.querySelector(`.${$toc.link$$active}`);
|
|
27
|
+
if (!activeElement) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {offsetHeight} = activeElement as HTMLElement;
|
|
32
|
+
setActiveTrackHeight(offsetHeight);
|
|
33
|
+
|
|
34
|
+
if (!activeElement?.parentElement) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {offsetTop} = activeElement.parentElement as HTMLElement;
|
|
39
|
+
setActiveTrackTop(offsetTop);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onChange(v: string) {
|
|
43
|
+
setValue(v)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// TODO: more reactish
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const observer = new IntersectionObserver(
|
|
50
|
+
(entries) => {
|
|
51
|
+
let set = false
|
|
52
|
+
entries.forEach(entry => {
|
|
53
|
+
if (set) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (!entry.isIntersecting) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (entry.target instanceof HTMLHeadingElement) {
|
|
61
|
+
const rect = entry.target.getBoundingClientRect();
|
|
62
|
+
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
63
|
+
|
|
64
|
+
if (isVisible) {
|
|
65
|
+
set = true
|
|
66
|
+
setValue(entry.target.innerText);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
{threshold: 0.3}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
document.querySelectorAll("h2").forEach(ref => {
|
|
75
|
+
if (ref) observer.observe(ref);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
observer.disconnect();
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
handleScroll(); // Initial call to set the values
|
|
85
|
+
}, [value]);
|
|
86
|
+
|
|
87
|
+
return <Context.Provider value={{
|
|
88
|
+
value: value,
|
|
89
|
+
onChange
|
|
90
|
+
}}>
|
|
91
|
+
<div className={$toc.host}>
|
|
92
|
+
<div className={$scroller.host}>
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
// @ts-ignore
|
|
96
|
+
"--active-track-height": `${activeTrackHeight}px`,
|
|
97
|
+
"--active-track-top": `${activeTrackTop}px`,
|
|
98
|
+
}}
|
|
99
|
+
className={$scroller.scroll}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<ul className={$toc.ul}>
|
|
103
|
+
{children}
|
|
104
|
+
</ul>
|
|
105
|
+
</div>
|
|
106
|
+
</Context.Provider>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface TocItemProps {
|
|
110
|
+
children: React.ReactNode;
|
|
111
|
+
value: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Toc.Item = function TocItem({
|
|
115
|
+
children,
|
|
116
|
+
value = "#",
|
|
117
|
+
}: TocItemProps) {
|
|
118
|
+
const {
|
|
119
|
+
value: rootValue,
|
|
120
|
+
onChange
|
|
121
|
+
} = useContext(Context);
|
|
122
|
+
|
|
123
|
+
const href = "#" + value
|
|
124
|
+
const active = rootValue === value;
|
|
125
|
+
|
|
126
|
+
return <li className={$toc.li}>
|
|
127
|
+
<a
|
|
128
|
+
className={`${$toc.link} ${active && $toc.link$$active}`}
|
|
129
|
+
href={href}
|
|
130
|
+
onClick={(e) => {
|
|
131
|
+
// TODO: use react-router but for some reason does not work
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
onChange(value)
|
|
134
|
+
|
|
135
|
+
let found = false
|
|
136
|
+
|
|
137
|
+
// TODO: below is only a temporary solution
|
|
138
|
+
document.querySelectorAll("h2").forEach(e => {
|
|
139
|
+
if (found) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (e.innerText === value) {
|
|
144
|
+
found = true
|
|
145
|
+
e.scrollIntoView()
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{children}
|
|
151
|
+
</a>
|
|
152
|
+
</li>
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {Toc} from "./Toc";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ITOC {
|
|
2
|
+
depth: number
|
|
3
|
+
value: string
|
|
4
|
+
children: ITOC[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface IBreadcrumb {
|
|
8
|
+
title: string,
|
|
9
|
+
href: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface INavLinks {
|
|
13
|
+
prev?: {
|
|
14
|
+
title: string,
|
|
15
|
+
href: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
next?: {
|
|
19
|
+
title: string,
|
|
20
|
+
href: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "esnext",
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"target": "ES6",
|
|
7
|
+
"lib": [
|
|
8
|
+
"dom",
|
|
9
|
+
"dom.iterable",
|
|
10
|
+
"esnext"
|
|
11
|
+
],
|
|
12
|
+
"allowJs": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"strict": false,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"incremental": false,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"jsx": "react",
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"strictNullChecks": true
|
|
26
|
+
},
|
|
27
|
+
"include": [
|
|
28
|
+
"next-env.d.ts",
|
|
29
|
+
"**/*.ts",
|
|
30
|
+
"**/*.tsx",
|
|
31
|
+
".next/types/**/*.ts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": [
|
|
34
|
+
"node_modules"
|
|
35
|
+
]
|
|
36
|
+
}
|