feat: simple landing with contact form and applications showcase
Some checks failed
Build & Deploy (prod) / deploy (push) Failing after 11s

This commit is contained in:
Raul Lugo
2026-02-05 12:53:35 +01:00
parent 385175b49c
commit dd227618cd
98 changed files with 4182 additions and 298 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
.DS_Store
.astro
.git
.github
.vscode
node_modules
dist
.env
.env.*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
README.md

25
.github/workflows/deploy.yaml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Build & Deploy (prod)
"on":
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build, push, bump infra
uses: https://git.rlugo.dev/resuely/ci-actions/.github/actions/build-push-bump@main
with:
registry: git.rlugo.dev
image: git.rlugo.dev/resuely/landing
infraRepo: git.rlugo.dev/resuely/infra.git
stackEnvPath: stacks/resuely/prod/stack.env
stackEnvKey: LANDING_IMAGE_TAG
registryUsername: ${{ secrets.REGISTRY_USERNAME }}
registryToken: ${{ secrets.REGISTRY_TOKEN }}
infraPushToken: ${{ secrets.INFRA_PUSH_TOKEN }}

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# ----- Build deps -----
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json starwind-*.tgz ./
RUN npm ci
# ----- Build -----
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json package-lock.json starwind-*.tgz ./
COPY astro.config.* ./
COPY tsconfig.json ./
COPY src ./src
COPY public ./public
ENV NODE_ENV=production
RUN npm run build
# ----- Production deps -----
FROM node:22-alpine AS prod-deps
WORKDIR /app
COPY package.json package-lock.json starwind-*.tgz ./
RUN npm ci --omit=dev
# ----- Runtime -----
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=4321
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 4321
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get({host:'127.0.0.1',port:process.env.PORT||4321,path:'/api/healthz'},(r)=>process.exit((r.statusCode||500)<500?0:1)).on('error',()=>process.exit(1))"
CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,12 +1,18 @@
import tailwindcss from "@tailwindcss/vite";
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import tailwindcss from "@tailwindcss/vite";
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({ export default defineConfig({
vite: { site: "https://resuely.com",
plugins: [tailwindcss()] output: "server",
} adapter: node({ mode: "standalone" }),
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
"@": decodeURI(new URL("./src", import.meta.url).pathname),
},
},
},
}); });

730
package-lock.json generated
View File

@@ -8,10 +8,17 @@
"name": "landing", "name": "landing",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.2",
"@astrojs/ts-plugin": "^1.10.6",
"@starwind-ui/core": "^1.15.2",
"@tabler/icons": "^3.36.1", "@tabler/icons": "^3.36.1",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.9", "astro": "^5.16.9",
"embla-carousel": "^8.6.0",
"embla-carousel-autoplay": "^8.6.0",
"motion": "^12.26.2",
"starwind": "file:starwind-1.15.2.tgz",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
@@ -59,6 +66,20 @@
"vfile": "^6.0.3" "vfile": "^6.0.3"
} }
}, },
"node_modules/@astrojs/node": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.5.2.tgz",
"integrity": "sha512-85/x+FRwbNGDip1TzSGMiak31/6LvBhA8auqd9lLoHaM5XElk+uIfIr3KjJqucDojE0PtiLk1lMSwD9gd3YlGg==",
"license": "MIT",
"dependencies": {
"@astrojs/internal-helpers": "0.7.5",
"send": "^1.2.1",
"server-destroy": "^1.0.1"
},
"peerDependencies": {
"astro": "^5.14.3"
}
},
"node_modules/@astrojs/prism": { "node_modules/@astrojs/prism": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz",
@@ -89,6 +110,30 @@
"node": "18.20.8 || ^20.3.0 || >=22.0.0" "node": "18.20.8 || ^20.3.0 || >=22.0.0"
} }
}, },
"node_modules/@astrojs/ts-plugin": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/@astrojs/ts-plugin/-/ts-plugin-1.10.6.tgz",
"integrity": "sha512-Ke5CNwxn/ozsh6THJKuayUlBToa3uiPDi2oSwcXmTdeiJ0PGr+UkdQJf9hdMgBjbIka9fhnSn3UhYamfNfJ73A==",
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.10.3",
"@astrojs/yaml2ts": "^0.2.2",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@volar/language-core": "~2.4.23",
"@volar/typescript": "~2.4.23",
"semver": "^7.3.8",
"vscode-languageserver-textdocument": "^1.0.11"
}
},
"node_modules/@astrojs/yaml2ts": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.2.tgz",
"integrity": "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==",
"license": "MIT",
"dependencies": {
"yaml": "^2.5.0"
}
},
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -147,6 +192,27 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@clack/core": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@clack/prompts": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
"license": "MIT",
"dependencies": {
"@clack/core": "0.5.0",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
@@ -1443,6 +1509,12 @@
"win32" "win32"
] ]
}, },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@shikijs/core": { "node_modules/@shikijs/core": {
"version": "3.21.0", "version": "3.21.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz",
@@ -1510,6 +1582,24 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@starwind-ui/core": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/@starwind-ui/core/-/core-1.15.2.tgz",
"integrity": "sha512-4D4+/AdcSfaujNXNKq0rsCm/3IF0f8W63Qa3z4REn16S0LYRJK/w2qPSKiK8PprpyWyqHuYBp/wtCxcFjoUQ7Q==",
"license": "MIT"
},
"node_modules/@tabler/icons": { "node_modules/@tabler/icons": {
"version": "3.36.1", "version": "3.36.1",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
@@ -1849,6 +1939,32 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@volar/language-core": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz",
"integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==",
"license": "MIT",
"dependencies": {
"@volar/source-map": "2.4.27"
}
},
"node_modules/@volar/source-map": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz",
"integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==",
"license": "MIT"
},
"node_modules/@volar/typescript": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz",
"integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==",
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.27",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2283,6 +2399,20 @@
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/crossws": { "node_modules/crossws": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
@@ -2560,6 +2690,21 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
},
"node_modules/embla-carousel-autoplay": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
"integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "10.6.0", "version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
@@ -2665,6 +2810,32 @@
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/execa": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
"integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.6",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.1",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.2.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.1.1"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/extend": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -2688,6 +2859,21 @@
} }
} }
}, },
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"license": "MIT",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flattie": { "node_modules/flattie": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
@@ -2718,6 +2904,47 @@
"node": ">=24.12.0" "node": ">=24.12.0"
} }
}, },
"node_modules/framer-motion": {
"version": "12.26.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.26.2.tgz",
"integrity": "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.26.2",
"motion-utils": "^12.24.10",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2744,6 +2971,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"license": "MIT",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/github-slugger": { "node_modules/github-slugger": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
@@ -2972,6 +3215,15 @@
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/human-signals": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/import-meta-resolve": { "node_modules/import-meta-resolve": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
@@ -3045,6 +3297,30 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": { "node_modules/is-wsl": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
@@ -3060,6 +3336,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -3081,6 +3363,18 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -4191,6 +4485,47 @@
"mini-svg-data-uri": "cli.js" "mini-svg-data-uri": "cli.js"
} }
}, },
"node_modules/motion": {
"version": "12.26.2",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.26.2.tgz",
"integrity": "sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.26.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.26.2",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.26.2.tgz",
"integrity": "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.24.10"
}
},
"node_modules/motion-utils": {
"version": "12.24.10",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz",
"integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==",
"license": "MIT"
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -4267,6 +4602,34 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"license": "MIT",
"dependencies": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nth-check": { "node_modules/nth-check": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -4380,6 +4743,18 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -4392,6 +4767,21 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/piccolore": { "node_modules/piccolore": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
@@ -4444,6 +4834,21 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/pretty-ms": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
"integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
"license": "MIT",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.30.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -4727,7 +5132,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
}, },
@@ -4833,6 +5237,27 @@
"@img/sharp-win32-x64": "0.34.5" "@img/sharp-win32-x64": "0.34.5"
} }
}, },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shiki": { "node_modules/shiki": {
"version": "3.21.0", "version": "3.21.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz",
@@ -4849,6 +5274,18 @@
"@types/hast": "^3.0.4" "@types/hast": "^3.0.4"
} }
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -4886,6 +5323,37 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/starwind": {
"version": "1.15.2",
"resolved": "file:starwind-1.15.2.tgz",
"integrity": "sha512-Grr5oaW3PGkKhQGA10pqlcb40bhk6b5HJOIY+jP9hGFOp+fcEqmgRiSRFqCf8stXNQMC4vl9+Yzp6YM0Yz92qg==",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^0.11.0",
"@starwind-ui/core": "1.15.2",
"chalk": "^5.6.2",
"commander": "^14.0.2",
"execa": "^9.6.0",
"fs-extra": "^11.3.2",
"semver": "^7.7.3",
"zod": "^3.25.74"
},
"bin": {
"starwind": "dist/index.js"
},
"engines": {
"node": "^20.6.0 || >=22.0.0"
}
},
"node_modules/starwind/node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@@ -4932,6 +5400,18 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
@@ -4962,7 +5442,6 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/dcastil" "url": "https://github.com/sponsors/dcastil"
@@ -4991,8 +5470,7 @@
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@@ -5082,8 +5560,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD", "license": "0BSD"
"optional": true
}, },
"node_modules/tw-animate-css": { "node_modules/tw-animate-css": {
"version": "1.4.0", "version": "1.4.0",
@@ -5138,6 +5615,18 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unified": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -5291,6 +5780,15 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unstorage": { "node_modules/unstorage": {
"version": "1.17.4", "version": "1.17.4",
"resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz",
@@ -5434,7 +5932,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -5523,6 +6020,18 @@
} }
} }
}, },
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"license": "MIT"
},
"node_modules/web-namespaces": { "node_modules/web-namespaces": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@@ -5533,6 +6042,21 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/which-pm-runs": { "node_modules/which-pm-runs": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz",
@@ -5580,6 +6104,21 @@
"integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yargs-parser": { "node_modules/yargs-parser": {
"version": "21.1.1", "version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
@@ -5633,7 +6172,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -5665,6 +6203,182 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/server-destroy": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
} }
} }
} }

View File

