[
  {
    "path": ".env.example",
    "content": "PASSWORD=\"password\"\nSECRET_KEY=\"6ft0ryZAeb3DdFIeEwi4uv5zI69GE2ez\"\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Deploy Documentation site to GitHub Pages\n\non:\n  push:\n    paths:\n      - 'docs/**'\n      - '.github/workflows/docs.yml'\n      - '*.md'\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Install pnpm\n        uses: pnpm/action-setup@v2\n        with:\n          version: 10\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: pnpm\n      - name: Setup Pages\n        uses: actions/configure-pages@v5\n      - name: Install dependencies\n        run: pnpm install\n      - name: Build with VitePress\n        run: |\n          pnpm run docs:build\n          touch docs/.vitepress/dist/.nojekyll\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          name: github-pages\n          path: docs/.vitepress/dist\n\n  deploy:\n    name: Deploy\n    needs: build\n    permissions:\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/push-main.yml",
    "content": "name: Push Main\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\non:\n  push:\n    paths-ignore:\n      - 'docs/**'\n      - '.md'\n    branches:\n      - main\n  pull_request:\n    paths-ignore:\n      - 'docs/**'\n      - '*.md'\n    branches:\n      - main\n\njobs:\n  build-and-push-image:\n    name: Push Docker image to GitHub Packages\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n      contents: read\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to GitHub Docker Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Docker Image\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n\njobs:\n  build-and-push-image:\n    name: Push Docker image to GitHub Packages\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n      contents: read\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to GitHub Docker Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n\n# Output\n.output\n.vercel\n.netlify\n.wrangler\n/.svelte-kit\n/build\n/data\n\n# OS\n.DS_Store\nThumbs.db\n\n# Env\n.env\n.env.*\n!.env.example\n!.env.test\n!.env.ci\n\n# Vite\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n"
  },
  {
    "path": ".npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n\t\"useTabs\": true,\n\t\"singleQuote\": true,\n\t\"trailingComma\": \"none\",\n\t\"printWidth\": 100,\n\t\"plugins\": [\"prettier-plugin-svelte\", \"prettier-plugin-tailwindcss\"],\n\t\"overrides\": [\n\t\t{\n\t\t\t\"files\": \"*.svelte\",\n\t\t\t\"options\": {\n\t\t\t\t\"parser\": \"svelte\"\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n\n## v0.0.6\n\n[compare changes](https://github.com/codewec/dashlit/compare/v0.0.5...v0.0.6)\n\n### 🚀 Enhancements\n\n- Icon custom color ([5e4fc05](https://github.com/codewec/dashlit/commit/5e4fc05))\n- Add select url target ([b886d5d](https://github.com/codewec/dashlit/commit/b886d5d))\n- Show URL options ([a8e5e10](https://github.com/codewec/dashlit/commit/a8e5e10))\n- Check valid ORIGIN ([2e6cf56](https://github.com/codewec/dashlit/commit/2e6cf56))\n\n### 🩹 Fixes\n\n- Changelogen params ([6ae906e](https://github.com/codewec/dashlit/commit/6ae906e))\n\n### ❤️ Contributors\n\n- Wec <codeforwec@gmail.com>\n\n## v0.0.5\n\n[compare changes](https://github.com/codewec/dashlit/compare/0.0.4...v0.0.5)\n\n### 🩹 Fixes\n\n- GH workflow ([2b23083](https://github.com/codewec/dashlit/commit/2b23083))\n\n### ❤️ Contributors\n\n- Wec <codeforwec@gmail.com>\n\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:22 AS builder\nWORKDIR /app\n\nCOPY . .\n\nRUN mv .env.example .env\nRUN corepack enable && corepack prepare pnpm@latest --activate\nRUN pnpm install --frozen-lockfile --force\nRUN npm run build\n\n# second stage\nFROM node:22-alpine\n\nWORKDIR /app\nCOPY --from=builder /app/build ./build\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/package.json .\n\nEXPOSE 3000\nCMD [\"sh\",\"-c\",\"node build\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 codewec\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">DashLit</h1>\n<p align=\"center\">\n    <i>DashLit is a simple, self-hosted Startpage solution. It’s incredibly easy to set up and use, and its built-in editors let you quickly create your own application hub – even with a convenient drag-and-drop interface. You don’t even need to edit any files!</i>\n    <br/><br/>\n    <img width=\"130\" alt=\"DashLit\" src=\"https://raw.githubusercontent.com/codewec/dashlit/main/static/favicon.svg\"/>\n    <br/> <br/>\n    <img src=\"https://img.shields.io/github/v/release/codewec/dashlit?logo=hackthebox&color=609966&logoColor=fff\" alt=\"Current Version\"/>\n    <img src=\"https://img.shields.io/github/last-commit/codewec/dashlit?logo=github&color=609966&logoColor=fff\" alt=\"Last commit\"/>\n    <a href=\"https://github.com/codewec/dashlit/blob/main/LICENSE\"><img src=\"https://img.shields.io/badge/License-MIT-609966?logo=opensourceinitiative&logoColor=fff\" alt=\"License MIT\"/></a>\n    <a href=\"https://dashlit.cwec.dev/\" target=\"_blank\"><img src=\"https://img.shields.io/badge/doc-609966\"/></a>\n    <a href=\"https://demo.dashlit.cwec.dev/\" target=\"_blank\"><img src=\"https://img.shields.io/badge/live-demo-609966\"/></a>\n    <br/><br/>\n    <img src=\"https://raw.githubusercontent.com/codewec/dashlit/main/docs/public/main_page.png\" alt=\"DashLit\" width=\"100%\"/>\n</p>\n\n\n## 🚀 Getting started\n\n<!-- #region docker-configuration -->\n### Docker\n\nThis Docker image is published on the GitHub container registry - `ghcr.io/codewec/dashlit`.\n\n#### Minimal configuration without password\n\n```yaml\nservices:\n  app:\n    container_name: dashlit-app\n    image: ghcr.io/codewec/dashlit:latest\n    restart: unless-stopped\n    environment:\n      ORIGIN: '${ORIGIN:-http://localhost:3000}' # please provide URL if different\n    ports:\n      - '3000:3000'\n    volumes:\n      - ./data:/app/data\n```\n\n#### Full configuration with password\n\n```yaml\nservices:\n  app:\n    container_name: dashlit-app\n    image: ghcr.io/codewec/dashlit:latest\n    environment:\n      ORIGIN: '${ORIGIN:-http://localhost:3000}' # please provide URL if different\n      NODE_ENV: '${NODE_ENV:-production}' # optional for production environment\n      HOST_HEADER: '${HOST_HEADER:-HOST}' # optional for nginx reverse proxy\n      ADDRESS_HEADER: '${ADDRESS_HEADER:-X-Real-IP}' # optional for nginx reverse proxy\n      PROTOCOL_HEADER: '${PROTOCOL_HEADER:-X-Forwarded-Proto}' # optional for nginx reverse proxy\n      PASSWORD: '${PASSWORD:-password}'\n      SECRET_KEY: '${SECRET_KEY:-any-secret-string-for-jwt-auth}' # optional key for JWT authentication\n    restart: unless-stopped\n    ports:\n      - '3000:3000'\n    volumes:\n      - ./data:/app/data\n```\n<!-- #endregion docker-configuration -->\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  app:\n    container_name: dashlit-app\n    image: ghcr.io/codewec/dashlit:latest\n    restart: unless-stopped\n    environment:\n      ORIGIN: '${ORIGIN:-http://localhost:3000}' # please provide URL if different\n    ports:\n      - '3000:3000'\n    volumes:\n      - ./data:/app/data\n"
  },
  {
    "path": "docs/.gitignore",
    "content": ".vitepress/cache\n.vitepress/dist\n"
  },
  {
    "path": "docs/.vitepress/config.ts",
    "content": "import { defineConfig } from 'vitepress';\nimport { getVersion } from './utils';\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n\ttitle: 'DashLit',\n\tdescription: 'DashLit - A simple solution for self-hosting your home page.',\n\thead: [['link', { rel: 'icon', href: '/favicon.png' }]],\n\tthemeConfig: {\n\t\tsearch: {\n\t\t\tprovider: 'local'\n\t\t},\n\n\t\tlogo: {\n\t\t\tsrc: '/logo.png',\n\t\t\tinnerWidth: 50,\n\t\t\theight: 50\n\t\t},\n\t\tnav: [\n\t\t\t{ text: 'Home', link: '/' },\n\t\t\t{ text: 'Getting Started', link: '/guide/getting-started' },\n\t\t\t{ text: 'Demo', link: 'https://demo.dashlit.cwec.dev' },\n\t\t\t{\n\t\t\t\ttext: getVersion(),\n\t\t\t\titems: [\n\t\t\t\t\t{ text: 'Changelog', link: '/changelog' },\n\t\t\t\t\t{ text: 'Contributing', link: '/contributing' }\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tsocialLinks: [{ icon: 'github', link: 'https://github.com/codewec/dashlit' }],\n\t\tsidebar: [\n\t\t\t{\n\t\t\t\ttext: 'Guide',\n\t\t\t\tbase: '/guide',\n\t\t\t\titems: [\n\t\t\t\t\t{ text: 'What is DashLit?', link: '/what-is' },\n\t\t\t\t\t{ text: 'Getting Started', link: '/getting-started' }\n\t\t\t\t]\n\t\t\t},\n\t\t\t{ text: 'Contributing', link: '/contributing' },\n\t\t\t{ text: 'Changelog', link: '/changelog' },\n\t\t\t{ text: 'License', link: '/license' }\n\t\t],\n\n\t\teditLink: {\n\t\t\tpattern: 'https://github.com/codewec/dashlit/edit/main/docs/:path',\n\t\t\ttext: 'Edit this page on GitHub'\n\t\t},\n\n\t\tlastUpdated: {\n\t\t\ttext: 'Last updated',\n\t\t\tformatOptions: {\n\t\t\t\tdateStyle: 'short',\n\t\t\t\ttimeStyle: 'medium'\n\t\t\t}\n\t\t},\n\t\tfooter: {\n\t\t\tmessage: 'Released under the MIT License.',\n\t\t\tcopyright: 'Copyright © 2025 CodeWec'\n\t\t}\n\t}\n});\n"
  },
  {
    "path": "docs/.vitepress/utils/index.ts",
    "content": "import currentPackage from '../../../package.json';\n\nexport function getVersion() {\n\treturn currentPackage.version || '0.0.0';\n}\n"
  },
  {
    "path": "docs/CNAME",
    "content": "dashlit.cwec.dev\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "---\nsearch: false\n---\n\n<!--@include: ../CHANGELOG.md-->\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# Contributing\n\nFirst of all, thank you for deciding to contribute to the project. There are several ways to help the project develop.\n\n## Bug fix\n\nPlease note that occasional bugs or issues may arise when using this application – this is standard practice.  Given your knowledge of JavaScript, you may be able to resolve the problem yourself. Alternatively, please [report the issue](https://github.com/codewec/dashlit/issues/new?assignees=codewec&labels=bug&projects=&template=bug.yml&title=%5BBUG%5D+%3Ctitle%3E) and I’ll be happy to assist you.\n\n## Star on Github\nThe simplest thing you can do is leave us a star on [Github](https://github.com/codewec/dashlit) – it only takes a few seconds, and I really appreciate it!\n"
  },
  {
    "path": "docs/guide/getting-started.md",
    "content": "# 🚀 Getting started\n\n<!--@include: ../../README.md#docker-configuration-->\n"
  },
  {
    "path": "docs/guide/what-is.md",
    "content": "# What is DashLit?\n\n`DashLit` is a flexible tool for creating homepages, especially useful for those managing their own server and services. It helps you collect and organize all your links in one place.\n\n`DashLit` simplifies creating and managing your own online services. It boasts a user-friendly drag-and-drop interface, eliminating the need for complex configuration files like YAML. All service management is handled directly through the intuitive web interface.\n\nPlus, `DashLit` offers secure authentication, making it ideal for deploying your services publicly online with confidence.\n\n![DashLit](/main_page.png)\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nlayout: home\n\nhero:\n  name: \"DashLit\"\n  text: \"Personal home page\"\n  tagline: A simple solution for self-hosting your home page.\n  image:\n    src: /logo.png\n    alt: DashLit\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /guide/getting-started\n    - theme: alt\n      text: Live Demo\n      link: https://demo.dashlit.cwec.dev\n    - theme: alt\n      text: View on GitHub\n      link: https://github.com/codewec/dashlit\n\nfeatures:\n  - title: Privacy\n    icon: 🔐\n    details: Offers secure authentication.\n  - title: Themes\n    icon: 🌗\n    details: Enjoy a light or dark theme – your choice!\n  - title: Grouping\n    icon: 🗂\n    details: Create custom service groups.\n  - title: Easy setup\n    icon: 👌\n    details: Does not use manual configuration files.\n  - title: Drag and drop\n    icon: ✨\n    details: Quickly organize links with a simple drag-and-drop interface.\n  - title: Docker\n    icon: 🐳\n    details: Optimized docker images for popular platforms.\n  - title: Free\n    icon: 🚀\n    details: Dashlit is completely free and open source.\n  - title: PWA\n    icon: 📲\n    details: Installable application.\n---\n\n## Screenshot\n\n![DashLit](/main_page.png \"DashLit\")\n"
  },
  {
    "path": "docs/license.md",
    "content": "# License\n\n<!--@include: ../LICENSE-->\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"dashlit\",\n\t\"private\": true,\n\t\"version\": \"0.0.6\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite dev\",\n\t\t\"build\": \"vite build\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"prepare\": \"svelte-kit sync || echo ''\",\n\t\t\"check\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\",\n\t\t\"check:watch\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch\",\n\t\t\"docs:dev\": \"vitepress dev docs\",\n\t\t\"docs:build\": \"vitepress build docs\",\n\t\t\"docs:preview\": \"vitepress preview docs\",\n\t\t\"release\": \"vite build && changelogen --hideAuthorEmail --release --push\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@iconify/svelte\": \"^5.0.0\",\n\t\t\"@sveltejs/adapter-auto\": \"^6.0.0\",\n\t\t\"@sveltejs/adapter-node\": \"^5.2.12\",\n\t\t\"@sveltejs/kit\": \"^2.16.0\",\n\t\t\"@sveltejs/vite-plugin-svelte\": \"^5.0.0\",\n\t\t\"@tailwindcss/forms\": \"^0.5.9\",\n\t\t\"@tailwindcss/vite\": \"^4.0.0\",\n\t\t\"@types/node\": \"^24.0.1\",\n\t\t\"changelogen\": \"^0.6.1\",\n\t\t\"flowbite\": \"^3.1.2\",\n\t\t\"flowbite-svelte\": \"^1.6.4\",\n\t\t\"flowbite-svelte-icons\": \"^2.2.0\",\n\t\t\"postcss\": \"^8.5.4\",\n\t\t\"prettier\": \"^3.5.3\",\n\t\t\"prettier-plugin-svelte\": \"^3.4.0\",\n\t\t\"prettier-plugin-tailwindcss\": \"^0.6.12\",\n\t\t\"svelte\": \"^5.0.0\",\n\t\t\"svelte-check\": \"^4.0.0\",\n\t\t\"tailwindcss\": \"^4.0.0\",\n\t\t\"typescript\": \"^5.0.0\",\n\t\t\"vite\": \"^6.2.6\",\n\t\t\"vitepress\": \"^1.6.3\"\n\t},\n\t\"pnpm\": {\n\t\t\"onlyBuiltDependencies\": [\n\t\t\t\"@tailwindcss/oxide\",\n\t\t\t\"esbuild\"\n\t\t]\n\t},\n\t\"dependencies\": {\n\t\t\"@thisux/sveltednd\": \"^0.0.20\",\n\t\t\"dotenv\": \"^16.6.0\",\n\t\t\"jose\": \"^6.0.11\",\n\t\t\"svelte-5-french-toast\": \"^2.0.4\"\n\t}\n}\n"
  },
  {
    "path": "src/app.css",
    "content": "@import 'tailwindcss';\n@plugin '@tailwindcss/forms';\n@plugin 'flowbite/plugin';\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n@theme {\n\t--color-primary-50: #fff5f2;\n\t--color-primary-100: #fff1ee;\n\t--color-primary-200: #ffe4de;\n\t--color-primary-300: #ffd5cc;\n\t--color-primary-400: #ffbcad;\n\t--color-primary-500: #fe795d;\n\t--color-primary-600: #ef562f;\n\t--color-primary-700: #eb4f27;\n\t--color-primary-800: #cc4522;\n\t--color-primary-900: #a5371b;\n\n\t--color-secondary-50: #f0f9ff;\n\t--color-secondary-100: #e0f2fe;\n\t--color-secondary-200: #bae6fd;\n\t--color-secondary-300: #7dd3fc;\n\t--color-secondary-400: #38bdf8;\n\t--color-secondary-500: #0ea5e9;\n\t--color-secondary-600: #0284c7;\n\t--color-secondary-700: #0369a1;\n\t--color-secondary-800: #075985;\n\t--color-secondary-900: #0c4a6e;\n}\n\n@source \"../node_modules/flowbite-svelte/dist\";\n@source \"../node_modules/flowbite-svelte-icons/dist\";\n\n@layer base {\n\t/* disable chrome cancel button */\n\tinput[type='search']::-webkit-search-cancel-button {\n\t\tdisplay: none;\n\t}\n}\n\n.toaster {\n\t.wrapper {\n\t\t.base {\n\t\t\t@apply bg-gray-100 dark:bg-slate-900 dark:text-gray-400;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/app.d.ts",
    "content": "// See https://svelte.dev/docs/kit/types#app.d.ts\n// for information about these interfaces\ndeclare global {\n\tnamespace App {\n\t\tinterface Locals {\n\t\t\tuserAuthenticated: boolean;\n\t\t}\n\t\t// interface Error {}\n\t\t// interface Locals {}\n\t\t// interface PageData {}\n\t\t// interface PageState {}\n\t\t// interface Platform {}\n\t}\n}\n\nexport {};\n"
  },
  {
    "path": "src/app.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" type=\"image/png\" href=\"%sveltekit.assets%/favicon-96x96.png\" sizes=\"96x96\" />\n\t\t<link rel=\"icon\" type=\"image/svg+xml\" href=\"%sveltekit.assets%/favicon.svg\" />\n\t\t<link rel=\"shortcut icon\" href=\"%sveltekit.assets%/favicon.ico\" />\n\t\t<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"%sveltekit.assets%/apple-touch-icon.png\" />\n\t\t<meta name=\"apple-mobile-web-app-title\" content=\"DashLit\" />\n\t\t<link rel=\"manifest\" href=\"%sveltekit.assets%/site.webmanifest\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t%sveltekit.head%\n\t\t<link rel=\"stylesheet\" href=\"%sveltekit.assets%/custom.css\" />\n\t</head>\n\t<body\n\t\tdata-sveltekit-preload-data=\"hover\"\n\t\tclass=\"bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200\"\n\t>\n\t\t<div style=\"display: contents\">%sveltekit.body%</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "src/hooks.server.ts",
    "content": "import type { Handle } from '@sveltejs/kit';\nimport { env } from '$env/dynamic/private';\nimport * as jose from 'jose';\nimport { hashString } from '$lib/helpers';\nimport { cookie_token_key } from '$lib';\nimport { getSecretKey } from '$lib/server/helper';\n\nexport const handle: Handle = async ({ event, resolve }) => {\n\tif (!env.PASSWORD || env.PASSWORD?.length === 0) {\n\t\tevent.locals.userAuthenticated = true;\n\t\treturn resolve(event);\n\t}\n\n\tconst token = event.cookies.get(cookie_token_key);\n\tif (!token) {\n\t\treturn resolve(event);\n\t}\n\n\tconst secret = await getSecretKey(env.PASSWORD);\n\tawait jose\n\t\t.jwtVerify(token, new TextEncoder().encode(secret))\n\t\t.then(() => {\n\t\t\tevent.locals.userAuthenticated = true;\n\t\t})\n\t\t.catch(() => {\n\t\t\tevent.cookies.delete(cookie_token_key, { path: '/' });\n\t\t});\n\n\treturn resolve(event);\n};\n"
  },
  {
    "path": "src/lib/components/actionButtons.svelte",
    "content": "<script lang=\"ts\">\n\timport { ActionType, type DeletionEntity } from '$lib/types';\n\timport { Button } from 'flowbite-svelte';\n\timport { EditOutline, CloseCircleOutline } from 'flowbite-svelte-icons';\n\n\tconst {\n\t\tid,\n\t\thandleHover,\n\t\thandleClick\n\t}: {\n\t\tid: string;\n\t\thandleHover: (id: string | undefined) => void;\n\t\thandleClick: (type: ActionType) => void;\n\t} = $props();\n</script>\n\n<div class=\"action-buttons inline-flex gap-1\">\n\t<Button\n\t\tpill={true}\n\t\tcolor=\"light\"\n\t\tonmouseenter={() => {\n\t\t\thandleHover(id);\n\t\t}}\n\t\tonmouseleave={() => {\n\t\t\thandleHover(undefined);\n\t\t}}\n\t\tonclick={() => {\n\t\t\thandleClick(ActionType.EDIT);\n\t\t}}\n\t\tclass=\"edit-button h-4 w-4 cursor-pointer p-3!\"\n\t\t><EditOutline class=\"h-4 w-4\" color=\"gray\" /></Button\n\t>\n\t<Button\n\t\tpill={true}\n\t\tcolor=\"light\"\n\t\tonmouseenter={() => {\n\t\t\thandleHover(id);\n\t\t}}\n\t\tonmouseleave={() => {\n\t\t\thandleHover(undefined);\n\t\t}}\n\t\tonclick={() => {\n\t\t\thandleClick(ActionType.DELETE);\n\t\t}}\n\t\tclass=\"delete-button h-4 w-4 cursor-pointer p-3!\"\n\t\t><CloseCircleOutline class=\"h-4 w-4\" color=\"gray\" /></Button\n\t>\n</div>\n"
  },
  {
    "path": "src/lib/components/dashboard.svelte",
    "content": "<script lang=\"ts\">\n\timport { ActionType, ShowUrlType, type Group, type Item } from '$lib/types';\n\timport Icon from '@iconify/svelte';\n\timport { droppable, draggable, type DragDropState } from '@thisux/sveltednd';\n\timport { flip } from 'svelte/animate';\n\timport { fade } from 'svelte/transition';\n\timport ActionButtons from './actionButtons.svelte';\n\timport { getIds, hasField, isUrlString } from '$lib/helpers';\n\timport EmptyItem from './emptyItem.svelte';\n\timport EmptyGroup from './emptyGroup.svelte';\n\timport { newGroup, newItem } from '$lib/factory';\n\timport { on } from 'svelte/events';\n\n\tconst {\n\t\teditMode,\n\t\tgroups,\n\t\thandleClickItem,\n\t\thandleClickItemAction,\n\t\thandleClickGroupAction\n\t}: {\n\t\teditMode: boolean;\n\t\tgroups: Group[];\n\t\thandleClickItem: (item: Item) => void;\n\t\thandleClickItemAction: (type: ActionType, groupId: string, item: Item) => void;\n\t\thandleClickGroupAction: (type: ActionType, group: Group) => void;\n\t} = $props();\n\n\tlet hoveredOnActionsEnitytId = $state<string | undefined>(undefined); // if hover on actions buttons on edit mode\n\tlet hoveredItemId = $state<string | undefined>(undefined); // if hover on item (not group) on not edit mode\n\tlet disableGroupsDrag = $state(true);\n\tlet disableItemDrag = $state(true);\n\n\t$effect(() => {\n\t\tdisableGroupsDrag = !editMode;\n\t\tdisableItemDrag = !editMode;\n\t});\n\n\tconst getHoverDescription = (groupId: string, item: Item) => {\n\t\tif (!hoveredItemId) {\n\t\t\treturn item.description;\n\t\t}\n\t\tconst ids = getIds(hoveredItemId);\n\t\tif (!ids) {\n\t\t\treturn item.description;\n\t\t}\n\n\t\tif (ids.groupId === groupId && ids.itemId === item.id) {\n\t\t\tif (item.showUrl === ShowUrlType.HOVER) {\n\t\t\t\treturn item.url;\n\t\t\t}\n\t\t}\n\t\treturn item.description;\n\t};\n\n\tconst getDescription = (groupId: string, item: Item) => {\n\t\tswitch (item.showUrl) {\n\t\t\tcase ShowUrlType.NEVER:\n\t\t\t\treturn item.description;\n\t\t\tcase ShowUrlType.ALWAYS:\n\t\t\t\treturn item.description ? item.description : item.url;\n\t\t\tcase ShowUrlType.HOVER:\n\t\t\t\treturn getHoverDescription(groupId, item);\n\t\t\tcase ShowUrlType.DESC_EMPTY:\n\t\t\t\treturn item.description ? item.description : item.url;\n\t\t\tdefault:\n\t\t\t\treturn item.description ? item.description : item.url;\n\t\t}\n\t};\n\n\tconst getUrl = (item: Item) => {\n\t\tswitch (item.showUrl) {\n\t\t\tcase ShowUrlType.NEVER:\n\t\t\t\treturn undefined;\n\t\t\tcase ShowUrlType.ALWAYS:\n\t\t\t\treturn item.description ? item.url : undefined;\n\t\t\tcase ShowUrlType.HOVER:\n\t\t\t\treturn undefined;\n\t\t\tcase ShowUrlType.DESC_EMPTY:\n\t\t\t\treturn undefined;\n\t\t\tdefault:\n\t\t\t\treturn undefined;\n\t\t}\n\t};\n\n\tconst isDisabledGroupDrag = (id: string) => {\n\t\tif (hoveredOnActionsEnitytId) {\n\t\t\tconst ids = getIds(hoveredOnActionsEnitytId);\n\t\t\tif (ids && ids.groupId == id) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn disableGroupsDrag;\n\t};\n\n\tconst isDisabledItemDrag = (id: string) => {\n\t\tif (id == hoveredOnActionsEnitytId) {\n\t\t\treturn true;\n\t\t}\n\t\treturn disableItemDrag;\n\t};\n\n\tconst isDisabledGroupDrop = (group: Group) => {\n\t\tif (group.items.length === 0) {\n\t\t\treturn false;\n\t\t}\n\t\treturn disableGroupsDrag;\n\t};\n\n\tconst onDropInGroup = (state: DragDropState<Item | Group>) => {\n\t\tconst { draggedItem, sourceContainer, targetContainer } = state;\n\t\tif (hasField(draggedItem, 'url')) {\n\t\t\tstate.targetContainer = `${targetContainer}-0`;\n\t\t\tonDropInItem(state as DragDropState<Item>);\n\t\t} else {\n\t\t\tif (!targetContainer) {\n\t\t\t\tconsole.log('Target container not found');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst sourceIndex = groups.findIndex((t) => t.id === sourceContainer);\n\t\t\tconst targetIndex = groups.findIndex((t) => t.id === targetContainer);\n\n\t\t\tif (sourceIndex === undefined || targetIndex === undefined) {\n\t\t\t\tconsole.log('Source or target index not found');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tgroups.splice(sourceIndex, 1);\n\t\t\tgroups.splice(targetIndex, 0, draggedItem);\n\t\t}\n\t};\n\n\tfunction onDropInItem(state: DragDropState<Item>) {\n\t\tconst { draggedItem, sourceContainer, targetContainer } = state;\n\t\tif (!targetContainer) {\n\t\t\tconsole.log('Target container not found');\n\t\t\treturn;\n\t\t}\n\n\t\tconst sourceIds = getIds(sourceContainer);\n\t\tif (!sourceIds) {\n\t\t\tconsole.log('Source IDs not found');\n\t\t\treturn;\n\t\t}\n\n\t\tconst targetIds = getIds(targetContainer);\n\t\tif (!targetIds) {\n\t\t\tconsole.log('Target IDs not found');\n\t\t\treturn;\n\t\t}\n\n\t\tconst sourceGroup = groups.find((g) => g.id === sourceIds.groupId);\n\t\tconst sourceIndex = sourceGroup?.items.findIndex((t) => t.id === sourceIds.itemId);\n\n\t\tconst targetGroup = groups.find((g) => g.id === targetIds.groupId);\n\t\tconst targetIndex = targetGroup?.items.findIndex((t) => t.id === targetIds.itemId);\n\n\t\tif (sourceIndex === undefined || targetIndex === undefined) {\n\t\t\tconsole.log('Source or target index not found');\n\t\t\treturn;\n\t\t}\n\n\t\tsourceGroup?.items.splice(sourceIndex, 1);\n\t\ttargetGroup?.items.splice(targetIndex, 0, draggedItem);\n\t}\n</script>\n\n<div class=\"group-container grid grid-cols-1 gap-4\">\n\t{#if editMode}\n\t\t<EmptyGroup handleClick={() => handleClickGroupAction(ActionType.CREATE, newGroup())} />\n\t{/if}\n\t{#each groups as group (`g_${group.id}`)}\n\t\t<div\n\t\t\tclass:edit-mode={editMode}\n\t\t\tclass=\"group rounded-md bg-gray-50 p-4 shadow-sm ring-1 ring-gray-200 dark:bg-slate-900 dark:ring-slate-800\"\n\t\t\tuse:draggable={{\n\t\t\t\tcontainer: group.id,\n\t\t\t\tdragData: group,\n\t\t\t\tdisabled: isDisabledGroupDrag(group.id),\n\t\t\t\tcallbacks: {\n\t\t\t\t\tonDragStart: () => (disableItemDrag = true),\n\t\t\t\t\tonDragEnd: () => (disableItemDrag = false)\n\t\t\t\t}\n\t\t\t}}\n\t\t\tuse:droppable={{\n\t\t\t\tdragData: group,\n\t\t\t\tcontainer: `${group.id}`,\n\t\t\t\tdisabled: isDisabledGroupDrop(group),\n\t\t\t\tcallbacks: {\n\t\t\t\t\tonDrop: onDropInGroup\n\t\t\t\t}\n\t\t\t}}\n\t\t>\n\t\t\t<div class=\"mb-4 flex items-center justify-between\">\n\t\t\t\t<div class=\"inline-flex gap-2\">\n\t\t\t\t\t<h2 class=\"title font-semibold text-gray-900 capitalize dark:text-gray-200\">\n\t\t\t\t\t\t{group.title}\n\t\t\t\t\t</h2>\n\t\t\t\t\t{#if group.description}\n\t\t\t\t\t\t<p class=\"description text-xs text-gray-500\">{group.description}</p>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t{#if editMode}\n\t\t\t\t\t<ActionButtons\n\t\t\t\t\t\tid={`${group.id}-0`}\n\t\t\t\t\t\thandleHover={(id) => {\n\t\t\t\t\t\t\thoveredOnActionsEnitytId = id;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\thandleClick={(action) => handleClickGroupAction(action, group)}\n\t\t\t\t\t/>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t<div\n\t\t\t\tclass=\"item-container grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"\n\t\t\t>\n\t\t\t\t{#each group.items as item (`i_${item.id}`)}\n\t\t\t\t\t<div\n\t\t\t\t\t\ttabindex=\"0\"\n\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\tonkeyup={(e) => {\n\t\t\t\t\t\t\tif (e.key === 'Enter') {\n\t\t\t\t\t\t\t\thandleClickItem(item);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonmouseenter={() => {\n\t\t\t\t\t\t\tif (editMode) {\n\t\t\t\t\t\t\t\thoveredItemId = undefined;\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\thoveredItemId = `${group.id}-${item.id}`;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonmouseleave={() => {\n\t\t\t\t\t\t\thoveredItemId = undefined;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\thandleClickItem(item);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tuse:draggable={{\n\t\t\t\t\t\t\tcontainer: `${group.id}-${item.id}`,\n\t\t\t\t\t\t\tdragData: item,\n\t\t\t\t\t\t\tdisabled: isDisabledItemDrag(`${group.id}-${item.id}`),\n\t\t\t\t\t\t\tcallbacks: {\n\t\t\t\t\t\t\t\tonDragStart: () => (disableGroupsDrag = true),\n\t\t\t\t\t\t\t\tonDragEnd: () => (disableGroupsDrag = false)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tuse:droppable={{\n\t\t\t\t\t\t\tdragData: item,\n\t\t\t\t\t\t\tdisabled: disableItemDrag,\n\t\t\t\t\t\t\tcontainer: `${group.id}-${item.id}`,\n\t\t\t\t\t\t\tcallbacks: {\n\t\t\t\t\t\t\t\tonDrop: onDropInItem\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tanimate:flip={{ duration: 200 }}\n\t\t\t\t\t\tin:fade={{ duration: 150 }}\n\t\t\t\t\t\tout:fade={{ duration: 150 }}\n\t\t\t\t\t\tclass=\"item svelte-dnd-touch-feedback\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div class=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t{#if item.icon}\n\t\t\t\t\t\t\t\t<div class=\"h-14 w-19\">\n\t\t\t\t\t\t\t\t\t{#if isUrlString(item.icon)}\n\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\tsrc={item.icon}\n\t\t\t\t\t\t\t\t\t\t\talt={item.title}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"h-full w-full rounded-full object-cover\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t<Icon\n\t\t\t\t\t\t\t\t\t\t\tcolor={item.iconColor ?? 'gray'}\n\t\t\t\t\t\t\t\t\t\t\ticon={item.icon}\n\t\t\t\t\t\t\t\t\t\t\twidth={56}\n\t\t\t\t\t\t\t\t\t\t\theight={56}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t<div class=\"w-full truncate\">\n\t\t\t\t\t\t\t\t<h3 class=\"title font-medium text-gray-900 dark:text-gray-100\">\n\t\t\t\t\t\t\t\t\t{item.title}\n\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t{#if getDescription(group.id, item)}\n\t\t\t\t\t\t\t\t\t<p class=\"description text-sm text-gray-500\">\n\t\t\t\t\t\t\t\t\t\t{getDescription(group.id, item)}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t{#if getUrl(item)}\n\t\t\t\t\t\t\t\t\t<p class=\"url text-[10px] text-gray-400 dark:text-gray-500\">\n\t\t\t\t\t\t\t\t\t\t{getUrl(item)}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"absolute top-1 right-1\">\n\t\t\t\t\t\t\t{#if editMode}\n\t\t\t\t\t\t\t\t<ActionButtons\n\t\t\t\t\t\t\t\t\tid={`${group.id}-${item.id}`}\n\t\t\t\t\t\t\t\t\thandleHover={(id) => {\n\t\t\t\t\t\t\t\t\t\thoveredOnActionsEnitytId = id;\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\thandleClick={(action) => handleClickItemAction(action, group.id, item)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t\t{#if editMode}\n\t\t\t\t\t<EmptyItem\n\t\t\t\t\t\tid={`${group.id}-0`}\n\t\t\t\t\t\thandleHover={(id) => {\n\t\t\t\t\t\t\thoveredOnActionsEnitytId = id;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\thandleClick={() => handleClickItemAction(ActionType.CREATE, group.id, newItem())}\n\t\t\t\t\t/>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</div>\n\t{/each}\n</div>\n\n<style lang=\"postcss\">\n\t@reference \"$lib/../app.css\";\n\t:global(.dragging) {\n\t\t@apply !opacity-50 !shadow-lg !ring-2 !ring-blue-400;\n\t}\n\n\t:global(.drag-over) {\n\t\t@apply !bg-blue-50 !ring-2 !ring-blue-400 dark:!bg-slate-800 dark:ring-blue-600;\n\t}\n\n\t.item {\n\t\t@apply relative rounded-lg bg-white p-3 shadow-sm ring-1 ring-gray-200 transition-all duration-200 dark:bg-black dark:ring-gray-800;\n\t}\n\n\t.item:not(.edit-mode) {\n\t\t@apply cursor-pointer hover:shadow-md hover:ring-2 hover:ring-blue-300 dark:hover:ring-blue-900;\n\t}\n\n\t.edit-mode {\n\t\t@apply cursor-move hover:shadow-md hover:ring-2 hover:ring-blue-200 dark:hover:ring-blue-900;\n\n\t\t.item {\n\t\t\t@apply cursor-move hover:shadow-md hover:ring-2 hover:ring-blue-200 dark:hover:ring-blue-900;\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "src/lib/components/emptyGroup.svelte",
    "content": "<script lang=\"ts\">\n\timport Button from 'flowbite-svelte/Button.svelte';\n\n\tconst { handleClick }: { handleClick: () => void } = $props();\n</script>\n\n<Button\n\tonclick={handleClick}\n\tcolor=\"light\"\n\tclass=\"group-empty w-full cursor-pointer rounded-lg border border-dashed border-gray-300 bg-transparent p-3 text-center text-sm text-gray-500 ring-0 focus-within:ring-0 hover:border-gray-500\"\n\t>Add group</Button\n>\n"
  },
  {
    "path": "src/lib/components/emptyItem.svelte",
    "content": "<script lang=\"ts\">\n\timport Button from 'flowbite-svelte/Button.svelte';\n\tconst {\n\t\tid,\n\t\thandleHover,\n\t\thandleClick\n\t}: {\n\t\tid: string;\n\t\thandleHover: (id: string | undefined) => void;\n\t\thandleClick: () => void;\n\t} = $props();\n</script>\n\n<Button\n\tonmouseenter={() => {\n\t\thandleHover(id);\n\t}}\n\tonmouseleave={() => {\n\t\thandleHover(undefined);\n\t}}\n\tonclick={handleClick}\n\tcolor=\"light\"\n\tclass=\"item-empty cursor-pointer rounded-lg border border-dashed border-gray-300 bg-transparent p-3 text-center text-sm text-gray-500 ring-0 focus-within:ring-0 hover:border-gray-500\"\n\t>Add item</Button\n>\n"
  },
  {
    "path": "src/lib/components/header.svelte",
    "content": "<script lang=\"ts\">\n\timport { ButtonGroup, GradientButton, Button, DarkMode } from 'flowbite-svelte';\n\n\tlet {\n\t\teditMode,\n\t\tcanLogout,\n\t\thandleSave,\n\t\thandleEdit\n\t}: { editMode: boolean; canLogout: boolean; handleSave: () => void; handleEdit: () => void } =\n\t\t$props();\n</script>\n\n<div class=\"header mb-8 flex justify-between gap-2\">\n\t<h1 class=\"title text-2xl font-bold\">DashLit</h1>\n\n\t<div class=\"inline-flex items-center\">\n\t\t<DarkMode size=\"sm\" class=\"theme-switcher cursor-pointer p-2\" />\n\t\t<ButtonGroup size=\"sm\" class=\"*:ring-0!\">\n\t\t\t{#if editMode}\n\t\t\t\t<GradientButton\n\t\t\t\t\trole=\"button\"\n\t\t\t\t\tcolor=\"purpleToBlue\"\n\t\t\t\t\tclass=\"cursor-pointer hover:text-white\"\n\t\t\t\t\tonclick={() => handleSave()}>Save</GradientButton\n\t\t\t\t>\n\t\t\t{:else}\n\t\t\t\t<Button role=\"button\" class=\"cursor-pointer\" onclick={() => handleEdit()}>Edit</Button>\n\t\t\t{/if}\n\t\t\t{#if canLogout}\n\t\t\t\t<Button data-sveltekit-preload-data=\"off\" href=\"/logout\">Logout</Button>\n\t\t\t{/if}\n\t\t</ButtonGroup>\n\t</div>\n</div>\n"
  },
  {
    "path": "src/lib/components/modalDelete.svelte",
    "content": "<script lang=\"ts\">\n\timport type { DeletionEntity } from '$lib/types';\n\timport { Button } from 'flowbite-svelte';\n\timport { ExclamationCircleOutline } from 'flowbite-svelte-icons';\n\timport Modal from 'flowbite-svelte/Modal.svelte';\n\timport { slide } from 'svelte/transition';\n\n\tconst {\n\t\tentity,\n\t\thandleClose,\n\t\thandleConfirm\n\t}: {\n\t\tentity: DeletionEntity | undefined;\n\t\thandleClose: () => void;\n\t\thandleConfirm: () => void;\n\t} = $props();\n</script>\n\n<Modal open={entity !== undefined} onclose={() => handleClose()} transition={slide} size=\"xs\">\n\t{#if entity}\n\t\t<div class=\"text-center\">\n\t\t\t<ExclamationCircleOutline class=\"mx-auto mb-4 h-12 w-12 text-gray-400 dark:text-gray-200\" />\n\t\t\t<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">\n\t\t\t\tAre you sure you want to delete \"{entity.element.title}\"?\n\t\t\t</h3>\n\t\t\t<Button color=\"red\" onclick={() => handleConfirm()} class=\"me-2\">Yes, I'm sure</Button>\n\t\t\t<Button color=\"alternative\" onclick={() => handleClose()}>No, cancel</Button>\n\t\t</div>\n\t{/if}\n</Modal>\n"
  },
  {
    "path": "src/lib/components/modalFormGroup.svelte",
    "content": "<script lang=\"ts\">\n\timport type { Group } from '$lib/types';\n\timport { Button, Input, Label, Modal } from 'flowbite-svelte';\n\timport { slide } from 'svelte/transition';\n\n\tconst {\n\t\tisOpen,\n\t\tgroup,\n\t\thandleClose\n\t}: { isOpen: boolean; group: Group; handleClose: (group: Group | undefined) => void } = $props();\n\n\tconst form = $derived(group);\n</script>\n\n<Modal open={isOpen} onclose={() => handleClose(undefined)} transition={slide} size=\"xs\">\n\t<form\n\t\tclass=\"flex flex-col space-y-6 pt-4\"\n\t\tonsubmit={(e: SubmitEvent) => {\n\t\t\te.preventDefault();\n\t\t\thandleClose(form);\n\t\t}}\n\t>\n\t\t<Label class=\"space-y-2\">\n\t\t\t<span>Title</span>\n\t\t\t<Input bind:value={form.title} type=\"text\" name=\"title\" placeholder=\"Title\" required />\n\t\t</Label>\n\t\t<Label class=\"space-y-2\">\n\t\t\t<span>Description</span>\n\t\t\t<Input\n\t\t\t\tbind:value={form.description}\n\t\t\t\ttype=\"text\"\n\t\t\t\tname=\"description\"\n\t\t\t\tplaceholder=\"Description\"\n\t\t\t/>\n\t\t</Label>\n\t\t<Button type=\"submit\" class=\"w-full\">Save</Button>\n\t</form>\n</Modal>\n"
  },
  {
    "path": "src/lib/components/modalFormItem.svelte",
    "content": "<script lang=\"ts\">\n\timport { ShowUrlType, type Item } from '$lib/types';\n\timport {\n\t\tButton,\n\t\tButtonGroup,\n\t\tCheckbox,\n\t\tHelper,\n\t\tInput,\n\t\tInputAddon,\n\t\tLabel,\n\t\tModal,\n\t\tSelect\n\t} from 'flowbite-svelte';\n\timport { slide } from 'svelte/transition';\n\n\tconst {\n\t\tisOpen,\n\t\titem,\n\t\thandleClose\n\t}: { isOpen: boolean; item: Item; handleClose: (item: Item | undefined) => void } = $props();\n\n\tconst urlTargetOptions = [\n\t\t{ value: '_blank', name: 'New tab' },\n\t\t{ value: '_self', name: 'Current tab' }\n\t];\n\n\tconst showUrlOptions = [\n\t\t{ value: ShowUrlType.NEVER, name: 'Never' },\n\t\t{ value: ShowUrlType.ALWAYS, name: 'Always' },\n\t\t{ value: ShowUrlType.DESC_EMPTY, name: 'If description is empty' },\n\t\t{ value: ShowUrlType.HOVER, name: 'On hover' }\n\t];\n\n\tconst form = $derived(item);\n\n\t$effect(() => {\n\t\tif (!form.target) {\n\t\t\tform.target = '_blank';\n\t\t}\n\t\tif (!form.showUrl) {\n\t\t\tform.showUrl = ShowUrlType.DESC_EMPTY;\n\t\t}\n\t});\n</script>\n\n<Modal open={isOpen} onclose={() => handleClose(undefined)} transition={slide} size=\"xs\">\n\t<form\n\t\tclass=\"flex flex-col space-y-6 pt-4\"\n\t\tonsubmit={(e: SubmitEvent) => {\n\t\t\te.preventDefault();\n\t\t\thandleClose(form);\n\t\t}}\n\t>\n\t\t<Label class=\"space-y-2\">\n\t\t\t<span>Title</span>\n\t\t\t<Input bind:value={form.title} type=\"text\" name=\"title\" placeholder=\"Title\" required />\n\t\t</Label>\n\t\t<Label class=\"space-y-2\">\n\t\t\t<span>Description</span>\n\t\t\t<Input\n\t\t\t\tbind:value={form.description}\n\t\t\t\ttype=\"text\"\n\t\t\t\tname=\"description\"\n\t\t\t\tplaceholder=\"Description\"\n\t\t\t/>\n\t\t</Label>\n\t\t<div>\n\t\t\t<Label for=\"url\">Url</Label>\n\t\t\t<ButtonGroup class=\"inline-flex w-full items-stretch\">\n\t\t\t\t<Input bind:value={form.url} type=\"text\" name=\"url\" placeholder=\"Url\" required />\n\t\t\t\t<Select\n\t\t\t\t\tselectClass=\"min-w-30 !rounded-tl-none !rounded-bl-none border-l-0\"\n\t\t\t\t\tbind:value={form.target}\n\t\t\t\t\titems={urlTargetOptions}\n\t\t\t\t\tplaceholder=\"Target\"\n\t\t\t\t/>\n\t\t\t</ButtonGroup>\n\t\t</div>\n\t\t<Label class=\"space-y-2\">\n\t\t\t<span>Show Url</span>\n\t\t\t<Select bind:value={form.showUrl} items={showUrlOptions} placeholder=\"When show url\" />\n\t\t</Label>\n\t\t<div>\n\t\t\t<Label for=\"icon\">Icon</Label>\n\t\t\t<ButtonGroup class=\"inline-flex w-full items-stretch\">\n\t\t\t\t<Input bind:value={form.icon} type=\"text\" name=\"icon\" placeholder=\"URL or Icon name\" />\n\t\t\t\t<span class=\"color-picker\">\n\t\t\t\t\t<Input\n\t\t\t\t\t\tbind:value={form.iconColor}\n\t\t\t\t\t\tdefaultValue=\"#808080\"\n\t\t\t\t\t\tclass=\"h-full w-20 !rounded-tl-none !rounded-bl-none border-l-0\"\n\t\t\t\t\t\ttype=\"color\"\n\t\t\t\t\t\tname=\"iconColor\"\n\t\t\t\t\t\tplaceholder=\"URL or Icon name\"\n\t\t\t\t\t/>\n\t\t\t\t</span>\n\t\t\t</ButtonGroup>\n\t\t\t<Helper class=\"text-sm\">\n\t\t\t\tURL or Icon name from <a\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\thref=\"https://icon-sets.iconify.design/\"\n\t\t\t\t\tclass=\"text-primary-600 dark:text-primary-500 font-medium hover:underline\">Iconify</a\n\t\t\t\t>. The color is applied only to the <b>Iconify</b> icon.\n\t\t\t</Helper>\n\t\t</div>\n\t\t<Button type=\"submit\" class=\"w-full\">Save</Button>\n\t</form>\n</Modal>\n"
  },
  {
    "path": "src/lib/factory.ts",
    "content": "import { generateRandomString } from './helpers';\nimport type { Group, Item } from './types';\n\nexport const newGroup = (): Group => {\n\treturn {\n\t\tid: generateRandomString(10),\n\t\ttitle: '',\n\t\titems: []\n\t};\n};\n\nexport const newItem = (): Item => {\n\treturn {\n\t\tid: generateRandomString(10),\n\t\ttitle: '',\n\t\tdescription: '',\n\t\turl: ''\n\t};\n};\n"
  },
  {
    "path": "src/lib/helpers.ts",
    "content": "import type { Ids } from './types';\n\nexport const hasField = <T>(data: any, key: string): data is T => {\n\treturn key in data;\n};\n\nexport const getIds = (str: string): Ids | undefined => {\n\tconst parts = str.split('-');\n\tif (parts.length !== 2) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tgroupId: parts[0],\n\t\titemId: parts[1]\n\t};\n};\n\nexport const hashString = async (message: string): Promise<string> => {\n\tconst msgBuffer = new TextEncoder().encode(message);\n\tconst hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer));\n\tconst hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\treturn hashHex;\n};\n\nexport const generateRandomString = (length: number) => {\n\treturn Math.random()\n\t\t.toString(36)\n\t\t.substring(2, 2 + length);\n};\n\nexport const isUrlString = (str: string): boolean => {\n\tif (!str) {\n\t\treturn false;\n\t}\n\tconst urlRegex = /^(ftp|http|https):\\/\\/[^ \"]+$/;\n\treturn urlRegex.test(str);\n};\n"
  },
  {
    "path": "src/lib/index.ts",
    "content": "export const page_title = 'Dashlit';\nexport const default_dashboard = 'dashboard.json';\nexport const data_path = '/app/data';\nexport const cookie_token_key = 'token';\n"
  },
  {
    "path": "src/lib/server/helper.ts",
    "content": "import { env } from '$env/dynamic/private';\nimport { hashString } from '$lib/helpers';\nimport currentPackage from '../../../package.json';\nimport { default_dashboard, data_path } from '$lib';\n\nexport const getSecretKey = async (password: string) => {\n\treturn (env.SECRET_KEY?.length ?? 0 > 0) ? env.SECRET_KEY : await hashString(password);\n};\n\nexport const getVersion = () => {\n\treturn currentPackage.version || '0.0.0';\n};\n\nexport const dataPath = () => {\n\treturn env.DATA_PATH ?? data_path;\n};\n\nexport const filePath = () => {\n\treturn `${dataPath()}/${default_dashboard}`;\n};\n"
  },
  {
    "path": "src/lib/styles/dnd.css",
    "content": "/* Base draggable styles */\n.svelte-dnd-draggable {\n    touch-action: none; /* Prevents touch scrolling while dragging */\n    user-select: none; /* Prevents text selection during drag */\n}\n\n/* Active dragging state */\n.svelte-dnd-dragging {\n    opacity: 0.5;\n    cursor: grabbing;\n}\n\n/* Draggable hover state */\n.svelte-dnd-draggable:hover {\n    cursor: grab;\n}\n\n/* Droppable area styles */\n.svelte-dnd-droppable {\n    position: relative;\n}\n\n/* Active drop target */\n.svelte-dnd-drop-target {\n    outline: 2px dashed #4caf50;\n}\n\n/* Invalid drop target */\n.svelte-dnd-invalid-target {\n    outline: 2px dashed #f44336;\n}\n\n/* Drop preview/placeholder */\n.svelte-dnd-placeholder {\n    border: 2px dashed #9e9e9e;\n}\n\n/* Media queries for responsive design */\n@media (max-width: 600px) {\n    .svelte-dnd-draggable {\n        width: 100%;\n        touch-action: none; /* Prevents scrolling during drag */\n    }\n\n    .svelte-dnd-droppable {\n        padding: 10px;\n    }\n}\n"
  },
  {
    "path": "src/lib/types.ts",
    "content": "export enum ActionType {\n\tCREATE = 'create',\n\tDELETE = 'delete',\n\tEDIT = 'edit'\n}\n\nexport enum ShowUrlType {\n\tNEVER = 'never',\n\tALWAYS = 'always',\n\tDESC_EMPTY = 'empty_desc',\n\tHOVER = 'hover'\n}\n\nexport interface Item {\n\tid: string;\n\ttitle: string;\n\turl: string;\n\tshowUrl?: ShowUrlType;\n\ttarget?: string;\n\tdescription?: string;\n\ticon?: string;\n\ticonColor?: string;\n}\n\nexport interface Group {\n\tid: string;\n\ttitle: string;\n\tdescription?: string;\n\titems: Item[];\n}\n\nexport interface Dashboard {\n\tversion: string;\n\tgroups: Group[];\n}\n\nexport interface Ids {\n\tgroupId: string;\n\titemId: string;\n}\n\nexport type DeletionEntity = {\n\tids: Ids;\n\telement: Group | Item;\n};\n\nexport type EditableItem = {\n\tgroupId: string;\n\titem: Item;\n};\n"
  },
  {
    "path": "src/routes/(auth)/login/+page.server.ts",
    "content": "import type { PageServerLoad } from './$types';\nimport { fail, redirect } from '@sveltejs/kit';\nimport { env } from '$env/dynamic/private';\nimport * as jose from 'jose';\nimport { cookie_token_key } from '$lib';\nimport { getSecretKey } from '$lib/server/helper';\n\nexport const load: PageServerLoad = async (event) => {\n\tif (event.locals.userAuthenticated) {\n\t\treturn redirect(302, '/');\n\t}\n\n\treturn {};\n};\n\nexport const actions = {\n\tlogin: async ({ request, cookies }) => {\n\t\tconst form = await request.formData();\n\t\tlet password = String(form.get('password'));\n\n\t\tif (!password) {\n\t\t\treturn fail(400, { error: 'Password is required' });\n\t\t}\n\n\t\tif (password !== env.PASSWORD) {\n\t\t\treturn fail(401, { error: 'Invalid password' });\n\t\t}\n\n\t\tconst secretKey = await getSecretKey(password);\n\t\tconst sign = new TextEncoder().encode(secretKey);\n\n\t\tconst token = await new jose.SignJWT()\n\t\t\t.setProtectedHeader({ alg: 'HS256' })\n\t\t\t.setIssuedAt()\n\t\t\t.setExpirationTime('4weeks')\n\t\t\t.sign(sign);\n\n\t\tcookies.set(cookie_token_key, token, {\n\t\t\tpath: '/',\n\t\t\thttpOnly: true,\n\t\t\tsameSite: 'lax',\n\t\t\tsecure: process.env.NODE_ENV === 'production',\n\t\t\tmaxAge: 60 * 60 * 24 * 30\n\t\t});\n\n\t\tredirect(302, '/');\n\t}\n};\n"
  },
  {
    "path": "src/routes/(auth)/login/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport { enhance } from '$app/forms';\n\timport toast from 'svelte-5-french-toast';\n\timport { page_title } from '$lib';\n\timport { Card, Button, Label, Input, Helper, DarkMode } from 'flowbite-svelte';\n</script>\n\n<svelte:head>\n\t<title>{page_title}</title>\n</svelte:head>\n\n<div\n\tclass=\"flex h-screen items-center justify-center bg-gradient-to-r from-blue-50 via-indigo-50 to-sky-50 p-4 dark:from-slate-950 dark:via-gray-950 dark:to-zinc-950\"\n>\n\t<DarkMode class=\"absolute top-4 right-4 cursor-pointer\" />\n\t<Card class=\"p-4 sm:p-6 md:p-8\">\n\t\t<form\n\t\t\tmethod=\"POST\"\n\t\t\tuse:enhance={() => {\n\t\t\t\tconst toastId = toast.loading('Checking...');\n\t\t\t\treturn async ({ result, update }) => {\n\t\t\t\t\tawait update();\n\t\t\t\t\tif (result.type === 'failure') {\n\t\t\t\t\t\tconst message = (result.data?.error as string) ?? 'An error occurred';\n\t\t\t\t\t\ttoast.error(message, { id: toastId });\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttoast.success('Welcome back!', { icon: '👋', id: toastId });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}}\n\t\t\taction=\"?/login\"\n\t\t\tclass=\"flex flex-col space-y-6\"\n\t\t>\n\t\t\t<Label class=\"space-y-2\">\n\t\t\t\t<span>Your password</span>\n\t\t\t\t<Input type=\"password\" name=\"password\" placeholder=\"•••••\" required />\n\t\t\t</Label>\n\t\t\t<Button type=\"submit\" class=\"w-full\">Login</Button>\n\t\t</form>\n\t</Card>\n</div>\n"
  },
  {
    "path": "src/routes/(auth)/logout/+page.server.ts",
    "content": "import { cookie_token_key } from '$lib';\nimport type { PageServerLoad } from './$types';\nimport { redirect } from '@sveltejs/kit';\n\nexport const load: PageServerLoad = async (event) => {\n\tevent.cookies.delete(cookie_token_key, { path: '/' });\n\tevent.locals.userAuthenticated = false;\n\treturn redirect(302, '/login');\n};\n"
  },
  {
    "path": "src/routes/+error.svelte",
    "content": "<script>\n\timport { page } from '$app/state';\n\timport { DarkMode } from 'flowbite-svelte';\n</script>\n\n<svelte:head>\n\t<title>{page.status} - error</title>\n</svelte:head>\n\n<DarkMode class=\"absolute top-4 right-4 cursor-pointer\" />\n<div class=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform text-center\">\n\t<h1 class=\"mb-4 text-xl font-extrabold\">\n\t\t{page.status} - {page.error?.message ?? 'Ошибка'}\n\t</h1>\n\t<a\n\t\thref=\"/\"\n\t\tonclick={(e) => {\n\t\t\te.preventDefault();\n\t\t\twindow.history.back();\n\t\t}}\n\t\tclass=\"text-blue-500 hover:underline\">Go to Back</a\n\t>\n</div>\n"
  },
  {
    "path": "src/routes/+layout.server.ts",
    "content": "import type { LayoutServerLoad } from './$types';\nimport { env } from '$env/dynamic/private';\n\nexport const load: LayoutServerLoad = async ({ locals }) => {\n\treturn {\n\t\tenvOrigin: env.ORIGIN ?? '',\n\t\tuserAuthenticated: locals.userAuthenticated\n\t};\n};\n"
  },
  {
    "path": "src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n\timport { Toaster } from 'svelte-5-french-toast';\n\timport { page } from '$app/state';\n\timport '../app.css';\n\tlet { children, data } = $props();\n</script>\n\n<Toaster />\n{#if data.envOrigin !== page.url.origin}\n\t<div class=\"w-full bg-red-600 p-2 text-center text-white\">\n\t\t<p>\n\t\t\tInvalid ORIGIN found. Please specify ORIGIN={page.url.origin}. Check the\n\t\t\t<a\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tclass=\"underline\"\n\t\t\t\thref=\"https://dashlit.cwec.dev/guide/getting-started.html\">Docs</a\n\t\t\t>.\n\t\t</p>\n\t</div>\n{/if}\n{@render children()}\n"
  },
  {
    "path": "src/routes/+page.server.ts",
    "content": "import type { PageServerLoad } from './$types';\nimport { fail, redirect } from '@sveltejs/kit';\nimport fs from 'node:fs/promises';\nimport { env } from '$env/dynamic/private';\nimport type { Dashboard } from '$lib/types';\nimport { dataPath, filePath, getVersion } from '$lib/server/helper';\n\nexport const load: PageServerLoad = async (event) => {\n\tif (!event.locals.userAuthenticated) {\n\t\treturn redirect(302, '/login');\n\t}\n\tconst data = await fs.readFile(filePath(), { encoding: 'utf8' }).catch(() => {\n\t\tconsole.log(`File ${filePath()} not found`);\n\t\treturn '{}';\n\t});\n\tconst dashboard: Dashboard = JSON.parse(data);\n\treturn {\n\t\tgroups: dashboard.groups,\n\t\tcanLogout: (env.PASSWORD?.length ?? 0) > 0,\n\t\tisDemoMode: env.DEMO_MODE === 'true'\n\t};\n};\n\nexport const actions = {\n\tdefault: async ({ locals, request }) => {\n\t\tif (!locals.userAuthenticated) {\n\t\t\treturn redirect(302, '/login');\n\t\t}\n\t\tconst json = await request.json();\n\n\t\tawait fs.mkdir(dataPath(), { recursive: true }).catch(console.error);\n\t\tawait fs\n\t\t\t.writeFile(filePath(), JSON.stringify({ version: getVersion(), groups: json }))\n\t\t\t.catch((error) => {\n\t\t\t\tconsole.log(`Cant write ${filePath()}`);\n\t\t\t\treturn fail(500);\n\t\t\t});\n\t\treturn { status: 'ok' };\n\t}\n};\n"
  },
  {
    "path": "src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport '$lib/styles/dnd.css';\n\timport {\n\t\ttype Item,\n\t\ttype Group,\n\t\ttype DeletionEntity,\n\t\tActionType,\n\t\ttype EditableItem\n\t} from '$lib/types';\n\timport Header from '$lib/components/header.svelte';\n\timport Dashboard from '$lib/components/dashboard.svelte';\n\timport ModalDelete from '$lib/components/modalDelete.svelte';\n\timport ModalFormItem from '$lib/components/modalFormItem.svelte';\n\timport ModalFormGroup from '$lib/components/modalFormGroup.svelte';\n\timport { newGroup, newItem } from '$lib/factory';\n\timport { page_title } from '$lib';\n\timport toast from 'svelte-5-french-toast';\n\n\tlet { data } = $props();\n\n\tlet groups = $state(data.groups ?? []);\n\tlet editMode = $state(false);\n\tlet deletionEntity = $state<DeletionEntity | undefined>(undefined);\n\tlet editableGroup = $state<Group | undefined>(undefined);\n\tlet editableItem = $state<EditableItem | undefined>(undefined);\n\n\t$effect(() => {\n\t\tif (groups.length === 0) {\n\t\t\teditMode = true;\n\t\t}\n\t});\n\n\t// dashboard\n\tconst handleSaveDashboard = async () => {\n\t\tif (data.isDemoMode) {\n\t\t\ttoast.success('Demo mode is enabled. Changes are not saved.');\n\t\t\teditMode = false;\n\t\t\treturn;\n\t\t}\n\t\tconst toastId = toast.loading('Saving...');\n\t\tawait fetch('', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify(groups)\n\t\t})\n\t\t\t.then(() => {\n\t\t\t\teditMode = false;\n\t\t\t\ttoast.success('Saved!', { icon: '✅', id: toastId });\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\ttoast.error('Error saving dashboard', { icon: '⚠️', id: toastId });\n\t\t\t});\n\t};\n\n\tconst handleDeleteEntity = () => {\n\t\tif (deletionEntity === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (deletionEntity.ids.groupId.length === 0 && deletionEntity.ids.itemId.length === 0) {\n\t\t\tdeletionEntity = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tconst isGroup = deletionEntity.ids.itemId.length === 0;\n\n\t\tif (isGroup) {\n\t\t\tconst groupIndex = groups.findIndex((s) => s.id === deletionEntity?.ids.groupId);\n\t\t\tif (groupIndex === undefined) {\n\t\t\t\tconsole.log('Group index not found');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tgroups.splice(groupIndex, 1);\n\t\t} else {\n\t\t\tconst group = groups.find((g) => g.id === deletionEntity?.ids.groupId);\n\t\t\tconst itemIndex = group?.items.findIndex((i) => i.id === deletionEntity?.ids.itemId);\n\t\t\tif (itemIndex === undefined) {\n\t\t\t\tconsole.log('Item index not found');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tgroup?.items.splice(itemIndex, 1);\n\t\t}\n\t\tdeletionEntity = undefined;\n\t};\n\n\tconst handleSaveGroup = (group: Group) => {\n\t\tlet g = groups.find((g) => g.id === group.id);\n\t\tif (g) {\n\t\t\tg.title = group.title;\n\t\t\tg.description = group.description;\n\t\t} else {\n\t\t\tgroups.unshift(group);\n\t\t}\n\t\teditableGroup = undefined;\n\t};\n\n\tconst handleSaveItem = (groupId: string, item: Item) => {\n\t\tlet group = groups.find((g) => g.id === groupId);\n\t\tconst i = group?.items.find((i) => i.id === item.id);\n\t\tif (i) {\n\t\t\ti.title = item.title;\n\t\t\ti.description = item.description;\n\t\t\ti.url = item.url;\n\t\t\ti.icon = item.icon;\n\t\t\ti.iconColor = item.iconColor;\n\t\t\ti.target = item.target;\n\t\t\ti.showUrl = item.showUrl;\n\t\t} else {\n\t\t\tgroup?.items.push(item);\n\t\t}\n\t\teditableItem = undefined;\n\t};\n\n\t// groups\n\tconst handleActionGroup = (type: ActionType, group: Group) => {\n\t\tswitch (type) {\n\t\t\tcase ActionType.CREATE:\n\t\t\t\teditableGroup = group;\n\t\t\t\tbreak;\n\t\t\tcase ActionType.EDIT:\n\t\t\t\teditableGroup = { ...group };\n\t\t\t\tbreak;\n\t\t\tcase ActionType.DELETE:\n\t\t\t\tdeletionEntity = {\n\t\t\t\t\tids: { groupId: group.id, itemId: '' },\n\t\t\t\t\telement: { ...group }\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t}\n\t};\n\n\t// items\n\tconst handleActionItem = (type: ActionType, groupId: string, item: Item) => {\n\t\tswitch (type) {\n\t\t\tcase ActionType.CREATE:\n\t\t\t\teditableItem = {\n\t\t\t\t\tgroupId: groupId,\n\t\t\t\t\titem: item\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase ActionType.EDIT:\n\t\t\t\teditableItem = {\n\t\t\t\t\tgroupId: groupId,\n\t\t\t\t\titem: { ...item }\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase ActionType.DELETE:\n\t\t\t\tdeletionEntity = {\n\t\t\t\t\tids: { groupId: groupId, itemId: item.id },\n\t\t\t\t\telement: { ...item }\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t}\n\t};\n\n\tconst handleClickItem = (item: Item) => {\n\t\tif (editMode) {\n\t\t\treturn;\n\t\t}\n\t\twindow.open(item.url, item.target ?? '_blank');\n\t};\n</script>\n\n<svelte:head>\n\t<title>{page_title}</title>\n</svelte:head>\n\n<div class=\"dashboard p-4\">\n\t<Header\n\t\t{editMode}\n\t\tcanLogout={data.canLogout}\n\t\thandleSave={handleSaveDashboard}\n\t\thandleEdit={() => (editMode = !editMode)}\n\t/>\n\t<Dashboard\n\t\t{editMode}\n\t\t{groups}\n\t\t{handleClickItem}\n\t\thandleClickItemAction={handleActionItem}\n\t\thandleClickGroupAction={handleActionGroup}\n\t/>\n</div>\n\n<ModalDelete\n\tentity={deletionEntity}\n\thandleClose={() => (deletionEntity = undefined)}\n\thandleConfirm={handleDeleteEntity}\n/>\n\n<ModalFormGroup\n\tisOpen={editableGroup !== undefined}\n\tgroup={editableGroup ?? newGroup()}\n\thandleClose={(group) => {\n\t\tif (group) {\n\t\t\thandleSaveGroup(group);\n\t\t} else {\n\t\t\teditableGroup = undefined;\n\t\t}\n\t}}\n/>\n\n<ModalFormItem\n\tisOpen={editableItem !== undefined}\n\titem={editableItem?.item ?? newItem()}\n\thandleClose={(item) => {\n\t\tif (item && editableItem) {\n\t\t\thandleSaveItem(editableItem.groupId, item);\n\t\t} else {\n\t\t\teditableItem = undefined;\n\t\t}\n\t}}\n/>\n"
  },
  {
    "path": "src/routes/custom.css/+server.ts",
    "content": "import { dataPath, filePath } from '$lib/server/helper';\nimport { type RequestHandler } from '@sveltejs/kit';\nimport fs from 'node:fs/promises';\n\nexport const GET: RequestHandler = async ({ url }) => {\n\tconst fileName = `${dataPath()}/custom.css`;\n\tconst data = await fs.readFile(fileName, { encoding: 'utf8' }).catch(() => {\n\t\tconsole.log(`File ${fileName} not found`);\n\t\treturn '';\n\t});\n\n\treturn new Response(String(data), {\n\t\theaders: {\n\t\t\t'Content-type': 'text/css'\n\t\t}\n\t});\n};\n"
  },
  {
    "path": "static/site.webmanifest",
    "content": "{\n  \"name\": \"DashLit\",\n  \"short_name\": \"DashLit\",\n  \"icons\": [\n    {\n      \"src\": \"/web-app-manifest-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"/web-app-manifest-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    }\n  ],\n  \"theme_color\": \"#ffffff\",\n  \"background_color\": \"#ffffff\",\n  \"display\": \"standalone\"\n}"
  },
  {
    "path": "svelte.config.js",
    "content": "import adapter from '@sveltejs/adapter-node';\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\nconst config = {\n\tpreprocess: vitePreprocess(),\n\tkit: {\n\t\tadapter: adapter({\n\t\t\tout: 'build',\n\t\t\tprecompress: true,\n\t\t\tenvPrefix: ''\n\t\t})\n\t}\n};\n\nexport default config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t}\n\t// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias\n\t// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files\n\t//\n\t// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes\n\t// from the referenced tsconfig.json - TypeScript does not merge them in\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import tailwindcss from '@tailwindcss/vite';\nimport { sveltekit } from '@sveltejs/kit/vite';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n\tplugins: [tailwindcss(), sveltekit()],\n\tserver: {\n\t\tallowedHosts: true\n\t}\n});\n"
  }
]