├── .gitattributes
├── src
├── vite-env.d.ts
├── main.tsx
├── components
│ ├── page-title.tsx
│ ├── markdown-view.tsx
│ ├── main-content.tsx
│ ├── markdown-view.css
│ ├── heading.tsx
│ ├── markdown-outline-item.tsx
│ ├── markdown-outline-view.tsx
│ └── markdown-outline-active-section-highlight.tsx
├── atoms
│ └── container.tsx
├── index.css
├── app.css
├── utils
│ └── mdast-extract-headings.ts
├── app.tsx
├── stores
│ ├── logger.ts
│ ├── content.ts
│ └── toc.ts
├── hooks
│ └── use-visible-sections.ts
├── markdown-renderer.ts
├── assets
│ └── react.svg
├── example.md
└── tokens.css
├── bun.lockb
├── images
├── screenshot.png
└── video-tutorial.png
├── tsconfig.json
├── kuma.config.ts
├── .gitignore
├── index.html
├── .prettierrc.json
├── vite.config.ts
├── tsconfig.node.json
├── tsconfig.app.json
├── README.md
├── eslint.config.js
├── package.json
└── public
└── vite.svg
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.lockb binary diff=lockb
2 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftzdog/smooth-toc-example/HEAD/bun.lockb
--------------------------------------------------------------------------------
/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftzdog/smooth-toc-example/HEAD/images/screenshot.png
--------------------------------------------------------------------------------
/images/video-tutorial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftzdog/smooth-toc-example/HEAD/images/video-tutorial.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './app.tsx'
4 | import './index.css'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/src/components/page-title.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Heading } from '@kuma-ui/core'
3 | import { useContentStore } from '@/stores/content'
4 |
5 | export const PageTitle: React.FC = () => {
6 | const { title } = useContentStore()
7 |
8 | return {title}
9 | }
10 |
--------------------------------------------------------------------------------
/kuma.config.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from '@kuma-ui/core'
2 |
3 | const theme = createTheme({
4 | radii: {
5 | sm: '0.5rem',
6 | md: '1rem'
7 | }
8 | })
9 |
10 | type UserTheme = typeof theme
11 |
12 | declare module '@kuma-ui/core' {
13 | export interface Theme extends UserTheme {}
14 | }
15 |
16 | export default theme
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/atoms/container.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps } from '@kuma-ui/core'
2 |
3 | interface Props extends BoxProps {}
4 |
5 | export const Container = ({ children, ...boxProps }: Props) => {
6 | return (
7 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "singleQuote": true,
4 | "bracketSpacing": true,
5 | "endOfLine": "lf",
6 | "semi": false,
7 | "tabWidth": 2,
8 | "trailingComma": "none",
9 | "plugins": ["@ianvs/prettier-plugin-sort-imports"],
10 | "importOrder": [
11 | "",
12 | "^react",
13 | "",
14 | "^@inkdropapp",
15 | "^@[/]",
16 | "^[.].*$",
17 | "",
18 | "^[/].*$",
19 | "[.]css$"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'url'
2 | import KumaUI from '@kuma-ui/vite'
3 | import react from '@vitejs/plugin-react'
4 | import { defineConfig } from 'vite'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | react(),
10 | KumaUI({
11 | wasm: true
12 | })
13 | ],
14 | resolve: {
15 | alias: [
16 | {
17 | find: '@',
18 | replacement: fileURLToPath(new URL('./src', import.meta.url))
19 | }
20 | ]
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/markdown-view.tsx:
--------------------------------------------------------------------------------
1 | import './markdown-view.css'
2 | import React, { useEffect } from 'react'
3 | import { Box } from '@kuma-ui/core'
4 | import markdownContent from '@/example.md?raw'
5 | import { useVisibleSections } from '@/hooks/use-visible-sections'
6 | import { useContentStore } from '@/stores/content'
7 |
8 | export const MarkdownView: React.FC = () => {
9 | const { dom, render } = useContentStore()
10 |
11 | useEffect(() => {
12 | render(markdownContent)
13 | }, [render])
14 |
15 | useVisibleSections()
16 |
17 | return {dom ? dom : 'rendering...'}
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @layer reset,
2 | base,
3 | tokens,
4 | recipes,
5 | utilities,
6 | theme;
7 |
8 | @import 'modern-normalize' layer(reset);
9 | @import './tokens.css';
10 |
11 | :root {
12 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
13 | line-height: 1.5;
14 | font-weight: 400;
15 |
16 | color-scheme: dark;
17 |
18 | font-synthesis: none;
19 | text-rendering: optimizeLegibility;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | }
23 |
24 | html {
25 | margin: 0;
26 | }
27 |
28 | body {
29 | margin: 0;
30 | width: 100vw;
31 | min-height: 100vh;
32 | }
33 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | #root {
2 | display: contents;
3 | }
4 |
5 | :root {
6 | --page-background: var(--color-gray-950);
7 | --color-fg: var(--color-gray-200);
8 | --color-primary: var(--color-blue-500);
9 | --color-active: var(--color-pink-500);
10 | --color-muted: hsl(var(--hsl-gray-200) / 10%);
11 | --main-content-bg: var(--color-gray-900);
12 | --main-content-box-shadow: hsl(var(--hsl-gray-50) / 0.14) 0px 0px 0px 1px;
13 | --outline-active-fg: var(--color-fg);
14 | --outline-default-fg: var(--color-gray-500);
15 | }
16 |
17 | html {
18 | scroll-behavior: smooth;
19 | }
20 |
21 | body {
22 | background: var(--page-background);
23 | color: var(--color-fg);
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/main-content.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps } from '@kuma-ui/core'
2 |
3 | interface Props extends BoxProps {}
4 |
5 | export const MainContent = ({ children, ...boxProps }: Props) => {
6 | return (
7 |
8 |
21 | {children}
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/markdown-view.css:
--------------------------------------------------------------------------------
1 | .markdown-view {
2 | margin: 0;
3 | line-height: 1.5;
4 | word-wrap: break-word;
5 | padding: 1em 2em;
6 |
7 | h1,
8 | h2,
9 | h3,
10 | h4,
11 | h5,
12 | h6 {
13 | margin-top: 24px;
14 | margin-bottom: 16px;
15 | font-weight: 600;
16 | line-height: 1.25;
17 | }
18 |
19 | h2 {
20 | padding-bottom: 0.3em;
21 | font-size: 1.5em;
22 | }
23 |
24 | h3 {
25 | font-size: 1.25em;
26 | }
27 |
28 | h4 {
29 | font-size: 1em;
30 | }
31 |
32 | h5 {
33 | font-size: 0.875em;
34 | }
35 |
36 | h6 {
37 | font-size: 0.85em;
38 | }
39 |
40 | p {
41 | margin-top: 0;
42 | margin-bottom: 10px;
43 | }
44 |
45 | pre {
46 | overflow: auto;
47 | }
48 |
49 | pre > code {
50 | padding: 0;
51 | margin: 0;
52 | word-break: normal;
53 | white-space: pre;
54 | background: transparent;
55 | border: 0;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/heading.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { useTocStore } from '@/stores/toc'
3 |
4 | type Props = React.DetailedHTMLProps<
5 | React.HTMLAttributes,
6 | HTMLHeadingElement
7 | >
8 |
9 | export const createRehypeHeading = (level: number) => {
10 | const RehypeHeading = (props: Props) => {
11 | const { id, children } = props
12 | const refHeading = useRef(null)
13 | const registerHeading = useTocStore(state => state.registerHeading)
14 |
15 | const HeadingTag = `h${level || 1}` as
16 | | 'h1'
17 | | 'h2'
18 | | 'h3'
19 | | 'h4'
20 | | 'h5'
21 | | 'h6'
22 |
23 | useEffect(() => {
24 | if (id) registerHeading(id, refHeading)
25 | }, [id, registerHeading])
26 |
27 | return (
28 |
29 | {children}
30 |
31 | )
32 | }
33 |
34 | return (props: Props) =>
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "esModuleInterop": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "moduleDetection": "force",
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": false,
22 | "noImplicitAny": false,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true,
25 |
26 | "baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
27 | "paths": {
28 | /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
29 | "@/*": ["src/*"]
30 | }
31 | },
32 | "include": ["src"]
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/mdast-extract-headings.ts:
--------------------------------------------------------------------------------
1 | import Slugger from 'github-slugger'
2 | import { toString } from 'mdast-util-to-string'
3 | import { visit } from 'unist-util-visit'
4 | import type { Root } from 'mdast'
5 |
6 | const slugs = new Slugger()
7 |
8 | export type TOCHeading = {
9 | value: string
10 | id: string
11 | level: number
12 | }
13 |
14 | export const mdastExtractHeadings = (
15 | mdast: Root,
16 | { maxDepth }: { maxDepth: number } = { maxDepth: 3 }
17 | ) => {
18 | slugs.reset()
19 | const headings: TOCHeading[] = []
20 |
21 | visit(mdast, 'heading', function (node, _position, _parent) {
22 | const value = toString(node, { includeImageAlt: false })
23 | const id =
24 | node.data && node.data.hProperties && (node.data.hProperties.id as string)
25 | const slug = slugs.slug(id || value)
26 |
27 | if (node.depth <= maxDepth) {
28 | headings.push({
29 | value,
30 | id: slug,
31 | level: node.depth
32 | })
33 | }
34 | })
35 |
36 | return headings
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Smoothly animated table of contents example
2 |
3 | 
4 |
5 | This is a demo project of a smoothly animated table of contents.
6 | It is a simple web app rendering Markdown content with a table of contents that smoothly highlights currently active headings.
7 |
8 | For more details, check out [the blog post](https://www.devas.life/how-to-build-a-smoothly-animated-table-of-contents-with-framer-motion-and-kuma-ui/) and the video tutorial:
9 |
10 | [](https://youtu.be/4g26x6FzuBU)
11 |
12 | ## Stack
13 |
14 | - [Bun — A fast all-in-one JavaScript runtime](https://bun.sh/)
15 | - [`framer-motion`](https://www.framer.com/motion/) for animations.
16 | - [`zustand`](https://zustand-demo.pmnd.rs/) for state management.
17 | - [`kuma-ui`](https://www.kuma-ui.com/) for building UI components
18 | - [Installation | Kuma UI](https://www.kuma-ui.com/docs/install)
19 | - `prettier` for formtting code
20 | - [`modern-normalize`](https://github.com/sindresorhus/modern-normalize) for normalizing browsers' default style
21 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import reactHooks from 'eslint-plugin-react-hooks'
3 | import reactRefresh from 'eslint-plugin-react-refresh'
4 | import globals from 'globals'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | '@typescript-eslint/no-empty-object-type': 'off',
24 | '@typescript-eslint/no-unused-vars': [
25 | 'warn',
26 | {
27 | argsIgnorePattern: '^_',
28 | varsIgnorePattern: '^_'
29 | }
30 | ],
31 | 'react-refresh/only-export-components': [
32 | 'warn',
33 | { allowConstantExport: true }
34 | ]
35 | }
36 | }
37 | )
38 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import './app.css'
2 | import { Box, HStack } from '@kuma-ui/core'
3 | import { MarkdownView } from '@/components/markdown-view'
4 | import { Container } from './atoms/container'
5 | import { MainContent } from './components/main-content'
6 | import { MarkdownOutlineView } from './components/markdown-outline-view'
7 | import { PageTitle } from './components/page-title'
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | export default App
40 |
--------------------------------------------------------------------------------
/src/components/markdown-outline-item.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { Box, Link } from '@kuma-ui/core'
3 | import { useTocStore } from '@/stores/toc'
4 | import { TOCHeading } from '@/utils/mdast-extract-headings'
5 |
6 | type Props = TOCHeading & { active: boolean }
7 |
8 | export const MarkdownOutlineItem = (props: Props) => {
9 | const { id, level, value, active } = props
10 | const refItem = useRef(null)
11 | const registerOutlineItem = useTocStore(state => state.registerOutlineItem)
12 |
13 | useEffect(() => {
14 | if (id) registerOutlineItem(id, refItem)
15 | }, [id, registerOutlineItem])
16 |
17 | return (
18 |
27 |
28 |
36 | {value}
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/stores/logger.ts:
--------------------------------------------------------------------------------
1 | import type { StoreApi } from 'zustand'
2 |
3 | type GetState = StoreApi['getState']
4 | type SetState = StoreApi['setState']
5 |
6 | type ConfigFn = (
7 | set: SetState,
8 | get: GetState,
9 | api: StoreApi
10 | ) => T
11 |
12 | const enableLogging = import.meta.env.DEV
13 |
14 | export const loggerMiddleware =
15 | (config: ConfigFn) =>
16 | (set: SetState, get: GetState, api: StoreApi) =>
17 | config(
18 | args => {
19 | if (enableLogging) {
20 | const prevState = get()
21 | console.groupCollapsed(
22 | `%cZustand Action @ ${new Date().toLocaleTimeString()}`,
23 | 'font-weight: bold;'
24 | )
25 | console.log(
26 | '%cprev state',
27 | 'color: #9E9E9E; font-weight: bold;',
28 | prevState
29 | )
30 | console.log(
31 | '%caction ',
32 | 'color: #03A9F4; font-weight: bold;',
33 | args
34 | )
35 | set(args)
36 | const nextState = get()
37 | console.log(
38 | '%cnext state',
39 | 'color: #4CAF50; font-weight: bold;',
40 | nextState
41 | )
42 | console.groupEnd()
43 | } else {
44 | set(args)
45 | }
46 | },
47 | get,
48 | api
49 | )
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smooth-toc-example.rev2",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "bunx --bun vite",
8 | "build": "tsc -b && bunx --bun vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@kuma-ui/core": "^1.5.8",
14 | "framer-motion": "^11.4.0",
15 | "hast": "^1.0.0",
16 | "mdast": "^3.0.0",
17 | "modern-normalize": "^3.0.0",
18 | "react": "^18.3.1",
19 | "react-dom": "^18.3.1",
20 | "rehype-react": "^8.0.0",
21 | "rehype-slug": "^6.0.0",
22 | "remark-frontmatter": "^5.0.0",
23 | "remark-gfm": "^4.0.0",
24 | "remark-parse": "^11.0.0",
25 | "remark-rehype": "^11.1.0",
26 | "unified": "^11.0.5",
27 | "unist-util-visit": "^5.0.0",
28 | "yaml": "^2.5.1",
29 | "zustand": "^4.5.5"
30 | },
31 | "devDependencies": {
32 | "@eslint/js": "^9.9.0",
33 | "@ianvs/prettier-plugin-sort-imports": "^4.3.1",
34 | "@kuma-ui/vite": "^1.3.2",
35 | "@types/react": "^18.3.3",
36 | "@types/react-dom": "^18.3.0",
37 | "@vitejs/plugin-react": "^4.3.1",
38 | "eslint": "^9.9.0",
39 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
40 | "eslint-plugin-react-refresh": "^0.4.9",
41 | "globals": "^15.9.0",
42 | "prettier": "^3.3.3",
43 | "typescript": "^5.5.3",
44 | "typescript-eslint": "^8.0.1",
45 | "vite": "^5.4.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/markdown-outline-view.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Heading } from '@kuma-ui/core'
2 | import { useTocStore } from '@/stores/toc'
3 | import { MarkdownOutlineActiveSectionHighlight } from './markdown-outline-active-section-highlight'
4 | import { MarkdownOutlineItem } from './markdown-outline-item'
5 |
6 | export const MarkdownOutlineView = () => {
7 | const sections = useTocStore(state => state.sections)
8 | const minimumLevel = (sections || []).reduce(
9 | (p, c) => Math.min(p, c.level),
10 | 6
11 | )
12 |
13 | return (
14 |
15 |
16 | On this page
17 |
18 |
19 |
27 |
28 |
29 | {sections &&
30 | sections.map(heading => (
31 |
37 | ))}
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/markdown-outline-active-section-highlight.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@kuma-ui/core'
2 | import { motion } from 'framer-motion'
3 | import { Section } from '@/stores/toc'
4 |
5 | type Props = {
6 | sections: Section[]
7 | }
8 |
9 | export const MarkdownOutlineActiveSectionHighlight = (props: Props) => {
10 | const { sections } = props
11 | const visibleSectionIds = sections
12 | .filter(section => section.isVisible)
13 | .map(section => section.id)
14 | const elTocItems = sections.reduce((map, s) => {
15 | return {
16 | ...map,
17 | [s.id]: s.outlineItemRef?.current
18 | }
19 | }, {}) as Record
20 |
21 | const firstVisibleSectionIndex = Math.max(
22 | 0,
23 | sections.findIndex(section => section.id === visibleSectionIds[0])
24 | )
25 |
26 | const height: number | string = visibleSectionIds.reduce(
27 | (h, id) => h + (elTocItems[id]?.offsetHeight || 0),
28 | 0
29 | )
30 | const top = sections
31 | .slice(0, firstVisibleSectionIndex)
32 | .reduce((t, s) => t + (elTocItems[s.id]?.offsetHeight || 0), 0)
33 |
34 | return (
35 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/stores/content.ts:
--------------------------------------------------------------------------------
1 | import { EXIT, visit } from 'unist-util-visit'
2 | import YAML from 'yaml'
3 | import { create } from 'zustand'
4 | import { useTocStore } from './toc'
5 | import type { Root as HastRoot } from 'hast'
6 | import type { Root as MdastRoot } from 'mdast'
7 |
8 | type ContentType = React.ReactElement<
9 | unknown,
10 | string | React.JSXElementConstructor
11 | >
12 |
13 | interface ContentState {
14 | dom: ContentType | null
15 | mdast: MdastRoot | null
16 | hast: HastRoot | null
17 | title: string | null
18 | render: (markdown: string) => Promise
19 | lastError: Error | null | undefined
20 | }
21 |
22 | export const useContentStore = create(set => ({
23 | renderId: 0,
24 | dom: null,
25 | mdast: null,
26 | hast: null,
27 | title: null,
28 | lastError: null,
29 | render: async (markdown: string) => {
30 | try {
31 | const { MarkdownRenderer } = await import('@/markdown-renderer')
32 | const renderer = new MarkdownRenderer()
33 | const { result: dom, mdast, hast } = await renderer.render(markdown)
34 | let title = ''
35 |
36 | visit(mdast, 'yaml', node => {
37 | const frontmatter = YAML.parse(node.value)
38 | title = frontmatter.title || ''
39 | return EXIT
40 | })
41 |
42 | set({ dom, mdast, hast, title, lastError: null })
43 | useTocStore.getState().update(mdast)
44 | } catch (e: any) {
45 | console.error(`Failed to render preview: ${e.stack}`)
46 | set({
47 | dom: null,
48 | mdast: null,
49 | hast: null,
50 | title: null,
51 | lastError: new Error('Failed to render Markdown')
52 | })
53 | }
54 | }
55 | }))
56 |
--------------------------------------------------------------------------------
/src/hooks/use-visible-sections.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useLayoutEffect } from 'react'
2 | import { useTocStore } from '@/stores/toc'
3 |
4 | export function useVisibleSections() {
5 | const { sections, setVisibleHeadings } = useTocStore()
6 |
7 | const checkVisibleSections = useCallback(() => {
8 | const { innerHeight, scrollY } = window
9 | const newVisibleSections: string[] = []
10 |
11 | for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
12 | const { id, headingRef } = sections[sectionIndex]
13 |
14 | if (!headingRef?.current) {
15 | continue
16 | }
17 |
18 | const top = headingRef?.current.getBoundingClientRect().top + scrollY
19 |
20 | const nextSection = sections[sectionIndex + 1]
21 | const bottom =
22 | (nextSection?.headingRef?.current?.getBoundingClientRect().top ??
23 | Infinity) + scrollY
24 |
25 | if (
26 | (top > scrollY && top < scrollY + innerHeight) ||
27 | (bottom > scrollY && bottom < scrollY + innerHeight) ||
28 | (top <= scrollY && bottom >= scrollY + innerHeight)
29 | ) {
30 | newVisibleSections.push(id)
31 | }
32 | }
33 |
34 | // check if the visible sections have changed
35 | const oldVisibleHeadings = sections.filter(s => s.isVisible).map(s => s.id)
36 | const hasChanged = oldVisibleHeadings.join() !== newVisibleSections.join()
37 | if (hasChanged) setVisibleHeadings(newVisibleSections)
38 | }, [sections, setVisibleHeadings])
39 |
40 | useEffect(() => {
41 | window.addEventListener('scroll', checkVisibleSections, { passive: true })
42 | window.addEventListener('resize', checkVisibleSections)
43 |
44 | return () => {
45 | window.removeEventListener('scroll', checkVisibleSections)
46 | window.removeEventListener('resize', checkVisibleSections)
47 | }
48 | }, [sections, checkVisibleSections])
49 |
50 | useLayoutEffect(() => checkVisibleSections())
51 | }
52 |
--------------------------------------------------------------------------------
/src/markdown-renderer.ts:
--------------------------------------------------------------------------------
1 | import * as jsxRuntime from 'react/jsx-runtime'
2 | import rehype2react, { Components as JSXComponents } from 'rehype-react'
3 | import rehypeSlug from 'rehype-slug'
4 | import frontmatter from 'remark-frontmatter'
5 | import remarkGfm from 'remark-gfm'
6 | import remarkParse from 'remark-parse'
7 | import remark2rehype from 'remark-rehype'
8 | import { unified } from 'unified'
9 | import { createRehypeHeading } from '@/components/heading'
10 |
11 | const rehypeReactComponents: Partial = {
12 | h1: createRehypeHeading(1),
13 | h2: createRehypeHeading(2),
14 | h3: createRehypeHeading(3),
15 | h4: createRehypeHeading(4),
16 | h5: createRehypeHeading(5),
17 | h6: createRehypeHeading(6)
18 | }
19 |
20 | export interface BuiltInPluginOptions {
21 | rehypeSlug: boolean
22 | rehypeMetadataSection: boolean
23 | }
24 |
25 | export class MarkdownRenderer {
26 | processor: Awaited> | null = null
27 |
28 | createProcessor() {
29 | const remarkParser = unified()
30 | .use(remarkParse)
31 | .use(remarkGfm)
32 | .use(frontmatter)
33 | const rehypedRemark = remarkParser()
34 | .use(remark2rehype, {
35 | allowDangerousHtml: true
36 | })
37 | .use(rehypeSlug)
38 | const renderer = rehypedRemark.use(rehype2react, {
39 | Fragment: jsxRuntime.Fragment as any,
40 | jsx: jsxRuntime.jsx as any,
41 | jsxs: jsxRuntime.jsxs as any,
42 | components: rehypeReactComponents
43 | })
44 |
45 | this.processor = renderer
46 | return renderer
47 | }
48 |
49 | async getProcessor() {
50 | if (this.processor) return this.processor
51 | return this.createProcessor()
52 | }
53 |
54 | async render(markdown: string) {
55 | const processor = await this.getProcessor()
56 | const mdast = processor.parse(markdown)
57 | const hast = await processor.run(mdast)
58 | const result = processor.stringify(hast)
59 |
60 | return {
61 | result,
62 | mdast,
63 | hast
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/stores/toc.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import {
3 | mdastExtractHeadings,
4 | TOCHeading
5 | } from '@/utils/mdast-extract-headings'
6 | import { loggerMiddleware } from './logger'
7 | import type { Root } from 'mdast'
8 | import type { RefObject } from 'react'
9 |
10 | export type Section = TOCHeading & {
11 | headingRef: RefObject | null
12 | outlineItemRef: RefObject | null
13 | isVisible: boolean
14 | }
15 | export type TocState = {
16 | sections: Section[]
17 | update: (mdast: Root) => void
18 | registerHeading: (id: string, ref: RefObject) => void
19 | registerOutlineItem: (id: string, ref: RefObject) => void
20 | setVisibleHeadings: (ids: string[]) => void
21 | }
22 |
23 | export const useTocStore = create(
24 | loggerMiddleware((set, get) => ({
25 | sections: [],
26 | update: (mdast: Root) => {
27 | if (mdast) {
28 | const prevSections = get().sections
29 | const sections = mdastExtractHeadings(mdast).map(h => {
30 | const prev = prevSections.find(s => s.id === h.id)
31 | return {
32 | ...h,
33 | isVisible: false,
34 | headingRef: prev ? prev.headingRef : null,
35 | outlineItemRef: prev ? prev.outlineItemRef : null
36 | }
37 | })
38 |
39 | set({ sections })
40 | } else {
41 | set({ sections: [] })
42 | }
43 | },
44 | registerHeading: (id: string, ref: RefObject) => {
45 | set(state => ({
46 | sections: state.sections.map(s =>
47 | s.id === id ? { ...s, headingRef: ref } : s
48 | )
49 | }))
50 | },
51 | registerOutlineItem: (id: string, ref: RefObject) => {
52 | set(state => ({
53 | sections: state.sections.map(s =>
54 | s.id === id ? { ...s, outlineItemRef: ref } : s
55 | )
56 | }))
57 | },
58 | setVisibleHeadings: (ids: string[]) => {
59 | set(state => ({
60 | sections: state.sections.map(s => ({
61 | ...s,
62 | isVisible: ids.includes(s.id)
63 | }))
64 | }))
65 | }
66 | }))
67 | )
68 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/example.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How to use PouchDB on React Native >= 0.73
3 | ---
4 |
5 | Hi, it's [Takuya](https://x.com/inkdrop_app) here.
6 |
7 | I've been updating the mobile version of [Inkdrop](https://www.inkdrop.app/), which is built with React Native.
8 | It is a simple Markdown note-taking app, which supports syncing notes with the server and offer an offline-first viewing and editing experience.
9 | To accomplish this, I've been using [PouchDB](https://pouchdb.com/), the JavaScript-based database that can sync with [Apache CouchDB](https://couchdb.apache.org/).
10 |
11 | The PouchDB community was struggling to get it to work on React Native smoothly since it doesn't provide NodeJS-compatible APIs like encoding/decoding base64 or the `crypto` module out of the box.
12 | In 2022, I shared [how to use PouchDB on React Native in this blog post](https://dev.to/craftzdog/a-performant-way-to-use-pouchdb7-on-react-native-in-2022-24ej).
13 | The technique was adequately performant by using a JSI-based SQLite driver, and polyfilling the missing modules with the native implementations respectively.
14 |
15 | Since then, the circumstances around the React Native ecosystem has been sifnificantly changed.
16 | This article is an updated version of how to use PouchDB on the latest React Native.
17 |
18 | ## Mature JSI-based libraries and the NULL char problem solved
19 |
20 | There are now JSI-based libraries that are better designed and more performant, for example:
21 |
22 | - [op-sqlite](https://github.com/OP-Engineering/op-sqlite): Fastest SQLite library for react-native by [@ospfranco](https://github.com/ospfranco)
23 | - It is approximately 8~9x faster than [react-native-quick-sqlite](https://github.com/margelo/react-native-quick-sqlite), especially on Android.
24 | - [react-native-quick-crypto](https://github.com/margelo/react-native-quick-crypto): ⚡️ A fast implementation of Node's `crypto` module written in C/C++ JSI
25 | - It internally uses my `@craftzdog/react-native-buffer` module, which improves encoding/decoding base64 performance.
26 |
27 | React Native also introduced the new architecture: TurboModules and Fabric, which themselves leverage JSI to improve the communication performance between the native and JS layers.
28 | It appears that a recent RN update, though I'm not use which commit, has solved [the `\u0000` string termination](https://github.com/facebook/react-native/issues/12731)! It means that you no longer need [a hack escaping `\u0000` chars](https://github.com/craftzdog/pouchdb-react-native/commit/228f68220fe31236f6630b71c030eef29ae6e7a8). Yay!
29 |
30 | With these new libraries and fundamental improvements, you can use PouchDB much more smoothly and straightforwardly.
31 |
32 | ## Introducing pouchdb-adapter-react-native-sqlite@4
33 |
34 | Previsouly, I made [pouchdb-adapter-react-native-sqlite](https://github.com/craftzdog/pouchdb-adapter-react-native-sqlite) to let PouchDB use SQLite on React Native.
35 | I'm excited to announce v4, which now uses [op-sqlite](https://github.com/OP-Engineering/op-sqlite).
36 | This time, I managed to make it directly call SQLite APIs and to get rid of the websql layer.
37 |
38 | Thanks to the NULL termination issue gone, attachment support is back available now.
39 |
40 | You can try an example project for a quick hands-on experience.
41 |
42 | https://github.com/craftzdog/pouchdb-adapter-react-native-sqlite/tree/master/example
43 |
44 | ## How to use
45 |
46 | Setting up the adapter is pretty easy in v4.
47 |
48 | ### Install libraries
49 |
50 | ```sh
51 | yarn add @op-engineering/op-sqlite react-native-quick-crypto @craftzdog/react-native-buffer
52 | npx pod-install
53 | ```
54 |
55 | ### Polyfill NodeJS APIs
56 |
57 | Create a `shim.ts` file like so:
58 |
59 | ```ts
60 | import { install } from 'react-native-quick-crypto'
61 |
62 | install()
63 | ```
64 |
65 | Configure babel to use the shim modules. First, you need to install `babel-plugin-module-resolver`.
66 |
67 | ```sh
68 | yarn add --dev babel-plugin-module-resolver
69 | ```
70 |
71 | Then, in your `babel.config.js`, add the plugin to swap the `crypto`, `stream` and `buffer` dependencies:
72 |
73 | ```js
74 | plugins: [
75 | [
76 | 'module-resolver',
77 | {
78 | extensions: ['.tsx', '.ts', '.js', '.json'],
79 | alias: {
80 | crypto: 'react-native-quick-crypto',
81 | stream: 'readable-stream',
82 | buffer: '@craftzdog/react-native-buffer',
83 | },
84 | },
85 | ],
86 | ],
87 | ```
88 |
89 | Then restart your bundler using `yarn start --reset-cache`.
90 |
91 | ### Install PouchDB and adapter
92 |
93 | Now it's ready to use PouchDB!
94 |
95 | ```sh
96 | yarn add pouchdb-core pouchdb-mapreduce pouchdb-replication pouchdb-adapter-http pouchdb-adapter-react-native-sqlite
97 | ```
98 |
99 | Create `pouchdb.ts`:
100 |
101 | ```ts
102 | import HttpPouch from 'pouchdb-adapter-http'
103 | import sqliteAdapter from 'pouchdb-adapter-react-native-sqlite'
104 | import PouchDB from 'pouchdb-core'
105 | import mapreduce from 'pouchdb-mapreduce'
106 | import replication from 'pouchdb-replication'
107 |
108 | export default PouchDB.plugin(HttpPouch)
109 | .plugin(replication)
110 | .plugin(mapreduce)
111 | .plugin(sqliteAdapter)
112 | ```
113 |
114 | ### How to use PouchDB
115 |
116 | ```ts
117 | import PouchDB from './pouchdb'
118 |
119 | const pouch = new PouchDB('mydb', {
120 | adapter: 'react-native-sqlite'
121 | })
122 | ```
123 |
124 | That's it!
125 | Hope you find it useful and helpful.
126 |
127 | ## Troubleshootings
128 |
129 | ### Fails to install crypto shim with `install()` on launch
130 |
131 | You amy get the following error when new arch is enabled:
132 |
133 | ```
134 | (NOBRIDGE) ERROR Error: Failed to install react-native-quick-crypto: React Native is not running on-device. QuickCrypto can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.
135 | (NOBRIDGE) ERROR TypeError: Cannot read property 'install' of undefined
136 | ```
137 |
138 | - This is a know issue: [Error: Failed to install react-native-quick-crypto: React Native is not running on-device. · Issue #333 · margelo/react-native-quick-crypto · GitHub](https://github.com/margelo/react-native-quick-crypto/issues/333)
139 |
140 | For now, you have to edit:
141 |
142 | - `lib/module/NativeQuickCrypto/NativeQuickCrypto.js`
143 |
144 | And comment them out:
145 |
146 | ```
147 | // Check if we are running on-device (JSI)
148 | // if (global.nativeCallSyncHook == null || QuickCryptoModule.install == null) {
149 | // throw new Error('Failed to install react-native-quick-crypto: React Native is not running on-device. QuickCrypto can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.');
150 | // }
151 | ```
152 |
--------------------------------------------------------------------------------
/src/tokens.css:
--------------------------------------------------------------------------------
1 | @layer tokens {
2 | :where(:root, :host) {
3 | --font-weights-thin: 100;
4 | --font-weights-extralight: 200;
5 | --font-weights-light: 300;
6 | --font-weights-normal: 400;
7 | --font-weights-medium: 500;
8 | --font-weights-semibold: 600;
9 | --font-weights-bold: 700;
10 | --font-weights-extrabold: 800;
11 | --font-weights-black: 900;
12 | --color-current: currentColor;
13 | --color-black: #000;
14 | --color-white: #fff;
15 | --color-transparent: rgb(0 0 0 / 0);
16 | --hsl-black: 0deg 0% 0%;
17 | --hsl-white: 0deg 100% 100%;
18 | --hsl-rose-50: 356deg 100% 97%;
19 | --hsl-rose-100: 356deg 100% 95%;
20 | --hsl-rose-200: 353deg 96% 90%;
21 | --hsl-rose-300: 353deg 96% 82%;
22 | --hsl-rose-400: 351deg 95% 71%;
23 | --hsl-rose-500: 350deg 89% 60%;
24 | --hsl-rose-600: 347deg 77% 50%;
25 | --hsl-rose-700: 345deg 83% 41%;
26 | --hsl-rose-800: 343deg 80% 35%;
27 | --hsl-rose-900: 342deg 75% 30%;
28 | --hsl-rose-950: 343deg 88% 16%;
29 | --hsl-pink-50: 327deg 73% 97%;
30 | --hsl-pink-100: 326deg 78% 95%;
31 | --hsl-pink-200: 326deg 85% 90%;
32 | --hsl-pink-300: 327deg 87% 82%;
33 | --hsl-pink-400: 329deg 86% 70%;
34 | --hsl-pink-500: 330deg 81% 60%;
35 | --hsl-pink-600: 333deg 71% 51%;
36 | --hsl-pink-700: 335deg 78% 42%;
37 | --hsl-pink-800: 336deg 74% 35%;
38 | --hsl-pink-900: 336deg 69% 30%;
39 | --hsl-pink-950: 336deg 84% 17%;
40 | --hsl-fuchsia-50: 289deg 100% 98%;
41 | --hsl-fuchsia-100: 287deg 100% 95%;
42 | --hsl-fuchsia-200: 288deg 96% 91%;
43 | --hsl-fuchsia-300: 291deg 93% 83%;
44 | --hsl-fuchsia-400: 292deg 91% 73%;
45 | --hsl-fuchsia-500: 292deg 84% 61%;
46 | --hsl-fuchsia-600: 293deg 69% 49%;
47 | --hsl-fuchsia-700: 295deg 72% 40%;
48 | --hsl-fuchsia-800: 295deg 70% 33%;
49 | --hsl-fuchsia-900: 297deg 64% 28%;
50 | --hsl-fuchsia-950: 297deg 90% 16%;
51 | --hsl-purple-50: 270deg 100% 98%;
52 | --hsl-purple-100: 269deg 100% 95%;
53 | --hsl-purple-200: 269deg 100% 92%;
54 | --hsl-purple-300: 269deg 97% 85%;
55 | --hsl-purple-400: 270deg 95% 75%;
56 | --hsl-purple-500: 271deg 91% 65%;
57 | --hsl-purple-600: 271deg 81% 56%;
58 | --hsl-purple-700: 272deg 72% 47%;
59 | --hsl-purple-800: 273deg 67% 39%;
60 | --hsl-purple-900: 274deg 66% 32%;
61 | --hsl-purple-950: 274deg 87% 21%;
62 | --hsl-violet-50: 250deg 100% 98%;
63 | --hsl-violet-100: 251deg 91% 95%;
64 | --hsl-violet-200: 251deg 95% 92%;
65 | --hsl-violet-300: 252deg 95% 85%;
66 | --hsl-violet-400: 255deg 92% 76%;
67 | --hsl-violet-500: 258deg 90% 66%;
68 | --hsl-violet-600: 262deg 83% 58%;
69 | --hsl-violet-700: 263deg 70% 50%;
70 | --hsl-violet-800: 263deg 69% 42%;
71 | --hsl-violet-900: 264deg 67% 35%;
72 | --hsl-violet-950: 261deg 73% 23%;
73 | --hsl-indigo-50: 226deg 100% 97%;
74 | --hsl-indigo-100: 226deg 100% 94%;
75 | --hsl-indigo-200: 228deg 96% 89%;
76 | --hsl-indigo-300: 230deg 94% 82%;
77 | --hsl-indigo-400: 234deg 89% 74%;
78 | --hsl-indigo-500: 239deg 84% 67%;
79 | --hsl-indigo-600: 243deg 75% 59%;
80 | --hsl-indigo-700: 245deg 58% 51%;
81 | --hsl-indigo-800: 244deg 55% 41%;
82 | --hsl-indigo-900: 242deg 47% 34%;
83 | --hsl-indigo-950: 244deg 47% 20%;
84 | --hsl-blue-50: 214deg 100% 97%;
85 | --hsl-blue-100: 214deg 95% 93%;
86 | --hsl-blue-200: 213deg 97% 87%;
87 | --hsl-blue-300: 212deg 96% 78%;
88 | --hsl-blue-400: 213deg 94% 68%;
89 | --hsl-blue-500: 217deg 91% 60%;
90 | --hsl-blue-600: 221deg 83% 53%;
91 | --hsl-blue-700: 224deg 76% 48%;
92 | --hsl-blue-800: 226deg 71% 40%;
93 | --hsl-blue-900: 224deg 64% 33%;
94 | --hsl-blue-950: 226deg 57% 21%;
95 | --hsl-sky-50: 204deg 100% 97%;
96 | --hsl-sky-100: 204deg 94% 94%;
97 | --hsl-sky-200: 201deg 94% 86%;
98 | --hsl-sky-300: 199deg 95% 74%;
99 | --hsl-sky-400: 198deg 93% 60%;
100 | --hsl-sky-500: 199deg 89% 48%;
101 | --hsl-sky-600: 200deg 98% 39%;
102 | --hsl-sky-700: 201deg 96% 32%;
103 | --hsl-sky-800: 201deg 90% 27%;
104 | --hsl-sky-900: 202deg 80% 24%;
105 | --hsl-sky-950: 204deg 80% 16%;
106 | --hsl-cyan-50: 183deg 100% 96%;
107 | --hsl-cyan-100: 185deg 96% 90%;
108 | --hsl-cyan-200: 186deg 94% 82%;
109 | --hsl-cyan-300: 187deg 92% 69%;
110 | --hsl-cyan-400: 188deg 86% 53%;
111 | --hsl-cyan-500: 189deg 94% 43%;
112 | --hsl-cyan-600: 192deg 91% 36%;
113 | --hsl-cyan-700: 193deg 82% 31%;
114 | --hsl-cyan-800: 194deg 70% 27%;
115 | --hsl-cyan-900: 196deg 64% 24%;
116 | --hsl-cyan-950: 197deg 79% 15%;
117 | --hsl-teal-50: 166deg 76% 97%;
118 | --hsl-teal-100: 167deg 85% 89%;
119 | --hsl-teal-200: 168deg 84% 78%;
120 | --hsl-teal-300: 171deg 77% 64%;
121 | --hsl-teal-400: 172deg 66% 50%;
122 | --hsl-teal-500: 173deg 80% 40%;
123 | --hsl-teal-600: 175deg 84% 32%;
124 | --hsl-teal-700: 175deg 77% 26%;
125 | --hsl-teal-800: 176deg 69% 22%;
126 | --hsl-teal-900: 176deg 61% 19%;
127 | --hsl-teal-950: 179deg 84% 10%;
128 | --hsl-emerald-50: 152deg 81% 96%;
129 | --hsl-emerald-100: 149deg 80% 90%;
130 | --hsl-emerald-200: 152deg 76% 80%;
131 | --hsl-emerald-300: 156deg 72% 67%;
132 | --hsl-emerald-400: 158deg 64% 52%;
133 | --hsl-emerald-500: 160deg 84% 39%;
134 | --hsl-emerald-600: 161deg 94% 30%;
135 | --hsl-emerald-700: 163deg 94% 24%;
136 | --hsl-emerald-800: 163deg 88% 20%;
137 | --hsl-emerald-900: 164deg 86% 16%;
138 | --hsl-emerald-950: 166deg 91% 9%;
139 | --hsl-green-50: 138deg 76% 97%;
140 | --hsl-green-100: 141deg 84% 93%;
141 | --hsl-green-200: 141deg 79% 85%;
142 | --hsl-green-300: 142deg 77% 73%;
143 | --hsl-green-400: 142deg 69% 58%;
144 | --hsl-green-500: 142deg 71% 45%;
145 | --hsl-green-600: 142deg 76% 36%;
146 | --hsl-green-700: 142deg 72% 29%;
147 | --hsl-green-800: 143deg 64% 24%;
148 | --hsl-green-900: 144deg 61% 20%;
149 | --hsl-green-950: 145deg 80% 10%;
150 | --hsl-lime-50: 78deg 92% 95%;
151 | --hsl-lime-100: 80deg 89% 89%;
152 | --hsl-lime-200: 81deg 88% 80%;
153 | --hsl-lime-300: 82deg 85% 67%;
154 | --hsl-lime-400: 83deg 78% 55%;
155 | --hsl-lime-500: 84deg 81% 44%;
156 | --hsl-lime-600: 85deg 85% 35%;
157 | --hsl-lime-700: 86deg 78% 27%;
158 | --hsl-lime-800: 86deg 69% 23%;
159 | --hsl-lime-900: 88deg 61% 20%;
160 | --hsl-lime-950: 89deg 80% 10%;
161 | --hsl-yellow-50: 55deg 92% 95%;
162 | --hsl-yellow-100: 55deg 97% 88%;
163 | --hsl-yellow-200: 53deg 98% 77%;
164 | --hsl-yellow-300: 50deg 98% 64%;
165 | --hsl-yellow-400: 48deg 96% 53%;
166 | --hsl-yellow-500: 45deg 93% 47%;
167 | --hsl-yellow-600: 41deg 96% 40%;
168 | --hsl-yellow-700: 35deg 92% 33%;
169 | --hsl-yellow-800: 32deg 81% 29%;
170 | --hsl-yellow-900: 28deg 73% 26%;
171 | --hsl-yellow-950: 26deg 83% 14%;
172 | --hsl-amber-50: 48deg 100% 96%;
173 | --hsl-amber-100: 48deg 96% 89%;
174 | --hsl-amber-200: 48deg 97% 77%;
175 | --hsl-amber-300: 46deg 97% 65%;
176 | --hsl-amber-400: 43deg 96% 56%;
177 | --hsl-amber-500: 38deg 92% 50%;
178 | --hsl-amber-600: 32deg 95% 44%;
179 | --hsl-amber-700: 26deg 90% 37%;
180 | --hsl-amber-800: 23deg 83% 31%;
181 | --hsl-amber-900: 22deg 78% 26%;
182 | --hsl-amber-950: 21deg 92% 14%;
183 | --hsl-orange-50: 33deg 100% 96%;
184 | --hsl-orange-100: 34deg 100% 92%;
185 | --hsl-orange-200: 32deg 98% 83%;
186 | --hsl-orange-300: 31deg 97% 72%;
187 | --hsl-orange-400: 27deg 96% 61%;
188 | --hsl-orange-500: 25deg 95% 53%;
189 | --hsl-orange-600: 21deg 90% 48%;
190 | --hsl-orange-700: 17deg 88% 40%;
191 | --hsl-orange-800: 15deg 79% 34%;
192 | --hsl-orange-900: 15deg 75% 28%;
193 | --hsl-orange-950: 13deg 81% 15%;
194 | --hsl-red-50: 0deg 86% 97%;
195 | --hsl-red-100: 0deg 93% 94%;
196 | --hsl-red-200: 0deg 96% 89%;
197 | --hsl-red-300: 0deg 94% 82%;
198 | --hsl-red-400: 0deg 91% 71%;
199 | --hsl-red-500: 0deg 84% 60%;
200 | --hsl-red-600: 0deg 72% 51%;
201 | --hsl-red-700: 0deg 74% 42%;
202 | --hsl-red-800: 0deg 70% 35%;
203 | --hsl-red-900: 0deg 63% 31%;
204 | --hsl-red-950: 0deg 75% 15%;
205 | --hsl-neutral-50: 0deg 0% 98%;
206 | --hsl-neutral-100: 0deg 0% 96%;
207 | --hsl-neutral-200: 0deg 0% 90%;
208 | --hsl-neutral-300: 0deg 0% 83%;
209 | --hsl-neutral-400: 0deg 0% 64%;
210 | --hsl-neutral-500: 0deg 0% 45%;
211 | --hsl-neutral-600: 0deg 0% 32%;
212 | --hsl-neutral-700: 0deg 0% 25%;
213 | --hsl-neutral-800: 0deg 0% 15%;
214 | --hsl-neutral-900: 0deg 0% 9%;
215 | --hsl-neutral-950: 0deg 0% 4%;
216 | --hsl-stone-50: 60deg 9% 98%;
217 | --hsl-stone-100: 60deg 5% 96%;
218 | --hsl-stone-200: 20deg 6% 90%;
219 | --hsl-stone-300: 24deg 6% 83%;
220 | --hsl-stone-400: 24deg 5% 64%;
221 | --hsl-stone-500: 25deg 5% 45%;
222 | --hsl-stone-600: 33deg 5% 32%;
223 | --hsl-stone-700: 30deg 6% 25%;
224 | --hsl-stone-800: 12deg 6% 15%;
225 | --hsl-stone-900: 24deg 10% 10%;
226 | --hsl-stone-950: 20deg 14% 4%;
227 | --hsl-zinc-50: 0deg 0% 98%;
228 | --hsl-zinc-100: 240deg 5% 96%;
229 | --hsl-zinc-200: 240deg 6% 90%;
230 | --hsl-zinc-300: 240deg 5% 84%;
231 | --hsl-zinc-400: 240deg 5% 65%;
232 | --hsl-zinc-500: 240deg 4% 46%;
233 | --hsl-zinc-600: 240deg 5% 34%;
234 | --hsl-zinc-700: 240deg 5% 26%;
235 | --hsl-zinc-800: 240deg 4% 16%;
236 | --hsl-zinc-900: 240deg 6% 10%;
237 | --hsl-zinc-950: 240deg 10% 4%;
238 | --hsl-gray-50: 210deg 20% 98%;
239 | --hsl-gray-100: 220deg 14% 96%;
240 | --hsl-gray-200: 220deg 13% 91%;
241 | --hsl-gray-300: 216deg 12% 84%;
242 | --hsl-gray-400: 218deg 11% 65%;
243 | --hsl-gray-500: 220deg 9% 46%;
244 | --hsl-gray-600: 215deg 14% 34%;
245 | --hsl-gray-700: 217deg 19% 27%;
246 | --hsl-gray-800: 215deg 28% 17%;
247 | --hsl-gray-900: 221deg 39% 11%;
248 | --hsl-gray-950: 224deg 71% 4%;
249 | --hsl-slate-50: 210deg 40% 98%;
250 | --hsl-slate-100: 210deg 40% 96%;
251 | --hsl-slate-200: 214deg 32% 91%;
252 | --hsl-slate-300: 213deg 27% 84%;
253 | --hsl-slate-400: 215deg 20% 65%;
254 | --hsl-slate-500: 215deg 16% 47%;
255 | --hsl-slate-600: 215deg 19% 35%;
256 | --hsl-slate-700: 215deg 25% 27%;
257 | --hsl-slate-800: 217deg 33% 17%;
258 | --hsl-slate-900: 222deg 47% 11%;
259 | --hsl-slate-950: 229deg 84% 5%;
260 |
261 | --color-rose-50: hsl(var(--hsl-rose-50));
262 | --color-rose-100: hsl(var(--hsl-rose-100));
263 | --color-rose-200: hsl(var(--hsl-rose-200));
264 | --color-rose-300: hsl(var(--hsl-rose-300));
265 | --color-rose-400: hsl(var(--hsl-rose-400));
266 | --color-rose-500: hsl(var(--hsl-rose-500));
267 | --color-rose-600: hsl(var(--hsl-rose-600));
268 | --color-rose-700: hsl(var(--hsl-rose-700));
269 | --color-rose-800: hsl(var(--hsl-rose-800));
270 | --color-rose-900: hsl(var(--hsl-rose-900));
271 | --color-rose-950: hsl(var(--hsl-rose-950));
272 | --color-pink-50: hsl(var(--hsl-pink-50));
273 | --color-pink-100: hsl(var(--hsl-pink-100));
274 | --color-pink-200: hsl(var(--hsl-pink-200));
275 | --color-pink-300: hsl(var(--hsl-pink-300));
276 | --color-pink-400: hsl(var(--hsl-pink-400));
277 | --color-pink-500: hsl(var(--hsl-pink-500));
278 | --color-pink-600: hsl(var(--hsl-pink-600));
279 | --color-pink-700: hsl(var(--hsl-pink-700));
280 | --color-pink-800: hsl(var(--hsl-pink-800));
281 | --color-pink-900: hsl(var(--hsl-pink-900));
282 | --color-pink-950: hsl(var(--hsl-pink-950));
283 | --color-fuchsia-50: hsl(var(--hsl-fuchsia-50));
284 | --color-fuchsia-100: hsl(var(--hsl-fuchsia-100));
285 | --color-fuchsia-200: hsl(var(--hsl-fuchsia-200));
286 | --color-fuchsia-300: hsl(var(--hsl-fuchsia-300));
287 | --color-fuchsia-400: hsl(var(--hsl-fuchsia-400));
288 | --color-fuchsia-500: hsl(var(--hsl-fuchsia-500));
289 | --color-fuchsia-600: hsl(var(--hsl-fuchsia-600));
290 | --color-fuchsia-700: hsl(var(--hsl-fuchsia-700));
291 | --color-fuchsia-800: hsl(var(--hsl-fuchsia-800));
292 | --color-fuchsia-900: hsl(var(--hsl-fuchsia-900));
293 | --color-fuchsia-950: hsl(var(--hsl-fuchsia-950));
294 | --color-purple-50: hsl(var(--hsl-purple-50));
295 | --color-purple-100: hsl(var(--hsl-purple-100));
296 | --color-purple-200: hsl(var(--hsl-purple-200));
297 | --color-purple-300: hsl(var(--hsl-purple-300));
298 | --color-purple-400: hsl(var(--hsl-purple-400));
299 | --color-purple-500: hsl(var(--hsl-purple-500));
300 | --color-purple-600: hsl(var(--hsl-purple-600));
301 | --color-purple-700: hsl(var(--hsl-purple-700));
302 | --color-purple-800: hsl(var(--hsl-purple-800));
303 | --color-purple-900: hsl(var(--hsl-purple-900));
304 | --color-purple-950: hsl(var(--hsl-purple-950));
305 | --color-violet-50: hsl(var(--hsl-violet-50));
306 | --color-violet-100: hsl(var(--hsl-violet-100));
307 | --color-violet-200: hsl(var(--hsl-violet-200));
308 | --color-violet-300: hsl(var(--hsl-violet-300));
309 | --color-violet-400: hsl(var(--hsl-violet-400));
310 | --color-violet-500: hsl(var(--hsl-violet-500));
311 | --color-violet-600: hsl(var(--hsl-violet-600));
312 | --color-violet-700: hsl(var(--hsl-violet-700));
313 | --color-violet-800: hsl(var(--hsl-violet-800));
314 | --color-violet-900: hsl(var(--hsl-violet-900));
315 | --color-violet-950: hsl(var(--hsl-violet-950));
316 | --color-indigo-50: hsl(var(--hsl-indigo-50));
317 | --color-indigo-100: hsl(var(--hsl-indigo-100));
318 | --color-indigo-200: hsl(var(--hsl-indigo-200));
319 | --color-indigo-300: hsl(var(--hsl-indigo-300));
320 | --color-indigo-400: hsl(var(--hsl-indigo-400));
321 | --color-indigo-500: hsl(var(--hsl-indigo-500));
322 | --color-indigo-600: hsl(var(--hsl-indigo-600));
323 | --color-indigo-700: hsl(var(--hsl-indigo-700));
324 | --color-indigo-800: hsl(var(--hsl-indigo-800));
325 | --color-indigo-900: hsl(var(--hsl-indigo-900));
326 | --color-indigo-950: hsl(var(--hsl-indigo-950));
327 | --color-blue-50: hsl(var(--hsl-blue-50));
328 | --color-blue-100: hsl(var(--hsl-blue-100));
329 | --color-blue-200: hsl(var(--hsl-blue-200));
330 | --color-blue-300: hsl(var(--hsl-blue-300));
331 | --color-blue-400: hsl(var(--hsl-blue-400));
332 | --color-blue-500: hsl(var(--hsl-blue-500));
333 | --color-blue-600: hsl(var(--hsl-blue-600));
334 | --color-blue-700: hsl(var(--hsl-blue-700));
335 | --color-blue-800: hsl(var(--hsl-blue-800));
336 | --color-blue-900: hsl(var(--hsl-blue-900));
337 | --color-blue-950: hsl(var(--hsl-blue-950));
338 | --color-sky-50: hsl(var(--hsl-sky-50));
339 | --color-sky-100: hsl(var(--hsl-sky-100));
340 | --color-sky-200: hsl(var(--hsl-sky-200));
341 | --color-sky-300: hsl(var(--hsl-sky-300));
342 | --color-sky-400: hsl(var(--hsl-sky-400));
343 | --color-sky-500: hsl(var(--hsl-sky-500));
344 | --color-sky-600: hsl(var(--hsl-sky-600));
345 | --color-sky-700: hsl(var(--hsl-sky-700));
346 | --color-sky-800: hsl(var(--hsl-sky-800));
347 | --color-sky-900: hsl(var(--hsl-sky-900));
348 | --color-sky-950: hsl(var(--hsl-sky-950));
349 | --color-cyan-50: hsl(var(--hsl-cyan-50));
350 | --color-cyan-100: hsl(var(--hsl-cyan-100));
351 | --color-cyan-200: hsl(var(--hsl-cyan-200));
352 | --color-cyan-300: hsl(var(--hsl-cyan-300));
353 | --color-cyan-400: hsl(var(--hsl-cyan-400));
354 | --color-cyan-500: hsl(var(--hsl-cyan-500));
355 | --color-cyan-600: hsl(var(--hsl-cyan-600));
356 | --color-cyan-700: hsl(var(--hsl-cyan-700));
357 | --color-cyan-800: hsl(var(--hsl-cyan-800));
358 | --color-cyan-900: hsl(var(--hsl-cyan-900));
359 | --color-cyan-950: hsl(var(--hsl-cyan-950));
360 | --color-teal-50: hsl(var(--hsl-teal-50));
361 | --color-teal-100: hsl(var(--hsl-teal-100));
362 | --color-teal-200: hsl(var(--hsl-teal-200));
363 | --color-teal-300: hsl(var(--hsl-teal-300));
364 | --color-teal-400: hsl(var(--hsl-teal-400));
365 | --color-teal-500: hsl(var(--hsl-teal-500));
366 | --color-teal-600: hsl(var(--hsl-teal-600));
367 | --color-teal-700: hsl(var(--hsl-teal-700));
368 | --color-teal-800: hsl(var(--hsl-teal-800));
369 | --color-teal-900: hsl(var(--hsl-teal-900));
370 | --color-teal-950: hsl(var(--hsl-teal-950));
371 | --color-emerald-50: hsl(var(--hsl-emerald-50));
372 | --color-emerald-100: hsl(var(--hsl-emerald-100));
373 | --color-emerald-200: hsl(var(--hsl-emerald-200));
374 | --color-emerald-300: hsl(var(--hsl-emerald-300));
375 | --color-emerald-400: hsl(var(--hsl-emerald-400));
376 | --color-emerald-500: hsl(var(--hsl-emerald-500));
377 | --color-emerald-600: hsl(var(--hsl-emerald-600));
378 | --color-emerald-700: hsl(var(--hsl-emerald-700));
379 | --color-emerald-800: hsl(var(--hsl-emerald-800));
380 | --color-emerald-900: hsl(var(--hsl-emerald-900));
381 | --color-emerald-950: hsl(var(--hsl-emerald-950));
382 | --color-green-50: hsl(var(--hsl-green-50));
383 | --color-green-100: hsl(var(--hsl-green-100));
384 | --color-green-200: hsl(var(--hsl-green-200));
385 | --color-green-300: hsl(var(--hsl-green-300));
386 | --color-green-400: hsl(var(--hsl-green-400));
387 | --color-green-500: hsl(var(--hsl-green-500));
388 | --color-green-600: hsl(var(--hsl-green-600));
389 | --color-green-700: hsl(var(--hsl-green-700));
390 | --color-green-800: hsl(var(--hsl-green-800));
391 | --color-green-900: hsl(var(--hsl-green-900));
392 | --color-green-950: hsl(var(--hsl-green-950));
393 | --color-lime-50: hsl(var(--hsl-lime-50));
394 | --color-lime-100: hsl(var(--hsl-lime-100));
395 | --color-lime-200: hsl(var(--hsl-lime-200));
396 | --color-lime-300: hsl(var(--hsl-lime-300));
397 | --color-lime-400: hsl(var(--hsl-lime-400));
398 | --color-lime-500: hsl(var(--hsl-lime-500));
399 | --color-lime-600: hsl(var(--hsl-lime-600));
400 | --color-lime-700: hsl(var(--hsl-lime-700));
401 | --color-lime-800: hsl(var(--hsl-lime-800));
402 | --color-lime-900: hsl(var(--hsl-lime-900));
403 | --color-lime-950: hsl(var(--hsl-lime-950));
404 | --color-yellow-50: hsl(var(--hsl-yellow-50));
405 | --color-yellow-100: hsl(var(--hsl-yellow-100));
406 | --color-yellow-200: hsl(var(--hsl-yellow-200));
407 | --color-yellow-300: hsl(var(--hsl-yellow-300));
408 | --color-yellow-400: hsl(var(--hsl-yellow-400));
409 | --color-yellow-500: hsl(var(--hsl-yellow-500));
410 | --color-yellow-600: hsl(var(--hsl-yellow-600));
411 | --color-yellow-700: hsl(var(--hsl-yellow-700));
412 | --color-yellow-800: hsl(var(--hsl-yellow-800));
413 | --color-yellow-900: hsl(var(--hsl-yellow-900));
414 | --color-yellow-950: hsl(var(--hsl-yellow-950));
415 | --color-amber-50: hsl(var(--hsl-amber-50));
416 | --color-amber-100: hsl(var(--hsl-amber-100));
417 | --color-amber-200: hsl(var(--hsl-amber-200));
418 | --color-amber-300: hsl(var(--hsl-amber-300));
419 | --color-amber-400: hsl(var(--hsl-amber-400));
420 | --color-amber-500: hsl(var(--hsl-amber-500));
421 | --color-amber-600: hsl(var(--hsl-amber-600));
422 | --color-amber-700: hsl(var(--hsl-amber-700));
423 | --color-amber-800: hsl(var(--hsl-amber-800));
424 | --color-amber-900: hsl(var(--hsl-amber-900));
425 | --color-amber-950: hsl(var(--hsl-amber-950));
426 | --color-orange-50: hsl(var(--hsl-orange-50));
427 | --color-orange-100: hsl(var(--hsl-orange-100));
428 | --color-orange-200: hsl(var(--hsl-orange-200));
429 | --color-orange-300: hsl(var(--hsl-orange-300));
430 | --color-orange-400: hsl(var(--hsl-orange-400));
431 | --color-orange-500: hsl(var(--hsl-orange-500));
432 | --color-orange-600: hsl(var(--hsl-orange-600));
433 | --color-orange-700: hsl(var(--hsl-orange-700));
434 | --color-orange-800: hsl(var(--hsl-orange-800));
435 | --color-orange-900: hsl(var(--hsl-orange-900));
436 | --color-orange-950: hsl(var(--hsl-orange-950));
437 | --color-red-50: hsl(var(--hsl-red-50));
438 | --color-red-100: hsl(var(--hsl-red-100));
439 | --color-red-200: hsl(var(--hsl-red-200));
440 | --color-red-300: hsl(var(--hsl-red-300));
441 | --color-red-400: hsl(var(--hsl-red-400));
442 | --color-red-500: hsl(var(--hsl-red-500));
443 | --color-red-600: hsl(var(--hsl-red-600));
444 | --color-red-700: hsl(var(--hsl-red-700));
445 | --color-red-800: hsl(var(--hsl-red-800));
446 | --color-red-900: hsl(var(--hsl-red-900));
447 | --color-red-950: hsl(var(--hsl-red-950));
448 | --color-neutral-50: hsl(var(--hsl-neutral-50));
449 | --color-neutral-100: hsl(var(--hsl-neutral-100));
450 | --color-neutral-200: hsl(var(--hsl-neutral-200));
451 | --color-neutral-300: hsl(var(--hsl-neutral-300));
452 | --color-neutral-400: hsl(var(--hsl-neutral-400));
453 | --color-neutral-500: hsl(var(--hsl-neutral-500));
454 | --color-neutral-600: hsl(var(--hsl-neutral-600));
455 | --color-neutral-700: hsl(var(--hsl-neutral-700));
456 | --color-neutral-800: hsl(var(--hsl-neutral-800));
457 | --color-neutral-900: hsl(var(--hsl-neutral-900));
458 | --color-neutral-950: hsl(var(--hsl-neutral-950));
459 | --color-stone-50: hsl(var(--hsl-stone-50));
460 | --color-stone-100: hsl(var(--hsl-stone-100));
461 | --color-stone-200: hsl(var(--hsl-stone-200));
462 | --color-stone-300: hsl(var(--hsl-stone-300));
463 | --color-stone-400: hsl(var(--hsl-stone-400));
464 | --color-stone-500: hsl(var(--hsl-stone-500));
465 | --color-stone-600: hsl(var(--hsl-stone-600));
466 | --color-stone-700: hsl(var(--hsl-stone-700));
467 | --color-stone-800: hsl(var(--hsl-stone-800));
468 | --color-stone-900: hsl(var(--hsl-stone-900));
469 | --color-stone-950: hsl(var(--hsl-stone-950));
470 | --color-zinc-50: hsl(var(--hsl-zinc-50));
471 | --color-zinc-100: hsl(var(--hsl-zinc-100));
472 | --color-zinc-200: hsl(var(--hsl-zinc-200));
473 | --color-zinc-300: hsl(var(--hsl-zinc-300));
474 | --color-zinc-400: hsl(var(--hsl-zinc-400));
475 | --color-zinc-500: hsl(var(--hsl-zinc-500));
476 | --color-zinc-600: hsl(var(--hsl-zinc-600));
477 | --color-zinc-700: hsl(var(--hsl-zinc-700));
478 | --color-zinc-800: hsl(var(--hsl-zinc-800));
479 | --color-zinc-900: hsl(var(--hsl-zinc-900));
480 | --color-zinc-950: hsl(var(--hsl-zinc-950));
481 | --color-gray-50: hsl(var(--hsl-gray-50));
482 | --color-gray-100: hsl(var(--hsl-gray-100));
483 | --color-gray-200: hsl(var(--hsl-gray-200));
484 | --color-gray-300: hsl(var(--hsl-gray-300));
485 | --color-gray-400: hsl(var(--hsl-gray-400));
486 | --color-gray-500: hsl(var(--hsl-gray-500));
487 | --color-gray-600: hsl(var(--hsl-gray-600));
488 | --color-gray-700: hsl(var(--hsl-gray-700));
489 | --color-gray-800: hsl(var(--hsl-gray-800));
490 | --color-gray-900: hsl(var(--hsl-gray-900));
491 | --color-gray-950: hsl(var(--hsl-gray-950));
492 | --color-slate-50: hsl(var(--hsl-slate-50));
493 | --color-slate-100: hsl(var(--hsl-slate-100));
494 | --color-slate-200: hsl(var(--hsl-slate-200));
495 | --color-slate-300: hsl(var(--hsl-slate-300));
496 | --color-slate-400: hsl(var(--hsl-slate-400));
497 | --color-slate-500: hsl(var(--hsl-slate-500));
498 | --color-slate-600: hsl(var(--hsl-slate-600));
499 | --color-slate-700: hsl(var(--hsl-slate-700));
500 | --color-slate-800: hsl(var(--hsl-slate-800));
501 | --color-slate-900: hsl(var(--hsl-slate-900));
502 | --color-slate-950: hsl(var(--hsl-slate-950));
503 | }
504 | }
505 |
--------------------------------------------------------------------------------