@@ -5,14 +5,22 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"start": "node ./dist/server/entry.mjs",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.2",
"@astrojs/ts-plugin": "^1.10.6",
"@starwind-ui/core": "^1.15.2",
"@tabler/icons": "^3.36.1", "@tabler/icons": "^3.36.1",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.9", "astro": "^5.16.9",
"embla-carousel": "^8.6.0",
"embla-carousel-autoplay": "^8.6.0",
"motion": "^12.26.2",
"starwind": "file:starwind-1.15.2.tgz",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,9 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><style>
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> #light-icon {
<style> display: inline;
path { fill: #000; } }
@media (prefers-color-scheme: dark) { #dark-icon {
path { fill: #FFF; } display: none;
} }
</style>
</svg> @media (prefers-color-scheme: dark) {
#light-icon {
display: none;
}
#dark-icon {
display: inline;
}
}
</style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1394)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(1,0,0,1,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><style>
#light-icon {
display: inline;
}
#dark-icon {
display: none;
}
@media (prefers-color-scheme: dark) {
#light-icon {
display: none;
}
#dark-icon {
display: inline;
}
}
</style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1358)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(1.953125,0,0,1.953125,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M255.618 109.018C257.055 108.86 261.937 111.967 263.523 112.86L275.433 119.513L313.792 140.951L355.839 164.515C362.115 168.044 369.665 171.995 375.658 175.714L375.693 323.705C364.077 330.668 350.632 337.812 338.755 344.45C311.289 359.735 283.906 375.164 256.603 390.738C255.219 390.968 147.135 329.589 136.553 323.824C136.766 307.58 136.584 290.906 136.591 274.632L136.565 175.469L255.618 109.018ZM262.362 248.348C262.371 252.797 262.198 295.619 262.58 296.117L331.071 257.876L353.137 245.528C356.16 243.815 360.172 241.698 363.014 239.904C362.541 228.029 363.188 215.211 362.942 203.26C362.912 201.762 363.118 193.293 362.856 192.48L362.635 192.322C341.659 203.458 319.904 216.135 299.041 227.748L274.501 241.393C271.177 243.252 265.383 246.274 262.362 248.348ZM154.263 180.297C160.313 183.877 167.39 187.476 173.656 191.002C191.779 201.263 209.959 211.423 228.196 221.481L247.774 232.36C249.157 233.127 255.094 236.521 256.155 236.827L256.758 236.631L298.766 213.247C270.411 197.272 241.923 181.535 213.304 166.038C211.169 164.868 197.408 156.998 196.423 156.848C190.193 159.755 181.151 165.198 175.053 168.664C168.503 172.387 160.559 176.453 154.263 180.297ZM205.322 290.281C218.163 296.796 231.119 305.217 244.055 311.777C245.174 312.344 248.784 314.629 249.64 314.831L249.775 314.606C249.458 292.528 249.877 270.293 249.703 248.201C246.126 245.983 242.189 243.794 238.461 241.809C227.307 235.869 216.402 229.26 205.299 223.263C205.006 227.931 205.112 233.471 205.11 238.199L205.116 261.402V279.26C205.116 282.575 204.992 287.069 205.322 290.281ZM310.635 154.311C295.545 163.041 279.46 170.94 264.327 179.697C280.332 188.197 296.226 196.904 312.004 205.818C323.247 199.123 335.56 193.092 346.913 186.457C350.417 184.409 354.86 182.309 358.172 180.244C348.009 174.683 337.886 169.048 327.806 163.34C325.333 161.948 312.234 154.101 310.635 154.311ZM192.589 216.174C178.028 208.379 163.807 200.048 149.351 192.146C148.996 196.508 148.964 201.707 149.089 206.108C149.365 215.792 148.628 226.093 149.216 235.699C158.022 240.22 167.205 245.577 175.876 250.45C180.768 253.199 188.104 257.065 192.615 260.036C192.627 245.657 192.88 230.485 192.589 216.174ZM209.675 149.446C223.394 156.69 236.878 164.623 250.585 171.91C250.983 172.121 251.326 172.095 251.736 172.023C262.255 166.029 273.22 160.434 283.835 154.585C288.527 151.999 293.67 149.502 298.26 146.823C294.509 144.753 256.796 123.645 256.03 123.58C253.183 124.865 249.867 126.89 247.106 128.461C243.021 130.784 238.92 133.08 234.804 135.349L218.972 144.081C216.14 145.646 212.258 147.679 209.675 149.446ZM205.227 347.835C210.75 350.614 217.104 354.385 222.558 357.447L249.774 372.776C249.992 368.474 249.714 362.059 249.704 357.562C249.685 348.207 249.846 338.65 249.7 329.324C239.992 324.023 230.316 318.666 220.671 313.251C216.102 310.696 209.836 306.899 205.29 304.683C204.965 307.466 205.084 311.234 205.114 314.078C205.23 325.298 204.913 336.625 205.227 347.835ZM192.599 340.676C192.641 318.921 192.983 295.888 192.55 274.235C178.363 267.042 163.375 257.721 149.136 250.06L149.087 292.454C149.088 299.63 148.813 309.464 149.241 316.464C156.553 320.222 164.35 324.78 171.538 328.846C178.58 332.751 185.6 336.694 192.599 340.676ZM362.869 254.606L334.271 270.626C329.819 273.123 323.818 276.261 319.602 278.889C319.658 283.878 319.422 339.653 319.947 340.413L344.367 326.739C349.619 323.744 357.736 318.812 363.003 316.473C362.9 310.789 363.06 254.952 362.869 254.606ZM306.886 285.909C293.902 293.017 281.103 300.587 268.081 307.646C266.209 308.661 264.16 309.756 262.383 310.895L262.382 351.968C262.383 357.392 262.066 367.309 262.433 372.297L262.741 372.429L307.106 347.74C306.682 345.098 306.893 333.849 306.894 330.443L306.919 294.04C306.921 292.916 307.104 286.442 306.886 285.909Z" fill="black"></path>
</svg></svg></g></g><defs><clipPath id="SvgjsClipPath1358"><rect width="1000" height="1000" x="0" y="0" rx="150" ry="150"></rect></clipPath><clipPath id="SvgjsClipPath1394"><rect width="1000" height="1000" x="0" y="0" rx="350" ry="350"></rect></clipPath></defs></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g><g transform="matrix(1.953125,0,0,1.953125,0,0)" style="filter: invert(100%)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M255.618 109.018C257.055 108.86 261.937 111.967 263.523 112.86L275.433 119.513L313.792 140.951L355.839 164.515C362.115 168.044 369.665 171.995 375.658 175.714L375.693 323.705C364.077 330.668 350.632 337.812 338.755 344.45C311.289 359.735 283.906 375.164 256.603 390.738C255.219 390.968 147.135 329.589 136.553 323.824C136.766 307.58 136.584 290.906 136.591 274.632L136.565 175.469L255.618 109.018ZM262.362 248.348C262.371 252.797 262.198 295.619 262.58 296.117L331.071 257.876L353.137 245.528C356.16 243.815 360.172 241.698 363.014 239.904C362.541 228.029 363.188 215.211 362.942 203.26C362.912 201.762 363.118 193.293 362.856 192.48L362.635 192.322C341.659 203.458 319.904 216.135 299.041 227.748L274.501 241.393C271.177 243.252 265.383 246.274 262.362 248.348ZM154.263 180.297C160.313 183.877 167.39 187.476 173.656 191.002C191.779 201.263 209.959 211.423 228.196 221.481L247.774 232.36C249.157 233.127 255.094 236.521 256.155 236.827L256.758 236.631L298.766 213.247C270.411 197.272 241.923 181.535 213.304 166.038C211.169 164.868 197.408 156.998 196.423 156.848C190.193 159.755 181.151 165.198 175.053 168.664C168.503 172.387 160.559 176.453 154.263 180.297ZM205.322 290.281C218.163 296.796 231.119 305.217 244.055 311.777C245.174 312.344 248.784 314.629 249.64 314.831L249.775 314.606C249.458 292.528 249.877 270.293 249.703 248.201C246.126 245.983 242.189 243.794 238.461 241.809C227.307 235.869 216.402 229.26 205.299 223.263C205.006 227.931 205.112 233.471 205.11 238.199L205.116 261.402V279.26C205.116 282.575 204.992 287.069 205.322 290.281ZM310.635 154.311C295.545 163.041 279.46 170.94 264.327 179.697C280.332 188.197 296.226 196.904 312.004 205.818C323.247 199.123 335.56 193.092 346.913 186.457C350.417 184.409 354.86 182.309 358.172 180.244C348.009 174.683 337.886 169.048 327.806 163.34C325.333 161.948 312.234 154.101 310.635 154.311ZM192.589 216.174C178.028 208.379 163.807 200.048 149.351 192.146C148.996 196.508 148.964 201.707 149.089 206.108C149.365 215.792 148.628 226.093 149.216 235.699C158.022 240.22 167.205 245.577 175.876 250.45C180.768 253.199 188.104 257.065 192.615 260.036C192.627 245.657 192.88 230.485 192.589 216.174ZM209.675 149.446C223.394 156.69 236.878 164.623 250.585 171.91C250.983 172.121 251.326 172.095 251.736 172.023C262.255 166.029 273.22 160.434 283.835 154.585C288.527 151.999 293.67 149.502 298.26 146.823C294.509 144.753 256.796 123.645 256.03 123.58C253.183 124.865 249.867 126.89 247.106 128.461C243.021 130.784 238.92 133.08 234.804 135.349L218.972 144.081C216.14 145.646 212.258 147.679 209.675 149.446ZM205.227 347.835C210.75 350.614 217.104 354.385 222.558 357.447L249.774 372.776C249.992 368.474 249.714 362.059 249.704 357.562C249.685 348.207 249.846 338.65 249.7 329.324C239.992 324.023 230.316 318.666 220.671 313.251C216.102 310.696 209.836 306.899 205.29 304.683C204.965 307.466 205.084 311.234 205.114 314.078C205.23 325.298 204.913 336.625 205.227 347.835ZM192.599 340.676C192.641 318.921 192.983 295.888 192.55 274.235C178.363 267.042 163.375 257.721 149.136 250.06L149.087 292.454C149.088 299.63 148.813 309.464 149.241 316.464C156.553 320.222 164.35 324.78 171.538 328.846C178.58 332.751 185.6 336.694 192.599 340.676ZM362.869 254.606L334.271 270.626C329.819 273.123 323.818 276.261 319.602 278.889C319.658 283.878 319.422 339.653 319.947 340.413L344.367 326.739C349.619 323.744 357.736 318.812 363.003 316.473C362.9 310.789 363.06 254.952 362.869 254.606ZM306.886 285.909C293.902 293.017 281.103 300.587 268.081 307.646C266.209 308.661 264.16 309.756 262.383 310.895L262.382 351.968C262.383 357.392 262.066 367.309 262.433 372.297L262.741 372.429L307.106 347.74C306.682 345.098 306.893 333.849 306.894 330.443L306.919 294.04C306.921 292.916 307.104 286.442 306.886 285.909Z" fill="white"></path>
</svg></svg></g></g></svg></g></svg></svg></g></g></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1395)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(1,0,0,1,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><style>
#light-icon {
display: inline;
}
#dark-icon {
display: none;
}
@media (prefers-color-scheme: dark) {
#light-icon {
display: none;
}
#dark-icon {
display: inline;
}
}
</style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1358)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(1.953125,0,0,1.953125,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M255.618 109.018C257.055 108.86 261.937 111.967 263.523 112.86L275.433 119.513L313.792 140.951L355.839 164.515C362.115 168.044 369.665 171.995 375.658 175.714L375.693 323.705C364.077 330.668 350.632 337.812 338.755 344.45C311.289 359.735 283.906 375.164 256.603 390.738C255.219 390.968 147.135 329.589 136.553 323.824C136.766 307.58 136.584 290.906 136.591 274.632L136.565 175.469L255.618 109.018ZM262.362 248.348C262.371 252.797 262.198 295.619 262.58 296.117L331.071 257.876L353.137 245.528C356.16 243.815 360.172 241.698 363.014 239.904C362.541 228.029 363.188 215.211 362.942 203.26C362.912 201.762 363.118 193.293 362.856 192.48L362.635 192.322C341.659 203.458 319.904 216.135 299.041 227.748L274.501 241.393C271.177 243.252 265.383 246.274 262.362 248.348ZM154.263 180.297C160.313 183.877 167.39 187.476 173.656 191.002C191.779 201.263 209.959 211.423 228.196 221.481L247.774 232.36C249.157 233.127 255.094 236.521 256.155 236.827L256.758 236.631L298.766 213.247C270.411 197.272 241.923 181.535 213.304 166.038C211.169 164.868 197.408 156.998 196.423 156.848C190.193 159.755 181.151 165.198 175.053 168.664C168.503 172.387 160.559 176.453 154.263 180.297ZM205.322 290.281C218.163 296.796 231.119 305.217 244.055 311.777C245.174 312.344 248.784 314.629 249.64 314.831L249.775 314.606C249.458 292.528 249.877 270.293 249.703 248.201C246.126 245.983 242.189 243.794 238.461 241.809C227.307 235.869 216.402 229.26 205.299 223.263C205.006 227.931 205.112 233.471 205.11 238.199L205.116 261.402V279.26C205.116 282.575 204.992 287.069 205.322 290.281ZM310.635 154.311C295.545 163.041 279.46 170.94 264.327 179.697C280.332 188.197 296.226 196.904 312.004 205.818C323.247 199.123 335.56 193.092 346.913 186.457C350.417 184.409 354.86 182.309 358.172 180.244C348.009 174.683 337.886 169.048 327.806 163.34C325.333 161.948 312.234 154.101 310.635 154.311ZM192.589 216.174C178.028 208.379 163.807 200.048 149.351 192.146C148.996 196.508 148.964 201.707 149.089 206.108C149.365 215.792 148.628 226.093 149.216 235.699C158.022 240.22 167.205 245.577 175.876 250.45C180.768 253.199 188.104 257.065 192.615 260.036C192.627 245.657 192.88 230.485 192.589 216.174ZM209.675 149.446C223.394 156.69 236.878 164.623 250.585 171.91C250.983 172.121 251.326 172.095 251.736 172.023C262.255 166.029 273.22 160.434 283.835 154.585C288.527 151.999 293.67 149.502 298.26 146.823C294.509 144.753 256.796 123.645 256.03 123.58C253.183 124.865 249.867 126.89 247.106 128.461C243.021 130.784 238.92 133.08 234.804 135.349L218.972 144.081C216.14 145.646 212.258 147.679 209.675 149.446ZM205.227 347.835C210.75 350.614 217.104 354.385 222.558 357.447L249.774 372.776C249.992 368.474 249.714 362.059 249.704 357.562C249.685 348.207 249.846 338.65 249.7 329.324C239.992 324.023 230.316 318.666 220.671 313.251C216.102 310.696 209.836 306.899 205.29 304.683C204.965 307.466 205.084 311.234 205.114 314.078C205.23 325.298 204.913 336.625 205.227 347.835ZM192.599 340.676C192.641 318.921 192.983 295.888 192.55 274.235C178.363 267.042 163.375 257.721 149.136 250.06L149.087 292.454C149.088 299.63 148.813 309.464 149.241 316.464C156.553 320.222 164.35 324.78 171.538 328.846C178.58 332.751 185.6 336.694 192.599 340.676ZM362.869 254.606L334.271 270.626C329.819 273.123 323.818 276.261 319.602 278.889C319.658 283.878 319.422 339.653 319.947 340.413L344.367 326.739C349.619 323.744 357.736 318.812 363.003 316.473C362.9 310.789 363.06 254.952 362.869 254.606ZM306.886 285.909C293.902 293.017 281.103 300.587 268.081 307.646C266.209 308.661 264.16 309.756 262.383 310.895L262.382 351.968C262.383 357.392 262.066 367.309 262.433 372.297L262.741 372.429L307.106 347.74C306.682 345.098 306.893 333.849 306.894 330.443L306.919 294.04C306.921 292.916 307.104 286.442 306.886 285.909Z" fill="black"></path>
</svg></svg></g></g><defs><clipPath id="SvgjsClipPath1358"><rect width="1000" height="1000" x="0" y="0" rx="150" ry="150"></rect></clipPath><clipPath id="SvgjsClipPath1395"><rect width="1000" height="1000" x="0" y="0" rx="350" ry="350"></rect></clipPath></defs></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g><g transform="matrix(1.953125,0,0,1.953125,0,0)" style="filter: invert(100%)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M255.618 109.018C257.055 108.86 261.937 111.967 263.523 112.86L275.433 119.513L313.792 140.951L355.839 164.515C362.115 168.044 369.665 171.995 375.658 175.714L375.693 323.705C364.077 330.668 350.632 337.812 338.755 344.45C311.289 359.735 283.906 375.164 256.603 390.738C255.219 390.968 147.135 329.589 136.553 323.824C136.766 307.58 136.584 290.906 136.591 274.632L136.565 175.469L255.618 109.018ZM262.362 248.348C262.371 252.797 262.198 295.619 262.58 296.117L331.071 257.876L353.137 245.528C356.16 243.815 360.172 241.698 363.014 239.904C362.541 228.029 363.188 215.211 362.942 203.26C362.912 201.762 363.118 193.293 362.856 192.48L362.635 192.322C341.659 203.458 319.904 216.135 299.041 227.748L274.501 241.393C271.177 243.252 265.383 246.274 262.362 248.348ZM154.263 180.297C160.313 183.877 167.39 187.476 173.656 191.002C191.779 201.263 209.959 211.423 228.196 221.481L247.774 232.36C249.157 233.127 255.094 236.521 256.155 236.827L256.758 236.631L298.766 213.247C270.411 197.272 241.923 181.535 213.304 166.038C211.169 164.868 197.408 156.998 196.423 156.848C190.193 159.755 181.151 165.198 175.053 168.664C168.503 172.387 160.559 176.453 154.263 180.297ZM205.322 290.281C218.163 296.796 231.119 305.217 244.055 311.777C245.174 312.344 248.784 314.629 249.64 314.831L249.775 314.606C249.458 292.528 249.877 270.293 249.703 248.201C246.126 245.983 242.189 243.794 238.461 241.809C227.307 235.869 216.402 229.26 205.299 223.263C205.006 227.931 205.112 233.471 205.11 238.199L205.116 261.402V279.26C205.116 282.575 204.992 287.069 205.322 290.281ZM310.635 154.311C295.545 163.041 279.46 170.94 264.327 179.697C280.332 188.197 296.226 196.904 312.004 205.818C323.247 199.123 335.56 193.092 346.913 186.457C350.417 184.409 354.86 182.309 358.172 180.244C348.009 174.683 337.886 169.048 327.806 163.34C325.333 161.948 312.234 154.101 310.635 154.311ZM192.589 216.174C178.028 208.379 163.807 200.048 149.351 192.146C148.996 196.508 148.964 201.707 149.089 206.108C149.365 215.792 148.628 226.093 149.216 235.699C158.022 240.22 167.205 245.577 175.876 250.45C180.768 253.199 188.104 257.065 192.615 260.036C192.627 245.657 192.88 230.485 192.589 216.174ZM209.675 149.446C223.394 156.69 236.878 164.623 250.585 171.91C250.983 172.121 251.326 172.095 251.736 172.023C262.255 166.029 273.22 160.434 283.835 154.585C288.527 151.999 293.67 149.502 298.26 146.823C294.509 144.753 256.796 123.645 256.03 123.58C253.183 124.865 249.867 126.89 247.106 128.461C243.021 130.784 238.92 133.08 234.804 135.349L218.972 144.081C216.14 145.646 212.258 147.679 209.675 149.446ZM205.227 347.835C210.75 350.614 217.104 354.385 222.558 357.447L249.774 372.776C249.992 368.474 249.714 362.059 249.704 357.562C249.685 348.207 249.846 338.65 249.7 329.324C239.992 324.023 230.316 318.666 220.671 313.251C216.102 310.696 209.836 306.899 205.29 304.683C204.965 307.466 205.084 311.234 205.114 314.078C205.23 325.298 204.913 336.625 205.227 347.835ZM192.599 340.676C192.641 318.921 192.983 295.888 192.55 274.235C178.363 267.042 163.375 257.721 149.136 250.06L149.087 292.454C149.088 299.63 148.813 309.464 149.241 316.464C156.553 320.222 164.35 324.78 171.538 328.846C178.58 332.751 185.6 336.694 192.599 340.676ZM362.869 254.606L334.271 270.626C329.819 273.123 323.818 276.261 319.602 278.889C319.658 283.878 319.422 339.653 319.947 340.413L344.367 326.739C349.619 323.744 357.736 318.812 363.003 316.473C362.9 310.789 363.06 254.952 362.869 254.606ZM306.886 285.909C293.902 293.017 281.103 300.587 268.081 307.646C266.209 308.661 264.16 309.756 262.383 310.895L262.382 351.968C262.383 357.392 262.066 367.309 262.433 372.297L262.741 372.429L307.106 347.74C306.682 345.098 306.893 333.849 306.894 330.443L306.919 294.04C306.921 292.916 307.104 286.442 306.886 285.909Z" fill="white"></path>
</svg></svg></g></g></svg></g></svg></svg></g></g></svg></g></svg>

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/og-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://resuely.com/sitemap.xml

21
public/site.webmanifest Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "Resuely",
"short_name": "Resuely",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

6
public/sitemap.xml Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://resuely.com/</loc>
</url>
</urlset>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
src/assets/faq.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

BIN
src/assets/handy.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

BIN
src/assets/hero1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

BIN
src/assets/hero2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

BIN
src/assets/hero3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
src/assets/hero4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
src/assets/hero5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

4
src/assets/logo-dark.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M255.618 109.018C257.055 108.86 261.937 111.967 263.523 112.86L275.433 119.513L313.792 140.951L355.839 164.515C362.115 168.044 369.665 171.995 375.658 175.714L375.693 323.705C364.077 330.668 350.632 337.812 338.755 344.45C311.289 359.735 283.906 375.164 256.603 390.738C255.219 390.968 147.135 329.589 136.553 323.824C136.766 307.58 136.584 290.906 136.591 274.632L136.565 175.469L255.618 109.018ZM262.362 248.348C262.371 252.797 262.198 295.619 262.58 296.117L331.071 257.876L353.137 245.528C356.16 243.815 360.172 241.698 363.014 239.904C362.541 228.029 363.188 215.211 362.942 203.26C362.912 201.762 363.118 193.293 362.856 192.48L362.635 192.322C341.659 203.458 319.904 216.135 299.041 227.748L274.501 241.393C271.177 243.252 265.383 246.274 262.362 248.348ZM154.263 180.297C160.313 183.877 167.39 187.476 173.656 191.002C191.779 201.263 209.959 211.423 228.196 221.481L247.774 232.36C249.157 233.127 255.094 236.521 256.155 236.827L256.758 236.631L298.766 213.247C270.411 197.272 241.923 181.535 213.304 166.038C211.169 164.868 197.408 156.998 196.423 156.848C190.193 159.755 181.151 165.198 175.053 168.664C168.503 172.387 160.559 176.453 154.263 180.297ZM205.322 290.281C218.163 296.796 231.119 305.217 244.055 311.777C245.174 312.344 248.784 314.629 249.64 314.831L249.775 314.606C249.458 292.528 249.877 270.293 249.703 248.201C246.126 245.983 242.189 243.794 238.461 241.809C227.307 235.869 216.402 229.26 205.299 223.263C205.006 227.931 205.112 233.471 205.11 238.199L205.116 261.402V279.26C205.116 282.575 204.992 287.069 205.322 290.281ZM310.635 154.311C295.545 163.041 279.46 170.94 264.327 179.697C280.332 188.197 296.226 196.904 312.004 205.818C323.247 199.123 335.56 193.092 346.913 186.457C350.417 184.409 354.86 182.309 358.172 180.244C348.009 174.683 337.886 169.048 327.806 163.34C325.333 161.948 312.234 154.101 310.635 154.311ZM192.589 216.174C178.028 208.379 163.807 200.048 149.351 192.146C148.996 196.508 148.964 201.707 149.089 206.108C149.365 215.792 148.628 226.093 149.216 235.699C158.022 240.22 167.205 245.577 175.876 250.45C180.768 253.199 188.104 257.065 192.615 260.036C192.627 245.657 192.88 230.485 192.589 216.174ZM209.675 149.446C223.394 156.69 236.878 164.623 250.585 171.91C250.983 172.121 251.326 172.095 251.736 172.023C262.255 166.029 273.22 160.434 283.835 154.585C288.527 151.999 293.67 149.502 298.26 146.823C294.509 144.753 256.796 123.645 256.03 123.58C253.183 124.865 249.867 126.89 247.106 128.461C243.021 130.784 238.92 133.08 234.804 135.349L218.972 144.081C216.14 145.646 212.258 147.679 209.675 149.446ZM205.227 347.835C210.75 350.614 217.104 354.385 222.558 357.447L249.774 372.776C249.992 368.474 249.714 362.059 249.704 357.562C249.685 348.207 249.846 338.65 249.7 329.324C239.992 324.023 230.316 318.666 220.671 313.251C216.102 310.696 209.836 306.899 205.29 304.683C204.965 307.466 205.084 311.234 205.114 314.078C205.23 325.298 204.913 336.625 205.227 347.835ZM192.599 340.676C192.641 318.921 192.983 295.888 192.55 274.235C178.363 267.042 163.375 257.721 149.136 250.06L149.087 292.454C149.088 299.63 148.813 309.464 149.241 316.464C156.553 320.222 164.35 324.78 171.538 328.846C178.58 332.751 185.6 336.694 192.599 340.676ZM362.869 254.606L334.271 270.626C329.819 273.123 323.818 276.261 319.602 278.889C319.658 283.878 319.422 339.653 319.947 340.413L344.367 326.739C349.619 323.744 357.736 318.812 363.003 316.473C362.9 310.789 363.06 254.952 362.869 254.606ZM306.886 285.909C293.902 293.017 281.103 300.587 268.081 307.646C266.209 308.661 264.16 309.756 262.383 310.895L262.382 351.968C262.383 357.392 262.066 367.309 262.433 372.297L262.741 372.429L307.106 347.74C306.682 345.098 306.893 333.849 306.894 330.443L306.919 294.04C306.921 292.916 307.104 286.442 306.886 285.909Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M255.618 109.018C257.055 108.86 261.937 111.967 263.523 112.86L275.433 119.513L313.792 140.951L355.839 164.515C362.115 168.044 369.665 171.995 375.658 175.714L375.693 323.705C364.077 330.668 350.632 337.812 338.755 344.45C311.289 359.735 283.906 375.164 256.603 390.738C255.219 390.968 147.135 329.589 136.553 323.824C136.766 307.58 136.584 290.906 136.591 274.632L136.565 175.469L255.618 109.018ZM262.362 248.348C262.371 252.797 262.198 295.619 262.58 296.117L331.071 257.876L353.137 245.528C356.16 243.815 360.172 241.698 363.014 239.904C362.541 228.029 363.188 215.211 362.942 203.26C362.912 201.762 363.118 193.293 362.856 192.48L362.635 192.322C341.659 203.458 319.904 216.135 299.041 227.748L274.501 241.393C271.177 243.252 265.383 246.274 262.362 248.348ZM154.263 180.297C160.313 183.877 167.39 187.476 173.656 191.002C191.779 201.263 209.959 211.423 228.196 221.481L247.774 232.36C249.157 233.127 255.094 236.521 256.155 236.827L256.758 236.631L298.766 213.247C270.411 197.272 241.923 181.535 213.304 166.038C211.169 164.868 197.408 156.998 196.423 156.848C190.193 159.755 181.151 165.198 175.053 168.664C168.503 172.387 160.559 176.453 154.263 180.297ZM205.322 290.281C218.163 296.796 231.119 305.217 244.055 311.777C245.174 312.344 248.784 314.629 249.64 314.831L249.775 314.606C249.458 292.528 249.877 270.293 249.703 248.201C246.126 245.983 242.189 243.794 238.461 241.809C227.307 235.869 216.402 229.26 205.299 223.263C205.006 227.931 205.112 233.471 205.11 238.199L205.116 261.402V279.26C205.116 282.575 204.992 287.069 205.322 290.281ZM310.635 154.311C295.545 163.041 279.46 170.94 264.327 179.697C280.332 188.197 296.226 196.904 312.004 205.818C323.247 199.123 335.56 193.092 346.913 186.457C350.417 184.409 354.86 182.309 358.172 180.244C348.009 174.683 337.886 169.048 327.806 163.34C325.333 161.948 312.234 154.101 310.635 154.311ZM192.589 216.174C178.028 208.379 163.807 200.048 149.351 192.146C148.996 196.508 148.964 201.707 149.089 206.108C149.365 215.792 148.628 226.093 149.216 235.699C158.022 240.22 167.205 245.577 175.876 250.45C180.768 253.199 188.104 257.065 192.615 260.036C192.627 245.657 192.88 230.485 192.589 216.174ZM209.675 149.446C223.394 156.69 236.878 164.623 250.585 171.91C250.983 172.121 251.326 172.095 251.736 172.023C262.255 166.029 273.22 160.434 283.835 154.585C288.527 151.999 293.67 149.502 298.26 146.823C294.509 144.753 256.796 123.645 256.03 123.58C253.183 124.865 249.867 126.89 247.106 128.461C243.021 130.784 238.92 133.08 234.804 135.349L218.972 144.081C216.14 145.646 212.258 147.679 209.675 149.446ZM205.227 347.835C210.75 350.614 217.104 354.385 222.558 357.447L249.774 372.776C249.992 368.474 249.714 362.059 249.704 357.562C249.685 348.207 249.846 338.65 249.7 329.324C239.992 324.023 230.316 318.666 220.671 313.251C216.102 310.696 209.836 306.899 205.29 304.683C204.965 307.466 205.084 311.234 205.114 314.078C205.23 325.298 204.913 336.625 205.227 347.835ZM192.599 340.676C192.641 318.921 192.983 295.888 192.55 274.235C178.363 267.042 163.375 257.721 149.136 250.06L149.087 292.454C149.088 299.63 148.813 309.464 149.241 316.464C156.553 320.222 164.35 324.78 171.538 328.846C178.58 332.751 185.6 336.694 192.599 340.676ZM362.869 254.606L334.271 270.626C329.819 273.123 323.818 276.261 319.602 278.889C319.658 283.878 319.422 339.653 319.947 340.413L344.367 326.739C349.619 323.744 357.736 318.812 363.003 316.473C362.9 310.789 363.06 254.952 362.869 254.606ZM306.886 285.909C293.902 293.017 281.103 300.587 268.081 307.646C266.209 308.661 264.16 309.756 262.383 310.895L262.382 351.968C262.383 357.392 262.066 367.309 262.433 372.297L262.741 372.429L307.106 347.74C306.682 345.098 306.893 333.849 306.894 330.443L306.919 294.04C306.921 292.916 307.104 286.442 306.886 285.909Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
src/assets/programming.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,27 @@
---
import {
Dropdown,
DropdownTrigger,
DropdownContent,
DropdownItem,
DropdownLabel,
DropdownSeparator
} from "@/components/starwind/dropdown";
import { Button } from "@/components/starwind/button";
import Hamburger from "@tabler/icons/outline/menu-2.svg";
---
<Dropdown >
<DropdownTrigger asChild>
<DropdownItem>
<Button variant="outline"><Hamburger /></Button>
</DropdownItem>
</DropdownTrigger>
<DropdownContent align="end">
<DropdownLabel>Aplicaciones</DropdownLabel>
<DropdownSeparator />
<DropdownItem as="a" href="https://handy.resuely.com" target="_blank" class="cursor-pointer"
>Handy</DropdownItem
>
</DropdownContent>
</Dropdown>

View File

@@ -0,0 +1,154 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import { Button } from "@/components/starwind/button";
import { Card, CardContent } from "@/components/starwind/card";
import { Input } from "@/components/starwind/input";
import { Label } from "@/components/starwind/label";
import { Textarea } from "@/components/starwind/textarea";
interface Props extends HTMLAttributes<"section"> {
badge: string;
heading: string;
description: string;
}
const sectionStyles = tv({
base: "@container mx-auto w-full max-w-3xl px-4 py-24",
});
const {
badge,
heading,
description,
class: className,
...rest
} = Astro.props;
---
<section class={sectionStyles({ class: className })} {...rest}>
<div class="text-center">
<span
class="bg-foreground/10 text-foreground inline-block rounded-full px-4 py-1.5 text-sm font-medium"
>
{badge}
</span>
<h2 class="font-heading mt-6 text-3xl font-bold tracking-tight @xl:text-4xl @3xl:text-5xl">
{heading}
</h2>
<p class="text-muted-foreground mx-auto mt-4 max-w-xl text-lg">
{description}
</p>
</div>
<Card class="mt-10">
<CardContent>
<form
class="space-y-6"
id="contact-form-02"
name="contact-form"
method="post"
action="/api/contact"
>
<div class="grid gap-6 @xl:grid-cols-2">
<div class="flex flex-col gap-1">
<Label for="contact-name-02">Nombre</Label>
<Input id="contact-name-02" name="name" type="text" placeholder="Tu nombre" required />
</div>
<div class="flex flex-col gap-1">
<Label for="contact-email-02">Correo</Label>
<Input
id="contact-email-02"
name="email"
type="email"
placeholder="tu@correo.com"
required
/>
</div>
</div>
<div class="flex flex-col gap-1">
<Label for="contact-subject-02">Asunto</Label>
<Input
id="contact-subject-02"
name="subject"
type="text"
placeholder="¿En qué te ayudamos?"
required
/>
</div>
<div class="flex flex-col gap-1">
<Label for="contact-message-02">Mensaje</Label>
<Textarea
id="contact-message-02"
name="message"
placeholder="Cuéntanos un poco más..."
rows={5}
required
/>
</div>
<Button variant="default" type="submit" class="w-full @xl:w-auto">Enviar mensaje</Button>
</form>
</CardContent>
</Card>
<p class="text-muted-foreground mt-6 text-center text-sm" aria-live="polite" data-contact-status>
Te respondemos por correo lo antes posible.
</p>
</section>
<script>
function handleFormSubmit() {
const form = document.querySelector("#contact-form-02") as HTMLFormElement;
const status = document.querySelector("[data-contact-status]") as HTMLElement | null;
const updateStatus = (nextText: string) => {
if (status) status.textContent = nextText;
};
if (form) {
form.addEventListener("submit", async (e) => {
e.preventDefault();
updateStatus("Enviando...");
try {
const formData = new FormData(form);
const response = await fetch(form.action, {
method: "POST",
body: formData,
headers: {
"Accept": "application/json",
},
});
const contentType = response.headers.get("content-type") ?? "";
const body: unknown = contentType.includes("application/json")
? await response.json()
: await response.text();
if (!response.ok) {
const message =
typeof body === "object" && body !== null && "error" in body
? "No se pudo enviar. Intenta de nuevo en un rato."
: "No se pudo enviar. Intenta de nuevo en un rato.";
updateStatus(message);
return;
}
updateStatus("Listo. Recibimos tu mensaje. Gracias.");
form.reset();
} catch {
updateStatus("No se pudo enviar. Revisa tu conexión e inténtalo de nuevo.");
}
});
}
}
handleFormSubmit();
document.addEventListener("astro:after-swap", handleFormSubmit);
</script>

View File

@@ -0,0 +1,9 @@
---
import Contact2 from "./Contact2.astro";
---
<Contact2
badge="Get in Touch"
heading="We'd Love to Hear From You"
description="Have a question about our services? Need a quote? Drop us a message and our team will respond promptly."
/>

View File

@@ -0,0 +1,86 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/starwind/accordion";
export interface FAQItem {
question: string;
answer: string;
}
interface Props extends HTMLAttributes<"section"> {
title?: string;
description?: string;
items?: FAQItem[];
}
const faqStyles = tv({
base: "@container mx-auto w-full max-w-4xl px-4 py-24",
});
const faqs = [
{
question: "¿Qué es Resuely?",
answer:
"Resuely es una marca paraguas de aplicaciones web gratuitas diseñadas para usuarios venezolanos. Nuestra misión es proporcionar herramientas útiles sin costo alguno, sin anuncios intrusivos y sin necesidad de registro.",
},
{
question: "¿Son realmente gratuitas las apps?",
answer:
"Sí, absolutamente todas nuestras aplicaciones son 100% gratuitas. No cobramos por usar ninguna de nuestras herramientas, no tienes que registrarte para acceder a ellas, y no mostramos anuncios mientras las usas.",
},
{
question: "¿Necesito registrarme?",
answer:
"No, no necesitas crear una cuenta ni registrarte para usar ninguna de nuestras aplicaciones. Creemos que tu privacidad es importante y que deberías poder usar herramientas útiles sin compartir tus datos personales.",
},
{
question: "¿Dónde están alojados mis datos?",
answer:
"Todas nuestras aplicaciones funcionan entirely en tu navegador. Esto significa que ningún dato se envía a nuestros servidores ni se almacena externamente. Tu información permanece en tu dispositivo y desaparece cuando cierras la aplicación.",
},
{
question: "¿Tienen más apps en desarrollo?",
answer:
"Sí, estamos trabajando constantemente en nuevas herramientas. Puedes seguir nuestras actualizaciones en nuestras redes sociales.",
},
{
question: "¿Cómo puedo sugerir una nueva herramienta?",
answer:
"Nos encanta recibir sugerencias. Puedes contactarnos a través de nuestras redes sociales o correo electrónico.",
},
];
const {
title = "Preguntas frecuentes",
description = "Todo lo que necesitas saber sobre Resuely y nuestras herramientas gratuitas.",
items = faqs,
class: className,
...rest
} = Astro.props;
---
<section class={faqStyles({ class: className })} {...rest}>
<div class="mb-12 text-center">
<h2 class="font-heading mb-4 text-3xl font-semibold tracking-tight text-foreground @xl:text-4xl @3xl:text-5xl">
{title}
</h2>
<p class="text-muted-foreground mx-auto max-w-2xl text-pretty @lg:text-lg">
{description}
</p>
</div>
<Accordion type="single" defaultValue="item-1">
{items.map((item, index) => (
<AccordionItem value={`item-${index + 1}`}>
<AccordionTrigger>{item.question}</AccordionTrigger>
<AccordionContent>{item.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</section>

View File

@@ -0,0 +1,66 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/starwind/accordion";
import { Image } from "@/components/starwind/image"
import type { ImageMetadata } from "astro";
import FaqImage from "@/assets/faq.jpg"
export interface FaqItem {
title: string;
content: string;
}
interface Props extends HTMLAttributes<"section"> {
heading: string;
description: string;
items: FaqItem[];
image?: ImageMetadata;
}
const faq4Styles = tv({
base: "@container mx-auto w-full max-w-5xl px-4 pt-24",
});
const {
heading,
description,
items = [],
image = FaqImage,
class: className,
...rest
} = Astro.props;
---
<section class={faq4Styles({ class: className })} {...rest}>
<div class="grid gap-8 @2xl:grid-cols-2 @2xl:items-start">
<div class="flex flex-col @2xl:items-start">
<h2 class="font-heading text-center text-4xl font-semibold @2xl:text-left">{heading}</h2>
<p class="text-muted-foreground mt-2 text-center @2xl:text-left">
{description}
</p>
<Accordion defaultValue="item-1" class="mt-6 space-y-2">
{
items.map((item, idx) => (
<AccordionItem value={`item-${idx + 1}`} class="bg-card rounded-2xl border border-border last:border">
<AccordionTrigger class="[&_svg]:bg-foreground [&_svg]:text-primary-foreground px-4 [&_svg]:size-7 [&_svg]:rounded-full [&_svg]:p-1">
{item.title}
</AccordionTrigger>
<AccordionContent class="text-muted-foreground px-4">{item.content}</AccordionContent>
</AccordionItem>
))
}
</Accordion>
</div>
<div class="bg-muted relative aspect-4/3 w-full overflow-hidden rounded-xl border border-muted">
<Image src={image} alt="FAQ" loading="lazy" />
<!-- <img src={image} alt="" class="h-full w-full object-cover" loading="lazy" /> -->
</div>
</div>
</section>

View File

@@ -1,84 +0,0 @@
---
import IconCheck from "@tabler/icons/outline/check.svg";
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import { Image } from "@/components/starwind/image";
export interface FeatureItem {
text: string;
}
interface Props extends HTMLAttributes<"section"> {
/** Section title */
title?: string;
/** Section description */
description?: string;
/** List of features */
features?: FeatureItem[];
/** Image configuration */
image?: {
src: string;
alt: string;
};
}
const feature2Styles = tv({
base: "@container mx-auto w-full max-w-7xl px-4 py-24",
});
const {
title = "Everything you need to succeed",
description = "Our platform provides all the tools and features you need to streamline your workflow and boost productivity.",
features = [
{ text: "Real-time collaboration with your team" },
{ text: "Advanced analytics and reporting" },
{ text: "Seamless integrations with your favorite tools" },
{ text: "Enterprise-grade security and compliance" },
{ text: "24/7 customer support" },
],
image = {
src: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop",
alt: "Dashboard analytics",
},
class: className,
...rest
} = Astro.props;
---
<section class={feature2Styles({ class: className })} {...rest}>
<div class="grid gap-12 @lg:grid-cols-2 @lg:gap-16">
<!-- Left Content -->
<div class="flex flex-col justify-center">
<h2
class="font-heading mb-4 text-3xl font-semibold tracking-tight @xl:text-4xl @3xl:text-5xl"
>
{title}
</h2>
<p class="text-muted-foreground mb-8 text-pretty @lg:text-lg">
{description}
</p>
<!-- Features List -->
<ul class="space-y-4">
{
features.map((feature) => (
<li class="flex items-start gap-3">
<div class="bg-primary-accent/10 flex size-8 shrink-0 items-center justify-center rounded-full">
<IconCheck class="text-primary-accent size-4" aria-hidden="true" />
</div>
<span class="mt-1.5 text-sm font-medium">{feature.text}</span>
</li>
))
}
</ul>
</div>
<!-- Right Image -->
<div class="order-first @lg:order-last">
<div class="overflow-hidden rounded-2xl border">
<Image src={image.src} alt={image.alt} class="size-full object-cover" loading="lazy" />
</div>
</div>
</div>
</section>

View File

@@ -1,21 +0,0 @@
---
import Feature2, { type FeatureItem } from "./Feature2.astro";
const features: FeatureItem[] = [
{ text: "Real-time collaboration with your team" },
{ text: "Advanced analytics and reporting" },
{ text: "Seamless integrations with your favorite tools" },
{ text: "Enterprise-grade security and compliance" },
{ text: "24/7 customer support" },
];
---
<Feature2
title="Everything you need to succeed"
description="Our platform provides all the tools and features you need to streamline your workflow and boost productivity."
features={features}
image={{
src: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop",
alt: "Dashboard analytics",
}}
/>

View File

@@ -0,0 +1,83 @@
---
import IconCalculator from "@tabler/icons/outline/calculator.svg";
import IconCurrencyDollar from "@tabler/icons/outline/currency-dollar.svg";
import IconPercentage from "@tabler/icons/outline/circle-percentage.svg";
import IconClock from "@tabler/icons/outline/clock-2.svg";
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
export interface FeatureCard {
icon: any;
title: string;
description: string;
}
interface Props extends HTMLAttributes<"section"> {
/** Section title */
title?: string;
/** Section description */
description?: string;
/** Array of feature cards */
features?: FeatureCard[];
}
const feature6Styles = tv({
base: "@container mx-auto w-full max-w-7xl px-4 py-24",
});
const {
title = "Herramientas disponibles",
description = "Apps web gratuitas diseñadas para ayudarte en tu día a día. Sin registro, sin anuncios, sin complicaciones.",
features = [
{
icon: IconCalculator,
title: "Calculadora",
description: "Realiza cálculos rápidos y precisos con nuestra calculadora online.",
},
{
icon: IconCurrencyDollar,
title: "Conversor de divisas",
description: "Convierte entre diferentes monedas con tipos de cambio actualizados.",
},
{
icon: IconPercentage,
title: "Calculadora de porcentajes",
description: "Calcula porcentajes, incrementos y descuentos de forma sencilla.",
},
{
icon: IconClock,
title: "Zonas horarias",
description: "Convierte horarios entre diferentes zonas horarias del mundo.",
},
],
class: className,
...rest
} = Astro.props;
---
<section class={feature6Styles({ class: className })} {...rest}>
<div class="mb-12 text-center">
<h2 class="font-heading mb-4 text-3xl font-semibold tracking-tight text-foreground @xl:text-4xl @3xl:text-5xl">
{title}
</h2>
<p class="text-muted-foreground mx-auto max-w-2xl text-pretty @lg:text-lg">
{description}
</p>
</div>
<div class="grid gap-12 @lg:grid-cols-2 @3xl:grid-cols-4 @3xl:gap-16">
{
features.map((feature) => (
<div class="text-center">
<div class="bg-muted mx-auto mb-4 inline-flex rounded-2xl p-4">
<feature.icon class="text-foreground size-8" aria-hidden="true" />
</div>
<h3 class="font-heading mb-2 text-lg font-semibold">{feature.title}</h3>
<p class="text-muted-foreground mx-auto max-w-[300px] text-sm text-pretty">
{feature.description}
</p>
</div>
))
}
</div>
</section>

View File

@@ -0,0 +1,61 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import GitIcon from "@tabler/icons/outline/brand-git.svg";
interface Props extends HTMLAttributes<"footer"> {
siteName?: string;
description?: string;
copyright?: string;
socialLinks?: {
label: string;
href: string;
icon: string;
}[];
}
const footerStyles = tv({
base: "border-t border-border/50 bg-muted/20",
});
const {
siteName = "Resuely",
description = "Apps web gratuitas y simples para el día a día.",
copyright = `© ${new Date().getFullYear()} ${siteName}. Todos los derechos reservados.`,
socialLinks = [
{ label: "Código", href: "https://git.rlugo.dev/resuely", icon: "icon-git" },
],
class: className,
...rest
} = Astro.props;
---
<footer class={footerStyles({ class: className })} {...rest}>
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<div class="text-center sm:text-left">
<p class="text-sm font-semibold text-foreground">{siteName}</p>
<p class="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
<div class="flex items-center gap-4">
{socialLinks.map((link) => (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
class="text-muted-foreground hover:text-foreground transition-colors rounded-md p-2 hover:bg-background/60"
>
<span class="sr-only">{link.label}</span>
{link.icon === "icon-git" && <GitIcon class="size-5" />}
</a>
))}
</div>
</div>
<div class="mt-8 border-t border-border/50 pt-8 text-center">
<p class="text-sm text-muted-foreground">{copyright}</p>
</div>
</div>
</footer>

View File

@@ -0,0 +1,49 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import { Button } from "@/components/starwind/button";
interface Props extends HTMLAttributes<"section"> {
headline?: string;
subheadline?: string;
primaryCtaText?: string;
primaryCtaHref?: string;
secondaryCtaText?: string;
secondaryCtaHref?: string;
}
const heroStyles = tv({
base: "relative overflow-hidden py-20 md:py-32 lg:py-40",
});
const {
headline = "Herramientas gratuitas para tu día a día",
subheadline = "Resuely es una marca paraguas de aplicaciones web gratuitas diseñadas para usuarios venezolanos",
primaryCtaText = "Explorar apps",
primaryCtaHref = "/#apps",
secondaryCtaText = "Saber más",
secondaryCtaHref = "/#about",
class: className,
...rest
} = Astro.props;
---
<section class={heroStyles({ class: className })} {...rest}>
<div class="mx-auto max-w-7xl px-4 text-center">
<h1 class="font-heading text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl">
{headline}
</h1>
<p class="mt-6 text-lg text-muted-foreground md:text-xl max-w-2xl mx-auto">
{subheadline}
</p>
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<Button href={primaryCtaHref} size="lg">
{primaryCtaText}
</Button>
<Button href={secondaryCtaHref} variant="outline" size="lg">
{secondaryCtaText}
</Button>
</div>
</div>
</section>

View File

@@ -0,0 +1,174 @@
---
import IconArrowRight from "@tabler/icons/outline/arrow-right.svg";
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import { Button } from "@/components/starwind/button";
import { Carousel, CarouselContent, CarouselItem } from "@/components/starwind/carousel";
export interface CarouselImage {
src: string;
alt: string;
}
interface Props extends HTMLAttributes<"section"> {
heading?: string;
description?: string;
primaryButtonText?: string;
primaryButtonHref?: string;
secondaryButtonText?: string;
secondaryButtonHref?: string;
carouselImages?: CarouselImage[];
}
const hero9Styles = tv({
base: "dark bg-background text-foreground @container relative h-full max-h-[70vh] w-full overflow-hidden",
});
const {
heading,
description,
primaryButtonText,
primaryButtonHref,
secondaryButtonText,
secondaryButtonHref,
carouselImages = [],
class: className,
...rest
} = Astro.props;
---
<section class={hero9Styles({ class: className })} {...rest}>
<Carousel id="hero9-carousel" autoInit={false} opts={{ loop: true }} class="relative min-h-full">
<CarouselContent>
{
carouselImages.map((image) => (
<CarouselItem>
{/* you may need to adjust the "min-h-[500px]" class here depending on your content overlay size */}
<img src={image.src} alt={image.alt} class="h-full min-h-[500px] w-full object-cover object-center brightness-50" />
</CarouselItem>
))
}
</CarouselContent>
<div
class="absolute inset-0 h-full w-full bg-linear-to-r from-black/70 to-black/50 @2xl:to-black/40"
>
</div>
</Carousel>
<!-- Content Overlay -->
<div class="absolute inset-0 flex items-center">
<div class="relative mx-auto w-full max-w-7xl px-4 @md:pr-10 @lg:px-8">
<div class="max-w-2xl">
<!-- Heading -->
<h1
class="text-foreground font-heading text-4xl font-semibold tracking-tight text-pretty @xl:text-5xl @4xl:text-6xl"
>
{heading}
</h1>
<!-- Description -->
<p class="text-foreground/90 mt-6 text-pretty @xl:text-lg @4xl:text-xl">
{description}
</p>
<!-- Buttons -->
<div class="mt-8 flex flex-col gap-4 @sm:flex-row">
<Button variant="default" class="rounded-full" href={primaryButtonHref}
>{primaryButtonText}</Button
>
{
secondaryButtonText && (
<Button variant="outline" class="group rounded-full" href={secondaryButtonHref}>
{secondaryButtonText}
<IconArrowRight class="transition-transform group-hover:translate-x-1" />
</Button>
)
}
</div>
</div>
</div>
</div>
<!-- Pagination Dots (Right Side) -->
<div class="absolute top-1/2 right-8 -translate-y-1/2">
<div id="hero9-dots" class="hidden flex-col items-center gap-3 @2xl:flex">
{
carouselImages.map((_, index) => (
<button
type="button"
data-index={index}
class="carousel-dot size-2.5 shrink-0 rounded-full bg-white/40 transition-all hover:bg-white/60"
aria-label={`Go to slide ${index + 1}`}
/>
))
}
</div>
</div>
</section>
<style>
.carousel-dot.active {
background: white;
height: calc(var(--spacing) * 5);
}
</style>
<script>
import Autoplay from "embla-carousel-autoplay";
import { initCarousel } from "@/components/starwind/carousel";
function initHero9Carousel() {
const carouselElement = document.getElementById("hero9-carousel");
if (!carouselElement) return;
const dotsContainer = document.getElementById("hero9-dots");
if (!dotsContainer) return;
const dots = dotsContainer.querySelectorAll<HTMLButtonElement>(".carousel-dot");
// Initialize carousel with autoplay
const carouselManager = initCarousel(carouselElement, {
opts: { loop: true },
plugins: [
Autoplay({
delay: 5000,
stopOnInteraction: true,
}),
],
setApi: (api) => {
// Update dots on slide change
const updateDots = () => {
const selectedIndex = api.selectedScrollSnap();
dots.forEach((dot, index) => {
if (index === selectedIndex) {
dot.classList.add("active");
} else {
dot.classList.remove("active");
}
});
};
// Initial update
updateDots();
// Listen for slide changes
api.on("select", updateDots);
// Add click handlers to dots
dots.forEach((dot, index) => {
dot.addEventListener("click", () => {
api.scrollTo(index);
});
});
},
});
}
// Initialize on page load
initHero9Carousel();
// Reinitialize on Astro page transitions
document.addEventListener("astro:after-swap", initHero9Carousel);
</script>

View File

@@ -0,0 +1,111 @@
---
import IconArrowRight from "@tabler/icons/outline/arrow-right.svg";
import IconChevronLeft from "@tabler/icons/outline/chevron-left.svg";
import IconChevronRight from "@tabler/icons/outline/chevron-right.svg";
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
import { Button } from "@/components/starwind/button";
import { Image } from "@/components/starwind/image";
import type { ImageMetadata } from "astro";
/** Service item configuration */
export interface Service {
image: ImageMetadata;
title: string;
tagline: string;
href?: string;
}
interface Props extends HTMLAttributes<"section"> {
badge?: string;
heading?: string;
description?: string;
services?: Service[];
ctaText?: string;
ctaHref?: string;
}
const sectionStyles = tv({
base: "@container w-full pt-24",
});
const {
badge,
heading,
description,
services = [],
ctaText,
ctaHref = "#",
class: className,
...rest
} = Astro.props;
---
<section class={sectionStyles({ class: className })} {...rest}>
<!-- Header -->
<div class="mx-auto mb-12 max-w-7xl px-4">
<div class="flex flex-col gap-6 @2xl:flex-row @2xl:items-end @2xl:justify-between">
<div class="max-w-2xl">
<span class="text-foreground text-sm font-semibold tracking-widest uppercase">
{badge}
</span>
<h2
class="font-heading mt-3 text-3xl font-semibold tracking-tight @xl:text-4xl @3xl:text-5xl"
>
{heading}
</h2>
<p class="text-muted-foreground mt-4 text-pretty @xl:text-lg">
{description}
</p>
</div>
{
ctaText && (
<Button variant="outline" href={ctaHref} class="group w-fit shrink-0">
{ctaText}
<IconArrowRight class="size-4 transition-transform group-hover:translate-x-1" />
</Button>
)
}
</div>
</div>
<!-- Scrolling Cards -->
<div class="relative mx-auto mb-12 max-w-7xl">
<div
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 overflow-x-auto scroll-smooth px-4 pb-4"
>
{
services.map((service, index) => (
<a
href={service.href || undefined}
target="_blank"
class="group relative shrink-0 overflow-hidden rounded-2xl first:ml-0 w-full"
style={`--index: ${index}`}
>
<Image
src={service.image}
alt={service.title}
class="size-full object-cover transition-transform duration-700 group-hover:scale-110 brightness-75"
loading="lazy"
/>
<div class="absolute inset-0 bg-linear-to-t from-black/80 via-black/20 to-transparent" />
<div class="absolute inset-x-0 bottom-0 p-6 @xl:p-8">
<p class="text-sm font-medium text-white/70">{service.tagline}</p>
<h3 class="font-heading mt-2 text-xl font-semibold text-white @xl:text-2xl">
{service.title}
</h3>
{service.href &&
<div class="mt-4 flex items-center gap-2 text-sm font-medium text-white opacity-0 transition-all duration-300 group-hover:opacity-100">
<span>{service.href ? "visitar" : ""}</span>
<IconArrowRight class="size-4 transition-transform group-hover:translate-x-1" />
</div>}
{!service.href && <div class="mt-4 h-6 w-full" />}
</div>
</a>
))
}
</div>
</div>
</section>

View File

@@ -0,0 +1,254 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div"> & {
/**
* The type of accordion. If "single", only one item can be open at a time.
*/
type?: "single" | "multiple";
/**
* The value of the item that should be open by default
*/
defaultValue?: string;
};
export const accordion = tv({ base: "starwind-accordion" });
const { type = "single", defaultValue, class: className, ...rest } = Astro.props;
---
<div
class={accordion({ class: className })}
data-type={type}
data-value={defaultValue}
data-slot="accordion"
{...rest}
>
<slot />
</div>
<script>
type AccordionType = "single" | "multiple";
type AccordionState = "open" | "closed";
/** Represents a single accordion item with its associated elements */
interface AccordionItem {
element: HTMLElement;
trigger: HTMLElement;
content: HTMLElement;
value: string;
}
/**
* Handles the functionality of an accordion component.
* Supports single and multiple open items, keyboard navigation,
* and maintains ARIA accessibility standards.
*/
class AccordionHandler {
private accordion: HTMLElement;
private type: AccordionType;
private items: AccordionItem[];
private itemsByValue: Map<string, AccordionItem>;
private accordionId: string;
private isInitialized: boolean = false;
/**
* Creates a new AccordionHandler instance
* @param accordion - The root accordion element
* @param idx - Unique index for this accordion instance
*/
constructor(accordion: HTMLElement, idx: number) {
this.accordion = accordion;
this.type = (accordion.dataset.type || "single") as AccordionType;
this.accordionId = `starwind-accordion${idx}`;
// Cache all items and create lookup maps
this.items = this.initializeItems();
this.itemsByValue = new Map(this.items.map((item) => [item.value, item]));
this.setupItems();
this.setInitialState();
this.isInitialized = true;
}
/**
* Initializes accordion items by querying the DOM and setting up data structures
* @returns Array of AccordionItem objects
*/
private initializeItems(): AccordionItem[] {
return Array.from(this.accordion.querySelectorAll<HTMLElement>(".starwind-accordion-item"))
.map((element, idx) => {
const trigger = element.querySelector<HTMLElement>(".starwind-accordion-trigger");
const content = element.querySelector<HTMLElement>(".starwind-accordion-content");
const value = element.getAttribute("data-value") || String(idx);
if (!trigger || !content) return null;
return { element, trigger, content, value };
})
.filter((item): item is AccordionItem => item !== null);
}
/**
* Sets up initial state and event listeners for all accordion items
*/
private setupItems(): void {
this.items.forEach((item, idx) => {
this.setupAccessibility(item, idx);
this.setContentHeight(item.content);
this.setupEventListeners(item);
});
}
/**
* Sets up ARIA attributes and IDs for accessibility
* @param item - The accordion item to setup
* @param idx - Index of the item
*/
private setupAccessibility(item: AccordionItem, idx: number): void {
const triggerId = `${this.accordionId}-t${idx}`;
const contentId = `${this.accordionId}-c${idx}`;
item.trigger.id = triggerId;
item.trigger.setAttribute("aria-controls", contentId);
item.trigger.setAttribute("aria-expanded", "false");
item.content.id = contentId;
item.content.setAttribute("aria-labelledby", triggerId);
item.content.setAttribute("role", "region");
}
/**
* Calculates and sets the content height CSS variable for animations
* @param content - The content element to measure
*/
private setContentHeight(content: HTMLElement): void {
const contentInner = content.firstElementChild as HTMLElement;
if (contentInner) {
const height = contentInner.getBoundingClientRect().height;
content.style.setProperty("--starwind-accordion-content-height", `${height}px`);
}
}
/**
* Sets the initial state based on the default value attribute
*/
private setInitialState(): void {
const defaultValue = this.accordion.dataset.value;
if (defaultValue) {
const item = this.itemsByValue.get(defaultValue);
if (item) {
this.setItemState(item, true);
}
}
}
/**
* Sets up click and keyboard event listeners for an accordion item
* @param item - The accordion item to setup listeners for
*/
private setupEventListeners(item: AccordionItem): void {
item.trigger.addEventListener("click", () => this.handleClick(item));
item.trigger.addEventListener("keydown", (e) => this.handleKeyDown(e, item));
}
/**
* Handles click events on accordion triggers
* @param item - The clicked accordion item
*/
private handleClick(item: AccordionItem): void {
const isOpen = item.element.getAttribute("data-state") === "open";
this.toggleItem(item, !isOpen);
}
/**
* Handles keyboard navigation events
* @param event - The keyboard event
* @param item - The current accordion item
*/
private handleKeyDown(event: KeyboardEvent, item: AccordionItem): void {
const index = this.items.indexOf(item);
const keyActions: Record<string, () => void> = {
ArrowDown: () => this.focusItem(index + 1),
ArrowUp: () => this.focusItem(index - 1),
Home: () => this.focusItem(0),
End: () => this.focusItem(this.items.length - 1),
};
const action = keyActions[event.key];
if (action) {
event.preventDefault();
action();
}
}
/**
* Focuses an accordion item by index with wrapping
* @param index - The target index to focus
*/
private focusItem(index: number): void {
const targetIndex = (index + this.items.length) % this.items.length;
this.items[targetIndex].trigger.focus();
}
/**
* Toggles an accordion item's state
* @param item - The item to toggle
* @param shouldOpen - Whether the item should be opened
*/
private toggleItem(item: AccordionItem, shouldOpen: boolean): void {
if (this.type === "single" && shouldOpen) {
// Close other items if in single mode
this.items.forEach((otherItem) => {
if (otherItem !== item && otherItem.element.getAttribute("data-state") === "open") {
this.setItemState(otherItem, false);
}
});
}
this.setItemState(item, shouldOpen);
}
/**
* Sets the state of an accordion item
* @param item - The item to update
* @param isOpen - Whether the item should be open
*/
private setItemState(item: AccordionItem, isOpen: boolean): void {
const state: AccordionState = isOpen ? "open" : "closed";
// Skip animation during initial setup, enable for subsequent toggles
if (!this.isInitialized) {
item.content.style.setProperty("animation", "none");
} else {
item.content.style.removeProperty("animation");
}
// Set content height variable for animations
this.setContentHeight(item.content);
item.element.setAttribute("data-state", state);
item.content.setAttribute("data-state", state);
item.trigger.setAttribute("data-state", state);
item.trigger.setAttribute("aria-expanded", isOpen.toString());
}
}
// Store instances in a WeakMap to avoid memory leaks
const accordionInstances = new WeakMap<HTMLElement, AccordionHandler>();
let accordionCounter = 0;
const setupAccordions = () => {
document.querySelectorAll<HTMLElement>(".starwind-accordion").forEach((accordion) => {
if (!accordionInstances.has(accordion)) {
accordionInstances.set(accordion, new AccordionHandler(accordion, accordionCounter++));
}
});
};
setupAccordions();
document.addEventListener("astro:after-swap", setupAccordions);
document.addEventListener("starwind:init", setupAccordions);
</script>

View File

@@ -0,0 +1,33 @@
---
/**
* NOTE: style="animation: none;" makes it so the close animation doesn't run on page load
* It is later removed in the Accordion.astro script
*/
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const accordionContent = tv({
base: [
"starwind-accordion-content",
"transform-gpu overflow-hidden",
"data-[state=closed]:animate-accordion-up data-[state=closed]:h-0",
"data-[state=open]:animate-accordion-down",
],
});
const { class: className, ...rest } = Astro.props;
---
<div
class={accordionContent({ class: className })}
data-state="closed"
style="animation: none;"
data-slot="accordion-content"
{...rest}
>
<div class="pt-0 pb-4">
<slot />
</div>
</div>

View File

@@ -0,0 +1,27 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div"> & {
/**
* The value of the item
*/
value: string;
};
export const accordionItem = tv({
base: "starwind-accordion-item border-b last:border-b-0",
});
const { value, class: className, ...rest } = Astro.props;
---
<div
class={accordionItem({ class: className })}
data-value={value}
data-state="closed"
data-slot="accordion-item"
{...rest}
>
<slot />
</div>

View File

@@ -0,0 +1,32 @@
---
import ChevronDown from "@tabler/icons/outline/chevron-down.svg";
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"button">;
export const accordionTrigger = tv({
base: [
"starwind-accordion-trigger",
"flex w-full items-center justify-between gap-4 rounded-md py-4",
"hover:text-muted-foreground text-left font-medium transition-all",
"[&[data-state=open]>svg]:rotate-180",
"focus-visible:border-outline focus-visible:ring-outline/50 outline-none focus-visible:ring-3",
],
});
const { class: className, ...rest } = Astro.props;
---
<button
type="button"
class={accordionTrigger({ class: className })}
data-slot="accordion-trigger"
aria-expanded="false"
{...rest}
>
<slot />
<slot name="icon">
<ChevronDown class="size-5 shrink-0 transition-transform duration-200" />
</slot>
</button>

View File

@@ -0,0 +1,15 @@
import Accordion, { accordion } from "./Accordion.astro";
import AccordionContent, { accordionContent } from "./AccordionContent.astro";
import AccordionItem, { accordionItem } from "./AccordionItem.astro";
import AccordionTrigger, { accordionTrigger } from "./AccordionTrigger.astro";
const AccordionVariants = { accordion, accordionContent, accordionItem, accordionTrigger };
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AccordionVariants };
export default {
Root: Accordion,
Content: AccordionContent,
Item: AccordionItem,
Trigger: AccordionTrigger,
};

View File

@@ -0,0 +1,55 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "tailwind-variants";
interface Props
extends
HTMLAttributes<"button">,
Omit<HTMLAttributes<"a">, "type">,
VariantProps<typeof button> {}
const { variant, size, class: className, ...rest } = Astro.props;
export const button = tv({
base: [
"inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
"transition-all outline-none focus-visible:ring-3",
"disabled:pointer-events-none disabled:opacity-50",
"aria-invalid:border-error aria-invalid:focus-visible:ring-error/40",
],
variants: {
variant: {
default: "bg-foreground text-background hover:bg-foreground/90 focus-visible:ring-outline/50",
primary:
"bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-primary/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/90 focus-visible:ring-secondary/50",
outline:
"dark:border-input focus-visible:ring-outline/50 bg-background dark:bg-input/30 focus-visible:border-outline hover:bg-muted dark:hover:bg-input/50 hover:text-foreground border shadow-xs",
ghost: "hover:bg-muted hover:text-foreground focus-visible:ring-outline/50",
info: "bg-info text-info-foreground hover:bg-info/90 focus-visible:ring-info/50",
success:
"bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/50",
warning:
"bg-warning text-warning-foreground hover:bg-warning/90 focus-visible:ring-warning/50",
error: "bg-error text-error-foreground hover:bg-error/90 focus-visible:ring-error/50",
},
size: {
sm: "h-9 px-4 text-sm has-[>svg]:px-3 [&_svg:not([class*='size-'])]:size-3.5",
md: "h-11 px-5 text-base has-[>svg]:px-4 [&_svg:not([class*='size-'])]:size-4.5",
lg: "h-12 px-8 text-lg has-[>svg]:px-6 [&_svg:not([class*='size-'])]:size-5",
"icon-sm": "size-9 [&_svg:not([class*='size-'])]:size-3.5",
icon: "size-11 [&_svg:not([class*='size-'])]:size-4.5",
"icon-lg": "size-12 [&_svg:not([class*='size-'])]:size-5",
},
},
defaultVariants: { variant: "default", size: "md" },
});
const Tag = Astro.props.href ? "a" : "button";
---
<Tag class={button({ variant, size, class: className })} data-slot="button" {...rest}>
<slot />
</Tag>

View File

@@ -0,0 +1,7 @@
import Button, { button } from "./Button.astro";
const ButtonVariants = { button };
export { Button, ButtonVariants };
export default Button;

View File

@@ -0,0 +1,34 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "tailwind-variants";
type Props = HTMLAttributes<"div"> & VariantProps<typeof card>;
export const card = tv({
base: [
"bg-card text-card-foreground group/card ring-border flex flex-col rounded-xl ring-1",
"has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0",
"*:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
],
variants: {
size: {
default: "gap-6 py-6",
sm: "gap-4 py-4 text-sm",
},
},
defaultVariants: {
size: "default",
},
});
const { class: className, size, ...rest } = Astro.props;
---
<div
class={card({ size, class: className })}
data-slot="card"
data-size={size ?? "default"}
{...rest}
>
<slot />
</div>

View File

@@ -0,0 +1,16 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const cardAction = tv({
base: "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
});
const { class: className, ...rest } = Astro.props;
---
<div class={cardAction({ class: className })} data-slot="card-action" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,16 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const cardContent = tv({
base: "px-6 group-data-[size=sm]/card:px-4",
});
const { class: className, ...rest } = Astro.props;
---
<div class={cardContent({ class: className })} data-slot="card-content" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,16 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const cardDescription = tv({
base: "text-muted-foreground text-base group-data-[size=sm]/card:text-sm",
});
const { class: className, ...rest } = Astro.props;
---
<div class={cardDescription({ class: className })} data-slot="card-description" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,16 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const cardFooter = tv({
base: "bg-muted/50 flex items-center rounded-b-xl border-t p-6 group-data-[size=sm]/card:p-4",
});
const { class: className, ...rest } = Astro.props;
---
<div class={cardFooter({ class: className })} data-slot="card-footer" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,19 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const cardHeader = tv({
base: [
"@container/card-header grid auto-rows-min items-start gap-1 px-6 group-data-[size=sm]/card:px-4",
"has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
],
});
const { class: className, ...rest } = Astro.props;
---
<div class={cardHeader({ class: className })} data-slot="card-header" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,16 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const cardTitle = tv({
base: "font-heading text-xl leading-snug font-medium group-data-[size=sm]/card:text-base",
});
const { class: className, ...rest } = Astro.props;
---
<div class={cardTitle({ class: className })} data-slot="card-title" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,38 @@
import Card, { card } from "./Card.astro";
import CardAction, { cardAction } from "./CardAction.astro";
import CardContent, { cardContent } from "./CardContent.astro";
import CardDescription, { cardDescription } from "./CardDescription.astro";
import CardFooter, { cardFooter } from "./CardFooter.astro";
import CardHeader, { cardHeader } from "./CardHeader.astro";
import CardTitle, { cardTitle } from "./CardTitle.astro";
const CardVariants = {
card,
cardAction,
cardContent,
cardDescription,
cardFooter,
cardHeader,
cardTitle,
};
export {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
CardVariants,
};
export default {
Root: Card,
Header: CardHeader,
Footer: CardFooter,
Title: CardTitle,
Description: CardDescription,
Content: CardContent,
Action: CardAction,
};

View File

@@ -0,0 +1,55 @@
---
import type { HTMLAttributes } from "astro/types";
import { type EmblaOptionsType } from "embla-carousel";
import { tv } from "tailwind-variants";
const carousel = tv({
base: "starwind-carousel group/carousel relative",
});
export interface Props extends HTMLAttributes<"div"> {
orientation?: "horizontal" | "vertical";
opts?: EmblaOptionsType;
autoInit?: boolean;
}
const {
class: className,
orientation = "horizontal",
opts = {},
autoInit = true,
...rest
} = Astro.props;
---
<div
class={carousel({ class: className })}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
data-axis={orientation === "horizontal" ? "x" : "y"}
data-opts={JSON.stringify(opts)}
data-auto-init={autoInit}
{...rest}
>
<slot />
</div>
<script>
import { initCarousel } from "./index";
const setupCarousels = () => {
const carousels = document.querySelectorAll(".starwind-carousel") as NodeListOf<HTMLElement>;
carousels.forEach((carousel) => {
if (carousel.dataset.autoInit === "false") {
return;
}
initCarousel(carousel);
});
};
document.addEventListener("DOMContentLoaded", setupCarousels);
// Re-initialize after Astro page transitions
document.addEventListener("astro:after-swap", setupCarousels);
</script>

View File

@@ -0,0 +1,26 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
const carouselContent = tv({
base: "overflow-hidden",
});
const carouselContainer = tv({
base: [
"flex group-data-[axis=y]/carousel:flex-col",
"group-data-[axis=x]/carousel:-ml-4",
"group-data-[axis=y]/carousel:-mt-4",
],
});
type Props = HTMLAttributes<"div">;
const { class: className = "", ...rest } = Astro.props;
---
<div class={carouselContent()} data-slot="carousel-content" {...rest}>
<div class={carouselContainer({ class: className })} data-slot="carousel-container">
<slot />
</div>
</div>

View File

@@ -0,0 +1,26 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
const carouselItem = tv({
base: [
"min-w-0 shrink-0 grow-0 basis-full",
"group-data-[axis=x]/carousel:pl-4",
"group-data-[axis=y]/carousel:pt-4",
],
});
type Props = HTMLAttributes<"div">;
const { class: className = "", ...rest } = Astro.props;
---
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
class={carouselItem({ class: className })}
{...rest}
>
<slot />
</div>

View File

@@ -0,0 +1,37 @@
---
import ArrowRight from "@tabler/icons/outline/arrow-right.svg";
import type { ComponentProps } from "astro/types";
import { tv } from "tailwind-variants";
import { Button } from "@/components/starwind/button";
export const carouselNext = tv({
base: [
"starwind-carousel-next absolute size-8 rounded-full",
// Horizontal positioning
"group-data-[axis=x]/carousel:top-1/2 group-data-[axis=x]/carousel:-right-12 group-data-[axis=x]/carousel:-translate-y-1/2",
// Vertical positioning
"group-data-[axis=y]/carousel:-bottom-12 group-data-[axis=y]/carousel:left-1/2 group-data-[axis=y]/carousel:-translate-x-1/2 group-data-[axis=y]/carousel:rotate-90",
],
});
type Props = ComponentProps<typeof Button>;
const { class: className = "", variant = "outline", size = "icon", ...rest } = Astro.props;
---
<Button
data-slot="carousel-next"
variant={variant}
size={size}
class={carouselNext({ class: className })}
aria-label="Next slide"
{...rest}
>
<slot name="icon">
<ArrowRight />
</slot>
<slot>
<span class="sr-only">Next slide</span>
</slot>
</Button>

View File

@@ -0,0 +1,37 @@
---
import ArrowLeft from "@tabler/icons/outline/arrow-left.svg";
import type { ComponentProps } from "astro/types";
import { tv } from "tailwind-variants";
import { Button } from "@/components/starwind/button";
export const carouselPrevious = tv({
base: [
"starwind-carousel-previous absolute size-8 rounded-full",
// Horizontal positioning
"group-data-[axis=x]/carousel:top-1/2 group-data-[axis=x]/carousel:-left-12 group-data-[axis=x]/carousel:-translate-y-1/2",
// Vertical positioning
"group-data-[axis=y]/carousel:-top-12 group-data-[axis=y]/carousel:left-1/2 group-data-[axis=y]/carousel:-translate-x-1/2 group-data-[axis=y]/carousel:rotate-90",
],
});
type Props = ComponentProps<typeof Button>;
const { class: className = "", variant = "outline", size = "icon", ...rest } = Astro.props;
---
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
class={carouselPrevious({ class: className })}
aria-label="Previous slide"
{...rest}
>
<slot name="icon">
<ArrowLeft />
</slot>
<slot>
<span class="sr-only">Previous slide</span>
</slot>
</Button>

View File

@@ -0,0 +1,191 @@
import EmblaCarousel, {
type EmblaCarouselType,
type EmblaEventType,
type EmblaOptionsType,
type EmblaPluginType,
} from "embla-carousel";
export type CarouselApi = EmblaCarouselType;
export interface CarouselOptions {
opts?: EmblaOptionsType;
plugins?: EmblaPluginType[];
setApi?: (api: CarouselApi) => void;
}
export interface CarouselManager {
api: CarouselApi;
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: () => boolean;
canScrollNext: () => boolean;
destroy: () => void;
}
export function initCarousel(
carouselElement: HTMLElement,
options: CarouselOptions = {},
): CarouselManager | null {
// don't re-initialize if already initialized
if (carouselElement.dataset.initialized === "true") return null;
carouselElement.dataset.initialized = "true";
if (!carouselElement) {
console.warn("Carousel element not found");
return null;
}
// Find content element - Embla expects the viewport element, not the container
const viewportElement = carouselElement.querySelector(
'[data-slot="carousel-content"]',
) as HTMLElement;
if (!viewportElement) {
console.warn("Carousel content element not found");
return null;
}
// Get configuration from data attributes
const axisData = carouselElement.dataset.axis;
const axis: EmblaOptionsType["axis"] = axisData === "y" ? "y" : "x";
// Safely parse data options
let dataOpts = {};
try {
const optsString = carouselElement.dataset.opts;
if (optsString && optsString !== "undefined" && optsString !== "null") {
dataOpts = JSON.parse(optsString);
}
} catch (e) {
console.warn("Failed to parse carousel opts:", e);
dataOpts = {};
}
// Ensure dataOpts is a valid object
if (!dataOpts || typeof dataOpts !== "object") {
dataOpts = {};
}
// Merge options - ensure we always have a valid object
const emblaOptions: EmblaOptionsType = {
axis,
...dataOpts,
...(options.opts || {}),
};
// Handle plugins - EmblaCarousel expects undefined when no plugins, not empty array
const plugins = options.plugins && options.plugins.length > 0 ? options.plugins : undefined;
// console.log("ID:", carouselElement.id);
// console.log("Plugins:", plugins);
// console.log("Options:", emblaOptions);
// Find navigation buttons
const prevButton = carouselElement.querySelector(
'[data-slot="carousel-previous"]',
) as HTMLButtonElement;
const nextButton = carouselElement.querySelector(
'[data-slot="carousel-next"]',
) as HTMLButtonElement;
// Initialize Embla
let emblaApi: EmblaCarouselType;
if (plugins) {
emblaApi = EmblaCarousel(viewportElement, emblaOptions, plugins);
} else {
emblaApi = EmblaCarousel(viewportElement, emblaOptions);
}
// Update button states
const updateButtons = () => {
const canScrollPrev = emblaApi.canScrollPrev();
const canScrollNext = emblaApi.canScrollNext();
if (prevButton) {
prevButton.disabled = !canScrollPrev;
prevButton.setAttribute("aria-disabled", (!canScrollPrev).toString());
}
if (nextButton) {
nextButton.disabled = !canScrollNext;
nextButton.setAttribute("aria-disabled", (!canScrollNext).toString());
}
};
// Event handlers for cleanup
const prevClickHandler = () => emblaApi.scrollPrev();
const nextClickHandler = () => emblaApi.scrollNext();
const keydownHandler = (event: KeyboardEvent) => {
if (axis === "y") {
// Vertical axis: ArrowUp = previous, ArrowDown = next
if (event.key === "ArrowUp") {
event.preventDefault();
emblaApi.scrollPrev();
} else if (event.key === "ArrowDown") {
event.preventDefault();
emblaApi.scrollNext();
}
} else {
// Horizontal axis (default): ArrowLeft = previous, ArrowRight = next
if (event.key === "ArrowLeft") {
event.preventDefault();
emblaApi.scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
emblaApi.scrollNext();
}
}
};
// Setup event listeners
const setupEventListeners = () => {
// Navigation button listeners
prevButton?.addEventListener("click", prevClickHandler);
nextButton?.addEventListener("click", nextClickHandler);
// Keyboard navigation
carouselElement.addEventListener("keydown", keydownHandler);
};
// Setup user API callback
const setupUserCallbacks = () => {
if (options.setApi) {
options.setApi(emblaApi);
}
};
// Initialize everything
updateButtons();
setupEventListeners();
setupUserCallbacks();
// Setup internal event listeners
emblaApi.on("select", updateButtons);
emblaApi.on("init", () => {
updateButtons();
});
emblaApi.on("reInit", () => {
updateButtons();
});
// Return manager interface
return {
api: emblaApi,
scrollPrev: () => emblaApi.scrollPrev(),
scrollNext: () => emblaApi.scrollNext(),
canScrollPrev: () => emblaApi.canScrollPrev(),
canScrollNext: () => emblaApi.canScrollNext(),
destroy: () => {
// Remove event listeners to prevent memory leaks
if (prevButton) {
prevButton.removeEventListener("click", prevClickHandler);
}
if (nextButton) {
nextButton.removeEventListener("click", nextClickHandler);
}
carouselElement.removeEventListener("keydown", keydownHandler);
// Destroy the Embla instance
emblaApi.destroy();
},
};
}

View File

@@ -0,0 +1,32 @@
import Carousel from "./Carousel.astro";
import {
type CarouselApi,
type CarouselManager,
type CarouselOptions,
initCarousel,
} from "./carousel-script";
import CarouselContent from "./CarouselContent.astro";
import CarouselItem from "./CarouselItem.astro";
import CarouselNext from "./CarouselNext.astro";
import CarouselPrevious from "./CarouselPrevious.astro";
export {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
type CarouselManager,
CarouselNext,
type CarouselOptions,
CarouselPrevious,
initCarousel,
};
export default {
Root: Carousel,
Content: CarouselContent,
Item: CarouselItem,
Next: CarouselNext,
Previous: CarouselPrevious,
init: initCarousel,
};

View File

@@ -0,0 +1,377 @@
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"div"> & {
/**
* When true, the dropdown will open on hover in addition to click
*/
openOnHover?: boolean;
/**
* Time in milliseconds to wait before closing when hover open is enabled
* @default 200
*/
closeDelay?: number;
children: any;
};
const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Astro.props;
---
<div
class:list={["starwind-dropdown", "relative", className]}
data-open-on-hover={openOnHover ? "true" : undefined}
data-close-delay={closeDelay}
data-slot="dropdown"
{...rest}
>
<slot />
</div>
<script>
class DropdownHandler {
private dropdown: HTMLElement;
private trigger: HTMLButtonElement | null;
private content: HTMLElement | null;
private items: HTMLElement[] = [];
private currentFocusIndex: number = -1;
private isOpen: boolean = false;
private isClosing: boolean = false;
private animationDuration = 150;
private openOnHover: boolean;
private closeDelay: number;
private closeTimerRef: number | null = null;
private lastOpenSource: "keyboard" | "mouse" = "keyboard";
private lastCloseSource: "keyboard" | "mouse" = "keyboard";
constructor(dropdown: HTMLElement, dropdownIdx: number) {
this.dropdown = dropdown;
this.openOnHover = dropdown.getAttribute("data-open-on-hover") === "true";
this.closeDelay = parseInt(dropdown.getAttribute("data-close-delay") || "200");
// Get the temporary trigger element
const tempTrigger = dropdown.querySelector(".starwind-dropdown-trigger") as HTMLElement;
// if trigger is set with asChild, use the first child element for trigger button
if (tempTrigger?.hasAttribute("data-as-child")) {
this.trigger = tempTrigger.firstElementChild as HTMLButtonElement;
} else {
this.trigger = tempTrigger as HTMLButtonElement;
}
this.content = dropdown.querySelector(".starwind-dropdown-content");
if (!this.trigger || !this.content) return;
// Get animation duration from inline styles if available
const animationDurationString = this.content.style.animationDuration;
if (animationDurationString.endsWith("ms")) {
this.animationDuration = parseFloat(animationDurationString);
} else if (animationDurationString.endsWith("s")) {
this.animationDuration = parseFloat(animationDurationString) * 1000;
}
this.init(dropdownIdx);
}
private init(dropdownIdx: number) {
this.setupAccessibility(dropdownIdx);
this.setupEvents();
}
private setupAccessibility(dropdownIdx: number) {
if (!this.trigger || !this.content) return;
// Generate unique IDs for accessibility
this.trigger.id = `starwind-dropdown${dropdownIdx}-trigger`;
this.content.id = `starwind-dropdown${dropdownIdx}-content`;
// Set up additional ARIA attributes
this.trigger.setAttribute("aria-controls", this.content.id);
this.content.setAttribute("aria-labelledby", this.trigger.id);
}
private setupEvents() {
if (!this.trigger || !this.content) return;
// Handle trigger click
this.trigger.addEventListener("click", (e) => {
e.preventDefault();
this.lastOpenSource = e.detail === 0 ? "keyboard" : "mouse";
this.toggleDropdown();
});
// Handle keyboard navigation
this.trigger.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.lastOpenSource = "keyboard";
this.toggleDropdown();
} else if (e.key === "Escape" && this.isOpen) {
e.preventDefault();
this.lastCloseSource = "keyboard";
this.closeDropdown();
} else if (this.isOpen && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
e.preventDefault();
this.lastOpenSource = "keyboard";
this.updateDropdownItems();
if (e.key === "ArrowDown") {
this.focusItem(0); // Focus first item when opening with arrow down
} else {
this.focusItem(this.items.length - 1); // Focus last item when opening with arrow up
}
}
});
// Close dropdown when clicking outside for mouse
document.addEventListener("pointerdown", (e) => {
if (this.isOpen && !this.dropdown.contains(e.target as Node)) {
// only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
// but not when the control key is pressed (avoiding MacOS right click); also not for touch
// devices because that would open the menu on scroll. (pen devices behave as touch on iOS).
if (e.button === 0 && e.ctrlKey === false && e.pointerType === "mouse") {
this.closeDropdown();
}
}
});
// Handle click outside select content to close for mobile
document.addEventListener("click", (e) => {
if (
!(this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)) &&
this.isOpen
) {
this.closeDropdown();
}
});
// Handle keyboard navigation and item selection within dropdown
this.content.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.preventDefault();
this.closeDropdown();
this.trigger?.focus();
} else if (this.isOpen) {
this.handleMenuKeydown(e);
}
});
// Handle item selection
this.content.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
const item = target.closest('[role="menuitem"]');
if (item && !(item as HTMLElement).hasAttribute("data-disabled")) {
// Close the dropdown after item selection
this.closeDropdown();
// console.log("click closing");
}
});
// Handle hover on dropdown items
this.content.addEventListener("mouseover", (e) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('[role="menuitem"]');
if (menuItem && menuItem instanceof HTMLElement && this.isOpen === true) {
// Update items list before focusing to ensure the index is correct
this.updateDropdownItems();
// Focus the item when hovering
menuItem.focus();
// Update the current focus index
this.currentFocusIndex = this.items.indexOf(menuItem);
}
});
if (this.openOnHover) {
this.trigger.addEventListener("pointerenter", (e) => {
if (e.pointerType !== "mouse") return;
if (this.isClosing) return;
if (!this.isOpen) {
this.lastOpenSource = "mouse";
this.openDropdown();
} else {
// If the dropdown is already open, make sure to clear any close timer
this.clearCloseTimer();
}
});
this.dropdown.addEventListener("pointerleave", (e) => {
if (e.pointerType !== "mouse") return;
if (this.isOpen) {
this.lastCloseSource = "mouse";
this.closeDropdownDelayed();
}
});
this.content.addEventListener("pointerenter", (e) => {
if (e.pointerType !== "mouse") return;
// If the user moves the mouse to the content, cancel the close timer
this.clearCloseTimer();
});
}
}
private handleMenuKeydown(e: KeyboardEvent) {
// Make sure we've got an updated list of menu items
this.updateDropdownItems();
// Skip if no items
if (this.items.length === 0) return;
const currentIdx = this.currentFocusIndex;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
this.focusItem(currentIdx === -1 ? 0 : currentIdx + 1);
break;
case "ArrowUp":
e.preventDefault();
this.focusItem(currentIdx === -1 ? this.items.length - 1 : currentIdx - 1);
break;
case "Home":
e.preventDefault();
this.focusItem(0);
break;
case "End":
e.preventDefault();
this.focusItem(this.items.length - 1);
break;
case "Enter":
case " ":
if (currentIdx !== -1) {
e.preventDefault();
this.items[currentIdx].click();
}
break;
}
}
private updateDropdownItems() {
if (!this.content) return;
// Get all interactive menuitem elements
this.items = Array.from(
this.content.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'),
) as HTMLElement[];
}
private focusItem(idx: number) {
// Ensure the index wraps around properly
const targetIdx = (idx + this.items.length) % this.items.length;
if (this.items[targetIdx]) {
this.items[targetIdx].focus();
this.currentFocusIndex = targetIdx;
}
}
private toggleDropdown() {
if (this.isOpen) {
this.closeDropdown();
} else {
this.openDropdown();
}
}
private openDropdown() {
if (this.isClosing) return;
if (!this.content || !this.trigger || this.trigger.disabled) return;
this.isOpen = true;
this.content.setAttribute("data-state", "open");
this.trigger.setAttribute("aria-expanded", "true");
this.content.style.removeProperty("display");
// Update the list of dropdown items
this.updateDropdownItems();
// Reset focus index when opening
this.currentFocusIndex = -1;
this.positionContent();
}
private closeDropdown() {
if (!this.content || !this.trigger) return;
this.isClosing = true;
this.isOpen = false;
this.content.setAttribute("data-state", "closed");
// Set focus back on trigger only if opened or closed by keyboard
if (
!this.openOnHover ||
this.lastOpenSource === "keyboard" ||
this.lastCloseSource === "keyboard"
) {
requestAnimationFrame(() => {
if (!this.trigger) return;
this.trigger.focus();
});
}
// Give the content time to animate before hiding
setTimeout(() => {
if (!this.content) return;
this.content.style.display = "none";
this.isClosing = false;
}, this.animationDuration);
this.trigger.setAttribute("aria-expanded", "false");
// Reset focus index when closing
this.currentFocusIndex = -1;
}
private closeDropdownDelayed() {
if (!this.content || !this.trigger) return;
// Clear any existing close timer
this.clearCloseTimer();
// Set a new timer to close the dropdown after the delay
this.closeTimerRef = window.setTimeout(() => {
if (this.isOpen) {
this.closeDropdown();
}
this.closeTimerRef = null;
}, this.closeDelay);
}
private clearCloseTimer() {
if (this.closeTimerRef !== null) {
window.clearTimeout(this.closeTimerRef);
this.closeTimerRef = null;
}
}
private positionContent() {
if (!this.content || !this.trigger) return;
// Set content width to match trigger width
this.content.style.width = "var(--starwind-dropdown-trigger-width)";
this.content.style.setProperty(
"--starwind-dropdown-trigger-width",
`${this.trigger.offsetWidth}px`,
);
}
}
// Store instances in a WeakMap to avoid memory leaks
const dropdownInstances = new WeakMap<HTMLElement, DropdownHandler>();
let dropdownCounter = 0;
// Initialize dropdowns
const initDropdowns = () => {
document.querySelectorAll(".starwind-dropdown").forEach((dropdown) => {
if (dropdown instanceof HTMLElement && !dropdownInstances.has(dropdown)) {
dropdownInstances.set(dropdown, new DropdownHandler(dropdown, dropdownCounter++));
}
});
};
initDropdowns();
document.addEventListener("astro:after-swap", initDropdowns);
document.addEventListener("starwind:init", initDropdowns);
</script>

