├── .dockerignore ├── .github └── workflows │ ├── demo.yml │ └── main.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── compose.yml ├── default.conf ├── demo ├── css │ └── main.css ├── favicon.ico ├── icons │ ├── authelia.png │ ├── calibre.jpg │ ├── drawio.png │ ├── excalidraw.png │ ├── filegator.png │ ├── gitlab.jpg │ ├── home-assistant.svg │ ├── jellyfin.svg │ ├── lemmy.jpg │ ├── linkace.png │ ├── mastodon.jpg │ ├── mealie.jpg │ ├── metube.jpg │ ├── miniflux.jpg │ ├── outline.jpg │ ├── penpot.png │ ├── portainer.png │ ├── proxmox.png │ ├── roher-twins.png │ ├── router.png │ ├── shamir.jpg │ ├── synology.png │ ├── wallabag.png │ └── wastebin.jpg ├── index.html └── logo.png ├── docker-entrypoint.sh ├── dockerfile ├── index.html ├── license ├── package-lock.json ├── package.json ├── preview-dark.jpg ├── preview.jpg ├── public ├── favicon.ico └── logo.png ├── readme.md ├── src ├── components │ ├── anchor.ts │ ├── header.ts │ ├── icon.ts │ ├── service-catalogs.ts │ └── services.ts ├── config.json ├── config │ └── config.json ├── css-cache-break.ts ├── index.ts ├── pages │ └── index.ts ├── shared │ ├── files.ts │ ├── is.ts │ └── types.ts ├── tailwind.css └── variables.ts ├── tailwind.config.js ├── tsconfig.json └── version /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | node_modules 4 | dist 5 | dist-ssr 6 | compose.yml -------------------------------------------------------------------------------- /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: "./demo" 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: docker-ci 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | 9 | env: 10 | ORIGIN_REPO: notclickable-jordan 11 | USER: jordanroher 12 | REPO: starbase-80 13 | 14 | jobs: 15 | build: 16 | permissions: 17 | contents: write 18 | packages: write 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set variables 25 | run: | 26 | VER=$(cat version) 27 | echo "VERSION=$VER" >> $GITHUB_ENV 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v2 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v2 34 | 35 | - name: Login to Docker Hub 36 | if: ${{ github.repository_owner == env.ORIGIN_REPO }} 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKERHUB_USERNAME }} 40 | password: ${{ secrets.DOCKERHUB_TOKEN }} 41 | 42 | - name: Log in to the Container registry 43 | uses: docker/login-action@v3 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.repository_owner }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Extract metadata (tags, labels) for Docker 50 | id: meta 51 | uses: docker/metadata-action@v5 52 | with: 53 | tags: | 54 | type=raw,enable=true,value=${{ env.VERSION }} 55 | type=raw,enable=true,value=latest 56 | images: | 57 | name=${{env.USER}}/${{ env.REPO }},enable=${{ github.repository_owner == env.ORIGIN_REPO }} 58 | name=ghcr.io/${{ github.repository }} 59 | 60 | - name: Build and push 61 | uses: docker/build-push-action@v4 62 | with: 63 | context: . 64 | push: true 65 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | src/*.js 5 | src/**/*.js 6 | public/index.html 7 | public/main.css 8 | public/css 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "node_modules": true 10 | }, 11 | "editor.formatOnSave": true, 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll": "explicit", 14 | "source.organizeImports": "explicit" 15 | }, 16 | "editor.defaultFormatter": "esbenp.prettier-vscode", 17 | "editor.tabSize": 4 18 | } 19 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | homepage: 3 | image: jordanroher/starbase-80 4 | ports: 5 | - 4173:4173 6 | volumes: 7 | - ./config.json:/app/src/config/config.json 8 | - ./icons:/app/public/icons # or wherever, JSON icon paths are relative to /app/public 9 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 4173; 3 | 4 | root /app/public; 5 | 6 | location /app { 7 | try_files $uri $uri/ =404; 8 | 9 | # Cache settings for static content 10 | expires 7d; # Cache static content for 7 days 11 | add_header Cache-Control "public, max-age=604800, immutable"; 12 | } 13 | 14 | error_page 404 /index.html; 15 | error_page 500 502 503 504 /index.html; 16 | } 17 | -------------------------------------------------------------------------------- /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/favicon.ico -------------------------------------------------------------------------------- /demo/icons/authelia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/authelia.png -------------------------------------------------------------------------------- /demo/icons/calibre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/calibre.jpg -------------------------------------------------------------------------------- /demo/icons/drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/drawio.png -------------------------------------------------------------------------------- /demo/icons/excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/excalidraw.png -------------------------------------------------------------------------------- /demo/icons/filegator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/filegator.png -------------------------------------------------------------------------------- /demo/icons/gitlab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/gitlab.jpg -------------------------------------------------------------------------------- /demo/icons/home-assistant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/icons/jellyfin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/icons/lemmy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/lemmy.jpg -------------------------------------------------------------------------------- /demo/icons/linkace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/linkace.png -------------------------------------------------------------------------------- /demo/icons/mastodon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/mastodon.jpg -------------------------------------------------------------------------------- /demo/icons/mealie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/mealie.jpg -------------------------------------------------------------------------------- /demo/icons/metube.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/metube.jpg -------------------------------------------------------------------------------- /demo/icons/miniflux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/miniflux.jpg -------------------------------------------------------------------------------- /demo/icons/outline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/outline.jpg -------------------------------------------------------------------------------- /demo/icons/penpot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/penpot.png -------------------------------------------------------------------------------- /demo/icons/portainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/portainer.png -------------------------------------------------------------------------------- /demo/icons/proxmox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/proxmox.png -------------------------------------------------------------------------------- /demo/icons/roher-twins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/roher-twins.png -------------------------------------------------------------------------------- /demo/icons/router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/router.png -------------------------------------------------------------------------------- /demo/icons/shamir.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/shamir.jpg -------------------------------------------------------------------------------- /demo/icons/synology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/synology.png -------------------------------------------------------------------------------- /demo/icons/wallabag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/wallabag.png -------------------------------------------------------------------------------- /demo/icons/wastebin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/icons/wastebin.jpg -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Starbase 80 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
18 |
19 | Starbase 80 20 |

Starbase 80

