feat: simple landing with contact form and applications showcase
Some checks failed
Build & Deploy (prod) / deploy (push) Failing after 11s
17
.dockerignore
Normal 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
@@ -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
@@ -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"]
|
||||||
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -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 |
BIN
public/fonts/Inter_28pt-Bold.ttf
Normal file
BIN
public/fonts/Inter_28pt-Medium.ttf
Normal file
BIN
public/fonts/Inter_28pt-Regular.ttf
Normal file
BIN
public/fonts/Inter_28pt-SemiBold.ttf
Normal file
BIN
public/fonts/Poppins-Bold.ttf
Normal file
BIN
public/fonts/Poppins-Medium.ttf
Normal file
BIN
public/fonts/Poppins-Regular.ttf
Normal file
BIN
public/fonts/Poppins-SemiBold.ttf
Normal file
BIN
public/fonts/fonts/Inter_28pt-Bold.ttf
Normal file
BIN
public/fonts/fonts/Inter_28pt-Medium.ttf
Normal file
BIN
public/fonts/fonts/Inter_28pt-Regular.ttf
Normal file
BIN
public/fonts/fonts/Inter_28pt-SemiBold.ttf
Normal file
BIN
public/fonts/fonts/Poppins-Bold.ttf
Normal file
BIN
public/fonts/fonts/Poppins-Medium.ttf
Normal file
BIN
public/fonts/fonts/Poppins-Regular.ttf
Normal file
BIN
public/fonts/fonts/Poppins-SemiBold.ttf
Normal file
BIN
public/og-image.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
4
public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://resuely.com/sitemap.xml
|
||||||
21
public/site.webmanifest
Normal 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
@@ -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>
|
||||||
BIN
public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/faq.jpg
Normal file
|
After Width: | Height: | Size: 540 KiB |
BIN
src/assets/handy.jpg
Normal file
|
After Width: | Height: | Size: 801 KiB |
BIN
src/assets/hero1.jpg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
src/assets/hero2.jpg
Normal file
|
After Width: | Height: | Size: 539 KiB |
BIN
src/assets/hero3.jpg
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
src/assets/hero4.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/hero5.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
4
src/assets/logo-dark.svg
Normal 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 |
4
src/assets/logo-light.svg
Normal 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
|
After Width: | Height: | Size: 1.0 MiB |
27
src/components/TopBarDropdown.astro
Normal 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>
|
||||||
154
src/components/starwind-pro/contact-02/Contact2.astro
Normal 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>
|
||||||
@@ -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."
|
||||||
|
/>
|
||||||
86
src/components/starwind-pro/faq-01/Faq1.astro
Normal 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>
|
||||||
66
src/components/starwind-pro/faq-04/Faq4.astro
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
83
src/components/starwind-pro/feature-06/Feature6.astro
Normal 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>
|
||||||
61
src/components/starwind-pro/footer-01/Footer1.astro
Normal 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>
|
||||||
49
src/components/starwind-pro/hero-02/Hero2.astro
Normal 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>
|
||||||
174
src/components/starwind-pro/hero-09/Hero9.astro
Normal 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>
|
||||||
111
src/components/starwind-pro/services-07/Services7.astro
Normal 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>
|
||||||
|
|
||||||
254
src/components/starwind/accordion/Accordion.astro
Normal 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>
|
||||||
33
src/components/starwind/accordion/AccordionContent.astro
Normal 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>
|
||||||
27
src/components/starwind/accordion/AccordionItem.astro
Normal 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>
|
||||||
32
src/components/starwind/accordion/AccordionTrigger.astro
Normal 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>
|
||||||
15
src/components/starwind/accordion/index.ts
Normal 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,
|
||||||
|
};
|
||||||
55
src/components/starwind/button/Button.astro
Normal 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>
|
||||||
7
src/components/starwind/button/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Button, { button } from "./Button.astro";
|
||||||
|
|
||||||
|
const ButtonVariants = { button };
|
||||||
|
|
||||||
|
export { Button, ButtonVariants };
|
||||||
|
|
||||||
|
export default Button;
|
||||||
34
src/components/starwind/card/Card.astro
Normal 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>
|
||||||
16
src/components/starwind/card/CardAction.astro
Normal 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>
|
||||||
16
src/components/starwind/card/CardContent.astro
Normal 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>
|
||||||
16
src/components/starwind/card/CardDescription.astro
Normal 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>
|
||||||
16
src/components/starwind/card/CardFooter.astro
Normal 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>
|
||||||
19
src/components/starwind/card/CardHeader.astro
Normal 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>
|
||||||
16
src/components/starwind/card/CardTitle.astro
Normal 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>
|
||||||
38
src/components/starwind/card/index.ts
Normal 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,
|
||||||
|
};
|
||||||
55
src/components/starwind/carousel/Carousel.astro
Normal 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>
|
||||||
26
src/components/starwind/carousel/CarouselContent.astro
Normal 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>
|
||||||
26
src/components/starwind/carousel/CarouselItem.astro
Normal 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>
|
||||||
37
src/components/starwind/carousel/CarouselNext.astro
Normal 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>
|
||||||
37
src/components/starwind/carousel/CarouselPrevious.astro
Normal 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>
|
||||||
191
src/components/starwind/carousel/carousel-script.ts
Normal 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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
32
src/components/starwind/carousel/index.ts
Normal 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,
|
||||||
|
};
|
||||||
377
src/components/starwind/dropdown/Dropdown.astro
Normal 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>
|
||||||
81
src/components/starwind/dropdown/DropdownContent.astro
Normal 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>
|
||||||
48
src/components/starwind/dropdown/DropdownItem.astro
Normal 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>
|
||||||
29
src/components/starwind/dropdown/DropdownLabel.astro
Normal 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>
|
||||||
21
src/components/starwind/dropdown/DropdownSeparator.astro
Normal 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>
|
||||||
52
src/components/starwind/dropdown/DropdownTrigger.astro
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
33
src/components/starwind/dropdown/index.ts
Normal 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,
|
||||||
|
};
|
||||||
25
src/components/starwind/input/Input.astro
Normal 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} />
|
||||||
7
src/components/starwind/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Input, { input } from "./Input.astro";
|
||||||
|
|
||||||
|
const InputVariants = { input };
|
||||||
|
|
||||||
|
export { Input, InputVariants };
|
||||||
|
|
||||||
|
export default Input;
|
||||||
22
src/components/starwind/label/Label.astro
Normal 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>
|
||||||
7
src/components/starwind/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Label, { label } from "./Label.astro";
|
||||||
|
|
||||||
|
const LabelVariants = { label };
|
||||||
|
|
||||||
|
export { Label, LabelVariants };
|
||||||
|
|
||||||
|
export default Label;
|
||||||
29
src/components/starwind/textarea/Textarea.astro
Normal 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>
|
||||||
9
src/components/starwind/textarea/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Textarea, { textarea } from "./Textarea.astro";
|
||||||
|
|
||||||
|
const TextareaVariants = {
|
||||||
|
textarea,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Textarea, TextareaVariants };
|
||||||
|
|
||||||
|
export default Textarea;
|
||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,15 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"src/*"
|
"src/*",
|
||||||
|
"components/*",
|
||||||
|
"assets/*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@astrojs/ts-plugin"
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||