View File

@@ -0,0 +1,81 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div"> & {
/**
* Side of the dropdown
* @default bottom
*/
side?: "top" | "bottom";
/**
* Alignment of the dropdown
* @default start
*/
align?: "start" | "center" | "end";
/**
* Offset distance in pixels
* @default 4
*/
sideOffset?: number;
/**
* Open and close animation duration in milliseconds
* @default 150
*/
animationDuration?: number;
};
export const dropdownContent = tv({
base: [
"starwind-dropdown-content",
"bg-popover text-popover-foreground z-50 min-w-[9rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"data-[state=open]:animate-in fade-in zoom-in-95",
"data-[state=closed]:animate-out data-[state=closed]:fill-mode-forwards fade-out zoom-out-95",
"absolute will-change-transform",
],
variants: {
side: {
bottom: "slide-in-from-top-2 slide-out-to-top-2 top-full",
top: "slide-in-from-bottom-2 slide-out-to-bottom-2 bottom-full",
},
align: {
start: "slide-in-from-left-1 slide-out-to-left-1 left-0",
center: "left-1/2 -translate-x-1/2",
end: "slide-in-from-right-1 slide-out-to-right-1 right-0",
},
},
defaultVariants: {
side: "bottom",
align: "start",
},
});
const {
class: className,
side = "bottom",
align = "start",
sideOffset = 4,
animationDuration = 150,
...rest
} = Astro.props;
---
<div
class={dropdownContent({ side, align, class: className })}
role="menu"
data-side={side}
data-align={align}
data-state="closed"
data-slot="dropdown-content"
tabindex="-1"
aria-orientation="vertical"
style={{
display: "none",
animationDuration: `${animationDuration}ms`,
marginTop: side === "bottom" ? `${sideOffset}px` : undefined,
marginBottom: side === "top" ? `${sideOffset}px` : undefined,
}}
{...rest}
>
<slot />
</div>