21 |
22 |
23 |
24 | 483 |
484 |
485 |
486 |
487 | 488 | 489 | -------------------------------------------------------------------------------- /demo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/demo/logo.png -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Escape slashes 4 | LOGO=$(echo "${LOGO}" | sed 's/\//\\\//g') 5 | 6 | # HTML replacement 7 | sed -i -e 's/My Website/'"${TITLE}"'/g' /app/index.html 8 | sed -i -e 's/\/logo\.png/'"${LOGO}"'/g' /app/index.html 9 | 10 | # TypeScript replacement 11 | sed -i -e 's/PAGETITLE = "My Website"/PAGETITLE = "'"${TITLE}"'"/g' /app/src/variables.ts 12 | sed -i -e 's/PAGEICON = "\/logo\.png"/PAGEICON = "'"${LOGO}"'"/g' /app/src/variables.ts 13 | sed -i -e 's/SHOWHEADER = true/SHOWHEADER = '"${HEADER}"'/g' /app/src/variables.ts 14 | sed -i -e 's/SHOWHEADERLINE = true/SHOWHEADERLINE = '"${HEADERLINE}"'/g' /app/src/variables.ts 15 | sed -i -e 's/SHOWHEADERTOP = false/SHOWHEADERTOP = '"${HEADERTOP}"'/g' /app/src/variables.ts 16 | sed -i -e 's/CATEGORIES = "normal"/CATEGORIES = "'"${CATEGORIES}"'"/g' /app/src/variables.ts 17 | sed -i -e 's/NEWWINDOW = true/NEWWINDOW = '"${NEWWINDOW}"'/g' /app/src/variables.ts 18 | 19 | # CSS replacement 20 | sed -i -e 's/background-color: theme(colors\.slate\.50)/background-color: '"${BGCOLOR}"'/g' /app/src/tailwind.css 21 | sed -i -e 's/background-color: theme(colors\.gray\.950)/background-color: '"${BGCOLORDARK}"'/g' /app/src/tailwind.css 22 | sed -i -e 's/background-color: theme(colors\.white)\; \/\* category light \*\//background-color: '"${CATEGORYBUBBLECOLORLIGHT}"\;'/g' /app/src/tailwind.css 23 | sed -i -e 's/background-color: theme(colors\.black)\; \/\* category dark \*\//background-color: '"${CATEGORYBUBBLECOLORDARK}"\;'/g' /app/src/tailwind.css 24 | 25 | # Dark theme 26 | if [ "$THEME" = "dark" ]; then sed -i -e 's/darkMode: "media"/darkMode: "selector"/g' /app/tailwind.config.js; fi 27 | if [ "$THEME" = "dark" ]; then sed -i -e 's/ 2 | 3 | 4 | 5 | 6 | My Website 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright 2023 Jordan Roher 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starbase-80", 3 | "version": "1.4.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "starbase-80", 9 | "version": "1.4.0", 10 | "dependencies": { 11 | "@types/node": "^20.10.5", 12 | "clean-css": "^5.3.3", 13 | "html-minifier": "^4.0.0", 14 | "prettier": "^3.1.1", 15 | "tailwindcss": "^3.3.6", 16 | "typescript": "^5.3.3" 17 | }, 18 | "devDependencies": { 19 | "@types/clean-css": "^4.2.11" 20 | } 21 | }, 22 | "node_modules/@alloc/quick-lru": { 23 | "version": "5.2.0", 24 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 25 | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 26 | "engines": { 27 | "node": ">=10" 28 | }, 29 | "funding": { 30 | "url": "https://github.com/sponsors/sindresorhus" 31 | } 32 | }, 33 | "node_modules/@jridgewell/gen-mapping": { 34 | "version": "0.3.3", 35 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", 36 | "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", 37 | "dependencies": { 38 | "@jridgewell/set-array": "^1.0.1", 39 | "@jridgewell/sourcemap-codec": "^1.4.10", 40 | "@jridgewell/trace-mapping": "^0.3.9" 41 | }, 42 | "engines": { 43 | "node": ">=6.0.0" 44 | } 45 | }, 46 | "node_modules/@jridgewell/resolve-uri": { 47 | "version": "3.1.0", 48 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", 49 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", 50 | "engines": { 51 | "node": ">=6.0.0" 52 | } 53 | }, 54 | "node_modules/@jridgewell/set-array": { 55 | "version": "1.1.2", 56 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", 57 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", 58 | "engines": { 59 | "node": ">=6.0.0" 60 | } 61 | }, 62 | "node_modules/@jridgewell/sourcemap-codec": { 63 | "version": "1.4.15", 64 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 65 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" 66 | }, 67 | "node_modules/@jridgewell/trace-mapping": { 68 | "version": "0.3.18", 69 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", 70 | "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", 71 | "dependencies": { 72 | "@jridgewell/resolve-uri": "3.1.0", 73 | "@jridgewell/sourcemap-codec": "1.4.14" 74 | } 75 | }, 76 | "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { 77 | "version": "1.4.14", 78 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", 79 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" 80 | }, 81 | "node_modules/@nodelib/fs.scandir": { 82 | "version": "2.1.5", 83 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 84 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 85 | "dependencies": { 86 | "@nodelib/fs.stat": "2.0.5", 87 | "run-parallel": "^1.1.9" 88 | }, 89 | "engines": { 90 | "node": ">= 8" 91 | } 92 | }, 93 | "node_modules/@nodelib/fs.stat": { 94 | "version": "2.0.5", 95 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 96 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 97 | "engines": { 98 | "node": ">= 8" 99 | } 100 | }, 101 | "node_modules/@nodelib/fs.walk": { 102 | "version": "1.2.8", 103 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 104 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 105 | "dependencies": { 106 | "@nodelib/fs.scandir": "2.1.5", 107 | "fastq": "^1.6.0" 108 | }, 109 | "engines": { 110 | "node": ">= 8" 111 | } 112 | }, 113 | "node_modules/@types/clean-css": { 114 | "version": "4.2.11", 115 | "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", 116 | "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", 117 | "dev": true, 118 | "dependencies": { 119 | "@types/node": "*", 120 | "source-map": "^0.6.0" 121 | } 122 | }, 123 | "node_modules/@types/node": { 124 | "version": "20.10.5", 125 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", 126 | "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", 127 | "dependencies": { 128 | "undici-types": "~5.26.4" 129 | } 130 | }, 131 | "node_modules/any-promise": { 132 | "version": "1.3.0", 133 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 134 | "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" 135 | }, 136 | "node_modules/anymatch": { 137 | "version": "3.1.3", 138 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 139 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 140 | "dependencies": { 141 | "normalize-path": "^3.0.0", 142 | "picomatch": "^2.0.4" 143 | }, 144 | "engines": { 145 | "node": ">= 8" 146 | } 147 | }, 148 | "node_modules/arg": { 149 | "version": "5.0.2", 150 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", 151 | "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" 152 | }, 153 | "node_modules/balanced-match": { 154 | "version": "1.0.2", 155 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 156 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 157 | }, 158 | "node_modules/binary-extensions": { 159 | "version": "2.2.0", 160 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 161 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 162 | "engines": { 163 | "node": ">=8" 164 | } 165 | }, 166 | "node_modules/brace-expansion": { 167 | "version": "1.1.11", 168 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 169 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 170 | "dependencies": { 171 | "balanced-match": "^1.0.0", 172 | "concat-map": "0.0.1" 173 | } 174 | }, 175 | "node_modules/braces": { 176 | "version": "3.0.2", 177 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 178 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 179 | "dependencies": { 180 | "fill-range": "^7.0.1" 181 | }, 182 | "engines": { 183 | "node": ">=8" 184 | } 185 | }, 186 | "node_modules/camel-case": { 187 | "version": "3.0.0", 188 | "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", 189 | "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", 190 | "dependencies": { 191 | "no-case": "^2.2.0", 192 | "upper-case": "^1.1.1" 193 | } 194 | }, 195 | "node_modules/camelcase-css": { 196 | "version": "2.0.1", 197 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 198 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", 199 | "engines": { 200 | "node": ">= 6" 201 | } 202 | }, 203 | "node_modules/chokidar": { 204 | "version": "3.5.3", 205 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 206 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 207 | "funding": [ 208 | { 209 | "type": "individual", 210 | "url": "https://paulmillr.com/funding/" 211 | } 212 | ], 213 | "dependencies": { 214 | "anymatch": "~3.1.2", 215 | "braces": "~3.0.2", 216 | "glob-parent": "~5.1.2", 217 | "is-binary-path": "~2.1.0", 218 | "is-glob": "~4.0.1", 219 | "normalize-path": "~3.0.0", 220 | "readdirp": "~3.6.0" 221 | }, 222 | "engines": { 223 | "node": ">= 8.10.0" 224 | }, 225 | "optionalDependencies": { 226 | "fsevents": "~2.3.2" 227 | } 228 | }, 229 | "node_modules/chokidar/node_modules/glob-parent": { 230 | "version": "5.1.2", 231 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 232 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 233 | "dependencies": { 234 | "is-glob": "^4.0.1" 235 | }, 236 | "engines": { 237 | "node": ">= 6" 238 | } 239 | }, 240 | "node_modules/clean-css": { 241 | "version": "5.3.3", 242 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", 243 | "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", 244 | "dependencies": { 245 | "source-map": "~0.6.0" 246 | }, 247 | "engines": { 248 | "node": ">= 10.0" 249 | } 250 | }, 251 | "node_modules/commander": { 252 | "version": "4.1.1", 253 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 254 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 255 | "engines": { 256 | "node": ">= 6" 257 | } 258 | }, 259 | "node_modules/concat-map": { 260 | "version": "0.0.1", 261 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 262 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 263 | }, 264 | "node_modules/cssesc": { 265 | "version": "3.0.0", 266 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 267 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", 268 | "bin": { 269 | "cssesc": "bin/cssesc" 270 | }, 271 | "engines": { 272 | "node": ">=4" 273 | } 274 | }, 275 | "node_modules/didyoumean": { 276 | "version": "1.2.2", 277 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", 278 | "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" 279 | }, 280 | "node_modules/dlv": { 281 | "version": "1.1.3", 282 | "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", 283 | "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" 284 | }, 285 | "node_modules/fast-glob": { 286 | "version": "3.3.2", 287 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 288 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 289 | "dependencies": { 290 | "@nodelib/fs.stat": "^2.0.2", 291 | "@nodelib/fs.walk": "^1.2.3", 292 | "glob-parent": "^5.1.2", 293 | "merge2": "^1.3.0", 294 | "micromatch": "^4.0.4" 295 | }, 296 | "engines": { 297 | "node": ">=8.6.0" 298 | } 299 | }, 300 | "node_modules/fast-glob/node_modules/glob-parent": { 301 | "version": "5.1.2", 302 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 303 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 304 | "dependencies": { 305 | "is-glob": "^4.0.1" 306 | }, 307 | "engines": { 308 | "node": ">= 6" 309 | } 310 | }, 311 | "node_modules/fastq": { 312 | "version": "1.15.0", 313 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", 314 | "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", 315 | "dependencies": { 316 | "reusify": "^1.0.4" 317 | } 318 | }, 319 | "node_modules/fill-range": { 320 | "version": "7.0.1", 321 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 322 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 323 | "dependencies": { 324 | "to-regex-range": "^5.0.1" 325 | }, 326 | "engines": { 327 | "node": ">=8" 328 | } 329 | }, 330 | "node_modules/fs.realpath": { 331 | "version": "1.0.0", 332 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 333 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 334 | }, 335 | "node_modules/fsevents": { 336 | "version": "2.3.3", 337 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 338 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 339 | "hasInstallScript": true, 340 | "optional": true, 341 | "os": [ 342 | "darwin" 343 | ], 344 | "engines": { 345 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 346 | } 347 | }, 348 | "node_modules/function-bind": { 349 | "version": "1.1.2", 350 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 351 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 352 | "funding": { 353 | "url": "https://github.com/sponsors/ljharb" 354 | } 355 | }, 356 | "node_modules/glob": { 357 | "version": "7.1.6", 358 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 359 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 360 | "dependencies": { 361 | "fs.realpath": "^1.0.0", 362 | "inflight": "^1.0.4", 363 | "inherits": "2", 364 | "minimatch": "^3.0.4", 365 | "once": "^1.3.0", 366 | "path-is-absolute": "^1.0.0" 367 | }, 368 | "engines": { 369 | "node": "*" 370 | }, 371 | "funding": { 372 | "url": "https://github.com/sponsors/isaacs" 373 | } 374 | }, 375 | "node_modules/glob-parent": { 376 | "version": "6.0.2", 377 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 378 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 379 | "dependencies": { 380 | "is-glob": "^4.0.3" 381 | }, 382 | "engines": { 383 | "node": ">=10.13.0" 384 | } 385 | }, 386 | "node_modules/has": { 387 | "version": "1.0.3", 388 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 389 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 390 | "dependencies": { 391 | "function-bind": "^1.1.1" 392 | }, 393 | "engines": { 394 | "node": ">= 0.4.0" 395 | } 396 | }, 397 | "node_modules/he": { 398 | "version": "1.2.0", 399 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 400 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 401 | "bin": { 402 | "he": "bin/he" 403 | } 404 | }, 405 | "node_modules/html-minifier": { 406 | "version": "4.0.0", 407 | "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", 408 | "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", 409 | "dependencies": { 410 | "camel-case": "^3.0.0", 411 | "clean-css": "^4.2.1", 412 | "commander": "^2.19.0", 413 | "he": "^1.2.0", 414 | "param-case": "^2.1.1", 415 | "relateurl": "^0.2.7", 416 | "uglify-js": "^3.5.1" 417 | }, 418 | "bin": { 419 | "html-minifier": "cli.js" 420 | }, 421 | "engines": { 422 | "node": ">=6" 423 | } 424 | }, 425 | "node_modules/html-minifier/node_modules/clean-css": { 426 | "version": "4.2.4", 427 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", 428 | "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", 429 | "dependencies": { 430 | "source-map": "~0.6.0" 431 | }, 432 | "engines": { 433 | "node": ">= 4.0" 434 | } 435 | }, 436 | "node_modules/html-minifier/node_modules/commander": { 437 | "version": "2.20.3", 438 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 439 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 440 | }, 441 | "node_modules/inflight": { 442 | "version": "1.0.6", 443 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 444 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 445 | "dependencies": { 446 | "once": "^1.3.0", 447 | "wrappy": "1" 448 | } 449 | }, 450 | "node_modules/inherits": { 451 | "version": "2.0.4", 452 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 453 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 454 | }, 455 | "node_modules/is-binary-path": { 456 | "version": "2.1.0", 457 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 458 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 459 | "dependencies": { 460 | "binary-extensions": "^2.0.0" 461 | }, 462 | "engines": { 463 | "node": ">=8" 464 | } 465 | }, 466 | "node_modules/is-core-module": { 467 | "version": "2.12.0", 468 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", 469 | "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", 470 | "dependencies": { 471 | "has": "^1.0.3" 472 | }, 473 | "funding": { 474 | "url": "https://github.com/sponsors/ljharb" 475 | } 476 | }, 477 | "node_modules/is-extglob": { 478 | "version": "2.1.1", 479 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 480 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 481 | "engines": { 482 | "node": ">=0.10.0" 483 | } 484 | }, 485 | "node_modules/is-glob": { 486 | "version": "4.0.3", 487 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 488 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 489 | "dependencies": { 490 | "is-extglob": "^2.1.1" 491 | }, 492 | "engines": { 493 | "node": ">=0.10.0" 494 | } 495 | }, 496 | "node_modules/is-number": { 497 | "version": "7.0.0", 498 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 499 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 500 | "engines": { 501 | "node": ">=0.12.0" 502 | } 503 | }, 504 | "node_modules/jiti": { 505 | "version": "1.21.0", 506 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", 507 | "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", 508 | "bin": { 509 | "jiti": "bin/jiti.js" 510 | } 511 | }, 512 | "node_modules/lilconfig": { 513 | "version": "2.1.0", 514 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", 515 | "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", 516 | "engines": { 517 | "node": ">=10" 518 | } 519 | }, 520 | "node_modules/lines-and-columns": { 521 | "version": "1.2.4", 522 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 523 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" 524 | }, 525 | "node_modules/lower-case": { 526 | "version": "1.1.4", 527 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", 528 | "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==" 529 | }, 530 | "node_modules/merge2": { 531 | "version": "1.4.1", 532 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 533 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 534 | "engines": { 535 | "node": ">= 8" 536 | } 537 | }, 538 | "node_modules/micromatch": { 539 | "version": "4.0.5", 540 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 541 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 542 | "dependencies": { 543 | "braces": "^3.0.2", 544 | "picomatch": "^2.3.1" 545 | }, 546 | "engines": { 547 | "node": ">=8.6" 548 | } 549 | }, 550 | "node_modules/minimatch": { 551 | "version": "3.1.2", 552 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 553 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 554 | "dependencies": { 555 | "brace-expansion": "^1.1.7" 556 | }, 557 | "engines": { 558 | "node": "*" 559 | } 560 | }, 561 | "node_modules/mz": { 562 | "version": "2.7.0", 563 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 564 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 565 | "dependencies": { 566 | "any-promise": "^1.0.0", 567 | "object-assign": "^4.0.1", 568 | "thenify-all": "^1.0.0" 569 | } 570 | }, 571 | "node_modules/nanoid": { 572 | "version": "3.3.7", 573 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 574 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 575 | "funding": [ 576 | { 577 | "type": "github", 578 | "url": "https://github.com/sponsors/ai" 579 | } 580 | ], 581 | "bin": { 582 | "nanoid": "bin/nanoid.cjs" 583 | }, 584 | "engines": { 585 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 586 | } 587 | }, 588 | "node_modules/no-case": { 589 | "version": "2.3.2", 590 | "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", 591 | "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", 592 | "dependencies": { 593 | "lower-case": "^1.1.1" 594 | } 595 | }, 596 | "node_modules/normalize-path": { 597 | "version": "3.0.0", 598 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 599 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 600 | "engines": { 601 | "node": ">=0.10.0" 602 | } 603 | }, 604 | "node_modules/object-assign": { 605 | "version": "4.1.1", 606 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 607 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 608 | "engines": { 609 | "node": ">=0.10.0" 610 | } 611 | }, 612 | "node_modules/object-hash": { 613 | "version": "3.0.0", 614 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 615 | "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 616 | "engines": { 617 | "node": ">= 6" 618 | } 619 | }, 620 | "node_modules/once": { 621 | "version": "1.4.0", 622 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 623 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 624 | "dependencies": { 625 | "wrappy": "1" 626 | } 627 | }, 628 | "node_modules/param-case": { 629 | "version": "2.1.1", 630 | "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", 631 | "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", 632 | "dependencies": { 633 | "no-case": "^2.2.0" 634 | } 635 | }, 636 | "node_modules/path-is-absolute": { 637 | "version": "1.0.1", 638 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 639 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 640 | "engines": { 641 | "node": ">=0.10.0" 642 | } 643 | }, 644 | "node_modules/path-parse": { 645 | "version": "1.0.7", 646 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 647 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 648 | }, 649 | "node_modules/picocolors": { 650 | "version": "1.0.0", 651 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 652 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 653 | }, 654 | "node_modules/picomatch": { 655 | "version": "2.3.1", 656 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 657 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 658 | "engines": { 659 | "node": ">=8.6" 660 | }, 661 | "funding": { 662 | "url": "https://github.com/sponsors/jonschlinkert" 663 | } 664 | }, 665 | "node_modules/pify": { 666 | "version": "2.3.0", 667 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 668 | "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 669 | "engines": { 670 | "node": ">=0.10.0" 671 | } 672 | }, 673 | "node_modules/pirates": { 674 | "version": "4.0.5", 675 | "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", 676 | "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", 677 | "engines": { 678 | "node": ">= 6" 679 | } 680 | }, 681 | "node_modules/postcss": { 682 | "version": "8.4.32", 683 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", 684 | "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", 685 | "funding": [ 686 | { 687 | "type": "opencollective", 688 | "url": "https://opencollective.com/postcss/" 689 | }, 690 | { 691 | "type": "tidelift", 692 | "url": "https://tidelift.com/funding/github/npm/postcss" 693 | }, 694 | { 695 | "type": "github", 696 | "url": "https://github.com/sponsors/ai" 697 | } 698 | ], 699 | "dependencies": { 700 | "nanoid": "^3.3.7", 701 | "picocolors": "^1.0.0", 702 | "source-map-js": "^1.0.2" 703 | }, 704 | "engines": { 705 | "node": "^10 || ^12 || >=14" 706 | } 707 | }, 708 | "node_modules/postcss-import": { 709 | "version": "15.1.0", 710 | "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", 711 | "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", 712 | "dependencies": { 713 | "postcss-value-parser": "^4.0.0", 714 | "read-cache": "^1.0.0", 715 | "resolve": "^1.1.7" 716 | }, 717 | "engines": { 718 | "node": ">=14.0.0" 719 | }, 720 | "peerDependencies": { 721 | "postcss": "^8.0.0" 722 | } 723 | }, 724 | "node_modules/postcss-js": { 725 | "version": "4.0.1", 726 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", 727 | "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", 728 | "dependencies": { 729 | "camelcase-css": "^2.0.1" 730 | }, 731 | "engines": { 732 | "node": "^12 || ^14 || >= 16" 733 | }, 734 | "funding": { 735 | "type": "opencollective", 736 | "url": "https://opencollective.com/postcss/" 737 | }, 738 | "peerDependencies": { 739 | "postcss": "^8.4.21" 740 | } 741 | }, 742 | "node_modules/postcss-load-config": { 743 | "version": "4.0.1", 744 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", 745 | "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", 746 | "dependencies": { 747 | "lilconfig": "^2.0.5", 748 | "yaml": "^2.1.1" 749 | }, 750 | "engines": { 751 | "node": ">= 14" 752 | }, 753 | "funding": { 754 | "type": "opencollective", 755 | "url": "https://opencollective.com/postcss/" 756 | }, 757 | "peerDependencies": { 758 | "postcss": ">=8.0.9", 759 | "ts-node": ">=9.0.0" 760 | }, 761 | "peerDependenciesMeta": { 762 | "postcss": { 763 | "optional": true 764 | }, 765 | "ts-node": { 766 | "optional": true 767 | } 768 | } 769 | }, 770 | "node_modules/postcss-nested": { 771 | "version": "6.0.1", 772 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", 773 | "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", 774 | "dependencies": { 775 | "postcss-selector-parser": "^6.0.11" 776 | }, 777 | "engines": { 778 | "node": ">=12.0" 779 | }, 780 | "funding": { 781 | "type": "opencollective", 782 | "url": "https://opencollective.com/postcss/" 783 | }, 784 | "peerDependencies": { 785 | "postcss": "^8.2.14" 786 | } 787 | }, 788 | "node_modules/postcss-selector-parser": { 789 | "version": "6.0.13", 790 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", 791 | "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", 792 | "dependencies": { 793 | "cssesc": "^3.0.0", 794 | "util-deprecate": "^1.0.2" 795 | }, 796 | "engines": { 797 | "node": ">=4" 798 | } 799 | }, 800 | "node_modules/postcss-value-parser": { 801 | "version": "4.2.0", 802 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 803 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" 804 | }, 805 | "node_modules/prettier": { 806 | "version": "3.1.1", 807 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", 808 | "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", 809 | "bin": { 810 | "prettier": "bin/prettier.cjs" 811 | }, 812 | "engines": { 813 | "node": ">=14" 814 | }, 815 | "funding": { 816 | "url": "https://github.com/prettier/prettier?sponsor=1" 817 | } 818 | }, 819 | "node_modules/queue-microtask": { 820 | "version": "1.2.3", 821 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 822 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 823 | "funding": [ 824 | { 825 | "type": "github", 826 | "url": "https://github.com/sponsors/feross" 827 | }, 828 | { 829 | "type": "patreon", 830 | "url": "https://www.patreon.com/feross" 831 | }, 832 | { 833 | "type": "consulting", 834 | "url": "https://feross.org/support" 835 | } 836 | ] 837 | }, 838 | "node_modules/read-cache": { 839 | "version": "1.0.0", 840 | "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", 841 | "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 842 | "dependencies": { 843 | "pify": "^2.3.0" 844 | } 845 | }, 846 | "node_modules/readdirp": { 847 | "version": "3.6.0", 848 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 849 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 850 | "dependencies": { 851 | "picomatch": "^2.2.1" 852 | }, 853 | "engines": { 854 | "node": ">=8.10.0" 855 | } 856 | }, 857 | "node_modules/relateurl": { 858 | "version": "0.2.7", 859 | "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", 860 | "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", 861 | "engines": { 862 | "node": ">= 0.10" 863 | } 864 | }, 865 | "node_modules/resolve": { 866 | "version": "1.22.2", 867 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", 868 | "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", 869 | "dependencies": { 870 | "is-core-module": "^2.11.0", 871 | "path-parse": "^1.0.7", 872 | "supports-preserve-symlinks-flag": "^1.0.0" 873 | }, 874 | "bin": { 875 | "resolve": "bin/resolve" 876 | }, 877 | "funding": { 878 | "url": "https://github.com/sponsors/ljharb" 879 | } 880 | }, 881 | "node_modules/reusify": { 882 | "version": "1.0.4", 883 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 884 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 885 | "engines": { 886 | "iojs": ">=1.0.0", 887 | "node": ">=0.10.0" 888 | } 889 | }, 890 | "node_modules/run-parallel": { 891 | "version": "1.2.0", 892 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 893 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 894 | "funding": [ 895 | { 896 | "type": "github", 897 | "url": "https://github.com/sponsors/feross" 898 | }, 899 | { 900 | "type": "patreon", 901 | "url": "https://www.patreon.com/feross" 902 | }, 903 | { 904 | "type": "consulting", 905 | "url": "https://feross.org/support" 906 | } 907 | ], 908 | "dependencies": { 909 | "queue-microtask": "^1.2.2" 910 | } 911 | }, 912 | "node_modules/source-map": { 913 | "version": "0.6.1", 914 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 915 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 916 | "engines": { 917 | "node": ">=0.10.0" 918 | } 919 | }, 920 | "node_modules/source-map-js": { 921 | "version": "1.0.2", 922 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 923 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 924 | "engines": { 925 | "node": ">=0.10.0" 926 | } 927 | }, 928 | "node_modules/sucrase": { 929 | "version": "3.32.0", 930 | "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", 931 | "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", 932 | "dependencies": { 933 | "@jridgewell/gen-mapping": "^0.3.2", 934 | "commander": "^4.0.0", 935 | "glob": "7.1.6", 936 | "lines-and-columns": "^1.1.6", 937 | "mz": "^2.7.0", 938 | "pirates": "^4.0.1", 939 | "ts-interface-checker": "^0.1.9" 940 | }, 941 | "bin": { 942 | "sucrase": "bin/sucrase", 943 | "sucrase-node": "bin/sucrase-node" 944 | }, 945 | "engines": { 946 | "node": ">=8" 947 | } 948 | }, 949 | "node_modules/supports-preserve-symlinks-flag": { 950 | "version": "1.0.0", 951 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 952 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 953 | "engines": { 954 | "node": ">= 0.4" 955 | }, 956 | "funding": { 957 | "url": "https://github.com/sponsors/ljharb" 958 | } 959 | }, 960 | "node_modules/tailwindcss": { 961 | "version": "3.3.6", 962 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz", 963 | "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==", 964 | "dependencies": { 965 | "@alloc/quick-lru": "^5.2.0", 966 | "arg": "^5.0.2", 967 | "chokidar": "^3.5.3", 968 | "didyoumean": "^1.2.2", 969 | "dlv": "^1.1.3", 970 | "fast-glob": "^3.3.0", 971 | "glob-parent": "^6.0.2", 972 | "is-glob": "^4.0.3", 973 | "jiti": "^1.19.1", 974 | "lilconfig": "^2.1.0", 975 | "micromatch": "^4.0.5", 976 | "normalize-path": "^3.0.0", 977 | "object-hash": "^3.0.0", 978 | "picocolors": "^1.0.0", 979 | "postcss": "^8.4.23", 980 | "postcss-import": "^15.1.0", 981 | "postcss-js": "^4.0.1", 982 | "postcss-load-config": "^4.0.1", 983 | "postcss-nested": "^6.0.1", 984 | "postcss-selector-parser": "^6.0.11", 985 | "resolve": "^1.22.2", 986 | "sucrase": "^3.32.0" 987 | }, 988 | "bin": { 989 | "tailwind": "lib/cli.js", 990 | "tailwindcss": "lib/cli.js" 991 | }, 992 | "engines": { 993 | "node": ">=14.0.0" 994 | } 995 | }, 996 | "node_modules/thenify": { 997 | "version": "3.3.1", 998 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 999 | "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 1000 | "dependencies": { 1001 | "any-promise": "^1.0.0" 1002 | } 1003 | }, 1004 | "node_modules/thenify-all": { 1005 | "version": "1.6.0", 1006 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 1007 | "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 1008 | "dependencies": { 1009 | "thenify": ">= 3.1.0 < 4" 1010 | }, 1011 | "engines": { 1012 | "node": ">=0.8" 1013 | } 1014 | }, 1015 | "node_modules/to-regex-range": { 1016 | "version": "5.0.1", 1017 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1018 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1019 | "dependencies": { 1020 | "is-number": "^7.0.0" 1021 | }, 1022 | "engines": { 1023 | "node": ">=8.0" 1024 | } 1025 | }, 1026 | "node_modules/ts-interface-checker": { 1027 | "version": "0.1.13", 1028 | "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", 1029 | "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" 1030 | }, 1031 | "node_modules/typescript": { 1032 | "version": "5.3.3", 1033 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 1034 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 1035 | "bin": { 1036 | "tsc": "bin/tsc", 1037 | "tsserver": "bin/tsserver" 1038 | }, 1039 | "engines": { 1040 | "node": ">=14.17" 1041 | } 1042 | }, 1043 | "node_modules/uglify-js": { 1044 | "version": "3.17.4", 1045 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", 1046 | "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", 1047 | "bin": { 1048 | "uglifyjs": "bin/uglifyjs" 1049 | }, 1050 | "engines": { 1051 | "node": ">=0.8.0" 1052 | } 1053 | }, 1054 | "node_modules/undici-types": { 1055 | "version": "5.26.5", 1056 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1057 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 1058 | }, 1059 | "node_modules/upper-case": { 1060 | "version": "1.1.3", 1061 | "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", 1062 | "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==" 1063 | }, 1064 | "node_modules/util-deprecate": { 1065 | "version": "1.0.2", 1066 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1067 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1068 | }, 1069 | "node_modules/wrappy": { 1070 | "version": "1.0.2", 1071 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1072 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1073 | }, 1074 | "node_modules/yaml": { 1075 | "version": "2.3.1", 1076 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", 1077 | "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", 1078 | "engines": { 1079 | "node": ">= 14" 1080 | } 1081 | } 1082 | } 1083 | } 1084 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starbase-80", 3 | "displayName": "Starbase 80", 4 | "homepage": "https://www.starbase80.dev/", 5 | "repository": { 6 | "url": "https://github.com/notclickable-jordan/starbase-80/" 7 | }, 8 | "version": "1.4.0", 9 | "type": "commonjs", 10 | "scripts": { 11 | "build": "tsc && node ./dist/index.js && npm run html && npm run tailwind && npm run css-cache-break", 12 | "html": "html-minifier --remove-comments --collapse-whitespace --input-dir ./public --output-dir ./public --file-ext html", 13 | "tailwind": "npx tailwindcss -i ./src/tailwind.css -o ./public/main.css", 14 | "css-cache-break": "node ./dist/css-cache-break.js" 15 | }, 16 | "dependencies": { 17 | "@types/node": "^20.10.5", 18 | "clean-css": "^5.3.3", 19 | "html-minifier": "^4.0.0", 20 | "prettier": "^3.1.1", 21 | "tailwindcss": "^3.3.6", 22 | "typescript": "^5.3.3" 23 | }, 24 | "prettier": { 25 | "arrowParens": "avoid", 26 | "jsxBracketSameLine": true, 27 | "printWidth": 120, 28 | "semi": true, 29 | "tabWidth": 4, 30 | "trailingComma": "es5", 31 | "useTabs": true 32 | }, 33 | "devDependencies": { 34 | "@types/clean-css": "^4.2.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /preview-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/preview-dark.jpg -------------------------------------------------------------------------------- /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/preview.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notclickable-jordan/starbase-80/b1316fbd2050a32367af5406185ff5d2f95406cd/public/logo.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Starbase 80 2 | 3 | > DR T'ANA: (to Mariner) "You wanna goof around, go work on Starbase 80!"
4 | > JET: "Damn, Starbase 80?!" 5 | 6 | # About 7 | 8 | A nice looking homepage for Docker containers or any services and links. 9 | 10 | No actual integration with Docker. Loads instantly. Dark mode follows your OS. 11 | 12 | If you make a change to the config JSON, restart this container and refresh. 13 | 14 | Inspired by [Ben Phelps' Homepage](https://gethomepage.dev/) and [Umbrel](https://umbrel.com/). Dedicated to [Star Trek: Lower Decks](https://www.paramountplus.com/shows/star-trek-lower-decks/). 15 | 16 | # Docker and source code 17 | 18 | - [Docker image](https://hub.docker.com/r/jordanroher/starbase-80) 19 | - [Source code on GitHub](https://github.com/notclickable-jordan/starbase-80) 20 | 21 | # Live demo 22 | 23 | - [Starbase 80](https://notclickable-jordan.github.io/starbase-80/) 24 | 25 | # Preview 26 | 27 | Light mode 28 | 29 |
30 | 31 | Dark mode 32 | 33 | # Change history 34 | 35 | ## 1.6.1 36 | 37 | - Added `iconBubblePadding` boolean option to categories and services 38 | 39 | ## 1.6.0 40 | 41 | - Added `CATEGORYBUBBLECOLORLIGHT` and `CATEGORYBUBBLECOLORDARK` for greater control of category bubble colors 42 | - Added `category.bubbleBGLight` and `category.bubbleBGDark` to theme individual category bubble background colors 43 | 44 | ## 1.5.5 45 | 46 | - Updated to node:23-slim 47 | 48 | ## 1.5.4 49 | 50 | - Re-added wget to allow for health checks 51 | 52 | ## 1.5.3 53 | 54 | - Added support for [selfh.st](https://selfh.st/icons/) icons 55 | 56 | ## 1.5.2 57 | 58 | - Fixed issue with Material icons having incorrect left margin 59 | - Fixed aspect ratio issues on images 60 | 61 | ## 1.5.1 62 | 63 | - Changed links to be on the entire service object 64 | - Added underline option 65 | 66 | ## 1.5.0 67 | 68 | - Fixed dark mode manual override 69 | 70 | ## 1.4.2 71 | 72 | - Added `apple-touch-icon-precomposed` link 73 | 74 | ## 1.4.0 75 | 76 | - Rewrote the entire application to not use React. Now it's just a Node application that emits static HTML. 77 | - Removed lots of packages 78 | 79 | ## 1.3.0 80 | 81 | - Removed all JavaScript as part of the build step. The image will be slightly larger and take longer to start up and shut down, but the page will be even lighter. 82 | 83 | ## 1.2.0 84 | 85 | - Moved `config.json` bind mount destination to `/app/src/config/config.json` for improved Portainer and volume support. The previous bind mount location will continue to work, but we recommend updating your bind mounts. 86 | 87 | # Docker 88 | 89 | ## Sample Docker compose 90 | 91 | ```yaml 92 | services: 93 | starbase80: 94 | image: jordanroher/starbase-80 95 | ports: 96 | - 80:4173 97 | environment: 98 | - TITLE=Starbase 80 99 | - LOGO=/starbase80.jpg 100 | volumes: 101 | - ./config.json:/app/src/config/config.json 102 | - ./public/favicon.ico:/app/public/favicon.ico 103 | - ./public/logo.png:/app/public/logo.png 104 | - ./public/icons:/app/public/icons 105 | ``` 106 | 107 | ## Environment variables 108 | 109 | | Variable | Default | Notes | 110 | | ------------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | 111 | | TITLE | My Website | Set to TITLE= to hide the title | 112 | | LOGO | /logo.png | Set to LOGO= to hide the logo | 113 | | HEADER | true | Set to false to hide the title and logo | 114 | | HEADERLINE | true | Set to false to turn off the header border line | 115 | | CATEGORIES | normal | Set to "small" for smaller, uppercase category labels | 116 | | BGCOLOR | theme(colors.slate.50) | Page background color in light mode. Set to any hex color or Tailwind color using the theme syntax. | 117 | | BGCOLORDARK | theme(colors.gray.950) | Page background color in dark mode. Set to any hex color or Tailwind color using the theme syntax. | 118 | | CATEGORYBUBBLECOLORLIGHT | theme(colors.white) | Background color for category bubbles (if enabled) in light mode. Set to any hex color or Tailwind color using the theme syntax. | 119 | | CATEGORYBUBBLECOLORDARK | theme(colors.black) | Background color for category bubbles (if enabled) in dark mode. Set to any hex color or Tailwind color using the theme syntax. | 120 | | NEWWINDOW | true | Set to false to not have links open in a new window | 121 | | THEME | auto | Set to "auto", or "dark" to force a display mode (e.g. dark mode) | 122 | | HOVER | none | Set to "underline" for an underline effect on titles when hovering/focusing on that service | 123 | 124 | ## Volumes (bind mounts) 125 | 126 | | Path | Required | Notes | 127 | | --------------------------- | -------- | ----------------------------------------------------------------------------- | 128 | | /app/src/config/config.json | true | Configuration with list of sites and links | 129 | | /app/public/favicon.ico | false | Website favicon | 130 | | /app/public/logo.png | false | Logo in the header | 131 | | /app/public/icons | false | Or wherever you want to put them, JSON icon paths are relative to /app/public | 132 | 133 | # Configuration 134 | 135 | ## Sample config.json 136 | 137 | ```json 138 | [ 139 | { 140 | "category": "Services", 141 | "services": [ 142 | { 143 | "name": "Archivebox", 144 | "uri": "https://archivebox.mywebsite.com", 145 | "description": "Backup webpages", 146 | "icon": "/icons/archivebox.jpg" 147 | }, 148 | { 149 | "name": "Authelia", 150 | "uri": "https://auth.mywebsite.com", 151 | "description": "Authentication", 152 | "icon": "selfhst-authelia" 153 | }, 154 | { 155 | "name": "Calibre", 156 | "uri": "https://calibre.mywebsite.com", 157 | "description": "eBook library", 158 | "icon": "/icons/calibre.png" 159 | } 160 | ] 161 | }, 162 | { 163 | "category": "Devices", 164 | "bubble": true, 165 | "services": [ 166 | { 167 | "name": "Router", 168 | "uri": "http://192.168.1.1/", 169 | "description": "Netgear Orbi", 170 | "icon": "/icons/router.png" 171 | }, 172 | { 173 | "name": "Home Assistant", 174 | "uri": "http://homeassistant.local:8123/", 175 | "description": "Home automation", 176 | "icon": "home-assistant", 177 | "iconBubble": false 178 | }, 179 | { 180 | "name": "Synology", 181 | "uri": "http://synology:5000", 182 | "description": "Network storage", 183 | "icon": "/icons/synology.png" 184 | } 185 | ] 186 | } 187 | ] 188 | ``` 189 | 190 | ## Category variables 191 | 192 | | Name | Default | Required | Notes | 193 | | ----------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | 194 | | category | | false | Displays above the list of services | 195 | | bubble | false | false | Shows a bubble around category | 196 | | bubbleBGLight | | false | Background color for category bubbles. Must be a [Tailwind color](https://tailwindcss.com/docs/background-color) (do not prefix with `bg-`). | 197 | | bubbleBGDark | | false | Background color for category bubbles in dark mode. Must be a [Tailwind color](https://tailwindcss.com/docs/background-color) (do not prefix with `bg-`). | 198 | | iconBubblePadding | false | false | If `true`, adds a slight padding around each service's icons which are in a bubble. | 199 | | services | | true | Array of services | 200 | 201 | ## Service variables 202 | 203 | | Name | Default | Required | Notes | 204 | | ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 205 | | name | | true | Title of service | 206 | | uri | | true | Hyperlink to resource | 207 | | description | | false | 2-3 words which appear below the title | 208 | | icon | | false | Relative URI, absolute URI, service name ([Dashboard icon](https://github.com/walkxcode/dashboard-icons)), `mdi-` service name ([Material Design icon](https://icon-sets.iconify.design/mdi/)), `selfhst-` icon name [selfh.st icon](https://selfh.st/icons/) | 209 | | iconBG | | false | Background color for icons. Hex code or [Tailwind color](https://tailwindcss.com/docs/background-color) (do not prefix with `bg-`). | 210 | | iconColor | | false | Only used as the fill color for Material Design icons. Hex code or [Tailwind color](https://tailwindcss.com/docs/background-color) (do not prefix with `bg-`). | 211 | | iconBubble | true | false | If `false` the bubble and shadow are removed from the icon | 212 | | iconBubblePadding | false | false | Overrides `bubblePadding` set at the category level | 213 | | iconAspect | square | false | Set to `"width"` or `"height"` to constrain the icon to the width or height of the icon, respectively | 214 | | newWindow | | false | Set to `true` or `false` to override the environment variable `NEWWINDOW` for this service | 215 | 216 | # Icons 217 | 218 | ## Volume / bind mount 219 | 220 | Create a volume or bind mount to a subfolder of `/app/public` and specify a relative path. 221 | 222 | ```bash 223 | # Your folder 224 | compose.yml 225 | - icons 226 | - jellyfin.jpg 227 | - ghost.jpg 228 | - etc 229 | 230 | # Bind mount 231 | ./icons:/app/public/icons 232 | 233 | # Icon in config.json 234 | "icon": "/icons/jellyfin.jpg" 235 | ``` 236 | 237 | ## Dashboard icons 238 | 239 | Use [Dashboard icons](https://github.com/walkxcode/dashboard-icons) by specifying a name without any prefix. 240 | 241 | ```bash 242 | # Icon in config.json 243 | "icon": "jellyfin" 244 | ``` 245 | 246 | ## Material design 247 | 248 | Use any [Material Design icon](https://icon-sets.iconify.design/mdi/) by prefixing the name with `mdi-`. 249 | 250 | Fill the icon by providing an "iconColor." 251 | 252 | Use "black" or "white" for those colors. 253 | 254 | ```bash 255 | # Icon in config.json 256 | "icon": "mdi-cloud", 257 | "iconColor": "black" 258 | ``` 259 | 260 | ## Selfh.st icons 261 | 262 | Use any [selfh.st icon](https://selfh.st/icons/) by prefixing the name with `selfhst-`. 263 | 264 | ```bash 265 | # Icon in config.json 266 | "icon": "selfhst-couchdb" 267 | ``` 268 | -------------------------------------------------------------------------------- /src/components/anchor.ts: -------------------------------------------------------------------------------- 1 | import { is } from "../shared/is"; 2 | import { NEWWINDOW } from "../variables"; 3 | 4 | interface IProps { 5 | uri: string; 6 | title?: string; 7 | className?: string; 8 | children?: any; 9 | newWindow?: boolean; 10 | } 11 | 12 | export const Anchor = function (props: IProps) { 13 | const { uri, children, title, className, newWindow } = props; 14 | 15 | let newWindowLocal = NEWWINDOW; 16 | 17 | if (!is.null(newWindow)) { 18 | newWindowLocal = newWindow as boolean; 19 | } 20 | 21 | if (newWindowLocal) { 22 | return ` 23 | 24 | ${children} 25 | 26 | `; 27 | } 28 | 29 | return ` 30 | 31 | ${children} 32 | 33 | `; 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/header.ts: -------------------------------------------------------------------------------- 1 | import { is } from "../shared/is"; 2 | 3 | interface IProps { 4 | title?: string; 5 | icon?: string; 6 | } 7 | 8 | export const Header = function (props: IProps) { 9 | const { icon, title } = props; 10 | return ` 11 |
12 | ${!is.null(icon) ? 13 | (`${title || `) : 14 | `` 15 | } 16 |

