@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 ADDED
@@ -0,0 +1,3 @@
1
+ # @xyd-js/ui
2
+
3
+ Next version of internal `xyd` ui components.
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./src/components"
2
+
3
+ export * from "./src/types"
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
+ }
@@ -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,7 @@
1
+ import {css} from "@linaria/core";
2
+
3
+ export const $anchor = {
4
+ host: css`
5
+
6
+ `,
7
+ };
@@ -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,7 @@
1
+ export {
2
+ UIAnchor
3
+ } from "./Anchor"
4
+
5
+ export type {
6
+ UIAnchorProps,
7
+ } from "./Anchor";
@@ -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,8 @@
1
+ export {
2
+ Nav
3
+ } from "./Nav"
4
+
5
+ export type {
6
+ NavProps,
7
+ NavItemProps
8
+ } from "./Nav";
@@ -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,11 @@
1
+ export {
2
+ UISidebar,
3
+ } from "./Sidebar";
4
+
5
+ export type {
6
+ UISidebarProps,
7
+ UISidebarItemProps,
8
+ UISidebarItemHeaderProps,
9
+
10
+ SidebarFooterItemProps
11
+ } from "./Sidebar";
@@ -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,7 @@
1
+ export {
2
+ SubNav
3
+ } from "./SubNav"
4
+
5
+ export type {
6
+ SubNavProps
7
+ } from "./SubNav";
@@ -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,13 @@
1
+ export * from './Nav';
2
+
3
+ export * from './Sidebar';
4
+
5
+ export * from './SubNav';
6
+
7
+ export * from './Toc';
8
+
9
+
10
+
11
+
12
+
13
+
@@ -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
+ }