View File

@@ -0,0 +1,48 @@
---
import type { HTMLTag, Polymorphic } from "astro/types";
import { tv } from "tailwind-variants";
type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & {
/**
* Whether the item is inset (has left padding)
*/
inset?: boolean;
/**
* Whether the item is disabled
*/
disabled?: boolean;
};
export const dropdownItem = tv({
base: [
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 transition-colors outline-none select-none",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"[&>svg]:size-4 [&>svg]:shrink-0",
],
variants: {
inset: {
true: "pl-8",
},
disabled: {
true: "pointer-events-none opacity-50",
},
},
defaultVariants: {
inset: false,
disabled: false,
},
});
const { class: className, inset = false, disabled = false, as: Tag = "div", ...rest } = Astro.props;
---
<Tag
class={dropdownItem({ inset, disabled, class: className })}
role="menuitem"
tabindex={disabled ? "-1" : "0"}
data-disabled={disabled ? "true" : undefined}
data-slot="dropdown-item"
{...rest}
>
<slot />
</Tag>

View File

@@ -0,0 +1,29 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div"> & {
/**
* Whether the label is inset (has left padding)
*/
inset?: boolean;
};
export const dropdownLabel = tv({
base: ["px-2 py-1.5 font-semibold"],
variants: {
inset: {
true: "pl-8",
},
},
defaultVariants: {
inset: false,
},
});
const { class: className, inset = false, ...rest } = Astro.props;
---
<div class={dropdownLabel({ inset, class: className })} data-slot="dropdown-label" {...rest}>
<slot />
</div>