${title}

17 |
18 | `; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/icon.ts: -------------------------------------------------------------------------------- 1 | import { is } from "../shared/is"; 2 | import { IconAspect } from "../shared/types"; 3 | import { Anchor } from "./anchor"; 4 | 5 | const iconColors = [ 6 | "blue", 7 | "rose", 8 | "green", 9 | "red", 10 | "yellow", 11 | "cyan", 12 | "pink", 13 | "orange", 14 | "sky", 15 | "slate", 16 | "emerald", 17 | "zinc", 18 | "neutral", 19 | "amber", 20 | "violet", 21 | ]; 22 | const iconLevel = 300; 23 | 24 | const getIconColor = (index: number) => `bg-${iconColors[iconColors.length % index]}-${iconLevel}`; 25 | 26 | interface IProps { 27 | name: string; 28 | index: number; 29 | icon?: string; 30 | iconColor?: string; 31 | iconBG?: string; 32 | iconBubble?: boolean; 33 | iconBubblePadding?: boolean; 34 | iconAspect?: IconAspect; 35 | uri?: string; 36 | newWindow?: boolean; 37 | categoryBubblePadding?: boolean; 38 | } 39 | 40 | export const Icon = function (props: IProps): string { 41 | const { 42 | name, 43 | uri, 44 | icon, 45 | index, 46 | iconBG, 47 | iconBubble, 48 | iconBubblePadding, 49 | iconColor, 50 | iconAspect, 51 | newWindow, 52 | categoryBubblePadding, 53 | } = props; 54 | 55 | if (is.null(icon)) { 56 | if (!is.null(uri)) { 57 | return Anchor({ uri: uri as string, title: name, newWindow, children: IconBlank({ index }) }); 58 | } 59 | 60 | return IconBlank({ index }); 61 | } 62 | 63 | let bubblePadding = categoryBubblePadding || false; 64 | 65 | if (iconBubblePadding === true) { 66 | bubblePadding = true; 67 | } else if (iconBubblePadding === false) { 68 | bubblePadding = false; 69 | } 70 | 71 | return IconBase({ 72 | icon: icon as string, 73 | iconBG, 74 | iconColor, 75 | iconBubble, 76 | iconBubblePadding: bubblePadding, 77 | iconAspect, 78 | }); 79 | }; 80 | 81 | interface IIconBlankProps { 82 | index: number; 83 | } 84 | 85 | function IconBlank(props: IIconBlankProps) { 86 | const { index } = props; 87 | return ` 88 | 91 | `; 92 | } 93 | 94 | enum IconType { 95 | uri, 96 | material, 97 | dashboard, 98 | selfhst, 99 | } 100 | 101 | interface IIconBaseProps { 102 | icon: string; 103 | iconColor?: string; 104 | iconBG?: string; 105 | iconBubble?: boolean; 106 | iconBubblePadding?: boolean; 107 | iconAspect?: IconAspect; 108 | } 109 | 110 | function IconBase(props: IIconBaseProps) { 111 | let { icon, iconBG, iconBubble, iconBubblePadding, iconColor, iconAspect = "square" } = props; 112 | 113 | let iconType: IconType = IconType.uri; 114 | 115 | if (icon.startsWith("http") || icon.startsWith("/")) { 116 | iconType = IconType.uri; 117 | } else if (icon.startsWith("mdi-")) { 118 | iconType = IconType.material; 119 | } else if (icon.startsWith("selfhst-")) { 120 | iconType = IconType.selfhst; 121 | } else { 122 | iconType = IconType.dashboard; 123 | } 124 | 125 | // Everyone starts the same size 126 | let bubbleClassName = "flex items-center justify-center overflow-hidden bg-contain"; 127 | let iconWrapperWidthHeightClassName = ""; 128 | let iconItselfWidthHeightClassName = ""; 129 | 130 | switch (iconAspect) { 131 | case "width": 132 | iconItselfWidthHeightClassName = "w-16"; 133 | iconWrapperWidthHeightClassName += " w-16"; 134 | 135 | if (iconBubblePadding) { 136 | iconItselfWidthHeightClassName = "w-14"; 137 | } 138 | break; 139 | case "height": 140 | iconItselfWidthHeightClassName = "h-16"; 141 | iconWrapperWidthHeightClassName += " h-16"; 142 | 143 | if (iconBubblePadding) { 144 | iconItselfWidthHeightClassName = "h-14"; 145 | } 146 | break; 147 | case "square": 148 | default: 149 | iconItselfWidthHeightClassName = "w-16 h-16 object-contain"; 150 | iconWrapperWidthHeightClassName += " w-16 h-16"; 151 | 152 | if (iconBubblePadding) { 153 | iconItselfWidthHeightClassName = "w-14 h-14 object-contain"; 154 | } 155 | break; 156 | } 157 | 158 | bubbleClassName += iconWrapperWidthHeightClassName; 159 | 160 | if (is.null(iconBubble) || iconBubble !== false) { 161 | bubbleClassName += " rounded-2xl border border-black/5 shadow-sm"; 162 | } 163 | 164 | const bubbleStyle: string[] = []; 165 | 166 | switch (iconType) { 167 | case IconType.uri: 168 | case IconType.dashboard: 169 | case IconType.selfhst: 170 | // Default to bubble and no background for URI, Dashboard and selfhst icons 171 | if (!is.null(iconBG)) { 172 | if (iconBG?.startsWith("#")) { 173 | bubbleStyle.push(`background-color: ${iconBG}`); 174 | } else { 175 | bubbleClassName += ` bg-${iconBG}`; 176 | } 177 | } 178 | break; 179 | case IconType.material: 180 | // Material icons get a color and a background by default, then an optional bubble 181 | if (is.null(iconBG)) { 182 | bubbleClassName += ` bg-slate-200 dark:bg-gray-900`; 183 | } else { 184 | if (iconBG?.startsWith("#")) { 185 | bubbleStyle.push(`background-color: ${iconBG}`); 186 | } else { 187 | bubbleClassName += ` bg-${iconBG}`; 188 | } 189 | } 190 | 191 | if (is.null(iconColor)) { 192 | iconColor = "black dark:bg-white"; 193 | } 194 | break; 195 | } 196 | 197 | const mdiIconStyle: string[] = []; 198 | let mdiIconColorFull = "bg-" + iconColor; 199 | 200 | if (!is.null(iconColor) && iconColor?.startsWith("#")) { 201 | mdiIconColorFull = ""; 202 | mdiIconStyle.push(`background-color: ${iconColor}`); 203 | } 204 | 205 | switch (iconType) { 206 | case IconType.uri: 207 | return ``; 210 | case IconType.dashboard: 211 | icon = icon.replace(".png", "").replace(".jpg", "").replace(".svg", ""); 212 | return ` 213 | 218 | `; 219 | case IconType.selfhst: 220 | icon = icon.replace("selfhst-", "").replace(".png", "").replace(".svg", ""); 221 | return ` 222 | 227 | `; 228 | case IconType.material: 229 | icon = icon.replace("mdi-", "").replace(".svg", ""); 230 | return ` 231 | 232 | 233 | 242 | 243 | 244 | `; 245 | } 246 | } 247 | 248 | function unwrapStyles(styles: string[]): string { 249 | if (is.null(styles)) { 250 | return ""; 251 | } 252 | 253 | return styles.join(";"); 254 | } 255 | -------------------------------------------------------------------------------- /src/components/service-catalogs.ts: -------------------------------------------------------------------------------- 1 | import { is } from "../shared/is"; 2 | import { IServiceCategory } from "../shared/types"; 3 | import { CATEGORIES } from "../variables"; 4 | import { Services } from "./services"; 5 | 6 | interface IProps { 7 | categories: IServiceCategory[]; 8 | } 9 | 10 | export const ServiceCatalogList = function (props: IProps) { 11 | const { categories } = props; 12 | 13 | return ` 14 | 17 | `; 18 | }; 19 | 20 | interface ICatalogProps { 21 | category: IServiceCategory; 22 | } 23 | 24 | const ServiceCatalog = function (props: ICatalogProps) { 25 | const { category } = props; 26 | 27 | let categoryClassName = "dark:text-slate-200"; 28 | 29 | switch (CATEGORIES as string) { 30 | case "small": 31 | categoryClassName += " text-sm text-slate-800 font-semibold py px-4 uppercase"; 32 | break; 33 | case "normal": 34 | default: 35 | categoryClassName += " text-2xl text-slate-600 font-light py-2 px-4"; 36 | break; 37 | } 38 | 39 | let liClassName = "mt-12 first:mt-0 xl:first:mt-6"; 40 | 41 | if (category.bubble) { 42 | liClassName += " var-category-bubble-bg rounded-2xl px-6 py-6 ring-1 ring-slate-900/5 shadow-xl"; 43 | 44 | if (!is.null(category.bubbleBGLight)) { 45 | liClassName += ` !bg-${category.bubbleBGLight}`; 46 | } 47 | 48 | if (!is.null(category.bubbleBGDark)) { 49 | liClassName += ` dark:!bg-${category.bubbleBGDark}`; 50 | } 51 | } 52 | 53 | return ` 54 |
  • 55 |

    ${category.category}

    56 | ${Services({ services: category.services, categoryBubblePadding: category.iconBubblePadding })} 57 |
  • 58 | `; 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/services.ts: -------------------------------------------------------------------------------- 1 | import { is } from "../shared/is"; 2 | import { IService } from "../shared/types"; 3 | import { Anchor } from "./anchor"; 4 | import { Icon } from "./icon"; 5 | 6 | interface IServicesProps { 7 | services: IService[]; 8 | categoryBubblePadding?: boolean; 9 | } 10 | 11 | export const Services = function (props: IServicesProps) { 12 | const { services, categoryBubblePadding } = props; 13 | 14 | return ` 15 | 18 | `; 19 | }; 20 | 21 | interface IServiceProps { 22 | service: IService; 23 | index: number; 24 | categoryBubblePadding?: boolean; 25 | } 26 | 27 | function Service(props: IServiceProps) { 28 | const { service, index, categoryBubblePadding } = props; 29 | 30 | const { name, uri, description, icon, iconBG, iconBubble, iconBubblePadding, iconColor, iconAspect, newWindow } = 31 | service; 32 | 33 | return ` 34 |
  • 35 | ${Anchor({ 36 | uri, 37 | newWindow, 38 | className: "flex gap-4", 39 | children: `${ 40 | !is.null(icon) 41 | ? `${Icon({ 42 | name, 43 | icon, 44 | uri, 45 | index, 46 | iconColor, 47 | iconBG, 48 | iconBubble, 49 | iconAspect, 50 | newWindow, 51 | categoryBubblePadding, 52 | iconBubblePadding, 53 | })}` 54 | : `` 55 | } 56 |
    57 |

    ${name}

    58 | ${!is.null(description) ? `

    ${description}

    ` : ``} 59 |
    `, 60 | })} 61 | 62 |
  • 63 | `; 64 | } 65 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /src/config/config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "Example category", 4 | "services": [ 5 | { 6 | "name": "Check the readme", 7 | "uri": "https://github.com/notclickable-jordan/starbase-80", 8 | "description": "Use your own icons", 9 | "icon": "github" 10 | }, 11 | { 12 | "name": "Bind mount", 13 | "uri": "https://github.com/notclickable-jordan/docker-symphony", 14 | "description": "/app/src/config/config.json", 15 | "icon": "docker" 16 | }, 17 | { 18 | "name": "Watch Lower Decks", 19 | "uri": "https://www.paramountplus.com/", 20 | "description": "On Paramount+", 21 | "icon": "mdi-image-filter-hdr", 22 | "iconColor": "blue-500" 23 | } 24 | ] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /src/css-cache-break.ts: -------------------------------------------------------------------------------- 1 | import CleanCSS from "clean-css"; 2 | import * as path from "path"; 3 | import { sbMakeFolder, sbReadFile, sbRemoveAllCSS, sbWriteFile } from "./shared/files"; 4 | 5 | const mainCSSFilename = `main.${new Date().getTime()}.css`; 6 | 7 | const cssFilePath = path.join(__dirname, "../public/css"); 8 | const cssFileInPath = path.join(__dirname, "../public", "main.css"); 9 | const cssFileOutPath = path.join(cssFilePath, mainCSSFilename); 10 | const indexFileInOutPath = path.join(__dirname, "../public", "index.html"); 11 | 12 | async function start(): Promise { 13 | await sbRemoveAllCSS(cssFilePath); 14 | await sbMakeFolder(cssFilePath); 15 | 16 | const css = await sbReadFile(cssFileInPath); 17 | await sbWriteFile(cssFileOutPath, new CleanCSS().minify(css).styles); 18 | 19 | const index = await sbReadFile(indexFileInOutPath); 20 | const cssLink = ``; 21 | const cssLinkReplacement = ``; 22 | 23 | await sbWriteFile(indexFileInOutPath, index.replace(cssLink, cssLinkReplacement)); 24 | } 25 | 26 | start(); 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { IndexPage } from "./pages/index"; 3 | import { sbReadFile, sbWriteFile } from "./shared/files"; 4 | import { PAGEICON, PAGETITLE } from "./variables"; 5 | 6 | const indexFileInPath = path.join(__dirname, "../", "index.html"); 7 | const indexFileOutPath = path.join(__dirname, "../", "./public", "index.html"); 8 | 9 | async function start(): Promise { 10 | const index = await sbReadFile(indexFileInPath); 11 | 12 | const newText = IndexPage({ icon: PAGEICON, title: PAGETITLE }); 13 | const rootDiv = '
    '; 14 | const rootDivReplacement = '
    $1
    '; 15 | 16 | const newIndex = index.replace(rootDiv, rootDivReplacement.replace("$1", newText)); 17 | 18 | await sbWriteFile(indexFileOutPath, newIndex); 19 | } 20 | 21 | start(); 22 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import { Header } from "../components/header"; 2 | import { ServiceCatalogList } from "../components/service-catalogs"; 3 | import userServicesOld from "../config.json"; 4 | import userServices from "../config/config.json"; 5 | import { is } from "../shared/is"; 6 | import { IServiceCategory } from "../shared/types"; 7 | import { SHOWHEADER, SHOWHEADERLINE, SHOWHEADERTOP } from "../variables"; 8 | 9 | interface IProps { 10 | title?: string; 11 | icon?: string; 12 | } 13 | 14 | export const IndexPage = function (props: IProps): string { 15 | const { icon, title } = props; 16 | const myServices = (is.null(userServicesOld) ? userServices : userServicesOld) as IServiceCategory[]; 17 | 18 | let headerClassName = "p-4"; 19 | 20 | if (SHOWHEADERTOP) { 21 | headerClassName += " w-full"; 22 | } else { 23 | headerClassName += " w-full xl:w-auto xl:max-w-xs xl:min-h-screen"; 24 | } 25 | 26 | if (SHOWHEADERLINE) { 27 | headerClassName += "border-0 border-solid border-gray-300 dark:border-gray-700"; 28 | 29 | if (SHOWHEADERTOP) { 30 | headerClassName += " border-b"; 31 | } else { 32 | headerClassName += " border-b xl:border-r xl:border-b-0"; 33 | } 34 | } 35 | 36 | let pageWrapperClassName = "min-h-screen flex flex-col max-w-screen-2xl mx-auto"; 37 | 38 | if (!SHOWHEADERTOP) { 39 | pageWrapperClassName += " xl:flex-row"; 40 | } 41 | 42 | let serviceCatalogListWrapperClassName = "p-4 flex-grow"; 43 | 44 | if (!SHOWHEADERTOP) { 45 | serviceCatalogListWrapperClassName += " min-h-screen"; 46 | } 47 | 48 | return ` 49 |
    50 |
    51 | ${ 52 | SHOWHEADER 53 | ? ` 54 |
    55 | ${Header({ icon, title })} 56 |
    57 | ` 58 | : `` 59 | } 60 |
    61 | ${ServiceCatalogList({ categories: myServices })} 62 |
    63 |
    64 |
    65 | `; 66 | }; 67 | -------------------------------------------------------------------------------- /src/shared/files.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs/promises"; 2 | 3 | export async function sbReadFile(fileName: string): Promise { 4 | return new Promise(async (resolve, reject) => { 5 | try { 6 | const index = await fs.readFile(fileName); 7 | 8 | return resolve(index.toString()); 9 | } catch (exception) { 10 | console.error(`Could not read file: ${fileName}`); 11 | reject(exception); 12 | } 13 | }); 14 | } 15 | 16 | export async function sbWriteFile(fileName: string, contents: string): Promise { 17 | return new Promise(async (resolve, reject) => { 18 | try { 19 | await fs.writeFile(fileName, contents); 20 | return resolve(true); 21 | } catch (exception) { 22 | console.error(`Could not write file: ${fileName}`); 23 | reject(exception); 24 | } 25 | }); 26 | } 27 | 28 | export async function sbRename(fileNameOld: string, fileNameNew: string): Promise { 29 | return new Promise(async (resolve, reject) => { 30 | try { 31 | await fs.rename(fileNameOld, fileNameNew); 32 | return resolve(true); 33 | } catch (exception) { 34 | console.error(`Could not rename file: ${fileNameOld} to ${fileNameNew}`); 35 | reject(exception); 36 | } 37 | }); 38 | } 39 | 40 | export async function sbMakeFolder(path: string): Promise { 41 | return new Promise(async (resolve, reject) => { 42 | try { 43 | await fs.mkdir(path, { recursive: true }); 44 | return resolve(true); 45 | } catch (exception) { 46 | console.error(`Could not make folder: ${path}`, exception); 47 | reject(exception); 48 | } 49 | }); 50 | } 51 | 52 | export async function sbRemoveAllCSS(path: string): Promise { 53 | return new Promise(async (resolve, reject) => { 54 | try { 55 | await fs.rm(path, { 56 | recursive: true, 57 | force: true, 58 | }); 59 | return resolve(true); 60 | } catch (exception) { 61 | console.error(`Could not remove all CSS from path: ${path}`); 62 | reject(exception); 63 | } 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/shared/is.ts: -------------------------------------------------------------------------------- 1 | function IsArray(data: any): boolean { 2 | if (data === null || typeof data === "undefined") { 3 | return false; 4 | } 5 | 6 | return data.constructor === Array; 7 | } 8 | 9 | function IsNull(data: any): boolean { 10 | return ( 11 | typeof data === "undefined" || 12 | data === null || 13 | (typeof data === "string" && data.length === 0) || 14 | (IsArray(data) && data.length === 0) 15 | ); 16 | } 17 | 18 | export const is = { 19 | null: IsNull, 20 | }; 21 | -------------------------------------------------------------------------------- /src/shared/types.ts: -------------------------------------------------------------------------------- 1 | export interface IServiceCategory { 2 | category: string; 3 | bubble?: boolean; 4 | bubbleBGLight?: string; 5 | bubbleBGDark?: string; 6 | services: IService[]; 7 | iconBubblePadding?: boolean; 8 | } 9 | 10 | export interface IService { 11 | name: string; 12 | uri: string; 13 | 14 | description?: string; 15 | icon?: string; 16 | iconColor?: string; 17 | iconBG?: string; 18 | iconBubble?: boolean; 19 | iconBubblePadding?: boolean; 20 | iconAspect?: IconAspect; 21 | 22 | newWindow?: boolean; 23 | } 24 | 25 | export type IconAspect = "square" | "width" | "height"; 26 | -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | background-color: theme(colors.slate.50); 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | html { 11 | background-color: theme(colors.gray.950); 12 | } 13 | } 14 | 15 | html.dark { 16 | background-color: theme(colors.gray.950); 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | } 22 | 23 | @media (prefers-color-scheme: dark) { 24 | body { 25 | color: theme(colors.slate.200); 26 | } 27 | } 28 | 29 | html.dark body { 30 | color: theme(colors.slate.200); 31 | } 32 | 33 | h1 { 34 | font-size: theme(fontSize.3xl); 35 | font-weight: theme(fontWeight.semibold); 36 | margin: 0; 37 | padding: 0; 38 | } 39 | 40 | a:hover h3, 41 | a:focus h3 { 42 | @apply no-underline; 43 | } 44 | 45 | .var-category-bubble-bg { 46 | background-color: theme(colors.white); /* category light */ 47 | } 48 | 49 | @media (prefers-color-scheme: dark) { 50 | .var-category-bubble-bg { 51 | background-color: theme(colors.black); /* category dark */ 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/variables.ts: -------------------------------------------------------------------------------- 1 | export const PAGETITLE = "My Website"; 2 | export const PAGEICON = "/logo.png"; 3 | export const SHOWHEADER = true; 4 | export const SHOWHEADERLINE = true; 5 | export const SHOWHEADERTOP = false; 6 | export const CATEGORIES = "normal"; 7 | export const THEME = "auto"; 8 | export const NEWWINDOW = true; 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./public/index.html"], 4 | safelist: [ 5 | /* Built from icon.tsx */ 6 | { 7 | pattern: 8 | /bg-(black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/, 9 | }, 10 | "bg-transparent", 11 | "bg-black", 12 | "bg-white", 13 | ], 14 | theme: { 15 | extend: {}, 16 | }, 17 | plugins: [], 18 | darkMode: "media", 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "module": "CommonJS" 10 | }, 11 | "include": ["src/index.ts", "src/css-cache-break.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 1.6.1 --------------------------------------------------------------------------------