diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..f78d7f1
--- /dev/null
+++ b/.dockerignore
@@ -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
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 0000000..e7a6538
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -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 }}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..dd5c3d3
--- /dev/null
+++ b/Dockerfile
@@ -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"]
diff --git a/astro.config.mjs b/astro.config.mjs
index 6b749b9..0e2815e 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,12 +1,18 @@
-import tailwindcss from "@tailwindcss/vite";
// @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({
- vite: {
- plugins: [tailwindcss()]
- }
-});
\ No newline at end of file
+ site: "https://resuely.com",
+ output: "server",
+ adapter: node({ mode: "standalone" }),
+ vite: {
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ "@": decodeURI(new URL("./src", import.meta.url).pathname),
+ },
+ },
+ },
+});
diff --git a/package-lock.json b/package-lock.json
index 377119e..90d7ecd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,10 +8,17 @@
"name": "landing",
"version": "0.0.1",
"dependencies": {
+ "@astrojs/node": "^9.5.2",
+ "@astrojs/ts-plugin": "^1.10.6",
+ "@starwind-ui/core": "^1.15.2",
"@tabler/icons": "^3.36.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.1.18",
"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-variants": "^3.2.2",
"tailwindcss": "^4.1.18",
@@ -59,6 +66,20 @@
"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": {
"version": "3.3.0",
"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_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": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -147,6 +192,27 @@
"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": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
@@ -1443,6 +1509,12 @@
"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": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz",
@@ -1510,6 +1582,24 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"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": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
@@ -1849,6 +1939,32 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"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": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2283,6 +2399,20 @@
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
"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": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
@@ -2560,6 +2690,21 @@
"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": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
@@ -2665,6 +2810,32 @@
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"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": {
"version": "3.0.2",
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
@@ -2718,6 +2904,47 @@
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2744,6 +2971,22 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
@@ -2972,6 +3215,15 @@
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"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": {
"version": "4.2.0",
"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"
}
},
+ "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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
@@ -3060,6 +3336,12 @@
"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": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -3081,6 +3363,18 @@
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -4191,6 +4485,47 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -4267,6 +4602,34 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -4380,6 +4743,18 @@
"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": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -4392,6 +4767,21 @@
"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": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
@@ -4444,6 +4834,21 @@
"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": {
"version": "1.30.0",
"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",
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -4833,6 +5237,27 @@
"@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": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz",
@@ -4849,6 +5274,18 @@
"@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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -4886,6 +5323,37 @@
"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": {
"version": "7.2.0",
"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"
}
},
+ "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": {
"version": "4.0.0",
"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",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -4991,8 +5470,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -5082,8 +5560,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "optional": true
+ "license": "0BSD"
},
"node_modules/tw-animate-css": {
"version": "1.4.0",
@@ -5138,6 +5615,18 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"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": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -5291,6 +5780,15 @@
"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": {
"version": "1.17.4",
"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",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@@ -5533,6 +6042,21 @@
"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": {
"version": "1.1.0",
"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==",
"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": {
"version": "21.1.1",
"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",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -5665,6 +6203,182 @@
"type": "github",
"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"
+ }
}
}
}
diff --git a/package.json b/package.json
index 2c1e8b7..c3d985d 100644
--- a/package.json
+++ b/package.json
@@ -5,14 +5,22 @@
"scripts": {
"dev": "astro dev",
"build": "astro build",
+ "start": "node ./dist/server/entry.mjs",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
+ "@astrojs/node": "^9.5.2",
+ "@astrojs/ts-plugin": "^1.10.6",
+ "@starwind-ui/core": "^1.15.2",
"@tabler/icons": "^3.36.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.1.18",
"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-variants": "^3.2.2",
"tailwindcss": "^4.1.18",
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..62db97a
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png
new file mode 100644
index 0000000..42631da
Binary files /dev/null and b/public/favicon-96x96.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..bb49f6b
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/favicon.svg b/public/favicon.svg
index f157bd1..3402cdd 100644
--- a/public/favicon.svg
+++ b/public/favicon.svg
@@ -1,9 +1,57 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/fonts/Inter_28pt-Bold.ttf b/public/fonts/Inter_28pt-Bold.ttf
new file mode 100644
index 0000000..14db994
Binary files /dev/null and b/public/fonts/Inter_28pt-Bold.ttf differ
diff --git a/public/fonts/Inter_28pt-Medium.ttf b/public/fonts/Inter_28pt-Medium.ttf
new file mode 100644
index 0000000..00120fe
Binary files /dev/null and b/public/fonts/Inter_28pt-Medium.ttf differ
diff --git a/public/fonts/Inter_28pt-Regular.ttf b/public/fonts/Inter_28pt-Regular.ttf
new file mode 100644
index 0000000..855b6f4
Binary files /dev/null and b/public/fonts/Inter_28pt-Regular.ttf differ
diff --git a/public/fonts/Inter_28pt-SemiBold.ttf b/public/fonts/Inter_28pt-SemiBold.ttf
new file mode 100644
index 0000000..8b84efc
Binary files /dev/null and b/public/fonts/Inter_28pt-SemiBold.ttf differ
diff --git a/public/fonts/Poppins-Bold.ttf b/public/fonts/Poppins-Bold.ttf
new file mode 100644
index 0000000..1982f38
Binary files /dev/null and b/public/fonts/Poppins-Bold.ttf differ
diff --git a/public/fonts/Poppins-Medium.ttf b/public/fonts/Poppins-Medium.ttf
new file mode 100644
index 0000000..a590f5c
Binary files /dev/null and b/public/fonts/Poppins-Medium.ttf differ
diff --git a/public/fonts/Poppins-Regular.ttf b/public/fonts/Poppins-Regular.ttf
new file mode 100644
index 0000000..0bda228
Binary files /dev/null and b/public/fonts/Poppins-Regular.ttf differ
diff --git a/public/fonts/Poppins-SemiBold.ttf b/public/fonts/Poppins-SemiBold.ttf
new file mode 100644
index 0000000..c30ad10
Binary files /dev/null and b/public/fonts/Poppins-SemiBold.ttf differ
diff --git a/public/fonts/fonts/Inter_28pt-Bold.ttf b/public/fonts/fonts/Inter_28pt-Bold.ttf
new file mode 100644
index 0000000..14db994
Binary files /dev/null and b/public/fonts/fonts/Inter_28pt-Bold.ttf differ
diff --git a/public/fonts/fonts/Inter_28pt-Medium.ttf b/public/fonts/fonts/Inter_28pt-Medium.ttf
new file mode 100644
index 0000000..00120fe
Binary files /dev/null and b/public/fonts/fonts/Inter_28pt-Medium.ttf differ
diff --git a/public/fonts/fonts/Inter_28pt-Regular.ttf b/public/fonts/fonts/Inter_28pt-Regular.ttf
new file mode 100644
index 0000000..855b6f4
Binary files /dev/null and b/public/fonts/fonts/Inter_28pt-Regular.ttf differ
diff --git a/public/fonts/fonts/Inter_28pt-SemiBold.ttf b/public/fonts/fonts/Inter_28pt-SemiBold.ttf
new file mode 100644
index 0000000..8b84efc
Binary files /dev/null and b/public/fonts/fonts/Inter_28pt-SemiBold.ttf differ
diff --git a/public/fonts/fonts/Poppins-Bold.ttf b/public/fonts/fonts/Poppins-Bold.ttf
new file mode 100644
index 0000000..1982f38
Binary files /dev/null and b/public/fonts/fonts/Poppins-Bold.ttf differ
diff --git a/public/fonts/fonts/Poppins-Medium.ttf b/public/fonts/fonts/Poppins-Medium.ttf
new file mode 100644
index 0000000..a590f5c
Binary files /dev/null and b/public/fonts/fonts/Poppins-Medium.ttf differ
diff --git a/public/fonts/fonts/Poppins-Regular.ttf b/public/fonts/fonts/Poppins-Regular.ttf
new file mode 100644
index 0000000..0bda228
Binary files /dev/null and b/public/fonts/fonts/Poppins-Regular.ttf differ
diff --git a/public/fonts/fonts/Poppins-SemiBold.ttf b/public/fonts/fonts/Poppins-SemiBold.ttf
new file mode 100644
index 0000000..c30ad10
Binary files /dev/null and b/public/fonts/fonts/Poppins-SemiBold.ttf differ
diff --git a/public/og-image.jpg b/public/og-image.jpg
new file mode 100644
index 0000000..ead2fc9
Binary files /dev/null and b/public/og-image.jpg differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..11154fe
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://resuely.com/sitemap.xml
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..b28bd9f
--- /dev/null
+++ b/public/site.webmanifest
@@ -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"
+}
\ No newline at end of file
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..5a29ed2
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,6 @@
+
+
+
+ https://resuely.com/
+
+
diff --git a/public/web-app-manifest-192x192.png b/public/web-app-manifest-192x192.png
new file mode 100644
index 0000000..e5ca106
Binary files /dev/null and b/public/web-app-manifest-192x192.png differ
diff --git a/public/web-app-manifest-512x512.png b/public/web-app-manifest-512x512.png
new file mode 100644
index 0000000..63e8f94
Binary files /dev/null and b/public/web-app-manifest-512x512.png differ
diff --git a/src/assets/faq.jpg b/src/assets/faq.jpg
new file mode 100644
index 0000000..f1b87c1
Binary files /dev/null and b/src/assets/faq.jpg differ
diff --git a/src/assets/handy.jpg b/src/assets/handy.jpg
new file mode 100644
index 0000000..22cba96
Binary files /dev/null and b/src/assets/handy.jpg differ
diff --git a/src/assets/hero1.jpg b/src/assets/hero1.jpg
new file mode 100644
index 0000000..96a6a89
Binary files /dev/null and b/src/assets/hero1.jpg differ
diff --git a/src/assets/hero2.jpg b/src/assets/hero2.jpg
new file mode 100644
index 0000000..76a9900
Binary files /dev/null and b/src/assets/hero2.jpg differ
diff --git a/src/assets/hero3.jpg b/src/assets/hero3.jpg
new file mode 100644
index 0000000..47e428c
Binary files /dev/null and b/src/assets/hero3.jpg differ
diff --git a/src/assets/hero4.jpg b/src/assets/hero4.jpg
new file mode 100644
index 0000000..775b405
Binary files /dev/null and b/src/assets/hero4.jpg differ
diff --git a/src/assets/hero5.jpg b/src/assets/hero5.jpg
new file mode 100644
index 0000000..7f56500
Binary files /dev/null and b/src/assets/hero5.jpg differ
diff --git a/src/assets/logo-dark.svg b/src/assets/logo-dark.svg
new file mode 100644
index 0000000..07e0103
--- /dev/null
+++ b/src/assets/logo-dark.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/logo-light.svg b/src/assets/logo-light.svg
new file mode 100644
index 0000000..411a956
--- /dev/null
+++ b/src/assets/logo-light.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/programming.jpg b/src/assets/programming.jpg
new file mode 100644
index 0000000..9db37fe
Binary files /dev/null and b/src/assets/programming.jpg differ
diff --git a/src/components/TopBarDropdown.astro b/src/components/TopBarDropdown.astro
new file mode 100644
index 0000000..83f6a7d
--- /dev/null
+++ b/src/components/TopBarDropdown.astro
@@ -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";
+---
+
+
+
+
+
+
+
+
+ Aplicaciones
+
+ Handy
+
+
diff --git a/src/components/starwind-pro/contact-02/Contact2.astro b/src/components/starwind-pro/contact-02/Contact2.astro
new file mode 100644
index 0000000..65b5cc5
--- /dev/null
+++ b/src/components/starwind-pro/contact-02/Contact2.astro
@@ -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;
+---
+
+
+
+
+ {badge}
+
+
+ {heading}
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+
+ Te respondemos por correo lo antes posible.
+
+
+
+
diff --git a/src/components/starwind-pro/contact-02/Contact2Demo.astro b/src/components/starwind-pro/contact-02/Contact2Demo.astro
new file mode 100644
index 0000000..b5c2bbd
--- /dev/null
+++ b/src/components/starwind-pro/contact-02/Contact2Demo.astro
@@ -0,0 +1,9 @@
+---
+import Contact2 from "./Contact2.astro";
+---
+
+
diff --git a/src/components/starwind-pro/faq-01/Faq1.astro b/src/components/starwind-pro/faq-01/Faq1.astro
new file mode 100644
index 0000000..d6b02d8
--- /dev/null
+++ b/src/components/starwind-pro/faq-01/Faq1.astro
@@ -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;
+---
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ {items.map((item, index) => (
+
+ {item.question}
+ {item.answer}
+
+ ))}
+
+
diff --git a/src/components/starwind-pro/faq-04/Faq4.astro b/src/components/starwind-pro/faq-04/Faq4.astro
new file mode 100644
index 0000000..f738a4b
--- /dev/null
+++ b/src/components/starwind-pro/faq-04/Faq4.astro
@@ -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;
+---
+
+
+
+
+
{heading}
+
+ {description}
+
+
+ {
+ items.map((item, idx) => (
+
+
+ {item.title}
+
+ {item.content}
+
+ ))
+ }
+
+
+
+
+
+
+
+
diff --git a/src/components/starwind-pro/feature-02/Feature2.astro b/src/components/starwind-pro/feature-02/Feature2.astro
deleted file mode 100644
index 9ce5412..0000000
--- a/src/components/starwind-pro/feature-02/Feature2.astro
+++ /dev/null
@@ -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;
----
-
-
-
-
-
-
- {title}
-
-
- {description}
-
-
-
-
- {
- features.map((feature) => (
-
-
-
-
- {feature.text}
-
- ))
- }
-
-
-
-
-
-
-
diff --git a/src/components/starwind-pro/feature-02/Feature2Demo.astro b/src/components/starwind-pro/feature-02/Feature2Demo.astro
deleted file mode 100644
index f1b6f2b..0000000
--- a/src/components/starwind-pro/feature-02/Feature2Demo.astro
+++ /dev/null
@@ -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" },
-];
----
-
-
diff --git a/src/components/starwind-pro/feature-06/Feature6.astro b/src/components/starwind-pro/feature-06/Feature6.astro
new file mode 100644
index 0000000..ccd15ea
--- /dev/null
+++ b/src/components/starwind-pro/feature-06/Feature6.astro
@@ -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;
+---
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ {
+ features.map((feature) => (
+
+
+
+
+
{feature.title}
+
+ {feature.description}
+
+
+ ))
+ }
+
+
diff --git a/src/components/starwind-pro/footer-01/Footer1.astro b/src/components/starwind-pro/footer-01/Footer1.astro
new file mode 100644
index 0000000..df95bd4
--- /dev/null
+++ b/src/components/starwind-pro/footer-01/Footer1.astro
@@ -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;
+---
+
+
diff --git a/src/components/starwind-pro/hero-02/Hero2.astro b/src/components/starwind-pro/hero-02/Hero2.astro
new file mode 100644
index 0000000..f4a7c8a
--- /dev/null
+++ b/src/components/starwind-pro/hero-02/Hero2.astro
@@ -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;
+---
+
+
+
+
+ {headline}
+
+
+ {subheadline}
+
+
+
+ {primaryCtaText}
+
+
+ {secondaryCtaText}
+
+
+
+
diff --git a/src/components/starwind-pro/hero-09/Hero9.astro b/src/components/starwind-pro/hero-09/Hero9.astro
new file mode 100644
index 0000000..0351f1a
--- /dev/null
+++ b/src/components/starwind-pro/hero-09/Hero9.astro
@@ -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;
+---
+
+
+
+
+ {
+ carouselImages.map((image) => (
+
+ {/* you may need to adjust the "min-h-[500px]" class here depending on your content overlay size */}
+
+
+ ))
+ }
+
+
+
+
+
+
+
+
+
+
+
+ {heading}
+
+
+
+
+ {description}
+
+
+
+
+ {primaryButtonText}
+ {
+ secondaryButtonText && (
+
+ {secondaryButtonText}
+
+
+ )
+ }
+
+
+
+
+
+
+
+
+ {
+ carouselImages.map((_, index) => (
+
+ ))
+ }
+
+
+
+
+
+
+
diff --git a/src/components/starwind-pro/services-07/Services7.astro b/src/components/starwind-pro/services-07/Services7.astro
new file mode 100644
index 0000000..c52c457
--- /dev/null
+++ b/src/components/starwind-pro/services-07/Services7.astro
@@ -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;
+---
+
+
+
+
+
+
+
+ {badge}
+
+
+ {heading}
+
+
+ {description}
+
+
+ {
+ ctaText && (
+
+ {ctaText}
+
+
+ )
+ }
+
+
+
+
+
+
+
diff --git a/src/components/starwind/accordion/Accordion.astro b/src/components/starwind/accordion/Accordion.astro
new file mode 100644
index 0000000..700d29d
--- /dev/null
+++ b/src/components/starwind/accordion/Accordion.astro
@@ -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;
+---
+
+
+
+
+
+
diff --git a/src/components/starwind/accordion/AccordionContent.astro b/src/components/starwind/accordion/AccordionContent.astro
new file mode 100644
index 0000000..bfed8d6
--- /dev/null
+++ b/src/components/starwind/accordion/AccordionContent.astro
@@ -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;
+---
+
+
diff --git a/src/components/starwind/accordion/AccordionItem.astro b/src/components/starwind/accordion/AccordionItem.astro
new file mode 100644
index 0000000..5a1ee69
--- /dev/null
+++ b/src/components/starwind/accordion/AccordionItem.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/accordion/AccordionTrigger.astro b/src/components/starwind/accordion/AccordionTrigger.astro
new file mode 100644
index 0000000..626e658
--- /dev/null
+++ b/src/components/starwind/accordion/AccordionTrigger.astro
@@ -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;
+---
+
+
+
+
+
+
+
diff --git a/src/components/starwind/accordion/index.ts b/src/components/starwind/accordion/index.ts
new file mode 100644
index 0000000..9e7730e
--- /dev/null
+++ b/src/components/starwind/accordion/index.ts
@@ -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,
+};
diff --git a/src/components/starwind/button/Button.astro b/src/components/starwind/button/Button.astro
new file mode 100644
index 0000000..2eede85
--- /dev/null
+++ b/src/components/starwind/button/Button.astro
@@ -0,0 +1,55 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import { tv, type VariantProps } from "tailwind-variants";
+
+interface Props
+ extends
+ HTMLAttributes<"button">,
+ Omit, "type">,
+ VariantProps {}
+
+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";
+---
+
+
+
+
diff --git a/src/components/starwind/button/index.ts b/src/components/starwind/button/index.ts
new file mode 100644
index 0000000..57f3575
--- /dev/null
+++ b/src/components/starwind/button/index.ts
@@ -0,0 +1,7 @@
+import Button, { button } from "./Button.astro";
+
+const ButtonVariants = { button };
+
+export { Button, ButtonVariants };
+
+export default Button;
diff --git a/src/components/starwind/card/Card.astro b/src/components/starwind/card/Card.astro
new file mode 100644
index 0000000..cf71a03
--- /dev/null
+++ b/src/components/starwind/card/Card.astro
@@ -0,0 +1,34 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import { tv, type VariantProps } from "tailwind-variants";
+
+type Props = HTMLAttributes<"div"> & VariantProps;
+
+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;
+---
+
+
+
+
diff --git a/src/components/starwind/card/CardAction.astro b/src/components/starwind/card/CardAction.astro
new file mode 100644
index 0000000..dce7c08
--- /dev/null
+++ b/src/components/starwind/card/CardAction.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/card/CardContent.astro b/src/components/starwind/card/CardContent.astro
new file mode 100644
index 0000000..865e2cf
--- /dev/null
+++ b/src/components/starwind/card/CardContent.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/card/CardDescription.astro b/src/components/starwind/card/CardDescription.astro
new file mode 100644
index 0000000..b03f706
--- /dev/null
+++ b/src/components/starwind/card/CardDescription.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/card/CardFooter.astro b/src/components/starwind/card/CardFooter.astro
new file mode 100644
index 0000000..f774d17
--- /dev/null
+++ b/src/components/starwind/card/CardFooter.astro
@@ -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;
+---
+
+
diff --git a/src/components/starwind/card/CardHeader.astro b/src/components/starwind/card/CardHeader.astro
new file mode 100644
index 0000000..262196f
--- /dev/null
+++ b/src/components/starwind/card/CardHeader.astro
@@ -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;
+---
+
+
diff --git a/src/components/starwind/card/CardTitle.astro b/src/components/starwind/card/CardTitle.astro
new file mode 100644
index 0000000..f2d1aa8
--- /dev/null
+++ b/src/components/starwind/card/CardTitle.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/card/index.ts b/src/components/starwind/card/index.ts
new file mode 100644
index 0000000..10ea222
--- /dev/null
+++ b/src/components/starwind/card/index.ts
@@ -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,
+};
diff --git a/src/components/starwind/carousel/Carousel.astro b/src/components/starwind/carousel/Carousel.astro
new file mode 100644
index 0000000..2cfc721
--- /dev/null
+++ b/src/components/starwind/carousel/Carousel.astro
@@ -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;
+---
+
+
+
+
+
+
diff --git a/src/components/starwind/carousel/CarouselContent.astro b/src/components/starwind/carousel/CarouselContent.astro
new file mode 100644
index 0000000..7c51e55
--- /dev/null
+++ b/src/components/starwind/carousel/CarouselContent.astro
@@ -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;
+---
+
+
diff --git a/src/components/starwind/carousel/CarouselItem.astro b/src/components/starwind/carousel/CarouselItem.astro
new file mode 100644
index 0000000..511cac7
--- /dev/null
+++ b/src/components/starwind/carousel/CarouselItem.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/carousel/CarouselNext.astro b/src/components/starwind/carousel/CarouselNext.astro
new file mode 100644
index 0000000..1922854
--- /dev/null
+++ b/src/components/starwind/carousel/CarouselNext.astro
@@ -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;
+
+const { class: className = "", variant = "outline", size = "icon", ...rest } = Astro.props;
+---
+
+
+
+
+
+
+ Next slide
+
+
diff --git a/src/components/starwind/carousel/CarouselPrevious.astro b/src/components/starwind/carousel/CarouselPrevious.astro
new file mode 100644
index 0000000..973dffe
--- /dev/null
+++ b/src/components/starwind/carousel/CarouselPrevious.astro
@@ -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;
+
+const { class: className = "", variant = "outline", size = "icon", ...rest } = Astro.props;
+---
+
+
+
+
+
+
+ Previous slide
+
+
diff --git a/src/components/starwind/carousel/carousel-script.ts b/src/components/starwind/carousel/carousel-script.ts
new file mode 100644
index 0000000..7d745da
--- /dev/null
+++ b/src/components/starwind/carousel/carousel-script.ts
@@ -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();
+ },
+ };
+}
diff --git a/src/components/starwind/carousel/index.ts b/src/components/starwind/carousel/index.ts
new file mode 100644
index 0000000..2d82f5b
--- /dev/null
+++ b/src/components/starwind/carousel/index.ts
@@ -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,
+};
diff --git a/src/components/starwind/dropdown/Dropdown.astro b/src/components/starwind/dropdown/Dropdown.astro
new file mode 100644
index 0000000..0907fd4
--- /dev/null
+++ b/src/components/starwind/dropdown/Dropdown.astro
@@ -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;
+---
+
+
+
+
+
+
diff --git a/src/components/starwind/dropdown/DropdownContent.astro b/src/components/starwind/dropdown/DropdownContent.astro
new file mode 100644
index 0000000..1166fc2
--- /dev/null
+++ b/src/components/starwind/dropdown/DropdownContent.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/dropdown/DropdownItem.astro b/src/components/starwind/dropdown/DropdownItem.astro
new file mode 100644
index 0000000..f2b53e5
--- /dev/null
+++ b/src/components/starwind/dropdown/DropdownItem.astro
@@ -0,0 +1,48 @@
+---
+import type { HTMLTag, Polymorphic } from "astro/types";
+import { tv } from "tailwind-variants";
+
+type Props = 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;
+---
+
+
+
+
diff --git a/src/components/starwind/dropdown/DropdownLabel.astro b/src/components/starwind/dropdown/DropdownLabel.astro
new file mode 100644
index 0000000..edd0ef0
--- /dev/null
+++ b/src/components/starwind/dropdown/DropdownLabel.astro
@@ -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;
+---
+
+
+
+
diff --git a/src/components/starwind/dropdown/DropdownSeparator.astro b/src/components/starwind/dropdown/DropdownSeparator.astro
new file mode 100644
index 0000000..5fded82
--- /dev/null
+++ b/src/components/starwind/dropdown/DropdownSeparator.astro
@@ -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;
+---
+
+
+
diff --git a/src/components/starwind/dropdown/DropdownTrigger.astro b/src/components/starwind/dropdown/DropdownTrigger.astro
new file mode 100644
index 0000000..940ae5c
--- /dev/null
+++ b/src/components/starwind/dropdown/DropdownTrigger.astro
@@ -0,0 +1,52 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import { tv } from "tailwind-variants";
+
+type Props = Omit, "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 ? (
+
+
+
+ ) : (
+
+
+
+ )
+}
diff --git a/src/components/starwind/dropdown/index.ts b/src/components/starwind/dropdown/index.ts
new file mode 100644
index 0000000..09774fc
--- /dev/null
+++ b/src/components/starwind/dropdown/index.ts
@@ -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,
+};
diff --git a/src/components/starwind/input/Input.astro b/src/components/starwind/input/Input.astro
new file mode 100644
index 0000000..86af786
--- /dev/null
+++ b/src/components/starwind/input/Input.astro
@@ -0,0 +1,25 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import { tv, type VariantProps } from "tailwind-variants";
+
+type Props = HTMLAttributes<"input"> & VariantProps;
+
+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;
+---
+
+
diff --git a/src/components/starwind/input/index.ts b/src/components/starwind/input/index.ts
new file mode 100644
index 0000000..618d3e9
--- /dev/null
+++ b/src/components/starwind/input/index.ts
@@ -0,0 +1,7 @@
+import Input, { input } from "./Input.astro";
+
+const InputVariants = { input };
+
+export { Input, InputVariants };
+
+export default Input;
diff --git a/src/components/starwind/label/Label.astro b/src/components/starwind/label/Label.astro
new file mode 100644
index 0000000..f252249
--- /dev/null
+++ b/src/components/starwind/label/Label.astro
@@ -0,0 +1,22 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import { tv, type VariantProps } from "tailwind-variants";
+
+type Props = HTMLAttributes<"label"> & VariantProps;
+
+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 */}
+
+
+
diff --git a/src/components/starwind/label/index.ts b/src/components/starwind/label/index.ts
new file mode 100644
index 0000000..56664af
--- /dev/null
+++ b/src/components/starwind/label/index.ts
@@ -0,0 +1,7 @@
+import Label, { label } from "./Label.astro";
+
+const LabelVariants = { label };
+
+export { Label, LabelVariants };
+
+export default Label;
diff --git a/src/components/starwind/textarea/Textarea.astro b/src/components/starwind/textarea/Textarea.astro
new file mode 100644
index 0000000..f9d5e80
--- /dev/null
+++ b/src/components/starwind/textarea/Textarea.astro
@@ -0,0 +1,29 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import { tv, type VariantProps } from "tailwind-variants";
+
+type Props = HTMLAttributes<"textarea"> & VariantProps;
+
+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;
+---
+
+
diff --git a/src/components/starwind/textarea/index.ts b/src/components/starwind/textarea/index.ts
new file mode 100644
index 0000000..f343c2c
--- /dev/null
+++ b/src/components/starwind/textarea/index.ts
@@ -0,0 +1,9 @@
+import Textarea, { textarea } from "./Textarea.astro";
+
+const TextareaVariants = {
+ textarea,
+};
+
+export { Textarea, TextareaVariants };
+
+export default Textarea;
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index 1b7ed0b..078da42 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -1,15 +1,107 @@
---
+import "@/styles/starwind.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"],
+};
---
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts
new file mode 100644
index 0000000..ab737c7
--- /dev/null
+++ b/src/pages/api/contact.ts
@@ -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();
+
+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;
+ 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 = {
+ 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.",
+ },
+ });
+};
diff --git a/src/pages/api/healthz.ts b/src/pages/api/healthz.ts
new file mode 100644
index 0000000..7ec5492
--- /dev/null
+++ b/src/pages/api/healthz.ts
@@ -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",
+ },
+ });
+};
diff --git a/src/pages/index.astro b/src/pages/index.astro
index bf3b87c..7c07fce 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,7 +1,83 @@
---
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,
+ },
+ })),
+};
---
-
-
+
+
+
+
+
+
+
diff --git a/src/styles/fonts.css b/src/styles/fonts.css
new file mode 100644
index 0000000..9e6a9fa
--- /dev/null
+++ b/src/styles/fonts.css
@@ -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;
+}
diff --git a/src/styles/global.css b/src/styles/global.css
index 23089c8..8c21d6a 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1,167 +1,16 @@
@import "tailwindcss";
-@import "tw-animate-css";
-@plugin "@tailwindcss/forms";
-@custom-variant dark (&:where(.dark, .dark *));
+@import "./fonts.css";
@theme {
- --animate-accordion-down: accordion-down 0.2s ease-out;
- --animate-accordion-up: accordion-up 0.2s ease-out;
+ --font-sans: 'Inter', system-ui, sans-serif;
+ --font-heading: 'Poppins', sans-serif;
- @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);
+ --tracking-resuely: 0.2em;
}
@layer base {
- * {
- @apply border-border outline-outline/50;
- }
- body {
- @apply bg-background text-foreground scheme-light dark:scheme-dark;
- }
- button {
- @apply cursor-pointer;
+ html {
+ scroll-behavior: smooth;
}
}
+
diff --git a/src/styles/starwind.css b/src/styles/starwind.css
new file mode 100644
index 0000000..23089c8
--- /dev/null
+++ b/src/styles/starwind.css
@@ -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;
+ }
+}
diff --git a/starwind.config.json b/starwind.config.json
index bea6059..6acf6c4 100644
--- a/starwind.config.json
+++ b/starwind.config.json
@@ -1,15 +1,51 @@
{
"$schema": "https://starwind.dev/config-schema.json",
"tailwind": {
- "css": "src/styles/global.css",
+ "css": "src/styles/starwind.css",
"baseColor": "neutral",
"cssVariables": true
},
"componentDir": "src/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",
"version": "1.0.0"
+ },
+ {
+ "name": "card",
+ "version": "2.0.0"
}
]
}
diff --git a/tsconfig.json b/tsconfig.json
index f36744a..0e9e45b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,8 +11,15 @@
"baseUrl": ".",
"paths": {
"@/*": [
- "src/*"
+ "src/*",
+ "components/*",
+ "assets/*"
]
- }
+ },
+ "plugins": [
+ {
+ "name": "@astrojs/ts-plugin"
+ },
+ ],
}
}