View File

@@ -0,0 +1,21 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = HTMLAttributes<"div">;
export const dropdownSeparator = tv({
base: "bg-border -mx-1 my-1 h-px",
});
const { class: className, ...rest } = Astro.props;
---
<div
class={dropdownSeparator({ class: className })}
role="separator"
aria-orientation="horizontal"
data-slot="dropdown-separator"
{...rest}
>
</div>

View File

@@ -0,0 +1,52 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv } from "tailwind-variants";
type Props = Omit<HTMLAttributes<"button">, "role" | "type"> & {
/**
* When true, the component will render its child element with a simple wrapper instead of a button component
*/
asChild?: boolean;
};
export const dropdownTrigger = tv({
base: [
"starwind-dropdown-trigger",
"inline-flex items-center justify-center",
"focus-visible:ring-outline/50 transition-[color,box-shadow] outline-none focus-visible:ring-3",
"disabled:pointer-events-none",
],
});
const { class: className, asChild = false, ...rest } = Astro.props;
// Get the first child element if asChild is true
let hasChildren = false;
if (Astro.slots.has("default")) {
hasChildren = true;
}
---
{
asChild && hasChildren ? (
<div
class:list={["starwind-dropdown-trigger", className]}
data-slot="dropdown-trigger"
data-as-child
>
<slot />
</div>
) : (
<button
class={dropdownTrigger({ class: className })}
type="button"
aria-haspopup="true"
aria-expanded="false"
data-state="closed"
data-slot="dropdown-trigger"
{...rest}
>
<slot />
</button>
)
}

