First Commit
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
dsql-schema-to-typedef.json
|
||||||
|
.data
|
||||||
|
.bunext
|
||||||
1
.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
|
||||||
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"css.lint.unknownAtRules": "ignore"
|
||||||
|
}
|
||||||
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM oven/bun:1.3-debian
|
||||||
|
|
||||||
|
RUN apt update
|
||||||
|
RUN apt install -y curl bash nano wget zip unzip
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN touch /root/.bashrc
|
||||||
|
RUN echo 'alias ll="ls -laF"' >/root/.bashrc
|
||||||
|
|
||||||
|
COPY . /app/.
|
||||||
|
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
CMD ["bun", "start"]
|
||||||
7
buncid.config.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"start": "bun start",
|
||||||
|
"preflight": ["bunx buncid-builds-next"],
|
||||||
|
"postflight": ["echo 'Server Running ...'"],
|
||||||
|
"first_run": true,
|
||||||
|
"port": [3000]
|
||||||
|
}
|
||||||
6
bunext.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { BunextConfig } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const config: BunextConfig = {
|
||||||
|
port: 3070,
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
3
bunfig.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[install.scopes]
|
||||||
|
|
||||||
|
"@moduletrace" = "https://git.tben.me/api/packages/moduletrace/npm/"
|
||||||
41
package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "new-personal-site",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "NODE_ENV=development bunx bunext dev",
|
||||||
|
"build": "bunx bunext build",
|
||||||
|
"start": "NODE_ENV=production bunx bunext start",
|
||||||
|
"schema-to-typedef": "bunx dsql-schema-to-typedef"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@moduletrace/buncid": "^1.0.7",
|
||||||
|
"@moduletrace/bunext": "^1.0.40",
|
||||||
|
"@moduletrace/datasquirel": "^5.7.51",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"html-to-react": "^1.7.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lucide-react": "^0.462.0",
|
||||||
|
"marked": "^17.0.5",
|
||||||
|
"openai": "^6.21.0",
|
||||||
|
"prism-react-renderer": "^2.4.1",
|
||||||
|
"prism-themes": "^1.9.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"rehype-prism-plus": "^2.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"showdown": "^2.1.0",
|
||||||
|
"tailwind-merge": "^2.5.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
"@types/lodash": "^4.17.13",
|
||||||
|
"@types/marked": "^6.0.0",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@types/showdown": "^2.0.6",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/documents/Resume-Benjamin-Toby-Linkedin.pdf
Normal file
BIN
public/documents/Resume-Benjamin-Toby.pdf
Normal file
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 320 B |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 320 B |
BIN
public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 984 B |
BIN
public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/icon-256x256.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/icons/touch-icon-ipad-retina.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/touch-icon-ipad.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/touch-icon-iphone-retina.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/touch-icon-iphone.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/Base-Fitness-Screenshot.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/images/Homeruntoken-graphic.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/images/Module-Trace-Usage.gif
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/images/Module-trace-image.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/images/castcord-graphic.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/images/datasquirel-img.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/images/external-link-dark.png
Normal file
|
After Width: | Height: | Size: 983 B |
BIN
public/images/external-link.png
Normal file
|
After Width: | Height: | Size: 785 B |
BIN
public/images/github-white.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/images/guaranteed.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
public/images/linkedin-white.png
Normal file
|
After Width: | Height: | Size: 919 B |
1
public/images/logo-icon-white.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 611 611" width="611pt" height="611pt"><defs><clipPath id="_clipPath_2zfa3boh6FNyOkB9zT4Cm84iY9vA0NWV"><rect width="611" height="611"/></clipPath></defs><g clip-path="url(#_clipPath_2zfa3boh6FNyOkB9zT4Cm84iY9vA0NWV)"><rect x="0" y="0" width="610.63" height="610.63" transform="matrix(1,0,0,1,0,0)" fill="none"/><clipPath id="_clipPath_7KSpsgRxupOAmqFAFnlf5LCTNooKCwdC"><rect x="0" y="0" width="610.63" height="610.63" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_7KSpsgRxupOAmqFAFnlf5LCTNooKCwdC)"><g><g><g><path d=" M 610.63 515.37 L 610.63 619.37 L 590.72 607.63 L 461.72 531.55 L 306.83 617 L 151.94 702.39 L 138.33 709.89 L 0 786.14 L 0 0 L 362 0 C 434.173 0 490.98 15.707 532.42 47.12 C 534.38 48.613 536.297 50.137 538.17 51.69 C 575.177 82.423 593.98 125.773 594.58 181.74 L 594.58 184.49 C 594.58 225.95 583.717 260.877 561.99 289.27 C 540.263 317.663 511.353 336.547 475.26 345.92 C 518.027 355.92 551.283 376.477 575.03 407.59 C 598.777 438.703 610.643 474.63 610.63 515.37 Z M 368.49 519.89 C 378.163 512.21 383 499.673 383 482.28 C 383 448.193 363.28 431.15 323.84 431.15 L 222.6 431.15 L 222.6 531.42 L 323.87 531.42 C 343.923 531.42 358.797 527.577 368.49 519.89 Z M 368 227.61 C 368 210.23 363.153 197.36 353.46 189 C 343.767 180.64 328.893 176.463 308.84 176.47 L 222.6 176.47 L 222.6 276.74 L 308.83 276.74 C 328.877 276.74 343.75 272.74 353.45 264.74 C 363.15 256.74 368 244.363 368 227.61 Z " fill="rgb(255,255,255)"/></g></g></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
15
public/images/logo-icon.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 611 611" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<rect id="Artboard1" x="0" y="0" width="610.63" height="610.63" style="fill:none;"/>
|
||||||
|
<clipPath id="_clip1">
|
||||||
|
<rect id="Artboard11" serif:id="Artboard1" x="0" y="0" width="610.63" height="610.63"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip1)">
|
||||||
|
<g id="Layer_2">
|
||||||
|
<g id="Layer_1-2">
|
||||||
|
<path d="M610.63,515.37L610.63,619.37L590.72,607.63L461.72,531.55L306.83,617L151.94,702.39L138.33,709.89L0,786.14L0,0L362,0C434.173,0 490.98,15.707 532.42,47.12C534.38,48.613 536.297,50.137 538.17,51.69C575.177,82.423 593.98,125.773 594.58,181.74L594.58,184.49C594.58,225.95 583.717,260.877 561.99,289.27C540.263,317.663 511.353,336.547 475.26,345.92C518.027,355.92 551.283,376.477 575.03,407.59C598.777,438.703 610.643,474.63 610.63,515.37ZM368.49,519.89C378.163,512.21 383,499.673 383,482.28C383,448.193 363.28,431.15 323.84,431.15L222.6,431.15L222.6,531.42L323.87,531.42C343.923,531.42 358.797,527.577 368.49,519.89ZM368,227.61C368,210.23 363.153,197.36 353.46,189C343.767,180.64 328.893,176.463 308.84,176.47L222.6,176.47L222.6,276.74L308.83,276.74C328.877,276.74 343.75,272.74 353.45,264.74C363.15,256.74 368,244.363 368,227.61Z" style="fill:rgb(99,105,176);fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/images/logo-v3-cropped.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
public/images/logo-v3.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
1
public/images/logo-v3.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 610.63 1042.32"><defs><style>.cls-1{fill:#565e9a;}.cls-2{fill:#6369b0;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><polygon class="cls-1" points="610.63 784.67 610.63 952.95 461.71 1042.32 461.71 874.03 464.52 872.35 610.63 784.67"/><polygon class="cls-1" points="610.63 619.33 610.63 784.67 461.71 702.53 468.56 698.7 563.9 645.44 610.63 619.33"/><path class="cls-2" d="M610.63,515.37v104l-19.91-11.74-129-76.08L306.83,617,151.94,702.39l-13.61,7.5L0,786.14V0H362Q470.26,0,532.42,47.12q2.94,2.24,5.75,4.57,55.51,46.1,56.41,130.05c0,.92,0,1.83,0,2.75q0,62.19-32.59,104.78t-86.73,56.65q64.15,15,99.77,61.67T610.63,515.37Zm-242.14,4.52Q383,508.37,383,482.28q0-51.13-59.16-51.13H222.6V531.42H323.87Q353.95,531.42,368.49,519.89ZM368,227.61q0-26.07-14.54-38.61t-44.62-12.53H222.6V276.74h86.23q30.07,0,44.62-12T368,227.61Z"/><polygon class="cls-2" points="461.71 874.03 461.71 1042.32 306.83 957.3 306.83 789.02 461.71 874.03"/><polygon class="cls-1" points="306.83 789.02 306.83 957.3 151.94 1042.32 151.94 875.51 156.15 873.19 306.83 789.02"/><polygon class="cls-2" points="151.94 875.51 151.94 1042.32 0 958.98 0 786.14 151.94 875.51"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
14
public/images/logo-white.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 611 1043" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g id="Layer_2">
|
||||||
|
<g id="Layer_1-2">
|
||||||
|
<path d="M610.63,784.67L610.63,952.95L461.71,1042.32L461.71,874.03L464.52,872.35L610.63,784.67Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M610.63,619.33L610.63,784.67L461.71,702.53L468.56,698.7L563.9,645.44L610.63,619.33Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M610.63,515.37L610.63,619.37L590.72,607.63L461.72,531.55L306.83,617L151.94,702.39L138.33,709.89L0,786.14L0,0L362,0C434.173,0 490.98,15.707 532.42,47.12C534.38,48.613 536.297,50.137 538.17,51.69C575.177,82.423 593.98,125.773 594.58,181.74L594.58,184.49C594.58,225.95 583.717,260.877 561.99,289.27C540.263,317.663 511.353,336.547 475.26,345.92C518.027,355.92 551.283,376.477 575.03,407.59C598.777,438.703 610.643,474.63 610.63,515.37ZM368.49,519.89C378.163,512.21 383,499.673 383,482.28C383,448.193 363.28,431.15 323.84,431.15L222.6,431.15L222.6,531.42L323.87,531.42C343.923,531.42 358.797,527.577 368.49,519.89ZM368,227.61C368,210.23 363.153,197.36 353.46,189C343.767,180.64 328.893,176.463 308.84,176.47L222.6,176.47L222.6,276.74L308.83,276.74C328.877,276.74 343.75,272.74 353.45,264.74C363.15,256.74 368,244.363 368,227.61Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M461.71,874.03L461.71,1042.32L306.83,957.3L306.83,789.02L461.71,874.03Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M306.83,789.02L306.83,957.3L151.94,1042.32L151.94,875.51L156.15,873.19L306.83,789.02Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M151.94,875.51L151.94,1042.32L0,958.98L0,786.14L151.94,875.51Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/mattermost-logo.webp
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
public/images/my-photo-2.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
public/images/my-photo-3.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
public/images/my-photo-stroked.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
public/images/my-photo.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
public/images/next-7-graphic-min.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
public/images/next7screenshot.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
public/images/programming-laptop.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/images/projects-section-image.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
public/images/renition-graphic.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/images/rm378-07c-min.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
public/images/showmerebates.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/images/showmerebates.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
BIN
public/images/stirrmediascreenshot.png
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
public/images/summit-lending.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/images/trader-hub-graphic-min.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
public/images/traderhubscreenshot.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/images/why-so-serious.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
11
public/robots.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://www.tben.me robots.txt
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemaps
|
||||||
|
Sitemap: https://www.tben.me/sitemap.xml
|
||||||
|
|
||||||
|
# Optional: disallow common non-content paths (uncomment if needed)
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
58
public/sitemap.xml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<!-- Homepage -->
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<!-- Main Pages -->
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/about</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/skills</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/work</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/blog</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/contact</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<!-- Blog Posts -->
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/blog/nginx-reverse-proxy-caching-rate-limiting-static-files</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/blog/solving-the-database-hassle</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/blog/find-your-perfect-framework</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.tben.me/blog/choosing-your-tech-stack</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
22
src/components/general/Logo.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
type Props = {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
export default function Logo({ size }: Props) {
|
||||||
|
const sizeRatio = 50 / 100;
|
||||||
|
const width = size || 50;
|
||||||
|
const height = width * sizeRatio;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href="/">
|
||||||
|
<img
|
||||||
|
src="/images/logo-white.svg"
|
||||||
|
alt="Main Logo"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
style={{
|
||||||
|
minWidth: width + "px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/components/general/markdown-renderer-2.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { Highlight, themes } from "prism-react-renderer";
|
||||||
|
|
||||||
|
const MarkdownRenderer2 = ({ content }: { content: string }) => {
|
||||||
|
return (
|
||||||
|
<ReactMarkdown
|
||||||
|
components={{
|
||||||
|
code({ node, inline, className, children, ...props }: any) {
|
||||||
|
const { key, ...cleanProps } = props;
|
||||||
|
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
const language = match ? match[1] : "";
|
||||||
|
|
||||||
|
if (!inline && match) {
|
||||||
|
return (
|
||||||
|
<Highlight
|
||||||
|
key={key}
|
||||||
|
theme={themes.vsDark}
|
||||||
|
code={String(children).replace(/\n$/, "")}
|
||||||
|
language={language}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
tokens,
|
||||||
|
getLineProps,
|
||||||
|
getTokenProps,
|
||||||
|
}) => (
|
||||||
|
<pre
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
padding: "1.5rem",
|
||||||
|
borderRadius: "8px",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
overflowX: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tokens.map((line, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
{...getLineProps({
|
||||||
|
line,
|
||||||
|
key: i,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{line.map((token, key) => (
|
||||||
|
<span
|
||||||
|
key={key}
|
||||||
|
{...getTokenProps({
|
||||||
|
token,
|
||||||
|
key,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for inline code or code without a language tag
|
||||||
|
return (
|
||||||
|
<code key={key} className={className} {...cleanProps}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownRenderer2;
|
||||||
15
src/components/general/markdown-renderer.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import sd from "showdown";
|
||||||
|
|
||||||
|
const conv = new sd.Converter({});
|
||||||
|
|
||||||
|
const MarkdownRenderer = ({ content }: { content: string }) => {
|
||||||
|
const html = conv.makeHtml(content);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="markdown-rendered"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownRenderer;
|
||||||
75
src/components/lib/(functions)/popver/grab-popover-styles.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import React from "react";
|
||||||
|
import { TWUIPopoverStyles } from "../../elements/Modal";
|
||||||
|
import twuiNumberfy from "../../utils/numberfy";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
targetElRef: React.RefObject<HTMLElement | null>;
|
||||||
|
position: (typeof TWUIPopoverStyles)[number];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function twuiGrabPopoverStyles({
|
||||||
|
position,
|
||||||
|
targetElRef,
|
||||||
|
}: Params): React.CSSProperties {
|
||||||
|
if (!targetElRef.current) return {};
|
||||||
|
const rect = targetElRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const targetElCurrStyles = window.getComputedStyle(targetElRef.current);
|
||||||
|
|
||||||
|
const targetElRightPadding = twuiNumberfy(targetElCurrStyles.paddingRight);
|
||||||
|
|
||||||
|
let popoverStyle: React.CSSProperties = {
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultBottomStyle: React.CSSProperties = {
|
||||||
|
top: rect.bottom + window.scrollY + 8,
|
||||||
|
left: rect.left + window.scrollX + rect.width / 2,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTopStyleStyle: React.CSSProperties = {
|
||||||
|
bottom: window.innerHeight - (rect.top + window.scrollY) + 8,
|
||||||
|
left: rect.left + window.scrollX + rect.width / 2,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (position === "bottom") {
|
||||||
|
popoverStyle = _.merge(popoverStyle, defaultBottomStyle);
|
||||||
|
} else if (position === "bottom-left") {
|
||||||
|
popoverStyle = _.merge(
|
||||||
|
popoverStyle,
|
||||||
|
_.omit(defaultBottomStyle, ["transform"]),
|
||||||
|
{
|
||||||
|
left: rect.left,
|
||||||
|
} as React.CSSProperties
|
||||||
|
);
|
||||||
|
} else if (position === "bottom-right") {
|
||||||
|
popoverStyle = _.merge(
|
||||||
|
popoverStyle,
|
||||||
|
_.omit(defaultBottomStyle, ["left", "transform"]),
|
||||||
|
{
|
||||||
|
right:
|
||||||
|
window.innerWidth -
|
||||||
|
(rect.left + window.scrollX) -
|
||||||
|
rect.width -
|
||||||
|
targetElRightPadding,
|
||||||
|
} as React.CSSProperties
|
||||||
|
);
|
||||||
|
} else if (position === "top") {
|
||||||
|
popoverStyle = _.merge(popoverStyle, defaultTopStyleStyle);
|
||||||
|
} else if (position === "right") {
|
||||||
|
popoverStyle.top = rect.top + window.scrollY + rect.height / 2;
|
||||||
|
popoverStyle.left = rect.right + window.scrollX + 8;
|
||||||
|
popoverStyle.transform = "translateY(-50%)";
|
||||||
|
} else if (position === "left") {
|
||||||
|
popoverStyle.top = rect.top + window.scrollY + rect.height / 2;
|
||||||
|
popoverStyle.right =
|
||||||
|
window.innerWidth - (rect.left + window.scrollX) + 8;
|
||||||
|
popoverStyle.transform = "translateY(-50%)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return popoverStyle;
|
||||||
|
}
|
||||||
65
src/components/lib/(partials)/ModalComponent.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { TWUI_MODAL_PROPS } from "../elements/Modal";
|
||||||
|
import Paper from "../elements/Paper";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
type Props = TWUI_MODAL_PROPS & {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Modal Main Component
|
||||||
|
*/
|
||||||
|
export default function ModalComponent({ open, setOpen, ...props }: Props) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"fixed z-[200] top-0 left-0 w-screen h-screen",
|
||||||
|
"flex flex-col items-center justify-center p-4",
|
||||||
|
"twui-modal-root"
|
||||||
|
)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"absolute top-0 left-0 bg-dark/80 z-0",
|
||||||
|
"w-screen h-screen"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<Paper
|
||||||
|
{..._.omit(props, ["targetWrapperProps"])}
|
||||||
|
className={twMerge(
|
||||||
|
"z-10 max-w-modal bg-background-light dark:bg-background-dark",
|
||||||
|
"w-full relative max-h-[95vh] overflow-y-auto",
|
||||||
|
"twui-modal-content",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
<Button
|
||||||
|
className="absolute top-0 right-0 p-2"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
title="Close Modal Button"
|
||||||
|
>
|
||||||
|
<X size={30} />
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</div>,
|
||||||
|
document.getElementById("twui-modal-root") as HTMLElement
|
||||||
|
);
|
||||||
|
}
|
||||||
115
src/components/lib/(partials)/PopoverComponent.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { TWUI_MODAL_PROPS } from "../elements/Modal";
|
||||||
|
import Paper from "../elements/Paper";
|
||||||
|
import _ from "lodash";
|
||||||
|
import twuiGrabPopoverStyles from "../(functions)/popver/grab-popover-styles";
|
||||||
|
|
||||||
|
type Props = TWUI_MODAL_PROPS & {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
targetElRef?: React.RefObject<HTMLElement | null>;
|
||||||
|
popoverTargetActiveRef: React.MutableRefObject<boolean>;
|
||||||
|
popoverContentActiveRef: React.MutableRefObject<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Modal Main Component
|
||||||
|
*/
|
||||||
|
export default function PopoverComponent({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
targetElRef,
|
||||||
|
position = "bottom",
|
||||||
|
trigger = "hover",
|
||||||
|
debounce,
|
||||||
|
popoverTargetActiveRef,
|
||||||
|
popoverContentActiveRef,
|
||||||
|
popoverReferenceRef,
|
||||||
|
isPopover,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const [style, setStyle] = React.useState({});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open && targetElRef?.current) {
|
||||||
|
const popoverStyle = twuiGrabPopoverStyles({
|
||||||
|
position,
|
||||||
|
targetElRef,
|
||||||
|
});
|
||||||
|
setStyle(popoverStyle);
|
||||||
|
}
|
||||||
|
}, [open, targetElRef, position]);
|
||||||
|
|
||||||
|
let closeTimeout: any;
|
||||||
|
|
||||||
|
const popoverEnterFn = React.useCallback(() => {
|
||||||
|
popoverContentActiveRef.current = true;
|
||||||
|
popoverTargetActiveRef.current = false;
|
||||||
|
setOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const popoverLeaveFn = React.useCallback(() => {
|
||||||
|
window.clearTimeout(closeTimeout);
|
||||||
|
closeTimeout = setTimeout(() => {
|
||||||
|
if (popoverTargetActiveRef.current) {
|
||||||
|
popoverTargetActiveRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
}, debounce);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<Paper
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"max-w-[300px] z-[250]",
|
||||||
|
"twui-popover-content",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
style={{ ...style, ...props.style }}
|
||||||
|
onMouseEnter={
|
||||||
|
trigger === "hover" ? popoverEnterFn : props.onMouseEnter
|
||||||
|
}
|
||||||
|
onMouseLeave={
|
||||||
|
trigger === "hover" ? popoverLeaveFn : props.onMouseLeave
|
||||||
|
}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
{/* <div
|
||||||
|
className="absolute w-0 h-0 border-8 border-transparent bg-white"
|
||||||
|
style={{
|
||||||
|
...(position === "bottom" && {
|
||||||
|
top: "-16px",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}),
|
||||||
|
...(position === "top" && {
|
||||||
|
bottom: "-16px",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}),
|
||||||
|
...(position === "right" && {
|
||||||
|
top: "50%",
|
||||||
|
left: "-16px",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}),
|
||||||
|
...(position === "left" && {
|
||||||
|
top: "50%",
|
||||||
|
right: "-16px",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
{props.children}
|
||||||
|
</Paper>,
|
||||||
|
document.getElementById("twui-popover-root") as HTMLElement
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/components/lib/Readme.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Tailwind CSS UI
|
||||||
|
|
||||||
|
A modular skeletal framework for tailwind css
|
||||||
|
|
||||||
|
## Perequisites
|
||||||
|
|
||||||
|
You need a couple of packages and settings to integrate this package
|
||||||
|
|
||||||
|
### Packages
|
||||||
|
|
||||||
|
- React
|
||||||
|
- React Dom
|
||||||
|
- Tailwind CSS **version 4**
|
||||||
|
|
||||||
|
### CSS Base
|
||||||
|
|
||||||
|
This package contains a `base.css` file which has all the base css rules required to run. This css file must be imported in your base project, and it can be update in a separate `.css` file.
|
||||||
|
|
||||||
|
### Install packages
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun add lucide-react tailwind-merge html-to-react gray-matter mdx typescript lodash react-code-blocks react-responsive-modal next-mdx-remote remark-gfm rehype-prism-plus openai
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun add -D @types/ace @types/react @types/react-dom tailwindcss @types/mdx @next/mdx
|
||||||
|
```
|
||||||
173
src/components/lib/base.css
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--breakpoint-xs: 350px;
|
||||||
|
--breakpoint-xxs: 300px;
|
||||||
|
--breakpoint-xxl: 1600px;
|
||||||
|
|
||||||
|
--color-background-light: #ffffff;
|
||||||
|
--color-foreground-light: #171717;
|
||||||
|
--color-background-dark: #0a0a0a;
|
||||||
|
--color-foreground-dark: #ededed;
|
||||||
|
|
||||||
|
--color-dark: #000000;
|
||||||
|
|
||||||
|
--color-primary: #000000;
|
||||||
|
--color-primary-hover: #29292b;
|
||||||
|
--color-primary-outline: #29292b;
|
||||||
|
--color-primary-text: #29292b;
|
||||||
|
--color-primary-dark: #29292b;
|
||||||
|
--color-primary-dark-hover: #4b4b4b;
|
||||||
|
--color-primary-dark-outline: #4b4b4b;
|
||||||
|
--color-primary-dark-text: #ffffff;
|
||||||
|
|
||||||
|
--color-secondary: #000000;
|
||||||
|
--color-secondary-hover: #dddddd;
|
||||||
|
--color-secondary-outline: #dddddd;
|
||||||
|
--color-secondary-text: #dddddd;
|
||||||
|
--color-secondary-dark: #000000;
|
||||||
|
--color-secondary-dark-hover: #dddddd;
|
||||||
|
--color-secondary-dark-outline: #dddddd;
|
||||||
|
--color-secondary-dark-text: #dddddd;
|
||||||
|
|
||||||
|
--color-accent: #000000;
|
||||||
|
--color-accent-hover: #dddddd;
|
||||||
|
--color-accent-outline: #dddddd;
|
||||||
|
--color-accent-text: #dddddd;
|
||||||
|
--color-accent-dark: #000000;
|
||||||
|
--color-accent-dark-hover: #dddddd;
|
||||||
|
--color-accent-dark-outline: #dddddd;
|
||||||
|
--color-accent-dark-text: #dddddd;
|
||||||
|
|
||||||
|
--color-gray: #dfe6ef;
|
||||||
|
--color-gray-hover: #dfe6ef;
|
||||||
|
--color-gray-dark: #1d2b3f;
|
||||||
|
--color-gray-dark-hover: #132033;
|
||||||
|
|
||||||
|
--color-success: #0aa156;
|
||||||
|
--color-success-dark: #0aa156;
|
||||||
|
|
||||||
|
--color-error: #e5484d;
|
||||||
|
--color-error-dark: #e5484d;
|
||||||
|
|
||||||
|
--color-warning: #ff6900;
|
||||||
|
|
||||||
|
--color-link: #0051c9;
|
||||||
|
--color-link-dark: #548adb;
|
||||||
|
|
||||||
|
--radius-default: 5px;
|
||||||
|
--radius-default-sm: 3px;
|
||||||
|
--radius-default-xs: 1px;
|
||||||
|
--radius-default-lg: 7px;
|
||||||
|
--radius-default-xl: 10px;
|
||||||
|
|
||||||
|
--container-container: 1200px;
|
||||||
|
--container-modal: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background-light dark:bg-background-dark;
|
||||||
|
@apply text-foreground-light dark:text-foreground-dark;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tox-tinymce {
|
||||||
|
@apply w-full !rounded-default !border-slate-300 dark:!border-white/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .moving-object {
|
||||||
|
@apply !bg-green-500;
|
||||||
|
} */
|
||||||
|
|
||||||
|
option {
|
||||||
|
@apply dark:bg-background-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-paper-hidden {
|
||||||
|
@apply max-md:p-0 max-md:border-none max-md:bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
@apply w-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-gray rounded-full dark:bg-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-foreground-light/40 rounded-full hover:bg-foreground-light/60;
|
||||||
|
@apply dark:bg-foreground-dark/40 rounded-full hover:bg-foreground-dark/60;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: theme("colors.gray.400") theme("colors.gray.100");
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (selector(:where(*))) {
|
||||||
|
:where(*) {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: theme("colors.gray.400") theme("colors.gray.100");
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark :where(*) {
|
||||||
|
scrollbar-color: theme("colors.gray.500") theme("colors.gray.800");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace_editor {
|
||||||
|
@apply dark:bg-background-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tox-editor-header,
|
||||||
|
.tox-toolbar-overlord,
|
||||||
|
.tox .tox-toolbar,
|
||||||
|
.tox .tox-toolbar__overflow,
|
||||||
|
.tox .tox-toolbar__primary,
|
||||||
|
.tox .tox-tbtn,
|
||||||
|
.tox .tox-sidebar,
|
||||||
|
.tox .tox-statusbar,
|
||||||
|
.tox .tox-view-wrap,
|
||||||
|
.tox .tox-view-wrap__slot-container,
|
||||||
|
.tox .tox-editor-container,
|
||||||
|
.tox .tox-edit-area__iframe,
|
||||||
|
.twui-tinymce {
|
||||||
|
@apply dark:!bg-background-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twui-tinymce *:focus {
|
||||||
|
@apply !outline-white/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tox .tox-tbtn:hover {
|
||||||
|
@apply dark:!bg-white/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace_gutter {
|
||||||
|
@apply dark:!bg-background-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace_active-line,
|
||||||
|
.ace_gutter-active-line {
|
||||||
|
@apply dark:!bg-white/5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal-text {
|
||||||
|
@apply text-foreground-light dark:text-foreground-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
193
src/components/lib/bun.lock
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "tailwind-ui",
|
||||||
|
"dependencies": {
|
||||||
|
"@xterm/xterm": "latest",
|
||||||
|
"html-to-react": "^1.7.0",
|
||||||
|
"lodash": "latest",
|
||||||
|
"lucide-react": "latest",
|
||||||
|
"react-code-blocks": "latest",
|
||||||
|
"react-responsive-modal": "latest",
|
||||||
|
"tailwind-merge": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@next/mdx": "latest",
|
||||||
|
"@types/ace": "latest",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/lodash": "latest",
|
||||||
|
"@types/mdx": "latest",
|
||||||
|
"@types/node": "latest",
|
||||||
|
"@types/react": "latest",
|
||||||
|
"@types/react-dom": "latest",
|
||||||
|
"postcss": "latest",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||||
|
|
||||||
|
"@bedrock-layout/use-forwarded-ref": ["@bedrock-layout/use-forwarded-ref@1.6.1", "", { "dependencies": { "@bedrock-layout/use-stateful-ref": "^1.4.1" }, "peerDependencies": { "react": "^16.8 || ^17 || ^18" } }, "sha512-GD9A9AFLzFNjr7k6fgerSqxfwDWl+wsPS11PErOKe1zkVz0y7RGC9gzlOiX/JrgpyB3NFHWIuGtoOQqifJQQpw=="],
|
||||||
|
|
||||||
|
"@bedrock-layout/use-stateful-ref": ["@bedrock-layout/use-stateful-ref@1.4.1", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18" } }, "sha512-4eKO2KdQEXcR5LI4QcxqlJykJUDQJWDeWYAukIn6sRQYoabcfI5kDl61PUi6FR6o8VFgQ8IEP7HleKqWlSe8SQ=="],
|
||||||
|
|
||||||
|
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.2.2", "", { "dependencies": { "@emotion/memoize": "^0.8.1" } }, "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw=="],
|
||||||
|
|
||||||
|
"@emotion/memoize": ["@emotion/memoize@0.8.1", "", {}, "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="],
|
||||||
|
|
||||||
|
"@emotion/unitless": ["@emotion/unitless@0.8.1", "", {}, "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="],
|
||||||
|
|
||||||
|
"@next/mdx": ["@next/mdx@15.3.2", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-D6lSSbVzn1EiPwrBKG5QzXClcgdqiNCL8a3/6oROinzgZnYSxbVmnfs0UrqygtGSOmgW7sdJJSEOy555DoAwvw=="],
|
||||||
|
|
||||||
|
"@types/ace": ["@types/ace@0.0.52", "", {}, "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||||
|
|
||||||
|
"@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
||||||
|
|
||||||
|
"@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="],
|
||||||
|
|
||||||
|
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.15.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3vMNr4TzNQyjHcRZadojpRaD9Ofr6LsonZAoQ+HMUa/9ORTPoxVIw0e0mpqWpdjj8xybyCM+oKOUH2vwFu/oEw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="],
|
||||||
|
|
||||||
|
"@types/stylis": ["@types/stylis@4.2.5", "", {}, "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
|
"@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
|
||||||
|
|
||||||
|
"body-scroll-lock": ["body-scroll-lock@3.1.5", "", {}, "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
|
||||||
|
|
||||||
|
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
||||||
|
|
||||||
|
"character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
|
||||||
|
|
||||||
|
"character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
|
||||||
|
|
||||||
|
"character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
|
||||||
|
|
||||||
|
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||||
|
|
||||||
|
"comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="],
|
||||||
|
|
||||||
|
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
|
||||||
|
|
||||||
|
"css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
|
|
||||||
|
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||||
|
|
||||||
|
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||||
|
|
||||||
|
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
|
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
|
||||||
|
|
||||||
|
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||||
|
|
||||||
|
"hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],
|
||||||
|
|
||||||
|
"hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
|
||||||
|
|
||||||
|
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
|
||||||
|
|
||||||
|
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
|
||||||
|
|
||||||
|
"html-to-react": ["html-to-react@1.7.0", "", { "dependencies": { "domhandler": "^5.0", "htmlparser2": "^9.0", "lodash.camelcase": "^4.3.0" }, "peerDependencies": { "react": "^0.13.0 || ^0.14.0 || >=15" } }, "sha512-b5HTNaTGyOj5GGIMiWVr1k57egAZ/vGy0GGefnCQ1VW5hu9+eku8AXHtf2/DeD95cj/FKBKYa1J7SWBOX41yUQ=="],
|
||||||
|
|
||||||
|
"htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
|
||||||
|
|
||||||
|
"is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
|
||||||
|
|
||||||
|
"is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
|
||||||
|
|
||||||
|
"is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="],
|
||||||
|
|
||||||
|
"is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||||
|
|
||||||
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
|
|
||||||
|
"property-information": ["property-information@5.6.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
|
"react-code-blocks": ["react-code-blocks@0.1.6", "", { "dependencies": { "@babel/runtime": "^7.10.4", "react-syntax-highlighter": "^15.5.0", "styled-components": "^6.1.0", "tslib": "^2.6.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||||
|
|
||||||
|
"react-responsive-modal": ["react-responsive-modal@6.4.2", "", { "dependencies": { "@bedrock-layout/use-forwarded-ref": "^1.3.1", "body-scroll-lock": "^3.1.5", "classnames": "^2.3.1" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18", "react-dom": "^16.8.0 || ^17 || ^18" } }, "sha512-ARjGEKE5Gu5CSvyA8U9ARVbtK4SMAtdXsjtzwtxRlQIHC99RQTnOUctLpl7+/sp1Kg1OJZ6yqvp6ivd4TBueEw=="],
|
||||||
|
|
||||||
|
"react-syntax-highlighter": ["react-syntax-highlighter@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg=="],
|
||||||
|
|
||||||
|
"refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||||
|
|
||||||
|
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
|
||||||
|
|
||||||
|
"styled-components": ["styled-components@6.1.18", "", { "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" } }, "sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw=="],
|
||||||
|
|
||||||
|
"stylis": ["stylis@4.3.2", "", {}, "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.7", "", {}, "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
|
||||||
|
|
||||||
|
"styled-components/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
|
||||||
|
|
||||||
|
"styled-components/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/components/lib/composites/docs/TWUIDocsAside.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { DocsLinkType } from ".";
|
||||||
|
import Stack from "../../layout/Stack";
|
||||||
|
import TWUIDocsLink from "./TWUIDocsLink";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
|
||||||
|
DocsLinks: DocsLinkType[];
|
||||||
|
before?: React.ReactNode;
|
||||||
|
after?: React.ReactNode;
|
||||||
|
autoExpandAll?: boolean;
|
||||||
|
};
|
||||||
|
export default function TWUIDocsAside({
|
||||||
|
DocsLinks,
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
autoExpandAll,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"pb-10 hidden xl:flex sticky top-6",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
{before}
|
||||||
|
{DocsLinks.map((link, index) => (
|
||||||
|
<TWUIDocsLink
|
||||||
|
docLink={link}
|
||||||
|
key={index}
|
||||||
|
autoExpandAll={autoExpandAll}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{after}
|
||||||
|
</Stack>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
128
src/components/lib/composites/docs/TWUIDocsLink.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, {
|
||||||
|
AnchorHTMLAttributes,
|
||||||
|
ComponentProps,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
} from "react";
|
||||||
|
import { DocsLinkType } from ".";
|
||||||
|
import Stack from "../../layout/Stack";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Row from "../../layout/Row";
|
||||||
|
import Divider from "../../layout/Divider";
|
||||||
|
import { ChevronDown, Circle } from "lucide-react";
|
||||||
|
import Button from "../../layout/Button";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
HTMLAnchorElement
|
||||||
|
> & {
|
||||||
|
docLink: DocsLinkType;
|
||||||
|
wrapperProps?: ComponentProps<typeof Stack>;
|
||||||
|
strict?: boolean;
|
||||||
|
childWrapperProps?: ComponentProps<typeof Stack>;
|
||||||
|
autoExpandAll?: boolean;
|
||||||
|
child?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # TWUI Docs Left Aside Link
|
||||||
|
* @note use dataset attribute `data-strict` for strict matching
|
||||||
|
*
|
||||||
|
* @className `twui-docs-left-aside-link`
|
||||||
|
*/
|
||||||
|
export default function TWUIDocsLink({
|
||||||
|
docLink,
|
||||||
|
wrapperProps,
|
||||||
|
childWrapperProps,
|
||||||
|
strict,
|
||||||
|
autoExpandAll,
|
||||||
|
child,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
|
const [expand, setExpand] = React.useState(autoExpandAll || false);
|
||||||
|
const linkRef = React.useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const basePathMatch = window.location.pathname.includes(
|
||||||
|
docLink.href
|
||||||
|
);
|
||||||
|
|
||||||
|
const isStrictMatch = Boolean(
|
||||||
|
linkRef.current?.getAttribute("data-strict")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (strict || isStrictMatch) {
|
||||||
|
setIsActive(window.location.pathname === docLink.href);
|
||||||
|
} else {
|
||||||
|
setIsActive(basePathMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basePathMatch) {
|
||||||
|
setExpand(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
className={twMerge("gap-2 w-full", wrapperProps?.className)}
|
||||||
|
{...wrapperProps}
|
||||||
|
>
|
||||||
|
<Row className="flex-nowrap grow justify-between w-full">
|
||||||
|
{child && <Circle size={6} />}
|
||||||
|
<a
|
||||||
|
href={docLink.href}
|
||||||
|
title={docLink.title}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"twui-docs-left-aside-link whitespace-nowrap",
|
||||||
|
"grow overflow-hidden overflow-ellipsis",
|
||||||
|
isActive ? "active" : "",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
ref={linkRef}
|
||||||
|
data-strict={strict || docLink.strict}
|
||||||
|
>
|
||||||
|
{docLink.title}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{docLink.children?.[0] && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
className={twMerge(
|
||||||
|
"p-1 hover:opacity-100",
|
||||||
|
expand ? "rotate-180 opacity-30" : "opacity-70"
|
||||||
|
)}
|
||||||
|
onClick={() => setExpand(!expand)}
|
||||||
|
title="Docs Aside Links Dropdown Button"
|
||||||
|
>
|
||||||
|
<ChevronDown className="text-slate-500" size={20} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
{docLink.children && expand && (
|
||||||
|
<Row className="items-stretch gap-4 grow w-full flex-nowrap">
|
||||||
|
<Stack
|
||||||
|
className={twMerge(
|
||||||
|
"gap-2 w-full pl-3",
|
||||||
|
childWrapperProps?.className
|
||||||
|
)}
|
||||||
|
{...childWrapperProps}
|
||||||
|
>
|
||||||
|
{docLink.children.map((link, index) => (
|
||||||
|
<TWUIDocsLink
|
||||||
|
key={index}
|
||||||
|
docLink={link}
|
||||||
|
className="opacity-70"
|
||||||
|
autoExpandAll={autoExpandAll}
|
||||||
|
child
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
src/components/lib/composites/docs/TWUIDocsRightAside.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { DocsLinkType } from ".";
|
||||||
|
import Stack from "../../layout/Stack";
|
||||||
|
import TWUIDocsLink from "./TWUIDocsLink";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Span from "../../layout/Span";
|
||||||
|
import Row from "../../layout/Row";
|
||||||
|
import { ArrowUpRight, LinkIcon, ListIcon } from "lucide-react";
|
||||||
|
import Link from "../../layout/Link";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
|
||||||
|
before?: React.ReactNode;
|
||||||
|
after?: React.ReactNode;
|
||||||
|
autoExpandAll?: boolean;
|
||||||
|
editPageURL?: string;
|
||||||
|
};
|
||||||
|
export default function TWUIDocsRightAside({
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
autoExpandAll,
|
||||||
|
editPageURL,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const [links, setLinks] = React.useState<DocsLinkType[]>([]);
|
||||||
|
const [ready, setReady] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!ready) return;
|
||||||
|
|
||||||
|
const headerHrefs = document.querySelectorAll(
|
||||||
|
".twui-docs-header-anchor"
|
||||||
|
);
|
||||||
|
|
||||||
|
const linksArr: DocsLinkType[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < headerHrefs.length; i++) {
|
||||||
|
const anchorEl = headerHrefs[i] as HTMLAnchorElement;
|
||||||
|
|
||||||
|
const isH2Element = anchorEl.querySelector("h2") !== null;
|
||||||
|
|
||||||
|
if (isH2Element) {
|
||||||
|
let newLink: DocsLinkType = {
|
||||||
|
title: anchorEl.textContent || "",
|
||||||
|
href: `#${anchorEl.id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
let nexElIndex = i + 1;
|
||||||
|
|
||||||
|
while (nexElIndex < headerHrefs.length) {
|
||||||
|
const nextElement = headerHrefs[
|
||||||
|
nexElIndex
|
||||||
|
] as HTMLAnchorElement;
|
||||||
|
|
||||||
|
const nextElementH3 = nextElement.querySelector("h3");
|
||||||
|
|
||||||
|
const isNextElementH2 =
|
||||||
|
nextElement.querySelector("h2") !== null;
|
||||||
|
|
||||||
|
if (isNextElementH2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextElementH3) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newLink.children) {
|
||||||
|
newLink.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
newLink.children.push({
|
||||||
|
title: nextElementH3.textContent || "",
|
||||||
|
href: `#${nextElement.id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
nexElIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
linksArr.push(newLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLinks(linksArr);
|
||||||
|
}, [ready]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!links.length) return;
|
||||||
|
|
||||||
|
const headerHrefs = document.querySelectorAll(
|
||||||
|
"a.twui-docs-header-anchor"
|
||||||
|
);
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const id = entry.target.id;
|
||||||
|
|
||||||
|
const link = document.querySelector(
|
||||||
|
`.twui-docs-right-aside a[href="#${id}"]`
|
||||||
|
);
|
||||||
|
if (link) {
|
||||||
|
link.classList.add("active");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const id = entry.target.id;
|
||||||
|
const link = document.querySelector(
|
||||||
|
`.twui-docs-right-aside a[href="#${id}"]`
|
||||||
|
);
|
||||||
|
if (link) {
|
||||||
|
link.classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
headerHrefs.forEach((headerHref) => {
|
||||||
|
observer.observe(headerHref);
|
||||||
|
});
|
||||||
|
}, [links]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setReady(true);
|
||||||
|
}, 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"pb-10 hidden xl:flex min-w-[150px] max-w-[200px]",
|
||||||
|
"sticky top-6 twui-docs-right-aside",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stack className="w-full overflow-hidden">
|
||||||
|
{before}
|
||||||
|
<Stack className="w-full">
|
||||||
|
<Row>
|
||||||
|
<ListIcon size={12} opacity={0.5} />
|
||||||
|
<Span size="smaller" variant="faded">
|
||||||
|
On this page
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<TWUIDocsLink
|
||||||
|
docLink={link}
|
||||||
|
key={index}
|
||||||
|
autoExpandAll
|
||||||
|
childWrapperProps={{
|
||||||
|
className: "pl-2",
|
||||||
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
className:
|
||||||
|
"[&_svg]:hidden [&_a]:text-xs [&_a]:overflow-hidden [&_a]:overflow-ellipsis",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
{after}
|
||||||
|
{editPageURL && (
|
||||||
|
<Link
|
||||||
|
href={editPageURL}
|
||||||
|
target="_blank"
|
||||||
|
className="text-[12px] mt-2"
|
||||||
|
>
|
||||||
|
<Row className="gap-2">
|
||||||
|
<LinkIcon size={12} opacity={0.5} />
|
||||||
|
<span>Edit This Page</span>
|
||||||
|
<ArrowUpRight size={15} className="-ml-1" />
|
||||||
|
</Row>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
src/components/lib/composites/docs/index.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
ComponentProps,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
PropsWithChildren,
|
||||||
|
} from "react";
|
||||||
|
import Stack from "../../layout/Stack";
|
||||||
|
import Container from "../../layout/Container";
|
||||||
|
import Row from "../../layout/Row";
|
||||||
|
import TWUIDocsAside from "./TWUIDocsAside";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Paper from "../../elements/Paper";
|
||||||
|
import TWUIDocsRightAside from "./TWUIDocsRightAside";
|
||||||
|
|
||||||
|
export type DocsLinkType = {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
strict?: boolean;
|
||||||
|
children?: DocsLinkType[];
|
||||||
|
editPage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = PropsWithChildren & {
|
||||||
|
DocsLinks: DocsLinkType[];
|
||||||
|
docsAsideBefore?: React.ReactNode;
|
||||||
|
docsAsideAfter?: React.ReactNode;
|
||||||
|
wrapperProps?: ComponentProps<typeof Stack>;
|
||||||
|
docsContentProps?: ComponentProps<typeof Row>;
|
||||||
|
leftAsideProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLElement>,
|
||||||
|
HTMLElement
|
||||||
|
>;
|
||||||
|
autoExpandAll?: boolean;
|
||||||
|
editPageURL?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # TWUI Docs
|
||||||
|
* @className `twui-docs-content`
|
||||||
|
*/
|
||||||
|
export default function TWUIDocs({
|
||||||
|
children,
|
||||||
|
DocsLinks,
|
||||||
|
docsAsideAfter,
|
||||||
|
docsAsideBefore,
|
||||||
|
wrapperProps,
|
||||||
|
docsContentProps,
|
||||||
|
leftAsideProps,
|
||||||
|
autoExpandAll,
|
||||||
|
editPageURL,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
center
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge("w-full px-4 sm:px-6", wrapperProps?.className)}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<Paper className="xl:p-8 mobile-paper-hidden">
|
||||||
|
<Row
|
||||||
|
{...docsContentProps}
|
||||||
|
className={twMerge(
|
||||||
|
"items-start gap-8 w-full flex-nowrap",
|
||||||
|
docsContentProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TWUIDocsAside
|
||||||
|
DocsLinks={DocsLinks}
|
||||||
|
after={docsAsideAfter}
|
||||||
|
before={docsAsideBefore}
|
||||||
|
autoExpandAll={autoExpandAll}
|
||||||
|
{...leftAsideProps}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"block twui-docs-content pl-0 xl:pl-6 grow",
|
||||||
|
"overflow-hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<TWUIDocsRightAside editPageURL={editPageURL} />
|
||||||
|
</Row>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
src/components/lib/editors/AceEditor.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React, { MutableRefObject } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import AceEditorModes from "./ace-editor-modes";
|
||||||
|
import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||||
|
|
||||||
|
export type AceEditorComponentType = {
|
||||||
|
editorRef?: MutableRefObject<AceAjax.Editor | undefined>;
|
||||||
|
readOnly?: boolean;
|
||||||
|
/** Function to call when Ctrl+Enter is pressed */
|
||||||
|
ctrlEnterFn?: (editor: AceAjax.Editor) => void;
|
||||||
|
content?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
title?: string;
|
||||||
|
mode?: (typeof AceEditorModes)[number];
|
||||||
|
fontSize?: string;
|
||||||
|
previewMode?: boolean;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
delay?: number;
|
||||||
|
wrapperProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
refreshDepArr?: any[];
|
||||||
|
editorOptions?: AceEditorOptions;
|
||||||
|
showLabel?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Powerful Ace Editor
|
||||||
|
* @note **NOTE** head scripts required
|
||||||
|
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js`
|
||||||
|
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js`
|
||||||
|
*/
|
||||||
|
export default function AceEditor({
|
||||||
|
editorRef,
|
||||||
|
readOnly,
|
||||||
|
ctrlEnterFn,
|
||||||
|
content = "",
|
||||||
|
placeholder,
|
||||||
|
mode,
|
||||||
|
fontSize,
|
||||||
|
previewMode,
|
||||||
|
onChange,
|
||||||
|
delay = 500,
|
||||||
|
refreshDepArr,
|
||||||
|
wrapperProps,
|
||||||
|
editorOptions,
|
||||||
|
showLabel,
|
||||||
|
title,
|
||||||
|
}: AceEditorComponentType) {
|
||||||
|
try {
|
||||||
|
const editorElementRef = React.useRef<HTMLDivElement>();
|
||||||
|
const editorRefInstance = React.useRef<AceAjax.Editor>();
|
||||||
|
|
||||||
|
const [refresh, setRefresh] = React.useState(0);
|
||||||
|
const [darkMode, setDarkMode] = React.useState(false);
|
||||||
|
const [ready, setReady] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!ready) return;
|
||||||
|
|
||||||
|
if (!ace?.edit || !editorElementRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setRefresh((prev) => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = ace.edit(editorElementRef.current);
|
||||||
|
|
||||||
|
editor.setOptions({
|
||||||
|
mode: `ace/mode/${mode ? mode : "javascript"}`,
|
||||||
|
theme: darkMode
|
||||||
|
? "ace/theme/tomorrow_night_eighties"
|
||||||
|
: "ace/theme/ace_light",
|
||||||
|
value: (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(content), null, 4);
|
||||||
|
} catch (error) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
placeholder: placeholder ? placeholder : "",
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
|
readOnly: readOnly ? true : false,
|
||||||
|
fontSize: fontSize ? fontSize : null,
|
||||||
|
showLineNumbers: previewMode ? false : true,
|
||||||
|
wrap: true,
|
||||||
|
wrapMethod: "code",
|
||||||
|
...editorOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.commands.addCommand({
|
||||||
|
name: "myCommand",
|
||||||
|
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
|
||||||
|
exec: function (editor) {
|
||||||
|
if (ctrlEnterFn) ctrlEnterFn(editor);
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.getSession().on("change", function (e) {
|
||||||
|
if (onChange) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
onChange(editor.getValue());
|
||||||
|
} catch (error) {}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editorRefInstance.current = editor;
|
||||||
|
if (editorRef) editorRef.current = editor;
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
editor.destroy();
|
||||||
|
};
|
||||||
|
}, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const htmlClassName = document.documentElement.className;
|
||||||
|
if (htmlClassName.match(/dark/i)) setDarkMode(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setReady(true);
|
||||||
|
}, 200);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full h-[400px] block rounded-default",
|
||||||
|
"border border-slate-200 border-solid relative",
|
||||||
|
"dark:border-white/20",
|
||||||
|
showLabel && title ? "pt-4" : "",
|
||||||
|
wrapperProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{showLabel && title ? (
|
||||||
|
<label
|
||||||
|
className={twMerge(
|
||||||
|
"bg-background-light dark:bg-background-dark text-xs",
|
||||||
|
"-top-3 left-2 px-2 py-1 absolute z-10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
ref={editorElementRef as any}
|
||||||
|
className="w-full h-full"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className="m-0">
|
||||||
|
Editor Error:{" "}
|
||||||
|
<b className="text-red-600">{error.message}</b>
|
||||||
|
</span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/components/lib/editors/TinyMCE/index.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import React, { ComponentProps, MutableRefObject } from "react";
|
||||||
|
import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
|
||||||
|
import Border from "../../elements/Border";
|
||||||
|
import useTinyMCE from "./useTinyMCE";
|
||||||
|
|
||||||
|
export type TinyMCEEditorProps<KeyType extends string> = {
|
||||||
|
options?: RawEditorOptions;
|
||||||
|
editorRef?: React.MutableRefObject<Editor | null>;
|
||||||
|
setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
|
||||||
|
wrapperProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
wrapperWrapperProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
borderProps?: ComponentProps<typeof Border>;
|
||||||
|
defaultValue?: string;
|
||||||
|
name?: KeyType;
|
||||||
|
changeHandler?: (content: string) => void;
|
||||||
|
showLabel?: boolean;
|
||||||
|
useParentCSS?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
refreshDependencyArray?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Tiny MCE Editor Component
|
||||||
|
* @className_wrapper twui-rte-wrapper
|
||||||
|
*/
|
||||||
|
export default function TinyMCEEditor<KeyType extends string>({
|
||||||
|
options,
|
||||||
|
editorRef: passedEditorRef,
|
||||||
|
setEditor: passedSetEditor,
|
||||||
|
wrapperProps,
|
||||||
|
defaultValue,
|
||||||
|
changeHandler,
|
||||||
|
wrapperWrapperProps,
|
||||||
|
borderProps,
|
||||||
|
name,
|
||||||
|
showLabel,
|
||||||
|
useParentCSS,
|
||||||
|
placeholder,
|
||||||
|
refreshDependencyArray,
|
||||||
|
}: TinyMCEEditorProps<KeyType>) {
|
||||||
|
const { tinyMCE } = useTinyMCE();
|
||||||
|
|
||||||
|
const editorComponentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const editorRef: MutableRefObject<Editor | null> =
|
||||||
|
passedEditorRef || React.useRef(null);
|
||||||
|
|
||||||
|
const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
|
||||||
|
|
||||||
|
const FINAL_HEIGHT = options?.height || 500;
|
||||||
|
const [themeReady, setThemeReady] = React.useState(false);
|
||||||
|
const [ready, setReady] = React.useState(false);
|
||||||
|
const [darkMode, setDarkMode] = React.useState(false);
|
||||||
|
const [refresh, setRefresh] = React.useState(0);
|
||||||
|
const [editor, setEditor] = React.useState<Editor>();
|
||||||
|
|
||||||
|
const title = name ? twuiSlugToNormalText(name) : "Rich Text";
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!tinyMCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlClassName = document.documentElement.className;
|
||||||
|
|
||||||
|
if (htmlClassName.match(/dark/i)) setDarkMode(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setThemeReady(true);
|
||||||
|
}, 200);
|
||||||
|
}, [tinyMCE]);
|
||||||
|
|
||||||
|
let valueTimeout: any;
|
||||||
|
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editorComponentRef.current || !themeReady || !tinyMCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = "https://www.datasquirel.com/tinymce-public";
|
||||||
|
|
||||||
|
tinyMCE.init({
|
||||||
|
height: FINAL_HEIGHT,
|
||||||
|
menubar: false,
|
||||||
|
plugins:
|
||||||
|
"advlist lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount",
|
||||||
|
toolbar:
|
||||||
|
"undo redo | blocks | bold italic underline link image | bullist numlist outdent indent | removeformat code searchreplace wordcount preview insertdatetime",
|
||||||
|
content_style:
|
||||||
|
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
|
||||||
|
init_instance_callback: (editor) => {
|
||||||
|
setEditor(editor as any);
|
||||||
|
if (editorRef) {
|
||||||
|
editorRef.current = editor;
|
||||||
|
passedSetEditor?.(editor);
|
||||||
|
}
|
||||||
|
if (defaultValue) editor.setContent(defaultValue);
|
||||||
|
setReady(true);
|
||||||
|
|
||||||
|
// editor.on("change", (e) => {
|
||||||
|
// changeHandler?.(editor.getContent());
|
||||||
|
// });
|
||||||
|
|
||||||
|
editor.on("input", (e) => {
|
||||||
|
if (changeHandler) {
|
||||||
|
window.clearTimeout(valueTimeout);
|
||||||
|
|
||||||
|
valueTimeout = setTimeout(() => {
|
||||||
|
changeHandler(editor.getContent());
|
||||||
|
}, EDITOR_VALUE_CHANGE_TIMEOUT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (useParentCSS) {
|
||||||
|
useParentStyles(editor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
base_url: baseUrl,
|
||||||
|
body_class: "twui-tinymce",
|
||||||
|
placeholder,
|
||||||
|
relative_urls: true,
|
||||||
|
remove_script_host: true,
|
||||||
|
convert_urls: false,
|
||||||
|
...options,
|
||||||
|
license_key: "gpl",
|
||||||
|
target: editorComponentRef.current,
|
||||||
|
content_css: darkMode ? "dark" : undefined,
|
||||||
|
skin: darkMode ? "oxide-dark" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
if (!ready) return;
|
||||||
|
|
||||||
|
const instance = editorComponentRef.current
|
||||||
|
? tinyMCE?.get(editorComponentRef.current?.id)
|
||||||
|
: undefined;
|
||||||
|
instance?.remove();
|
||||||
|
};
|
||||||
|
}, [tinyMCE, themeReady, refresh, ...(refreshDependencyArray || [])]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const instance = editorRef.current;
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
instance.setContent(defaultValue || "");
|
||||||
|
}
|
||||||
|
}, [defaultValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...wrapperWrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"relative w-full [&_.tox-tinymce]:!border-none",
|
||||||
|
"bg-background-light dark:bg-background-dark",
|
||||||
|
wrapperWrapperProps?.className,
|
||||||
|
)}
|
||||||
|
onInput={(e) => {
|
||||||
|
console.log(`Input Detected`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showLabel && (
|
||||||
|
<label
|
||||||
|
className={twMerge(
|
||||||
|
"absolute z-10 -top-[7px] left-[10px] px-2 text-xs",
|
||||||
|
"bg-background-light dark:bg-background-dark text-gray-500",
|
||||||
|
"dark:text-white/80 rounded",
|
||||||
|
)}
|
||||||
|
htmlFor={id}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<Border
|
||||||
|
{...borderProps}
|
||||||
|
className={twMerge(
|
||||||
|
"dark:border-white/30 p-0 pt-2",
|
||||||
|
borderProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
ref={editorComponentRef}
|
||||||
|
style={{
|
||||||
|
height:
|
||||||
|
String(FINAL_HEIGHT).replace(/[^\d]/g, "") + "px",
|
||||||
|
...wrapperProps?.style,
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"bg-slate-200 dark:bg-slate-700 rounded-sm w-full",
|
||||||
|
"twui-rte-wrapper",
|
||||||
|
)}
|
||||||
|
id={id}
|
||||||
|
></div>
|
||||||
|
</Border>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useParentStyles(editor: Editor) {
|
||||||
|
const doc = editor.getDoc();
|
||||||
|
const parentStylesheets = document.styleSheets;
|
||||||
|
|
||||||
|
for (const sheet of parentStylesheets) {
|
||||||
|
try {
|
||||||
|
if (sheet.href) {
|
||||||
|
const link = doc.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = sheet.href;
|
||||||
|
doc.head.appendChild(link);
|
||||||
|
} else {
|
||||||
|
const rules = sheet.cssRules || sheet.rules;
|
||||||
|
if (rules) {
|
||||||
|
const style = doc.createElement("style");
|
||||||
|
for (const rule of rules) {
|
||||||
|
try {
|
||||||
|
style.appendChild(doc.createTextNode(rule.cssText));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not copy CSS rule:", rule, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error processing stylesheet:", sheet, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3313
src/components/lib/editors/TinyMCE/tinymce.d.ts
vendored
Normal file
50
src/components/lib/editors/TinyMCE/useTinyMCE.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TinyMCE } from "./tinymce";
|
||||||
|
|
||||||
|
let interval: any;
|
||||||
|
|
||||||
|
export default function useTinyMCE() {
|
||||||
|
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE>();
|
||||||
|
const [refresh, setRefresh] = React.useState(0);
|
||||||
|
const [scriptLoaded, setScriptLoaded] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (refresh >= 5) return;
|
||||||
|
|
||||||
|
const clientWindow = window as Window & { tinymce?: TinyMCE };
|
||||||
|
|
||||||
|
if (clientWindow.tinymce) {
|
||||||
|
setScriptLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
|
||||||
|
const baseUrl = "https://www.datasquirel.com/tinymce-public";
|
||||||
|
|
||||||
|
script.src = `${baseUrl}/tinymce.min.js`;
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
setScriptLoaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!scriptLoaded) return;
|
||||||
|
|
||||||
|
const clientWindow = window as Window & { tinymce?: TinyMCE };
|
||||||
|
|
||||||
|
let tinyMCE = clientWindow.tinymce;
|
||||||
|
|
||||||
|
if (tinyMCE) {
|
||||||
|
setTinyMCE(tinyMCE);
|
||||||
|
} else {
|
||||||
|
setRefresh((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
}, [scriptLoaded]);
|
||||||
|
|
||||||
|
return { tinyMCE };
|
||||||
|
}
|
||||||
125
src/components/lib/editors/ace-editor-modes.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
const AceEditorModes = [
|
||||||
|
"abap",
|
||||||
|
"abc",
|
||||||
|
"actionscript",
|
||||||
|
"ada",
|
||||||
|
"apache_conf",
|
||||||
|
"asciidoc",
|
||||||
|
"assembly_x86",
|
||||||
|
"autohotkey",
|
||||||
|
"batchfile",
|
||||||
|
"c9search",
|
||||||
|
"c_cpp",
|
||||||
|
"cirru",
|
||||||
|
"clojure",
|
||||||
|
"cobol",
|
||||||
|
"coffee",
|
||||||
|
"coldfusion",
|
||||||
|
"csharp",
|
||||||
|
"css",
|
||||||
|
"curly",
|
||||||
|
"d",
|
||||||
|
"dart",
|
||||||
|
"diff",
|
||||||
|
"dockerfile",
|
||||||
|
"dot",
|
||||||
|
"dummy",
|
||||||
|
"dummysyntax",
|
||||||
|
"eiffel",
|
||||||
|
"ejs",
|
||||||
|
"elixir",
|
||||||
|
"elm",
|
||||||
|
"erlang",
|
||||||
|
"forth",
|
||||||
|
"ftl",
|
||||||
|
"gcode",
|
||||||
|
"gherkin",
|
||||||
|
"gitignore",
|
||||||
|
"glsl",
|
||||||
|
"golang",
|
||||||
|
"groovy",
|
||||||
|
"haml",
|
||||||
|
"handlebars",
|
||||||
|
"haskell",
|
||||||
|
"haxe",
|
||||||
|
"html",
|
||||||
|
"html_ruby",
|
||||||
|
"ini",
|
||||||
|
"io",
|
||||||
|
"jack",
|
||||||
|
"jade",
|
||||||
|
"java",
|
||||||
|
"javascript",
|
||||||
|
"json",
|
||||||
|
"jsoniq",
|
||||||
|
"jsp",
|
||||||
|
"jsx",
|
||||||
|
"julia",
|
||||||
|
"latex",
|
||||||
|
"less",
|
||||||
|
"liquid",
|
||||||
|
"lisp",
|
||||||
|
"livescript",
|
||||||
|
"logiql",
|
||||||
|
"lsl",
|
||||||
|
"lua",
|
||||||
|
"luapage",
|
||||||
|
"lucene",
|
||||||
|
"makefile",
|
||||||
|
"markdown",
|
||||||
|
"mask",
|
||||||
|
"matlab",
|
||||||
|
"mel",
|
||||||
|
"mushcode",
|
||||||
|
"mysql",
|
||||||
|
"nix",
|
||||||
|
"objectivec",
|
||||||
|
"ocaml",
|
||||||
|
"pascal",
|
||||||
|
"perl",
|
||||||
|
"pgsql",
|
||||||
|
"php",
|
||||||
|
"powershell",
|
||||||
|
"praat",
|
||||||
|
"prolog",
|
||||||
|
"properties",
|
||||||
|
"protobuf",
|
||||||
|
"python",
|
||||||
|
"r",
|
||||||
|
"rdoc",
|
||||||
|
"rhtml",
|
||||||
|
"ruby",
|
||||||
|
"rust",
|
||||||
|
"sass",
|
||||||
|
"scad",
|
||||||
|
"scala",
|
||||||
|
"scheme",
|
||||||
|
"scss",
|
||||||
|
"sh",
|
||||||
|
"sjs",
|
||||||
|
"smarty",
|
||||||
|
"snippets",
|
||||||
|
"soy_template",
|
||||||
|
"space",
|
||||||
|
"sql",
|
||||||
|
"stylus",
|
||||||
|
"svg",
|
||||||
|
"tcl",
|
||||||
|
"tex",
|
||||||
|
"text",
|
||||||
|
"textile",
|
||||||
|
"toml",
|
||||||
|
"twig",
|
||||||
|
"typescript",
|
||||||
|
"vala",
|
||||||
|
"vbscript",
|
||||||
|
"velocity",
|
||||||
|
"verilog",
|
||||||
|
"vhdl",
|
||||||
|
"xml",
|
||||||
|
"xquery",
|
||||||
|
"yaml",
|
||||||
|
"shell",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default AceEditorModes;
|
||||||
42
src/components/lib/elements/Border.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { DetailedHTMLProps, HTMLAttributes, RefObject } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export type TWUI_BORDER_PROPS = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
spacing?: "normal" | "loose" | "tight" | "wide" | "tightest";
|
||||||
|
componentRef?: RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Toggle Component
|
||||||
|
* @className_wrapper twui-border
|
||||||
|
*/
|
||||||
|
export default function Border({
|
||||||
|
spacing,
|
||||||
|
componentRef,
|
||||||
|
...props
|
||||||
|
}: TWUI_BORDER_PROPS) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"relative flex items-center gap-2 border border-solid rounded-default",
|
||||||
|
"border-slate-200 dark:border-white/10",
|
||||||
|
spacing
|
||||||
|
? spacing == "normal"
|
||||||
|
? "px-3 py-2"
|
||||||
|
: spacing == "tight"
|
||||||
|
? "px-2 py-1"
|
||||||
|
: ""
|
||||||
|
: "px-3 py-2",
|
||||||
|
"twui-border",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
ref={componentRef}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
228
src/components/lib/elements/Breadcrumbs.tsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import React, { ComponentProps, ReactNode } from "react";
|
||||||
|
import Link from "../layout/Link";
|
||||||
|
import Divider from "../layout/Divider";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import lowerToTitleCase from "../utils/lower-to-title-case";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
|
||||||
|
type LinkObject = {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
excludeRegexMatch?: RegExp;
|
||||||
|
linkProps?: ComponentProps<typeof Link>;
|
||||||
|
currentLinkProps?: ComponentProps<typeof Link>;
|
||||||
|
dividerProps?: ComponentProps<typeof Divider>;
|
||||||
|
backButtonProps?: ComponentProps<typeof Button>;
|
||||||
|
backButton?: boolean;
|
||||||
|
pageUrl?: string;
|
||||||
|
currentTitle?: string;
|
||||||
|
skipHome?: boolean;
|
||||||
|
divider?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # TWUI Breadcrumbs
|
||||||
|
* @className `twui-breadcrumb-link`
|
||||||
|
* @className `twui-current-breadcrumb-wrapper`
|
||||||
|
* @className `twui-breadcrumbs-divider`
|
||||||
|
* @className `twui-breadcrumbs-back-button`
|
||||||
|
*/
|
||||||
|
export default function Breadcrumbs({
|
||||||
|
excludeRegexMatch,
|
||||||
|
linkProps,
|
||||||
|
currentLinkProps,
|
||||||
|
dividerProps,
|
||||||
|
backButton,
|
||||||
|
backButtonProps,
|
||||||
|
pageUrl,
|
||||||
|
currentTitle,
|
||||||
|
skipHome,
|
||||||
|
divider,
|
||||||
|
}: Props) {
|
||||||
|
const [links, setLinks] = React.useState<LinkObject[] | null>(
|
||||||
|
pageUrl
|
||||||
|
? twuiBreadcrumbsGenerateLinksFromUrl({ url: pageUrl, skipHome })
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (links) return;
|
||||||
|
|
||||||
|
let pathname = window.location.pathname;
|
||||||
|
|
||||||
|
let validPathLinks = twuiBreadcrumbsGenerateLinksFromUrl({
|
||||||
|
url: pathname,
|
||||||
|
excludeRegexMatch,
|
||||||
|
skipHome,
|
||||||
|
});
|
||||||
|
|
||||||
|
setLinks(validPathLinks);
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
setLinks(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!links?.[1]) {
|
||||||
|
return <React.Fragment></React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={twMerge(
|
||||||
|
"overflow-x-auto",
|
||||||
|
"twui-current-breadcrumb-wrapper"
|
||||||
|
)}
|
||||||
|
aria-label="Breadcrumb"
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
className={twMerge(
|
||||||
|
"gap-4 flex-nowrap whitespace-nowrap overflow-x-auto overflow-y-hidden w-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{backButton && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
{...backButtonProps}
|
||||||
|
className={twMerge(
|
||||||
|
"p-1 -my-2 -mx-2",
|
||||||
|
"twui-breadcrumbs-back-button",
|
||||||
|
backButtonProps?.className
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
window.history.back();
|
||||||
|
backButtonProps?.onClick?.(e);
|
||||||
|
}}
|
||||||
|
title="Breadcrumbs Back Button"
|
||||||
|
beforeIcon={<ChevronLeft size={20} />}
|
||||||
|
/>
|
||||||
|
{divider || (
|
||||||
|
<Divider
|
||||||
|
vertical
|
||||||
|
className={twMerge(
|
||||||
|
"twui-breadcrumbs-divider",
|
||||||
|
dividerProps?.className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{links.map((linkObject, index, array) => {
|
||||||
|
const isTarget = array.length - 1 == index;
|
||||||
|
|
||||||
|
if (index === links.length - 1) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={index}
|
||||||
|
href={linkObject.path}
|
||||||
|
{...linkProps}
|
||||||
|
{...(isTarget ? currentLinkProps : {})}
|
||||||
|
className={twMerge(
|
||||||
|
"text-primary-text/50 dark:text-primary-dark-text/50 text-xs",
|
||||||
|
"max-w-[200px] text-ellipsis overflow-hidden",
|
||||||
|
isTarget ? "current" : "",
|
||||||
|
"twui-breadcrumb-link",
|
||||||
|
linkProps?.className,
|
||||||
|
isTarget && currentLinkProps?.className
|
||||||
|
)}
|
||||||
|
title={
|
||||||
|
currentLinkProps?.title || linkObject.title
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{currentTitle || linkObject.title}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Link
|
||||||
|
href={linkObject.path}
|
||||||
|
{...linkProps}
|
||||||
|
{...(isTarget ? currentLinkProps : {})}
|
||||||
|
className={twMerge(
|
||||||
|
"text-xs",
|
||||||
|
isTarget ? "current" : "",
|
||||||
|
"twui-breadcrumb-link",
|
||||||
|
linkProps?.className,
|
||||||
|
isTarget && currentLinkProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{currentLinkProps?.title ||
|
||||||
|
linkObject.title}
|
||||||
|
</Link>
|
||||||
|
{divider || (
|
||||||
|
<Divider
|
||||||
|
vertical
|
||||||
|
{...dividerProps}
|
||||||
|
className={twMerge(
|
||||||
|
"twui-breadcrumbs-divider",
|
||||||
|
dividerProps?.className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
////////////////////////////////////////
|
||||||
|
////////////////////////////////////////
|
||||||
|
////////////////////////////////////////
|
||||||
|
}
|
||||||
|
|
||||||
|
export function twuiBreadcrumbsGenerateLinksFromUrl({
|
||||||
|
url,
|
||||||
|
excludeRegexMatch,
|
||||||
|
skipHome,
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
excludeRegexMatch?: RegExp;
|
||||||
|
skipHome?: boolean;
|
||||||
|
}) {
|
||||||
|
let pathLinks = url.split("/");
|
||||||
|
|
||||||
|
let validPathLinks = [];
|
||||||
|
|
||||||
|
if (!skipHome) {
|
||||||
|
validPathLinks.push({
|
||||||
|
title: "Home",
|
||||||
|
path: url.match(/admin/) ? "/admin" : "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pathLinks.forEach((linkText, index, array) => {
|
||||||
|
if (!linkText?.match(/./)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeRegexMatch && excludeRegexMatch.test(linkText)) return;
|
||||||
|
|
||||||
|
validPathLinks.push({
|
||||||
|
title: lowerToTitleCase(linkText),
|
||||||
|
path: (() => {
|
||||||
|
let path = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
const lnText = array[i];
|
||||||
|
if (i > index || !lnText.match(/./)) continue;
|
||||||
|
|
||||||
|
path += `/${lnText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
})(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return validPathLinks;
|
||||||
|
}
|
||||||
72
src/components/lib/elements/Card.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Link from "../layout/Link";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
variant?: "normal";
|
||||||
|
href?: string;
|
||||||
|
linkProps?: ComponentProps<typeof Link>;
|
||||||
|
noHover?: boolean;
|
||||||
|
elRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
linkRef?: React.RefObject<HTMLAnchorElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # General Card
|
||||||
|
* @className twui-card
|
||||||
|
* @className twui-card-link
|
||||||
|
*
|
||||||
|
* @info use the classname `nested-link` to prevent the card from being clickable when
|
||||||
|
* a link (or the target element with this calss) inside the card is clicked.
|
||||||
|
*/
|
||||||
|
export default function Card({
|
||||||
|
href,
|
||||||
|
variant,
|
||||||
|
linkProps,
|
||||||
|
noHover,
|
||||||
|
elRef,
|
||||||
|
linkRef,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const component = (
|
||||||
|
<div
|
||||||
|
ref={elRef}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-row items-center p-4 rounded-default bg-background-light dark:bg-background-dark",
|
||||||
|
"border border-slate-200 dark:border-white/10 border-solid",
|
||||||
|
noHover ? "" : "twui-card",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
ref={linkRef}
|
||||||
|
href={href}
|
||||||
|
{...linkProps}
|
||||||
|
className={twMerge(
|
||||||
|
"cursor-pointer",
|
||||||
|
"twui-card",
|
||||||
|
"twui-card-link",
|
||||||
|
linkProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{component}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
61
src/components/lib/elements/CheckBulletPoints.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentProps, ReactNode } from "react";
|
||||||
|
import Stack from "../layout/Stack copy";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import { Check, CheckCircle, CheckCircle2 } from "lucide-react";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type BulletPoint = {
|
||||||
|
title: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWUI_CHECK_BULLET_POINTS_PROPS = ComponentProps<typeof Stack> & {
|
||||||
|
bulletPoints: BulletPoint[];
|
||||||
|
bulletWrapperProps?: ComponentProps<typeof Row>;
|
||||||
|
iconProps?: ComponentProps<typeof CheckCircle2>;
|
||||||
|
titleProps?: ComponentProps<typeof Span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Check Bullet Points Component
|
||||||
|
* @className_wrapper twui-check-bullet-points-wrapper
|
||||||
|
*/
|
||||||
|
export default function CheckBulletPoints({
|
||||||
|
bulletPoints,
|
||||||
|
bulletWrapperProps,
|
||||||
|
iconProps,
|
||||||
|
titleProps,
|
||||||
|
...props
|
||||||
|
}: TWUI_CHECK_BULLET_POINTS_PROPS) {
|
||||||
|
return (
|
||||||
|
<Stack {...props} className={twMerge("gap-3", props.className)}>
|
||||||
|
{bulletPoints.map((bulletPoint, index) => {
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={index}
|
||||||
|
{...bulletWrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"gap-2 xl:flex-nowrap",
|
||||||
|
bulletWrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{bulletPoint.icon || (
|
||||||
|
<CheckCircle2
|
||||||
|
className="text-success min-w-[20px]"
|
||||||
|
size={20}
|
||||||
|
{...iconProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Span
|
||||||
|
{...titleProps}
|
||||||
|
className={twMerge("", titleProps?.className)}
|
||||||
|
>
|
||||||
|
{bulletPoint.title}
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/components/lib/elements/CodeBlock.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { Check, Copy } from "lucide-react";
|
||||||
|
import React, {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
PropsWithChildren,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Stack from "../layout/Stack";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
import Divider from "../layout/Divider";
|
||||||
|
|
||||||
|
export const TWUIPrismLanguages = ["shell", "javascript"] as const;
|
||||||
|
|
||||||
|
type Props = PropsWithChildren &
|
||||||
|
DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement> & {
|
||||||
|
wrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
"data-title"?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
singleBlock?: boolean;
|
||||||
|
language?: (typeof TWUIPrismLanguages)[number];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # CodeBlock
|
||||||
|
*
|
||||||
|
* @className `twui-code-block-wrapper`
|
||||||
|
* @className `twui-code-pre-wrapper`
|
||||||
|
* @className `twui-code-block-pre`
|
||||||
|
* @className `twui-code-block-header`
|
||||||
|
*/
|
||||||
|
export default function CodeBlock({
|
||||||
|
children,
|
||||||
|
wrapperProps,
|
||||||
|
backgroundColor,
|
||||||
|
singleBlock,
|
||||||
|
language,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const codeRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
const title = props?.["data-title"];
|
||||||
|
|
||||||
|
const finalBackgroundColor = backgroundColor || "#28272b";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"outline-[1px] outline-slate-200 dark:outline-white/10",
|
||||||
|
`rounded w-full transition-all items-start`,
|
||||||
|
"relative max-w-[80vw] sm:max-w-[85vw] xl:max-w-[880px]",
|
||||||
|
"twui-code-block-wrapper",
|
||||||
|
wrapperProps?.className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
boxShadow: copied
|
||||||
|
? "0 0 10px 10px rgba(18, 139, 99, 0.2)"
|
||||||
|
: undefined,
|
||||||
|
backgroundColor: finalBackgroundColor,
|
||||||
|
...props.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className={twMerge(
|
||||||
|
"gap-0 w-full overflow-x-auto relative",
|
||||||
|
"max-h-[600px] overflow-y-auto"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
className={twMerge(
|
||||||
|
"w-full px-1 h-10 sticky top-0 py-2",
|
||||||
|
singleBlock ? "absolute !bg-transparent" : "",
|
||||||
|
"twui-code-block-header"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: finalBackgroundColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title && <span className="text-white/70">{title}</span>}
|
||||||
|
<div className="ml-auto">
|
||||||
|
{copied ? (
|
||||||
|
<Row>
|
||||||
|
<span className="text-white text-xs twui-code-block-copied-text">
|
||||||
|
Copied!
|
||||||
|
</span>
|
||||||
|
<div className="w-5 h-5 rounded-full bg-emerald-600 text-white flex items-center justify-center">
|
||||||
|
<Check size={15} />
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
beforeIcon={<Copy size={17} color="white" />}
|
||||||
|
className="!p-1 !bg-transparent opacity-50"
|
||||||
|
onClick={() => {
|
||||||
|
const content =
|
||||||
|
codeRef.current?.textContent;
|
||||||
|
if (!content) {
|
||||||
|
window.alert("No Content to copy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.navigator.clipboard
|
||||||
|
.writeText(content)
|
||||||
|
.then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title="Copy Code Snippet"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
{!singleBlock && (
|
||||||
|
<Divider className="!border-white/10 sticky top-10" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
`p-1 w-full [&_pre]:!bg-transparent`,
|
||||||
|
singleBlock ? "" : "-mt-1",
|
||||||
|
"twui-code-pre-wrapper"
|
||||||
|
)}
|
||||||
|
ref={codeRef as any}
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"!my-0 whitespace-pre-wrap",
|
||||||
|
language ? `language-${language}` : "",
|
||||||
|
"twui-code-block-pre",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/components/lib/elements/ColorSchemeSelector.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
active?: boolean;
|
||||||
|
setActive?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
iconWrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
defaultScheme?: "light" | "dark";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Color Scheme Loader
|
||||||
|
* @className_wrapper twui-color-scheme-selector
|
||||||
|
*/
|
||||||
|
export default function ColorSchemeSelector({
|
||||||
|
active: initialActive,
|
||||||
|
setActive: externalSetActive,
|
||||||
|
iconWrapperProps,
|
||||||
|
defaultScheme,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const [active, setActive] = React.useState(initialActive);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const isDocumentDark =
|
||||||
|
document.documentElement.classList.contains("dark");
|
||||||
|
const isDocumentLight =
|
||||||
|
document.documentElement.classList.contains("light");
|
||||||
|
|
||||||
|
if (isDocumentDark) {
|
||||||
|
setActive(true);
|
||||||
|
return;
|
||||||
|
} else if (isDocumentLight) {
|
||||||
|
setActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingTheme = localStorage.getItem("theme");
|
||||||
|
|
||||||
|
if (existingTheme === "dark") {
|
||||||
|
setActive(true);
|
||||||
|
} else if (existingTheme === "light") {
|
||||||
|
setActive(false);
|
||||||
|
} else if (defaultScheme) {
|
||||||
|
setActive(defaultScheme == "dark" ? false : true);
|
||||||
|
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
setActive(true);
|
||||||
|
} else if (typeof active == "undefined") {
|
||||||
|
setActive(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof active == "undefined") return;
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
document.documentElement.className = "dark";
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.className = "light";
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
}
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-row items-center",
|
||||||
|
"twui-color-scheme-selector",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Color Scheme Selector Button"
|
||||||
|
onClick={() => setActive(!active)}
|
||||||
|
className={twMerge(
|
||||||
|
"cursor-pointer hover:opacity-70 flex items-center justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...iconWrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"w-6 h-6 flex items-center justify-center",
|
||||||
|
iconWrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{active == false && <Sun />}
|
||||||
|
{active == true && <Moon />}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/components/lib/elements/CopySlug.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
} from "react";
|
||||||
|
import { Copy, LucideProps } from "lucide-react";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
|
||||||
|
type Props = Omit<ComponentProps<typeof Button>, "title"> & {
|
||||||
|
slugText: string;
|
||||||
|
justIcon?: boolean;
|
||||||
|
noIcon?: boolean;
|
||||||
|
title?: string;
|
||||||
|
outlined?: boolean;
|
||||||
|
successMsg?: string | ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
iconProps?: LucideProps;
|
||||||
|
setToastOpen?: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CopySlug({
|
||||||
|
slugText,
|
||||||
|
justIcon,
|
||||||
|
noIcon,
|
||||||
|
title,
|
||||||
|
outlined,
|
||||||
|
successMsg,
|
||||||
|
iconProps,
|
||||||
|
icon,
|
||||||
|
setToastOpen,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title={title || slugText}
|
||||||
|
size="smaller"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
{...props}
|
||||||
|
onClick={(e) => {
|
||||||
|
navigator.clipboard.writeText(slugText).then(() => {
|
||||||
|
setToastOpen?.(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setToastOpen?.(true);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
props.onClick?.(e);
|
||||||
|
}}
|
||||||
|
style={{ ...(outlined ? {} : { padding: 0 }), ...props.style }}
|
||||||
|
>
|
||||||
|
{noIcon
|
||||||
|
? null
|
||||||
|
: icon || <Copy size={outlined ? 15 : 20} {...iconProps} />}
|
||||||
|
{!justIcon && (title ? title : "Copy Slug")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
191
src/components/lib/elements/Dropdown.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import React, {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
PropsWithChildren,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const TWUIDropdownContentPositions = [
|
||||||
|
"left",
|
||||||
|
"bottom-left",
|
||||||
|
"top-left",
|
||||||
|
"top",
|
||||||
|
"bottom",
|
||||||
|
"right",
|
||||||
|
"bottom-right",
|
||||||
|
"top-right",
|
||||||
|
"center",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type TWUI_DROPDOWN_PROPS = PropsWithChildren &
|
||||||
|
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
|
target: React.ReactNode;
|
||||||
|
contentWrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
targetWrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
debounce?: number;
|
||||||
|
openDebounce?: number;
|
||||||
|
hoverOpen?: boolean;
|
||||||
|
above?: boolean;
|
||||||
|
position?: (typeof TWUIDropdownContentPositions)[number];
|
||||||
|
topOffset?: number;
|
||||||
|
externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
externalOpen?: boolean;
|
||||||
|
keepOpen?: boolean;
|
||||||
|
disableClickActions?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Toggle Component
|
||||||
|
* @className_wrapper twui-dropdown-wrapper
|
||||||
|
* @className_wrapper twui-dropdown-target
|
||||||
|
* @className_wrapper twui-dropdown-content
|
||||||
|
*
|
||||||
|
* @note use the class `cancel-link` to prevent popup open on click
|
||||||
|
*/
|
||||||
|
export default function Dropdown({
|
||||||
|
contentWrapperProps,
|
||||||
|
targetWrapperProps,
|
||||||
|
hoverOpen,
|
||||||
|
above,
|
||||||
|
debounce = 200,
|
||||||
|
openDebounce = 200,
|
||||||
|
target,
|
||||||
|
position = "center",
|
||||||
|
topOffset,
|
||||||
|
externalSetOpen,
|
||||||
|
keepOpen,
|
||||||
|
disableClickActions,
|
||||||
|
externalOpen,
|
||||||
|
...props
|
||||||
|
}: TWUI_DROPDOWN_PROPS) {
|
||||||
|
const [open, setOpen] = React.useState(externalOpen);
|
||||||
|
|
||||||
|
let timeout: any;
|
||||||
|
let openTimeout: any;
|
||||||
|
|
||||||
|
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const dropdownContentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleClickOutside = React.useCallback((e: MouseEvent) => {
|
||||||
|
const targetEl = e.target as HTMLElement;
|
||||||
|
const closestWrapper = targetEl.closest(".twui-dropdown-wrapper");
|
||||||
|
|
||||||
|
if (!closestWrapper) {
|
||||||
|
externalSetOpen?.(false);
|
||||||
|
return setOpen(false);
|
||||||
|
}
|
||||||
|
if (closestWrapper && closestWrapper !== dropdownRef.current) {
|
||||||
|
externalSetOpen?.(false);
|
||||||
|
return setOpen(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (keepOpen) return;
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setOpen(externalOpen);
|
||||||
|
}, [externalOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-col items-center relative",
|
||||||
|
"twui-dropdown-wrapper",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
window.clearTimeout(openTimeout);
|
||||||
|
|
||||||
|
openTimeout = setTimeout(() => {
|
||||||
|
externalSetOpen?.(true);
|
||||||
|
setOpen(true);
|
||||||
|
}, openDebounce);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
|
||||||
|
window.clearTimeout(openTimeout);
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
externalSetOpen?.(false);
|
||||||
|
setOpen(false);
|
||||||
|
}, debounce);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}}
|
||||||
|
ref={dropdownRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
const targetEl = e.target as HTMLElement | null;
|
||||||
|
if (targetEl?.closest?.(".cancel-link")) return;
|
||||||
|
if (disableClickActions) return;
|
||||||
|
externalSetOpen?.(!open);
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"cursor-pointer",
|
||||||
|
"twui-dropdown-target",
|
||||||
|
targetWrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{target}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
{...contentWrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"absolute z-10 mt-1",
|
||||||
|
position == "left"
|
||||||
|
? "left-[100%] top-[50%] -translate-y-[50%]"
|
||||||
|
: position == "right"
|
||||||
|
? "right-[100%] top-[50%] -translate-y-[50%]"
|
||||||
|
: position == "bottom-left"
|
||||||
|
? "left-0 top-[100%]"
|
||||||
|
: position == "bottom-right"
|
||||||
|
? "right-0 top-[100%]"
|
||||||
|
: position == "center"
|
||||||
|
? "left-[50%] -translate-x-[50%] top-[100%]"
|
||||||
|
: position == "top"
|
||||||
|
? "left-[50%] -translate-x-[50%] bottom-[100%]"
|
||||||
|
: "top-[100%]",
|
||||||
|
above ? "-translate-y-[120%]" : "",
|
||||||
|
open ? "flex" : "hidden",
|
||||||
|
"twui-dropdown-content",
|
||||||
|
contentWrapperProps?.className
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
// top: `calc(100% + ${topOffset || 0}px)`,
|
||||||
|
...contentWrapperProps?.style,
|
||||||
|
}}
|
||||||
|
ref={dropdownContentRef}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
src/components/lib/elements/EmptyContent.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React, { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Stack from "../layout/Stack";
|
||||||
|
import Border from "./Border";
|
||||||
|
import Center from "../layout/Center";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
import Link from "../layout/Link";
|
||||||
|
|
||||||
|
export const ToastStyles = ["normal", "success", "error"] as const;
|
||||||
|
export const ToastColors = ToastStyles;
|
||||||
|
|
||||||
|
export type TWUIEmptyContentProps = ComponentProps<typeof Stack> & {
|
||||||
|
title: string;
|
||||||
|
url?: string;
|
||||||
|
linkProps?: ComponentProps<typeof Link>;
|
||||||
|
borderProps?: ComponentProps<typeof Border>;
|
||||||
|
textProps?: ComponentProps<typeof Span>;
|
||||||
|
contentWrapperProps?: ComponentProps<typeof Row>;
|
||||||
|
icon?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # EmptyC ontent Component
|
||||||
|
* @className twui-empty-content
|
||||||
|
* @className twui-empty-content-border
|
||||||
|
* @className twui-empty-content-link
|
||||||
|
*/
|
||||||
|
export default function EmptyContent({
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
linkProps,
|
||||||
|
icon,
|
||||||
|
borderProps,
|
||||||
|
textProps,
|
||||||
|
contentWrapperProps,
|
||||||
|
...props
|
||||||
|
}: TWUIEmptyContentProps) {
|
||||||
|
const mainComponent = (
|
||||||
|
<Stack
|
||||||
|
{...props}
|
||||||
|
className={twMerge("w-full", "twui-empty-content", props.className)}
|
||||||
|
>
|
||||||
|
<Border
|
||||||
|
{...borderProps}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full",
|
||||||
|
borderProps?.className,
|
||||||
|
"twui-empty-content-border"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Center>
|
||||||
|
<Row {...contentWrapperProps}>
|
||||||
|
{icon && <div className="opacity-50">{icon}</div>}
|
||||||
|
<Span
|
||||||
|
size="small"
|
||||||
|
{...textProps}
|
||||||
|
className={twMerge(
|
||||||
|
"opacity-70 text-foreground-light dark:text-foreground-dark",
|
||||||
|
textProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
</Center>
|
||||||
|
</Border>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
{...linkProps}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full",
|
||||||
|
"twui-empty-content-link",
|
||||||
|
linkProps?.className
|
||||||
|
)}
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
{mainComponent}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainComponent;
|
||||||
|
}
|
||||||
33
src/components/lib/elements/HeaderLink.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import Link from "../layout/Link";
|
||||||
|
import { TwuiHeaderLink } from "./HeaderNav";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
|
||||||
|
export type TWUI_HEADER_LINK_PROPS = ComponentProps<typeof Link> & {
|
||||||
|
link: TwuiHeaderLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Header Nav Component
|
||||||
|
* @className_wrapper twui-header-link
|
||||||
|
*/
|
||||||
|
export default function HeaderLink({ link, ...props }: TWUI_HEADER_LINK_PROPS) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={link.url}
|
||||||
|
strict={link.strict}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"grow p-2 hover:opacity-50",
|
||||||
|
"twui-header-link",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
{link.icon}
|
||||||
|
{link.title}
|
||||||
|
</Row>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/components/lib/elements/HeaderNav.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import HeaderNavLinkComponent from "./HeaderNavLinkComponent";
|
||||||
|
|
||||||
|
export type TWUI_HEADER_NAV_PROPS = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLElement>,
|
||||||
|
HTMLElement
|
||||||
|
> & {
|
||||||
|
headerLinks: TwuiHeaderLink[];
|
||||||
|
customDropdowns?: {
|
||||||
|
url: string;
|
||||||
|
content: ReactNode;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TwuiHeaderLink = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
strict?: boolean;
|
||||||
|
dropdown?: ReactNode;
|
||||||
|
children?: TwuiHeaderLink[];
|
||||||
|
icon?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Header Nav Component
|
||||||
|
* @className twui-header-nav
|
||||||
|
* @className twui-header-nav-link-component
|
||||||
|
* @className twui-header-nav-link-icon
|
||||||
|
* @className twui-header-nav-link-dropdown
|
||||||
|
*/
|
||||||
|
export default function HeaderNav({
|
||||||
|
headerLinks,
|
||||||
|
customDropdowns,
|
||||||
|
...props
|
||||||
|
}: TWUI_HEADER_NAV_PROPS) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
twuiAddActiveLinksFn({ selector: ".twui-header-nav-link-component a" });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"twui-header-nav w-full xl:w-auto",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Row className="gap-x-2 gap-y-2 flex-col xl:flex-row items-start xl:items-stretch">
|
||||||
|
{headerLinks.map((link, index) => {
|
||||||
|
const targetCustomDropdown = customDropdowns?.find(
|
||||||
|
(d) => d.url == link.url
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<HeaderNavLinkComponent
|
||||||
|
link={link}
|
||||||
|
key={index}
|
||||||
|
dropdown={targetCustomDropdown?.content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddActiveLinkParams = {
|
||||||
|
selector?: string;
|
||||||
|
wrapperEl?: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function twuiAddActiveLinksFn({
|
||||||
|
selector,
|
||||||
|
wrapperEl,
|
||||||
|
}: AddActiveLinkParams) {
|
||||||
|
(wrapperEl || document).querySelectorAll(selector || "a").forEach((ln) => {
|
||||||
|
const linkEl = ln as HTMLAnchorElement;
|
||||||
|
const isLinkStrict = linkEl.dataset.strict;
|
||||||
|
|
||||||
|
const linkAttr = linkEl.getAttribute("href");
|
||||||
|
|
||||||
|
if (window.location.pathname === "/" && linkAttr == "/") {
|
||||||
|
linkEl.classList.add("active");
|
||||||
|
} else if (
|
||||||
|
isLinkStrict &&
|
||||||
|
linkEl.getAttribute("href") === window.location.pathname
|
||||||
|
) {
|
||||||
|
linkEl.classList.add("active");
|
||||||
|
} else if (
|
||||||
|
linkAttr &&
|
||||||
|
window.location.pathname.startsWith(linkAttr) &&
|
||||||
|
!isLinkStrict
|
||||||
|
) {
|
||||||
|
linkEl.classList.add("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
141
src/components/lib/elements/HeaderNavLinkComponent.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import HeaderLink from "./HeaderLink";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import Dropdown from "./Dropdown";
|
||||||
|
import { TwuiHeaderLink } from "./HeaderNav";
|
||||||
|
import Card from "./Card";
|
||||||
|
import Stack from "../layout/Stack";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Header Nav Main Link Component
|
||||||
|
* @className twui-header-nav-link-component
|
||||||
|
* @className twui-header-nav-link-icon
|
||||||
|
* @className twui-header-nav-link-dropdown
|
||||||
|
*/
|
||||||
|
export default function HeaderNavLinkComponent({
|
||||||
|
link,
|
||||||
|
dropdown,
|
||||||
|
}: {
|
||||||
|
link: TwuiHeaderLink;
|
||||||
|
dropdown?: ReactNode;
|
||||||
|
}) {
|
||||||
|
const isDropdown = dropdown || link.dropdown || link.children?.[0];
|
||||||
|
|
||||||
|
const mainLinkComponent = (
|
||||||
|
<Row className="gap-0 grow">
|
||||||
|
<HeaderLink link={link} strict={link.strict} />
|
||||||
|
{isDropdown && (
|
||||||
|
<ChevronDown
|
||||||
|
className={twMerge(
|
||||||
|
"hidden xl:flex xl:-ml-1",
|
||||||
|
"twui-header-nav-link-icon"
|
||||||
|
)}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showMobileDropdown, setShowMobileDropdown] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"relative w-full xl:w-auto [&_a.active]:font-bold",
|
||||||
|
"twui-header-nav-link-component"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isDropdown ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Stack className="flex xl:hidden w-full">
|
||||||
|
<Row className="w-full justify-between">
|
||||||
|
{mainLinkComponent}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
setShowMobileDropdown(!showMobileDropdown)
|
||||||
|
}
|
||||||
|
title="Header Links Dropdown Button"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={twMerge(
|
||||||
|
"twui-header-nav-link-icon !text-link dark:!text-white"
|
||||||
|
)}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{showMobileDropdown && (
|
||||||
|
<Stack className="w-full">
|
||||||
|
{dropdown ? (
|
||||||
|
dropdown
|
||||||
|
) : link.children?.[0] ? (
|
||||||
|
<Card
|
||||||
|
className={twMerge(
|
||||||
|
"w-full p-0",
|
||||||
|
"twui-header-nav-link-dropdown"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stack className="w-full items-stretch gap-0 py-2">
|
||||||
|
{link.children.map(
|
||||||
|
(_ch, _index) => {
|
||||||
|
return (
|
||||||
|
<HeaderLink
|
||||||
|
link={_ch}
|
||||||
|
key={_index}
|
||||||
|
className="px-6 py-4"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
) : link.dropdown ? (
|
||||||
|
link.dropdown
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
target={mainLinkComponent}
|
||||||
|
position="center"
|
||||||
|
hoverOpen
|
||||||
|
className="hidden xl:flex"
|
||||||
|
>
|
||||||
|
{dropdown ? (
|
||||||
|
dropdown
|
||||||
|
) : link.children?.[0] ? (
|
||||||
|
<Card
|
||||||
|
className={twMerge(
|
||||||
|
"min-w-[200px] mt-2 p-0",
|
||||||
|
"twui-header-nav-link-dropdown"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stack className="w-full items-stretch gap-0 py-2">
|
||||||
|
{link.children.map((_ch, _index) => {
|
||||||
|
return (
|
||||||
|
<HeaderLink
|
||||||
|
link={_ch}
|
||||||
|
key={_index}
|
||||||
|
className="px-6 py-4"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
) : link.dropdown ? (
|
||||||
|
link.dropdown
|
||||||
|
) : null}
|
||||||
|
</Dropdown>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
mainLinkComponent
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
src/components/lib/elements/HtmlToReactComponent.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import HtmlToReact from "html-to-react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
html: string;
|
||||||
|
componentRef?: React.RefObject<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # HTML String to React Component
|
||||||
|
* @className_wrapper twui-html-react
|
||||||
|
*/
|
||||||
|
export default function HtmlToReactComponent({
|
||||||
|
html,
|
||||||
|
componentRef,
|
||||||
|
...props
|
||||||
|
}: TWUI_TOGGLE_PROPS) {
|
||||||
|
const htmlToReactParser = HtmlToReact.Parser();
|
||||||
|
const reactElement = htmlToReactParser.parse(html);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge("", props.className)}
|
||||||
|
ref={componentRef}
|
||||||
|
>
|
||||||
|
{reactElement}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
src/components/lib/elements/LinkList.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Link from "../layout/Link";
|
||||||
|
import { twuiAddActiveLinksFn } from "./HeaderNav";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import Divider from "../layout/Divider";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
|
||||||
|
export type TWUI_LINK_LIST_LINK_OBJECT = {
|
||||||
|
title?: string;
|
||||||
|
component?: ReactNode;
|
||||||
|
url?: string;
|
||||||
|
strict?: boolean;
|
||||||
|
icon?: ReactNode;
|
||||||
|
iconPosition?: "before" | "after";
|
||||||
|
linkProps?: ComponentProps<typeof Link>;
|
||||||
|
buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
|
||||||
|
linkType?: "button" | "link";
|
||||||
|
divider?: ReactNode;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWUI_LINK_LIST_PROPS = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
links: (
|
||||||
|
| TWUI_LINK_LIST_LINK_OBJECT
|
||||||
|
| TWUI_LINK_LIST_LINK_OBJECT[]
|
||||||
|
| undefined
|
||||||
|
)[];
|
||||||
|
linkProps?: ComponentProps<typeof Link>;
|
||||||
|
buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
|
||||||
|
divider?: boolean;
|
||||||
|
dividerComponent?: ReactNode;
|
||||||
|
linkType?: "button" | "link";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Link List Component
|
||||||
|
* @description A component that renders a list of links.
|
||||||
|
* @className_wrapper twui-link-list
|
||||||
|
*/
|
||||||
|
export default function LinkList({
|
||||||
|
links,
|
||||||
|
linkProps,
|
||||||
|
buttonProps,
|
||||||
|
divider,
|
||||||
|
dividerComponent,
|
||||||
|
linkType,
|
||||||
|
...props
|
||||||
|
}: TWUI_LINK_LIST_PROPS) {
|
||||||
|
const listRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
twuiAddActiveLinksFn({
|
||||||
|
wrapperEl: listRef.current || undefined,
|
||||||
|
selector: "a",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={listRef}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-row items-center gap-1",
|
||||||
|
"twui-link-list",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{links
|
||||||
|
.flat()
|
||||||
|
.filter((ln) => Boolean(ln))
|
||||||
|
.map((link, index) => {
|
||||||
|
if (!link) return null;
|
||||||
|
|
||||||
|
if (link.divider)
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{link.divider}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalDivider =
|
||||||
|
index < links.length - 1 &&
|
||||||
|
(dividerComponent ? (
|
||||||
|
dividerComponent
|
||||||
|
) : divider ? (
|
||||||
|
<Divider />
|
||||||
|
) : undefined);
|
||||||
|
|
||||||
|
if (linkType == "button" || link.linkType == "button") {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Button
|
||||||
|
title={link.title || "Link Button"}
|
||||||
|
variant="ghost"
|
||||||
|
{...buttonProps}
|
||||||
|
{...link.buttonProps}
|
||||||
|
className={twMerge(
|
||||||
|
"p-2 cursor-pointer whitespace-nowrap",
|
||||||
|
linkProps?.className,
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
link.onClick?.(e);
|
||||||
|
link.buttonProps?.onClick?.(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
{link.icon}
|
||||||
|
{link.component || link.title}
|
||||||
|
</Row>
|
||||||
|
</Button>
|
||||||
|
{finalDivider}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Link
|
||||||
|
href={link.url}
|
||||||
|
title={link.title}
|
||||||
|
{...linkProps}
|
||||||
|
{...link.linkProps}
|
||||||
|
className={twMerge(
|
||||||
|
"p-2 cursor-pointer whitespace-nowrap",
|
||||||
|
linkProps?.className,
|
||||||
|
link.linkProps?.className,
|
||||||
|
)}
|
||||||
|
strict={link.strict}
|
||||||
|
onClick={(e) => {
|
||||||
|
link.onClick?.(e);
|
||||||
|
link.linkProps?.onClick?.(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
{!link.iconPosition ||
|
||||||
|
link.iconPosition == "before"
|
||||||
|
? link.icon
|
||||||
|
: null}
|
||||||
|
{link.component || link.title}
|
||||||
|
{link.iconPosition == "after"
|
||||||
|
? link.icon
|
||||||
|
: null}
|
||||||
|
</Row>
|
||||||
|
</Link>
|
||||||
|
{finalDivider}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
src/components/lib/elements/Loading.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
size?: "small" | "normal" | "medium" | "large" | "smaller";
|
||||||
|
svgClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Loading Component
|
||||||
|
* @className_wrapper twui-loading
|
||||||
|
*/
|
||||||
|
export default function Loading({ size, svgClassName, ...props }: Props) {
|
||||||
|
const sizeClassName = (() => {
|
||||||
|
switch (size) {
|
||||||
|
case "smaller":
|
||||||
|
return "w-4 h-4";
|
||||||
|
case "small":
|
||||||
|
return "w-5 h-5";
|
||||||
|
case "normal":
|
||||||
|
return "w-6 h-6";
|
||||||
|
case "large":
|
||||||
|
return "w-7 h-7";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "w-6 h-6";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
{...props}
|
||||||
|
className={twMerge(`twui-loading`, props.className)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className={twMerge(
|
||||||
|
"text-gray animate-spin dark:text-gray-dark fill-primary",
|
||||||
|
"dark:fill-white twui-loading",
|
||||||
|
sizeClassName,
|
||||||
|
svgClassName,
|
||||||
|
)}
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/components/lib/elements/LoadingBlock.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # General paper
|
||||||
|
* @className_wrapper twui-loading-block
|
||||||
|
*/
|
||||||
|
export default function LoadingBlock({
|
||||||
|
...props
|
||||||
|
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"bg-slate-200 dark:bg-white/10",
|
||||||
|
"rounded animate-pulse w-full h-[60px]",
|
||||||
|
"twui-loading-block",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/components/lib/elements/LoadingOverlay.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Center from "../layout/Center";
|
||||||
|
import Loading from "./Loading";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
loadingProps?: ComponentProps<typeof Loading>;
|
||||||
|
label?: string;
|
||||||
|
fixed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Loading Overlay Component
|
||||||
|
* @className_wrapper twui-loading-overlay
|
||||||
|
*/
|
||||||
|
export default function LoadingOverlay({
|
||||||
|
loadingProps,
|
||||||
|
label,
|
||||||
|
fixed,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"top-0 left-0 w-full h-full z-[500]",
|
||||||
|
"bg-background-light/90 dark:bg-background-dark/90",
|
||||||
|
fixed ? "fixed" : "absolute",
|
||||||
|
props.className,
|
||||||
|
"twui-loading-overlay",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Center>
|
||||||
|
<Row>
|
||||||
|
<Loading {...loadingProps} />
|
||||||
|
{label && <Span>{label}</Span>}
|
||||||
|
</Row>
|
||||||
|
</Center>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
src/components/lib/elements/Modal.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import ModalComponent from "../(partials)/ModalComponent";
|
||||||
|
import PopoverComponent from "../(partials)/PopoverComponent";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const TWUIPopoverStyles = [
|
||||||
|
"top",
|
||||||
|
"bottom",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
"transform",
|
||||||
|
"bottom-left",
|
||||||
|
"bottom-right",
|
||||||
|
] as const;
|
||||||
|
export const TWUIPopoverTriggers = ["hover", "click"] as const;
|
||||||
|
|
||||||
|
export type TWUI_MODAL_PROPS = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
target?: React.ReactNode;
|
||||||
|
targetRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
popoverReferenceRef?: React.RefObject<HTMLElement | null>;
|
||||||
|
targetWrapperProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
open?: boolean;
|
||||||
|
isPopover?: boolean;
|
||||||
|
position?: (typeof TWUIPopoverStyles)[number];
|
||||||
|
trigger?: (typeof TWUIPopoverTriggers)[number];
|
||||||
|
debounce?: number;
|
||||||
|
onClose?: () => any;
|
||||||
|
hoverOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Modal Component
|
||||||
|
* @ID twui-modal-root
|
||||||
|
* @className twui-modal-content
|
||||||
|
* @className twui-modal
|
||||||
|
* @ID twui-popover-root
|
||||||
|
* @className twui-popover-content
|
||||||
|
* @className twui-popover-target
|
||||||
|
*/
|
||||||
|
export default function Modal(props: TWUI_MODAL_PROPS) {
|
||||||
|
const {
|
||||||
|
target,
|
||||||
|
targetRef,
|
||||||
|
targetWrapperProps,
|
||||||
|
open: existingOpen,
|
||||||
|
setOpen: existingSetOpen,
|
||||||
|
isPopover,
|
||||||
|
popoverReferenceRef,
|
||||||
|
trigger = "hover",
|
||||||
|
debounce = 500,
|
||||||
|
onClose,
|
||||||
|
hoverOpen,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [ready, setReady] = React.useState(false);
|
||||||
|
const [open, setOpen] = React.useState(existingOpen || false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const IDName = isPopover ? "twui-popover-root" : "twui-modal-root";
|
||||||
|
const modalRoot = document.getElementById(IDName);
|
||||||
|
|
||||||
|
if (modalRoot) {
|
||||||
|
if (isPopover) {
|
||||||
|
modalRoot.style.zIndex = "1000";
|
||||||
|
}
|
||||||
|
setReady(true);
|
||||||
|
} else {
|
||||||
|
const newModalRootEl = document.createElement("div");
|
||||||
|
newModalRootEl.id = IDName;
|
||||||
|
document.body.appendChild(newModalRootEl);
|
||||||
|
setReady(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
existingSetOpen?.(open);
|
||||||
|
if (open == false) onClose?.();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setOpen(existingOpen || false);
|
||||||
|
}, [existingOpen]);
|
||||||
|
|
||||||
|
const finalTargetRef = targetRef || React.useRef<HTMLDivElement>(null);
|
||||||
|
const finalPopoverReferenceRef = popoverReferenceRef || finalTargetRef;
|
||||||
|
|
||||||
|
const popoverTargetActiveRef = React.useRef(false);
|
||||||
|
const popoverContentActiveRef = React.useRef(false);
|
||||||
|
|
||||||
|
let closeTimeout: any;
|
||||||
|
|
||||||
|
const popoverEnterFn = React.useCallback((e: any) => {
|
||||||
|
popoverTargetActiveRef.current = true;
|
||||||
|
popoverContentActiveRef.current = false;
|
||||||
|
setOpen(true);
|
||||||
|
props.onMouseEnter?.(e);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const popoverLeaveFn = React.useCallback((e: any) => {
|
||||||
|
window.clearTimeout(closeTimeout);
|
||||||
|
closeTimeout = setTimeout(() => {
|
||||||
|
// if (popoverTargetActiveRef.current) {
|
||||||
|
// popoverTargetActiveRef.current = false;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (popoverContentActiveRef.current) {
|
||||||
|
popoverContentActiveRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
}, debounce);
|
||||||
|
props.onMouseLeave?.(e);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClickOutside = React.useCallback((e: MouseEvent) => {
|
||||||
|
const targetEl = e.target as HTMLElement;
|
||||||
|
|
||||||
|
const closestWrapper = targetEl.closest(".twui-popover-content");
|
||||||
|
const closestTarget = targetEl.closest(".twui-popover-target");
|
||||||
|
|
||||||
|
if (closestTarget) return;
|
||||||
|
|
||||||
|
if (!closestWrapper) {
|
||||||
|
return setOpen(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isPopover) return;
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{target ? (
|
||||||
|
<div
|
||||||
|
{...targetWrapperProps}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
ref={finalTargetRef}
|
||||||
|
onMouseEnter={
|
||||||
|
isPopover && (trigger === "hover" || hoverOpen)
|
||||||
|
? popoverEnterFn
|
||||||
|
: targetWrapperProps?.onMouseEnter
|
||||||
|
}
|
||||||
|
onMouseLeave={
|
||||||
|
isPopover && (trigger === "hover" || hoverOpen)
|
||||||
|
? popoverLeaveFn
|
||||||
|
: targetWrapperProps?.onMouseLeave
|
||||||
|
}
|
||||||
|
className={twMerge(
|
||||||
|
"twui-popover-target",
|
||||||
|
targetWrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{target}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{ready ? (
|
||||||
|
isPopover ? (
|
||||||
|
<PopoverComponent
|
||||||
|
{...props}
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
targetElRef={finalPopoverReferenceRef}
|
||||||
|
debounce={debounce}
|
||||||
|
popoverTargetActiveRef={popoverTargetActiveRef}
|
||||||
|
popoverContentActiveRef={popoverContentActiveRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ModalComponent {...props} open={open} setOpen={setOpen} />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
src/components/lib/elements/Pagination.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { ComponentProps, Dispatch, SetStateAction } from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
import EmptyContent from "./EmptyContent";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
|
||||||
|
type Props = ComponentProps<typeof Row> & {
|
||||||
|
page?: number;
|
||||||
|
setPage?: Dispatch<SetStateAction<number>>;
|
||||||
|
count?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Pagination Component
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function Pagination({
|
||||||
|
count,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
limit,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
if (!count || !page || !limit)
|
||||||
|
return (
|
||||||
|
<EmptyContent title={`count, page, and limit are all required`} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLimit = limit * page >= count;
|
||||||
|
|
||||||
|
const pages = Math.ceil(count / limit);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full justify-between flex-nowrap",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{pages > 1 && (
|
||||||
|
<Button
|
||||||
|
title="Next Page Button"
|
||||||
|
onClick={() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
setPage?.((prev) => prev - 1);
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
className={twMerge(
|
||||||
|
"p-1",
|
||||||
|
page == 1 ? "opacity-40 pointer-events-none" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Row className={twMerge("gap-6 w-full flex-nowrap justify-center")}>
|
||||||
|
<Span size="small" variant="faded">
|
||||||
|
Page {page} / {pages}
|
||||||
|
</Span>
|
||||||
|
{pages > 1 && (
|
||||||
|
<Row
|
||||||
|
className={twMerge(
|
||||||
|
"flex-nowrap overflow-x-auto p-1 max-w-[90%]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Array(pages)
|
||||||
|
.fill(0)
|
||||||
|
.map((p, index) => {
|
||||||
|
const isCurrent = page == index + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title={`Page ${index + 1}`}
|
||||||
|
onClick={() => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
setPage?.(index + 1);
|
||||||
|
}}
|
||||||
|
variant={
|
||||||
|
isCurrent ? "normal" : "outlined"
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
color={isCurrent ? "primary" : "gray"}
|
||||||
|
className={twMerge(
|
||||||
|
"p-1 w-6 h-6 min-w-6"
|
||||||
|
)}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{pages > 1 && (
|
||||||
|
<Button
|
||||||
|
title="Next Page Button"
|
||||||
|
onClick={() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
setPage?.((prev) => prev + 1);
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
className={twMerge(
|
||||||
|
"p-1",
|
||||||
|
isLimit ? "opacity-40 pointer-events-none" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/components/lib/elements/Paper.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes, RefObject } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # General paper
|
||||||
|
* @className_wrapper twui-paper
|
||||||
|
*/
|
||||||
|
export default function Paper({
|
||||||
|
variant,
|
||||||
|
linkProps,
|
||||||
|
componentRef,
|
||||||
|
...props
|
||||||
|
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
|
variant?: "normal";
|
||||||
|
linkProps?: DetailedHTMLProps<
|
||||||
|
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
HTMLAnchorElement
|
||||||
|
>;
|
||||||
|
componentRef?: RefObject<HTMLDivElement | null>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
ref={componentRef as any}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-col items-start p-4 rounded bg-background-light dark:bg-background-dark gap-4",
|
||||||
|
"border border-slate-200 dark:border-white/10 border-solid w-full",
|
||||||
|
"relative",
|
||||||
|
"twui-paper",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||