View File

@@ -0,0 +1,33 @@
import Dropdown from "./Dropdown.astro";
import DropdownContent, { dropdownContent } from "./DropdownContent.astro";
import DropdownItem, { dropdownItem } from "./DropdownItem.astro";
import DropdownLabel, { dropdownLabel } from "./DropdownLabel.astro";
import DropdownSeparator, { dropdownSeparator } from "./DropdownSeparator.astro";
import DropdownTrigger, { dropdownTrigger } from "./DropdownTrigger.astro";
const DropdownVariants = {
dropdownContent,
dropdownItem,
dropdownLabel,
dropdownSeparator,
dropdownTrigger,
};
export {
Dropdown,
DropdownContent,
DropdownItem,
DropdownLabel,
DropdownSeparator,
DropdownTrigger,
DropdownVariants,
};
export default {
Root: Dropdown,
Trigger: DropdownTrigger,
Content: DropdownContent,
Item: DropdownItem,
Label: DropdownLabel,
Separator: DropdownSeparator,
};

View File

@@ -0,0 +1,25 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "tailwind-variants";
type Props = HTMLAttributes<"input"> & VariantProps<typeof input>;
export const input = tv({
base: [
"border-input dark:bg-input/30 text-foreground w-full rounded-md border bg-transparent shadow-xs",
"focus-visible:border-outline focus-visible:ring-outline/50 transition-[color,box-shadow] focus-visible:ring-3",
"file:text-foreground file:my-auto file:mr-4 file:h-full file:border-0 file:bg-transparent file:text-sm file:font-medium",
"disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:border-error aria-invalid:focus-visible:ring-error/40",
"peer placeholder:text-muted-foreground",
],
variants: {
size: { sm: "h-9 px-2 text-sm", md: "h-11 px-3 text-base", lg: "h-12 px-4 text-lg" },
},
defaultVariants: { size: "md" },
});
const { size, class: className, ...rest } = Astro.props;
---
<input class={input({ size, class: className })} data-slot="input" {...rest} />

View File

@@ -0,0 +1,7 @@
import Input, { input } from "./Input.astro";
const InputVariants = { input };
export { Input, InputVariants };
export default Input;

View File

@@ -0,0 +1,22 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "tailwind-variants";
type Props = HTMLAttributes<"label"> & VariantProps<typeof label>;
export const label = tv({
base: [
"text-foreground leading-none font-medium",
"peer-disabled:cursor-not-allowed peer-disabled:opacity-70 has-[+:disabled]:cursor-not-allowed has-[+:disabled]:opacity-70",
],
variants: { size: { sm: "text-sm", md: "text-base", lg: "text-lg" } },
defaultVariants: { size: "md" },
});
const { size, class: className, ...rest } = Astro.props;
---
{/* eslint-disable-next-line astro/jsx-a11y/label-has-associated-control */}
<label class={label({ size, class: className })} data-slot="label" {...rest}>
<slot />
</label>

View File

@@ -0,0 +1,7 @@
import Label, { label } from "./Label.astro";
const LabelVariants = { label };
export { Label, LabelVariants };
export default Label;

View File

@@ -0,0 +1,29 @@
---
import type { HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "tailwind-variants";
type Props = HTMLAttributes<"textarea"> & VariantProps<typeof textarea>;
export const textarea = tv({
base: [
"border-input dark:bg-input/30 text-foreground ring-offset-background min-h-10 w-full rounded-md border bg-transparent shadow-xs",
"focus-visible:border-outline focus-visible:ring-outline/50 transition-[color,box-shadow] focus-visible:ring-3",
"file:text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium",
"disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:border-error aria-invalid:focus-visible:ring-error/40",
"peer placeholder:text-muted-foreground",
],
variants: {
size: {
sm: "min-h-9 px-2 py-1 text-sm",
md: "min-h-10 px-3 py-2 text-base",
lg: "min-h-12 px-4 py-3 text-lg",
},
},
defaultVariants: { size: "md" },
});
const { size, class: className, ...rest } = Astro.props;
---
<textarea class={textarea({ size, class: className })} data-slot="textarea" {...rest}></textarea>

View File

@@ -0,0 +1,9 @@
import Textarea, { textarea } from "./Textarea.astro";
const TextareaVariants = {
textarea,
};
export { Textarea, TextareaVariants };
export default Textarea;

View File

@@ -1,15 +1,107 @@
--- ---
import "@/styles/starwind.css";
import "@/styles/global.css"; import "@/styles/global.css";
import "../styles/global.css" import Footer1 from "@/components/starwind-pro/footer-01/Footer1.astro";
import TopBarDropdown from "@/components/TopBarDropdown.astro";
import { Image } from "astro:assets";
import LogoLight from "@/assets/logo-light.svg";
interface Props {
title: string;
description?: string;
image?: string;
url?: string;
}
const {
title,
description = "Resuely",
image = "/og-image.jpg",
url = "https://resuely.com",
} = Astro.props;
const siteUrl = Astro.site?.toString() ?? url;
const canonicalUrl = new URL(Astro.url.pathname, siteUrl).toString();
const ogImageUrl = new URL(image, siteUrl).toString();
const orgLogoUrl = new URL("/apple-touch-icon.png", siteUrl).toString();
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Resuely",
url: siteUrl,
inLanguage: "es-VE",
};
const organizationSchema = {
"@context": "https://schema.org",
"@type": "Organization",
name: "Resuely",
url: siteUrl,
logo: orgLogoUrl,
sameAs: ["https://git.rlugo.dev/resuely"],
};
--- ---
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<meta name="description" content={description}>
<meta name="robots" content="index,follow">
<link rel="canonical" href={canonicalUrl}>
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#ffffff">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Resuely">
<meta property="og:locale" content="es_VE">
<meta property="og:url" content={canonicalUrl}>
<meta property="og:title" content={title}>
<meta property="og:description" content={description}>
<meta property="og:image" content={ogImageUrl}>
<meta property="og:image:alt" content="Resuely">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@resuely">
<meta name="twitter:url" content={canonicalUrl}>
<meta name="twitter:title" content={title}>
<meta name="twitter:description" content={description}>
<meta name="twitter:image" content={ogImageUrl}>
<script type="application/ld+json" is:inline set:html={JSON.stringify(organizationSchema)}></script>
<script type="application/ld+json" is:inline set:html={JSON.stringify(websiteSchema)}></script>
<slot name="head" />
</head> </head>
<body> <body>
<header
class="sticky top-0 border-b border-neutral-200 bg-background/60 backdrop-blur z-50"
>
<nav
class="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-4"
>
<a
href="/"
class="flex items-center font-heading text-lg text-heading uppercase font-bold tracking-resuely"
>
<Image class="h-12 w-min" src={LogoLight} alt="Resuely" />
<p class="">resuely</p>
</a>
<TopBarDropdown />
</nav>
</header>
<main>
<slot /> <slot />
</main>
<footer>
<Footer1 />
</footer>
</body> </body>
</html> </html>

222
src/pages/api/contact.ts Normal file
View File

@@ -0,0 +1,222 @@
import type { APIRoute } from "astro";
export const prerender = false;
type ApiErrorCode =
| "methodNotAllowed"
| "invalidRequest"
| "rateLimited"
| "notConfigured"
| "emailFailed";
type ApiErrorResponse = {
ok: false;
error: {
code: ApiErrorCode;
message: string;
};
};
type ApiOkResponse = {
ok: true;
};
const respondJson = (
status: number,
body: ApiOkResponse | ApiErrorResponse,
) => {
return new Response(JSON.stringify(body), {
status,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
};
const getClientIp = (request: Request, fallbackIp: string) => {
const xff = request.headers.get("x-forwarded-for");
if (xff) return xff.split(",")[0]?.trim() || fallbackIp;
const xri = request.headers.get("x-real-ip");
if (xri) return xri.trim();
return fallbackIp;
};
type RateLimitEntry = {
windowStartMs: number;
count: number;
};
const rateLimitWindowMs = 60_000;
const rateLimitMax = 5;
const rateLimitMap = new Map<string, RateLimitEntry>();
const isRateLimited = (key: string, nowMs: number) => {
const existing = rateLimitMap.get(key);
if (!existing) {
rateLimitMap.set(key, { windowStartMs: nowMs, count: 1 });
return false;
}
if (nowMs - existing.windowStartMs > rateLimitWindowMs) {
rateLimitMap.set(key, { windowStartMs: nowMs, count: 1 });
return false;
}
existing.count += 1;
rateLimitMap.set(key, existing);
return existing.count > rateLimitMax;
};
const normalizeText = (value: string, maxLen: number) => {
const collapsed = value.replace(/[\u0000-\u001F\u007F]/g, " ").trim();
if (collapsed.length <= maxLen) return collapsed;
return collapsed.slice(0, maxLen);
};
const isEmailLike = (value: string) => {
const v = value.trim();
if (v.length < 6 || v.length > 254) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
};
const readBody = async (request: Request) => {
const contentType = request.headers.get("content-type") ?? "";
if (
contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("multipart/form-data")
) {
const data = await request.formData();
return {
name: String(data.get("name") ?? ""),
email: String(data.get("email") ?? ""),
subject: String(data.get("subject") ?? ""),
message: String(data.get("message") ?? ""),
};
}
if (contentType.includes("application/json")) {
const json: unknown = await request.json();
if (typeof json !== "object" || json === null) return null;
const record = json as Record<string, unknown>;
return {
name: typeof record.name === "string" ? record.name : "",
email: typeof record.email === "string" ? record.email : "",
subject: typeof record.subject === "string" ? record.subject : "",
message: typeof record.message === "string" ? record.message : "",
};
}
return null;
};
export const POST: APIRoute = async ({ request, clientAddress }) => {
const nowMs = Date.now();
const ip = getClientIp(request, clientAddress);
const rateKey = `contact:${ip}`;
if (isRateLimited(rateKey, nowMs)) {
return respondJson(429, {
ok: false,
error: {
code: "rateLimited",
message: "Demasiados intentos. Intenta de nuevo en un minuto.",
},
});
}
const raw = await readBody(request);
if (!raw) {
return respondJson(400, {
ok: false,
error: {
code: "invalidRequest",
message: "Solicitud inválida.",
},
});
}
const name = normalizeText(raw.name, 80);
const email = normalizeText(raw.email, 254);
const subject = normalizeText(raw.subject, 120);
const message = normalizeText(raw.message, 4000);
if (!name || !email || !subject || !message || !isEmailLike(email)) {
return respondJson(400, {
ok: false,
error: {
code: "invalidRequest",
message: "Revisa los campos e inténtalo de nuevo.",
},
});
}
const resendApiKey = import.meta.env.RESEND_API_KEY as string | undefined;
const contactToEmail = import.meta.env.CONTACT_TO_EMAIL as string | undefined;
const contactFromEmail = import.meta.env.CONTACT_FROM_EMAIL as
| string
| undefined;
if (!resendApiKey || !contactToEmail || !contactFromEmail) {
return respondJson(501, {
ok: false,
error: {
code: "notConfigured",
message: "Contacto no está configurado todavía.",
},
});
}
const emailText = [
"Nuevo mensaje desde resuely.com",
"",
`Nombre: ${name}`,
`Correo: ${email}`,
`Asunto: ${subject}`,
"",
"Mensaje:",
message,
].join("\n");
const resendPayload: Record<string, unknown> = {
from: contactFromEmail,
to: contactToEmail,
subject: `[Resuely] ${subject}`,
text: emailText,
};
resendPayload["reply_to"] = email;
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
authorization: `Bearer ${resendApiKey}`,
"content-type": "application/json",
accept: "application/json",
},
body: JSON.stringify(resendPayload),
});
if (!response.ok) {
console.log(await response.json());
return respondJson(502, {
ok: false,
error: {
code: "emailFailed",
message: "No pudimos enviar tu mensaje. Intenta de nuevo más tarde.",
},
});
}
return respondJson(200, { ok: true });
};
export const ALL: APIRoute = async () => {
return respondJson(405, {
ok: false,
error: {
code: "methodNotAllowed",
message: "Método no permitido.",
},
});
};

11
src/pages/api/healthz.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { APIRoute } from "astro";
export const GET: APIRoute = async () => {
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
};

View File

@@ -1,7 +1,83 @@
--- ---
import BaseLayout from "../layouts/BaseLayout.astro" import BaseLayout from "../layouts/BaseLayout.astro"
import Feature2Demo from "@/components/starwind-pro/feature-02/Feature2Demo.astro" import Hero, { type CarouselImage } from "@/components/starwind-pro/hero-09/Hero9.astro"
import Services7, { type Service } from "@/components/starwind-pro/services-07/Services7.astro"
import Contact2 from "@/components/starwind-pro/contact-02/Contact2.astro"
import Faq4, { type FaqItem } from "@/components/starwind-pro/faq-04/Faq4.astro"
import Img1 from "@/assets/hero1.jpg";
import Img2 from "@/assets/hero2.jpg";
import Img3 from "@/assets/hero3.jpg";
import Img4 from "@/assets/hero4.jpg";
import Img5 from "@/assets/hero5.jpg";
import Handy from "@/assets/handy.jpg";
import Programming from "@/assets/programming.jpg";
const carouselImages: CarouselImage[] = [
{src: Img1.src, alt: "Paisaje de Venezuela"},
{src: Img2.src, alt: "Ciudad en Venezuela"},
{src: Img3.src, alt: "Montañas en Venezuela"},
{src: Img4.src, alt: "Costa de Venezuela"},
{src: Img5.src, alt: "Atardecer en Venezuela"},
]
const services: Service[] = [
{ title: "Handy", tagline: "Encuentra y contacta profesionales", image: Handy, href: "https://handy.resuely.com" },
{ title: "Próximamente", tagline: "Más soluciones simples en camino", image: Programming }
]
const faqItems: FaqItem[] = [
{ title: "¿Qué es Resuely?", content: "Resuely es un conjunto de aplicaciones web gratuitas creadas por venezolanos para hacer más fácil el día a día. Entras, eliges una app y resuelves en minutos." },
{ title: "¿Por qué existe Resuely?", content: "Porque muchas herramientas son demasiado complicadas o vienen con cosas que no necesitas. Nosotros preferimos soluciones simples, claras y confiables." },
{ title: "¿Cuánto cuesta?", content: "Nada. Las apps de Resuely son gratis." },
{ title: "¿Necesito instalar algo?", content: "No. Resuely funciona desde el navegador, en tu teléfono o computadora." },
{ title: "¿Necesito crear una cuenta?", content: "Depende de la app. Algunas funcionan sin cuenta; otras te piden registrarte para guardar tu información y poder volver cuando lo necesites." },
{ title: "¿Qué pasa con mis datos?", content: "Usamos tu información solo para que la app funcione. No la vendemos ni la publicamos." },
{ title: "¿Puedo usar Resuely fuera de Venezuela?", content: "Sí. Puedes usar nuestras apps desde cualquier lugar con internet, aunque están pensadas para necesidades comunes en Venezuela." },
{ title: "¿Puedo sugerir una app o una mejora?", content: "Sí. Usa el formulario de contacto y cuéntanos tu idea. Leemos todo." },
{ title: "¿Dónde pido ayuda si algo no me funciona?", content: "Usa el formulario de contacto y descríbenos qué intentaste hacer y qué viste en pantalla." },
]
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqItems.map((item) => ({
"@type": "Question",
name: item.title,
acceptedAnswer: {
"@type": "Answer",
text: item.content,
},
})),
};
--- ---
<BaseLayout>
<Feature2Demo /> <BaseLayout
title="Resuely | Apps gratis para resolver tu día a día"
description="Apps web gratuitas, simples y confiables. Hechas por venezolanos para resolver tareas reales en minutos, sin pagos ni complicaciones."
>
<script
slot="head"
type="application/ld+json"
is:inline
set:html={JSON.stringify(faqSchema)}
></script>
<Hero
heading="Apps gratis para resolver tu día a día"
description="Herramientas simples, rápidas y confiables. Hechas por venezolanos para necesidades reales, sin complicaciones."
primaryButtonText="Ver aplicaciones"
primaryButtonHref="#aplicaciones"
carouselImages={carouselImages}
/>
<Services7
id="aplicaciones"
badge="Aplicaciones"
heading="Lo que puedes usar hoy"
description="Elige una app y empieza. Sin pagos, sin letras pequeñas."
services={services} />
<Faq4 heading="Preguntas frecuentes" description="Respuestas rápidas a las dudas más comunes." items={faqItems} />
<Contact2
badge="Contacto"
heading="Hablemos"
description="Cuéntanos tu idea, una mejora o un problema. Te respondemos por correo."
/>
</BaseLayout> </BaseLayout>

63
src/styles/fonts.css Normal file
View File

@@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter_28pt-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter_28pt-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter_28pt-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter_28pt-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}

View File

@@ -1,167 +1,16 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "./fonts.css";
@plugin "@tailwindcss/forms";
@custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--animate-accordion-down: accordion-down 0.2s ease-out; --font-sans: 'Inter', system-ui, sans-serif;
--animate-accordion-up: accordion-up 0.2s ease-out; --font-heading: 'Poppins', sans-serif;
@keyframes accordion-down { --tracking-resuely: 0.2em;
from {
height: 0;
}
to {
height: var(--starwind-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--starwind-accordion-content-height);
}
to {
height: 0;
}
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-accent: var(--primary-accent);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary-accent: var(--secondary-accent);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-error: var(--error);
--color-error-foreground: var(--error-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-outline: var(--outline);
--radius-xs: calc(var(--radius) - 0.375rem);
--radius-sm: calc(var(--radius) - 0.25rem);
--radius-md: calc(var(--radius) - 0.125rem);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 0.25rem);
--radius-2xl: calc(var(--radius) + 0.5rem);
--radius-3xl: calc(var(--radius) + 1rem);
--color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-outline: var(--sidebar-outline);
}
:root {
--background: var(--color-white);
--foreground: var(--color-neutral-950);
--card: var(--color-white);
--card-foreground: var(--color-neutral-950);
--popover: var(--color-white);
--popover-foreground: var(--color-neutral-950);
--primary: var(--color-blue-700);
--primary-foreground: var(--color-neutral-50);
--primary-accent: var(--color-blue-700);
--secondary: var(--color-fuchsia-700);
--secondary-foreground: var(--color-neutral-50);
--secondary-accent: var(--color-fuchsia-700);
--muted: var(--color-neutral-100);
--muted-foreground: var(--color-neutral-600);
--accent: var(--color-neutral-100);
--accent-foreground: var(--color-neutral-900);
--info: var(--color-sky-300);
--info-foreground: var(--color-sky-950);
--success: var(--color-green-300);
--success-foreground: var(--color-green-950);
--warning: var(--color-amber-300);
--warning-foreground: var(--color-amber-950);
--error: var(--color-red-700);
--error-foreground: var(--color-neutral-50);
--border: var(--color-neutral-200);
--input: var(--color-neutral-200);
--outline: var(--color-neutral-400);
--radius: 0.625rem;
/* sidebar variables */
--sidebar-background: var(--color-neutral-50);
--sidebar-foreground: var(--color-neutral-950);
--sidebar-primary: var(--color-blue-700);
--sidebar-primary-foreground: var(--color-neutral-50);
--sidebar-accent: var(--color-neutral-100);
--sidebar-accent-foreground: var(--color-neutral-900);
--sidebar-border: var(--color-neutral-200);
--sidebar-outline: var(--color-neutral-400);
}
.dark {
--background: var(--color-neutral-950);
--foreground: var(--color-neutral-50);
--card: var(--color-neutral-900);
--card-foreground: var(--color-neutral-50);
--popover: var(--color-neutral-800);
--popover-foreground: var(--color-neutral-50);
--primary: var(--color-blue-700);
--primary-foreground: var(--color-neutral-50);
--primary-accent: var(--color-blue-400);
--secondary: var(--color-fuchsia-700);
--secondary-foreground: var(--color-neutral-50);
--secondary-accent: var(--color-fuchsia-400);
--muted: var(--color-neutral-800);
--muted-foreground: var(--color-neutral-400);
--accent: var(--color-neutral-700);
--accent-foreground: var(--color-neutral-100);
--info: var(--color-sky-300);
--info-foreground: var(--color-sky-950);
--success: var(--color-green-300);
--success-foreground: var(--color-green-950);
--warning: var(--color-amber-300);
--warning-foreground: var(--color-amber-950);
--error: var(--color-red-800);
--error-foreground: var(--color-neutral-50);
--border: --alpha(var(--color-neutral-50) / 10%);
--input: --alpha(var(--color-neutral-50) / 15%);
--outline: var(--color-neutral-500);
/* sidebars variables */
--sidebar-background: var(--color-neutral-900);
--sidebar-foreground: var(--color-neutral-50);
--sidebar-primary: var(--color-blue-700);
--sidebar-primary-foreground: var(--color-neutral-50);
--sidebar-accent: var(--color-neutral-800);
--sidebar-accent-foreground: var(--color-neutral-100);
--sidebar-border: var(--color-neutral-800);
--sidebar-outline: var(--color-neutral-600);
} }
@layer base { @layer base {
* { html {
@apply border-border outline-outline/50; scroll-behavior: smooth;
}
body {
@apply bg-background text-foreground scheme-light dark:scheme-dark;
}
button {
@apply cursor-pointer;
} }
} }

167
src/styles/starwind.css Normal file
View File

@@ -0,0 +1,167 @@
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/forms";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--starwind-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--starwind-accordion-content-height);
}
to {
height: 0;
}
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-accent: var(--primary-accent);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary-accent: var(--secondary-accent);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-error: var(--error);
--color-error-foreground: var(--error-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-outline: var(--outline);
--radius-xs: calc(var(--radius) - 0.375rem);
--radius-sm: calc(var(--radius) - 0.25rem);
--radius-md: calc(var(--radius) - 0.125rem);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 0.25rem);
--radius-2xl: calc(var(--radius) + 0.5rem);
--radius-3xl: calc(var(--radius) + 1rem);
--color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-outline: var(--sidebar-outline);
}
:root {
--background: var(--color-white);
--foreground: var(--color-neutral-950);
--card: var(--color-white);
--card-foreground: var(--color-neutral-950);
--popover: var(--color-white);
--popover-foreground: var(--color-neutral-950);
--primary: var(--color-blue-700);
--primary-foreground: var(--color-neutral-50);
--primary-accent: var(--color-blue-700);
--secondary: var(--color-fuchsia-700);
--secondary-foreground: var(--color-neutral-50);
--secondary-accent: var(--color-fuchsia-700);
--muted: var(--color-neutral-100);
--muted-foreground: var(--color-neutral-600);
--accent: var(--color-neutral-100);
--accent-foreground: var(--color-neutral-900);
--info: var(--color-sky-300);
--info-foreground: var(--color-sky-950);
--success: var(--color-green-300);
--success-foreground: var(--color-green-950);
--warning: var(--color-amber-300);
--warning-foreground: var(--color-amber-950);
--error: var(--color-red-700);
--error-foreground: var(--color-neutral-50);
--border: var(--color-neutral-200);
--input: var(--color-neutral-200);
--outline: var(--color-neutral-400);
--radius: 0.625rem;
/* sidebar variables */
--sidebar-background: var(--color-neutral-50);
--sidebar-foreground: var(--color-neutral-950);
--sidebar-primary: var(--color-blue-700);
--sidebar-primary-foreground: var(--color-neutral-50);
--sidebar-accent: var(--color-neutral-100);
--sidebar-accent-foreground: var(--color-neutral-900);
--sidebar-border: var(--color-neutral-200);
--sidebar-outline: var(--color-neutral-400);
}
.dark {
--background: var(--color-neutral-950);
--foreground: var(--color-neutral-50);
--card: var(--color-neutral-900);
--card-foreground: var(--color-neutral-50);
--popover: var(--color-neutral-800);
--popover-foreground: var(--color-neutral-50);
--primary: var(--color-blue-700);
--primary-foreground: var(--color-neutral-50);
--primary-accent: var(--color-blue-400);
--secondary: var(--color-fuchsia-700);
--secondary-foreground: var(--color-neutral-50);
--secondary-accent: var(--color-fuchsia-400);
--muted: var(--color-neutral-800);
--muted-foreground: var(--color-neutral-400);
--accent: var(--color-neutral-700);
--accent-foreground: var(--color-neutral-100);
--info: var(--color-sky-300);
--info-foreground: var(--color-sky-950);
--success: var(--color-green-300);
--success-foreground: var(--color-green-950);
--warning: var(--color-amber-300);
--warning-foreground: var(--color-amber-950);
--error: var(--color-red-800);
--error-foreground: var(--color-neutral-50);
--border: --alpha(var(--color-neutral-50) / 10%);
--input: --alpha(var(--color-neutral-50) / 15%);
--outline: var(--color-neutral-500);
/* sidebars variables */
--sidebar-background: var(--color-neutral-900);
--sidebar-foreground: var(--color-neutral-50);
--sidebar-primary: var(--color-blue-700);
--sidebar-primary-foreground: var(--color-neutral-50);
--sidebar-accent: var(--color-neutral-800);
--sidebar-accent-foreground: var(--color-neutral-100);
--sidebar-border: var(--color-neutral-800);
--sidebar-outline: var(--color-neutral-600);
}
@layer base {
* {
@apply border-border outline-outline/50;
}
body {
@apply bg-background text-foreground scheme-light dark:scheme-dark;
}
button {
@apply cursor-pointer;
}
}

View File

@@ -1,15 +1,51 @@
{ {
"$schema": "https://starwind.dev/config-schema.json", "$schema": "https://starwind.dev/config-schema.json",
"tailwind": { "tailwind": {
"css": "src/styles/global.css", "css": "src/styles/starwind.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true "cssVariables": true
}, },
"componentDir": "src/components", "componentDir": "src/components",
"components": [ "components": [
{
"name": "navbar",
"version": "1.0.0"
},
{
"name": "button",
"version": "2.3.0"
},
{
"name": "input",
"version": "1.3.1"
},
{
"name": "accordion",
"version": "1.3.3"
},
{
"name": "carousel",
"version": "1.0.1"
},
{
"name": "dropdown",
"version": "1.2.3"
},
{
"name": "label",
"version": "1.2.0"
},
{
"name": "textarea",
"version": "1.3.1"
},
{ {
"name": "image", "name": "image",
"version": "1.0.0" "version": "1.0.0"
},
{
"name": "card",
"version": "2.0.0"
} }
] ]
} }

View File

@@ -11,8 +11,15 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "@/*": [
"src/*" "src/*",
"components/*",
"assets/*"
] ]
} },
"plugins": [
{
"name": "@astrojs/ts-plugin"
},
],
} }
} }